이미지 로딩 중...

Polars 인터랙티브 필터링 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 15. · 6 Views

Polars 인터랙티브 필터링 완벽 가이드

Polars를 활용한 인터랙티브 데이터 필터링 기법을 초급자도 쉽게 따라할 수 있도록 설명합니다. 실무에서 자주 사용하는 필터링 패턴과 함께 성능 최적화 팁까지 제공합니다.


목차

  1. 기본_필터링_구현
  2. 인터랙티브_날짜_필터링
  3. 문자열_패턴_필터링
  4. 조건부_다중_필터링
  5. Lazy_Evaluation_필터링
  6. 동적_조건_생성
  7. 윈도우_함수_필터링
  8. Null_값_필터링
  9. 정규표현식_고급_필터링
  10. 타입_기반_필터링

1. 기본_필터링_구현

시작하며

여러분이 수백만 건의 데이터를 다루다가 특정 조건에 맞는 데이터만 추출해야 하는 상황을 겪어본 적 있나요? 예를 들어, 전체 고객 데이터에서 최근 3개월 동안 구매한 고객만 찾아야 하거나, 특정 금액 이상 결제한 사람들만 분석해야 하는 경우 말이죠.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 특히 Pandas로 대용량 데이터를 필터링하다 보면 성능 문제로 고민하게 되는데, 메모리 사용량이 급증하거나 처리 시간이 오래 걸려서 작업 효율이 떨어지게 됩니다.

바로 이럴 때 필요한 것이 Polars의 기본 필터링입니다. Polars는 Rust 기반으로 만들어져 Pandas보다 훨씬 빠른 속도로 데이터를 필터링할 수 있으며, LazyFrame을 사용하면 쿼리 최적화까지 자동으로 해줍니다.

개요

간단히 말해서, Polars의 기본 필터링은 DataFrame에서 조건에 맞는 행만 선택하는 작업입니다. filter() 메서드를 사용하면 SQL의 WHERE 절처럼 직관적으로 데이터를 걸러낼 수 있습니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 데이터 분석의 첫 단계는 항상 필요한 데이터만 추출하는 것입니다. 예를 들어, 전자상거래 데이터에서 환불 건만 분석하거나, 특정 지역의 매출만 집계할 때 필터링이 필수적입니다.

기존에는 Pandas의 boolean indexing이나 query() 메서드를 사용했다면, 이제는 Polars의 filter()로 더 빠르고 메모리 효율적으로 처리할 수 있습니다. 특히 대용량 데이터에서는 5배 이상 성능 차이가 납니다.

Polars 필터링의 핵심 특징은 첫째, 표현식(Expression) 기반으로 작동하여 복잡한 조건도 쉽게 표현할 수 있고, 둘째, Lazy Evaluation을 지원하여 실제 실행 전에 쿼리를 최적화하며, 셋째, 멀티스레드를 자동으로 활용하여 병렬 처리가 가능하다는 점입니다. 이러한 특징들이 실무에서 대용량 데이터를 다룰 때 생산성과 성능을 크게 향상시켜줍니다.

코드 예제

import polars as pl

# 샘플 DataFrame 생성
df = pl.DataFrame({
    "name": ["Alice", "Bob", "Charlie", "David", "Emma"],
    "age": [25, 30, 35, 28, 32],
    "salary": [50000, 60000, 75000, 55000, 70000],
    "department": ["IT", "HR", "IT", "Finance", "IT"]
})

# 기본 필터링: 나이가 30세 이상인 직원 찾기
filtered = df.filter(pl.col("age") >= 30)
print(filtered)

# 여러 조건 결합: IT 부서이면서 연봉이 60000 이상인 직원
it_high_earners = df.filter(
    (pl.col("department") == "IT") & (pl.col("salary") >= 60000)
)
print(it_high_earners)

설명

이것이 하는 일: Polars의 filter() 메서드는 DataFrame에서 지정한 조건을 만족하는 행들만 추출하여 새로운 DataFrame을 반환합니다. 원본 데이터는 변경되지 않고, 조건에 맞는 데이터만 담긴 새로운 객체가 생성됩니다.

첫 번째로, pl.col("age")는 'age' 컬럼을 나타내는 표현식 객체를 생성합니다. 이 표현식에 >= 30 같은 비교 연산자를 적용하면 각 행마다 True/False를 반환하는 불린 표현식이 만들어집니다.

왜 이렇게 하는지 궁금하실 텐데, 이 방식은 타입 안정성을 보장하고 쿼리 최적화를 가능하게 합니다. 두 번째로, filter() 메서드가 실행되면서 불린 표현식을 각 행에 적용합니다.

내부에서는 Rust로 작성된 고성능 엔진이 멀티스레드로 병렬 처리를 수행하여, Pandas보다 훨씬 빠르게 조건을 검사합니다. 특히 수백만 건 데이터에서 이 차이가 극명하게 드러납니다.

세 번째로, 여러 조건을 결합할 때는 &(AND), |(OR), ~(NOT) 연산자를 사용합니다. 각 조건을 괄호로 감싸야 하는데, 이는 Python의 연산자 우선순위 때문입니다.

마지막으로 조건을 모두 만족하는 행들만 모아서 새로운 DataFrame을 만들어냅니다. 여러분이 이 코드를 사용하면 대용량 데이터에서도 밀리초 단위로 빠른 필터링을 수행할 수 있습니다.

메모리 사용량도 최소화되고, 코드도 SQL처럼 읽기 쉬워서 유지보수가 간편해집니다. 또한 표현식 기반이라 복잡한 조건도 조합하기 쉽고, 나중에 Lazy Evaluation으로 전환하기도 쉽습니다.

실전 팁

💡 여러 조건을 결합할 때는 반드시 각 조건을 괄호로 감싸세요. (pl.col("age") >= 30) & (pl.col("salary") > 50000) 형식으로 작성하지 않으면 연산자 우선순위 오류가 발생합니다.

💡 대용량 데이터는 LazyFrame으로 변환 후 필터링하세요. df.lazy().filter(...).collect() 형태로 사용하면 쿼리 최적화가 자동으로 적용되어 성능이 향상됩니다.

💡 문자열 비교 시 대소문자를 구분하지 않으려면 pl.col("name").str.to_lowercase()로 변환 후 비교하세요. 실무에서 사용자 입력 데이터를 다룰 때 매우 유용합니다.

💡 is_null(), is_not_null() 메서드로 결측치를 필터링할 수 있습니다. df.filter(pl.col("email").is_not_null())처럼 사용하면 유효한 데이터만 추출할 수 있습니다.

💡 성능 비교가 필요하면 %%timeit 매직 커맨드로 측정하세요. Pandas와 Polars의 속도 차이를 직접 확인하면 Polars의 장점을 체감할 수 있습니다.


2. 인터랙티브_날짜_필터링

시작하며

여러분이 시계열 데이터를 분석하면서 "최근 1주일", "지난달", "특정 기간" 같은 조건으로 데이터를 추출해야 하는 상황을 자주 만나셨을 겁니다. 특히 실시간 대시보드나 리포트를 만들 때 날짜 범위를 동적으로 변경하면서 데이터를 확인해야 하는 경우가 많죠.

이런 작업이 어려운 이유는 날짜 데이터 타입 변환, 시간대 처리, 기간 계산 등이 복잡하기 때문입니다. Pandas에서는 to_datetime()으로 변환하고 timedelta로 계산하는 과정이 번거롭고, 코드도 길어져서 가독성이 떨어집니다.

바로 이럴 때 필요한 것이 Polars의 날짜 필터링 기능입니다. dt 네임스페이스를 활용하면 날짜 연산과 비교를 직관적으로 수행할 수 있고, 문자열 파싱도 자동으로 처리되어 생산성이 크게 향상됩니다.

개요

간단히 말해서, Polars의 날짜 필터링은 datetime 타입 컬럼에 대해 기간 조건을 적용하여 시계열 데이터를 추출하는 기능입니다. dt 접근자를 통해 연, 월, 일, 요일 등을 추출하고 비교할 수 있습니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 로그 분석, 매출 추이 분석, 사용자 행동 분석 등 대부분의 데이터 분석 작업에서 날짜 기반 필터링이 핵심입니다. 예를 들어, 최근 30일간의 활성 사용자를 추출하거나, 특정 분기의 매출 데이터만 분석할 때 필수적으로 사용됩니다.

