country continent ... pop gdpPercap
0 Afghanistan Asia ... 8425333 779.445314
1 Afghanistan Asia ... 9240934 820.853030
[2 rows x 6 columns]
12 그룹과 집계
데이터 분석에서 활용도가 높은 Split-Apply-Combine 방식은 Hadley Wickham이 제시한 패턴[1]으로, 복잡한 문제를 관리 가능한 조각으로 나누고, 각 조각에 독립적으로 연산을 적용한 다음, 결과를 다시 합치는 과정을 의미합니다. 이 방식은 다음과 같이 동작합니다.
Split (분할): 큰 문제를 관리 가능한 조각으로 나눕니다. 데이터를 특정 기준(그룹, 시간, 공간 등)에 따라 여러 하위 집합으로 분할합니다.
Apply (적용): 각 조각에 독립적으로 함수나 연산을 적용합니다. 각 그룹에 대해 평균, 합계, 변환 등의 작업을 수행합니다.
Combine (결합): 처리된 결과를 다시 하나의 데이터 구조로 합칩니다. 각 그룹의 결과를 적절한 형태로 결합하여 최종 결과를 생성합니다.
12.1 Split-Apply-Combine 전략
Split-Apply-Combine 전략은 Google이 대규모 데이터 처리에 사용하는 Map-Reduce 전략과 유사합니다. Map-Reduce에서 Map 단계는 Split과 Apply에 해당하고, Reduce 단계는 Combine에 해당합니다. 다만 Map-Reduce는 수백, 수천 대의 독립적인 컴퓨터에서 병렬로 작업하는 고도로 병렬화된 환경을 위해 설계되었고, 데이터 분석보다 더 넓은 범위의 데이터 처리 요구를 다룹니다.
Split-Apply-Combine 전략은 다음과 같은 가정을 기반으로 합니다.
독립성: 각 데이터 조각이 독립적으로 처리됩니다. 각 조각은 다른 조각과 겹치지 않으며, 이전 반복의 결과에 의존하지 않습니다.
일회성 처리: 각 데이터 조각은 한 번만 처리됩니다.
다음과 같은 경우에는 이 전략이 적합하지 않습니다. 이런 경우에는 전통적인 루프가 더 적합합니다. 루프는 각 데이터 조각을 순차적으로 처리하는 방식으로, 독립성과 일회성 처리를 보장하지 않습니다.
- 겹치는 데이터가 필요한 경우(이동 평균 계산)
- 이전 반복에 의존하는 경우(동적 시뮬레이션)
먼저, 개념을 살펴보기 위해서 갭마인더(Gapminder) 데이터셋을 사용하여 Split-Apply-Combine 과정을 단계별로 살펴보겠습니다.
먼저 연도(year) 열의 고윳값을 확인해봅시다.
세부 과정을 단계별로 살펴봅시다.
12.1.1 Split
Split에서는 데이터를 특정 기준에 따라 여러 하위 집합으로 나눕니다. 분할 방식은 데이터 구조의 타입에 따라 달라집니다.
데이터프레임의 경우 하나 이상의 변수(열)의 값 조합을 기준으로 분할합니다. 예를 들어 year 변수로 분할하면 각 연도별로 그룹이 생성됩니다. 여러 변수를 사용하면 변수들의 상호작용(interaction)으로 그룹이 형성됩니다. 배열(Array)의 경우 차원(dimension)을 따라 슬라이싱합니다. 2차원 배열(행렬)의 경우 행별, 열별, 또는 개별 셀별로 분할 가능합니다. 3차원 이상의 배열도 마찬가지로 차원을 따라 분할합니다. 리스트의 경우 각 요소가 자연스럽게 하나의 조각이 됩니다. 추가 분할 인수가 필요 없습니다.
아래 예제와 같이 loc 속성을 사용하여 특정 연도(1952년)의 데이터를 추출할 수 있습니다.
12.1.2 Apply
Apply 단계에서는 각 분할된 조각에 동일한 함수나 연산을 독립적으로 적용합니다. 이 단계의 핵심 특징은 다음과 같습니다. 각 그룹의 처리는 다른 그룹과 완전히 독립적입니다. 이는 병렬화가 가능함을 의미합니다. 그리고 모든 그룹에 동일한 함수가 적용됩니다. 함수는 그룹의 크기나 특성에 관계없이 동일하게 작동해야 합니다. 예를 들어, mean() 함수는 그룹의 크기나 특성에 관계없이 동일하게 작동합니다.
12.1.3 Combine
Combine 단계에서는 각 그룹에서 나온 결과를 하나의 데이터 구조로 합칩니다. 결합 방식은 출력 타입에 따라 달라집니다. 배열(Array)의 경우 분할 차원이 출력 배열의 앞쪽 차원을 형성하고, 각 결과의 차원이 뒤쪽에 추가됩니다. 데이터프레임의 경우 그룹 식별을 위한 레이블 열이 자동으로 추가됩니다. 리스트의 경우 각 결과를 리스트 요소로 저장합니다.
연도 1952년: 평균 기대수명 및 데이터 개수
year avg_lifeExp n_rows
0 1952 49.05762 142
12.1.3.1 입력과 출력 타입의 개념
Split-Apply-Combine 전략을 이해하는 데 있어 입력 타입과 출력 타입의 개념이 중요합니다. 이는 데이터를 어떻게 분할하고, 결과를 어떻게 결합할지를 결정합니다.
- 입력 타입 (Input Type): 입력 타입은 데이터를 어떻게 분할할지를 결정합니다.
- 데이터프레임 (Data Frame): 변수의 값 조합을 기준으로 분할, 예를 들어
groupby(['year', 'continent'])는 연도와 대륙의 모든 조합으로 그룹 생성 - 배열 (Array): 차원을 따라 슬라이싱하여 분할, 예를 들어 2차원 배열을 행별 또는 열별로 분할
- 리스트 (List): 각 요소가 자연스럽게 하나의 조각, 추가 분할 인수가 필요 없음
- 데이터프레임 (Data Frame): 변수의 값 조합을 기준으로 분할, 예를 들어
- 출력 타입 (Output Type): 출력 타입은 결과를 어떻게 결합할지를 결정합니다.
- 배열 (Array): 결과를 배열 형태로 결합, 분할 차원이 출력 배열의 앞쪽 차원을 형성, 각 결과의 차원이 뒤쪽에 추가됨
- 데이터프레임 (Data Frame): 결과를 데이터프레임으로 결합, 그룹 식별을 위한 레이블 열이 자동으로 추가됨,
reset_index()로 평탄화 가능 - 리스트 (List): 각 결과를 리스트 요소로 저장, 복잡한 객체(모델)를 저장하는 데 유용, 나중에 필요한 부분만 추출 가능
- 무시 (Discarded): 결과를 저장하지 않고 부수 효과(side effect)만 수행(플롯 생성, 파일 저장 등)
- 입력과 출력 타입의 조합은 다음과 같은 의미를 가집니다.
- 데이터프레임 입력에서 데이터프레임 출력: 일반적인 패턴으로, 그룹별 집계 결과를 데이터프레임으로 얻음
- 데이터프레임 입력에서 리스트 출력: 각 그룹에 복잡한 객체(모델)를 적합하고 나중에 사용
- 리스트 입력에서 데이터프레임 출력: 리스트에 저장된 결과를 데이터프레임으로 변환
이러한 타입 시스템은 데이터 구조 간의 변환을 명확하고 일관되게 만들어줍니다.
12.1.4 Split-Apply-Combine 전략 장점
Split-Apply-Combine 전략을 명시적으로 사용하는 것의 큰 이점은 인지적 부담 감소입니다.
의도 명확화: 코드가 무엇을 하려는지 명확하게 드러납니다. 루프를 사용할 때는 중요한 로직이 부수적인 코드(인덱스 관리, 결과 저장 등)에 묻히기 쉽습니다.
구조 추상화: 데이터 구조의 세부 사항(배열인지, 데이터프레임인지)보다는 연산의 본질에 집중할 수 있습니다.
재사용성: 한 번 작성한 함수를 다양한 데이터 구조에 적용할 수 있습니다.
오류 감소: 분할과 결합 로직이 자동화되어 인덱스 오류나 레이블 불일치 같은 실수를 줄일 수 있습니다.
12.2 GroupBy
pandas의 groupby() 메서드는 데이터 분석 작업에서 강력하면서도 자주 사용되는 도구 중 하나입니다. Split-Apply-Combine 패턴을 구현하는 이 메서드는 Hadley Wickham이 제시한 데이터 분석 전략을 pandas 환경에서 활용할 수 있습니다. 아래 예제와 같이 groupby() 메서드를 사용하여 연도별, 대륙별, 연도와 대륙별로 그룹화하여 평균 기대 수명을 계산할 수 있습니다.
연도별 평균 기대 수명:
year
1952 49.057620
1957 51.507401
1962 53.609249
...
1987 63.212613
1992 64.160338
1997 65.014676
Name: lifeExp, Length: 10, dtype: float64
대륙별로도 그룹화할 수 있습니다.
대륙별 평균 기대 수명:
continent
Africa 48.865330
Americas 64.658737
Asia 60.064903
Europe 71.903686
Oceania 74.326208
Name: lifeExp, dtype: float64
연도와 대륙별과 같이 여러 기준으로 그룹화할 수도 있습니다.
연도와 대륙별 평균 기대 수명 (일부):
year continent
1952 Africa 39.135500
Americas 53.279840
Asia 46.314394
...
1957 Asia 49.318544
Europe 66.703067
Oceania 70.295000
Name: lifeExp, Length: 10, dtype: float64
12.2.1 GroupBy 객체의 특성
groupby() 메서드는 DataFrame 또는 Series에 호출될 때 즉시 데이터를 분할하지 않고, 대신 특수한 GroupBy 객체를 반환합니다. 이 객체는 DataFrameGroupBy 또는 SeriesGroupBy 형태로 분류되며, 게으른 평가(lazy evaluation) 방식으로 작동합니다. 즉, 실제 계산은 집계 메서드(sum(), mean(), agg())를 호출할 때만 수행됩니다.
12
dict_keys([1952, 1957, 1962, 1967, 1972, 1977, 1982, 1987, 1992, 1997, 2002, 2007])
GroupBy 객체는 다음과 같은 정보를 내부적으로 보유합니다.
- 그룹 레이블: 분할 기준이 되는 컬럼의 고유값들
- 인덱스 매핑: 원본 DataFrame의 각 행이 어느 그룹에 속하는지를 나타내는 매핑 정보
- 데이터 참조: 원본 DataFrame의 메모리 위치 참조
이러한 구조는 메모리 효율성을 높이면서도 유연한 연산을 가능하게 합니다. 특히 groupby() 메서드의 as_index=False 매개변수는 결과를 일반 DataFrame 형태로 반환하고, observed=True 매개변수는 실제 데이터에 존재하는 카테고리만 처리하여 성능을 개선합니다.
| 매개변수 | 기본값 | 설명 |
|---|---|---|
| by | 없음 | 분할 기준 (컬럼명, 함수, Series, dict, Grouper) |
| level | None | MultiIndex의 특정 레벨로 분할 |
| as_index | True | 결과의 그룹 키를 인덱스로 사용할지 여부 |
| sort | True | 그룹 키를 정렬할지 여부 |
| group_keys | True | apply() 결과에 그룹 키를 포함할지 여부 |
| observed | True | Categorical 컬럼의 미사용 카테고리 포함 여부 |
| dropna | True | 결측값(NaN)을 그룹 키로 사용할지 여부 |
12.2.2 GroupBy 분할 방식
분할 단계에서 pandas는 단순히 데이터를 나누는 것이 아니라, 효율적인 인덱싱 구조를 구축합니다. 다양한 분할 방식이 가능합니다. 아래 예제와 같이 단일 컬럼, 다중 컬럼, 함수 기반 분할 방식이 가능합니다.
- 단일 컬럼 분할: 단일 컬럼 분할은 그룹화의 기본적인 형태로, DataFrame의 특정 컬럼을 기준으로 데이터를 여러 그룹으로 나누는 방식입니다. 예를 들어
df.groupby('year')와 같이 사용하면, 각year(연도) 값마다 별도의 그룹이 생성되어 연도별로 데이터를 나누어 분석할 수 있습니다. 이 GroupBy 객체를 활용하면 각 그룹별로 합계, 평균, 갯수 등 다양한 집계와 변환 연산을 손쉽게 적용할 수 있으며, 결과는 원본 DataFrame은 변경하지 않고 새로운 그룹 단위의 데이터로 반환됩니다. 이런 방식은 대용량 데이터에서 빠르고 효율적인 집단 분석(group-wise analysis)을 가능하게 해줍니다.
- 다중 컬럼 분할: 다중 컬럼 분할은 여러 컬럼의 값을 조합하여 각 고유 조합별로 그룹을 생성하는 방식입니다. 예를 들어
df.groupby(['year', 'continent'])와 같이 리스트 형태로 여러 컬럼명을 인자로 전달하면, pandas는year와continent의 모든 고유값 조합(2002-Asia, 2002-Europe 등)을 각각 하나의 그룹으로 나누어 처리합니다. 이 때 결과는 MultiIndex(계층적 인덱스) 구조로 반환되어, 복잡한 집계나 분석에서 세부 그룹별로 손쉽게 연산을 적용할 수 있습니다. 아래 예제에서 보는 것처럼 각 그룹의 이름은 튜플(연도, 대륙) 형태로 제공되며, 그룹화된 데이터는 반복문이나 집계 함수에서 매우 유용하게 활용할 수 있습니다.
- 함수 기반 분할: 함수 기반 분할은 데이터의 특정 조건을 기준으로 그룹을 생성하는 방식입니다. 예를 들어
df.groupby(df['pop'].apply(lambda x: 'High' if x > pop_median else 'Low'))와 같이 사용하면,pop컬럼의 값이 중간값보다 크면 ’High’로, 작으면 ’Low’로 그룹을 생성합니다. 이 때 결과는 Series 형태로 반환되어, 각 그룹별로 통계 연산을 손쉽게 적용할 수 있습니다.
| year | lifeExp | pop | gdpPercap | |
|---|---|---|---|---|
| pop | ||||
| High | 1982.586854 | 61.557398 | 5.628883e+07 | 7380.160109 |
| Low | 1976.413146 | 57.391481 | 2.913598e+06 | 7050.494053 |
- 딕셔너리/Series 매핑 분할: 딕셔너리/Series 매핑 분할은 컬럼 값을 매핑 규칙에 따라 그룹을 생성하는 방식입니다. 예를 들어
{'Asia': '그룹1', 'Europe': '그룹2', 'Africa': '그룹3', 'Americas': '그룹4', 'Oceania': '그룹5'}와 같은 딕셔너리를 사용하면,continent컬럼의 값이 ’Asia’이면 ’그룹1’로, ’Europe’이면 ’그룹2’로, ’Africa’이면 ’그룹3’로, ’Americas’이면 ’그룹4’로, ’Oceania’이면 ’그룹5’로 그룹을 생성합니다. 이 때 결과는 Series 형태로 반환되어, 각 그룹별로 통계 연산을 손쉽게 적용할 수 있습니다.
| year | lifeExp | pop | gdpPercap | |
|---|---|---|---|---|
| continent | ||||
| 그룹1 | 783882 | 23785.70168 | 30507333901 | 3.129252e+06 |
| 그룹2 | 712620 | 25885.32700 | 6181115304 | 5.209011e+06 |
| 그룹3 | 1235208 | 30491.96600 | 6187585961 | 1.368903e+06 |
| 그룹4 | 593850 | 19397.62100 | 7351438499 | 2.140833e+06 |
| 그룹5 | 47508 | 1783.82900 | 212992136 | 4.469186e+05 |
각 분할 방식은 내부적으로 정렬된 인덱스 배열을 생성하여, Apply 단계에서 빠른 접근을 가능하게 합니다.
12.2.3 연산의 독립성
Apply에서는 각 그룹이 독립적으로 처리됩니다. 이는 병렬화가 가능함을 의미합니다. 그리고 모든 그룹에 동일한 함수가 적용됩니다. 함수는 그룹의 크기나 특성에 관계없이 동일하게 작동해야 합니다.
country
Afghanistan 9632.095181
Albania 39064.399592
Algeria 53112.311678
...
Yemen, Rep. 18831.296066
Zambia 16298.392908
Zimbabwe 7630.296508
Name: gdpPercap, Length: 142, dtype: float64
Apply에서 제공되는 연산 메서드들은 다음과 같습니다.
- 집계 메서드: 집계 메서드(aggregation methods)는 그룹별로 데이터를 하나의 값으로 요약·축약하는 데 사용됩니다. 대표적으로 평균(
mean()), 합계(sum()), 표준편차(std()), 분산(var()), 최솟값(min()), 최댓값(max())을 구할 수 있으며, 그룹별 누적 합계(cumsum()), 그룹 내 항목 수(count()), 전체 행의 수(size()), 순위 계산(rank(),nlargest(),nsmallest())도 자주 활용됩니다. 이러한 메서드는groupby()결과에 바로 이어서 사용할 수 있어, 그룹별 통계량 산출, 요약 리포트 생성, 그룹 단위 비교 등에 매우 빠르고 직관적으로 사용됩니다.
- 통계:
mean(),sum(),std(),var(),min(),max() - 누적:
cumsum(),cummax(),cummin(),cumprod() - 카운팅:
count(),size() - 순위:
rank(),nlargest(),nsmallest()
- 변환 메서드: 변환 메서드는 각 그룹별로 계산된 값을 원본 데이터의 인덱스 구조를 유지한 채로 다시 할당하는 기능을 제공합니다. 대표적으로
transform()함수가 있으며, 예를 들어 그룹별 평균, 표준편차, 누적합 등 그룹 단위 통계량을 계산하되 결과를 원래의 데이터프레임 인덱스에 맞추어 재배열합니다. 이 방식은 그룹별 정규화, 표준화, 결측치 대체와 같이 원본 행 개수나 행 위치를 보존하면서 그룹별 정보를 활용해야 할 때 매우 유용합니다. 즉, 변환 메서드를 사용하면 집계처럼 데이터의 크기가 줄어들지 않고, Apply 이후 결과가 원본 데이터와 1:1로 일치하므로 추가적인 인덱스 맞춤 과정 없이 바로 비교·분석 작업을 이어갈 수 있습니다.
transform(): 그룹별 통계를 원본 인덱스에 재배치apply(): 임의의 함수 적용
- 필터 메서드: 필터 메서드는 그룹화된 데이터에서 특정 조건을 만족하는 그룹만 선별할 때 사용합니다. 대표적으로
filter()함수가 있으며, 이 메서드는 각 그룹별로 사용자가 정의한 조건(함수)을 평가하여 True를 반환하는 그룹만 결과에 남깁니다. 예를 들어, 그룹의 행 개수가 일정 기준을 넘는 경우나, 그룹의 평균값이 특정 임계치 이상인 경우 등 다양하게 활용할 수 있습니다. 필터링 과정에서 각 그룹에 대해 함수가 개별적으로 적용되며, 조건에 부합하지 않는 그룹은 제거되어 데이터 분석의 효율성과 집중도를 높여줍니다.
filter(): 조건을 만족하는 그룹만 유지
12.2.4 결합
pandas는 Combine 단계에서 결과 형태를 자동으로 감지하여 최적의 결합 방식을 선택합니다.
- 스칼라 결과 \(\to\) Series (또는 DataFrame)
GroupBy 연산에서 각 그룹에 대해 집계 함수(mean(), sum(), count() 등)를 적용하면, 각 그룹별 스칼라 값(평균값 하나, 합계 하나 등)이 반환됩니다. 이 결과는 pandas가 자동으로 GroupBy의 그룹 키를 인덱스로 하는 Series(또는 상황에 따라 DataFrame)로 결합합니다. 즉, 각 그룹마다 하나의 값이 있고, 이 값들이 자연스럽게 인덱스에 맵핑됩니다. 아래 예제를 보면 groupby('country')['gdpPercap'].mean()은 각 country별 GDP의 평균값이 Series로 반환되며, 인덱스가 country의 값을 따르게 됩니다.
country
Afghanistan 802.674598
Albania 3255.366633
Algeria 4426.025973
...
Yemen, Rep. 1569.274672
Zambia 1358.199409
Zimbabwe 635.858042
Name: gdpPercap, Length: 142, dtype: float64
이처럼 스칼라 값을 반환하는 집계 연산의 경우, 결과가 Series로 결합되어 매우 직관적으로 사용할 수 있고, 바로 시각화나 후처리로 연결하기 용이합니다. 반면 여러 컬럼에 대해 집계하거나 복수의 집계 함수를 사용하는 경우에는 DataFrame 형태로 결합됩니다.
- Series 결과 \(\to\) DataFrame (또는 Series)
각 그룹별로 Series를 반환하는 집계 연산(여러 열 또는 다중 함수 결과의 경우)에서는 pandas가 결과를 자동으로 DataFrame으로 결합합니다. 예를 들어 다음과 같은 코드를 살펴봅시다.
| lifeExp | gdpPercap | |
|---|---|---|
| country | ||
| Afghanistan | 37.478833 | 802.674598 |
| Albania | 68.432917 | 3255.366633 |
| Algeria | 59.030167 | 4426.025973 |
| ... | ... | ... |
| Yemen, Rep. | 46.780417 | 1569.274672 |
| Zambia | 45.996333 | 1358.199409 |
| Zimbabwe | 52.663167 | 635.858042 |
142 rows × 2 columns
여기서는 country별로 그룹화한 뒤, 각 그룹에서 lifeExp와 gdpPercap 두 열의 평균값을 계산합니다. 결과는 각 국가를 인덱스로, 평균값이 계산된 두 열(lifeExp, gdpPercap)을 컬럼으로 갖는 DataFrame 형태로 반환됩니다. 즉, 여러 개의 시리즈(각 열의 평균값)가 하나의 DataFrame에 옆으로 나란히 결합되어 출력됩니다. 만약 하나의 열에만 집계 함수를 적용한다면 결과는 Series로 나오지만, 여러 열 혹은 여러 함수를 사용하는 경우에는 DataFrame이 반환된다는 점에 유의해야 합니다. 이러한 동작은 pandas가 GroupBy의 출력 형태를 자동으로 감지하여, 사용자 편의에 맞게 적절하게 결합하기 때문입니다.
- DataFrame 결과 \(\to\) 계층 구조
GroupBy 연산에서 각 그룹에 대해 DataFrame 형태의 결과가 반환되는 경우, pandas는 자연스럽게 계층적 인덱스(MultiIndex)를 사용해 결과를 결합합니다. 이 패턴은 각 그룹별로 복수의 값을 갖거나, 커스텀 함수를 통해 DataFrame 형태를 반환할 때 자주 발생합니다.
예를 들어, 다음 코드는 각 국가별로 lifeExp의 평균과 gdpPercap의 표준편차를 계산하여 DataFrame으로 반환합니다.
mean std
country
Afghanistan Afghanistan 37.478833 108.202929
Albania Albania 68.432917 1192.351513
이때 반환되는 result는 첫 번째 수준 인덱스에 그룹 키(country), 두 번째 수준 인덱스에 원래의 DataFrame 인덱스(또는 그룹 내부 커스텀 인덱스)가 계층적으로 설정된 MultiIndex 구조가 됩니다. 이런 방식은 그룹별 결과를 DataFrame으로 반환해야 하거나, 복잡한 집계 또는 커스텀 결과를 각 그룹마다 다르게 관리할 때 매우 유용합니다.
MultiIndex를 통해 그룹과 그룹별 결과를 직관적으로 탐색할 수 있고, 필요에 따라 reset_index() 등을 활용해 평탄화할 수도 있습니다. 이처럼 DataFrame 결과가 계층 구조로 결합되는 것은 GroupBy 연산의 유연성과 강력함을 보여주는 대표적인 예시입니다.
12.2.4.1 agg() vs aggregate() vs 내장 함수
pandas는 유연한 집계 인터페이스를 제공합니다. agg() 메서드는 여러 집계 함수를 한 번에 적용할 수 있게 해주는 강력한 도구입니다. 이는 여러 번 groupby를 호출하는 것보다 훨씬 효율적이며, 데이터를 한 번만 순회하여 모든 집계를 수행합니다.
country
Afghanistan 9632.095181
Albania 39064.399592
Algeria 53112.311678
...
Yemen, Rep. 18831.296066
Zambia 16298.392908
Zimbabwe 7630.296508
Name: gdpPercap, Length: 142, dtype: float64
sum(), agg('sum'), agg(np.sum)은 모두 같은 결과를 반환합니다. sum()은 내장 함수이며, agg('sum')은 함수명 문자열을 전달하는 방식, agg(np.sum)은 함수 객체를 전달하는 방식입니다. 결과의 차이점은 없지만, 내장 함수를 사용하는 것이 더 빠릅니다. 하지만, 다른 형태의 연산을 적용해야 하는 경우 agg()를 사용하는 것이 좋습니다.
- 단일 열에 여러 함수 적용
하나의 컬럼에 대해 여러 통계량을 동시에 계산할 수 있습니다. 결과는 각 통계량을 컬럼으로 하는 DataFrame이 됩니다.
- 서로 다른 열에 서로 다른 함수 적용
딕셔너리를 사용하여 각 컬럼에 대해 다른 집계 함수를 지정할 수 있습니다. 이는 복잡한 요구사항을 만족하는 요약 통계를 생성할 때 매우 유용합니다.
- 커스텀 함수 사용
표준 집계 함수뿐만 아니라 사용자 정의 함수도 사용할 수 있습니다. 이는 특정 도메인에 맞는 집계를 수행할 때 유용합니다. 다만, 커스텀 함수는 내장 함수보다 느릴 수 있으므로, 가능하면 내장 함수를 사용하는 것이 좋습니다.
groupby는 단순한 집계뿐만 아니라 변환(transformation)과 필터링(filtering)도 지원합니다. 이러한 고급 패턴은 데이터 전처리와 분석에서 매우 유용합니다.
12.2.4.2 transform: 형태 보존 변환
Transform은 각 그룹에 함수를 적용한 후 결과를 원본 인덱스 형태로 반환합니다.
- 인덱스 유지: 원본 DataFrame과 연결하기 쉬움
- 체이닝 용이: 추가 연산에 바로 사용 가능
- 백프레시 자동화: 그룹의 통계를 전체에 적용 가능
12.2.4.3 filter: 조건 기반 그룹 선택
Filter는 조건을 만족하는 그룹의 모든 행을 유지합니다.
- 이진 결과: 전체 행 유지 또는 제거
- 조건 명확성: 데이터 정제에 효과적
- 성능: 조기 필터링으로 후속 연산 비용 감소
| country | continent | ... | pop | gdpPercap | |
|---|---|---|---|---|---|
| 0 | Afghanistan | Asia | ... | 8425333 | 779.445314 |
| 1 | Afghanistan | Asia | ... | 9240934 | 820.853030 |
| 2 | Afghanistan | Asia | ... | 10267083 | 853.100710 |
| ... | ... | ... | ... | ... | ... |
| 1701 | Zimbabwe | Africa | ... | 11404948 | 792.449960 |
| 1702 | Zimbabwe | Africa | ... | 11926563 | 672.038623 |
| 1703 | Zimbabwe | Africa | ... | 12311143 | 469.709298 |
1704 rows × 6 columns
12.2.4.4 apply: 임의의 함수 적용
Apply는 유연하지만 느린 옵션입니다. apply를 사용하면 임의의 함수를 행과 열에 일괄적으로 적용할 수 있으며, 그룹별로 함수를 적용할 수 있습니다.
df.groupby("country").apply(
lambda x: x.nlargest(2, "gdpPercap"),
include_groups=False,
)
def iqr(series):
q1 = series.quantile(0.25)
q3 = series.quantile(0.75)
return q3 - q1
df.groupby("country")["gdpPercap"].apply(iqr)
def complex_transform(group):
return pd.DataFrame(
{
"original": group["gdpPercap"],
"scaled": (
group["gdpPercap"]
- group["gdpPercap"].mean()
)
/ group["gdpPercap"].std(),
"rank": group["gdpPercap"].rank(),
}
)
df.groupby("country").apply(
complex_transform, include_groups=False
)| original | scaled | rank | ||
|---|---|---|---|---|
| country | ||||
| Afghanistan | 0 | 779.445314 | -0.214683 | 5.0 |
| 1 | 820.853030 | 0.168003 | 7.0 | |
| 2 | 853.100710 | 0.466033 | 10.0 | |
| ... | ... | ... | ... | ... |
| Zimbabwe | 1701 | 792.449960 | 1.171313 | 11.0 |
| 1702 | 672.038623 | 0.270632 | 6.0 | |
| 1703 | 469.709298 | -1.242798 | 2.0 |
1704 rows × 3 columns
12.2.5 MultiIndex와 GroupBy의 결합
MultiIndex를 사용하면 다차원 데이터를 효율적으로 다룰 수 있습니다.
- 계층적 조직화: 복잡한 데이터 구조 표현
- 고속 선택: 계층 구조를 활용한 빠른 부분 선택
- 자동 정렬: 계층별 자동 정렬
| lifeExp | pop | gdpPercap | ||
|---|---|---|---|---|
| year | continent | |||
| 1952 | Africa | 39.135500 | 4.570010e+06 | 1252.572466 |
| Americas | 53.279840 | 1.380610e+07 | 4079.062552 | |
| Asia | 46.314394 | 4.228356e+07 | 5195.484004 | |
| ... | ... | ... | ... | ... |
| 2007 | Asia | 70.728485 | 1.155138e+08 | 12473.026870 |
| Europe | 77.648600 | 1.953662e+07 | 25054.481636 | |
| Oceania | 80.719500 | 1.227497e+07 | 29810.188275 |
60 rows × 3 columns
level=0은 첫 번째 레벨(year)을 의미하며, level=['year', 'continent']는 두 번째 레벨(year)과 세 번째 레벨(continent)을 의미합니다.
12.2.6 순위 및 상위 N개 함수
각 그룹 내에서 특정 항목을 선택하는 기능을 제공합니다. nlargest(), nsmallest(), idxmax(), rank() 등의 함수를 사용할 수 있습니다. nlargest()는 상위 N개를 선택하고, nsmallest()는 하위 N개를 선택합니다. idxmax()는 최대값의 인덱스를 반환하고, rank()는 순위를 반환합니다. rank(pct=True)는 백분위 순위를 반환합니다.
df.groupby("continent")["gdpPercap"].nlargest(3)
df.groupby("continent")["gdpPercap"].nsmallest(2)
idx_max = df.groupby("continent")["gdpPercap"].idxmax()
df.loc[df.groupby("continent")["gdpPercap"].idxmax()]
df.groupby("continent")["gdpPercap"].rank(ascending=False)
df.groupby("continent")["gdpPercap"].rank(pct=True)0 0.164141
1 0.186869
2 0.202020
...
1701 0.269231
1702 0.189103
1703 0.060897
Name: gdpPercap, Length: 1704, dtype: float64
12.2.7 성능 최적화 전략
12.2.7.1 Categorical 데이터 타입 활용
Categorical은 반복되는 문자열 값에 대해 메모리 효율성과 속도를 모두 개선합니다.
- 메모리: 고유값 저장소만 유지하고 인덱스 참조
- 속도: 정수 기반 비교로 문자열 비교보다 빠름
- 정렬: 카테고리 순서 유지 가능
12.2.7.2 내장 함수 우선 사용
일반적으로 내장 함수를 사용하는 것이 더 빠릅니다. apply는 느리지만, 복잡한 로직을 적용할 때 유용합니다.
| sum | mean | std | |
|---|---|---|---|
| continent | |||
| Africa | 1.368903e+06 | 2193.754578 | 2827.929863 |
| Americas | 2.140833e+06 | 7136.110356 | 6396.764112 |
| Asia | 3.129252e+06 | 7902.150428 | 14045.373112 |
| Europe | 5.209011e+06 | 14469.475533 | 9355.213498 |
| Oceania | 4.469186e+05 | 18621.609223 | 6358.983321 |
12.2.7.3 조기 필터링
filter는 조건을 만족하는 그룹의 모든 행을 유지하고, 만족하지 않는 그룹은 통째로 제거합니다. 이는 데이터 정제에 효과적입니다. 행을 걸러내는 일은 filter가 담당하고, transform은 걸러진 결과에 대해 그룹별 통계를 원래 행 수 그대로 되돌려주는 역할을 합니다. 아래처럼 둘을 이어 쓰면 “관측치가 충분한 대륙만 남긴 뒤, 각 행이 자기 대륙 평균에서 얼마나 떨어져 있는지”를 계산할 수 있습니다.
0 -7122.705114
1 -7081.297398
2 -7049.049718
3 -7065.953290
4 -7162.169322
Name: gdpPercap, dtype: float64
이때 그룹 키로는 범주형 성격의 컬럼을 써야 합니다. gdpPercap 같은 연속형 실수를 그룹 키로 주면 값이 거의 모두 달라 그룹 하나에 행이 하나씩만 들어가고, 그 결과 x - x.mean()이 전부 0이 되어 아무 의미 없는 출력이 나옵니다. 연속형 값을 기준으로 묶고 싶다면 pd.cut()이나 pd.qcut()으로 구간을 만든 뒤 그 구간을 키로 사용하세요.
12.2.7.4 데이터 정렬과 NumPy 활용
대규모 데이터의 경우 NumPy와 조합하면 성능이 향상됩니다.
import numpy as np
result_pandas = df.groupby("continent")["gdpPercap"].agg(
["sum", "mean"]
)
df_sorted = df.sort_values("continent")
ids = df_sorted["continent"].values
values = df_sorted["gdpPercap"].values
unique_ids, indices = np.unique(ids, return_index=True)
splits = np.split(values, indices[1:])
result_numpy = pd.DataFrame(
{
"sum": [s.sum() for s in splits],
"mean": [s.mean() for s in splits],
},
index=unique_ids,
)12.2.7.5 중복 제거 전략
대규모 데이터의 경우 중복을 제거하는 것이 성능을 향상시킵니다.
pandas GroupBy는 단순한 집계 도구를 넘어 데이터 분석의 패러다임을 제시합니다. 효과적인 사용을 위한 핵심 원칙은 다음과 같습니다.
- 의도 명확화: 분할 기준, 연산, 결합 형태를 명확히 구분
- 성능 의식: 대규모 데이터에는 Categorical과 내장 함수 우선 사용
- 타입 이해: 입력 타입과 출력 타입의 관계 명확히 파악
- 조기 필터링: 불필요한 데이터를 조기에 제거
- 도구 선택: 목적에 맞는 메서드 선택 (agg vs transform vs filter)
GroupBy를 마스터하면 복잡한 데이터 조작도 간결하고 읽기 쉬운 코드로 표현할 수 있으며, 이는 데이터 분석 작업의 생산성과 유지보수성을 크게 향상시킵니다.