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.總結
最後,結合上述内容和我個人的使用經驗,總結一些我個人最佳實踐
- 資料在讀取時就需要進行處理,首先一點就是應該将每一行作為一個樣本,一列作為一個特征,由于大多數時候是根據屬性對樣本進行篩選,是以此時篩選起來就會更加的便捷。(
可以獲得經過轉置的DataFrame即行列交換)df.T
- 每一個樣本即每一行都必須使用唯一的index(行名),這一點有點資料庫的主鍵的意思,這樣就能保證我們能準确選到我們需要的資料,至于列名,如果根據前面的規則,則應該不會存在相同的特征,且全部行名和列名統一為一種類型,推介字元串類型。
- 當需要選中一列時,直接使用
的方法選中一列,而需要選中一行的時候,直接使用df[col]
來選中一行。df.loc[row]
- 在任何操作中盡可能避免使用坐标進行定位。
- 當需要同時對行列進行操作時,使用
并在心裡默念先行後列,同時,這種方式的參數比較多樣,可以輸入判斷結果,也可輸入單個行列名或者包含多個行列名的清單,可以按需選擇。df.loc[row, loc]
- 更多待補充。
作者:yangmqglobe
連結:https://www.jianshu.com/p/127587a80491