packages feed

hanalyze-0.2.0.0: src/Hanalyze/Stat/SPC.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BangPatterns #-}
-- |
-- Module      : Hanalyze.Stat.SPC
-- Description : 統計的工程管理 (SPC) — 管理図 (X̄-R/I-MR/p/np/c/u) + 判定ルール
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- 統計的工程管理 (Statistical Process Control) — 管理図 + 判定ルール。
--
-- 変数管理図 (X̄-R / I-MR) と属性管理図 (p / np / c / u) を共通 API で扱う。
-- 判定ルール (Western Electric / Nelson) は fit と分離した pure 関数。
--
-- ===  公開 API
--
-- * 'SPCChart' / 'SPCInput' / 'SPCChartResult'
-- * 'fitSPC'
-- * 'westernElectricRules' / 'nelsonRules' / 'checkRules'
--
-- ===  典型的な使い方
--
-- > case fitSPC XR (VarSubgroups subs) of
-- >   Left err -> ...
-- >   Right [xbar, rChart] -> do
-- >     let viols = checkRules westernElectricRules xbar
-- >     ...
module Hanalyze.Stat.SPC
  ( -- * chart 種別
    SPCChart (..)
  , SPCInput  (..)
  , SPCChartResult (..)
    -- * fit
  , fitSPC
    -- * 判定ルール
  , SPCRule (..)
  , SPCViolation (..)
  , westernElectricRules
  , nelsonRules
  , checkRules
  ) where

import qualified Data.Text     as T
import qualified Data.Vector   as V
import           Data.Text     (Text)
import           Data.Vector   (Vector)

-- ===========================================================================
-- 型定義
-- ===========================================================================

-- | 管理図の種別。
data SPCChart
  = XR    -- ^ X̄-R chart (subgroup 平均 + range)
  | IMR   -- ^ I-MR chart (individual + moving range)
  | P     -- ^ p chart (不良率、 subgroup size 可変)
  | NP    -- ^ np chart (不良数、 subgroup size 一定)
  | C     -- ^ c chart (単位あたり欠陥数、 unit size 一定)
  | U     -- ^ u chart (単位あたり欠陥率、 unit size 可変)
  | EWMAChart    -- ^ EWMA (Exponentially Weighted Moving Average) chart (Phase 11)
  | CUSUMChart   -- ^ CUSUM (Cumulative Sum) chart 両側 (Phase 11)
  deriving (Show, Eq)

-- | 管理図入力。 chart 種別に対応した構成のみ受け付ける。
data SPCInput
  = -- | 変数管理図 (X̄-R) 用。 各 subgroup の観測値ベクトル。
    --   subgroup サイズ (内側 Vector の長さ) は全 subgroup で同一であること。
    VarSubgroups   !(Vector (Vector Double))
  | -- | I-MR 用。 個別観測値の系列。
    VarIndividual  !(Vector Double)
  | -- | p chart 用。 (不良数, sample size) の系列。
    AttrProportion !(Vector Int) !(Vector Int)
  | -- | np chart 用。 (不良数の系列, 一定 sample size)。
    AttrCount      !(Vector Int) !Int
  | -- | c chart 用。 欠陥数の系列 (unit size は一定と仮定)。
    AttrDefects    !(Vector Int)
  | -- | u chart 用。 (欠陥数, unit size) の系列。
    AttrDefectRate !(Vector Int) !(Vector Int)
  | -- | EWMA 用。 (個別観測値 xs, λ ∈ (0,1], L (sigma 倍数), μ₀ target, σ₀ baseline σ)。
    --   σ₀ ≤ 0 を渡すと xs の標本標準偏差で代用。
    EWMAInput      !(Vector Double) !Double !Double !Double !Double
  | -- | CUSUM 用。 (個別観測値 xs, μ₀ target, σ₀ baseline σ, k (allowance, σ単位), h (decision interval, σ単位))。
    --   σ₀ ≤ 0 を渡すと xs の標本標準偏差で代用。 両側 CUSUM (C+, C-) を返す。
    CUSUMInput     !(Vector Double) !Double !Double !Double !Double
  deriving (Show, Eq)

