Pandasで重複除外してから結合(Merge)する方法について(duplicatedメソッド)

# はじめに

PandasでDataFrameを結合(Merge)するときに、マスタだと思っているデータ側が実は一意でなかったら、どうやって結合すればよいかについて考えてみます。

# 実際にやってみる

```python
import numpy as np
import pandas as pd
```

## トランザクションデータの作成

まずはトランザクションデータとして、注文データ`transaction`を作成します。

```python
transaction = pd.DataFrame({
    '注文番号': ['C1201-0001', 'C1201-0002','C1201-0003' , 'C1201-0004'],
    '注文数': [4, 5, 6, 1],
    '商品コード': ['S0000001', 'S0000002', 'S0000003', 'S0000005']
})
transaction
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1110109/f84724f9-5535-b3ff-01a4-375afd27c58a.png)

実行すると、4件の注文データがあることがわかります。このあと作成する商品マスタを見てもらうとわかりますが、注文番号`C1201-0004`の商品コード`S0000005`は商品マスタにないコードです。

## マスタデータの作成

次にマスタデータとして、商品マスタを作成します。

```python
master = pd.DataFrame({
    '商品コード': ['S0000001', 'S0000002', 'S0000002', 'S0000003', 'S0000004'],
    '商品名': ['商品１', '商品２(旧価格)', '商品２(新価格)', '商品３', '商品４'],
    '単価': [10000, 20000, 30000, 40000, 50000]
})
master
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1110109/db972ec5-9c7d-b8b7-68df-4280bfb6eb5b.png)

商品コード`S0000002`が2行あることがわかります。商品名を見る限り、これらは同じ商品で一つは旧価格でもう一つは新価格のようです。
ですが、これによりこの商品マスタは商品コードでは一意になりません。

## マージしてみる

`how='inner'`で結合すると、存在しない商品コードを持つ`C1201-0004`が消えてしまうので、`how='left'`で結合します。

```python
pd.merge(transaction, master, how='left', on='商品コード')
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1110109/7997a244-e557-b506-784a-88b4f3ffd887.png)

注文番号`C1201-0004`は消えませんでしたが、注文番号`C1201-0002`が2レコードに増幅してしまっています。これでは正しく計算できません。

## 他の結合方法も試してみる

結論からいうと、いずれも期待した結果にならないので駄目ですが、念のため、他の方法も試してみます。

```python
pd.merge(transaction, master, how='inner', on='商品コード')
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1110109/8966444a-ba9e-8f25-6bdf-0805220e2126.png)

```python
pd.merge(transaction, master, how='right', on='商品コード')
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1110109/c669a6a3-2869-e57b-3ef0-dd5374aa7edb.png)

```python
pd.merge(transaction, master, how='outer', on='商品コード')
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1110109/443e4011-8f2c-a04e-63a3-9815b3fac8f0.png)

## ではどうするか

いくつかやり方はあると思いますが、ここでは商品マスタから重複するデータを除外してからマージすることにします。`duplicated`メソッドを使って重複している商品コードをあぶり出して除外してみます。
入門書ではあまり見かけないメソッドなので、1つずつ順番に確認していきます。

まずは、商品マスタのキーである商品コードを取り出します。

```python
master['商品コード']
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1110109/7acf644a-cf2a-5fa5-c239-16030216f37a.png)

この時点でインデックス`1`と`2`の商品コードが同じであることがわかります。
ちなみに、キー(PK)が複数あるときは、

```python
master[['キー１', 'キー２']]
```

のようにリストで複数並べてください。

次に、このSeriesのどのインデックスが重複しているかを`duplicated`メソッドを用いて表示してみます。

```python
master['商品コード'].duplicated()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1110109/3e612ee0-b89d-35e3-66a8-9379e4a507a6.png)

これをみると、重複しているインデックス`1`と`2`のうち、`2`の方が`True`となりました。
`True`や`False`が返ってくるということは、これを使って商品マスタをフィルタすることができます。ただし、`True`となっているインデックスが残り、`False`となっているインデックスが消えるので、`~`を使って反転させます。

```python
~master['商品コード'].duplicated()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1110109/3300107e-4478-03ac-c268-00deb5162c6d.png)

なんとなく、これで重複除外できそうなので、商品マスタをフィルタしてみます。

```python
master[~master['商品コード'].duplicated()]
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1110109/bf62f24d-1c65-7022-21c0-a219066c178d.png)

商品コード`S0000002`が行になり、重複が除外されましたが、ここで1つ気になる点があります。
商品コード`S0000002`の商品名が`商品２(旧価格)`になっている点です。
これは`duplicated`メソッドの引数`keep`のデフォルト値が`first`になっているためで、重複した複数のインデックスのうち、最初のインデックスを`False`(キープ)と判定し、それ以外を`True`(重複)と判定したものを`~`で反転したさせたためです。
ここでは最新価格の方を使いたいということにして、`keep='last'`(最後をキープする)を指定します。

少し周りくどいですが、もう一度`True`と`False`のどちらに判定されるか見てみます。

```python
master['商品コード'].duplicated(keep='last')
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1110109/74c877b6-13a3-dffa-ed9f-679b52cc8852.png)

重複しているインデックス`1`と`2`のうち、最初のインデックス`1`が`True`(重複)となり、最後のインデックス`2`が`False`(キープ)されていることがわかります。
では、再度反転してフィルタしてみます。

```python
master[~master['商品コード'].duplicated(keep='last')]
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1110109/4cab9cf9-186a-2b4e-7749-8f1de31283f7.png)

これで、重複した商品コード`S0000002`の重複が除外され、最新価格の方だけが残りました。

最後に、これを使って結合(Merge)してみます。

```python
pd.merge(transaction, master[~master['商品コード'].duplicated(keep='last')], how='left', on='商品コード')
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1110109/397ee376-03d6-e202-0c4e-5c707c0fbc22.png)

期待通りに結合ができました。

# 最後に

やり方の説明のために注文データと商品マスタを使いましたが、実務的にはこれでは駄目です。
なぜかと言うと、注文番号`C1201-0002`で販売した商品が`S0000002`であることは正しいと思いますが、どちらの価格で販売したのかがわかりません。ここでは新価格を適用しましたが、旧価格で販売していたら売上金額にズレが生じます。
普通は価格が変わるごとに商品コードを変えたり、商品コード＋価格コード等の複合キーの形で一意になるようにするなどするので、この例のようなマスタの持ち方でDBに格納されていることはありえません。

あくまでも`duplicated`メソッドを使った重複除外の例としてみていただけたら幸いです。