SettingWithCopyWarning是pandas中一个经典问题,也是pandas库中位数不多的坑之一。关于这个问题,我们先看下面的一个例子。
import pandas as pd
def t1():
data = {
'name': ['a', 'b', 'c', 'd', 'e', 'f'],
'num': [1, 2, 3, 4, 5, 6],
'ss': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
}
df = pd.DataFrame(data)
print(df, "\n")
df[df['name'] == 'a']['num'] = 10
print(df)
上面的代码本意,是想将df中name为’a’的行中,num赋值为10。我们看一下代码运行的结果:
name num ss
0 a 1 0.1
1 b 2 0.2
2 c 3 0.3
3 d 4 0.4
4 e 5 0.5
5 f 6 0.6
/Users/wanglei/wanglei/code/python/finance-trade/p2/DfCopyCode.py:19: 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
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
df[df['name'] == 'a']['num'] = 10
name num ss
0 a 1 0.1
1 b 2 0.2
2 c 3 0.3
3 d 4 0.4
4 e 5 0.5
5 f 6 0.6
首先代码给出了SettingWithCopyWarning的告警。注意是Warning,而不是Error。
然后我们观察输出,发现赋值并没有起到预期的效果,df的值没有发生变化。
具体原因在哪里?
要了解 SettingWithCopyWarning,首先要知道,Pandas 中的某些操作会返回数据的视图(View),某些操作会返回数据的副本(Copy)。
如上所示,左侧的视图 df2 只是原始数据 df1 一个子集,而右侧的副本创建了一个新的对象 df2。
当我们尝试对数据集进行更改时,这可能会出现问题:
根据需求,我们可能想要修改原始 df1(左),也可能想要修改 df2(右)。警告提醒我们,代码可能并没有符合需求,修改到的可能并不是我们想要修改的那个数据集。
(该部分图文来自参考文献1)
如何解决上面的问题?其实在SettingWithCopyWarning中已经给出了答案,使用loc。
def t2():
data = {
'name': ['a', 'b', 'c', 'd', 'e', 'f'],
'num': [1, 2, 3, 4, 5, 6],
'ss': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
}
df = pd.DataFrame(data)
df.loc[df['name'] == 'a', 'num'] = 10
print(df)
上面方法的输出为
name num ss
0 a 10 0.1
1 b 2 0.2
2 c 3 0.3
3 d 4 0.4
4 e 5 0.5
5 f 6 0.6
这样就达到了我们预期的目的。
为什么loc方法能保证达到预期?具体可以查阅参考文献1,解释得非常清楚详细。
下面我们再来看一个例子
def t3():
data = {
'name': ['a', 'b', 'c', 'd', 'e', 'f'],
'num': [1, 2, 3, 4, 5, 6],
'ss': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
}
df = pd.DataFrame(data)
subdf = df.loc[df.num <= 3]
print(subdf)
subdf.loc[subdf['name'] == 'a', 'num'] = 10
print(subdf)
上面代码的输出为
name num ss
0 a 1 0.1
1 b 2 0.2
2 c 3 0.3
/Users/wanglei/anaconda3/anaconda3/lib/python3.7/site-packages/pandas/core/indexing.py:965: 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
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
self.obj[item] = s
name num ss
0 a 10 0.1
1 b 2 0.2
2 c 3 0.3
我们看赋值已经成功了,subdf已经进行了改变,也使用了loc的方式进行复制,为什么还会有SettingWithCopyWarning告警呢?
链式索引可能在一行代码内发生,也可能跨越两行代码。因为subdf变量是作为 Get 操作的输出创建的,它可能是原始 DataFrame 的副本,也可能不是,除非检查,否则我们不能确认。对subdf进行索引时,实际上使用的就是链式索引。
这种情况下的警告解决方案是:创建新 DataFrame 时明确告知 Pandas 创建一个副本 (来自参考文献1)
def t4():
data = {
'name': ['a', 'b', 'c', 'd', 'e', 'f'],
'num': [1, 2, 3, 4, 5, 6],
'ss': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
}
df = pd.DataFrame(data)
subdf = df.loc[df.num <= 3].copy()
print(subdf)
subdf.loc[subdf['name'] == 'a', 'num'] = 10
print(subdf)
代码的输出为:
name num ss
0 a 1 0.1
1 b 2 0.2
2 c 3 0.3
name num ss
0 a 10 0.1
1 b 2 0.2
2 c 3 0.3
这种情况下,使用copy方法,明确告诉是创建的新副本即可。
有关SettingWithCopyWarning的问题,参考文献1中解释得非常详细,强烈建议仔细阅读!
参考文献:
1.https://zhuanlan.zhihu.com/p/41202576