PandasでのSettingWithCopyWarningの対処法

python pandas dataframe chained-assignment


Background

パンダを0.11から0.13.0rc1にアップグレードしました。今、アプリケーションは多くの新しい警告をポップアウトしています。そのうちの一つがこんな感じ。

E:\FinReporter\FM_EXT.py:449: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  quote_df['TVol']   = quote_df['TVol']/TVOL_SCALE

具体的にどういう意味なのか知りたいです。何かを変える必要があるのか?

quote_df['TVol'] = quote_df['TVol']/TVOL_SCALE を使用するように要求した場合、警告を一時停止するにはどうすればよいですか?

エラーを与える関数

def _decode_stock_quote(list_of_150_stk_str):
    """decode the webpage and return dataframe"""

    from cStringIO import StringIO

    str_of_all = "".join(list_of_150_stk_str)

    quote_df = pd.read_csv(StringIO(str_of_all), sep=',', names=list('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg')) #dtype={'A': object, 'B': object, 'C': np.float64}
    quote_df.rename(columns={'A':'STK', 'B':'TOpen', 'C':'TPCLOSE', 'D':'TPrice', 'E':'THigh', 'F':'TLow', 'I':'TVol', 'J':'TAmt', 'e':'TDate', 'f':'TTime'}, inplace=True)
    quote_df = quote_df.ix[:,[0,3,2,1,4,5,8,9,30,31]]
    quote_df['TClose'] = quote_df['TPrice']
    quote_df['RT']     = 100 * (quote_df['TPrice']/quote_df['TPCLOSE'] - 1)
    quote_df['TVol']   = quote_df['TVol']/TVOL_SCALE
    quote_df['TAmt']   = quote_df['TAmt']/TAMT_SCALE
    quote_df['STK_ID'] = quote_df['STK'].str.slice(13,19)
    quote_df['STK_Name'] = quote_df['STK'].str.slice(21,30)#.decode('gb2312')
    quote_df['TDate']  = quote_df.TDate.map(lambda x: x[0:4]+x[5:7]+x[8:10])

    return quote_df

その他のエラーメッセージ

E:\FinReporter\FM_EXT.py:449: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  quote_df['TVol']   = quote_df['TVol']/TVOL_SCALE
E:\FinReporter\FM_EXT.py:450: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  quote_df['TAmt']   = quote_df['TAmt']/TAMT_SCALE
E:\FinReporter\FM_EXT.py:453: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  quote_df['TDate']  = quote_df.TDate.map(lambda x: x[0:4]+x[5:7]+x[8:10])



Answer 1 Garrett


SettingWithCopyWarning は、潜在的な最初の選択を返す場合は特に、期待通りにいつもの仕事をしません以下、として「連鎖」の割り当て、混乱フラグに作成されたコピーを。[ 背景説明については、GH5390およびGH5597を参照してください。]

df[df['A'] > 2]['B'] = new_val  # new_val not set in df

警告では、以下のように書き換えることを提案しています。

df.loc[df['A'] > 2, 'B'] = new_val

しかし、これはあなたの使い方には合わないということで、それに相当します。

df = df[df['A'] > 2]
df['B'] = new_val

書き込みを元のフレームに戻すことを気にしないことは明らかですが(参照を上書きしているため)、残念ながらこのパターンは最初の連鎖割り当ての例と区別できません。したがって、(誤検知)警告です。誤検知が発生する可能性については、インデックスについてのドキュメントをご覧ください。この新しい警告は、次の割り当てで安全に無効にできます。

import pandas as pd
pd.options.mode.chained_assignment = None  # default='warn'



Answer 2 cs95


パンダで SettingWithCopyWarning を処理する方法?

この投稿は、以下のような読者のためのものです。

  1. この警告の意味を理解したい
  2. この警告を抑制するためのさまざまな方法を理解したい
  3. 将来的にこの警告を避けるために、コードを改善し、良い習慣に従う方法を理解したいと思います。

Setup

np.random.seed(0)
df = pd.DataFrame(np.random.choice(10, (3, 5)), columns=list('ABCDE'))
df
   A  B  C  D  E
0  5  0  3  3  7
1  9  3  5  2  4
2  7  6  8  8  1

SettingWithCopyWarning とは何ですか?

この警告の対処法を知るためには、そもそも警告がどういう意味を持つのか、なぜ警告が出されるのかを理解しておくことが大切です。

