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

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

> 本文示例程式碼已上傳至我的`Github`倉庫[https://github.com/CNFeffery/DataScienceStudyNotes](https://github.com/CNFeffery/DataScienceStudyNotes) # 1 簡介   利用`pandas`進行資料分析的過程,不僅僅是計算出結果那麼簡單,很多初學者喜歡在計算過程中建立一堆命名**隨心所欲**的中間變數,一方面使得程式碼讀起來費勁,另一方面越多的不必要的中間變數意味著越高的記憶體佔用,越多的計算資源消耗。   因此很多時候為了提升整個資料分析工作流的**執行效率**以及程式碼的**簡潔性**,需要配合一些`pandas`中的高階特性。本文就將帶大家學習如何在`pandas`中化繁為簡,利用`query()`和`eval()`來實現高效簡潔的資料查詢與運算。
圖1
# 2 基於query()的高效查詢   `query()`顧名思義,是`pandas`中專門執行資料查詢的API,其實早在2014年,`pandas`0.13版本中這個特性就已經出現了,隨著後續眾多版本的迭代更新,目前`pandas`中的`query()`已經進化得非常好用(筆者目前使用的`pandas`版本為1.1.0)。   首先從一個實際例子認識一下`query()`的用法,這裡我們使用到**netflix**電影與劇集發行資料集,包含了6234個作品的基本屬性資訊,你可以在文章開頭的`Github`倉庫對應目錄下找到它。
圖2
  正常讀入資料後,我們分別使用傳統方法和`query()`來執行這樣的組合條件查詢,不同的條件之間用對應的`and or`或`& |`連線均可: >
找出型別為**TV Show**且國家不含**美國**的**Kids' TV**
圖3
  通過比較可以發現在使用`query()`時我們在不需要重複書寫`資料框名稱[欄位名]`這樣的內容,欄位名也直接可以當作變數使用,而且不同條件之間不需要用括號隔開,在條件繁雜的時候簡化程式碼的效果更為明顯。   通過上面的小例子我們認識到`query()`的強大之處,下面我們就來學習`query()`的常用特性: ## 2.1 直接解析欄位名   `query()`最核心的特性就是可以直接根據傳入的查詢表示式,將欄位名解析為對應的列,其中對欄位名的命名規範有一定要求:當欄位名符合`Python`中對變數命名規範的要求時,即變數名完全由**字母**、**數字**、**下劃線**構成且不以**數字**開頭,這樣的欄位是可以直接寫入`query()`表示式的。   但大家如果嘗試過會發現一些不符合上述規範的變數名也不報錯,譬如:
圖4
  因此可以記住只要在`Python`裡作為變數名不報錯,就可以直接填入欄位名,否則需要在欄位名兩邊加上`,譬如下面的例子:
圖5
## 2.2 鏈式表示式   `query()`中還支援鏈式表示式(*chained expressions*),使得我們可以進一步簡化多條件組合時的語法: ```python 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的作品: ```Python netflix.query("release_year in [2018, 2019]") ```
圖7
## 2.4 對外部變數的支援   `query()`表示式還支援使用外部變數,只需要在外部變數前加上`@`符號即可:
圖8
## 2.5 對常規語句的支援   `query()`我個人覺得最驚人的功能就是其可以直接解析`Python`語句,這賦予我們極大的自由度: ```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`: ```python # 找出索引列中包含king的記錄,忽略大小寫 netflix.set_index('title').query("index.str.contains('king', case=False)") ```
圖10
- **names為空的MultiIndex**   對於`MultiIndex`的情況,可分為兩種,首先我們來看看`MultiIndex`的`names`為空的情況,按照順序,用`ilevel_n`表示`MultiIndex`中的第n列index: ```Python # 構造含有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**   而對於`MultiIndex`的`names`有內容的情況,直接用對應的名稱傳入表示式即可: ```python # 構造含有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()`類似`Python`的`eval()`函式,可以將字串形式的命令直接解析並執行。   而`pandas`中的`eval()`有兩種,一種是`top-level`級別的`eval()`函式,而另一種是針對資料框的`DataFrame.eval()`,我們接下來要介紹的是後者,其與`query()`有很多相同之處,下面只介紹其獨有特點。   同樣從實際例子出發,同樣針對**netflix**資料,我們按照一定的計算方法為其新增兩列資料,對基於`assign()`的方式和基於`eval()`的方式進行比較,其中最後一列是False是因為日期轉換使用`coerce`策略之後無法被解析的日期會填充pd.NAT,而缺失值之間是無法進行相等比較的: ```python # 利用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()`中每個新欄位的賦值必須寫在同一行,否則會出錯: ```python 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()`表示式的目的: ```python 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()`,你是無法在**不建立中間變數**的前提下如此簡潔地完成需求的: ```python 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`資料分析操作。 ---   以上就是本文的全部內容,歡迎在評論區與