packages feed

hanalyze-0.2.0.0: src/Hanalyze/Fit.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}
-- |
-- Module      : Hanalyze.Fit
-- Description : plot 非依存の df |-> spec 統一 fit 演算子と各種モデル spec 型
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- データ源 → モデル学習層 (= @df |-> spec@ の plot 非依存部分)。
--
-- 'Hanalyze.Plot' から **図 ('VisualSpec') に依存しない**「データ源から
-- モデルを当てはめる」部分 (各 @*Spec@ 型・'Fit' instance・'(|->)' / '(|->!)'
-- 演算子・それらの smart ctor) をここへ切り出した非ゲートモジュール (常時 build)。
-- これにより @df |-> lm "x" "y"@ 等が cabal flag @plot-integration@ を on にせず
-- とも使える (Phase 71.3)。
--
-- ⚠ 本モジュールは 'Hgg.Plot' を一切 import しない (= 図に依存する toPlot /
-- Plottable instance 等は 'Hanalyze.Plot' 側に残す)。 'Fit' クラス本体・
-- ラッパ型・reqColV/reqColsM 等は 'Hanalyze.Model.Wrappers' から取り込む。
module Hanalyze.Fit
  ( -- * df |-> spec 統一 fit 演算子 (Phase 51)
    (|->)
  , (|->!)
    -- * 二変量近道 spec (列名 2 つ) — Phase 51.2
  , LMSpec (..)
  , lm
  , GLMSpec (..)
  , glm
  , SplineSpec (..)
  , spline
  , RobustSpec (..)
  , rlm
  , QuantileSpec (..)
  , rq
    -- * 重回帰 spec (列名リスト・formula 不要) — Phase 70.D
  , LMMultiSpec (..)
  , lmMulti
  , GLMMultiSpec (..)
  , glmMulti
  , RobustMultiSpec (..)
  , rlmMulti
  , QuantileMultiSpec (..)
  , rqMulti
    -- * formula 多変量 spec (R 流) — Phase 51.3
  , LMFormulaSpec (..)
  , lmF
  , GLMFormulaSpec (..)
  , glmF
  , GLMMFormulaSpec (..)
  , glmmF
    -- * DOE ワークフロー (Phase 78・設計オブジェクト + designModel)
  , Design (..)
  , DesignFactor (..)
  , FactorKind (..)
  , FactorScale (..)
  , DesignKind (..)
  , contFactor
  , contFactorLog
  , numFactor
  , catFactor
  , CustomSpec (..)
  , customSpec
  , customDesign
  , Structure (..)
  , splitPlot
  , stripPlot
  , blocked
  , Constraint (..)
  , ConstraintRel (..)
  , ConstraintGuard (..)
  , FactorValue (..)
  , NatConstraint (..)
  , natLeq
  , natGeq
  , natEq
  , natForbid
  , formulaToCustomModel
  , factorialDesign
  , centralCompositeDesign
  , boxBehnkenDesign
  , Resolution (..)
  , resNum
  , fractionalDesign
  , fractionalDesignGen
  , fractionalDesignInter
  , fractionalDesignGenInter
  , fractionalCatalog
  , fracResolution
  , aliasStructure
  , OATable (..)
  , taguchiDesign
  , taguchiDesignOA
  , OptCriterion (..)
  , optimalDesign
  , optimalDesignWith
  , optimalDesignLevels
  , mainEffects
  , twoWay
  , quadratic
  , designTable
  , designFrame
  , designFrameRound
  , designFactorNames
  , designFormula
  , RSMNature (..)
  , RSMReport (..)
  , rsmAnalysis
  , steepestAscentNatural
  , saveDesign
  , planFromFrame
  , DesignModelSpec (..)
  , designModel
  , DesignModelGPSpec (..)
  , designModelGP
  , ranIntercept
  , ranSlope
  , designHBMProgram
  , DesignHBMFit (..)
  , DesignModelHBMSpec (..)
  , designModelHBM
    -- * 多出力 (複数応答) fit コンビネータ — Phase 78.F
  , MultiOutputSpec (..)
  , multiOutput
  , modelFor
    -- * 重み付き最小二乗 (WLS) spec — Phase 52.A6
  , WeightedLMSpec (..)
  , weighted
  , weightedR2
    -- * 透過標準化ラッパ (自動逆変換) — Phase 70.3 項目 C
  , StandardizedSpec (..)
  , standardized
  , standardizedY
    -- * 群別フィット spec — Phase 52.A4
  , GroupedSpec (..)
  , grouped
  , groupModels
  , groupLabels
    -- * GAM 高レベル spec (基底一般化 + GCV) — Phase 70.6 F3
  , GAMConfig (..)
  , defaultGAMConfig
  , GAMSpec (..)
  , gam
  , gamMulti
    -- * カーネル法 (GP / KRR / RFF) spec — Phase 70.5 項目 E
  , GPConfig (..)
  , defaultGP
  , GPSpec
  , gp
  , GPMultiSpec
  , gpMulti
    -- * 罰則付き回帰 統合 spec — Phase 70.7 項目 G
  , LambdaStrat (..)
  , RegConfig (..)
  , defaultRidge
  , defaultLasso
  , RegSpec
  , regularized
  , regularizedMulti
  , ridge
  , ridgeMulti
  , lasso
  , lassoMulti
  , elasticNet
  , elasticNetMulti
    -- * 行列入力モデルの高レベル spec — Phase 70.A
  , PCASpec (..)
  , pca
    -- * MDS (多次元尺度構成法) — Phase 75.21
  , MDSSpec (..)
  , mds
  , MDSConfig (..)
  , MDSMethod (..)
  , defaultMDS
  , PLSSpec (..)
  , pls
  , LDASpec (..)
  , lda
  , CCASpec (..)
  , ccaOf
    -- * 教師あり ML 分類器/回帰器 spec — Phase 70.A
  , GBRSpec (..)
  , gbmReg
  , GBCSpec (..)
  , gbmCls
  , DTSpec (..)
  , decisionTree
  , KNNCSpec (..)
  , knnCls
  , KNNRSpec (..)
  , knnReg
  , NBSpec (..)
  , naiveBayes
    -- * seed 純粋化した RNG モデル spec (KMeans / RandomForest) — Phase 70.A
  , KMeansSpec (..)
  , kmeans
  , RFSpec (..)
  , randomForestReg
    -- * 因果探索 LiNGAM (高レベル df|-> ・Phase 77)
  , DirectLiNGAMSpec (..)
  , directLingam
  , ParceLiNGAMSpec (..)
  , parceLingam
  , MultiGroupLiNGAMSpec (..)
  , multiGroupLingam
  , VARLiNGAMSpec (..)
  , varLingam
  , PairwiseLiNGAMSpec (..)
  , pairwiseLingam
  , BootstrapLiNGAMSpec (..)
  , bootstrapLingam
  , ICALiNGAMSpec (..)
  , icaLingam
  , CorrelationSpec (..)
  , correlationOf
  , CorrelationGraph (..)
  , RFCSpec (..)
  , randomForestCls
    -- * SVM / 古典 MLP 高レベル spec (純粋・df |->) — Phase 75.9
  , MLPClsSpec (..)
  , mlpCls
  , MLPRegSpec (..)
  , mlpReg
  , SVMSpec (..)
  , svmCls
    -- * HBM spec — Phase 51.4
  , HBMSpec
  , hbm
  ) where

import           Data.List             (elemIndex, nub, sort, sortBy, transpose)
import qualified Data.Map.Strict       as Map
import           Data.Maybe            (fromMaybe)
import           Data.Ord              (comparing)
import           Data.Word             (Word32)
import qualified Data.Vector           as V
import qualified Data.Vector.Unboxed    as VU
import           System.Random.MWC     (initialize)
import           Control.Monad.ST      (runST)
import qualified Numeric.LinearAlgebra as LA

import           Data.Text             (Text)
import qualified Data.Text             as T

import           Hanalyze.Data.ColumnSource     (ColumnSource (..))
import           Hanalyze.DataIO.Convert        (getTextVec)
import           Hanalyze.DataIO.Preprocess     (dropMissingRows)
import           Hanalyze.Model.Formula         (Formula (..))
import           Hanalyze.Model.Formula.Mixed  (fitMixedLME, RandomSpec (..))
import           Hanalyze.Model.Formula.Frame   (ModelFrame (..), modelFrame)
import           Hanalyze.Model.Formula.Design  (designMatrixF, responseVec)
import           Hanalyze.Model.Formula.RFormula (parseModel)
import           Hanalyze.Model.GLMM           (GLMMResultRE (..), buildGroups)
import           Hanalyze.MCMC.Core             (chainVals)

import           Hanalyze.Model.Wrappers
import           Hanalyze.Diagnostics
import           Hanalyze.Model.Core   (coefficientsV)
import           Hanalyze.Model.GLM    ( Family (..), LinkFn (..) )
import           Hanalyze.Model.GP     (GPResult (..), Kernel (..), GPParams (..))
import qualified Hanalyze.Model.GP     as GP
import qualified Hanalyze.Model.RFF    as RFF
import qualified Debug.Trace                  as Trace
import           Hanalyze.Model.LM     ( designMatrix, fitLMVec )
import           Hanalyze.Model.Spline ( SplineKind (..) )
import           Hanalyze.Model.GAM    ( fitGAMAuto
                                              , GAMBasis (..), GAMLambda (..) )
import           Hanalyze.Model.Robust ( RobustEstimator )
import           Hanalyze.Stat.CorrelationNetwork (CorrelationGraph (..), correlationMatrix)
import           Hanalyze.Design.Workflow (Design (..), DesignFactor (..), FactorKind (..)
                                       , DesignKind (..), FactorScale (..), contFactor, contFactorLog, numFactor, catFactor
                                       , CustomSpec (..), customSpec, customDesign
                                       , Structure (..), splitPlot, stripPlot, blocked
                                       , Constraint (..), ConstraintRel (..), ConstraintGuard (..), FactorValue (..)
                                       , NatConstraint (..), natLeq, natGeq, natEq, natForbid
                                       , formulaToCustomModel
                                       , factorialDesign, centralCompositeDesign, boxBehnkenDesign
                                       , Resolution (..), resNum, fractionalDesign, fractionalDesignGen
                                       , fractionalDesignInter, fractionalDesignGenInter
                                       , fractionalCatalog, fracResolution, aliasStructure
                                       , OATable (..), taguchiDesign, taguchiDesignOA
                                       , OptCriterion (..), optimalDesign, optimalDesignWith, optimalDesignLevels
                                       , mainEffects, twoWay, quadratic
                                       , designTable, designFrame, designFrameRound
                                       , designFactorNames, designFormula
                                       , RSMNature (..), RSMReport (..), rsmAnalysis, steepestAscentNatural
                                       , saveDesign, planFromFrame)
-- 低レベル 'Hanalyze.Model.PCA.pca' は高レベル spec 'pca' (= PCASpec) と
-- 同名衝突するため qualified import で回避する (Phase 75.16)。
import           Hanalyze.Model.PCA     (PCAStandardize (..))
import qualified Hanalyze.Model.PCA     as PCALow
import           Hanalyze.Stat.Standardize
                   ( Standardizer (..), fitStandardizer, applyStandardizer
                   , applyStandardizerCol )
import           Hanalyze.Model.RandomForest (RandomForest (..), RFConfig (..), fitRFVPure)
import           Hanalyze.Model.LiNGAM.Direct (DirectLiNGAMConfig (..), DirectLiNGAMFit (..)
                                       , fitDirectLiNGAM)
import           Hanalyze.Model.LiNGAM.Parce (ParceConfig, ParceFit (..), fitParceLiNGAM)
import           Hanalyze.Model.LiNGAM.MultiGroup (MultiGroupConfig, MultiGroupFit (..)
                                       , fitMultiGroupLiNGAM)
import           Hanalyze.Model.LiNGAM.VAR (VARLiNGAMConfig, VARLiNGAMFit (..), fitVARLiNGAM)
import           Hanalyze.Model.LiNGAM.Pairwise (PairwiseResult (..), pairwiseLiNGAM)
import           Hanalyze.Model.LiNGAM.Bootstrap (BootstrapConfig, BootstrapResult (..)
                                       , fitBootstrapLiNGAMPure)
import           Hanalyze.Model.LiNGAM.ICA (ICALiNGAMConfig, ICALiNGAMFit (..), fitICALiNGAMPure)
import           Hanalyze.Model.RandomForestClassifier (RFClassifierFit (..), RFCConfig (..), fitRFClassifierPure)
import           Hanalyze.Model.GradientBoosting (GBRegressor (..), GBClassifier (..)
                                       , fitGBRegressor, fitGBClassifier, GBConfig (..))
import           Hanalyze.Model.DecisionTree (DTree (..), DTFit (..), fitDTV, DTConfig (..))
import           Hanalyze.Model.Discriminant (DiscriminantFit (..), fitLDA)
import           Hanalyze.Model.Multivariate (cca, CCAFit (..))
import           Hanalyze.Model.NaiveBayes (NBModel (..), GaussianNB (..), fitGNB)
import           Hanalyze.Model.KNN (KNNClassifier (..), KNNRegressor (..), fitKNNC, fitKNNR)
import           Hanalyze.Model.SVM (SVMConfig (..), SVMMulti (..)
                                       , fitSVMMulti
                                       , SVMHyper (..)
                                       , SVMTuneGrid (..), tuneSVM)
import           Hanalyze.Model.NeuralNetwork (MLPConfig (..), MLPFit (..)
                                       , fitMLPClassifierPure, fitMLPRegressorPure)
import           Hanalyze.Model.PLS (PLSFit (..), fitPLS, PLSConfig (..))
import           Hanalyze.Model.Cluster (KMeansResult (..)
                                       , KMeansConfig (..), kMeansPure)
import           Hanalyze.Model.Quantile (fitQuantile)
import           Hanalyze.Model.Regularized (RegFit (..),
                                           fitRidge, fitLasso, fitElasticNet)
import qualified Hanalyze.Model.RegularizedAdvanced as RA
import qualified Hanalyze.Stat.CV          as HCV
import           Hanalyze.Model.PCA     (PCAResult (..))
import           Hanalyze.Model.MDS     (MDSConfig (..), MDSMethod (..)
                                               , defaultMDS, MDSResult, runMDS)
