hanalyze-core-0.2.0.1: src/Hanalyze/Stat/GroupComparison.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BangPatterns #-}
-- |
-- Module : Hanalyze.Stat.GroupComparison
-- Description : 2 群間の多変量比較ランキング (Spotfire 風 "Good vs Bad")
-- Copyright : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License : BSD-3-Clause
--
-- [日本語]: 2 群間の多変量比較ランキング (Spotfire 風 "Good vs Bad")。
--
-- 「良品 vs 不良品」 を二値ラベルで分け、 各説明変数について
-- (i) 平均差、 (ii) Cohen's d 効果量、 (iii) Welch t-test p 値 を計算し、
-- 効果量の絶対値降順にランク付けして返す。 半導体品質解析等で頻出。
--
-- 単独検定ではなく __複数変数の並列比較に最適化__ された helper。
-- 多重比較補正は呼び出し側で `Hanalyze.Stat.MultipleTesting` を使う。
--
-- [English]: Multivariate group-comparison ranking between 2 groups
-- (Spotfire-style "Good vs Bad").
--
-- Splits observations into "good" vs "defective" via a binary label, and
-- for each explanatory variable computes (i) the mean difference, (ii)
-- Cohen's d effect size, and (iii) Welch's t-test p-value, returning them
-- ranked in descending order of absolute effect size. Common in
-- semiconductor quality analysis and similar domains.
--
-- A helper __optimized for comparing multiple variables in parallel__,
-- rather than a single test. Multiple-comparison correction is left to
-- the caller via `Hanalyze.Stat.MultipleTesting`.
module Hanalyze.Stat.GroupComparison
( -- * 結果型
GroupCompResult (..)
-- * 比較
, goodVsBad
) where
import qualified Data.Vector as V
import qualified Numeric.LinearAlgebra as LA
import Data.List (sortBy)
import Data.Ord (comparing, Down (..))
import Data.Text (Text)
import Data.Vector (Vector)
import qualified Hanalyze.Stat.Test as ST
import qualified Hanalyze.Stat.Effect as Eff
-- ===========================================================================
-- 型
-- ===========================================================================
-- | [日本語]: 1 変数の Good vs Bad 比較結果。
-- [English]: Good vs Bad comparison result for a single variable.
data GroupCompResult = GroupCompResult
{ gcrVarName :: !Text -- ^ [日本語]: 変数名 [English]: Variable name
, gcrMeanG :: !Double -- ^ [日本語]: Good 群 (label = True) の平均 [English]: Mean of the Good group (label = True)
, gcrMeanB :: !Double -- ^ [日本語]: Bad 群 (label = False) の平均 [English]: Mean of the Bad group (label = False)
, gcrMeanDiff :: !Double -- ^ Mean(Bad) − Mean(Good)
, gcrEffect :: !Double -- ^ [日本語]: Cohen's d (signed; |gcrEffect| でランク) [English]: Cohen's d (signed; ranked by |gcrEffect|)
, gcrPValue :: !Double -- ^ [日本語]: Welch's two-sided t-test の p 値 [English]: p-value from Welch's two-sided t-test
, gcrNG :: !Int -- ^ [日本語]: Good 群サイズ [English]: Good group size
, gcrNB :: !Int -- ^ [日本語]: Bad 群サイズ [English]: Bad group size
} deriving (Show, Eq)
-- ===========================================================================
-- 公開関数
-- ===========================================================================
-- | [日本語]: 各説明変数について 2 群間の差を計算し、 効果量絶対値降順でランク付け。
--
-- 入力契約:
--
-- * 変数リストは非空 (1 変数以上)
-- * 各変数の Vector 長 = labels の長さ (一致しないと 'Left')
-- * 両群とも 2 個以上の観測必須 (Welch t-test の前提)
--
-- [English]: For each explanatory variable, compute the difference
-- between the 2 groups and rank in descending order of absolute effect
-- size.
--
-- Input contract:
--
-- * The variable list is non-empty (1 or more variables)
-- * Each variable's Vector length equals the length of labels (a
-- mismatch returns 'Left')
-- * Both groups must have 2 or more observations (a prerequisite for
-- Welch's t-test)
goodVsBad
:: [(Text, Vector Double)] -- ^ [日本語]: (変数名, 値ベクトル) のリスト [English]: List of (variable name, value vector)
-> Vector Bool -- ^ [日本語]: 群ラベル (True = Good、 False = Bad) [English]: Group label (True = Good, False = Bad)
-> Either Text [GroupCompResult]
goodVsBad vars labels
| null vars = Left "goodVsBad: empty variable list"
| V.null labels = Left "goodVsBad: empty labels"
| any (\(_, v) -> V.length v /= V.length labels) vars
= Left "goodVsBad: variable length mismatch with labels"
| nG < 2 || nB < 2 = Left "goodVsBad: each group needs at least 2 observations"
| otherwise =
let results = map (compareOne labels) vars
in Right (sortBy (comparing (Down . absEffect)) results)
where
nG = V.length (V.filter id labels)
nB = V.length labels - nG
absEffect = abs . gcrEffect
-- ---------------------------------------------------------------------------
-- 1 変数の比較
-- ---------------------------------------------------------------------------
compareOne :: Vector Bool -> (Text, Vector Double) -> GroupCompResult
compareOne labels (name, vals) =
let (goodList, badList) = partitionByLabels labels vals
gVec = LA.fromList goodList
bVec = LA.fromList badList
tr = ST.tTestWelch gVec bVec ST.TwoSided
pVal = ST.trPValue tr
d = Eff.cohenD bVec gVec -- Mean(Bad) − Mean(Good) 方向
mG = mean goodList
mB = mean badList
in GroupCompResult
{ gcrVarName = name
, gcrMeanG = mG
, gcrMeanB = mB
, gcrMeanDiff = mB - mG
, gcrEffect = d
, gcrPValue = pVal
, gcrNG = length goodList
, gcrNB = length badList
}
-- | [日本語]: label が True の要素を good、 False を bad として分割。
-- [English]: Split elements into good (label = True) and bad (label =
-- False).
partitionByLabels :: Vector Bool -> Vector Double -> ([Double], [Double])
partitionByLabels labels vals = go 0 ([], [])
where
n = V.length vals
go !i (gs, bs)
| i >= n = (reverse gs, reverse bs)
| otherwise =
let v = vals V.! i
l = labels V.! i
in if l then go (i + 1) (v : gs, bs)
else go (i + 1) (gs, v : bs)
mean :: [Double] -> Double
mean [] = 0
mean xs = sum xs / fromIntegral (length xs)