見出し画像

pandasのDataFrameで発生する SettingwithCopyWarningの原因と解決方法

自分がこの問題にあたり、なかなか理解ができずに解決方法を見つけるまでに時間がかかったためここに書いておきます

現象

DataFrameを作って行を選択したりした後に代入をしたら以下のようなSettingwithCopyWarningというワーニングが表示されてしまう。

import pandas as pd
import numpy as np

# 適当なDataFrame
In [1]: df = pd.DataFrame(np.arange(6).reshape(3,2), columns=list('AB'))

# DataFrameの中身を確認
In [2]: df
Out[2]:
  A  B
0  0  1
1  2  3
2  4  5

# 行を選択する
In [3]: df2 = df.loc[:1]

# 選択した2行を確認
In [4]: df2
Out[4]:
  A  B
0  0  1
1  2  3

# 1行目に値を代入しようとする。と、ワーニングが表示されてしまう
In [5]: df2.loc[1:] = 4
/Users/shohei.hagiwara/miniconda3/lib/python3.8/site-packages/pandas/core/indexing.py:670: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
 iloc._setitem_with_indexer(indexer, value)
<ipython-input-48-eefb948c0879>:1: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

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

原因

まず、DataFrameの行や列の一部を選択したときの動作を理解する必要があります。

df2 = df.loc[:1]

などとしたときにdf2に入るのは、もとのDataFrameの部分への参照か、または、コピーへの参照かどちらかわからないが、どちらかが入ります。

なぜわからないのかというと歴史的な経緯や実装上の都合などあるらしいのですが、詳しくは僕も知りません。とりあえずここではどちらかが入るということだけを理解しておくことにします。

この、どちらかがわからない状態で、このdf2の中の行や列に代入をしようとすると「あ、df2ってdfへの参照かもしれないからdfまで変わってしまうかもしれないし、もしかしたらdf2はコピーであってdfへは変更が反映されないかもしれないけど、だいじょうぶ?」という警告をPythonが出してくれているのがSettingwithCopyWarningになります。

解決方法

意図を明確にするようにコーディングすることでワーニングが出なくなります。上記の例で言えば、df2だけに変更を反映させたいのであればdf.copyを使って、コピーを作成したいという意図を明確にします。逆に、dfに変更を反映させたいということであれば、dfに対して変更をかけるのがよいでしょう。

1. コピーする意図を明確にする場合

# コピーを明示的に行う
In [1]: df2 = df.loc[:1].copy()

# コピーに代入してもワーニングが出なくなる
In [2]: df2.loc[1:] = 4

2. dfに対して変更をかける場合

# dfを変更することが明確であれば、dfに対して直接変更を行う
In [1]: df.loc[1] = 4

実はワーニングが出ないことも、、、

これが実は一番厄介でありこのワーニングを理解するのに苦労する点なのですが、実はこのワーニングが出ないケースもあります。GitHubにissueもあがっており2015年からあるようなのですが直っていないようです。https://github.com/pandas-dev/pandas/issues/9767

以下、issueから拝借したコードにコメントを加えたものです。コピーに対して値を代入しているのですがワーニングが出るケースと出ないケースがあることがわかります。

# dfを準備
In [1]: df = pd.DataFrame({"A" : [1,2,3,4,5], "B": [3.1, 4, 5, 6, 7]})

# ワーニングが出ないケース
In [2]: df[["A", "B"]]["A"] = 5  # no warning - bug?!

# コピーに対して代入したため値が変わっていない
In [3]: df
Out[3]:
  A    B
0  1  3.1
1  2  4.0
2  3  5.0
3  4  6.0
4  5  7.0

# こちらはワーニングが出る
In [4]: df[["A"]]["A"] = 5
<ipython-input-63-a98d0e811898>: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[["A"]]["A"] = 5

まとめ

SettingwithCopyWarningは、元のDataFrameからスライスなどで取得した行や列が、元のDataFrameへの参照なのか、それともコピーへの参照なのかがわからないために発生するワーニングだということを見てきました。元のDataFrameを変更するのか、それともコピーなのかを明示的にコーディングすることで解決ができます。しかし、GitHubのissueにもあがっているようにワーニングがでないこともあるようです。

ワーニングがでたときに対応するのではなく、DataFrameの行や列に代入をするときには常に「それは元のDataFrameに代入をしたいのか、それともコピーだけに代入をしたいのか」に気をつけてコーディングすることが大切です。

よく考えてみると、これはDataFrameに限った話ではなく普段の変数の扱いでも同じだと思います。「この変数はなにか他のオブジェクトの一部ではないか?それとも関係のない独立の変数だから変更しても問題ないか?」といったことは普段から考えながらコーディングをしていると思います。

そのような一般的なことをDataFrameでも気をつけましょう、というお話でしたとまとめられるのかもしれません。

参考

この記事を書く際にとても参考にさせていただいた記事です。より詳しいことが載っていますので興味ある方はぜひ。
SettingwithCopyWarning: How to Fix This Warning in Pandas
https://www.dataquest.io/blog/settingwithcopywarning/#

お役に立ちましたぜひスキをお願いいたします。もっと記事を書くモチベーションになります


この記事が気に入ったらサポートをしてみませんか?