기존에는 Pandas의 dt 접근자와 datetime 모듈을 조합해서 사용했다면, 이제는 Polars의 통합된 날짜 표현식으로 더 간결하고 빠르게 처리할 수 있습니다. 특히 문자열을 자동으로 datetime으로 파싱하는 기능이 편리합니다.

Polars 날짜 필터링의 핵심 특징은 첫째, str_to_datetime()으로 다양한 날짜 형식을 자동 인식하고, 둘째, dt 네임스페이스로 날짜 구성요소에 쉽게 접근하며, 셋째, duration 타입으로 기간 계산이 직관적이라는 점입니다. 이러한 특징들이 시계열 데이터 분석을 훨씬 쉽고 빠르게 만들어줍니다.

코드 예제

import polars as pl
from datetime import datetime, timedelta

# 날짜가 포함된 DataFrame 생성
df = pl.DataFrame({
    "transaction_date": ["2024-01-15", "2024-02-20", "2024-03-10", "2024-03-25", "2024-04-05"],
    "amount": [1000, 1500, 2000, 1200, 1800],
    "customer": ["Alice", "Bob", "Charlie", "Alice", "David"]
}).with_columns(
    pl.col("transaction_date").str.strptime(pl.Date, "%Y-%m-%d")
)

# 최근 60일 이내의 거래만 필터링
today = datetime.now().date()
recent_cutoff = today - timedelta(days=60)
recent_transactions = df.filter(pl.col("transaction_date") >= recent_cutoff)

# 특정 월의 데이터 필터링 (3월)
march_data = df.filter(pl.col("transaction_date").dt.month() == 3)
print(march_data)

설명

이것이 하는 일: 날짜 필터링은 문자열 형태의 날짜 데이터를 datetime 타입으로 변환한 후, 특정 기간이나 조건에 맞는 행들만 추출합니다. 이를 통해 시계열 데이터에서 원하는 시간 범위의 데이터만 분석할 수 있습니다.

첫 번째로, str.strptime() 메서드가 문자열을 날짜 타입으로 파싱합니다. "%Y-%m-%d" 형식 지정자를 사용하여 "2024-01-15" 같은 문자열을 Polars의 Date 타입으로 변환합니다.

왜 이렇게 하는지 궁금하실 텐데, 날짜 타입으로 변환해야만 날짜 연산과 비교가 가능하고, 내부적으로 효율적인 저장 방식을 사용할 수 있습니다. 두 번째로, datetime 모듈로 기준 날짜를 계산합니다.

today - timedelta(days=60)처럼 현재 날짜에서 60일을 빼서 기준점을 만들고, 이 값보다 크거나 같은 날짜만 필터링합니다. 내부에서는 날짜를 정수로 변환하여 비교하므로 매우 빠릅니다.

세 번째로, dt.month() 메서드로 날짜에서 월 정보만 추출합니다. dt 네임스페이스는 year(), day(), weekday() 등 다양한 메서드를 제공하여, 복잡한 날짜 조건도 쉽게 표현할 수 있습니다.

마지막으로 조건을 만족하는 행들이 새로운 DataFrame으로 반환됩니다. 여러분이 이 코드를 사용하면 동적인 날짜 범위 필터링이 가능해집니다.

예를 들어 매일 실행되는 리포트에서 "최근 N일" 조건을 적용할 때, 날짜를 하드코딩하지 않고 자동으로 계산할 수 있습니다. 또한 월별, 분기별, 연도별 집계를 위한 그룹핑 전에 필요한 데이터만 추출하여 처리 속도를 높일 수 있습니다.

실전 팁

💡 날짜 형식이 일관되지 않으면 try_strptime()을 사용하세요. 파싱 실패 시 null을 반환하므로 에러 없이 안전하게 처리할 수 있습니다.

💡 시간대(timezone) 정보가 중요한 경우 dt.convert_time_zone()으로 변환하세요. 글로벌 서비스에서 사용자 시간대별 분석 시 필수적입니다.

💡 날짜 범위 필터링은 is_between() 메서드를 활용하면 더 간결합니다. pl.col("date").is_between(start, end)처럼 사용하면 가독성이 좋아집니다.

💡 요일별 분석이 필요하면 dt.weekday()를 사용하세요. 0(월요일)부터 6(일요일)까지 숫자를 반환하여 주중/주말 구분이 쉽습니다.

💡 성능을 위해 날짜 컬럼에는 인덱스를 생성하거나, 날짜순으로 정렬된 상태로 유지하세요. 범위 쿼리 성능이 크게 향상됩니다.


3. 문자열_패턴_필터링

시작하며

여러분이 사용자 입력 데이터나 로그 파일을 분석할 때, 특정 키워드를 포함하거나 특정 패턴과 일치하는 데이터만 찾아야 하는 경우가 많습니다. 예를 들어, 이메일 주소 형식이 올바른지 검증하거나, 특정 도메인의 사용자만 추출하거나, 에러 메시지에서 특정 패턴을 찾아야 하는 상황이죠.

이런 문제는 실제 데이터 전처리 과정에서 가장 빈번하게 발생합니다. 문자열 데이터는 일관성이 없는 경우가 많아서, 단순 비교만으로는 원하는 데이터를 추출하기 어렵습니다.

정규표현식을 사용하면 복잡한 패턴도 찾을 수 있지만, 성능과 가독성이 문제가 됩니다. 바로 이럴 때 필요한 것이 Polars의 문자열 필터링 기능입니다.

str 네임스페이스에서 제공하는 다양한 메서드로 포함 여부 검사, 패턴 매칭, 대소문자 변환 등을 빠르고 직관적으로 수행할 수 있습니다.

개요

간단히 말해서, Polars의 문자열 필터링은 str 네임스페이스의 메서드를 활용하여 텍스트 데이터에서 특정 조건에 맞는 행을 추출하는 기능입니다. contains(), starts_with(), ends_with() 등의 메서드로 다양한 패턴을 검사할 수 있습니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 실제 데이터는 대부분 정형화되지 않은 텍스트를 포함합니다. 예를 들어, 고객 피드백에서 특정 키워드를 포함한 의견만 분석하거나, URL에서 특정 도메인만 필터링하거나, 제품 코드가 특정 접두사로 시작하는 항목만 추출할 때 필수적으로 사용됩니다.

기존에는 Pandas의 str.contains()나 apply() 함수로 처리했다면, 이제는 Polars의 최적화된 문자열 연산으로 훨씬 빠르게 처리할 수 있습니다. 특히 정규표현식 사용 시 Rust 기반 regex 엔진이 적용되어 성능이 뛰어납니다.

Polars 문자열 필터링의 핵심 특징은 첫째, 다양한 문자열 메서드를 체이닝으로 조합할 수 있고, 둘째, 정규표현식을 네이티브로 지원하여 복잡한 패턴도 빠르게 처리하며, 셋째, null 값을 안전하게 다루어 예외 없이 작동한다는 점입니다. 이러한 특징들이 텍스트 데이터 전처리와 분석을 매우 효율적으로 만들어줍니다.

코드 예제

import polars as pl

# 사용자 데이터 생성
df = pl.DataFrame({
    "user_id": [1, 2, 3, 4, 5],
    "email": ["alice@gmail.com", "bob@company.co.kr", "charlie@gmail.com", None, "emma@yahoo.com"],
    "status": ["active", "ACTIVE", "inactive", "pending", "Active"],
    "comment": ["Great product!", "Need help", "Excellent service", "Bug report: error 404", "Thanks!"]
})

# Gmail 사용자만 필터링
gmail_users = df.filter(pl.col("email").str.contains("gmail"))

# 대소문자 구분 없이 active 상태인 사용자 필터링
active_users = df.filter(pl.col("status").str.to_lowercase() == "active")

# 특정 키워드를 포함한 댓글 필터링 (정규표현식 사용)
bug_reports = df.filter(pl.col("comment").str.contains(r"(?i)bug|error"))
print(bug_reports)

설명

이것이 하는 일: 문자열 필터링은 텍스트 컬럼에 대해 패턴 매칭, 포함 여부 검사, 대소문자 변환 등을 수행하여 조건에 맞는 행만 추출합니다. null 값은 자동으로 제외되어 안전하게 처리됩니다.

첫 번째로, str.contains("gmail") 메서드가 각 이메일 주소에 "gmail" 문자열이 포함되어 있는지 검사합니다. 이 메서드는 불린 시리즈를 반환하는데, None 값이 있으면 해당 행은 False로 처리됩니다.

