Data Scientist之編程日常 | 寫給研究院時期的我——如何有效地編程?
投身 data science industry 後,在公司的同事/前輩身上學到不少高效能的 coding practice。若有機會坐時光機回到研究院時期,想跟研究生 Clare 分享的第一件事:良好的編程習慣能為我節省的時間,應該夠多寫幾篇 paper… …
不少學術界朋友問我,現時我作為 data scientist 的工作和研究院時期有沒有相類之處。我先說說兩者要處理的數據吧:我做氣象研究時,要分析的是 4-D (經、緯、氣壓/高度、時間)的氣象數據,如風速和溫度,量以 terabyte (TB) 為單位,有時則要做電算模擬 (numerical models) ——input 少量參數,output 大量 3-D/4-D 數據;而在數位廣告公司要處理的則是表列數據,如網站瀏覽紀錄、交易紀錄等等,每日以過兆(trillion)行——即 petabytes (PB) 的量 — 湧入數據庫。
雖然兩者數據量差了幾個 order of magnitude,但難題的性質是相似的 — 都是要先把程式優化了(小數怕長計…),送到遠端電腦才能執行。基於數據量之大,程序需要好一段時間才能完成,不能即時根據結果除錯。研究院時期,不時發生這些情況:「今早才發現某行 code 打錯了一個字,昨晚開始 run 的程式要重來了」、「入錯了一些參數,程式跑到中途 crash 掉」… … 在分秒必爭的 industry,要在短時間內反覆改良、並如期推出產品,又怎可頻犯這種昂貴的錯誤?
Disclaimer: 以下的心得分享,是專門給研究/工作涉及大量編程/數據分析但本業不是 software engineering 的朋友,但求夠用,不求完美。若要學習嚴謹 software engineering practice,建議另覓參考資料。
進入正文前,想問問大家寫 code/run program crash了幾遍時,有否想過以下的問題:
- 編寫程式的過程中,你有快速測試流程的程序嗎?
- 你有把經常重用的程序寫成 module/functions 嗎?還是純粹 copy and paste 然後改幾個字?
- 你知道你的 code 裡最耗時的計算在哪部份嗎?
- 當你/合作者要對程式作出大改動時,如何記錄哪行 code 被修改了?一旦出了狀況,如何把程序還原到之前可行的版本?
- 當執行程序的環境(如 package version 或 dependencies)改變的時候,你如何檢查 functions 的運作有否改變?
若其中一些問題有困擾到你,或你對以上提到的概念感到陌生,希望以下的分享能助大家研究事半功倍~
1. 編寫程式的過程中,你有快速測試流程的程序嗎?
Short answer: Use IDE. Iterate at small-scale. Create test data.
在把複雜的程式發到遠端電腦之前,你有否確定你的程序能夠由頭到尾完整執行呢?
無論你用的是程式語言是 compiler (如 Fortran/C/C++)還是 interpreter(如 MATLAB/python),巿面上也有 IDE (integrated development environment) 方便你在自己的電腦執行程式和debug,如 Microsoft Visual Studio(free student license免費)、PyCharm等(free Community edition)。
(註:IDE 的 debugger 可以替你執行程式至特定一行 code,你也可以監察過程中 variables 的值,是很方便的工具 ——不用這行一個 print 那行一個 print 手動查數)
建設好在自己電腦執行程式的環境後,還要準備好 input data 讓程序能夠由頭到尾執行一遍。真正要分析的巨量數據在遠端電腦。純粹測試程序的話,只需要在自己電腦準備一份跟遠端的 data 格式一樣的小型的 test data 就可 ——這可以是從遠端複製下來的數據 subset,甚至自己製造的 fake data,例如:測試氣象數據分析的程序,我會用只有幾個 time steps 兼低解析度的 netCDF data 作測試;若是要測試在 hadoop cluster 上執行的 (py)spark code,我會從每個需要的 table 裡取幾行,去摸擬 database 上的 tables。
若有些程序無可避免要花很長時間執行,可以 fake data/output format一樣的fake function 取而代之。如此先在自己電腦快速測試程序和除錯,大大減少程式在遠端 crash 的機會。
2. 你有把經常重用的程序寫成 module/functions 嗎?還是純粹 copy and paste 然後改幾個字?
Short answer: modularization.
當你做某範疇的數據分析好一段時間,應該會有一些步驟是每次分析都會用到的。若你意識到你有重覆 copy and paste 一些 code(然後可能改一兩個數值)的傾向,就該把那段 code 寫成 function,給它起一個易懂的名字,放在另一個檔案(或package)。那些會更改的數字,就是這 function 的 input variables。功用相類的 functions 可放置在同一個 module。
理想而言,送到遠端的程序,最好由一連串 functions 組成,而非七零八落的 spaghetti code。這除了能減少手誤,確保 analysis 程序在不同 scripts 的統一性,也使閱讀 run scripts 變得相對容易 ——要比較兩個 run scripts ,只要檢查 call 了哪些 functions (以及其 input arguments ),就了解兩者的異同了。
把 code 寫成 functions/module 也有利和他人合作/交流。要是別人想了解其中一個程序如何運作,你把包含那 function 檔案發給對方就行。在 functions 裡寫清楚 docstring ——簡單交代這 function 的用處,還有 input/output 的格式,對方就無須逐行閱讀 function 裡的內容也能知道如何使用。
# 以下用 python code 示範簡單 docstring 的例子。 def word_count(input): """ Count the number of words separated by space or punctuation in a string. Args: input(str): the string of interest Returns: An integer which indicates the number of words in the input. """ return len( "".join((character if character.isalpha() else " " for character in input).split())
在公司裡,我們會把整個 machine learning pipeline 包成 python package,並使用 Sphinx 根據程式碼裡的 docstring 生成說明文檔,方便其他部門使用。
3. 你知道你的 code 裡最耗時的計算在哪部份嗎?
Short answer: Use profiling.
當然,你可以在你的程序不同地方把時間 print 出來以解答這問題(我在研究院時最初的做法),但這樣所得的資訊很少,而且你不知道程序最基本的各單位耗時多少。
處理大量數據的時候,耗時的地方,通常是一些要執行許多遍而沒有優化的程序 ——計算的 code 實際上怎麼寫、使用什麼 algorithm、data structure 等等,都影響程序執行所需的時間。若一個程序在 script 中會被 call 十萬次,而該程序被優化了後可快 0.1 秒,那優化過後,我們就省下兩個多小時。
分析程式中各單位被 call 多少次以及每次需時的程序叫 Profiling 或 Performance analysis ——python 內置的 package cProfile 就有此功用。
記得在研究院分析氣象數據時,我用 python 寫的其中一段 analysis code 耗時甚多,而 python/numpy 沒有優化了的 functions 可執行我需要的計算,此部份不得不寫 do-loop。我知道要是用 compiler 語言(如 Fortran/C)執行此 loop 部分,速度會快很多,於是我把這部份的 code 抽出來,寫成 Fortran subroutine,然後用 f2py 建立 python interface 去使用這 Fortran subroutine ——程式碼的編排沒怎麼改動,但省下了許多運算時間。
(註:Fortran/C 等 compiler 語言需要先把程式碼 compile 成執行檔後才能執行;而Python/MatLab/R 等簡明易讀、可以逐行執行的,是 interpreter 語言 — 每執行一行code,就是一輪翻譯和compilation,故執行結構一樣的 code,後者通常較慢。)
4. 當你/合作者要對程式作出大改動時,如何記錄哪行 code 被修改了?一旦出了狀況,如何把程序還原到之前可行的版本?
Short answer: Use Git version control.
倘若你的 analysis 變得複雜了,或者會有其他人改動的話,單靠檔案命名/在code裡的comments記錄改動/溝通,很容易出錯喔。Git 版本控制系統除了能夠記錄程式碼內容的增減及其次序,不同人還可以同時在不同的 branches 修改 code base 的內容然後互相比較,而且隨時可以把現時的 code 還原到較早期的版本。一切改動都記錄在 project folder 的 .git directory 裡,使用簡明的 git command 就可以輕鬆存取。
5. 當執行程序的環境(如 package version 或 dependencies)改變的時候,你如何檢查 functions 的運作有否改變?
Short answer: Run unit tests.
第一時間,你應該會想到放個 test case 進去,看看出來的結果是否如期吧。
在 software engineering 就有類似的概念,叫 unit test ——每個 function(程式最小單位)都有一組 test 去檢查特定的 input 能否得出預期的 output。(也有測試由不同 functions 構成的 pipeline 是否能運作的,叫 integration test。)
研究院時期,我的 analysis code 需要在不同的電腦跑 ——除了我自己的電腦給研究組的 server,後來我們申請到 NCAR 的 supercomputer 跑 CESM (Community Earth System Model),也得確保我的 analysis code 可以在他們的電腦執行。
若你也有 code 需要在不同環境的電腦上執行,可能會遇到一些狀況,如:兩部電腦上不同版本的 python packages,同名的 function 給出不同格式的結果。這會不會使你的 run script 跑到中途 error 呢?為你的 modules/functions 配備 unit tests,可使你快速檢查程序能否如常運作。
(註:若你的程式涉及一些其他環境沒有的 dependencies,也許使用 Docker 會是一個快捷的應變方案 ——你可以把所有 dependencies 都配置在獨立於其所在環境的 Docker container 中,那你就可在任何電腦執行裡面的 run script。)
結語
要是你的研究著重編程,在建立 research code base 時有仔細思考過上面的問題,我擔保日後的你會為節省了出錯的時間感到欣慰的(我是過來人 XD)。
以上非常簡單地介紹了一些工具的名稱/應用,大家若有興趣深鑽,應該可以 google 到不少學習資源(知道要 google 什麼才能 google 呢——當年我連要 google 什麼都不知道)。若大家希望我舉些實質例子,或對我的 research use case 有興趣,可給我留個言,我會在往後的文章分享~