Help us understand the problem. What is going on with this article?

Pandas の groupby の使い方

More than 1 year has passed since last update.

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%

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away