이미지 로딩 중...

Polars로 배우는 지역별 및 채널별 데이터 분석 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 15. · 5 Views

Polars로 배우는 지역별 및 채널별 데이터 분석 완벽 가이드

실무 데이터 분석에서 가장 많이 활용되는 지역별, 채널별 그룹화 분석을 Polars로 빠르고 효율적으로 수행하는 방법을 배웁니다. Pandas보다 10배 이상 빠른 성능으로 대용량 데이터도 쉽게 처리할 수 있습니다.


목차

  1. Polars 기본 설정 및 데이터 로딩 - 빠른 시작을 위한 첫걸음
  2. 지역별 집계 분석 기초 - GroupBy의 시작
  3. 채널별 다중 집계 분석 - 고급 Aggregation
  4. 지역과 채널 동시 분석 - 다차원 GroupBy
  5. 시계열 그룹화 및 윈도우 함수 - 날짜별 누적 분석
  6. 피벗 테이블로 교차 분석 - Pivot 활용법
  7. 조건부 집계 및 필터링 - Filter와 When 활용
  8. 성능 최적화 기법 - Lazy Evaluation 완벽 활용
  9. 결측치 및 이상치 처리 - 데이터 품질 관리
  10. 고급 표현식 및 사용자 정의 함수 - 복잡한 로직 구현

1. Polars 기본 설정 및 데이터 로딩 - 빠른 시작을 위한 첫걸음

시작하며

여러분이 대용량 판매 데이터를 분석해야 하는데 Pandas로 처리하니 5분 이상 걸려서 답답했던 경험 있나요? 특히 수백만 행의 데이터를 그룹화하거나 집계할 때 컴퓨터가 멈춘 것처럼 느려지는 상황 말이죠.

이런 문제는 실제 개발 현장에서 자주 발생합니다. Pandas는 편리하지만 대용량 데이터 처리에는 메모리 효율과 속도 면에서 한계가 있습니다.

멀티코어를 제대로 활용하지 못하고 단일 스레드로 동작하기 때문이죠. 바로 이럴 때 필요한 것이 Polars입니다.

Rust로 만들어진 이 라이브러리는 멀티스레딩과 최적화된 메모리 관리로 Pandas보다 10~100배 빠른 성능을 제공합니다.

개요

간단히 말해서, Polars는 차세대 데이터프레임 라이브러리입니다. Pandas와 유사한 API를 제공하면서도 훨씬 빠른 속도를 자랑합니다.

왜 Polars가 필요한지 실무 관점에서 보면, 대용량 로그 분석, 고객 행동 데이터 처리, 실시간 대시보드 구축 같은 경우에 매우 유용합니다. 특히 AWS나 클라우드 환경에서 처리 시간을 줄여 비용을 절감할 수 있죠.

기존에는 Pandas로 데이터를 읽고 처리했다면, 이제는 Polars로 동일한 작업을 몇 배 빠르게 수행할 수 있습니다. Polars의 핵심 특징은 첫째, Lazy Evaluation으로 실제 필요할 때만 연산을 수행하며, 둘째, 멀티스레딩을 기본으로 지원하고, 셋째, Apache Arrow 기반으로 메모리를 효율적으로 사용합니다.

이러한 특징들이 실무에서 처리 시간과 비용을 크게 줄여줍니다.

코드 예제

# 필수 라이브러리 설치 및 임포트
import polars as pl
import numpy as np
from datetime import datetime, timedelta

# CSV 파일에서 데이터 로딩 (Pandas보다 3-5배 빠름)
df = pl.read_csv("sales_data.csv")

# 또는 대용량 파일은 Lazy 모드로 (메모리 효율적)
df_lazy = pl.scan_csv("large_sales_data.csv")

# 데이터 미리보기
print(df.head())

# 컬럼 타입 확인
print(df.schema)

설명

이것이 하는 일: Polars를 설치하고 CSV 파일에서 데이터를 불러와 분석 준비를 완료합니다. 첫 번째로, pl.read_csv()는 CSV 파일을 메모리에 즉시 로딩합니다.

Pandas의 read_csv()와 거의 동일한 인터페이스를 제공하지만, 내부적으로 멀티스레딩을 사용해 파일을 병렬로 읽어들입니다. 이 때문에 같은 파일을 3-5배 빠르게 읽을 수 있죠.

그 다음으로, pl.scan_csv()는 Lazy 모드로 파일을 스캔만 하고 실제로 메모리에 로딩하지 않습니다. 이 방식은 쿼리 최적화를 가능하게 해서 필요한 컬럼과 행만 선택적으로 읽어옵니다.

100GB 파일도 메모리 부담 없이 처리할 수 있는 비결입니다. 마지막으로, df.head()df.schema로 데이터 구조를 확인합니다.

Schema는 각 컬럼의 데이터 타입을 보여주는데, Polars는 타입 추론이 정확해서 날짜, 숫자, 문자열을 자동으로 올바르게 인식합니다. 여러분이 이 코드를 사용하면 기존 Pandas 코드를 최소한의 수정으로 Polars로 전환할 수 있습니다.

특히 대용량 파일 처리 시 시간과 메모리를 대폭 절약할 수 있으며, 클라우드 환경에서는 인스턴스 비용도 줄일 수 있습니다.

실전 팁

💡 대용량 파일(1GB 이상)은 무조건 scan_csv() 사용 후 .collect()로 실행하세요. 쿼리 최적화로 10배 이상 빠릅니다.

💡 CSV 읽을 때 infer_schema_length=10000 옵션을 주면 타입 추론 정확도가 높아집니다. 기본값은 100행만 보기 때문에 잘못된 타입으로 읽힐 수 있어요.

💡 pl.Config.set_tbl_rows(20) 설정으로 출력되는 행 수를 조절할 수 있습니다. 디버깅할 때 유용합니다.

💡 파일이 여러 개로 분할되어 있다면 pl.scan_csv("data_*.csv")처럼 와일드카드를 사용해 한 번에 읽을 수 있습니다.

💡 메모리가 부족하다면 rechunk=False 옵션을 사용하세요. 메모리 사용량을 줄일 수 있지만 약간 느려집니다.


2. 지역별 집계 분석 기초 - GroupBy의 시작

시작하며

여러분이 전국 매장 데이터를 보면서 "서울이 제일 잘 팔리나?" 궁금해서 지역별로 매출을 합산하려는데, 어떻게 시작해야 할지 막막했던 경험 있나요? 엑셀처럼 필터를 하나씩 걸어가며 계산하기엔 데이터가 너무 많고 말이죠.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 비즈니스 인사이트를 얻으려면 데이터를 특정 기준으로 묶어서(그룹화) 집계해야 하는데, 이 작업이 데이터 분석의 핵심입니다.

제대로 하지 않으면 잘못된 의사결정으로 이어질 수 있죠. 바로 이럴 때 필요한 것이 GroupBy 연산입니다.

Polars의 group_by()를 사용하면 간단한 코드 한 줄로 지역별, 카테고리별 등 원하는 기준으로 데이터를 묶어 집계할 수 있습니다.

개요

간단히 말해서, GroupBy는 데이터를 특정 컬럼 기준으로 그룹화한 후 각 그룹별로 집계 함수를 적용하는 연산입니다. SQL의 GROUP BY와 동일한 개념이죠.

