이미지 로딩 중...
AI Generated
2025. 11. 14. · 2 Views
Polars 그룹화와 집계 완벽 가이드
Polars의 group_by와 다양한 집계 함수를 활용하여 데이터를 효율적으로 분석하는 방법을 배웁니다. 실무에서 자주 사용하는 그룹화 패턴과 집계 연산을 실전 예제와 함께 알아봅니다.
목차
- group_by 기본 사용법 - 데이터를 그룹으로 나누기
- 기본 집계 함수들 - sum, mean, count, min, max
- 복잡한 집계 - std, var, median, quantile
- 리스트 집계 - 그룹의 모든 값을 리스트로 수집
- 조건부 집계 - filter와 when-then을 활용한 집계
- 여러 컬럼 동시 집계 - all()과 exclude()
- 다중 컬럼 그룹화 - 여러 기준으로 동시에 그룹핑
- 집계 후 필터링 - having 절 구현하기
- 윈도우 함수 활용 - 그룹 내 순위와 누적 계산
- first와 last - 각 그룹의 첫/마지막 값 추출
1. group_by 기본 사용법 - 데이터를 그룹으로 나누기
시작하며
여러분이 수천 개의 판매 데이터를 분석하면서 "지역별로 총 매출이 얼마나 되지?"라는 질문을 받은 적 있나요? 또는 "각 제품 카테고리별로 평균 가격은 얼마야?" 같은 질문을 처리해야 할 때 말이죠.
이런 문제는 데이터 분석에서 가장 기본적이면서도 빈번하게 발생합니다. 원본 데이터는 개별 거래 기록으로 구성되어 있지만, 우리가 원하는 것은 특정 기준으로 묶인 요약 정보입니다.
일일이 반복문을 돌면서 계산하면 코드도 복잡해지고 성능도 떨어집니다. 바로 이럴 때 필요한 것이 Polars의 group_by입니다.
데이터를 특정 컬럼의 값에 따라 자동으로 그룹으로 묶고, 각 그룹에 대한 계산을 간단하고 빠르게 수행할 수 있습니다.
개요
간단히 말해서, group_by는 데이터프레임의 행들을 특정 컬럼의 값에 따라 그룹으로 나누는 연산입니다. 실무에서 여러분이 데이터를 분석할 때 "카테고리별", "지역별", "날짜별" 같은 기준으로 데이터를 묶어서 통계를 내야 하는 경우가 정말 많습니다.
예를 들어, 전자상거래 데이터에서 제품 카테고리별로 판매량을 집계하거나, 사용자 행동 데이터에서 연령대별로 평균 체류 시간을 계산하는 경우에 매우 유용합니다. 기존에 pandas를 사용했다면 groupby() 메서드를 사용했을 것입니다.
Polars에서도 동일한 개념이지만, 훨씬 빠른 성능과 더 직관적인 메서드 체이닝을 제공합니다. group_by의 핵심 특징은 첫째, 여러 컬럼을 동시에 그룹화 기준으로 사용할 수 있다는 점입니다.
둘째, lazy evaluation을 지원하여 실제로 필요할 때까지 연산을 지연시켜 최적화할 수 있습니다. 셋째, 다양한 집계 함수들과 자연스럽게 연결되어 강력한 데이터 분석 파이프라인을 구성할 수 있습니다.
이러한 특징들이 여러분의 데이터 분석 작업을 훨씬 효율적으로 만들어줍니다.
코드 예제
import polars as pl
# 샘플 판매 데이터 생성
df = pl.DataFrame({
'product': ['A', 'B', 'A', 'C', 'B', 'A', 'C'],
'region': ['East', 'West', 'East', 'West', 'East', 'West', 'East'],
'sales': [100, 150, 120, 200, 180, 110, 220]
})
# 제품별로 그룹화하여 총 판매량 계산
result = df.group_by('product').agg(
pl.col('sales').sum().alias('total_sales')
)
print(result)
설명
이것이 하는 일: group_by는 데이터프레임의 행들을 지정한 컬럼의 고유 값들을 기준으로 분리하여 각 그룹에 대한 집계 연산을 가능하게 합니다. 첫 번째로, df.group_by('product') 부분은 'product' 컬럼의 고유한 값들(A, B, C)을 찾아서 각각을 하나의 그룹으로 만듭니다.
이 시점에서는 실제로 데이터가 복사되거나 이동하지 않고, Polars가 내부적으로 각 그룹의 인덱스 정보만 기록합니다. 왜 이렇게 하냐면, 메모리 효율성과 성능을 극대화하기 위해서입니다.
그 다음으로, agg() 메서드가 실행되면서 각 그룹에 대해 지정된 집계 연산이 적용됩니다. pl.col('sales').sum()은 각 그룹 내의 'sales' 컬럼 값들을 모두 더하라는 의미입니다.
alias('total_sales')는 결과 컬럼의 이름을 지정하는 부분으로, 더 의미 있는 이름을 부여할 수 있습니다. 마지막으로, 모든 그룹에 대한 계산이 완료되면 새로운 데이터프레임이 생성됩니다.
이 결과 데이터프레임은 각 그룹별로 하나의 행을 가지며, 그룹화 기준이 된 컬럼과 집계 결과 컬럼으로 구성됩니다. 예를 들어, product 'A'에 해당하는 모든 sales 값들(100, 120, 110)이 합쳐져서 330이라는 하나의 값으로 나타납니다.
여러분이 이 코드를 사용하면 수천, 수만 행의 데이터도 빠르게 그룹별로 요약할 수 있습니다. SQL의 GROUP BY와 유사한 개념이지만, Python 환경에서 훨씬 유연하게 활용할 수 있고, pandas보다 월등히 빠른 성능을 자랑합니다.
특히 대용량 데이터를 다룰 때 그 차이가 명확하게 드러납니다.
실전 팁
💡 여러 컬럼으로 동시에 그룹화하려면 group_by(['col1', 'col2']) 형태로 리스트를 전달하세요. 예를 들어, 지역과 제품을 동시에 기준으로 사용할 수 있습니다.
💡 group_by 후에는 반드시 agg()나 다른 집계 메서드를 호출해야 합니다. 그렇지 않으면 GroupBy 객체만 반환되고 실제 결과는 나오지 않습니다.
💡 대용량 데이터를 다룰 때는 lazy API를 사용하세요. df.lazy().group_by()로 시작하면 Polars가 쿼리 최적화를 수행하여 성능이 크게 향상됩니다.
💡 그룹화 전에 데이터를 필터링하면 성능이 좋아집니다. 필요 없는 데이터를 미리 제거하고 group_by를 수행하세요.
💡 결과의 순서가 중요하다면 .sort()를 체이닝하세요. group_by의 결과 순서는 보장되지 않으므로, 명시적으로 정렬해야 합니다.
2. 기본 집계 함수들 - sum, mean, count, min, max
시작하며
여러분이 각 그룹에 대해 "합계가 얼마인지", "평균은 얼마인지", "몇 개나 있는지" 알고 싶을 때 어떻게 하시나요? 매번 복잡한 로직을 작성하는 것은 비효율적입니다.
이런 기본적인 통계 계산은 데이터 분석에서 가장 자주 수행되는 작업입니다. 제대로 활용하지 못하면 불필요하게 긴 코드를 작성하게 되고, 실수할 가능성도 높아집니다.
또한 성능 최적화의 기회도 놓치게 됩니다. 바로 이럴 때 필요한 것이 Polars의 기본 집계 함수들입니다.
sum, mean, count, min, max 같은 함수들을 사용하면 한 줄의 코드로 원하는 통계를 바로 계산할 수 있습니다.
개요
간단히 말해서, 집계 함수는 여러 값들을 하나의 대표 값으로 요약하는 함수들입니다. 실무에서 여러분이 데이터를 요약할 때 가장 많이 사용하는 통계량들이 바로 합계, 평균, 개수, 최솟값, 최댓값입니다.
예를 들어, 매출 데이터를 분석할 때 총 매출액(sum), 평균 거래액(mean), 거래 건수(count), 최소/최대 거래액(min/max)을 모두 함께 보는 것이 일반적입니다. 기존에는 이런 계산들을 각각 따로 수행하거나, 직접 반복문을 작성해야 했습니다.
이제는 Polars의 집계 함수들을 사용하면 간결하고 읽기 쉬운 코드로 모든 통계를 한 번에 계산할 수 있습니다. 이러한 함수들의 핵심 특징은 첫째, 메서드 체이닝을 통해 여러 집계를 동시에 수행할 수 있다는 점입니다.
둘째, Null 값을 자동으로 처리하여 안전하게 계산합니다. 셋째, Polars의 병렬 처리 엔진을 활용하여 대용량 데이터도 빠르게 처리합니다.
이러한 특징들이 여러분의 데이터 분석 코드를 간결하고 강력하게 만들어줍니다.
코드 예제
import polars as pl
df = pl.DataFrame({
'category': ['A', 'B', 'A', 'B', 'A', 'C'],
'value': [10, 20, 15, 25, 30, 18],
'quantity': [5, 3, 7, 2, 4, 6]
})
# 카테고리별로 다양한 집계 동시 수행
result = df.group_by('category').agg([
pl.col('value').sum().alias('total_value'), # 합계
pl.col('value').mean().alias('avg_value'), # 평균
pl.col('value').count().alias('count'), # 개수
pl.col('quantity').min().alias('min_qty'), # 최솟값
pl.col('quantity').max().alias('max_qty') # 최댓값
])
print(result)
설명
이것이 하는 일: 각 집계 함수는 그룹 내의 여러 값들을 읽어서 하나의 의미 있는 통계값으로 변환합니다. 첫 번째로, agg() 메서드 안에 리스트 형태로 여러 집계 연산을 전달합니다.
각 집계 함수는 pl.col('컬럼명')을 통해 어떤 컬럼에 적용할지 지정하고, 그 뒤에 .sum(), .mean() 같은 메서드를 체이닝합니다. 이렇게 하면 Polars가 각 그룹에 대해 모든 집계를 한 번의 패스로 효율적으로 계산합니다.
그 다음으로, 각 집계 함수가 실행되면서 그룹 내의 데이터가 처리됩니다. sum()은 모든 값을 더하고, mean()은 합계를 개수로 나누며, count()는 Null이 아닌 값의 개수를 세고, min()과 max()는 가장 작은 값과 큰 값을 찾습니다.
이 모든 연산이 Polars의 최적화된 Rust 엔진에서 병렬로 처리되어 매우 빠릅니다. 마지막으로, alias() 메서드를 사용하여 각 결과 컬럼에 의미 있는 이름을 부여합니다.
이렇게 하지 않으면 컬럼 이름이 자동 생성되어 가독성이 떨어집니다. 'total_value', 'avg_value' 같은 명확한 이름을 사용하면 나중에 코드를 읽을 때나 결과를 해석할 때 훨씬 이해하기 쉽습니다.
여러분이 이 코드를 사용하면 단 몇 줄로 포괄적인 통계 분석을 수행할 수 있습니다. 카테고리 A의 총 value는 55(10+15+30), 평균은 18.33, 개수는 3개라는 것을 한눈에 파악할 수 있습니다.
이런 패턴은 실무에서 데이터 탐색 단계나 리포트 작성 시 매우 자주 사용되며, 코드의 가독성과 유지보수성을 크게 향상시킵니다.
실전 팁
💡 여러 컬럼에 같은 집계를 적용하려면 pl.col(['col1', 'col2']).sum() 형태로 여러 컬럼을 한 번에 지정할 수 있습니다.
💡 count()는 Null을 제외하고 셉니다. 전체 행 수를 세려면 pl.count()를 사용하세요(컬럼 지정 없이).
💡 평균 계산 시 Null 값이 많다면 결과가 왜곡될 수 있으니 먼저 drop_nulls()로 제거하거나, fill_null()로 채워주세요.
💡 성능이 중요하다면 필요한 집계만 수행하세요. 불필요한 집계가 많으면 계산 시간이 늘어납니다.
💡 통계 결과를 시각화할 때는 sort()로 정렬한 후 그래프로 만들면 인사이트를 찾기 쉽습니다.
3. 복잡한 집계 - std, var, median, quantile
시작하며
여러분이 데이터의 분포나 변동성을 분석해야 할 때, 단순히 평균만으로는 충분하지 않은 경우가 있습니다. 예를 들어, 두 영업팀의 평균 매출은 같지만 한 팀은 매출이 안정적이고 다른 팀은 변동이 심하다면 어떻게 알 수 있을까요?
이런 문제는 금융, 품질 관리, 위험 분석 등 다양한 분야에서 자주 발생합니다. 데이터의 산포도, 중앙값, 백분위수 같은 고급 통계량을 계산해야만 데이터의 진짜 모습을 이해할 수 있습니다.
단순 평균에만 의존하면 중요한 패턴을 놓칠 수 있습니다. 바로 이럴 때 필요한 것이 Polars의 고급 집계 함수들입니다.
표준편차(std), 분산(var), 중앙값(median), 분위수(quantile) 같은 함수들을 사용하면 데이터의 깊이 있는 통계 분석이 가능합니다.
개요
간단히 말해서, 고급 집계 함수는 데이터의 분포, 변동성, 중심 경향을 다각도로 분석할 수 있는 통계 함수들입니다. 실무에서 여러분이 데이터의 안정성을 평가하거나, 이상치를 감지하거나, 분포의 특성을 파악해야 할 때 이런 함수들이 필수적입니다.
예를 들어, 서버 응답 시간의 표준편차가 크면 성능이 불안정하다는 신호이고, 매출 데이터의 중앙값이 평균보다 훨씬 낮다면 일부 큰 거래가 평균을 끌어올리고 있다는 것을 알 수 있습니다. 기존에 이런 통계를 계산하려면 numpy나 scipy 같은 라이브러리를 별도로 사용하거나, 직접 수식을 구현해야 했습니다.
이제는 Polars가 이런 함수들을 내장하고 있어서 그룹화된 데이터에 바로 적용할 수 있습니다. 이러한 함수들의 핵심 특징은 첫째, 통계적으로 정확한 알고리즘을 사용한다는 점입니다.
둘째, 대용량 데이터에서도 메모리 효율적으로 동작합니다. 셋째, 다른 집계 함수들과 자유롭게 조합하여 종합적인 통계 분석을 수행할 수 있습니다.
이러한 특징들이 여러분의 데이터 분석을 한 단계 더 전문적으로 만들어줍니다.
코드 예제
import polars as pl
df = pl.DataFrame({
'team': ['A', 'A', 'A', 'B', 'B', 'B'],
'sales': [100, 120, 110, 200, 150, 180],
'response_time': [1.2, 1.5, 1.3, 2.1, 1.8, 2.5]
})
# 팀별로 고급 통계 계산
result = df.group_by('team').agg([
pl.col('sales').mean().alias('avg_sales'), # 평균
pl.col('sales').std().alias('std_sales'), # 표준편차
pl.col('sales').var().alias('var_sales'), # 분산
pl.col('sales').median().alias('median_sales'), # 중앙값
pl.col('response_time').quantile(0.95).alias('p95_response') # 95 백분위수
])
print(result)
설명
이것이 하는 일: 각 고급 집계 함수는 데이터의 분포 특성을 수치화하여 단순 평균만으로는 알 수 없는 정보를 제공합니다. 첫 번째로, std()와 var() 함수는 데이터가 평균으로부터 얼마나 흩어져 있는지를 측정합니다.
표준편차는 분산의 제곱근이며, 원래 데이터와 같은 단위를 가지므로 해석하기 쉽습니다. 예를 들어, 팀 A의 매출 표준편차가 10이고 팀 B가 25라면, 팀 B의 매출 변동성이 훨씬 크다는 것을 알 수 있습니다.
이는 위험 관리나 예측의 불확실성을 평가할 때 중요합니다. 그 다음으로, median() 함수는 데이터를 크기 순으로 정렬했을 때 정중앙에 있는 값을 찾습니다.
이는 평균과 달리 극단값(이상치)의 영향을 덜 받습니다. 만약 한 팀의 평균 매출은 150인데 중앙값이 110이라면, 소수의 아주 큰 매출이 평균을 끌어올리고 있다는 의미입니다.
이런 경우 중앙값이 더 대표적인 값이 될 수 있습니다. 마지막으로, quantile() 함수는 데이터의 특정 백분위 지점의 값을 계산합니다.
0.95를 전달하면 95 백분위수(상위 5% 지점)를 반환하는데, 이는 성능 분석에서 매우 유용합니다. 예를 들어, 응답 시간의 95 백분위수가 2.5초라면, 대부분의 요청(95%)은 2.5초 이내에 처리되고 있다는 뜻입니다.
평균보다 이런 백분위수가 SLA 설정에 더 적합합니다. 여러분이 이 코드를 사용하면 데이터의 전체적인 모습을 입체적으로 파악할 수 있습니다.
평균만 보면 놓칠 수 있는 변동성, 분포의 비대칭성, 극단값의 존재 등을 한눈에 알 수 있습니다. 이는 데이터 기반 의사결정을 할 때 훨씬 신뢰할 수 있는 근거를 제공하며, 잘못된 결론을 내릴 위험을 줄여줍니다.
실전 팁
💡 표준편차와 분산은 ddof 매개변수로 자유도를 조정할 수 있습니다. 표본의 경우 ddof=1(기본값), 모집단은 ddof=0을 사용하세요.
💡 중앙값 계산은 정렬이 필요해서 상대적으로 느립니다. 대용량 데이터에서는 근사값으로 충분하다면 quantile(0.5)를 고려하세요.
💡 quantile은 0과 1 사이의 값을 받습니다. 0.25, 0.5, 0.75는 각각 1사분위수, 중앙값, 3사분위수에 해당합니다.
💡 이상치 탐지를 하려면 평균 ± 3*표준편차 범위를 벗어나는 값을 찾으세요. 이는 정규분포에서 99.7% 구간입니다.
💡 여러 백분위수를 동시에 계산하려면 리스트 컴프리헨션을 활용하세요: [pl.col('value').quantile(q).alias(f'p{int(q*100)}') for q in [0.25, 0.5, 0.75, 0.95]]
4. 리스트 집계 - 그룹의 모든 값을 리스트로 수집
시작하며
여러분이 각 그룹에 속한 모든 개별 값들을 하나의 리스트로 모아야 할 때가 있습니다. 예를 들어, 각 사용자가 구매한 모든 제품 목록을 보고 싶거나, 각 지역에서 발생한 모든 거래 금액들을 한눈에 보고 싶을 때 말이죠.
이런 작업은 추천 시스템, 사용자 행동 분석, 세부 리포팅 등에서 자주 필요합니다. 단순히 합계나 평균만 알면 되는 게 아니라, 실제로 어떤 값들이 있었는지 전부 보관해야 하는 경우가 있습니다.
직접 구현하면 딕셔너리와 반복문을 사용해야 하고 코드가 복잡해집니다. 바로 이럴 때 필요한 것이 Polars의 리스트 집계 기능입니다.
list()나 implode() 함수를 사용하면 그룹 내의 모든 값을 하나의 리스트로 깔끔하게 수집할 수 있습니다.
개요
간단히 말해서, 리스트 집계는 그룹 내의 모든 값들을 하나의 Python 리스트로 묶어주는 연산입니다. 실무에서 여러분이 고객별 구매 이력, 제품별 리뷰 목록, 센서별 측정값 시계열 같은 데이터를 다룰 때 이 기능이 매우 유용합니다.
예를 들어, 각 고객이 구매한 제품들을 리스트로 만들면, 나중에 협업 필터링이나 장바구니 분석 같은 고급 분석을 수행할 수 있습니다. 기존에는 이런 작업을 하려면 groupby 후에 apply를 사용하거나, 직접 딕셔너리를 만들어서 append하는 방식을 사용해야 했습니다.
이제는 Polars의 리스트 집계를 사용하면 한 줄로 간결하게 처리할 수 있고, 성능도 훨씬 좋습니다. 리스트 집계의 핵심 특징은 첫째, 원본 데이터의 순서를 유지할 수 있다는 점입니다.
둘째, 리스트 컬럼은 Polars의 강력한 리스트 연산 API와 함께 사용할 수 있습니다. 셋째, 메모리 효율적으로 설계되어 대용량 데이터에서도 잘 동작합니다.
이러한 특징들이 복잡한 데이터 구조를 다룰 때 큰 도움이 됩니다.
코드 예제
import polars as pl
df = pl.DataFrame({
'customer': ['Alice', 'Bob', 'Alice', 'Charlie', 'Bob', 'Alice'],
'product': ['Apple', 'Banana', 'Orange', 'Apple', 'Apple', 'Banana'],
'price': [1.2, 0.5, 0.8, 1.2, 1.2, 0.5]
})
# 고객별로 구매한 제품과 가격을 리스트로 수집
result = df.group_by('customer').agg([
pl.col('product').list().alias('purchased_products'), # 제품 리스트
pl.col('price').list().alias('prices'), # 가격 리스트
pl.col('price').sum().alias('total_spent') # 총 지출액도 함께
])
print(result)
설명
이것이 하는 일: 리스트 집계는 각 그룹에 속한 모든 행의 값들을 순서대로 하나의 리스트 자료구조로 모읍니다. 첫 번째로, group_by('customer')가 고객별로 데이터를 그룹으로 나눕니다.
Alice, Bob, Charlie 각각이 하나의 그룹이 되는 것이죠. 이때 각 그룹은 여러 행을 포함할 수 있습니다.
Alice의 경우 세 번의 구매 기록이 있으므로 세 개의 행이 한 그룹을 이룹니다. 그 다음으로, pl.col('product').list()가 실행되면 각 그룹 내의 모든 'product' 값들이 수집됩니다.
Alice 그룹의 경우 'Apple', 'Orange', 'Banana'가 하나의 Python 리스트 ['Apple', 'Orange', 'Banana']로 변환됩니다. 마찬가지로 'price' 컬럼도 [1.2, 0.8, 0.5] 같은 리스트가 됩니다.
이 리스트들은 원본 데이터의 순서를 그대로 유지합니다. 마지막으로, 결과 데이터프레임에서 각 행은 하나의 고객을 나타내며, 'purchased_products' 컬럼은 리스트 타입을 가집니다.
이 리스트 컬럼에 대해서는 나중에 .list.len(), .list.unique(), .list.contains() 같은 Polars의 리스트 전용 메서드들을 사용하여 추가 분석을 할 수 있습니다. 예를 들어, 각 고객이 몇 개의 서로 다른 제품을 구매했는지 세는 것도 간단합니다.
여러분이 이 코드를 사용하면 상세한 정보를 잃지 않으면서도 데이터를 효과적으로 그룹화할 수 있습니다. 단순히 "Alice가 총 2.5달러를 썼다"뿐만 아니라 "무엇을 얼마에 샀는지"까지 알 수 있습니다.
이는 추후 분석, 시각화, 머신러닝 피처 엔지니어링 등 다양한 용도로 활용 가능하며, 데이터의 맥락을 보존하는 데 매우 중요합니다.
실전 팁
💡 리스트의 길이가 그룹마다 다를 수 있으니 주의하세요. 필요하다면 .list.len()으로 각 리스트의 크기를 확인할 수 있습니다.
💡 중복을 제거하고 싶다면 .list().list.unique()를 체이닝하세요. 예: pl.col('product').list().list.unique()
💡 리스트를 정렬하려면 .list().list.sort()를 사용하세요. 시계열 데이터나 순서가 중요한 경우 유용합니다.
💡 대용량 데이터에서 리스트 집계는 메모리를 많이 사용할 수 있습니다. 그룹당 수천 개 이상의 항목이 있다면 다른 접근 방법을 고려하세요.
💡 리스트 컬럼을 다시 개별 행으로 펼치려면 .explode('컬럼명')를 사용하세요. 이는 group_by의 역연산과 비슷합니다.
5. 조건부 집계 - filter와 when-then을 활용한 집계
시작하며
여러분이 특정 조건을 만족하는 데이터만 선택적으로 집계해야 할 때가 있습니다. 예를 들어, "각 지역에서 100달러 이상 구매한 고객의 수만 세고 싶다"거나 "특정 날짜 이후의 매출만 합산하고 싶다"는 경우 말이죠.
이런 조건부 집계는 실무에서 정말 자주 사용됩니다. 단순한 전체 집계만으로는 원하는 인사이트를 얻기 어렵고, 필터링과 집계를 별도로 수행하면 코드가 길어지고 비효율적입니다.
여러 조건에 따른 집계를 동시에 수행해야 할 때는 더욱 복잡해집니다. 바로 이럴 때 필요한 것이 Polars의 조건부 집계 기능입니다.
filter()를 사용하거나 when().then().otherwise() 표현식을 활용하면 조건에 맞는 데이터만 선택적으로 집계할 수 있습니다.
개요
간단히 말해서, 조건부 집계는 특정 조건을 만족하는 값들만 골라서 집계를 수행하는 방식입니다. 실무에서 여러분이 세그먼트별 분석, A/B 테스트 결과 비교, 임계값 기반 알림 등을 구현할 때 이 기능이 필수적입니다.
예를 들어, 온라인 쇼핑몰에서 "각 카테고리에서 프리미엄 제품(가격 > 100)의 평균 평점"과 "일반 제품의 평균 평점"을 동시에 계산하는 경우가 있습니다. 기존에는 데이터를 먼저 필터링한 후 집계하거나, SQL의 CASE WHEN 같은 복잡한 구문을 사용해야 했습니다.
이제는 Polars의 표현식 API를 사용하면 더 직관적이고 효율적으로 조건부 집계를 수행할 수 있습니다. 조건부 집계의 핵심 특징은 첫째, 여러 조건에 대한 집계를 한 번의 패스로 수행할 수 있다는 점입니다.
둘째, filter()를 사용하면 집계 전에 행을 필터링할 수 있습니다. 셋째, when().then()을 사용하면 값 수준에서 조건부 로직을 적용할 수 있습니다.
이러한 특징들이 복잡한 비즈니스 로직을 간결하게 표현할 수 있게 해줍니다.
코드 예제
import polars as pl
df = pl.DataFrame({
'category': ['A', 'A', 'B', 'B', 'A', 'B'],
'price': [50, 150, 80, 200, 120, 60],
'status': ['active', 'inactive', 'active', 'active', 'active', 'inactive']
})
# 카테고리별로 조건부 집계
result = df.group_by('category').agg([
# active 상태인 것만 카운트
pl.col('status').filter(pl.col('status') == 'active').count().alias('active_count'),
# 가격이 100 이상인 것만 평균
pl.col('price').filter(pl.col('price') >= 100).mean().alias('premium_avg'),
# when-then을 사용한 조건부 합계
pl.when(pl.col('price') >= 100).then(pl.col('price')).otherwise(0).sum().alias('premium_total')
])
print(result)
설명
이것이 하는 일: 조건부 집계는 전체 그룹에서 특정 조건을 만족하는 값들만 골라내어 집계 연산을 적용합니다. 첫 번째로, filter() 메서드는 집계 함수 체인 중간에 삽입되어 조건을 만족하는 값들만 통과시킵니다.
pl.col('status').filter(pl.col('status') == 'active')는 'status'가 'active'인 행들만 선택하고, 그 다음 .count()가 그 개수를 셉니다. 이는 SQL의 COUNT(CASE WHEN status = 'active' THEN 1 END)와 유사하지만 훨씬 읽기 쉽습니다.
그 다음으로, when().then().otherwise() 표현식은 각 값에 대해 조건을 평가하고 그 결과에 따라 다른 값을 반환합니다. pl.when(pl.col('price') >= 100).then(pl.col('price')).otherwise(0)은 가격이 100 이상이면 그 가격을 그대로 사용하고, 아니면 0으로 바꿉니다.
이렇게 변환된 값들을 .sum()으로 합산하면, 100 이상인 가격들만의 합계가 계산됩니다. 마지막으로, 이런 조건부 집계들을 여러 개 조합하면 한 번의 group_by로 다양한 세그먼트 분석을 수행할 수 있습니다.
예를 들어, 카테고리 A에서 active 상태가 2개, premium 제품의 평균 가격이 135, premium 제품 총액이 270이라는 세 가지 서로 다른 통계를 동시에 얻을 수 있습니다. 이는 데이터를 여러 번 순회할 필요 없이 효율적으로 처리됩니다.
여러분이 이 코드를 사용하면 복잡한 비즈니스 룰을 명확하고 간결하게 표현할 수 있습니다. "프리미엄 제품만", "활성 사용자만", "특정 기간만" 같은 다양한 조건을 자유롭게 조합하여 정교한 분석이 가능합니다.
또한 성능 측면에서도 데이터를 한 번만 읽으면서 여러 조건부 집계를 동시에 수행하므로, 각 조건별로 따로 필터링하고 집계하는 것보다 훨씬 빠릅니다.
실전 팁
💡 filter()는 집계 체인 중간에 여러 번 사용할 수 있습니다. 복잡한 조건은 &(and)와 |(or)로 연결하세요.
💡 when().then()은 여러 조건을 체이닝할 수 있습니다. when(조건1).then(값1).when(조건2).then(값2).otherwise(기본값) 형태로 사용하세요.
💡 조건부 집계가 많으면 성능이 저하될 수 있습니다. 가능하면 사전에 필터링을 먼저 하고 집계하는 것이 좋습니다.
💡 Null 값 처리에 주의하세요. filter()는 Null을 제외하지만, when-then은 명시적으로 처리해야 합니다.
💡 디버깅할 때는 조건별로 별도의 집계 컬럼을 만들어서 결과를 확인하세요. 나중에 통합할 수 있습니다.
6. 여러 컬럼 동시 집계 - all()과 exclude()
시작하며
여러분이 데이터프레임에 수십 개의 숫자 컬럼이 있고, 모든 컬럼에 대해 같은 집계를 수행해야 할 때 어떻게 하시나요? 각 컬럼마다 일일이 pl.col('컬럼명').sum() 같은 코드를 반복해서 작성하는 것은 비효율적이고 실수하기 쉽습니다.
이런 상황은 센서 데이터, 재무 데이터, 다변량 시계열 같은 넓은 형태의 데이터를 다룰 때 자주 발생합니다. 컬럼이 많을수록 코드가 길어지고 유지보수가 어려워집니다.
컬럼이 추가되거나 변경되면 코드도 매번 수정해야 합니다. 바로 이럴 때 필요한 것이 Polars의 all()과 exclude() 선택자입니다.
여러 컬럼을 한 번에 선택하고 동일한 집계를 일괄 적용할 수 있습니다.
개요
간단히 말해서, all()과 exclude()는 여러 컬럼을 패턴으로 선택하여 한 번에 집계할 수 있게 해주는 선택자입니다. 실무에서 여러분이 대시보드를 만들거나 종합 리포트를 생성할 때, 모든 숫자 컬럼의 합계나 평균을 한 번에 계산해야 하는 경우가 많습니다.
예를 들어, 판매 데이터에서 '매출', '수량', '할인액', '배송비' 같은 여러 컬럼을 모두 지역별로 합산해야 한다면, all()을 사용하면 간단합니다. 기존에는 각 컬럼명을 하나하나 나열하거나, 반복문과 동적 표현식 생성 같은 복잡한 방법을 사용해야 했습니다.
이제는 Polars의 선택자를 사용하면 코드 몇 줄로 모든 컬럼에 집계를 적용할 수 있습니다. 이러한 선택자의 핵심 특징은 첫째, 코드 중복을 크게 줄일 수 있다는 점입니다.
둘째, 데이터 스키마가 변경되어도 코드를 수정할 필요가 없습니다. 셋째, exclude()를 사용하면 특정 컬럼만 제외하고 나머지 모두에 적용할 수 있습니다.
이러한 특징들이 유지보수성과 생산성을 크게 향상시킵니다.
코드 예제
import polars as pl
df = pl.DataFrame({
'region': ['East', 'West', 'East', 'West'],
'sales': [100, 150, 120, 180],
'quantity': [10, 15, 12, 18],
'discount': [5, 8, 6, 9],
'shipping': [10, 12, 11, 13]
})
# 지역별로 region을 제외한 모든 숫자 컬럼의 합계 계산
result = df.group_by('region').agg(
pl.all().exclude('region').sum()
)
# 혹은 특정 타입의 컬럼만 선택
# result = df.group_by('region').agg(
# pl.col(pl.Int64, pl.Float64).sum()
# )
print(result)
설명
이것이 하는 일: all()과 exclude()는 패턴 매칭 방식으로 여러 컬럼을 선택하고, 선택된 모든 컬럼에 동일한 집계 연산을 일괄 적용합니다. 첫 번째로, pl.all()은 데이터프레임의 모든 컬럼을 선택합니다.
이는 말 그대로 모든 컬럼을 대상으로 하겠다는 의미입니다. 하지만 group_by의 키로 사용된 컬럼까지 집계하면 의미가 없으므로, .exclude('region')을 체이닝하여 'region' 컬럼을 제외합니다.
결과적으로 'sales', 'quantity', 'discount', 'shipping' 네 개의 컬럼만 선택됩니다. 그 다음으로, 선택된 각 컬럼에 대해 .sum()이 적용됩니다.
Polars는 내부적으로 이를 "sales 컬럼의 sum, quantity 컬럼의 sum, discount 컬럼의 sum, shipping 컬럼의 sum"으로 확장하여 처리합니다. 이는 마치 네 개의 집계 표현식을 작성한 것과 동일하지만, 코드는 한 줄로 간결합니다.
마지막으로, 결과 데이터프레임은 'region' 컬럼과 함께 각 숫자 컬럼의 합계가 담긴 컬럼들을 포함합니다. East 지역의 경우 sales는 220(100+120), quantity는 22(10+12) 같은 식으로 자동으로 계산됩니다.
만약 나중에 'tax'라는 새 컬럼이 추가되더라도 코드를 수정할 필요 없이 자동으로 집계에 포함됩니다. 여러분이 이 코드를 사용하면 넓은 형태의 데이터를 다룰 때 엄청난 편의성을 경험할 수 있습니다.
특히 데이터 스키마가 자주 변경되는 환경에서는 유지보수 부담이 크게 줄어듭니다. 또한 타입 기반 선택(pl.col(pl.Int64))을 사용하면 "모든 정수 컬럼", "모든 실수 컬럼" 같은 방식으로도 선택할 수 있어, 더욱 유연한 코드 작성이 가능합니다.
실전 팁
💡 pl.col()에 정규표현식을 사용할 수 있습니다. pl.col('^sales_')는 'sales_'로 시작하는 모든 컬럼을 선택합니다.
💡 여러 컬럼을 리스트로 명시적으로 제외하려면 exclude(['col1', 'col2']) 형태로 사용하세요.
💡 타입 기반 선택은 매우 강력합니다. pl.col(pl.Utf8)는 모든 문자열 컬럼을, pl.col(pl.NUMERIC_DTYPES)는 모든 숫자 컬럼을 선택합니다.
💡 다른 집계를 적용하고 싶다면 suffix 매개변수를 사용하세요. agg([pl.all().sum().suffix('_sum'), pl.all().mean().suffix('_avg')])
💡 컬럼이 정말 많다면(수백 개 이상) 선택 범위를 좁히는 것이 성능에 도움이 됩니다. 필요한 컬럼만 정확히 선택하세요.
7. 다중 컬럼 그룹화 - 여러 기준으로 동시에 그룹핑
시작하며
여러분이 데이터를 분석할 때 단일 기준이 아닌 여러 기준을 조합하여 그룹화해야 하는 경우가 자주 있습니다. 예를 들어, "지역별, 제품 카테고리별 매출"이나 "연도별, 월별, 부서별 평균 비용" 같은 다차원 분석을 해야 할 때 말이죠.
이런 다차원 분석은 비즈니스 인텔리전스, 데이터 웨어하우징, 피벗 테이블 생성 등에서 핵심적인 작업입니다. 단일 기준 그룹화만으로는 데이터의 복잡한 패턴을 파악하기 어렵습니다.
여러 번 그룹화를 반복하면 코드도 복잡해지고 성능도 떨어집니다. 바로 이럴 때 필요한 것이 Polars의 다중 컬럼 그룹화입니다.
group_by에 여러 컬럼을 리스트로 전달하면 모든 조합에 대한 그룹을 자동으로 만들어줍니다.
개요
간단히 말해서, 다중 컬럼 그룹화는 두 개 이상의 컬럼을 동시에 기준으로 사용하여 모든 고유 조합별로 데이터를 그룹핑하는 것입니다. 실무에서 여러분이 교차 분석이나 세분화된 세그먼트 분석을 수행할 때 이 기능이 필수적입니다.
예를 들어, 온라인 쇼핑몰에서 "지역 + 연령대 + 제품 카테고리"의 조합별로 평균 구매액을 분석하면, 어떤 세그먼트가 가장 가치 있는 고객인지 파악할 수 있습니다. 기존에는 여러 컬럼을 하나의 복합 키로 합치거나, 중첩된 groupby를 사용해야 했습니다.
이제는 Polars에서 리스트 형태로 컬럼들을 전달하기만 하면 모든 조합이 자동으로 처리됩니다. 다중 컬럼 그룹화의 핵심 특징은 첫째, SQL의 GROUP BY col1, col2와 동일한 의미론을 가진다는 점입니다.
둘째, 그룹화 키의 순서는 결과에 영향을 주지 않지만 정렬 시 고려됩니다. 셋째, 10개 이상의 컬럼으로도 그룹화할 수 있어 매우 유연합니다.
이러한 특징들이 복잡한 다차원 분석을 단순하게 만들어줍니다.
코드 예제
import polars as pl
df = pl.DataFrame({
'region': ['East', 'West', 'East', 'West', 'East', 'West'],
'category': ['A', 'A', 'B', 'B', 'A', 'B'],
'quarter': ['Q1', 'Q1', 'Q1', 'Q1', 'Q2', 'Q2'],
'revenue': [100, 150, 120, 180, 110, 200]
})
# 지역, 카테고리, 분기별로 동시에 그룹화
result = df.group_by(['region', 'category', 'quarter']).agg([
pl.col('revenue').sum().alias('total_revenue'),
pl.col('revenue').count().alias('transaction_count')
]).sort(['region', 'category', 'quarter'])
print(result)
설명
이것이 하는 일: 다중 컬럼 그룹화는 지정된 여러 컬럼의 값 조합이 동일한 행들을 하나의 그룹으로 묶습니다. 첫 번째로, group_by(['region', 'category', 'quarter'])는 이 세 컬럼의 고유한 조합들을 모두 찾습니다.
예를 들어, (East, A, Q1), (East, A, Q2), (East, B, Q1) 같은 조합이 각각 별도의 그룹이 됩니다. 데이터에 (East, A, Q1) 조합을 가진 행이 여러 개 있다면, 그 모든 행이 하나의 그룹을 이룹니다.
Polars는 해시 테이블을 사용하여 이런 조합들을 효율적으로 추적합니다. 그 다음으로, 각 그룹에 대해 지정된 집계 연산들이 수행됩니다.
total_revenue는 해당 조합에 속하는 모든 revenue 값을 합산하고, transaction_count는 그룹의 행 개수를 셉니다. 예를 들어, (East, A, Q1) 그룹이 revenue 100을 가진 행 하나만 있다면 total_revenue는 100이고 transaction_count는 1입니다.
마지막으로, sort()를 사용하여 결과를 정렬합니다. 다중 컬럼 정렬이므로 먼저 region으로 정렬하고, 같은 region 내에서는 category로, 같은 category 내에서는 quarter로 정렬됩니다.
이렇게 하면 결과가 계층적으로 보기 좋게 정리되어, 피벗 테이블이나 리포트로 활용하기 쉽습니다. 여러분이 이 코드를 사용하면 매우 세밀한 수준의 분석이 가능합니다.
"동부 지역의 A 카테고리 제품이 1분기에 얼마나 팔렸는가" 같은 구체적인 질문에 바로 답할 수 있습니다. 또한 이런 세분화된 데이터를 시각화하면 인사이트를 발견하기 쉽습니다.
예를 들어, 특정 지역+카테고리 조합이 특정 분기에만 매출이 높다면, 계절적 패턴이나 프로모션 효과를 추론할 수 있습니다.
실전 팁
💡 그룹화 컬럼이 많을수록 그룹 수가 기하급수적으로 증가합니다. 실제로 의미 있는 조합만 선택하세요.
💡 결과에 빈 그룹(조합은 있지만 데이터가 없는)을 포함하려면 join을 사용하여 모든 조합의 카티션 곱을 만들어야 합니다.
💡 그룹화 순서는 성능에 영향을 줄 수 있습니다. 카디널리티가 낮은 컬럼(고유값이 적은)을 먼저 두는 것이 일반적으로 유리합니다.
💡 다중 컬럼 그룹화 결과는 피벗 테이블로 변환하기 좋습니다. pivot() 메서드를 체이닝하여 넓은 형태로 만들 수 있습니다.
💡 너무 많은 조합이 생성되면 메모리와 성능 문제가 발생할 수 있습니다. 먼저 데이터를 필터링하여 범위를 좁히세요.
8. 집계 후 필터링 - having 절 구현하기
시작하며
여러분이 그룹별로 집계를 수행한 후, 특정 조건을 만족하는 그룹만 남기고 싶을 때가 있습니다. 예를 들어, "평균 매출이 1000 이상인 지역만 보고 싶다"거나 "거래 건수가 10건 이상인 고객만 분석하고 싶다"는 경우 말이죠.
이런 작업은 SQL의 HAVING 절에 해당하며, WHERE 절(일반 필터링)과는 다릅니다. HAVING은 집계 결과에 대한 필터링이므로, 집계를 먼저 수행한 후에 적용해야 합니다.
순서를 잘못 이해하면 원하는 결과를 얻을 수 없습니다. 바로 이럴 때 필요한 것이 Polars의 집계 후 필터링 패턴입니다.
group_by와 agg를 수행한 후 filter를 체이닝하면 집계 결과를 기준으로 그룹을 선택할 수 있습니다.
개요
간단히 말해서, 집계 후 필터링은 그룹별 집계를 수행한 다음, 집계 결과 값을 기준으로 특정 그룹만 선택하는 것입니다. 실무에서 여러분이 중요한 세그먼트를 찾거나, 이상치 그룹을 탐지하거나, 임계값을 넘는 그룹만 추출해야 할 때 이 패턴이 매우 유용합니다.
예를 들어, VIP 고객을 정의할 때 "총 구매액이 10000 이상인 고객"이라는 조건을 사용하는데, 이는 집계 후 필터링으로 구현됩니다. 기존에는 집계 결과를 별도 변수에 저장한 후 다시 필터링하거나, 복잡한 서브쿼리를 작성해야 했습니다.
이제는 Polars의 메서드 체이닝을 사용하면 자연스럽게 집계와 필터링을 연결할 수 있습니다. 집계 후 필터링의 핵심 특징은 첫째, SQL의 HAVING과 정확히 동일한 의미론을 가진다는 점입니다.
둘째, filter를 agg 이후에 체이닝하여 집계 결과 컬럼을 참조할 수 있습니다. 셋째, 여러 집계 조건을 AND/OR로 조합할 수 있습니다.
이러한 특징들이 정교한 데이터 필터링을 가능하게 합니다.
코드 예제
import polars as pl
df = pl.DataFrame({
'customer': ['Alice', 'Bob', 'Charlie', 'Alice', 'Bob', 'Alice'],
'amount': [100, 50, 200, 150, 80, 120],
'quantity': [2, 1, 5, 3, 2, 4]
})
# 총 구매액이 200 이상인 고객만 선택
result = df.group_by('customer').agg([
pl.col('amount').sum().alias('total_amount'),
pl.col('quantity').sum().alias('total_quantity'),
pl.col('amount').count().alias('purchase_count')
]).filter(
pl.col('total_amount') >= 200 # 집계 결과를 기준으로 필터링
)
print(result)
설명
이것이 하는 일: 집계 후 필터링은 먼저 모든 그룹에 대해 집계를 완료한 다음, 집계 결과가 특정 조건을 만족하는 그룹만 최종 결과에 포함시킵니다. 첫 번째로, group_by('customer')와 agg()가 실행되어 각 고객별로 total_amount, total_quantity, purchase_count가 계산됩니다.
이 시점에서는 모든 고객(Alice, Bob, Charlie)에 대한 집계가 완료되어 중간 결과 데이터프레임이 생성됩니다. Alice의 total_amount는 370(100+150+120), Bob은 130(50+80), Charlie는 200입니다.
그 다음으로, filter(pl.col('total_amount') >= 200)가 이 중간 결과 데이터프레임에 적용됩니다. 여기서 중요한 점은 'total_amount'가 원본 데이터가 아니라 방금 집계로 만들어진 컬럼이라는 것입니다.
이 필터는 "집계 결과 total_amount가 200 이상인 행만 통과"시킵니다. 따라서 Alice(370)와 Charlie(200)는 조건을 만족하지만 Bob(130)은 제외됩니다.
마지막으로, 최종 결과 데이터프레임에는 조건을 만족하는 그룹만 남습니다. 이는 SQL의 "SELECT customer, SUM(amount) as total_amount ...
GROUP BY customer HAVING SUM(amount) >= 200"과 정확히 동일한 로직입니다. 만약 filter를 group_by 전에 사용했다면 전혀 다른 결과가 나왔을 것입니다(개별 거래가 200 이상인 것만 필터링).
여러분이 이 코드를 사용하면 중요한 세그먼트를 정확하게 추출할 수 있습니다. "활발한 사용자", "대량 구매자", "이상 패턴을 보이는 그룹" 같은 비즈니스 로직을 명확하게 구현할 수 있습니다.
또한 여러 조건을 조합할 수도 있습니다. 예를 들어, filter((pl.col('total_amount') >= 200) & (pl.col('purchase_count') >= 3))처럼 "총액도 크고 구매 횟수도 많은 VIP 고객"을 찾을 수 있습니다.
실전 팁
💡 집계 전 필터링(WHERE)과 집계 후 필터링(HAVING)을 헷갈리지 마세요. 전자는 group_by 전에, 후자는 agg 후에 적용합니다.
💡 여러 집계 조건을 결합하려면 &(and), |(or), ~(not) 연산자를 사용하세요. 괄호로 우선순위를 명확히 하세요.
💡 성능을 위해 가능하면 집계 전 필터링을 먼저 적용하세요. 불필요한 데이터를 미리 제거하면 집계 비용이 줄어듭니다.
💡 집계 결과 컬럼명이 명확하지 않으면 filter에서 참조하기 어렵습니다. 항상 alias()로 의미 있는 이름을 부여하세요.
💡 복잡한 조건은 with_columns()로 중간 컬럼을 만든 후 filter하면 가독성이 좋아집니다.
9. 윈도우 함수 활용 - 그룹 내 순위와 누적 계산
시작하며
여러분이 각 그룹 내에서 순위를 매기거나, 누적 합계를 계산하거나, 이동 평균을 구해야 할 때가 있습니다. 예를 들어, "각 카테고리에서 매출 상위 3개 제품"이나 "월별 누적 매출"을 계산하고 싶을 때 말이죠.
이런 작업은 시계열 분석, 랭킹 시스템, 누적 통계 등에서 매우 자주 사용됩니다. 일반적인 집계로는 불가능하고, 각 그룹 내에서 행 간의 관계를 고려해야 합니다.
직접 구현하면 복잡한 반복문과 상태 관리가 필요합니다. 바로 이럴 때 필요한 것이 Polars의 윈도우 함수(window functions)입니다.
over() 표현식을 사용하면 그룹 내에서 순위, 누적, 이동 통계 같은 고급 계산을 간단하게 수행할 수 있습니다.
개요
간단히 말해서, 윈도우 함수는 각 그룹 내에서 행들 간의 관계를 고려하여 계산을 수행하되, 그룹의 행 수를 유지하는 함수입니다. 실무에서 여러분이 리더보드, 이동 평균 차트, 누적 KPI 대시보드, 전년 대비 증감 분석 등을 구현할 때 윈도우 함수가 필수적입니다.
예를 들어, "각 영업사원의 월별 매출 순위"를 계산하면 성과 평가에 활용할 수 있고, "일별 누적 신규 가입자 수"를 계산하면 성장 추이를 시각화할 수 있습니다. 기존에 group_by와 agg를 사용하면 각 그룹이 한 행으로 축약되지만, 윈도우 함수는 원본 행 수를 유지하면서 각 행에 그룹 내 계산 결과를 추가합니다.
이것이 가장 큰 차이점입니다. 윈도우 함수의 핵심 특징은 첫째, SQL의 OVER 절과 동일한 개념이라는 점입니다.
둘째, rank, row_number, cumulative_sum 같은 다양한 윈도우 전용 함수를 제공합니다. 셋째, 정렬 순서를 지정하여 순서 의존적 계산을 수행할 수 있습니다.
이러한 특징들이 복잡한 분석 쿼리를 간결하게 만들어줍니다.
코드 예제
import polars as pl
df = pl.DataFrame({
'category': ['A', 'A', 'A', 'B', 'B', 'B'],
'product': ['P1', 'P2', 'P3', 'P4', 'P5', 'P6'],
'sales': [100, 150, 120, 200, 180, 190]
})
# 카테고리별 매출 순위와 누적 합계 계산
result = df.with_columns([
# 카테고리 내 매출 순위 (내림차순)
pl.col('sales').rank(method='ordinal', descending=True)
.over('category').alias('rank_in_category'),
# 카테고리 내 누적 매출
pl.col('sales').cum_sum().over('category').alias('cumulative_sales')
]).sort(['category', 'rank_in_category'])
print(result)
설명
이것이 하는 일: 윈도우 함수는 각 그룹을 정의하고, 그 그룹 내에서 각 행이 어떤 위치에 있는지 또는 이전 행들과 어떤 관계인지를 계산합니다. 첫 번째로, .over('category') 부분은 'category' 컬럼을 기준으로 윈도우(그룹)를 정의합니다.
이는 group_by와 유사하지만, 결과적으로 행을 축약하지 않습니다. 대신 각 행이 어느 윈도우에 속하는지만 기억합니다.
카테고리 A의 세 행은 하나의 윈도우를, 카테고리 B의 세 행은 다른 윈도우를 형성합니다. 그 다음으로, rank() 함수가 각 윈도우 내에서 독립적으로 실행됩니다.
descending=True이므로 sales 값이 큰 것부터 순위 1, 2, 3이 매겨집니다. 카테고리 A에서는 P2(150)가 1위, P3(120)이 2위, P1(100)이 3위가 됩니다.
중요한 점은 카테고리 B의 순위는 완전히 별개로 계산되어, P4(200)가 B 내에서 1위가 된다는 것입니다. 카테고리를 넘어서 전체 순위가 아닙니다.
마지막으로, cum_sum()은 누적 합계를 계산합니다. 정렬 순서에 따라 이전 행들의 sales를 계속 더해갑니다.
카테고리 A의 첫 행은 100, 두 번째 행은 100+150=250, 세 번째 행은 250+120=370이 됩니다. 이는 시계열 데이터에서 "지금까지의 총계"를 추적할 때 매우 유용합니다.
여러분이 이 코드를 사용하면 원본 데이터의 모든 행을 유지하면서도 그룹별 통계를 각 행에 추가할 수 있습니다. 이는 상세 데이터와 요약 통계를 동시에 보고 싶을 때 완벽한 솔루션입니다.
예를 들어, 제품 상세 정보와 함께 "이 제품은 카테고리 내 몇 위인가"를 보여주는 리포트를 만들 수 있습니다. 또한 시각화할 때도 매우 유용하여, 개별 데이터 포인트와 누적 추세선을 함께 그릴 수 있습니다.
실전 팁
💡 정렬이 중요한 경우 sort_by 매개변수를 사용하세요. over('category', order_by='date')는 날짜 순으로 정렬된 윈도우를 만듭니다.
💡 rank 메서드에는 'ordinal', 'dense', 'min', 'max' 등이 있습니다. 동점 처리 방식이 다르므로 용도에 맞게 선택하세요.
💡 이동 평균을 계산하려면 rolling_mean(window_size=3).over()를 사용하세요. 시계열 스무딩에 유용합니다.
💡 여러 컬럼으로 윈도우를 정의하려면 over(['col1', 'col2']) 형태로 사용할 수 있습니다.
💡 성능이 중요하다면 먼저 정렬을 수행한 후 윈도우 함수를 적용하세요. 일부 윈도우 함수는 정렬된 데이터에서 더 빠릅니다.
10. first와 last - 각 그룹의 첫/마지막 값 추출
시작하며
여러분이 각 그룹에서 가장 최근 거래, 가장 오래된 기록, 첫 번째 이벤트, 마지막 상태 같은 특정 위치의 값을 추출해야 할 때가 있습니다. 예를 들어, "각 고객의 가장 최근 구매 제품"이나 "각 센서의 최신 측정값"을 알고 싶을 때 말이죠.
이런 작업은 시계열 데이터, 로그 분석, 상태 추적 등에서 매우 자주 발생합니다. 단순히 최댓값이나 최솟값이 아니라, 시간 순서나 특정 순서에서 첫 번째 또는 마지막 항목이 필요한 경우입니다.
정렬과 인덱싱을 조합하면 복잡해집니다. 바로 이럴 때 필요한 것이 Polars의 first()와 last() 집계 함수입니다.
각 그룹에서 첫 번째 또는 마지막 행의 값을 간단하게 추출할 수 있습니다.
개요
간단히 말해서, first()와 last()는 각 그룹 내에서 첫 번째 행과 마지막 행의 특정 컬럼 값을 추출하는 집계 함수입니다. 실무에서 여러분이 최신 상태 조회, 최초 접촉 시점 분석, 이벤트 시퀀스의 시작/끝 추출 등을 할 때 이 함수들이 매우 편리합니다.
예를 들어, 사용자별 최근 로그인 시간, 주문별 최종 배송 상태, 세션별 첫 방문 페이지 같은 정보를 쉽게 가져올 수 있습니다. 기존에는 sort 후 groupby를 적용하고 각 그룹의 첫 행을 가져오는 복잡한 방법을 사용해야 했습니다.
이제는 first()와 last()를 사용하면 훨씬 직관적이고 간결합니다. 이러한 함수들의 핵심 특징은 첫째, 데이터의 순서에 의존한다는 점입니다.
따라서 먼저 정렬을 수행하는 것이 중요합니다. 둘째, 여러 컬럼에 동시에 적용할 수 있습니다.
셋째, Null 값 처리 옵션이 있어 유연하게 사용할 수 있습니다. 이러한 특징들이 시계열과 순서가 있는 데이터를 다룰 때 매우 유용합니다.
코드 예제
import polars as pl
from datetime import datetime
df = pl.DataFrame({
'user': ['Alice', 'Bob', 'Alice', 'Bob', 'Alice'],
'action': ['login', 'login', 'purchase', 'logout', 'logout'],
'timestamp': [
datetime(2024, 1, 1, 10, 0),
datetime(2024, 1, 1, 11, 0),
datetime(2024, 1, 1, 12, 0),
datetime(2024, 1, 1, 13, 0),
datetime(2024, 1, 1, 14, 0)
]
})
# 사용자별 첫 액션과 마지막 액션 추출
result = df.sort('timestamp').group_by('user').agg([
pl.col('action').first().alias('first_action'), # 첫 번째 액션
pl.col('action').last().alias('last_action'), # 마지막 액션
pl.col('timestamp').first().alias('first_time'), # 최초 시간
pl.col('timestamp').last().alias('last_time') # 최종 시간
])
print(result)
설명
이것이 하는 일: first()와 last()는 각 그룹 내에서 현재 순서상 첫 번째와 마지막 위치에 있는 행의 지정된 컬럼 값을 가져옵니다. 첫 번째로, sort('timestamp')가 전체 데이터를 시간 순으로 정렬합니다.
이 단계가 매우 중요합니다. 왜냐하면 first()와 last()는 "현재 데이터프레임의 순서"를 기준으로 동작하기 때문입니다.
정렬하지 않으면 임의의 첫 행과 마지막 행을 가져오게 되어 의미 없는 결과가 나올 수 있습니다. 시간 순 정렬 후에는 각 사용자의 행들이 시간 순서대로 배열됩니다.
그 다음으로, group_by('user')가 사용자별로 그룹을 만듭니다. Alice의 세 행(login, purchase, logout)이 시간 순으로 정렬된 상태로 하나의 그룹을 이루고, Bob의 두 행(login, logout)이 다른 그룹을 이룹니다.
각 그룹 내에서 행의 순서는 이전 정렬에 의해 결정됩니다. 마지막으로, first()와 last() 함수가 각 그룹에 적용됩니다.
Alice 그룹에서 pl.col('action').first()는 'login'(첫 번째 행의 action)을, last()는 'logout'(마지막 행의 action)을 반환합니다. 마찬가지로 timestamp도 첫 번째와 마지막 값이 추출되어, Alice의 활동 기간(시작 시각과 종료 시각)을 한눈에 볼 수 있습니다.
여러분이 이 코드를 사용하면 각 엔티티의 수명 주기나 활동 범위를 쉽게 파악할 수 있습니다. "언제 시작해서 언제 끝났는가", "처음 상태와 최종 상태는 무엇인가" 같은 질문에 바로 답할 수 있습니다.
이는 사용자 행동 분석, 프로세스 모니터링, 이벤트 추적 등에서 매우 유용하며, 데이터의 시작과 끝을 명확히 정의할 수 있게 해줍니다.
실전 팁
💡 반드시 먼저 정렬을 수행하세요. 특히 시계열 데이터에서는 sort를 빼먹으면 잘못된 결과가 나옵니다.
💡 Null 값이 있을 때 주의하세요. first()는 첫 번째 값이 Null이면 Null을 반환합니다. drop_nulls=True 옵션을 고려하세요.
💡 여러 컬럼을 동시에 추출하려면 pl.all().first()를 사용할 수 있습니다. 그룹의 첫 행 전체를 가져옵니다.
💡 n번째 값을 가져오려면 .slice(n-1, 1)이나 nth(n) 함수를 사용하세요. first()는 nth(0)과 동일합니다.
💡 성능을 위해 불필요한 정렬을 피하세요. 이미 정렬된 데이터라면 sort를 생략할 수 있습니다.