Closed6
pandasで複数カラムの値が一致する行を抽出する

こういった2つのDataFrame、df1とdf2の共通カラムを持つ行を抽出したい。
import pandas as pd
# NOTE: [3, 4], [5, 6], [7, 8]が共通行
df1 = pd.DataFrame([
[1, 2],
[3, 4], # 共通行
[5, 6], # 共通行
[7, 8], # 共通行
], columns=["a", "b"])
df2 = pd.DataFrame([
[3, 4], # 共通行
[5, 6], # 共通行
[7, 8], # 共通行
[9, 0],
], columns=["a", "b"])
想定する共通行
a b
0 3 4
1 5 6
2 7 8

inner mergeする方法
共通行の抽出だけなら比較したいカラムでinner mergeすれば良い。
# NOTE: inner joinで共通カラムを持つ行を抽出する
df_concat = pd.merge(df1, df2, on=["a", "b"], how="inner")
print(df_concat)
出力
a b
0 3 4
1 5 6
2 7 8
ただし、この方法だと一致する行を抽出することはできるが一致しない行の抽出はできない。

行のハッシュ値を作成して抽出する方法
書くカラムをがっちゃんこしてハッシュ値が一致しているかどうかで共通行を抽出する。
pd.DaraFrame.apply(lambda x: hash(tuple(x)), axis=1)
で各行のハッシュ値を算出することができるのでそれを利用する。
# NOTE: 指定カラムのハッシュ値を用いて共通カラムを持つ行を抽出する
# NOTICE: unhashableな型の値が入ってる場合使えないので注意
df1_hashed = df1.copy()
df1_hashed["hash"] = df1_hashed.apply(lambda x: hash(tuple(x)), axis=1)
df2_hashed = df2.copy()
df2_hashed["hash"] = df2_hashed.apply(lambda x: hash(tuple(x)), axis=1)
こんな感じでハッシュ値になる
>>> print(df1_hashed)
a b hash
0 1 2 -3550055125485641917
1 3 4 1079245023883434373
2 5 6 -7007623702649218251
3 7 8 -5025995458930630658
>>> print(df2_hashed)
a b hash
0 3 4 1079245023883434373
1 5 6 -7007623702649218251
2 7 8 -5025995458930630658
3 9 0 -6675171581329655070
>>>
あとはハッシュ値どうしでisinしてやれば共通行を抽出することができる。
df_hashed = df1_hashed[df1_hashed["hash"].isin(df2_hashed["hash"])]
print(df_hashed)
出力
a b hash
1 3 4 1079245023883434373
2 5 6 -7007623702649218251
3 7 8 -5025995458930630658
※この方法なら df1_hashed[~df1_hashed["hash"].isin(df2_hashed["hash"])]
とかすれば不一致行の抽出もできる

(おまけ) MultiIndexを使用する方法
ハッシュ値使う方法とアプローチはほぼ同じ。
(個人的にはちょっとこれは一目見てどんなことやってるかわからないのでハッシュ値使う方法を使いたい)
ググったら出てきたのでついでに投稿。
# NOTE: MultiIndexを使って共通カラムを持つ行を抽出する
# https://stackoverflow.com/questions/45198786/how-to-use-pandas-isin-for-multiple-columns
df1_indexed = df1.set_index(["a", "b"])
df2_indexed = df2.set_index(["a", "b"])
df_indexed = df1_indexed[df1_indexed.index.isin(df2_indexed.index)].reset_index()
print(df_indexed)
出力
a b
0 3 4
1 5 6
2 7 8

outer mergeでindicatorを使う
(indicatorなんて普段使わない引数だから知らんかった…)
df_merged = pd.merge(df1, df2, on=["a", "b"], how="outer", indicator=True)
df_merged = df_merged[df_merged["_merge"] == "both"]
print(df_merged)
出力
a b _merge
1 3 4 both
2 5 6 both
3 7 8 both
pandas/daskで閉じてるならいいけど、便利だけどpolarsやsparkではこのアプローチできないのでちょっと使い勝手悪そう。
(pandas -> pysparkにマイグレーションするPJとかだと結局他の方法にしないといけないし)

(おまけ) ChatGPTに雑に一致しない行を抽出する方法を聞いた
# 2つのDataFrameを結合します
merged_df = pd.merge(df1, df2, on=['a', 'b'], how='outer', indicator=True)
# 結合結果から両方のDataFrameに存在しない行を抽出します
not_matching_rows = merged_df[merged_df['_merge'] != 'both']
# '_merge' カラムは結合の結果を示すために使用されますので、不要なので削除します
not_matching_rows = not_matching_rows.drop('_merge', axis=1)
print(not_matching_rows)
出力
a b
0 1 2
4 9 0
まぁそれっぽいもの吐き出すのね。意外。
このスクラップは2024/02/09にクローズされました