pandasのgroupby()でグルーピングし統計量を算出

Modified: | Tags: Python, pandas

pandasでは、DataFrameSeriesgroupby()メソッドでデータをグルーピング(グループ分け)できる。グループごとにデータを集約して、それぞれの平均・最小値・最大値・合計などの統計量を算出したり、任意の関数で処理したりすることが可能。

なお、マルチインデックスを設定することでも同様の処理ができる。以下の記事を参照。

また、pd.pivot_table()pd.crosstab()を用いてカテゴリごとの統計量やサンプル数を算出することもできる。

本記事のサンプルコードのpandasのバージョンは以下の通り。バージョンによって仕様が異なる可能性があるので注意。以下のDataFrameを例とする。

import pandas as pd

print(pd.__version__)
# 2.1.2

df = pd.DataFrame(
    {'c_0': ['A', 'A', 'B', 'B', 'B', 'B'],
     'c_1': ['X', 'Y', 'X', 'Y', 'X', 'Y'],
     'c_2': [0, 1, 4, 9, 16, 25],
     'c_3': [125, 64, 27, 16, 1, 0]},
    index=['r_0', 'r_1', 'r_2', 'r_3', 'r_4', 'r_5']
)
print(df)
#     c_0 c_1  c_2  c_3
# r_0   A   X    0  125
# r_1   A   Y    1   64
# r_2   B   X    4   27
# r_3   B   Y    9   16
# r_4   B   X   16    1
# r_5   B   Y   25    0

groupby()の基本的な使い方

DataFramegroupby()メソッドでグルーピング(グループ分け)する。Seriesにも同様にgroupby()メソッドがある。

第一引数byに列名を指定するとその列の値ごとにグルーピングされる。返されるのはGroupByオブジェクトで、それ自体をprint()で出力しても中身は表示されない。

grouped = df.groupby('c_0')
print(grouped)
# <pandas.core.groupby.generic.DataFrameGroupBy object at 0x1272139d0>

print(type(grouped))
# <class 'pandas.core.groupby.generic.DataFrameGroupBy'>

GroupByオブジェクトからメソッドを実行することでグループごとに処理を適用できる。

例えば、mean()メソッドはそれぞれのグループの平均を算出する。引数numeric_onlyTrueとすると数値以外の列が無視される。DataFrameが返される。

df_mean = grouped.mean(numeric_only=True)
print(df_mean)
#       c_2   c_3
# c_0            
# A     0.5  94.5
# B    13.5  11.0

print(type(df_mean))
# <class 'pandas.core.frame.DataFrame'>

groupby()とそのメソッドを続けて書いてもよい。

print(df.groupby('c_0').mean(numeric_only=True))
#       c_2   c_3
# c_0            
# A     0.5  94.5
# B    13.5  11.0

print(df.groupby('c_1').mean(numeric_only=True))
#            c_2        c_3
# c_1                      
# X     6.666667  51.000000
# Y    11.666667  26.666667

GroupByオブジェクトに[列名]または[列名のリスト]を適用すると、任意の列のみが処理の対象となる。不要な列がある場合に便利。

print(df.groupby('c_0')['c_2'].mean())
# c_0
# A     0.5
# B    13.5
# Name: c_2, dtype: float64

print(df.groupby('c_0')[['c_2', 'c_3']].mean())
#       c_2   c_3
# c_0            
# A     0.5  94.5
# B    13.5  11.0

mean()のほかにも、合計を算出するsum()や欠損値ではない要素数をカウントするcount()など、様々なメソッドが提供されている。

print(df.groupby('c_0').sum(numeric_only=True))
#      c_2  c_3
# c_0          
# A      1  189
# B     54   44

print(df.groupby('c_0').count())
#      c_1  c_2  c_3
# c_0               
# A      2    2    2
# B      4    4    4

メソッド一覧は公式ドキュメントを参照。

複数の処理を適用するagg()メソッドや複数の統計量を一括算出するdescribe()、各グループに任意の処理を適用するapply()については後述。

複数列をキーとしてグルーピング