왜 이렇게 하는지 궁금하실 텐데, null 값에 대한 안전한 처리가 내장되어 있어서 별도의 null 체크 없이 사용할 수 있습니다. 두 번째로, str.to_lowercase()로 모든 문자를 소문자로 변환한 후 비교합니다.

원본 데이터에 "active", "ACTIVE", "Active"처럼 대소문자가 섞여 있어도, 변환 후 비교하면 일관되게 필터링할 수 있습니다. 내부에서는 유니코드를 올바르게 처리하여 다국어 텍스트에도 안전합니다.

세 번째로, 정규표현식 패턴 r"(?i)bug|error"를 사용하여 "bug" 또는 "error"를 포함한 댓글을 찾습니다. (?i) 플래그는 대소문자를 무시하라는 의미이고, | 는 OR 연산을 나타냅니다.

Polars는 Rust의 regex 크레이트를 사용하여 정규표현식을 매우 빠르게 처리합니다. 여러분이 이 코드를 사용하면 복잡한 텍스트 데이터를 쉽게 필터링할 수 있습니다.

사용자 입력 검증, 로그 분석, 텍스트 마이닝 등 다양한 상황에서 활용 가능합니다. 특히 대용량 텍스트 데이터에서 키워드 검색 시 Pandas보다 몇 배 빠른 성능을 경험할 수 있습니다.

실전 팁

💡 정규표현식 사용 시 literal=False 파라미터를 명시하세요. contains()는 기본적으로 리터럴 문자열 검색이므로, 정규표현식을 사용하려면 이 옵션이 필요합니다.

💡 여러 키워드 중 하나라도 포함되어 있는지 확인하려면 정규표현식의 | 연산자를 사용하세요. "keyword1|keyword2|keyword3" 패턴으로 여러 조건을 한 번에 검사할 수 있습니다.

💡 starts_with()와 ends_with()는 contains()보다 훨씬 빠릅니다. 접두사나 접미사 검사가 목적이라면 이 메서드들을 사용하세요.

💡 null 값을 명시적으로 처리하려면 fill_null("")로 빈 문자열로 채운 후 필터링하세요. 빈 문자열과 null을 구분해야 하는 경우 유용합니다.

💡 대량의 텍스트 검색은 인덱싱을 고려하세요. 자주 검색하는 패턴이 있다면 별도 컬럼으로 추출하여 인덱싱하면 성능이 크게 향상됩니다.


4. 조건부_다중_필터링

시작하며

여러분이 데이터를 분석하다 보면 하나의 조건이 아니라 여러 조건을 조합해야 하는 경우가 훨씬 많습니다. 예를 들어, "IT 부서의 연봉 6000 이상인 30세 미만 직원" 같은 복잡한 조건이나, "프리미엄 회원이면서 최근 3개월 내 구매 이력이 있고 리뷰를 3개 이상 작성한 고객" 같은 다층적 조건을 처리해야 하죠.

이런 문제는 실제 비즈니스 로직을 구현할 때 필수적입니다. 단순히 데이터를 조회하는 것을 넘어서, 특정 타겟 세그먼트를 추출하거나, 이상 거래를 탐지하거나, 맞춤형 추천을 위한 사용자 그룹을 만들 때 복잡한 조건 조합이 필요합니다.

바로 이럴 때 필요한 것이 Polars의 조건부 다중 필터링입니다. 논리 연산자를 활용하여 여러 조건을 AND, OR, NOT으로 결합하고, when-then-otherwise 구문으로 조건별로 다른 처리를 할 수 있습니다.

개요

간단히 말해서, 조건부 다중 필터링은 여러 개의 필터 조건을 논리 연산자(&, |, ~)로 결합하거나, when() 메서드로 복잡한 조건부 로직을 구현하는 기법입니다. SQL의 WHERE 절처럼 AND, OR 조건을 자유롭게 조합할 수 있습니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 실제 데이터 분석에서는 단일 조건만으로는 의미 있는 인사이트를 얻기 어렵습니다. 예를 들어, 마케팅 캠페인 대상자를 선정할 때 "구매 금액, 방문 빈도, 회원 등급, 지역" 등 여러 조건을 동시에 고려해야 정확한 타겟팅이 가능합니다.

기존에는 Pandas에서 boolean indexing을 중첩하거나 query() 메서드를 사용했다면, 이제는 Polars의 표현식 체이닝으로 더 읽기 쉽고 유지보수하기 좋은 코드를 작성할 수 있습니다. 특히 복잡한 조건도 명확하게 표현됩니다.

Polars 다중 필터링의 핵심 특징은 첫째, 괄호로 조건을 그룹화하여 우선순위를 명확히 할 수 있고, 둘째, when-then-otherwise로 if-elif-else 로직을 표현식으로 구현할 수 있으며, 셋째, is_in() 메서드로 여러 값 중 하나와 일치하는지 쉽게 확인할 수 있다는 점입니다. 이러한 특징들이 복잡한 비즈니스 로직을 데이터 쿼리로 직접 표현할 수 있게 해줍니다.

코드 예제

import polars as pl

# 고객 데이터 생성
df = pl.DataFrame({
    "customer_id": [1, 2, 3, 4, 5, 6],
    "age": [25, 35, 45, 28, 32, 50],
    "membership": ["Gold", "Silver", "Gold", "Bronze", "Gold", "Silver"],
    "purchase_count": [10, 5, 20, 3, 15, 8],
    "total_spent": [5000, 2000, 10000, 1000, 7000, 3000]
})

# 복잡한 다중 조건: (Gold 회원이면서 구매 10회 이상) OR (총 구매액 5000 이상이면서 30세 이상)
premium_customers = df.filter(
    ((pl.col("membership") == "Gold") & (pl.col("purchase_count") >= 10)) |
    ((pl.col("total_spent") >= 5000) & (pl.col("age") >= 30))
)

# is_in으로 여러 값 중 하나 매칭
target_members = df.filter(pl.col("membership").is_in(["Gold", "Silver"]))

# when-then-otherwise로 조건부 필터링
df_with_segment = df.with_columns(
    pl.when(pl.col("total_spent") >= 7000).then(pl.lit("VIP"))
      .when(pl.col("total_spent") >= 3000).then(pl.lit("Premium"))
      .otherwise(pl.lit("Standard"))
      .alias("segment")
)
vip_customers = df_with_segment.filter(pl.col("segment") == "VIP")
print(vip_customers)

설명

이것이 하는 일: 조건부 다중 필터링은 여러 개의 불린 조건을 논리 연산자로 결합하여 복잡한 필터링 로직을 구현합니다. 각 조건은 독립적으로 평가된 후, AND나 OR 연산으로 최종 결과가 결정됩니다.

첫 번째로, 각 조건을 괄호로 감싸서 명확히 구분합니다. ((조건1) & (조건2)) | ((조건3) & (조건4)) 형태로 작성하면 연산자 우선순위 문제를 방지할 수 있습니다.

왜 이렇게 하는지 궁금하실 텐데, Python의 비트 연산자(&, |)는 비교 연산자보다 우선순위가 높아서 괄호 없이 사용하면 예상과 다른 결과가 나올 수 있습니다. 두 번째로, is_in() 메서드는 SQL의 IN 절처럼 작동합니다.

리스트에 포함된 값 중 하나라도 일치하면 True를 반환하여, 여러 개의 OR 조건을 간결하게 표현할 수 있습니다. 내부적으로는 해시 테이블을 사용하여 검색 속도가 매우 빠릅니다.

세 번째로, when-then-otherwise 체인은 조건부 값 생성을 위한 표현식입니다. 첫 번째 when 조건이 참이면 해당 then 값을, 거짓이면 다음 when을 검사하고, 모든 조건이 거짓이면 otherwise 값을 반환합니다.

이를 활용하면 조건에 따라 새로운 컬럼을 만들고, 그 컬럼으로 필터링할 수 있습니다. 여러분이 이 코드를 사용하면 SQL처럼 직관적으로 복잡한 조건을 표현할 수 있습니다.

비즈니스 로직이 복잡해도 코드는 읽기 쉽게 유지할 수 있고, 나중에 조건이 변경되어도 수정이 간편합니다. 또한 여러 조건을 한 번에 평가하므로 성능도 우수합니다.

실전 팁