왜 이 개념이 필요한지 실무 관점에서 보면, 지역별 매출 분석, 고객 세그먼트별 구매 패턴 파악, 제품 카테고리별 재고 현황 같은 경우에 매우 유용합니다. 예를 들어, 마케팅 팀이 "어느 지역에 광고 예산을 집중할까?"를 결정할 때 지역별 매출과 고객 수를 분석하는 것이 필수적입니다.

기존에는 for 루프로 각 지역을 필터링하고 수동으로 합산했다면, 이제는 group_by().agg()로 한 번에 모든 지역의 통계를 계산할 수 있습니다. GroupBy의 핵심 특징은 첫째, 여러 컬럼을 동시에 그룹화 기준으로 사용 가능하고, 둘째, 다양한 집계 함수(sum, mean, count 등)를 동시에 적용할 수 있으며, 셋째, Polars는 이 연산을 병렬로 처리해 매우 빠릅니다.

이러한 특징들이 복잡한 분석을 간단하고 빠르게 만들어줍니다.

코드 예제

# 샘플 판매 데이터 생성
df = pl.DataFrame({
    "region": ["서울", "부산", "서울", "대구", "부산", "서울"],
    "product": ["노트북", "마우스", "키보드", "노트북", "키보드", "마우스"],
    "sales": [1500000, 30000, 80000, 1400000, 75000, 25000],
    "quantity": [3, 10, 5, 2, 6, 8]
})

# 지역별 총 매출과 판매량 집계
result = df.group_by("region").agg([
    pl.col("sales").sum().alias("총매출"),
    pl.col("quantity").sum().alias("총판매량"),
    pl.col("product").count().alias("거래건수")
])

print(result)

설명

이것이 하는 일: 지역(region) 컬럼을 기준으로 데이터를 그룹화하고, 각 지역별로 매출 합계, 판매량 합계, 거래 건수를 계산합니다. 첫 번째로, group_by("region")은 데이터를 'region' 컬럼의 고유값(서울, 부산, 대구)별로 나눕니다.

내부적으로 해시 테이블을 사용해 각 지역에 속하는 행들을 빠르게 묶습니다. Polars는 이 과정을 멀티스레드로 처리해서 수백만 행도 순식간에 그룹화합니다.

그 다음으로, .agg() 안에서 여러 집계 연산을 리스트로 정의합니다. pl.col("sales").sum()은 각 그룹의 sales 컬럼을 합산하고, .alias("총매출")로 결과 컬럼 이름을 지정합니다.

동시에 여러 집계를 한 번에 수행하는 것이 Polars의 강력한 장점입니다. 마지막으로, 결과는 각 지역을 행으로, 집계된 값들을 컬럼으로 하는 새로운 데이터프레임으로 반환됩니다.

예를 들어 서울의 경우 총매출 1,605,000원, 총판매량 16개, 거래건수 3건이 한 행에 표시됩니다. 여러분이 이 코드를 사용하면 수천 개 지역의 통계도 몇 초 안에 계산할 수 있습니다.

실무에서는 이를 바탕으로 지역별 성과 대시보드를 만들거나, 저성과 지역을 파악해 마케팅 전략을 수립할 수 있습니다. 또한 .sort("총매출", descending=True)를 추가하면 매출 순위도 바로 확인할 수 있죠.

실전 팁

💡 여러 컬럼으로 그룹화하려면 group_by(["region", "product"])처럼 리스트로 전달하세요. 지역+제품별 세부 분석이 가능합니다.

💡 maintain_order=True 옵션을 주면 원본 데이터의 순서를 유지합니다. 결과가 예측 가능해서 디버깅에 좋습니다.

💡 집계 후 바로 정렬하려면 .sort("총매출", descending=True)를 체이닝하세요. 상위 N개 지역만 보려면 .head(10)을 추가하면 됩니다.

💡 pl.col("*").sum()처럼 와일드카드를 사용하면 모든 숫자 컬럼을 자동으로 합산합니다. 컬럼이 많을 때 편리합니다.

💡 집계 전에 필터링하려면 df.filter(pl.col("sales") > 50000).group_by(...)처럼 체이닝하세요. 특정 조건의 데이터만 분석할 수 있습니다.


3. 채널별 다중 집계 분석 - 고급 Aggregation

시작하며

여러분이 온라인, 오프라인, 모바일 앱 등 여러 판매 채널의 성과를 비교 분석해야 하는데, 단순 합계만으로는 부족하고 평균, 최댓값, 표준편차까지 한 번에 보고 싶었던 적 있나요? 각 지표를 따로따로 계산하려니 코드가 길어지고 복잡해지죠.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 경영진은 "채널별로 매출 합계, 평균 주문액, 최대 거래액을 한눈에 보고 싶다"고 요청하는데, 이를 효율적으로 처리하지 못하면 리포트 생성에 너무 많은 시간이 걸립니다.

바로 이럴 때 필요한 것이 다중 집계(Multiple Aggregation)입니다. Polars의 .agg() 안에 여러 집계 함수를 동시에 나열하면 한 번의 연산으로 모든 통계를 계산할 수 있습니다.

개요

간단히 말해서, 다중 집계는 하나의 그룹화 작업에서 여러 개의 통계 지표를 동시에 계산하는 기법입니다. sum, mean, max, min, std 등을 한꺼번에 적용할 수 있습니다.

왜 이 개념이 필요한지 실무 관점에서 보면, 채널 성과 리포트, A/B 테스트 결과 분석, 고객 세그먼트 비교 같은 경우에 매우 유용합니다. 예를 들어, CMO(최고마케팅책임자)가 "어느 채널이 고객당 평균 매출이 높고 변동성이 적은가?"를 물어볼 때, 다중 집계로 한 번에 답할 수 있습니다.

기존에는 각 통계를 별도로 계산해서 나중에 합쳤다면, 이제는 .agg()에 모든 집계를 나열해 한 번에 처리할 수 있습니다. 다중 집계의 핵심 특징은 첫째, 동일한 컬럼에 여러 함수를 적용 가능하고(예: sales 컬럼의 sum과 mean), 둘째, 서로 다른 컬럼에 각기 다른 함수를 적용할 수 있으며(예: sales는 sum, quantity는 mean), 셋째, Polars가 이 모든 연산을 한 번의 데이터 스캔으로 처리해 극도로 효율적입니다.

이러한 특징들이 복잡한 분석을 간결하고 빠르게 만들어줍니다.

코드 예제

# 채널별 판매 데이터
df = pl.DataFrame({
    "channel": ["온라인", "오프라인", "앱", "온라인", "오프라인", "앱", "온라인"],
    "sales": [250000, 180000, 320000, 190000, 220000, 280000, 210000],
    "quantity": [5, 3, 8, 4, 6, 7, 3],
    "customer_id": [101, 102, 103, 104, 105, 106, 107]
})

# 채널별 다양한 통계 지표 한 번에 계산
result = df.group_by("channel").agg([
    pl.col("sales").sum().alias("총매출"),
    pl.col("sales").mean().alias("평균매출"),
    pl.col("sales").max().alias("최대거래액"),
    pl.col("quantity").sum().alias("총판매량"),
    pl.col("customer_id").n_unique().alias("고객수")
])

