1. 問題不在算 Cpk,在算之前那十分鐘

Cpk 的公式不難,難的是把量測儀器吐出來的檔案變成能算的資料。我們品管室的自動影像量測系統按一下就產生一個 CSV,一次可能是幾百筆量測值;客戶要的則是一份製程能力報表。中間這一段長年是這樣做的:開 Excel、複製貼上、拉公式

這件事的成本不只是時間。它是一段沒有紀錄、沒有版本、每次都靠人重做一遍的流程——同一批資料換個人整理,結果可能不一樣。

真正讓我們決定動手的,是發現有人把量測軟體輸出的 ---(代表這個尺寸沒量到)當成 0 貼進試算表算了平均值。那份報表上的 Cpk 是錯的,而且錯得很有說服力——數字看起來完全正常,沒有任何地方會跳出來告訴你它是錯的。

2. 技術選型:為什麼是零依賴的 Python

我們的 ERP 主體是 .NET,但這支工具刻意選了 Python,而且不使用 pandas、numpy 或任何第三方套件,只用標準函式庫。

理由很現實:這支工具要能在品管室那台電腦上跑。工廠現場的電腦通常不能隨便裝東西、不一定連得上外網、也不會有人願意為了一份報表去處理套件相依問題。「複製資料夾過去就能跑」是這個場景唯一可靠的安裝方式。

架構刻意切成四塊,因為會變的東西和不會變的東西不一樣:reader 處理檔案格式(每換一台機器就可能要調)、spec 處理規格定義(每個料號不同)、stats 是純統計(永遠不變)、report 負責輸出。統計那一層不碰檔案、不碰 I/O,所以可以被完整測試。

cpk_report/
  reader.py   # 編碼偵測、資料列判斷、數值解析
  spec.py     # 規格檔(哪一欄是什麼尺寸、公差多少)
  stats.py    # Cp / Cpk / Pp / Ppk / Ca — 純函式,可完整測試
  report.py   # 文字 + 單一自足 HTML

3. 踩坑其一:檔案不是 UTF-8,而且錯了不會報錯

第一版直接用 UTF-8 開檔,當場 UnicodeDecodeError。台灣現場的量測軟體多半輸出 Big5 / cp950

但真正危險的不是這個 error——是那些不會噴 error 的情況。Big5 被誤判成別的編碼時,程式不會停,只會安靜地把中文欄位變成亂碼繼續跑下去。所以最後的做法是依序嘗試候選編碼,並且把實際用到的編碼印在報表上

DEFAULT_ENCODINGS = ("utf-8-sig", "big5", "cp950", "utf-8", "latin-1")

latin-1 放在最後是保底——它對任何位元組都不會失敗。這代表工具永遠不會因為遇到沒見過的編碼就整個停擺,寧可讓人在報表上看到怪字元、去指定 --encoding,也不要在半夜的排程裡靜默失敗。

4. 踩坑其二:用固定列號切資料,一定會切歪

匯出檔的前幾行是機台標頭,很自然會想「跳過前 4 行」。實際跑下去才發現現場檔案沒那麼乖:標頭行數會因設定而變、資料中間會夾空行、檔尾還有註記行。任何固定列號的寫法都會在某一天切錯。

所以判斷改成「這一列長得像不像資料列」——欄數夠、而且時間戳記欄真的像時間戳記:

def looks_like_data_row(parts, min_columns, timestamp_column, timestamp_prefix):
    if len(parts) < min_columns:
        return False
    cell = parts[timestamp_column].strip()
    return len(cell) > 8 and cell.startswith(timestamp_prefix)

這裡刻意檢查數值欄位能不能解析——因為「判定為 NG、數值是 ---」也是一列完全合法的資料,把它濾掉就會低估不良率。

5. 踩坑其三:「沒量到」永遠不是 0(這才是重點)

這是整支工具存在的理由,也是 Excel 做不到的一件事。

