DataFrame数据选取全攻略

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内置的布尔运算符andor不可以进行覆写,而此时又需要将布尔运算扩展至每个元素,因而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.总结

最后,结合上述内容和我个人的使用经验,总结一些我个人最佳实践

  1. 数据在读取时就需要进行处理,首先一点就是应该将每一行作为一个样本,一列作为一个特征,由于大多数时候是根据属性对样本进行筛选,所以此时筛选起来就会更加的便捷。(df.T可以获得经过转置的DataFrame即行列交换)
  2. 每一个样本即每一行都必须使用唯一的index(行名),这一点有点数据库的主键的意思,这样就能保证我们能准确选到我们需要的数据,至于列名,如果根据前面的规则,则应该不会存在相同的特征,且全部行名和列名统一为一种类型,推介字符串类型。
  3. 当需要选中一列时,直接使用df[col]的方法选中一列,而需要选中一行的时候,直接使用df.loc[row]来选中一行。
  4. 在任何操作中尽可能避免使用坐标进行定位。
  5. 当需要同时对行列进行操作时,使用df.loc[row, loc]并在心里默念先行后列,同时,这种方式的参数比较多样,可以输入判断结果,也可输入单个行列名或者包含多个行列名的列表,可以按需选择。
  6. 更多待补充。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341

推荐阅读更多精彩内容