こんにちは!
今回は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つのデータフレームがあります。
key1
とkey2
の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つのデータフレームを例に説明します。
key1
とkey2
の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
が次のように002foo
、003bar
という同じ値で紐づいてしまいます。
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 |
なので、通常はkey1
、key2
のどちらにも絶対に含まれない文字を区切り文字にして文字列連結するのが定石です。
例えば、次のように\
を区切り文字にして文字列連結します。
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']
も省略可です。
(ただし私は、可読性、メンテナンス性からhow
やon
は省略しない派です。
抽出が目的なのに高コストなmerge()
関数を使うのはどんくさい気がします。
(と偉そうなことを言っていますが、実は私はこの方法から始めました・・・)
また、df1
からdf2
とキーが一致しない行を抽出する目的でmerge()
関数を使う方法は考えつきませんでした。
ということで、[方法3]はあまりおススメの方法ではありません。
サンプルコード
これまでに説明したデータフレームでつくったサンプルコードを掲載します。
まとめ
今回は「複数のカラムが一致する行/一致しない行を抽出する」ための方法をご紹介しました。
方法としては次の3つがありました。
- [方法1] MultiIndex で
isin()
を使う方法 - [方法2] 一時的なカラムを作って
isin()
を使う方法 - [方法3]
pd.merge()
を使う方法 (ただし一致するときのみ可)
私のおススメは[方法1]、何らかの事情で結合キーをインデックスにできないなら[方法2]がおススメでした。
可読性、メンテナンス性を確保した分析や開発のコーディングに役立てば嬉しいです!
なお、Pandasを含め、Pythonには様々なコツが必要なシーンが多々あります。
Python、Pandas、データ分析、に関するコツなどを次の記事にまとめてありますので、是非読んでみてください!
intellista.hatenablog.com