import           Hanalyze.Model.HBM     (ModelP, dataSlots, dataIxSlots,
                                                 Distribution (Normal, HalfNormal),
                                                 sample, observe, reNormal, at, observeLMR,
                                                 lkjCorrCholesky, deterministic,
                                                 plateI, plateI_, plateForM_, (.#),
                                                 REff (..),
                                                 LMFamily (LMGaussian))

-- ---------------------------------------------------------------------------
-- GAM 高レベル (df|->) — Phase 70.6 F3
--
-- 'gamModel' は単変量・B-spline 固定・λ 手動だった。 'gam' / 'gamMulti' は **基底を選べ**
-- (B-spline / 自然3次 / 多項 / Fourier / RBF)、 **λ を GCV 自動選択**でき、 'lm'/'lmMulti'
-- と対称な命名で受ける (@df |-> gam cfg "x" "y"@ / @df |-> gamMulti cfg ["x1","x2"] "y"@)。 描画は第1予測子
-- 軸の平滑曲線 (多予測子では他を訓練平均に固定した偏依存曲線・band 非提供は GAM 共通)。
-- ---------------------------------------------------------------------------

-- | GAM の設定。 全項共通の基底 'gcBasis' と λ 方略 'gcLambda'。
data GAMConfig = GAMConfig
  { gcBasis  :: GAMBasis   -- ^ 各平滑項に使う基底 (全項共通)。
  , gcLambda :: GAMLambda  -- ^ ridge λ の決め方 ('FixedL' / 'GCV')。
  } deriving (Show)

-- | 既定設定: 3 次 B-spline・内部 6 ノット・λ は GCV 自動選択。
defaultGAMConfig :: GAMConfig
defaultGAMConfig = GAMConfig (BSplineB 3 6) GCV

-- | GAM spec (単/多予測子)。 @gam cfg "x" "y"@ または @gamMulti cfg ["x1","x2"] "y"@。
data GAMSpec = GAMSpec !GAMConfig ![Text] !Text

-- | @gam cfg xCol yCol@ — 単一予測子 GAM ('lm' と対)。
gam :: GAMConfig -> Text -> Text -> GAMSpec
gam cfg xn yn = GAMSpec cfg [xn] yn

-- | @gamMulti cfg xCols yCol@ — 多予測子 GAM ('lmMulti' と対)。 第1予測子を描画軸にする。
gamMulti :: GAMConfig -> [Text] -> Text -> GAMSpec
gamMulti = GAMSpec

instance Fit GAMSpec where
  type Fitted GAMSpec = GAMModelN
  fitEither (GAMSpec cfg xns yn) d = do
    xs <- mapM (`reqColV` d) xns
    y  <- reqColV yn d
    let xss   = map (V.fromList . LA.toList) xs
        yv    = V.fromList (LA.toList y)
        bases = [ gcBasis cfg | _ <- xns ]
        fit   = fitGAMAuto bases (gcLambda cfg) xss yv
    pure GAMModelN { gamNFit = fit, gamNXraws = xs, gamNNames = xns }

-- ===========================================================================
-- df |-> spec 統一 fit API (Phase 51)
-- ===========================================================================
--
-- 任意のデータ源 ('ColumnSource') から spec が指すモデルを学習する単一の動詞。
-- 新規の数値核は無く、 既存の fit 関数 (lmModel / glmModel / …) を列抽出で
-- 配線するだけの薄いエルゴノミック層。
--
-- * 二変量近道 (列名2つ): @lm "x" "y"@ / @glm fam link "x" "y"@ 等 (本節・51.2)。
-- * formula 多変量 (R 流): @lmF "y ~ x1 + x2"@ 等 (Phase 51.3)。
-- * HBM (手書き model): @hbm cfg model@ (Phase 51.4)。

-- | @df |-> spec = fitWith spec df@。 plot の @|>>@ と同列 (@infixl 1@)。
infixl 1 |->
(|->) :: (ColumnSource d, Fit spec) => d -> spec -> Fitted spec
d |-> spec = fitWith spec d

-- | @df |->! spec = fitIO spec df@ (進捗つき IO 学習動詞・Phase 61.4)。
-- 純粋動詞 '(|->)' との違いは**副作用 (進捗表示) の有無が動詞の選択に現れる**
-- こと: HBM のような重い学習は @|->!@ で stderr に進捗 1 行が出る。
-- 結果は同 cfg の @|->@ と**ビット一致** (seed 規約共有)。
infixl 1 |->!
(|->!) :: (ColumnSource d, Fit spec) => d -> spec -> IO (Fitted spec)
d |->! spec = fitIO spec d

-- --- 二変量近道 spec (列名 x, y) — Phase 51.2 -------------------------------

-- | 単回帰 spec。 @lm "x" "y"@。
data LMSpec       = LMSpec       !Text !Text
-- | 二変量 GLM spec。 @glm fam link "x" "y"@。
data GLMSpec      = GLMSpec      !Family !LinkFn !Text !Text
-- | 二変量 spline spec。 @spline kind knots "x" "y"@。
data SplineSpec   = SplineSpec   !SplineKind ![Double] !Text !Text
-- | 二変量 robust spec。 @robust est "x" "y"@。
data RobustSpec   = RobustSpec   !RobustEstimator !Text !Text
-- | 分位点回帰 spec。 @quantile taus "x" "y"@。
data QuantileSpec = QuantileSpec ![Double] !Text !Text

-- | @lm xCol yCol@ — 単回帰 ('LMModel') を当てはめる spec。
lm :: Text -> Text -> LMSpec
lm = LMSpec

-- | @glm fam link xCol yCol@ — 二変量 GLM ('GLMModel') の spec。
glm :: Family -> LinkFn -> Text -> Text -> GLMSpec
glm = GLMSpec

-- | @spline kind knots xCol yCol@ — spline 回帰 ('SplineModel') の spec。
spline :: SplineKind -> [Double] -> Text -> Text -> SplineSpec
spline = SplineSpec

-- | @rlm est xCol yCol@ — ロバスト回帰 ('RobustModel') の spec (R @MASS::rlm@)。
rlm :: RobustEstimator -> Text -> Text -> RobustSpec
rlm = RobustSpec

-- | @rq taus xCol yCol@ — 分位点回帰 ('QuantileModel') の spec (R @quantreg::rq@)。
rq :: [Double] -> Text -> Text -> QuantileSpec
rq = QuantileSpec

instance Fit LMSpec where
  type Fitted LMSpec = LMModel
  fitEither (LMSpec xn yn) d = lmModel <$> reqColV xn d <*> reqColV yn d
  predictorCols (LMSpec xn _) = [xn]
  responseCol   (LMSpec _ yn) = Just yn


-- --- 重み付き最小二乗 (WLS) spec — Phase 52.A6 -----------------------------
--
-- WLS は各観測に重み @wᵢ@ を付けて @Σ wᵢ(yᵢ − ŷᵢ)²@ を最小化する (不等分散・
-- 観測の信頼度差に。 ggplot @geom_smooth(method=lm, aes(weight=w))@ 相当)。
-- 数値核 = √w スケール OLS: @X_w = diag(√w)·X@, @y_w = diag(√w)·y@ を OLS すると
-- β̂ = (XᵀWX)⁻¹XᵀWy・s² = Σ wᵢ(yᵢ−x̂ᵢ)²/(n−p) が得られる。
--
-- ★描画の整合: grid 評価 ('svGrid' → 'confidenceBandAt') は評価点を**非スケール**
-- @[1, gx]@ で渡すので、 設計行列を**スケール済** ('lmDesign'=X_w) にしておけば
-- @se = t·√(s²·x₀ᵀ(XᵀWX)⁻¹x₀)@ = 正しい WLS pointwise CI が元 x スケールで出る。
-- 一方 'LMModel' の素の 'toPlot' (訓練点経路 'confidenceBand') は評価点がスケール済
-- 行になり中心も se も √wᵢ 倍に壊れる。 ゆえに専用ラッパ 'WeightedLMModel' を設け
-- 'toPlot' を **grid 経路 ('statModel')** に固定する (= 元データ散布図と整合)。

-- | 重み付き最小二乗 spec。 @weighted "w" (lm "x" "y")@ (重みは **df の列名**・全て @≥ 0@)。
data WeightedLMSpec = WeightedLMSpec !Text !LMSpec

-- | @weighted wCol (lm xCol yCol)@ — 重み**列** @wCol@ で WLS を当てはめる spec ラッパ。
--   重みは x / y と同一 'ColumnSource' の列名で受ける (statsmodels @wls(…, weights=col)@ /
--   R @weights=@ と同型)。 現状 'LMSpec' 専用 (WLS は LM 固有。 GLM の重みは別軸ゆえ対象外)。
weighted :: Text -> LMSpec -> WeightedLMSpec
weighted = WeightedLMSpec

instance Fit WeightedLMSpec where
  type Fitted WeightedLMSpec = WeightedLMModel
  fitEither (WeightedLMSpec wn (LMSpec xn yn)) d = do
    xs <- reqColV xn d
    ys <- reqColV yn d
    wv <- reqColV wn d
    let n  = LA.size xs
        ws = LA.toList wv
    if length ws /= n
      then Left ("weighted: 重み列 " <> show wn <> " の長さ " <> show (length ws)
                  <> " が観測数 " <> show n <> " と一致しません")
      else if any (< 0) ws
        then Left "weighted: 重みに負値が含まれます (全て ≥ 0 が必要)"
        else
          let sw  = map sqrt ws                                       -- √w (長さ n)
              dm0 = designMatrix (V.fromList (LA.toList xs))          -- [1, x] 非スケール
              dmW = LA.fromRows (zipWith LA.scale sw (LA.toRows dm0)) -- diag(√w)·X
              yW  = LA.fromList (zipWith (*) sw (LA.toList ys))       -- √w·y
              res = fitLMVec dmW yW
          in Right WeightedLMModel
               { wlmInner   = LMModel { lmDesign = dmW, lmResult = res, lmXraw = xs }
               , wlmWeights = ws
               , wlmY       = LA.toList ys
               }

-- | 重み付き R²。 statsmodels WLS @rsquared@ と一致: @1 − Σwᵢ(yᵢ−ŷᵢ)² / Σwᵢ(yᵢ−ȳ_w)²@
--   (中心化は重み付き平均 @ȳ_w = Σwᵢyᵢ/Σwᵢ@)。 ★スケール空間の素朴な R² とは中心化が
--   異なる (= 内側 LM の 'svCoefR2' をそのまま使うと不一致になる罠)。
weightedR2 :: [Double] -> [Double] -> [Double] -> Double
weightedR2 ws ys yhats =
  let sw   = sum ws
      ybar = sum (zipWith (*) ws ys) / sw
      ssr  = sum (zipWith3 (\w y yh -> w * (y - yh) ^ (2 :: Int)) ws ys yhats)
      tss  = sum (zipWith  (\w y    -> w * (y - ybar) ^ (2 :: Int)) ws ys)
  in 1 - ssr / tss

-- --- 透過標準化ラッパ (自動逆変換) — Phase 70.3 項目 C ----------------------
--
-- スケール敏感なモデル (とくに距離ベース kNN) は予測子の z-score 標準化が要る。
-- 'standardized' / 'standardizedY' は spec を**透過的に**ラップし、 学習は標準化空間で
-- 行いつつ図・予測は元スケールへ自動逆変換する (tidymodels @step_normalize@ /
-- sklearn @Pipeline@ 相当)。 標準化対象列は spec 自身が 'predictorCols' / 'responseCol'
-- で返すので**列名を二重に書かない**。 内部標準化済 (GP/Reg/PCA/PLS) や木系は
-- @predictorCols = []@ ゆえラッパが 'Left' で誤用を弾く (= 二重標準化バグ回避)。

-- | spec を透過標準化でラップする (@Bool@ = 応答 y も標準化するか)。
--   構成子は直接使わず 'standardized' / 'standardizedY' から作る。
data StandardizedSpec spec = StandardizedSpec !Bool !spec

-- | @standardized spec@ — **予測子列のみ** z-score 標準化して内側 @spec@ を学習し、
--   描画・予測は**元スケール**で返す透過ラッパ。 距離ベース kNN
--   (@knnReg@ / @knnCls@) が本命。 標準化対象列は spec の 'predictorCols' から
--   取得する (列名を二重に書かない)。 被せる意味の無い spec
--   (内部標準化済 GP\/Reg\/PCA\/PLS・スケール不変な木系) は @predictorCols = []@
--   ゆえ 'fitEither' が 'Left' で誤用を弾く。
--
--   @df |-> standardized (knnReg 5 [\"x1\",\"x2\"] \"y\")@
standardized :: spec -> StandardizedSpec spec
standardized = StandardizedSpec False

-- | @standardizedY spec@ — 予測子 **+ 応答 y** を標準化する版 (多目的最適化向け・
--   opt-in)。 y 列名は spec の 'responseCol' から取得し、 予測・図は元スケールへ
--   逆変換する。 応答がクラスラベル / family・link 拘束で標準化不可な spec
--   (@responseCol = Nothing@: 分類・GLM) に付けると 'fitEither' が 'Left' (誤用ガード)。
standardizedY :: spec -> StandardizedSpec spec
standardizedY = StandardizedSpec True

instance Fit spec => Fit (StandardizedSpec spec) where
  type Fitted (StandardizedSpec spec) = StandardizedModel (Fitted spec)
  fitEither (StandardizedSpec stdY inner) d =
    fitStd (predictorCols inner) (responseCol inner) stdY inner d
  -- 入れ子ラップは更なる標準化対象を持たない (二重ラップ無効化)。
  predictorCols _ = []
  responseCol   _ = Nothing

-- | 透過標準化の本体: 予測子列 (と opt-in で応答 y) を z-score 標準化した派生
--   'ColumnSource' を作り、 内側 spec をそれに当てはめる。 逆変換用の (μ,σ) を保持。
--   失敗 (空 predictorCols / 標準化不能応答への @standardizedY@ / 列欠落) は 'Left'。
fitStd :: (ColumnSource d, Fit spec)
       => [Text]       -- ^ 標準化対象の予測子列 (spec の 'predictorCols')。
       -> Maybe Text   -- ^ 応答列 (spec の 'responseCol')。 散布用 + y 標準化に使う。
       -> Bool         -- ^ y も標準化するか ('standardizedY' なら True)。
       -> spec
       -> d
       -> Either String (StandardizedModel (Fitted spec))
fitStd predCols mRespCol stdY inner d
  | null predCols =
      Left "standardized: この spec は標準化対象の予測子列を持ちません \
           \(predictorCols = [])。 内部標準化済 (GP/Reg/PCA/PLS) か木系 (スケール不変) \
           \ゆえ透過標準化は無意味です。"
  | stdY, Nothing <- mRespCol =
      Left "standardizedY: この spec は標準化可能な応答列を持ちません \
           \(responseCol = Nothing)。 分類 (クラスラベル) や GLM (family/link 拘束) の \
           \応答は標準化できません。 X のみ標準化する standardized を使ってください。"
  | otherwise = do
      xm <- reqColsM predCols d                       -- n × p (予測子行列)
      let xStdr = fitStandardizer xm
          xmZ   = applyStandardizer xStdr xm
          predZ = zip predCols (map LA.toList (LA.toColumns xmZ))
      (yStd, yOverride) <-
        if stdY
          then case mRespCol of
                 Nothing -> Left "fitStd: internal — stdY without responseCol"  -- ガード済で到達せず
                 Just yc -> do
                   yv <- reqColV yc d
                   let ys         = LA.toList yv
                       (muY, sdY) = meanSdSafe ys
                   Right (Just (muY, sdY), [(yc, [ (v - muY) / sdY | v <- ys ])])
          else Right (Nothing, [])
      -- 派生 ColumnSource: 元の全数値列を assoc 化し、 標準化列で同名上書き。
      let derived = overrideAssoc (numericCols d) (predZ ++ yOverride)
      innerM <- fitEither inner derived
      -- 単変量散布用に元スケール (x,y) を保持 (予測子 1 列 + 応答列ありの時のみ)。
      let mTrain = case (predCols, mRespCol) of
                     ([xc], Just yc)
                       | Just xs <- lookupCol xc d
                       , Just ys <- lookupCol yc d -> Just (xs, ys)
                     _ -> Nothing
      Right StandardizedModel { smInner = innerM, smXStd = xStdr
                              , smYStd = yStd,   smTrain = mTrain }

-- | assoc を別 assoc で同名上書き (上書きキーは元の位置・値だけ差替え、 元に
--   無いキーは末尾追加)。 透過標準化の派生 'ColumnSource' 生成に使う。
overrideAssoc :: [(Text, [Double])] -> [(Text, [Double])] -> [(Text, [Double])]
overrideAssoc base ovr =
  let ovrMap   = Map.fromList ovr
      baseKeys = map fst base
  in [ (k, Map.findWithDefault v k ovrMap) | (k, v) <- base ]
     ++ [ kv | kv@(k, _) <- ovr, k `notElem` baseKeys ]

-- | 標本平均と (n-1) 標準偏差。 σ≈0 (定数列) は 1 に丸めて 0 割を回避
--   ('Stat.Standardize' の 'fitStandardizer' と同流儀)。
meanSdSafe :: [Double] -> (Double, Double)
meanSdSafe xs =
  let n = length xs
  in if n == 0 then (0, 1)
     else let mu = sum xs / fromIntegral n
          in if n == 1 then (mu, 1)
             else let var = sum [ (x - mu) ^ (2 :: Int) | x <- xs ] / fromIntegral (n - 1)
                      sd0 = sqrt var
                  in (mu, if sd0 < 1e-12 then 1 else sd0)

instance Fit GLMSpec where
  type Fitted GLMSpec = GLMModel
  fitEither (GLMSpec fam lnk xn yn) d =
    glmModel fam lnk <$> reqColV xn d <*> reqColV yn d
  predictorCols (GLMSpec _ _ xn _) = [xn]
  -- responseCol = Nothing (既定)。 GLM の応答は family/link でスケールが拘束される
  -- (count\/binary\/正値) ため標準化は不正。 X 標準化 ('standardized') のみ許す。

instance Fit SplineSpec where
  type Fitted SplineSpec = SplineModel
  fitEither (SplineSpec kind knots xn yn) d =
    splineModel kind knots <$> reqColV xn d <*> reqColV yn d
  predictorCols (SplineSpec _ _ xn _) = [xn]
  responseCol   (SplineSpec _ _ _ yn) = Just yn

instance Fit RobustSpec where
  type Fitted RobustSpec = RobustModel
  fitEither (RobustSpec est xn yn) d =
    robustModel est <$> reqColV xn d <*> reqColV yn d
  predictorCols (RobustSpec _ xn _) = [xn]
  responseCol   (RobustSpec _ _ yn) = Just yn

instance Fit QuantileSpec where
  type Fitted QuantileSpec = QuantileModel
  fitEither (QuantileSpec taus xn yn) d =
    quantileModel taus <$> reqColV xn d <*> reqColV yn d
  predictorCols (QuantileSpec _ xn _) = [xn]
  responseCol   (QuantileSpec _ _ yn) = Just yn

-- ===========================================================================
-- カーネル法ファミリ統合 — GP / KRR / RFF を 1 spec @gp@ に — Phase 70.5 項目 E
--
-- 4 つの実装は 2 軸で尽くされる: {厳密 (Gram) | 近似 (RFF)} × {分布あり (GP) |
-- 分布なし (Ridge 点)}。 本体は GP であり、 KRR の予測 ≡ GP 事後平均 (λ = σ_n²・
-- mean のみ)・RFF は GP の低ランク近似。 ゆえに内部カーネル評価を GP.hs に一本化し、
-- 4 象限を 'GPMethod' の明示コンストラクタで選ぶ。 'lm'/'lmMulti' 規約と対称な動詞
-- 'gp'/'gpMulti' (gpMulti は E3)。
-- ===========================================================================

-- | 'gp' / 'gpMulti' の設定。 カーネル種 × 象限 × ハイパラ方略。
data GPConfig = GPConfig
  { gpcKernel :: !Kernel        -- ^ RBF / Matern52 / Periodic (GP.hs 由来・新型は作らない)。
  , gpcMethod :: !GPMethod      -- ^ 象限 + (RFF 時) 近似次元・seed。
  , gpcHyper  :: !HyperStrategy -- ^ ハイパラの決め方 + 固定時の値。
  }

-- | 既定: 厳密 GP・RBF・周辺尤度自動。 @df |-> gp defaultGP "x" "y"@。
defaultGP :: GPConfig
defaultGP = GPConfig RBF Gp AutoMarginalLik

-- | 二変量カーネル回帰 spec。 @gp cfg "x" "y"@ ('lm' と対称)。
data GPSpec = GPSpec !GPConfig !Text !Text

-- | @gp cfg xCol yCol@ — 統合カーネル回帰 ('GPRegModel') を当てはめる spec。
gp :: GPConfig -> Text -> Text -> GPSpec
gp = GPSpec

-- | RFF 非対応カーネルのフォールバック通知 (RFF は定常カーネル限定 = Bochner)。
-- Periodic は定常だがスペクトル密度未実装、 Linear/Poly は非定常ゆえ RFF 不可。
gpNonRffMsg :: Kernel -> String
gpNonRffMsg ker =
  "[gp] " <> show ker <> " カーネルは RFF 近似不可"
  <> " (Periodic はスペクトル密度未実装・Linear/Poly は非定常 = Bochner)。"
  <> " 厳密象限へフォールバックします。"

-- | RFF 近似象限 + RFF 非対応カーネル (Periodic/Linear/Poly) は厳密象限へ
-- フォールバック ('log' 通知)。 定常カーネル (RBF/Matern52) はそのまま。
gpResolveMethod :: Kernel -> GPMethod -> GPMethod
gpResolveMethod ker (GpRff  _ _) | not (gpRffSupported ker) = Trace.trace (gpNonRffMsg ker) Gp
gpResolveMethod ker (KrrRff _ _) | not (gpRffSupported ker) = Trace.trace (gpNonRffMsg ker) Krr
gpResolveMethod _   m            = m

-- | RFF (Random Fourier Features) が近似可能なカーネルか (= 定常 + スペクトル密度実装済)。
gpRffSupported :: Kernel -> Bool
gpRffSupported RBF      = True
gpRffSupported Matern52 = True
gpRffSupported _        = False   -- Periodic/Linear/Poly

-- | ハイパラ方略を解決して 'GPParams' を得る。
gpResolveHyper :: HyperStrategy -> Kernel -> [Double] -> [Double] -> GPParams
gpResolveHyper (FixedHyper p)  _   _  _  = p
gpResolveHyper AutoMarginalLik ker xs ys = GP.optimizeGP ker xs ys (GP.initParamsFromData xs ys)
gpResolveHyper AutoCV          ker xs ys = GP.autoCVHyperGP ker xs ys

-- | 象限ごとの予測子を組む。 渡される @method@ は Periodic フォールバック後なので、
-- RFF 象限 (GpRff/RidgeRff) に来るカーネルは RBF / Matern52 に限られる。
gpBuildPredict
  :: GPMethod -> Kernel -> GPParams -> [Double] -> [Double]
  -> ([Double] -> ([Double], Maybe [Double]))
gpBuildPredict method ker params xs ys = case method of
  Gp  -> \q -> let r = GP.fitGP (GP.GPModel ker params) xs ys q
               in (gpMean r, Just (gpVar r))
  Krr -> \q -> let r = GP.fitGP (GP.GPModel ker params) xs ys q
               in (gpMean r, Nothing)                 -- KRR ≡ GP 事後平均・帯は捨てる
  GpRff dDim seed ->
    let feats = gpSampleFeats ker dDim seed
        fit   = RFF.rffGP feats xs ys (sqrt (max 1e-12 (gpNoiseVar params)))
    in \q -> let ps = RFF.predictRFFGP fit q
             in (map fst ps, Just (map snd ps))
  KrrRff dDim seed ->
    let feats = gpSampleFeats ker dDim seed
        fit   = RFF.rffRidge feats xs ys (max 1e-12 (gpNoiseVar params))
    in \q -> (RFF.predictRFFRidge fit q, Nothing)
  where
    ell = gpLengthScale params
    sf  = sqrt (max 1e-12 (gpSignalVar params))
    gpSampleFeats Matern52 dDim seed = RFF.sampleRFFMatern52Pure dDim ell sf seed
    gpSampleFeats _        dDim seed = RFF.sampleRFFRBFPure      dDim ell sf seed

instance Fit GPSpec where
  type Fitted GPSpec = GPRegModel
  fitEither (GPSpec cfg xn yn) d = do
    xv <- reqColV xn d
    yv <- reqColV yn d
    let xs     = LA.toList xv
        ys     = LA.toList yv
        ker    = gpcKernel cfg
        method = gpResolveMethod ker (gpcMethod cfg)
        params = gpResolveHyper (gpcHyper cfg) ker xs ys
    Right GPRegModel
      { gprMethod  = method
      , gprKernel  = ker
      , gprParams  = params
      , gprXraw    = xv
      , gprY       = yv
      , gprPredict = gpBuildPredict method ker params xs ys
      }


-- ---------------------------------------------------------------------------
-- 多変量 gpMulti (E3)。 'gamMulti' / 'GAMModelN' と同型 = 第1予測子を描画軸に、
-- 他予測子を訓練平均に固定した偏依存曲線 ('SingleVarModel' として扱う)。 4 象限の
-- MV 実装: Gp/Ridge=fitGPMV (Ridge は mean のみ)、 GpRff=rffGPMV、 RidgeRff=rffRidgeMV。
-- ---------------------------------------------------------------------------

-- | 多変量カーネル回帰 spec。 @gpMulti cfg ["x1","x2"] "y"@ ('lmMulti' と対称)。
data GPMultiSpec = GPMultiSpec !GPConfig ![Text] !Text

-- | @gpMulti cfg xCols yCol@ — 多予測子カーネル回帰 ('lmMulti' と対)。 第1予測子を描画軸に。
gpMulti :: GPConfig -> [Text] -> Text -> GPMultiSpec
gpMulti = GPMultiSpec

-- | MV ハイパラ方略を解決 ('gpResolveHyper' の行列版)。
gpResolveHyperMV :: HyperStrategy -> Kernel -> LA.Matrix Double -> LA.Vector Double -> GPParams
gpResolveHyperMV (FixedHyper p)  _   _ _ = p
gpResolveHyperMV AutoMarginalLik ker x y = GP.optimizeGPMV ker x y (GP.initParamsFromDataMV x y)
gpResolveHyperMV AutoCV          ker x y = GP.autoCVHyperGPMV ker x y

-- | 象限ごとの MV 予測子。 渡される @method@ は Periodic フォールバック後。
gpBuildPredictMV
  :: GPMethod -> Kernel -> GPParams -> LA.Matrix Double -> [Double]
  -> (LA.Matrix Double -> ([Double], Maybe [Double]))
gpBuildPredictMV method ker params trainX ys = case method of
  Gp  -> \tx -> let r = GP.fitGPMV (GP.GPModel ker params) trainX yV tx
                in (LA.toList (GP.gpmvMean r), Just (LA.toList (GP.gpmvVar r)))
  Krr -> \tx -> let r = GP.fitGPMV (GP.GPModel ker params) trainX yV tx
                in (LA.toList (GP.gpmvMean r), Nothing)
  GpRff dDim seed ->
    let feats = featsMV dDim seed
        fit   = RFF.rffGPMV feats trainX ys (sqrt (max 1e-12 (gpNoiseVar params)))
    in \tx -> let ps = RFF.predictRFFGPMV fit tx in (map fst ps, Just (map snd ps))
  KrrRff dDim seed ->
    let feats = featsMV dDim seed
        fit   = RFF.rffRidgeMV feats trainX ys (max 1e-12 (gpNoiseVar params))
    in \tx -> (RFF.predictRFFRidgeMV fit tx, Nothing)
  where
    yV  = LA.fromList ys
    pP  = LA.cols trainX
    ell = gpLengthScale params
    sf  = sqrt (max 1e-12 (gpSignalVar params))
    featsMV dDim seed = case ker of
      Matern52 -> RFF.sampleRFFMatern52MVPure pP dDim ell sf seed
      _        -> RFF.sampleRFFRBFMVPure      pP dDim ell sf seed

instance Fit GPMultiSpec where
  type Fitted GPMultiSpec = GPRegModelN
  fitEither (GPMultiSpec cfg xns yn) d = do
    xs <- mapM (`reqColV` d) xns
    yv <- reqColV yn d
    let trainX = LA.fromColumns xs        -- n × p
        ys     = LA.toList yv
        ker    = gpcKernel cfg
        method = gpResolveMethod ker (gpcMethod cfg)
        params = gpResolveHyperMV (gpcHyper cfg) ker trainX yv
    pure GPRegModelN
      { gprnMethod  = method
      , gprnKernel  = ker
      , gprnParams  = params
      , gprnXraws   = xs
      , gprnNames   = xns
      , gprnYraw    = yv
      , gprnPredict = gpBuildPredictMV method ker params trainX ys
      }

-- ===========================================================================
-- 罰則付き回帰の高レベル df|-> 化 — Phase 70.7 項目 G (G1)
--
-- Ridge / Lasso / Elastic Net / MCP / SCAD / Adaptive Lasso / Group Lasso を
-- 1 spec `regularized` (+近道 `ridge`/`lasso`/`elasticNet`) に統合。`lmMulti` と対称な
-- 多予測子のみ (罰則回帰は本質的に多特徴ゆえ単回帰版は作らない)。λ は `FixedLambda` /
-- `LambdaLOOCV` (Ridge 専用閉形式) / `LambdaCV` / `LambdaCV1SE` (k-fold・seed 純粋)。
-- X は内部で標準化し係数を元スケールに戻す (MCP/SCAD の非凸 CD が標準化を要件とするため
-- ・glmnet 慣例)。 [[selectLambdaCV]] の純粋化 (`selectLambdaCVPure`/`HCV.kFold` PrimMonad 化) を土台にする。
-- ===========================================================================

-- | λ の決め方 (GP の 'HyperStrategy' と同型)。
data LambdaStrat
  = FixedLambda !Double        -- ^ 固定 λ。
  | LambdaLOOCV                -- ^ 閉形式 LOOCV (★Ridge 等の線形平滑器専用・他は Left)。
  | LambdaCV    !Int !Word32   -- ^ k-fold CV (k, seed)・best (CV-MSE 最小)。
  | LambdaCV1SE !Int !Word32   -- ^ k-fold CV + 1-SE rule (R glmnet 既定推奨)。
  deriving (Eq, Show)

-- | 罰則回帰の設定。
data RegConfig = RegConfig { rcMethod :: !RegMethod, rcLambda :: !LambdaStrat }

-- | 既定 (CV で λ 選択・5-fold・seed 42)。
defaultRidge, defaultLasso :: RegConfig
defaultRidge = RegConfig Ridge (LambdaCV 5 42)
defaultLasso = RegConfig Lasso (LambdaCV 5 42)

-- | 罰則回帰 spec。 @regularized cfg ["x1","x2"] "y"@。
data RegSpec = RegSpec !RegConfig ![Text] !Text

-- | @regularized cfg xCols yCol@。 多予測子のみ。
regularized, regularizedMulti :: RegConfig -> [Text] -> Text -> RegSpec
regularized      = RegSpec
regularizedMulti = RegSpec               -- 同一関数の別名 (Q4)

-- | 近道 (λ 方略は既定 CV)。 bare = 多変量、 @*Multi@ は同一関数の別名。
ridge, ridgeMulti, lasso, lassoMulti :: [Text] -> Text -> RegSpec
ridge      = RegSpec defaultRidge
ridgeMulti = ridge
lasso      = RegSpec defaultLasso
lassoMulti = lasso

-- | Elastic Net 近道 (α 指定・λ 方略は既定 CV)。
elasticNet, elasticNetMulti :: Double -> [Text] -> Text -> RegSpec
elasticNet a    = RegSpec (RegConfig (ElasticNet a) (LambdaCV 5 42))
elasticNetMulti = elasticNet

-- 列を平均0・分散1 (population sd・guard 1e-12) に標準化。 (Xstd, means, sds)。
regStandardize :: LA.Matrix Double -> (LA.Matrix Double, [Double], [Double])
regStandardize x =
  let n     = fromIntegral (LA.rows x)
      cols  = LA.toColumns x
      means = [ LA.sumElements c / n | c <- cols ]
      sds   = [ max 1e-12 (sqrt (sum [ (v - m) ^ (2 :: Int) | v <- LA.toList c ] / n))
              | (c, m) <- zip cols means ]
      stdCol c m s = LA.cmap (\v -> (v - m) / s) c
  in (LA.fromColumns (zipWith3 stdCol cols means sds), means, sds)

-- log 等間隔 λ grid。
regLogspace :: Double -> Double -> Int -> [Double]
regLogspace lo hi k
  | k <= 1    = [lo]
  | otherwise = [ exp (log lo + (log hi - log lo) * fromIntegral i / fromIntegral (k - 1))
                | i <- [0 .. k - 1] ]

-- データ駆動 λ grid (標準化 X・中心化 y 前提)。
regLambdaGrid :: RegMethod -> LA.Matrix Double -> LA.Vector Double -> [Double]
regLambdaGrid method x y =
  let n     = fromIntegral (LA.rows x)
      lmaxL = max 1e-8 (maximum [ abs (LA.dot c y) | c <- LA.toColumns x ] / n)
  in case method of
       Ridge -> regLogspace (n * 1e-3) (n * 1e2)   50   -- ridge は XᵀX スケール (1/n 無し)
       _     -> regLogspace (lmaxL * 1e-3) lmaxL    50

-- 群 ID (列順) → fitGroupLasso の [[列 index]] パーティション (群の初出順)。
regGroupsToPartition :: [Int] -> Int -> [[Int]]
regGroupsToPartition gids p =
  let distinct []       = []
      distinct (g : gs) = g : distinct (filter (/= g) gs)
  in [ [ j | (j, g) <- zip [0 .. p - 1] gids, g == gid ] | gid <- distinct gids ]

-- Adaptive Lasso = 列リスケール法。 penalty λΣwⱼ|βⱼ| ⇔ 列 Xⱼ を 1/wⱼ 倍 → Lasso → βⱼ = β'ⱼ/wⱼ。
regAdaptiveLasso :: Double -> Double -> LA.Matrix Double -> LA.Vector Double -> RegFit
regAdaptiveLasso gamma lam x y =
  let w    = RA.adaptiveWeightsFromOLS gamma x y
      xr   = LA.fromColumns (zipWith (\c wj -> LA.scale (1 / wj) c)
                               (LA.toColumns x) (LA.toList w))
      f    = fitLasso lam xr y 1000 1e-4
      bAdj = LA.fromList (zipWith (/) (LA.toList (rfBeta f)) (LA.toList w))
      yh   = x LA.#> bAdj
  in f { rfBeta = bAdj, rfYHat = yh, rfResid = y - yh }

-- 1 つの RegMethod を λ で当てはめる (標準化 X・中心化 y 前提)。
regFitAt :: RegMethod -> Double -> LA.Matrix Double -> LA.Vector Double -> RegFit
regFitAt method lam x y = case method of
  Ridge            -> fitRidge lam x y
  Lasso            -> fitLasso lam x y 1000 1e-4
  ElasticNet a     -> fitElasticNet (a * lam) ((1 - a) * lam) x y 1000 1e-4
  MCP g            -> RA.fitMCP lam g x y 1000 1e-4
  SCAD a           -> RA.fitSCAD lam a x y 1000 1e-4
  AdaptiveLasso g  -> regAdaptiveLasso g lam x y
  GroupLasso gids  -> RA.fitGroupLasso lam (regGroupsToPartition gids (LA.cols x)) x y 1000 1e-4

-- generic k-fold CV (全 RegMethod 対応・pure 'HCV.kFold' を seed で回す)。 (bestλ, 1seλ, λ grid, mean MSE)。
regKFoldCV :: RegMethod -> Int -> Word32 -> [Double] -> LA.Matrix Double -> LA.Vector Double
           -> (Double, Double, [Double], [Double])
regKFoldCV method k seed lams x y =
  let folds = runST (initialize (V.singleton seed) >>= HCV.kFold k (LA.rows x))
      sub idx = ( x LA.? idx
                , LA.fromList [ y `LA.atIndex` i | i <- idx ] )
      foldMSE lam (tr, te) =
        let (xTr, yTr) = sub tr
            (xTe, yTe) = sub te
            fit = regFitAt method lam xTr yTr
            r   = yTe - (xTe LA.#> rfBeta fit)
        in LA.sumElements (LA.cmap (^ (2 :: Int)) r) / fromIntegral (max 1 (length te))
      perLam lam =
        let scores = [ foldMSE lam f | f@(_, te) <- folds, not (null te) ]
            nF = fromIntegral (max 1 (length scores))
            m  = sum scores / nF
            v  = sum [ (s - m) ^ (2 :: Int) | s <- scores ] / max 1 (nF - 1)
        in (lam, m, sqrt (v / nF))
      stats = map perLam lams
      pick a@(_, ma, _) b@(_, mb, _) = if ma <= mb then a else b
      (bestL, bestM, bestSE) = foldr1 pick stats
      thr   = bestM + bestSE
      oneSe = maximum (bestL : [ lam | (lam, m, _) <- stats, m <= thr ])
  in (bestL, oneSe, lams, [ m | (_, m, _) <- stats ])

-- λ 方略を解決して (選択 λ, 標準化空間 fit, CV/LOOCV パス) を返す。
regResolve :: RegConfig -> LA.Matrix Double -> LA.Vector Double
           -> Either String (Double, RegFit, Maybe ([Double], [Double]))
regResolve (RegConfig method strat) x y = case strat of
  FixedLambda lam -> Right (lam, regFitAt method lam x y, Nothing)
  LambdaLOOCV -> case method of
    Ridge ->
      let lams   = regLambdaGrid Ridge x y
          scores = [ RFF.loocvFromPhi x y lam | lam <- lams ]
          best   = fst (foldr1 (\a b -> if snd a <= snd b then a else b) (zip lams scores))
      in Right (best, regFitAt Ridge best x y, Just (lams, scores))
    _ -> Left "LambdaLOOCV は線形平滑器 (Ridge) 専用です。 L1/非凸は LambdaCV を使ってください"
  LambdaCV k seed ->
    let lams = regLambdaGrid method x y
        (best, _, gl, ms) = regKFoldCV method k seed lams x y
    in Right (best, regFitAt method best x y, Just (gl, ms))
  LambdaCV1SE k seed ->
    let lams = regLambdaGrid method x y
        (_, one, gl, ms) = regKFoldCV method k seed lams x y
    in Right (one, regFitAt method one x y, Just (gl, ms))

instance Fit RegSpec where
  type Fitted RegSpec = RegModel
  fitEither (RegSpec cfg xns yn) d = do
    xRaw <- reqColsM xns d
    yv   <- reqColV yn d
    case rcMethod cfg of
      GroupLasso gids | length gids /= LA.cols xRaw ->
        Left ("GroupLasso: 群 ID の長さ " <> show (length gids)
              <> " が列数 " <> show (LA.cols xRaw) <> " と一致しません")
      _ -> Right ()
    let (xStd, means, sds) = regStandardize xRaw
        n    = fromIntegral (LA.rows xRaw)
        ybar = LA.sumElements yv / n
        yC   = LA.cmap (subtract ybar) yv
    (lam, fitStd, mbCV) <- regResolve cfg xStd yC
    let betaOrig  = zipWith (/) (LA.toList (rfBeta fitStd)) sds   -- βⱼ = β̃ⱼ / sⱼ
        intercept = ybar - sum (zipWith (*) betaOrig means)
    Right RegModel
      { rmgMethod    = rcMethod cfg
      , rmgNames     = xns
      , rmgLambda    = lam
      , rmgIntercept = intercept
      , rmgCoefs     = betaOrig
      , rmgFitStd    = fitStd
      , rmgCVPath    = mbCV
      , rmgXraw      = xRaw
      , rmgYraw      = yv
      }

-- | 罰則化回帰 ('RegModel') の case (行) bootstrap 係数サマリ。
--
-- 各 replicate で行を再標本化し、 **λ は full-fit で選択済の 'rmgLambda' に固定**
-- (CV を再実行しない) して 'regFitAt' で refit、 元スケール係数を集めて
-- 'bootCoefRows' で要約する。 標準化・中心化は replicate ごとに再計算する。
--
-- ⚠ これは **penalized の bootstrap percentile 区間であって有意性検定ではない**。
-- Lasso 等の変数選択を伴う罰則化では post-selection inference が別問題として
-- 存在し、 単純な percentile 区間はカバレッジを保証しない (selection の不確実性は
-- λ 固定 bootstrap だけでは捉えきれない)。 探索的な不確実性目安として使うこと。
instance HasCoefBoot RegModel where
  coefSummaryBoot seed b m =
    let xRaw    = rmgXraw m
        yv      = rmgYraw m
        n       = LA.rows xRaw
        idxSets = resampleRows seed b n
        repBeta idxs =
          let xr            = xRaw LA.? idxs
              yr            = LA.fromList [ yv `LA.atIndex` i | i <- idxs ]
              (xStd, _, sds) = regStandardize xr
              ybar          = LA.sumElements yr / fromIntegral (max 1 n)
              yC            = LA.cmap (subtract ybar) yr
              fitStd        = regFitAt (rmgMethod m) (rmgLambda m) xStd yC
          in zipWith (/) (LA.toList (rfBeta fitStd)) sds
        reps = map repBeta idxSets
    in bootCoefRows (rmgNames m) (rmgCoefs m) reps

-- | 罰則化回帰 ('RegModel') の統一要約玄関。
--
-- ⚠ これは bootstrap percentile (seed=42 / B=2000) であって**有意性検定ではない**
-- (post-selection inference は別問題。 'HasCoefBoot' RegModel の注記参照)。
-- 'HasCoefBoot' RegModel が Fit 層にあるため、 'HasReport' instance もここに置く。
instance HasReport RegModel where
  modelReport m = CoefReport (coefSummaryBoot 42 2000 m)

-- --- 行列入力モデルの高レベル化 spec (列名リスト) — Phase 70.A -------------
--
-- PCA / PLS など「行列入力」モデルも 'df |-> spec' で当てられるように、 列名リストを
-- 取る spec + 'Fit' instance を設ける ('reqColsM' で列名 → 行列)。 行列直叩き
-- ('pca' / 'fitPLS') は低レベル退避。 結果型 ('PCAResult' / 'PLSFit') は既存のまま。

-- | PCA spec。 @pca std mK ["x1","x2",…]@ (std = 標準化方針・mK = 保持成分数)。
data PCASpec = PCASpec !PCAStandardize !(Maybe Int) ![Text]

-- | @pca std mK cols@ — 列 @cols@ で PCA ('PCAResult') を当てる spec。
--   @df |-> pca CenterScale (Just 3) ["x1","x2","x3"]@。
pca :: PCAStandardize -> Maybe Int -> [Text] -> PCASpec
pca = PCASpec

instance Fit PCASpec where
  type Fitted PCASpec = PCAResult
  fitEither (PCASpec std mK cols) d = PCALow.pca std mK <$> reqColsM cols d

-- Phase 75.21: MDS (多次元尺度構成法) 高レベル spec。 教師なし変換ゆえ PCA と同様
-- @df |->@ に乗り、 **モデル型 'MDSResult'** (PCAResult 同格) を返す。 群色付けのため
-- 'toFrame' で **元データ (factor 温存)** を保持する (数値源なら数値列を再構築)。
data MDSSpec = MDSSpec !MDSConfig ![Text]

-- | @mds cfg cols@ — 列 @cols@ で MDS ('MDSResult') を当てる spec。
--   @m = df |-> mds defaultMDS ["x1","x2","x3"]@ → @noDf |>> toPlot m@ (単色散布)。
--   Sammon は @mds defaultMDS { mdsMethod = MDSSammon } cols@。
mds :: MDSConfig -> [Text] -> MDSSpec
mds = MDSSpec

instance Fit MDSSpec where
  type Fitted MDSSpec = MDSResult
  fitEither (MDSSpec cfg cols) d = runMDS cfg (toFrame d) cols

-- | PLS spec。 @pls cfg ["x1","x2"] ["y1","y2"]@ (X 列・Y 列を分けて指定)。
data PLSSpec = PLSSpec !PLSConfig ![Text] ![Text]

-- | @pls cfg xcols ycols@ — X 列・Y 列で PLS ('PLSFit') を当てる spec。
--   @df |-> pls defaultPLS ["x1","x2"] ["y1","y2"]@。
pls :: PLSConfig -> [Text] -> [Text] -> PLSSpec
pls = PLSSpec

instance Fit PLSSpec where
  type Fitted PLSSpec = PLSFit
  fitEither (PLSSpec cfg xcols ycols) d = do
    x <- reqColsM xcols d
    y <- reqColsM ycols d
    either (Left . T.unpack) Right (fitPLS cfg x y)

-- | 判別分析 (LDA) spec。 @lda ["x1","x2"] "class"@ (特徴列 + クラス列・クラスは整数化)。
data LDASpec = LDASpec ![Text] !Text

-- | @lda featureCols classCol@ — LDA ('DiscriminantFit') を当てる spec。
--   クラス列は数値を 'round' で整数ラベル化する。
lda :: [Text] -> Text -> LDASpec
lda = LDASpec

instance Fit LDASpec where
  type Fitted LDASpec = DiscriminantFit
  fitEither (LDASpec feats clsCol) d = do
    x  <- reqColsM feats d
    yv <- reqColV clsCol d
    let yInt = V.fromList (map round (LA.toList yv)) :: V.Vector Int
    either (Left . T.unpack) Right (fitLDA x yInt)
  predictorCols (LDASpec feats _) = feats   -- 特徴標準化は可 (応答=クラスゆえ y は標準化せず)

-- | 正準相関分析 (CCA) spec。 @ccaOf ["x1","x2"] ["y1","y2"]@ (2 ブロックの列)。
data CCASpec = CCASpec ![Text] ![Text]

-- | @ccaOf xcols ycols@ — CCA ('CCAFit') を当てる spec (CCAFit は現状 toPlot 非対象)。
ccaOf :: [Text] -> [Text] -> CCASpec
ccaOf = CCASpec

instance Fit CCASpec where
  type Fitted CCASpec = CCAFit
  fitEither (CCASpec xcols ycols) d = cca <$> reqColsM xcols d <*> reqColsM ycols d

-- | ラベル列を整数 ('round') の 'VU.Vector Int' で引く (分類器の y)。
reqLabelI :: ColumnSource d => Text -> d -> Either String (VU.Vector Int)
reqLabelI n d = VU.fromList . map round . LA.toList <$> reqColV n d

-- | ラベル列を 'VU.Vector Double' で引く (回帰の y)。
reqLabelD :: ColumnSource d => Text -> d -> Either String (VU.Vector Double)
reqLabelD n d = VU.fromList . LA.toList <$> reqColV n d

-- --- 教師あり ML 分類器/回帰器の高レベル化 spec (特徴列 + ラベル列) — Phase 70.A ---
--
-- いずれも純粋 fit (RNG なし)。 特徴は 'reqColsM' で行列化、 ラベルは整数 / 実数で引く。

-- | 勾配ブースティング回帰 spec。 @gbmReg cfg ["x1","x2"] "y"@。
data GBRSpec = GBRSpec !GBConfig ![Text] !Text
-- | @gbmReg cfg featCols yCol@ — GBM 回帰 ('GBRegressor')。
gbmReg :: GBConfig -> [Text] -> Text -> GBRSpec
gbmReg = GBRSpec
instance Fit GBRSpec where
  type Fitted GBRSpec = GBRegressor
  fitEither (GBRSpec cfg feats yc) d = fitGBRegressor cfg <$> reqColsM feats d <*> reqLabelD yc d

-- | 勾配ブースティング分類 spec。 @gbmCls cfg ["x1","x2"] "cls"@ (ラベルは {0,1})。
data GBCSpec = GBCSpec !GBConfig ![Text] !Text
-- | @gbmCls cfg featCols clsCol@ — GBM 分類 ('GBClassifier')。
gbmCls :: GBConfig -> [Text] -> Text -> GBCSpec
gbmCls = GBCSpec
instance Fit GBCSpec where
  type Fitted GBCSpec = GBClassifier
  fitEither (GBCSpec cfg feats cc) d = fitGBClassifier cfg <$> reqColsM feats d <*> reqLabelI cc d

-- | 決定木 spec。 @decisionTree cfg ["x1","x2"] "cls"@。
data DTSpec = DTSpec !DTConfig ![Text] !Text
-- | @decisionTree cfg featCols clsCol@ — 決定木 ('DTFit'・行列版 'fitDTV')。 fit 時に
--   実列名 (feats) とクラス列の levels を載せて返すので 'treePlot'/'printRpart' が
--   名前を手渡し不要 ('RFClassifierFit' と同型)。 クラス列は factor(text)/数値の両対応。
decisionTree :: DTConfig -> [Text] -> Text -> DTSpec
decisionTree = DTSpec
instance Fit DTSpec where
  type Fitted DTSpec = DTFit
  fitEither (DTSpec cfg feats cc) d = do
    x            <- reqColsM feats d
    (y, classes) <- reqLabelWithLevels cc d
    pure (DTFit (fitDTV cfg x y) feats classes)

-- | k-NN 分類 spec。 @knnCls 5 ["x1","x2"] "cls"@。
data KNNCSpec = KNNCSpec !Int ![Text] !Text
-- | @knnCls k featCols clsCol@ — k-NN 分類 ('KNNClassifier')。
knnCls :: Int -> [Text] -> Text -> KNNCSpec
knnCls = KNNCSpec
instance Fit KNNCSpec where
  type Fitted KNNCSpec = KNNClassifier
  -- fit 時にクラス列の levels を注入 (confusion/凡例でクラス名表示・factor/数値両対応)。
  fitEither (KNNCSpec k feats cc) d = do
    x            <- reqColsM feats d
    (y, classes) <- reqLabelWithLevels cc d
    pure ((fitKNNC k x y) { knnCClassNames = classes })
  predictorCols (KNNCSpec _ feats _) = feats   -- ★距離ベース・標準化の本命
  -- responseCol = Nothing (既定)。 分類の応答はクラスラベルゆえ標準化しない。

-- | k-NN 回帰 spec。 @knnReg 5 ["x1","x2"] "y"@。
data KNNRSpec = KNNRSpec !Int ![Text] !Text
-- | @knnReg k featCols yCol@ — k-NN 回帰 ('KNNRegressor')。
knnReg :: Int -> [Text] -> Text -> KNNRSpec
knnReg = KNNRSpec
instance Fit KNNRSpec where
  type Fitted KNNRSpec = KNNRegressor
  fitEither (KNNRSpec k feats yc) d = fitKNNR k <$> reqColsM feats d <*> reqLabelD yc d
  predictorCols (KNNRSpec _ feats _) = feats   -- ★距離ベース・標準化の本命
  responseCol   (KNNRSpec _ _ yc)    = Just yc

-- | Naive Bayes (Gaussian) spec。 @naiveBayes ["x1","x2"] "cls"@ (連続特徴)。
data NBSpec = NBSpec ![Text] !Text
-- | @naiveBayes featCols clsCol@ — Gaussian NB ('NBModel' = 'NBGaussian')。
naiveBayes :: [Text] -> Text -> NBSpec
naiveBayes = NBSpec
instance Fit NBSpec where
  type Fitted NBSpec = NBModel
  fitEither (NBSpec feats cc) d = do
    x            <- reqColsM feats d
    (y, classes) <- reqLabelWithLevels cc d
    pure (NBGaussian ((fitGNB x y) { gnbClassNames = classes }))
  predictorCols (NBSpec feats _) = feats   -- 特徴標準化は可 (応答=クラスゆえ y は標準化せず)

-- --- seed 純粋化した RNG モデル spec (KMeans / RandomForest) — Phase 70.A ---
--
-- KMeans / RandomForest は RNG を使う ('MWC.GenIO -> IO')。 'df |->' (純粋 'fitEither')
-- に載せるため、 サンプラ本体を seed 純粋化 (Phase 50 と同方針) した変種
-- 'kMeansPure' / 'fitRFVPure' を呼ぶ。 spec は @seed :: Word32@ を取り、 同 seed →
-- ビット同一の決定的結果を返す。

-- | KMeans クラスタリング spec。 @kmeans cfg seed ["x1","x2"]@ (cfg = k 等・seed = 乱数種)。
data KMeansSpec = KMeansSpec !KMeansConfig !Word32 ![Text]
-- | @kmeans cfg seed featCols@ — KMeans ('KMeansResult'・toPlot = centroid 散布)。
--   @df |-> kmeans (defaultKMeans 3) 42 ["x1","x2"]@。
kmeans :: KMeansConfig -> Word32 -> [Text] -> KMeansSpec
kmeans = KMeansSpec
instance Fit KMeansSpec where
  type Fitted KMeansSpec = KMeansResult
  fitEither (KMeansSpec cfg seed cols) d = (\x -> kMeansPure cfg x seed) <$> reqColsM cols d

-- | ランダムフォレスト回帰 spec。 @randomForestReg cfg seed ["x1","x2"] "y"@ (cfg = 木数等・seed = 乱数種)。
data RFSpec = RFSpec !RFConfig !Word32 ![Text] !Text
-- | @randomForestReg cfg seed featCols yCol@ — RF 回帰 ('RandomForest'・toPlot = 特徴重要度 bar)。
--   分類 'randomForestCls' と対の命名 (無印 randomForest は廃止)。
--   @df |-> randomForestReg defaultRandomForest 42 ["x1","x2"] "y"@。
randomForestReg :: RFConfig -> Word32 -> [Text] -> Text -> RFSpec
randomForestReg = RFSpec
instance Fit RFSpec where
  type Fitted RFSpec = RandomForest
  -- fit 時に手元にある実列名 (feats) をモデルに載せる (df|-> 経路の責務)。
  fitEither (RFSpec cfg seed feats yc) d =
    (\x y -> (fitRFVPure cfg x y seed) { rfFeatureNames = feats })
      <$> reqColsM feats d <*> reqLabelD yc d

-- | DirectLiNGAM (因果探索) の高レベル spec (Phase 77.A)。 @directLingam cfg cols@ =
--   列名で n×p 行列を組み因果構造を推定する (教師なし・y 無し)。 結果は変数名を添えた
--   'LiNGAMFitted' で、 @toPlot@ が**実変数名の DAG** を描く (低レベルは @x0..@)。
--   @df |-> directLingam defaultDirectLiNGAMConfig ["smoking","tar","cancer"]@。
data DirectLiNGAMSpec = DirectLiNGAMSpec !DirectLiNGAMConfig ![Text]

directLingam :: DirectLiNGAMConfig -> [Text] -> DirectLiNGAMSpec
directLingam = DirectLiNGAMSpec

instance Fit DirectLiNGAMSpec where
  type Fitted DirectLiNGAMSpec = LiNGAMFitted DirectLiNGAMFit
  fitEither (DirectLiNGAMSpec cfg cols) d =
    (\x -> LiNGAMFitted (fitDirectLiNGAM cfg x) cols) <$> reqColsM cols d

-- | ParceLiNGAM (bottom-up sink 探索・潜在交絡に頑健) の高レベル spec (Phase 77.B)。
--   @df |-> parceLingam defaultParceConfig cols@。 DAG は Direct と同型 (名前付き)。
data ParceLiNGAMSpec = ParceLiNGAMSpec !ParceConfig ![Text]

parceLingam :: ParceConfig -> [Text] -> ParceLiNGAMSpec
parceLingam = ParceLiNGAMSpec

instance Fit ParceLiNGAMSpec where
  type Fitted ParceLiNGAMSpec = LiNGAMFitted ParceFit
  fitEither (ParceLiNGAMSpec cfg cols) d =
    (\x -> LiNGAMFitted (fitParceLiNGAM cfg x) cols) <$> reqColsM cols d

-- | MultiGroupLiNGAM (複数群で共通 DAG 構造・Shimizu 2012) の高レベル spec (Phase 77.B)。
--   @df |-> multiGroupLingam defaultMultiGroupConfig cols groupCol@。 @groupCol@ = **数値の
--   群コード列**で行を分割し各群を fit する。 DAG は多数決の共通構造 (名前付き)。
data MultiGroupLiNGAMSpec = MultiGroupLiNGAMSpec !MultiGroupConfig ![Text] !Text

multiGroupLingam :: MultiGroupConfig -> [Text] -> Text -> MultiGroupLiNGAMSpec
multiGroupLingam = MultiGroupLiNGAMSpec

instance Fit MultiGroupLiNGAMSpec where
  type Fitted MultiGroupLiNGAMSpec = LiNGAMFitted MultiGroupFit
  fitEither (MultiGroupLiNGAMSpec cfg cols gcol) d = do
    x <- reqColsM cols d
    g <- reqColV gcol d
    let codes    = LA.toList g
        distinct = nub codes                          -- 群ラベル (出現順)
        rowsFor gc = [ i | (i, c) <- zip [0 ..] codes, c == gc ]
        groups   = [ x LA.? rowsFor gc | gc <- distinct ]
    if null distinct
      then Left "multiGroupLingam: group 列が空です"
      else Right (LiNGAMFitted (fitMultiGroupLiNGAM cfg groups) cols)

-- | VARLiNGAM (時系列因果・同時刻 + 時間ラグ) の高レベル spec (Phase 77.B)。
--   @df |-> varLingam defaultVARLiNGAMConfig cols@。 行 = 時刻順。 DAG は同時刻辺 + ラグ辺
--   (@x_j[t-l] → x_i[t]@) の**時間ラグ DAG**。
data VARLiNGAMSpec = VARLiNGAMSpec !VARLiNGAMConfig ![Text]

varLingam :: VARLiNGAMConfig -> [Text] -> VARLiNGAMSpec
varLingam = VARLiNGAMSpec

instance Fit VARLiNGAMSpec where
  type Fitted VARLiNGAMSpec = LiNGAMFitted VARLiNGAMFit
  fitEither (VARLiNGAMSpec cfg cols) d =
    (\x -> LiNGAMFitted (fitVARLiNGAM cfg x) cols) <$> reqColsM cols d

-- | PairwiseLiNGAM (2 変数の因果向き判定) の高レベル spec (Phase 77.B)。
--   @df |-> pairwiseLingam 0.0 "x" "y"@ (thr = |score| 閾値・0 で符号のみ)。 DAG でなく
--   2 ノード + 検出向きの矢印 (Inconclusive は無向)。
data PairwiseLiNGAMSpec = PairwiseLiNGAMSpec !Double !Text !Text

pairwiseLingam :: Double -> Text -> Text -> PairwiseLiNGAMSpec
pairwiseLingam = PairwiseLiNGAMSpec

instance Fit PairwiseLiNGAMSpec where
  type Fitted PairwiseLiNGAMSpec = LiNGAMFitted PairwiseResult
  fitEither (PairwiseLiNGAMSpec thr xc yc) d =
    (\x y -> LiNGAMFitted (pairwiseLiNGAM thr x y) [xc, yc])
      <$> reqColV xc d <*> reqColV yc d

-- | BootstrapLiNGAM (エッジ確信度) の高レベル spec (Phase 77.C)。 @df |-> bootstrapLingam cfg cols@。
--   B 回リサンプルして各エッジの出現確率を出す (seed 純粋・決定的)。 toPlot = 確信度 DAG。
data BootstrapLiNGAMSpec = BootstrapLiNGAMSpec !BootstrapConfig ![Text]

bootstrapLingam :: BootstrapConfig -> [Text] -> BootstrapLiNGAMSpec
bootstrapLingam = BootstrapLiNGAMSpec

instance Fit BootstrapLiNGAMSpec where
  type Fitted BootstrapLiNGAMSpec = LiNGAMFitted BootstrapResult
  fitEither (BootstrapLiNGAMSpec cfg cols) d =
    (\x -> LiNGAMFitted (fitBootstrapLiNGAMPure cfg x) cols) <$> reqColsM cols d

-- | ICA-LiNGAM (Shimizu 2006・FastICA + Hungarian) の高レベル spec (Phase 77.C)。
--   @df |-> icaLingam cfg cols@ (seed 純粋・決定的)。 DAG は名前付き (ilAdjacency)。
data ICALiNGAMSpec = ICALiNGAMSpec !ICALiNGAMConfig ![Text]

icaLingam :: ICALiNGAMConfig -> [Text] -> ICALiNGAMSpec
icaLingam = ICALiNGAMSpec

instance Fit ICALiNGAMSpec where
  type Fitted ICALiNGAMSpec = LiNGAMFitted ICALiNGAMFit
  fitEither (ICALiNGAMSpec cfg cols) d =
    (\x -> LiNGAMFitted (fitICALiNGAMPure cfg x) cols) <$> reqColsM cols d

-- | Pearson 相関ネットワーク (Phase 77)。 @df |-> correlationOf thr cols@ で相関行列を求め、
--   @toPlot@ で @|r| > thr@ の対を辺にしたグラフを描く (因果でなく**周辺相関**)。 LiNGAM DAG
--   と対比すると、 相関は間接・交絡も辺にして過剰に密になり、 LiNGAM が直接因果に削減するのが分かる。
data CorrelationSpec = CorrelationSpec !Double ![Text]

correlationOf :: Double -> [Text] -> CorrelationSpec
correlationOf = CorrelationSpec

instance Fit CorrelationSpec where
  type Fitted CorrelationSpec = CorrelationGraph
  fitEither (CorrelationSpec thr cols) d =
    (\x -> CorrelationGraph (correlationMatrix x) cols thr) <$> reqColsM cols d

-- | ランダムフォレスト分類 spec。 @randomForestCls cfg seed ["x1","x2"] "cls"@ (cfg = 木数等・seed = 乱数種)。
data RFCSpec = RFCSpec !RFCConfig !Word32 ![Text] !Text
-- | @randomForestCls cfg seed featCols clsCol@ — RF 分類 ('RFClassifierFit'・toPlot = 重要度 2 パネル
--   permutation/gini)。 回帰 'randomForestReg' と対の命名 (旧名 rfClassifier)。
--   @df |-> randomForestCls defaultRFCConfig 42 ["x1","x2"] "cls"@。
randomForestCls :: RFCConfig -> Word32 -> [Text] -> Text -> RFCSpec
randomForestCls = RFCSpec
instance Fit RFCSpec where
  type Fitted RFCSpec = RFClassifierFit
  -- 回帰 RFSpec と同方針: fit 時の実列名 (feats) をモデルへ。 seed 純粋版で df|-> に載せる。
  fitEither (RFCSpec cfg seed feats cc) d =
    (\x y -> (fitRFClassifierPure cfg x y seed) { rfcFeatureNames = feats })
      <$> reqColsM feats d <*> reqLabelI cc d
  -- 木ベースはスケール不変ゆえ predictorCols は設けない (標準化不要)。

-- Phase 75.12: カーネル SVM 分類 (多クラス one-vs-rest・SMO は乱数不使用ゆえ純粋) 高レベル spec。
data SVMSpec = SVMSpec !SVMConfig ![Text] !Text
-- | @svmCls cfg featCols clsCol@ — カーネル SVM 分類 ('SVMMulti'・RBF/poly で
--   非線形境界・真の SV)。 決定境界/SV 可視化は 'decisionBoundaryOf'/'svmSupportVectorsOf'。
--   ハイパラ調整は @svmHyper@ ('SVMConfig') に畳む (GP と同型): 'SVMFixed' で固定 fit、
--   'SVMTuneCV' grid で k-fold CV 探索 → 最良ハイパラで再学習 (別動詞は作らない)。
svmCls :: SVMConfig -> [Text] -> Text -> SVMSpec
svmCls = SVMSpec
instance Fit SVMSpec where
  type Fitted SVMSpec = SVMMulti
  fitEither (SVMSpec cfg feats clsCol) d = do
    x <- reqColsM feats d
    (y, classes) <- reqLabelWithLevels clsCol d
    let cfg' = case svmHyper cfg of
                 SVMFixed       -> cfg
                 SVMTuneCV grid -> fst (tuneSVM cfg grid x y)
    pure ((fitSVMMulti cfg' x y) { svmmClassNames = classes })

-- Phase 75.9: 古典 MLP 分類 (seed 純粋 'fitMLPClassifierPure') 高レベル spec。
data MLPClsSpec = MLPClsSpec !MLPConfig !Word32 ![Text] !Text
-- | @mlpCls cfg seed featCols clsCol@ — 古典 MLP 分類 ('MLPFit')。 決定境界/混同行列対応。
mlpCls :: MLPConfig -> Word32 -> [Text] -> Text -> MLPClsSpec
mlpCls = MLPClsSpec
instance Fit MLPClsSpec where
  type Fitted MLPClsSpec = MLPFit
  fitEither (MLPClsSpec cfg seed feats clsCol) d = do
    x            <- reqColsM feats d
    (y, classes) <- reqLabelWithLevels clsCol d
    pure ((fitMLPClassifierPure cfg x y seed) { mlpClassNames = classes })

-- Phase 75.9: 古典 MLP 回帰 (seed 純粋 'fitMLPRegressorPure') 高レベル spec。
data MLPRegSpec = MLPRegSpec !MLPConfig !Word32 ![Text] !Text
-- | @mlpReg cfg seed featCols yCol@ — 古典 MLP 回帰 ('MLPFit'・応答は実数列)。
mlpReg :: MLPConfig -> Word32 -> [Text] -> Text -> MLPRegSpec
mlpReg = MLPRegSpec
instance Fit MLPRegSpec where
  type Fitted MLPRegSpec = MLPFit
  fitEither (MLPRegSpec cfg seed feats yc) d =
    (\x y -> fitMLPRegressorPure cfg x y seed) <$> reqColsM feats d <*> reqColV yc d


-- --- 群別フィット spec (群別フィット) — Phase 52.A4 ------------------------
--
-- 群カテゴリ列で行を分割し、 各群に同じ spec を当てはめて N 個のモデルを得る。
-- ★HBM 整合 = fit 結果 ('GroupedFit') が各群の 'Fitted spec' を保持し、
-- 'groupModels' で取り出せる (各群 'LMModel' 等に既存診断 = 'Hanalyze.Model.LM.Diagnostics'
-- がそのまま適用でき、 群間で傾きが本当に違うかを診断できる)。 描画は 'Plottable'
-- で N 曲線を群色 + 凡例 ('ColorByCol' + 'scaleColorManual') で重畳する (A3 機構の一般化)。
-- 群列は factor (文字) でも数値でも可 (factor は 'toFrame'+'getTextVec'、 数値は
-- 'lookupCol' を show でラベル化)。 主経路 = 二変量近道、 formula spec も副で許す。

-- | spec を「群別フィット」 でラップする。 @grouped "g" (lm "x" "y")@。
--   既存 spec ('lm'\/'glm'\/'spline'\/'robust'\/'quantile'、 formula も可) は不変。
data GroupedSpec spec = GroupedSpec !Text spec

-- | @grouped "g" spec@ — 群列 @g@ で行を分け別々に @spec@ を当てはめる spec ラッパ。
grouped :: Text -> spec -> GroupedSpec spec
grouped = GroupedSpec

-- | 各群の (ラベル, fit 済みモデル) を取り出す (hbmDraws\/forestOf 流アクセサ)。
--   各群モデルに既存の診断 (例 LM なら 'Hanalyze.Model.LM.Diagnostics') がそのまま使える。
groupModels :: GroupedFit spec -> [(Text, Fitted spec)]
groupModels = gfGroups

-- | 群ラベルの一覧 (出現順)。
groupLabels :: GroupedFit spec -> [Text]
groupLabels = map fst . gfGroups

instance Fit spec => Fit (GroupedSpec spec) where
  type Fitted (GroupedSpec spec) = GroupedFit spec
  fitEither (GroupedSpec gcol sp) d = do
    keys <- groupKeysCS gcol d
    let cols     = numericCols d                       -- 数値列 (x/y 含む) 全部
        ordered  = nub keys                            -- 群ラベル (出現順)
        rowsOf k = [ i | (i, k') <- zip [0 :: Int ..] keys, k' == k ]
        subOf ix = [ (n, [ vs !! i | i <- ix ]) | (n, vs) <- cols ]  -- 行抽出した sub 源
    fits <- mapM (\k -> (,) k <$> fitEither sp (subOf (rowsOf k))) ordered
    pure (GroupedFit fits)

-- | 群列を行ごとのラベル ('Text') 列に正規化する。 factor (文字) 列は
--   'toFrame'+'getTextVec'、 数値列は 'lookupCol' を show でラベル化。
groupKeysCS :: ColumnSource d => Text -> d -> Either String [Text]
groupKeysCS gcol d =
  case getTextVec gcol (toFrame d) of
    Just ts -> Right (V.toList ts)
    Nothing -> case lookupCol gcol d of
      Just vs -> Right (map showGroupLabel vs)
      Nothing -> Left ("grouped: 群列が見つかりません: " <> T.unpack gcol)

-- | クラス列を (整数ラベル列 0..K-1, levels 名) に正規化する。 factor(text) 列は
--   'toFrame'+'getTextVec' で **辞書順 unique** を levels とし、 数値列は round して
--   **昇順 unique** を levels とする (それぞれ show)。 いずれも各行を levels 中の
--   index (0..K-1) に符号化するので、 @levels !! label@ でクラス名が引ける。
--   ('reqLabelI' の levels 付き版・'treePlot' 等でクラス名を出すのに使う。)
reqLabelWithLevels :: ColumnSource d => Text -> d -> Either String (VU.Vector Int, [Text])
reqLabelWithLevels n d =
  case getTextVec n (toFrame d) of
    Just ts ->
      let labels = V.toList ts
          levels = sort (nub labels)                     -- factor は辞書順
          ix x   = fromMaybe 0 (elemIndex x levels)
      in Right (VU.fromList (map ix labels), levels)
    Nothing -> case lookupCol n d of
      Just vs ->
        let ints   = map round vs :: [Int]
            levels = sort (nub ints)                     -- 数値は昇順
            ix x   = fromMaybe 0 (elemIndex x levels)
        in Right (VU.fromList (map ix ints), map (T.pack . show) levels)
      Nothing -> Left ("label 列が見つかりません: " <> T.unpack n)

-- | 数値群ラベルの表示 (整数値は小数点を出さない)。
showGroupLabel :: Double -> Text
showGroupLabel v
  | fromIntegral (round v :: Integer) == v = T.pack (show (round v :: Integer))
  | otherwise                              = T.pack (show v)

-- --- formula 多変量 spec (R 流) — Phase 51.3 -------------------------------
--
-- 二変量近道と違い、 formula 文字列で多変量を表す (R の @lm(y ~ x1 + x2, df)@)。
-- 既存 'multiLMModel' / 'multiGLMModel' / 'fitMixedLME' を配線するだけ。
-- データ源は 'toFrame' で 'DX.DataFrame' に変換し **Phase 47 経路**
-- (MissingPolicy / contrast / 応答列判定) を通す (再実装しない)。 'DX.DataFrame'
-- 源は 'toFrame'=id ゆえ factor/NA を温存。

-- | formula LM spec。 @lmF "y ~ x1 + x2"@。
data LMFormulaSpec   = LMFormulaSpec   !Text
-- | formula GLM spec。 @glmF fam link "y ~ x1 + x2"@。
data GLMFormulaSpec  = GLMFormulaSpec  !Family !LinkFn !Text
-- | formula 混合モデル spec (random effects)。 @glmmF "y ~ x + (1|g)"@。
data GLMMFormulaSpec = GLMMFormulaSpec !Text

-- | @lmF "y ~ x1 + x2"@ — formula 多変量 LM ('MultiLMModel') の spec。
lmF :: Text -> LMFormulaSpec
lmF = LMFormulaSpec

-- | @glmF fam link "y ~ x1 + x2"@ — formula 多変量 GLM ('MultiGLMModel') の spec。
glmF :: Family -> LinkFn -> Text -> GLMFormulaSpec
glmF = GLMFormulaSpec

-- | @glmmF "y ~ x + (1|g)"@ — 線形混合モデル (Phase 48 'fitMixedLME') の spec。
--   返り型は @('GLMMResultRE', [Text])@ (固定効果係数名つき)。
glmmF :: Text -> GLMMFormulaSpec
glmmF = GLMMFormulaSpec

-- | DOE 設計 (`Design`) の解析 spec (Phase 78.B)。 設計が含意する formula
--   (要因計画=全交互作用 / RSM=2 次) で **LM を当てる** (`MultiLMModel`)。 同じ @plan@ を
--   sim データ・実物データに使い回せる (formula は plan 由来・データは任意)。
--   @filledDf |-> designModel plan "y"@。
data DesignModelSpec = DesignModelSpec !Design !Text

designModel :: Design -> Text -> DesignModelSpec
designModel = DesignModelSpec

instance Fit DesignModelSpec where
  type Fitted DesignModelSpec = MultiLMModel
  fitEither (DesignModelSpec plan y) d = multiLMModel (designFormula plan y) (toFrame d)

-- | DOE 設計 (`Design`) の **GP/RFF 解析** spec (Phase 78.G-e)。`designModel` (LM) の
--   **非 LM 版**で、 plan の因子名で `gpMulti` を当てる (formula 不要 = kernel が非線形を
--   吸収するので 2 次項展開が要らない)。 同じ @plan@ を sim/実物データに使い回せる
--   (`designModel` と対称)。 結果 (`GPRegModelN`) は `MultiVarModel` ゆえ profiler/contour に
--   **GP 事後予測帯**を出せる。 ★**連続因子専用** — 非連続因子 (`Num`/`Cat`) 混在は error
--   (GP kernel はカテゴリ距離を扱えない・`centralCompositeDesign` と同方針)。
--   @filledDf |-> designModelGP defaultGP plan "y"@。
data DesignModelGPSpec = DesignModelGPSpec !GPConfig !Design !Text

designModelGP :: GPConfig -> Design -> Text -> DesignModelGPSpec
designModelGP = DesignModelGPSpec

instance Fit DesignModelGPSpec where
  type Fitted DesignModelGPSpec = GPRegModelN
  fitEither (DesignModelGPSpec cfg plan y) d =
    case [ dfName f | f <- dsFactors plan, notCont (dfKind f) ] of
      []  -> fitEither (GPMultiSpec cfg (map dfName (dsFactors plan)) y) d
      bad -> Left ("designModelGP: 連続因子専用 (GP kernel は非連続因子を扱えない)。"
                   <> " 非連続因子: " <> show bad)
    where notCont (Cont _ _ _) = False
          notCont _            = True

-- | 型付きランダム効果項 (Phase 78.G-f)。lme4 の @(1|g)@ / @(1+s|g)@ を型で表す。
--   文字列 formula を経由せず 'designModelHBM' に渡す。
ranIntercept :: Text -> RandomSpec
ranIntercept g = RandomSpec True [] g

ranSlope :: [Text] -> Text -> RandomSpec
ranSlope slopes g = RandomSpec True slopes g

-- | 前処理済みランダム効果 (Phase 78.G-f / G-f2)。
--   @(群 idx (post-drop 行ごと), 群数, 傾き共変量列)@。 傾き列が空 = 切片のみ
--   (@(1|g)@)、 非空 = 相関ランダム傾き (@(1+s|g)@) で各列が観測ごとの共変量値
--   (長さ n・designX/ys と同じ post-drop 行整合)。
type PreparedRE = ([Int], Int, [[Double]])

-- | DOE 階層モデルの手書き 'ModelP' (Phase 78.G-f・核心 / G-f2 で相関傾きを高速化)。
--   固定効果 = designX·β (β に弱情報 prior)、 観測ノイズ = σ。
--   ★HBM に formula 文字列を食わせる経路は無い (Fit.hs の方針) ため手書きで組む。
--
--   ランダム効果は 2 経路:
--   * **切片のみ** (全 RE の傾き列が空): 'reNormal' + 'observeLMR' の解析勾配 REff 経路
--     (観測は affine = compiled 高速経路)。
--   * **相関ランダム傾き** (いずれかの RE に傾き列): lme4 @(1+s|g)@。 Phase 80.2b で
--     **非中心化** 化。 群成分 raw latent @z_g^c ~ N(0,1)@、 スケール @τ_c ~ HalfNormal@、
--     相関 @Lcorr = LKJ Cholesky@ から @b_g^c = τ_c·Σ_{j≤c} Lcorr[c][j]·z_g^j@ を組み、
--     観測 μ = β·X + Σ_c b_g^c·x_c を per-obs scalar 'observe' に載せる。 μ は τ·z / L·z の
--     latent×latent 積を含む非 affine ゆえ (b) 閉形式には載らないが、 'synthVecIR' が
--     (a) source-to-source AD (vecIR) に載せる (Phase 80.2a spike で per-eval 5×・funnel
--     消滅を実測)。 centered の @potential@ 相関 prior + funnel (160×) は撤去。
designHBMProgram :: [[Double]] -> [Text] -> [PreparedRE] -> [Double] -> ModelP ()
designHBMProgram designX betaNames res ys
  | all (\(_, _, sc) -> null sc) res = do
      -- === 切片のみ: 解析勾配 REff 高速経路 ===
      mapM_ (\nm -> sample nm (Normal 0 10)) betaNames
      _sigma <- sample "sigma" (HalfNormal 5)
      reffs  <- mapM mkInterceptRE (zip [0 ..] res)
      observeLMR "y" betaNames designX reffs (LMGaussian "sigma") ys
  | otherwise = do
      -- === 相関ランダム傾き (非中心化): μ に τ·L·z (latent×latent) → (a) vecIR ===
      -- Phase 80.2b: centered (potential 相関 prior + observeLMR + funnel 160×) を撤去し、
      -- 非中心化 b_g^c = τ_c·Σ_{j≤c} Lcorr[c][j]·z_g^j を per-obs scalar 'observe' に載せる。
      -- μ が latent×latent 非 affine ゆえ (b) 閉形式には載らないが、 'synthVecIR' が
      -- (a) source-to-source AD に載せる (Phase 80.2a spike で per-eval 5×・funnel 消滅を実測)。
      betas <- mapM (\nm -> sample nm (Normal 0 10)) betaNames
      sigma <- sample "sigma" (HalfNormal 5)
      -- 各 RE 群の per-obs 寄与関数 (i → Σ_c b_{g(i)}^c · w_c(i))。
      contribs <- mapM (uncurry correlatedRE) (zip [0 ..] res)
      let nObs   = length ys
          muAt i = sum (zipWith (\b x -> b * realToFrac x) betas (designX !! i))
                   + sum [ c i | c <- contribs ]
      mapM_ (\i -> observe ("y_" <> T.pack (show i)) (Normal (muAt i) sigma) [ys !! i])
            [0 .. nObs - 1]
  where
    -- 切片のみ RE (解析勾配): reNormal + at。
    mkInterceptRE (gi :: Int, (idxRow, nG, _)) = do
      let base = "u_g" <> T.pack (show gi)
      tau <- sample ("tau_" <> base) (HalfNormal 5)
      u   <- reNormal base nG ("tau_" <> base) tau
      pure (u `at` idxRow)

    -- 相関 RE (非中心化): 群成分 raw latent z_g^c ~ N(0,1) (成分ごと 1D plate = family)、
    -- スケール τ_c ~ HalfNormal、 相関 Cholesky Lcorr = LKJ。 群効果は
    -- b_g^c = τ_c·Σ_{j≤c} Lcorr[c][j]·z_g^j = (diag(τ)·Lcorr·z)_c を deterministic で記録。
    -- 返り値 = per-obs 寄与関数 @i → Σ_c b_{g(i)}^c · w_c(i)@ (w_0=1 / w_c=slope列)。
    correlatedRE (gi :: Int) (idxRow, nG, slopeCols) = do
      let k       = 1 + length slopeCols
          tag     = "g" <> T.pack (show gi)
          znm c g = "z_" <> tag <> "_" <> T.pack (show c) <> "_" <> T.pack (show g)
          bnm c g = "b_" <> tag <> "_" <> T.pack (show c) <> "_" <> T.pack (show g)
      -- 成分 c ごとに群 raw latent の 1D plate: zByComp !! c !! g = z_g^c (family 単位)。
      zByComp <- mapM (\c -> plateI ("z_" <> tag <> "_" <> T.pack (show c)) nG
                               (\g -> sample (znm c g) (Normal 0 1)))
                      [0 .. k - 1]
      taus  <- mapM (\c -> sample ("tau_" <> tag <> "_" <> T.pack (show c)) (HalfNormal 5))
                    [0 .. k - 1]
      lcorr <- lkjCorrCholesky ("Lcorr_" <> tag) k 2.0
      -- b_g^c = τ_c·Σ_{j≤c} Lcorr[c][j]·z_g^j を deterministic で記録 (chain 出力用) + 使用。
      bByComp <- mapM (\c ->
                    mapM (\g ->
                      deterministic (bnm c g)
                        (taus !! c * sum [ (lcorr !! c !! j) * ((zByComp !! j) !! g)
                                         | j <- [0 .. c] ]))
                      [0 .. nG - 1])
                  [0 .. k - 1]
      -- per-obs 寄与: 重み c=0→1 / c>=1→slope 列 (post-drop 行整合)。
      let wOf c i | c == 0    = 1
                  | otherwise = realToFrac ((slopeCols !! (c - 1)) !! i)
      pure $ \i -> let g = idxRow !! i
                   in sum [ ((bByComp !! c) !! g) * wOf c i | c <- [0 .. k - 1] ]

-- | DOE 設計の **階層ベイズ (mixed-effects)** fit spec (Phase 78.G-f)。 固定効果 =
--   'designFormula' (factorial=交互作用 / RSM=2次) を LM 経路 (`modelFrame`/`designMatrixF`)
--   で設計行列化し、 ランダム効果 = 'RandomSpec' の群 (v1 = random intercept のみ) を
--   'designHBMProgram' で手書き 'ModelP' に組み、 'hbm' (NUTS) で学習する。 事後 draw を
--   'DesignHBMFit' に格納する。 同じ @plan@ を sim/実物データに使い回せる
--   ('designModel' / 'designModelGP' と対称)。
--   @filledDf |-> designModelHBM defaultHBM plan [ranIntercept "lot"] "y"@。
data DesignHBMFit = DesignHBMFit
  { dhfFormula    :: !Formula      -- ^ 固定効果 formula ('designFormula' plan y の parse 結果)。
  , dhfBetaNames  :: ![Text]       -- ^ 固定効果係数名 (設計列順)。
  , dhfBetaDraws  :: ![[Double]]   -- ^ draws × p ('dhfBetaNames' 列順)。
  , dhfSigmaDraws :: ![Double]     -- ^ 観測ノイズ σ の事後 draw。
  , dhfFrame      :: !ModelFrame   -- ^ 訓練 frame (mvFrame 用)。
  , dhfModel      :: !HBMModel     -- ^ 学習済 HBM 本体。 診断抽出子 ('dagOf' / 'tracesOf' /
                                   --   'ppcOf' / 'energyOf' 等) に @dhfModel fit@ で渡せる。
  }

data DesignModelHBMSpec = DesignModelHBMSpec !HBMConfig !Design ![RandomSpec] !Text

designModelHBM :: HBMConfig -> Design -> [RandomSpec] -> Text -> DesignModelHBMSpec
designModelHBM = DesignModelHBMSpec

instance Fit DesignModelHBMSpec where
  type Fitted DesignModelHBMSpec = DesignHBMFit
  fitEither (DesignModelHBMSpec cfg plan res y) d = do
    fml               <- parseModel (designFormula plan y)
    let df       = toFrame d
        -- 'modelFrame' (DropRows) は formula 関与列 (resp:dvars) に NA を含む行を listwise
        -- 削除する。 群 idx (prepRE) は raw df からでなく、 designX/ys と同じ post-drop 行
        -- 集合 (dfClean) から作る必要がある (でないと NA 行がある場合に群 idx が行ずれし、
        -- observeLMR へ渡す群インデックスが誤対応する)。 dropMissingRows は modelFrame 内部
        -- (modelFrameWith) と同一関数・同一 involved 列なので、 落とす行集合・順序が一致する。
        involved = formResponse fml : formDataVars fml
        dfClean  = dropMissingRows involved df
    mf                <- modelFrame fml df
    (xMat, betaNames) <- designMatrixF fml mf
    yv                <- responseVec mf
    let designX = map LA.toList (LA.toRows xMat)
        ys      = V.toList yv
    resPrepared <- mapM (prepRE dfClean) res
    let prog :: ModelP ()
        prog      = designHBMProgram designX betaNames resPrepared ys
        model     = d |-> hbm cfg prog :: HBMModel
        betaDraws = [ concatMap (chainVals nm) (hbmChainsR model) | nm <- betaNames ]
    pure DesignHBMFit
      { dhfFormula    = fml
      , dhfBetaNames  = betaNames
      , dhfBetaDraws  = transpose betaDraws
      , dhfSigmaDraws = concatMap (chainVals "sigma") (hbmChainsR model)
      , dhfFrame      = mf
      , dhfModel      = model
      }
    where
      prepRE frame (RandomSpec _ slopes g) = case getTextVec g frame of
        Nothing -> Left ("designModelHBM: grouping 列 '" <> T.unpack g <> "' が見つかりません")
        Just gv -> do
          let (labels, idx, _) = buildGroups gv
          -- 傾き共変量列 (post-drop 行整合 = 群列と同じ dfClean 由来)。 空なら切片のみ。
          slopeCols <- mapM (getSlopeCol frame) slopes
          Right (V.toList idx, V.length labels, slopeCols)
      getSlopeCol frame s = case lookupCol s frame of
        Just col -> Right col
        Nothing  -> Left ("designModelHBM: 傾き共変量列 '" <> T.unpack s
                          <> "' が見つかりません (数値列である必要があります)")

-- | 多出力 (複数応答) fit コンビネータ (Phase 78.F)。 応答名のリストと **応答名から spec を作る
--   関数**を受け、 各応答を同じデータ源で当てはめて @[(応答名, Fitted spec)]@ を返す。
--   @designModel plan@ が既にカレー化 (@Text -> DesignModelSpec@) なので接着剤なしで slot に嵌る:
--
--   > let model = filledDf |-> multiOutput ["strength","yield"] (designModel plan)
--   >     -- model :: [(Text, MultiLMModel)]
--
--   designModel 専用でなく汎用 (@multiOutput ys (lmF . mkFormula)@ 等も可)。 結果は profiler
--   ('Hanalyze.Plot.ML.profiler') が「行=応答 × 列=因子」 のグリッドに描ける。
data MultiOutputSpec spec = MultiOutputSpec ![Text] (Text -> spec)

-- | @multiOutput responseNames mkSpec@ — 各応答名に @mkSpec@ を適用して当てはめる。
multiOutput :: [Text] -> (Text -> spec) -> MultiOutputSpec spec
multiOutput = MultiOutputSpec

-- | @multiOutput@ の結果 @[(応答名, model)]@ から**応答名でモデルを 1 つ取り出す**。 単一応答の
--   可視化 (@contourOf@ / @surfaceOf@) に multiOutput の結果を渡すとき @snd (head model)@ の代わりに使う。
--
--   > let models = filledDf |-> multiOutput ["strength","yield"] (designModel plan)
--   > noDf |>> contourOf (modelFor "strength" models) "temp" "time"
--
--   応答名が無ければ利用可能な名前を添えて error (対話で気付ける)。
modelFor :: Text -> [(Text, m)] -> m
modelFor r models = case lookup r models of
  Just m  -> m
  Nothing -> error
    ("modelFor: 応答 '" <> T.unpack r <> "' が見つかりません。 利用可能: "
      <> show (map (T.unpack . fst) models))

instance Fit spec => Fit (MultiOutputSpec spec) where
  type Fitted (MultiOutputSpec spec) = [(Text, Fitted spec)]
  fitEither (MultiOutputSpec ys mk) d =
    traverse (\y -> (,) y <$> fitEither (mk y) d) ys

instance Fit LMFormulaSpec where
  type Fitted LMFormulaSpec = MultiLMModel
  fitEither (LMFormulaSpec fml) d = multiLMModel fml (toFrame d)

instance Fit GLMFormulaSpec where
  type Fitted GLMFormulaSpec = MultiGLMModel
  fitEither (GLMFormulaSpec fam lnk fml) d = multiGLMModel fam lnk fml (toFrame d)

instance Fit GLMMFormulaSpec where
  type Fitted GLMMFormulaSpec = (GLMMResultRE, [Text])
  fitEither (GLMMFormulaSpec fml) d = fitMixedLME fml (toFrame d)

-- --- 重回帰 spec (列名リスト・formula 不要) — Phase 70.D --------------------
--
-- 重回帰 = 説明変数の**列名リスト**で多変量回帰を当てる (formula 文字列を書かない)。
-- 内部は 'additiveFormula' で設計行列 @[1, x1,…,xp]@ を直接合成し、 既存の
-- 'multiLMModelF' / 'multiGLMModelF' / 'multiRobustModelF' を配線する。 返り値は
-- effect plot ('statModelMulti' / along / holdAt / byVar) と係数サマリ
-- ('coefSummary') の両方が即使える。 'lmF' / 'glmF' は formula 糖衣として併存。

-- | 重回帰 (多変量 LM) spec。 @lmMulti [\"x1\",\"x2\",\"x3\"] \"y\"@。
data LMMultiSpec     = LMMultiSpec     ![Text] !Text
-- | 重回帰 (多変量 GLM) spec。 @glmMulti fam link [\"x1\",\"x2\"] \"y\"@。
data GLMMultiSpec    = GLMMultiSpec    !Family !LinkFn ![Text] !Text
-- | 重回帰 (多変量ロバスト) spec。 @robustMulti est [\"x1\",\"x2\"] \"y\"@。
data RobustMultiSpec = RobustMultiSpec !RobustEstimator ![Text] !Text

-- | @lmMulti predCols yCol@ — 列名リストで多変量線形回帰 ('MultiLMModel')。
--   @df |-> lmMulti [\"age\",\"bmi\",\"bp\"] \"y\"@。
lmMulti :: [Text] -> Text -> LMMultiSpec
lmMulti = LMMultiSpec

-- | @glmMulti fam link predCols yCol@ — 列名リストで多変量 GLM ('MultiGLMModel')。
glmMulti :: Family -> LinkFn -> [Text] -> Text -> GLMMultiSpec
glmMulti = GLMMultiSpec

-- | @rlmMulti est predCols yCol@ — 列名リストで多変量ロバスト回帰 ('MultiRobustModel')。
rlmMulti :: RobustEstimator -> [Text] -> Text -> RobustMultiSpec
rlmMulti = RobustMultiSpec

instance Fit LMMultiSpec where
  type Fitted LMMultiSpec = MultiLMModel
  fitEither (LMMultiSpec xs y) d = multiLMModelF (additiveFormula y xs) (toFrame d)
  predictorCols (LMMultiSpec xs _) = xs
  responseCol   (LMMultiSpec _ y)  = Just y

instance Fit GLMMultiSpec where
  type Fitted GLMMultiSpec = MultiGLMModel
  fitEither (GLMMultiSpec fam lnk xs y) d =
    multiGLMModelF fam lnk (additiveFormula y xs) (toFrame d)
  predictorCols (GLMMultiSpec _ _ xs _) = xs
  -- responseCol = Nothing (既定)。 GLM 応答は family/link 拘束ゆえ標準化不可。

instance Fit RobustMultiSpec where
  type Fitted RobustMultiSpec = MultiRobustModel
  fitEither (RobustMultiSpec est xs y) d =
    multiRobustModelF est (additiveFormula y xs) (toFrame d)
  predictorCols (RobustMultiSpec _ xs _) = xs
  responseCol   (RobustMultiSpec _ _ y)  = Just y

-- | 多変量 (重回帰) 分位点回帰 spec。 @quantileMulti [0.1,0.5,0.9] ["x1","x2"] "y"@。
--   単変量 'quantile' の多予測子版 (各 τ を設計行列 @[1,x₁..xₚ]@ に当てる・statsmodels
--   @QuantReg@ 多予測子と同型)。 予測子は数値列を 'reqColsM' で直接行列化する。
data QuantileMultiSpec = QuantileMultiSpec ![Double] ![Text] !Text

-- | @rqMulti taus predCols yCol@ — 多変量分位点回帰 ('MultiQuantileModel')。
rqMulti :: [Double] -> [Text] -> Text -> QuantileMultiSpec
rqMulti = QuantileMultiSpec

instance Fit QuantileMultiSpec where
  type Fitted QuantileMultiSpec = MultiQuantileModel
  fitEither (QuantileMultiSpec taus xs yn) d = do
    xm <- reqColsM xs d                                   -- n × p (予測子)
    yv <- reqColV yn d
    let nR   = LA.rows xm
        dm   = LA.fromColumns (LA.konst 1 nR : LA.toColumns xm)  -- [1, x₁..xₚ]
        fits = [ (t, fitQuantile t dm yv) | t <- taus ]
    Right MultiQuantileModel { mqmTaus = taus, mqmNames = xs, mqmFits = fits, mqmX = dm }
  predictorCols (QuantileMultiSpec _ xs _) = xs
  responseCol   (QuantileMultiSpec _ _ yn) = Just yn

-- --- HBM spec + データ散布図 — Phase 51.4 ----------------------------------
--
-- HBM は formula を取らず手書き 'ModelP' を学習する (brms 風 formula→HBM は別 Phase)。
-- spec は既存 'hbmModelPure' (純粋・seed 決定的) を配線するだけ。 データ源の
-- 数値列をすべて取り出し列名 assoc にして渡す (HBM 側 'dataNamed' 名と突合)。

-- | HBM spec。 @hbm cfg model@ (設定 + 手書き確率プログラム)。
data HBMSpec = HBMSpec HBMConfig (ModelP ())

-- | @hbm cfg model@ — HBM ('HBMModel') を学習する spec。 cfg の seed で決定的。
hbm :: HBMConfig -> ModelP () -> HBMSpec
hbm = HBMSpec

-- | データ源の数値列をすべて列名 assoc に取り出す (HBM の入力形)。
numericCols :: ColumnSource d => d -> [(Text, [Double])]
numericCols d = [ (n, vs) | n <- columnNames d, Just vs <- [lookupCol n d] ]

instance Fit HBMSpec where
  type Fitted HBMSpec = HBMModel
  -- Phase 60.3: 空 placeholder slot の突合を loud error 化 (旧: 黙って空 [] の
  -- まま学習が走り gids=[] 等の不可解な失敗になっていた)。 DataIx slot は
  -- Int/Integer 列直結 + Text factor 列の sort 順自動コード化。
  fitEither (HBMSpec cfg model) d = do
    checkDataSlots model d
    (ixCols, levels) <- resolveIxSlots model d
    Right (hbmModelPureWith cfg model (numericCols d) ixCols levels)
  -- Phase 61.4: '(|->!)' 経路 = 同じ列解決 + 進捗表示つき IO 学習。
  -- seed 規約は pure 経路と共有ゆえ結果はビット一致 (test 固定)。
  fitIO (HBMSpec cfg model) d =
    case (do checkDataSlots model d; resolveIxSlots model d) of
      Left err              -> ioError (userError err)
      Right (ixCols, levels) ->
        hbmModelIOWith cfg model (numericCols d) ixCols levels

-- | 'dataNamed' slot の突合検査 (Phase 60.3): **空 placeholder** (@dataNamed n []@)
-- なのに対応する数値列が無ければ loud error。 実値入り placeholder の列欠落は
-- default 続行 (データ直書きの正当パターンを壊さない)。
checkDataSlots :: ColumnSource d => ModelP () -> d -> Either String ()
checkDataSlots model d =
  case [ n | (n, True) <- dataSlots model
           , Nothing <- [lookupCol n d] ] of
    [] -> Right ()
    ns -> Left ("HBM: 空 placeholder の dataNamed slot に対応する数値列が"
                <> "ありません: " <> T.unpack (T.intercalate ", " ns)
                <> " (利用可能列: "
                <> T.unpack (T.intercalate ", " (columnNames d))
                <> ")。 Integer/Text 数値文字列は許容、 factor 列は dataNamedIx で")

-- | 'dataNamedIx' slot を列から解決する (Phase 60.3)。
--   * Text factor 列 → **sort 順** (辞書順) levels に 0.. コード化
--     (R @factor()@ / pandas parity・行順 shuffle に不変)
--   * 数値列 (Int / Integer / 整数値の Double) → @round@ で [Int]
--   * 空 placeholder で列なし / 非整数値 → loud error ('Left')
--   * 実値入り placeholder で列なし → default 続行 (bind しない)
resolveIxSlots :: ColumnSource d => ModelP () -> d
               -> Either String ([(Text, [Int])], [(Text, [Text])])
resolveIxSlots model d = do
  rs <- mapM resolve (dataIxSlots model)
  pure ( [ (n, is) | (n, Just is, _) <- rs ]
       , [ (n, ls) | (n, _, Just ls) <- rs ] )
  where
    fr = toFrame d
    resolve (n, isEmpty) = case getTextVec n fr of
      Just tv ->
        let vals   = V.toList tv
            levels = sort (nub vals)
            code t = fromMaybe 0 (elemIndex t levels)   -- levels 由来ゆえ必ず命中
        in Right (n, Just (map code vals), Just levels)
      Nothing -> case lookupCol n d of
        Just vs
          | all (\v -> fromIntegral (round v :: Int) == v) vs ->
              Right (n, Just (map round vs), Nothing)
          | otherwise ->
              Left ("HBM: dataNamedIx slot " <> T.unpack n
                    <> " の列に非整数値があります (離散 index 専用)")
        Nothing
          | isEmpty ->
              Left ("HBM: 空 placeholder の dataNamedIx slot に対応する列が"
                    <> "ありません: " <> T.unpack n
                    <> " (利用可能列: "
                    <> T.unpack (T.intercalate ", " (columnNames d)) <> ")")
          | otherwise -> Right (n, Nothing, Nothing)