packages feed

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

{-# OPTIONS_GHC -Wno-unused-imports #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Hanalyze.Model.GLMSpec (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 Numeric.LinearAlgebra as LA
import qualified Hanalyze.Model.Core        as Core
import qualified Hanalyze.Model.GLM         as GLM
import qualified Hanalyze.MCMC.Core as Core
import SpecHelper

spec :: Spec
spec = do
  describe "Hanalyze.Model.GLM IRLS 収束 (28d1feb7 回帰の防止)" $ do
    -- ★かつて runIRLS の converge が初回 dLL=0 (seed llP=ll(β0) と llHere=ll(β0) が一致)
    -- で IRLS を 1 ステップで早期停止し、 μ̂ が系統的に過大だった (28d1feb7 perf リライトの
    -- 回帰)。 真の MLE はスコア方程式 Xᵀ(y-μ̂)=0 を満たすので、 それを外部非依存オラクル
    -- として検証する (statsmodels の β とも突合)。
    let xsP   = [1,2,3,4,5,6,7,8] :: [Double]
        ysP   = [1,1,3,4,7,10,15,22] :: [Double]
        xMatP = LA.matrix 2 (concatMap (\x -> [1, x]) xsP)
        yVecP = LA.fromList ysP
        (frP, _) = GLM.fitGLMFull GLM.Poisson GLM.Log xMatP yVecP
        betaP = LA.toList (head (LA.toColumns (Core.coefficients frP)))
        muP   = head (LA.toColumns (Core.fitted frP))

    it "Poisson/Log: スコア方程式 Xᵀ(y-μ̂)=0 (真の MLE に収束)" $ do
      let score = LA.tr xMatP LA.#> (yVecP - muP)
      LA.norm_Inf score `shouldSatisfy` (< 1e-6)

    it "Poisson/Log: 係数が statsmodels GLM と一致 (-0.33398, 0.43288)" $ do
      abs (betaP !! 0 - (-0.33397763)) `shouldSatisfy` (< 1e-4)
      abs (betaP !! 1 -   0.43287516)  `shouldSatisfy` (< 1e-4)

    it "Binomial/Logit: スコア方程式 Xᵀ(y-μ̂)=0 (分離しない data で MLE)" $ do
      let xsB   = [0,1,2,3,4,5,6,7,8,9] :: [Double]
          ysB   = [0,0,0,0,1,0,1,1,1,1] :: [Double]
          xMatB = LA.matrix 2 (concatMap (\x -> [1, x]) xsB)
          yVecB = LA.fromList ysB
          (frB, _) = GLM.fitGLMFull GLM.Binomial GLM.Logit xMatB yVecB
          muB   = head (LA.toColumns (Core.fitted frB))
          score = LA.tr xMatB LA.#> (yVecB - muB)
      LA.norm_Inf score `shouldSatisfy` (< 1e-6)

  describe "Hanalyze.Model.GLM diagnostics (request/090-AB)" $ do
    let xsG = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] :: [Double]
        ysG = [1.1, 2.9, 5.2, 6.8, 9.1, 11.0, 13.2, 14.8, 17.0, 19.1] :: [Double]
        xMatG = LA.matrix 2 (concatMap (\x -> [1, x]) xsG)
        yVecG = LA.fromList ysG
        (frG, sigmaG) = GLM.fitGLMFull GLM.Gaussian GLM.Identity xMatG yVecG
        betaG = head (LA.toColumns (Core.coefficients frG))
        muVG  = head (LA.toColumns (Core.fitted frG))

    it "glmPearsonResiduals (Gaussian, V=1) == raw residuals" $ do
      let pr = GLM.glmPearsonResiduals GLM.Gaussian yVecG muVG
          rr = yVecG - muVG
      LA.norm_2 (pr - rr) `shouldSatisfy` (< 1e-9)

    it "glmDevianceResiduals (Gaussian) == sign(y-μ)·|y-μ|" $ do
      let dr  = GLM.glmDevianceResiduals GLM.Gaussian yVecG muVG
          ref = LA.fromList
                  [ signum (y - m) * abs (y - m)
                  | (y, m) <- zip ysG (LA.toList muVG) ]
      LA.norm_2 (dr - ref) `shouldSatisfy` (< 1e-9)

    it "glmVariance: Binomial μ(1-μ); Poisson μ" $ do
      GLM.glmVariance GLM.Binomial 0.3 `shouldBe` 0.3 * 0.7
      GLM.glmVariance GLM.Poisson  4.0 `shouldBe` 4.0

    it "predictGlmEtaWithSE: η = xᵀβ, SE > 0" $ do
      let xNew = LA.fromList [1, 5.0]
          (eta, se) = GLM.predictGlmEtaWithSE betaG sigmaG xNew
      eta `shouldSatisfy` (\e -> abs (e - (1 + 2 * 5.0)) < 0.5)
      se  `shouldSatisfy` (> 0)
      se  `shouldSatisfy` (< 1)

    it "predictGlmMuWithCI (Identity): half-width ≈ 1.96·SE" $ do
      let xNew    = LA.fromList [1, 4.5]
          ci      = GLM.predictGlmMuWithCI GLM.Identity 0.95 betaG sigmaG xNew
          (_, se) = GLM.predictGlmEtaWithSE betaG sigmaG xNew
          halfW   = (GLM.gpHi ci - GLM.gpLo ci) / 2
      abs (halfW - 1.96 * se) `shouldSatisfy` (< 1e-2)

    it "predictGlmMuWithCI (Logit): CI stays in (0,1)" $ do
      let xs2  = LA.matrix 2 (concatMap (\i -> [1, fromIntegral (i :: Int)]) [0..9])
          ys2  = LA.fromList [0,1,0,1,0,1,0,1,0,1]
          (_, sigma2) = GLM.fitGLMFull GLM.Binomial GLM.Logit xs2 ys2
          beta2 = LA.fromList [0.0, 0.05]
          xNew  = LA.fromList [1, 5.0]
          ci    = GLM.predictGlmMuWithCI GLM.Logit 0.95 beta2 sigma2 xNew
      GLM.gpMu ci `shouldSatisfy` (\v -> v > 0 && v < 1)
      GLM.gpLo ci `shouldSatisfy` (\v -> v >= 0)
      GLM.gpHi ci `shouldSatisfy` (\v -> v <= 1)
      GLM.gpLo ci `shouldSatisfy` (< GLM.gpHi ci)