不要对链式索引赋值!趟过了这个坑,才能真正迈入Pandas高手之列

实话实说,我一贯不太喜欢Pandas,由于它的功能实在太过强大了,想要熟练地驾驭它,对于我这样的中老年人来讲,学习成本偏高。不过,对于接受能力超强的年轻人而言,Pandas确实是数据处理方面不可或缺的利器,个人子侄辈中就有多人喜欢使用。正是由于他们在Pandas的使用过程当中,不断地向我咨询问题,我在帮他们解决问题的过程当中,也逐渐熟悉了Pandas。这不,今天中午又有问题提出来了,此次是一个很是经典的问题,几乎每一个人都会遇到。能够说,学会了解决这个问题,才算真正理解了Pandas。我把这个问题产生的背景、缘由和解决方案,尽量用浅显的文字完整地记录在这里,但愿这一篇文章可以成为Pandas的入门读物。更详细的教程,请参考个人另外一篇博客《Pandas简明教程》html

1. 问题产生的背景

1.1 构造一个测试用的DataFrame

DataFrame是Pandas最核心最经常使用的数据结构,能够理解为二维的异构表格。所谓异构,是指DataFrame的每一列均可以拥有独立的数据类型,而没必要像Numpy的多维数组(ndarray)那样全部元素必须是同一种数据类型。DataFrame的每一列都有一个列名,每一行都有一个索引。python

有多种方式能够建立DataFrame对象,将字典数据转换为DataFrame对象是最多见的建立方法,字典的键对应的是DataFrame的列,键名自动称为列名。若是没有指定索引,则使用默认索引(从0开始的连续整数)。web

>>> import pandas as pd
>>> data = {
	'华东科技': [1.91, 1.90, 1.86, 1.84],
	'长安汽车': [11.27, 11.14, 11.28, 11.71],
	'西藏矿业': [7.89, 7.79, 7.61, 7.50],
	'重庆啤酒': [50.46, 50.17, 50.28, 50.28]
}
>>> df = pd.DataFrame(data)
>>>> df
   华东科技   长安汽车  西藏矿业   重庆啤酒
0  1.91  11.27  7.89  50.46
1  1.90  11.14  7.79  50.17
2  1.86  11.28  7.61  50.28
3  1.84  11.71  7.50  50.28

1.2 条件检索

Pandas的条件检索很是灵活,下面的代码演示了最经常使用的几种方式。数组

>>> df[df.长安汽车 > 11.2] # 检索长安汽车股价大于11.2的全部行
   华东科技   长安汽车  西藏矿业   重庆啤酒
0  1.91  11.27  7.89  50.46
2  1.86  11.28  7.61  50.28
3  1.84  11.71  7.50  50.28
>>> df[(df.长安汽车 > 11.2) & (df.华东科技 < 1.9)] # 检索知足“与”条件的全部行
   华东科技   长安汽车  西藏矿业   重庆啤酒
2  1.86  11.28  7.61  50.28
3  1.84  11.71  7.50  50.28
>>> df[df.西藏矿业.isin([7.61, 7.89])] # 检索西藏矿业股价等于多个指定值的全部行
   华东科技   长安汽车  西藏矿业   重庆啤酒
0  1.91  11.27  7.89  50.46
2  1.86  11.28  7.61  50.28
>>> df[df.index.isin([1,3])] # 检索索引号等于指定值的全部行
   华东科技   长安汽车  西藏矿业   重庆啤酒
1  1.90  11.14  7.79  50.17
3  1.84  11.71  7.50  50.28

Pandas的条件检索,本质上和Numpy是同样的,返回的是布尔型的结果,咱们再用这个布尔型的结果去索引,获得了检索结果。数据结构

>>> df.长安汽车 > 11.2
0     True
1    False
2     True
3     True
Name: 长安汽车, dtype: bool

一样,使用Numpy的取反符号(~)能够反向选取检索结果。ide

>>> df[~df.index.isin([1,3])] # 取反
   华东科技   长安汽车  西藏矿业   重庆啤酒
