pandasのjoinで直積を作る

pandasのjoinはデータフレームのインデックスの値が同じもの同士を結合するのにかなり手軽に使えて、個人的にはpd.mergeよりはるかに使用頻度は高い。デフォルトはinner joinだけど、how=’outer’指定でouter joinもできる。

そんなjoinですが、全く気付いていなかったんだけどいつの間にか直積(the cartesian product: デカルト積)が作れるようになっていた。バージョン1.2.0(2020年12月リリース)からなので実はそんな最近の話でもないんだけど。

https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.join.html

how=’cross’ 指定で “cross: creates the cartesian product from both frames, preserves the order of the left keys.” とのことなので、早速やってみようと思う。

直積としてよく使うのは、組み合わせの総当たりなんじゃないかな。僕の場合だと年×月とか日×時の総当たりを出してdf用のindexを作ることが多い。先に総当たりを作っておいて後から値を代入していくと、一通り終わった後の欠損値の有無がわかりやすくなる。

とりあえずデータとして、2020~2022年の3年分のyearを持ったデータフレームd1と、1~12月の12ヶ月分のmonthを持ったデータフレームd2を作ってみる。

import numpy as np
import pandas as pd
d1 = pd.DataFrame({'year' :[i+2020 for i in range(3) ]})
d2 = pd.DataFrame({'month':[i+1    for i in range(12)]})
display(d1, d2)

これらの直積として、2020年1月〜2022年12月まで36個のObsを持つdfができればいい。

d = d1.join(d2, how = 'cross')
print(len(d))
display(d)

できた。簡単すぎて草。

だからまあたとえばこんなふうに使うんかな。もっと簡単にやれる気もするけど。

import pandas as pd
d1 = pd.DataFrame({'year' :[i+2020 for i in range(3) ]})
d2 = pd.DataFrame({'month':[i+1    for i in range(12)]})
d = d1.join(d2, how='cross')
d['str'] = d['year'].astype(str) + '-' + d['month'].astype(str).str.zfill(2) + '-01'
d['datetime'] = pd.to_datetime(d['str'], format = '%Y-%m-%d').dt.normalize()
display(d)

これだってこれまでだと多重ループとかitertoolsとか使って実装してた内容だし、まあシンプルに書けていいんじゃないですか。

ただしここで注意すべき点として、基本的にjoinはindexベースで新しいdfを作ってくれるものだと理解しているんだけど、今回はindexは全く無関係にこの挙動が起きているということ。なにしろd1.index = [0, 1, 2]、d2.index = [0, 1, …, 11]なのだ。

仮にindexに指定しておくと何が起きるかというと…

d1 = pd.DataFrame({'year' :[i+2020 for i in range(3) ]}).set_index('year')
d2 = pd.DataFrame({'month':[i+1    for i in range(12)]}).set_index('month')
d1.join(d2, how='cross')

36個の空のObsが吐き出される。つまりcrossの挙動は2つのdfのオブザベーションをlen(d1)×len(d2)の総当たりで作っているっぽい。そのときindexの値は特に見ていないよう。

d1 = pd.DataFrame({'x1' :[0,1,1,2,2,2]})
d2 = pd.DataFrame({'x2' :[-1, -2, 0, 0, 40]})
print(len(d1), len(d2), len(d1)*len(d2))
d1.join(d2, how='cross')

つまり重複があろうがなんだろうがとにかく2つのdfの直積が出てくるというわけだ。なるほどね。