如何優化基於Jupyter的分析/挖掘測試項目

對於一個有軟件工程項目基礎的程序員而言,我們這羣來源「可疑」的Data Scientist最被人詬病的就是期代碼質量堪憂到讓人崩潰的程度。本篇文章將介紹自己在以python/Jupyter Notebook爲基礎的分析/挖掘項目時是如何優化代碼使其具有更大的可讀性(執行效率不是本文的主要目的)。

Python語法級別的優化

合適的style

當然,這個層面的優化是最簡單的,大家熟悉的PEP風格和GOOGLE風格都是不錯的實踐。參加下面兩個文檔:

值得一試的命名方案

多年經(踩)驗(坑)摸索出一套比較適合的變量命名方案,基本的方法是

[具象詞](_[操作])(_[介詞短語])_[數據結構]

具象詞是描述數據的具體用處的詞語,例如一個班級的男生的成績,可以用boy_score來表述。

操作則是概括了對數據做過什麼處理,如果是個人團隊可以維護一個簡單的縮略詞詞庫。比如,我們要描述一個班級的成績做過去空值的處理,可以用drop_null或者rm_na來表示。在這種情況下,我們可以對上面的對象完整描述成boy_score_rm_na

介詞短語其實也是操作的一部分,往往以in/over/top(不嚴謹地歸在此類)/group by等。比如,我們這裏要描述一個班級的學生按性別取前10的成績並做了去重處理,可以寫成score_unique_groupbysex_top10,如果長度過長,維護一個簡寫的映射表當然也是不錯的(犧牲部分可讀性)。

數據結構則是描述數據是存儲在什麼數據結構中的,常見的比如listpandas.DataFramedict等,在上面的例子裏,我們儲存在pandas.DataFrame則可以寫成score_unique_groupbysex_top10_df

操作介詞短語在很多場合下可以不寫。當然在更加抽象地機器學習訓練中時,可以以test_dftrain_df這種抽象描述是更適合的方案。

Jupyter級別的優化

線性執行

這點是容易忽視的一個問題,任何Jupyter裏面的cell一定要保證,具體的cell中的代碼是自上而下執行的。這在工作中,可能由於反覆調試,導致在編輯cell的過程中不是線性操作。保證notebook可以線性執行的原因,一部分是方便其他閱讀人可以正常執行整個notebook;另一部分,也是爲了增加可讀性。

<!-- 可接受的例子 -->

​```cell 1
import pandas as pd
​```

​```cell 2
df = pd.read_csv('train.csv')
​```

​```cell 3
def sum_ab(row):
    return row['a'] + row['b']
​```

​```cell 4
df.apply(sum_ab, axis=1)
​```
<!-- 不可接受的例子,不能正常運行 -->

​```cell 1
import pandas as pd
​```

​```cell 2
df = pd.read_csv('train.csv')
​```

​```cell 3
df.apply(sum_ab, axis=1)
​```

​```cell 4
def sum_ab(row):
    return row['a'] + row['b']
​```

載入模塊和讀入數據放在開頭

在具體分析中,載入模塊和數據是非常常見的工作,而放置在notebook開始容易幫助閱讀者注意,需要的依賴以及數據。如果零散地出現在notebook任何地方,這樣就容易造成困難的路徑依賴檢查、模塊重命名方式和模塊依賴檢查。

如果有些模塊並不常用,但在notebook引用到了,建議在載入模塊前的cell中加入一個cell用來安裝依賴。jupyter可以用!來實現臨時調用系統的命令行,這個可以很好地利用在這個場景中。

此外,所有數據和自寫模塊使用相對鏈接的方式導入是不錯的選擇。因爲,後期可能別人會通過git的方法獲取你的代碼,相對鏈接可以允許我們不修改鏈接也能使用git項目中的模塊和數據。

​```cell 1
!pip install scikit-learn
​```

​```cell 2
import panda as pd
from sklearn import metrics
import sys

## 自編模塊
sys.path.append('../')
from my_module import my_func
​```

​```cell 3
df = pd.read_csv('./train.csv')
​```

一個Cell一個功能

我們在具體撰寫Jupyter時,無法避免,會反覆嘗試,並且測試中間輸出的結果。因此,很多同行的cell代碼往往會呈現以下狀態。

​```cell 1
df_1 = df.sum(axis = 1)
​```

​```cell 2
df_2 = df_1.fill_na(0)
​```

​```cell 3
ggplot(df_2, aes(x = 'x', y = 'y')) + geom_point()
​```

這樣的做法無可厚非,且並沒有錯誤,但是,這樣的缺點是,讀者可能需要run三個cells才能得出最終的結果,而中間的處理過程,在閱讀報告中,往往是無關緊要的甚至會影響到可讀性,具體查看中間步驟只有復現和糾錯時纔是必要的。因此,我們要保持一個原則,就是一個Cell理應要承擔一個功能需求的要求。具體優化如下:

​```cell 1
df_1 = df.sum(axis = 1)
df_2 = df_1.fill_na(0)

## 繪圖
ggplot(df_2, aes(x = 'x', y = 'y')) + geom_point()
​```

但很多朋友可能會遇到,過程過於複雜,導致一個cell看起來非常的冗餘,或者處理過程異常漫長,需要一些中間表的情況,後面的建議中,我會提到如何改善這兩個問題。

數據(包括中間結果)與運算分離