💡 복잡한 조건은 미리 변수로 추출하세요. condition1 = pl.col("age") > 30 형태로 저장하면 재사용이 쉽고 가독성도 향상됩니다.

💡 NOT 연산은 ~(틸드) 연산자를 사용하세요. ~pl.col("status").is_in(["inactive", "banned"])처럼 조건을 부정할 수 있습니다.

💡 is_in() 사용 시 대량의 값은 set으로 변환하세요. 리스트보다 set이 검색 속도가 빠르며, Polars가 자동으로 최적화합니다.

💡 when-then-otherwise는 select()나 with_columns()에서만 사용 가능합니다. filter() 내부에서 직접 사용할 수 없으므로, 먼저 컬럼을 생성한 후 필터링하세요.

💡 조건이 너무 복잡하면 UDF(User Defined Function)보다 표현식을 권장합니다. UDF는 Python으로 실행되어 느리지만, 표현식은 Rust로 최적화됩니다.


5. Lazy_Evaluation_필터링

시작하며

여러분이 수백 GB 규모의 대용량 데이터를 다룰 때, 메모리 부족 문제나 느린 처리 속도 때문에 고민해본 적 있으신가요? 특히 여러 단계의 필터링과 변환을 거쳐야 할 때, 각 단계마다 전체 데이터를 처리하느라 시간과 리소스가 낭비되는 경우가 많습니다.

이런 문제는 대규모 데이터 파이프라인에서 심각한 병목이 됩니다. Pandas는 즉시 실행(eager execution) 방식이라 각 연산마다 중간 결과를 메모리에 저장하므로, 데이터가 커질수록 메모리 오버플로우가 발생하기 쉽습니다.

바로 이럴 때 필요한 것이 Polars의 Lazy Evaluation입니다. 쿼리를 정의만 하고 실제 실행은 collect() 호출 시점까지 미루어, 전체 쿼리를 최적화한 후 한 번에 실행하여 성능을 극대화합니다.

개요

간단히 말해서, Lazy Evaluation은 데이터 처리 쿼리를 즉시 실행하지 않고 계획만 세워두었다가, collect()를 호출할 때 전체 쿼리를 최적화하여 한 번에 실행하는 방식입니다. lazy() 메서드로 LazyFrame을 생성하여 사용합니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 실제 데이터 분석에서는 여러 단계의 필터링, 조인, 집계가 필요합니다. 예를 들어, 로그 데이터를 읽어서 특정 사용자만 필터링하고, 날짜별로 집계한 후, 다른 테이블과 조인하는 복잡한 파이프라인에서 Lazy Evaluation을 사용하면 중복 작업을 제거하고 실행 계획을 최적화합니다.

기존에는 Pandas에서 각 단계마다 중간 DataFrame을 생성하며 실행했다면, 이제는 Polars의 LazyFrame으로 전체 파이프라인을 선언한 후 한 번에 실행할 수 있습니다. 특히 쿼리 옵티마이저가 자동으로 조건 푸시다운, 중복 제거, 병렬화 등을 적용합니다.

Polars Lazy Evaluation의 핵심 특징은 첫째, 쿼리 최적화가 자동으로 수행되어 불필요한 연산을 제거하고, 둘째, 메모리 사용량이 크게 줄어들어 대용량 데이터 처리가 가능하며, 셋째, 병렬 처리가 최대한 활용되어 멀티코어 성능을 100% 발휘한다는 점입니다. 이러한 특징들이 대규모 데이터 파이프라인의 성능을 몇 배에서 수십 배까지 향상시킵니다.

코드 예제

import polars as pl

# 큰 데이터셋 가정 (실제로는 CSV, Parquet 등에서 로드)
df = pl.DataFrame({
    "user_id": range(1, 1000001),
    "age": [20 + (i % 50) for i in range(1000000)],
    "purchase_amount": [100 + (i % 1000) for i in range(1000000)],
    "country": ["US" if i % 3 == 0 else "UK" if i % 3 == 1 else "KR" for i in range(1000000)]
})

# Lazy Evaluation으로 복잡한 쿼리 정의 (실행은 아직 안 됨)
lazy_query = (
    df.lazy()  # LazyFrame으로 변환
    .filter(pl.col("age") >= 30)  # 30세 이상 필터링
    .filter(pl.col("country").is_in(["US", "KR"]))  # 국가 필터링
    .filter(pl.col("purchase_amount") >= 500)  # 구매액 필터링
    .select(["user_id", "age", "purchase_amount"])  # 필요한 컬럼만 선택
    .sort("purchase_amount", descending=True)  # 정렬
    .head(100)  # 상위 100개만
)

# 쿼리 최적화 계획 확인 (선택사항)
print(lazy_query.explain())

# 실제 실행 및 결과 수집
result = lazy_query.collect()
print(result)

설명

이것이 하는 일: Lazy Evaluation은 데이터 처리 작업을 즉시 실행하지 않고 논리적 계획(Logical Plan)으로 저장해두었다가, collect() 호출 시 최적화된 물리적 계획(Physical Plan)으로 변환하여 실행합니다. 이를 통해 불필요한 연산을 제거하고 성능을 극대화합니다.

첫 번째로, lazy() 메서드가 DataFrame을 LazyFrame으로 변환합니다. 이 시점부터는 어떤 연산도 실제로 실행되지 않고, 쿼리 그래프에 노드로 추가만 됩니다.

왜 이렇게 하는지 궁금하실 텐데, 전체 쿼리를 보지 않으면 최적화할 수 없기 때문입니다. 예를 들어, 나중에 특정 컬럼만 선택한다는 것을 알면 처음부터 그 컬럼만 읽을 수 있습니다.

두 번째로, 여러 filter() 호출이 체이닝됩니다. Polars의 쿼리 옵티마이저는 이 필터들을 분석하여 조건 푸시다운(Predicate Pushdown)을 적용합니다.

예를 들어, 파일에서 데이터를 읽을 때부터 조건을 적용하여 불필요한 데이터는 아예 메모리에 로드하지 않습니다. select()로 컬럼을 선택하면 프로젝션 푸시다운(Projection Pushdown)도 적용되어 필요한 컬럼만 읽습니다.

세 번째로, explain() 메서드로 최적화된 실행 계획을 확인할 수 있습니다. 어떤 최적화가 적용되었는지 보면 Lazy Evaluation의 효과를 이해할 수 있습니다.

마지막으로 collect()를 호출하면 실제 실행이 시작되고, 최적화된 계획에 따라 멀티스레드로 병렬 처리되어 결과가 반환됩니다. 여러분이 이 코드를 사용하면 대용량 데이터를 메모리에 모두 올리지 않고도 처리할 수 있습니다.

필터링 조건이 많아도 성능 저하가 거의 없으며, 복잡한 파이프라인도 간결하게 표현할 수 있습니다. 또한 쿼리 계획을 미리 검증할 수 있어 프로덕션 환경에서 안정적으로 사용할 수 있습니다.

실전 팁

💡 파일 읽기부터 Lazy로 시작하세요. pl.scan_csv(), pl.scan_parquet()를 사용하면 처음부터 LazyFrame으로 로드되어 최적화 효과가 극대화됩니다.

💡 explain() 메서드로 쿼리 계획을 확인하는 습관을 들이세요. 어떤 최적화가 적용되었는지 보면 쿼리를 더 효율적으로 작성할 수 있습니다.

💡 collect()는 최종 결과가 필요할 때만 호출하세요. 중간에 collect()를 호출하면 Lazy Evaluation의 이점이 사라지므로, 가능한 한 모든 작업을 Lazy 체인으로 연결하세요.

💡 streaming=True 옵션을 사용하면 메모리보다 큰 데이터도 처리 가능합니다. lazy_query.collect(streaming=True)로 스트리밍 실행하면 청크 단위로 처리됩니다.

💡 개발 시에는 작은 샘플로 테스트하고, 프로덕션에서는 Lazy로 전환하세요. head()나 sample()로 쿼리 로직을 검증한 후, 전체 데이터에 Lazy Evaluation을 적용하면 안전합니다.


6. 동적_조건_생성

시작하며

여러분이 사용자 입력이나 설정 파일에 따라 필터링 조건을 동적으로 변경해야 하는 상황을 만나본 적 있나요? 예를 들어, 웹 대시보드에서 사용자가 선택한 필터 옵션에 따라 쿼리를 다르게 실행하거나, 설정 파일의 조건에 따라 데이터 처리 파이프라인을 다르게 구성해야 하는 경우가 있습니다.

