[Pandas] groupby.aggのnested renamingの代替手段

In [1]:
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()でできる、次のような書き方があることを知った。

In [2]:
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をバージョンアップした方が良さそうだ。