intellista

engineer's notes about application development, data analysis, and so on

Pandasで複数のカラムが一致する行/一致しない行を抽出したい


こんにちは!
今回はPandasの話です。

データフレームが2つあるとき、一致する行(または一致しない行)を抽出したいことはありませんか?
このとき、一致するかしないかは、特定のカラムをキーにして比較します。

キーが1つであればisin()関数を使えば簡単にできます。
例えば、次の2つのデータフレームがあります。
keyカラムを結合キーとします。

df1

key value
000 70
001 64
002 87
003 26

df2

key time
002 2021-08-01 08:00:00
003 2021-08-01 08:01:00
004 2021-08-01 08:02:00

このとき、一致する行はdf1[df1.key.isin(df2.key.values)]とすれば、次のように抽出できます。

key value
002 87
003 26

一致しない行はdf1[~df1.key.isin(df2.key.values)]と、条件にチルダ~をつけて否定すれば、次のように抽出できます。

key value
000 70
001 64

とても簡単です。

しかし、キーが2つになると、こう簡単にはいきません。
例えば、次の2つのデータフレームがあります。
key1key2の2つのカラムを結合キーとします。

df1

key1 key2 value
000 foo 70
001 bar 64
002 foo 87
003 bar 26

df2

key1 key2 time
002 foo 2021-08-01 08:00:00
003 bar 2021-08-01 08:01:00
004 foo 2021-08-01 08:02:00
005 bar 2021-08-01 08:03:00

このとき、一致する行を抽出したくてもdf1[df1['key1', 'key2'].isin(df2['key1', 'key2'])]とは書けません。
(エラーになります。)

ではどうすればよいのでしょうか?

結論からすると、ちょっと工夫すればできます。
ということで、今回はこの「複数のカラムが一致する行/一致しない行を抽出する」ための方法をご紹介します。

複数のカラムをキーにして行を抽出する方法

次の2つのデータフレームを例に説明します。
key1key2の2つのカラムを結合キーとします。

df1

key1 key2 value
000 foo 70
001 bar 64
002 foo 87
003 bar 26

df2

key1 key2 time
002 foo 2021-08-01 08:00:00
003 bar 2021-08-01 08:01:00
004 foo 2021-08-01 08:02:00
005 bar 2021-08-01 08:03:00

df1からdf2とキーがキーが一致する行を抽出すると、次のデータフレームが得られます。

key1 key2 value
002 foo 87
003 bar 26

df1からdf2とキーがキーが一致しない行を抽出すると、次のデータフレームが得られます。

key1 key2 value
000 foo 70
001 bar 64

方法としては次の3つがあります。

  • [方法1] MultiIndex で isin() を使う方法
  • [方法2] 一時的なカラムを作って isin() を使う方法
  • [方法3] pd.merge() を使う方法 (ただし一致するときのみ可)

私のおススメは[方法1]、何らかの事情で結合キーをインデックスにできないなら[方法2]がおススメです。

[方法1] MultiIndex で isin() を使う方法

2つのデータフレームのインデックスを使っていないのであれば、[方法1]がシンプルでエレガントだと思います。
[方法1]は、次のように結合キーを階層型インデックス(MultiIndex)にして抽出する方法です。
df1からdf2とキーが一致する行を抽出します。

df1i = df1.copy().set_index(['key1', 'key2'])
df2i = df2.copy().set_index(['key1', 'key2'])
df = df1i[df1i.index.isin(df2i.index.values)]

初めから結合キーがインデックスになっていれば、3行目だけでOKです。
とてもシンプルです。

df1からdf2とキーが一致しない行を抽出するには、3行目の条件をチルダ~で否定するだけです。

df1i = df1.copy().set_index(['key1', 'key2'])
df2i = df2.copy().set_index(['key1', 'key2'])
df = df1i[~df1i.index.isin(df2i.index.values)]

[方法2] 一時的なカラムを作って isin() を使う方法

何らかの事情で結合キーをインデックスにできなければ[方法2][方法3]となります。

[方法2]は、複数の結合キーを1つのカラムにまとめた一時的な結合キーのカラムをつくり、isin()で抽出する方法です。
df1からdf2とキーが一致する行を抽出します。

df = df1.copy()
df['key'] = df['key1'] + df['key2']
df1tmp = df

