import numpy as np
import pandas as pd
np.random.seed(9)
df = pd.DataFrame({
'組': np.random.randint(3, size=20),
'身長': np.random.randint(950, 1050, size=20) / 10,
'地域': np.random.randint(5, size=20)})
df['組'] = df['組'].map({0: 'もも', 1: 'さくら', 2: 'ばら'})
df['地域'] = df['地域'].map({0: 'A町', 1: 'B町', 2: 'C町', 3: 'D町', 4: 'E町'})
df.head()
Out [1]:
組 | 身長 | 地域 | |
---|---|---|---|
0 | ばら | 101.0 | B町 |
1 | もも | 100.9 | A町 |
2 | ばら | 103.8 | D町 |
3 | さくら | 102.4 | C町 |
4 | ばら | 100.6 | B町 |
こういうDataFrameがあり、組ごとに、
・身長の最低値と最高値
・A町の子が含まれているか、B町の子が含まれているか
を求めたいとする。
あまりいい例題では無いが、筆者が仕事で実際に必要になった処理と等価な、他にましな例題を思い付かなかった。
筆者は当初、df.groupby('組')['地域'].agg(lambda x: ...)
のようにして集計結果を1列ずつ求め、後で結合していたのだが、先週、1回のgroupby.agg()でできる、次のような書き方があることを知った。
df.groupby('組')[['身長', '地域']].agg({
'身長': {
'最低身長': np.min,
'最高身長': np.max
},
'地域': {
'A町の子あり': lambda x: any(x == 'A町'),
'B町の子あり': lambda x: any(x == 'B町')
}})
Out [2]:
最低身長 | 最高身長 | A町の子あり | B町の子あり | |
---|---|---|---|---|
組 | ||||
さくら | 95.9 | 104.9 | True | False |
ばら | 100.6 | 103.8 | False | True |
もも | 95.1 | 104.3 | True | False |
同じ列に複数の集約関数を適用し、しかもそれぞれの結果の列に任意の列名を付与できるのである。
これは便利、と思って早速これを使うように書き直して、ローカルPCで動作確認して別PCにコピーして実行すると、
SpecificationError: nested renamer is not supportedというエラーになってしまった。
調べてみると、上のdict-of-dictを渡す書き方(nested renamingというらしい)はPandas v0.20.0でdeprecatedとされ、v1.0で廃止されたらしい。
What's new in 1.0.0より:
Removed support for nested renaming in DataFrame.aggregate(), Series.aggregate(), core.groupby.DataFrameGroupBy.aggregate(), ...ローカルPCのPandasはv0.25.3だったので、nested renamingが動いた。
それでは代わりの方法は無いのかと思って探すと、"named aggregation"が推奨と書かれているのを見つけた。
What's new in 0.25.0より:
Named aggregation is the recommended replacement for the deprecated "dict-of-dicts" approach to naming the output of column-specific aggregations他に、aggに列と関数のリストだけのdictを与えて、後で列名をrenameする方法もあるが、通常はaggに渡す関数名が結果の列名になるのに対し、lambda関数を渡すと列名が勝手に付けられるので、面倒なことになる。
Named aggregationを使うと、上のv1.0でエラーになったコードは次のように書ける。
In [3]:
df.groupby('組').agg(
最低身長=('身長', np.min),
最高身長=('身長', np.max),
A町の子あり=('地域', lambda x: any(x == 'A町')),
B町の子あり=('地域', lambda x: any(x == 'B町')))
Out [3]:
最低身長 | 最高身長 | A町の子あり | B町の子あり | |
---|---|---|---|---|
組 | ||||
さくら | 95.9 | 104.9 | True | False |
ばら | 100.6 | 103.8 | False | True |
もも | 95.1 | 104.3 | True | False |
列名をクォーテーションマークで括ったり括らなかったりするのが統一感に欠けるが、得られる結果が少しわかりやすくなったと思う。それから、前のコードでは[['身長', '地域']]
でやっていた、aggに渡す前に列を絞るのが不要になった(絞らないとnested renamingでは列がMultiIndexになってしまう)ので、すっきりしたと感じる。
普段Pandas v0.25.3を使っていて、他の環境と実行結果が異なるのは何度も経験している。さっさとPandasをバージョンアップした方が良さそうだ。
コメント