-- | 1 つの管理図の fit 結果。 X̄-R / I-MR では 2 つ並んで返る。
--
-- 不変条件:
--
--   * @V.length spcPoints == V.length spcUCL == V.length spcLCL@
--   * 固定 limit chart (X̄-R / I-MR / np / c) では UCL/LCL は全要素同値
--   * 変動 limit chart (p / u) では UCL/LCL が点ごとに異なる
data SPCChartResult = SPCChartResult
  { spcPoints    :: !(Vector Double)
    -- ^ 点ごとにプロットする統計量 (X̄、 R、 個別値、 MR、 p̂、 np、 c、 u 等)
  , spcCenter    :: !Double
    -- ^ 中心線 (CL)
  , spcUCL       :: !(Vector Double)
    -- ^ 上方管理限界 (点ごと)
  , spcLCL       :: !(Vector Double)
    -- ^ 下方管理限界 (点ごと)
  , spcSigma     :: !Double
    -- ^ 推定 σ (rule 判定用、 zone A/B/C の境界を計算するのに使う)
  , spcChartName :: !Text
    -- ^ "X-bar" / "R" / "I" / "MR" / "p" / "np" / "c" / "u"
  } deriving (Show)

-- ===========================================================================
-- Montgomery 定数 (n = 2..15)
-- ===========================================================================

-- | 出典: Montgomery, "Introduction to Statistical Quality Control" 9th ed.
--   Appendix VI。 @(A2, D3, D4, d2)@。
--   subgroup size 範囲外の @n@ では 'Nothing'。
subgroupConst :: Int -> Maybe (Double, Double, Double, Double)
subgroupConst n = case n of
  2  -> Just (1.880, 0.000, 3.267, 1.128)
  3  -> Just (1.023, 0.000, 2.574, 1.693)
  4  -> Just (0.729, 0.000, 2.282, 2.059)
  5  -> Just (0.577, 0.000, 2.115, 2.326)
  6  -> Just (0.483, 0.000, 2.004, 2.534)
  7  -> Just (0.419, 0.076, 1.924, 2.704)
  8  -> Just (0.373, 0.136, 1.864, 2.847)
  9  -> Just (0.337, 0.184, 1.816, 2.970)
  10 -> Just (0.308, 0.223, 1.777, 3.078)
  11 -> Just (0.285, 0.256, 1.744, 3.173)
  12 -> Just (0.266, 0.283, 1.717, 3.258)
  13 -> Just (0.249, 0.307, 1.693, 3.336)
  14 -> Just (0.235, 0.328, 1.672, 3.407)
  15 -> Just (0.223, 0.347, 1.653, 3.472)
  _  -> Nothing

-- ===========================================================================
-- 内部ヘルパ
-- ===========================================================================

vmean :: Vector Double -> Double
vmean v
  | V.null v  = 0
  | otherwise = V.sum v / fromIntegral (V.length v)

vrange :: Vector Double -> Double
vrange v
  | V.null v  = 0
  | otherwise = V.maximum v - V.minimum v

-- | 単一値で埋めた長さ @n@ の Vector。
vconst :: Int -> Double -> Vector Double
vconst n x = V.replicate n x

tshow :: Show a => a -> Text
tshow = T.pack . show

chartTag :: SPCChart -> Text
chartTag XR  = "XR"
chartTag IMR = "IMR"
chartTag P   = "P"
chartTag NP  = "NP"
chartTag C   = "C"
chartTag U   = "U"
chartTag EWMAChart  = "EWMA"
chartTag CUSUMChart = "CUSUM"

inputTag :: SPCInput -> Text
inputTag VarSubgroups{}   = "VarSubgroups"
inputTag VarIndividual{}  = "VarIndividual"
inputTag AttrProportion{} = "AttrProportion"
inputTag AttrCount{}      = "AttrCount"
inputTag AttrDefects{}    = "AttrDefects"
inputTag AttrDefectRate{} = "AttrDefectRate"
inputTag EWMAInput{}      = "EWMAInput"
inputTag CUSUMInput{}     = "CUSUMInput"

-- ===========================================================================
-- 公開関数
-- ===========================================================================

-- | 管理図を fit する。 X̄-R / I-MR は 2 chart を返す
-- (順に X̄ chart / R chart、 I chart / MR chart)。
-- chart 種別と入力の組合せが不正な場合 'Left' を返す。
fitSPC :: SPCChart -> SPCInput -> Either Text [SPCChartResult]
fitSPC XR  (VarSubgroups subs)    = fitXR subs
fitSPC IMR (VarIndividual xs)     = fitIMR xs
fitSPC P   (AttrProportion ds ns) = fitP  ds ns
fitSPC NP  (AttrCount ds n)       = fitNP ds n
fitSPC C   (AttrDefects ds)       = fitC  ds
fitSPC U   (AttrDefectRate ds ns) = fitU  ds ns
fitSPC EWMAChart  (EWMAInput xs lam ll mu0 s0)        = fitEWMA xs lam ll mu0 s0
fitSPC CUSUMChart (CUSUMInput xs mu0 s0 k h)          = fitCUSUM xs mu0 s0 k h
fitSPC chart inp =
  Left $ "Hanalyze.Stat.SPC.fitSPC: chart kind "
       <> chartTag chart
       <> " does not match input "
       <> inputTag inp