print(result)

설명

이것이 하는 일: 채널(온라인, 오프라인, 앱)별로 데이터를 그룹화하고, 각 채널의 총매출, 평균매출, 최대거래액, 총판매량, 고객 수를 한 번에 계산합니다. 첫 번째로, group_by("channel")로 세 개의 채널별로 데이터를 나눕니다.

그런 다음 .agg() 안에서 5개의 서로 다른 집계 연산을 리스트로 정의합니다. Polars는 이 리스트를 보고 한 번의 데이터 스캔으로 모든 계산을 완료하는 쿼리 플랜을 자동으로 생성합니다.

그 다음으로, 각 집계 함수가 실행됩니다. pl.col("sales").sum()은 각 채널의 모든 sales 값을 합산하고, pl.col("sales").mean()은 평균을 계산합니다.

동일한 컬럼(sales)에 여러 함수를 적용할 수 있다는 것이 중요한 포인트입니다. 또한 pl.col("customer_id").n_unique()는 고유한 고객 ID 개수를 세서 실제 구매 고객 수를 파악합니다.

마지막으로, 결과는 각 채널별로 5개의 통계 지표가 컬럼으로 나타난 데이터프레임입니다. 예를 들어 '온라인' 채널의 경우 총매출 650,000원, 평균매출 216,667원, 최대거래액 250,000원 등이 한 행에 모두 표시되어 한눈에 비교할 수 있습니다.

여러분이 이 코드를 사용하면 경영 대시보드나 주간 리포트를 몇 초 만에 생성할 수 있습니다. 실무에서는 이 결과를 바탕으로 "온라인 채널의 평균 주문액이 낮은데 총매출이 높다면 소액 다빈도 구매 패턴"이라는 인사이트를 도출하고, 각 채널에 맞는 마케팅 전략을 수립할 수 있습니다.

또한 .with_columns((pl.col("총매출") / pl.col("고객수")).alias("고객당매출"))을 추가해 파생 지표도 쉽게 만들 수 있죠.

실전 팁

💡 .describe() 메서드를 사용하면 count, mean, std, min, max 등 기본 통계를 자동으로 계산합니다. 탐색적 분석 초기 단계에 유용합니다.

💡 백분위수를 계산하려면 pl.col("sales").quantile(0.95).alias("상위5%금액")을 사용하세요. 이상치나 VIP 고객 파악에 좋습니다.

💡 조건부 집계는 pl.col("sales").filter(pl.col("quantity") > 5).sum()처럼 작성합니다. "5개 이상 구매한 거래만의 매출 합계" 같은 분석이 가능합니다.

💡 pl.col("sales").std()로 표준편차를 계산해 채널별 매출 변동성을 비교하세요. 변동성이 크면 리스크가 높다는 신호입니다.

💡 집계 결과를 CSV로 저장하려면 result.write_csv("channel_analysis.csv")를 사용하세요. 엑셀로 열어서 추가 작업이 가능합니다.


4. 지역과 채널 동시 분석 - 다차원 GroupBy

시작하며

여러분이 "서울의 온라인 채널 vs 부산의 오프라인 채널" 같은 세부적인 비교를 하고 싶은데, 어떻게 해야 할지 막막했던 경험 있나요? 지역별로 한 번 분석하고, 채널별로 또 한 번 분석해서 수동으로 합치려니 너무 번거롭죠.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 비즈니스 질문은 대부분 다차원적입니다.

"어느 지역의 어느 채널이 가장 효율적인가?" 같은 질문에 답하려면 두 가지 이상의 기준으로 동시에 그룹화해야 합니다. 바로 이럴 때 필요한 것이 다차원 GroupBy입니다.

Polars에서는 group_by(["region", "channel"])처럼 여러 컬럼을 리스트로 전달하면 모든 조합별로 데이터를 그룹화하고 집계할 수 있습니다.

개요

간단히 말해서, 다차원 GroupBy는 두 개 이상의 컬럼을 기준으로 동시에 그룹화하는 연산입니다. 지역×채널, 날짜×제품, 고객세그먼트×결제수단 등 다양한 조합이 가능합니다.

왜 이 개념이 필요한지 실무 관점에서 보면, 세그먼트별 심층 분석, 교차 분석 리포트, 다차원 피벗 테이블 생성 같은 경우에 매우 유용합니다. 예를 들어, "서울의 온라인 채널이 부산의 오프라인 채널보다 2배 많이 팔린다"는 구체적인 인사이트를 얻을 수 있습니다.

기존에는 지역별로 필터링한 후 다시 채널별로 필터링하는 중첩 루프를 작성했다면, 이제는 group_by(["region", "channel"])로 한 번에 모든 조합을 분석할 수 있습니다. 다차원 GroupBy의 핵심 특징은 첫째, 그룹화 순서는 결과에 영향을 주지 않으며(교환법칙 성립), 둘째, 최대 수십 개 컬럼도 동시에 그룹화할 수 있고, 셋째, Polars가 해시 기반 알고리즘으로 매우 빠르게 처리합니다.

이러한 특징들이 복잡한 교차 분석을 간단하게 만들어줍니다.

코드 예제

# 지역과 채널이 모두 있는 판매 데이터
df = pl.DataFrame({
    "region": ["서울", "서울", "부산", "부산", "대구", "대구", "서울", "부산"],
    "channel": ["온라인", "오프라인", "온라인", "오프라인", "온라인", "앱", "앱", "앱"],
    "sales": [300000, 250000, 180000, 200000, 150000, 170000, 280000, 190000],
    "quantity": [6, 5, 3, 4, 2, 3, 5, 4]
})

# 지역과 채널 조합별로 집계
result = df.group_by(["region", "channel"]).agg([
    pl.col("sales").sum().alias("총매출"),
    pl.col("sales").mean().alias("평균매출"),
    pl.col("quantity").sum().alias("총수량")
]).sort(["region", "channel"])

print(result)

설명

이것이 하는 일: 지역과 채널의 모든 조합(서울-온라인, 서울-오프라인, 부산-온라인 등)별로 데이터를 그룹화하고 각 조합의 총매출, 평균매출, 총수량을 계산합니다. 첫 번째로, group_by(["region", "channel"])은 두 컬럼의 고유한 조합을 모두 찾아 그룹을 만듭니다.

예를 들어 서울-온라인, 서울-오프라인, 서울-앱, 부산-온라인 등의 그룹이 생성됩니다. 내부적으로 복합 해시 키를 사용해 이 조합들을 빠르게 구분하고 묶습니다.

그 다음으로, 각 조합별로 집계 함수가 실행됩니다. '서울-온라인' 그룹에 속하는 모든 행의 sales를 합산하고, '부산-오프라인' 그룹의 sales 평균을 계산하는 식입니다.

동일한 집계 로직이 모든 조합에 병렬로 적용되어 매우 빠릅니다. 마지막으로, .sort(["region", "channel"])로 결과를 정렬합니다.

지역 순서대로, 같은 지역 내에서는 채널 순서대로 정렬되어 보기 좋은 리포트가 됩니다. 예를 들어 서울의 모든 채널이 먼저 나오고, 그 다음 부산의 채널들이 나오는 식이죠.