이런 문제는 실제 프로덕션 환경에서 매우 흔합니다. 하드코딩된 필터 조건만으로는 유연성이 부족하여, 사용자 요구사항이나 비즈니스 로직이 변경될 때마다 코드를 수정해야 합니다.

특히 조건의 개수나 타입이 실행 시점에 결정되는 경우 처리가 복잡해집니다. 바로 이럴 때 필요한 것이 동적 조건 생성 기법입니다.

Polars 표현식을 프로그래밍 방식으로 조합하고, 리스트 컴프리헨션이나 반복문으로 조건을 동적으로 만들어 유연한 쿼리를 구성할 수 있습니다.

개요

간단히 말해서, 동적 조건 생성은 런타임에 사용자 입력이나 설정에 따라 필터 조건을 프로그래밍 방식으로 만드는 기법입니다. Polars 표현식을 변수에 저장하고 조합하여, 코드 중복 없이 다양한 필터링 시나리오를 처리할 수 있습니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 데이터 분석 도구나 BI 대시보드는 사용자가 UI에서 선택한 옵션에 따라 쿼리를 실행해야 합니다. 예를 들어, 사용자가 "나이 범위", "지역", "회원 등급" 필터를 선택적으로 적용할 수 있다면, 모든 조합에 대한 if-else 문을 작성하는 것은 비효율적입니다.

동적으로 조건을 생성하면 코드가 간결해지고 유지보수가 쉬워집니다. 기존에는 문자열로 쿼리를 조합하거나 복잡한 if-else 분기를 사용했다면, 이제는 Polars 표현식을 리스트에 모아서 reduce()나 fold()로 결합할 수 있습니다.

타입 안정성도 유지되고 코드도 명확해집니다. 동적 조건 생성의 핵심 특징은 첫째, 표현식을 변수에 저장하여 재사용할 수 있고, 둘째, 반복문으로 조건 리스트를 생성하여 유연하게 대응할 수 있으며, 셋째, fold()나 reduce()로 여러 조건을 하나로 결합할 수 있다는 점입니다.

이러한 특징들이 복잡한 비즈니스 로직을 코드 중복 없이 구현할 수 있게 해줍니다.

코드 예제

import polars as pl
from functools import reduce

# 사용자 입력 시뮬레이션 (실제로는 웹 요청, 설정 파일 등에서 가져옴)
user_filters = {
    "min_age": 25,
    "max_age": 40,
    "countries": ["US", "KR"],
    "min_purchase": 1000
}

# 데이터 생성
df = pl.DataFrame({
    "user_id": range(1, 101),
    "age": [20 + (i % 40) for i in range(100)],
    "country": ["US" if i % 3 == 0 else "KR" if i % 3 == 1 else "UK" for i in range(100)],
    "purchase_amount": [500 + (i * 50) for i in range(100)]
})

# 동적으로 조건 리스트 생성
conditions = []
if "min_age" in user_filters and user_filters["min_age"]:
    conditions.append(pl.col("age") >= user_filters["min_age"])
if "max_age" in user_filters and user_filters["max_age"]:
    conditions.append(pl.col("age") <= user_filters["max_age"])
if "countries" in user_filters and user_filters["countries"]:
    conditions.append(pl.col("country").is_in(user_filters["countries"]))
if "min_purchase" in user_filters and user_filters["min_purchase"]:
    conditions.append(pl.col("purchase_amount") >= user_filters["min_purchase"])

# 모든 조건을 AND로 결합
if conditions:
    combined_condition = reduce(lambda a, b: a & b, conditions)
    result = df.filter(combined_condition)
    print(result)

설명

이것이 하는 일: 동적 조건 생성은 사용자 입력이나 설정에 따라 Polars 표현식을 프로그래밍 방식으로 만들고 결합하여, 하드코딩 없이 다양한 필터링 시나리오를 처리합니다. 조건의 개수와 타입이 실행 시점에 결정되어도 문제없이 작동합니다.

첫 번째로, 사용자 입력을 딕셔너리로 받아서 어떤 필터가 활성화되었는지 확인합니다. 각 필터 키가 존재하고 값이 유효하면 해당하는 Polars 표현식을 생성하여 conditions 리스트에 추가합니다.

왜 이렇게 하는지 궁금하실 텐데, 사용자가 일부 필터만 선택할 수 있기 때문에 조건부로 표현식을 생성해야 합니다. 두 번째로, 각 필터 타입에 맞는 Polars 표현식을 생성합니다.

예를 들어 min_age는 >= 연산자를, countries는 is_in() 메서드를 사용합니다. 모든 표현식은 불린 시리즈를 반환하므로, 나중에 AND나 OR로 결합할 수 있습니다.

세 번째로, reduce() 함수로 모든 조건을 하나의 표현식으로 결합합니다. lambda a, b: a & b는 두 조건을 AND로 연결하는 함수이고, reduce는 이를 리스트의 모든 요소에 순차적으로 적용합니다.

조건이 하나만 있어도 작동하고, 여러 개여도 자동으로 결합됩니다. 마지막으로 결합된 조건으로 filter()를 호출하여 결과를 얻습니다.

여러분이 이 코드를 사용하면 사용자 인터페이스에서 동적으로 필터를 추가하거나 제거해도 백엔드 코드를 수정할 필요가 없습니다. 설정 기반의 데이터 파이프라인을 구축할 때도 매우 유용하며, 테스트도 쉬워집니다.

또한 표현식을 재사용할 수 있어 코드 중복이 줄어들고 유지보수성이 향상됩니다.

실전 팁

💡 조건이 없을 때를 대비하여 if conditions: 체크를 필수로 하세요. 빈 리스트에 reduce()를 적용하면 에러가 발생합니다.

💡 OR 조건으로 결합하려면 lambda a, b: a | b를 사용하세요. 비즈니스 로직에 따라 AND와 OR을 선택할 수 있습니다.

💡 복잡한 조건은 함수로 추출하세요. create_age_filter(min_age, max_age) 같은 헬퍼 함수를 만들면 재사용성과 가독성이 좋아집니다.

💡 사용자 입력 검증을 반드시 하세요. 타입이 올바른지, 범위가 유효한지 확인하지 않으면 런타임 에러나 보안 문제가 발생할 수 있습니다.

💡 조건 생성 로직을 테스트하세요. 다양한 입력 조합에 대해 유닛 테스트를 작성하면 예상치 못한 버그를 조기에 발견할 수 있습니다.


7. 윈도우_함수_필터링

시작하며

여러분이 데이터를 분석하다가 "각 그룹 내에서 상위 N개", "이동 평균보다 큰 값", "순위가 특정 범위인 행" 같은 조건으로 필터링해야 하는 상황을 만나신 적 있나요? 예를 들어, 각 부서별 연봉 상위 3명, 각 고객의 최근 5개 구매 내역, 주가가 7일 이동평균보다 높은 날짜 등을 찾아야 하는 경우입니다.

이런 문제는 단순 필터링으로는 해결할 수 없습니다. 그룹 내에서의 순위, 누적 합계, 이동 평균 같은 윈도우 함수 결과를 기반으로 필터링해야 하는데, SQL의 WINDOW 함수처럼 Pandas에서는 구현이 복잡하고 성능도 좋지 않습니다.

바로 이럴 때 필요한 것이 Polars의 윈도우 함수 필터링입니다. over() 메서드로 그룹별 윈도우 연산을 수행하고, 그 결과를 필터 조건으로 사용하여 복잡한 분석 쿼리를 간결하게 표현할 수 있습니다.

개요

간단히 말해서, 윈도우 함수 필터링은 over() 메서드로 그룹별 집계나 순위를 계산한 후, 그 결과 컬럼을 필터 조건으로 사용하는 기법입니다. SQL의 OVER (PARTITION BY) 절과 동일한 개념으로, 그룹별 계산 결과를 각 행에 할당합니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 실제 비즈니스 분석에서는 그룹 내 비교가 매우 중요합니다. 예를 들어, 각 카테고리별 베스트셀러, 각 지역별 매출 상위 매장, 각 사용자의 최근 활동 등을 찾을 때 윈도우 함수가 필수적입니다.

