Python
pandas
Jupyter
GroupBy

Pandas の groupby の使い方

Python でデータ処理するライブラリの定番 Pandas の groupby がなかなか難しいので整理する。特に apply の仕様はパラメータの関数の戻り値によって予想外の振る舞いをするので凶悪に思える。

まず必要なライブラリを import する。

import pandas as pd

import numpy as np


DataFrame を作る

サンプル用のデータを適当に作る。

df = pd.DataFrame({

'city': ['osaka', 'osaka', 'osaka', 'osaka', 'tokyo', 'tokyo', 'tokyo'],
'food': ['apple', 'orange', 'banana', 'banana', 'apple', 'apple', 'banana'],
'price': [100, 200, 250, 300, 150, 200, 400],
'quantity': [1, 2, 3, 4, 5, 6, 7]
})
df




city
food
price
quantity




0
osaka
apple
100
1


1
osaka
orange
200
2


2
osaka
banana
250
3


3
osaka
banana
300
4


4
tokyo
apple
150
5


5
tokyo
apple
200
6


6
tokyo
banana
400
7

余談だが、本題に入る前に Pandas の二次元データ構造 DataFrame について軽く触れる。余談だが Pandas は列志向のデータ構造なので、データの作成は縦にカラムごとに行う。列ごとの処理は得意で速いが、行ごとの処理はイテレータ等を使って Python の世界で行うので遅くなる。

DataFrame には index と呼ばれる特殊なリストがある。上の例では、'city', 'food', 'price' のように各列を表す index と 0, 1, 2, 3, ... のように各行を表す index がある。また、各 index の要素を label と呼ぶ。それぞれ以下のようなプロパティで取得出来る。

df.columns # 列 label の取得

Index(['city', 'food', 'price', 'quantity'], dtype='object')

df.index # 行 label の取得

RangeIndex(start=0, stop=7, step=1)


簡単な groupby の使い方

余談終わり。groupby は、同じ値を持つデータをまとめて、それぞれの塊に対して共通の操作を行いたい時に使う。例えば一番簡単な使い方として、city ごとの price の平均を求めるには次のようにする。groupby で出来た GroupBy オブジェクトに対して、平均をとる mean メソッドを呼ぶと良い。

df.groupby('city').mean()




price
quantity


city






osaka
212.5
2.5


tokyo
250.0
6.0

グループの指定に複数の label を指定する事も出来る。city と food の組み合わせで平均をとるには次のようにする。

df.groupby(['city', 'food']).mean()





price
quantity


city
food






osaka
apple
100.0
1.0


banana
275.0
3.5


orange
200.0
2.0


tokyo
apple
175.0
5.5


banana
400.0
7.0

groupby を使うと、デフォルトでグループラベルが index になる。index にしたく無い場合は as_index=False を指定する。

df.groupby(['city', 'food'], as_index=False).mean()




city
food
price
quantity




0
osaka
apple
100.0
1.0


1
osaka
banana
275.0
3.5


2
osaka
orange
200.0
2.0


3
tokyo
apple
175.0
5.5


4
tokyo
banana
400.0
7.0


GroupBy オブジェクトの性質

デバッグ以外で使うところは無いかも知れないが、groupby によって作られた GroupBy オブジェクトの性質を調べるプロパティが幾つかある。まず、groupby によってどのように DataFrame が分割されたかを知るには groups を使う。{ 列 label: [行 label, 行 label, ...], ... } のような形で、どのグループにどの列が入ったか分かる。

df.groupby('city').groups

{'osaka': Int64Index([0, 1, 2, 3], dtype='int64'),

'tokyo': Int64Index([4, 5, 6], dtype='int64')}

あるグループにどのようなデータが入ったかを知るには get_group を使う。

df.groupby('city').get_group('osaka')




city
food
price
quantity




0
osaka
apple
100
1


1
osaka
orange
200
2


2
osaka
banana
250
3


3
osaka
banana
300
4

各グループのサイズは size で取得出来る。

df.groupby('city').size()

city

osaka 4
tokyo 3
dtype: int64

size の結果は Series という一次元列を表すオブジェクトが返る。Series を使うと、osaka グループのサイズは添字を使って取得出来る。

df.groupby('city').size()['osaka']

4


さまざまな Aggregation

GroupBy.mean() のように、グループごとに値を求めて表を作るような操作を Aggregation と呼ぶ。このように GroupBy オブジェクトには Aggregation に使う関数が幾つか定義されているが、これらは agg() を使っても実装出来る。

df.groupby('city').agg(np.mean)




price
quantity


city






osaka
212.5
2.5


tokyo
250.0
6.0

agg には多様な使い方がある。上の例では、mean() を使って各グループごとに price と quantity 両方の平均を求めたが、例えば price の平均と quantity の合計を同時に知りたいときは以下のように { グループ名: 関数 } の dict を渡す。関数には Series を受け取って一つの値を返す物を期待されている。

