25

有无数关于可怕的问题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导致接到那种电话。我想把它说清楚,因为这是混乱的根源,我正试图弄清楚。我想看看爆炸的边缘案例!

4

4 回答 4

14

这是我的 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

顺便说一句,副本只是意味着q它将成为内存中的一个新对象。切片在内存中共享相同的原始对象

imo,使用.copy()非常安全。例如,df.loc[:, 'a']返回一个切片但df.loc[df.index, 'a']返回一个副本。Jeff 告诉我这是一个意外的行为,:或者df.index应该与 .loc[] 中的索引器具有相同的行为,但是.copy()在两者上使用都会返回一个副本,最好是安全的。因此,.copy()如果您不想影响原始数据框,请使用。

现在使用.copy()返回 DataFrame 的深层副本,这是一种非常安全的方法,不会接到您正在谈论的电话。

但是使用df.is_copy = None, 只是一个不复制任何东西的技巧,这是一个非常糟糕的主意,您仍将处理原始 DataFrame 的一部分

人们往往不知道的另一件事:

df[columns] 可能会返回视图。

df.loc[indexer, columns]可能会返回一个视图,但在实践中几乎总是不会。这里 强调五月

于 2017-04-22T12:57:05.233 回答
8

尽管其他答案提供了有关为什么不应简单地忽略警告的很好信息,但我认为您的原始问题尚未得到解答。

@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.


编辑: 即使你正在做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 秒时,两个数据帧都在内存中:

在此处输入图像描述

于 2017-04-27T19:01:23.247 回答
2

更新:

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_copyFalseorNone只是绕过警告的一种方式,而不是解决分配时的真正问题。设置df1 = df1.copy()还以另一种更错误的方式绕过警告,因为df1不是weakrefof 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

于 2017-04-21T11:26:18.733 回答
2

编辑:

在我们交换评论并阅读了一下之后(我什至找到了@Jeff 的答案),我可能会将猫头鹰带到雅典,但在panda-docs中存在这个代码示例:

有时SettingWithCopy在没有明显的链式索引进行时会出现警告。这些是 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]

如果您不关心“原始”框架,那就可以了

如果您期望

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

实际上会设置原始框架(它们在这里都被称为“df”,这令人困惑)然后你会感到惊讶。

(此警告主要针对新用户,避免设置副本)

最后他得出结论:

副本通常无关紧要,除非您尝试以链式方式设置它们。

综上所述,我们可以得出这样的结论

  1. SettingWithCopyWarning有一个含义,并且存在(如 jreback 所提出的)这种警告很重要并且可以避免并发症的情况。
  2. 该警告主要是为新用户提供一个“安全网”,让他们注意自己在做什么,并且可能会导致链式操作出现意外行为。因此,更高级的用户可以关闭警告(来自 jreback 的回答):
pd.set_option('chained_assignement',None)

或者你可以这样做:

df.is_copy = False
于 2017-04-21T07:55:46.050 回答