전체 데이터를 그룹화하지 않고도 그룹별 계산 결과를 유지할 수 있습니다. 기존에는 Pandas에서 groupby() 후 merge()하거나, transform()을 사용했다면, 이제는 Polars의 over()로 한 줄에 표현할 수 있습니다.

특히 여러 그룹 키나 복잡한 윈도우 정의도 쉽게 처리됩니다. Polars 윈도우 함수의 핵심 특징은 첫째, over()로 PARTITION BY를 간결하게 표현할 수 있고, 둘째, rank(), row_number() 등 다양한 윈도우 함수를 지원하며, 셋째, 계산 결과를 즉시 필터 조건으로 사용할 수 있다는 점입니다.

이러한 특징들이 복잡한 분석 쿼리를 SQL처럼 직관적으로 작성할 수 있게 해줍니다.

코드 예제

import polars as pl

# 부서별 직원 데이터 생성
df = pl.DataFrame({
    "employee": ["Alice", "Bob", "Charlie", "David", "Emma", "Frank", "Grace"],
    "department": ["IT", "IT", "IT", "HR", "HR", "Sales", "Sales"],
    "salary": [70000, 80000, 60000, 65000, 75000, 55000, 85000]
})

# 각 부서별 연봉 순위 계산 후, 상위 2명만 필터링
result = df.with_columns(
    pl.col("salary").rank(method="ordinal", descending=True).over("department").alias("rank_in_dept")
).filter(
    pl.col("rank_in_dept") <= 2
)
print(result)

# 부서 평균보다 연봉이 높은 직원만 필터링
above_avg = df.filter(
    pl.col("salary") > pl.col("salary").mean().over("department")
)
print(above_avg)

# 각 부서에서 최고 연봉을 받는 직원 찾기
top_earners = df.filter(
    pl.col("salary") == pl.col("salary").max().over("department")
)
print(top_earners)

설명

이것이 하는 일: 윈도우 함수 필터링은 over() 메서드로 그룹별 집계, 순위, 누적 계산 등을 수행하고, 그 결과를 각 행에 할당한 후, 그 값을 기준으로 필터링합니다. 원본 행은 유지하면서 그룹 내 계산 결과를 활용할 수 있습니다.

첫 번째로, rank() 함수와 over("department")를 결합하여 각 부서 내에서의 연봉 순위를 계산합니다. descending=True로 높은 연봉부터 순위를 매기고, method="ordinal"로 동점자도 다른 순위를 받게 합니다.

왜 이렇게 하는지 궁금하실 텐데, over() 없이 rank()만 사용하면 전체 데이터에 대한 순위가 계산되지만, over("department")를 추가하면 각 부서별로 독립적인 순위가 계산됩니다. 두 번째로, with_columns()로 순위 컬럼을 추가한 후 filter()로 상위 2명만 선택합니다.

이 방식은 SQL의 ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC)와 동일합니다. 내부적으로는 각 그룹별로 정렬 후 순위를 매기므로, 그룹 수가 많아도 효율적으로 처리됩니다.

세 번째로, mean().over("department")로 부서별 평균 연봉을 계산하고, 각 직원의 연봉과 비교합니다. 이 표현식은 각 행에 해당 부서의 평균값을 반환하므로, 직접 비교 연산에 사용할 수 있습니다.

마지막으로 max().over()로 부서별 최대값과 일치하는 행만 필터링하여 최고 연봉자를 찾습니다. 여러분이 이 코드를 사용하면 복잡한 그룹별 분석을 간단하게 수행할 수 있습니다.

베스트셀러 분석, 이상치 탐지, 시계열 패턴 분석 등 다양한 상황에서 활용 가능합니다. 특히 대용량 데이터에서도 Polars의 최적화된 윈도우 함수가 빠른 성능을 보장합니다.

실전 팁

💡 rank() 메서드의 method 파라미터를 용도에 맞게 선택하세요. "ordinal"은 동점자도 다른 순위, "dense"는 연속된 순위, "min"은 동점자에게 같은 순위를 부여합니다.

💡 여러 컬럼으로 파티션하려면 over(["col1", "col2"])처럼 리스트를 전달하세요. 복합 그룹핑이 필요할 때 유용합니다.

💡 이동 평균 같은 rolling 윈도우는 rolling() 메서드를 사용하세요. over()와 함께 사용하면 그룹별 이동 평균을 계산할 수 있습니다.

💡 상위 N개를 추출할 때 동점자 처리에 주의하세요. rank <= N은 동점자를 모두 포함하므로, 정확히 N개만 원하면 row_number()를 사용하세요.

💡 윈도우 함수는 Lazy Evaluation과 함께 사용하면 더욱 최적화됩니다. 쿼리 플래너가 윈도우 연산을 효율적으로 배치합니다.


8. Null_값_필터링

시작하며

여러분이 실제 데이터를 다루다 보면 결측치(null, None, NaN)가 포함된 경우가 대부분입니다. 사용자가 입력을 건너뛰거나, 센서가 값을 측정하지 못하거나, 데이터 통합 과정에서 매칭되지 않은 경우 등 다양한 이유로 null 값이 생깁니다.

이런 null 값을 어떻게 처리하느냐가 분석 결과의 정확성을 좌우합니다. 이런 문제는 데이터 품질 관리에서 가장 기본적이면서도 중요한 부분입니다.

null 값을 무시하고 분석하면 편향된 결과가 나올 수 있고, 잘못 처리하면 에러가 발생할 수 있습니다. 특히 집계 함수나 통계 계산에서 null 처리 방식에 따라 결과가 크게 달라집니다.

바로 이럴 때 필요한 것이 Polars의 null 값 필터링 기능입니다. is_null(), is_not_null(), fill_null() 등의 메서드로 결측치를 안전하게 감지하고 처리하여, 데이터 품질을 높이고 신뢰할 수 있는 분석 결과를 얻을 수 있습니다.

개요

간단히 말해서, null 값 필터링은 is_null()과 is_not_null() 메서드로 결측치가 있는 행을 찾거나 제외하는 작업입니다. 또한 fill_null()로 결측치를 특정 값으로 대체하거나, drop_nulls()로 결측치가 있는 행을 제거할 수 있습니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 데이터 전처리의 첫 단계는 항상 결측치 확인입니다. 예를 들어, 고객 이메일이 없는 경우 마케팅 대상에서 제외하거나, 가격 정보가 없는 상품은 분석에서 빼거나, 결측치를 평균값으로 대체하는 등의 작업이 필수적입니다.

기존에는 Pandas에서 isna(), notna(), fillna()를 사용했다면, 이제는 Polars의 명확한 네이밍으로 더 직관적으로 사용할 수 있습니다. 특히 null과 NaN을 명확히 구분하여 처리합니다.

Polars null 처리의 핵심 특징은 첫째, null과 NaN을 구분하여 정확한 타입 처리가 가능하고, 둘째, fill_null()에서 다양한 전략(forward fill, backward fill, mean 등)을 지원하며, 셋째, 모든 null 연산이 벡터화되어 매우 빠르다는 점입니다. 이러한 특징들이 데이터 품질 관리를 효율적이고 안전하게 만들어줍니다.

코드 예제

import polars as pl

# 결측치가 포함된 데이터 생성
df = pl.DataFrame({
    "customer_id": [1, 2, 3, 4, 5, 6],
    "email": ["alice@example.com", None, "charlie@example.com", None, "emma@example.com", "frank@example.com"],
    "age": [25, 30, None, 28, 32, None],
    "purchase_amount": [1000, None, 1500, 2000, None, 1200]
})

# 이메일이 있는 고객만 필터링
with_email = df.filter(pl.col("email").is_not_null())
print(with_email)

# 모든 컬럼에 null이 없는 행만 선택
complete_rows = df.drop_nulls()
print(complete_rows)

# 특정 컬럼들에만 null이 없는 행 선택
df_filtered = df.drop_nulls(subset=["email", "age"])

# null 값을 특정 값으로 대체 후 필터링
df_filled = df.with_columns(
    pl.col("purchase_amount").fill_null(0)
).filter(
    pl.col("purchase_amount") > 0
)
print(df_filled)

설명

이것이 하는 일: null 값 필터링은 데이터에서 결측치를 식별하여 포함하거나 제외하고, 필요하면 특정 값으로 대체하는 작업입니다. 각 컬럼별로 독립적으로 처리할 수도 있고, 전체 행 단위로 처리할 수도 있습니다.

