🔥알림🔥
① 테디노트 유튜브 -
구경하러 가기!
② LangChain 한국어 튜토리얼
바로가기 👀
③ 랭체인 노트 무료 전자책(wikidocs)
바로가기 🙌
④ RAG 비법노트 LangChain 강의오픈
바로가기 🙌
⑤ 서울대 PyTorch 딥러닝 강의
바로가기 🙌
판다스(Pandas) .groupby()로 할 수 있는 거의 모든 것! (통계량, 전처리)
판다스(Pandas)의 .groupby() 기능은 데이터를 그룹별로 분할하여 독립된 그룹에 대하여 별도로 데이터를 처리(혹은 적용)하거나 그룹별 통계량을 확인하고자 할 때 유용한 함수 입니다.
.groupby()의 동작 원리는 아래 그림과 같습니다.
이미지 출처: www.w3resource.com
-
Split 단계: 위에 이미지에서 Split 단계에서
.groupby()에서 정의한 컬럼 조건에 따라 독립된 그룹으로 나누어 줍니다. 예시에서는ID값을 기준으로 그룹을 나누었는데, 3개의 sub-group으로 분할된 모습입니다. -
Apply 단계: 나뉘어진 독립된 그룹별 함수를 적용하는 단계 입니다. 예시에서는 합계(sum)함수를 적용하여 각 그룹별 총계가 합산된 결과를 확인할 수 있습니다.
-
Combine 단계: 최종 단계이며, 각각의 독립된 그룹별로 함수가 적용된 결과를 종합하여 다시 하나의 테이블로 합칩니다.
판다스(Pandas)의 .groupby() 메서드는 앞서 언급한 바와 같이 데이터를 특정 기준으로 그룹화하여 처리할 수 있는 기능 덕분에, 데이터 전처리/분석 시 유용하게 활용할 수 있습니다.
아래는 다양한 활용 사례에 대하여 소개해 드리고자 합니다.
# 모듈 import
import pandas as pd
import seaborn as sns
# 샘플 데이터 로드
df = sns.load_dataset('tips')
df
| total_bill | tip | sex | smoker | day | time | size | |
|---|---|---|---|---|---|---|---|
| 0 | 16.99 | 1.01 | Female | No | Sun | Dinner | 2 |
| 1 | 10.34 | 1.66 | Male | No | Sun | Dinner | 3 |
| 2 | 21.01 | 3.50 | Male | No | Sun | Dinner | 3 |
| 3 | 23.68 | 3.31 | Male | No | Sun | Dinner | 2 |
| 4 | 24.59 | 3.61 | Female | No | Sun | Dinner | 4 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 239 | 29.03 | 5.92 | Male | No | Sat | Dinner | 3 |
| 240 | 27.18 | 2.00 | Female | Yes | Sat | Dinner | 2 |
| 241 | 22.67 | 2.00 | Male | Yes | Sat | Dinner | 2 |
| 242 | 17.82 | 1.75 | Male | No | Sat | Dinner | 2 |
| 243 | 18.78 | 3.00 | Female | No | Thur | Dinner | 2 |
244 rows × 7 columns
그룹별 통계량 확인
-
데이터 프레임에
.groupby(컬럼)+ 통계함수로 그룹별 통계량을 확인할 수 있습니다. -
통계 결과는 통계계산이 가능한 수치형(numerical) 컬럼에 대해서만 산출합니다.
# 평균(mean)
df.groupby('sex').mean()
| total_bill | tip | size | |
|---|---|---|---|
| sex | |||
| Male | 20.744076 | 3.089618 | 2.630573 |
| Female | 18.056897 | 2.833448 | 2.459770 |
# 분산(var)
df.groupby('sex').var()
| total_bill | tip | size | |
|---|---|---|---|
| sex | |||
| Male | 85.497185 | 2.217424 | 0.913931 |
| Female | 64.147429 | 1.344428 | 0.879177 |
.agg()에 적용할 수 있는 통계함수 문자열 표
| 함수 | 내용 |
|---|---|
| count | 데이터의 개수 |
| sum | 합계 |
| mean | 평균 |
| median | 중앙값 |
| var, std | 분산, 표준편차 |
| min, max | 최소, 최대값 |
| unique, nunique | 고유값, 고유값 개수 |
| prod | 곲 |
| first, last | 첫째, 마지막값 |
.agg()를 활용하여 다중 통계량을 구할 수 있습니다.
# 다중 통계 적용
df.groupby('sex').agg(['mean', 'var'])
| total_bill | tip | size | ||||
|---|---|---|---|---|---|---|
| mean | var | mean | var | mean | var | |
| sex | ||||||
| Male | 20.744076 | 85.497185 | 3.089618 | 2.217424 | 2.630573 | 0.913931 |
| Female | 18.056897 | 64.147429 | 2.833448 | 1.344428 | 2.459770 | 0.879177 |
.agg()에서 문자열로 지정할 수 있는 함수 목록은 다음과 같습니다.
.agg()로 다중 통계량을 구할 때 컬럼별 적용할 통계 함수를 다르게 적용할 수 있습니다.
# 컬럼별 다른 통계량 산출
df.groupby('sex').agg({'total_bill': 'mean',
'tip': ['sum', 'var'],
'size': 'median'
})
| total_bill | tip | size | ||
|---|---|---|---|---|
| mean | sum | var | median | |
| sex | ||||
| Male | 20.744076 | 485.07 | 2.217424 | 2.0 |
| Female | 18.056897 | 246.51 | 1.344428 | 2.0 |
바로 이전 출력 결과에서는 MultiIndex로 된 컬럼 형태로 출력이 되는데, 이를 평탄화(Flatten) 하여 보기 좋게 만들 수 있습니다.
# MultiIndex 컬럼을 평탄화 하는 함수
def flat_cols(df):
df.columns = [' / '.join(x) for x in df.columns.to_flat_index()]
return df
# 컬럼별 다른 통계량 산출
df.groupby('sex').agg({'total_bill': 'mean',
'tip': ['sum', 'var'],
'size': 'median'
}).pipe(flatten_cols)
| total_bill / mean | tip / sum | tip / var | size / median | |
|---|---|---|---|---|
| sex | ||||
| Male | 20.744076 | 485.07 | 2.217424 | 2.0 |
| Female | 18.056897 | 246.51 | 1.344428 | 2.0 |
만약 위의 결과 테이블에서 출력결과의 소수점 자릿수를 지정하고 싶다면, 끝에 .round(소수점 자릿수)를 추가할 수 있습니다.
# .round(2): 소수점 둘째자리까지 반올림하여 결과 출력
df.groupby('sex').agg({'total_bill': 'mean',
'tip': ['sum', 'var'],
'size': 'median'
}).pipe(flatten_cols).round(2)
| total_bill / mean | tip / sum | tip / var | size / median | |
|---|---|---|---|---|
| sex | ||||
| Male | 20.74 | 485.07 | 2.22 | 2.0 |
| Female | 18.06 | 246.51 | 1.34 | 2.0 |
.agg()함수에 사용자 정의 함수(Custom Function)를 적용할 수 있습니다.
- 단, 사용자 정의 함수는 합산(aggregated)된 결과를 반환해야 합니다.
df.groupby('sex')[['total_bill', 'tip']].agg(lambda x: x.mean() / x.std())
| total_bill | tip | |
|---|---|---|
| sex | ||
| Male | 2.243459 | 2.074820 |
| Female | 2.254517 | 2.443693 |
.agg() 적용한 결과에 대하여 reset_index()를 적용하여 왼쪽 Index를 초기화할 수 있습니다.
# 인덱스 초기화 전
display(df.groupby('sex').mean())
# 인덱스 초기화 후
df.groupby('sex').mean().reset_index()
| total_bill | tip | size | |
|---|---|---|---|
| sex | |||
| Male | 20.744076 | 3.089618 | 2.630573 |
| Female | 18.056897 | 2.833448 | 2.459770 |
| sex | total_bill | tip | size | |
|---|---|---|---|---|
| 0 | Male | 20.744076 | 3.089618 | 2.630573 |
| 1 | Female | 18.056897 | 2.833448 | 2.459770 |
분리된 group 순회
groupby를 활용한 순회시 첫번째 인자는 key값을, 두번재 인자는 group을 반환합니다.
# sex, smoker 기준으로 그룹한 후 순회하며 출력
for (k1, k2), group in df.groupby(['sex', 'smoker']):
print((k1, k2))
# 데이터프레임 출력
display(group.head())
('Male', 'Yes')
| total_bill | tip | sex | smoker | day | time | size | |
|---|---|---|---|---|---|---|---|
| 56 | 38.01 | 3.00 | Male | Yes | Sat | Dinner | 4 |
| 58 | 11.24 | 1.76 | Male | Yes | Sat | Dinner | 2 |
| 60 | 20.29 | 3.21 | Male | Yes | Sat | Dinner | 2 |
| 61 | 13.81 | 2.00 | Male | Yes | Sat | Dinner | 2 |
| 62 | 11.02 | 1.98 | Male | Yes | Sat | Dinner | 2 |
('Male', 'No')
| total_bill | tip | sex | smoker | day | time | size | |
|---|---|---|---|---|---|---|---|
| 1 | 10.34 | 1.66 | Male | No | Sun | Dinner | 3 |
| 2 | 21.01 | 3.50 | Male | No | Sun | Dinner | 3 |
| 3 | 23.68 | 3.31 | Male | No | Sun | Dinner | 2 |
| 5 | 25.29 | 4.71 | Male | No | Sun | Dinner | 4 |
| 6 | 8.77 | 2.00 | Male | No | Sun | Dinner | 2 |
('Female', 'Yes')
| total_bill | tip | sex | smoker | day | time | size | |
|---|---|---|---|---|---|---|---|
| 67 | 3.07 | 1.00 | Female | Yes | Sat | Dinner | 1 |
| 72 | 26.86 | 3.14 | Female | Yes | Sat | Dinner | 2 |
| 73 | 25.28 | 5.00 | Female | Yes | Sat | Dinner | 2 |
| 92 | 5.75 | 1.00 | Female | Yes | Fri | Dinner | 2 |
| 93 | 16.32 | 4.30 | Female | Yes | Fri | Dinner | 2 |
('Female', 'No')
| total_bill | tip | sex | smoker | day | time | size | |
|---|---|---|---|---|---|---|---|
| 0 | 16.99 | 1.01 | Female | No | Sun | Dinner | 2 |
| 4 | 24.59 | 3.61 | Female | No | Sun | Dinner | 4 |
| 11 | 35.26 | 5.00 | Female | No | Sun | Dinner | 4 |
| 14 | 14.83 | 3.02 | Female | No | Sun | Dinner | 2 |
| 16 | 10.33 | 1.67 | Female | No | Sun | Dinner | 3 |
다음과 같이 dict로 변환한 후 키 값으로 조회하여 그룹별 데이터프레임(DataFrame)을 조회할 수 있습니다.
# 그룹별 데이터프레임을 생성 후 dict에 저장
output = dict(list(df.groupby(['sex', 'smoker'])))
output.keys()
dict_keys([('Male', 'Yes'), ('Male', 'No'), ('Female', 'Yes'), ('Female', 'No')])
# sex == Male & smoker == Yes 인 그룹 조회
output[('Male', 'Yes')].head()
| total_bill | tip | sex | smoker | day | time | size | |
|---|---|---|---|---|---|---|---|
| 56 | 38.01 | 3.00 | Male | Yes | Sat | Dinner | 4 |
| 58 | 11.24 | 1.76 | Male | Yes | Sat | Dinner | 2 |
| 60 | 20.29 | 3.21 | Male | Yes | Sat | Dinner | 2 |
| 61 | 13.81 | 2.00 | Male | Yes | Sat | Dinner | 2 |
| 62 | 11.02 | 1.98 | Male | Yes | Sat | Dinner | 2 |
apply()로 그룹별 데이터 전처리
# 샘플 데이터셋 로드
df = sns.load_dataset('titanic')
df.head()
| survived | pclass | sex | age | sibsp | parch | fare | embarked | class | who | adult_male | deck | embark_town | alive | alone | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 3 | male | 22.0 | 1 | 0 | 7.2500 | S | Third | man | True | NaN | Southampton | no | False |
| 1 | 1 | 1 | female | 38.0 | 1 | 0 | 71.2833 | C | First | woman | False | C | Cherbourg | yes | False |
| 2 | 1 | 3 | female | 26.0 | 0 | 0 | 7.9250 | S | Third | woman | False | NaN | Southampton | yes | True |
| 3 | 1 | 1 | female | 35.0 | 1 | 0 | 53.1000 | S | First | woman | False | C | Southampton | yes | False |
| 4 | 0 | 3 | male | 35.0 | 0 | 0 | 8.0500 | S | Third | man | True | NaN | Southampton | no | True |
타아타닉(titanic) 데이터셋을 로드하였습니다.
현재 age 컬럼에는 총 177건의 결측 데이터가 존재합니다.
# age 컬럼의 결측치 조회
df['age'].isnull().sum()
177
나이 컬럼에 대한 결측치를 단순 통계량이나 임의의 값으로 채울 수 있지만, .groupby()를 활용하여 그룹별 통계량으로 채울 수 있습니다.
아래의 예시는 먼저 .groupby('sex')로 성별 그룹으로 나눈 뒤, 나이 컬럼에 대하여 각 그룹의 나이 평균으로 결측치를 채웁니다.
즉, 남자는 남자 나이의 평균값으로 / 여자는 여자 나이의 평균값으로 결측치가 채워집니다.
# 성별 나이의 평균으로 각각 성별 그룹의 나이의 평균으로 값을 채웁니다.
df.groupby('sex')['age'].apply(lambda x: x.fillna(x.mean()))
0 22.000000
1 38.000000
2 26.000000
3 35.000000
4 35.000000
...
886 27.000000
887 19.000000
888 27.915709
889 26.000000
890 32.000000
Name: age, Length: 891, dtype: float64
.apply() 함수에 사용자 정의 함수(Custom Function)를 적용하는 것도 가능합니다.
아래 예시는 상위 5개의 행을 출력하는 함수를 적용한 예시입니다.
def get_top5(x):
# 나이를 기준으로 정렬하여 상위 5개 행을 반환
return x.sort_values('age').head()
df.groupby('sex').apply(get_top5)
| survived | pclass | sex | age | sibsp | parch | fare | embarked | class | who | adult_male | deck | embark_town | alive | alone | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| sex | ||||||||||||||||
| female | 469 | 1 | 3 | female | 0.75 | 2 | 1 | 19.2583 | C | Third | child | False | NaN | Cherbourg | yes | False |
| 644 | 1 | 3 | female | 0.75 | 2 | 1 | 19.2583 | C | Third | child | False | NaN | Cherbourg | yes | False | |
| 381 | 1 | 3 | female | 1.00 | 0 | 2 | 15.7417 | C | Third | child | False | NaN | Cherbourg | yes | False | |
| 172 | 1 | 3 | female | 1.00 | 1 | 1 | 11.1333 | S | Third | child | False | NaN | Southampton | yes | False | |
| 642 | 0 | 3 | female | 2.00 | 3 | 2 | 27.9000 | S | Third | child | False | NaN | Southampton | no | False | |
| male | 803 | 1 | 3 | male | 0.42 | 0 | 1 | 8.5167 | C | Third | child | False | NaN | Cherbourg | yes | False |
| 755 | 1 | 2 | male | 0.67 | 1 | 1 | 14.5000 | S | Second | child | False | NaN | Southampton | yes | False | |
| 831 | 1 | 2 | male | 0.83 | 1 | 1 | 18.7500 | S | Second | child | False | NaN | Southampton | yes | False | |
| 78 | 1 | 2 | male | 0.83 | 0 | 2 | 29.0000 | S | Second | child | False | NaN | Southampton | yes | False | |
| 305 | 1 | 1 | male | 0.92 | 1 | 2 | 151.5500 | S | First | child | False | C | Southampton | yes | False |
댓글남기기