0  1.91  11.27  7.89  50.46
2  1.86  11.28  7.61  50.28

1.3 修改检索到的数据

对于检索到的结果数据,若是想修改的话,好比改为无效值nan(须要提早导入Numpy),通常会写成这样:svg

>>> import numpy as np
>>> df[~df.index.isin([1,3])].iloc[:,:] = np.nan

然而,这样倒是行不通的。有趣的是,这不是一个错误,而是警告。到这里,恭喜你,经典的SettingWithCopyWarning问题终于出现了!解决了它,你就能够步入Pandas高手之列了。学习

Warning (from warnings module):
  File "C:\Users\xufive\AppData\Local\Programs\Python\Python37\lib\site-packages\pandas\core\indexing.py", line 671
    self._setitem_with_indexer(indexer, value)
SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

Warning (from warnings module):
  File "__main__", line 1
SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

2. 缘由分析

是使用loc选取数据的方法错误吗?显然不是,由于用loc直接选取df的数据作修改是没有任何问题的。测试

>>> df.iloc[:,:] = np.nan
>>> df
   华东科技  长安汽车  西藏矿业  重庆啤酒
0   NaN   NaN   NaN   NaN
1   NaN   NaN   NaN   NaN
2   NaN   NaN   NaN   NaN
3   NaN   NaN   NaN   NaN

虽然检索结果看起来也是一个DataFrame,但对检索结果再使用loc选取并修改数据,就出现了问题。原始的DataFrame和做为检索结果的DataFrame有什么不一样呢?ui

原来,这是Pandas的针对链式赋值(Chained Assignment)的保护机制致使的结果。所谓链式赋值,就是对索引的索引结果赋值。当咱们使用条件检索时,至关于一次索引,在对这个结果作loc选取,就是二次索引,也就是链式索引,而链式索引在Pandas体系中被禁止赋值。简单理解,就是咱们没法对经过两个方括号选取的数据赋值。

3. 解决方案

了解问题产生的缘由,就容易找到解决方案了:用检索所条件做为loc的行参数,将两次索引变成一次,天然就没有链式索引,赋值也就再也不受限了。如下是完整代码。

>>> import pandas as pd
>>>> import numpy as np
>>> data = {
	'华东科技': [1.91, 1.90, 1.86, 1.84],
	'长安汽车': [11.27, 11.14, 11.28, 11.71],
	'西藏矿业': [7.89, 7.79, 7.61, 7.50],
	'重庆啤酒': [50.46, 50.17, 50.28, 50.28]
}
>>> df = pd.DataFrame(data)
>>> df
   华东科技   长安汽车  西藏矿业   重庆啤酒
0  1.91  11.27  7.89  50.46
1  1.90  11.14  7.79  50.17
2  1.86  11.28  7.61  50.28
3  1.84  11.71  7.50  50.28
>>> df.loc[~df.index.isin([1,3]), :] = np.nan
>>> df
   华东科技   长安汽车  西藏矿业   重庆啤酒
0   NaN    NaN   NaN    NaN
1  1.90  11.14  7.79  50.17
2   NaN    NaN   NaN    NaN
3  1.84  11.71  7.50  50.28

loc的列参数,除了冒号(:)指定所有列,也能够用列名指定单列,或者用元组指定多列。

>>> df = pd.DataFrame(data)
>>> df
   华东科技   长安汽车  西藏矿业   重庆啤酒
0  1.91  11.27  7.89  50.46
1  1.90  11.14  7.79  50.17
2  1.86  11.28  7.61  50.28
3  1.84  11.71  7.50  50.28
>>> df.loc[df.长安汽车 > 11.2, ('华东科技', '西藏矿业', '重庆啤酒')] = np.nan
>>> df
   华东科技   长安汽车  西藏矿业   重庆啤酒
0   NaN  11.27   NaN    NaN
1   1.9  11.14  7.79  50.17
2   NaN  11.28   NaN    NaN
3   NaN  11.71   NaN    NaN

玩转Pandas就是这么简单,你get到了吗?