data science  

Nov 6, 2016 • Michael Chen

即使現在是 21 世紀,筆者仍然喜歡用 Unix 命令列工作。Unix 命令列有許多有用的小工具,使用起來相當地靈活。然而,你可能會有類似以下的經驗:你在某個星期密集地進行某項資料分析的任務,你飛快地使用各種命令列工具以及撰寫了數個命令稿,終於完成了這項任務,這些數據和命令稿就暫時放在一旁。過了三個月,你因為某個因素,需要重新挖出這個專案的某些資料,然而,整個專案資料夾中散落著許多中繼檔案及命令稿,你已經想不起來三個月前你做了些什麼,你只好試著從 shell history 中挖出先前相關的步驟。

資料分析和撰寫程式的過程不太一樣。寫程式的時候,我們有原始碼,有測試程式(也是原始碼),可以在原始碼中撰寫註解,還可以將原始碼放入版本控制系統中。我們實作的方式有明確的記錄,要再重現這個過程不會太難。然而,我們在資料處理和分析時,如果沒有額外去記錄這個流程,命令列操作的過程往往就淹沒在 shell history 中,而 shell history 通常有長度限制,時間一久,這些記錄就完全消失了。如果使用 GUI 的數據分析軟體,情形就更糟了,有可能會沒有任何自動機制將這些過程記錄下來。

然而,資料處理和分析往往不是一個直線的過程。通常都要來回反復個幾次,才能得到滿意的結果。有時候,我們要檢查這個流程,以確定是資料本身的問題還是操作流程不當;或者,我們需要修改這個流程,以得到新的結果。如果能夠有效率且正確地還原我們資料分析的過程,就可以減少重覆的工作,增進資料分析的效率。如果資料分析的流程能夠程式化,那麼,就能提高我們的工作效率。

有些讀者可能誤會我們要放棄所有的命令列工具,改用命令稿語言來完成所有的事,這樣又有點太因噎廢食了。要達到同樣的任務,使用命令列工具往往可以使用更短的代碼來完成(如果我們把指令也當成一種程式碼)。然而,除了 shell history 這種被動的記錄方式外,命令列本身缺乏良好、有規畫的記錄功能。命令稿的功能在於可以完整地記錄程式運作的過程,但是,同樣的任務,如果全部都用命令稿語言實作,需要較多的程式碼。其實,我們可以結合兩種工具的優點,使用命令稿來「記錄」終端機指令。許多的命令稿語言,都有和命令列環境互動的功能。透過規畫良好的命令稿,我們不僅是被動地記錄我們使用過的指令,我們還可以透過執行這個命令稿來重現我們資料分析的過程。

那麼,要使用何種命令稿語言呢?這種工作模式不限定命令稿語言,只要能夠和命令列環境進行互動,不論是各種 shell script 或是 Perl、Python、Ruby 等通用語言都可以。我們會將這個記錄分析流程的命令稿分為兩部分,命令稿的註解部分用來記載敘述性的文字,這部分是給其他人或是未來的自己看的,其他的部分則是放入在工作流程會用到的命令列,透過適當地安排程式碼,我們可以透過執行這個命令稿,重現這個分析的過程。

接下來,我們以 Ruby 這個語言來演示相關的概念。這不代表只能用 Ruby 來完成這些概念,其他的命令稿語言也有相似的概念,只是使用的語法各有不同,有興趣的讀者可再自行查詢。

有時候,我們只是要執行指令,但不需其回傳值,可以用 system 這個方法 (method):

# Make a new directory
system("mkdir -p path/to/directory")

如果,我們執行的程式有可能失敗,可以接收 system 的回傳值,再做相對應的處理:

# Run some analysis in R
status = system("Rscript path/to/script.R")
raise "Error!" unless status == true

