(資料科學學習手札92)利用query()與eval()優化pandas程式碼

費弗裡發表於2020-08-07

本文示例程式碼已上傳至我的Github倉庫https://github.com/CNFeffery/DataScienceStudyNotes

1 簡介

  利用pandas進行資料分析的過程,不僅僅是計算出結果那麼簡單,很多初學者喜歡在計算過程中建立一堆命名隨心所欲的中間變數,一方面使得程式碼讀起來費勁,另一方面越多的不必要的中間變數意味著越高的記憶體佔用,越多的計算資源消耗。

  因此很多時候為了提升整個資料分析工作流的執行效率以及程式碼的簡潔性,需要配合一些pandas中的高階特性。本文就將帶大家學習如何在pandas中化繁為簡,利用query()eval()來實現高效簡潔的資料查詢與運算。

(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖1

2 基於query()的高效查詢

  query()顧名思義,是pandas中專門執行資料查詢的API,其實早在2014年,pandas0.13版本中這個特性就已經出現了,隨著後續眾多版本的迭代更新,目前pandas中的query()已經進化得非常好用(筆者目前使用的pandas版本為1.1.0)。

  首先從一個實際例子認識一下query()的用法,這裡我們使用到netflix電影與劇集發行資料集,包含了6234個作品的基本屬性資訊,你可以在文章開頭的Github倉庫對應目錄下找到它。

(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖2

  正常讀入資料後,我們分別使用傳統方法和query()來執行這樣的組合條件查詢,不同的條件之間用對應的and or& |連線均可:

找出型別為TV Show且國家不含美國Kids' TV

(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖3

  通過比較可以發現在使用query()時我們在不需要重複書寫資料框名稱[欄位名]這樣的內容,欄位名也直接可以當作變數使用,而且不同條件之間不需要用括號隔開,在條件繁雜的時候簡化程式碼的效果更為明顯。

  通過上面的小例子我們認識到query()的強大之處,下面我們就來學習query()的常用特性:

2.1 直接解析欄位名

  query()最核心的特性就是可以直接根據傳入的查詢表示式,將欄位名解析為對應的列,其中對欄位名的命名規範有一定要求:當欄位名符合Python中對變數命名規範的要求時,即變數名完全由字母數字下劃線構成且不以數字開頭,這樣的欄位是可以直接寫入query()表示式的。

  但大家如果嘗試過會發現一些不符合上述規範的變數名也不報錯,譬如:

(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖4

  因此可以記住只要在Python裡作為變數名不報錯,就可以直接填入欄位名,否則需要在欄位名兩邊加上`,譬如下面的例子:

(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖5

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")
(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖6

2.3 支援in與not in判斷

  query()支援Python原生的in判斷以及not in判斷,從而簡化了多條件判斷,比如我們針對netflix資料集想找出release_year等於2018或2019的作品:

netflix.query("release_year in [2018, 2019]")
(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖7

2.4 對外部變數的支援

  query()表示式還支援使用外部變數,只需要在外部變數前加上@符號即可:

(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖8

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")
(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖9

2.6 對Index與MultiIndex的支援

  除了對常規欄位進行條件篩選,query()還支援對資料框自身的index進行條件篩選,具體可分為三種情況:

  • 常規index

  對於只具有單列Index的資料框,直接在表示式中使用index

# 找出索引列中包含king的記錄,忽略大小寫
netflix.set_index('title').query("index.str.contains('king', case=False)")
(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖10
  • names為空的MultiIndex

  對於MultiIndex的情況,可分為兩種,首先我們來看看MultiIndexnames為空的情況,按照順序,用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'")
(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖11
  • names不為空的MultiIndex

  而對於MultiIndexnames有內容的情況,直接用對應的名稱傳入表示式即可:

# 構造含有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'")
(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖12

3 基於eval()的高效運算

  而eval()類似Pythoneval()函式,可以將字串形式的命令直接解析並執行。

  而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()
(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖13

  雖然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')''')
(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖14

  因此如果你要使用到的函式引數很多,可以利用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('月度發行數量排名')
(資料科學學習手札92)利用query()與eval()優化pandas程式碼
圖15

  使用query()+eval(),昇華pandas資料分析操作。


  以上就是本文的全部內容,歡迎在評論區與我討論~

相關文章