groupby()の第一引数byに列名のリストを指定すると、複数列をキーとしてグルーピングできる。

print(df.groupby(['c_0', 'c_1']).mean())
#           c_2    c_3
# c_0 c_1             
# A   X     0.0  125.0
#     Y     1.0   64.0
# B   X    10.0   14.0
#     Y    17.0    8.0

マルチインデックスのDataFrameが返される。

列名を結果のインデックスにしない: 引数as_index

これまでの例のように、デフォルトではgroupby()の第一引数byに指定した列名が結果のインデックスになる。引数as_indexFalseとするとインデックスにならない。

print(df.groupby('c_0', as_index=False).mean(numeric_only=True))
#   c_0   c_2   c_3
# 0   A   0.5  94.5
# 1   B  13.5  11.0

print(df.groupby(['c_0', 'c_1'], as_index=False).mean())
#   c_0 c_1   c_2    c_3
# 0   A   X   0.0  125.0
# 1   A   Y   1.0   64.0
# 2   B   X  10.0   14.0
# 3   B   Y  17.0    8.0

欠損値NaNの扱い: 引数dropna

groupby()の第一引数byに指定した列に欠損値NaNが含まれている場合、デフォルトではその行は無視される。引数dropnaFalseとするとNaNも一つのキーとして扱われる。

df_nan = df.copy()
df_nan.iloc[0, 1] = float('nan')
df_nan.iloc[5, 1] = float('nan')
print(df_nan)
#     c_0  c_1  c_2  c_3
# r_0   A  NaN    0  125
# r_1   A    Y    1   64
# r_2   B    X    4   27
# r_3   B    Y    9   16
# r_4   B    X   16    1
# r_5   B  NaN   25    0

print(df_nan.groupby(['c_0', 'c_1']).mean())
#           c_2   c_3
# c_0 c_1            
# A   Y     1.0  64.0
# B   X    10.0  14.0
#     Y     9.0  16.0

print(df_nan.groupby(['c_0', 'c_1'], dropna=False).mean())
#           c_2    c_3
# c_0 c_1             
# A   Y     1.0   64.0
#     NaN   0.0  125.0
# B   X    10.0   14.0
#     Y     9.0   16.0
#     NaN  25.0    0.0

pandasにおける欠損値については以下の記事を参照。

各グループに含まれるデータを取得: get_group()

各グループに含まれるデータはGroupByオブジェクトのget_group()メソッドで取得できる。

引数に列名を指定する。キーが複数列の場合はタプルを使う。キーとして指定した列も含むDataFrameが返される。

print(df.groupby('c_0').get_group('B'))
#     c_0 c_1  c_2  c_3
# r_2   B   X    4   27
# r_3   B   Y    9   16
# r_4   B   X   16    1
# r_5   B   Y   25    0

print(df.groupby(['c_0', 'c_1']).get_group(('B', 'X')))
#     c_0 c_1  c_2  c_3
# r_2   B   X    4   27
# r_4   B   X   16    1

なお、各グループに含まれるデータ数(行数)はsize()メソッドで取得できる。

print(df.groupby('c_0').size())
# c_0
# A    2
# B    4
# dtype: int64

print(df.groupby(['c_0', 'c_1']).size())
# c_0  c_1
# A    X      1
#      Y      1
# B    X      2
#      Y      2
# dtype: int64

複数の処理を適用: agg()

GroupByオブジェクトのagg()メソッドで複数の処理をまとめて適用できる。

GroupByオブジェクトのメソッド名を文字列で指定できる。リストで指定すると複数の処理が適用される。列名をキーとした辞書dictで、列ごとに異なる処理を適用することも可能。

print(df.groupby(['c_0', 'c_1']).agg('mean'))
#           c_2    c_3
# c_0 c_1             
# A   X     0.0  125.0
#     Y     1.0   64.0
# B   X    10.0   14.0
#     Y    17.0    8.0

print(df.groupby(['c_0', 'c_1']).agg(['mean', 'min', 'max']))
#           c_2            c_3          
#          mean min max   mean  min  max
# c_0 c_1                               
# A   X     0.0   0   0  125.0  125  125
#     Y     1.0   1   1   64.0   64   64
# B   X    10.0   4  16   14.0    1   27
#     Y    17.0   9  25    8.0    0   16