-- ---------------------------------------------------------------------------
-- X̄-R chart
-- ---------------------------------------------------------------------------

-- | X̄-R chart:
--
--   * X̄ chart: CL = X̿、 UCL = X̿ + A2·R̄、 LCL = X̿ − A2·R̄、 σ̂ = R̄ / d2
--   * R chart: CL = R̄、 UCL = D4·R̄、 LCL = D3·R̄
fitXR :: Vector (Vector Double) -> Either Text [SPCChartResult]
fitXR subs
  | V.null subs = Left "fitSPC XR: empty subgroup list"
  | otherwise =
      let !n  = V.length (V.head subs)
          !k  = V.length subs
          sizesOk = V.all (\s -> V.length s == n) subs
      in if not sizesOk
           then Left "fitSPC XR: subgroup sizes are not uniform"
           else case subgroupConst n of
             Nothing -> Left $ "fitSPC XR: subgroup size n=" <> tshow n
                            <> " is outside supported range (2..15)"
             Just (a2, d3, d4, d2c) ->
               let means   = V.map vmean  subs
                   ranges  = V.map vrange subs
                   xBarBar = vmean means
                   rBar    = vmean ranges
                   sigma   = rBar / d2c
                   uclX    = xBarBar + a2 * rBar
                   lclX    = xBarBar - a2 * rBar
                   uclR    = d4 * rBar
                   lclR    = d3 * rBar
                   xChart  = SPCChartResult
                     { spcPoints    = means
                     , spcCenter    = xBarBar
                     , spcUCL       = vconst k uclX
                     , spcLCL       = vconst k lclX
                     , spcSigma     = sigma
                     , spcChartName = "X-bar"
                     }
                   rChart  = SPCChartResult
                     { spcPoints    = ranges
                     , spcCenter    = rBar
                     , spcUCL       = vconst k uclR
                     , spcLCL       = vconst k lclR
                     , spcSigma     = sigma
                     , spcChartName = "R"
                     }
               in Right [xChart, rChart]

-- ---------------------------------------------------------------------------
-- I-MR chart
-- ---------------------------------------------------------------------------

-- | I-MR chart:
--
--   * MR_i = |x_i − x_{i−1}|  for i = 1..N−1
--   * I chart:  CL = x̄、 σ̂ = MR̄ / d2(n=2) = MR̄ / 1.128、 UCL/LCL = x̄ ± 3σ̂
--   * MR chart: CL = MR̄、 UCL = D4(2)·MR̄ = 3.267·MR̄、 LCL = D3(2)·MR̄ = 0
fitIMR :: Vector Double -> Either Text [SPCChartResult]
fitIMR xs
  | V.length xs < 2 = Left "fitSPC IMR: need at least 2 individual observations"
  | otherwise =
      let !n      = V.length xs
          xBar    = vmean xs
          mr      = V.generate (n - 1) (\i -> abs (xs V.! (i + 1) - xs V.! i))
          mrBar   = vmean mr
          (_, d3, d4, d2c) = case subgroupConst 2 of
            Just t  -> t
            Nothing -> (0, 0, 0, 1.128)  -- 到達不能
          sigma   = mrBar / d2c
          uclI    = xBar + 3 * sigma
          lclI    = xBar - 3 * sigma
          uclMR   = d4 * mrBar
          lclMR   = d3 * mrBar
          iChart  = SPCChartResult
            { spcPoints    = xs
            , spcCenter    = xBar
            , spcUCL       = vconst n uclI
            , spcLCL       = vconst n lclI
            , spcSigma     = sigma
            , spcChartName = "I"
            }
          mrChart = SPCChartResult
            { spcPoints    = mr
            , spcCenter    = mrBar
            , spcUCL       = vconst (n - 1) uclMR
            , spcLCL       = vconst (n - 1) lclMR
            , spcSigma     = sigma
            , spcChartName = "MR"
            }
      in Right [iChart, mrChart]

