packages feed

hanalyze-0.2.0.0: bench/haskell/BenchFormulaRef.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Phase 47 A5: Formula DSL の Haskell 参照値生成器。
--   bench/python/bench_formula.py が statsmodels / scipy と突合するための ŷ/R²/係数を
--   実際の Hanalyze 実装で計算し formula_haskell_ref.json に書き出す (再現可能化)。
--
--   実行: cabal run formula-ref-gen   (cwd は repo root を想定、 bench/python/ に書く)
module Main (main) where

import           Data.List              (intercalate)
import           Data.Text              (Text)
import qualified Data.Text              as T
import qualified DataFrame.Internal.Column    as DX
import qualified DataFrame.Internal.DataFrame  as DX
import qualified Numeric.LinearAlgebra  as LA

import qualified Hanalyze.Model.Core              as Core
import           Hanalyze.Model.Formula.RFormula  (parseModel)
import           Hanalyze.Model.Formula.Design    (fitLMF, fitWLSF, defaultWLS,
                                                   WLSConfig (..))
import           Hanalyze.Model.Formula.Nonlinear (fitNLS, nlsParams)
import           Hanalyze.Model.Formula.Mixed     (fitMixedLME)
import           Hanalyze.Model.GLMM              (GLMMResultRE (..))

-- ============================================================================
-- データ (Python 側と完全一致させる)
-- ============================================================================

dfOLS :: DX.DataFrame
dfOLS = DX.fromNamedColumns
  [ ("y", DX.fromList ([10,20,30,40,50,60,12,22,34,44,52,62] :: [Double]))
  , ("g", DX.fromList (["A","A","B","B","C","C","A","A","B","B","C","C"] :: [Text]))
  , ("t", DX.fromList (["P","Q","P","Q","P","Q","P","Q","P","Q","P","Q"] :: [Text]))
  , ("x", DX.fromList ([1,2,3,4,5,6,1,2,3,4,5,6] :: [Double]))
  ]

olsFormulas :: [Text]
olsFormulas =
  [ "y ~ x"
  , "y ~ C(g) * C(t)"
  , "y ~ C(g) + C(g):x"
  , "y ~ x + I(x**2)"
  , "y ~ C(g, Sum)"                  -- A2 contrast (ŷ は treatment と不変 → statsmodels 一致)
  , "y ~ C(g, Sum) + C(g, Sum):x"
  ]

dfWLS :: DX.DataFrame
dfWLS = DX.fromNamedColumns
  [ ("y", DX.fromList ([2.1,3.9,6.2,7.8,10.1,12.2,13.8,16.1] :: [Double]))
  , ("x", DX.fromList ([1,2,3,4,5,6,7,8] :: [Double]))
  , ("w", DX.fromList ([1,1,2,2,3,3,4,4] :: [Double]))
  ]

-- Phase 48: mixed-effects (random intercept + slope) 突合用データ。
-- 4 群 × 5 obs、 群ごとに切片・傾きが異なる線形 (固定平均 β≈[2,3])。
dfMixed :: DX.DataFrame
dfMixed = DX.fromNamedColumns
  [ ("y", DX.fromList ([ 3.01,6.49,10.01,13.49,17.00
                       , 1.01,3.49, 6.01, 8.49,11.00
                       , 2.51,5.19, 7.91,10.59,13.30
                       , 1.51,4.79, 8.11,11.39,14.70 ] :: [Double]))
  , ("x", DX.fromList (concat (replicate 4 [0,1,2,3,4]) :: [Double]))
  , ("g", DX.fromList (concatMap (replicate 5) (["A","B","C","D"] :: [Text])))
  ]

nlsXs :: [Double]
nlsXs = [0,0.5,1,1.5,2,2.5,3,3.5,4,4.5,5]

dfNLS :: DX.DataFrame
dfNLS = DX.fromNamedColumns
  [ ("y", DX.fromList (map (\x -> 3.0 * exp (negate 0.5 * x)) nlsXs))   -- a=3 b=0.5
  , ("x", DX.fromList nlsXs)
  ]

-- ============================================================================
-- JSON (手書き・依存最小)
-- ============================================================================

jstr :: String -> String
jstr s = "\"" ++ s ++ "\""

jarr :: [Double] -> String
jarr xs = "[" ++ intercalate ", " (map show xs) ++ "]"

main :: IO ()
main = do
  olsEntries <- mapM olsEntry olsFormulas
  wlsJson    <- pure wlsEntry
  nlsJson    <- pure nlsEntry
  mixedJson  <- pure mixedEntry
  let body = intercalate ",\n  " (olsEntries ++ [wlsJson, nlsJson, mixedJson])
      json = "{\n  " ++ body ++ "\n}\n"
  writeFile "bench/python/formula_haskell_ref.json" json
  putStrLn ("formula_haskell_ref.json 生成: OLS " ++ show (length olsEntries)
            ++ " + __wls__ + __nls__ + __mixed__")

olsEntry :: Text -> IO String
olsEntry f =
  case parseModel f >>= \fm -> fitLMF fm dfOLS of
    Left e        -> error ("OLS " ++ T.unpack f ++ ": " ++ e)
    Right (fr, _) ->
      pure $ jstr (T.unpack f) ++ ": {\"r2\": " ++ show (Core.rSquared1 fr)
             ++ ", \"yhat\": " ++ jarr (Core.fittedList fr) ++ "}"

-- WLS 係数 (parameterization 同一ゆえ係数も突合可)。
wlsEntry :: String
wlsEntry =
  case parseModel "y ~ x" >>= \fm -> fitWLSF defaultWLS { wcWeights = Just "w" } fm dfWLS of
    Left e        -> error ("WLS: " ++ e)
    Right (fr, _) ->
      jstr "__wls__" ++ ": {\"coef\": " ++ jarr (LA.toList (Core.coefficientsV fr)) ++ "}"

-- NLS パラメータ。
nlsEntry :: String
nlsEntry =
  case parseModel "y x = a * exp(-b * x)" of
    Left e   -> error ("NLS parse: " ++ e)
    Right fm -> case fitNLS fm dfNLS [("a",1),("b",1)] of
      Left e  -> error ("NLS: " ++ e)
      Right r -> let pm = nlsParams r
                     val k = maybe (0/0) id (lookup k pm)
                 in jstr "__nls__" ++ ": {\"a\": " ++ show (val "a")
                    ++ ", \"b\": " ++ show (val "b") ++ "}"

-- 混合効果 (random intercept + slope) の β / G (2×2) / σ²。
-- 突合先 = statsmodels smf.mixedlm(..., re_formula="~x").fit(reml=False) (ML)。
mixedEntry :: String
mixedEntry =
  case fitMixedLME "y ~ x + (1+x|g)" dfMixed of
    Left e         -> error ("mixed: " ++ e)
    Right (res, _) ->
      let beta = LA.toList (Core.coefficientsV (reFixed res))
          g    = LA.toLists (reRandCov res)        -- [[g00,g01],[g10,g11]]
          flatG = concat g
      in jstr "__mixed__" ++ ": {\"beta\": " ++ jarr beta
         ++ ", \"cov_re\": " ++ jarr flatG
         ++ ", \"sigma2\": " ++ show (reResidVar res) ++ "}"