Python pandas アクセサ / Grouperで少し高度なグルーピング/集計
日本語の説明がなさそうなので。
概要
pandas では groupby
メソッドを使って、指定したカラムの値でデータをグループ分けできる。ここでは少し凝った方法を説明。
※ dtアクセサ の追加、またグルーピング関連のバグ修正がいろいろ入っているので、0.15以降が必要。
※簡単な処理については下の記事でまとめ。
はじめに
例えばこんなデータがあったとして、
import pandas as pd import datetime df = pd.DataFrame({'dt1': [datetime.datetime(2014, 10, 1), datetime.datetime(2014, 10, 2), datetime.datetime(2014, 10, 3), datetime.datetime(2014, 10, 4), datetime.datetime(2014, 10, 5), datetime.datetime(2014, 10, 6), datetime.datetime(2014, 10, 7), datetime.datetime(2014, 11, 1), datetime.datetime(2014, 11, 2), datetime.datetime(2014, 11, 3), datetime.datetime(2014, 11, 4), datetime.datetime(2014, 11, 5), datetime.datetime(2014, 11, 6), datetime.datetime(2014, 11, 7)], 'col1': 'a b'.split() * 7}) # col1 dt1 num1 # 0 a 2014-10-01 3 # 1 b 2014-10-02 4 # 2 a 2014-10-03 2 # 3 b 2014-10-04 5 # 4 a 2014-10-05 1 # 5 b 2014-11-01 3 # 6 a 2014-11-02 2 # 7 b 2014-11-03 3 # 8 a 2014-11-04 5 # 9 b 2014-11-05 2
まずは普通の groupby
。DataFrame
を groupby
すると、結果は DataFrameGroupBy
インスタンスとして返ってくる。このとき、groups
プロパティに グループのラベル : グループに含まれる index からなる辞書が入っている。サンプルスクリプトでの結果は、この groups
プロパティを使って表示することにする。
下の結果は、上の DataFrame
をカラム "col1" の値でグループ分けした結果、グループ "a" に 0, 2, 4, 6, 8行目, グループ "b" に 1, 3, 5, 7, 9行目が含まれていることを示す。
df.groupby('col1').groups # {'a': [0L, 2L, 4L, 6L, 8L], # 'b': [1L, 3L, 5L, 7L, 9L]}
groupby へのリスト渡し
groupby
には 対象の DataFrame
と同じ長さのリスト, numpy.array
, pandas.Series
, pandas.Index
が渡せる。このとき、渡した リスト-likeなものをキーとしてグルーピングしてくれる。
grouper = ['g1', 'g2', 'g3', 'g4', 'g5'] * 2 df.groupby(grouper).groups # {'g5': [4L, 9L], # 'g4': [3L, 8L], # 'g3': [2L, 7L], # 'g2': [1L, 6L], # 'g1': [0L, 5L]}
そのため、一度 外部の関数を通せばどんな柔軟な処理でも書ける。"num1" カラムが偶数か奇数かでグループ分けしたければ、
grouper = df['num1'] % 2 df.groupby(grouper).groups # {0: [1L, 2L, 6L, 9L], # 1: [0L, 3L, 4L, 5L, 7L, 8L]}
アクセサ
でも わざわざ関数書くのめんどくさい、、、。そんなときの アクセサ。例えばカラムが datetime64
型のとき、dt
アクセサを利用して datetime64
の各プロパティにアクセスできる。
例えば "dt1" カラムの各日時から "月" を取得するには、
df['dt1'].dt.month # 0 10 # 1 10 # 2 10 # 3 10 # 4 10 # 5 11 # 6 11 # 7 11 # 8 11 # 9 11 # dtype: int64
これを使えば月次のグルーピングも簡単。
df.groupby(df['dt1'].dt.month).groups # {10: [0L, 1L, 2L, 3L, 4L], # 11: [5L, 6L, 7L, 8L, 9L]}
複数要素、例えば年月の組み合わせでグループ分けしたければ、 dt.year
と dt.month
をリストで渡す。
df.groupby([df['dt1'].dt.year, df['dt1'].dt.month]).groups # {(2014L, 11L): [5L, 6L, 7L, 8L, 9L], # (2014L, 10L): [0L, 1L, 2L, 3L, 4L]}
Grouperで周期的なグルーピング
数日おきなど、特定の周期/間隔でグループ分けしければ Grouper
。freq
キーワードの記法は Documentation 参照。
※ Grouper
で datetime64
をグループ分けした場合、groups
プロパティの読み方はちょっと難しくなる (辞書の値として、グループに含まれる indexではなくグループの切れ目になる index が入る)
df.groupby(pd.Grouper(key='dt1', freq='4d')).groups # {Timestamp('2014-10-09 00:00:00', offset='4D'): 5, # Timestamp('2014-11-02 00:00:00', offset='4D'): 10, # Timestamp('2014-10-13 00:00:00', offset='4D'): 5, # Timestamp('2014-10-01 00:00:00', offset='4D'): 4, # Timestamp('2014-10-05 00:00:00', offset='4D'): 5, # Timestamp('2014-10-25 00:00:00', offset='4D'): 5, # Timestamp('2014-10-29 00:00:00', offset='4D'): 6, # Timestamp('2014-10-17 00:00:00', offset='4D'): 5, # Timestamp('2014-10-21 00:00:00', offset='4D'): 5}
読めないので同じ処理をした結果を普通に print
。4日おきに グループ分けされていることがわかる。
対応する日付のない期間はパディングされる。
for name, group in df.groupby(pd.Grouper(key='dt1', freq='4d')): print(name) print(group) # 2014-10-01 00:00:00 # col1 dt1 num1 # 0 a 2014-10-01 3 # 1 b 2014-10-02 4 # 2 a 2014-10-03 2 # 3 b 2014-10-04 5 # 2014-10-05 00:00:00 # col1 dt1 num1 # 4 a 2014-10-05 1 # 2014-10-09 00:00:00 # Empty DataFrame # 2014-10-13 00:00:00 # Empty DataFrame # 2014-10-17 00:00:00 # Empty DataFrame # 2014-10-21 00:00:00 # Empty DataFrame # 2014-10-25 00:00:00 # Empty DataFrame # 2014-10-29 00:00:00 # col1 dt1 num1 # 5 b 2014-11-01 3 # 2014-11-02 00:00:00 # col1 dt1 num1 # 6 a 2014-11-02 2 # 7 b 2014-11-03 3 # 8 a 2014-11-04 5 # 9 b 2014-11-05 2
集計
上のようにグルーピングした後、集約関数、もしくは aggregate
で普通に集約してもよいが、
df.groupby(df['dt1'].dt.month)['num1'].sum() # 10 15 # 11 15 # Name: num1, dtype: int64
pandas.pivot_table
にも リスト-like、 Grouper
を渡して直接集約できる。例えば "dt1" の year を列, month を行として集計したければ、
pd.pivot_table(df, index=df['dt1'].dt.month, columns=df['dt1'].dt.year, values='num1', aggfunc=sum) # 2014 # 10 15 # 11 15
"col1" を列, "dt1"の値を4日おきに行として集計したければ、
pd.pivot_table(df, index=pd.Grouper(key='dt1', freq='4d'), columns='col1', values='num1', aggfunc=sum) # col1 a b # dt1 # 2014-10-01 5 9 # 2014-10-05 1 NaN # 2014-10-29 NaN 3 # 2014-11-02 7 5
補足 Grouper
での集約には二つ既知の バグがあるので注意。
- グルーピング対象の列の値に
NaT
が入っているときvar
,std
,mean
で集約できない first
,last
,nth
で返ってくる行が 通常のgroupby
と異なる。
まとめ
groupby
,pivot_table
にはリスト-likeが渡せる。複雑な処理を書きたければ 外部の関数で配列作成してグルーピング/集計する。- アクセサ,
Grouper
便利。
おまけ
groupby
には 関数、辞書も渡せる。
df.groupby(lambda x: x % 3).groups # {0: [0L, 3L, 6L, 9L], # 1: [1L, 4L, 7L], # 2: [2L, 5L, 8L]}
辞書渡しの場合、存在しないキーはフィルタされる。
grouper = {1: 'a', 2: 'b', 3: 'a'} df.groupby(by=grouper).groups # {'a': [1L, 3L], # 'b': [2L]} [asin:4873116554:detail]