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% |
参考
- ガイド: Group By: split-apply-combine
- groupby 関数のリファレンス: pandas.DataFrame.groupby
- GroupBy オブジェクトのリファレンス: GroupBy
- Series
- DataFrame