为什么盲目地使用 df.copy() 来修复 SettingWithCopyWarning 是个坏主意
why is blindly using df.copy() a bad idea to fix the SettingWithCopyWarning
有无数关于可怕的问题SettingWithCopyWarning
我很清楚它是如何产生的。 (注意我说的是好,不是很好)
当数据帧 df
通过存储在 is_copy
中的属性 "attached" 到另一个数据帧时会发生这种情况。
这是一个例子
df = pd.DataFrame([[1]])
d1 = df[:]
d1.is_copy
<weakref at 0x1115a4188; to 'DataFrame' at 0x1119bb0f0>
我们可以将该属性设置为 None
或
d1 = d1.copy()
我见过像@Jeff 这样的开发者,但我不记得还有谁,警告不要这样做。引用 SettingWithCopyWarning
是有目的的。
问题
好的,那么有什么具体的例子可以说明为什么通过将 copy
赋值回原件来忽略警告是一个坏主意。
我将定义 "bad idea" 以进行说明。
坏主意
将代码投入生产是一个坏主意,这将导致在周六晚上接到一个 phone 电话说您的代码已损坏,需要修复。
现在 如何使用 df = df.copy()
绕过 SettingWithCopyWarning
导致获得那种 phone 调用。我想把它拼写出来,因为这是混乱的根源,我正试图弄清楚。我想看看爆炸的边缘情况!
编辑:
经过我们的评论交流和阅读(我什至发现 @Jeff's answer), I may bring owls to Athens, but in panda-docs 存在这个代码示例:
Sometimes a SettingWithCopy
warning will arise at times when there’s
no obvious chained indexing going on. These are the bugs that
SettingWithCopy is designed to catch! Pandas is probably trying to
warn you that you’ve done this:
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
对于有经验的人来说,这可能是一个很容易避免的问题 user/developer 但 pandas 不仅适用于有经验的人...
你仍然可能不会在星期天的半夜接到关于这个的 phone 电话,但它 可能 在很长一段时间内损害你的数据完整性你没有早点发现它。
此外,正如 Murphy's law 所述,您将要执行的最耗时和最复杂的数据操作 将在副本上进行 ,该副本将在使用前被丢弃,您将花费小时尝试调试它!
注意: 所有这些都是假设,因为文档中的定义是基于(不幸)事件概率的假设...SettingWithCopy
是一个新的-用户友好的警告,用于警告新用户他们的代码可能存在随机和不需要的行为。
从 2014 年开始存在 this issue。
在这种情况下导致警告的代码如下所示:
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对此事发表一些评论:
You are in fact setting a copy.
You prob don't care; it is mainly to address situations like:
df['foo'][0] = 123...
which sets the copy (and thus is not visible to
the user)
This operation, make the df now point to a copy of the original
df = df [df['column2']!=8]
If you don't care about the 'original' frame, then its ok
If you are expecting that the
df['column1'] = df['columns'] + 'c'
would actually set the original frame (they are both called 'df' here
which is confusing) then you would be suprised.
和
(this warning is mainly for new users to avoid setting the copy)
最后他总结道:
Copies don't normally matter except when you are then trying to set
them in a chained manner.
从上面我们可以得出这个结论:
SettingWithCopyWarning
有含义,在某些情况下(如 jreback 所述)此警告很重要并且可以避免并发症。
- 这个警告主要是 "safety net" 给新用户的警告,让他们注意他们在做什么,这可能会导致链式操作出现意外行为。因此,更高级的用户可以关闭警告(来自 jreback 的回答):
pd.set_option('chained_assignement',None)
or you could do:
df.is_copy = False
更新:
TL;DR: 我认为如何对待 SettingWithCopyWarning
取决于目的。如果想避免修改 df
,那么在 df.copy()
上工作是安全的并且警告是多余的。如果要修改df
,那么使用.copy()
就是错误的方式,需要注意警告。
免责声明: 我没有像其他回答者那样与 Pandas 专家进行 private/personal 交流。所以这个答案是基于官方 Pandas 文档,一个典型的用户会基于什么,以及我自己的经验。
SettingWithCopyWarning
不是真正的问题,它警告真正的问题。用户需要了解并解决真正的问题,而不是绕过警告。
真正的问题是,索引一个dataframe可能return一个副本,然后修改这个副本不会改变原来的dataframe。该警告要求用户检查并避免该逻辑错误。例如:
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()
是完全正确的。但是如果他们想修改原始df
中的数据,他们需要尊重警告。关键是,用户需要了解他们在做什么。
万一由于链式索引分配而出现警告,正确的解决方案是避免将值分配给 df[cond1][cond2]
生成的副本,而是使用 df.loc[cond1, cond2]
生成的视图。
文档中显示了更多使用副本 warning/error 和解决方案进行设置的示例:http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
这是我的 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']
return 一个切片但是 df.loc[df.index, 'a']
return 一个副本。 Jeff 告诉我这是一个意外行为,:
或 df.index
应该与 .loc[] 中的索引器具有相同的行为,但是在两者上使用 .copy()
将 return副本,最好是安全的。所以如果你不想影响原始数据帧,请使用 .copy()
。
现在使用 .copy()
return DataFrame 的深拷贝,这是一种非常安全的方法,不会让 phone 打电话给你正在谈论。
但是使用 df.is_copy = None
只是一个不会复制任何东西的技巧,这是一个非常糟糕的主意,你仍然会在原始 DataFrame 的一部分上工作
人们往往不知道的一件事:
df[columns]
可以return一个视图。
df.loc[indexer, columns]
也 可能 return 一个视图,但几乎总是不会在实践中。
强调可能这里
虽然其他答案提供了很好的信息,说明为什么不应该简单地忽略警告,但我认为您最初的问题还没有得到回答。
@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.6s 时两个数据帧都在内存中:
有无数关于可怕的问题SettingWithCopyWarning
我很清楚它是如何产生的。 (注意我说的是好,不是很好)
当数据帧 df
通过存储在 is_copy
中的属性 "attached" 到另一个数据帧时会发生这种情况。
这是一个例子
df = pd.DataFrame([[1]])
d1 = df[:]
d1.is_copy
<weakref at 0x1115a4188; to 'DataFrame' at 0x1119bb0f0>
我们可以将该属性设置为 None
或
d1 = d1.copy()
我见过像@Jeff 这样的开发者,但我不记得还有谁,警告不要这样做。引用 SettingWithCopyWarning
是有目的的。
问题
好的,那么有什么具体的例子可以说明为什么通过将 copy
赋值回原件来忽略警告是一个坏主意。
我将定义 "bad idea" 以进行说明。
坏主意
将代码投入生产是一个坏主意,这将导致在周六晚上接到一个 phone 电话说您的代码已损坏,需要修复。
现在 如何使用 df = df.copy()
绕过 SettingWithCopyWarning
导致获得那种 phone 调用。我想把它拼写出来,因为这是混乱的根源,我正试图弄清楚。我想看看爆炸的边缘情况!
编辑:
经过我们的评论交流和阅读(我什至发现 @Jeff's answer), I may bring owls to Athens, but in panda-docs 存在这个代码示例:
Sometimes a
SettingWithCopy
warning will arise at times when there’s no obvious chained indexing going on. These are the bugs that SettingWithCopy is designed to catch! Pandas is probably trying to warn you that you’ve done this: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
对于有经验的人来说,这可能是一个很容易避免的问题 user/developer 但 pandas 不仅适用于有经验的人...
你仍然可能不会在星期天的半夜接到关于这个的 phone 电话,但它 可能 在很长一段时间内损害你的数据完整性你没有早点发现它。
此外,正如 Murphy's law 所述,您将要执行的最耗时和最复杂的数据操作 将在副本上进行 ,该副本将在使用前被丢弃,您将花费小时尝试调试它!
注意: 所有这些都是假设,因为文档中的定义是基于(不幸)事件概率的假设...SettingWithCopy
是一个新的-用户友好的警告,用于警告新用户他们的代码可能存在随机和不需要的行为。
从 2014 年开始存在 this issue。
在这种情况下导致警告的代码如下所示:
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对此事发表一些评论:
You are in fact setting a copy.
You prob don't care; it is mainly to address situations like:
df['foo'][0] = 123...
which sets the copy (and thus is not visible to the user)
This operation, make the df now point to a copy of the original
df = df [df['column2']!=8]
If you don't care about the 'original' frame, then its ok
If you are expecting that the
df['column1'] = df['columns'] + 'c'
would actually set the original frame (they are both called 'df' here which is confusing) then you would be suprised.
和
(this warning is mainly for new users to avoid setting the copy)
最后他总结道:
Copies don't normally matter except when you are then trying to set them in a chained manner.
从上面我们可以得出这个结论:
SettingWithCopyWarning
有含义,在某些情况下(如 jreback 所述)此警告很重要并且可以避免并发症。- 这个警告主要是 "safety net" 给新用户的警告,让他们注意他们在做什么,这可能会导致链式操作出现意外行为。因此,更高级的用户可以关闭警告(来自 jreback 的回答):
pd.set_option('chained_assignement',None)
or you could do:
df.is_copy = False
更新:
TL;DR: 我认为如何对待 SettingWithCopyWarning
取决于目的。如果想避免修改 df
,那么在 df.copy()
上工作是安全的并且警告是多余的。如果要修改df
,那么使用.copy()
就是错误的方式,需要注意警告。
免责声明: 我没有像其他回答者那样与 Pandas 专家进行 private/personal 交流。所以这个答案是基于官方 Pandas 文档,一个典型的用户会基于什么,以及我自己的经验。
SettingWithCopyWarning
不是真正的问题,它警告真正的问题。用户需要了解并解决真正的问题,而不是绕过警告。
真正的问题是,索引一个dataframe可能return一个副本,然后修改这个副本不会改变原来的dataframe。该警告要求用户检查并避免该逻辑错误。例如:
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()
是完全正确的。但是如果他们想修改原始df
中的数据,他们需要尊重警告。关键是,用户需要了解他们在做什么。
万一由于链式索引分配而出现警告,正确的解决方案是避免将值分配给 df[cond1][cond2]
生成的副本,而是使用 df.loc[cond1, cond2]
生成的视图。
文档中显示了更多使用副本 warning/error 和解决方案进行设置的示例:http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
这是我的 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']
return 一个切片但是 df.loc[df.index, 'a']
return 一个副本。 Jeff 告诉我这是一个意外行为,:
或 df.index
应该与 .loc[] 中的索引器具有相同的行为,但是在两者上使用 .copy()
将 return副本,最好是安全的。所以如果你不想影响原始数据帧,请使用 .copy()
。
现在使用 .copy()
return DataFrame 的深拷贝,这是一种非常安全的方法,不会让 phone 打电话给你正在谈论。
但是使用 df.is_copy = None
只是一个不会复制任何东西的技巧,这是一个非常糟糕的主意,你仍然会在原始 DataFrame 的一部分上工作
人们往往不知道的一件事:
df[columns]
可以return一个视图。
df.loc[indexer, columns]
也 可能 return 一个视图,但几乎总是不会在实践中。
强调可能这里
虽然其他答案提供了很好的信息,说明为什么不应该简单地忽略警告,但我认为您最初的问题还没有得到回答。
@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.6s 时两个数据帧都在内存中: