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." とのことで、割と簡単そうなので早速やってみることに。
直積としてよく使うのは、組み合わせの総当たりなんじゃないかな。itertoolsとかでやってもいいんだけど、その前の段階でpd.DataFrame使ってたりすると、一旦リストにしてitertoolsで総当たりしてまたdfに戻すとか結構面倒だったりする。
僕の場合だと年×月とか日×時の総当たりを出してdf用のindexを作ることが多い。先に総当たりの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の値は特に見ていないようだ。マジかよ。joinのメソッドとして設計する意味あるか?
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の直積が出てくるというわけだ。
なるほどね。まあなんだかんだ多用するんだろうな。