-- ---------------------------------------------------------------------------
-- p chart (proportion defective, variable subgroup size)
-- ---------------------------------------------------------------------------

-- | p chart:
--
--   * p̂_i = d_i / n_i
--   * p̄   = Σ d_i / Σ n_i
--   * CL  = p̄
--   * UCL_i = p̄ + 3·sqrt(p̄(1−p̄)/n_i)、 LCL_i = max(0, …)
--
-- σ̂ は **平均 n** に基づく代表値 (rule 判定用)。
fitP :: Vector Int -> Vector Int -> Either Text [SPCChartResult]
fitP ds ns
  | V.length ds /= V.length ns
      = Left "fitSPC P: defectives and sample-size series differ in length"
  | V.null ds = Left "fitSPC P: empty series"
  | V.any (< 0) ds = Left "fitSPC P: defectives must be non-negative"
  | V.any (<= 0) ns = Left "fitSPC P: sample sizes must be positive"
  | V.or (V.zipWith (>) ds ns) = Left "fitSPC P: defectives exceed sample size"
  | otherwise =
      let k       = V.length ds
          totalD  = sum (V.toList ds) :: Int
          totalN  = sum (V.toList ns) :: Int
          pBar    = fromIntegral totalD / fromIntegral totalN
          phat    = V.zipWith (\d n -> fromIntegral d / fromIntegral n) ds ns
          ucl     = V.map (\ni -> pBar + 3 * sqrt (pBar * (1 - pBar) /
                                                   fromIntegral ni)) ns
          lcl     = V.map (\ni -> max 0 $ pBar - 3 * sqrt (pBar * (1 - pBar) /
                                                           fromIntegral ni)) ns
          nMean   = fromIntegral totalN / fromIntegral k :: Double
          sigma   = sqrt (pBar * (1 - pBar) / nMean)
      in Right [SPCChartResult
        { spcPoints    = phat
        , spcCenter    = pBar
        , spcUCL       = ucl
        , spcLCL       = lcl
        , spcSigma     = sigma
        , spcChartName = "p"
        }]

-- ---------------------------------------------------------------------------
-- np chart (count defective, constant subgroup size n)
-- ---------------------------------------------------------------------------

-- | np chart (n は全 subgroup で一定):
--
--   * CL  = n·p̄ = 平均不良数
--   * σ̂  = sqrt(n·p̄·(1−p̄))
--   * UCL = n·p̄ + 3·σ̂、 LCL = max(0, …)
fitNP :: Vector Int -> Int -> Either Text [SPCChartResult]
fitNP ds n
  | V.null ds        = Left "fitSPC NP: empty defectives series"
  | n <= 0           = Left "fitSPC NP: sample size n must be positive"
  | V.any (< 0) ds   = Left "fitSPC NP: defectives must be non-negative"
  | V.any (> n) ds   = Left "fitSPC NP: defectives exceed sample size"
  | otherwise =
      let k       = V.length ds
          totalD  = sum (V.toList ds) :: Int
          pBar    = fromIntegral totalD / fromIntegral (n * k) :: Double
          cl      = fromIntegral n * pBar
          sigma   = sqrt (fromIntegral n * pBar * (1 - pBar))
          ucl     = cl + 3 * sigma
          lcl     = max 0 (cl - 3 * sigma)
          pts     = V.map fromIntegral ds :: Vector Double
      in Right [SPCChartResult
        { spcPoints    = pts
        , spcCenter    = cl
        , spcUCL       = vconst k ucl
        , spcLCL       = vconst k lcl
        , spcSigma     = sigma
        , spcChartName = "np"
        }]

-- ---------------------------------------------------------------------------
-- c chart (count of defects, constant unit size)
-- ---------------------------------------------------------------------------

-- | c chart:
--
--   * CL  = c̄ = 平均欠陥数
--   * σ̂  = sqrt(c̄)
--   * UCL = c̄ + 3·sqrt(c̄)、 LCL = max(0, …)
fitC :: Vector Int -> Either Text [SPCChartResult]
fitC ds
  | V.null ds         = Left "fitSPC C: empty defects series"
  | V.any (< 0) ds    = Left "fitSPC C: defects must be non-negative"
  | otherwise =
      let k       = V.length ds
          cBar    = fromIntegral (sum (V.toList ds)) / fromIntegral k :: Double
          sigma   = sqrt cBar
          ucl     = cBar + 3 * sigma
          lcl     = max 0 (cBar - 3 * sigma)
          pts     = V.map fromIntegral ds :: Vector Double
      in Right [SPCChartResult
        { spcPoints    = pts
        , spcCenter    = cBar
        , spcUCL       = vconst k ucl
        , spcLCL       = vconst k lcl
        , spcSigma     = sigma
        , spcChartName = "c"
        }]