print(df.groupby(['c_0', 'c_1']).agg({'c_2': 'sum', 'c_3': ['min', 'max']}))
#         c_2  c_3     
#         sum  min  max
# c_0 c_1              
# A   X     0  125  125
#     Y     1   64   64
# B   X    20    1   27
#     Y    34    0   16

存在しないメソッド名を指定するとエラー。

# print(df.groupby(['row_0', 'row_1']).agg('xxx'))
# AttributeError: 'xxx' is not a valid function for 'DataFrameGroupBy' object

# print(df.groupby(['row_0', 'row_1']).agg(['xxx']))
# AttributeError: 'SeriesGroupBy' object has no attribute 'xxx'

上のエラーメッセージから分かるように、文字列単独で指定した場合はDataFrameGroupByのメソッド、リストで指定した場合はSeriesGroupByのメソッドが使われる。

defで定義した関数やラムダ式(無名関数)など、呼び出し可能オブジェクトも指定できる。

def my_func(x):
    return x.max() + x.min()

print(df.groupby(['c_0', 'c_1']).agg([my_func, lambda x: x.sum() - x.mean()]))
#             c_2                c_3           
#         my_func <lambda_0> my_func <lambda_0>
# c_0 c_1                                      
# A   X         0        0.0     250        0.0
#     Y         2        0.0     128        0.0
# B   X        20       10.0      28       14.0
#     Y        34       17.0      16        8.0

リストで指定しても、単体で指定しても、呼び出し可能オブジェクトにはSeriesが渡される。

print(df.groupby(['c_0', 'c_1']).agg(lambda x: str(type(x))).iloc[0, 0])
# <class 'pandas.core.series.Series'>

print(df.groupby(['c_0', 'c_1']).agg([lambda x: str(type(x))]).iloc[0, 0])
# <class 'pandas.core.series.Series'>

print(df.groupby(['c_0', 'c_1']).agg(lambda x: str(x.values)))
#              c_2      c_3
# c_0 c_1                  
# A   X        [0]    [125]
#     Y        [1]     [64]
# B   X    [ 4 16]  [27  1]
#     Y    [ 9 25]  [16  0]

複数の統計量を一括算出: describe()

describe()メソッドを使うと、グループごとの主要な統計量を一括で算出できる。いちいちagg()で指定するよりも簡単。

以下の例ではc_2列に対する結果のみ出力している。

print(df.groupby(['c_0', 'c_1']).describe()['c_2'])
#          count  mean        std  min   25%   50%   75%   max
# c_0 c_1                                                     
# A   X      1.0   0.0        NaN  0.0   0.0   0.0   0.0   0.0
#     Y      1.0   1.0        NaN  1.0   1.0   1.0   1.0   1.0
# B   X      2.0  10.0   8.485281  4.0   7.0  10.0  13.0  16.0
#     Y      2.0  17.0  11.313708  9.0  13.0  17.0  21.0  25.0

各項目の意味などは以下の記事を参照。

各グループに任意の処理を適用: apply()

各グループに任意の処理を適用するにはapply()メソッドを使う。

第一引数に指定した呼び出し可能オブジェクトに、各グループがDataFrameとして渡される。渡されるDataFrameにはキーとして指定した列も含まれるので注意。

print(df.groupby(['c_0', 'c_1']).apply(lambda x: type(x)))
# c_0  c_1
# A    X      <class 'pandas.core.frame.DataFrame'>
#      Y      <class 'pandas.core.frame.DataFrame'>
# B    X      <class 'pandas.core.frame.DataFrame'>
#      Y      <class 'pandas.core.frame.DataFrame'>
# dtype: object

dfs = []
df.groupby(['c_0', 'c_1']).apply(lambda x: dfs.append(x))
print(dfs[0])
#     c_0 c_1  c_2  c_3
# r_0   A   X    0  125

print(dfs[1])
#     c_0 c_1  c_2  c_3
# r_1   A   Y    1   64

print(dfs[2])
#     c_0 c_1  c_2  c_3
# r_2   B   X    4   27
# r_4   B   X   16    1

print(dfs[3])
#     c_0 c_1  c_2  c_3
# r_3   B   Y    9   16
# r_5   B   Y   25    0

apply()の第一引数に指定する呼び出し可能オブジェクトが返す型やgroupby()の引数によって結果の形が変わる。

以下、いくつかのパターンを示す。かなり複雑なので、とりあえずいろんなパターンがあるということだけ覚えておいて、実際に利用する際には想定の入力で結果を確かめてみるとよいだろう。

スカラー値を返す処理を指定した場合はSeriesが返される。as_index=FalseだとDataFrameが返される。

print(df.groupby(['c_0', 'c_1']).apply(lambda x: x['c_2'].max()))
# c_0  c_1
# A    X       0
#      Y       1
# B    X      16
#      Y      25
# dtype: int64

print(df.groupby(['c_0', 'c_1'], as_index=False).apply(lambda x: x['c_2'].max()))
#   c_0 c_1  None
# 0   A   X     0
# 1   A   Y     1
# 2   B   X    16
# 3   B   Y    25

Seriesを返す処理を指定した場合、そのSeriesのインデックスが元の列名と一致する場合はDataFrame、異なる場合はSeriesが返される。

print(dfs[0][['c_2', 'c_3']].max())
# c_2      0
# c_3    125
# dtype: int64

print(dfs[0][['c_2', 'c_3']].max(axis=1))
# r_0    125
# dtype: int64

print(df.groupby(['c_0', 'c_1']).apply(lambda x: x[['c_2', 'c_3']].max()))
#          c_2  c_3
# c_0 c_1          
# A   X      0  125
#     Y      1   64
# B   X     16   27
#     Y     25   16

print(df.groupby(['c_0', 'c_1']).apply(lambda x: x[['c_2', 'c_3']].max(axis=1)))
# c_0  c_1     
# A    X    r_0    125
#      Y    r_1     64
# B    X    r_2     27
#           r_4     16
#      Y    r_3     16
#           r_5     25
# dtype: int64

さらに、Seriesが返される場合は、groupby()の引数as_indexおよびgroup_keysによってインデックスが変わる。

print(
    df.groupby(['c_0', 'c_1'], as_index=False).apply(
        lambda x: x[['c_2', 'c_3']].max(axis=1)
    )
)
# 0  r_0    125
# 1  r_1     64
# 2  r_2     27
#    r_4     16
# 3  r_3     16
#    r_5     25
# dtype: int64

print(
    df.groupby(['c_0', 'c_1'], group_keys=False).apply(
        lambda x: x[['c_2', 'c_3']].max(axis=1)
    )
)
# r_0    125
# r_1     64
# r_2     27
# r_4     16
# r_3     16
# r_5     25
# dtype: int64

DataFrameを返す処理を指定した場合はDataFrameが返される。groupby()の引数as_indexおよびgroup_keysによってインデックスが変わる。

print(df.groupby(['c_0', 'c_1']).apply(lambda x: x[['c_2', 'c_3']] * 10))
#              c_2   c_3
# c_0 c_1               
# A   X   r_0    0  1250
#     Y   r_1   10   640
# B   X   r_2   40   270
#         r_4  160    10
#     Y   r_3   90   160
#         r_5  250     0

print(
    df.groupby(['c_0', 'c_1'], as_index=False).apply(lambda x: x[['c_2', 'c_3']] * 10)
)
#        c_2   c_3
# 0 r_0    0  1250
# 1 r_1   10   640
# 2 r_2   40   270
#   r_4  160    10
# 3 r_3   90   160
#   r_5  250     0

print(
    df.groupby(['c_0', 'c_1'], group_keys=False).apply(lambda x: x[['c_2', 'c_3']] * 10)
)
#      c_2   c_3
# r_0    0  1250
# r_1   10   640
# r_2   40   270
# r_3   90   160
# r_4  160    10
# r_5  250     0

関連カテゴリー

関連記事