def my_mean(s):

"""わざとらしいサンプル"""
return np.mean(s)

df.groupby('city').agg({'price': my_mean, 'quantity': np.sum})




price
quantity


city






osaka
212.5
10


tokyo
250.0
18


Group ごとに複数行を返す

Aggregation の結果はグループごとに一行にまとめられるが、もっと柔軟に結果を作りたいときは apply を使う。apply に渡す関数には get_group で得られるようなグループごとの DataFrame が渡される。グループ名は df.name で取得出来る。


apply 関数の結果としてスカラを返す場合。全体の結果は Series になる。


  • groupby で作った label が結果の row index になる。

  • 行数はグループの数と同じになる。

  • as_index の効果は無い。

df.groupby(['city', 'food'], as_index=False).apply(lambda d: (d.price * d.quantity).sum())

city   food  

osaka apple 100
banana 1950
orange 400
tokyo apple 1950
banana 2800
dtype: int64

グループ名 にアクセスしてみた例

df.groupby(['city', 'food'], as_index=False).apply(lambda d: d.name)

city   food  

osaka apple (osaka, apple)
banana (osaka, banana)
orange (osaka, orange)
tokyo apple (tokyo, apple)
banana (tokyo, banana)
dtype: object


apply 関数の結果として Series を返す場合。全体の結果は Series になる。


  • groupby で作った label に加えて、apply 関数の結果の index が結果全体の row index になる。

  • 全体の行数は関数から返す結果に依存する。

  • as_index=False を指定すると、index が消えて連番になる。

def total_series(d):

return d.price * d.quantity

df.groupby(['city', 'food']).apply(total_series)

city   food     

osaka apple 0 100
banana 2 750
3 1200
orange 1 400
tokyo apple 4 750
5 1200
banana 6 2800
dtype: int64


apply 関数の結果として元の row index を保存した DataFrame を返す場合

DataFrame を返す場合、返す DataFrame に含まれる row index によって振る舞いが違う。非常に凶悪な仕様!!!!

元の index を保存した場合、下記 Transformation と同じ動作ように groupby の label は消える。


  • apply 関数の結果を連結した DataFrame が作られる。groupby で対象になる label は index にならない。

  • as_index=False の効果なし

def total_keepindex(d):

return pd.DataFrame({
'total': d.price * d.quantity # ここで返る DataFrame の row index は d の row index と同じ
})

df.groupby(['city', 'food']).apply(total_keepindex)




total




0
100


1
400


2
750


3
1200


4
750


5
1200


6
2800


apply 関数の結果として元の row index を保存しない DataFrame を返す場合

元の index を保存しないと groupby で作った label が結果の row index になる。


  • groupby で作った label が結果の row index になる。

  • as_index=False の効果あり

def total_keepnoindex(d):

return pd.DataFrame({
'total': (d.price * d.quantity).sum()
}, index=['hoge'])
df.groupby(['city', 'food']).apply(total_keepnoindex)






total


city
food






osaka
apple
hoge
100


banana
hoge
1950


orange
hoge
400


tokyo
apple
hoge
1950


banana
hoge
2800


注意!! apply 関数が一度も呼ばれないとカラムが出来ない

Pandas の凶悪な所でありまた動的型付け言語の欠点なのだが、apply 関数の結果で動的にカラムを決めているからか、ゼロ行の DataFrame に対して apply を実行するとカラムが作成されない。ゼロ行だけ特別扱いしないと行けないので分かりづらいバグを生む。

例えばこの場合3行の DataFrame の場合カラムが出来る。

pd.DataFrame({'hoge': [1,1,3], 'fuga': [10, 20, 30]}).groupby('hoge').apply(np.sum)




hoge
fuga


hoge






1
2
30


3
3
30

ところがゼロ行の DataFrame に対して同じ apply を実行するとカラムが消えてしまう。

pd.DataFrame({'hoge': [], 'fuga': []}).groupby('hoge').apply(np.sum)






hoge




Transformation

グループごとの統計情報を使ってすべての行を集計したい場合は Transformation を使う。説明が難しい。。。transformation の引数にはグループごとの列の Series が与えられる。戻り値は引数と同様の Series かスカラを渡す。スカラを渡した場合は引数と同じ個数だけ繰り返される。

例えば、グループごとに各アイテムの割合を求めるには次のようにする。

def transformation_sample(s):

return (s / s.sum() * 100).astype(str) + '%'

df.groupby(['city']).transform(transformation_sample)




price
quantity




0
11.76470588235294%
10.0%


1
23.52941176470588%
20.0%


2
29.411764705882355%
30.0%


3
35.294117647058826%
40.0%


4
20.0%
27.77777777777778%


5
26.666666666666668%
33.33333333333333%


6
53.333333333333336%
38.88888888888889%


参考