-- ---------------------------------------------------------------------------
-- u chart (defect rate, variable unit size)
-- ---------------------------------------------------------------------------

-- | u chart:
--
--   * u_i = d_i / n_i
--   * ū   = Σ d_i / Σ n_i
--   * CL  = ū
--   * UCL_i = ū + 3·sqrt(ū/n_i)、 LCL_i = max(0, …)
fitU :: Vector Int -> Vector Int -> Either Text [SPCChartResult]
fitU ds ns
  | V.length ds /= V.length ns
      = Left "fitSPC U: defects and unit-size series differ in length"
  | V.null ds       = Left "fitSPC U: empty series"
  | V.any (< 0) ds  = Left "fitSPC U: defects must be non-negative"
  | V.any (<= 0) ns = Left "fitSPC U: unit sizes must be positive"
  | otherwise =
      let k       = V.length ds
          totalD  = fromIntegral (sum (V.toList ds)) :: Double
          totalN  = fromIntegral (sum (V.toList ns)) :: Double
          uBar    = totalD / totalN
          us      = V.zipWith (\d n -> fromIntegral d / fromIntegral n) ds ns
          ucl     = V.map (\ni -> uBar + 3 * sqrt (uBar / fromIntegral ni)) ns
          lcl     = V.map (\ni -> max 0 (uBar - 3 * sqrt (uBar / fromIntegral ni))) ns
          nMean   = totalN / fromIntegral k
          sigma   = sqrt (uBar / nMean)
      in Right [SPCChartResult
        { spcPoints    = us
        , spcCenter    = uBar
        , spcUCL       = ucl
        , spcLCL       = lcl
        , spcSigma     = sigma
        , spcChartName = "u"
        }]

-- ===========================================================================
-- 判定ルール (Phase 1.4 / 1.5 で実装)
-- ===========================================================================

-- | 判定ルール 1 個。
data SPCRule = SPCRule
  { ruleName   :: !Text                       -- ^ "Western Electric 1" / "Nelson 1" 等
  , ruleNumber :: !Int                        -- ^ ルール番号 (1..8)
  , ruleCheck  :: SPCChartResult -> [Int]     -- ^ 違反点の 0-origin index list
  }

-- | ルール違反 1 件。
data SPCViolation = SPCViolation
  { vRuleName    :: !Text
  , vRuleNumber  :: !Int
  , vPointIndex  :: !Int
  , vChartName   :: !Text   -- ^ どの chart で違反したか (X-bar / R / 等)
  } deriving (Show, Eq)

-- ---------------------------------------------------------------------------
-- 内部パターン検出 (rule 共通)
-- ---------------------------------------------------------------------------

-- $patternDetectors
-- ゾーン境界は CL ± k·σ で定義 (σ は 'spcSigma' フィールド)。
-- 可変 limit chart (p / u) では σ は代表値 (平均 n から算出) なので、
-- ゾーン判定はやや近似となる (canvas display 用途では実用上問題なし)。

-- | k·σ の絶対値を超えた点の index (0-origin)。 chart 種別非依存。
beyondSigma :: Double -> SPCChartResult -> [Int]
beyondSigma k r =
  let cl    = spcCenter r
      sigma = spcSigma r
      pts   = V.toList (spcPoints r)
  in [ i | (i, x) <- zip [0..] pts
         , abs (x - cl) > k * sigma ]

-- | k·σ を超える点について「+ なら +1、 − なら −1、 ゾーン内なら 0」。
sideAtSigma :: Double -> SPCChartResult -> [Int]
sideAtSigma k r =
  let cl    = spcCenter r
      sigma = spcSigma r
      pts   = V.toList (spcPoints r)
      classify x
        | x - cl >  k * sigma =  1
        | x - cl < -k * sigma = -1
        | otherwise           =  0
  in map classify pts

-- | CL に対する符号 (上 = +1, 下 = -1, 上 = 0)。
sideOfCenter :: SPCChartResult -> [Int]
sideOfCenter r =
  let cl    = spcCenter r
      pts   = V.toList (spcPoints r)
      classify x
        | x >  cl =  1
        | x <  cl = -1
        | otherwise = 0
  in map classify pts