如果我們需要接收其回傳值,可以用 backtick (`):

# List items in a directory
output = `ls path/to/directory`

# Print the output
print output

如果需要更精細的控制,可以用 Open3 模組。

stdin, stdout, stderr, wait_thr = Open3.popen3("echo something")
# Manipulate stdin/stdout/stderr streams here

# Don't forget to close streams.
stdin.close
stdout.close
stderr.close

# Get cmd status
exit_status = wait_thr.value

當然,還有其他的使用方式,不過,以上的幾種方式應該適用大部分的情形,再搭配適當的註解,這個命令稿就可以變成一份可執行的文件。以下我們透過一個虛擬的案例來說明。

首先,應該將資料、文件、命令稿集中,以便管理。

$ mkdir my_project
$ cd my_project

接下來的操作,都在這個專案的根目錄中進行。首先,建立數個子資料夾,以便將檔案分開管理。日後隨著專案的進行,檔案和命令稿會越來越多,我們不想將所有的東西散落在同一個資料夾中。

$ mkdir {bin,data,files,log,reports,scripts,tmp}

# Record our main workflow here.
$ touch WORKFLOW.rb

# You might record non-scriptable text in this file.
$ touch README.md

這些資料夾的名稱,可依自己的習慣更改,這裡僅僅是習慣的用法。以下 (以筆者個人的習慣) 說明這些資料夾的用途:

在這個範例中,我們將主要的任務流程記載 WORKFLOW.rb 中,你也可以用其他的名稱。不過,通當會避開 README.mdREADME.txt,這個檔案依慣例會保留給靜態的專案說明文字。

在程式一開始執行時,將 working directory 改至專案的根目錄,這樣子,我們的指令就可以使用相對目錄,專案仍可正確執行。

# Change working directory to the root of the project
Dir.chdir File.dirname(__FILE__)

檢查是否有安裝專案所需的程式環境或執行檔。

require 'mkmf'  # For find_executable method

# Check language environments
raise "No Java Platform" if find_executable("java").nil?

# Check applications
raise "No FastQC installed" if find_executable("fastqc").nil?

如果需要檢查程式的版本,可以用字串比對:

# Check Python version
python_version = `python --version`
raise "Wrong Python version" unless python_version.include? "2.7"

在這裡,我們撰寫一個簡易的 helper method,這個 method 會將傳入的字串做為指令執行,如果執行時發出錯誤,就發出一個例外。

# Helper method
# Raise exception if error status
def run(cmd)
  status = system(cmd)
  raise "Error: #{cmd}" unless status == true
end

之後,就視需要執行相關的命令列程式:

# Execute a global command
run("ruby -e 'puts \"Hello, World.\"")

# Execute a local script
run("ruby scripts/some_script.rb some_file")

# Execute a local library packed in a JAR
run("java -jar scripts/some_library.jar some_file")

# Execute a local executable
run("bin/some_binary some_file")

# Delegate our workflow to other script
run("make target")

使用這樣的流程,儘量要減少副作用 (side effect) 的發生,簡單地說,儘量不要覆寫數據,而是將結果導向一個新的檔案,才不會在多次執行工作流程後,資料已被破壞而無法還原。大抵上,透過這樣的方式,就可以記錄我們的工作流程。我們還可以將整個專案用版本控制軟體來管理,不過,一些敏感的資料和數據,記得要用 .gitignore 去排除,有興趣的讀者可自行嘗試。

這裡介紹的方式,主要適用於 Unix 命令列環境中。如果是其他的軟體或程式語言,也可能有自己的工作方式,像是 R 的 R Markdown 或是 Python 的 Jupyter 等,讀者也可以多加善用。有一些商業軟體,也有自己的工作流程方案,像是 SAS,整個工作流程就是存成一個命令稿,可隨時再呼叫。如果是使用 GUI 的資料分析軟體,就要自行找出其 log 以何種方式存取,至少也要用截圖或其他手動的方式保存工作流程。記錄工作流程,除了對自己日後的任務有幫助,也是可再現研究 (reproducible research) 的其中一環,希望大家都可以找到最有效率的工作流程。