量測軟體用 --- 表示這個尺寸沒有量到。貼進 Excel,它就是 0。而 0 會同時做兩件事:把平均值拉偏、把標準差撐大。算出來的 Cpk 於是變成一個看似合理的錯誤結論。

工具的做法是把這些值排除出統計,而且在報表上單獨列出「排除」筆數

NOT_MEASURED = {"", "-", "--", "---", "----", "n/a", "na", "null", "none"}

def parse_number(cell):
    text = cell.strip()
    if text.lower() in NOT_MEASURED:
        return None      # 關鍵:回 None,不是 0.0
    ...

報表上那個「排除」欄位存在的唯一理由,是讓看報表的人知道這批資料到底有幾筆是真的量到的。60 筆裡有 2 筆沒量到,跟 60 筆全部量到,是兩件事——但如果報表不講,沒有人會發現。

6. 一個延伸決定:零變異回報「無法計算」,不回報「完美」

如果 60 筆量測值全部一模一樣,標準差是 0,Cpk 在數學上是無限大。

但在現場,這幾乎永遠代表量具的解析度不夠,不代表製程完美。所以工具在這種情況印 而不是一個漂亮的數字。這是同一個原則的延伸:report 的責任是反映真實狀況,不是產生好看的結果。

同樣的原則也用在輸出上——Cpk 和 Ppk 一律並列。兩者接近代表製程穩定;Cpk 明顯大於 Ppk,代表機台本身能力沒問題,但存在批間漂移(刀具磨耗、換料、換班),該查的是製程管制而不是機台精度。只印一個數字,看不出這件事。

7. 還有一個決定:只讀,不寫

工具不會修改、搬動或刪除來源 CSV,連寫入的程式碼路徑都不存在。

這聽起來像廢話,但值得說明白:我們自己的 ERP 裡就有一段「把已處理的列從 CSV 砍掉、用剩餘列當處理進度」的設計。在那個情境下它是刻意的取捨——人工匯入與自動匯入需要共用同一個進度來源。但代價是,一旦砍行失敗,原始資料就回不來了

一支分析工具沒有理由承擔這種風險。這條規則有測試守著:讀完之後,來源檔的位元組必須與讀取前完全相同。

8. 成果

項目原本(Excel 手工)現在(工具)
單份報表耗時10–15 分鐘數秒
「沒量到」的處理當成 0 帶入計算排除,並列出排除筆數
使用的編碼不明(亂碼才發現)印在報表上
Cpk / Ppk通常只算一個並列,可看出批間漂移
單邊公差公式要另外改直接支援,Cp 標示為無定義
結果一致性依整理的人而異相同輸入必得相同輸出
可否進排程不行可,離開碼區分品質與程式問題

離開碼刻意分成三種:0 正常、1 有項目未達門檻或超規、2 執行錯誤。「品質不達標」和「程式出錯」是兩件事,分開才知道該找製程的人還是找寫程式的人。

$ cpk-report data.csv --spec spec.json

項目        n  排除        平均       σ組內     Cp    Cpk    Ppk  超規  判定
------------------------------------------------------------------------
全長       60     0     42.4993      0.0117   1.42   1.40   1.38     0  合格
刃厚       60     0      0.6004      0.0043   1.55   1.52   1.48     0  合格
平面度     58     2      0.0072      0.0046      —   0.94   1.06     0  不足

9. 為什麼開源

這支工具沒有任何商業機密——它不知道我們做什麼零件、不知道客戶是誰,規格與公差全部由使用者自己的設定檔提供。它解決的是所有做精密加工的工廠都會遇到的同一件事

對我們自己而言,開源的意義是把內部工具攤在陽光下:程式碼、測試、以及上面那些踩坑的理由都寫清楚了,任何人(包括來稽核我們的客戶)都可以自己判斷這套邏輯站不站得住腳。這比在簡報上寫「我們有數位化品管」有意義得多。

如果你也在處理量測資料,歡迎直接拿去改。有踩到不一樣的坑,開 issue 聊聊。

原始碼:github.com/jet113102/cpk-report(MIT 授權,Python 3.8+,無相依套件)