实话实说,我一向不太喜欢Pandas,因为它的功能实在太过强大了,想要熟练地驾驭它,对于我这样的中老年人来说,学习成本偏高。不过,对于接受能力超强的年轻人而言,Pandas确实是数据处理方面不可或缺的利器,我的子侄辈中就有多人喜欢使用。正是因为他们在Pandas的使用过程中,不断地向我咨询问题,我在帮他们解决问题的过程中,也逐渐熟悉了Pandas。这不,今天中午又有问题提出来了,这次是一个非常经典的问题,几乎每个人都会遇到。可以说,学会了解决这个问题,才算真正理解了Pandas。我把这个问题产生的背景、原因和解决方案,尽可能用浅显的文字完整地记录在这里,希望这一篇文章能够成为Pandas的入门读物。更详细的教程,请参考我的另一篇博客《Pandas简明教程》。
1. 问题产生的背景
1.1 构造一个测试用的DataFrame
DataFrame是Pandas最核心最常用的数据结构,可以理解为二维的异构表格。所谓异构,是指DataFrame的每一列都可以拥有独立的数据类型,而不必像Numpy的多维数组(ndarray)那样所有元素必须是同一种数据类型。DataFrame的每一列都有一个列名,每一行都有一个索引。
有多种方式可以创建DataFrame对象,将字典数据转换为DataFrame对象是最常见的创建方法,字典的键对应的是DataFrame的列,键名自动称为列名。如果没有指定索引,则使用默认索引(从0开始的连续整数)。
>>> 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的取反符号(~)可以反向选取检索结果。
>>> 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),一般会写成这样:
>>> 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有什么不同呢?
原来,这是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到了吗?