對於一個有軟件工程項目基礎的程序員而言,我們這羣來源「可疑」的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
,如果長度過長,維護一個簡寫的映射表當然也是不錯的(犧牲部分可讀性)。
數據結構則是描述數據是存儲在什麼數據結構中的,常見的比如list
,pandas.DataFrame
、dict
等,在上面的例子裏,我們儲存在pandas.DataFrame
則可以寫成score_unique_groupbysex_top10_df
。
而操作和介詞短語在很多場合下可以不寫。當然在更加抽象地機器學習訓練中時,可以以test_df
、train_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
在這方面的原生支持並不友善,甚至重開內核運行所有都比手寫這個重載代碼快。這時候我們需要一個ipython
的magic 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代碼並非不可能,只是看是否具有相關的習慣,增加可讀性對自己以及團隊的工作和開源社區的聲望都會有利,希望上面的建議對大家有所幫助。