Post

[Pandas] 데이터 그룹 집계1 : groupby


해당 포스팅에서는 pandas의 groupby에 대해서 정리하였습니다.



이후에 소개될 예제들은 해당 데이터를 이용한다. 이 데이터는 스포츠 뉴스를 다루는 rotowire에서 기록한 2021 KBO의 타자별 데이터를 전처리한 데이터이다. 총 305명 선수의 소속 팀(team)과 수비할 때의 선수 포지션(pos), 그리고 시리즈 동안의 홈런 수(home_runs)를 담고 있으며, Null값은 존재하지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
project_path = os.getcwd()
full_file_path = project_path + '\data_example\kbo-player-stats-Batters.csv'
df = pd.read_csv(full_file_path, encoding = 'utf-8')
df = df.rename(columns = {'Player': 'name', 'Team': 'team', 
                        'Pos': 'pos', 'HR': 'home_runs'})
df = df[['name', 'team', 'pos', 'home_runs']]

def team_eng_to_kor(team_ab):
    ...
def pos_eng_to_kor(pos_ab):
    ...

df['team']=df['team'].apply(team_eng_to_kor)
df['pos']=df['pos'].apply(pos_eng_to_kor)

print(df.head(5))
print(df.info())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
            name  team    pos  home_runs
0   Won Jun Choi   KIA  외야수          4
1    Sung Bum Na    NC  외야수         33
2  Hye Seong Kim   키움  유격수         3
3    Jose Pirela   삼성  외야수        29
4   Jun Woo Jeon   롯데  외야수         7

RangeIndex: 305 entries, 0 to 304
Data columns (total 4 columns):
 #   Column     Non-Null Count  Dtype 
---  ------     --------------  ----- 
 0   name       305 non-null    object
 1   team       305 non-null    object
 2   pos        305 non-null    object
 3   home_runs  305 non-null    int64 
dtypes: int64(1), object(3)



groupby 메소드

groupby메소드의 기본 format및 해당 포스팅에서 확인할 파라미터는 다음과 같다.

1
DataFrame.groupby(by=None, as_index=True, level=None,	dropna=True)
  • by ( mapping, function, label, or list of labels )
    • 해당 파라미터를 통해서 그룹 집계의 기준이되는 key를 결정한다.
    • 보통 list 형태로 기준으로 지정하는 key를 묶어서 전달한다.
    • dict혹은 Series가 전달될 경우 해당 객체들의 value가 그룹의 기준이 된다.
    • function혹은 mapping함수가 전달될 경우, Index를 함수의 input값으로 사용하였을 때 반환된 결과가 그룹의 기준이 된다.
    • 더불어, 해당 파라미터를 코드에서 굳이 명시하지 않아도 괜찮다.
  • level ( int, level name, or sequence of such, default None )
    • MultiIndex를 가진 DataFrame에 적용할 경우 특정 level에 의해서 그룹을 수행한다.
    • level=0일 경우 첫 번째 인덱스를 그룹의 key로 지정한다.
    • level=1일 경우 두 번째 인덱스를 그룹의 key로 지정한다.
  • as_index ( bool, default True )
    • True : 그룹의 키로 지정된 열이 집계함수로 반환된 DataFrame의 Index로 지정된다.
    • False : 위와 같이 Index를 지정하지 않는다. (일반적인 정수형 Index로 반환)
  • dropna ( bool, default True )
    • True : group의 key의 value가 NaN인 경우를 제외한다.
    • False : group의 key의 value가 NaN인 경우를 포함한다.
    • 대부분의 경우 default로 지정하여 사용한다.


1개 열을 기준으로 그룹화

다음의 예제는 예시 DataFrame에 오직 team열만을 기준으로 groupby메소드를 적용한 결과이다. DataFrameGroupBy라는 클래스 개체가 반환된 것을 확인할 수 있다.

그리고, 해당 객체는 그룹의 key와 DataFrame을 튜플로 관리하기 때문에 반복문을 이용해서 그룹 key와 그룹핑된 DataFrame 객체를 확인할 수 있다.

1
2
3
4
5
6
7
group_df = df.groupby(['team'])

print(type(group_df))
for key, group in group_df:
    print(f'Group Key : {key}')
    print(f'DataFrame Row : {len(group)}')
    print(group.head(2))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<class 'pandas.core.groupby.generic.DataFrameGroupBy'>
Group Key : KIA
DataFrame Row : 31
    name        team  pos      home_runs
0   Won Jun Choi  KIA   외야수    4
13  Sun Bin Kim   KIA   2루수     5

Group Key : LG
DataFrame Row : 30
    name        team  pos      home_runs
9   Chang Ki Hong LG    외야수    4
12  Hyun Soo Kim  LG    외야수    17

... 생략 ...

Group Key : 키움
DataFrame Row : 31
    name          team  pos      home_runs
2   Hye Seong Kim  키움  유격수    3
23  Jung Hoo Lee   키움  외야수    7

Group Key : 한화
DataFrame Row : 36
    name          team  pos      home_runs
8   Ju Suk Ha     한화  유격수     10
14  Eun Won Jung   한화  2루수      6