回到剛纔的問題,很多時候,我們會遇到一箇中間過程的運行時間過長,如何改變這種狀況呢,比如上個例子,我們發現fillna的時間很長,如果放在一個cell中,讀者在自己重新運行時或者自己測試時就會非常耗時。一個具體的解決方案就是,寫出臨時結果,並且在自己編輯過程中,維持小cell,只在最後呈遞時做處理。比如上面的任務,我會如此工作。

​```cell 1
df_1 = df.sum(axis = 1)
​```

​```cell 2
df_2 = df_1.fill_na(0)
df_2.to_pickle('../temp/df_2.pickle')
​```

​```cell 3
ggplot(df_2, aes(x = 'x', y = 'y')) + geom_point()
​```

最後呈遞notebook時改成如下樣子

​```cell 1
##~~~~ 中間處理 ~~~~##
# df_1 = df.sum(axis = 1)
# df_2 = df_1.fill_na(0)
# df_2.to_pickle('../temp/df_2.pickle')
##~~~~ 中間處理 ~~~~##

df_2 = pd.read_pickle('../temp/df_2.pickle')

## 繪圖
ggplot(df_2, aes(x = 'x', y = 'y')) + geom_point()
​```

這樣做的另外個好處可以實現,數據在notebook之間的交互,我們會在之後提到具體場景。

抽象以及可複用分離到Notebook外部

我們在撰寫notebook時遇到的另一個問題,很多具體地清洗或者特徵工程時的方法,過於隆長,而這些方法,可能在別的地方也會複用,此時寫在一個cell裏就非常不好看,例如下面一個方法。

​```cell 1
def func1(x):
    """add 1
    """
    return x + 1

def func2(x):
    temp = list(map(func1, x))
    temp.sorted()
    return temp[0] + temp[-1]

df.a.apply(func2, axis)
​```

這種情況的解決方案,是獨立於notebook之外維護一個文件夾專門存放notebook中會用到的代碼。例如,使用如下結構存放模塊,具體使用時再載入。

--- my_module
  |__ __init__.py
  |__ a.py
___ notebook
  |__ test.ipynb

注意使用sys模塊添加環境來載入模塊。

## import cell
import pandas as pd
import sys

sys.path.append('../')
from my_module import *

但這種方案,存在一個問題——我們會經常改動模塊的內容,特別是在測試時,這時候需要能重載模塊,這個時候我們需要反覆重載模塊,python在這方面的原生支持並不友善,甚至重開內核運行所有都比手寫這個重載代碼快。這時候我們需要一個ipythonmagic extension——autoreload,具體的方法參考這個問答

我們只需要在載入模塊的開頭寫上如下行,這樣我們每當修改我們自己編寫的module時就會重載模塊,實現更新。

%load_ext autoreload
%autoreload 2

import pandas as pd
import sys

sys.path.append('../')
from my_module import *

而模塊分離最終的好處是,我們最後可以容易的把這些模塊最後移植到生產環境的項目中。

項目級別的優化

一個notebook解決一個問題

爲了讓項目更加具有可讀性,對任務做一個分解是不錯的方案,要實現一個notebook只執行一個問題。具體,我在工作中會以下列方案來做一個jupyter的notebook分類。其中0. introduction and contents.ipynb是必須的,裏面要介紹該項目中其他notebook的任務,並提供索引。這種方案,就可以提高一個分析/挖掘項目的可讀性。

- 0. introduction and contents.ipynb
- eda.1 EDA問題一.ipynb
- eda.2 EDA問題二.ipynb
- eda. ...
- 1.1 方案一+特徵工程.ipynb
- 1.2 方案一訓練和結果.ipynb
- 2.1 方案二+特徵工程.ipynb
- 2.2 方案二訓練和結果.ipynb
- 3.1 方案三+特徵工程.ipynb
- 3.2 方案三訓練和結果.ipynb
- ...
- final.1 結論.ipynb

對文件進行必要的整理

一個分析、挖掘的項目,經常包括但不限於數據源中間文件臨時文件最終報告等內容,因此一個好的整理項目文件的習慣是必要的。我在工作中,具體採用下面這個例子來維護整個分析/挖掘項目。當然,初始化這些文件夾是一個非常麻煩的,因此,這裏分享一個初始化腳本(支持python版本3.6+),大家可以根據自己整理習慣稍作修改。

-- 項目根目錄
  |__ SQL:存儲需要用的SQL
  |__ notebook: 存放notebook的地方
     |__ 0. introduction and contents.ipynb
     |__ eda.1 EDA問題一.ipynb
     |__ eda.2 EDA問題二.ipynb
     |__ eda. ...
     |__ 1.1 方案一+特徵工程.ipynb
     |__ 1.2 方案一訓練和結果.ipynb
     |__ 2.1 方案二+特徵工程.ipynb
     |__ 2.2 方案二訓練和結果.ipynb
     |__ 3.1 方案三+特徵工程.ipynb
     |__ 3.2 方案三訓練和結果.ipynb
     |__ ...
     |__ final.1 結論.ipynb
  |__ src: 撰寫報告或者文檔時需要引用的文件
  |__ data: 存放原始數據
     |__ csv: csv文件
         |__ train.csv
         |__ ...
     |__ ...
  |__ temp: 存放中間數據
  |__ output: 最後報告需要的綜合分析結果
     |__ *.pptx
     |__ *.pdf
     |__ src
         |__ example.png
         |__ ...
  |__ temp_module: 自己寫的notebook需要引用的模塊

結語

優化一個Jupyter代碼並非不可能,只是看是否具有相關的習慣,增加可讀性對自己以及團隊的工作和開源社區的聲望都會有利,希望上面的建議對大家有所幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章