-- | N 個連続で同符号 (CL の同じ側) になっている末尾点の index を返す。
--   例: 8 連続 → 連続区間の 8 点目以降を全部 violation として返す。
runSameSide :: Int -> SPCChartResult -> [Int]
runSameSide n r = go 0 0 0 (sideOfCenter r) []
  where
    go !i !curSide !runLen ss acc = case ss of
      []     -> reverse acc
      (s:xs) ->
        let (curSide', runLen')
              | s == 0           = (0, 0)
              | s == curSide     = (curSide, runLen + 1)
              | otherwise        = (s, 1)
            acc' | runLen' >= n = i : acc
                 | otherwise    = acc
        in go (i + 1) curSide' runLen' xs acc'

-- | N 個連続で単調 (全て上昇 or 全て下降) のパターンの末尾 index。
trendMono :: Int -> SPCChartResult -> [Int]
trendMono n r = go 0 0 0 (V.toList (spcPoints r)) []
  where
    -- direction: +1 = increasing, -1 = decreasing, 0 = none yet
    go _ _ _ [] acc = reverse acc
    go _ _ _ [_] acc = reverse acc
    go !i !dir !runLen (x : ys@(y : _)) acc =
      let d | y > x =  1
            | y < x = -1
            | otherwise = 0
          (dir', runLen')
            | d == 0      = (0, 0)
            | d == dir    = (dir, runLen + 1)
            | otherwise   = (d, 2)   -- 始まり: 2 点で run=2
          -- 違反 = runLen が n 以上、 i+1 (現在の y) の index を記録
          acc' | runLen' >= n = (i + 1) : acc
               | otherwise    = acc
      in go (i + 1) dir' runLen' ys acc'

-- | N 個連続で交互上下のパターンの末尾 index。
alternating :: Int -> SPCChartResult -> [Int]
alternating n r = go 0 0 0 (V.toList (spcPoints r)) []
  where
    go _ _ _ [] acc = reverse acc
    go _ _ _ [_] acc = reverse acc
    go !i !lastDir !runLen (x : ys@(y : _)) acc =
      let d | y > x =  1
            | y < x = -1
            | otherwise = 0
          (lastDir', runLen')
            | d == 0                       = (0, 0)
            | lastDir == 0                 = (d, 2)
            | d == negate lastDir          = (d, runLen + 1)
            | otherwise                    = (d, 2)
          acc' | runLen' >= n = (i + 1) : acc
               | otherwise    = acc
      in go (i + 1) lastDir' runLen' ys acc'

-- | k 個連続で σ 倍の絶対値以内 (= ゾーン C 内のみ) の末尾 index。
--   stratification (W-E rule 6 / Nelson 7)。
withinSigma :: Int -> Double -> SPCChartResult -> [Int]
withinSigma n k r =
  let cl    = spcCenter r
      sigma = spcSigma r
      pts   = V.toList (spcPoints r)
      flags = map (\x -> abs (x - cl) <= k * sigma) pts
  in collectRun n flags

-- | k 個連続で σ 倍の絶対値より外 (= ゾーン A or B、 中央線の同/異側問わず) の末尾 index。
--   mixture (W-E rule 7 / Nelson 8)。
beyondSigmaEither :: Int -> Double -> SPCChartResult -> [Int]
beyondSigmaEither n k r =
  let cl    = spcCenter r
      sigma = spcSigma r
      pts   = V.toList (spcPoints r)
      flags = map (\x -> abs (x - cl) > k * sigma) pts
  in collectRun n flags

-- | True が n 個以上連続するパターンの末尾 index 集合。
collectRun :: Int -> [Bool] -> [Int]
collectRun n = go 0 0 []
  where
    go _ _ acc [] = reverse acc
    go !i !rn acc (f : fs) =
      let rn'  = if f then rn + 1 else 0
          acc' | rn' >= n = i : acc
               | otherwise = acc
      in go (i + 1) rn' acc' fs

-- | 「直近 m 点のうち k 点以上が k·σ を **同じ側** で超えている」 末尾 index。
--   Western Electric 2 / 3 用 (m, k, σ係数)。
kOfMBeyondSameSide :: Int -> Int -> Double -> SPCChartResult -> [Int]
kOfMBeyondSameSide kth m sigK r = go 0 (sideAtSigma sigK r) []
  where
    go _ ss acc | length ss < m = reverse acc
    go !i ss acc =
      let window = take m ss
          posCount = length (filter (==  1) window)
          negCount = length (filter (== -1) window)
          hit      = posCount >= kth || negCount >= kth
          -- 違反 index は window の末尾 (= i + m - 1)
          acc' | hit       = (i + m - 1) : acc
               | otherwise = acc
      in case ss of
           []     -> reverse acc'
           (_:xs) -> go (i + 1) xs acc'

