はてだBlog(仮称)

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

1対Nのデータを結合してネストオブジェクトのJSON Lines出力のイディオム(Python/ Pandas) my 手グセ紹介

たまたま自分のまわりだけかもしれませんが、次項に示した例のように、1対Nのデータを結合してネストオブジェクトのJSON Lines出力をざっくりで良い精度でやってしまいたいという例にしばしば遭遇します。

ブームの時に手グセになっていたり、その案件でガチで必要ならそれなりの関数にしてしまうので良いのです。

ただ、大抵はそこまでいかないのと、忘れたころに、あれ?そんなに難しくないんだけど、どんな感じだったけ、めんどくさいなぁとなって、自分の書き捨てのコードなど頼ることになります。

おそらく程度としては中途半端なので、世のイディオムやTIPSにはあがってこないのと、ググラビリティが低そうなので、やりたいことのイメージ残しを含めて、この記事でメモってみます。

やりたいこと概要

こんなデータ(LEFT)と

名前 所属
0 山田 営業1
1 佐藤 営業2
2 田中 営業2

こんなデータ(RIGHT)があって、

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

こんなデータ(出力)を

名前 所属 関連データ
0 山田 営業1 [{'名前': '山田', '月': 1, '値': 100}, {'名前': '山田', '月': 2, '値': 33}, {'名前': '山田', '月': 4, '値': 60}]
1 佐藤 営業2 [{'名前': '佐藤', '月': 6, '値': 9}]
2 田中 営業2 [{'名前': '田中', '月': 4, '値': 209}]

出力したい。

なお、リストにしている部分は、「月」順にソートしておきたい。

Pythonので例

import pandas as pd
import io

debug = False

left = pd.read_csv(io.StringIO(
"""名前,所属
山田,営業1
佐藤,営業2
田中,営業2"""
))

if debug:
    print(left.to_markdown(),'\n\n')                                                                                       

right = pd.read_csv(io.StringIO(
"""名前,月,値
山田,1,100 
山田,4,60 
山田,2,33 
佐藤,6,9
田中,4,209""" ))
if debug:
    print(right.to_markdown(),'\n\n')                                                                                                                                       

keycols = ['名前', '月']
on =  ['名前']
grpcols = on
sortcol = '月'
dictcol = '関連データ'
_renamecols = {i: j for i, j in zip(range(len(keycols)), keycols)}
関連データカラム名称変更 = {len(keycols): dictcol}

_func = lambda s,keycols: pd.Series([s[k] for k in keycols] + [ {k:v for k,v in s.items()}])

rightdf = right.apply(_func, axis=1, keycols=keycols).rename(columns=_renamecols).rename(columns=関連データカラム名称変更)

rightdf_gr = rightdf.groupby(grpcols)[dictcol]

_listsorted = lambda s: sorted(list(s), key=lambda el: el[sortcol])

出力 = pd.merge(left,rightdf_gr.apply(_listsorted).reset_index(),on=on)

if debug:
    print(出力.to_markdown(),'\n\n')

print(出力.to_json(orient='records',lines=True,force_ascii=False))

自分の中でのポイント

  1. _funcのあたりで、全てのカラムを取り込んだdictを生成する。ここでは元のDataFrameをさわりたくなかったので、rightdfに代入している。
  2. 一度rightdfのようなDataFrameを作成してから、それをgroupbyする。*1
  3. groupbyしたのち、dictcolで指定のカラムをグループ単位に配列にする。

補足

  1. そもそも pandasを使用。今回の用途であれば、collectionsあたりを使いこなした方がスマートかもしれない。
  2. 利用 pandas のバージョンは 1.0.1。この記事のやりたいことのためには本来必須ではないが、pandasバージョンアップ記念に、ver 1で導入された(らしい) to_markdown() を使ってみている。
  3. 見てのとおりpandasが入っていれば動作するレベルだが、to_markdown()で、tabulateが古いと怒られるので、pipでアップデートが必要かもしれない。

pandas.pydata.org

pandas.pydata.org

docs.python.org

*1:groupbyのグループ化に「'名前','月'」を使うつもりだったが、ここでは「名前」だけにしたので、事例としてはkeycolsを定義した意味が薄れているが書き直すのも手間なのでこのままとしている。