昨天,我們打開韓國急診檢傷與急迫度分級量表(Korean Triage and Acuity Scale, KTAS)的 Kaggle 公開資料,確認了 1,267 筆紀錄、24 個欄位、兩種人工級數,以及哪些事後欄位不能交給模型。
但「我曾經成功讀到資料」和「別人能重建完全相同的資料」是兩件事。
假設三位讀者都說自己下載了同一份 data.csv:第一位使用分號切欄,得到 24 欄;第二位使用預設逗號,只得到 1 欄;第三位下載時 Kaggle 上的檔案已更新,卻沒有發現內容不同。三個檔名看起來一樣,實際上已經不是同一份實驗輸入。
所以今天不會靠一連串手動指令與記憶操作。我們要建立一條資料管線(Data Pipeline):讓程式依固定順序取得來源、驗證內容、解壓縮、解析、正規化、依欄位角色拆檔,最後留下可核對的產物清單。
下圖先用工作台呈現這個概念。請觀察同一個來源資料包,如何依序通過多道檢查,而不是下載後直接進入模型。

上圖的重點不是把流程畫得很長,而是讓每一關都有「通過條件」。只要來源雜湊、壓縮檔內容、欄位名稱或資料筆數不符合預期,流程就應該停止,而不是勉強產生一份看似正常的結果。
讀完並實際操作後,你應該能夠:
data.csv 的內容。| 中文名稱 | 英文全名/縮寫 | 本篇用途 |
|---|---|---|
| 資料管線 | Data Pipeline | 依固定順序把來源資料轉成可驗證產物的流程 |
| 資料集識別字串 | Dataset Slug | Kaggle 用來穩定識別資料集的短字串 |
| ZIP 壓縮格式 | ZIP Archive Format | 封裝 data.csv 的下載檔格式 |
| 逗號分隔值 | Comma-Separated Values, CSV | 表格檔案格式;本資料雖叫 CSV,實際使用分號切欄 |
| 安全雜湊演算法 256 位元版本 | Secure Hash Algorithm 256-bit, SHA-256 | 用內容產生固定長度檢查碼,協助鎖定檔案版本 |
| JavaScript 物件表示法 | JavaScript Object Notation, JSON | 保存資料卡、欄位政策與 manifest 的結構化文字格式 |
| 註冊護理師 | Registered Nurse, RN | 說明來源欄位 KTAS_RN 保存的護理師登錄級數 |
| 執行產物清單 | Preparation Manifest | 記錄輸入、程式、環境、尺寸與輸出雜湊 |
| 冪等性 | Idempotency | 相同輸入與規則重跑後,核心產物仍相同的性質 |
| 原子寫入 | Atomic Write | 先寫暫存檔,成功後一次替換正式檔,避免留下半份檔案 |
| 程式碼儲存庫 | Repository | 保存版本化程式、設定與文章的專案空間 |
| 原始資料層 | Raw Data Layer | 保存下載後的來源位元,不做人工修改 |
| 中介資料層 | Interim Data Layer | 保存可追溯的正規化與欄位角色拆分結果 |
| 模型資料層 | Processed Data Layer | 保存切分、補值與特徵處理後,能交給模型的版本 |
詞彙表提供快速索引,下面會再說明每個概念如何落到路徑、指令與驗證結果。
資料管線不是把多行指令包成一行而已。這條管線至少要同時回答五個問題:
本篇把「可重跑」定義為:在相同 Kaggle 檔案、資料卡、欄位政策、處理程式與相依套件下,流程能從空白資料目錄建立相同的核心輸出。
這個定義沒有宣稱任何電腦、任何未鎖定套件版本都一定產生完全相同的位元。Day 13 會再處理環境鎖定與整個 repository 的執行契約;今天先把資料來源與資料轉換鎖住。
安全雜湊演算法 256 位元版本(Secure Hash Algorithm 256-bit, SHA-256)會依檔案內容產生 64 個十六進位字元。內容只要改變,檢查碼通常也會改變。
本系列目前鎖定:
Kaggle ZIP
f78df0a06c31b6df873c410b0596ecb95ac9335485beaf890dcabf7c69560f26
data.csv
0e2c088e358fd4cdfd0dcc2fd4c2f085aa303856e3a6df8ceb44a69cef8ed2de
SHA-256 能回答「這次檔案是否和已稽核版本相同」,不能回答資料欄位是否適合臨床使用、標籤是否完美或來源是否代表整體族群。後三個問題仍要依資料卡、欄位政策與來源研究判斷。
資料集識別字串(Dataset Slug)是 Kaggle 網址中的 ilkeryildiz/emergency-service-triage-application。它告訴程式要去哪一個資料集頁面,不足以單獨鎖定某次下載內容。
本篇稍後會帶你建立完整的 Kaggle KTAS 資料卡 configs/data/day-12-kaggle-ktas-dataset-card.json。它會保存在專案設定資料夾中,內容包括:
執行產物清單(Preparation Manifest)則是某一次本機處理留下的機器可讀證據。它會記錄實際讀到的來源雜湊、政策與程式雜湊、Python 與 Pandas 版本、輸出尺寸,以及每個核心產物的 SHA-256。
一句話區分:資料卡描述這份資料應該是什麼;manifest 記錄這次程式實際做出了什麼。
在繼續之前,要先解釋指令中的 scripts/ 到底是什麼。
scripts/ 是這個專案存放自動化程式的資料夾腳本(Script)是一份可以交給直譯器依序執行的程式。這個專案把資料下載、驗證與轉換等自動化程式集中放在 scripts/ 資料夾;它不是需要另外安裝的 Python 套件,也不是網路服務。
以後面會執行的指令為例:
python scripts/run_ktas_pipeline.py
這行指令可以拆成三部分理解:
python:啟動 Python 直譯器,也就是實際讀取並執行程式的工具。scripts/:從專案根目錄往下找到存放自動化程式的資料夾。run_ktas_pipeline.py:要執行的 Python 原始碼檔;.py 是 Python 程式的副檔名。因此,這行指令的完整意思是:「請 Python 執行 scripts/ 資料夾裡的 run_ktas_pipeline.py。」後面的完整程式碼小節會先帶你建立這支程式;只要目前位於專案根目錄,Python 就能依相對路徑找到它。
資料管線使用兩份設定檔與兩支 Python 程式。設定檔負責保存決策,Python 程式負責執行動作:
專案根目錄/
├── configs/
│ └── data/
│ ├── day-11-ktas-field-policy.json
│ └── day-12-kaggle-ktas-dataset-card.json
└── scripts/
├── run_ktas_pipeline.py
└── prepare_ktas.py
configs/ 是組態設定資料夾。組態(Configuration)指的是程式執行時要遵守、但不需要寫死在程式內的決策,例如來源雜湊、欄位角色與輸出限制。這兩份設定檔使用 JavaScript 物件表示法(JavaScript Object Notation, JSON)保存,因此可以直接用文字閱讀,也能由 Python 載入。
四個檔案的責任如下:
| 檔案 | 它是什麼 | 接收或保存的內容 | 產生或影響的結果 |
|---|---|---|---|
day-12-kaggle-ktas-dataset-card.json |
資料卡設定檔 | Kaggle 來源、檔名、ZIP 與 CSV 雜湊、預期筆數及欄數 | 決定要下載哪個來源,以及什麼內容才算正確版本 |
day-11-ktas-field-policy.json |
欄位政策設定檔 | 24 個來源欄位的型別、角色、缺失代碼與禁止輸入規則 | 決定哪些欄位進候選輸入、稽核檔或標籤檔 |
run_ktas_pipeline.py |
管線協調程式 | 兩份設定檔,以及網路下載或 --archive 指定的 ZIP |
建立原始層、呼叫資料準備程式,最後寫入來源與處理 manifest |
prepare_ktas.py |
資料轉換程式 | 已驗證的 data.csv 與欄位政策 |
解析分號與 cp1254、正規化缺失代碼、驗證尺寸與標籤,再依角色輸出五個檔案 |
run_ktas_pipeline.py 是最外層入口,負責安排工作順序。它本身不猜測欄位用途,而是依序完成:
data.csv,再驗證 CSV 雜湊。prepare_ktas.py。prepare_ktas.py 完成拆檔後,計算輸出雜湊並建立 preparation manifest。prepare_ktas.py 的責任比較單純:它不連上 Kaggle,也不負責下載。它只處理已經驗證過的 data.csv,把來源表格轉成候選輸入、稽核欄位、參考標籤、禁止欄位清單與摘要。這樣拆成兩支程式,是為了讓「來源取得與版本驗證」和「表格內容轉換」能分開測試;其中一邊出錯時,也能更快判斷問題發生在哪一層。
讀到這裡,應該已經知道每個檔案的角色。Day 11 已完整提供 prepare_ktas.py 與欄位政策;本篇下一個實作小節會完整提供資料卡與 run_ktas_pipeline.py,讀者不需要前往其他程式碼網站尋找內容。
下圖把一個命令背後的七道關卡展開。閱讀方向先由左到右走完上排,再從第四關往下,最後由右向左走到 manifest。