첫 번째로, is_not_null() 메서드가 각 값이 null이 아닌지 검사하여 불린 시리즈를 반환합니다. None, NULL, null 등 모든 형태의 결측치를 동일하게 처리하며, NaN(Not a Number)과는 구분됩니다.

왜 이렇게 하는지 궁금하실 텐데, Polars는 엄격한 타입 시스템을 사용하여 null과 NaN을 별도로 관리하므로, 숫자 데이터에서 더 정확한 처리가 가능합니다. 두 번째로, drop_nulls() 메서드는 하나라도 null 값이 있는 행 전체를 제거합니다.

subset 파라미터로 특정 컬럼들만 검사할 수도 있어서, 중요한 컬럼에만 null이 없는 행만 유지할 수 있습니다. 내부적으로는 모든 컬럼을 병렬로 검사하여 성능이 우수합니다.

세 번째로, fill_null() 메서드로 결측치를 다양한 방식으로 대체할 수 있습니다. 상수 값으로 채우거나, "forward"로 이전 값을 전파하거나, "mean"으로 평균값을 사용하거나, 표현식으로 동적 값을 계산할 수 있습니다.

대체 후 필터링하면 모든 행을 유지하면서도 안전하게 조건을 적용할 수 있습니다. 여러분이 이 코드를 사용하면 결측치로 인한 분석 오류를 예방할 수 있습니다.

데이터 품질 리포트를 만들 때 결측치 비율을 계산하거나, 머신러닝 전처리 시 결측치를 적절히 처리하거나, 비즈니스 규칙에 따라 불완전한 데이터를 제외할 수 있습니다.

실전 팁

💡 null 개수를 먼저 확인하세요. df.null_count()로 각 컬럼별 결측치 수를 파악하면 어떤 전략을 사용할지 결정하기 쉽습니다.

💡 NaN과 null을 동시에 처리하려면 is_nan()과 is_null()을 OR로 결합하세요. 숫자 컬럼에서 두 가지 결측치 형태를 모두 감지할 수 있습니다.

💡 fill_null(strategy="forward")는 시계열 데이터에 유용합니다. 이전 관측값으로 채우면 시간적 연속성을 유지할 수 있습니다.

💡 결측치 대체는 신중하게 선택하세요. 0으로 채우면 실제 0 값과 구분이 안 되고, 평균으로 채우면 분산이 줄어듭니다. 분석 목적에 맞는 전략을 사용하세요.

💡 중요한 컬럼의 결측치는 제거하고, 부차적인 컬럼은 대체하세요. 예를 들어 주요 식별자는 null이면 행을 제거하고, 선택적 속성은 기본값으로 채우는 전략이 효과적입니다.


9. 정규표현식_고급_필터링

시작하며

여러분이 복잡한 패턴을 가진 텍스트 데이터를 다뤄본 적 있나요? 예를 들어, 이메일 주소 형식을 검증하거나, 전화번호 패턴을 찾거나, 특정 형식의 제품 코드를 추출하거나, 로그 파일에서 에러 메시지를 파싱해야 하는 경우처럼 단순 문자열 매칭으로는 불가능한 작업들이 있습니다.

이런 문제는 비정형 텍스트 데이터를 다루는 모든 분야에서 발생합니다. 사용자 입력 검증, 로그 분석, 웹 스크래핑, 데이터 정제 등에서 정규표현식은 필수적인 도구입니다.

하지만 정규표현식은 문법이 복잡하고, 성능 최적화가 어려워서 제대로 사용하기 쉽지 않습니다. 바로 이럴 때 필요한 것이 Polars의 정규표현식 필터링입니다.

str.contains(), str.extract(), str.replace() 등의 메서드에 정규표현식을 전달하면, Rust 기반의 고성능 regex 엔진이 빠르게 처리하여 복잡한 패턴도 대용량 데이터에서 효율적으로 찾을 수 있습니다.

개요

간단히 말해서, 정규표현식 고급 필터링은 복잡한 텍스트 패턴을 정규표현식으로 정의하고, Polars의 문자열 메서드에 적용하여 조건에 맞는 데이터를 추출하거나 변환하는 기법입니다. 이메일, URL, 전화번호 같은 구조화된 텍스트를 다룰 때 매우 강력합니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 실제 데이터는 일정한 패턴은 있지만 정확히 같지 않은 경우가 많습니다. 예를 들어, 사용자가 전화번호를 "010-1234-5678", "01012345678", "010.1234.5678" 등 다양한 형식으로 입력했다면, 정규표현식으로 모든 형식을 한 번에 검증하고 정규화할 수 있습니다.

기존에는 Python의 re 모듈과 Pandas의 apply()를 조합했다면, 이제는 Polars의 네이티브 정규표현식 지원으로 수백 배 빠른 속도로 처리할 수 있습니다. Rust의 regex 크레이트는 세계에서 가장 빠른 정규표현식 엔진 중 하나입니다.

Polars 정규표현식의 핵심 특징은 첫째, str.contains()에 정규표현식을 직접 사용할 수 있고, 둘째, str.extract()로 패턴의 특정 그룹을 추출할 수 있으며, 셋째, 대소문자 무시, 멀티라인 등 다양한 플래그를 지원한다는 점입니다. 이러한 특징들이 복잡한 텍스트 처리를 간단하고 빠르게 만들어줍니다.

코드 예제

import polars as pl

# 다양한 형식의 연락처 정보 포함한 데이터
df = pl.DataFrame({
    "user_id": [1, 2, 3, 4, 5, 6],
    "contact": [
        "Email: alice@company.com",
        "Phone: 010-1234-5678",
        "alice.bob@gmail.co.kr",
        "Tel: 01012345678",
        "Invalid contact info",
        "support@example.com"
    ],
    "product_code": ["PRD-2024-001", "INV-2024-100", "PRD-2023-999", "SVC-2024-050", "PRD-2024-002", None]
})

# 이메일 형식을 포함한 행만 필터링 (정규표현식 사용)
email_pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
emails_only = df.filter(pl.col("contact").str.contains(email_pattern))
print(emails_only)

# 전화번호 패턴 필터링 (여러 형식 허용)
phone_pattern = r"0\d{2}[-.]?\d{4}[-.]?\d{4}"
phones_only = df.filter(pl.col("contact").str.contains(phone_pattern))
print(phones_only)

# 제품 코드에서 "PRD-2024-" 패턴만 필터링
prd_2024 = df.filter(pl.col("product_code").str.contains(r"^PRD-2024-\d{3}$"))
print(prd_2024)

# 이메일 도메인 추출 후 필터링
df_with_domain = df.with_columns(
    pl.col("contact").str.extract(r"@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})", 1).alias("domain")
).filter(pl.col("domain").is_not_null())
print(df_with_domain)

설명

이것이 하는 일: 정규표현식 고급 필터링은 복잡한 텍스트 패턴을 정규표현식으로 정의하고, Polars의 문자열 메서드를 통해 패턴에 일치하는 행을 찾거나, 패턴의 특정 부분을 추출하여 필터링에 활용합니다. 이메일, URL, 코드 같은 구조화된 텍스트를 효율적으로 처리합니다.

첫 번째로, 이메일 패턴 r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}"는 이메일 주소의 일반적인 구조를 표현합니다. @의 앞뒤로 허용된 문자들을 정의하고, 도메인 확장자는 최소 2글자 이상이어야 한다고 지정합니다.

왜 이렇게 하는지 궁금하실 텐데, 정규표현식은 유연한 패턴 매칭을 제공하여 다양한 변형을 한 번에 처리할 수 있습니다. 두 번째로, 전화번호 패턴 r"0\d{2}[-.]?\d{4}[-.]?\d{4}"는 010으로 시작하고, 구분자가 있거나 없을 수 있는 한국 휴대폰 번호를 표현합니다.

[-.]?는 하이픈이나 점이 0개 또는 1개 있을 수 있다는 의미이고, \d{4}는 정확히 4자리 숫자를 의미합니다. 내부적으로 Rust regex 엔진이 유한 상태 기계를 사용하여 매우 빠르게 매칭합니다.

세 번째로, str.extract() 메서드는 정규표현식의 캡처 그룹을 추출합니다. r"@([a-zA-Z0-9.-]+.[a-zA-Z]{2,})"에서 괄호로 감싼 부분이 첫 번째 그룹이고, 파라미터 1을 전달하면 이 그룹만 추출됩니다.

