packages feed

hanalyze-0.1.0.0: demo/Demo.hs

{-# LANGUAGE OverloadedStrings #-}
module Main where

import qualified DataFrame as DX
import Hanalyze.Model.GLMM
import Hanalyze.Model.Core (coeffList, rSquared1, fittedList)
import Hanalyze.Model.LM   (multiPolyDesignMatrix, fitLMVec)

import qualified Data.Vector           as V
import qualified Data.Text             as T
import qualified Numeric.LinearAlgebra as LA
import Data.List   (zip4)
import Text.Printf (printf)

-- ---------------------------------------------------------------------------
-- テストデータ: 3クラスの試験結果
--
-- 真のモデル: score = 64 + u_school + 2×hours + ε
--   u_A ≈ +20,  u_B ≈ 0,  u_C ≈ -20
--
-- クラスA(優秀): 1〜5時間、成績80台  ← 少ない時間で高得点
-- クラスB(平均): 3〜7時間、成績60台
-- クラスC(苦手): 6〜10時間、成績40台 ← 多くの時間で低得点
--
-- OLSで見ると: 「時間 ↑ → 成績 ↓」(Simpson's paradox)
-- GLMMで見ると: 「時間 +1h → +2点」(真の効果)
-- ---------------------------------------------------------------------------

hoursVec :: V.Vector Double
hoursVec = V.fromList [1,2,3,4,5, 3,4,5,6,7, 6,7,8,9,10]

scoresVec :: V.Vector Double
scoresVec = V.fromList
  [ 80.2, 82.0, 84.1, 86.0, 88.2   -- class A
  , 59.9, 62.1, 64.0, 66.2, 68.1   -- class B
  , 40.1, 42.2, 44.0, 45.9, 47.8 ] -- class C

schoolVec :: V.Vector String   -- annotated for readability; converted below
schoolVec = V.fromList
  ["A","A","A","A","A", "B","B","B","B","B", "C","C","C","C","C"]

main :: IO ()
main = do
  let df = DX.insertColumn "hours"  (DX.fromList (V.toList hoursVec :: [Double]))
         $ DX.insertColumn "score"  (DX.fromList (V.toList scoresVec :: [Double]))
         $ DX.insertColumn "school" (DX.fromList
             (["A","A","A","A","A","B","B","B","B","B","C","C","C","C","C"] :: [T.Text]))
         $ DX.empty

  -- ── OLS (school を無視した単純回帰) ──────────────────────────────────
  let dm     = multiPolyDesignMatrix [(hoursVec, 1)]
      y      = LA.fromList (V.toList scoresVec)
      olsRes = fitLMVec dm y
      (b0, b1) = case coeffList olsRes of { (a:b:_) -> (a,b); _ -> (0,0) }

  putStrLn "╔══════════════════════════════════════════════════════════╗"
  putStrLn "║  OLS  (school を無視した単純回帰)                       ║"
  putStrLn "╚══════════════════════════════════════════════════════════╝"
  printf "  β₀ (切片)   : %8.3f\n" b0
  printf "  β₁ (hours)  : %8.3f   ← 負! 時間が増えると成績が下がる?\n" b1
  printf "  R²           : %8.3f\n" (rSquared1 olsRes)
  putStrLn "  ↑ Simpson's paradox: schoolベースライン差がhours効果を逆転させている"

  -- ── GLMM (school ランダム切片) ────────────────────────────────────────
  putStrLn ""
  putStrLn "╔══════════════════════════════════════════════════════════╗"
  putStrLn "║  GLMM (school ランダム切片モデル)                       ║"
  putStrLn "╚══════════════════════════════════════════════════════════╝"
  case fitLMEDataFrame [("hours", 1)] "school" "score" df of
    Nothing -> putStrLn "Error: GLMM推定に失敗"
    Just gr -> do
      let (g0, g1) = case coeffList (glmmFixed gr) of { (a:b:_) -> (a,b); _ -> (0,0) }

      putStrLn "  固定効果:"
      printf "    β₀ (切片)   : %8.3f\n" g0
      printf "    β₁ (hours)  : %8.3f   ← 正! 真の効果を回収\n" g1
      putStrLn "  分散成分:"
      printf "    σ²_u (school間) : %8.3f\n" (glmmRandVar gr)
      printf "    σ²   (残差)     : %8.3f\n" (glmmResidVar gr)
      printf "    ICC              : %8.3f  (分散の%.0f%%がschool間)\n"
             (glmmICC gr) (glmmICC gr * 100)
      putStrLn "  BLUPs (schoolごとのランダム切片 û_j):"
      mapM_ (\(s, u) -> printf "    %s : %+8.3f\n" s u)
            (zip (V.toList (glmmGroups gr)) (V.toList (glmmBLUPs gr)))
      putStrLn ""
      putStrLn "  観測値 vs 条件付きフィット値:"
      putStrLn "  school  hours  actual  fitted  resid"
      let fitted  = fittedList (glmmFixed gr)
          sLabels = ["A","A","A","A","A","B","B","B","B","B","C","C","C","C","C"] :: [String]
      mapM_ (\(s, h, ya, yf) ->
               printf "    %-4s   %5.0f  %6.1f  %6.1f  %+5.2f\n"
                      s h ya yf (ya - yf))
            (zip4 sLabels (V.toList hoursVec) (V.toList scoresVec) fitted)