Pandas学习笔记:如何处理Pandas中的SettingWi
在用pandas时经常会提示:SettingWithCopyWarning,虽然运行没有出错,但是总是会有错误提示。对此问题,在stackoverflow有很好的回答,这里翻译一个写的比较清楚的回答。
原文地址:https://stackoverflow.com/questions/20625582/how-to-deal-with-settingwithcopywarning-in-pandas
问题:
如何处理Pandas中的SettingWithCopyWarning?
背景:
我刚从pandas0.11升级到0.13.0rc1,结果出现了很多新的警告,其中一个是:
E:\FinReporter\FM_EXT.py:449: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
quote_df['TVol'] = quote_df['TVol']/TVOL_SCALE
我想知道这个警告到底是什么意思?我需要改变什么吗?
如果我坚持用quote_df['TVol'] = quote_df['TVol']/TVOL_SCALE
这条代码,该怎么去掉这个警告呢?
触发错误的函数:
def _decode_stock_quote(list_of_150_stk_str):
"""decode the webpage and return dataframe"""
from cStringIO import StringIO
str_of_all = "".join(list_of_150_stk_str)
quote_df = pd.read_csv(StringIO(str_of_all), sep=',', names=list('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg')) #dtype={'A': object, 'B': object, 'C': np.float64}
quote_df.rename(columns={'A':'STK', 'B':'TOpen', 'C':'TPCLOSE', 'D':'TPrice', 'E':'THigh', 'F':'TLow', 'I':'TVol', 'J':'TAmt', 'e':'TDate', 'f':'TTime'}, inplace=True)
quote_df = quote_df.ix[:,[0,3,2,1,4,5,8,9,30,31]]
quote_df['TClose'] = quote_df['TPrice']
quote_df['RT'] = 100 * (quote_df['TPrice']/quote_df['TPCLOSE'] - 1)
quote_df['TVol'] = quote_df['TVol']/TVOL_SCALE
quote_df['TAmt'] = quote_df['TAmt']/TAMT_SCALE
quote_df['STK_ID'] = quote_df['STK'].str.slice(13,19)
quote_df['STK_Name'] = quote_df['STK'].str.slice(21,30)#.decode('gb2312')
quote_df['TDate'] = quote_df.TDate.map(lambda x: x[0:4]+x[5:7]+x[8:10])
return quote_df
更多错误信息
E:\FinReporter\FM_EXT.py:449: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
quote_df['TVol'] = quote_df['TVol']/TVOL_SCALE
E:\FinReporter\FM_EXT.py:450: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
quote_df['TAmt'] = quote_df['TAmt']/TAMT_SCALE
E:\FinReporter\FM_EXT.py:453: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
quote_df['TDate'] = quote_df.TDate.map(lambda x: x[0:4]+x[5:7]+x[8:10])
回答:
对于这个问题比较全面的回答
作者:coldspeed(https://stackoverflow.com/users/4909087/coldspeed)
这篇文章是为这些读者准备的:
1.想要理解这个警告(warning)的具体含义的人;
2.想要了解如何关掉这个警告的不同方式的人;
3.想要弄清楚如何改善自己代码并在未来使用使用优秀的代码用例来避免此类警告发生的人。
前言(setup)
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)?
(这种常用词下文首次出现时用中英文,后面直接用英文)
要弄清楚如何处理这种警告,首先要弄清楚它的含义和出现的原因。
当过滤(filter)数据集(DataFrame)时,对数据集进行切片或者引用操作有可能会返回一个视图(view),也可能返回一个副本(copy),这取决于内在的程序设计或者各种执行细节。View顾名思义,就是对原始数据的观察,因此修改视图也可能会直接改变原数据。另一方面,副本(copy)是对原数据的复制,因此修改副本对于原数据没有影响。
其他答案已经提到过了,SettingWithCopyWarning是为了标识连锁分派(chained assignment)操作。思考一下前言中的df数据集。假设你要选取B列的数据,要求其关联的A列数据大于5。在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
这些操作返回的结果是一样的,如果你只是看这些值得话,返回的结果没有任何区别。那么,关键点在哪?对于chained assignment来说,问题就在于通常很难判断返回的是view还是copy,当你试图撤回某些值得时候,这就会成为很大的一个问题。结合前面的例子,考虑一下编译器是如何执行这条代码的。
df.loc[df.A > 5, 'B'] = 4
# becomes
df.__setitem__((df.A > 5, 'B'), 4)
这里只调用了setitem。
再来看这条代码:
df[df.A > 5]['B'] = 4
# becomes
df.__getitem__(df.A > 5).__setitem__('B", 4)
在这种情况下,setitem可能不会工作,这取决于getitem返回的是view还是copy。
通常情况下,应该对于标签指派查询,应该使用loc方法,对于整数/位置指派查询,应该使用iloc。此外,对于一个单元格数据,应该使用at和iat。
pandas文档中有更多说明(http://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html)。
提示
所有使用loc的布尔型索引操作都可以用iloc完成。唯一的区别就在于iloc对于index位置上操作要求整数/位置或者一个有布尔值的numpy数据,对于列的位置用整数/位置的索引。
举个例子
df.loc[df.A > 5, 'B'] = 4
可以写成
df.iloc[(df.A > 5).values, 1] = 4
再有
df.loc[1, 'A'] = 100
也可以写成
df.iloc[1, 0] = 100
等等。
仅仅告诉我如何关掉这个警告!
举个简单的例子,我们现在对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
下面是关掉这个警告的方法。
使用深层拷贝(deepcopy)
df2 = df[['A']].copy(deep=True)
df2['A'] /= 2
设置is_copy=False
通过设置is_copy=False来关掉对某个DataFrame的检查。
df2.is_copy = False
df2['A'] /= 2
(译者注:使用这个方法要小心。因为系统会提示:Attribute 'is_copy' is deprecated and will be removed in a future version也就是说未来版本中会移除这个属性。)
改变 pd.options.mode.chained_assignment这个状态
pd.options.mode.chained_assignment能被设置为"None"、"warn"或"raise"三种。"warn"是默认状态。"None"会完全关掉警告。"raise"则会扔出一个SettingWithCopyError,防止操作发生。
pd.options.mode.chained_assignment = None
df2['A'] /= 2
其他方法
@Peter Cotton提出了一种不改变内在状态(即pd.options.mode.chained_assignment)的解决方法,他使用的是文本管理器(context manager),只有在需要时才改变状态,并且在操作完成后回复原状态。
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
用法如下:
# some code here
with ChainedAssignent():
df2['A'] /= 2
# more code follows
或者提出异常
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 Problem":我到底做错什么了?
很多时候,用户们都在试图找到方法避免这种异常,但是又没有完全理解为什么会提出这个异常。这个例子是个很好的XY问题,即当用户试图去解决Y问题,而实际上这个问题的根源在X问题。关于这个警告的问题我会列出,解决方法也会展示出来。
问题1
现在有一个数据集(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
我想把A列大于5的值改为1000。我期望的结果是:
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.A返回一个试图
df[df.A > 5]['A'] = 1000 # 不工作
df.loc[df.A 5]['A'] = 1000 # 不工作
正确的方法应该是使用loc:
df.loc[df.A > 5, 'A'] = 1000
问题2
这个问题和这个警告并不具体相关,但是能帮助我们正确如何使用正确的操作,从而防止未来可能出现的警告。
我想把单元格(1,'D')的值变为12345,我期望的结果是:
A B C D E
0 5 0 3 3 7
1 9 3 5 12345 4
2 7 6 8 8 1
我使用了很多不同的方法来选取这个单元格,比如df['D'][1].哪一个才是最好的方式呢?
方法很多,任何一种都行。
df.loc[1, 'D'] = 12345
df.iloc[1, 3] = 12345
df.at[1, 'D'] = 12345
df.iat[1, 3] = 12345
问题3
我想通过条件筛选出子集,例如数据集:
A B C D E
1 9 3 5 2 4
2 7 6 8 8 1
我想给D列的某些值赋值123,比如在C列等于5的情况下。我使用了
df2.loc[df2.C == 5, 'D'] = 123
这个法子看起来不错,但是还是出现了SettingWithCopyWarning!我该怎么解决这个问题?
这可能源于你前面的代码。你之前是通过条件筛选创建的df2吗?比如:
df2 = df[df.A > 5]
在这种情况下,布尔型的索引将会返回一个view,因此df2将指向原始数据。你需要做的是将df2声明为df的一个副本。
df2 = df[df.A > 5].copy()
或者
df2 = df.loc[df.A > 5, :]
问题4
我想在原数据集里删除C列:
A B C D E
1 9 3 5 2 4
2 7 6 8 8 1
但是使用
df2.drop('C', axis=1, inplace=True)
仍然触发了SettingWithCopyWarning,为什么会出现这种情况呢?
这种情况应该也是因为df2一个从别的切片(slicing)操作中创建的view,比如:
df2 = df[df.A > 5]
解决方法是要么使用copy()或者使用loc,就像前文一样。