-- ---------------------------------------------------------------------------
-- Western Electric rules (WECO 8 rules)
-- ---------------------------------------------------------------------------

-- | Western Electric Company (WECO) rules。 8 rules。
--
-- (Western Electric Statistical Quality Control Handbook 1956 +
-- 一般的な 8-rule 拡張)
--
--   * Rule 1: 1 点が 3σ 超
--   * Rule 2: 3 点中 2 点が同じ側で 2σ 超
--   * Rule 3: 5 点中 4 点が同じ側で 1σ 超
--   * Rule 4: 8 点連続で CL の同じ側
--   * Rule 5: 6 点連続で単調 (上昇 or 下降)
--   * Rule 6: 15 点連続で 1σ 以内 (stratification)
--   * Rule 7: 8 点連続で 1σ 外 (mixture; どちら側でも可)
--   * Rule 8: 14 点連続で交互上下
westernElectricRules :: [SPCRule]
westernElectricRules =
  [ SPCRule "Western Electric 1" 1 (beyondSigma 3)
  , SPCRule "Western Electric 2" 2 (kOfMBeyondSameSide 2 3 2)
  , SPCRule "Western Electric 3" 3 (kOfMBeyondSameSide 4 5 1)
  , SPCRule "Western Electric 4" 4 (runSameSide 8)
  , SPCRule "Western Electric 5" 5 (trendMono 6)
  , SPCRule "Western Electric 6" 6 (withinSigma 15 1)
  , SPCRule "Western Electric 7" 7 (beyondSigmaEither 8 1)
  , SPCRule "Western Electric 8" 8 (alternating 14)
  ]

-- ---------------------------------------------------------------------------
-- Nelson rules (1984、 8 rules)
-- ---------------------------------------------------------------------------

-- | Nelson rules (Nelson, L.S. 1984, J. Qual. Tech.)。 8 rules。
--
-- WE 8 rules と多くが重複するが、 ルール番号と一部の N が異なる:
--
--   * Rule 1: 1 点が 3σ 超                                   (= WE 1)
--   * Rule 2: 9 点連続で CL の同じ側                          (WE 4 は 8 点)
--   * Rule 3: 6 点連続で単調                                  (= WE 5)
--   * Rule 4: 14 点連続で交互上下                              (= WE 8)
--   * Rule 5: 3 点中 2 点が同じ側で 2σ 超                      (= WE 2)
--   * Rule 6: 5 点中 4 点が同じ側で 1σ 超                      (= WE 3)
--   * Rule 7: 15 点連続で 1σ 以内                              (= WE 6)
--   * Rule 8: 8 点連続で 1σ 外 (どちら側でも可)                (= WE 7)
--
-- 検出ロジックは [[westernElectricRules]] と同じヘルパを再利用。
nelsonRules :: [SPCRule]
nelsonRules =
  [ SPCRule "Nelson 1" 1 (beyondSigma 3)
  , SPCRule "Nelson 2" 2 (runSameSide 9)
  , SPCRule "Nelson 3" 3 (trendMono 6)
  , SPCRule "Nelson 4" 4 (alternating 14)
  , SPCRule "Nelson 5" 5 (kOfMBeyondSameSide 2 3 2)
  , SPCRule "Nelson 6" 6 (kOfMBeyondSameSide 4 5 1)
  , SPCRule "Nelson 7" 7 (withinSigma 15 1)
  , SPCRule "Nelson 8" 8 (beyondSigmaEither 8 1)
  ]

-- | 指定したルール集合で違反点を検出する。
checkRules :: [SPCRule] -> SPCChartResult -> [SPCViolation]
checkRules rs r =
  [ SPCViolation (ruleName ru) (ruleNumber ru) i (spcChartName r)
  | ru <- rs
  , i  <- ruleCheck ru r
  ]

-- ---------------------------------------------------------------------------
-- EWMA chart (Phase 11)
-- ---------------------------------------------------------------------------

