packages feed

hanalyze-0.2.0.0: test/Hanalyze/Model/PLSSpec.hs

{-# OPTIONS_GHC -Wno-unused-imports #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Hanalyze.Model.PLSSpec (spec) where

import Test.Hspec
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck
import Hanalyze.Model.Formula
import Hanalyze.Model.Formula.Frame
import Hanalyze.Model.Formula.Design
import Hanalyze.Model.Formula.RFormula
import Hanalyze.Model.Formula.Nonlinear
import Hanalyze.Model.Formula.Mixed
import Hanalyze.Model.GLMM
import Hanalyze.Model.GLM (Family (..), LinkFn (..))
import Hanalyze.Stat.Distribution (Transform)
import Data.List (sort, nub)
import Control.Monad (forM, forM_)
import System.IO.Temp (withSystemTempFile)
import System.IO     (hPutStr, hClose)
import           Hanalyze.Model.HBM.Ast (Expr (..), Lit (..), DoStmt (..), Err)
import           Data.IORef         (newIORef, readIORef, modifyIORef')
import qualified Data.Text   as T
import qualified Numeric.LinearAlgebra as LA
import qualified System.Random.MWC as MWC
import qualified System.Random.MWC as MWC
import qualified Hanalyze.Model.PLS         as PLS
import SpecHelper

spec :: Spec
spec = do
  describe "Hanalyze.Model.PLS (Phase 9)" $ do
    -- 合成データ: y = 2 x1 + 1 x2 + 0 x3 (3 features) + noise
    let synthData :: IO (LA.Matrix Double, LA.Vector Double, LA.Vector Double)
        synthData = do
          gen <- MWC.create
          let n = 100; p = 5
          xs <- LA.fromLists <$> sequence
                  [ sequence [ MWC.uniformR (-1, 1) gen | _ <- [1..p] ]
                  | _ <- [1..n] ]
          let trueB = LA.fromList [2.0, 1.0, 0.0, 0.0, 0.0] :: LA.Vector Double
              y0 = xs LA.#> trueB
          noise <- LA.fromList <$> sequence
                     [ MWC.uniformR (-0.1, 0.1) gen | _ <- [1..n] ]
          pure (xs, y0 + noise, trueB)

    it "fitPLS1: n_components=2 で fit、 shape 整合性" $ do
      (xs, y, _) <- synthData
      let cfg = PLS.defaultPLS { PLS.plsN_Components = 2 }
      case PLS.fitPLS1 cfg xs y of
        Left e -> expectationFailure (T.unpack e)
        Right f -> do
          LA.rows (PLS.plsScoresT f)    `shouldBe` 100
          LA.cols (PLS.plsScoresT f)    `shouldBe` 2
          LA.rows (PLS.plsLoadingsP f)  `shouldBe` 5
          LA.cols (PLS.plsLoadingsP f)  `shouldBe` 2
          LA.size (PLS.plsVIP f)        `shouldBe` 5
          LA.size (PLS.plsR2X f)        `shouldBe` 2

    it "predictPLS1: 合成データで真値に近い予測" $ do
      (xs, y, _) <- synthData
      let cfg = PLS.defaultPLS { PLS.plsN_Components = 3 }
      case PLS.fitPLS1 cfg xs y of
        Left e -> expectationFailure (T.unpack e)
        Right f -> do
          let yHat = PLS.predictPLS1 f xs
              resid = y - yHat
              n = LA.size y :: Int
              mse = LA.sumElements (resid * resid) / fromIntegral n
          mse `shouldSatisfy` (< 0.05)

    it "VIP: 強い変数 (x1, x2) は VIP > 1、 ノイズ変数は VIP < 1 に近い" $ do
      (xs, y, _) <- synthData
      let cfg = PLS.defaultPLS { PLS.plsN_Components = 3 }
      case PLS.fitPLS1 cfg xs y of
        Left e -> expectationFailure (T.unpack e)
        Right f -> do
          let vip = LA.toList (PLS.plsVIP f)
          -- VIP[0] (x1) は最大級
          (vip !! 0) `shouldSatisfy` (> 1.0)
          -- VIP[1] (x2) も 1 超え
          (vip !! 1) `shouldSatisfy` (> 0.5)

    it "n_components が n-1 超えで Left" $ do
      (xs, y, _) <- synthData
      let cfg = PLS.defaultPLS { PLS.plsN_Components = 200 }
      case PLS.fitPLS1 cfg xs y of
        Left _ -> pure ()
        Right _ -> expectationFailure "expected Left for too many components"

    it "SIMPLS は現状未実装で Left" $ do
      (xs, y, _) <- synthData
      let cfg = PLS.defaultPLS { PLS.plsAlgorithm = PLS.SIMPLS }
      case PLS.fitPLS1 cfg xs y of
        Left _ -> pure ()
        Right _ -> expectationFailure "expected Left for SIMPLS (not implemented)"

    it "selectPLSComponentsCV: best K が grid 内" $ do
      (xs, y, _) <- synthData
      gen <- MWC.create
      sel <- PLS.selectPLSComponentsCV 5 4 xs (LA.asColumn y) gen
      PLS.plsBestK sel `shouldSatisfy` (\k -> k >= 1 && k <= 4)
      length (PLS.plsCVMSEs sel) `shouldBe` 4
      PLS.plsOneSeK sel `shouldSatisfy` (<= PLS.plsBestK sel)