譯者:山陰少年
原文連結:
https://nbviewer.jupyter.org/github/justmarkham/pandas-videos/blob/master/top_25_pandas_tricks.ipynb
pandas是Python的一個數據分析庫,提供如DataFrame等十分容易操作的數據結構,是近年做數據分析時不可或缺的工具之一。本文一共為大家分享25個pandas技巧。
輸入下面的命令查詢pandas版本:
In [7]:pd.__version__Out[7]:'0.24.2'如果你還想知道pandas所依賴的模塊的版本,你可以使用show_versions()函數:
In [9]:pd.show_versions()
INSTALLED VERSIONS --- commit: None python: 3.7.3.final.0 python-bits: 64 OS: Darwin OS-release: 18.6.0 machine: x86_64 processor: i386 byteorder: little LC_ALL: None LANG: en_US.UTF-8 LOCALE: en_US.UTF-8
pandas: 0.24.2 pytest: None pip: 19.1.1 setuptools: 41.0.1 Cython: None numpy: 1.16.4 scipy: None pyarrow: None xarray: None IPython: 7.5.0 sphinx: None patsy: None dateutil: 2.8.0 pytz: 2019.1 blosc: None bottleneck: None tables: None numexpr: None feather: None matplotlib: 3.1.0 openpyxl: None xlrd: None xlwt: None xlsxwriter: None lxml.etree: None bs4: None html5lib: None sqlalchemy: None pymysql: None psycopg2: None jinja2: 2.10.1 s3fs: None fastparquet: None pandas_gbq: None pandas_datareader: None gcsfs: None你可以查看到Python,pandas, Numpy, matplotlib等的版本信息。
假設你需要創建一個示例DataFrame。有很多種實現的途徑,我最喜歡的方式是傳一個字典給DataFrame constructor,其中字典中的keys為列名,values為列的取值。
現在如果你需要創建一個更大的DataFrame,上述方法則需要太多的輸入。在這種情況下,你可以使用Numpy的random.rand()函數,告訴它行數和列數,將它傳遞給DataFrame constructor:
這種方式很好,但如果你還想把列名變為非數值型的,你可以強制地將一串字符賦值給columns參數:
你可以想到,你傳遞的字符串的長度必須與列數相同。
3.更改列名
讓我們來看一下剛才我們創建的示例DataFrame:
我更喜歡在選取pandas列的時候使用點(.),但是這對那麼列名中含有空格的列不會生效。讓我們來修復這個問題。
更改列名最靈活的方式是使用rename()函數。你可以傳遞一個字典,其中keys為原列名,values為新列名,還可以指定axis:In [14]:df = df.rename({'col one':'col_one', 'col two':'col_two'}, axis='columns')使用這個函數最好的方式是你需要更改任意數量的列名,不管是一列或者全部的列。
如果你需要一次性重新命令所有的列名,更簡單的方式就是重寫DataFrame的columns屬性:In [15]:df.columns = ['col_one', 'col_two']如果你需要做的僅僅是將空格換成下劃線,那麼更好的辦法是使用str.replace()方法,這是因為你都不需要輸入所有的列名:
In [16]:df.columns = df.columns.str.replace(' ', '_')上述三個函數的結果都一樣,可以更改列名使得列名中不含有空格:
最後,如果你需要在列名中添加前綴或者後綴,你可以使用add_prefix()函數:
或者使用add_suffix()函數:
讓我們來看一下drinks這個DataFame:
In [20]:drinks.head()
Out[20]:
countrybeer_servingsspirit_servingswine_servingstotal_litres_of_pure_alcoholcontinent0Afghanistan0000.0Asia1Albania89132544.9Europe2Algeria250140.7Africa3Andorra24513831212.4Europe4Angola21757455.9Africa該數據集描述了每個國家的平均酒消費量。如果你想要將行序反轉呢?
最直接的辦法是使用loc函數並傳遞::-1,跟Python中列表反轉時使用的切片符號一致:
In [21]:drinks.loc[::-1].head()
Out[21]:
countrybeer_servingsspirit_servingswine_servingstotal_litres_of_pure_alcoholcontinent192Zimbabwe641844.7Africa191Zambia321942.5Africa190Yemen6000.1Asia189Vietnam111212.0Asia188Venezuela33310037.7South America如果你還想重置索引使得它從0開始呢?
你可以使用reset_index()函數,告訴他去掉完全拋棄之前的索引:In [22]:drinks.loc[::-1].reset_index(drop=True).head()
Out[22]:
countrybeer_servingsspirit_servingswine_servingstotal_litres_of_pure_alcoholcontinent0Zimbabwe641844.7Africa1Zambia321942.5Africa2Yemen6000.1Asia3Vietnam111212.0Asia4Venezuela33310037.7South America你可以看到,行序已經反轉,索引也被重置為默認的整數序號。
跟之前的技巧一樣,你也可以使用loc函數將列從左至右反轉:In [23]:drinks.loc[:, ::-1].head()
Out[23]:
continenttotal_litres_of_pure_alcoholwine_servingsspirit_servingsbeer_servingscountry0Asia0.0000Afghanistan1Europe4.95413289Albania2Africa0.714025Algeria3Europe12.4312138245Andorra4Africa5.94557217Angola逗號之前的冒號表示選擇所有行,逗號之後的::-1表示反轉所有的列,這就是為什麼country這一列現在在最右邊。
這裡有drinks這個DataFrame的數據類型:
In [24]:drinks.dtypes
Out[24]:country objectbeer_servings int64spirit_servings int64wine_servings int64total_litres_of_pure_alcohol float64continent objectdtype: object假設你僅僅需要選取數值型的列,那麼你可以使用select_dtypes()函數:
In [25]:drinks.select_dtypes(include='number').head()
Out[25]:
beer_servingsspirit_servingswine_servingstotal_litres_of_pure_alcohol00000.0189132544.92250140.7324513831212.4421757455.9這包含了int和float型的列。
你也可以使用這個函數來選取數據類型為object的列:
你還可以選取多種數據類型,只需要傳遞一個列表即可:
你還可以用來排除特定的數據類型:
讓我們來創建另一個示例DataFrame:
這些數字實際上儲存為字符型,導致其數據類型為object:
為了對這些列進行數學運算,我們需要將數據類型轉換成數值型。你可以對前兩列使用astype()函數:
但是,如果你對第三列也使用這個函數,將會引起錯誤,這是因為這一列包含了破折號(用來表示0)但是pandas並不知道如何處理它。
你可以對第三列使用to_numeric()函數,告訴其將任何無效數據轉換為NaN:
如果你知道NaN值代表0,那麼你可以fillna()函數將他們替換成0:
最後,你可以通過apply()函數一次性對整個DataFrame使用這個函數:
僅需一行代碼就完成了我們的目標,因為現在所有的數據類型都轉換成float:
pandas DataFrame被設計成可以適應內存,所以有些時候你可以減小DataFrame的空間大小,讓它在你的系統上更好地運行起來。
這是drinks這個DataFrame所佔用的空間大小:
可以看到它使用了30.4KB。
如果你對你的DataFrame有操作方面的問題,或者你不能將它讀進內存,那麼在讀取文件的過程中有兩個步驟可以使用來減小DataFrame的空間大小。第一個步驟是只讀取那些你實際上需要用到的列,可以調用usecols參數:
通過僅讀取用到的兩列,我們將DataFrame的空間大小縮小至13.6KB。
第二步是將所有實際上為類別變量的object列轉換成類別變量,可以調用dtypes參數:
通過將continent列讀取為category數據類型,我們進一步地把DataFrame的空間大小縮小至2.3KB。
值得注意的是,如果跟行數相比,category數據類型的列數相對較小,那麼catefory數據類型可以減小內存佔用。9.按行從多個文件中構建DataFrame
假設你的數據集分化為多個文件,但是你需要將這些數據集讀到一個DataFrame中。
舉例來說,我有一些關於股票的小數聚集,每個數據集為單天的CSV文件。這是第一天的:
這是第二天的:
這是第三天的:
你可以將每個CSV文件讀取成DataFrame,將它們結合起來,然後再刪除原來的DataFrame,但是這樣會多佔用內存且需要許多代碼
更好的方式為使用內置的glob模塊。你可以給glob()函數傳遞某種模式,包括未知字符,這樣它會返回符合該某事的文件列表。在這種方式下,glob會查找所有以stocks開頭的CSV文件:
glob會返回任意排序的文件名,這就是我們為什麼要用Python內置的sorted()函數來對列表進行排序。
我們以生成器表達式用read_csv()函數來讀取每個文件,並將結果傳遞給concat()函數,這會將單個的DataFrame按行來組合:
不幸的是,索引值存在重複。為了避免這種情況,我們需要告訴concat()函數來忽略索引,使用默認的整數索引:
上一個技巧對於數據集中每個文件包含行記錄很有用。但是如果數據集中的每個文件包含的列信息呢?
這裡有一個例子,dinks數據集被劃分成兩個CSV文件,每個文件包含三列:
同上一個技巧一樣,我們以使用glob()函數開始。這一次,我們需要告訴concat()函數按列來組合:
現在我們的DataFrame已經有六列了。
假設你將一些數據儲存在Excel或者Google Sheet中,你又想要儘快地將他們讀取至DataFrame中。
你需要選擇這些數據並複製至剪貼板。然後,你可以使用read_clipboard()函數將他們讀取至DataFrame中:
和read_csv()類似,read_clipboard()會自動檢測每一列的正確的數據類型:
讓我們再複製另外一個數據至剪貼板:
神奇的是,pandas已經將第一列作為索引了:
需要注意的是,如果你想要你的工作在未來可複製,那麼read_clipboard()並不值得推薦。
假設你想要將一個DataFrame劃分為兩部分,隨機地將75%的行給一個DataFrame,剩下的25%的行給另一個DataFrame。
舉例來說,我們的movie ratings這個DataFrame有979行:
我們可以使用sample()函數來隨機選取75%的行,並將它們賦值給"movies_1"DataFrame:
接著我們使用drop()函數來捨棄「moive_1」中出現過的行,將剩下的行賦值給"movies_2"DataFrame:
你可以發現總的行數是正確的:
你還可以檢查每部電影的索引,或者"moives_1":
或者"moives_2":
需要注意的是,這個方法在索引值不唯一的情況下不起作用。
註:該方法在機器學習或者深度學習中很有用,因為在模型訓練前,我們往往需要將全部數據集按某個比例劃分成訓練集和測試集。該方法既簡單又高效,值得學習和嘗試。
13.多種類型過濾DataFrame
讓我們先看一眼movies這個DataFrame:In [60]:movies.head()
Out[60]:
其中有一列是genre(類型):
比如我們想要對該DataFrame進行過濾,我們只想顯示genre為Action或者Drama或者Western的電影,我們可以使用多個條件,以"or"符號分隔:
In [62]:movies[(movies.genre == 'Action') | (movies.genre == 'Drama') | (movies.genre == 'Western')].head()
Out[62]:
但是,你實際上可以使用isin()函數將代碼寫得更加清晰,將genres列表傳遞給該函數:
In [63]:movies[movies.genre.isin(['Action', 'Drama', 'Western'])].head()
Out[63]:
如果你想要進行相反的過濾,也就是你將吧剛才的三種類型的電影排除掉,那麼你可以在過濾條件前加上破浪號:
In [64]:movies[~movies.genre.isin(['Action', 'Drama', 'Western'])].head()
Out[64]:
這種方法能夠起作用是因為在Python中,波浪號表示「not」操作。
14.DataFrame篩選數量最多類型
假設你想要對movies這個DataFrame通過genre進行過濾,但是只需要前3個數量最多的genre。
我們對genre使用value_counts()函數,並將它保存成counts(type為Series):
該Series的nlargest()函數能夠輕鬆地計算出Series中前3個最大值:
事實上我們在該Series中需要的是索引:
最後,我們將該索引傳遞給isin()函數,該函數會把它當成genre列表:
In [68]:movies[movies.genre.isin(counts.nlargest(3).index)].head()
Out[68]:
這樣,在DataFrame中只剩下Drame, Comdey, Action這三種類型的電影了。
15.處理缺失值
讓我們來看一看UFO sightings這個DataFrame:
你將會注意到有些值是缺失的。
為了找出每一列中有多少值是缺失的,你可以使用isna()函數,然後再使用sum():
isna()會產生一個由True和False組成的DataFrame,sum()會將所有的True值轉換為1,False轉換為0並把它們加起來。
類似地,你可以通過mean()和isna()函數找出每一列中缺失值的百分比。
如果你想要捨棄那些包含了缺失值的列,你可以使用dropna()函數:
或者你想要捨棄那麼缺失值佔比超過10%的列,你可以給dropna()設置一個閾值:
len(ufo)返回總行數,我們將它乘以0.9,以告訴pandas保留那些至少90%的值不是缺失值的列。
16.一個字符串劃分為多列
我們先創建另一個新的示例DataFrame:
如果我們需要將「name」這一列劃分為三個獨立的列,用來表示first, middle, last name呢?我們將會使用str.split()函數,告訴它以空格進行分隔,並將結果擴展成一個DataFrame:
這三列實際上可以通過一行代碼保存至原來的DataFrame:
如果我們想要劃分一個字符串,但是僅保留其中一個結果列呢?比如說,讓我們以", "來劃分location這一列:
如果我們只想保留第0列作為city name,我們僅需要選擇那一列並保存至DataFrame:
17.Series擴展成DataFrame
讓我們創建一個新的示例DataFrame:
這裡有兩列,第二列包含了Python中的由整數元素組成的列表。
如果我們想要將第二列擴展成DataFrame,我們可以對那一列使用apply()函數並傳遞給Series constructor:
通過使用concat()函數,我們可以將原來的DataFrame和新的DataFrame組合起來:
18.對多個函數進行聚合
讓我們來看一眼從Chipotle restaurant chain得到的orders這個DataFrame:
In [82]:orders.head(10)
Out[82]:
每個訂單(order)都有訂單號(order_id),包含一行或者多行。為了找出每個訂單的總價格,你可以將那個訂單號的價格(item_price)加起來。比如,這裡是訂單號為1的總價格:
如果你想要計算每個訂單的總價格,你可以對order_id使用groupby(),再對每個group的item_price進行求和。
但是,事實上你不可能在聚合時僅使用一個函數,比如sum()。為了對多個函數進行聚合,你可以使用agg()函數,傳給它一個函數列表,比如sum()和count():
這將告訴我們沒定訂單的總價格和數量。
19.聚合結果與DataFrame組合
讓我們再看一眼orders這個DataFrame:
In [86]:orders.head(10)
Out[86]:
如果我們想要增加新的一列,用於展示每個訂單的總價格呢?回憶一下,我們通過使用sum()函數得到了總價格:
sum()是一個聚合函數,這表明它返回輸入數據的精簡版本(reduced version )。
換句話說,sum()函數的輸出:
比這個函數的輸入要小:
解決的辦法是使用transform()函數,它會執行相同的操作但是返回與輸入數據相同的形狀:
我們將這個結果存儲至DataFrame中新的一列:
In [91]:orders['total_price'] = total_priceorders.head(10)
Out[91]:
你可以看到,每個訂單的總價格在每一行中顯示出來了。
這樣我們就能方便地甲酸每個訂單的價格佔該訂單的總價格的百分比:
In [92]:orders['percent_of_total'] = orders.item_price / orders.total_priceorders.head(10)
In [92]:
20.選取行和列的切片
讓我們看一眼另一個數據集:
In [93]:titanic.head()
Out[93]:
這就是著名的Titanic數據集,它保存了Titanic上乘客的信息以及他們是否存活。
如果你想要對這個數據集做一個數值方面的總結,你可以使用describe()函數:
但是,這個DataFrame結果可能比你想要的信息顯示得更多。
如果你想對這個結果進行過濾,只想顯示「五數概括法」(five-number summary)的信息,你可以使用loc函數並傳遞"min"到"max"的切片:
如果你不是對所有列都感興趣,你也可以傳遞列名的切片:
21.Multilndexed Series重塑
Titanic數據集的Survived列由1和0組成,因此你可以對這一列計算總的存活率:
如果你想對某個類別,比如「Sex」,計算存活率,你可以使用groupby():
如果你想一次性對兩個類別變量計算存活率,你可以對這些類別變量使用groupby():
該結果展示了由Sex和Passenger Class聯合起來的存活率。它存儲為一個MultiIndexed Series,也就是說它對實際數據有多個索引層級。
這使得該數據難以讀取和交互,因此更為方便的是通過unstack()函數將MultiIndexed Series重塑成一個DataFrame:
該DataFrame包含了與MultiIndexed Series一樣的數據,不同的是,現在你可以用熟悉的DataFrame的函數對它進行操作。
22.創建數據透視表
如果你經常使用上述的方法創建DataFrames,你也許會發現用pivot_table()函數更為便捷:
想要使用數據透視表,你需要指定索引(index), 列名(columns), 值(values)和聚合函數(aggregation function)。
數據透視表的另一個好處是,你可以通過設置margins=True輕鬆地將行和列都加起來:
這個結果既顯示了總的存活率,也顯示了Sex和Passenger Class的存活率。
最後,你可以創建交叉表(cross-tabulation),只需要將聚合函數由"mean"改為"count":
這個結果展示了每一對類別變量組合後的記錄總數。
23.連續數據轉類別數據
讓我們來看一下Titanic數據集中的Age那一列:
它現在是連續性數據,但是如果我們想要將它轉變成類別數據呢?
一個解決辦法是對年齡範圍打標籤,比如"adult", "young adult", "child"。實現該功能的最好方式是使用cut()函數:
這會對每個值打上標籤。0到18歲的打上標籤"child",18-25歲的打上標籤"young adult",25到99歲的打上標籤「adult」。
注意到,該數據類型為類別變量,該類別變量自動排好序了(有序的類別變量)。
24.Style a DataFrame
上一個技巧在你想要修改整個jupyter notebook中的顯示會很有用。但是,一個更靈活和有用的方法是定義特定DataFrame中的格式化(style)。
讓我們回到stocks這個DataFrame:
我們可以創建一個格式化字符串的字典,用於對每一列進行格式化。然後將其傳遞給DataFrame的style.format()函數:
注意到,Date列是month-day-year的格式,Close列包含一個$符號,Volume列包含逗號。
我們可以通過鏈式調用函數來應用更多的格式化:
我們現在隱藏了索引,將Close列中的最小值高亮成紅色,將Close列中的最大值高亮成淺綠色。
這裡有另一個DataFrame格式化的例子:
Volume列現在有一個漸變的背景色,你可以輕鬆地識別出大的和小的數值。
最後一個例子:
現在,Volumn列上有一個條形圖,DataFrame上有一個標題。
請注意,還有許多其他的選項你可以用來格式化DataFrame。
25.額外技巧
Profile a DataFrame
假設你拿到一個新的數據集,你不想要花費太多力氣,只是想快速地探索下。那麼你可以使用pandas-profiling這個模塊。
在你的系統上安裝好該模塊,然後使用ProfileReport()函數,傳遞的參數為任何一個DataFrame。它會返回一個互動的HTML報告:
使用示例如下(只顯示第一部分的報告):