-- | EWMA chart:
--
--   * 再帰: @z_i = λ x_i + (1 − λ) z_{i−1}@, @z_0 = μ₀@
--   * 時変管理限界: @μ₀ ± L σ √(λ/(2−λ) · (1 − (1−λ)^{2i}))@
--   * σ₀ ≤ 0 のとき xs の標本標準偏差で代用。
--
-- 入力検証: 0 < λ ≤ 1, L > 0, |xs| ≥ 1。
fitEWMA :: Vector Double -> Double -> Double -> Double -> Double
        -> Either Text [SPCChartResult]
fitEWMA xs lam ll mu0 s0In
  | V.null xs                = Left "fitSPC EWMA: empty input"
  | not (lam > 0 && lam <= 1) = Left "fitSPC EWMA: λ must be in (0, 1]"
  | ll <= 0                  = Left "fitSPC EWMA: L must be > 0"
  | otherwise =
      let !n     = V.length xs
          !sigma = if s0In > 0 then s0In else sampleSD xs
          zs     = V.scanl' (\z x -> lam * x + (1 - lam) * z) mu0 xs
          -- scanl' includes initial → drop the seed
          zsTail = V.tail zs
          ucl = V.generate n (\i ->
            let i1 = fromIntegral (i + 1) :: Double
                factor = lam / (2 - lam) * (1 - (1 - lam) ** (2 * i1))
            in mu0 + ll * sigma * sqrt factor)
          lcl = V.generate n (\i ->
            let i1 = fromIntegral (i + 1) :: Double
                factor = lam / (2 - lam) * (1 - (1 - lam) ** (2 * i1))
            in mu0 - ll * sigma * sqrt factor)
      in Right [ SPCChartResult
                   { spcPoints    = zsTail
                   , spcCenter    = mu0
                   , spcUCL       = ucl
                   , spcLCL       = lcl
                   , spcSigma     = sigma
                   , spcChartName = "EWMA"
                   } ]

-- ---------------------------------------------------------------------------
-- CUSUM chart (Phase 11)
-- ---------------------------------------------------------------------------

-- | CUSUM (両側) chart:
--
--   * @C⁺_i = max(0, x_i − (μ₀ + k σ) + C⁺_{i−1})@,  @C⁺_0 = 0@
--   * @C⁻_i = max(0, (μ₀ − k σ) − x_i + C⁻_{i−1})@,  @C⁻_0 = 0@
--   * 決定限界: @H = h σ@  (上側のみ、 下側は @−H@ として描画用に @-1 × C⁻@ を返す)
--
-- 返り値: [C⁺ chart, C⁻ chart]。 C⁻ chart は points が負方向に出るよう
-- @spcPoints = − C⁻@ として表現し、 LCL = −H、 UCL = 0 とする。
fitCUSUM :: Vector Double -> Double -> Double -> Double -> Double
         -> Either Text [SPCChartResult]
fitCUSUM xs mu0 s0In k h
  | V.null xs = Left "fitSPC CUSUM: empty input"
  | k < 0     = Left "fitSPC CUSUM: k must be ≥ 0"
  | h <= 0    = Left "fitSPC CUSUM: h must be > 0"
  | otherwise =
      let !n     = V.length xs
          !sigma = if s0In > 0 then s0In else sampleSD xs
          kAbs   = k * sigma
          hAbs   = h * sigma
          cPos   = V.scanl' (\c x -> max 0 (c + (x - mu0) - kAbs)) 0 xs
          cNeg   = V.scanl' (\c x -> max 0 (c + (mu0 - x) - kAbs)) 0 xs
          cPosT  = V.tail cPos
          cNegT  = V.tail cNeg
          chartPos = SPCChartResult
            { spcPoints    = cPosT
            , spcCenter    = 0
            , spcUCL       = vconst n hAbs
            , spcLCL       = vconst n 0
            , spcSigma     = sigma
            , spcChartName = "CUSUM+"
            }
          chartNeg = SPCChartResult
            { spcPoints    = V.map negate cNegT
            , spcCenter    = 0
            , spcUCL       = vconst n 0
            , spcLCL       = vconst n (-hAbs)
            , spcSigma     = sigma
            , spcChartName = "CUSUM-"
            }
      in Right [chartPos, chartNeg]

-- | 標本標準偏差 (n-1 補正)。 EWMA/CUSUM の σ₀ デフォルト用。
sampleSD :: Vector Double -> Double
sampleSD xs
  | V.length xs < 2 = 0
  | otherwise =
      let m  = vmean xs
          ss = V.sum (V.map (\x -> (x - m) ** 2) xs)
      in sqrt (ss / fromIntegral (V.length xs - 1))