web-dev-qa-db-ja.com

なぜ盲目的にdf.copy()を使用して、SettingWithCopyWarningを修正するのが悪い考えなのか

恐ろしいSettingWithCopyWarningについて無数の質問があります

私はそれがどのようにして起こるかについての良いハンドルを持っています。 (私は良いと言ったが、素晴らしいとは言えない)

これは、データフレームdfが_is_copy_に格納されている属性を介して別のデータフレームに「アタッチ」されている場合に発生します。

ここに例があります

_df = pd.DataFrame([[1]])

d1 = df[:]

d1.is_copy

<weakref at 0x1115a4188; to 'DataFrame' at 0x1119bb0f0>
_

その属性をNoneに設定するか、

_d1 = d1.copy()
_

私は@Jeffのような開発者を見てきましたが、他に誰がそれをやっているかについて警告することはできません。 SettingWithCopyWarningに目的があることを引用します。

質問
それでは、copyを元に戻すことによって警告を無視することが悪い考えである理由を示す具体的な例は何でしょうか。

明確にするために「悪い考え」を定義します。

悪いアイデア
コードを本番環境に配置して、電話での問い合わせにつながるのは悪い考え土曜日の真夜中に、コードが壊れているので修正する必要があると言っています。

NowSettingWithCopyWarningリードをバイパスしてそのような電話をかけるためにdf = df.copy()をどのように使用できるか。これは混乱の元であり、明快さを見つけようとしているので、詳しく説明してほしい。爆破するエッジケースが見たい!

25
piRSquared

ここに私の2セントですが、警告が重要である理由は非常に単純です。

だから私はそのようなdfを作成していると仮定すると

_x = pd.DataFrame(list(Zip(range(4), range(4))), columns=['a', 'b'])
print(x)
   a  b
0  0  0
1  1  1
2  2  2
3  3  3
_

ここで、元のサブセットに基づいて新しいデータフレームを作成し、次のように変更します。

_ q = x.loc[:, 'a']
_

これは元のスライスですと私がそれに何をしてもxに影響します:

_q += 2
print(x)  # checking x again, wow! it changed!
   a  b
0  2  0
1  3  1
2  4  2
3  5  3
_

これは警告があなたに伝えていることです。あなたはスライスで作業しているので、スライスで行うすべてのことが元のDataFrameに反映されます

.copy()を使用すると、元のスライスにはなりませんなので、qで操作を実行してもxは影響を受けません。

_x = pd.DataFrame(list(Zip(range(4), range(4))), columns=['a', 'b'])
print(x)
   a  b
0  0  0
1  1  1
2  2  2
3  3  3

q = x.loc[:, 'a'].copy()
q += 2
print(x)  # oh, x did not change because q is a copy now
   a  b
0  0  0
1  1  1
2  2  2
3  3  3
_

ところで、コピーはqがメモリ内の新しいオブジェクトになることを意味します。スライスがメモリ内の同じ元のオブジェクトを共有する場合

imo、.copy()の使用は非常に安全です。例として、_df.loc[:, 'a']_はスライスを返しますが、_df.loc[df.index, 'a']_はコピーを返します。これは予期しない動作であり、_:_または_df.index_は.loc []のインデクサーと同じ動作をするはずですが、両方で.copy()を使用するとコピーが返されます、より安全に。したがって、元のデータフレームに影響を与えたくない場合は、.copy()を使用してください。

現在.copy() return the DataFrameのディープコピーを使用しています。これは、あなたが話している電話を受け取らないようにするための非常に安全なアプローチです。

しかし、_df.is_copy = None_の使用は、何もコピーしないトリックであり、これは非常に悪い考えです元のDataFrameのスライスで引き続き作業します

人々が知らない傾向があるもう1つのこと:

_df[columns]_ 返される場合がありますビュー。

_df.loc[indexer, columns]_も戻る可能性がありますビュー、ただし、ほとんどの場合実際にはありませんここでmayを強調

13
Steven G

他の回答は警告を無視すべきではない理由についての良い情報を提供しますが、私はあなたの元の質問はまだ回答されていないと思います。

@thnは、copy()の使用は、手元のシナリオに完全に依存することを指摘しています。元のデータを保持する場合は、.copy()を使用します。それ以外の場合は使用しません。 copy()を使用してSettingWithCopyWarningを回避している場合は、ソフトウェアに論理的なバグが発生する可能性があるという事実を無視しています。これがあなたがしたいことであると絶対に確信している限り、あなたは元気です。

ただし、.copy()を盲目的に使用すると、別の問題が発生する可能性があります。これは、実際にはpandas=固有ではありませんが、データをコピーするたびに発生します。

問題をより明確にするために、サンプルコードを少し変更しました。

_@profile
def foo():
    df = pd.DataFrame(np.random.randn(2 * 10 ** 7))

    d1 = df[:]
    d1 = d1.copy()

if __name__ == '__main__':
    foo()
_

memory_profile を使用すると、.copy()がメモリ消費量を2倍にすることがはっきりとわかります。

_> python -m memory_profiler demo.py 
Filename: demo.py