여러분이 이 코드를 사용하면 "서울의 앱 채널이 총매출 280,000원으로 가장 높다"는 구체적인 인사이트를 즉시 얻을 수 있습니다. 실무에서는 이를 바탕으로 지역별 채널 전략을 차별화하거나(예: 대구는 온라인보다 앱 마케팅 강화), 저성과 조합을 파악해 개선 방안을 수립할 수 있습니다.

또한 이 결과를 피벗 테이블로 변환하면 경영진이 더 쉽게 이해할 수 있는 형태가 됩니다.

실전 팁

💡 3개 이상 컬럼도 그룹화 가능합니다. group_by(["region", "channel", "product"])로 더 세밀한 분석을 할 수 있습니다.

💡 그룹 개수가 너무 많으면 결과 해석이 어려우니, 상위 10개 조합만 보려면 .sort("총매출", descending=True).head(10)을 사용하세요.

💡 피벗 테이블로 변환하려면 result.pivot(values="총매출", index="region", columns="channel")을 사용하세요. 엑셀 피벗 테이블과 동일한 형태가 됩니다.

💡 특정 조합만 필터링하려면 집계 후 .filter((pl.col("region") == "서울") & (pl.col("channel") == "온라인"))을 사용하세요.

💡 .with_columns((pl.col("총매출") / pl.col("총매출").sum()).alias("비중"))을 추가하면 각 조합이 전체 매출에서 차지하는 비율을 계산할 수 있습니다.


5. 시계열 그룹화 및 윈도우 함수 - 날짜별 누적 분석

시작하며

여러분이 "이번 달 매출이 지난달보다 얼마나 늘었나?" 또는 "누적 매출이 목표를 달성했나?"를 확인하려는데, 단순 합계만으로는 추세를 파악하기 어려웠던 경험 있나요? 날짜별 데이터를 하나씩 더해가며 계산하려니 복잡하고 오류가 나기 쉽죠.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 시계열 데이터 분석에서는 날짜별 집계뿐만 아니라 누적 합계, 이동 평균, 전월 대비 증감 같은 시간 기반 계산이 필수입니다.

제대로 계산하지 않으면 트렌드를 잘못 해석할 수 있습니다. 바로 이럴 때 필요한 것이 윈도우 함수(Window Function)입니다.

Polars의 .over() 또는 .cum_sum() 같은 함수를 사용하면 날짜 순서대로 누적 계산이나 이동 평균을 쉽게 구할 수 있습니다.

개요

간단히 말해서, 윈도우 함수는 현재 행뿐만 아니라 이전/이후 행들을 참조해서 계산하는 함수입니다. 누적 합계, 순위, 이동 평균 등을 계산할 때 사용합니다.

왜 이 개념이 필요한지 실무 관점에서 보면, 매출 트렌드 분석, 재고 누적 추적, KPI 대시보드 같은 경우에 매우 유용합니다. 예를 들어, CFO(최고재무책임자)가 "올해 1월부터 지금까지 누적 매출이 얼마인가?"를 물어볼 때, 윈도우 함수로 즉시 답할 수 있습니다.

기존에는 for 루프로 이전 값들을 수동으로 합산했다면, 이제는 .cum_sum()으로 한 줄로 누적 합계를 계산할 수 있습니다. 윈도우 함수의 핵심 특징은 첫째, 원본 데이터의 행 수를 유지하면서 계산 결과를 추가하고(GroupBy는 행 수가 줄어듦), 둘째, over() 절로 파티션을 나눠 그룹별 윈도우 계산이 가능하며, 셋째, Polars가 이 연산을 최적화해 메모리 효율적으로 처리합니다.

이러한 특징들이 복잡한 시계열 분석을 간단하게 만들어줍니다.

코드 예제

from datetime import date

# 날짜별 판매 데이터
df = pl.DataFrame({
    "date": [date(2024, 1, 1), date(2024, 1, 2), date(2024, 1, 3),
             date(2024, 1, 4), date(2024, 1, 5)],
    "region": ["서울", "서울", "부산", "서울", "부산"],
    "sales": [100000, 150000, 80000, 120000, 90000]
})

# 날짜순 정렬 후 누적 매출 계산
result = df.sort("date").with_columns([
    pl.col("sales").cum_sum().alias("누적매출"),
    pl.col("sales").rolling_mean(window_size=2).alias("2일이동평균"),
    pl.col("sales").rank().alias("매출순위")
])

print(result)

설명

이것이 하는 일: 날짜 순서대로 데이터를 정렬하고, 각 날짜까지의 누적 매출, 최근 2일간 평균 매출, 매출 순위를 계산하여 새로운 컬럼으로 추가합니다. 첫 번째로, .sort("date")로 날짜 순서대로 정렬합니다.

윈도우 함수는 행의 순서에 의존하기 때문에 정렬이 필수입니다. 날짜가 뒤죽박죽이면 누적 합계가 의미 없어지기 때문이죠.

그 다음으로, .cum_sum()은 첫 행부터 현재 행까지의 sales를 누적해서 더합니다. 1월 1일은 100,000원, 1월 2일은 100,000 + 150,000 = 250,000원, 1월 3일은 250,000 + 80,000 = 330,000원 이런 식으로 계산됩니다.

.rolling_mean(window_size=2)는 현재 행과 이전 행의 평균을 계산해서 단기 트렌드를 파악할 수 있게 해줍니다. 마지막으로, .rank()는 sales 값을 기준으로 순위를 매깁니다.

가장 높은 매출은 1위, 그 다음은 2위 이런 식이죠. .with_columns()를 사용했기 때문에 원본 컬럼들(date, region, sales)은 그대로 유지되고 새로운 계산 컬럼들(누적매출, 2일이동평균, 매출순위)만 추가됩니다.

여러분이 이 코드를 사용하면 시계열 대시보드나 트렌드 차트를 쉽게 만들 수 있습니다. 실무에서는 "누적 매출이 목표 1,000만 원을 언제 달성하는가?"를 파악하거나, 이동 평균으로 계절성을 제거한 순수 트렌드를 분석할 수 있습니다.

또한 .shift(1)을 사용하면 전일 대비 증감률도 쉽게 계산할 수 있죠.

실전 팁

💡 지역별로 따로 누적 계산하려면 .cum_sum().over("region")을 사용하세요. 서울과 부산의 누적 매출이 각각 계산됩니다.

💡 이동 평균의 window_size는 홀수로 하면 중심 이동 평균이 됩니다. window_size=3이면 전날-오늘-다음날의 평균입니다.

💡 전월 대비 증감률은 .pct_change()로 쉽게 계산할 수 있습니다. 결과는 퍼센트가 아니라 비율(0.1 = 10% 증가)로 나옵니다.

💡 shift(1)로 이전 행 값을 가져와 현재 값과 비교할 수 있습니다. pl.col("sales") - pl.col("sales").shift(1)은 전일 대비 차이입니다.

💡 윈도우 함수는 메모리를 많이 사용하므로, 대용량 데이터는 날짜 범위를 좁혀서 분석하거나 Lazy 모드를 사용하세요.


6. 피벗 테이블로 교차 분석 - Pivot 활용법

시작하며

