DataFrameでSettingWithCopyWarningの意味と対処法

`Pandas`の DataFrame で`SettingWithCopyWarning`の警告の意味と対処方法について書きます。
DataFrame使っていると`SettingWithCopyWarning`によく遭遇していました。その度にその場しのぎの修正をして対応していましたが、さすがにそろそろ根本的に理解しないと時間がもったいないと思い、この記事で整理しました。
[公式`Pandas`のヘルプ](https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy)と偉大な先人が書いてくれた以下の記事が非常にわかりやすいです。この記事では、簡潔に内容を書きます。

https://linus-mk.hatenablog.com/entry/2019/02/02/200000

# 結論
結論から先に述べます。
**暗黙のCopyをやめる。そのためにChain Indexingをやめる**。警告をなくすにはそれだけです。

# 現象
`SettingWithCopyWarning`が起こる一例を最初に紹介します。
シンプルなDataFrameを作ります。

```python
import pandas as pd
df = pd.DataFrame([[1, 11],
                   [2, 11],
                   [3, 33]],
                 columns=['A', 'B'],
                 index=['a', 'b', 'c'])
print(df)
```

```:DataFrameの最初の中身
   A   B
a  1  11
b  2  11
c  3  33
```

で、B列の値が11のA列の値を代入しようとします。

```python
df[df['B']==11]['A'] = 4
print(df)
```

値は変わりません。

```:DataFrameの最初の中身
   A   B
a  1  11
b  2  11
c  3  33
```

で、`SettingWithCopyWarning`が出力。

```python
<ipython-input-62-77f8496ed860>:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df[df['B']==11]['A'] = 4
```

# 警告が起こる前提知識(View or Copy)
`SettingWithCopyWarning`を理解するためには、DataFrame処理の種類を知る必要があります。
PandasはDataFrameを処理するときにViewまたはCopyを返します。
ViewとCopyで以下のように動きが異なります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/2a2aaa45-8a0d-03b3-b2d3-699b86fc9407.png)
**上図の右側のCopyに対して値代入をした場合に`SettingWithCopyWarning`**が発生し、元のDataFrameへの値代入が行われません。理由は元のDataFrameと別メモリのオブジェクトだからです。
つまり、**値代入時にCopyではなくViewを使うことで警告がなくなり、問題が発生しません**。

# 警告発生パターンと対処法
`Pandas`はCopyを返すかViewを返すかの関数が明示的に決まっているわけでないので、問題を単純化できません。そのため、多くの人が悩みます。詳しくは[記事「pandasのSettingWithCopyWarningを理解する (3/3)」](https://linus-mk.hatenablog.com/entry/2019/05/18/161328)の「歴史」章を読んでください。

## Chained Indexing
### Chained Indexingの再現条件
最も基本的な`SettingWithCopyWarning`が発生するパターンはChained Indexingと呼ばれる処理です。
下記のように`df[df['B']==11]`という抽出処理に、さらに`['A']`の抽出を重ねて(Chainして)います。この場合はViewではなくCopyとなるみたいです。また、警告が発生する以外にもそもそもパフォーマンスが悪く望ましくない書き方です。
ブールインデックスを使っていますが、`query`メソッドでも同じです。
※[記事「DataFrameレシピ: データ抽出条件」](https://qiita.com/FukuharaYohei/items/c3cc966b11f5fc3ed0bf)にブールインデックスや`query`関数を使った抽出方法も書いています。

```python
df[df['B']==11]['A'] = 4
print(df)
```

実行しても、Copyなので元のDataFrameである`df`の値は変化がありません。

```:実行結果
   A   B
a  1  11
b  2  11
c  3  33
```

で、警告が出力されます。

```python:警告内容
<ipython-input-62-77f8496ed860>:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df[df['B']==11]['A'] = 4
```

### Chained Indexing対処法
警告で出ている`Try using .loc[row_indexer,col_indexer] = value instead`をそのまま実施。
`loc`プロパティを使います。

```python
df.loc[df['B']==11, ['A']] = 4
print(df)
```

警告は消え、A列の値が置き換わっているのがわかります。

```:実行結果
   A   B
a  4  11
b  4  11
c  3  33
```

## 隠れ連鎖
一見するとChained Indexingを使っていないように見える隠れた連鎖(Chain)です。

### 隠れ連鎖の再現条件
今度は、copyしたDataFrameに対して代入をした場合です。`df2`という`df`のCopyを作り、列`A`に値の代入をします。`df2['A'] = 4`部分だけを見るとなぜ警告が出るのか理解できないパターンです。

```python
df2 = df[df['B']==11]
df2['A'] = 4
print('--df--')
print(df)
print('--df2--')
print(df2)
```

`df`の値は変わっておらずCopyである`df2`は値が変更されます。

```:実行結果
--df--
   A   B
a  1  11
b  2  11
c  3  33
--df2--
   A   B
a  4  11
b  4  11
```

警告が出力されます。

```python:警告内容
<ipython-input-82-a08d45cf3537>:2: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df2['A'] = 4
```

上記の警告を見て単純に`loc`使っても警告は出たままです。

```python
df2 = df[df['B']==11]
df2.loc[:,'A'] = 4  # df2['A'] = 4 から変更
print('--df--')
print(df)
print('--df2--')
print(df2)
```

```:実行結果(さっきと変わらず)
--df--
   A   B
a  1  11
b  2  11
c  3  33
--df2--
   A   B
a  4  11
b  4  11
```

警告内容は少し変わっています。

```python:警告内容
/home/i348221/Apps/python/venv/py39/lib/python3.9/site-packages/pandas/core/indexing.py:1843: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  self.obj[item_labels[indexer[info_axis]]] = value
```

### 隠れ連鎖の対処法 
`copy`関数を使って`df2`は`df`のCopyだということ明示的に処理すれば警告は消えます。(熟考していませんが)おそらく、元のDataFrameである`df`の値も変えたいことは無いのではないでしょうか。

```python
df2 = df[df['B']==11].copy()
df2['A'] = 4 
print('--df--')
print(df)
print('--df2--')
print(df2)
```

実行結果は、警告が消える以外は変わりません。

```:実行結果
--df--
   A   B
a  1  11
b  2  11
c  3  33
--df2--
   A   B
a  4  11
b  4  11

```

# おまけ(偽陰性)
警告が出ないけど、値代入が失敗するパターンもあります。やはりChain Indexingが原因です。警告出ないので普段からChain Indexをしない癖をつけておくことが大事みたいです。
※偽陰性(False Negative)の意味は[別記事「【入門者向け】機械学習の分類問題評価指標解説(正解率・適合率・再現率など)」](https://qiita.com/FukuharaYohei/items/be89a99c53586fa4e2e4)を参照ください。

```python
df.loc[df.B == 11, ('A', 'B')]['A'] = 4
print(df)
```

```:実行結果
   A   B
a  1  11
b  2  11
c  3  33
```

Chain Index やめればもちろん代入成功です。

```python
df.loc[df.B == 11, 'A'] = 4
print(df)
```

```:実行結果
   A   B
a  4  11
b  4  11
c  3  33
```

他にも偽陽性(警告出る必要ないのに誤って警告出力)パターンもあるらしいですが、最新のPandasで出るかわからないので、確認していません。