上圖顯示,ZIP 與 CSV 需要分開驗證。前者能確認整個下載包相同,後者能確認真正交給表格處理程式的內容相同。即使未來壓縮方式改變但 CSV 沒變,也不能靜默略過差異;我們要先記錄版本變化,再決定是否更新資料卡。
安全解壓縮也不是多餘的步驟。程式只接受 ZIP 中剛好有一個 data.csv,而且檔名不能帶有上層路徑。這能同時偵測來源結構改變,並避免壓縮檔把內容寫到預期目錄之外。
接下來不會要求你前往任何程式碼網站。請在自己的電腦開啟專案資料夾,依下列順序建立檔案;每個程式碼區塊都是該檔案的完整內容,不含省略號。
本篇沿用 Day 11 已完整建立的欄位政策與 prepare_ktas.py;現在新增資料卡與最外層管線入口。
先從專案根目錄建立需要的資料夾:
mkdir -p configs/data scripts
如果指令沒有印出訊息是正常的。可用 test -d 資料夾路徑 && echo "資料夾已建立" 驗證單一資料夾。接著使用你熟悉的文字編輯器新增各檔案,把對應區塊完整貼入後儲存。
configs/data/day-12-kaggle-ktas-dataset-card.json鎖定 Kaggle 來源、ZIP 與 CSV 雜湊、格式、資料規模及使用限制。
請在文字編輯器建立 configs/data/day-12-kaggle-ktas-dataset-card.json,貼入以下完整內容並儲存:
{
"schema_version": "0.1",
"status": "source-audited-pipeline-ready",
"title": "Kaggle KTAS 公開急診資料卡",
"dataset_id": "kaggle_ktas_emergency_service_triage",
"source": {
"display_name": "Emergency Service - Triage Application",
"kaggle_slug": "ilkeryildiz/emergency-service-triage-application",
"kaggle_url": "https://www.kaggle.com/datasets/ilkeryildiz/emergency-service-triage-application",
"download_url": "https://www.kaggle.com/api/v1/datasets/download/ilkeryildiz/emergency-service-triage-application",
"provenance_article": "https://doi.org/10.1371/journal.pone.0216972",
"checked_on": "2026-08-02"
},
"version_lock": {
"archive_filename": "ktas-kaggle.zip",
"archive_sha256": "f78df0a06c31b6df873c410b0596ecb95ac9335485beaf890dcabf7c69560f26",
"data_filename": "data.csv",
"data_sha256": "0e2c088e358fd4cdfd0dcc2fd4c2f085aa303856e3a6df8ceb44a69cef8ed2de",
"record_count": 1267,
"field_count": 24,
"delimiter": ";",
"encoding": "cp1254"
},
"intended_use": [
"建立五級 KTAS 離線分類與檢索增強生成實驗",
"比較模型預測、護理師登錄級數與專家重新判定級數",
"分析檢傷不足、檢傷過度、跨級距離與各級召回率"
],
"not_intended_use": [
"直接支援真實臨床決策或取代檢傷護理師",
"把 KTAS 結果改稱 TTAS 或 ESI 效能",
"使用急診診斷、去向、停留時間或衍生誤差欄位預測檢傷級數",
"宣稱資料代表韓國或其他地區所有急診病患"
],
"label_policy": {
"primary_reference_label": "KTAS_expert",
"human_reference": "KTAS_RN",
"label_direction": "1 最緊急,5 最不緊急"
},
"known_limits": [
"資料只有 1,267 筆紀錄,專家第一級只有 26 筆",
"資料沒有可用的病患識別碼,無法保證病患層級切分",
"資料來自兩所都市教學醫院與隨機選出的 20 天",
"Kaggle CSV 不是完整 KTAS 規則手冊"
],
"storage_policy": "逐筆原始與處理後資料只保存在不納入版本控制的 data/ 目錄;專案的版本控制只保存來源、雜湊、欄位政策、程式與聚合摘要。"
}
儲存後先確認檔名與相對路徑完全一致,再繼續建立下一個檔案。
scripts/run_ktas_pipeline.py負責下載或接收 ZIP、安全解壓、驗證雜湊、呼叫 Day 11 轉換程式並建立 manifest。
請在文字編輯器建立 scripts/run_ktas_pipeline.py,貼入以下完整內容並儲存:
#!/usr/bin/env python3
"""從 Kaggle ZIP 建立可追溯的 KTAS 本機資料產物。
預設會從公開 Kaggle 下載端點取得資料;也可用 ``--archive`` 指定已下載的
ZIP,方便離線驗證。逐筆資料只會寫入被 Git 忽略的 data/ 目錄。
"""
from __future__ import annotations
import argparse
import hashlib
from importlib.metadata import version
import json
import os
from pathlib import Path
import platform
import shutil
import subprocess
import sys
from typing import Any
from urllib.request import Request, urlopen
import zipfile
ROOT = Path(__file__).resolve().parents[1]
POLICY_PATH = ROOT / "configs" / "data" / "day-11-ktas-field-policy.json"
DATASET_CARD_PATH = (
ROOT / "configs" / "data" / "day-12-kaggle-ktas-dataset-card.json"
)
PREPARE_SCRIPT = ROOT / "scripts" / "prepare_ktas.py"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="下載、驗證、解壓縮並準備 Kaggle KTAS 公開資料。"
)
parser.add_argument(
"--archive",
type=Path,
help="選用:使用既有 Kaggle ZIP;省略時從資料卡的公開端點下載。",
)
parser.add_argument(
"--raw-dir",
type=Path,
default=ROOT / "data" / "raw" / "ktas",
help="不可變來源層;預設為 data/raw/ktas。",
)
parser.add_argument(
"--output-dir",
type=Path,
default=ROOT / "data" / "interim" / "ktas-v1",
help="角色拆分與正規化輸出;預設為 data/interim/ktas-v1。",
)
parser.add_argument("--policy", type=Path, default=POLICY_PATH)
parser.add_argument("--dataset-card", type=Path, default=DATASET_CARD_PATH)
return parser.parse_args()
def read_json(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as file:
return json.load(file)
def write_json(path: Path, payload: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
with temporary.open("w", encoding="utf-8") as file:
json.dump(payload, file, ensure_ascii=False, indent=2)
file.write("\n")
os.replace(temporary, path)
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def verify_hash(path: Path, expected: str, label: str) -> None:
if not path.is_file():
raise FileNotFoundError(f"找不到{label}:{path}")
observed = sha256(path)
if observed != expected:
raise ValueError(
f"{label}的 SHA-256 不符。\n預期:{expected}\n實際:{observed}\n"
"流程已停止;請確認來源版本,不要直接改掉預期雜湊。"
)
def download(url: str, destination: Path) -> None:
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = destination.with_suffix(destination.suffix + ".part")
request = Request(url, headers={"User-Agent": "ktas-sideproject30/0.1"})
try:
with urlopen(request, timeout=120) as response, temporary.open("wb") as file:
shutil.copyfileobj(response, file)
os.replace(temporary, destination)
except Exception:
temporary.unlink(missing_ok=True)
raise
def materialize_archive(source: Path | None, target: Path, download_url: str) -> str:
if source is not None:
source = source.expanduser().resolve()
if not source.is_file():
raise FileNotFoundError(f"找不到 --archive 指定的檔案:{source}")
target.parent.mkdir(parents=True, exist_ok=True)
if source != target.resolve():
if target.exists() and sha256(target) != sha256(source):
raise FileExistsError(
f"{target} 已存在但內容不同。請改用空目錄,流程不會覆寫來源層。"
)
if not target.exists():
shutil.copyfile(source, target)
return "local-archive"
if not target.exists():
download(download_url, target)
return "kaggle-download"
return "verified-cache"
def safe_extract(zip_path: Path, data_path: Path, expected_member: str) -> None:
temporary = data_path.with_suffix(".csv.tmp")
with zipfile.ZipFile(zip_path) as archive:
files = [member for member in archive.infolist() if not member.is_dir()]
names = [member.filename for member in files]
if names != [expected_member]:
raise ValueError(
f"ZIP 內容與鎖定版本不同;預期只有 {expected_member},實際為 {names}"
)
member = files[0]
if Path(member.filename).name != member.filename:
raise ValueError("ZIP 成員包含路徑,為避免路徑穿越,流程已停止。")
try:
with archive.open(member) as source, temporary.open("wb") as target:
shutil.copyfileobj(source, target)
os.replace(temporary, data_path)
except Exception:
temporary.unlink(missing_ok=True)
raise
def project_path(path: Path) -> str:
resolved = path.resolve()
try:
return str(resolved.relative_to(ROOT))
except ValueError:
return str(resolved)
def run_prepare(data_path: Path, output_dir: Path, policy_path: Path) -> None:
subprocess.run(
[
sys.executable,
str(PREPARE_SCRIPT),
"--input",
str(data_path),
"--output-dir",
str(output_dir),
"--policy",
str(policy_path),
],
cwd=ROOT,
check=True,
)
def main() -> None:
args = parse_args()
policy = read_json(args.policy)
card = read_json(args.dataset_card)
source = policy["public_source"]
locked = card["version_lock"]
if source["archive_sha256"] != locked["archive_sha256"]:
raise ValueError("Day 11 欄位政策與 Day 12 資料卡的 ZIP 雜湊不一致。")
if source["data_sha256"] != locked["data_sha256"]:
raise ValueError("Day 11 欄位政策與 Day 12 資料卡的 CSV 雜湊不一致。")
raw_dir = args.raw_dir.expanduser().resolve()
output_dir = args.output_dir.expanduser().resolve()
raw_dir.mkdir(parents=True, exist_ok=True)
archive_path = raw_dir / locked["archive_filename"]
data_path = raw_dir / locked["data_filename"]
acquisition = materialize_archive(
args.archive,
archive_path,
card["source"]["download_url"],
)
verify_hash(archive_path, locked["archive_sha256"], "Kaggle ZIP")
if data_path.exists():
verify_hash(data_path, locked["data_sha256"], "Kaggle data.csv")
else:
safe_extract(archive_path, data_path, locked["data_filename"])
verify_hash(data_path, locked["data_sha256"], "Kaggle data.csv")
source_manifest = {
"status": "source-verified",
"dataset_id": card["dataset_id"],
"kaggle_slug": card["source"]["kaggle_slug"],
"acquisition_method": acquisition,
"archive": {
"path": project_path(archive_path),
"bytes": archive_path.stat().st_size,
"sha256": sha256(archive_path),
},
"data_file": {
"path": project_path(data_path),
"bytes": data_path.stat().st_size,
"sha256": sha256(data_path),
},
"note": "只有雜湊完全相同才視為本系列鎖定的來源版本。",
}
write_json(raw_dir / "source-manifest.json", source_manifest)
run_prepare(data_path, output_dir, args.policy)
summary = read_json(output_dir / "summary.json")
input_field_count = sum(
field["role"] == "model_input_candidate" for field in policy["fields"]
)
artifact_names = [
"model-input-candidates.csv",
"audit-strata.csv",
"reference-labels.csv",
"excluded-fields.json",
"summary.json",
]
artifacts = {
name: {
"bytes": (output_dir / name).stat().st_size,
"sha256": sha256(output_dir / name),
}
for name in artifact_names
}
preparation_manifest = {
"status": "preparation-complete",
"dataset_id": card["dataset_id"],
"source_manifest": project_path(raw_dir / "source-manifest.json"),
"inputs": {
"data_sha256": sha256(data_path),
"policy_path": project_path(args.policy),
"policy_sha256": sha256(args.policy),
"dataset_card_path": project_path(args.dataset_card),
"dataset_card_sha256": sha256(args.dataset_card),
},
"implementation": {
"runner_path": project_path(Path(__file__)),
"runner_sha256": sha256(Path(__file__)),
"preparer_path": project_path(PREPARE_SCRIPT),
"preparer_sha256": sha256(PREPARE_SCRIPT),
"python": platform.python_version(),
"pandas": version("pandas"),
},
"observed": {
"record_count": summary["record_count"],
"source_field_count": policy["observed_structure"]["field_count"],
"model_input_shape": [summary["record_count"], input_field_count + 1],
"expert_label_counts": summary["expert_label_counts"],
},
"artifacts": artifacts,
"reproducibility_note": "時間戳不寫入 manifest;相同來源、政策與程式應得到相同產物雜湊。",
}
write_json(output_dir / "preparation-manifest.json", preparation_manifest)
print("資料管線完成")
print(f"來源層:{raw_dir}")
print(f"處理層:{output_dir}")
print(
f"驗證:{summary['record_count']:,} 筆、"
f"{policy['observed_structure']['field_count']} 個來源欄位、"
f"{input_field_count + 1} 個候選輸入檔欄位"
)
if __name__ == "__main__":
main()
儲存後先確認檔名與相對路徑完全一致,再繼續建立下一個檔案。
以下命令會在需要時產生套件鎖定檔,接著檢查設定格式與 Python 語法;它們不會啟動模型,也不會把病患資料送到網路:
python3 -m json.tool configs/data/day-12-kaggle-ktas-dataset-card.json
python3 -m py_compile scripts/run_ktas_pipeline.py scripts/prepare_ktas.py
每個命令都應正常結束。若 JSON 顯示行號,先檢查貼上時是否遺漏逗號、引號或括號;若 py_compile 報錯,先依行號修正縮排或漏貼內容。靜態檢查通過後,再執行本文後面的正式步驟。
以下每個步驟都在專案根目錄執行。指令不會把逐筆資料加入 Git;但完成後仍要用 git status 驗證 .gitignore 是否生效。
python3 -m venv .venv
source .venv/bin/activate
python -m pip install 'pandas>=2.0,<3'
(.venv)。python -c "import platform, pandas; print(platform.python_version(), pandas.__version__)"
這裡先限制 Pandas 的主要版本範圍,實際版本會寫入 preparation manifest。Day 13 會再把相依套件鎖定方式納入整個 repository 的可重現設計。
data/raw/ktas/ 尚未放入內容不同的同名檔案。python scripts/run_ktas_pipeline.py
完成:1267 筆、24 欄
模型輸入不含護理師級數、專家級數、事後結果或衍生誤差欄位。
資料管線完成
來源層:.../data/raw/ktas
處理層:.../data/interim/ktas-v1
驗證:1,267 筆、24 個來源欄位、15 個候選輸入檔欄位
data/raw/ktas/ 應有 ZIP、CSV 與來源 manifest;data/interim/ktas-v1/ 應有五個資料/摘要檔與一份 preparation manifest。公開下載端點在本系列 2026 年 8 月 2 日稽核時可直接取得。若 Kaggle 後續調整下載流程,可以從公開資料頁手動取得同一個 ZIP,再改用:
python scripts/run_ktas_pipeline.py \
--archive /你的下載目錄/ktas-kaggle.zip
--archive 只改變取得方式,不會降低驗證標準。指定檔案仍要通過完全相同的 ZIP 與 CSV SHA-256。這份 Kaggle 公開資料不要求簽署醫療資料使用協議;Kaggle 平台本身是否調整帳號或下載介面,則要以資料頁當下規則為準。
data/raw/ktas/source-manifest.json。python -m json.tool data/raw/ktas/source-manifest.json
status 必須是 source-verified,兩個雜湊必須和資料卡一致。source-manifest.json 不會把「下載完成」當成「驗證完成」。網路下載即使成功,也可能拿到更新版資料、錯誤頁面或不完整檔案;只有內容雜湊相同,狀態才會進入 source-verified。
原始資料層(Raw Data Layer)保留 Kaggle 來源位元;中介資料層(Interim Data Layer)保存缺失代碼正規化與欄位角色拆分;模型資料層(Processed Data Layer)才會保存切分、補值與衍生特徵後的版本。
下圖請先看每層下方的責任,再看檔名。橘色的模型層目前刻意標成「尚未建立」。

上圖顯示,Day 12 的輸出仍叫「候選輸入」,不是「最終模型資料」。我們尚未決定缺失處理、交叉驗證折數、隨機種子與主訴文字正規化方式。若現在就建立訓練與測試檔,很容易在規則尚未鎖定前反覆調整測試集合。
data/raw/ktas/ 會包含:
data/raw/ktas/
├── ktas-kaggle.zip
├── data.csv
└── source-manifest.json
原始層的原則是不可變(Immutable):data.csv 若需要修正空白、缺失代碼或小數點,應把結果寫到另一層,不能直接存回原檔。否則來源 SHA-256 會改變,也無法判斷差異來自 Kaggle 還是我們的手動編輯。
data/interim/ktas-v1/ 會包含六個檔案:
| 檔案 | 尺寸/內容 | 可用方式 |
|---|---|---|
model-input-candidates.csv |
1,267 列 × 15 欄 | record_index 加 14 個檢傷當下候選輸入 |
audit-strata.csv |
1,267 列 × 3 欄 | 只用於場域與每小時到院人數的分層稽核 |
reference-labels.csv |
1,267 列 × 5 欄 | 保存護理師級數、專家級數與衍生評估欄位;不得交給模型 |
excluded-fields.json |
禁止輸入欄位清單 | 用程式檢查標籤與事後結果是否洩漏 |
summary.json |
筆數、缺失與級數分布 | 核對來源處理結果,不含模型成果 |
preparation-manifest.json |
輸入、程式、環境與產物雜湊 | 追溯這次資料準備如何形成 |
處理層不另外輸出急診診斷、去向、急診停留時間與檢傷作業時間。它們仍存在原始 CSV,未來若要做事後結果分析,必須建立另一條明確用途的流程;現在先避免「因為方便」而讓事後資訊靠近模型輸入。
模型資料層要等 Day 15 才建立,因為資料只有 1,267 筆,專家第一級只有 26 筆,而且沒有可用的病患識別碼。切分方式會直接影響每一級的樣本量與結論不確定性。
下一階段至少要先固定:
所以「尚未建立」不是缺少進度,而是防止資料洩漏的主動邊界。
逗號分隔值(Comma-Separated Values, CSV)是以純文字保存表格的格式,但副檔名並不保證分隔符真的為逗號。這份 data.csv 實際使用分號 ; 切欄,文字編碼是 Windows-1254,也就是 Python 的 cp1254。
scripts/prepare_ktas.py 明確指定:
pd.read_csv(
data_path,
sep=";",
encoding="cp1254",
dtype=object,
keep_default_na=False,
)
先以文字讀取,是為了在數值轉型前看見來源真正使用的缺失代碼。接著程式依下列順序處理:
?? 與 #BOŞ!。5,00 轉成 5.00。KTAS_RN 與 KTAS_expert 都沒有缺失,且只落在第一到第五級。上一步的輸出會自然成為下一步的輸入:來源文字先完成缺失辨識,才能安全做數值轉型;數值與標籤通過驗證後,才能依角色拆檔;拆檔完成後,資料管線入口程式才會計算產物雜湊並建立 manifest。
JavaScript 物件表示法(JavaScript Object Notation, JSON)是一種能同時讓人與程式閱讀的結構化文字格式。本篇用 JSON 保存資料卡、欄位政策與兩份 manifest,避免把關鍵版本只寫在終端機畫面或作者記憶裡。
data/interim/ktas-v1/preparation-manifest.json。python -m json.tool data/interim/ktas-v1/preparation-manifest.json
status、inputs、implementation、observed 與 artifacts。| 區塊 | 應該看到什麼 | 回答的問題 |
|---|---|---|
status |
preparation-complete |
流程是否完整走完 |
inputs |
CSV、欄位政策與資料卡的 SHA-256 | 這次讀了哪一版輸入 |
implementation |
管線入口與資料準備程式的 SHA-256,以及 Python、Pandas 版本 | 哪一版程式與環境做轉換 |
observed |
1,267 筆、24 個來源欄位、1,267 × 15 候選輸入 | 產物尺寸是否符合契約 |
artifacts |
五個核心資料/摘要檔的位元組數與 SHA-256 | 最後實際產生哪些內容 |
manifest 刻意不寫入現在時間。這讓相同輸入、程式與環境重跑時,manifest 本身也能保持相同內容。若要另外記錄下載或執行時間,可以放在未參與核心產物比較的操作日誌中;不要讓每次都變動的時間戳破壞冪等性檢查。
python - <<'PY'
import json
from pathlib import Path
import pandas as pd
root = Path("data/interim/ktas-v1")
inputs = pd.read_csv(root / "model-input-candidates.csv")
excluded = json.loads((root / "excluded-fields.json").read_text(encoding="utf-8"))
manifest = json.loads((root / "preparation-manifest.json").read_text(encoding="utf-8"))
forbidden = {
field
for role, fields in excluded.items()
if role != "note"
for field in fields
}
leaked = forbidden.intersection(inputs.columns)
if inputs.shape != (1267, 15):
raise SystemExit(f"候選輸入尺寸錯誤:{inputs.shape}")
if manifest["status"] != "preparation-complete":
raise SystemExit(f"manifest 狀態錯誤:{manifest['status']}")
if leaked:
raise SystemExit(f"發現禁止欄位:{sorted(leaked)}")
print("通過:1,267 × 15,manifest 完整,未發現禁止欄位")
PY
通過:1,267 × 15,manifest 完整,未發現禁止欄位。scripts/prepare_ktas.py;不能只在模型程式中暫時忽略多出的欄位。下圖把一次成功執行的最低驗證項目放在同一個畫面。請注意,這些是來源與管線結果,不是模型準確率。

上圖顯示「沒有報錯」只是第一層條件。來源內容、資料尺寸、缺失規則、輸出角色與 manifest 都一致,才能說本次資料準備通過契約。
冪等性(Idempotency)在本篇代表:相同的來源、資料卡、欄位政策、程式與環境重跑後,五個核心資料/摘要檔與 preparation manifest 的內容雜湊不變。
shasum -a 256 data/interim/ktas-v1/*
python scripts/run_ktas_pipeline.py
shasum -a 256 data/interim/ktas-v1/*
原子寫入(Atomic Write)則處理另一種失敗:如果下載或寫 manifest 的過程中斷,程式先清除 .part 或 .tmp 暫存檔,不會把半份內容冒充正式產物。這不能消除所有硬體與作業系統風險,但能避免最常見的中途失敗狀態。
若第一次執行是網路下載,source-manifest.json 的 acquisition_method 會記為 kaggle-download;使用已下載檔案時可能是 verified-cache 或 local-archive。取得方式可以不同,真正進入資料處理的 ZIP 與 CSV 雜湊仍必須相同。
git status --short
data/raw/、data/interim/ 或 data/processed/ 內的檔案。git check-ignore data/raw/ktas/data.csv,應印出該路徑,表示 .gitignore 規則生效。Kaggle 上可以公開取得,不代表我們就應該把逐筆資料重新鏡像到自己的專案。版本控制保存的是重建契約:來源頁、雜湊、資料卡、欄位政策、處理程式與聚合統計。
可能原因包括下載不完整、Kaggle 更新資料、拿到其他檔案,或原始層曾被修改。管線會停止,不提供「忽略檢查」選項。先重新下載並核對 dataset slug;若 Kaggle 確實發布新版,應另存新版本、比較內容差異、重新做 Day 11 欄位稽核,再明確更新資料卡。
這代表解壓後真正要讀取的內容不是鎖定版本。不要只因為筆數看起來一樣就繼續;相同列數仍可能有欄位值或文字不同。
通常是把檔案交給使用逗號作預設分隔符的工具。修正方式不是手動另存成 Excel,而是明確使用 sep=";",並重新驗證來源仍有 1,267 列、24 欄。
通常是用萬國碼轉換格式 8 位元(Unicode Transformation Format 8-bit, UTF-8)直接讀取 #BOŞ!。這份來源要使用 encoding="cp1254"。不要先用文字編輯器另存檔案,否則原始雜湊會改變。
空字串、?? 與 #BOŞ! 都應先轉成缺失,再做數值解析。它們不等於 0,也不能直接以整體平均值取代。Day 15 才會在固定切分內比較缺失處理策略。
依序比較來源雜湊、資料卡雜湊、欄位政策雜湊、管線入口與資料準備程式雜湊、Python 版本及 Pandas 版本。這些項目正是 manifest 存在的理由;少記任何一項,都可能只剩下猜測。
model-input-candidates.csv 已排除答案與事後結果,但尚未完成切分、缺失處理與特徵正規化。它只能成為下一階段輸入,不能直接拿整份資料調參後再宣稱是測試結果。
管線通過只代表固定檔案與固定轉換符合工程契約。它不會增加資料代表性,也不會消除小樣本、病患識別碼缺失、回溯病歷與場域限制。系統仍只定位為離線決策支援實驗,不取代急診專業判斷。
今天把 Day 11 的來源與欄位契約變成一條真正能執行的資料管線:
scripts/run_ktas_pipeline.py 依序完成下載、ZIP 驗證、安全解壓、CSV 驗證、格式解析、欄位契約與產物登錄。data.csv 必須用分號與 cp1254 解析;空字串、??、#BOŞ! 與逗號小數點都要用明確規則處理。Day 13 會把視角從資料管線拉到整個專案結構:
data/、configs/、src/、tests/ 與 results/ 各自負責什麼。Day 12 產生的資料卡、欄位政策、來源 manifest 與 preparation manifest,會成為 Day 13 設計專案追溯鏈的第一組實際案例。
zipfile — Work with ZIP archives.hashlib — Secure hashes and message digests.pandas.read_csv.