對於一個有軟件工程項目基礎的程序員而言,咱們這羣來源「可疑」的Data Scientist最被人詬病的就是期代碼質量堪憂到讓人崩潰的程度。本篇文章將介紹本身在以python
/Jupyter Notebook
爲基礎的分析/挖掘項目時是如何優化代碼使其具備更大的可讀性(執行效率不是本文的主要目的)。python
固然,這個層面的優化是最簡單的,你們熟悉的PEP
風格和GOOGLE
風格都是不錯的實踐。參加下面兩個文檔:git
多年經(踩)驗(坑)摸索出一套比較適合的變量命名方案,基本的方法是程序員
[具象詞](_[操做])(_[介詞短語])_[數據結構]
具象詞是描述數據的具體用處的詞語,例如一個班級的男生的成績,能夠用boy_score
來表述。github
操做則是歸納了對數據作過什麼處理,若是是我的團隊能夠維護一個簡單的縮略詞詞庫。好比,咱們要描述一個班級的成績作過去空值的處理,能夠用drop_null
或者rm_na
來表示。在這種狀況下,咱們能夠對上面的對象完整描述成boy_score_rm_na
。markdown
介詞短語其實也是操做的一部分,每每以in
/over
/top
(不嚴謹地歸在此類)/group by
等。好比,咱們這裏要描述一個班級的學生按性別取前10的成績並作了去重處理,能夠寫成score_unique_groupbysex_top10
,若是長度過長,維護一個簡寫的映射表固然也是不錯的(犧牲部分可讀性)。數據結構
數據結構則是描述數據是存儲在什麼數據結構中的,常見的好比list
,pandas.DataFrame
、dict
等,在上面的例子裏,咱們儲存在pandas.DataFrame
則能夠寫成score_unique_groupbysex_top10_df
。app
而操做和介詞短語在不少場合下能夠不寫。固然在更加抽象地機器學習訓練中時,能夠以test_df
、train_df
這種抽象描述是更適合的方案。機器學習
這點是容易忽視的一個問題,任何Jupyter
裏面的cell必定要保證,具體的cell中的代碼是自上而下執行的。這在工做中,可能因爲反覆調試,致使在編輯cell的過程當中不是線性操做。保證notebook能夠線性執行的緣由,一部分是方便其餘閱讀人能夠正常執行整個notebook;另外一部分,也是爲了增長可讀性。ide
<!-- 可接受的例子 --> ```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') ```
咱們在具體撰寫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時遇到的另外一個問題,不少具體地清洗或者特徵工程時的方法,過於隆長,而這些方法,可能在別的地方也會複用,此時寫在一個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只執行一個問題。具體,我在工做中會如下列方案來作一個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代碼並不是不可能,只是看是否具備相關的習慣,增長可讀性對本身以及團隊的工做和開源社區的聲望都會有利,但願上面的建議對你們有所幫助。