pandas で DataFrame を処理する際、 merge に苦手意識がありうまく使えないでいました。 SQL で簡単な INNER JOIN は経験があるものの、キーが重複していたり LEFT/RIGHT JOIN を使ったりしたらどうなるのかは理解できていなく、変なデータができてしまうのではないかという心配がありました。
うまく使えれば効率よく処理を書けるはずなので、 inner や outer などの種類によってどのようなデータが作られるのか、サンプルデータで試しながら基礎を勉強します。特に、キーが重複する場合や存在しない場合の動きを見れるようにします。
使用する pandas バージョン:3.0.3
サンプルデータ
merge を試す2つの DataFrame a, b について、同じキーを持つ行が a には M 個、 b には N 個だけ存在するようにデータを用意します(M, N ∈ {0, 1, 2})。このキーの名前は aMbN と機械的に付けます。
これにより、 M:N という組み合わせを 1:1 や 0:2 など網羅的に試せるほか、結合後にキーを見るだけで結合前の個数をすぐ特定できます。
import pandas as pd
a = pd.DataFrame(
[
["a1b0", 1],
["a1b1", 1],
["a1b2", 1],
["a2b0", 1], ["a2b0", 2],
["a2b1", 1], ["a2b1", 2],
["a2b2", 1], ["a2b2", 2],
],
columns=["key", "val_a"]
)
b = pd.DataFrame(
[
["a0b1", 1],
["a1b1", 1],
["a2b1", 1],
["a0b2", 1], ["a0b2", 2],
["a1b2", 1], ["a1b2", 2],
["a2b2", 1], ["a2b2", 2],
],
columns=["key", "val_b"]
)
DataFrame を表示
>>> a
key val_a
0 a1b0 1
1 a1b1 1
2 a1b2 1
3 a2b0 1
4 a2b0 2
5 a2b1 1
6 a2b1 2
7 a2b2 1
8 a2b2 2
>>> b
key val_b
0 a0b1 1
1 a1b1 1
2 a2b1 1
3 a0b2 1
4 a0b2 2
5 a1b2 1
6 a1b2 2
7 a2b2 1
8 a2b2 2
JOINの種類
以下のものを試し、 how の違いで結合がどのようになるのか見ていきます。
a.merge(b, how="inner", on="key")
a.merge(b, how="outer", on="key")
a.merge(b, how="left" , on="key")
a.merge(b, how="right", on="key")
a.merge(b, how="cross")
a.merge(b, how="left_anti" , on="key")
a.merge(b, how="right_anti", on="key")
inner
how を指定しなかった場合のデフォルトです。
>>> a.merge(b, how="inner", on="key")
key val_a val_b
0 a1b1 1 1
1 a1b2 1 1
2 a1b2 1 2
3 a2b1 1 1
4 a2b1 2 1
5 a2b2 1 1
6 a2b2 1 2
7 a2b2 2 1
8 a2b2 2 2
列は a, b にあったものが合体しています。 on に指定した列 key は1個に纏まっています。
キーの値でグループ分けして、行の個数や内容を見てみます。
-
a1b1は元の DataFramea,bにあったものをそのままマージした形です -
a1b2はaだと 1 個でしたが、結合後はbと同じ 2 個に増えています -
a2b2のキーを見ると、 [val_a, val_b] は [1, 1], [1, 2], [2, 1], [2, 2] と全ての組み合わせが登場しています -
a0b2のような片方に含まれないキーは消えています
つまり、 M:N のデータを持つキーは M×N 行のデータになります。
outer
>>> a.merge(b, how="outer", on="key")
key val_a val_b
0 a0b1 NaN 1.0
1 a0b2 NaN 1.0
2 a0b2 NaN 2.0
3 a1b0 1.0 NaN
4 a1b1 1.0 1.0
5 a1b2 1.0 1.0
6 a1b2 1.0 2.0
7 a2b0 1.0 NaN
8 a2b0 2.0 NaN
9 a2b1 1.0 1.0
10 a2b1 2.0 1.0
11 a2b2 1.0 1.0
12 a2b2 1.0 2.0
13 a2b2 2.0 1.0
14 a2b2 2.0 2.0
前節の inner と比べると、 M または N が 0 のキーが追加されていて、 val_a, val_b のうち存在しないほうは NaN となっています(この影響で他のデータも整数から浮動小数点数に変化していますが、今回は無視します)。 M, N の両方が1以上のキーについては、 inner と同じ M×N 行のデータです。
もし結合前からデータを埋めたと捉えるなら、 a, b それぞれが、存在しない各キーについて欠損値データを1行ずつ加えたことになります。
データを埋めてから INNER JOIN してみる
a には a0b1 と a0b2 を、 b には a1b0 と a2b0 を加えてみます。
a_ = pd.DataFrame(
[
["a0b1", None],
["a0b2", None],
["a1b0", 1],
["a1b1", 1],
["a1b2", 1],
["a2b0", 1], ["a2b0", 2],
["a2b1", 1], ["a2b1", 2],
["a2b2", 1], ["a2b2", 2],
],
columns=["key", "val_a"]
)
b_ = pd.DataFrame(
[
["a1b0", None],
["a2b0", None],
["a0b1", 1],
["a1b1", 1],
["a2b1", 1],
["a0b2", 1], ["a0b2", 2],
["a1b2", 1], ["a1b2", 2],
["a2b2", 1], ["a2b2", 2],
],
columns=["key", "val_b"]
)
これらのデータを INNER JOIN すると、以下のように outer と同じ結果が得られます。例えば a0b2 は元々 0:2 でしたが、事前に1行加えたことで 1:2 になり2行できています。
>>> a_.merge(b_, how="inner", on="key")
key val_a val_b
0 a0b1 NaN 1.0
1 a0b2 NaN 1.0
2 a0b2 NaN 2.0
3 a1b0 1.0 NaN
4 a1b1 1.0 1.0
5 a1b2 1.0 1.0
6 a1b2 1.0 2.0
7 a2b0 1.0 NaN
8 a2b0 2.0 NaN
9 a2b1 1.0 1.0
10 a2b1 2.0 1.0
11 a2b2 1.0 1.0
12 a2b2 1.0 2.0
13 a2b2 2.0 1.0
14 a2b2 2.0 2.0
left, right
outer が分かればこれらは楽です。
>>> a.merge(b, how="left", on="key")
key val_a val_b
0 a1b0 1 NaN
1 a1b1 1 1.0
2 a1b2 1 1.0
3 a1b2 1 2.0
4 a2b0 1 NaN
5 a2b0 2 NaN
6 a2b1 1 1.0
7 a2b1 2 1.0
8 a2b2 1 1.0
9 a2b2 1 2.0
10 a2b2 2 1.0
11 a2b2 2 2.0
>>> a.merge(b, how="right", on="key")
key val_a val_b
0 a0b1 NaN 1
1 a1b1 1.0 1
2 a2b1 1.0 1
3 a2b1 2.0 1
4 a0b2 NaN 1
5 a0b2 NaN 2
6 a1b2 1.0 1
7 a1b2 1.0 2
8 a2b2 1.0 1
9 a2b2 2.0 1
10 a2b2 1.0 2
11 a2b2 2.0 2
outer と比べると、指定した左(a)または右(b)では元のデータを尊重し NaN を追加しないようになっています。
cross
他と異なり、 on などキーに関する指定があるとエラーとなります。
>>> a.merge(b, how="cross", on="key")
pandas.errors.MergeError: Can not pass on, right_on, left_on or set right_index=True or left_index=True
指定をやめると大量の行が作られます。
>>> a.merge(b, how="cross")
key_x val_a key_y val_b
0 a1b0 1 a0b1 1
1 a1b0 1 a1b1 1
2 a1b0 1 a2b1 1
3 a1b0 1 a0b2 1
4 a1b0 1 a0b2 2
.. ... ... ... ...
76 a2b2 2 a0b2 2
77 a2b2 2 a1b2 1
78 a2b2 2 a1b2 2
79 a2b2 2 a2b2 1
80 a2b2 2 a2b2 2
[81 rows x 4 columns]
これは両方のデータの行の組み合わせを全て作っているためです。今回は9行同士なので 9×9 = 81 行となります。
ここからキーが一致するものだけ抽出すると、 inner と同様の結果が得られます。
>>> a.merge(b, how="cross").query("key_x == key_y")
key_x val_a key_y val_b
10 a1b1 1 a1b1 1
23 a1b2 1 a1b2 1
24 a1b2 1 a1b2 2
47 a2b1 1 a2b1 1
56 a2b1 2 a2b1 1
70 a2b2 1 a2b2 1
71 a2b2 1 a2b2 2
79 a2b2 2 a2b2 1
80 a2b2 2 a2b2 2
left_anti, right_anti
pandas 3.0 で追加されたということで、この機会に試してみます。
>>> a.merge(b, how="left_anti", on="key")
key val_a val_b
0 a1b0 1 NaN
4 a2b0 1 NaN
5 a2b0 2 NaN
>>> a.merge(b, how="right_anti", on="key")
key val_a val_b
0 a0b1 NaN 1
4 a0b2 NaN 1
5 a0b2 NaN 2
left, right の結果のうち、相手が存在しないものだけが抽出されています。 inner にあるものを取り除いたとも言えます。
まとめ
pandas で DataFrame の merge を色々と試し、 JOIN の種類について勉強しました。
cross を除くと、各 JOIN は同じキーの中で左右のデータを組み合わせて新しいデータを作ります。
inner と outer (および left/right)の違いは、キーが片方にしか無いところに現れます。
- 両方にあるキーについては同じで、元が M:N なら M×N 行のデータになります
- 片方にしか無いキーは、
- inner なら消えます(M×0 行または 0×N 行)
- outer なら欠損値データで補います(M×1 行または 1×N 行)
- left/right なら相手側だけ欠損値データで補います