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にクローズされました