はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

Python/Pandas演習(dictでグループ化相当)

Python/Pandasにはgroupbyの機能があるのですが、少しトリッキーなグループ化を考えてみたメモです。

複雑な名寄せ・グループ化のニーズ

何かの集計や名寄せの際に、複数のキーやキー自体がネストされた構造化データ相当のデータで名寄せしたいということはありませんか。

まあ、いい感じで管理されているデータについてはそのようなニーズはそれほどないのかもしれませんが、私の身近な例ではぼちぼち発生しているような気がします。

こんなデータ↓を

名前
0 山田 1 100
1 山田 1 100
2 山田 1 100
3 山田 4 60
4 山田 2 33
5 佐藤 6 9
6 田中 4 209

↓こんな感じで名寄せ(グループ化)したいです。

jsons
0 {"値": 100, "名前": "山田", "月": 1} [100, 100, 100]
1 {"値": 209, "名前": "田中", "月": 4} [209]
2 {"値": 33, "名前": "山田", "月": 2} [33]
3 {"値": 60, "名前": "山田", "月": 4} [60]
4 {"値": 9, "名前": "佐藤", "月": 6} [9]

データ例がリアルでないですね。またこの程度であれば、もっと正攻法でやった方が何かと良いというところがあるでしょう。

... と弱気になっておりますが、後には引けないので続けます。

ポイント

後述しますが、Pandasのgroupbyはdict型のカラムをグループキーにしたgroupbyはエラーになってしまうようです。

よって、ひとつの方法としては、このようなグループ化のキーにしたい項目自体をJSON文字列にしてしまう案があります。

Pythonもしかりですが、最近のプログラミング言語ではネスト型のデータについてJSON文字列との相互変換の標準ライブラリが充実しています。

オーバーヘッドや処理性能も気になりますが、あとでいろいろできることを期待して割り切って文字列にしてグループ化するアプローチはありだと思います。

なお、JSON文字列自体は、配列以外は順序に意味がないので、同じ意味のデータが同じと確実に見なされるようにJSON文字列化の際は、プロパティ名が昇順になるような編集になるようにするというのが隠れたポイントです。

コーディングの例

import pandas as pd
import io
import json

# 入力データ
right = pd.read_csv(io.StringIO(
"""名前,月,値
山田,1,100 
山田,1,100 
山田,1,100 
山田,4,60 
山田,2,33 
佐藤,6,9
田中,4,209""" ))


right['jsons'] =  right.apply(lambda s:  json.dumps({k:v for k,v in s.items()},sort_keys=True,ensure_ascii=False),axis=1)

x = right.groupby('jsons')['値'].apply(list).reset_index()

print(x.to_markdown(), '\n\n')

json.dumps()でdictを文字列にしています。

sort_keys=Trueでプロパティの出力順をがプロパティ名の昇順になるように指定しています。

ここでは、同じグループ内の「'値'」フィールドを「list」関数でリストにしたデータを取得しています。

結果

jsons
0 {"値": 100, "名前": "山田", "月": 1} [100, 100, 100]
1 {"値": 209, "名前": "田中", "月": 4} [209]
2 {"値": 33, "名前": "山田", "月": 2} [33]
3 {"値": 60, "名前": "山田", "月": 4} [60]
4 {"値": 9, "名前": "佐藤", "月": 6} [9]

ちょっと例がアレですが、[100,100,100]のようにまとめられています。

その他の例

groupbyのGrouperに関数を指定

文字列にすると、DataFrame.set_indexでDataFrameのインデックスに設定できます。

groupbyはグループ化の条件に関数を指定できますが、これは対象のDataFrameのインデックスに対して作用するようです*1

逆に言うと、生成した文字列化jsonデータをDataFrameのインデックスに設定することで、この値をフックしたGrouperに関数が使えるようになります。

例1

# jsonsフィールドをインデックスに設定する
x_ = right.set_index('jsons')

print(x_.to_markdown(), '\n\n') 

# これはあまり意味のない例だが、インデックスの格納値の文字列桁数ごとにグルーピングする。
x = x_.groupby(len)['値'].apply(list).reset_index()

print(x.to_markdown(), '\n\n')

↓ (出力例)

index
0 28 [9]
1 29 [60, 33]
2 30 [100, 100, 100, 209]

len( 【{"値": 100, "名前": "山田", "月": 1}の文字列】) → 長さ30

例2

また、止むを得ずdict/jsonを文字列にしましたが、関数の中でjsonをdictに戻してやってその上でのグループ化を行うことができるので、(やろうと思えばですが)関数を用いてdictの構造を読み解いたようなグループ化も可能です。

# 最初の10文字でグループ化(この例はこの方式のメリットを体現していないですが...)
x = x_.groupby(lambda idx: idx[0:10])['値'].apply(list).reset_index()

print(x.to_markdown(), '\n\n')

# 名前フィールドを引っ張り出してグループ化(この例はこの方式のメリットを体現していないですが...)
x = x_.groupby(lambda idx: json.loads(idx)['名前'] )['値'].apply(list).reset_index()

print(x.to_markdown(), '\n\n')

最初の10文字でグループ化

index
0 {"値": 100, [100, 100, 100]
1 {"値": 209, [209]
2 {"値": 33, [33]
3 {"値": 60, [60]
4 {"値": 9, " [9]

名前でグループ化((この例であれば、最初から x_.groupby('名前')で良い))

index
0 佐藤 [9]
1 山田 [100, 100, 100, 60, 33]
2 田中 [209]

以上です。

補足

dictが入っているフィールドでDataFrame.groupby()を行うと、groupbyの時点ではエラーにはならないのですが、その後何か演算しようとするとエラーになります。

ひょっとすると何かオプションがあるのかもしれませんが、公式ドキュメントではこれといって見当たりませんでしたので、この記事をおこしました。

In [324]: import pandas as pd 
     ...: import io 
     ...: import json 
     ...: import numpy as np 
     ...:  
     ...:  
     ...: right = pd.read_csv(io.StringIO( 
     ...: """名前,月,値 
     ...: 山田,1,100  
     ...: 山田,1,100  
     ...: 山田,1,100  
     ...: 山田,4,60  
     ...: 山田,2,33  
     ...: 佐藤,6,9 
     ...: 田中,4,209""" ))                                                                                                                              

In [325]: right['jsons'] =  right.apply(lambda s:  {k:v for k,v in s.items()},axis=1)                                                                   

In [326]: right.groupby('jsons').groups                                                                                                                 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-326-ecb97710e4c7> in <module>
----> 1 right.groupby('jsons').groups
(-------------------中略------------------)

TypeError: unhashable type: 'dict'

In [327]:                                                                                                                                               

参考リンク

pandas.pydata.org

docs.python.org

*1:公式リファレンスの読み解きが足りないかもしれませんが明示的な記述は見当たりませんでした。