淺談統計製程管制(SPC)&小小DIY

Statistic Process Control (SPC)

因為工作是個製程仔,所以每天最常看到的就是這個叫SPC的東西…
SPC中文叫統計製程管制,當然叫SPC比較簡潔乾脆,主要用途是將要監控的參數標準化後,依照時間順序呈現在圖表上,當參數發生異常變動時,可由SPC chart觀察出來,
當工程師發現管制圖異常,就要趕緊介入查看參數的異常原因。(不小心就說出我的工作日常QQ)

管制圖?怎麼管?

SPC的歷史就不贅述了,餵狗第一條就有了~想說的是如果你要監控理應穩定的參數隨著時間的表現,就可以用SPC來查看這項參數的變動情況。就好比下面這張圖:

譬如今天我們要生產一支iphone,供應商需要製造它螢幕上的玻璃,那麼在生產的過程中,為了確保玻璃面板的厚度、長度或寬度的參數不會異常,都會對這些參數訂一個目標值(Target),想盡辦法將生產出來的貨品,其量測到的參數越接近target越好。將這些在生產中量測到的這些貨品參數對target值標準化後,以時間軸繪製出來,就能得到上圖。

而這張圖畫出來,就可以發現他之所以能「管」,便是建立在中央極限定理上。一道穩定的製程,在製造手續、製造機器都受到穩定控制的情況下,產品的參數便如同隨機變數,在經過收集多筆資料後,應當符合常態分布,上面講到的Target就是此道生產流程的目標值,理應同為數據的平均值。而SPC chart則是將這多筆的資料以時間這個維度做展開。如果我們將上圖的資料繪成組體圖表示的話,你會看到:

既然是數據組會呈現常態分佈,那麼必然會有機率出現資料點在平均值的三倍標準差之外,就像圖一的那些紅點所標示的。也因此,通常我們會以此製程的3倍標準差,當作管制圖的管制界線(Control limit)。當生產中量測到的參數超出Control limit,就會說這批貨的此項參數Out-Of-Control (OOC),而將此批貨的生產視為出現異常

動手做-產生一組模擬的SPC table

先別一次說太多SPC的東西,太無聊了(工作日常誰想知道XD)
來聊聊上面這些圖怎麼畫吧~畢竟別的沒有,用python畫圖我還算蠻精的(?!)
不過在畫圖之前,總得先有data吧?所以這篇先來講準備資料的部分。
我們根據SPC的理論,需要先產生一組以常態分配產生的亂數,最後搞到dataframe裡面,形成一組SPC 資料表。
所以先把python 資料科學的兩寶(pandas, numpy),還有其他起手式也import進來。
接著,我先把如何產生這張SPC table的一些參數,用 SPCDataConfig 這個class定義起來,之後可以讓其他控制項讀取這個config class來決定要做的事情。
這裡面,資料是由numpy的random.randn函數以 \( (\mu, \sigma)=(0, 1) \) 常態分布所產生的。而其他的一些基本input參數如下:

  • start_date_str, end_date_str: 文字格式的資料起始/最後時間,格式須遵守’YYYY/mm/dd HH:MM:SS.f’
  • ctype: SPC chart要監控的指標統計參數,本文以每筆資料均為該批貨物量測到的參數之平均值為例。
  • cnt: 資料總筆數
  • data_order: 資料的數量級。0.1, 0.01, 0.001, …依此類推
  • cl: Control limit值,user根據data_order做相應的輸入。
  • target: 指標的target,本例中為0。

同時,因為start_date_str, end_date_str的輸入格式,資料表中的時間格式也將遵循同樣格式,故在轉化為datetime型態的變數時,使用dt_fmt定義相同格式,作為格式引數。 最後,為了在開始~結束的時間內隨機佈下時間撮點,所以再將datetime格式的時間參數數值化。

import pandas as pd
import numpy as np
import os, sys, time, random
from datetime import datetime


class SPCDataConfig:
    def __init__(self, start_date_str, end_date_str, ctype, cnt, data_order, cl, target):
        self.start_date_str = start_date_str
        self.end_date_str = end_date_str
        self.ctype = ctype
        self.cnt = cnt
        self.data_order = data_order
        self.cl = cl
        self.target = target
        self.dt_fmt = '%Y/%m/%d %H:%M:%S.%f'
        self.start_date_dt = datetime.strptime(self.start_date_str, self.dt_fmt)
        self.end_date_dt = datetime.strptime(self.end_date_str, self.dt_fmt)
        self.start_date_num = int(self.start_date_dt.timestamp())
        self.end_date_num = int(self.end_date_dt.timestamp())

接著,我將create一系列的class來作為SPC table從無到有的各個元件,

資料時序隨機產生器

DataTimeGenerator中,我用前一節提到的起始/最後時間數值化的結果,產生指定數量的隨機時間數值,並依遞增排序。

class DataTimeGenerator:
    def __init__(self, cfg):
        self.cfg = cfg

    def gen(self):
        time_series_num = np.random.randint(self.cfg.start_date_num, self.cfg.end_date_num, self.cfg.cnt)
        time_series_num = np.sort(time_series_num)
        time_series = [datetime.fromtimestamp(x) for x in time_series_num]
        return time_series

指標數值隨機產生器

RawDataGenerator中,同樣以隨機值產生SPC所要監控的指標,並搭配data_order控制指標的數量級。

