Подтвердить что ты не робот

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

4b9b3361

Ответ 1

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

и btw, копия просто означает, что q будет новым объектом в памяти. где фрагмент имеет один и тот же исходный объект в памяти

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

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

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

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

df[columns] может возвращать представление.

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

Ответ 2

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

@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() удваивает потребление памяти:

> 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 секунды оба кадра данных находятся в памяти:

введите описание изображения здесь

Ответ 3

EDIT:

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

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

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

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

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

Примечание. Все, что гипотетично, потому что само определение в документах является гипотезой, основанной на вероятности (неудачных) событий... 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 высказать некоторые замечания по этому вопросу:

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

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

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

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

В этой операции df теперь укажет на копию оригинала

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

Если вам не нужен "исходный" кадр, тогда его ok

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

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

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

и

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

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

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

Из вышесказанного можно сделать следующие выводы:

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

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

df.is_copy = False

Ответ 4

Update:

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

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