pandasのgroupby()でグルーピングし統計量を算出
pandasでは、DataFrame
やSeries
のgroupby()
メソッドでデータをグルーピング(グループ分け)できる。グループごとにデータを集約して、それぞれの平均・最小値・最大値・合計などの統計量を算出したり、任意の関数で処理したりすることが可能。
なお、マルチインデックスを設定することでも同様の処理ができる。以下の記事を参照。
また、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()の基本的な使い方
DataFrame
のgroupby()
メソッドでグルーピング(グループ分け)する。Series
にも同様にgroupby()
メソッドがある。
- pandas.DataFrame.groupby — pandas 2.1.3 documentation
- pandas.Series.groupby — pandas 2.1.3 documentation
第一引数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_only
をTrue
とすると数値以外の列が無視される。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_index
をFalse
とするとインデックスにならない。
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
が含まれている場合、デフォルトではその行は無視される。引数dropna
をFalse
とすると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