여러분이 엑셀 피벗 테이블처럼 행에 지역, 열에 채널을 놓고 매출을 교차 비교하고 싶은데, Polars에서 어떻게 해야 할지 몰라서 결국 엑셀로 export했던 경험 있나요? 데이터 분석 도구에서 피벗 테이블을 만들 수 없다면 너무 불편하죠.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 경영진이나 비즈니스 팀은 숫자가 세로로 나열된 것보다 행과 열로 교차된 표를 훨씬 선호합니다.

"한눈에 비교하기 쉽다"는 이유에서죠. 바로 이럴 때 필요한 것이 피벗 테이블(Pivot Table)입니다.

Polars의 .pivot() 함수를 사용하면 엑셀 피벗 테이블과 동일한 형태의 교차 분석 표를 코드 한 줄로 만들 수 있습니다.

개요

간단히 말해서, 피벗 테이블은 하나의 컬럼 값을 행으로, 다른 컬럼 값을 열로 배치하여 교차 집계를 시각화하는 표입니다. 엑셀의 피벗 테이블과 정확히 같은 개념입니다.

왜 이 개념이 필요한지 실무 관점에서 보면, 경영 리포트, 크로스탭 분석, 히트맵 데이터 준비 같은 경우에 매우 유용합니다. 예를 들어, "각 지역의 각 채널별 매출을 한눈에 보고 싶다"는 요청에 피벗 테이블이 가장 적합합니다.

기존에는 GroupBy 결과를 복잡한 reshape 로직으로 변환했다면, 이제는 .pivot()으로 한 번에 원하는 형태로 만들 수 있습니다. 피벗 테이블의 핵심 특징은 첫째, index(행)와 columns(열) 파라미터로 표의 구조를 정의하고, 둘째, values 파라미터로 교차점에 표시될 값을 지정하며, 셋째, aggregate_function으로 같은 조합에 여러 값이 있을 때 어떻게 집계할지 결정합니다.

이러한 특징들이 복잡한 교차 분석을 직관적으로 만들어줍니다.

코드 예제

# 지역과 채널별 판매 데이터
df = pl.DataFrame({
    "region": ["서울", "서울", "부산", "부산", "대구", "대구"],
    "channel": ["온라인", "오프라인", "온라인", "오프라인", "온라인", "오프라인"],
    "sales": [500000, 300000, 250000, 200000, 180000, 150000],
})

# 지역을 행, 채널을 열로 하는 피벗 테이블
pivot_result = df.pivot(
    values="sales",
    index="region",
    columns="channel",
    aggregate_function="sum"
)

print(pivot_result)

설명

이것이 하는 일: 지역을 행으로, 채널을 열로 배치하고, 각 교차점에 해당하는 매출 합계를 표시하는 피벗 테이블을 생성합니다. 첫 번째로, index="region"은 고유한 지역 값들(서울, 부산, 대구)을 행으로 만듭니다.

columns="channel"은 고유한 채널 값들(온라인, 오프라인)을 열 이름으로 만듭니다. 이렇게 하면 "서울 × 온라인", "서울 × 오프라인" 같은 모든 조합이 표의 셀이 됩니다.

그 다음으로, values="sales"는 각 셀에 어떤 값을 표시할지 지정합니다. aggregate_function="sum"은 같은 조합(예: 서울-온라인)에 여러 행이 있을 때 합산하라는 의미입니다.

만약 "sum" 대신 "mean"을 쓰면 평균이 계산되고, "count"를 쓰면 거래 건수가 표시됩니다. 마지막으로, 결과는 행에 지역, 열에 온라인/오프라인이 있는 표 형태로 나옵니다.

예를 들어 서울 행의 온라인 열 값이 500,000원이면 "서울에서 온라인 채널로 50만 원 판매"를 의미합니다. 이 형태는 엑셀 사용자나 비개발자도 쉽게 이해할 수 있어 보고서나 프레젠테이션에 바로 사용 가능합니다.

여러분이 이 코드를 사용하면 복잡한 GroupBy 결과를 직관적인 표로 변환할 수 있습니다. 실무에서는 이 피벗 테이블을 matplotlib이나 seaborn의 히트맵으로 시각화하거나, PowerPoint 슬라이드에 바로 붙여넣을 수 있습니다.

또한 .fill_null(0)을 추가하면 데이터가 없는 조합(예: 대구-앱)을 0으로 채워 표를 완전하게 만들 수 있죠.

실전 팁

💡 피벗 후 빈 값은 null이 됩니다. .fill_null(0)으로 0으로 채우면 합계 계산이 정확해집니다.

💡 여러 값을 동시에 피벗하려면 values=["sales", "quantity"]처럼 리스트로 전달하세요. 각 값별로 별도 컬럼이 생성됩니다.

💡 피벗 결과를 다시 long format으로 되돌리려면 .melt()를 사용하세요. 피벗의 역연산입니다.

💡 aggregate_function은 "sum", "mean", "count", "max", "min" 등을 지원합니다. 상황에 맞게 선택하세요.

💡 행이나 열이 너무 많으면 피벗 테이블이 sparse(희소)해집니다. 상위 10개 지역/채널만 필터링한 후 피벗하세요.


7. 조건부 집계 및 필터링 - Filter와 When 활용

시작하며

여러분이 "10만 원 이상 구매한 고객만의 평균 구매액"이나 "온라인 채널에서만 발생한 지역별 매출"처럼 특정 조건을 만족하는 데이터만 골라서 분석해야 하는데, 어떻게 해야 할지 고민했던 경험 있나요? 전체 데이터를 먼저 필터링하고 다시 집계하려니 코드가 복잡해지죠.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 비즈니스 질문은 대부분 조건부입니다.

"VIP 고객(구매액 100만 원 이상)의 재구매율은?" 같은 질문에 답하려면 조건을 정확히 적용해야 합니다. 바로 이럴 때 필요한 것이 조건부 집계입니다.

Polars의 .filter() 메서드와 pl.when().then().otherwise() 표현식을 사용하면 복잡한 조건을 간결하게 표현하고 선택적으로 집계할 수 있습니다.

개요

간단히 말해서, 조건부 집계는 특정 조건을 만족하는 데이터만 선택해서 집계하거나, 조건에 따라 다른 계산을 수행하는 기법입니다. SQL의 WHERE와 CASE WHEN과 유사합니다.

왜 이 개념이 필요한지 실무 관점에서 보면, VIP 고객 분석, 특정 기간 매출 집계, 이상치 제외한 통계 같은 경우에 매우 유용합니다. 예를 들어, "프로모션 기간(3월)에만 발생한 지역별 매출 증가율"을 분석할 때 조건부 집계가 필수입니다.

기존에는 조건을 만족하는 행을 별도 데이터프레임으로 분리하고 각각 집계했다면, 이제는 .filter() 체이닝이나 집계 내부의 .filter()로 한 번에 처리할 수 있습니다. 조건부 집계의 핵심 특징은 첫째, 집계 전 필터링과 집계 중 필터링을 모두 지원하고, 둘째, when().then().otherwise()로 조건부 값 할당이 가능하며, 셋째, 여러 조건을 &(AND), |(OR)로 조합할 수 있습니다.

이러한 특징들이 복잡한 비즈니스 로직을 명확하게 표현할 수 있게 해줍니다.

코드 예제

