为什么盲目地使用df.copy()一个坏主意来修复SettingWithCopyWarning

piR*_*red 25 python pandas chained-assignment

关于可怕的问题,有无数的问题 SettingWithCopyWarning

我已经很好地理解了它是如何产生的.(注意我说好,不好)

当数据df帧通过存储的属性"附加"到另一个数据帧时,就会发生这种情况is_copy.

这是一个例子

df = pd.DataFrame([[1]])

d1 = df[:]

d1.is_copy

<weakref at 0x1115a4188; to 'DataFrame' at 0x1119bb0f0>
Run Code Online (Sandbox Code Playgroud)

我们可以将该属性设置为None

d1 = d1.copy()
Run Code Online (Sandbox Code Playgroud)

我见过像@Jeff这样的开发者,我不记得还有谁,警告这样做.引用SettingWithCopyWarning有目的.

问题
好的,那么什么是一个具体的例子,说明为什么通过分配copy回原始来忽略警告是一个坏主意.

我会定义"坏主意"以澄清.

坏主意
这是一个坏主意来放置代码投入生产,这将导致越来越在星期六晚上说你的代码被打破,需要固定的中间一个电话.

现在如何使用df = df.copy()以绕过SettingWithCopyWarning导致获得那种电话.我想要它拼写出来,因为这是一个混乱的来源,我试图找到清晰度.我想看到爆炸的边缘情况!

Ste*_*n G 13

这是我的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
Run Code Online (Sandbox Code Playgroud)

现在我想基于原始子集创建一个新的数据框并修改它,如下所示:

 q = x.loc[:, 'a']
Run Code Online (Sandbox Code Playgroud)

现在这是原始片段,无论我做什么都会影响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
Run Code Online (Sandbox Code Playgroud)

这就是警告告诉你的.您正在处理切片,因此您在其上执行的所有操作都将反映在原始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
Run Code Online (Sandbox Code Playgroud)

和顺便说一句,副本只是意味着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]可能会返回一个视图,但几乎总是不会在实践中. 在这里强调可能


Jan*_*nes 8

虽然其他答案提供了关于为什么不应该忽略警告的良好信息,但我认为您的原始问题尚未得到解答.

@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()
Run Code Online (Sandbox Code Playgroud)

当使用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()
Run Code Online (Sandbox Code Playgroud)

这涉及这样的事实,即仍然存在df指向原始数据帧的reference().因此,df垃圾收集器不会清理它并保存在内存中.

在生产系统中使用此代码时,MemoryError根据所处理数据的大小和可用内存,可能会也可能不会.

总而言之,.copy() 盲目使用并不是一个明智的想法.这不仅仅是因为您可能会在软件中引入逻辑错误,还因为它可能会暴露运行时的危险,例如a 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()
Run Code Online (Sandbox Code Playgroud)

但是如果你可以看到内存消耗随着时间的推移,在1.6秒内,两个数据帧都在内存中:

在此输入图像描述