그리고, 여기서 get_group() 메소드를 통해서 특정 그룹만을 선택하여 가져올 수 있다.

1
2
grouped_kia = group_df.get_group('KIA')
print(grouped_kia.head(5))
1
2
3
4
5
6
              name team  pos  home_runs
0     Won Jun Choi  KIA  외야수        4
13     Sun Bin Kim  KIA  2루수         5
21  Preston Tucker  KIA  외야수        9
39    Chan Ho Park  KIA  유격수        1
51     Tae Jin Kim  KIA  3루수         1


여러 열을 기준으로 그룹화

다음은 팀과 포지션별로 예시 DataFrame을 그룹화시키는 예제이다. 그룹의 기준이 되는 두 key를 리스트로 묶어서 groupby메소드에 기입한다. groupby메소드의 결과 DataFrameGroupBy클래스 객체가 반환되는 것을 확인할 수 있으며 두 열이 갖는 원소 값들로 만들 수 있는 모든 조합으로 키를 생성하는 것을 확인할 수 있다.

1
2
3
4
5
6
multi_key = ['team', 'pos']
multi_group_df = df.groupby(multi_key)

print(type(multi_group_df))
for key, group in multi_group_df:
    print(f'Group Key : {key}')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<class 'pandas.core.groupby.generic.DataFrameGroupBy'>
Group Key : ('KIA', '1루수')
Group Key : ('KIA', '2루수')
Group Key : ('KIA', '3루수')
Group Key : ('KIA', '외야수')
Group Key : ('KIA', '유격수')
Group Key : ('KIA', '포수')

... 생략 ...

Group Key : ('한화', '1루수')
Group Key : ('한화', '2루수')
Group Key : ('한화', '3루수')
Group Key : ('한화', '외야수')
Group Key : ('한화', '유격수')
Group Key : ('한화', '포수')


단일 열 그룹 객체와 마찬가지로 get_group()메소드를 이용해서 특정 그룹의 DataFrame만 선택할 수 있다. 이때, 멀티 그룹 key는 튜플로 묶어서 get_group의 인자로 전달해야한다.

1
2
grouped_lg_ss = multi_group_df.get_group(("LG", "유격수"))
print(grouped_lg_ss)
1
2
3
4
              name team  pos     home_runs
24      Ji Hwan Oh   LG  유격수          8
121   Young Bin Lee   LG  유격수          2
203    Bon Hyeok Gu   LG  유격수          0


그룹 연산 메소드

위에서 생성한 DataFrameGroupBy에는 각 그룹별로 다양한 집계치 연산을 수행할 수 있다. 기본적으로 pandas의 DataFrameGroupBy객체에는 다음과 같은 기본 메소드가 내장되어 있다.

methoddescriptionmethoddescription
max()최대size()row 수
min()최소var()분산
sum()합계std()표준 편차
count()열별 카운팅(NaN제외)mean()평균
first()첫 번째 행describe()통계 요약
last()마지막 행nunique()Distinct 카운팅


그 중, 대표적으로 count()describe()를 사용해보자.

count()를 사용했을 때는 다음과 같이 각 열별 NaN이 아닌 값을 카운팅한다. 해당 데이터 프레임의 경우 위에서 보았듯이, NaN이 존재하는 열이 없었기 때문에 각 team 그룹별 모든 열의 count() 반환 값이 동일한 것을 확인할 수 있다.

1
2
x = df.groupby(['team'])
print(x.count())
1
2
3
4
5
6
7
8
9
10
11
12
        name  pos   home_runs
team                      
KIA       31   31         31
LG        30   30         30
NC        34   34         34
SSG       28   28         28
kt        27   27         27
두산      28   28         28
롯데      31   31         31
삼성      29   29         29
키움      31   31         31
한화      36   36         36


describe()의 경우 각 그룹에서의 실수 혹은 정수형 값이 포함된 열을 기준으로 통계값(row 수, 평균, 표준편차, 최소, 백분위, max)을 집계한다. 만약, 실수 혹은 정수형 타입의 값을 가진 열이 두 개이상 존재한다면, 각 열마다 통계값을 형성한다.

1
2
x = df.groupby(['team'])
print(x.describe())
1
2
3
4
5
6
7
8
9
10
11
12
13
home_runs                                               
         count      mean     std  min  25%  50%   75%   max
team                                                         
KIA       31.0  2.129032  3.432608  0.0  0.0  1.0  3.00  13.0
LG        30.0  3.666667  4.887234  0.0  0.0  1.5  7.25  17.0
NC        34.0  5.000000  8.984836  0.0  0.0  1.0  6.75  33.0
SSG       28.0  6.607143  9.538768  0.0  0.0  3.0  7.25  35.0
kt        27.0  3.925926  4.937501  0.0  0.0  1.0  6.50  16.0
두산        28.0  3.928571  7.512600  0.0  0.0  0.0  5.00  28.0
롯데        31.0  3.451613  5.111025  0.0  0.0  1.0  4.50  19.0
삼성        29.0  4.586207  8.130706  0.0  0.0  1.0  4.00  29.0
키움        31.0  2.935484  5.341258  0.0  0.0  1.0  3.50  22.0
한화        36.0  2.222222  3.742506  0.0  0.0  0.5  3.25  18.0