이를 새로운 컬럼으로 만들어 필터링에 사용하면 복잡한 조건도 간단히 표현할 수 있습니다. 여러분이 이 코드를 사용하면 비정형 텍스트 데이터를 정형화하고 검증할 수 있습니다.

사용자 입력 검증, 데이터 정제, 로그 파싱, 웹 스크래핑 후처리 등 다양한 상황에서 활용 가능하며, Pandas apply()보다 수백 배 빠른 성능을 경험할 수 있습니다.

실전 팁

💡 정규표현식 테스트는 regex101.com 같은 도구를 활용하세요. 패턴을 시각적으로 확인하고 테스트 케이스로 검증할 수 있습니다.

💡 복잡한 패턴은 raw string (r"...") 형식을 사용하세요. 백슬래시를 이스케이프하지 않아도 되어 가독성이 좋아집니다.

💡 대소문자를 무시하려면 (?i) 플래그를 패턴 앞에 추가하세요. r"(?i)error|warning"처럼 사용하면 대소문자 관계없이 매칭됩니다.

💡 성능을 위해 패턴을 최대한 구체적으로 작성하세요. .* 같은 탐욕적 패턴은 백트래킹을 유발하여 느려질 수 있으므로, [a-z]+ 같이 명확히 지정하세요.

💡 여러 패턴을 조합할 때는 | (OR) 연산자를 활용하세요. r"pattern1|pattern2|pattern3"로 여러 조건을 한 번에 검사하면 코드가 간결해집니다.


10. 타입_기반_필터링

시작하며

여러분이 다양한 타입의 데이터가 섞여 있는 컬럼을 다뤄본 적 있나요? 예를 들어, 사용자 입력 데이터에서 숫자로 파싱 가능한 값만 추출하거나, 날짜 형식이 올바른 행만 선택하거나, 특정 타입으로 변환이 가능한 데이터만 필터링해야 하는 경우가 있습니다.

특히 외부 시스템에서 받은 데이터는 타입이 혼재되어 있는 경우가 많습니다. 이런 문제는 데이터 검증과 정제 과정에서 매우 중요합니다.

잘못된 타입의 데이터가 분석에 포함되면 계산 오류나 예외가 발생할 수 있고, 타입 변환 실패를 미리 감지하지 못하면 파이프라인 전체가 중단될 수 있습니다. 바로 이럴 때 필요한 것이 Polars의 타입 기반 필터링입니다.

cast()와 try_cast()를 활용하여 타입 변환을 시도하고, 성공한 행만 선택하거나, dtype 속성으로 컬럼 타입을 확인하여 조건부로 처리할 수 있습니다.

개요

간단히 말해서, 타입 기반 필터링은 데이터의 타입을 확인하거나 변환을 시도하여, 특정 타입으로 안전하게 변환 가능한 행만 선택하는 기법입니다. try_cast()는 변환 실패 시 null을 반환하므로, is_not_null()과 결합하여 유효한 데이터만 필터링할 수 있습니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 실제 데이터는 형식이 일관되지 않은 경우가 많습니다. 예를 들어, CSV 파일에서 숫자 컬럼에 "N/A"나 "-" 같은 문자열이 섞여 있거나, 날짜 컬럼에 잘못된 형식이 포함된 경우, 타입 변환을 시도하여 유효한 데이터만 분석에 사용해야 합니다.

기존에는 Pandas에서 pd.to_numeric(errors='coerce')나 try-except 블록을 사용했다면, 이제는 Polars의 try_cast()로 한 줄에 처리할 수 있습니다. 변환 실패는 자동으로 null이 되어 안전하게 처리됩니다.

Polars 타입 처리의 핵심 특징은 첫째, strict=False 옵션으로 타입 변환 실패를 허용할 수 있고, 둘째, dtype 속성으로 런타임에 타입을 확인하여 동적 처리가 가능하며, 셋째, Arrow 타입 시스템을 사용하여 타입 안전성이 보장된다는 점입니다. 이러한 특징들이 다양한 소스의 데이터를 안전하게 통합하고 정제할 수 있게 해줍니다.

코드 예제

import polars as pl

# 타입이 혼재된 데이터 (문자열로 저장됨)
df = pl.DataFrame({
    "user_id": ["1", "2", "3", "4", "5"],
    "age": ["25", "thirty", "35", "N/A", "28"],
    "score": ["85.5", "90", "invalid", "88.0", "92.5"],
    "join_date": ["2024-01-15", "2024-02-20", "not a date", "2024-03-10", "2024-04-05"]
})

# 숫자로 변환 가능한 age 값만 필터링
df_with_valid_age = df.with_columns(
    pl.col("age").cast(pl.Int32, strict=False).alias("age_int")
).filter(
    pl.col("age_int").is_not_null()
)
print(df_with_valid_age)

# 실수로 변환 가능한 score만 필터링
df_with_valid_score = df.with_columns(
    pl.col("score").cast(pl.Float64, strict=False).alias("score_float")
).filter(
    pl.col("score_float").is_not_null()
).filter(
    pl.col("score_float") >= 85.0
)
print(df_with_valid_score)

# 날짜로 파싱 가능한 행만 필터링
df_with_valid_date = df.with_columns(
    pl.col("join_date").str.strptime(pl.Date, "%Y-%m-%d", strict=False).alias("date_parsed")
).filter(
    pl.col("date_parsed").is_not_null()
)
print(df_with_valid_date)

설명

이것이 하는 일: 타입 기반 필터링은 데이터를 특정 타입으로 변환을 시도하고, 변환에 성공한 행만 선택하여 타입이 안전한 데이터만 분석에 사용합니다. strict=False 옵션으로 변환 실패 시 에러 대신 null을 반환하게 하여 안전하게 처리합니다.

첫 번째로, cast(pl.Int32, strict=False)가 문자열을 정수로 변환을 시도합니다. "25"는 25로 변환되지만, "thirty"나 "N/A"는 정수로 변환할 수 없어서 null이 됩니다.

왜 이렇게 하는지 궁금하실 텐데, strict=True(기본값)면 변환 실패 시 예외가 발생하여 전체 파이프라인이 중단되지만, strict=False면 null로 대체되어 안전하게 계속 처리할 수 있습니다. 두 번째로, with_columns()로 변환된 값을 새로운 컬럼에 저장합니다.

원본 컬럼은 유지하면서 변환 결과를 별도로 보관하여, 나중에 검증이나 디버깅할 때 원본 값을 확인할 수 있습니다. 내부적으로는 벡터화된 연산으로 수백만 행도 빠르게 처리합니다.

세 번째로, is_not_null() 필터로 변환에 성공한 행만 선택합니다. null이 아니라는 것은 타입 변환이 성공했다는 의미이므로, 이후의 숫자 연산이나 날짜 연산을 안전하게 수행할 수 있습니다.

마지막으로 변환된 타입의 컬럼에 추가 조건(예: >= 85.0)을 적용하여 최종 필터링합니다. 여러분이 이 코드를 사용하면 데이터 품질 검증을 자동화할 수 있습니다.

외부에서 받은 CSV나 JSON 데이터의 타입을 검증하고, 유효하지 않은 행을 자동으로 제외하거나 별도로 기록하여 데이터 정제 보고서를 만들 수 있습니다. 또한 타입 변환 실패로 인한 런타임 에러를 예방할 수 있습니다.

실전 팁

💡 변환 실패한 원본 데이터를 별도로 저장하세요. 실패한 행을 로깅하거나 리포트에 포함하면 데이터 품질 개선에 도움이 됩니다.

💡 숫자 변환 시 오버플로우에 주의하세요. pl.Int32보다 큰 값은 pl.Int64를 사용하거나, pl.Float64로 먼저 변환 후 범위를 확인하세요.

💡 날짜 파싱은 여러 형식을 시도하세요. 하나의 형식으로 실패하면 다른 형식으로 다시 시도하는 로직을 추가하면 성공률이 높아집니다.

💡 타입 변환 전에 trim()으로 공백을 제거하세요. " 25 " 같은 값은 공백 때문에 변환 실패할 수 있으므로, str.strip_chars()로 먼저 정제하세요.

💡 대량 데이터는 샘플로 먼저 검증하세요. 변환 성공률을 확인한 후, 전체 데이터에 적용하면 예상치 못한 문제를 미리 발견할 수 있습니다.


#Polars#DataFrame#Filter#DataAnalysis#InteractiveFiltering#데이터분석,Python,Polars

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.