如何在 Pandas 中处理
SettingWithCopyWarning
和
ChainedAssignmentError
问题?
本文旨在帮助读者:
- 理解此警告的含义
- 了解不同方法来消除此警告
- 学习如何改进代码,遵循良好实践以避免在未来出现此警告。
设置
np.random.seed(0)
df = pd.DataFrame(np.random.choice(10, (3, 5)), columns=list('ABCDE'))
df
A B C D E
0 5 0 3 3 7
1 9 3 5 2 4
2 7 6 8 8 1
什么是SettingWithCopyWarning
?
要知道如何处理这个警告,重要的是要理解它的含义以及为什么会首先引发它。
在过滤数据框时,可能会对框架进行切片/索引,以返回一个视图或副本,具体取决于内部布局和各种实现细节。 "视图"就像其名称所示,是原始数据的视图,因此修改视图可能会修改原始对象。另一方面,"副本"是从原始数据复制的数据,修改副本不会影响原始数据。
正如其他答案所提到的,SettingWithCopyWarning
是为了标记"链式赋值"操作而创建的。考虑上面设置中的df
。假设您想选择列"B"中所有大于5的列"A"中的值。 Pandas允许您以不同的方式执行此操作,有些比其他方式更正确。例如:
df[df.A > 5]['B']
1 3
2 6
Name: B, dtype: int64
而且,
df.loc[df.A > 5, 'B']
1 3
2 6
Name: B, dtype: int64
这些返回相同的结果,因此如果您只是读取这些值,则没有区别。那么问题是什么?链式赋值的问题在于通常很难预测返回的是视图还是副本,因此当您尝试将值分配回去时,这在很大程度上成为一个问题。为了进一步说明,考虑解释器如何执行此代码:
df.loc[df.A > 5, 'B'] = 4
df.__setitem__((df.A > 5, 'B'), 4)
通过单个__setitem__
调用到df
。另一方面,考虑以下代码:
df[df.A > 5]['B'] = 4
df.__getitem__(df.A > 5).__setitem__('B', 4)
现在,取决于__getitem__
返回的是视图还是副本,__setitem__
操作可能无法正常工作。
一般来说,您应该使用loc
进行基于标签的赋值,使用iloc
进行基于整数/位置的赋值,因为规范保证它们始终操作原始数据。此外,对于设置单个单元格,您应该使用at
和iat
。
更多信息请参见文档。
Note
All boolean indexing operations done with loc
can also be done with iloc
. The only difference is that iloc
expects either
integers/positions for index or a numpy array of boolean values, and
integer/position indexes for the columns.
For example,
df.loc[df.A > 5, 'B'] = 4
Can be written nas
df.iloc[(df.A > 5).values, 1] = 4
And,
df.loc[1, 'A'] = 100
Can be written as
df.iloc[1, 0] = 100
And so on.
从pandas >= 2.0开始,你可以启用写时复制优化来节省内存并避免在可能的情况下进行数据复制,直到写入。
可以通过以下方式启用:
pd.options.mode.copy_on_write = True
在此之后,尝试进行链式赋值将会导致
ChainedAssignmentError: A value is trying to be set on a copy of a DataFrame or Series through chained assignment.
When using the Copy-on-Write mode, such chained assignment never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy.
Try using '.loc[row_indexer, col_indexer] = value' instead, to perform the assignment in a single step.
这个错误和SettingWithCopyWarning
类似,在相似的环境中被触发。
告诉我如何抑制警告!
考虑对df
的“A”列进行简单操作。选择“A”并除以2会引发警告,但操作仍将正常工作。
df2 = df[['A']]
df2['A'] /= 2
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/IPython/__main__.py:1: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
df2
A
0 2.5
1 4.5
2 3.5
有几种直接消除这个警告的方法:
(推荐) 使用 loc
切片子集:
df2 = df.loc[:, ['A']]
df2['A'] /= 2
更改 pd.options.mode.chained_assignment
可以设置为 None
、"warn"
或 "raise"
。默认为 "warn"
。 None
将完全抑制警告,"raise"
将抛出 SettingWithCopyError
,防止操作继续进行。
pd.options.mode.chained_assignment = None
df2['A'] /= 2
创建一个 deepcopy
df2 = df[['A']].copy(deep=True)
df2['A'] /= 2
@Peter Cotton 在评论中提出了一种非侵入式地改变模式的好方法(修改自this gist),使用上下文管理器,仅在需要时设置模式,并在完成后将其重置回原始状态。
class ChainedAssignent:
def __init__(self, chained=None):
acceptable = [None, 'warn', 'raise']
assert chained in acceptable, "chained must be in " + str(acceptable)
self.swcw = chained
def __enter__(self):
self.saved_swcw = pd.options.mode.chained_assignment
pd.options.mode.chained_assignment = self.swcw
return self
def __exit__(self, *args):
pd.options.mode.chained_assignment = self.saved_swcw
使用方法如下:
with ChainedAssignent():
df2['A'] /= 2
或者,抛出异常
with ChainedAssignent(chained='raise'):
df2['A'] /= 2
SettingWithCopyError:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
"XY 问题":我做错了什么?
很多时候,用户试图寻找抑制异常的方法,而不完全理解为什么首先会引发它。这是一个XY 问题的很好的例子,其中用户试图解决一个实际上是更深层次根源问题 "X" 的症状问题 "Y"。将根据遇到此警告的常见问题提出问题,并随后提供解决方案。
Question 1
I have a DataFrame
df
A B C D E
0 5 0 3 3 7
1 9 3 5 2 4
2 7 6 8 8 1
I want to assign values in col "A" > 5 to 1000. My expected output is
A B C D E
0 5 0 3 3 7
1 1000 3 5 2 4
2 1000 6 8 8 1
错误的做法:
df.A[df.A > 5] = 1000
df[df.A > 5]['A'] = 1000
df.loc[df.A > 5]['A'] = 1000
使用loc
的正确方式:
df.loc[df.A > 5, 'A'] = 1000
Question 21
I am trying to set the value in cell (1, 'D') to 12345. My expected output is
A B C D E
0 5 0 3 3 7
1 9 3 5 12345 4
2 7 6 8 8 1
I have tried different ways of accessing this cell, such as
df['D'][1]
. What is the best way to do this?
1. This question isn't specifically related to the warning, but
it is good to understand how to do this particular operation correctly
so as to avoid situations where the warning could potentially arise in
future.
您可以使用以下任何一种方法来实现此目的。
df.loc[1, 'D'] = 12345
df.iloc[1, 3] = 12345
df.at[1, 'D'] = 12345
df.iat[1, 3] = 12345
Question 3
I am trying to subset values based on some condition. I have a
DataFrame
A B C D E
1 9 3 5 2 4
2 7 6 8 8 1
I would like to assign values in "D" to 123 such that "C" == 5. I
tried
df2.loc[df2.C == 5, 'D'] = 123
Which seems fine but I am still getting the
SettingWithCopyWarning
! How do I fix this?
这可能是因为你的管道中更高层的代码问题。你是不是从一个更大的东西(比如)创建了
df2
?
df2 = df[df.A > 5]
在这种情况下,布尔索引将返回一个视图,因此
df2
将引用原始数据。你需要做的是将
df2
分配给一个
副本:
df2 = df[df.A > 5].copy()
Question 4
I'm trying to drop column "C" in-place from
A B C D E
1 9 3 5 2 4
2 7 6 8 8 1
But using
df2.drop('C', axis=1, inplace=True)
Throws SettingWithCopyWarning
. Why is this happening?
这是因为
df2
必须已经被创建为某些其他切片操作的视图,比如:
df2 = df[df.A > 5]
这里的解决方案是要么制作一个df
的copy()
,要么像以前一样使用loc
。
df.set_value
已被弃用,Pandas现在建议使用.at[]
或.iat[]
。文档在这里:https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.at.html - Kyle Cdf.loc[:, foo]
可以避免SettingWithCopyWarning
警告,而df[foo]
会引起SettingWithCopyWarning
警告。 - Asclepius