読者です 読者をやめる 読者になる 読者になる

StatsFragments

Python, R, Rust, 統計, 機械学習とか

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

まずは普通の groupbyDataFramegroupby すると、結果は 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.yeardt.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で周期的なグルーピング

数日おきなど、特定の周期/間隔でグループ分けしければ Grouperfreqキーワードの記法は Documentation 参照。

Grouperdatetime64 をグループ分けした場合、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 での集約には二つ既知の バグがあるので注意。

  1. グルーピング対象の列の値に NaTが入っているとき var, std, mean で集約できない
  2. 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]