本文示例程式碼已上傳至我的
Github
倉庫https://github.com/CNFeffery/DataScienceStudyNotes
1 簡介
利用pandas
進行資料分析的過程,不僅僅是計算出結果那麼簡單,很多初學者喜歡在計算過程中建立一堆命名隨心所欲的中間變數,一方面使得程式碼讀起來費勁,另一方面越多的不必要的中間變數意味著越高的記憶體佔用,越多的計算資源消耗。
因此很多時候為了提升整個資料分析工作流的執行效率以及程式碼的簡潔性,需要配合一些pandas
中的高階特性。本文就將帶大家學習如何在pandas
中化繁為簡,利用query()
和eval()
來實現高效簡潔的資料查詢與運算。
2 基於query()的高效查詢
query()
顧名思義,是pandas
中專門執行資料查詢的API,其實早在2014年,pandas
0.13版本中這個特性就已經出現了,隨著後續眾多版本的迭代更新,目前pandas
中的query()
已經進化得非常好用(筆者目前使用的pandas
版本為1.1.0)。
首先從一個實際例子認識一下query()
的用法,這裡我們使用到netflix電影與劇集發行資料集,包含了6234個作品的基本屬性資訊,你可以在文章開頭的Github
倉庫對應目錄下找到它。
正常讀入資料後,我們分別使用傳統方法和query()
來執行這樣的組合條件查詢,不同的條件之間用對應的and or
或& |
連線均可:
找出型別為TV Show且國家不含美國的Kids' TV
通過比較可以發現在使用query()
時我們在不需要重複書寫資料框名稱[欄位名]
這樣的內容,欄位名也直接可以當作變數使用,而且不同條件之間不需要用括號隔開,在條件繁雜的時候簡化程式碼的效果更為明顯。
通過上面的小例子我們認識到query()
的強大之處,下面我們就來學習query()
的常用特性:
2.1 直接解析欄位名
query()
最核心的特性就是可以直接根據傳入的查詢表示式,將欄位名解析為對應的列,其中對欄位名的命名規範有一定要求:當欄位名符合Python
中對變數命名規範的要求時,即變數名完全由字母、數字、下劃線構成且不以數字開頭,這樣的欄位是可以直接寫入query()
表示式的。
但大家如果嘗試過會發現一些不符合上述規範的變數名也不報錯,譬如:
因此可以記住只要在Python
裡作為變數名不報錯,就可以直接填入欄位名,否則需要在欄位名兩邊加上`,譬如下面的例子:
2.2 鏈式表示式
query()
中還支援鏈式表示式(chained expressions),使得我們可以進一步簡化多條件組合時的語法:
demo = pd.DataFrame({
'a': [5, 4, 3, 2, 1],
'b': [1, 2, 3, 4, 5]
})
demo.query("a <= b != 4")
2.3 支援in與not in判斷
query()
支援Python
原生的in
判斷以及not in
判斷,從而簡化了多條件判斷,比如我們針對netflix資料集想找出release_year
等於2018或2019的作品:
netflix.query("release_year in [2018, 2019]")
2.4 對外部變數的支援
query()
表示式還支援使用外部變數,只需要在外部變數前加上@
符號即可:
2.5 對常規語句的支援
query()
我個人覺得最驚人的功能就是其可以直接解析Python
語句,這賦予我們極大的自由度:
def country_count(s):
'''
計算涉及國家數量
'''
return s.split(',').__len__()
# 找出發行年份在2018或2019年且合作國家數量超過5個的劇集
netflix.query("release_year.isin([2018, 2019]) and country.apply(@country_count) > 5")
2.6 對Index與MultiIndex的支援
除了對常規欄位進行條件篩選,query()
還支援對資料框自身的index
進行條件篩選,具體可分為三種情況:
- 常規index
對於只具有單列Index
的資料框,直接在表示式中使用index
:
# 找出索引列中包含king的記錄,忽略大小寫
netflix.set_index('title').query("index.str.contains('king', case=False)")
- names為空的MultiIndex
對於MultiIndex
的情況,可分為兩種,首先我們來看看MultiIndex
的names
為空的情況,按照順序,用ilevel_n
表示MultiIndex
中的第n列index:
# 構造含有MultiIndex的資料框,並重置index的names為None
temp = netflix.set_index(['title', 'type']);temp.index.names = (None, None)
# 找出第一個index包含king(忽略大小寫),第二個index等於Movie的記錄
temp.query("ilevel_0.str.contains('king', case=False) and ilevel_1 == 'Movie'")
- names不為空的MultiIndex
而對於MultiIndex
的names
有內容的情況,直接用對應的名稱傳入表示式即可:
# 構造含有MultiIndex的資料框,並重置index的names為None
temp = netflix.set_index(['title', 'type'])
# 找出第一個index包含king(忽略大小寫),第二個index等於Movie的記錄
temp.query("title.str.contains('king', case=False) and type == 'Movie'")
3 基於eval()的高效運算
而eval()
類似Python
的eval()
函式,可以將字串形式的命令直接解析並執行。
而pandas
中的eval()
有兩種,一種是top-level
級別的eval()
函式,而另一種是針對資料框的DataFrame.eval()
,我們接下來要介紹的是後者,其與query()
有很多相同之處,下面只介紹其獨有特點。
同樣從實際例子出發,同樣針對netflix資料,我們按照一定的計算方法為其新增兩列資料,對基於assign()
的方式和基於eval()
的方式進行比較,其中最後一列是False是因為日期轉換使用coerce
策略之後無法被解析的日期會填充pd.NAT,而缺失值之間是無法進行相等比較的:
# 利用assign進行新增欄位計算並儲存為新資料框
result1 = netflix.assign(years_to_now=2020 - netflix['release_year'],
new_date_added=pd.to_datetime(netflix['date_added'].str.strip(),
format='%B %d, %Y', errors='coerce'))
# 利用eval()進行新增欄位計算並儲存為新資料框
result2 = netflix.eval('''
years_to_now = 2020 - release_year
new_date_added = @pd.to_datetime(date_added.str.strip(), format='%B %d, %Y', errors='coerce')''')
(result1 == result2).all()
雖然assign()
已經算是pandas
中簡化程式碼的很好用的API了,但面對eval()
,還是遜色不少
DataFrame.eval()
通過傳入多行表示式,每行作為獨立的賦值語句,其中對應前面資料框中資料欄位可以像query()
一樣直接書寫欄位名,亦可像query()
那樣直接執行Python
語句。
但要注意的是eval()
中每個新欄位的賦值必須寫在同一行,否則會出錯:
netflix.eval('''
years_to_now = 2020 - release_year
new_date_added = @pd.to_datetime(date_added.str.strip(),
format='%B %d, %Y',
errors='coerce')''')
因此如果你要使用到的函式引數很多,可以利用functools
中的partial
將一些引數固化並儲存,從而達到簡化eval()
表示式的目的:
from functools import partial
# 利用partial固化指定引數
func = partial(pd.to_datetime, format='%B %d, %Y', errors='coerce')
netflix.eval('''
years_to_now = 2020 - release_year
new_date_added = @func(date_added.str.strip())''')
而我最喜歡DataFrame.eval()
的地方在於配合他,我可以在很多資料分析場景中實現0中間變數,一直鏈式下去,延續上面的例子,當我們新增了這兩列資料之後,接下來我們按順序進行按月統計影片數量、欄位重新命名、新增當月數量在全部記錄排名欄位、排序,其中關鍵的是新增當月數量在全部記錄排名欄位,如果不用eval()
,你是無法在不建立中間變數的前提下如此簡潔地完成需求的:
netflix.eval('''
years_to_now = 2020 - release_year
new_date_added = @func(date_added.str.strip())''') \
.resample('M', on='new_date_added') \
.agg({'new_date_added': 'count'}) \
.rename(columns={'new_date_added': '月度發行數量'}) \
.eval('''月度發行數量排名 = 月度發行數量.rank(ascending=False).astype('int')''') \
.sort_values('月度發行數量排名')
使用query()+eval()
,昇華pandas
資料分析操作。
以上就是本文的全部內容,歡迎在評論區與我討論~