DataFrameをフィルタリングする場合、内部レイアウトとさまざまな実装の詳細に応じて、フレームをスライスまたはインデックス付けして、ビューまたはコピーを返すことができます。 「ビュー」は、用語が示すように、元のデータへのビューです。したがって、ビューを変更すると、元のオブジェクトが変更される場合があります。一方、「コピー」はオリジナルからのデータの複製であり、コピーを変更してもオリジナルには影響しません。

他の回答で述べたように、 SettingWithCopyWarning は「チェーン割り当て」操作にフラグを立てるために作成されました。上記のセットアップで df を検討してください。列 "A"の値が5より大きい列 "B"のすべての値を選択するとします。パンダでは、これをさまざまな方法で実行できます。例えば、

df[df.A > 5]['B']

1    3
2    6
Name: B, dtype: int64

And,

df.loc[df.A > 5, 'B']

1    3
2    6
Name: B, dtype: int64

これらは同じ結果を返すため、これらの値を読み取るだけの場合、違いはありません。それで、問題は何ですか?連鎖代入の問題は、ビューまたはコピーが返されるかどうかを予測することは一般に難しいため、値を代入し直そうとするときにこれが主に問題になります。前の例に基づいて、このコードがインタープリターによってどのように実行されるかを検討してください。

df.loc[df.A > 5, 'B'] = 4
# becomes
df.__setitem__((df.A > 5, 'B'), 4)

df への単一の __setitem__ 呼び出し。OTOH、このコードを検討してください:

