(數據科學學習手札92)利用query()與eval()優化pandas代碼

本文示例代碼已上傳至個人Github倉庫https://github.com/CNFeffery/DataScienceStudyNotespython

1 簡介

  利用pandas進行數據分析的過程,不只僅是計算出結果那麼簡單,不少初學者喜歡在計算過程當中建立一堆命名爲所欲爲的中間變量,一方面使得代碼讀起來費勁,另外一方面越多的沒必要要的中間變量意味着越高的內存佔用,越多的計算資源消耗。git

  所以不少時候爲了提高整個數據分析工做流的執行效率以及代碼的簡潔性,須要配合一些pandas中的高級特性。本文就將帶你們學習如何在pandas中化繁爲簡,利用query()eval()來實現高效簡潔的數據查詢與運算。github

圖1

2 基於query()的高效查詢

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

  首先從一個實際例子認識一下query()的用法,這裏咱們使用到netflix電影與劇集發行數據集,包含了6234個做品的基本屬性信息,你能夠在文章開頭的Github倉庫對應目錄下找到它。app

圖2

  正常讀入數據後,咱們分別使用傳統方法和query()來執行這樣的組合條件查詢,不一樣的條件之間用對應的and or& |鏈接都可:函數

找出類型爲TV Show且國家不含美國Kids' TV學習

圖3

  經過比較能夠發如今使用query()時咱們在不須要重複書寫數據框名稱[字段名]這樣的內容,字段名也直接能夠看成變量使用,並且不一樣條件之間不須要用括號隔開,在條件繁雜的時候簡化代碼的效果更爲明顯。3d

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

2.1 直接解析字段名

  query()最核心的特性就是能夠直接根據傳入的查詢表達式,將字段名解析爲對應的列,其中對字段名的命名規範有必定要求:當字段名符合Python中對變量命名規範的要求時,即變量名徹底由字母數字下劃線構成且不以數字開頭,這樣的字段是能夠直接寫入query()表達式的。orm

  但你們若是嘗試過會發現一些不符合上述規範的變量名也不報錯,譬如:

圖4

  所以能夠記住只要在Python裏做爲變量名不報錯,就能夠直接填入字段名,不然須要在字段名兩邊加上`,譬以下面的例子:

圖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")
圖6

2.3 支持in與not in判斷

  query()支持Python原生的in判斷以及not in判斷,從而簡化了多條件判斷,好比咱們針對netflix數據集想找出release_year等於2018或2019的做品:

netflix.query("release_year in [2018, 2019]")
圖7

2.4 對外部變量的支持

  query()表達式還支持使用外部變量,只須要在外部變量前加上@符號便可:

圖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")
圖9

2.6 對Index與MultiIndex的支持

  除了對常規字段進行條件篩選,query()還支持對數據框自身的index進行條件篩選,具體可分爲三種狀況:

  • 常規index

  對於只具備單列Index的數據框,直接在表達式中使用index

# 找出索引列中包含king的記錄,忽略大小寫
netflix.set_index('title').query("index.str.contains('king', case=False)")
圖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'")
圖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'")
圖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()
圖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')''')
圖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('月度發行數量排名')
圖15

  使用query()+eval(),昇華pandas數據分析操做。


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

相關文章
相關標籤/搜索