Python 为什么盲目使用df.copy()来修复CopyWarning设置是个坏主意

Python 为什么盲目使用df.copy()来修复CopyWarning设置是个坏主意,python,pandas,chained-assignment,Python,Pandas,Chained Assignment,关于可怕的设置和copywarning 我很清楚这是怎么发生的。(注意我说的好,不是很好) 当数据帧df通过存储在is\u copy中的属性“附加”到另一个数据帧时,就会发生这种情况 这里有一个例子 df = pd.DataFrame([[1]]) d1 = df[:] d1.is_copy <weakref at 0x1115a4188; to 'DataFrame' at 0x1119bb0f0> 我见过像@Jeff这样的开发人员,我不记得还有谁,警告一下。引用带有Cop

关于可怕的
设置和copywarning

我很清楚这是怎么发生的。(注意我说的好,不是很好)

当数据帧
df
通过存储在
is\u copy
中的属性“附加”到另一个数据帧时,就会发生这种情况

这里有一个例子

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

d1 = df[:]

d1.is_copy

<weakref at 0x1115a4188; to 'DataFrame' at 0x1119bb0f0>
我见过像@Jeff这样的开发人员,我不记得还有谁,警告一下。引用带有CopyWarning的
设置有一个目的

问题
好的,下面是一个具体的例子来说明为什么通过将
副本
分配回原始文件来忽略警告是个坏主意

我会定义“坏主意”来澄清

坏主意
将代码放置到生产中,这将是一个强烈的坏主意< /强>,这将导致在星期六晚上的一个电话中说你的代码坏了,需要修复。

现在如何使用
df=df.copy()
绕过
设置CopyWarning
来获取此类电话。我想把它说清楚,因为这是一个混乱的来源,我正试图找到清晰。我想看看爆炸的边缘案例

编辑:

在我们的评论交流之后,通过阅读一点(我甚至发现),我可能会这样做,但在这个代码示例中:

有时,出现以下情况时,会出现
设置with copy
警告: 没有明显的链接索引正在进行。这些就是 设置WithCopy旨在捕捉!熊猫可能正试图 警告您已执行此操作:

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
并就此事发表一些评论:

实际上,您正在设置副本

你可能不在乎;主要是针对以下情况:

df['foo'][0] = 123... 
设置副本(因此对用户不可见) (用户)

此操作,使df现在指向原始文件的副本

df = df [df['column2']!=8]
如果您不关心“原始”帧,则可以

如果您希望

df['column1'] = df['columns'] + 'c'
将实际设置原始帧(它们在这里都称为“df”) 这是令人困惑的)那么你会感到惊讶。

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

最后,他得出结论:

复制通常并不重要,除非您尝试设置 用链子锁着他们

从上面我们可以得出以下结论:

  • SettingWithCopyWarning
    具有一定的意义,在某些情况下(如jreback所示),此警告很重要,可以避免出现复杂情况
  • 该警告主要是为新用户提供一个“安全网”,让他们注意自己正在做的事情,并提醒他们可能会在连锁操作中导致意外行为。因此,更高级的用户可以打开警告(来自jreback的回答):
  • 或者你可以:

    df.is_copy = False
    

    更新:

    TL;DR:我认为如何使用CopyWarning处理
    设置取决于用途。如果希望避免修改
    df
    ,那么使用
    df.copy()
    是安全的,警告是多余的。如果要修改
    df
    ,则使用
    .copy()
    表示方法错误,需要遵守警告

    免责声明:我没有像其他回答者那样与熊猫专家进行私人/个人交流。所以这个答案是基于熊猫官方文档,一个典型的用户会基于什么,以及我自己的经验


    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
    不是
    df的
    weakref
    ,而是一个完全独立的数据帧。因此,如果用户想要更改
    df
    中的值,他们将不会收到任何警告,而是收到一个逻辑错误。没有经验的用户将无法理解为什么
    df
    在被分配新值后没有变化。这就是为什么完全避免这些方法是明智的

    如果用户只想处理数据的副本,即严格不修改原始的
    df
    ,那么显式调用
    .copy()
    是完全正确的。但我
    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
    
    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']
    
    q += 2
    print(x)  # checking x again, wow! it changed!
       a  b
    0  2  0
    1  3  1
    2  4  2
    3  5  3
    
    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
    
    @profile
    def foo():
        df = pd.DataFrame(np.random.randn(2 * 10 ** 7))
    
        d1 = df[:]
        d1 = d1.copy()
    
    if __name__ == '__main__':
        foo()
    
    > 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()
    
    > 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()