df[df.A > 5]['B'] = 4
# becomes
df.__getitem__(df.A > 5).__setitem__('B", 4)

これで、 __getitem__ がビューとコピーのどちらを返したかに応じて、 __setitem__ 操作が機能しない場合があります。

一般に、仕様は常にオリジナルで動作することを保証しているため、ラベルベースの割り当てには loc を、整数/位置ベースの割り当てには iloc を使用する必要があります。さらに、単一のセルを設定するには、 at および iat を使用する必要があります。

詳細については、ドキュメントをご覧ください。

Note
loc で実行されるすべてのブールインデックス操作は、 iloc でも実行できます。唯一の違いは、 iloc がインデックスの整数/位置またはブール値のnumpy配列のいずれかと、列の整数/位置のインデックスのいずれかを期待することです。

例えば

df.loc[df.A > 5, 'B'] = 4

鼻で書くことができます

df.iloc[(df.A > 5).values, 1] = 4

And,

df.loc[1, 'A'] = 100

として書くことができます。

df.iloc[1, 0] = 100

といった感じで。


予告を抑える方法だけでも教えてくれ!

df の「A」列に対する単純な操作を考えます。「A」を選択して2で除算すると警告が表示されますが、操作は機能します。

df2 = df[['A']]
df2['A'] /= 2
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/IPython/__main__.py: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

df2
     A
0  2.5
1  4.5
2  3.5

この警告を直接消すにはいくつかの方法があります。

  1. 作る deepcopy

    df2 = df[['A']].copy(deep=True)
    df2['A'] /= 2
  2. pd.options.mode.chained_assignment を変更する
    None"warn" 、または "raise" に設定できます。 "warn" がデフォルトです。 None は警告を完全に抑制し、 "raise"SettingWithCopyError をスローして、操作が通過できないようにします。

    pd.options.mode.chained_assignment = None
    df2['A'] /= 2

コメント内の@Peter Cottonは、コンテキストマネージャーを使用してモード(この要旨から変更)を非侵入的に変更し、必要な場合にのみモードを設定し、それをリセットして、終了時の元の状態。

class ChainedAssignent:
    def __init__(self, chained=None):
        acceptable = [None, 'warn', 'raise']
        assert chained in acceptable, "chained must be in " + str(acceptable)
        self.swcw = chained

    def __enter__(self):
        self.saved_swcw = pd.options.mode.chained_assignment
        pd.options.mode.chained_assignment = self.swcw
        return self

    def __exit__(self, *args):
        pd.options.mode.chained_assignment = self.saved_swcw

使い方は以下のようになります。

# some code here
with ChainedAssignent():
    df2['A'] /= 2
# more code follows

または、例外を上げるために

with ChainedAssignent(chained='raise'):
    df2['A'] /= 2

SettingWithCopyError: 
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

XY問題」。私は何を間違っているのか?

多くの場合、ユーザーは最初にこの例外が発生した理由を完全に理解せずに、この例外を抑制する方法を探します。これはXY問題の良い例であり、ユーザーは実際にはより根本的な問題「X」の症状である問題「Y」を解決しようとします。この警告が発生した一般的な問題に基づいて質問が出され、解決策が提示されます。

質問1
私が持っているのは、DataFrame

df
       A  B  C  D  E
    0  5  0  3  3  7
    1  9  3  5  2  4
    2  7  6  8  8  1

col "A"> 5〜1000の値を割り当てたいと思います。予想される出力は

      A  B  C  D  E
0     5  0  3  3  7
1  1000  3  5  2  4
2  1000  6  8  8  1

間違った方法だ

df.A[df.A > 5] = 1000         # works, because df.A returns a view
df[df.A > 5]['A'] = 1000      # does not work
df.loc[df.A  5]['A'] = 1000   # does not work

loc を使用した正しい方法:

df.loc[df.A > 5, 'A'] = 1000


質問2 1
セル(1,'D')の値を12345に設定しようとしています。私が期待している出力は

   A  B  C      D  E
0  5  0  3      3  7
1  9  3  5  12345  4
2  7  6  8      8  1

df['D'][1] など、このセルにアクセスするさまざまな方法を試しました 。これを行う最良の方法は何ですか?

1.この質問は特に警告とは関係ありませんが、今後この警告が発生する可能性がある状況を回避するために、この特定の操作を正しく行う方法を理解することは良いことです。

このような場合には、以下のいずれかの方法で行うことができます。

df.loc[1, 'D'] = 12345
df.iloc[1, 3] = 12345
df.at[1, 'D'] = 12345
df.iat[1, 3] = 12345


質問3
何らかの条件に基づいて値をサブセットしようとしています。私は、データフレームの

   A  B  C  D  E
1  9  3  5  2  4
2  7  6  8  8  1

D」の中の値を「C」==「5」となるように123に代入したいのですが、「D」の中の値を「C」==「5」となるように123に代入することはできますか?私が試したのは

df2.loc[df2.C == 5, 'D'] = 123

それは問題ないようですが、私はまだ SettingWithCopyWarning を取得してい ます!どうすれば修正できますか?

これはおそらく、パイプラインの上位にあるコードが原因であると考えられます。次のような大きなものから df2 を作成しましたか

df2 = df[df.A > 5]

?この場合、ブールインデックスはビューを返すため、 df2 はオリジナルを参照します。あなたがする必要があるのは、 df2コピーに割り当てることです:

df2 = df[df.A > 5].copy()
# Or,
# df2 = df.loc[df.A > 5, :]


問題4
からカラム「C」を代入で落とそうとしています。

   A  B  C  D  E
1  9  3  5  2  4
2  7  6  8  8  1

しかし、使用しているのは

df2.drop('C', axis=1, inplace=True)

例外 SettingWithCopyWarning は。なんでこんなことが起こっているの?

これは、 df2 が次のような他のスライス操作からのビューとして作成されている必要があるためです。

df2 = df[df.A > 5]

ここでの解決策は、以前のように dfcopy() を作成するか、 loc を使用することです。




Answer 3 Jeff


一般に、 SettingWithCopyWarning の目的は、ユーザー(特に新しいユーザー)が、元のユーザーではなくコピーで操作している可能性があることを示すことです。そこです(あなたはそれができる何をしているかを知っていればIOW偽陽性はOK)。1つの可能性は、@ Garrettが示唆するように(デフォルトではwarn)警告をオフにすることです。

ここにもう一つの選択肢があります。

In [1]: df = DataFrame(np.random.randn(5, 2), columns=list('AB'))

In [2]: dfa = df.ix[:, [1, 0]]

In [3]: dfa.is_copy
Out[3]: True

In [4]: dfa['A'] /= 2
/usr/local/bin/ipython:1: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  #!/usr/local/bin/python

is_copy フラグを False に設定すると、そのオブジェクトのチェックが事実上オフになります

In [5]: dfa.is_copy = False

In [6]: dfa['A'] /= 2

明示的にコピーした場合は、それ以上の警告は発生しません。

In [7]: dfa = df.ix[:, [1, 0]].copy()

In [8]: dfa['A'] /= 2

OPが上に示しているコードは正当であり、おそらく私も同様ですが、技術的にはこの警告のケースであり、誤検知ではありません。警告を表示しない別の方法は、 reindex を使用して選択操作を行うことです。たとえば、

quote_df = quote_df.reindex(columns=['STK', ...])

Or,

quote_df = quote_df.reindex(['STK', ...], axis=1)  # v.0.21



Answer 4 firelynx


パンダのデータフレームのコピー警告

こんなことをしに行くと

quote_df = quote_df.ix[:,[0,3,2,1,4,5,8,9,30,31]]

pandas.ix この場合、pandas.ix 新しいスタンドアロンのデータフレームを返します。

このデータフレーム内で変更した値は、元のデータフレームを変更することはありません。

パンダが注意を促そうとするのはこういうことです。


.ix が悪い考えである理由

.ix オブジェクトは、複数のことを行うためにしよう、ときれいなコードについては何も読んでたことのある人のために、これは強い香りです。

このデータフレームを考えると

df = pd.DataFrame({"a": [1,2,3,4], "b": [1,1,2,2]})

2つの行動。

dfcopy = df.ix[:,["a"]]
dfcopy.a.ix[0] = 2

動作1: dfcopy がスタンドアロンのデータフレームになりました。変更しても df は変わりません

df.ix[0, "a"] = 3

ビヘイビア2。これは元のデータフレームを変更します。


代わりに .loc を使用してください

パンダの開発者は、 .ix オブジェクトがかなり臭い[推測的]であることを認識し、データのアクセスと割り当てに役立つ2つの新しいオブジェクトを作成しました。(もう1つは .iloc です)

.loc はデータのコピーを作成しようとしないため、より高速です。

.loc は、既存のデータフレームをインプレースで変更するためのものであり、メモリ効率が向上します。

.loc は予測可能であり、1つの動作があります。


解決策

あなたのコード例でやっていることは、たくさんの列を持つ大きなファイルを読み込んで、それを小さく修正することです。

pd.read_csv の関数は、このの多くのあなたを助けるともはるかに高速ファイルの読み込みを行うことができます。

だから、こうするのではなく

quote_df = pd.read_csv(StringIO(str_of_all), sep=',', names=list('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg')) #dtype={'A': object, 'B': object, 'C': np.float64}
quote_df.rename(columns={'A':'STK', 'B':'TOpen', 'C':'TPCLOSE', 'D':'TPrice', 'E':'THigh', 'F':'TLow', 'I':'TVol', 'J':'TAmt', 'e':'TDate', 'f':'TTime'}, inplace=True)
quote_df = quote_df.ix[:,[0,3,2,1,4,5,8,9,30,31]]

これをしてください

columns = ['STK', 'TPrice', 'TPCLOSE', 'TOpen', 'THigh', 'TLow', 'TVol', 'TAmt', 'TDate', 'TTime']
df = pd.read_csv(StringIO(str_of_all), sep=',', usecols=[0,3,2,1,4,5,8,9,30,31])
df.columns = columns

これにより、関心のある列のみが読み取られ、適切な名前が付けられます。魔法のようなことをするために邪悪な .ix オブジェクトを使う必要はありません。




Answer 5 user443854


ここでは、その疑問に直接お答えします。どうやって対処すればいいのか?

作る .copy(deep=False) 使用すると、スライスした後。pandas.DataFrame.copyを参照してください。

待ってください、スライスはコピーを返さないのですか?結局のところ、これが警告メッセージが言おうとしていることなのでしょうか?長い答えを読んでください。

import pandas as pd
df = pd.DataFrame({'x':[1,2,3]})

これは警告を与える。

df0 = df[df.x>2]
df0['foo'] = 'bar'

これはしない。

df1 = df[df.x>2].copy(deep=False)
df1['foo'] = 'bar'

df0df1 はどちらも DataFrame オブジェクトですが、パンダが警告を出力できるようにするためのいくつかの違いがあります。それが何であるかを調べましょう。

import inspect
slice= df[df.x>2]
slice_copy = df[df.x>2].copy(deep=False)
inspect.getmembers(slice)
inspect.getmembers(slice_copy)

お好みの差分ツールを使用すると、いくつかのアドレスを除いて、唯一の物質的な違いはこれだけであることがわかります。

|          | slice   | slice_copy |
| _is_copy | weakref | None       |

警告するかどうかを決定するメソッドは、 DataFrame._check_setitem_copy をチェックする _is_copy です。だからここに行きます。作る copy ごDATAFRAMEではないように _is_copy

警告は .loc の使用を提案していますが、 _is_copy であるフレームで .loc を使用した場合でも、同じ警告が表示されます。誤解を招く?はい。迷惑ですか?あなたは賭けます。役に立ちましたか?潜在的に、連鎖割り当てが使用される場合。しかし、チェーンの割り当てを正しく検出できず、無差別に警告を出力します。