🔥알림🔥
① 테디노트 유튜브 -
구경하러 가기!
② 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 |
댓글남기기