Pandas: 복잡한 집계를 위한 고급 GroupBy 기술


작성자별 이미지
# 소개
하는 동안 groupby().sum() 그리고 groupby().mean() 빠른 확인에는 적합하지만 생산 수준 측정에는 보다 강력한 솔루션이 필요합니다. 실제 테이블에는 여러 키, 시계열 데이터, 가중치 및 판촉, 반품 또는 이상값과 같은 다양한 조건이 포함되는 경우가 많습니다.
즉, 총계와 요율을 계산하고, 각 세그먼트 내 항목의 순위를 매기고, 달력 버킷별로 데이터를 롤업한 다음, 모델링을 위해 그룹 통계를 원래 행에 다시 병합해야 하는 경우가 자주 발생합니다. 이 문서에서는 이러한 복잡한 시나리오를 효과적으로 처리하기 위해 Pandas 라이브러리를 사용하는 고급 그룹화 기술을 안내합니다.
# 올바른 모드 선택
// agg를 사용하여 그룹을 한 행으로 줄이기
사용 agg 총계, 평균, 중앙값, 최소/최대 값, 사용자 정의 벡터화된 감소 등 그룹당 하나의 레코드를 원할 때.
out = (
df.groupby(['store', 'cat'], as_index=False, sort=False)
.agg(sales=('rev', 'sum'),
orders=('order_id', 'nunique'),
avg_price=('price', 'mean'))
)
이는 KPI(핵심 성과 지표) 테이블, 주간 롤업 및 다중 측정항목 요약에 적합합니다.
// 변환을 사용하여 통계를 행으로 다시 브로드캐스트
그만큼 transform 메서드는 입력과 동일한 형태의 결과를 반환합니다. Z 점수, 그룹 내 공유, 그룹별 채우기 등 각 행에 필요한 기능을 만드는 데 이상적입니다.
g = df.groupby('store')['rev']
df['rev_z'] = (df['rev'] - g.transform('mean')) / g.transform('std')
df['rev_share'] = df['rev'] / g.transform('sum')
이는 모델링 기능, 품질 보증 비율 및 전가에 좋습니다.
// 사용자 정의 그룹별 논리에 적용 사용
사용 apply 내장된 함수로 필요한 로직을 표현할 수 없는 경우에만 가능합니다. 최적화하기가 더 느리고 어려우므로 시도해야 합니다. agg 또는 transform 첫 번째.
def capped_mean(s):
q1, q3 = s.quantile([.25, .75])
return s.clip(q1, q3).mean()
df.groupby('store')['rev'].apply(capped_mean)
이는 맞춤형 규칙과 소규모 그룹에 적합합니다.
// 필터를 사용하여 전체 그룹 유지 또는 삭제
그만큼 filter 방법을 사용하면 전체 그룹이 조건을 통과하거나 실패할 수 있습니다. 이는 데이터 품질 규칙 및 임계값 설정에 유용합니다.
big = df.groupby('store').filter(lambda g: g['order_id'].nunique() >= 100)
이는 최소 크기 코호트 및 집계 전에 희소 범주를 제거하는 데 적합합니다.
# 다중 키 그룹화 및 명명된 집계
// 여러 키로 그룹화
결과를 비즈니스 인텔리전스 도구에 바로 넣을 수 있도록 출력 모양과 순서를 제어할 수 있습니다.
g = df.groupby(['store', 'cat'], as_index=False, sort=False, observed=True)
as_index=False결합 및 내보내기가 더 쉬운 플랫 DataFrame을 반환합니다.sort=False그룹 재정렬을 방지하여 순서가 관련이 없을 때 작업을 절약합니다.observed=True(범주형 열 포함) 사용되지 않은 범주 쌍을 삭제합니다.
// 명명된 집계 사용
명명된 집계는 읽기 가능한 SQL과 유사한 열 이름을 생성합니다.
out = (
df.groupby(['store', 'cat'])
.agg(sales=('rev', 'sum'),
orders=('order_id', 'nunique'), # use your id column here
avg_price=('price', 'mean'))
)
// 기둥 정리
여러 집계를 쌓으면 MultiIndex. 한 번 평면화하고 열 순서를 표준화하세요.
out = out.reset_index()
out.columns = [
'_'.join(c) if isinstance(c, tuple) else c
for c in out.columns
]
# optional: ensure business-friendly column order
cols = ['store', 'cat', 'orders', 'sales', 'avg_price']
out = out[cols]
# 적용하지 않은 조건부 집계
// agg 내부에서 부울 마스크 수학 사용
마스크가 다른 열에 의존하는 경우 해당 인덱스를 기준으로 데이터를 정렬합니다.
# promo sales and promo rate by (store, cat)
cond = df['is_promo']
out = df.groupby(['store', 'cat']).agg(
promo_sales=('rev', lambda s: s[cond.loc[s.index]].sum()),
promo_rate=('is_promo', 'mean') # proportion of promo rows
)
// 비율 및 비율 계산
요금은 단순히 sum(mask) / size이는 부울 열의 평균과 동일합니다.
df['is_return'] = df['status'].eq('returned')
rates = df.groupby('store').agg(return_rate=('is_return', 'mean'))
// 코호트 스타일 창 만들기
먼저 날짜 범위가 포함된 마스크를 미리 계산한 다음 데이터를 집계합니다.
# example: repeat purchase within 30 days of first purchase per customer cohort
first_ts = df.groupby('customer_id')['ts'].transform('min')
within_30 = (df['ts'] first_ts)
# customer cohort = month of first purchase
df['cohort'] = first_ts.dt.to_period('M').astype(str)
repeat_30_rate = (
df.groupby('cohort')
.agg(repeat_30_rate=('within_30', 'mean'))
.rename_axis(None)
)
# 그룹당 가중 측정항목
// 가중 평균 패턴 구현
수학을 벡터화하고 무중력 분할을 방지합니다.
import numpy as np
tmp = df.assign(wx=df['price'] * df['qty'])
agg = tmp.groupby(['store', 'cat']).agg(wx=('wx', 'sum'), w=('qty', 'sum'))
# weighted average price per (store, cat)
agg['wavg_price'] = np.where(agg['w'] > 0, agg['wx'] / agg['w'], np.nan)
// NaN 값을 안전하게 처리하기
빈 그룹 또는 모두에 대해 무엇을 반환할지 결정합니다.NaN 가치. 두 가지 일반적인 선택은 다음과 같습니다.
# 1) Return NaN (transparent, safest for downstream stats)
agg['wavg_price'] = np.where(agg['w'] > 0, agg['wx'] / agg['w'], np.nan)
# 2) Fallback to unweighted mean if all weights are zero (explicit policy)
mean_price = df.groupby(['store', 'cat'])['price'].mean()
agg['wavg_price_safe'] = np.where(
agg['w'] > 0, agg['wx'] / agg['w'], mean_price.reindex(agg.index).to_numpy()
)
# 시간 인식 그룹화
// 빈도와 함께 pd.Grouper 사용
시계열 데이터를 특정 간격으로 그룹화하여 KPI에 대한 달력 경계를 존중합니다.
weekly = df.groupby(['store', pd.Grouper(key='ts', freq='W')], observed=True).agg(
sales=('rev', 'sum'), orders=('order_id', 'nunique')
)
// 그룹별 롤링/확장 창 적용
항상 데이터를 먼저 정렬하고 타임스탬프 열을 기준으로 정렬하세요.
df = df.sort_values(['customer_id', 'ts'])
df['rev_30d_mean'] = (
df.groupby('customer_id')
.rolling('30D', on='ts')['rev'].mean()
.reset_index(level=0, drop=True)
)
// 데이터 유출 방지
시간순으로 유지하고 창에서 과거 데이터만 “볼” 수 있도록 합니다. 시계열 데이터를 섞지 말고, 학습 및 테스트를 위해 분할하기 전에 전체 데이터세트에 대한 그룹 통계를 계산하지 마세요.
# 그룹 내 순위 및 상위 N개
// 그룹당 상위 k 행 찾기
다음은 각 그룹에서 상위 N개 행을 선택하기 위한 두 가지 실용적인 옵션입니다.
# Sort + head
top3 = (df.sort_values(['cat', 'rev'], ascending=[True, False])
.groupby('cat')
.head(3))
# Per-group nlargest on one metric
top3_alt = (df.groupby('cat', group_keys=False)
.apply(lambda g: g.nlargest(3, 'rev')))
// 도우미 기능 사용
Pandas는 순위 지정 및 선택을 위한 여러 도우미 기능을 제공합니다.
계급 — 연결 처리 방법을 제어합니다(예: method='dense' 또는 'first') 다음을 사용하여 백분위수 순위를 계산할 수 있습니다. pct=True.
df['rev_rank_in_cat'] = df.groupby('cat')['rev'].rank(method='dense', ascending=False)
누적 — 그룹 내 각 행의 0 기반 위치를 제공합니다.
df['pos_in_store'] = df.groupby('store').cumcount()
n번째 — 전체 DataFrame을 정렬하지 않고 그룹당 k번째 행을 선택합니다.
second_row = df.groupby('store').nth(1) # the second row present per store
# 변환을 통한 방송 기능
// 그룹별 정규화 수행
여러 그룹에서 행을 비교할 수 있도록 각 그룹 내의 측정항목을 표준화합니다.
g = df.groupby('store')['rev']
df['rev_z'] = (df['rev'] - g.transform('mean')) / g.transform('std')
// 누락된 값 대치
그룹 통계로 누락된 값을 채웁니다. 이는 전역 채우기 값을 사용하는 것보다 분포를 현실에 더 가깝게 유지하는 경우가 많습니다.
df['price'] = df['price'].fillna(df.groupby('cat')['price'].transform('median'))
// 그룹 공유 기능 생성
보다 명확한 비교를 위해 원시 숫자를 그룹 내 비율로 변환합니다.
df['rev_share_in_store'] = df['rev'] / df.groupby('store')['rev'].transform('sum')
# 범주, 빈 그룹 및 누락된 데이터 처리
// 범주형 유형으로 속도 향상
키가 고정 세트(예: 상점, 지역, 제품 카테고리)에서 나온 경우 범주형 유형으로 한 번 캐스팅하세요. 이것은 GroupBy 작업 속도가 빨라지고 메모리 효율성이 향상됩니다.
from pandas.api.types import CategoricalDtype
store_type = CategoricalDtype(categories=sorted(df['store'].dropna().unique()), ordered=False)
df['store'] = df['store'].astype(store_type)
cat_type = CategoricalDtype(categories=['Grocery', 'Electronics', 'Home', 'Clothing', 'Sports'])
df['cat'] = df['cat'].astype(cat_type)
// 사용하지 않는 조합 삭제
범주형 열을 그룹화할 때 설정 observed=True 데이터에 실제로 발생하지 않는 범주 쌍을 제외하여 노이즈가 적고 더 깨끗한 출력을 생성합니다.
out = df.groupby(['store', 'cat'], observed=True).size().reset_index(name="n")
// NaN 키를 사용한 그룹화
누락된 키를 처리하는 방법을 명시적으로 설명하세요. 기본적으로 Pandas는 NaN 여러 떼; 품질 보증 프로세스에 도움이 되는 경우에만 보관하세요.
# Default: NaN keys are dropped
by_default = df.groupby('region').size()
# Keep NaN as its own group when you need to audit missing keys
kept = df.groupby('region', dropna=False).size()
# 빠른 치트시트
// 그룹당 조건부 요율 계산
# mean of a boolean is a rate
df.groupby(keys).agg(rate=('flag', 'mean'))
# or explicitly: sum(mask)/size
df.groupby(keys).agg(rate=('flag', lambda s: s.sum() / s.size))
// 가중 평균 계산
df.assign(wx=df[x] * df[w])
.groupby(keys)
.apply(lambda g: g['wx'].sum() / g[w].sum() if g[w].sum() else np.nan)
.rename('wavg')
// 그룹별 Top-k 찾기
(df.sort_values([key, metric], ascending=[True, False])
.groupby(key)
.head(k))
# or
df.groupby(key, group_keys=False).apply(lambda g: g.nlargest(k, metric))
// 주간 측정항목 계산
df.groupby([key, pd.Grouper(key='ts', freq='W')], observed=True).agg(...)
// 그룹별 채우기 수행
df[col] = df[col].fillna(df.groupby(keys)[col].transform('median'))
// 그룹 내 지분 계산
df['share'] = df[val] / df.groupby(keys)[val].transform('sum')
# 마무리
먼저 작업에 적합한 모드를 선택하십시오. agg 감소시키다, transform 방송, 예약 apply 벡터화가 옵션이 아닌 경우. 기대다 pd.Grouper 상위 N개 선택을 위한 시간 기반 버킷 및 순위 도우미용입니다. 명확하고 벡터화된 패턴을 선호하면 출력을 플랫하고 이름이 지정되고 테스트하기 쉽게 유지할 수 있으므로 측정항목이 정확하게 유지되고 노트북이 빠르게 실행됩니다.
조셉 페레르 바르셀로나 출신의 분석 엔지니어입니다. 물리학공학과를 졸업하고 현재 인간 이동성에 적용되는 데이터 과학 분야에서 일하고 있다. 그는 데이터 과학 및 기술에 중점을 둔 시간제 콘텐츠 제작자입니다. Josep은 현장에서 진행 중인 폭발적인 적용을 다루면서 AI에 관한 모든 것에 대해 글을 씁니다.



Post Comment