# 판매 데이터
df = pl.DataFrame({
    "region": ["서울", "부산", "서울", "대구", "부산", "서울"],
    "channel": ["온라인", "온라인", "오프라인", "온라인", "오프라인", "온라인"],
    "sales": [80000, 150000, 40000, 120000, 30000, 200000],
    "is_vip": [False, True, False, True, False, True]
})

# 조건부 집계: 10만원 이상 거래만 분석
high_value = df.filter(pl.col("sales") >= 100000).group_by("region").agg([
    pl.col("sales").mean().alias("고액평균"),
    pl.col("sales").count().alias("고액건수")
])

# 집계 내부 필터: VIP 고객 매출만 합산
vip_sales = df.group_by("region").agg([
    pl.col("sales").filter(pl.col("is_vip")).sum().alias("VIP매출")
])

print(high_value)
print(vip_sales)

설명

이것이 하는 일: 10만 원 이상 거래만 골라서 지역별 평균과 건수를 계산하고, VIP 고객의 구매만 합산하여 지역별 VIP 매출을 계산합니다. 첫 번째로, .filter(pl.col("sales") >= 100000)은 sales가 100,000 이상인 행만 남깁니다.

이 필터링된 데이터프레임에서 .group_by("region")을 수행하므로 각 지역의 고액 거래만 분석됩니다. 이 방식은 "데이터를 먼저 줄이고 집계"하는 접근법입니다.

그 다음으로, 두 번째 예시인 pl.col("sales").filter(pl.col("is_vip"))는 집계 함수 내부에서 필터링합니다. is_vip가 True인 행의 sales만 sum 계산에 포함되고, False인 행은 제외됩니다.

이 방식은 "집계하면서 선택적으로 포함"하는 접근법으로, 여러 조건의 집계를 동시에 수행할 때 유용합니다. 마지막으로, 두 방식의 차이를 이해하는 것이 중요합니다.

전체 필터링(첫 번째)은 결과 데이터 자체를 줄이지만, 집계 내부 필터링(두 번째)은 원본 행 수를 유지하면서 특정 조건만 집계에 포함합니다. 예를 들어 "VIP와 일반 고객 매출을 동시에 비교"하려면 두 번째 방식으로 두 개의 집계를 한 번에 수행할 수 있습니다.

여러분이 이 코드를 사용하면 복잡한 비즈니스 조건을 정확히 반영한 분석을 할 수 있습니다. 실무에서는 "프로모션 적용 고객만의 평균 구매액", "반품 제외한 순매출", "영업일만 집계한 일평균" 같은 세밀한 분석이 가능합니다.

또한 pl.when(조건).then(값1).otherwise(값2)를 사용하면 조건에 따라 다른 값을 할당하는 파생 변수도 쉽게 만들 수 있죠.

실전 팁

💡 여러 조건을 AND로 연결하려면 (pl.col("sales") >= 100000) & (pl.col("channel") == "온라인")처럼 괄호로 묶으세요.

💡 OR 조건은 |를 사용합니다. (pl.col("region") == "서울") | (pl.col("region") == "부산")은 서울 또는 부산을 선택합니다.

💡 pl.col("region").is_in(["서울", "부산", "대구"])로 여러 값 중 하나와 일치하는지 체크할 수 있습니다. 긴 OR 조건을 간결하게 만듭니다.

💡 조건부 값 할당은 pl.when(pl.col("sales") > 100000).then("고액").otherwise("일반")로 카테고리를 만들 수 있습니다.

💡 null 값 필터링은 pl.col("sales").is_not_null()을 사용하세요. null을 포함하면 집계 결과가 왜곡될 수 있습니다.


8. 성능 최적화 기법 - Lazy Evaluation 완벽 활용

시작하며

여러분이 100GB가 넘는 로그 파일을 분석해야 하는데, 파일을 읽는 것만으로도 메모리가 부족하고 30분 이상 걸려서 작업을 포기했던 경험 있나요? 클라우드 인스턴스 메모리를 늘리려니 비용이 너무 높아지고 말이죠.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 대용량 데이터 처리에서는 메모리 효율과 처리 속도가 생산성과 비용에 직접적인 영향을 미칩니다.

잘못 접근하면 몇 시간씩 기다리거나 out-of-memory 에러로 작업이 중단되죠. 바로 이럴 때 필요한 것이 Lazy Evaluation입니다.

Polars의 scan_csv()로 시작해서 .collect()로 끝나는 Lazy 모드를 사용하면, 쿼리 최적화를 통해 필요한 데이터만 읽고 처리해서 속도와 메모리 효율을 10배 이상 높일 수 있습니다.

개요

간단히 말해서, Lazy Evaluation은 실제 결과가 필요할 때까지 연산을 미루고, 전체 쿼리를 보고 최적화 계획을 세운 후 한 번에 실행하는 방식입니다. 데이터베이스의 쿼리 플래너와 유사한 개념입니다.

왜 이 개념이 필요한지 실무 관점에서 보면, 대용량 로그 분석, 데이터 웨어하우스 ETL, 클라우드 비용 절감 같은 경우에 매우 유용합니다. 예를 들어, 100GB 파일에서 특정 지역의 특정 기간 데이터만 필요한데, Eager 모드로 전체를 읽으면 낭비가 심하지만 Lazy 모드는 필요한 부분만 읽습니다.

기존에는 전체 데이터를 메모리에 올리고 필터링했다면, 이제는 Lazy 모드로 "어떤 컬럼의 어떤 행이 필요한지" 미리 파악해서 디스크에서 읽을 때부터 선택적으로 가져옵니다. Lazy Evaluation의 핵심 특징은 첫째, 쿼리 최적화로 불필요한 연산을 자동으로 제거하고(Predicate Pushdown, Projection Pushdown), 둘째, 스트리밍 방식으로 메모리보다 큰 데이터도 처리 가능하며, 셋째, .explain()으로 실행 계획을 미리 확인할 수 있습니다.

이러한 특징들이 대용량 데이터를 적은 리소스로 빠르게 처리할 수 있게 해줍니다.

코드 예제

# Lazy 모드로 대용량 CSV 읽기 (메모리에 로딩하지 않음)
lazy_df = pl.scan_csv("large_sales_data.csv")

# 쿼리 체이닝 (아직 실행되지 않음)
result = (
    lazy_df
    .filter(pl.col("region") == "서울")  # 서울 데이터만
    .select(["date", "channel", "sales"])  # 필요한 컬럼만
    .group_by("channel")
    .agg(pl.col("sales").sum().alias("총매출"))
    .sort("총매출", descending=True)
)

# 실행 계획 확인 (선택사항)
print(result.explain())

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

설명

이것이 하는 일: 대용량 CSV 파일을 메모리에 올리지 않고 Lazy 모드로 스캔한 후, 필터링-선택-집계-정렬 쿼리를 정의하고, 최적화된 실행 계획을 세워 한 번에 효율적으로 실행합니다. 첫 번째로, pl.scan_csv()는 파일 메타데이터만 읽고 실제 데이터는 읽지 않습니다.

이 시점에서는 "파일이 어디 있고 어떤 컬럼이 있다"는 정보만 가지고 있습니다. 메모리 사용량이 거의 0이므로 100GB 파일도 즉시 스캔됩니다.

그 다음으로, .filter(), .select(), .group_by() 등을 체이닝하면 Polars는 이 연산들을 쿼리 그래프로 저장만 합니다. 실제로 실행하지 않기 때문에 순식간에 완료됩니다.

