почему слепо использует 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 вернуться к оригиналу-плохая идея.

я определю "плохая идея" для разъяснения.

Плохая Идея
Это плохая идея разместить код в производство, которое приведет к получению телефонного звонка в середине субботнего вечера, говоря, что ваш код сломан и должен быть исправлен.

теперь как использовать df = df.copy() в целях обхода SettingWithCopyWarning привести к получению такого рода телефонный звонок. Я хочу, чтобы это было написано, потому что это источник путаницы, и я пытаюсь найти ясность. Я хочу увидеть дело edge, которое взорвется!

4 ответов


вот мой 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

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

теперь используя .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'] возврат a копировать. Джефф сказал мне, что это было неожиданное поведение и : или df.index должно иметь то же поведение, что и индексатор .loc [], но используя .copy() на обоих будет возвращена копия, лучше быть в безопасности. так что используйте .copy() если вы не хотите влиять на исходный фрейм данных.

теперь с помощью .copy() верните deepcopy фрейма данных, это очень безопасный подход, чтобы не получить телефонный звонок, о котором вы говорите.

, но через df.is_copy = None, это просто уловка это не копирует ничего, что является очень плохой идеей,вы все еще будете работать над фрагментом исходного фрейма данных

еще одна вещь, которую люди, как правило, не знают:

df[columns] может возвратить вид.

df.loc[indexer, columns] и может возвратить вид но почти всегда не на практике. акцент на мая здесь


в то время как другие ответы дают хорошую информацию о том, почему нельзя просто игнорировать предупреждение, я думаю, что на ваш первоначальный вопрос еще не ответили.

@thn указывает, что с помощью copy() полностью зависит от сценария под рукой. Если вы хотите, чтобы исходные данные были сохранены, вы используете .copy(), в противном случае вы этого не сделаете. Если вы используете copy() обойти SettingWithCopyWarning вы игнорируете тот факт, что вы можете ввести логическую ошибку в вашей программе. Как до тех пор, пока вы абсолютно уверены, что это то, что вы хотите сделать, вы в порядке.

Впрочем, при использовании .copy() вслепую вы можете столкнуться с другой проблемой, которая больше не является специфичной для панд, но возникает каждый раз, когда вы копируете данные.

я немного доработал ваш пример кода, чтобы сделать проблему более очевидной:

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

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

if __name__ == '__main__':
    foo()

при использовании memory_profile видно, что .copy() удваивает нашу память потребление:

> 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() втемную. Не только потому, что вы можете ввести логическую ошибку в свое программное обеспечение, но и потому, что это может подвергнуть опасности выполнения, такие как MemoryError.


Edit: Даже если вы делаете 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 s оба фрейма данных находятся в памяти:

enter image description here


EDIT:

после нашего обмена комментариями и от чтения вокруг немного (я даже нашел @Джефф), а принесите сов в Афины, но в panda-docs существует этот пример кода:

иногда SettingWithCopy предупреждение возникнет в моменты, когда нет очевидной цепной индексации. Это ошибки, которые SettingWithCopy предназначен для ловли! Панды, вероятно, пытаются предупреждаю вас, что вы сделали это:

def do_something(df):    
      foo = df[['bar', 'baz']]  # Is foo a view? A copy? Nobody knows! 
      # ... many lines here ...    
      foo['quux'] = value  # We don't know whether this will modify df or not!   
      return foo

что может легко избежать проблемы для опытного пользователя/разработчика, но панды не только для опытного...

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

Примечание: все это гипотетично, потому что само определение в документах является гипотезой, основанной на вероятности (неудачных) событий... SettingWithCopy новое - дружественное предупреждение которое существует для того чтобы предупредить новых потребителей потенциально случайного и излишнего поведения их код.


Существует этот вопрос С 2014 года.
Код, который вызывает предупреждение в этом случае выглядит так:
from pandas import DataFrame
# create example dataframe:
df = DataFrame ({'column1':['a', 'a', 'a'], 'column2': [4,8,9] })
df
# assign string to 'column1':
df['column1'] = df['column1'] + 'b'
df
# it works just fine - no warnings
#now remove one line from dataframe df:
df = df [df['column2']!=8]
df
# adding string to 'column1' gives warning:
df['column1'] = df['column1'] + 'c'
df

и jreback сделайте несколько комментариев по этому вопросу:

вы фактически устанавливаете копию.

вы prob не заботитесь; это в основном для решения таких ситуаций, как:

df['foo'][0] = 123... 

, который устанавливает копию (и таким образом не видны этот пользователь)

эта операция, сделать df теперь указывают на копию оригинала

df = df [df['column2']!=8]

если вы не заботитесь о "оригинальном" кадре, то его ok

если вы ожидаете, что

df['column1'] = df['columns'] + 'c'

фактически установил бы исходный кадр (они оба называются " df " здесь что сбивает с толку), тогда Вы были бы удивлены.

и

(это предупреждение в основном для новых пользователей, чтобы избежать создания копии)

наконец, он заключает:

копии обычно не имеют значения, за исключением случаев, когда вы пытаетесь установить их заковали в цепи.

из вышесказанного мы можем сделать это выводы:

  1. SettingWithCopyWarning имеет значение, и есть (как представлено jreback) ситуации, в которых это предупреждение имеет значение и осложнений можно избежать.
  2. предупреждение в основном является "сетью безопасности" для новых пользователей, чтобы заставить их обратить внимание на то, что они делают, и что это может вызвать неожиданное поведение при цепных операциях. Таким образом, более продвинутый пользователь может включить предупреждение (из ответа jreback):
pd.set_option('chained_assignement',None)

или вы могли бы сделать:

df.is_copy = False

обновление:

TL; DR: я думаю, как лечить SettingWithCopyWarning зависит от целей. Если вы хотите избежать изменения df, затем работал на df.copy() безопасно и предупреждение избыточно. Если кто-то хочет изменить df, затем через .copy() значит неправильный путь и предупреждение нужно уважать.

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


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 to False или None - это просто способ обойти предупреждение, а не решать реальную проблему при назначении. Настройка df1 = df1.copy() также обойти предупреждение еще более неправильным способом, потому что df1 - это не weakref of df, но полностью независимый фрейм данных. Поэтому, если пользователи хотят изменить значения в df, они не получат никакого предупреждения, но логическая ошибка. Неопытные пользователи не поймут, почему df не изменяется после присвоения новых значений. Именно поэтому целесообразно полностью избегать таких подходов.

если пользователи хотят работать только над копией данных, то есть строго не изменяя оригинал df, тогда совершенно правильно позвонить .copy() явно. Но если они хотят изменить данные в исходном df, они должны уважать предупреждение. Дело в том, что пользователям необходимо понимают, что они делают.

в случае предупреждения из-за цепного назначения индексирования правильное решение-избежать присвоения значений копии, созданной df[cond1][cond2], но использовать представление, созданное .

дополнительные примеры настройки с предупреждением/ошибкой копирования и решениями показаны в документах: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy