packages feed

hanalyze-0.2.0.0: bench/posteriordb/21-radon/Model.hs

{-# LANGUAGE OverloadedStrings #-}
-- | radon_mn-radon_hierarchical_intercept_noncentered (posteriordb) —
-- hanalyze (ModelP) 実装。
--
-- Phase 89: posteriordb 横断ベンチマーク。Gelman ラドン多水準回帰の
-- 古典例 (mc-stan.org radon case study)。ミネソタ州 J=85郡・N=919家屋の
-- 屋内ラドン濃度回帰 (郡ごとの varying intercept + 固定傾き2本、
-- non-centered パラメタ化)。
--
-- Stan 原典 (posteriordb `models/stan/radon_hierarchical_intercept_noncentered.stan`):
--   parameters { vector[J] alpha_raw; vector[2] beta; real mu_alpha;
--                real<lower=0> sigma_alpha; real<lower=0> sigma_y; }
--   transformed parameters { alpha = mu_alpha + sigma_alpha * alpha_raw; }
--   model {
--     sigma_alpha ~ normal(0,1); sigma_y ~ normal(0,1);
--     mu_alpha ~ normal(0,10); beta ~ normal(0,10); alpha_raw ~ normal(0,1);
--     for (n in 1:N) {
--       muj[n] = alpha[county_idx[n]] + log_uppm[n]*beta[1];
--       mu[n] = muj[n] + floor_measure[n]*beta[2];
--       log_radon[n] ~ normal(mu[n], sigma_y);
--     }
--   }
--
-- `sigma_alpha`/`sigma_y` の `<lower=0>` + `Normal(0,1)` prior は
-- half-normal と数学的に等価なので `HalfNormal 1` で移植 (09-eight-schools
-- 等と同じ流儀)。`alpha[j]` は `deterministic` (mu_alpha+sigma_alpha*
-- alpha_raw[j]) で登録し `Data.Vector` 経由で O(1) 索引する
-- (`county_idx` は1-based構造添字なのでclosureで直接渡す・05-mh/16-lda
-- と同じ流儀)。単一階層 (alphaのみ) なので10-ratsの「二重階層」DSL
-- ギャップは該当しない — eight-schools/seedsと同型の構造でvecIR成功が
-- 見込める規模 (J=85・N=919)。
--
-- reference_posterior_name = null (posteriordb に公式 reference posterior 無し)。
--
-- ビルド: cabal build --project-file=cabal.project.plot posteriordb-radon
module Main (main) where

import Control.Monad (unless)
import Data.Aeson (FromJSON (..), withObject, (.:), eitherDecodeFileStrict)
import qualified Data.Text as T
import qualified Data.Vector as V
import System.Environment (getArgs)
import Text.Printf (printf)

import Hanalyze.Model.HBM (ModelP, Distribution (..), sample, observe,
                                    dataNamedX, dataNamedObs, plateI, plateForM_,
                                    (.#), deterministic, augmentChainWithDeterministic)
import Hanalyze.Model.HBM (gradPathLabel)
import Hanalyze.Plot (hbmModelSpec)
import Hanalyze.Plot (HBMConfig (..), defaultHBM, hbm, (|->),
                              dashboardOf, hbmChainsR)
import Hgg.Plot.Spec (ColData (..))
import Hgg.Plot.Frame (BoundPlot, (|>>))
import Hgg.Plot.Backend.Rasterific (savePNGBound)

import Common (summarize, printSummary, timeSamplingMs)

-- | posteriordb の @radon_mn.json@ 形状 ({"N":919,"J":85,
-- "floor_measure":[...],"log_radon":[...],"log_uppm":[...],
-- "county_idx":[...]}・county_idx は 1-based)。
data RadonData = RadonData
  { rdJ            :: Int
  , rdCountyIdx    :: [Int]
  , rdFloorMeasure :: [Double]
  , rdLogRadon     :: [Double]
  , rdLogUppm      :: [Double]
  }

instance FromJSON RadonData where
  parseJSON = withObject "RadonData" $ \v ->
    RadonData <$> v .: "J" <*> v .: "county_idx" <*> v .: "floor_measure"
              <*> v .: "log_radon" <*> v .: "log_uppm"

noDf :: [(T.Text, ColData)]
noDf = []

dataPath :: FilePath
dataPath = "bench/posteriordb/21-radon/data/radon_mn.json"

figuresDir :: FilePath
figuresDir = "bench/posteriordb/21-radon/figures"

readData :: IO RadonData
readData = either fail pure =<< eitherDecodeFileStrict dataPath

-- | 郡ごとの varying intercept (non-centered) + 固定傾き2本の階層回帰
-- (Stan 原典と同一構造)。 @countyIdx@ は微分対象ではない構造的添字
-- (1-based) なので closure で直接渡す。
radonModel :: Int -> [Int] -> ModelP ()
radonModel nCounty countyIdx = do
  muAlpha    <- sample "mu_alpha"    (Normal 0 10)
  sigmaAlpha <- sample "sigma_alpha" (HalfNormal 1)
  sigmaY     <- sample "sigma_y"     (HalfNormal 1)
  beta1      <- sample "beta1" (Normal 0 10)
  beta2      <- sample "beta2" (Normal 0 10)
  alphaRaws  <- plateI "county" nCounty $ \j -> sample ("alpha_raw" .# j) (Normal 0 1)
  alphas     <- mapM (\(j, ar) -> deterministic ("alpha" .# j) (muAlpha + sigmaAlpha * ar))
                      (zip [0 :: Int ..] alphaRaws)
  let alphaV = V.fromList alphas
  logUppm      <- dataNamedX   "log_uppm"      []
  floorMeasure <- dataNamedX   "floor_measure" []
  ys           <- dataNamedObs "log_radon"     []
  plateForM_ "obs" (zip4' countyIdx logUppm floorMeasure ys) $ \(cid, uppm, floorM, yVal) ->
    let mu = (alphaV V.! (cid - 1)) + uppm * beta1 + floorM * beta2
    in observe "log_radon" (Normal mu sigmaY) [yVal]
  where
    zip4' (a : as) (b : bs) (c : cs) (d : ds) = (a, b, c, d) : zip4' as bs cs ds
    zip4' _ _ _ _ = []

main :: IO ()
main = do
  d <- readData
  -- Phase 102 A1: `prof` 引数で図出力 skip (15-dugongs と同じ理由。
  -- サンプリング設定は本番のまま)。
  args <- getArgs
  let profRun = elem "prof" args
  let df = [ ("log_uppm",      NumData (V.fromList (rdLogUppm d)))
           , ("floor_measure", NumData (V.fromList (rdFloorMeasure d)))
           , ("log_radon",     NumData (V.fromList (rdLogRadon d)))
           ] :: [(T.Text, ColData)]
      -- PyMC 側 (model.py) と同じ設定を定数で揃える。
      cfg = defaultHBM { hbmChains = 4, hbmSamples = 1000
                        , hbmWarmup = 1000, hbmSeed = Just 1 }
      model :: ModelP ()
      model = radonModel (rdJ d) (rdCountyIdx d)
      m = df |-> hbm cfg model

  -- 勾配経路 = compileGradUV が実際に選ぶ経路 (束縛済 hbmModelSpec で判定・
  -- Phase 91 A4: 生モデルを synthVecIR に渡すと data 空で誤表示するため差替)。
  putStrLn $ "勾配経路 = " ++ gradPathLabel (hbmModelSpec m)

  (_, samplingMs) <- timeSamplingMs (hbmChainsR m)
  printf "sampling wall = %.1f ms (draws only, no dashboard/startup)\n" samplingMs

  -- J=85郡分のalpha latentを含むため dashboardFullOf ではなく dashboardOf
  -- (健全性2x2パネルのみ・05-mh/10-ratsと同じ判断)。
  unless profRun $
    savePNGBound (figuresDir ++ "/hs_dashboard_full.png") $
      (noDf |>> dashboardOf m "log_radon" :: BoundPlot)

  let chainsAug = map (augmentChainWithDeterministic model) (hbmChainsR m)
  printSummary $ summarize
    ["mu_alpha", "sigma_alpha", "sigma_y", "beta1", "beta2"]
    chainsAug