class RawDataGenerator:
    def __init__(self, cfg):
        self.cfg = cfg

    def gen(self):
        values = np.random.randn(self.cfg.cnt) * self.cfg.data_order
        return values

Control Limit / Target 產生器

這兩個class,簡單地以np.ones()函數,產生指定control limit 或target的一維陣列。

class ControlLimitGenerator:
    def __init__(self, cfg):
        self.cfg = cfg

    def gen(self):
        CL = self.cfg.cl
        UCL = np.ones(self.cfg.cnt) * CL
        LCL = np.ones(self.cfg.cnt) * -CL
        return UCL, LCL

class TargetGenerator:
    def __init__(self, cfg):
        self.cfg = cfg

    def gen(self):
        TARGET = np.ones(self.cfg.cnt) * self.cfg.target
        return TARGET

貨品編號產生器

構想是想讓SPC chart裡每一批接受監控的貨物,各自有一個名稱,也就是貨號。一批貨中,通常含有數量不一的產品,通常基於產能考量,只會抽檢該批貨中的其中一片去監控製程穩定度。所以這邊LotIdGenerator會產生每一個時間點,受量測的貨物貨號,與產品編號。至於編號格式嘛… 而這批貨底下可能也會有為數不同的產品,所以再以貨號為基底,小數點後兩碼為產品編號:

class LotIdGenerator:
    def __init__(self, cfg):
        self.cfg = cfg

    def gen(self):
        LETTER = ['W', 'X', 'Y', 'Z']
        lotid = ['A21{0:s}{0:s}{1:03d}'.format(LETTER[random.randint(0, 3)], random.randint(1, 999)) for x in range(self.cfg.cnt)]
        itemid = ['{0:02d}'.format(random.randint(1, 25)) for x in range(self.cfg.cnt)]
        lot_info = pd.DataFrame({
            'Lot': lotid, 'Item2': itemid
        }, columns=['Lot', 'Item2'])
        lot_info["Lot_ID"] = lot_info['Lot']
        lot_info["Item_ID"] = lot_info['Lot'].str.cat(lot_info['Item2'].values, sep='.')
        return lot_info

結合所有產生器

定義完所有產生器,最後再用一個整合的產生器,去驅動各個產生器產生數列,並組合成dataframe輸出。
接著就可以定義好你想要的SPC data參數後,匯入Config物件, 再call SPCDataGenerator來產生SPC table

class SPCDataGenerator:
    def __init__(self, cfg):
        self.cfg = cfg

    def gen(self):
        TS = DataTimeGenerator(self.cfg)
        RD = RawDataGenerator(self.cfg)
        CL = ControlLimitGenerator(self.cfg)
        TG = TargetGenerator(self.cfg)
        LI = LotIdGenerator(self.cfg)

        time_series = TS.gen()
        values = RD.gen()
        UCL, LCL = CL.gen()
        target = TG.gen()
        lot_info = LI.gen()

        self.df = pd.DataFrame({
            'Data_Time': time_series,
            'Lot_ID': lot_info['Lot_ID'].values,
            'Item_ID': lot_info['Item_ID'].values,
            'Value': values,
            'Target': target,
            'UCL': UCL,
            'LCL': LCL,
        })
        self.df = self.df[['Data_Time', 'Lot_ID', 'Item_ID', 'Value', 'Target', 'UCL', 'LCL']]

        return self.df

# define configurations:
data_cnt = 100
chart_type = 'MEAN'

spc_cfg = SPCDataConfig('2020/11/17 00:00:00.0', '2020/12/18 00:00:00.0', ctype=chart_type, cnt=data_cnt, data_order=0.1, cl=0.3, target=0)
SPC = SPCDataGenerator(spc_cfg)
spc_df = SPC.gen()

於是可以得到下方的輸出結果~

要把pandas 的dataframe給格式化print出來,可以使用tabulate套件

from tabulate import tabulate
print(tabulate(spc_df.head(), headers='keys', tablefmt='psql'))
+----+---------+---------------------+----------+------------+-------------+----------+-------+-------+-------+-------+
|    |   index | Data_Time           | Lot_ID   | Item_ID    |       Value |   Target |   USL |   UCL |   LCL |   LSL |
|----+---------+---------------------+----------+------------+-------------+----------+-------+-------+-------+-------|
|  0 |       0 | 2020-11-17 07:24:41 | A21W641  | A21W641.10 |  0.0935012  |        0 |   0.3 |   0.2 |  -0.2 |  -0.3 |
|  1 |       1 | 2020-11-17 07:52:37 | A21Z267  | A21Z267.06 | -0.140788   |        0 |   0.3 |   0.2 |  -0.2 |  -0.3 |
|  2 |       2 | 2020-11-17 10:33:47 | A21W999  | A21W999.06 | -0.00277936 |        0 |   0.3 |   0.2 |  -0.2 |  -0.3 |
|  3 |       3 | 2020-11-17 15:18:14 | A21Z412  | A21Z412.05 | -0.196211   |        0 |   0.3 |   0.2 |  -0.2 |  -0.3 |
|  4 |       4 | 2020-11-17 18:54:46 | A21W311  | A21W311.16 |  0.116503   |        0 |   0.3 |   0.2 |  -0.2 |  -0.3 |
+----+---------+---------------------+----------+------------+-------------+----------+-------+-------+-------+-------+

產生資料的部分就到這裡,畫圖的部份富堅到下次吧!

如果這篇文章對你有所幫助,就幫按個讚吧!

留言