by에 function/dict(혹은 Series)가 전달되는 경우

다음은 홈런 수에 따라서 범주형 데이터로 나누는 함수를 groupby에 적용한 예제이다. 정의한 함수를 통해서 양적 데이터를 범주형 데이터로 변환하였으며, 해당 범주 열을 기준으로 고유 타자 수를 카운팅하였다. 결과적으로 각 범주형 데이터에 속하는 고유 타자 수를 집계할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
def create_category(col):
    if (0<col) & (col<=10):
        return '0<hit<=10'
    elif (10<col) & (col<=20):
        return '10<hit<=20'
    elif (20<col) & (col<=30):
        return '20<hit<=30'
    else:
        return '30<hit'

x = df.set_index('home_runs').groupby(create_category)[['name']].count()
print(x)
1
2
3
4
5
6
            name
home_runs       
0<hit<=10    141
10<hit<=20    18
20<hit<=30     8
30<hit       138


다음은 각 팀(key)에 따른 연고지(value)로 이루어진 dict와 Series를 groupby의 인자로 전달하여 연고지별 고유 타자 수를 카운팅하는 예제이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
region_dict = {'한화': '대전', 'NC': '창원', 'KIA': '광주',
             '롯데': '부산', '키움': '서울', 'LG': '서울',
             '삼성': '대구', 'SSG': '인천', '두산': '서울',
             'kt': '수원'}

region_sr = pd.Series(region_dict)

x = df.set_index('team').groupby(region_dict)[['name']].nunique()\
				.rename(columns={'name':'unique_cnt_by_dict'})
y = df.set_index('team').groupby(region_sr)[['name']].nunique()\
				.rename(columns={'name':'unique_cnt_by_sr'})
result = x.merge(y, left_index=True, right_index=True)
result.index.name = 'hometown'

print(result)
1
2
3
4
5
6
7
8
9
10
          unique_cnt_by_dict    unique_cnt_by_sr
hometown
광주                    31                31
대구                    29                29
대전                    36                36
부산                    31                31
서울                    88                88
수원                    27                27
인천                    28                28
창원                    34                34


as_index

위에서 볼 수 있듯이, 그룹 연산 메소드를 수행했을 때 그룹이 되는 기준 열을 기준으로 Index가 설정되어 결과물이 출력되는 것을 확인할 수 있다.

이와 같은 결과가 출력되는 이유는 as_index라는 파라미터가 기본적으로 True로 설정되어있기 때문이다. 따라서, Index를 열로 변환할 필요가 있을 경우에는 reset_index()를 사용할 수 있지만, groupby()as_index파라미터를 False로 지정해서사전에 방지할 수 있다.

다음의 예제에서 as_index=False로 지정하여 반환된 결과와 reset_index()를 적용했을 때 반환된 집계 DataFrame의 열이 동일한 것을 확인할 수 있다.

1
2
3
4
5
as_index_example = df.groupby(['team'], as_index=False).count()
reset_index_example = df.groupby(['team']).count().reset_index()

print(f"[ as_index를 False로 지정 했을 경우의 열 ]\n{as_index_example.columns}")
print(f"[ reset_index를 사용했을 경우의 열 ]\n{reset_index_example.columns}")
1
2
3
4
5
[ as_index를 False로 지정 했을 경우의 열 ]
Index(['team', 'name', 'pos', 'home_runs'], dtype='object')

[ reset_index를 사용했을 경우의 열 ] 
Index(['team', 'name', 'pos', 'home_runs'], dtype='object')


6) level

level파라미터는 멀티 인덱스에서의 집계치 연산을 수행할 때 도움이 된다.

예를 들어 다음과 같이 타자들의 팀과 수비 포지션(내야 수비 포지션만을 고려)을 기준으로 집계한 총 홈런 수 데이터가 있을 때,

1
2
3
group_df = df[df['pos'].isin(['1루수', '2루수', '3루수'])]\
              .groupby(['team', 'pos']).sum()
print(group_df)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
            home_runs
team   pos           
KIA   1루수         17
      2루수         5
      3루수         1
LG    1루수         27
      2루수         5
      3루수         17

... 생략 ...

키움  1루수          20
      2루수         4
      3루수         21
한화   1루수         15
      2루수         11
      3루수         18


level=0인 경우, 첫 번째 인덱스를 기준으로 집계함수를 적용하며,
level=1인 경우, 두 번째 인덱스를 기준으로 집계함수를 적용하는 것을 확인할 수 있다.

1
2
3
4
level0_df = group_df.groupby(level=0).mean()
level1_df = group_df.groupby(level=1).mean()
print(level0_df)
print(level1_df)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
        home_runs
team           
KIA     7.666667
LG      16.333333
NC      8.000000
SSG     27.666667
kt      14.333333
두산    20.666667
롯데    22.666667
삼성    12.666667
키움    15.000000
한화    14.666667

       home_runs
pos           
1루수       22.4
2루수       10.3
3루수       15.2



References

This post is licensed under CC BY 4.0 by the author.