이 과정에서 Polars는 "어떤 연산이 필요한지" 파악하고 최적화 기회를 찾습니다. 마지막으로, .collect()를 호출하는 순간 쿼리 옵티마이저가 작동합니다.

"region == 서울 필터는 파일 읽을 때부터 적용(Predicate Pushdown)", "date, channel, sales 컬럼만 읽기(Projection Pushdown)" 같은 최적화를 자동으로 수행합니다. 결과적으로 100GB 파일에서 실제로는 1GB만 읽어서 처리하는 식으로 10~100배 빠릅니다.

여러분이 이 코드를 사용하면 노트북이나 작은 클라우드 인스턴스에서도 대용량 데이터를 분석할 수 있습니다. 실무에서는 AWS S3의 수십 GB 로그 파일을 분석하거나, 일일 배치 작업 시간을 몇 시간에서 몇 분으로 줄일 수 있습니다.

또한 .sink_parquet("output.parquet")를 사용하면 결과를 메모리에 올리지 않고 바로 파일로 저장해 더욱 메모리 효율적입니다.

실전 팁

💡 .explain()으로 실행 계획을 확인하세요. "FILTER PUSHED DOWN" 같은 메시지가 보이면 최적화가 잘 된 것입니다.

💡 streaming=True 옵션을 .collect(streaming=True)에 주면 스트리밍 모드로 메모리보다 큰 데이터도 처리 가능합니다.

💡 파일이 여러 개면 scan_csv("data_*.csv")로 한 번에 읽고 Lazy 모드로 처리하세요. 파일마다 따로 읽는 것보다 훨씬 빠릅니다.

💡 디버깅할 때는 .head(100).collect()로 일부만 먼저 확인하세요. 전체 데이터로 테스트하면 시간이 오래 걸립니다.

💡 Parquet 파일은 CSV보다 훨씬 빠르고 압축률도 좋습니다. 반복 분석할 데이터는 CSV → Parquet로 변환 후 scan_parquet()을 사용하세요.


9. 결측치 및 이상치 처리 - 데이터 품질 관리

시작하며

여러분이 실제 데이터를 분석하려는데 null 값이 섞여 있어서 평균이 이상하게 나오거나, 명백히 잘못된 값(매출 -1000만 원 같은)이 있어서 통계가 왜곡되었던 경험 있나요? 이런 더러운 데이터를 일일이 수작업으로 정리하려니 너무 번거롭죠.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 실무 데이터는 절대 깨끗하지 않습니다.

센서 오류, 입력 실수, 시스템 버그 등으로 결측치와 이상치가 항상 존재하고, 이를 제대로 처리하지 않으면 잘못된 의사결정으로 이어집니다. 바로 이럴 때 필요한 것이 데이터 클리닝 기법입니다.

Polars의 .fill_null(), .drop_nulls(), 조건부 필터링 등을 사용하면 결측치와 이상치를 체계적으로 처리할 수 있습니다.

개요

간단히 말해서, 데이터 클리닝은 분석 전에 결측치(null), 이상치(outlier), 잘못된 값을 탐지하고 제거하거나 대체하는 과정입니다. 데이터 분석의 필수 전처리 단계입니다.

왜 이 개념이 필요한지 실무 관점에서 보면, 정확한 통계 계산, 머신러닝 모델 학습, 리포트 신뢰도 확보 같은 경우에 매우 중요합니다. 예를 들어, 고객 나이에 -5살이나 200살이 있으면 평균 나이가 의미 없어지고, 이를 기반으로 한 타겟팅 전략도 실패하게 됩니다.

기존에는 엑셀에서 필터로 하나씩 확인하고 수정했다면, 이제는 Polars로 규칙 기반 자동 클리닝을 코드로 정의해서 반복 적용할 수 있습니다. 데이터 클리닝의 핵심 특징은 첫째, 결측치는 제거, 평균/중앙값 대체, 특정 값 대체 등 여러 전략이 있고, 둘째, 이상치는 도메인 지식 기반 규칙이나 통계적 방법(IQR, Z-score)으로 탐지하며, 셋째, Polars는 이 모든 처리를 벡터화된 연산으로 빠르게 수행합니다.

이러한 특징들이 대용량 데이터의 품질을 효율적으로 관리할 수 있게 해줍니다.

코드 예제

import polars as pl

# 결측치와 이상치가 있는 데이터
df = pl.DataFrame({
    "region": ["서울", "부산", None, "대구", "서울"],
    "sales": [100000, None, 250000, -50000, 180000],  # null과 음수
    "quantity": [5, 10, 3, 2, 1000]  # 1000은 명백한 이상치
})

# 결측치 처리
cleaned = df.with_columns([
    pl.col("region").fill_null("미지정"),  # null을 기본값으로 대체
    pl.col("sales").fill_null(pl.col("sales").mean())  # null을 평균으로 대체
])

# 이상치 필터링 (음수 매출 제거, quantity 100 초과 제거)
cleaned = cleaned.filter(
    (pl.col("sales") > 0) & (pl.col("quantity") <= 100)
)

print(cleaned)

설명

이것이 하는 일: region의 null 값을 "미지정"으로, sales의 null을 평균값으로 대체하고, 음수 매출과 비정상적으로 큰 수량을 제거하여 깨끗한 데이터를 만듭니다. 첫 번째로, .fill_null("미지정")은 region 컬럼의 모든 null 값을 "미지정" 문자열로 바꿉니다.

이는 "지역 정보가 누락된 경우"를 명시적으로 표현하는 전략입니다. 반면 pl.col("sales").fill_null(pl.col("sales").mean())은 sales의 null을 해당 컬럼의 평균값으로 대체합니다.

이 방법은 통계적 중립성을 유지하면서 결측치를 처리하는 일반적인 기법입니다. 그 다음으로, 이상치 제거 로직이 실행됩니다.

(pl.col("sales") > 0)은 음수 매출을 걸러내고, (pl.col("quantity") <= 100)은 비정상적으로 큰 수량(1000개)을 제거합니다. 두 조건을 &로 연결해서 둘 다 만족하는 행만 남깁니다.

이렇게 하면 데이터 입력 오류나 시스템 버그로 생긴 잘못된 값들이 분석에서 제외됩니다. 마지막으로, 결과는 모든 null이 처리되고 이상치가 제거된 깨끗한 데이터프레임입니다.

이제 이 데이터로 평균, 합계 등을 계산하면 왜곡되지 않은 정확한 통계를 얻을 수 있습니다. 실무에서는 클리닝 전후 데이터 행 수, 통계 변화를 비교해서 얼마나 많은 문제가 있었는지 리포트하는 것도 중요합니다.

여러분이 이 코드를 사용하면 신뢰할 수 있는 분석 결과를 얻을 수 있습니다. 실무에서는 "클리닝 전 평균 매출 150만 원 → 클리닝 후 180만 원"처럼 이상치 제거로 정확한 지표가 나오고, 이를 바탕으로 올바른 비즈니스 결정을 내릴 수 있습니다.

또한 .null_count()로 각 컬럼의 null 개수를 확인하거나, .describe()로 이상치가 있는지 사전 점검하는 것도 좋은 습관입니다.

