pandas库的DataFrame,作为一种非常强大的数据处理手段,一直以来无论是从整个库的API设计和性能,都给我非常大的惊喜,但是,在由生疏到慢慢熟练的过程中,发现在利用DataFrame时,一个最大的问题就是,如何高效优雅地选取到自己需要的数据,毕竟大部分时候我们是不需要整个DataFrame中的所有数据的。而为了遵循python语言本身的设计哲学,这些操作几乎都是利用原有的运算符,pandas只是进行了覆写,因而这些技能都像是隐藏的大招,难以发现和掌握,故特意花时间研究了一下官方文档,现总结如下,见笑。
1.属性选取
属性选取,顾名思义,也就是直接使用面向对象思想中对象的属性这一特性,以进行数据的选取,这里我们先创建一个示例的数据
In [1]: import pandas as pd
In [2]: import numpy as np
In [3]: df = pd.DataFrame(np.random.randn(6, 6),
...: index=[1, 2, 'C', 'D', '5', '6'],
...: columns=['A', 'B', '3', '4', 5, 6])
注意,这里我们构建了一个非常特殊的DataFrame,特别是在index和columns这两个参数,我们使用了三种分别明显的index,有int型的,字符串型两种,字符型里面又有数字和字母不一,且两者是没有冲突的,下面会演示这些index和columns都有啥坑(以下可能将index称为行名,columns称为列名)。
正常情况下,使用属性选取方式是非常简单的,直接访问对象属性即可
In [4]: df.A
Out[4]:
1 0.124010
2 -1.520924
C -1.941521
D -0.613758
5 0.788941
6 0.894871
Name: A, dtype: float64
而如果我们访问不存在的列,将会抛出AttributeError
异常(为节约篇幅,以下错误演示去除了堆栈信息,仅保留异常信息,下同)
In [5]: df.G
AttributeError: 'DataFrame' object has no attribute 'G'
当然,属性选取有非常大的局限性,首先,这种方法不能选取到行,只能选取到列
In [6]: df.C
AttributeError: 'DataFrame' object has no attribute 'C'
其次就是其对于列名也有非常严格的要求,首先就是如果列名与Dataframe自带的方法、属性名一致的列都不能使用属性选取
In [7]: df.min
Out[7]:
<bound method DataFrame.min of A B 3 4
5 6
1 0.124010 -1.160599 -1.236521 -2.356423 0.089117 1.090875
2 -1.520924 -0.285893 0.918261 1.250223 2.028298 -0.007042
C -1.941521 0.716184 0.318086 -0.540593 -2.408134 0.526977
D -0.613758 0.718741 -1.420655 -0.182436 0.333909 -1.608134
5 0.788941 -0.541419 -1.195019 -0.924797 -0.261171 1.078212
6 0.894871 0.454283 -0.763443 0.065712 -0.336119 0.182736>
再次就是由于Python的命名规则,开头是数字的,或者带有其他非法符号的显然也不能使用属性选取
In [8]: df.3
SyntaxError: invalid syntax
最后,由于Python对象的动态特性,如果希望通过属性的方式添加一列,也是错误的
In [9]: df.G = np.arange(6)
In [10]: df
Out[10]:
A B 3 4 5 6
1 0.124010 -1.160599 -1.236521 -2.356423 0.089117 1.090875
2 -1.520924 -0.285893 0.918261 1.250223 2.028298 -0.007042
C -1.941521 0.716184 0.318086 -0.540593 -2.408134 0.526977
D -0.613758 0.718741 -1.420655 -0.182436 0.333909 -1.608134
5 0.788941 -0.541419 -1.195019 -0.924797 -0.261171 1.078212
6 0.894871 0.454283 -0.763443 0.065712 -0.336119 0.182736
In [11]: df.G
Out[11]: array([0, 1, 2, 3, 4, 5])
可以看到,我们只是动态地给DataFrame的实例对象加入了一个属性,而如果该列已经存在,是可以对该列进行更改的
In [12]: df.A = np.arange(6)
In [13]: df
Out[13]:
A B 3 4 5 6
1 0 -1.160599 -1.236521 -2.356423 0.089117 1.090875
2 1 -0.285893 0.918261 1.250223 2.028298 -0.007042
C 2 0.716184 0.318086 -0.540593 -2.408134 0.526977
D 3 0.718741 -1.420655 -0.182436 0.333909 -1.608134
5 4 -0.541419 -1.195019 -0.924797 -0.261171 1.078212
6 5 0.454283 -0.763443 0.065712 -0.336119 0.182736
所以,以上就是属性选取方法,可见,由于各种限制,属性选取有着非常大的局限,同时,这种操作还很可能得到意想不到的结果,所以我个人并不推介使用这种选取的方法。
2.基础选取
官方文档称这种方式为Basic Selecte,所以这里我也称之为基础选取方式,这种方式就是直接对DataFrame对象使用[]
运算符,其实现方式是实现类的__getitem__
魔术方法。
使用这种选取方法,非常简单,直接将需要选取的列名放入中括号中即可
In [14]: df['A']
Out[14]:
1 0
2 1
C 2
D 3
5 4
6 5
Name: A, dtype: int32
可以看到,这种方法可以非常方便地选取到一列数据,而当我们访问不存在的列时,将会抛出KeyError
,这与Python的魔术方法协议是一致的,这点也是我喜欢Python的原因之一,通过各种类似协议的魔术方法,使得类似的操作能得到同样的结果,比如求长度我就立刻知道应该使用len()
而不需要去猜是.length()
还是.size()
In [15]: df['G']
KeyError: 'G'
其次就是,这种方法只能选取一列,不可以同时选取到多个列
In [16]: df[('A', 'B')]
KeyError: ('A', 'B')
同时需要注意的是,我们传入的列名,其类型也必须和列名的类型相同
In [17]: df[3]
KeyError: 3
In [18]: df['3']
Out[18]:
1 -1.236521
2 0.918261
C 0.318086
D -1.420655
5 -1.195019
6 -0.763443
Name: 3, dtype: float64
最后,这种方法还可以对DataFrame进行切片,但此时是对行进行切片
In [19]: df[:'B']
KeyError: 'B'
In [20]: df[:3]
Out[20]:
A B 3 4 5 6
1 0 -1.160599 -1.236521 -2.356423 0.089117 1.090875
2 1 -0.285893 0.918261 1.250223 2.028298 -0.007042
C 2 0.716184 0.318086 -0.540593 -2.408134 0.526977
In [21]: df[:'D':-1]
Out[21]:
A B 3 4 5 6
6 5 0.454283 -0.763443 0.065712 -0.336119 0.182736
5 4 -0.541419 -1.195019 -0.924797 -0.261171 1.078212
D 3 0.718741 -1.420655 -0.182436 0.333909 -1.608134
可以看到,此时我们可以使用坐标进行切片,也可以输入行名,同时切片的第三个参数也是可以使用的,但如果我们尝试对列进行切片,则会抛出KeyError
。
3.标签选取
标签选取,即严格使用行列名(index)进行选取的方法,其需要使用.loc
属性
In [22]: df.loc['C']
Out[22]:
A 2.000000
B 0.716184
3 0.318086
4 -0.540593
5 -2.408134
6 0.526977
Name: C, dtype: float64
可以看到,这种方法其能够快速选定一行,而事实上,该方法可以同时定位行和列或者多行多列,此时只需要默念,先行后列,先行后列,先行后列,并使用逗号分隔
In [23]: df.loc['C', 'A']
Out[23]: 2.0
In [24]: df.loc[('C', 'D'), ('A', 'B')]
Out[24]:
A B
C 2 0.716184
D 3 0.718741
同时,该方法可以进行切片,并且可以行列同时进行,需要说明的是,这里只能接受两个参数,即行和列的切片,不能使用第三个参数
In [25]: df.loc['C':'6', '3':]
Out[25]:
3 4 5 6
C 0.318086 -0.540593 -2.408134 0.526977
D -1.420655 -0.182436 0.333909 -1.608134
5 -1.195019 -0.924797 -0.261171 1.078212
6 -0.763443 0.065712 -0.336119 0.182736
In [26]: df.loc['C':'6', '3':, -1]
IndexingError: Too many indexers
如果需要表示全部选中,比如选中所有的行,则必须要写出:
表示全部选中,即可以省略后面的参数,默认选中所有的列,但是,需要选中所有的行时,行参数不可以省略
In [27]: df.loc[:, 5]
Out[27]:
1 0.089117
2 2.028298
C -2.408134
D 0.333909
5 -0.261171
6 -0.336119
Name: 5, dtype: float64
如果我们选择不存在的位置,将会抛出KeyError
In [28]: df.loc['G']
KeyError: 'the label [G] is not in the [index]'
值得注意的是,该方法要求非常严格,其要求输入的行列名必须与DataFrame的行列名类型一致,否则抛出KeyError
In [29]: df.loc[6]
KeyError: 'the label [6] is not in the [index]'
4.坐标选取
大多数刚刚开始使用DataFrame的童鞋可能都非常喜欢使用坐标来选取数据,毕竟如果我们将DataFrame理解为一个二维的数组,那么使用坐标来选取就显得非常的浑然天成,但是这时候,我们会发现前面说到的方法多大都需要时行名列名且要确保其类型也是一致的,这时候传统的直接使用坐标进行选取就会失效。
如果我们仍然需要使用坐标进行数据选取,此时就需要使用.iloc
属性
In [30]: df.iloc[0]
Out[30]:
A 0.000000
B -1.160599
3 -1.236521
4 -2.356423
5 0.089117
6 1.090875
Name: 1, dtype: float64
与标签选取方法类似,这种方法也可以同时定位行和列、多行和多列,同样是先行后列,逗号分隔
In [31]: df.iloc[0, 5]
Out[31]: 1.0908752302725568
In [32]: df.iloc[(0, 2), (2, 3)]
Out[32]:
3 4
1 -1.236521 -2.356423
C 0.318086 -0.540593
同样的,也支持切片,使用:
表示全部选中,不可以有第三个参数
In [33]: df.iloc[:, :3]
Out[33]:
A B 3
1 0 -1.160599 -1.236521
2 1 -0.285893 0.918261
C 2 0.716184 0.318086
D 3 0.718741 -1.420655
5 4 -0.541419 -1.195019
6 5 0.454283 -0.763443
可以说,这个方法和标签选取的方法,其实是两兄弟,一个是使用标签,一个是使用坐标进行选取,当然,如果我们选取不存在的位置,其也会非常合理地抛出IndexError
In [34]: df.iloc[7]
IndexError: single positional indexer is out-of-bounds
而如果我们输入的参数不是整数,此时将会抛出TypeError
In [35]: df.iloc['C']
TypeError: cannot do positional indexing with these indexers [C] of <class 'str'>
最后需要注意的是,这里说的坐标都是实时的,所以,我们需要注意,不要与标签选取的方法混淆,标签选取无论该数据在表格的什么位置都能够选取到,而定位选取则有可能给我们不一样的结果
In [36]: df.sort_values('B').iloc[1]
Out[35]:
A 4.000000
B -0.541419
3 -1.195019
4 -0.924797
5 -0.261171
6 1.078212
Name: 5, dtype: float64
In [37]: df.sort_values('B').loc[1]
Out[37]:
A 0.000000
B -1.160599
3 -1.236521
4 -2.356423
5 0.089117
6 1.090875
Name: 1, dtype: float64
可以看到,即使顺序打乱了,我们仍能根据行名准确得到对应的行,而如果是是用坐标,那么我们得到的就是B列最小的那一行即这里的行名为5的行,所以在选择使用标签选取或者是定位选取的时候,一定要明确自己的需求。
5.条件筛选
当我们需要按列筛选行的时候,根据上面所说的方法,直接使用[]
,这里是一致的
In [38]: df[df['B'] > 0]
Out[38]:
A B 3 4 5 6
C 2 0.716184 0.318086 -0.540593 -2.408134 0.526977
D 3 0.718741 -1.420655 -0.182436 0.333909 -1.608134
6 5 0.454283 -0.763443 0.065712 -0.336119 0.182736
而按行筛选列时,则有一点不同,正确的方法是
In [39]: df.loc[:, df.loc['C'] > 0]
Out[39]:
A B 3 6
1 0 -1.160599 -1.236521 1.090875
2 1 -0.285893 0.918261 -0.007042
C 2 0.716184 0.318086 0.526977
D 3 0.718741 -1.420655 -1.608134
5 4 -0.541419 -1.195019 1.078212
6 5 0.454283 -0.763443 0.182736
是否觉得很诡异,这里我们可以探究一下pandas是如何实现这个筛选逻辑的
In [40]: df['B'] > 0
Out[40]:
1 False
2 False
C True
D True
5 False
6 True
Name: B, dtype: bool
In [41]: df.loc['C'] > 0
Out[41]:
A True
B True
3 True
4 False
5 False
6 True
Name: C, dtype: bool
可以看到,当我们取出一列进行大小判断时,其返回的是所有的行的判断结果(Series对象),而如果我们对一行进行判断,则返回的是各个列的判断结果,即pandas通过覆写大小判断运算符将判断操作自动扩展至对象中的每个元素,这点和R的逻辑是契合的。而上面我们也说到过,当我们使用的是[]
进行切片时,我们是对行进行切片,而这里,我们刚好就填入了行的判断结果(In [40]),所以,如果我们输入的切片参数是Series对象时,pandas将切出被判断为True
的行。对列进行筛选的操作也是同样的,只不过先行后列,所以前面我们需要:
来表示我们选中所有的行,同时也就意味着,使用.loc
时,可以同时对行和列进行筛选,只需要同时给出判断结果即可
In [42]: df.loc[df['B'] > 0, df.loc['C'] > 0]
Out[42]:
A B 3 6
C 2 0.716184 0.318086 0.526977
D 3 0.718741 -1.420655 -1.608134
6 5 0.454283 -0.763443 0.182736
到这里,相信这个筛选操作就比较好理解了,但是有一点需要注意的是,我们不能使用定位方法进行筛选
In [43]: df.iloc[1] > 0
Out[43]:
A True
B False
3 True
4 True
5 True
6 False
Name: 2, dtype: bool
可以看到,即使我们使用定位方法选中的行列并进行判断后,其仍然使用标签进行定位,所以我们可以知道,在pandas的设计当中,标签定位的优先级是大于坐标定位的,因而在使用pandas库时,应该转变思想,要将理念从二维数组中跳脱出来,将pandas理解为一个更像数据库的存在。
最后,也就是多条件联合筛选,由于python内置的布尔运算符and
和or
不可以进行覆写,而此时又需要将布尔运算扩展至每个元素,因而pandas覆写了&
和|
这两个运算符,写过其他语言的小伙伴应该不陌生,要注意的是,这里只需要写一次,不像Java等需要写两次。
In [44]: df[(df['B'] > 0) & (df['3'] > 0)]
Out[44]:
A B 3 4 5 6
C 2 0.716184 0.318086 -0.540593 -2.408134 0.526977
In [45]: df.loc[:, (df.loc['C'] > 0) | (df.loc['5'] > 0)]
Out[45]:
A B 3 6
1 0 -1.160599 -1.236521 1.090875
2 1 -0.285893 0.918261 -0.007042
C 2 0.716184 0.318086 0.526977
D 3 0.718741 -1.420655 -1.608134
5 4 -0.541419 -1.195019 1.078212
6 5 0.454283 -0.763443 0.182736
需要注意的是,由于只有进行判断结束返回的Series对象,才可以进行布尔运算,所以这里需要给每个判断条件加上小括号以提高运算优先级。
6.总结
最后,结合上述内容和我个人的使用经验,总结一些我个人最佳实践
- 数据在读取时就需要进行处理,首先一点就是应该将每一行作为一个样本,一列作为一个特征,由于大多数时候是根据属性对样本进行筛选,所以此时筛选起来就会更加的便捷。(
df.T
可以获得经过转置的DataFrame即行列交换) - 每一个样本即每一行都必须使用唯一的index(行名),这一点有点数据库的主键的意思,这样就能保证我们能准确选到我们需要的数据,至于列名,如果根据前面的规则,则应该不会存在相同的特征,且全部行名和列名统一为一种类型,推介字符串类型。
- 当需要选中一列时,直接使用
df[col]
的方法选中一列,而需要选中一行的时候,直接使用df.loc[row]
来选中一行。 - 在任何操作中尽可能避免使用坐标进行定位。
- 当需要同时对行列进行操作时,使用
df.loc[row, loc]
并在心里默念先行后列,同时,这种方式的参数比较多样,可以输入判断结果,也可输入单个行列名或者包含多个行列名的列表,可以按需选择。 - 更多待补充。