無法重跑的分析之恐怖
週五下午四點。你的主管走過來說:「上個月的營收數字很棒——可以把這週的資料加進去重跑一次嗎?」你的心一沉。三週前你打開了一個試算表,手動刪掉幾列明顯壞掉的資料,複製了一欄、排了序、做了一張樞紐分析表,然後把那個關鍵數字打進投影片裡。這些步驟你一個都沒記下來。現在你連同一個數字都跑不出第二次,更別說更新它了。那股寒意,正是這篇指南要幫你避免的。
我們先用白話把目標講清楚。一份分析是可重現的(reproducible),意思是別人——或半年後的你自己——拿同一份原始資料、走同一套步驟,就能得到一模一樣的結果。注意「一模一樣」這四個字:相同的輸入進去,相同的數字出來,每次都一樣,完全不靠記憶。這和可複製性(replicability)不同,後者問的是「換一份全新的資料集,會不會得到類似的結論」。可複製性關乎一個發現是否為真;可重現性則是更基本的承諾——你自己的工作究竟跑不跑得起來。本篇談的是可重現性,它是其他一切的地基。
為什麼要這麼在意?有四個理由。第一,信任:沒人能重現的數字只是傳言,不是結果。第二,除錯:當一個數字看起來不對勁,唯有能逐步重走它的產生過程,你才找得到錯在哪。第三,協作:同事應該能在你的成果上接著做,而不必你花一小時從頭講解。第四,更新:商業問題從來不會只問一次——「用這個月的資料重跑一次」才是常態,不是例外。
資料科學迴圈的環狀示意圖:提問、蒐集資料、清理、探索、建模、決策,然後回到提問。
手工作業有一個令人不安的算術。假設你的分析有 20 個手動步驟,而每次重跑時,你都能把每一步做得一模一樣——同樣的篩選、同樣的排序、選到同一格——可靠度是 95%。那麼整條鏈跑出完全相同結果的機率,就是 0.95 自乘 20 次。在看公式前先抓住這個直覺:每個小心翼翼的步驟幾乎都對,但錯誤會層層累積。
即使每步都有 95% 的可靠度,20 個手動步驟也只有約 36% 的機率能重現——大約每三次就有兩次拿不到相同答案。自動化不是吹毛求疵,而是讓這個算式對你有利的唯一辦法。
腳本勝過點擊
資料工作中最重要的一個習慣是:每一次轉換都應該是檔案裡的程式碼、能從頭到尾一次跑完,而不是一連串你得用力記住的滑鼠點擊。點擊不留痕跡;一行程式碼則是你所做之事的永久、精確、可分享的紀錄。當步驟都活在腳本(script)裡——一個從上到下讓電腦執行的純文字指令檔——重跑整份分析就變成一道指令,而不是一整個下午小心翼翼地重新點擊。
比較兩個世界。手動版:「我依日期排序、刪掉訂單編號空白的列、移除重複,然後做了一張樞紐表。」這句話不能執行、無法檢查、也無法精確重複。腳本版:同樣的邏輯寫成程式碼,任何人都能讀、能跑、能比對差異。下面就是把那段手動故事,變成一個小而誠實的可重現清理腳本。它讀取原始檔、絕不修改它,並另外寫出一個乾淨的檔案。
import pandas as pd
# raw data is read-only: we read it, we never overwrite it
raw = pd.read_csv("data/raw/orders.csv")
clean = (
raw
.drop_duplicates(subset="order_id") # remove exact duplicate orders
.dropna(subset=["order_id", "amount"]) # drop rows we cannot use
.assign(amount=lambda d: d["amount"].astype(float))
)
# write a NEW derived file; the raw file is left untouched
clean.to_csv("data/clean/orders.csv", index=False)一個腳本很好;一串能互相重建的腳本更好。一個小小的 Makefile(一個食譜檔,內容是「要產生這個輸出,就跑那道指令,但只在它的輸入有變動時才跑」)讓你用單一指令,就能依正確順序、從原始資料一路重建到最終報表。下面那些相依關係的意思是:乾淨檔依賴原始檔與清理腳本,而報表又依賴乾淨檔。
# `make all` rebuilds every output from raw data, in dependency order all: reports/revenue.csv data/clean/orders.csv: data/raw/orders.csv clean_orders.py python clean_orders.py reports/revenue.csv: data/clean/orders.csv revenue.py python revenue.py
- 把你為了得到那個數字、手動做的每一個步驟,依序寫下來。
- 把每個步驟翻譯成腳本裡的一行或幾行程式碼。
- 讀取原始資料、寫出衍生資料——絕不覆蓋來源。
- 把試算表刪掉。若某個步驟不在腳本裡,那它就等於沒發生。
- 從乾淨狀態跑一次腳本,確認你得到相同的數字。
ETL 與 ELT:你什麼時候清理?
當你的步驟都變成腳本後,一個設計問題就浮現了:在這趟旅程的哪個環節,你才清理與重塑資料?兩個經典答案各有名字。ETL 是 Extract、Transform、Load(萃取、轉換、載入):你把資料從來源抽出,在途中先轉換(清理、合併、重塑),再把成品載入到一個集中的儲存處。ELT 把後兩個字母對調——Extract、Load、Transform(萃取、載入、轉換):你抽出資料,先把原始資料原封不動載入集中儲存處,然後才在那裡轉換它。名詞本身可看ETL 與 ELT;本節談的是「順序」為什麼重要。
用一家網路商店把它說具體。你有訂單躺在 App 的資料庫裡,廣告花費則躺在行銷 API 後面。在 ETL 的世界裡,一個 Python 工作會去抓這兩邊的資料,在記憶體中清理並合併,然後把一張整齊的「每日行銷投資報酬率」表寫進儲存處。在 ELT 的世界裡,你把這兩個來源——原始、未經修改——倒進一座資料倉儲(一個專為分析打造的集中式資料庫),然後寫 SQL 在倉儲裡,把原始表轉換成乾淨的 ROI 表。
並排的兩張流程圖:ETL(萃取、轉換、再載入)與 ELT(萃取、先載入原始資料、再在倉儲內轉換)。
為什麼業界大致從 ETL 翻轉到 ELT?雲端倉儲(想想 BigQuery、Snowflake)讓儲存變便宜、讓欄式運算快得驚人。一旦儲存原始資料幾乎不花錢,「先載入、後轉換」對可重現性就有兩大好處:你永遠保留原始資料,所以當某個轉換被發現做錯時,你可以從頭重新衍生出乾淨表;而且轉換邏輯以可讀的 SQL 形式存在,全團隊都能檢視、版本控制、重跑。下面是一個極小的 ELT 轉換。
-- ELT: the raw table is already loaded; we transform it with SQL
create or replace table analytics.clean_orders as
select
order_id,
cast(amount as float64) as amount,
date(created_at) as order_date
from raw.orders
where order_id is not null;資料倉儲的近親是資料湖(data lake)——一個更便宜的儲存處,在資料被結構化之前,先存放任何形狀的原始檔案(日誌、圖片、JSON)。許多團隊用資料湖作為原始資料的落地點、用倉儲做乾淨的分析,而像 dbt 這樣的工具,則把這些 SQL 轉換組織成一個整齊、有測試、懂相依關係的專案。你今天不必精通這些工具;你需要的是這個觀念:保留原始資料、用有版本的程式碼轉換,絕不用一次性的點擊。
以管線思考(以及步驟的有向無環圖)
當你有好幾個互相餵食的腳本時,你就有了一條資料管線(data pipeline)的雛形——一組自動化步驟,把原始輸入變成可信任的輸出,依排程執行或由事件觸發。讓管線變得可管理的心智模型是 DAG:directed acyclic graph,有向無環圖。聽起來嚇人,但它的意思不過是「一張用箭頭連起步驟的圖,而且箭頭永遠不會繞回去」。每個步驟是一個節點;從步驟 A 指向步驟 B 的箭頭,代表 B 需要 A 的輸出。
想像這家店的管線。原始訂單流入一個「清理訂單」步驟,再流入一個「每日營收」步驟。另外,原始廣告資料流入一個「清理廣告」步驟。最後,乾淨訂單與乾淨廣告一起流入一個「行銷 ROI 報表」。畫出來,就是一棵由箭頭構成、沒有任何循環的小樹。它的好處極大:當原始廣告來源一有變動,系統就知道它只需要重建廣告那條分支與最終報表——而不必動到沒變的訂單那一側。你不再用手把全部重建一遍,而是只重建確實變動的部分。
一張管線階段的有向無環圖,箭頭從原始輸入、經過清理與轉換、連到最終報表。
那麼,誰來準時跑這張圖、又盯著它有沒有失敗?一個編排器(orchestrator)——像 Airflow、Dagster、Prefect 這類工具。見排程編排:它是讀你那張 DAG 的指揮,替每個步驟排程、在失敗時重試、並在昨晚那次執行壞掉時通知你。你宣告步驟與它們的相依關係,編排器則算出順序與重建邏輯,這樣你就不必把它全記在腦子裡。
綱要與資料合約
一條管線的可信度,頂多等於流經它的資料的可信度;而那個安靜的殺手,是「形狀無預警改變」的資料。防線是一個綱要(schema)——一張表被約定好的形狀:它的欄位名稱、每一欄的資料型別(整數、浮點數、文字、日期)、哪些欄絕不能為空,以及哪一欄是能唯一辨識一列的主鍵。綱要把你對資料模糊的期望,變成明確、可檢查的規則。
再在上面加一個承諾,你就得到一份資料合約(data contract):在「產生資料的團隊」(比如負責記錄每筆訂單的工程師)與「使用資料的團隊」(你,分析師)之間的協議。這份合約白紙黑字地說:「order_id 永遠是唯一、非空的整數;amount 永遠是以美元計的非負數字;currency 永遠是 TWD、USD、JPY 其中之一。」當大家都同意、而這份協議又由自動化檢查來執行時,上游的變動就再也無法悄悄腐蝕你的數字。
為什麼這件事這麼要緊?想像 App 團隊在一次看似無害的重構中,開始把 amount 以「分」而不是「美元」傳送。沒有任何東西當掉。那一欄仍是非負數字,所以一條天真的管線照單全收——而下游每一個營收數字,突然就大了 100 倍。一個知道預期範圍的合約測試,本可以在當晚就抓到它,趕在它出現在執行長面前的投影片之前。
import pandera as pa
from pandera import Column, Check
orders_contract = pa.DataFrameSchema({
"order_id": Column(int, unique=True, nullable=False),
"amount": Column(float, Check.in_range(0, 100000)), # dollars, sane range
"currency": Column(str, Check.isin(["TWD", "USD", "JPY"])),
})
orders_contract.validate(clean) # raises an error the moment the data breaks the contract- 把綱要寫下來:欄位名稱、型別、可否為空,以及主鍵。
- 加上範圍與類別檢查,把「一個合理的值長什麼樣」編寫進去。
- 在管線的邊界、每次執行時,都自動跑這些檢查。
- 讓檢查失敗就中止管線——大聲失敗勝過無聲的損壞。
- 把合約和程式碼一起做版本控制,讓它的歷史也看得見。
同時為資料與程式碼做版本控制
要精確重現一個過去的結果,你需要找回的不是一樣、而是三樣東西:當時跑的程式碼、它跑在哪份資料上,以及它在什麼環境裡跑。三者缺一,「三月那個數字」就變得無法復原。程式碼是最簡單的部分——它放進 git,這個標準工具會記錄每個文字檔的每一個版本,讓你能簽出任何過去日期當時一模一樣的程式碼。
資料比較難。git 是為小型文字檔設計的,不是為數 GB 的資料集,碰到它就會噎住。標準答案是資料的版本控制:你不把資料塞進 git 本身,而是把那份確切資料快照的一個小指紋(內容雜湊值)存進 git,真正的位元組則放在便宜的儲存處。像 DVC、lakeFS 這類工具,以及 Delta、Iceberg 這類帶有「時光旅行(time travel)」功能的倉儲表格式,做的都是這件事的某種版本。重點都一樣:一次提交,就能把你釘在資料的某一個精確版本上。
# version the code in git as usual git add clean_orders.py revenue.py Makefile git commit -m "revenue pipeline v1" # version the data by content: the bytes go to storage, a tiny pointer goes to git dvc add data/raw/orders.csv git add data/raw/orders.csv.dvc git commit -m "snapshot raw orders 2026-03-01"
第三條腿是環境——你所用的語言與函式庫的特定版本。用 pandas 1.5 算出的結果,可能和用 pandas 2.0 算出的不同;「在我電腦上明明可以跑」正是環境沒釘住的典型症狀。解方是把相依套件釘在一個鎖定檔裡(一個 requirements 檔,或一個記錄確切版本的鎖定檔),或者把整個環境裝進一個容器(container)出貨,讓同事在一模一樣的盒子裡跑你的程式碼。
- 釘住程式碼:每個腳本都提交到 git,並附上有意義的訊息。
- 釘住資料:記下這個結果所用的確切快照/版本。
- 釘住環境:一個鎖定檔或容器,內含確切的函式庫版本。
- 固定亂數種子,讓任何抽樣或洗牌都能重複。
- 使用相對路徑,而非 /Users/你/桌面——路徑必須在任何機器上都能運作。
可重現性檢查清單
我們把一切收攏成一份檢查清單,讓你在宣布一份分析「完成」之前,真的可以逐項核對。這些項目沒有一個是高深的;但合在一起,就是「一個你能捍衛的數字」與「一個你只是記得自己做過的數字」之間的差別。把這份清單當成標準,未來的你會為此感謝你。
- 原始資料是唯讀的,且記錄了它確切的快照/版本。
- 每一次轉換都是版本控制裡的程式碼——沒有任何手動的試算表編輯。
- 一道指令就能從原始資料一路重建到最終輸出。
- 管線是一張由冪等步驟構成的 DAG;重跑永遠安全。
- 綱要與資料合約的檢查,在每次執行時都在邊界跑一遍。
- 環境已釘住(鎖定檔或容器),亂數種子也已固定。
- 一份 README 讓新人不靠任何口耳相傳的知識就能跑起來。
- 整套東西在另一台機器上,從頭到尾都能重現。
這就為「資料整理」這條主線收尾了。你現在已經能把雜亂、有缺漏、形狀不對的資料變成整齊資料,小心地清理與合併(見清理、遺漏值、去重複、合併),重塑與彙總(見樞紐與分組彙總),並且——本篇的主題——把這一整套包進一條有腳本、有版本、可重現的管線裡。那麼這些管線究竟住在哪裡、又在哪裡執行呢?那就是下一條主線:現代資料技術棧,在那裡,倉儲、SQL、dbt、排程編排,以及通往生產環境機器學習的那條路,會匯聚在一起。