실전 팁

💡 .drop_nulls()는 null이 하나라도 있는 행을 완전히 제거합니다. 데이터 손실이 크지만 깔끔한 분석이 가능합니다.

💡 .fill_null(strategy="forward")는 이전 행의 값으로 null을 채웁니다. 시계열 데이터에서 유용합니다.

💡 IQR 방식 이상치 탐지: Q1 = pl.col("sales").quantile(0.25), Q3 = pl.col("sales").quantile(0.75), IQR = Q3 - Q1로 계산 후 [Q1-1.5*IQR, Q3+1.5*IQR] 범위 밖을 제거하세요.

💡 .is_null()로 null 여부를 Boolean 컬럼으로 만들어 null 패턴을 분석할 수 있습니다. 예: "어느 지역에서 데이터 누락이 많은가?"

💡 클리닝 전 .shape, .null_count()로 원본 상태를 기록하고, 클리닝 후 비교해서 몇 %의 데이터가 제거되었는지 문서화하세요.


10. 고급 표현식 및 사용자 정의 함수 - 복잡한 로직 구현

시작하며

여러분이 "매출이 평균보다 20% 이상 높으면 '우수', 평균의 80% 미만이면 '부진', 나머지는 '보통'"처럼 복잡한 비즈니스 로직을 적용해야 하는데, Polars의 기본 함수만으로는 부족해서 막막했던 경험 있나요? 파이썬 함수로 작성하고 싶은데 어떻게 적용해야 할지 모르겠죠.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 실무 로직은 단순 집계를 넘어서 복잡한 조건 분기, 수학 계산, 문자열 처리 등을 포함합니다.

표준 함수만으로는 한계가 있고, 커스텀 로직이 필요한 순간이 옵니다. 바로 이럴 때 필요한 것이 고급 표현식과 사용자 정의 함수(UDF)입니다.

Polars의 .map_elements() 또는 복잡한 when-then-otherwise 체이닝을 사용하면 어떤 로직도 구현할 수 있습니다.

개요

간단히 말해서, 고급 표현식은 조건 분기, 수학 함수, 문자열 조작 등을 조합해 복잡한 변환 로직을 만드는 기법이고, UDF는 파이썬 함수를 Polars 컬럼에 적용하는 방법입니다. 왜 이 개념이 필요한지 실무 관점에서 보면, 비즈니스 규칙 적용, 복잡한 파생 변수 생성, 도메인 특화 계산 같은 경우에 매우 유용합니다.

예를 들어, "고객 등급을 LTV, 재구매율, 최근 구매일 기준으로 복합 계산"처럼 다단계 로직이 필요할 때 사용합니다. 기존에는 데이터를 Python list로 변환해서 for 루프를 돌렸다면, 이제는 Polars 표현식으로 벡터화된 연산을 수행하거나 .map_elements()로 함수를 병렬 적용할 수 있습니다.

고급 표현식의 핵심 특징은 첫째, when().then().when().then().otherwise()로 다중 조건 분기가 가능하고, 둘째, .map_elements()는 파이썬 함수를 각 요소에 적용하며, 셋째, Polars 내장 함수를 최대한 활용하면 UDF보다 훨씬 빠릅니다. 이러한 특징들이 복잡한 비즈니스 로직을 데이터 처리 파이프라인에 통합할 수 있게 해줍니다.

코드 예제

import polars as pl

# 판매 데이터
df = pl.DataFrame({
    "region": ["서울", "부산", "대구", "인천", "광주"],
    "sales": [250000, 180000, 120000, 200000, 90000]
})

# 평균 계산
avg_sales = df["sales"].mean()

# 복잡한 조건 분기로 등급 부여
result = df.with_columns([
    pl.when(pl.col("sales") >= avg_sales * 1.2)
      .then(pl.lit("우수"))
      .when(pl.col("sales") >= avg_sales * 0.8)
      .then(pl.lit("보통"))
      .otherwise(pl.lit("부진"))
      .alias("성과등급")
])

# 사용자 정의 함수 적용 (예: 지역명을 대문자로)
def format_region(region_name):
    return f"[{region_name.upper()}]"

result = result.with_columns([
    pl.col("region").map_elements(format_region, return_dtype=pl.Utf8).alias("지역코드")
])

print(result)

설명

이것이 하는 일: 매출이 평균의 120% 이상이면 '우수', 80% 이상이면 '보통', 그 외는 '부진'으로 등급을 부여하고, 지역명을 대문자로 변환해서 코드 형태로 만듭니다. 첫 번째로, avg_sales = df["sales"].mean()으로 전체 평균 매출을 계산합니다.

이 값이 조건 분기의 기준점이 됩니다. 그 다음 pl.when(pl.col("sales") >= avg_sales * 1.2).then(pl.lit("우수"))는 "sales가 평균의 120% 이상이면 '우수'"라는 첫 번째 조건을 정의합니다.

pl.lit()은 리터럴 값(문자열 "우수")을 Polars 표현식으로 변환합니다. 그 다음으로, .when().then()을 연속으로 체이닝하면 "else if" 로직이 됩니다.

두 번째 when은 첫 번째 조건이 False일 때만 평가되므로, "평균의 80% 이상 120% 미만"을 의미합니다. 마지막 .otherwise(pl.lit("부진"))은 모든 조건이 False일 때의 기본값입니다.

이 전체 체인이 "성과등급"이라는 새 컬럼으로 추가됩니다. 마지막으로, .map_elements(format_region, return_dtype=pl.Utf8)는 각 region 값에 대해 format_region 함수를 호출합니다.

"서울"이 입력되면 함수가 "[서울]"을 반환하고, 이것이 "지역코드" 컬럼이 됩니다. return_dtype은 함수 출력 타입을 명시해서 Polars가 스키마를 추론하도록 돕습니다.

여러분이 이 코드를 사용하면 어떤 복잡한 비즈니스 규칙도 데이터에 적용할 수 있습니다. 실무에서는 "VIP 등급 산정", "리스크 점수 계산", "추천 카테고리 태깅" 같은 작업이 가능합니다.

다만 .map_elements()는 파이썬 함수 호출 오버헤드로 느릴 수 있으므로, 가능하면 Polars 내장 함수로 해결하세요. 예를 들어 pl.col("region").str.to_uppercase()가 UDF보다 10배 빠릅니다.

실전 팁

💡 UDF는 최후의 수단입니다. Polars 내장 함수(str, dt, math 등)로 가능한지 먼저 확인하세요. 내장 함수가 훨씬 빠릅니다.

💡 .map_elements()return_dtype을 반드시 지정하세요. 안 하면 Polars가 타입을 추론하느라 느려집니다.

💡 복잡한 조건은 먼저 단계별로 테스트하세요. 전체를 한 번에 작성하면 디버깅이 어렵습니다.

💡 수학 함수는 pl.col("sales").sqrt(), pl.col("value").log() 같은 내장 함수를 사용하세요. import math보다 빠릅니다.

💡 조건이 5개 이상이면 딕셔너리 매핑 방식을 고려하세요. pl.col("code").replace({"A": "우수", "B": "보통"})가 더 간결할 수 있습니다.


#Polars#GroupBy#Aggregation#DataAnalysis#Performance#데이터분석,Python,Polars

댓글 (0)

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