packages feed

hanalyze-0.2.0.0: src/Hanalyze/Model/FitYByX.hs

{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module      : Hanalyze.Model.FitYByX
-- Description : JMP "Fit Y by X" platform 相当の自動 dispatch wrapper
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- JMP \"Fit Y by X\" platform 相当の wrapper。
--
-- X / Y それぞれが連続 (Continuous) か カテゴリ (Categorical) かで
-- 適切な解析を自動 dispatch する:
--
-- @
--   X \\ Y  | Continuous           | Categorical
--   --------+----------------------+---------------------
--   Cont    | 単回帰 (LM)          | logistic GLM
--   Cat     | one-way ANOVA        | chi-square independence
-- @
--
-- canvas frontend で 「変数 2 つドラッグ → 自動分析」 を支える backend wrapper。
module Hanalyze.Model.FitYByX
  ( VarType (..)
  , FitYByXResult (..)
  , fitYByX
  ) where

import qualified Data.Vector             as V
import qualified Numeric.LinearAlgebra   as LA
import           Data.List               (nub, sort)
import           Data.Text               (Text)

import qualified Hanalyze.Model.Core     as Core
import qualified Hanalyze.Model.LM       as LM
import qualified Hanalyze.Model.GLM      as GLM
import qualified Hanalyze.Stat.Test      as ST

-- ===========================================================================
-- 型
-- ===========================================================================

data VarType
  = Continuous
  | Categorical
  deriving (Show, Eq)

data FitYByXResult
  = FitContCont !Core.FitResult
    -- ^ 単回帰: y = β₀ + β₁ x
  | FitCatCont  !ST.TestResult ![Double]
    -- ^ one-way ANOVA + group means (group order = sort.nub of x)
  | FitContCat  !Core.FitResult
    -- ^ logistic GLM: P(Y=1) = sigmoid(β₀ + β₁ x)
  | FitCatCat   !ST.TestResult
    -- ^ chi-square independence
  deriving (Show)

-- ===========================================================================
-- 公開 API
-- ===========================================================================

-- | X / Y の型に応じて適切な解析を dispatch する。
--   入力は両方とも Double Vector。 Categorical の場合は整数値を Double 化
--   して渡す前提 (例: 0, 1, 2, ...)。
fitYByX
  :: VarType -> VarType
  -> LA.Vector Double      -- ^ X
  -> LA.Vector Double      -- ^ Y
  -> Either Text FitYByXResult
fitYByX xt yt x y
  | LA.size x /= LA.size y =
      Left "fitYByX: X and Y must have the same length"
  | LA.size x < 2 =
      Left "fitYByX: need at least 2 observations"
  | otherwise = case (xt, yt) of
      (Continuous, Continuous) ->
        let xMat = LA.fromColumns [LA.fromList (replicate (LA.size x) 1), x]
        in Right (FitContCont (LM.fitLMVec xMat y))

      (Categorical, Continuous) ->
        let levels = sort (nub (LA.toList x))
            groups = [ LA.fromList
                        [ LA.atIndex y i
                        | i <- [0 .. LA.size x - 1]
                        , LA.atIndex x i == lvl ]
                     | lvl <- levels ]
            tr     = ST.anovaOneWay groups
            means  = [ LA.sumElements g / fromIntegral (LA.size g)
                     | g <- groups ]
        in if any ((< 1) . LA.size) groups
             then Left "fitYByX (cat × cont): some groups are empty"
             else Right (FitCatCont tr means)

      (Continuous, Categorical) ->
        -- Y must be binary 0/1 for logistic
        let ys = LA.toList y
        in if not (all (\v -> v == 0 || v == 1) ys)
             then Left "fitYByX (cont × cat): Y must be binary 0/1 for logistic GLM"
             else
               let xMat = LA.fromColumns
                            [LA.fromList (replicate (LA.size x) 1), x]
               in Right (FitContCat (GLM.fitGLM GLM.Binomial xMat y))

      (Categorical, Categorical) ->
        let xLevels = sort (nub (LA.toList x))
            yLevels = sort (nub (LA.toList y))
            cell xl yl = fromIntegral $ length
              [ () | i <- [0 .. LA.size x - 1]
                   , LA.atIndex x i == xl
                   , LA.atIndex y i == yl ]
            tbl = LA.fromLists
              [ [ cell xl yl | yl <- yLevels ] | xl <- xLevels ]
        in if length xLevels < 2 || length yLevels < 2
             then Left "fitYByX (cat × cat): need at least 2 levels per axis"
             else Right (FitCatCat (ST.chiSquareIndep tbl))
  where
    _ = V.length :: V.Vector Int -> Int  -- silence warn