df = df2.copy()
df['key'] = df['key1'] + df['key2']
df2tmp = df

df = df1tmp[df1tmp['key'].isin(df2tmp['key'])]

ちょっとコード量が多めですね。
一時カラムというのも美しくないです。

df1からdf2とキーが一致しない行を抽出するには、3行目の条件をチルダ~で否定するだけです。

df = df1.copy()
df['key'] = df['key1'] + df['key2']
df1tmp = df

df = df2.copy()
df['key'] = df['key1'] + df['key2']
df2tmp = df

df = df1tmp[~df1tmp['key'].isin(df2tmp['key'])]
注意点

df['key'] = df['key1'] + df['key2']には注意が必要です。

df['key1'] + df['key2']のように単純に文字列連結すると、もともと別のキーなのに文字列連結後は一致してしまう可能性もあります。
例えば、
df1

key1 key2 value
002fo o 87
00 3bar 26

df2

key1 key2 time
00 2foo 2021-08-01 08:00:00
003 bar 2021-08-01 08:01:00

となっていたら、

df = df1.copy()
df['key'] = df['key1'] + df['key2']
df1tmp = df

df = df2.copy()
df['key'] = df['key1'] + df['key2']
df2tmp = df

により、結合キーのカラムkeyが次のように002foo003barという同じ値で紐づいてしまいます。
df1tmp

key1 key2 value key
002fo o 87 002foo
00 3bar 26 003bar

df2tmp

key1 key2 time key
00 2foo 2021-08-01 08:00:00 002foo
003 bar 2021-08-01 08:01:00 003bar

なので、通常はkey1key2のどちらにも絶対に含まれない文字を区切り文字にして文字列連結するのが定石です。
例えば、次のように\を区切り文字にして文字列連結します。

df['key'] = df['key1'] + '\\' + df['key2']

さらにこのとき、df['key1']df['key2']が文字列でない(数値など)の場合、+演算子でエラーになります。
(Pythonでは、文字列と文字列以外は+演算子で連結できないためです。JavaScriptならうまいことやってくれるのに・・・面倒ですね。)
ですので、実は次のように書くのが定石です。

df['key'] = df['key1'].str.cat(df['key2'], sep='\\')

これをdf['key'] = str(df['key1']) + '\\' + str(df['key2'])と書いてしまうと、シリーズdf['key1']全体の文字列表現とシリーズdf['key2']全体の文字列表現を文字列連結してしまうので注意が必要です。

文字列連結の話は今回の記事の本筋ではないので、説明のコードの例では省略しています。

[方法3] pd.merge() を使う方法 (ただし一致するときのみ可)

[方法3]は、いっそPandasのmerge()関数を使ってしまうという方法です。
df1からdf2とキーが一致する行を抽出します。

df = pd.merge(
    df1,
    df2,
    on=['key1', 'key2'],
    how='inner'
)

今回の目的では内部結合なのでhow='inner'は省略可、結合キーのカラム名も一致ししていればon=['key1', 'key2']も省略可です。
(ただし私は、可読性、メンテナンス性からhowonは省略しない派です。
抽出が目的なのに高コストなmerge()関数を使うのはどんくさい気がします。
(と偉そうなことを言っていますが、実は私はこの方法から始めました・・・)

また、df1からdf2とキーが一致しない行を抽出する目的でmerge()関数を使う方法は考えつきませんでした。

ということで、[方法3]はあまりおススメの方法ではありません。

サンプルコード

これまでに説明したデータフレームでつくったサンプルコードを掲載します。


まとめ

今回は「複数のカラムが一致する行/一致しない行を抽出する」ための方法をご紹介しました。

方法としては次の3つがありました。

  • [方法1] MultiIndex で isin() を使う方法
  • [方法2] 一時的なカラムを作って isin() を使う方法
  • [方法3] pd.merge() を使う方法 (ただし一致するときのみ可)

私のおススメは[方法1]、何らかの事情で結合キーをインデックスにできないなら[方法2]がおススメでした。

可読性、メンテナンス性を確保した分析や開発のコーディングに役立てば嬉しいです!

なお、Pandasを含め、Pythonには様々なコツが必要なシーンが多々あります。
Python、Pandas、データ分析、に関するコツなどを次の記事にまとめてありますので、是非読んでみてください!
intellista.hatenablog.com