Как работать с 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 в Pandas?

Этот пост предназначен для читателей,которые,

  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 ?

Чтобы знать,как обращаться с этим предупреждением,важно понимать,что оно означает и почему оно воспитывается в первую очередь.

При фильтрации DataFrames можно разрезать / индексировать кадр, чтобы вернуть либо представление , либо копию , в зависимости от внутренней компоновки и различных деталей реализации. «Представление» - это, как предполагает термин, представление исходных данных, поэтому изменение представления может изменить исходный объект. С другой стороны, «копия» - это репликация данных из оригинала, и изменение копии не влияет на оригинал.

Как уже упоминалось в других ответах, SettingWithCopyWarning был создан для пометки операций « связанного назначения». Рассмотрите df в настройке выше. Предположим, вы хотите выбрать все значения в столбце «B», где значения в столбце «A»> 5. Pandas позволяет вам делать это разными способами, некоторые из которых более правильные, чем другие. Например,

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)

С одним вызовом __setitem__ df . OTOH, рассмотрите этот код:

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

Теперь, в зависимости от того, вернул ли __getitem__ представление или копию, операция __setitem__ может не работать .

В общем, вы должны использовать loc для назначения на основе iloc и iloc для целочисленных / позиционных назначений, поскольку спецификация гарантирует, что они всегда работают с оригиналом. Кроме того, для настройки отдельной ячейки вы должны использовать at и iat .

Больше можно найти в документации .

Note
Все операции логического индексирования, выполняемые с помощью loc , также можно выполнять с помощью iloc . Единственное отличие состоит в том, что iloc ожидает либо целые числа / позиции для индекса, либо массивный массив логических значений, а также целочисленные / позиционные индексы для столбцов.

Например,

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

Могут быть написаны nas

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

And,

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

Может быть написано как

df.iloc[1, 0] = 100

И так далее.


Просто скажи мне,как подавить предупреждение!

Рассмотрим простую операцию со столбцом «A» в df . Выбор «А» и деление на 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 не SettingWithCopyError предупреждение полностью, а "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 , когда пользователи пытаются решить проблему «Y», которая на самом деле является признаком более глубокой проблемы «X». Вопросы будут подняты на основе общих проблем, которые встречаются с этим предупреждением, и затем будут представлены решения.

Вопрос 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

Я хочу присвоить значения в столбце "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
Я пытаюсь подмножествовать значения,основываясь на каком-то условии.У меня есть DataFrame

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

Я хотел бы присвоить значения в "D" 123 таким,что "C" ==5.Я пытался

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
Я пытаюсь опустить колонку "С" на место из

   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]

Решение здесь состоит в том, чтобы либо сделать copy() из df , либо использовать loc , как и раньше.




Answer 3 Jeff


В общем, смысл SettingWithCopyWarning - показать пользователям (и особенно новым пользователям), что они могут работать с копией, а не с оригиналом, как они думают. Там являются ложными срабатывания (IOW , если вы знаете , что вы делаете , это может быть хорошо ). Одна из возможностей - просто отключить (по умолчанию предупреждение ) предупреждение, как предлагает @Garrett.

Вот еще один вариант:

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]})

Два поведения:

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

Поведение dfcopy : dfcopy теперь является отдельным фреймом данных. Изменение не изменит df

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

Поведение второе:Это меняет исходную базу данных.


.loc этого используйте .loc

Разработчики панд признали, что объект .ix был довольно вонючим [спекулятивно] и таким образом создали два новых объекта, которые помогают в доступе и назначении данных. (Другое существо .iloc )

.loc быстрее, потому что он не пытается создать копию данных.

.loc предназначен для изменения вашего существующего информационного кадра на месте, что более эффективно использует память.

.loc предсказуемо, у него одно поведение.


Решение

То,что вы делаете в вашем примере кода,это загружаете большой файл с большим количеством столбцов,а затем модифицируете его,чтобы он был меньше.

Функция 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'

И df0 , и df1 являются объектами 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 , но если вы используете .loc для кадра, который _is_copy , вы все равно получите то же предупреждение. Вводя в заблуждение? Да. Раздражает? Вы ставите. Полезно? Потенциально, когда используется цепное назначение. Но он не может правильно определить назначение цепи и распечатывает предупреждение без разбора.