Line #    Mem usage    Increment   Line Contents
================================================
     4   61.195 MiB    0.000 MiB   @profile
     5                             def foo():
     6  213.828 MiB  152.633 MiB    df = pd.DataFrame(np.random.randn(2 * 10 ** 7))
     7                             
     8  213.863 MiB    0.035 MiB    d1 = df[:]
     9  366.457 MiB  152.594 MiB    d1 = d1.copy()
_

これは、元のデータフレームを指す参照(df)がまだ存在するという事実に関連しています。したがって、dfはガベージコレクターによってクリーンアップされず、メモリに保持されます。

本番システムでこのコードを使用している場合、処理するデータのサイズと使用可能なメモリによっては、MemoryErrorが得られる場合と得られない場合があります。

結論として、.copy()blindlyを使用することは賢明ではありません。ソフトウェアに論理的なバグが発生する可能性があるだけでなく、MemoryErrorなどの実行時の危険が明らかになる可能性もあります。


編集:df = df.copy()を実行している場合でも、元のdf、それでもcopy()は割り当て前に評価されます。つまり、しばらくの間、両方のデータフレームがメモリに格納されます。

例(この動作はメモリサマリーに表示されないことに注意してください)

_> mprof run -T 0.001 demo.py
Line #    Mem usage    Increment   Line Contents
================================================
     7     62.9 MiB      0.0 MiB   @profile
     8                             def foo():
     9    215.5 MiB    152.6 MiB    df = pd.DataFrame(np.random.randn(2 * 10 ** 7))
    10    215.5 MiB      0.0 MiB    df = df.copy()
_

しかし、経時的なメモリ消費量を視覚化すると、1.6秒で両方のデータフレームがメモリ内に存在します:

enter image description here

8
Jan Trienes

更新:

TL; DR:SettingWithCopyWarningの扱い方は目的によって異なると思います。 dfの変更を避けたい場合は、df.copy()での作業は安全であり、警告は冗長です。 dfを変更したい場合、.copy()の使用は間違った方法を意味し、警告を尊重する必要があります。

免責事項:私は他の回答者のようなPandasの専門家との私的/個人的なコミュニケーションはありません。したがって、この回答は、公式のPandas docs、一般的なユーザーがベースとするもの、および私自身の経験に基づいています。


SettingWithCopyWarningは実際の問題ではなく、実際の問題について警告します。ユーザーは、警告を回避するのではなく、実際の問題を理解して解決する必要があります。

本当の問題は、データフレームにインデックスを付けるとコピーが返されることがあり、このコピーを変更しても元のデータフレームは変更されないことです。警告は、ユーザーにその論理的なバグをチェックして回避するように求めます。例えば:

_import pandas as pd, numpy as np
np.random.seed(7)  # reproducibility
df = pd.DataFrame(np.random.randint(1, 10, (3,3)), columns=['a', 'b', 'c'])
print(df)
   a  b  c
0  5  7  4
1  4  8  8
2  8  9  9
# Setting with chained indexing: not work & warning.
df[df.a>4]['b'] = 1
print(df)
   a  b  c
0  5  7  4
1  4  8  8
2  8  9  9
# Setting with chained indexing: *may* work in some cases & no warning, but don't rely on it, should always avoid chained indexing.
df['b'][df.a>4] = 2
print(df)
   a  b  c
0  5  2  4
1  4  8  8
2  8  2  9
# Setting using .loc[]: guarantee to work.
df.loc[df.a>4, 'b'] = 3
print(df)
   a  b  c
0  5  3  4
1  4  8  8
2  8  3  9
_

警告をバイパスする間違った方法について:

_df1 = df[df.a>4]['b']
df1.is_copy = None
df1[0] = -1  # no warning because you trick pandas, but will not work for assignment
print(df)
   a  b  c
0  5  7  4
1  4  8  8
2  8  9  9

df1 = df[df.a>4]['b']
df1 = df1.copy()
df1[0] = -1  # no warning because df1 is a separate dataframe now, but will not work for assignment
print(df)
   a  b  c
0  5  7  4
1  4  8  8
2  8  9  9
_

したがって、_df1.is_copy_をFalseまたはNoneに設定することは、警告を回避するための単なる方法であり、割り当て時の実際の問題を解決するためではありません。 _df1_はweakrefdfではなく、完全に独立したデータフレームであるため、df1 = df1.copy()を設定すると警告がさらに誤ってバイパスされます。したがって、ユーザーがdfの値を変更したい場合、警告は表示されませんが、論理的なバグが発生します。経験の浅いユーザーは、新しい値を割り当ててもdfが変更されない理由を理解できません。そのため、これらのアプローチを完全に回避することをお勧めします。

ユーザーがデータのコピーでのみ作業する場合、つまり元のdfを厳密に変更しない場合は、.copy()を明示的に呼び出すことは完全に正しいことです。ただし、元のdfのデータを変更する場合は、警告を尊重する必要があります。ポイントは、ユーザーは自分が何をしているかを理解する必要があるということです。

連鎖インデックス割り当てによる警告の場合、正しい解決策は、値を_df[cond1][cond2]_によって生成されたコピーに割り当てないようにすることですが、代わりに_df.loc[cond1, cond2]_によって生成されたビューを使用することです。

コピーの警告/エラーと解決策を使用した設定のその他の例は、ドキュメントに示されています。 http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy =

2
THN