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