packages feed

hanalyze-plot (empty) → 0.2.0.1

raw patch · 12 files changed

+11084/−0 lines, 12 filesdep +addep +aesondep +array

Dependencies added: ad, aeson, array, async, base, bytestring, cassava, containers, dataframe-core, dataframe-csv, dataframe-json, dataframe-operations, dataframe-parquet, deepseq, directory, filepath, hanalyze, hanalyze-plot, hgg-3d, hgg-core, hgg-custom, hgg-svg, hmatrix, hspec, hvega, massiv, megaparsec, mwc-random, parallel, parser-combinators, primitive, process, reflection, regex-base, regex-tdfa, statistics, temporary, text, unicode-transforms, unordered-containers, vector, vector-algorithms

Files

+ README.ja.md view
@@ -0,0 +1,98 @@+# hanalyze-plot++[`hanalyze`](../README.ja.md) と姉妹プロジェクト **hgg** を+繋ぐ**統合層**。 fit 済みの解析モデルを hgg の `VisualSpec` へ+変換する `toPlot` / `Plottable` を提供する 8 module。++依存が他の層と違う点に注意:++- **umbrella package `hanalyze` の上**に乗る (`Fit` / `Wrappers` を+  import するため)。 umbrella → plot 方向にすると package 循環になるので、+  この向きは cabal file にも明記されている。+- sibling repo の `hgg-{core,svg,3d,custom}` に依存する。 このため+  **既定の `cabal.project` には含まれない**。 build には専用の build root+  `cabal.project.plot` を使う。++```bash+cabal build --project-file=cabal.project.plot hanalyze-plot+cabal test  --project-file=cabal.project.plot hanalyze-plot-test+```++> **Vega-Lite で HTML の図・レポートを出したい場合**は+> [`hanalyze-viz`](../hanalyze-viz/README.ja.md) を使う。+> こちらは SVG / PDF / PNG の静的描画が対象。++## 主要 module (全 8 module)++| Module | 役割 |+|---|---|+| `Hanalyze.Plot` | 統合層の入口。 下記 instance 群と `Fit` 系 API (`\|->` / `lm` / `glm` …) をまとめて再輸出する |+| `Plot.Core` | モデル族に依存しない骨格 — `Plottable` / `SingleVarModel` / `MultiVarModel` と grid 評価核 |+| `Plot.Linear` | 線形モデル族の instance (`LMModel` / `GLMModel` / `WeightedLMModel` …) |+| `Plot.Bayes` | ベイズ / HBM 族の instance (`ChainModel` / `ForestSpec` / `PPCSpec` / `DagSpec` / `GLMMResultRE`) + 抽出子 |+| `Plot.ML` | ML / 統計モデル族の instance + 抽出子 |+| `Plot.Robust` | ロバスト回帰・分位点回帰族の instance |+| `Plot.Smooth` | 平滑化・カーネル法族の instance |+| `Plot.Wrappers` | 汎用ラッパ型 (`MultiFit` / `RegModel`) の instance |++中核の型クラスは `Plot.Core` の 1 本だけ:++```haskell+class Plottable m where+  -- | 代表 1 枚の図 (= layer 重畳の主役、 <> で他 layer と合成可)。+  toPlot          :: m -> VisualSpec++  -- | 診断図の束 (= レポート用)。 既定は代表 1 枚のみ。+  diagnosticPlots :: m -> [VisualSpec]+  diagnosticPlots m = [toPlot m]+```++モデル型ごとに `Plottable` の instance を足していく設計なので、 新しいモデルを+描けるようにするのは「instance を 1 本書く」 だけで済む。++## 使い方++```cabal+build-depends: hanalyze-plot+```++```haskell+{-# LANGUAGE OverloadedStrings #-}+import Graphics.Hgg.Frame       ((|>>))+import Graphics.Hgg.Spec        (layer, scatter)+import Graphics.Hgg.Backend.SVG (saveSVGBound)+import Hanalyze.Plot     (toPlot, statModel, grid, (|->), lm)++main :: IO ()+main = do+  let m       = df |-> lm "x" "y"+      lmPlot  = df |>> (layer (scatter "x" "y") <> toPlot m)+  saveSVGBound "lm-scatter-ci.svg" lmPlot+```++`df |-> lm "x" "y"` (fit) → `toPlot` (図化) → `|>>` で他の layer と重ねる、+という 1 本の流れになる。 `toPlot` に渡す前に `statModel m <> grid 200` の+ように**描画オプションを合成**できる (grid 分割数・信頼帯の種類 `bandMode` /+`piMethod`・色 `statColor` 等)。++上の形は demo `plot-integration-demo`+(`hanalyze-demos/demo-plot/PlotIntegrationDemo.hs`) がそのまま使って+いる経路で、 LM / GLM / spline / GP / 分位点回帰などの実例が並んでいる:++```bash+cabal run --project-file=cabal.project.demos plot-integration-demo+```++## テスト++test-suite `hanalyze-plot-test` (`test-plot/Spec.hs`) が、 モデル種別ごとの+`toPlot` 結果の構造・数値を検証する。 元は umbrella 側にあったが、 umbrella の+component がこの package に依存すると循環するため Phase 106.4 で移設した。++## 関連 docs++- 静的描画との統合: [docs/visualization/03-plot-integration.ja.md](../docs/visualization/03-plot-integration.ja.md)+- 可視化の入口: [01-visualization.ja.md](../docs/visualization/01-visualization.ja.md)+- API 一覧 (en): [api-guide/12-plot.md](../docs/api-guide/12-plot.md)++← [repository README](../README.ja.md)
+ README.md view
@@ -0,0 +1,100 @@+# hanalyze-plot++The **integration layer** between [`hanalyze`](../README.md) and the+sibling project **hgg**. Its 8 modules provide `toPlot` /+`Plottable`, which turn a fitted analysis model into an hgg+`VisualSpec`.++Its dependencies differ from the other layers in two ways:++- It sits **above the umbrella package `hanalyze`** (it imports `Fit`+  and `Wrappers`). Pointing the dependency the other way — umbrella → plot —+  would create a package cycle, and the cabal file says so explicitly.+- It depends on `hgg-{core,svg,3d,custom}` from a sibling repo, so+  it is **not part of the default `cabal.project`**. Build it through the+  dedicated build root `cabal.project.plot`:++```bash+cabal build --project-file=cabal.project.plot hanalyze-plot+cabal test  --project-file=cabal.project.plot hanalyze-plot-test+```++> **If you want Vega-Lite figures and HTML reports**, use+> [`hanalyze-viz`](../hanalyze-viz/README.md) instead. This+> package targets static SVG / PDF / PNG rendering.++## Main modules (all 8)++| Module | Role |+|---|---|+| `Hanalyze.Plot` | Entry point of the integration layer; re-exports the instances below together with the `Fit` API (`\|->`, `lm`, `glm`, …) |+| `Plot.Core` | Model-family-agnostic skeleton — `Plottable` / `SingleVarModel` / `MultiVarModel` and the grid evaluation core |+| `Plot.Linear` | Instances for the linear family (`LMModel`, `GLMModel`, `WeightedLMModel`, …) |+| `Plot.Bayes` | Instances for the Bayesian / HBM family (`ChainModel`, `ForestSpec`, `PPCSpec`, `DagSpec`, `GLMMResultRE`) plus extractors |+| `Plot.ML` | Instances and extractors for the ML / statistical model family |+| `Plot.Robust` | Instances for robust and quantile regression |+| `Plot.Smooth` | Instances for smoothing and kernel methods |+| `Plot.Wrappers` | Instances for the generic wrapper types (`MultiFit`, `RegModel`) |++There is exactly one core type class, in `Plot.Core`:++```haskell+class Plottable m where+  -- | The one representative figure (composable with other layers via <>).+  toPlot          :: m -> VisualSpec++  -- | A bundle of diagnostic figures (for reports); defaults to just toPlot.+  diagnosticPlots :: m -> [VisualSpec]+  diagnosticPlots m = [toPlot m]+```++Support for a new model type is therefore a single instance away.++## Usage++```cabal+build-depends: hanalyze-plot+```++```haskell+{-# LANGUAGE OverloadedStrings #-}+import Graphics.Hgg.Frame       ((|>>))+import Graphics.Hgg.Spec        (layer, scatter)+import Graphics.Hgg.Backend.SVG (saveSVGBound)+import Hanalyze.Plot     (toPlot, statModel, grid, (|->), lm)++main :: IO ()+main = do+  let m       = df |-> lm "x" "y"+      lmPlot  = df |>> (layer (scatter "x" "y") <> toPlot m)+  saveSVGBound "lm-scatter-ci.svg" lmPlot+```++The flow is a single line: `df |-> lm "x" "y"` (fit) → `toPlot` (figure) →+`|>>` to overlay it on other layers. Before handing a model to `toPlot` you+can **compose rendering options** onto it, e.g. `statModel m <> grid 200`+(grid resolution, band type via `bandMode` / `piMethod`, colour via+`statColor`, …).++This is the exact path used by the `plot-integration-demo` executable+(`hanalyze-demos/demo-plot/PlotIntegrationDemo.hs`), which walks+through LM, GLM, spline, GP and quantile regression examples:++```bash+cabal run --project-file=cabal.project.demos plot-integration-demo+```++## Tests++The `hanalyze-plot-test` suite (`test-plot/Spec.hs`) checks the structure and+numerics of `toPlot` output per model type. It used to live in the umbrella+package and was moved here in Phase 106.4, because an umbrella component+depending on this package would close a cycle.++## Related docs++- Static-rendering integration: [docs/visualization/03-plot-integration.md](../docs/visualization/03-plot-integration.md)+- Visualization overview: [01-visualization.md](../docs/visualization/01-visualization.md)+- API reference: [api-guide/12-plot.md](../docs/api-guide/12-plot.md)++← [repository README](../README.md)
+ hanalyze-plot.cabal view
@@ -0,0 +1,117 @@+cabal-version: 3.0+name:          hanalyze-plot+version:       0.2.0.1+synopsis:      Static-plot integration for hanalyze (toPlot / Plottable)+description:+    The integration layer between hanalyze and the sibling+    hgg project. It provides the Plottable class, whose toPlot turns+    a fitted analysis model into an hgg VisualSpec that can be+    layered with other marks and rendered to static SVG / PDF / PNG. Instances+    cover the linear family (LM / GLM / weighted LM), Bayesian and HBM results+    (chains, forest plots, posterior predictive checks, model DAGs), robust+    and quantile regression, smoothing and kernel methods, and the generic fit+    wrappers.+    .+    Unlike the other layers this package sits above the umbrella package+    hanalyze (the reverse direction would close a package cycle) and+    depends on the sibling hgg packages, so it is built through the+    dedicated build root cabal.project.plot rather than the default+    cabal.project. For Vega-Lite figures and HTML reports see+    hanalyze-viz instead. See README.md for the module map and a+    usage example.+license:       BSD-3-Clause+author:        Toshiaki Honda+maintainer:    frenzieddoll@gmail.com+copyright:     2026 Aelysce Project (Toshiaki Honda)+category:      Math, Statistics, Numeric, Machine Learning+build-type:    Simple+tested-with:   GHC == 9.6.7+extra-source-files:+    README.md+    README.ja.md++common warnings+  ghc-options: -Wall -Wcompat -Widentities -Wredundant-constraints++-- -O2 は分割前と同一 (性能変更と構造変更を混ぜない、 層別 -O 調整は 106.5 後の別 Phase)+common opt+  ghc-options: -O2 -funbox-strict-fields++library+  import:           warnings, opt+  hs-source-dirs:   src+  default-language: GHC2021+  exposed-modules:+    Hanalyze.Plot+    Hanalyze.Plot.Bayes+    Hanalyze.Plot.Core+    Hanalyze.Plot.Linear+    Hanalyze.Plot.ML+    Hanalyze.Plot.Robust+    Hanalyze.Plot.Smooth+    Hanalyze.Plot.Wrappers+  build-depends:+      base                 >= 4.14 && < 5+    , array                >= 0.5  && < 0.6+    , async                >= 2.2  && < 2.3+    , bytestring           >= 0.11 && < 0.13+    , cassava              >= 0.5  && < 0.6+    , containers           >= 0.6  && < 0.8+    , filepath             >= 1.4  && < 1.6+    , hmatrix              >= 0.20 && < 0.22+    , hvega                >= 0.12 && < 0.13+    , mwc-random           >= 0.15 && < 0.16+    , primitive            >= 0.7  && < 0.10+    , deepseq              >= 1.4  && < 1.6+    , parallel             >= 3.2  && < 3.3+    , process              >= 1.6  && < 1.8+    , statistics           >= 0.16 && < 0.17+    , text                 >= 1.2  && < 2.2+    , aeson                >= 2.0  && < 2.3+    , directory            >= 1.3  && < 1.4+    , temporary            >= 1.3  && < 1.4+    , unordered-containers >= 0.2  && < 0.3+    , ad                   >= 4.4  && < 4.6+    , reflection           >= 2.1  && < 2.2+    , vector               >= 0.12 && < 0.14+    , dataframe-core        ^>= 1.1+    , dataframe-operations  >= 1.1.1 && < 1.2+    , dataframe-csv         ^>= 1.0.2+    , dataframe-json        ^>= 1.0+    , dataframe-parquet     ^>= 1.1+    , massiv               >= 1.0  && < 1.1+    , vector-algorithms    >= 0.9  && < 0.10+    , megaparsec           >= 9.0  && < 9.7+    , parser-combinators   >= 1.3  && < 1.4+    , unicode-transforms   >= 0.4  && < 0.5+    , regex-tdfa           >= 1.3  && < 1.4+    , regex-base           >= 0.94 && < 0.95+    , hanalyze       == 0.2.0.1+    , hgg-core     >= 0.2  && < 0.3+    , hgg-svg      >= 0.2  && < 0.3+    , hgg-3d       >= 0.2  && < 0.3+    , hgg-custom   >= 0.2  && < 0.3++-- Phase 46 (hgg 統合) のテスト。 Phase 106 で umbrella から移設+-- (umbrella component が本 package に依存すると package 循環になるため)。+test-suite hanalyze-plot-test+  import:           warnings+  type:             exitcode-stdio-1.0+  main-is:          Spec.hs+  hs-source-dirs:   test-plot+  default-language: GHC2021+  build-depends:+      base+    , hspec              >= 2.10 && < 2.12+    , hmatrix+    , vector+    , containers+    , text+    , mwc-random+    , dataframe-core        ^>= 1.1+    , hanalyze+    , hanalyze-plot+    , hgg-core+    , hgg-3d+    , hgg-custom+    , aeson
+ src/Hanalyze/Plot.hs view
@@ -0,0 +1,1069 @@+{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ImpredicativeTypes #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE UndecidableInstances #-}+-- |+-- Module      : Hanalyze.Plot+-- Description : 解析モデルを hgg の VisualSpec へ変換する連携層 (別パッケージ hanalyze-plot)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- [日本語]: hgg 連携層 (= 解析モデル → 図 @VisualSpec@)。+--+-- ⚠ 本モジュールは別パッケージ @hanalyze-plot@ に属し、+-- @cabal build --project-file=cabal.project.plot@ で build される。+-- @hgg-core@ に依存するため+-- __upstream hanalyze には cherry-pick しない__+-- (= 依存方向 analyze→plot-core を別パッケージへ切り出すことで隔離した設計)。+-- 中立 protocol ('Hanalyze.Model.Core' の+-- @ResidualModel@ / @PredictiveModel@) は portable、 こちらは非 portable。+--+-- 系統 A (モデル・アウト型): フィット済みモデルを @toPlot@ で @VisualSpec@ 化し、+-- hgg の layer 文法に @df |>> (layer scatter <> toPlot fit)@ で重畳する+-- (@VisualSpec@ は Monoid なので新コンビネータ不要)。+--+-- [English]: The hgg integration layer (= analysis models ->+-- figures via @VisualSpec@).+--+-- ⚠ This module lives in the separate package @hanalyze-plot@,+-- built via @cabal build --project-file=cabal.project.plot@. Because it+-- depends on @hgg-core@,+-- it is __not cherry-picked into the upstream hanalyze__ (= the+-- analyze->plot-core dependency direction is isolated by splitting it+-- into a separate package, by design). The neutral protocol+-- ('Hanalyze.Model.Core''s+-- @ResidualModel@ \/ @PredictiveModel@) is portable; this module is not.+--+-- Lineage A (model-out type): a fitted model is turned into a+-- @VisualSpec@ via @toPlot@, then layered onto hgg's layer+-- grammar with @df |>> (layer scatter <> toPlot fit)@ (no new combinator+-- is needed since @VisualSpec@ is a Monoid).+module Hanalyze.Plot+  ( Plottable (..)+    -- * ルート1 grid 評価 (滑らかな回帰曲線・CI 帯)+  , ModelSpec+  , SingleVarModel (..)+  , GridOpts (..)+  , statModel+  , grid+  , gridRange+  , BandMode (..)+  , bandMode+    -- 予測区間の算出法セレクタ (closed-form / bootstrap — Phase 70.H)+  , PIMethod (..)+  , piMethod+  , statColor+  , statFill+  , statLinetype+  , LineType (..)+  , statLinewidth+  , statAlpha+  , statLabel+  , statEquation+  , statR2+  , statLevel+  , predAt+    -- * 多変量 effect plot+  , MultiVarModel (..)+  , AlongSpec+  , along+  , statModelMulti+  , HoldAgg (..)+  , holdAt+  , byVar+  , MultiLMModel (..)+  , multiLMModel+  , multiLMModelF+  , MultiGLMModel (..)+  , multiGLMModel+  , multiGLMModelF+    -- 多変量ロバスト回帰 (formula 不要・列名リスト — Phase 70.D)+  , MultiRobustModel (..)+  , multiRobustModelF+  , additiveFormula+    -- PLS effect plot (frame 保持ラッパ + 出力セレクタ — Phase 70.B2/B3)+  , PLSModel (..)+  , plsModel+  , selectOutput+    -- * 応答曲面 3D 直結+  , SurfaceOpts (..)+  , defaultSurfaceOpts+  , surfaceGrid+  , surfaceOf+  , surfaceOfWith+  , dataScatter3DOf+  , epredSurfaceOf+  , epredSurfaceOfWith+    -- * モデル API 層 (描画と独立: predict / describe / coefficients)+  , ModelAPI (..)+  , Coef (..)+    -- * 統一係数サマリ (t/z・p 値・95% CI)+  , CoefRow (..)+  , HasCoefSummary (..)+  , HasCoefBoot (..)+  , coefSummaryBoot+    -- * 平滑項単位の近似有意性 (mgcv 流 edf + 近似 F)+  , TermRow (..)+  , HasTermSummary (..)+  , termSummary+    -- * 統一玄関 (.summary() 風)+  , ModelReport (..)+  , HasReport (..)+  , modelReport+  , showReport+    -- * 回帰診断の可視化 (係数 forest / 実測vs予測)+  , HasObsPred (..)+  , obsVsPred+  , obsPredSpec+  , coefForest+    -- * 線形モデル (描画可能 = X 同梱)+  , LMModel (..)+  , lmModel+    -- * 一般化線形モデル (描画可能 = X + family/link 同梱)+  , GLMModel (..)+  , glmModel+    -- * ガウス過程 (描画可能 = 予測 grid 同梱の 'GPResult' をそのまま)+  , GPResult (..)+    -- * カーネル法ファミリ統合 (GP / KRR / RFF・df |-> gp)+  , Kernel (..)+  , GPParams (..)+  , defaultGPParams+  , GPMethod (..)+  , HyperStrategy (..)+  , GPConfig (..)+  , defaultGP+  , GPSpec+  , gp+  , GPRegModel (..)+  , GPMultiSpec+  , gpMulti+  , GPRegModelN (..)+    -- * 罰則付き回帰 統合 (Ridge/Lasso/EN/MCP/SCAD/Adaptive/Group・df |-> regularized)+  , RegMethod (..)+  , LambdaStrat (..)+  , RegConfig (..)+  , defaultRidge+  , defaultLasso+  , RegSpec+  , regularized+  , regularizedMulti+  , ridge+  , ridgeMulti+  , lasso+  , lassoMulti+  , elasticNet+  , elasticNetMulti+  , RegModel (..)+  , regPredict+    -- * スプライン回帰 (描画可能 = X 同梱、 平滑曲線 + CI band)+  , SplineModel (..)+  , splineModel+    -- * 一般化加法モデル (描画可能 = X 同梱、 平滑曲線のみ・band 非提供)+  , GAMModel (..)+  , gamModel+    -- ** GAM 基底一般化 + GCV (df|-> 高レベル)+  , GAMBasis (..)+  , GAMLambda (..)+  , GAMConfig (..)+  , defaultGAMConfig+  , GAMSpec (..)+  , gam+  , gamMulti+  , GAMModelN (..)+  , fitGAMWith+    -- * ロバスト回帰 (描画可能 = X 同梱、 ロバスト直線・重み diagnostic)+  , RobustModel (..)+  , robustModel+    -- * 多出力線形回帰 (描画可能 = 自己完結の 'MultiFit'、 残差相関 heatmap)+  , MultiFit (..)+    -- * 分位点回帰 (描画可能 = X 同梱、 複数分位線を色分け重畳)+  , QuantileModel (..)+  , quantileModel+    -- * MCMC チェーン (描画可能 = trace + 周辺事後密度、 ベイズ出入口)+  , ChainModel (..)+  , chainModel+    -- * 生存解析 (描画可能 = 自己完結、 KM 生存曲線 / 競合リスク CIF)+  , KMResult (..)+  , CRFit (..)+    -- * 時系列予測 (描画可能 = 履歴 + AR 予測 + 予測区間 band)+  , ForecastModel (..)+  , forecastModel+    -- * 多変量・木 (描画可能 = 自己完結、 PCA scree / RF 重要度)+  , PCAResult (..)+  , RandomForest (..)+    -- * 木/アンサンブル (重要度 bar / 決定木 樹形図)+    --   GradientBoosting / RandomForestClassifier = 特徴重要度 bar、+    --   DecisionTree = MDAG 再利用の樹形図 (新規 mark 不要)+  , GBRegressor (..)+  , GBClassifier (..)+  , RFClassifierFit (..)+  , DTree (..)+  , DTFit (..)+  , treeImportances+  , treePlot+  , treePlotRaw+    -- * 分類 (決定境界 + confusion + 代表散布)+    --   Discriminant / NaiveBayes / KNN。 決定境界・confusion はヘルパ (要範囲/データ)、+    --   toPlot は KNN=訓練点散布 / Discriminant・NB=クラス平均散布+  , ClassPredict (..)+  , decisionBoundaryOf+  , confusionOf+  , MDSView+  , mdsView+  , mdsGroupBy+  , nnLossOf+  , ResidualMode (..)+  , ProfilerSpec (..)+  , profiler+  , profilerResidual+  , contourOf+    -- DOE ワークフロー (Phase 78・Hanalyze.Fit 由来)+  , 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+  , DesignHBMFit (..)+  , designModelHBM+  , MultiOutputSpec (..)+  , multiOutput+  , modelFor+  , svmSupportVectorsOf+  , ScorePredict (..)+  , decisionLineOf+    -- 部分従属図 (PDP / ICE) — Phase 75.27+  , RegPredict (..)+  , PDPView+  , pdp+  , pdpIce+  , pdpOf+  , pdpIceOf+  , pdpPlot+  , pdpIcePlot+  , partialDependencePlot+  , partialDependenceIcePlot+  , DiscriminantFit (..)+  , NBModel (..)+  , GaussianNB (..)+  , KNNClassifier (..)+    -- * 次元圧縮 (PLS score/loading/VIP, MultiGP 多出力 curve)+  , PLSFit (..)+    -- ** PLS 診断ビュー (中間 Plottable Spec・HBM 式統一)+  , PLSView (..)+  , PLSViewKind (..)+  , scoreView+  , loadingView+  , vipView+  , MultiGPResult (..)+  , multiGpCurves+    -- * 時系列・生存・FDA+    --   GARCH=volatility 帯付き線 / AFT=生存曲線 / FDA=平均+固有関数 / β(t)+  , GARCHFit (..)+  , garchVolatility+  , AFTFit (..)+  , aftSurvivalAt+  , FunctionalPCA (..)+  , FLMResult (..)+    -- * 罰則回帰・因果探索+    --   Regularized=係数 bar/係数パス / LiNGAM=因果 DAG (MDAG 再利用)+  , RegFit (..)+  , regPathPlot+  , DirectLiNGAMFit (..)+  , lingamDag+    -- * 記述統計・検定 (describe 分布図 / 検定 effect-CI forest)+  , TestResult (..)+  , testForest+  , testForestLabeled+  , describeBox+    -- * クラスタリング — KMeans の図+    --   'Plottable' 'KMeansResult' (toPlot = centroid 散布) + データ点ヘルパ+  , clusterScatterOf+  , centroidsOf+  , clusterHullOf+  , clusterEllipseOf+  , DendroOpts (..)+  , defaultDendroOpts+  , dendrogramOf+  , dendrogramOf'+    -- * HBM (ベイズ確率プログラム) の学習+  , HBMConfig (..)+  , defaultHBM+  , HBMModel (..)+  , hbmModel+  , hbmModelPure+  , hbmModelIO+    -- * HBM の出力抽出子 (trace / forest)+  , hbmParamNames+  , TraceOpts (..)+  , defaultTraceOpts+  , tracesOf+  , tracesOfWith+  , marginalsOf+  , marginalsByChainOf+    -- * HBM のサンプリング診断 (divergence 可視化)+  , divergencesOf+  , pairOf+  , energyOf+  , autocorrOf+  , autocorrOfLag+  , defaultAutocorrMaxLag+  , rankOf+  , rankOfBins+  , defaultRankBins+  , ForestSpec (..)+  , forestOf+  , forestOfLevel+    -- * HBM の出力抽出子 (epred = 事後予測平均 + HDI band)+  , epred+  , epredAt+    -- * HBM の出力抽出子 (ppc = 事後予測チェック)+  , PPCConfig (..)+  , defaultPPC+  , PPCSpec (..)+  , ppcOf+  , ppcOfWith+  , ppcOfIO+  , ppcOfWithIO+    -- * HBM の出力抽出子 (dag = モデル構造の DAG)+  , DagSpec (..)+  , dagOf+  , dagOfRaw+  , dagOfModel+  , dagOfModelWith+    -- * HBM 診断ダッシュボード (抽出子束ね)+  , dashboardOf+  , dashboardFullOf+  , traceDensityOf+    -- * df |-> spec 統一 fit API (ColumnSource から学習)+  , Fit (..)+  , (|->)+  , (|->!)+    -- ** 二変量近道 spec (列名2つ)+  , LMSpec (..)+  , lm+  , GLMSpec (..)+  , glm+  , SplineSpec (..)+  , spline+  , RobustSpec (..)+  , rlm+  , QuantileSpec (..)+  , rq+    -- ** 行列入力モデルの高レベル spec (列名リスト)+  , PCASpec (..)+  , pca+    -- MDS (Phase 75.21)+  , MDSSpec (..)+  , mds+  , MDSConfig (..)+  , MDSMethod (..)+  , defaultMDS+  , MDSResult (..)+  , PCAStandardize (..)+  , PLSSpec (..)+  , pls+  , PLSConfig (..)+  , defaultPLS+  , LDASpec (..)+  , lda+  , CCASpec (..)+  , ccaOf+  , CCAFit (..)+    -- ** 教師あり ML 分類器/回帰器 spec (特徴列 + ラベル列)+  , GBRSpec (..)+  , gbmReg+  , GBCSpec (..)+  , gbmCls+  , GBConfig (..)+  , defaultGBM+  , DTSpec (..)+  , decisionTree+  , DTConfig (..)+  , defaultDecisionTree+  , KNNCSpec (..)+  , knnCls+  , KNNRSpec (..)+  , knnReg+  , NBSpec (..)+  , naiveBayes+    -- ** seed 純粋化した RNG モデル spec (KMeans / RandomForest)+  , KMeansSpec (..)+  , kmeans+  , KMeansConfig (..)+  , defaultKMeans+  , RFSpec (..)+  , randomForestReg+    -- 因果探索 LiNGAM (高レベル df|-> ・Phase 77)+  , DirectLiNGAMSpec (..)+  , directLingam+  , ParceLiNGAMSpec (..)+  , parceLingam+  , MultiGroupLiNGAMSpec (..)+  , multiGroupLingam+  , VARLiNGAMSpec (..)+  , varLingam+  , PairwiseLiNGAMSpec (..)+  , pairwiseLingam+  , BootstrapLiNGAMSpec (..)+  , bootstrapLingam+  , ICALiNGAMSpec (..)+  , icaLingam+  , CorrelationSpec (..)+  , correlationOf+  , CorrelationGraph (..)+  , LiNGAMFitted (..)+  , lingamDagNamed+  , varLagDagNamed+  , bootstrapEdgeProbOf+  , RFCSpec (..)+  , randomForestCls+  , RFCConfig (..)+  , defaultRFCConfig+  , RFConfig (..)+  , defaultRandomForest+    -- ** SVM / 古典 MLP 高レベル spec (純粋・df |->)+  , MLPClsSpec (..)+  , mlpCls+  , MLPRegSpec (..)+  , mlpReg+  , SVMSpec (..)+  , svmCls+  , SVMHyper (..)+  , SVMTuneGrid (..)+  , defaultSVMTuneGrid+  , SVMConfig (..)+  , defaultSVM+  , SVM (..)+  , SVMMulti (..)+  , numSupportVectors+    -- ** 重み付き最小二乗 (WLS) spec+  , WeightedLMSpec (..)+  , weighted+  , WeightedLMModel (..)+    -- ** 透過標準化ラッパ (自動逆変換)+  , StandardizedSpec (..)+  , standardized+  , standardizedY+  , StandardizedModel (..)+    -- ** 群別フィット spec+  , GroupedSpec (..)+  , grouped+  , GroupedFit (..)+  , groupModels+  , groupLabels+  , groupedFullrange+    -- ** 係数診断の薄アクセサ+  , CoefStats (..)+  , lmDiag+  , groupedLmDiag+    -- ** formula 多変量 spec (R 流)+  , LMFormulaSpec (..)+  , lmF+  , GLMFormulaSpec (..)+  , glmF+  , GLMMFormulaSpec (..)+  , glmmF+    -- ** 重回帰 spec (列名リスト・formula 不要)+  , LMMultiSpec (..)+  , lmMulti+  , GLMMultiSpec (..)+  , glmMulti+  , RobustMultiSpec (..)+  , rlmMulti+  , QuantileMultiSpec (..)+  , rqMulti+  , MultiQuantileModel (..)+    -- ** HBM spec + データ散布図+  , HBMSpec+  , hbm+  , dataScatterOf+  ) where++import qualified Data.Map.Strict       as Map+import           Data.Maybe            (fromMaybe)+import qualified Data.Vector           as V+import qualified Data.Vector.Unboxed    as VU+import qualified Numeric.LinearAlgebra as LA++import           Data.Text             (Text)+import qualified Data.Text             as T+-- (DataFrame の直接 import は未使用のため削除 = upstream decomp PR#2 移植の副産物調査で判明)++import           Hanalyze.Data.ColumnSource     (ColumnSource (..))++import           Graphics.Hgg.Spec     ( VisualSpec, layer, inline, inlineCat+                                       , ColData (..)+                                       , scatter, line+                                       , heatmap, colorBy+                                       , scaleColorManual, legend+                                       , bar, title+                                       , LineType (..) )+import qualified Graphics.Hgg.ThreeD.Spec  as P3++import           Hanalyze.Model.Wrappers+import           Hanalyze.Plot.Core+-- 族別 instance module (Phase 71.5)。 orphan instance を scope に取り込み、+-- 移した族固有 helper (multiGpCurves) を re-export する。+import           Hanalyze.Plot.Linear ()+import           Hanalyze.Plot.Smooth (multiGpCurves)+import           Hanalyze.Plot.Robust ()+-- ベイズ / HBM 連携族 (Phase 71.6)。 orphan instance を scope に取り込み (())、+-- 移した抽出子・型を re-export する。 epredPredRange は本 module の+-- epredSurfaceOfWith でも使うため明示 import する。+import           Hanalyze.Plot.Bayes ()+import           Hanalyze.Plot.Bayes+                   ( hbmParamNames, TraceOpts (..), defaultTraceOpts+                   , tracesOf, tracesOfWith, marginalsOf+                   , marginalsByChainOf, divergencesOf+                   , pairOf, energyOf, autocorrOf, autocorrOfLag, defaultAutocorrMaxLag+                   , rankOf, rankOfBins, defaultRankBins+                   , ForestSpec (..), forestOf, forestOfLevel+                   , epred, epredAt, epredPredRange+                   , PPCConfig (..), defaultPPC, PPCSpec (..)+                   , ppcOf, ppcOfWith, ppcOfIO, ppcOfWithIO+                   , DagSpec (..), dagOf, dagOfRaw, dagOfModel, dagOfModelWith+                   , dashboardOf, dashboardFullOf, traceDensityOf+                   , epredSurfaceOf, epredSurfaceOfWith, dataScatterOf )+-- 汎用ラッパ族 (Phase 71.7)。 orphan instance を scope に取り込み (())、+-- 移したヘルパ (lmDiag / groupedLmDiag / groupedFullrange) を re-export する。+import           Hanalyze.Plot.Wrappers ()+import           Hanalyze.Plot.Wrappers+                   ( lmDiag, groupedLmDiag, groupedFullrange )+-- ML / 統計モデル連携族 (Phase 71.6)。 orphan instance を scope に取り込み (())、+-- 移した抽出子・ヘルパ・型を re-export する。+import           Hanalyze.Plot.ML ()+import           Hanalyze.Plot.ML+                   ( clusterScatterOf, centroidsOf, clusterHullOf, clusterEllipseOf+                   , DendroOpts (..), defaultDendroOpts, dendrogramOf, dendrogramOf'+                   , treeImportances, treePlot, treePlotRaw+                   , decisionBoundaryOf, confusionOf, MDSView, mdsView, mdsGroupBy, nnLossOf, svmSupportVectorsOf, ScorePredict (..), decisionLineOf+                   , RegPredict (..), PDPView, pdp, pdpIce+                   , pdpOf, pdpIceOf, pdpPlot, pdpIcePlot, partialDependencePlot, partialDependenceIcePlot+                   , PLSView (..), PLSViewKind (..), scoreView, loadingView, vipView+                   , garchVolatility, aftSurvivalAt+                   , regPathPlot, lingamDag, lingamDagNamed, varLagDagNamed, bootstrapEdgeProbOf+                   , ResidualMode (..), ProfilerSpec (..), profiler, profilerResidual, contourOf+                   , testForest, testForestLabeled, describeBox )+import           Hanalyze.Diagnostics+import           Hanalyze.Fit+import           Hanalyze.Model.SVM (SVMConfig (..)+                                       , defaultSVM, SVM (..)+                                       , SVMMulti (..), numSupportVectors+                                       , SVMHyper (..)+                                       , SVMTuneGrid (..), defaultSVMTuneGrid)+import           Hanalyze.Model.MDS (MDSResult (..))+import           Hanalyze.Model.LM.Diagnostics (CoefStats (..), lmCoefStats)+import           Hanalyze.Model.GP     (GPResult (..), Kernel (..), GPParams (..), defaultGPParams)+import           Hanalyze.Model.LM     (linspace)+import           Hanalyze.Model.GAM    (GAMBasis (..), GAMLambda (..)+                                              , fitGAMWith)+import           Hanalyze.Model.MultiLM (MultiFit (..))+import           Hanalyze.Model.Cluster (KMeansConfig (..), defaultKMeans)+import           Hanalyze.MCMC.Core     (Chain (..))+import           Hanalyze.Model.HBM     (ModelP, withData+                                       , runDeterministics)+import           Hanalyze.Model.Survival (KMResult (..))+import           Hanalyze.Model.CompetingRisks (CRFit (..))+import           Hanalyze.Model.PCA     (PCAResult (..), PCAStandardize (..))+import           Hanalyze.Stat.Standardize+                   ( Standardizer (..)+                   , applyStandardizerCol )+import           Hanalyze.Model.RandomForest (RandomForest (..)+                                       , RFConfig (..), defaultRandomForest)+import           Hanalyze.Model.GradientBoosting (GBRegressor (..), GBClassifier (..)+                                       , GBConfig (..), defaultGBM)+import           Hanalyze.Model.RandomForestClassifier (RFClassifierFit (..)+                                       , RFCConfig (..), defaultRFCConfig)+import           Hanalyze.Model.DecisionTree (DTree (..), DTFit (..), DTConfig (..), defaultDecisionTree)+import           Hanalyze.Model.Discriminant (DiscriminantFit (..))+import           Hanalyze.Model.Multivariate (CCAFit (..))+import           Hanalyze.Model.NaiveBayes (NBModel (..), GaussianNB (..))+import           Hanalyze.Model.KNN (KNNClassifier (..)+                                       , KNNRegressor (..), predictKNNR)+import           Hanalyze.Model.PLS (PLSFit (..), PLSConfig (..), defaultPLS)+import           Hanalyze.Model.MultiGP (MultiGPResult (..))+import           Hanalyze.Model.GARCH (GARCHFit (..))+import           Hanalyze.Model.AFT (AFTFit (..))+import           Hanalyze.Model.FDA (FunctionalPCA (..), FLMResult (..))+import           Hanalyze.Model.Regularized (RegFit (..))+import           Hanalyze.Model.LiNGAM.Direct (DirectLiNGAMFit (..))+import           Hanalyze.Stat.Test (TestResult (..))++-- ===========================================================================+-- 共通基盤 (class / ModelSpec / grid 評価核) は 'Hanalyze.Plot.Core' へ+-- 切り出した (Phase 71.4)。 本モジュールは Core を import して従来 export を+-- re-export しつつ、 各モデル族固有の instance を残置する。+-- ===========================================================================++-- ===========================================================================+-- ルート1 grid 評価 (ModelSpec) — Phase 16 §3 C1 [→ Plot.Core へ移動]+--+-- fit 済モデルの回帰曲線・CI 帯を **訓練点ではなく等間隔 grid** で評価して描く。+-- 疎・不均一データで曲線がガタつくのを解消する (散布図の点は従来通り訓練データ)。+-- 'statModel' で 'ModelSpec' を作り、 @<>@ でオプションを足す:+--+-- > df |>> (layer (scatter "x" "y") <> toPlot (statModel m <> grid 200))+--+-- 'ModelSpec' は Monoid。 学習済モデル @m@ はクロージャに閉じ込め、 予測は+-- @toPlot@ (描画時) に grid 評価する (ユーザ直感「m は学習・layer で予測」)。+-- ===========================================================================+++-- ===========================================================================+-- 多変量 effect plot (Phase 16 §3 C3)+--+-- 単変数 grid 評価 (C1) を多変量モデルへ一般化する。 along 変数を grid で動かし、+-- 他の説明変数を 'HoldAgg' で固定した「評価点 ModelFrame」 を合成して、 訓練 formula の+-- @designMatrixF@ で評価点設計行列を組み CI を評価する。+--+-- ★評価点 ModelFrame の合成は **DataFrame を経由せず VarRole を直接差し替える**+-- (@designMatrixF@ は 'mfRoles' のみ参照し応答列は使わない = Design.hs:331)。 列構造・+-- 順序が訓練と完全一致するので @confidenceBandAt@ / 'predictGlmMuWithCI' がそのまま使える。+-- 型で単/多変量を分離し ('SingleVarModel' / @MultiVarModel@)、 along 忘れをコンパイル時に弾く。+-- ===========================================================================++++-- ===========================================================================+-- 多変量モデル型 (effect plot 用、 新規 fit)+--+-- 既存の単変数 'LMModel' / 'GLMModel' (設計行列が @[1, x]@ 固定) とは別型。+-- formula 文字列 + @DataFrame@ で多変量 fit し、 formula を保持して評価点設計行列を+-- 組む (HoldAgg 固定 + along grid)。 ★GLM は formula 経路が未整備なので+-- @designMatrixF@ で設計行列を作り 'fitGLMFull' を直接呼ぶ。+-- ===========================================================================++-- (instance MultiVarModel MultiLMModel は Hanalyze.Plot.Linear へ移動 — Phase 71.5)++-- ===========================================================================+-- 列名リスト → 加法線形 Formula AST (パース無し直接合成) — Phase 70.D+--+-- 重回帰 (multiple regression) は formula DSL とは別概念: 説明変数の列名リストから+-- 設計行列 @[1, x1, …, xp]@ を作るだけ。 これを文字列を介さず 'Formula' AST に直接+-- 組み立て、 既存の 'multiLMModelF' / @designMatrixF@ / effect plot 機構をそのまま使う+-- (= @parseModel "y ~ x1 + … + xp"@ と同一 AST。 パラメータ名 @_p0.._pp@ も同じ規約)。+-- ===========================================================================++-- ===========================================================================+-- 多変量ロバスト回帰 (effect plot + 係数サマリ) — Phase 70.D+--+-- ロバスト回帰は formula 経路を持たない (単回帰 'RobustModel' のみだった) ので、+-- 'MultiLMModel' と同型の frame-carrying ラッパを新設する。 設計行列は+-- 'additiveFormula' 由来 (@designMatrixF@ で @[1, x1,…,xp]@)、 fit は 'fitRobustLM'、+-- CI 帯は M 推定量サンドイッチ共分散 ('robustCovBeta'・statsmodels RLM 一致)。+-- ===========================================================================++-- (instance MultiVarModel MultiRobustModel は Hanalyze.Plot.Robust へ移動 — Phase 71.5)++-- (instance MultiVarModel MultiGLMModel は Hanalyze.Plot.Linear へ移動 — Phase 71.5)++-- (instance MultiVarModel PLSModel は Hanalyze.Plot.ML へ移動 — Phase 71.6)++-- ===========================================================================+-- 線形モデル (描画可能)+--+-- 'FitResult' (数値核) は設計行列 X を保持しないが、 回帰線・CI band を描くには+-- X が要る (@confidenceBand@ は X 引数)。 そこで X と生 predictor を束ねた+-- 「描画可能なモデル」 を別型にする (= plot Phase 15 §2.1 の開放論点を (i) で確定)。+-- ===========================================================================+++-- (instance Plottable LMModel / SingleVarModel LMModel は+--  Hanalyze.Plot.Linear へ移動 — Phase 71.5)++-- ===========================================================================+-- 一般化線形モデル (描画可能)+--+-- GLM の不確実性帯は **μ (応答) スケールで非対称** (線形予測子 η の対称 Wald CI を+-- 逆リンク gInv で μ に写すため、 Logit/Log 等では下側・上側の半幅が異なる)。 ゆえに+-- LMModel/GPResult の対称 band (ŷ±se) では忠実に描けない。 そこで+-- 下境界 lo / 上境界 hi を別々に持てる 'band' layer (= MBand area fill) を使い、 μ 曲線は+-- 'line' で重ねる。 帯は **訓練点での Wald CI** を 'predictGlmMuWithCI' で評価する+-- (= grid 補間でなく fit と整合)。 'fitGLMFull' が返す逆 Fisher 情報 Σ=(XᵀWX)⁻¹ が要る。+-- ===========================================================================++-- (instance Plottable GLMModel / SingleVarModel GLMModel は+--  Hanalyze.Plot.Linear へ移動 — Phase 71.5)++-- ===========================================================================+-- ガウス過程 (描画可能)+--+-- 'GPResult' (Hanalyze.Model.GP) は予測 grid (gpTestX) + 事後平均 (gpMean) ++-- credible band (gpLower/gpUpper) を **自己完結** で保持する。 ゆえに LMModel の+-- ように X を別途束ねる必要がなく、 結果型をそのまま 'Plottable' にできる+-- (= 'FitResult' 系と異なる形でも protocol が成り立つことの実証 = plot Phase 15+-- / analyze Phase 46 A6)。+-- ===========================================================================++-- (instance Plottable GPResult は Hanalyze.Plot.Smooth へ移動 — Phase 71.5)++-- ===========================================================================+-- スプライン回帰 (描画可能)+--+-- 'SplineFit' (Hanalyze.Model.Spline) は基底係数 'sfBeta' と、 基底行列で fit した+-- 線形モデル核 'sfResult' (= 'FitResult') を保持する。 ゆえに **基底行列を設計行列と+-- みなせば** LMModel と同じ @confidenceBand@ (= X (XᵀX)⁻¹ Xᵀ の対角) がそのまま使える。+-- 違いは「曲線」 である点だけ: 単回帰の直線でなく、 訓練点を x 昇順に結ぶと基底展開に+-- よる平滑曲線になる ('renderRegression' は encX/encY を線形再フィットせず折れ線で+-- 結ぶため、 ソート済みの点列を渡せば曲線がそのまま描ける = GP と同じ性質)。 帯は+-- LM と同じ **線形モデルの対称 Wald CI** (基底空間での予測分散) なので意味付けも明快。+-- ===========================================================================++-- (splineBasisAt / instance Plottable SplineModel / SingleVarModel SplineModel /+--  Plottable GAMModel / gamGridCI / SingleVarModel GAMModel / SingleVarModel GAMModelN /+--  Plottable GAMModelN は Hanalyze.Plot.Smooth へ移動 — Phase 71.5)++-- ===========================================================================+-- ロバスト回帰 (描画可能)+--+-- 'RobustFit' (Hanalyze.Model.Robust) は M-estimator IRLS の係数 'rfCoef' / fitted+-- 'rfFitted' / 最終重み 'rfWeights' (≤ 1、 外れ値ほど小) を持つが、 **CI / 予測帯を+-- 返す helper を持たない** (sandwich 分散等を別途計算すれば帯は出せるが本 Phase 対象外)。+-- ゆえに代表図 (@toPlot@) は **ロバスト直線のみ** (band 無し)。 ロバスト回帰の価値=+-- 「どの点がダウンウェイトされたか」 は 'diagnosticPlots' 側で **点サイズ = IRLS 重み**+-- の散布図に encode して見せる (主図に点を描くと合成 @df |>> layer scatter <> toPlot@+-- で点が二重になるため、 主図は直線だけにして重み表示は診断束へ回す = user 決定 2026-06-04)。+-- ===========================================================================++-- (instance Plottable RobustModel / robustBand / SingleVarModel RobustModel は+--  Hanalyze.Plot.Robust へ移動 — Phase 71.5)++-- ===========================================================================+-- 多出力線形回帰 (描画可能)+--+-- 'MultiFit' (Hanalyze.Model.MultiLM) は q 個の応答を共通の予測子で同時回帰し、+-- 固有の成果物として **出力間の残差相関 'mfResidCor' (q×q)** を保持する。 q 本の回帰+-- 関係を単一図に素直に載せる方法は一意でない (出力ごとスケールが異なり得る) ため、+-- 代表図 (@toPlot@) は **残差相関 heatmap** とする (= 多出力回帰固有の図。 user 決定+-- 2026-06-04)。 'MultiFit' は heatmap に必要な相関行列を自己完結で持つので、 'GPResult'+-- 同様 X を別途束ねず結果型をそのまま 'Plottable' にできる。 個別の出力 j の回帰線は+-- 'predictMultiLM' で別途描ける (本 instance の対象外)。+--+-- ⚠ 'heatmap' (geom_tile) は **categorical 軸専用** (renderHeatmap が x/y をラベルとして+-- カテゴリ軸の index に引く。 実測: Render/Statistical.hs)。 ゆえに格子座標は数値でなく+-- **出力名ラベル** ("y1", "y2", …) を 'inlineCat' で渡す (数値だとカテゴリ軸が立たず+-- 全セルが drop されてタイルが描かれない = 計測で確認)。+-- ===========================================================================++-- (instance Plottable MultiFit は Hanalyze.Plot.Wrappers へ移動 — Phase 71.7)++-- ===========================================================================+-- 分位点回帰 (描画可能)+--+-- 'QRFit' (Hanalyze.Model.Quantile) は 1 つの分位 τ に対する係数 + fitted 'qfYHat' を+-- 持つ。 OLS が条件付き平均を引くのに対し分位回帰は条件付き τ-分位を引くので、 複数の+-- τ (例 0.1/0.5/0.9) の fit を重ねると **予測区間そのものを線群で** 表現できる+-- (= heteroscedastic データで帯より直接的)。 ゆえに 'QuantileModel' は複数の τ-fit を+-- 束ね、 @toPlot@ で **分位ごとに 1 本の line layer を色分けして重畳** する (band は使わ+-- ない。 分位線自体が区間の縁を成すため)。 各線は 'color' ('fromHex') で固定色を割り当てる。+-- ===========================================================================++-- (instance Plottable QuantileModel / Plottable MultiQuantileModel は+--  Hanalyze.Plot.Robust へ移動 — Phase 71.5)+++-- ===========================================================================+-- クラスタリング (KMeans) の図 — Phase 68 A1+--+-- KMeans の分野定番の図は「クラスタ別散布 (色=ラベル)」。 ただし+-- 'KMeansResult' は centroids + labels + inertia のみ保持し **生データ座標を+-- 持たない**。 そこで 'surfaceOf' <> 'dataScatter3DOf' と同じ **model 層 / data+-- 層の二層イディオム**に分ける:+--+--   * 'Plottable' 'KMeansResult' の @toPlot@ = centroid 散布のみ (データ不要・+--     クラス契約 @m -> VisualSpec@ を満たす)。 既定は centroid 行列の第 0/1 次元。+--   * 'clusterScatterOf' = データ点をラベル色で散布 (要データ源・列名指定)。+--   * 'centroidsOf' = centroid を任意 2 次元で重畳 (✚ マーカー・次元 index 明示)。+--+-- 定番図 = @df |>> (clusterScatterOf df res \"x\" \"y\" <> centroidsOf res 0 1)@。+-- ⚠ centroid 行列は **学習時の特徴量列順**のみで列名を持たない。 重畳時は+-- データ列 (@xn@, @yn@) と centroid 次元 (@i@, @j@) の対応をユーザが揃える。+-- ===========================================================================++-- (instance Plottable KMeansResult / clusterScatterOf / centroidsOf は+--  Hanalyze.Plot.ML へ移動 — Phase 71.6)++-- ===========================================================================+-- HBM (ベイズ確率プログラム) の学習 — Phase 49 A1+--+-- 'Hanalyze.Model.HBM' の free-monad DSL で書いた確率プログラム ('ModelP') を+-- NUTS で学習し、 「学習済 HBM モデル」 ('HBMModel') という第一級の値にする。+-- 命名は頻度論側の @lmModel → LMModel@ / @glmModel → GLMModel@ と対称的+-- ('hbmModel → HBMModel')。 違いは学習が MCMC ゆえ IO・重い・async 並列+-- (既存 'nutsChains' が 'mapConcurrently' で multi-chain 並列) という点のみ。+--+-- データは df 由来の列 (名前付き) を 'withData' でモデル中の placeholder+-- (@dataNamed@ / observe の参照名) に **自動 bind** する。 これは PyMC の+-- @pm.Data@ + @set_data@ と同型 (= 同じモデルを別データで再評価できる設計)。+--+-- ★ 'HBMModel' は **直接 'Plottable' にしない** (確率プログラムは「単一の図」 に+-- 一意に落ちない)。 描画は抽出子 (@epred@ / @tracesOf@ / 'ppcOf' / 'forestOf' /+-- @dagOf@、 後続 sub で追加) を明示する設計 (Phase 49 計画 Q1)。+-- ===========================================================================+++-- ===========================================================================+-- 生存解析 (描画可能)+--+-- 生存関数 Ŝ(t) (Kaplan-Meier) と累積発生関数 CIF (競合リスク) はいずれも **階段関数**+-- (イベント時刻で不連続にジャンプ、 その間は平坦)。 折れ線 ('line') は点間を線形に結ぶので、+-- そのまま渡すとジャンプが斜めになる。 ゆえに **階段頂点を明示展開** する helper+-- 'stepVerts' で (0, s0) から各イベント時刻の「水平→垂直」 2 頂点を作り、 line で結ぶ+-- (= 正しい階段形)。 KM は s0=1 で下降、 CIF は s0=0 で上昇。 KMResult / CRFit は時刻と+-- 値を自己完結で持つので 'GPResult' 同様そのまま 'Plottable' にできる。+-- ===========================================================================+++-- (instance Plottable KMResult / Plottable CRFit は+--  Hanalyze.Plot.ML へ移動 — Phase 71.6)++-- ===========================================================================+-- 時系列予測 (描画可能)+--+-- AR(p) の点予測 'forecastAR' は将来値の中心のみを返す。 予測の不確実性帯は **h-step+-- 予測分散** から得る: AR の MA(∞) 表現の ψ-weights (ψ₀=1, ψⱼ=Σφᵢψⱼ₋ᵢ) を用いて+-- @Var(ŷ_{n+k}) = σ² Σ_{j=0}^{k-1} ψⱼ²@ (σ² = 革新分散 'arResidVar')。 これは Gaussian+-- 革新の下での正統な予測区間 (地平 k とともに単調に広がる)。 対称ゆえ band は+-- @中心 ± z·se@。 @toPlot@ は履歴折れ線 + 予測折れ線 + 予測区間 band を 1 枚に重ねる。+-- ===========================================================================++-- (arPsiWeights / arForecastSE / instance Plottable ForecastModel は+--  Hanalyze.Plot.ML へ移動 — Phase 71.6)++-- ===========================================================================+-- 多変量・木 (描画可能)+--+-- PCA の代表図は **scree plot** (各主成分の寄与率 'pcaExplainedRatio' を棒で)、 木 (RF) の+-- 代表図は **特徴重要度バー** ('featureImportance')。 いずれも自己完結ゆえそのまま+-- 'Plottable'。 棒の x 軸はラベル ("PC1".. / "f1"..) なので 'inlineCat' (categorical) で渡す+-- (heatmap A9 と同じく 'bar' も categorical 軸が必要)。 優先低 (§3.5 A14) ゆえ scree/重要度+-- の 1 枚ずつに絞る (biplot や木構造図は将来拡張)。+-- ===========================================================================++-- (instance Plottable PCAResult / Plottable RandomForest は+--  Hanalyze.Plot.ML へ移動 — Phase 71.6)++-- ===========================================================================+-- 木/アンサンブル — Phase 68 A2+--+-- 各モデルの分野定番図を **既存 mark のみ**で描く (新規 plot mark 不要):+--+--   * GradientBoosting (回帰/分類)・RandomForestClassifier = **特徴重要度 bar**。+--     GBM は重要度フィールドを持たないので弱学習器 ('Tree') の split 使用回数から+--     純粋計算する ('treeImportances'・RF.'featureImportance' と同方式・正規化)。+--   * DecisionTree = **樹形図**。 決定木は DAG の特殊形 (二分木) ゆえ、 HBM の+--     ModelGraph と同じ MDAG (Sugiyama 階層 layout) を **再利用**して node-link で描く+--     (split ノード = "f{j} ≤ {thr}"、 葉 = "y={class}")。+--+-- ⚠ DecisionTree の edge True/False ラベル・gini・サンプル数表示 (sklearn plot_tree+-- 相当) は DAGNode/DAGEdge が持たないため v1 では描かない。 必要なら専用 mark を+-- plot 側 Phase として起こす (= dendrogram Phase 48 と同型の判断)。+-- ===========================================================================++-- (treeImportances / instance Plottable GBRegressor / GBClassifier /+--  RFClassifierFit / DTree / dtreeToDag は Hanalyze.Plot.ML へ移動 — Phase 71.6)++-- ===========================================================================+-- 分類 (Discriminant / NaiveBayes / KNN) — Phase 68 A3+--+-- 代表図は **決定境界** と **confusion 行列**。 いずれも「学習済モデルを評価点で+-- 走らせる」 図ゆえ、 KMeans (A1) と同じく **データ/範囲を取るヘルパ**で提供する+-- (新規 plot mark 不要):+--+--   * @decisionBoundaryOf@ = 2D grid を予測しクラス色で塗る (= 連続軸の散布を+--     四角マーカー・低 alpha で「領域」表現。 ★renderHeatmap はカテゴリ軸なので+--     連続 grid には不適 → 'MScatter' + 'colorBy' (離散色) を採用)。 2 特徴前提。+--   * 'confusionOf' = テストデータの真値×予測の件数を @MHeatmap@ で (カテゴリ軸が適合)。+--+-- 'Plottable' の @toPlot@ (データ非保持で描ける代表 1 枚):+--   * KNN は訓練データ ('knnCX'/'knnCY') を保持 → **ラベル色の訓練点散布**。+--   * Discriminant / NaiveBayes(Gaussian) は **クラス平均散布** (✚)、+--     NaiveBayes(Multinomial) は **クラス事前確率 bar**。+-- ===========================================================================+++-- (instance ClassPredict DiscriminantFit / NBModel / KNNClassifier /+--  decisionBoundaryOf / confusionOf / instance Plottable KNNClassifier /+--  DiscriminantFit / NBModel は Hanalyze.Plot.ML へ移動 — Phase 71.6)++-- ===========================================================================+-- 次元圧縮 (PLS / MultiGP) — Phase 68 A4+--+-- どちらも結果が自己完結 ('PCAResult' 同様) なので外部データ不要で 'Plottable':+--+--   * 'PLSFit' = 潜在空間の **score plot** (標本 T) を代表図に、 'loading plot' (変数 P)+--     と **VIP bar** を診断図束に。 いずれも既存 'MScatter'/'bar'。+--   * 'MultiGPResult' = **多出力の予測曲線 + 95% band** (出力ごとに色分け・x=index)。+--     'MLine' + 'MBand' を出力数ぶん重畳。+--+-- ※ 'Hanalyze.Model.MultiOutput' は変換+メトリクスの **ユーティリティ**で+-- fit 結果型を持たないため 'Plottable' 対象外 (多出力の「相関」図は既存+-- 'MultiFit' = 残差相関 heatmap が担当)。 新規 plot mark は不要。+-- ===========================================================================+++-- (PLSViewKind / PLSView / scoreView / loadingView / vipView /+--  instance Plottable PLSView / PLSFit は Hanalyze.Plot.ML へ移動 — Phase 71.6)++-- (multiGpCurves / instance Plottable MultiGPResult は+--  Hanalyze.Plot.Smooth へ移動 — Phase 71.5)++-- ===========================================================================+-- 時系列・生存・FDA (GARCH / AFT / FDA) — Phase 68 A5+--+-- 新規 plot mark は不要 (既存 line/band の重畳):+--+--   * 'GARCHFit'      = 系列 (μ + ε_t) + 条件付き volatility 帯 (μ ± 2σ_t) の帯付き線。+--   * 'AFTFit'        = パラメトリック生存曲線 S(t|x)。 fit は観測時刻を持たないので+--                       代表図 (@toPlot@) は **基準共変量** (intercept のみ) の曲線、+--                       任意共変量は 'aftSurvivalAt' ヘルパ。 t 範囲は予測平均寿命から導出。+--   * 'FunctionalPCA' = 平均関数 + 上位固有関数を grid 上に重畳 (x = grid index)。+--   * 'FLMResult'     = 関数回帰係数 β(t) の曲線。+-- ===========================================================================++-- (garchVolatility / instance Plottable GARCHFit / aftSurvivalAt /+--  instance Plottable AFTFit / FunctionalPCA / FLMResult は+--  Hanalyze.Plot.ML へ移動 — Phase 71.6)++-- ===========================================================================+-- 罰則回帰・因果探索 (Regularized / LiNGAM) — Phase 68 A6+--+-- 新規 plot mark は不要:+--+--   * 'RegFit'          = 単一 λ の係数 ('rfBeta') を bar (代表図)。+--   * 'regPathPlot'     = 正則化パス @[(λ, [β_j])]@ ('regularizationPath' 出力) を、+--                         係数ごとに 1 本の line で λ-横軸に重畳 (= LASSO 係数パス図)。+--   * @DirectLiNGAMFit@ = 推定した因果構造を **MDAG** で描く (B 行列 → node/edge、+--                         決定木と同じ MDAG 再利用)。 edge j→i は @|adjacency[i,j]|>0@。+-- ===========================================================================++-- (instance Plottable RegFit / regPathPlot / lingamDag /+--  instance Plottable DirectLiNGAMFit は Hanalyze.Plot.ML へ移動 — Phase 71.6)++-- ===========================================================================+-- 記述統計・検定 (Stat.*) — Phase 68 A7+--+-- 新規 plot mark は不要:+--+--   * 'TestResult'  = 効果量 + 95% CI の **forest** (検定パラメータの区間 + 0 基準線)。+--                     代表図 (@toPlot@) は 1 行 forest、 複数検定は 'testForest'。+--   * 'describeBox' = 生データ列の **box plot** (= describe の分布図・5 数要約を可視化)。+-- ===========================================================================++-- (testForest / testForestLabeled / instance Plottable TestResult /+--  describeBox は Hanalyze.Plot.ML へ移動 — Phase 71.6)+++-- (instance SingleVarModel WeightedLMModel / Plottable WeightedLMModel は+--  Hanalyze.Plot.Linear へ移動 — Phase 71.5)++++-- C2: 元スケール逆変換 instance (Phase 70.3 項目 C) -------------------------+--+-- 内側モデルは標準化空間で学習されている。 ここで予測子 x を入力時に標準化し、+-- (@standardizedY@ なら) 応答 y を出力時に逆変換することで、 図・予測を**元スケール**で+-- 返す。 単変量 (1 特徴) 描画が対象 (smXStd の 0 次元を使う)。++-- (instance SingleVarModel KNNRegressor / stMu1 / stSd1 / unstdY /+--  SingleVarModel (StandardizedModel m) / Plottable (StandardizedModel m) は+--  Hanalyze.Plot.Wrappers へ移動 — Phase 71.7)++-- ===========================================================================+-- 混合効果モデル (random effects) — Phase 52 D3+--+-- 'GLMMResultRE' (Phase 48 の vector random effects: random intercept + slope)+-- を caterpillar plot で描く。 各 group の BLUP @b̂_j@ を **値で昇順ソート**し、+-- forest mark (水平棒) で並べる。 0 (= 固定効果からの偏差ゼロ) に参照線を引く。+-- group 間の random effect のばらつき・外れ群を一目で読めるのが GLMM 固有の定番図。+--+-- ★ CI 帯は現状なし (点のみ): 'GLMMResultRE' は per-group の conditional variance+-- も観測数 @n_j@ も格納しておらず (scalar 専用の 'glmmBLUPSE' は 'GLMMResult' 用で+-- 流用不可)、 BLUP の標準誤差を単体から計算できない。 将来 conditional variance を+-- 持たせれば forest の誤差半幅を埋めて帯化できる (forest mark は対称 CI 対応済)。+--+-- @toPlot@          = random-effect 第 1 列 (通常 intercept) の caterpillar 1 枚。+-- 'diagnosticPlots' = 全 r 列 (intercept + 各 slope) の caterpillar list。+-- ===========================================================================++++-- (instance SingleVarModel GPRegModel / Plottable GPRegModel /+--  SingleVarModel GPRegModelN / Plottable GPRegModelN は+--  Hanalyze.Plot.Smooth へ移動 — Phase 71.5)+++-- (instance Plottable RegModel / regMethodName / roundTo は+--  Hanalyze.Plot.Wrappers へ移動 — Phase 71.7)++-- (familyObsDist は Hanalyze.Plot.Linear へ移動 — Phase 71.5)++-- (lmDiag / groupedLmDiag / instance Plottable (GroupedFit spec) /+--  renderGrouped / groupedFullrange / renderGroupedWith /+--  instance ColumnSource [(Text, ColData)] は+--  Hanalyze.Plot.Wrappers へ移動 — Phase 71.7)++-- (dataScatterOf は Hanalyze.Plot.Bayes へ移動 — Phase 71.7)+
+ src/Hanalyze/Plot/Bayes.hs view
@@ -0,0 +1,1341 @@+-- |+-- Module      : Hanalyze.Plot.Bayes+-- Description : hgg 連携層 — ベイズ / HBM 連携族の図化 instance + 抽出子+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- [日本語]: hgg 連携層 — __ベイズ / HBM 連携族__ の図化 instance + 抽出子。+--+-- ⚠ 親 'Hanalyze.Plot' と同じく別パッケージ @hanalyze-plot@ に属し、+-- @cabal build --project-file=cabal.project.plot@ で build される。 共通基盤 (class / ModelSpec / grid 評価核) は+-- 'Hanalyze.Plot.Core' を import して取り込む (orphan instance を許容)。+--+-- 担当する型・抽出子 (= MCMC chain / HBM 出力):+--   ChainModel の trace / 周辺事後密度・HBM の trace/forest/epred/ppc/dag 抽出子 (統一済)・+--   GLMMResultRE の caterpillar plot。 HBM の *学習* (hbmModel 等) は+--   'Hanalyze.Fit' / 'Hanalyze.Model.Wrappers' 側 (こちらは描画連携のみ)。+--+-- [English]: hgg integration layer — __Bayesian / HBM family__+-- plotting instances and extractors.+--+-- ⚠ Lives in the same separate package @hanalyze-plot@ as the parent+-- module 'Hanalyze.Plot', built via @cabal build --project-file=cabal.project.plot@.+-- It imports the+-- shared foundation (the class \/ ModelSpec \/ grid-evaluation core) from+-- 'Hanalyze.Plot.Core' (orphan instances are allowed here).+--+-- Types and extractors covered (= MCMC chains \/ HBM output):+--   'ChainModel'\'s trace \/ marginal posterior density; HBM's unified+--   trace\/forest\/epred\/ppc\/dag extractors; and 'GLMMResultRE'\'s+--   caterpillar plot. HBM *training* (@hbmModel@ etc.) lives on the+--   'Hanalyze.Fit' \/ 'Hanalyze.Model.Wrappers' side (this+--   module handles plotting integration only).+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ImpredicativeTypes #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleContexts #-}+module Hanalyze.Plot.Bayes+  ( -- * HBM の出力抽出子 (trace / forest)+    hbmParamNames+  , TraceOpts (..)+  , defaultTraceOpts+  , tracesOf+  , tracesOfWith+  , marginalsOf+  , marginalsByChainOf+    -- * HBM のサンプリング診断 (divergence 可視化)+  , divergencesOf+  , pairOf+  , energyOf+  , autocorrOf+  , autocorrOfLag+  , defaultAutocorrMaxLag+  , rankOf+  , rankOfBins+  , defaultRankBins+  , ForestSpec (..)+  , forestOf+  , forestOfLevel+    -- * HBM の出力抽出子 (epred = 事後予測平均 + HDI band)+  , epred+  , epredAt+  , epredPredRange+    -- * 応答曲面 3D / 散布 (HBM 固有)+  , epredSurfaceOf+  , epredSurfaceOfWith+  , dataScatterOf+    -- * HBM の出力抽出子 (ppc = 事後予測チェック)+  , PPCConfig (..)+  , defaultPPC+  , PPCSpec (..)+  , ppcOf+  , ppcOfWith+  , ppcOfIO+  , ppcOfWithIO+    -- * HBM の出力抽出子 (dag = モデル構造の DAG)+  , DagSpec (..)+  , dagOf+  , dagOfRaw+  , dagOfModel+  , dagOfModelWith+    -- * HBM 診断ダッシュボード (抽出子束ね)+  , dashboardOf+  , dashboardFullOf+  , traceDensityOf+  ) where++import           Data.List             (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           System.Random.MWC     (createSystemRandom, initialize, Gen)+import           Control.Monad.Primitive (PrimMonad, PrimState)+import           Control.Monad.ST      (runST)+import qualified Numeric.LinearAlgebra as LA++import           Data.Text             (Text)+import qualified Data.Text             as T++import           Graphics.Hgg.Spec     ( VisualSpec, layer, inline, inlineCat+                                       , Color (..), fromHex+                                       , scatter, line, band, bar+                                       , position, Position (..)+                                       , color, colorBy, lineRange+                                       , scaleColorManual, legendOff+                                       , legendPos, LegendPosition (..)+                                       , trace, density, forest, forestNull+                                       , subplots, subplotCols, width, height+                                       , xLabel, yLabel, title+                                       , ecdf, alpha+                                       , dagFromListsWithPlates+                                       , DAGNode (..), DAGEdge (..), DAGPlate (..)+                                       , DAGNodeKind (..), DAGLayoutAlgorithm (..) )+import           Graphics.Hgg.DAG      (layoutHierarchicalFullWithPlates)+import           Graphics.Hgg.Render.Special (bakeDAGRoutesInSpec)+import qualified Graphics.Hgg.ThreeD.Spec  as P3++import           Hanalyze.Model.Wrappers+import           Hanalyze.Plot.Core+import           Hanalyze.MCMC.Core     (Chain (..), chainVals)+import           Hanalyze.MCMC.BayesianTest (highestDensityInterval)+import           Hanalyze.Stat.MCMC    (kde, autocorr, rankHist)+import           Hanalyze.Model.HBM.Sampling (sampleObsRep)+import           Hanalyze.Model.HBM     (ModelP, withData+                                       , runDeterministics, runObserveDists+                                       , buildModelGraph, ModelGraph (..)+                                       , collapseIndexedPlateNodes+                                       , sampleNames+                                       , Node (..), NodeKind (..))+import           Hanalyze.Model.LM     (linspace)+import           Hanalyze.Model.GLMM   (GLMMResultRE (..))++-- ===========================================================================+-- MCMC チェーン (描画可能)+--+-- 'Chain' (Hanalyze.MCMC.Core) は post-burn-in の draw 列 'chainSamples' を保持する+-- (各 draw は Map パラメータ名→値)。 ベイズの「出入口」 = サンプラの収束診断と周辺事後の+-- 可視化。 1 つのパラメータを選び、 代表図 (@toPlot@) は **trace plot** (draw index 対値、+-- = 混合・定常性の目視)、 診断束 ('diagnosticPlots') に **周辺事後密度** (MDensity) を加える。+-- trace と density は座標系が異なる (index-値 vs 値-密度) ため 1 枚に混ぜず別図にする。+-- ===========================================================================++instance Plottable ChainModel where+  -- trace plot: draw index 対 パラメータ値 (折れ線 = MTrace)。+  toPlot m =+    let vals  = chainVals (cmParam m) (cmChain m)+        iters = [ fromIntegral i | i <- [1 .. length vals] ] :: [Double]+    in layer (trace (inline iters) (inline vals))++  -- 診断束: trace + 周辺事後密度 (MDensity)。+  diagnosticPlots m =+    let vals  = chainVals (cmParam m) (cmChain m)+        iters = [ fromIntegral i | i <- [1 .. length vals] ] :: [Double]+    in [ layer (trace (inline iters) (inline vals))+       , layer (density (inline vals))+       ]++-- ===========================================================================+-- HBM の出力抽出子 — Phase 49 A2 / Phase 74 (trace / forest)+--+-- 'HBMModel' は直接 'Plottable' にしない (確率プログラムは単一の図に一意に落ちない)。+-- 代わりに抽出子を明示する。 trace は @tracesOf@ / 'tracesOfWith' に統一:+--   * @tracesOf@     = 各 latent パラメータの trace plot を **param ごと独立パネル**+--                      ('[VisualSpec]') で返す。 divergence rug は既定 ON (ArviZ 流)。+--   * 'tracesOfWith' = 'TraceOpts' で divergence on/off と chain 別重畳を切り替える。+--   * 'forestOf'     = 各 latent の事後区間 (事後平均 + 94% HDI) を 'MForest' mark で。+--+-- ★ Phase 74 で旧 @traceOf@ ([ChainModel]) / @tracesByChainOf@ /+-- @tracesWithDivergencesOf@ の 3 本を統合した。 戻り型を兄弟抽出子 (marginalsOf 等)+-- と同じ '[VisualSpec]' に揃え、 @vconcat (tracesOf m)@ で param ごと縦並びに描ける+-- (旧 docs の @foldMap toPlot (traceOf m)@ = 全 param を 1 軸に重畳する誤りを排除)。+-- ===========================================================================++-- | [日本語]: 学習済モデルの latent パラメータ名 (= 事後を持つ未知数の一覧)。+--   [English]: The trained model's latent parameter names (= the list of+--   unknowns that have a posterior).+hbmParamNames :: HBMModel -> [Text]+hbmParamNames = sampleNames . hbmModelSpec++-- | [日本語]: 1 パラメータの post-burn-in draw を全 chain 連結で取り出す。+--   [English]: Retrieves one parameter's post-burn-in draws, concatenated+--   across all chains.+hbmDraws :: Text -> HBMModel -> [Double]+hbmDraws name = concatMap (chainVals name) . hbmChainsR++-- | [日本語]: 全 chain の draw を 1 本に連結した 'Chain' (trace 表示用)。 index は+--   chain を端から端へ並べた通し番号になる (trace は混合の目視が目的)。+--   divergence index も同じ連結順の通し番号に変換する ('pooledDivergences' が正本。+--   chain 内 index のまま連結すると merged frame で別の draw を指してしまう)。+--   [English]: A single 'Chain' with all chains' draws concatenated (for+--   trace display). The index becomes a running number across the+--   end-to-end chains (the trace's purpose is to visually check mixing).+--   Divergence indices are converted to the same concatenated running+--   numbering ('pooledDivergences' is the canonical source; concatenating+--   with raw within-chain indices would point at the wrong draw in the+--   merged frame).+mergeChains :: [Chain] -> Chain+mergeChains []  = Chain [] 0 0 [] [] []+mergeChains chs = Chain+  { chainSamples     = concatMap chainSamples chs+  , chainAccepted    = sum (map chainAccepted chs)+  , chainTotal       = sum (map chainTotal chs)+  , chainEnergy      = concatMap chainEnergy chs+  , chainDivergences = pooledDivergences chs+  , chainTreeDepths  = concatMap chainTreeDepths chs+  }++-- | [日本語]: trace 診断の設定 ('ppcOf' / 'PPCConfig' と同じ「関数 + config」 慣用)。+--   [English]: Trace-diagnostic settings (the same "function + config" idiom+--   as 'ppcOf' \/ 'PPCConfig').+data TraceOpts = TraceOpts+  { toShowDivergences :: !Bool  -- ^ [日本語]: 発散 draw の rug を重ねる (既定 True・ArviZ 流)。 [English]: Overlay a rug of divergent draws (default True, ArviZ-style).+  , toByChain         :: !Bool  -- ^ [日本語]: True で chain 別重畳、 False で全 chain merged (既定)。 [English]: True overlays chains separately; False merges all chains (the default).+  } deriving (Show, Eq)++-- | [日本語]: 既定の trace 設定 = divergence rug ON・全 chain merged。+--   [English]: Default trace settings = divergence rug ON, all chains+--   merged.+defaultTraceOpts :: TraceOpts+defaultTraceOpts = TraceOpts { toShowDivergences = True, toByChain = False }++-- | [日本語]: 各 latent パラメータの trace plot を __param ごと独立パネル__ ('[VisualSpec]')+--   で返す (divergence rug 既定 ON)。 @noDf |>> vconcat (tracesOf m)@ で param ごとに+--   縦並びの trace になる (= ArviZ @plot_trace@ 右列)。 設定は 'tracesOfWith'。+--   [English]: Returns the trace plot of each latent parameter as an+--   __independent panel per param__ ('[VisualSpec]') with the divergence rug+--   on by default. @noDf |>> vconcat (tracesOf m)@ stacks the traces+--   vertically per param (= ArviZ's @plot_trace@ right column). Use+--   'tracesOfWith' to configure it.+tracesOf :: HBMModel -> [VisualSpec]+tracesOf = tracesOfWith defaultTraceOpts++-- | [日本語]: 'TraceOpts' を明示する @tracesOf@。 旧 @traceOf@ (merged 単線) /+--   @tracesByChainOf@ (chain 別重畳) / @tracesWithDivergencesOf@ (chain 別 + rug) の+--   3 本を 1 つに統合したもの:+--+--     * @tracesOfWith (TraceOpts False False)@ = 旧 @traceOf@ 相当 (merged 単線・rug 無し)+--     * @tracesOfWith (TraceOpts False True )@ = 旧 @tracesByChainOf@ (chain 別重畳・rug 無し)+--     * @tracesOfWith (TraceOpts True  True )@ = 旧 @tracesWithDivergencesOf@ (chain 別 + rug)+--     * 既定 @tracesOf@ = @TraceOpts True False@ (merged + rug)+--+--   divergence rug は各図下端 (y = 当該 param の全 chain 最小値) に発散 draw の x 位置を+--   縦棒 ('lineRange') で打つ。 merged では通し index ('divergencesOf')、 chain 別では+--   chain 内 1-based iteration を x にする (それぞれの trace の x 軸と整合)。+--   divergence が無ければ rug レイヤは付かない。+--   [English]: @tracesOf@ with an explicit 'TraceOpts'. Unifies the former+--   three functions — @traceOf@ (merged single line), @tracesByChainOf@+--   (overlaid per chain), and @tracesWithDivergencesOf@ (per chain + rug) —+--   into one:+--+--     * @tracesOfWith (TraceOpts False False)@ = the old @traceOf@ (merged single line, no rug)+--     * @tracesOfWith (TraceOpts False True )@ = the old @tracesByChainOf@ (overlaid per chain, no rug)+--     * @tracesOfWith (TraceOpts True  True )@ = the old @tracesWithDivergencesOf@ (per chain + rug)+--     * default @tracesOf@ = @TraceOpts True False@ (merged + rug)+--+--   The divergence rug draws a vertical tick ('lineRange') at each divergent+--   draw's x position, at the bottom of each panel (y = that param's minimum+--   across all chains). Merged mode uses the running index+--   ('divergencesOf'); per-chain mode uses the 1-based iteration within the+--   chain (matching each trace's own x axis). No rug layer is added when+--   there are no divergences.+tracesOfWith :: TraceOpts -> HBMModel -> [VisualSpec]+tracesOfWith opts hbm =+  [ traceLayers nm <> rugLayer nm <> title nm | nm <- hbmParamNames hbm ]+  where+    chs = hbmChainsR hbm+    traceLayers nm+      | toByChain opts =+          foldMap (\(k, ch) ->+              let vals  = chainVals nm ch+                  iters = [ fromIntegral i | i <- [1 .. length vals] ] :: [Double]+              in layer (trace (inline iters) (inline vals) <> color (fromHex (chainColor k))))+            (zip [0 ..] chs)+      | otherwise =+          let vals  = chainVals nm (mergeChains chs)+              iters = [ fromIntegral i | i <- [1 .. length vals] ] :: [Double]+          in layer (trace (inline iters) (inline vals))+    rugLayer nm+      | not (toShowDivergences opts) = mempty+      | otherwise =+          let allVals = concatMap (chainVals nm) chs+              -- merged: 連結通し index (divergencesOf)。chain 別: chain 内 1-based iteration。+              xs | toByChain opts = [ fromIntegral (i + 1) | ch <- chs, i <- chainDivergences ch ] :: [Double]+                 | otherwise      = [ fromIntegral (i + 1) | i <- divergencesOf hbm ] :: [Double]+          in if null xs || null allVals+               then mempty+               -- ArviZ tick 同型 = 下端から値域 2% の短い縦棒。 定数 trace では 1e-9 最小高。+               -- ★lineRange の意味論は (x, 中心 y, ±err) = 下端 yMin〜yMin+tick の棒。+               else let yMin = minimum allVals+                        yMax = maximum allVals+                        tick = max ((yMax - yMin) * 0.02) 1e-9+                        nDiv = length xs+                    in layer (lineRange (inline xs)+                                        (inline (replicate nDiv (yMin + tick / 2)))+                                        (inline (replicate nDiv (tick / 2)))+                              <> color (fromHex divergenceColor))++-- | [日本語]: 各 latent パラメータの __周辺事後密度__ を per-param で list 返しする。+--   @tracesOf@ (per-param trace) の密度版で、 'ChainModel' の @diagnosticPlots@ が出す+--   周辺事後密度 (@density@、 root: 'diagnosticPlots' ChainModel 経路) を 1 パラメータ+--   1 図に切り出したもの。 全 chain の post-burn-in draw をプール ('hbmDraws') した+--   周辺分布を描き、 図タイトルにパラメータ名を付す。+--+--   @subplots (map toPlot (marginalsOf fit)) <> subplotCols 1@ で周辺事後の grid を組め、+--   入れ子 subplots と合わせて HBM ダッシュボードの 1 列になる。+--   [English]: Returns each latent parameter's __marginal posterior density__+--   as a per-param list. This is the density counterpart of+--   @tracesOf@ (per-param trace): it slices out the marginal posterior+--   density (@density@; sourced from 'ChainModel'\'s @diagnosticPlots@ path)+--   into one figure per parameter. It plots the marginal distribution built+--   by pooling all chains' post-burn-in draws ('hbmDraws'), with the+--   parameter name as the figure title.+--+--   @subplots (map toPlot (marginalsOf fit)) <> subplotCols 1@ builds a grid+--   of marginal posteriors, which combined with nested subplots becomes one+--   column of an HBM dashboard.+marginalsOf :: HBMModel -> [VisualSpec]+marginalsOf hbm =+  [ layer (density (inline (hbmDraws nm hbm))) <> title nm+  | nm <- hbmParamNames hbm ]++-- ===========================================================================+-- HBM のサンプリング診断 — Phase 59.4 / 74 (divergence の通し index + pair/energy)+--+-- 'Chain' は NUTS の発散 draw index ('chainDivergences' = chain 内 0-based・+-- post-burn-in、 root: request/255 §4) と Hamiltonian energy を記録済み。 ここでは+-- それを plot-core の語彙 (scatter + color) で図示する。 rug 用の新 MarkKind は+-- 追加しない (計画 md の設計判断: 既存 mark の組合せで足りることを確認してから諮る)。+-- trace の divergence rug 自体は 'tracesOfWith' (Phase 74 統合) に移譲した。+-- ===========================================================================++-- | [日本語]: [Chain] の発散 draw を連結順の通し index に変換する内部正本+--   (chain c の offset = それ以前の chain の draw 数合計)。 'mergeChains' /+--   'divergencesOf' の双方がこれを使う (重複実装しない)。+--   [English]: The internal canonical function converting each 'Chain'\'s+--   divergent draws into a running, concatenated index (chain c's offset =+--   the total draw count of the chains before it). Both 'mergeChains' and+--   'divergencesOf' use this (no duplicated implementation).+pooledDivergences :: [Chain] -> [Int]+pooledDivergences chs =+  concat [ map (+ off) (chainDivergences ch)+         | (off, ch) <- zip offsets chs ]+  where offsets = scanl (+) 0 (map (length . chainSamples) chs)++-- | [日本語]: 全 chain を pool した発散 draw の通し index ('mergeChains' の連結順と整合)。+--   @tracesOf@ (merged trace) の rug 位置や、 発散 draw の抽出+--   (@map (chainSamples merged !!) (divergencesOf fit)@) に使う。+--   [English]: The running index of divergent draws pooled across all+--   chains (consistent with 'mergeChains'\'s concatenation order). Used for+--   the rug position in @tracesOf@ (merged trace) and to extract divergent+--   draws (@map (chainSamples merged !!) (divergencesOf fit)@).+divergencesOf :: HBMModel -> [Int]+divergencesOf = pooledDivergences . hbmChainsR++-- | [日本語]: divergence rug / 強調点の色 ('Hanalyze.Viz.MCMC' の pairScatterDiv と同じ赤)。+--   [English]: The color for the divergence rug \/ highlighted points (the+--   same red as 'Hanalyze.Viz.MCMC'\'s @pairScatterDiv@).+divergenceColor :: Text+divergenceColor = "#dd2222"   -- 小文字 (toCss 出力と byte 一致・視覚は #DD2222 と同一)++-- | [日本語]: ArviZ @plot_pair(divergences=True)@ 流: 指定パラメータ対の joint 散布+--   (全 chain pool・薄表示) + 発散 draw を強調色で重畳。 funnel 診断の本命+--   (例: @pairOf fit [("tau_b1", "b1_2")]@ で漏斗の首に発散が集中するのが見える)。+--   発散 draw の抽出は 'divergencesOf' の通し index を pool 後の draw 列に引く+--   (chain 連結順は 'hbmDraws' = 'mergeChains' と同一)。+--   divergence が無ければ強調レイヤは付かない。+--   [English]: ArviZ @plot_pair(divergences=True)@-style: the joint scatter+--   of a given parameter pair (pooled across all chains, drawn faintly),+--   with divergent draws overlaid in a highlight color. This is the go-to+--   funnel diagnostic (e.g. @pairOf fit [("tau_b1", "b1_2")]@ shows+--   divergences concentrated at the neck of the funnel). Divergent draws are+--   extracted by indexing the pooled draw list with 'divergencesOf'\'s+--   running index (the chain concatenation order matches 'hbmDraws' =+--   'mergeChains'). No highlight layer is added when there are no+--   divergences.+pairOf :: HBMModel -> [(Text, Text)] -> [VisualSpec]+pairOf hbm prs =+  [ let xs   = hbmDraws xn hbm+        ys   = hbmDraws yn hbm+        n    = min (length xs) (length ys)+        dIdx = [ i | i <- divergencesOf hbm, i < n ]+        dxs  = map (xs !!) dIdx+        dys  = map (ys !!) dIdx+    in layer (scatter (inline xs) (inline ys) <> alpha 0.25)+       <> (if null dIdx+             then mempty+             else layer (scatter (inline dxs) (inline dys)+                         <> color (fromHex divergenceColor)))+       <> xLabel xn <> yLabel yn <> title (xn <> " × " <> yn)+  | (xn, yn) <- prs ]++-- | [日本語]: ArviZ @plot_energy@ 流: marginal energy (E − Ē、 chain 別中心化) と+--   transition energy (ΔE = E_{i+1} − E_i、 chain 内差分・境界を跨がない) の密度重畳。+--   ΔE 分布が marginal より極端に狭ければ、 サンプラが posterior の energy 分布を+--   探索しきれていないサイン (低 BFMI 相当。 数値は 'Hanalyze.Viz.MCMC' の bfmi)。+--   energy ('chainEnergy' = draw ごとの Hamiltonian) は HMC / NUTS のみ記録される+--   ため、 MH / Gibbs 等の fit では空図になる。 系列名は Viz 側 energyPlot と同一。+--+--   ★mark は 'density' でなく KDE ('Hanalyze.Stat.MCMC' の kde 200 = Viz energyPlot と+--   同一) + 'line'。 理由: 固定色 'color' と categorical 'colorBy' は同一 field (lyColor) の+--   Last で相互排他、 かつ renderDensity は categorical 色を見ない (staticColorOr のみ) ため、+--   density mark では「2 色の曲線 + 凡例」 が両立できない。 line は群色対応済なので+--   多モデル重畳 (line + color inlineCat + scaleColorManual + legend) の確立パターンに+--   乗せる。+--   [English]: ArviZ @plot_energy@-style: overlaid densities of the marginal+--   energy (E − Ē, centered per chain) and the transition energy+--   (ΔE = E_{i+1} − E_i, within-chain difference, never crossing a chain+--   boundary). If the ΔE distribution is markedly narrower than the+--   marginal, that's a sign the sampler hasn't fully explored the+--   posterior's energy distribution (equivalent to low BFMI; the numeric+--   diagnostic is 'Hanalyze.Viz.MCMC'\'s @bfmi@). Energy+--   ('chainEnergy' = the per-draw Hamiltonian) is only recorded for HMC \/+--   NUTS, so fits from MH \/ Gibbs etc. yield an empty figure. Series names+--   match the Viz-side @energyPlot@.+--+--   ★The mark is KDE ('Hanalyze.Stat.MCMC'\'s @kde@ with 200 points,+--   the same as Viz's @energyPlot@) + 'line', not 'density'. Reason: a fixed+--   'color' and a categorical 'colorBy' both target the same field+--   (@lyColor@) under Last-wins semantics, and are mutually exclusive; also+--   @renderDensity@ ignores categorical color (it only looks at+--   @staticColorOr@), so a @density@ mark cannot produce "two colored curves+--   + a legend" at once. @line@ already supports group coloring, so this+--   rides the established pattern for overlaying multiple models (@line@ ++--   @color inlineCat@ + @scaleColorManual@ + legend).+energyOf :: HBMModel -> VisualSpec+energyOf hbm =+  curve lblMar eMar <> curve lblTr eTrans <> legendSpec+    <> xLabel "Energy" <> yLabel "Density" <> title "energy"+  where+    lblMar = "marginal E (centered)"+    lblTr  = "transition ΔE"+    ess    = filter (not . null) (map chainEnergy (hbmChainsR hbm))+    center es = let mu = sum es / fromIntegral (length es)+                in map (subtract mu) es+    eMar   = concatMap center ess+    eTrans = concatMap (\es -> zipWith (-) (drop 1 es) es) ess+    curve lbl vals+      | length vals < 2 = mempty+      | otherwise =+          let (gx, gy) = unzip (kde 200 vals)+          in layer (line (inline gx) (inline gy)+                    <> colorBy (inlineCat (replicate (length gx) lbl)))+    legendSpec+      | null eMar = mempty+      | otherwise = scaleColorManual [ (lblMar, "#4C72B0"), (lblTr, "#DD8452") ]+                      -- 凡例は図内 (右上)。 密度は中央が高く右裾は 0 ゆえ右上が空く。+                      -- 外・右だと右に余白が出て subplot/dashboard が不格好になる。+                      <> legendPos LegendInsideTopRight++-- | [日本語]: chain 別の __周辺事後密度__ を 1 図に重畳した per-param list (= ArviZ @plot_trace@ 左側 /+--   @plot_posterior@ の chain 重ね)。 'marginalsOf' が全 chain プールの 1 本を描くのに対し、+--   こちらは chain ごとに別レイヤを 'color' ('fromHex') で重ねる。+--   [English]: A per-param list overlaying each chain's __marginal posterior density__+--   in one figure (= ArviZ's @plot_trace@ left column \/+--   the chain overlay in @plot_posterior@). Where 'marginalsOf' draws a+--   single pooled curve across all chains, this overlays a separate layer+--   per chain, colored via 'color' \/ 'fromHex'.+marginalsByChainOf :: HBMModel -> [VisualSpec]+marginalsByChainOf hbm =+  [ foldMap (\(k, ch) -> layer (density (inline (chainVals nm ch)) <> color (fromHex (chainColor k))))+            (zip [0 ..] (hbmChainsR hbm))+    <> title nm+  | nm <- hbmParamNames hbm ]++-- | [日本語]: 自己相関 plot の既定最大ラグ (= ArviZ @plot_autocorr@ の見やすさに合わせた 30。+--   ArviZ 既定の 100 は SVG では横に潰れるので短めにする)。+--   [English]: The default maximum lag for the autocorrelation plot (= 30,+--   chosen for readability with ArviZ's @plot_autocorr@; ArviZ's default of+--   100 gets squashed horizontally in SVG, so this is kept shorter).+defaultAutocorrMaxLag :: Int+defaultAutocorrMaxLag = 30++-- | [日本語]: 各 latent パラメータの __自己相関__ を per-param list で返す (= ArviZ @plot_autocorr@)。+--   lag 0..'defaultAutocorrMaxLag' の ACF を縦棒 ('bar') で描く。 chain 連結の境界アーティ+--   ファクトを避けるため __chain ごとに 'autocorr' を計算し lag ごとに平均__する+--   ('energyOf' が chain 別に算出して連結するのと同方針)。 ACF が速く 0 に減衰するほど+--   mixing が良い (高い自己相関 = ESS 低下のサイン)。+--   [English]: Returns each latent parameter's __autocorrelation__ as a+--   per-param list (= ArviZ's @plot_autocorr@). Plots the ACF for lags+--   0..'defaultAutocorrMaxLag' as vertical bars ('bar'). To avoid boundary+--   artifacts from concatenating chains, this __computes 'autocorr' per chain and averages per lag__+--   (the same policy as 'energyOf' computing+--   per chain before concatenating). The faster the ACF decays to 0, the+--   better the mixing (high autocorrelation is a sign of reduced ESS).+autocorrOf :: HBMModel -> [VisualSpec]+autocorrOf = autocorrOfLag defaultAutocorrMaxLag++-- | [日本語]: 最大ラグを明示する 'autocorrOf'。+--   [English]: 'autocorrOf' with an explicit maximum lag.+autocorrOfLag :: Int -> HBMModel -> [VisualSpec]+autocorrOfLag maxLag hbm =+  [ acSpec nm | nm <- hbmParamNames hbm ]+  where+    chains = hbmChainsR hbm+    acSpec nm =+      let perChain = [ autocorr maxLag vs+                     | c <- chains, let vs = chainVals nm c, not (null vs) ]+      in case perChain of+           []      -> mempty+           (ac0:_) ->+             let lags    = map (fromIntegral . fst) ac0 :: [Double]+                 acfByCh = map (map snd) perChain                  -- [chain][lag]+                 meanACF = map (\col -> sum col / fromIntegral (length col))+                               (transpose acfByCh)                 -- lag ごとの chain 平均+             -- y 軸ラベルは省く (図が潰れるため。 title でパラメータ名は分かる)。+             in layer (bar (inline lags) (inline meanACF))+                  <> title nm <> xLabel "lag"++-- | [日本語]: rank plot の既定ビン数 (= PyMC @plot_rank@ 既定 20)。+--   [English]: The default number of bins for the rank plot (= 20, matching+--   PyMC's @plot_rank@ default).+defaultRankBins :: Int+defaultRankBins = 20++-- | [日本語]: 各 latent パラメータの __rank plot__ を per-param list で返す (= ArviZ @plot_rank@・+--   Vehtari et al. 2021)。 全 chain をプールした値の rank を chain ごとにヒストグラム化し、+--   chain 別の棒を色分けして重畳する。 __収束時は各 chain がほぼ一様__ (= どのビンも同程度)。+--   chain が偏る (= 山ができる) と R̂ 悪化のサイン。 rank 計算は 'rankHist' (Stat.MCMC) に+--   一元化し Viz 経路と共有する。 __要 chain ≥ 2__ (1 本だと rank が自明に一様ゆえ空図)。+--   [English]: Returns each latent parameter's __rank plot__ as a per-param+--   list (= ArviZ's @plot_rank@; Vehtari et al. 2021). Ranks the values+--   pooled across all chains, histograms them per chain, and overlays the+--   per-chain bars in distinct colors. __At convergence each chain is nearly uniform__+--   (i.e. every bin has roughly equal counts); a chain skewing+--   toward a bump is a sign of worse R̂. Rank computation is centralized in+--   'rankHist' (Stat.MCMC) and shared with the Viz path. __Requires chain ≥ 2__+--   (with a single chain the rank is trivially uniform, so the+--   figure is empty).+rankOf :: HBMModel -> [VisualSpec]+rankOf = rankOfBins defaultRankBins++-- | [日本語]: ビン数を明示する 'rankOf'。+--   [English]: 'rankOf' with an explicit number of bins.+rankOfBins :: Int -> HBMModel -> [VisualSpec]+rankOfBins nBins hbm =+  [ rankSpec nm | nm <- hbmParamNames hbm ]+  where+    chains = hbmChainsR hbm+    nCh    = length chains+    rankSpec nm =+      let perChain = map (chainVals nm) chains+      in if nCh < 2 || all null perChain+           then mempty+           else+             -- chain を横並び (dodge) にした 1 層の bar (= ArviZ plot_rank の単一パネル版)。+             -- long-form: (bin, count, chain) を chain×bin 行で展開し colorBy + PosDodge。+             let hists    = rankHist nBins perChain                 -- [chain][bin]+                 -- ビンは categorical だが軸はアルファベット順ゆえ、 数値順を保つよう+                 -- 0 埋めラベル ("00".."19") にする (= 文字列ソート = 数値順)。+                 w        = length (show (nBins - 1))+                 pad i    = let s = show (i :: Int)+                            in T.pack (replicate (w - length s) '0' ++ s)+                 binCat   = concat [ [ pad b | b <- [0 .. nBins - 1] ] | _ <- [1 .. nCh] ]+                 cntCol   = concatMap (map fromIntegral) hists :: [Double]+                 chainCat = concat [ replicate nBins (T.pack ("chain " <> show k))+                                   | k <- [0 .. nCh - 1] ]+             -- y 軸ラベル・凡例は省く (図が潰れるため。 chain は色 dodge で判別可)。+             -- colorBy は既定で凡例を出すので legendOff で明示的に抑制する。+             in layer ( bar (inlineCat binCat) (inline cntCol)+                        <> colorBy (inlineCat chainCat)+                        <> position PosDodge )+                  <> title nm <> xLabel "rank bin" <> legendOff+++-- | [日本語]: 係数 forest plot の描画仕様。 'HBMModel' を直接 'Plottable' にしないため、+--   抽出後の図を包む薄い newtype (後続 sub の ppc/epred/dag も同型に揃える)。+--   [English]: The plotting spec for a coefficient forest plot. Since+--   'HBMModel' isn't made directly 'Plottable', this is a thin newtype+--   wrapping the extracted figure (the later ppc \/ epred \/ dag specs+--   follow the same shape).+newtype ForestSpec = ForestSpec { unForestSpec :: VisualSpec }++instance Plottable ForestSpec where+  toPlot = unForestSpec++-- | [日本語]: 各 latent パラメータの事後区間を 1 枚の forest plot にする (94% HDI 既定)。+--   [English]: Renders each latent parameter's posterior interval as a+--   single forest plot (default 94% HDI).+forestOf :: HBMModel -> ForestSpec+forestOf = forestOfLevel 0.94++-- | [日本語]: 信頼水準を明示する 'forestOf'。 point = 事後平均、 bar 半幅 = HDI 半幅。+--+--   ★ 'forest' mark は対称 CI (± 半幅) のみ対応するため、 非対称な HDI は+--   「事後平均 ± (hi−lo)/2」 の対称バーで近似表示する (mark 側の TODO = 非対称 forest 未対応)。+--   [English]: 'forestOf' with an explicit confidence level. The point is+--   the posterior mean; the bar half-width is the HDI half-width.+--+--   ★ Since the 'forest' mark only supports a symmetric CI (± half-width),+--   the asymmetric HDI is approximated as a symmetric bar of "posterior mean+--   ± (hi−lo)/2" (a TODO on the mark side: asymmetric forest bars are not+--   yet supported).+forestOfLevel :: Double -> HBMModel -> ForestSpec+forestOfLevel level hbm = ForestSpec $+  layer (forest (inlineCat names) (inline ests) (inline errs) <> forestNull 0)+  where+    names = hbmParamNames hbm+    rows  = [ (mean, (hi - lo) / 2)+            | nm <- names+            , let d        = hbmDraws nm hbm+                  mean     = if null d then 0 else sum d / fromIntegral (length d)+                  (lo, hi) = highestDensityInterval level d+            ]+    ests = map fst rows+    errs = map snd rows++-- ===========================================================================+-- HBM の事後予測平均 — Phase 49 A3 (epred = E[y|x] の grid 評価 + HDI band)+--+-- ベイズ回帰の代表図。 予測子 (@predName@、 学習時 @dataNamed@ の参照名) を grid 上で+-- 1 点ずつ動かし、 各 posterior draw でモデル中の deterministic ノード (@muName@、+-- 通常は線形予測子の平均 μ) を 'runDeterministics' で評価する。 これで grid 点ごとに+-- N draws 分の μ サンプルが得られ、 その **事後平均** (線) と **94% HDI** (帯、 ArviZ 既定)+-- を描く。 これは PyMC の @pm.sample_posterior_predictive@ で得る epred (expected value of+-- the posterior predictive) に相当する (観測ノイズを含まない平均の不確実性)。+--+-- ★ O1 規約: epred 用モデルは予測子を @dataNamed predName@ で受け、 その平均を+-- @deterministic muName@ で 1 点スカラとして公開する (学習 likelihood とは併存)。+-- grid 評価では @withData predName [xi]@ で 1 点に差し替えるため、 deterministic 内で+-- @head x@ を取れば @xi@ が読める。+--+-- ★ Phase 74: 多予測子の hold。 非軸の予測子 slot は、 既定では 'HoldAgg' に従って+-- bind データの集約値 ('Mean' 既定) で固定する (旧実装は bind データ先頭値 @head@ に+-- 固定で選択不能だった)。 頻度論 effect plot ('statModelMulti') と **同じ語彙**を共有:+--   * @epred fit "x1" "mu" \<\> holdAt Median@         … 非軸を中央値で固定+--   * @epred fit "x1" "mu" \<\> holdAt (Fixed [("x2", 5)])@  … x2 のみ 5・他は Mean+--   * @epred fit "x1" "mu" \<\> byVar "x2" [0, 1]@      … x2 の水準別に曲線色分け重畳+-- 'holdAt' / 'byVar' は 'Hanalyze.Plot.Core' の既存コンビネータ (= ModelSpec の+-- @msHoldAt@ / @msByVar@ を設定) をそのまま使う (epred 専用版は作らない)。+--+-- ★ 設計: 専用 newtype を作らず **'ModelSpec' を再利用**する (record は描画クロージャの+-- 容れ物で 'SingleVarModel' 束縛ではない)。 これにより @epred hbm "x" "mu" \<\> grid 200+-- \<\> statLevel 0.9@ が Phase 16 C1 のコンビネータと同綴りで合成できる (既定 level 0.94 と+-- 帯 ON = ArviZ 流の HDI 帯を焼き込む。 epred の帯はオプトアウト不可)。+-- ===========================================================================++-- | [日本語]: 1 つの予測子値 @x@ における事後予測平均と HDI (非軸予測子は bind データのまま)。+--   @predName@ を @[x]@ に差し替え、 全 chain の各 draw で deterministic @muName@ を+--   評価し、 (事後平均, (lo, hi)) を返す。 非軸予測子を固定する版は 'epredAtHeld'。+--   [English]: The posterior predictive mean and HDI at a single predictor+--   value @x@ (non-axis predictors stay at their bound data). Substitutes+--   @[x]@ for @predName@, evaluates the deterministic @muName@ node for+--   every draw across all chains, and returns (posterior mean, (lo, hi)).+--   The variant that also fixes non-axis predictors is 'epredAtHeld'.+epredAt+  :: HBMModel+  -> Text     -- ^ [日本語]: 予測子の data 参照名 (@dataNamed@ / @withData@ の名前)。 [English]: The predictor's data reference name (the name used by @dataNamed@ \/ @withData@).+  -> Text     -- ^ [日本語]: 平均の deterministic ノード名。 [English]: The deterministic node name for the mean.+  -> Double   -- ^ [日本語]: HDI 水準 (例 0.94)。 [English]: The HDI level (e.g. 0.94).+  -> Double   -- ^ [日本語]: 予測子値 x。 [English]: The predictor value x.+  -> (Double, (Double, Double))+epredAt hbm = epredAtHeld hbm []++-- | [日本語]: (slot 名, 固定値) のリストを @withData@ で 1 点ずつ bind してネストする。+--   'ModelP' は impredicative (@forall a. Model a r@) ゆえ foldr では多相が逃げる。+--   トップレベル再帰なら各 'withData' が @ModelP r -> ModelP r@ を保つので通る。+--   [English]: Binds a list of (slot name, fixed value) pairs one at a time+--   via @withData@, nesting them. Since 'ModelP' is impredicative+--   (@forall a. Model a r@), @foldr@ would let the polymorphism escape; a+--   top-level recursion works because each @withData@ preserves+--   @ModelP r -> ModelP r@.+bindHolds :: [(Text, Double)] -> ModelP r -> ModelP r+bindHolds []              m = m+bindHolds ((nm, v) : rest) m = withData nm [v] (bindHolds rest m)++-- | [日本語]: 'epredAt' の多予測子版。 @holds@ = 非軸予測子の (slot 名, 固定値) を 1 点ずつ+--   @withData@ で bind し ('head' でその値が読める)、 軸 @predName@ を @[gx]@ に差し替える。+--   [English]: The multi-predictor variant of 'epredAt'. @holds@ = the+--   non-axis predictors' (slot name, fixed value) pairs, each bound via+--   @withData@ (readable with @head@), while the axis predictor @predName@+--   is substituted with @[gx]@.+epredAtHeld+  :: HBMModel+  -> [(Text, Double)]   -- ^ [日本語]: 非軸予測子の固定 (slot 名, 値)。 [English]: The fixed (slot name, value) pairs for non-axis predictors.+  -> Text -> Text -> Double -> Double+  -> (Double, (Double, Double))+epredAtHeld hbm holds predName muName level gx =+  let bound :: ModelP ()+      bound = withData predName [gx] (bindHolds holds (hbmModelSpec hbm))+      draws = concatMap chainSamples (hbmChainsR hbm)+      mus   = [ v | ps <- draws+                  , Just v <- [Map.lookup muName (runDeterministics bound ps)] ]+      mean  = if null mus then 0 else sum mus / fromIntegral (length mus)+  in (mean, highestDensityInterval level mus)++-- | [日本語]: grid 点 @gx@ における事後予測区間 (PI = 観測ノイズ込みの新規 1 点の HDI)。+--   'epredAtHeld' が deterministic μ の HDI (= CI 相当) を返すのに対し、 こちらは+--   __観測ノードの予測分布から y をサンプルしてプール__し HDI を取る。 観測ノード名は+--   引数に取らず 'runObserveDists' でモデルから自動検出する (頻度論 @svGridPI@ が obs 名を+--   要らないのと対称)。 単一 likelihood の通常ケースが対象で、 observe が複数なら全プール。+--   任意の観測分布 (Normal/Poisson/NegBinom…) に効く (@ppc@ の @sampleDist@ を再利用)。+--   @runST@ + 固定 seed (既定 'epredPISeed' = 42・'ppcOfWith' と同方式) で純粋・決定的。+--   [English]: The posterior predictive interval at grid point @gx@+--   (PI = the HDI of a new single point, including observation noise).+--   Where 'epredAtHeld' returns the HDI of the deterministic μ (equivalent+--   to a CI), this one __samples y from the observation node's predictive distribution and pools it__+--   before taking the HDI. It doesn't take the+--   observation node name as an argument; it auto-detects it from the model+--   via 'runObserveDists' (symmetric with how the frequentist @svGridPI@+--   doesn't need an obs name either). It targets the usual single-likelihood+--   case; with multiple observe nodes, everything is pooled. It works with+--   any observation distribution (Normal\/Poisson\/NegBinom…) by reusing+--   @ppc@'s @sampleDist@. It stays pure and deterministic via @runST@ + a+--   fixed seed (default 'epredPISeed' = 42, the same scheme as+--   'ppcOfWith').+epredPIAtHeld+  :: HBMModel+  -> [(Text, Double)]   -- ^ [日本語]: 非軸予測子の固定 (slot 名, 値)。 [English]: The fixed (slot name, value) pairs for non-axis predictors.+  -> Text               -- ^ [日本語]: 軸予測子の data 参照名。 [English]: The axis predictor's data reference name.+  -> Word32             -- ^ [日本語]: サンプリング seed。 [English]: The sampling seed.+  -> Double             -- ^ [日本語]: HDI 水準。 [English]: The HDI level.+  -> Double             -- ^ [日本語]: 予測子値 x。 [English]: The predictor value x.+  -> (Double, Double)+epredPIAtHeld hbm holds predName seed level gx =+  let bound :: ModelP ()+      bound = withData predName [gx] (bindHolds holds (hbmModelSpec hbm))+      draws = concatMap chainSamples (hbmChainsR hbm)+      samples = runST $ do+        gen <- initialize (V.singleton seed)+        concat <$> mapM+          (\ps ->+             let nodes = [ (d, ys) | (_, d, ys) <- runObserveDists bound ps ]+             in concat <$> mapM (\(d, ys) -> sampleObsRep gen d ys) nodes)+          draws+  in if null samples then (0, 0) else highestDensityInterval level samples++-- | [日本語]: @epredPIAtHeld@ の既定サンプリング seed (純粋・決定的に閉じる。 'ppcOfWith' と同値)。+--   [English]: The default sampling seed for @epredPIAtHeld@ (keeps it pure+--   and deterministic; the same value as 'ppcOfWith').+epredPISeed :: Word32+epredPISeed = 42++-- | [日本語]: 非軸予測子 1 slot の固定値を 'HoldAgg' (+ byVar override) から決める。+--   @override@ (byVar の明示固定) が 'HoldAgg' より優先。 HBM データは数値列ゆえ+--   factor / Reference は無く、 Reference\/Marginalize は安全側に Mean とする+--   (Marginalize の真の周辺化は epred では未対応)。+--   [English]: Determines a single non-axis predictor slot's fixed value+--   from 'HoldAgg' (plus a byVar override). @override@ (an explicit byVar+--   fixed value) takes priority over 'HoldAgg'. Since HBM data is numeric+--   columns, there is no factor \/ Reference notion, so Reference \/+--   Marginalize safely fall back to Mean (true marginalization for+--   Marginalize is not yet supported in epred).+epredHoldValue :: HoldAgg -> Text -> [Double] -> [(Text, Double)] -> Double+epredHoldValue hold nm vs override =+  case lookup nm override of+    Just v  -> v+    Nothing -> case hold of+      Mean        -> meanL vs+      Median      -> medianL vs+      Mode        -> medianL vs                       -- 数値連続に最頻は無意味 → 中央値で代替+      Reference   -> meanL vs+      Marginalize -> meanL vs+      Fixed fm    -> fromMaybe (meanL vs) (lookup nm fm)+  where+    meanL xs   = if null xs then 0 else sum xs / fromIntegral (length xs)+    medianL xs = case xs of+      [] -> 0+      _  -> let s = sortBy compare xs+                k = length s+            in if even k+                 then (s !! (k `div` 2 - 1) + s !! (k `div` 2)) / 2+                 else s !! (k `div` 2)++-- | [日本語]: grid 上の事後予測平均線 + HDI 帯を組む 'GridOpts' クロージャ (@epred@ が設定)。+--   'renderGridMulti' (頻度論 effect plot) と同型: 非軸予測子を 'goHoldAt' で固定し、+--   'goByVar' があれば第2予測子の水準ごとに曲線を色分け重畳する。 各曲線は帯 (先) ++--   線 (後)。 'goPredAt' 指定点は lineRange (区間) + scatter (事後平均) で重畳する。+--   帯は非対称な HDI を lo/hi で忠実に描く。+--   [English]: The 'GridOpts' closure that builds the posterior predictive+--   mean line + HDI band on the grid (set up by @epred@). Shaped the same+--   as 'renderGridMulti' (the frequentist effect plot): non-axis predictors+--   are fixed via 'goHoldAt', and if 'goByVar' is present, a distinctly+--   colored curve is overlaid per level of the second predictor. Each curve+--   draws its band first, then its line. Points given via 'goPredAt' are+--   overlaid with a @lineRange@ (interval) + @scatter@ (posterior mean).+--   The band faithfully draws the asymmetric HDI using lo\/hi.+renderEpred :: HBMModel -> Text -> Text -> GridOpts -> VisualSpec+renderEpred hbm predName muName opts =+  let (lo0, hi0) = epredPredRange hbm predName+      (lo, hi)   = fromMaybe (lo0, hi0) (goRange opts)+      n          = max 2 (goN opts)+      gxs        = linspace lo hi n+      level      = goLevel opts+      hold       = goHoldAt opts+      -- 非軸予測子 (= hbmData の predName 以外の slot) を HoldAgg + byVar override で固定。+      -- 応答列も含むが deterministic μ は応答に依存しないため無害。+      holdBinds override =+        [ (nm, epredHoldValue hold nm vs override)+        | (nm, vs) <- hbmData hbm, nm /= predName ]+      -- 1 曲線分 (override = byVar 固定の (名,値)、 mCol = 線/帯色)。+      -- BandMode で CI (μ HDI) / PI (観測ノイズ込み) / CIPI (入れ子) / なし を切替+      -- (頻度論 'renderGrid' と同型)。 PI 系は遅延ゆえ CI/Off では評価されない。+      oneCurve override mCol =+        let holds = holdBinds override+            rows  = map (epredAtHeld hbm holds predName muName level) gxs+            mu    = map fst rows+            ciLos = map (fst . snd) rows+            ciHis = map (snd . snd) rows+            piPairs = map (epredPIAtHeld hbm holds predName epredPISeed level) gxs+            piLos = map fst piPairs+            piHis = map snd piPairs+            lineL = layer (goLineDeco opts mCol n (line (inline gxs) (inline mu)))+            ciDeco = goBandDeco opts mCol+            -- 入れ子時の PI 帯は薄め (CI が内側で見えるように・頻度論と同じ既定)。+            piA    = maybe 0.10 (* 0.5) (goAlpha opts)+            piDeco = goBandDeco (opts { goAlpha = Just piA }) mCol+            mkBand deco los his = layer (deco (band (inline gxs) (inline los) (inline his)))+        in case goBandMode opts of+             BandOff  -> lineL+             BandCI   -> mkBand ciDeco ciLos ciHis <> lineL+             BandPI   -> mkBand ciDeco piLos piHis <> lineL+             BandCIPI -> mkBand piDeco piLos piHis        -- 外: PI 薄 (下)+                      <> mkBand ciDeco ciLos ciHis        -- 内: CI 濃 (上)+                      <> lineL+      curves = case goByVar opts of+        Nothing         -> oneCurve [] Nothing+        Just (v2, vals) ->+          foldMap+            (\(i, val) ->+               let col = fromHex (effectPalette !! (i `mod` length effectPalette))+               in oneCurve [(v2, val)] (Just col))+            (zip [0 :: Int ..] vals)+      pts = goPredAt opts+      predLayers+        | null pts  = mempty+        | otherwise =+            let prows = map (epredAtHeld hbm (holdBinds []) predName muName level) pts+                pmu   = map fst prows+                mids  = map (\(_, (l, h)) -> (l + h) / 2) prows+                halfs = map (\(_, (l, h)) -> (h - l) / 2) prows+            in layer (lineRange (inline pts) (inline mids) (inline halfs))+                 <> layer (scatter (inline pts) (inline pmu))+  in curves <> predLayers <> labelLegend opts++-- | [日本語]: 予測子列の観測範囲 (grid 既定範囲)。 bind 済みデータ ('hbmData') から引く。+--   [English]: The predictor column's observed range (the default grid+--   range). Looked up from the bound data ('hbmData').+epredPredRange :: HBMModel -> Text -> (Double, Double)+epredPredRange hbm predName =+  case lookup predName (hbmData hbm) of+    Just vs | not (null vs) -> (minimum vs, maximum vs)+    _                       -> (0, 1)++-- | [日本語]: HBM の事後予測平均 (E[y|x]) を grid 評価する 'ModelSpec' を作る。 既定は 94% HDI 帯+--   (ArviZ 流・帯 ON 焼き込み)、 grid 100 点、 範囲 = 予測子の観測 min/max。 @\<\>@ で+--   'grid' / 'gridRange' / 'statLevel' / 'predAt' を合成できる (既存コンビネータと同じ綴り)。+--+--   @+--   noDf |>> toPlot (epred fit \"x\" \"mu\" \<\> grid 200 \<\> statLevel 0.9)+--   @+--   [English]: Builds a 'ModelSpec' that grid-evaluates the HBM's posterior+--   predictive mean (E[y|x]). Defaults to a 94% HDI band (ArviZ-style, band+--   baked on by default), a grid of 100 points, and a range spanning the+--   predictor's observed min\/max. Composable via @\<\>@ with 'grid' \/+--   'gridRange' \/ 'statLevel' \/ 'predAt' (using the same spelling as the+--   existing combinators).+--+--   @+--   noDf |>> toPlot (epred fit \"x\" \"mu\" \<\> grid 200 \<\> statLevel 0.9)+--   @+epred+  :: HBMModel+  -> Text   -- ^ [日本語]: 予測子の data 参照名。 [English]: The predictor's data reference name.+  -> Text   -- ^ [日本語]: 平均の deterministic ノード名。 [English]: The deterministic node name for the mean.+  -> ModelSpec+epred hbm predName muName = mempty+  { msRender = Just (renderEpred hbm predName muName)+  , msLevel  = Just 0.94          -- ArviZ 既定の 94% HDI (statLevel で上書き可)+  , msBandMode = Just BandCI      -- HDI 帯が epred の本体ゆえ既定で出す+  }++-- ===========================================================================+-- HBM の事後予測チェック — Phase 49 A4 (ppc = posterior predictive check)+--+-- 観測 y の分布に対して、 学習済モデルが再現する複製データ y_rep の分布を重ねる+-- (ArviZ @az.plot_ppc@ 相当)。 各 posterior draw について 'runObserveDists' で+-- observe ノードの分布 (= 観測ノイズ込みの予測分布) を取り出し、 @sampleDist@ で+-- 1 セット y_rep をサンプリングする。 これを N draw 分重ねると「観測がモデルの予測+-- 分布の典型から外れていないか」 を目視できる。+--+-- 描画 (ArviZ 流):+--   * 観測 density (濃色・実線)            … 実データ。+--   * y_rep density を N 本 (薄色・低 alpha) … 各 draw の複製データ。+--   * プール y_rep density (破線)          … 事後予測分布全体 (= ppc の中心)。+--+-- ★ サンプリングに RNG が要るため 'IO' (頻度論 toPlot や epred/forest と違い純粋に+-- できない)。 'hbmModel' 自体 IO なので非対称ではない。 cumulative 版は density を+-- 'ecdf' に差し替える ('ppcCumulative')。+-- ===========================================================================++-- | [日本語]: ppc の設定: 重ねる複製データ本数 ('ppcReps')、 乱数シード、 累積版 (ecdf) 切替。+--   [English]: The ppc settings: the number of overlaid replicated datasets+--   ('ppcReps'), the RNG seed, and whether to switch to the cumulative+--   (ecdf) variant.+data PPCConfig = PPCConfig+  { ppcReps       :: !Int            -- ^ [日本語]: 重ねる y_rep 本数 (既定 40・draw から等間隔抽出)。 [English]: The number of overlaid y_rep replicates (default 40, evenly sampled from the draws).+  , ppcSeed       :: !(Maybe Word32) -- ^ [日本語]: サンプリングのシード (Nothing = system)。 [English]: The sampling seed (Nothing = system RNG).+  , ppcCumulative :: !Bool           -- ^ [日本語]: True で density を ecdf (累積分布) に差し替える。 [English]: True switches the density to an ecdf (cumulative distribution).+  } deriving (Show, Eq)++-- | [日本語]: 既定 ppc 設定: y_rep 40 本・system 乱数・density 表示。+--   [English]: The default ppc settings: 40 y_rep replicates, the system+--   RNG, and density display.+defaultPPC :: PPCConfig+defaultPPC = PPCConfig { ppcReps = 40, ppcSeed = Nothing, ppcCumulative = False }++-- | [日本語]: 事後予測チェック plot の描画仕様 ('forestOf' 等と同型の薄い newtype)。+--   [English]: The plotting spec for a posterior predictive check (a thin+--   newtype shaped like 'forestOf' and friends).+newtype PPCSpec = PPCSpec { unPPCSpec :: VisualSpec }++instance Plottable PPCSpec where+  toPlot = unPPCSpec++-- | [日本語]: observe ノード名が prefix に一致するか。 単一 @observe \"obs\"@ (n == prefix) と+--   @observeColumns@ 由来の @\"obs_0\"@.. (prefix <> \"_\" が接頭辞) の両方を拾う。+--   [English]: Whether an observe node name matches the prefix. Matches+--   both a single @observe \"obs\"@ (n == prefix) and @observeColumns@-style+--   names like @\"obs_0\"@.. (where @prefix <> \"_\"@ is a prefix).+ppcMatches :: Text -> Text -> Bool+ppcMatches prefix n = n == prefix || (prefix <> "_") `T.isPrefixOf` n++-- | [日本語]: 1 draw 分の複製データ y_rep をサンプリングする。 prefix 一致の各 observe ノードの+--   分布から、 観測値と同数だけ引いてプールする。 'PrimMonad' に一般化してあるため+--   IO でも ST でも引ける (→ 純粋な 'ppcOf' が runST で決定的にサンプリングできる)。+--   [English]: Samples one draw's worth of replicated data y_rep. Draws the+--   same count as the observed values from each prefix-matching observe+--   node's distribution, and pools them. Generalized over 'PrimMonad', so it+--   can be run in either IO or ST (which lets the pure 'ppcOf' sample+--   deterministically via @runST@).+sampleYRep :: PrimMonad m+           => Gen (PrimState m) -> ModelP () -> Text -> Map.Map Text Double -> m [Double]+sampleYRep gen spec prefix ps =+  let nodes = [ (d, ys) | (n, d, ys) <- runObserveDists spec ps, ppcMatches prefix n ]+  in concat <$> mapM (\(d, ys) -> sampleObsRep gen d ys) nodes++-- | [日本語]: 観測値 (prefix 一致 observe ノードの ys をプール)。 params に依らないので任意 draw から。+--   [English]: The observed values (pooling the ys of prefix-matching+--   observe nodes). Independent of the params, so any draw can be used.+ppcObserved :: HBMModel -> Text -> [Double]+ppcObserved hbm prefix =+  case concatMap chainSamples (hbmChainsR hbm) of+    (p0:_) -> concat [ ys | (n, _, ys) <- runObserveDists (hbmModelSpec hbm) p0+                          , ppcMatches prefix n ]+    []     -> []++-- | [日本語]: ppc の対象 draw 群 ('ppcReps' 本に間引き)。+--   [English]: The draws targeted for ppc (thinned down to 'ppcReps'+--   replicates).+ppcDrawsFor :: PPCConfig -> HBMModel -> [Map.Map Text Double]+ppcDrawsFor cfg hbm = selectEvenly (ppcReps cfg) (concatMap chainSamples (hbmChainsR hbm))++-- | [日本語]: 観測値・y_rep 群から ppc plot を組む (純粋)。 薄い y_rep 群 (背景・各 draw) を先に、+--   観測 (濃) を上に重ねる。 純粋 'ppcOfWith' と IO 'ppcOfWithIO' で共有。+--+--   ★ 旧実装はプール y_rep (全 draw 連結) の密度を赤破線で重ねていたが、 KDE の Silverman+--   バンド幅が __n 依存__ (@h ∝ n^(-0.2)@) ゆえ、 n=Σ(draw×n_obs) のプールは観測 (n=n_obs) より+--   バンド幅が小さく過小平滑になり、 観測と異なる形 (外側へ膨らむ) に見えて誤解を招いた。+--   比較は観測 (黒) vs 各 draw の y_rep (青・同じ n) で行うべきなので、 プール線は削除した+--   (ArviZ @plot_ppc@ もプール KDE は描かない)。+--   [English]: Builds the ppc plot from the observed values and the y_rep+--   replicates (pure). Draws the faint y_rep replicates (background, one per+--   draw) first, then overlays the observed (dark) on top. Shared between+--   the pure 'ppcOfWith' and the IO 'ppcOfWithIO'.+--+--   ★ The former implementation overlaid a red dashed density of the pooled+--   y_rep (all draws concatenated), but since the KDE's Silverman bandwidth+--   is __n-dependent__ (@h ∝ n^(-0.2)@), the pool (n=Σ(draw×n_obs)) has a+--   smaller bandwidth than the observed (n=n_obs), under-smoothing it into a+--   shape that looked different from the observed (bulging outward) and was+--   misleading. Since the comparison should be observed (black) vs. each+--   draw's y_rep (blue, same n), the pooled line was removed (ArviZ's+--   @plot_ppc@ doesn't draw a pooled KDE either).+buildPPCSpec :: PPCConfig -> [Double] -> [[Double]] -> PPCSpec+buildPPCSpec cfg observed yreps =+  let densLayer = if ppcCumulative cfg then ecdf else density+      repLayers = foldMap+        (\yr -> layer (densLayer (inline yr) <> color (fromHex "#1f77b4") <> alpha 0.15))+        yreps+      obsLayer    = layer (densLayer (inline observed) <> color (fromHex "#000000"))+  in PPCSpec (repLayers <> obsLayer)++-- | [日本語]: draw 列から 'ppcReps' 本を等間隔で抽出する (本数以下ならそのまま)。+--   [English]: Extracts 'ppcReps' replicates evenly spaced from the draw+--   list (returns the input as-is if it already has fewer).+selectEvenly :: Int -> [a] -> [a]+selectEvenly k xs+  | k <= 0 || n <= k = xs+  | otherwise        = [ xs !! (i * n `div` k) | i <- [0 .. k - 1] ]+  where n = length xs++-- | [日本語]: 既定設定の事後予測チェック (純粋・決定的が__正本__。 'ppcOfWith' 'defaultPPC')。+--   y_rep サンプリングを @runST@ で閉じ、 @ppcSeed@ 既定 (42) で常に再現可能。 IO 版は 'ppcOfIO'。+--   [English]: The posterior predictive check with default settings (the+--   pure, deterministic version is __canonical__; 'ppcOfWith' 'defaultPPC').+--   Closes the y_rep sampling over @runST@, always reproducible via the+--   default @ppcSeed@ (42). The IO variant is 'ppcOfIO'.+ppcOf :: HBMModel -> Text -> PPCSpec+ppcOf = ppcOfWith defaultPPC++-- | [日本語]: 事後予測チェックを組む (純粋・正本)。 @prefix@ は observe ノード名 (@observeColumns@ なら接頭辞)。+--   y_rep サンプリングを @runST@ で閉じる。 @ppcSeed@ が 'Nothing' のときは固定既定 seed (42) で再現可能。+--   [English]: Builds the posterior predictive check (pure, canonical).+--   @prefix@ is the observe node name (or the prefix, for+--   @observeColumns@). Closes the y_rep sampling over @runST@. When+--   @ppcSeed@ is 'Nothing', a fixed default seed (42) keeps it reproducible.+ppcOfWith :: PPCConfig -> HBMModel -> Text -> PPCSpec+ppcOfWith cfg hbm prefix =+  let spec :: ModelP ()+      spec  = hbmModelSpec hbm+      draws = ppcDrawsFor cfg hbm+      seed  = fromMaybe 42 (ppcSeed cfg)+      yreps = runST $ do+        gen <- initialize (V.singleton seed)+        mapM (sampleYRep gen spec prefix) draws+  in buildPPCSpec cfg (ppcObserved hbm prefix) yreps++-- | [日本語]: 既定設定の事後予測チェック (IO 版・'ppcOfWithIO' 'defaultPPC')。 通常は純粋な 'ppcOf' を使う+--   (将来 deprecate 予定)。 @ppcSeed@ 'Nothing' でシステム乱数を引きたいときだけ IO 版が要る。+--   [English]: The posterior predictive check with default settings (IO+--   variant; 'ppcOfWithIO' 'defaultPPC'). Normally use the pure 'ppcOf'+--   instead (this is slated for future deprecation). The IO variant is only+--   needed when you want to draw the system RNG with @ppcSeed@ 'Nothing'.+ppcOfIO :: HBMModel -> Text -> IO PPCSpec+ppcOfIO = ppcOfWithIO defaultPPC++-- | [日本語]: 事後予測チェックを組む (IO 版)。 @ppcSeed@ 'Nothing' で 'createSystemRandom' を引く。+--   [English]: Builds the posterior predictive check (IO variant). Draws+--   'createSystemRandom' when @ppcSeed@ is 'Nothing'.+ppcOfWithIO :: PPCConfig -> HBMModel -> Text -> IO PPCSpec+ppcOfWithIO cfg hbm prefix = do+  let spec :: ModelP ()+      spec  = hbmModelSpec hbm+      draws = ppcDrawsFor cfg hbm+  gen <- case ppcSeed cfg of+           Nothing -> createSystemRandom+           Just w  -> initialize (V.singleton w)+  yreps <- mapM (sampleYRep gen spec prefix) draws+  pure $ buildPPCSpec cfg (ppcObserved hbm prefix) yreps++-- ===========================================================================+-- HBM 診断ダッシュボード — 複数の抽出子を 1 枚に束ねる便宜関数 (Phase 74.8)+--+-- 個別の抽出子 (dagOf / forestOf / ppcOf / energyOf / tracesOf / marginalsOf) を+-- 'subplots' で並べ「構造・推定・当てはまり・収束を一目で点検する」 パネル束にする。 2 種:+--   * 'dashboardOf'     … コンパクト 2×2 (構造 / 推定値 / 当てはまり / サンプラ健全性)。+--                          各 1 パネルゆえ param 数に依らず一定で見やすい。+--   * 'dashboardFullOf' … 上段に同じ 2×2、 その下に param ごと [事後分布 | trace] を 2 列で+--                          連結 (ArviZ @plot_trace@ 流)。 係数が増えると下へ行が増えるだけ。+-- どちらも observe ノード名を引数に取る (ppc 用)。 @noDf |>> dashboardOf m "obs"@。+-- autocorr/rank はダッシュボードに入れない (mixing は trace・BFMI は energy で見えるため。+-- ESS 定量は個別 'autocorrOf'、 chain 一様性は 'rankOf' で見る)。+-- ===========================================================================++-- | [日本語]: コンパクト健全性 2×2 のパネル群 (左上から 構造 / 推定値 / 当てはまり / サンプラ健全性)。+--   'dashboardOf' (単体) と 'dashboardFullOf' (上段) で共有する内部ヘルパ。+--   [English]: The compact 2×2 health-panel group (from top-left: structure+--   \/ estimates \/ fit \/ sampler health). An internal helper shared by+--   'dashboardOf' (standalone) and 'dashboardFullOf' (its top section).+dashboardHealthPanels :: HBMModel -> Text -> [VisualSpec]+dashboardHealthPanels hbm obsName =+  [ toPlot (dagOf hbm)         <> title "構造 (DAG)"+  , toPlot (forestOf hbm)      <> title "推定値 (forest 94% HDI)"+  , toPlot (ppcOf hbm obsName) <> title "当てはまり (PPC: 観測 vs 事後予測)"+  , energyOf hbm               <> title "サンプラ健全性 (energy / BFMI)" ]++-- | [日本語]: コンパクトな HBM 診断ダッシュボード (2×2)。 __構造__ (@dagOf@・左上)・__推定値__+--   ('forestOf'・94% HDI)・__当てはまり__ ('ppcOf'・観測 vs 事後予測の密度重ね)・+--   __サンプラ健全性__ ('energyOf'・BFMI) を 1 パネルずつ。 各 1 パネルゆえ param 数に依らず見やすい+--   (係数が増えても forest が縦に密になるだけ。 収束 R̂/trace は 'dashboardFullOf' で見る)。+--   [English]: A compact HBM diagnostic dashboard (2×2). One panel each for+--   __structure__ (@dagOf@, top-left), __estimates__ ('forestOf', 94% HDI),+--   __fit__ ('ppcOf', observed vs. posterior predictive density overlay),+--   and __sampler health__ ('energyOf', BFMI). Since it's one panel each, it+--   stays readable regardless of the parameter count (more coefficients+--   just make the forest more densely packed vertically; check convergence+--   R̂ \/ trace via 'dashboardFullOf' instead).+dashboardOf :: HBMModel -> Text -> VisualSpec+dashboardOf hbm obsName =+  subplots (dashboardHealthPanels hbm obsName)+    <> subplotCols 2 <> width 1100 <> height 760++-- | [日本語]: param ごと __[事後分布 (左) | trace (右)]__ のパネル群 (ArviZ @plot_trace@ の中身)。+--   'traceDensityOf' (単体) と 'dashboardFullOf' (下段) で共有する内部ヘルパ。 事後分布・+--   trace とも chain 別を色違いで重畳する ('marginalsByChainOf' / 'tracesOfWith' byChain)。+--   [English]: A per-param panel group of __[posterior (left) | trace (right)]__+--   (the content of ArviZ's @plot_trace@). An internal helper+--   shared by 'traceDensityOf' (standalone) and 'dashboardFullOf' (its+--   bottom section). Both the posterior and the trace overlay each chain in+--   a distinct color ('marginalsByChainOf' \/ 'tracesOfWith' with+--   @byChain@).+tracePostPanels :: HBMModel -> [VisualSpec]+tracePostPanels hbm =+  concat (zipWith (\p t -> [p, t])+            (marginalsByChainOf hbm)+            (tracesOfWith defaultTraceOpts { toByChain = True } hbm))++-- | [日本語]: trace と事後分布だけのダッシュボード (= ArviZ @plot_trace@ 相当)。 param ごとに+--   __[事後分布 (左) | trace (右)]__ を 2 列で並べる (chain は色違いで重畳)。 収束 (定常・+--   chain 一致) と事後の形を同時に確認する定番。 係数が増えると下に行が増える。+--   [English]: A dashboard of just the trace and posterior (= equivalent to+--   ArviZ's @plot_trace@). Lays out __[posterior (left) | trace (right)]__+--   in two columns per param (chains overlaid in distinct colors). The+--   standard way to check convergence (stationarity, chain agreement) and+--   the posterior's shape at the same time. More coefficients add more+--   rows below.+traceDensityOf :: HBMModel -> VisualSpec+traceDensityOf hbm =+  let np = max 1 (length (hbmParamNames hbm))+  in subplots (tracePostPanels hbm)+       <> subplotCols 2 <> width 900 <> height (180 * fromIntegral np)++-- | [日本語]: フルの HBM 診断ダッシュボード。 上段に 'dashboardOf' と同じ健全性 2×2、 その下に+--   param ごと __[事後分布 (左) | trace (右)]__ を 2 列で連結する (ArviZ @plot_trace@ 流・+--   chain は色違いで重畳)。 全体が 1 つの 2 列グリッドなので、+--   __係数が増えると下に行が増えるだけ__ (高さを行数 = 2 + param 数 に比例させ各パネルを潰さない)。 epred (予測曲線)+--   はモデル固有の予測子/平均ノード名と df が要るためここには含めない (個別に描く)。+--   [English]: The full HBM diagnostic dashboard. The top section is the+--   same 2×2 health group as 'dashboardOf'; below it, __[posterior (left) | trace (right)]__+--   is appended per param in two columns (ArviZ+--   @plot_trace@-style, chains overlaid in distinct colors). Since the+--   whole thing is one 2-column grid, __adding coefficients only adds more rows below__+--   (the height scales with the row count = 2 + the number of+--   params, so no panel gets squashed). epred (the prediction curve) isn't+--   included here, since it needs a model-specific predictor \/ mean node+--   name and a df (draw it separately instead).+dashboardFullOf :: HBMModel -> Text -> VisualSpec+dashboardFullOf hbm obsName =+  let np   = max 1 (length (hbmParamNames hbm))+      rows = 2 + np                                  -- 健全性 2 行 + param 行+  in subplots (dashboardHealthPanels hbm obsName ++ tracePostPanels hbm)+       <> subplotCols 2 <> width 1100 <> height (220 * fromIntegral rows)++-- ===========================================================================+-- HBM のモデル構造 DAG — Phase 49 A5 (dag = 確率プログラムの依存グラフ)+--+-- 確率プログラム ('ModelP') の依存構造を @buildModelGraph@ (= @extractDeps@ ++-- 同名ノード統合) で 'ModelGraph' (nodes / edges / plates) にし、 plot-core の+-- DAG 描画 ('dagFromListsWithPlates'、 Sugiyama 階層 layout) に橋渡しする。 PyMC の+-- @pm.model_to_graphviz@ に相当する「モデルの絵」。+--+-- ノード種 (latent / observed) と分布名は 'Node' のメタデータをそのまま 'DAGNode' に+-- 写す。 plate ('plate' で囲んだ繰り返し) は 'mgPlates' を 'DAGPlate' に変換する+-- (plate メンバは 'nodePlates' から逆引き)。 plate を使わないモデルでは+-- @observeColumns@ 由来の @obs_0..@ が個別ノードとして出る (collapse したい場合は+-- モデル側を 'plate' で囲む)。+-- ===========================================================================++-- | [日本語]: モデル構造 DAG の描画仕様 ('forestOf' 等と同型の薄い newtype)。+--   [English]: The plotting spec for a model-structure DAG (a thin newtype+--   shaped like 'forestOf' and friends).+newtype DagSpec = DagSpec { unDagSpec :: VisualSpec }++instance Plottable DagSpec where+  toPlot = unDagSpec++-- | [日本語]: 学習済モデルの構造を DAG にする (@buildModelGraph@ → plate-collapse →+--   plot-core DAG)。 layout は階層 ('LayoutHierarchical')。 学習結果には依存しない+--   (構造のみ)。 plate 内の indexed RV (@b0_0..b0_2@ 等) を+--   'collapseIndexedPlateNodes' で 1 ノードに畳むのが既定 (PyMC+--   @model_to_graphviz@ と同じ見た目)。 indexed 個別ノードのまま見たい場合は+--   'dagOfRaw'。+--   [English]: Turns a trained model's structure into a DAG+--   (@buildModelGraph@ → plate-collapse → plot-core DAG). The layout is+--   hierarchical ('LayoutHierarchical'). Independent of the training result+--   (structure only). By default, indexed RVs inside a plate (e.g.+--   @b0_0..b0_2@) are collapsed into a single node via+--   'collapseIndexedPlateNodes' (matching PyMC's @model_to_graphviz@ look).+--   Use 'dagOfRaw' to see the indexed nodes individually instead.+dagOf :: HBMModel -> DagSpec+dagOf = dagFromModelGraph . collapseIndexedPlateNodes . buildModelGraph . hbmModelSpec++-- | [日本語]: @dagOf@ の plate-collapse 無し版 (かつての旧既定。 plate 内 indexed RV を+--   個別ノードで列挙する。 展開後の全ノード/エッジを確認するデバッグ用)。+--   [English]: The plate-collapse-free variant of @dagOf@ (the former+--   default; enumerates plate-internal indexed RVs as individual nodes).+--   Useful for debugging by inspecting all expanded nodes \/ edges.+dagOfRaw :: HBMModel -> DagSpec+dagOfRaw = dagFromModelGraph . buildModelGraph . hbmModelSpec++-- | [日本語]: __学習前__にモデル構造だけを DAG にする (PyMC @pm.model_to_graphviz@ 相当)。+--   @dagOf@ が学習済 'HBMModel' を取るのに対し、 こちらは生の 'ModelP' を直接取り+--   __サンプリングを一切しない__ (構造は事後に依らないため)。 @noDf |>> toPlot (dagOfModel m)@。+--+--   ★ 注意: データ駆動 plate (@plateForM_@ / @observeColumns@ で plate サイズを __データ長__から+--   決めるモデル) は、 データ未束縛 (slot が @[]@) だとループ本体が回らず plate 内ノード+--   (mu / obs 等) が出ない。 その場合は 'dagOfModelWith' でダミーでないデータを束ねてから描く+--   (サンプリングは走らない)。 明示 plate (@plate name N@ / @plateI@ で N を直書き) のモデルは+--   データ無しでも構造が完全に出る。+--   [English]: Turns just the model structure into a DAG __before training__+--   (equivalent to PyMC's @pm.model_to_graphviz@). Where @dagOf@+--   takes a trained 'HBMModel', this one takes a raw 'ModelP' directly and+--   __never samples__ (structure doesn't depend on the posterior).+--   @noDf |>> toPlot (dagOfModel m)@.+--+--   ★ Caution: for data-driven plates (models where @plateForM_@ \/+--   @observeColumns@ determine the plate size from the __data length__),+--   leaving the data unbound (an empty @[]@ slot) means the loop body never+--   runs, so plate-internal nodes (mu \/ obs etc.) won't appear. In that+--   case, bind non-dummy data first with 'dagOfModelWith' before drawing+--   (still no sampling runs). Models with explicit plates (@plate name N@ \/+--   @plateI@ hard-coding N) show the full structure even without data.+dagOfModel :: ModelP () -> DagSpec+dagOfModel = dagFromModelGraph . collapseIndexedPlateNodes . buildModelGraph++-- | [日本語]: 'dagOfModel' のデータ束ね版 (PyMC で観測を渡してから @model_to_graphviz@ する形)。+--   @dat@ を 'bindCols' でモデルへ束ねてから DAG を組む =+--   __データ駆動 plate のサイズが正しく出る__。 'hbmModel' と同じ束ね方だが __NUTS は走らない__ (学習前のプレビュー)。+--   @noDf |>> toPlot (dagOfModelWith [("x", xs), ("y", ys)] m)@。+--   [English]: The data-bound variant of 'dagOfModel' (like passing+--   observations to PyMC before calling @model_to_graphviz@). Binds @dat@+--   to the model via 'bindCols' before building the DAG, so+--   __data-driven plate sizes come out correctly__. Binds the same way as 'hbmModel', but+--   __NUTS never runs__ (it's a pre-training preview).+--   @noDf |>> toPlot (dagOfModelWith [("x", xs), ("y", ys)] m)@.+dagOfModelWith :: [(Text, [Double])] -> ModelP () -> DagSpec+dagOfModelWith dat = dagOfModel . bindCols dat++-- | [日本語]: 'ModelGraph' → plot-core DAG 描画仕様 (@dagOf@ / 'dagOfRaw' の共通部)。+--   [English]: 'ModelGraph' → plot-core DAG plotting spec (the shared part+--   of @dagOf@ \/ 'dagOfRaw').+dagFromModelGraph :: ModelGraph -> DagSpec+dagFromModelGraph mg =+  -- ★ renderDAG は dnX/dnY をそのまま使い layout を実行しない。 ゆえに描画前に+  -- Sugiyama 階層 layout ('layoutHierarchicalFullWithPlates') で座標を確定させる+  -- (これを省くと全ノードが原点 (0,0) に重なる)。+  let (positioned, routed) = layoutHierarchicalFullWithPlates dnodes dedges dplates+  -- ★ HS=PS parity: routing を spec へ焼き込む (= 'deRoute' 充填)。 これが無いと PS canvas+  --   は 'deRoute = Nothing' で直線フォールバックになり、 HS の live routing (曲線) と乖離する。+  --   baking は area 非依存 (dagToScreen が 0..1 domain を正規化 pt 空間へ map・描画時に+  --   fitPrimsToArea で affine fit) なので layout 直後のここで焼ける。+  in DagSpec $ bakeDAGRoutesInSpec $+       layer (dagFromListsWithPlates positioned routed LayoutHierarchical dplates)+  where+    ns = mgNodes mg+    dnodes = map toDNode ns+    dedges = [ DAGEdge { deFrom = p, deTo = c, dePath = Nothing, deRoute = Nothing }+             | (p, c) <- mgEdges mg ]+    dplates = [ DAGPlate+                  { dpLabel   = nm <> " (" <> T.pack (show sz) <> ")"+                  , dpNodeIds = [ nodeName n | n <- ns, nm `elem` nodePlates n ] }+              | (nm, sz) <- Map.toList (mgPlates mg) ]+    toDNode n = DAGNode+      { dnId    = nodeName n+      , dnLabel = nodeName n+      , dnKind  = case nodeKind n of+                    LatentN        -> NodeLatent+                    ObservedN _    -> NodeObserved+                    DeterministicN -> NodeDeterministic+                    -- Phase 60.4: NodeData は plot-core に既実装 (Phase 26 §E-6)+                    DataN _        -> NodeData+      , dnDist  = Just (nodeDist n)+      , dnX     = 0+      , dnY     = 0+      }++-- | [日本語]: random-effect 第 @k@ 列の caterpillar plot。 BLUP を group ごとに取り、+--   __値で昇順ソート__して forest mark (errs=0 の点) で並べ、 0 に 'forestNull' 参照線。+--   [English]: The caterpillar plot for random-effect column @k@. Takes the+--   BLUPs per group, __sorts them ascending by value__, lays them out with+--   the forest mark (points with errs=0), and draws a 'forestNull'+--   reference line at 0.+caterpillarColumn :: GLMMResultRE -> Int -> VisualSpec+caterpillarColumn res k =+  let cols   = LA.toColumns (reBLUPs res)+      blups  = if k >= 0 && k < length cols then LA.toList (cols !! k) else []+      groups = V.toList (reGroups res)+      sorted = sortBy (comparing snd) (zip groups blups)+      gs     = map fst sorted+      es     = map snd sorted+      zeros  = map (const (0 :: Double)) es+  in layer (forest (inlineCat gs) (inline es) (inline zeros) <> forestNull 0)+       <> title ("Random effects (col " <> T.pack (show k) <> ")")++instance Plottable GLMMResultRE where+  -- 代表 1 枚 = 第 1 列 (通常 random intercept) の caterpillar。+  toPlot res = caterpillarColumn res 0+  -- 診断束 = 全 r 列 (intercept + slope) の caterpillar。+  diagnosticPlots res =+    [ caterpillarColumn res k | k <- [0 .. LA.cols (reBLUPs res) - 1] ]++-- | [日本語]: HBM の事後予測平均 (epred) 応答曲面。 2 つの予測子 slot (@p1@, @p2@) を+--   grid で動かし、 各点で deterministic @muName@ の事後平均を取る+--   ('epredAt' の 2 変数版・O1 規約は 'renderEpred' の節を参照)。+--   ★コスト = grid 点数² × 全 draw のモデル評価。 既定 n=30 (900 点)。+--   [English]: The HBM posterior predictive mean (epred) response surface.+--   Moves two predictor slots (@p1@, @p2@) across a grid, taking the+--   posterior mean of the deterministic @muName@ at each point (the+--   two-variable version of 'epredAt'; see the note on the O1 convention in+--   'renderEpred'\'s section).+--   ★Cost = (grid points)² × model evaluations across all draws. Default+--   n=30 (900 points).+epredSurfaceOf :: HBMModel -> Text -> Text -> Text -> P3.VisualSpec3D+epredSurfaceOf hbm p1 p2 muName =+  epredSurfaceOfWith hbm p1 p2 muName defaultSurfaceOpts { soN = 30 }++epredSurfaceOfWith :: HBMModel -> Text -> Text -> Text -> SurfaceOpts -> P3.VisualSpec3D+epredSurfaceOfWith hbm p1 p2 muName opts =+  let (xlo, xhi) = fromMaybe (epredPredRange hbm p1) (soXRange opts)+      (ylo, yhi) = fromMaybe (epredPredRange hbm p2) (soYRange opts)+      n     = max 2 (soN opts)+      gxs   = linspace xlo xhi n+      gys   = linspace ylo yhi n+      draws = concatMap chainSamples (hbmChainsR hbm)+      muAt gx gy =+        let bound :: ModelP ()+            bound = withData p1 [gx] (withData p2 [gy] (hbmModelSpec hbm))+            mus   = [ v | ps <- draws+                        , Just v <- [Map.lookup muName (runDeterministics bound ps)] ]+        in if null mus then 0 else sum mus / fromIntegral (length mus)+      grid = [ [ muAt gx gy | gx <- gxs ] | gy <- gys ]+  in P3.layer3D ( P3.surface3DGrid grid+               <> P3.xRange3D (xlo, xhi)+               <> P3.yRange3D (ylo, yhi)+               <> P3.colormap3D )++-- | [日本語]: 学習済 HBM が保持するデータ列 ('hbmData') から散布図層を作る。+--+--   @df |-> hbm cfg model@ で学習した後、 @dataScatterOf m \"x\" \"y\"@ で+--   観測散布図を出せるので、 epred\/forest 等の抽出子と重畳するとき+--   __df を学習時 1 回だけ__書けばよい:+--+--   > let m = df |-> hbm defaultHBM model+--   > noDf |>> (dataScatterOf m "x" "y" <> toPlot (epred m "x" "mu"))+--   [English]: Builds a scatter layer from the trained HBM's retained data+--   columns ('hbmData').+--+--   After training with @df |-> hbm cfg model@, @dataScatterOf m \"x\"+--   \"y\"@ produces the observed scatter, so when overlaying it with+--   extractors like epred \/ forest, you only need to+--   __write the df once, at training time__:+--+--   > let m = df |-> hbm defaultHBM model+--   > noDf |>> (dataScatterOf m "x" "y" <> toPlot (epred m "x" "mu"))+dataScatterOf :: HBMModel -> Text -> Text -> VisualSpec+dataScatterOf m xn yn =+  case (lookup xn (hbmData m), lookup yn (hbmData m)) of+    (Just xs, Just ys) -> layer (scatter (inline xs) (inline ys))+    _                  -> mempty
+ src/Hanalyze/Plot/Core.hs view
@@ -0,0 +1,1200 @@+-- |+-- Module      : Hanalyze.Plot.Core+-- Description : hgg 連携層の共通基盤 (モデル族非依存のクラス・型・評価核)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- [日本語]: hgg 連携層の __共通基盤__ (= モデル族非依存のクラス・型・評価核)。+--+-- ⚠ 本モジュールは親 'Hanalyze.Plot' と同じく別パッケージ @hanalyze-plot@+-- に属し、 @cabal build --project-file=cabal.project.plot@ で build される。+-- @hgg-core@ に依存するため+-- __upstream hanalyze には cherry-pick しない__。+--+-- ここに集約するもの:+--+--   * 図化能力の最終クラス 'Plottable'、 grid 評価クラス 'SingleVarModel' /+--     @MultiVarModel@、 分類器抽象 'ClassPredict'。+--   * grid 評価の仕様 'ModelSpec' (Semigroup\/Monoid)・確定オプション 'GridOpts'・+--     ブートストラップ素材 'BootKit'、 および @statModel@\/@grid@\/@bandMode@ 等の+--     合成子 (smart ctor)。+--   * grid 評価核 ('renderGrid' \/ 'renderGridMulti' \/ 'bootstrapBands' \/ 'evalFrame'+--     系)・応答曲面核 ('surfaceGrid' \/ 'surfaceOf' 系) と、 複数のモデル族が共有する+--     描画 helper。+--+-- 各モデル族固有の @instance Plottable XxxModel@ 等は親 'Hanalyze.Plot' 側に+-- 残置する (orphan instance を許容: クラスは Core・instance は Plot・型は Wrappers)。+--+-- [English]: The __common foundation__ of the hgg integration layer+-- (= model-family-agnostic classes, types, and the evaluation core).+--+-- ⚠ This module lives in the same separate package @hanalyze-plot@ as+-- its parent 'Hanalyze.Plot', built via+-- @cabal build --project-file=cabal.project.plot@. Because it+-- depends on @hgg-core@, it is __never cherry-picked__ into+-- upstream hanalyze.+--+-- Gathered here:+--+--   * The final plotting-capability class 'Plottable', the grid-evaluation+--     classes 'SingleVarModel' \/ @MultiVarModel@, and the classifier+--     abstraction 'ClassPredict'.+--   * The grid-evaluation spec 'ModelSpec' (Semigroup\/Monoid), its resolved+--     options 'GridOpts', bootstrap material 'BootKit', and the smart+--     constructors @statModel@\/@grid@\/@bandMode@ etc. that build it up.+--   * The grid-evaluation core ('renderGrid' \/ 'renderGridMulti' \/+--     'bootstrapBands' \/ 'evalFrame' family), the response-surface core+--     ('surfaceGrid' \/ 'surfaceOf' family), and the drawing helpers shared+--     across multiple model families.+--+-- Family-specific @instance Plottable XxxModel@ declarations etc. remain in+-- the parent 'Hanalyze.Plot' module (orphan instances are accepted by+-- design: classes live in Core, instances in Plot, types in Wrappers).+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleContexts #-}+module Hanalyze.Plot.Core+  ( -- * Plottable protocol+    Plottable (..)+    -- * ルート1 grid 評価 (ModelSpec)+  , ModelSpec (..)+  , GridOpts (..)+  , BootKit (..)+  , SingleVarModel (..)+  , MultiVarModel (..)+    -- ** 合成子 (smart ctor)+  , statModel+  , grid+  , gridRange+  , bandMode+  , piMethod+  , statColor+  , statFill+  , statLinetype+  , statLinewidth+  , statAlpha+  , statLabel+  , statEquation+  , statR2+  , statLevel+  , holdAt+  , byVar+  , predAt+  , statModelMulti+    -- ** 描画 deco / 凡例 helper+  , goLineDeco+  , goBandDeco+  , labelLegend+  , fitLabelText+    -- ** grid 評価核+  , bootstrapBands+  , renderGrid+  , renderGridMulti+  , marginalizeCurve+  , alongRange+  , evalFrame+  , setAlong+  , isResponseRole+  , holdRole+  , fixedRole+  , clampIdx+  , effectPalette+    -- * 応答曲面 3D 核+  , evalFrame2+  , surfaceGrid+  , chunkRows+  , surfaceOf+  , surfaceOfWith+  , dataScatter3DOf+    -- * 集約 helper (連続列の代表値)+  , meanV+  , medianV+  , modeV+  , modeIdx+  , mostCommon+    -- * 共有描画 helper (複数族が利用)+  , defaultCILevel+  , quantilePalette+  , stepVerts+  , gridCurves+  , importanceBar+  , matCols2+  , classMeansScatter+  , classMeansScatterNamed+  , chainColor+    -- * 分類器抽象+  , ClassPredict (..)+    -- * 回帰診断の可視化 (係数 forest / 実測vs予測)+  , HasObsPred (..)+  , obsVsPred+  , obsPredSpec+  , coefForest+  ) where++import           Control.Applicative   ((<|>))+import           Data.List             (group, maximumBy, sort, transpose)+import           Data.Maybe            (fromMaybe)+import           Data.Ord              (comparing)+import           Data.Word             (Word32)+import qualified Data.Vector           as V+import           System.Random.MWC     (initialize, uniformR)+import           Control.Monad.ST      (runST)+import           Control.Monad         (replicateM)+import           Hanalyze.Model.HBM.Interp (percentileOf)+import           Hanalyze.Model.HBM.Sampling (sampleDist)+import qualified Hanalyze.Model.HBM.Distribution as BD+import qualified Numeric.LinearAlgebra as LA++import           Data.Text             (Text)+import qualified Data.Text             as T++import           Graphics.Hgg.Spec     ( VisualSpec, Layer, layer, inline, inlineCat+                                       , Color (..), fromHex+                                       , scatter, line, band+                                       , shape, MarkShape (..)+                                       , color, colorBy, lineRange, bar+                                       , scaleColorManual, legend+                                       , forest, forestNull+                                       , xLabel, yLabel+                                       , LineType (..)+                                       , linetype, alpha, stroke )+import           Hanalyze.Diagnostics ( CoefRow (..), HasCoefSummary (..) )+import           Graphics.Hgg.Unit     (pt', (*~))+import qualified Graphics.Hgg.ThreeD.Spec  as P3+import           Graphics.Hgg.ThreeD.Types (Point3 (..))+import           Graphics.Hgg.Color        (toCss)++import           Hanalyze.Model.Wrappers+import           Hanalyze.Model.Formula.Frame   (ModelFrame (..), VarRole (..))+import           Hanalyze.Model.LM     (linspace)+import           Numeric               (showFFloat)++-- ===========================================================================+-- Plottable protocol+-- ===========================================================================++-- | [日本語]: 解析オブジェクトを図 (@VisualSpec@) に変換できる能力。+--+--   能力差は中立 protocol ('Hanalyze.Model.Core' の @ResidualModel@ /+--   @PredictiveModel@) 側に持たせ、 ここは「図にできる」 という最終能力のみを表す。+--   [English]: The capability to convert an analysis object into a figure+--   (@VisualSpec@).+--+--   Capability differences live on the neutral protocol side+--   ('Hanalyze.Model.Core''s @ResidualModel@ \/ @PredictiveModel@);+--   this class expresses only the final "can be plotted" capability.+class Plottable m where+  -- | [日本語]: 代表 1 枚の図 (= layer 重畳の主役、 @<>@ で他 layer と合成可)。+  --   [English]: The single representative figure (= the main layer to+  --   overlay onto; composable with other layers via @<>@).+  toPlot          :: m -> VisualSpec++  -- | [日本語]: 診断図の束 (= レポート用)。 既定は代表 1 枚のみ。+  --   [English]: A bundle of diagnostic figures (= for reports). Defaults+  --   to just the single representative figure.+  diagnosticPlots :: m -> [VisualSpec]+  diagnosticPlots m = [toPlot m]++-- ===========================================================================+-- ルート1 grid 評価 (ModelSpec) — Phase 16 §3 C1+--+-- fit 済モデルの回帰曲線・CI 帯を **訓練点ではなく等間隔 grid** で評価して描く。+-- 疎・不均一データで曲線がガタつくのを解消する (散布図の点は従来通り訓練データ)。+-- 'statModel' で 'ModelSpec' を作り、 @<>@ でオプションを足す:+--+-- > df |>> (layer (scatter "x" "y") <> toPlot (statModel m <> grid 200))+--+-- 'ModelSpec' は Monoid。 学習済モデル @m@ はクロージャに閉じ込め、 予測は+-- @toPlot@ (描画時) に grid 評価する (ユーザ直感「m は学習・layer で予測」)。+-- ===========================================================================++-- | [日本語]: grid 評価の確定オプション ('ModelSpec' の Maybe field を既定で埋めたもの)。+--   [English]: The resolved grid-evaluation options ('ModelSpec''s Maybe+--   fields, filled in with defaults).+data GridOpts = GridOpts+  { goN       :: Int                    -- ^ [日本語]: 評価点数 (既定 100)。 [English]: Number of evaluation points (default 100).+  , goRange   :: Maybe (Double, Double) -- ^ [日本語]: 評価範囲 (既定 = 説明変数 min/max)。 [English]: Evaluation range (default = predictor min/max).+  , goLevel   :: Double                 -- ^ [日本語]: CI 水準 (既定 0.95)。 [English]: CI confidence level (default 0.95).+  , goBandMode :: BandMode              -- ^ [日本語]: 帯モード (既定 'BandCI')。 [English]: Band mode (default 'BandCI').+  , goPIMethod :: PIMethod              -- ^ [日本語]: 帯の算出法 (既定 'PIClosedForm')。 [English]: Band computation method (default 'PIClosedForm').+  , goPredAt  :: [Double]               -- ^ [日本語]: 予測点 x のリスト。 [English]: List of prediction points x.+  , goHoldAt  :: HoldAgg                 -- ^ [日本語]: 多変量 effect の他変数固定方式 (既定 Mean)。 [English]: How other predictors are held fixed for a multivariate effect plot (default Mean).+  , goByVar   :: Maybe (Text, [Double])  -- ^ [日本語]: 層別 = 第2変数を複数値で固定。 [English]: Stratification = fixing a second variable at multiple values.+  , goColor   :: Maybe Color             -- ^ [日本語]: 線の固定色 (statColor)。 [English]: Fixed line color (statColor).+  , goFill    :: Maybe Color             -- ^ [日本語]: 帯の塗り色 (statFill)。 [English]: Band fill color (statFill).+  , goLinetype  :: Maybe LineType        -- ^ [日本語]: 線種 (statLinetype)。 [English]: Line type (statLinetype).+  , goLinewidth :: Maybe Double          -- ^ [日本語]: 線幅 = stroke (statLinewidth)。 [English]: Line width = stroke (statLinewidth).+  , goAlpha   :: Maybe Double            -- ^ [日本語]: 帯/線の透明度 (statAlpha)。 [English]: Band/line transparency (statAlpha).+  , goLabel   :: Maybe Text              -- ^ [日本語]: 単線の凡例ラベル (statLabel)。 [English]: Legend label for a single line (statLabel).+  , goShowEq  :: Bool                    -- ^ [日本語]: 回帰式を凡例ラベルに出す (statEquation)。 [English]: Show the regression equation in the legend label (statEquation).+  , goShowR2  :: Bool                    -- ^ [日本語]: R² を凡例ラベルに出す (statR2)。 [English]: Show R² in the legend label (statR2).+  }++-- | [日本語]: grid 上で曲線評価できる単変数モデル。 'statModel' が要求する能力。+--   [English]: A single-variable model that can be evaluated as a curve on a+--   grid. The capability required by 'statModel'.+class SingleVarModel m where+  -- | [日本語]: 生 predictor の範囲 (grid 既定範囲の算出元)。+  --   [English]: The range of the raw predictor (source of the grid's+  --   default range).+  svRange :: m -> (Double, Double)+  -- | [日本語]: 信頼水準と grid x 列から (中心 μ̂, 帯 @(lo, hi)@) を評価する。+  --   band を持たないモデル (GAM/Robust) は 'Nothing'。+  --   [English]: Evaluates (center μ̂, band @(lo, hi)@) from the confidence+  --   level and grid x column. Models without a band (GAM/Robust) return+  --   'Nothing'.+  svGrid  :: m -> Double -> [Double] -> ([Double], Maybe ([Double], [Double]))+  -- | [日本語]: __予測区間__ (PI) 帯 @(lo, hi)@ を評価する。 観測分散 σ̂² を持つモデル+  --   (LM・Gaussian/Identity GLM) のみ実装し、 それ以外は既定の 'Nothing' (= PI 非提供)。+  --   中心 μ̂ は 'svGrid' と共通ゆえここでは帯のみ返す。+  --   [English]: Evaluates the __prediction interval__ (PI) band @(lo,+  --   hi)@. Only implemented for models with an observation variance σ̂²+  --   (LM, Gaussian/Identity GLM); everything else falls back on the+  --   default 'Nothing' (= PI not provided). The center μ̂ is shared with+  --   'svGrid', so only the band is returned here.+  svGridPI :: m -> Double -> [Double] -> Maybe ([Double], [Double])+  svGridPI _ _ _ = Nothing+  -- | [日本語]: 当てはめ係数 @[β₀, β₁]@ と R² (式/R² 凡例注釈用)。 線形で「式」 の意味が+  --   明快なモデル (LM) のみ実装し、 それ以外は既定の 'Nothing' (= 式注釈を出さない)。+  --   GLM は係数が η (リンク) スケールゆえ @y = β₀ + β₁x@ の素朴な式が成り立たず Nothing。+  --   [English]: The fitted coefficients @[β₀, β₁]@ and R² (for the+  --   equation/R² legend annotation). Only implemented for models where+  --   "the equation" has an unambiguous linear meaning (LM); everything+  --   else defaults to 'Nothing' (= no equation annotation shown). GLM+  --   coefficients live on the η (link) scale, so the naive+  --   @y = β₀ + β₁x@ equation does not hold and it returns Nothing.+  svCoefR2 :: m -> Maybe ([Double], Double)+  svCoefR2 _ = Nothing+  -- | [日本語]: ブートストラップ ('piMethod (PIBootstrap …)') 用の素材。 訓練 (x, y)・+  --   再標本化データで refit する関数・新規観測の分布 (GLM family。 'Nothing' = 加法残差)+  --   を束ねて返す。 既定 'Nothing' (= ブートストラップ非対応 → 閉形式へフォールバック)。+  --   closed-form を持たないモデル (非 Gaussian GLM / ロバスト) でも、 これを実装すれば+  --   PI を出せる。+  --   [English]: Material for bootstrapping ('piMethod (PIBootstrap …)'). Bundles+  --   the training (x, y), a function that refits on resampled data, and the+  --   distribution of a new observation (GLM family; 'Nothing' = additive+  --   residual). Defaults to 'Nothing' (= bootstrap unsupported → falls back+  --   to closed form). Even models without a closed form (non-Gaussian GLM /+  --   robust) can offer a PI by implementing this.+  svBootKit :: m -> Maybe (BootKit m)+  svBootKit _ = Nothing++-- | [日本語]: ブートストラップに必要な素材 ('svBootKit' が返す。 内部利用)。+--   [English]: The material required for bootstrapping (returned by+--   'svBootKit'; internal use).+data BootKit m = BootKit+  { bkX       :: [Double]                              -- ^ [日本語]: 訓練 x。 [English]: Training x.+  , bkY       :: [Double]                              -- ^ [日本語]: 訓練 y。 [English]: Training y.+  , bkRefit   :: [Double] -> [Double] -> m             -- ^ [日本語]: 再標本化 (x, y) で refit。 [English]: Refit on a resampled (x, y).+  , bkObsDist :: Maybe (Double -> BD.Distribution Double) -- ^ [日本語]: 新規観測の分布 (GLM)。 Nothing=加法残差。 [English]: The distribution of a new observation (GLM). Nothing = additive residual.+  }++-- | [日本語]: grid 評価の仕様。 'statModel' で生成し @<>@ でオプション合成 (Monoid)。+--   @msRender@ にモデルの grid 評価関数をクロージャで保持する。+--   [English]: The grid-evaluation spec. Created via 'statModel' and+--   composed with options via @<>@ (Monoid). @msRender@ holds the model's+--   grid-evaluation function as a closure.+data ModelSpec = ModelSpec+  { msRender  :: Maybe (GridOpts -> VisualSpec)  -- ^ [日本語]: statModel が設定 (先勝ち)。 [English]: Set by statModel (first wins).+  , msN       :: Maybe Int                       -- ^ [日本語]: grid 点数 (後勝ち)。 [English]: Number of grid points (last wins).+  , msRange   :: Maybe (Double, Double)          -- ^ [日本語]: grid 範囲 (後勝ち)。 [English]: Grid range (last wins).+  , msLevel   :: Maybe Double                    -- ^ [日本語]: CI 水準 (後勝ち)。 [English]: CI confidence level (last wins).+  , msBandMode :: Maybe BandMode                 -- ^ [日本語]: 帯モード (後勝ち、 既定 'BandCI'。+                                                 --   帯 ON/OFF と CI/PI を 'bandMode' 1 本に統合)。+                                                 --   [English]: Band mode (last wins, default 'BandCI'.+                                                 --   'bandMode' unifies band on/off and CI/PI into one+                                                 --   knob).+  , msPIMethod :: Maybe PIMethod                 -- ^ [日本語]: 帯の算出法 (後勝ち、 既定 'PIClosedForm')。 [English]: Band computation method (last wins, default 'PIClosedForm').+  , msPredAt  :: [Double]                        -- ^ [日本語]: 予測点 x (リスト累積 ++)。 [English]: Prediction points x (accumulated via list ++).+  , msHoldAt  :: Maybe HoldAgg                   -- ^ [日本語]: 多変量 effect の固定方式 (後勝ち、 既定 Mean)。 [English]: How other predictors are held fixed for a multivariate effect plot (last wins, default Mean).+  , msByVar   :: Maybe (Text, [Double])          -- ^ [日本語]: 層別変数 (後勝ち)。 [English]: Stratification variable (last wins).+  , msColor   :: Maybe Color                      -- ^ [日本語]: 線の固定色 (後勝ち)。 [English]: Fixed line color (last wins).+  , msFill    :: Maybe Color                      -- ^ [日本語]: 帯の塗り色 (後勝ち)。 [English]: Band fill color (last wins).+  , msLinetype  :: Maybe LineType                 -- ^ [日本語]: 線種 (後勝ち)。 [English]: Line type (last wins).+  , msLinewidth :: Maybe Double                   -- ^ [日本語]: 線幅 = stroke (後勝ち)。 [English]: Line width = stroke (last wins).+  , msAlpha   :: Maybe Double                      -- ^ [日本語]: 帯/線の透明度 (後勝ち)。 [English]: Band/line transparency (last wins).+  , msLabel   :: Maybe Text                         -- ^ [日本語]: 単線の凡例ラベル (後勝ち)。 [English]: Legend label for a single line (last wins).+  , msShowEq  :: Bool                                 -- ^ [日本語]: 回帰式を凡例に出す (Any)。 [English]: Show the regression equation in the legend (Any monoid).+  , msShowR2  :: Bool                                 -- ^ [日本語]: R² を凡例に出す (Any)。 [English]: Show R² in the legend (Any monoid).+  }++instance Semigroup ModelSpec where+  a <> b = ModelSpec+    { msRender  = msRender a <|> msRender b      -- モデルは先勝ち (通常 1 個)+    , msN       = msN b      <|> msN a           -- オプションは後勝ち+    , msRange   = msRange b  <|> msRange a+    , msLevel   = msLevel b  <|> msLevel a+    , msBandMode = msBandMode b <|> msBandMode a  -- 帯モードは後勝ち+    , msPIMethod = msPIMethod b <|> msPIMethod a  -- 算出法も後勝ち+    , msPredAt  = msPredAt a ++ msPredAt b       -- 予測点はリスト累積+    , msHoldAt  = msHoldAt b  <|> msHoldAt a+    , msByVar   = msByVar b   <|> msByVar a+    , msColor   = msColor b     <|> msColor a     -- aes は後勝ち+    , msFill    = msFill b      <|> msFill a+    , msLinetype  = msLinetype b  <|> msLinetype a+    , msLinewidth = msLinewidth b <|> msLinewidth a+    , msAlpha   = msAlpha b     <|> msAlpha a+    , msLabel   = msLabel b     <|> msLabel a+    , msShowEq   = msShowEq a || msShowEq b           -- 注釈はオプトイン (Any)+    , msShowR2   = msShowR2 a || msShowR2 b+    }++instance Monoid ModelSpec where+  mempty = ModelSpec+    { msRender = Nothing, msN = Nothing, msRange = Nothing, msLevel = Nothing+    , msBandMode = Nothing, msPIMethod = Nothing, msPredAt = [], msHoldAt = Nothing, msByVar = Nothing+    , msColor = Nothing, msFill = Nothing, msLinetype = Nothing+    , msLinewidth = Nothing, msAlpha = Nothing, msLabel = Nothing+    , msShowEq = False, msShowR2 = False }++-- | [日本語]: 学習済の単変数モデルから grid 評価 'ModelSpec' を作る (along 不要)。+--   [English]: Builds a grid-evaluation 'ModelSpec' from a fitted+--   single-variable model (no along needed).+statModel :: SingleVarModel m => m -> ModelSpec+statModel m = mempty { msRender = Just (renderGrid m) }++-- | [日本語]: grid 評価点数を指定 (既定 100)。+--   [English]: Specifies the number of grid-evaluation points (default 100).+grid :: Int -> ModelSpec+grid n = mempty { msN = Just n }++-- | [日本語]: grid 評価範囲を指定 (既定 = 説明変数 min/max)。+--   [English]: Specifies the grid-evaluation range (default = predictor+--   min/max).+gridRange :: Double -> Double -> ModelSpec+gridRange lo hi = mempty { msRange = Just (lo, hi) }++-- | [日本語]: 出す帯を 1 つの値で選ぶ (帯 ON/OFF と CI/PI を統合)。 'BandMode' は+--   @BandOff@ (なし) \/ @BandCI@ (既定・信頼区間) \/ @BandPI@ (予測区間) \/ @BandCIPI@+--   (入れ子)。 既定 (未指定) は @BandCI@。 PI 非提供モデルでは PI 系は CI へフォールバック。+--+--   @statModel m \<\> bandMode BandPI@ \/ @… \<\> bandMode BandCIPI@ \/ @… \<\> bandMode BandOff@。+--   [English]: Picks which band to show via a single value (unifies band+--   on/off with CI/PI). 'BandMode' is @BandOff@ (none) \/ @BandCI@ (default;+--   confidence interval) \/ @BandPI@ (prediction interval) \/ @BandCIPI@+--   (nested). The default (unspecified) is @BandCI@. For models that don't+--   provide a PI, PI-based modes fall back to CI.+--+--   @statModel m \<\> bandMode BandPI@ \/ @… \<\> bandMode BandCIPI@ \/ @… \<\> bandMode BandOff@.+bandMode :: BandMode -> ModelSpec+bandMode m = mempty { msBandMode = Just m }++-- | [日本語]: 帯 (CI/PI) の__算出法__を選ぶ。 @bandMode@ が「どの帯を出すか」を選ぶのに対し、+--   @piMethod@ は「どう計算するか」を選ぶ直交軸:+--+--     * @PIClosedForm@   = 閉形式 (Wald / 基底空間 OLS。 __既定__)。+--     * @PIBootstrap seed draws@ = case-resampling ブートストラップ (seed で決定的)。+--       閉形式 CI/PI を持たないモデル (非 Gaussian GLM / ロバスト) でも PI を出せる。+--+--   @statModel m \<\> bandMode BandPI \<\> piMethod (PIBootstrap 42 2000)@。+--   [English]: Chooses the __computation method__ for a band (CI/PI). Where+--   @bandMode@ picks "which band to show," @piMethod@ is the orthogonal axis+--   that picks "how to compute it":+--+--     * @PIClosedForm@ = closed form (Wald \/ basis-space OLS. __default__).+--     * @PIBootstrap seed draws@ = case-resampling bootstrap (deterministic+--       given the seed). Lets even models without a closed-form CI/PI+--       (non-Gaussian GLM \/ robust) produce a PI.+--+--   @statModel m \<\> bandMode BandPI \<\> piMethod (PIBootstrap 42 2000)@.+piMethod :: PIMethod -> ModelSpec+piMethod p = mempty { msPIMethod = Just p }++-- | [日本語]: 回帰線の固定色 (ggplot @geom_smooth(color=)@)。 凡例は付かない (単線命名は+--   'statLabel')。 型安全な 'Color' を受ける (plot-core の 'color' と同じ方針)。+--   @statColor (fromHex "#ff0000")@ \/ @statColor N.red@ \/ @statColor (rgb 255 0 0)@。+--   Text→Color は 'fromHex' に委ねる。+--   [English]: Fixed color for the regression line (ggplot+--   @geom_smooth(color=)@). No legend is added (naming a single line is+--   'statLabel''s job). Takes a type-safe 'Color' (same policy as+--   plot-core's 'color'). @statColor (fromHex "#ff0000")@ \/ @statColor+--   N.red@ \/ @statColor (rgb 255 0 0)@. Text→Color conversion is delegated+--   to 'fromHex'.+statColor :: Color -> ModelSpec+statColor c = mempty { msColor = Just c }++-- | [日本語]: CI 帯の塗り色 (ggplot @geom_smooth(fill=)@)。 型安全な 'Color' を受ける。+--   [English]: Fill color for the CI band (ggplot @geom_smooth(fill=)@).+--   Takes a type-safe 'Color'.+statFill :: Color -> ModelSpec+statFill c = mempty { msFill = Just c }++-- | [日本語]: 回帰線の線種 (ggplot @geom_smooth(linetype=)@)。 'LineType' = 'LtSolid' /+--   'LtDashed' 等。+--   [English]: Line type for the regression line (ggplot+--   @geom_smooth(linetype=)@). 'LineType' = 'LtSolid' \/ 'LtDashed' etc.+statLinetype :: LineType -> ModelSpec+statLinetype lt = mempty { msLinetype = Just lt }++-- | [日本語]: 回帰線の太さ (= stroke 幅。 ggplot @geom_smooth(linewidth=)@)。+--   [English]: Thickness of the regression line (= stroke width; ggplot+--   @geom_smooth(linewidth=)@).+statLinewidth :: Double -> ModelSpec+statLinewidth w = mempty { msLinewidth = Just w }++-- | [日本語]: 帯/線の透明度 (ggplot @geom_smooth(alpha=)@)。 帯に適用 (薄い塗り潰しの ggplot 流)。+--   [English]: Transparency for the band/line (ggplot @geom_smooth(alpha=)@).+--   Applied to the band (following ggplot's convention of a light fill).+statAlpha :: Double -> ModelSpec+statAlpha a = mempty { msAlpha = Just a }++-- | [日本語]: 単線に凡例ラベルを付ける。 1 群カテゴリ (@ColorByCol@) + 'scaleColorManual' で+--   色を固定し凡例エントリを 1 つ出す (固定色 'color' は @hasColorEncoding=False@ で+--   凡例が出ない罠を回避)。 色は 'statColor' があればそれ、 なければ既定パレット先頭。+--   ★モデル比較で各線に名前を付ける用途 (= 群数 1 の @byGroup@ 特殊形)。+--   [English]: Adds a legend label to a single line. Fixes the color via a+--   single-group category (@ColorByCol@) + 'scaleColorManual' and emits one+--   legend entry (avoids the trap where a fixed 'color' has+--   @hasColorEncoding=False@ and no legend appears). Uses 'statColor' if+--   given, otherwise the first color in the default palette. ★Used to name+--   each line when comparing models (= a special case of @byGroup@ with a+--   single group).+statLabel :: Text -> ModelSpec+statLabel lbl = mempty { msLabel = Just lbl }++-- | [日本語]: 回帰式を凡例ラベルに出す (ggplot @ggpubr::stat_regline_equation@ 相当)。+--   @svCoefR2@ を持つモデル (LM) で @y = β₀ + β₁x@ を自動生成し 凡例機構に載せる。+--   明示 'statLabel' があればそちらを優先。 式の出せないモデル (GLM 等) では注釈なし。+--   'statR2' と併用すると @y = … + …x, R² = …@ のように 1 ラベルに連結する。+--   [English]: Shows the regression equation in the legend label (equivalent+--   to ggplot's @ggpubr::stat_regline_equation@). For models with+--   @svCoefR2@ (LM), auto-generates @y = β₀ + β₁x@ and feeds it into the+--   legend mechanism. An explicit 'statLabel' takes precedence. Models that+--   can't produce an equation (GLM etc.) get no annotation. Combined with+--   'statR2', the two are joined into one label as @y = … + …x, R² = …@.+statEquation :: ModelSpec+statEquation = mempty { msShowEq = True }++-- | [日本語]: R² を凡例ラベルに出す (ggplot @ggpubr::stat_cor(aes(label=..rr.label..))@ 相当)。+--   @svCoefR2@ を持つモデル (LM) の R² を @R² = 0.987@ の形で凡例に載せる。+--   [English]: Shows R² in the legend label (equivalent to ggplot's+--   @ggpubr::stat_cor(aes(label=..rr.label..))@). Puts the R² of a model+--   with @svCoefR2@ (LM) into the legend as @R² = 0.987@.+statR2 :: ModelSpec+statR2 = mempty { msShowR2 = True }++-- | [日本語]: CI 水準を指定 (既定 0.95)。+--   [English]: Specifies the CI confidence level (default 0.95).+statLevel :: Double -> ModelSpec+statLevel l = mempty { msLevel = Just l }++-- | [日本語]: 多変量 effect で along 以外の説明変数の固定方式を指定 (既定 'Mean')。+--   [English]: Specifies how predictors other than along are held fixed in+--   a multivariate effect plot (default 'Mean').+holdAt :: HoldAgg -> ModelSpec+holdAt h = mempty { msHoldAt = Just h }++-- | [日本語]: 層別 = 第2変数 @v@ を複数値 @vals@ で固定し、 値ごとに 1 曲線を色分け重畳する+--   (R @ggpredict@ terms 第2項相当)。 多変量モデル ('statModelMulti') 専用。+--   [English]: Stratification: fixes a second variable @v@ at multiple+--   values @vals@ and overlays one color-coded curve per value (equivalent+--   to the second term of R's @ggpredict@ terms). For multivariate models+--   ('statModelMulti') only.+byVar :: Text -> [Double] -> ModelSpec+byVar v vals = mempty { msByVar = Just (v, vals) }++-- | [日本語]: 予測点を 1 つ足す。 @<>@ でリスト累積 → @… <> predAt 1 <> predAt 3@ で複数点。+--   各点は μ̂ (scatter) + CI 区間 [lo, hi] (lineRange) で描かれる (band を持たない GAM/+--   Robust は μ̂ 点のみ)。 単変数モデル前提 (多変量 effect は statModelMulti で対応)。+--   [English]: Adds one prediction point. Accumulated via @<>@ → @… <>+--   predAt 1 <> predAt 3@ for multiple points. Each point is drawn as μ̂+--   (scatter) + a CI interval [lo, hi] (lineRange); models without a band+--   (GAM/Robust) get just the μ̂ point. Assumes a single-variable model+--   (multivariate effects are handled by statModelMulti).+predAt :: Double -> ModelSpec+predAt x = mempty { msPredAt = [x] }++-- | [日本語]: 線レイヤへ aes (色・線種・太さ) を適用。 色の決定順は+--   (1) 群色 @mCol@ (byVar) → 'color'、 (2) 'statLabel' (@goLabel@) → 1 群 'colorBy'+--   (凡例を出すため・@n@ 点ぶんのカテゴリ列)、 (3) 'statColor' → 'color'。+--   線種・太さは色と独立に適用。 @n@ = grid 点数 (label カテゴリ列の長さ)。+--   [English]: Applies aes (color, line type, width) to a line layer. Color+--   is resolved in order: (1) the group color @mCol@ (byVar) → 'color', (2)+--   'statLabel' (@goLabel@) → a single-group 'colorBy' (to show a legend; a+--   category column of length @n@), (3) 'statColor' → 'color'. Line type and+--   width are applied independently of color. @n@ = number of grid points+--   (the length of the label category column).+goLineDeco :: GridOpts -> Maybe Color -> Int -> Layer -> Layer+goLineDeco o mCol n l =+  let colorL = case (mCol, goLabel o) of+        (Just c, _)         -> color c                                   -- 群色優先+        (Nothing, Just lbl) -> colorBy (inlineCat (replicate n lbl))      -- statLabel: ColorByCol で凡例+        (Nothing, Nothing)  -> maybe mempty color (goColor o)            -- statColor or 無色+  in l <> colorL+       <> maybe mempty linetype (goLinetype o)+       <> maybe mempty (\lw -> stroke (lw *~ pt')) (goLinewidth o)++-- | [日本語]: 帯レイヤへ fill 色・透明度を適用。 群色 @mCol@ があれば fill は群色を優先+--   ('statFill' で上書き不可)。+--   [English]: Applies fill color and transparency to a band layer. If a+--   group color @mCol@ is present, fill prefers the group color (cannot be+--   overridden by 'statFill').+goBandDeco :: GridOpts -> Maybe Color -> Layer -> Layer+goBandDeco o mCol b =+  b <> maybe mempty color (mCol <|> goFill o)+    <> maybe mempty alpha       (goAlpha o)++-- | [日本語]: 'statLabel' があれば @scaleColorManual@ で色を固定し @legend@ を出す+--   @VisualSpec@。 色は 'statColor' (@goColor@) 優先・なければ既定パレット先頭。+--   ラベル無しは空。+--   [English]: A @VisualSpec@ that, when 'statLabel' is present, fixes the+--   color via @scaleColorManual@ and shows a @legend@. Color prefers+--   'statColor' (@goColor@), falling back to the first color in the default+--   palette. Empty when there is no label.+labelLegend :: GridOpts -> VisualSpec+labelLegend o = case goLabel o of+  Just lbl -> scaleColorManual [(lbl, maybe (head effectPalette) toCss (goColor o))] <> legend+  Nothing  -> mempty++-- | [日本語]: 式/R² 凡例ラベル文字列を組む。 @showEq@ で @y = β₀ + β₁x@、 @showR2@ で+--   @R² = 0.987@ を入れ、 両方なら @", "@ で連結する。 係数は単回帰 @[β₀, β₁]@ を想定+--   (β₁ の符号で @+@/@-@ を切替)。 どちらの flag も立っていなければ 'Nothing'。+--   [English]: Builds the equation/R² legend label string. Inserts @y = β₀ ++--   β₁x@ when @showEq@ is set and @R² = 0.987@ when @showR2@ is set, joining+--   the two with @", "@ if both are present. Coefficients are assumed to be+--   a simple regression @[β₀, β₁]@ (the sign of β₁ switches @+@\/@-@).+--   Returns 'Nothing' if neither flag is set.+fitLabelText :: Bool -> Bool -> [Double] -> Double -> Maybe Text+fitLabelText showEq showR2 coefs r2 =+  let f3 x = T.pack (showFFloat (Just 3) x "")          -- 小数 3 桁固定+      eqPart = case coefs of+        (b0 : b1 : _) ->+          let sgn = if b1 < 0 then " − " else " + "+          in "y = " <> f3 b0 <> sgn <> f3 (abs b1) <> "x"+        [b0]          -> "y = " <> f3 b0+        _             -> "y = ?"+      r2Part = "R² = " <> f3 r2+      parts  = [ eqPart | showEq ] ++ [ r2Part | showR2 ]+  in if null parts then Nothing else Just (T.intercalate ", " parts)++-- | [日本語]: case-resampling ブートストラップで grid 上の CI / PI 帯を計算する。+--   訓練 (x, y) を seed 付きで再標本化 → 'bkRefit' で refit → 'svGrid' で grid μ を予測、+--   を @draws@ 回。 CI = μ_b の分位点 (係数の不確実性)。 PI = 新規観測 y* の分位点+--   (加法残差 'bkObsDist'=Nothing、 または Family(μ) からの parametric ドロー)。 seed 純粋+--   (runST + mwc・同 seed でビット同一)。 戻り = (CI (lo,hi), PI (lo,hi))。+--   [English]: Computes CI / PI bands on the grid via case-resampling+--   bootstrap. Resamples the training (x, y) with a seed → refits via+--   'bkRefit' → predicts grid μ via 'svGrid', repeated @draws@ times. CI =+--   quantiles of μ_b (coefficient uncertainty). PI = quantiles of a new+--   observation y* (additive residual when 'bkObsDist'=Nothing, or a+--   parametric draw from Family(μ) otherwise). Pure given the seed (runST ++--   mwc; bit-identical for the same seed). Returns (CI (lo,hi), PI (lo,hi)).+bootstrapBands :: SingleVarModel m+               => m -> BootKit m -> Word32 -> Int -> Double -> [Double]+               -> (([Double], [Double]), ([Double], [Double]))+bootstrapBands m kit seed draws level gxs =+  let xs    = V.fromList (bkX kit)+      ys    = V.fromList (bkY kit)+      n     = V.length xs+      ng    = length gxs+      a2    = (1 - level) / 2+      resid = V.fromList (zipWith (-) (bkY kit) (fst (svGrid m level (bkX kit))))+      paths = runST $ do+        gen <- initialize (V.singleton seed)+        replicateM draws $ do+          idx <- replicateM n (uniformR (0, n - 1) gen)+          let xs' = [ xs V.! i | i <- idx ]+              ys' = [ ys V.! i | i <- idx ]+              muB = fst (svGrid (bkRefit kit xs' ys') level gxs)+          pis <- case bkObsDist kit of+            Just toDist -> mapM (\mu -> sampleDist (toDist mu) gen) muB+            Nothing     -> mapM (\mu -> do j <- uniformR (0, n - 1) gen+                                           pure (mu + resid V.! j)) muB+          pure (muB, pis)+      muT = transpose (map fst paths)   -- ng × draws+      piT = transpose (map snd paths)+      q lo xss = map (percentileOf lo) xss+  in if n < 2 || ng == 0+       then (([], []), ([], []))+       else ( (q a2 muT, q (1 - a2) muT), (q a2 piT, q (1 - a2) piT) )++-- | [日本語]: grid 評価して曲線 (+ 帯) + 予測点の @VisualSpec@ を組む。 'statModel' がクロージャ化。+--   帯がある場合は @band@ を先に置き @line@ (μ̂ 曲線) を上に重ねる。 予測点 (goPredAt) は+--   CI 区間を @lineRange@ (縦線 [lo,hi]) + μ̂ を @scatter@ で重ね、 μ̂ が区間内のどこにあるか+--   (非対称な GLM 帯でも) 忠実に示す。+--   [English]: Grid-evaluates the model and builds the @VisualSpec@ for the+--   curve (+ band) + prediction points. Closed over by 'statModel'. When a+--   band is present, @band@ is drawn first and @line@ (the μ̂ curve) is+--   layered on top. Prediction points (goPredAt) overlay the CI interval as+--   @lineRange@ (a vertical segment [lo,hi]) with μ̂ as a @scatter@ point,+--   faithfully showing where μ̂ sits within the interval (even for+--   asymmetric GLM bands).+renderGrid :: SingleVarModel m => m -> GridOpts -> VisualSpec+renderGrid m opts0 =+  -- A8: statEquation/statR2 が立っていれば svCoefR2 から式/R² 文字列を作り、+  -- A3 と同じ凡例経路 (goLabel) に流す。 明示 statLabel が優先 (上書きしない)。+  let autoLabel = case (goShowEq opts0 || goShowR2 opts0, svCoefR2 m) of+        (True, Just (coefs, r2)) -> fitLabelText (goShowEq opts0) (goShowR2 opts0) coefs r2+        _                        -> Nothing+      opts = case goLabel opts0 of+        Just _  -> opts0                              -- 明示ラベル優先+        Nothing -> opts0 { goLabel = autoLabel }+      (lo0, hi0) = svRange m+      (lo, hi)   = fromMaybe (lo0, hi0) (goRange opts)+      n          = max 2 (goN opts)+      gxs        = linspace lo hi n+      (mu, mbCIcf) = svGrid m (goLevel opts) gxs+      -- 帯の算出法 (Phase 70.H): 既定 closed-form、 PIBootstrap で case-resampling。+      -- bootstrap は CI/PI を両方その場で計算 ('svBootKit' を持つモデルのみ。 無ければ+      -- closed-form へフォールバック)。 中心曲線 mu は元の当てはめのまま。+      (mbCI, mbPI) = case goPIMethod opts of+        PIBootstrap seed draws+          | Just kit <- svBootKit m ->+              let (ci, pii) = bootstrapBands m kit seed draws (goLevel opts) gxs+              in (Just ci, Just pii)+        _ -> (mbCIcf, svGridPI m (goLevel opts) gxs)+      -- 帯モードで CI/PI/両方/なしを描く (Phase 70.F)。 PI 非提供は CI へフォールバック。+      lineL    = layer (goLineDeco opts Nothing n (line (inline gxs) (inline mu)))+      bandL deco mb = case mb of+        Just (los, his) -> layer (deco (band (inline gxs) (inline los) (inline his)))+        Nothing         -> mempty+      ciDeco = goBandDeco opts Nothing+      -- 入れ子時の PI 帯は薄め (CI が内側で見えるように)。+      piA    = maybe 0.10 (* 0.5) (goAlpha opts)+      piDeco = goBandDeco (opts { goAlpha = Just piA }) Nothing+      curve = case goBandMode opts of+        BandOff  -> lineL+        BandCI   -> bandL ciDeco mbCI <> lineL+        BandPI   -> case mbPI of+                      Just _  -> bandL ciDeco mbPI <> lineL   -- PI 単独 (通常の濃さ)+                      Nothing -> bandL ciDeco mbCI <> lineL   -- PI 非提供 → CI+        BandCIPI -> case mbPI of+                      Just _  -> bandL piDeco mbPI            -- 外: PI 薄 (下)+                              <> bandL ciDeco mbCI            -- 内: CI 濃 (上)+                              <> lineL+                      Nothing -> bandL ciDeco mbCI <> lineL   -- PI 非提供 → CI のみ+      pts = goPredAt opts+      predLayers+        | null pts  = mempty+        | otherwise =+            let (pmu, pmb) = svGrid m (goLevel opts) pts+            in case pmb of+                 Just (plos, phis) ->+                   let mids  = zipWith (\l h -> (l + h) / 2) plos phis+                       halfs = zipWith (\l h -> (h - l) / 2) plos phis+                   in layer (lineRange (inline pts) (inline mids) (inline halfs))+                        <> layer (scatter (inline pts) (inline pmu))+                 Nothing -> layer (scatter (inline pts) (inline pmu))+  in curve <> predLayers <> labelLegend opts++-- ★案B: 既存 'Plottable' の @toPlot@ を 'ModelSpec' にも overload (同綴り)。+instance Plottable ModelSpec where+  toPlot ms = case msRender ms of+    Nothing -> mempty   -- モデル未設定 (オプションのみ) は空図。+    Just f  -> f GridOpts+      { goN       = fromMaybe 100 (msN ms)+      , goRange   = msRange ms+      , goLevel   = fromMaybe 0.95 (msLevel ms)+      , goBandMode = fromMaybe BandCI (msBandMode ms)+      , goPIMethod = fromMaybe PIClosedForm (msPIMethod ms)+      , goPredAt  = msPredAt ms+      , goHoldAt  = fromMaybe Mean (msHoldAt ms)+      , goByVar   = msByVar ms+      , goColor     = msColor ms+      , goFill      = msFill ms+      , goLinetype  = msLinetype ms+      , goLinewidth = msLinewidth ms+      , goAlpha     = msAlpha ms+      , goLabel     = msLabel ms+      , goShowEq     = msShowEq ms+      , goShowR2     = msShowR2 ms+      }++-- ===========================================================================+-- 多変量 effect plot (Phase 16 §3 C3)+--+-- 単変数 grid 評価 (C1) を多変量モデルへ一般化する。 along 変数を grid で動かし、+-- 他の説明変数を 'HoldAgg' で固定した「評価点 ModelFrame」 を合成して、 訓練 formula の+-- @designMatrixF@ で評価点設計行列を組み CI を評価する。+--+-- ★評価点 ModelFrame の合成は **DataFrame を経由せず VarRole を直接差し替える**+-- (@designMatrixF@ は 'mfRoles' のみ参照し応答列は使わない = Design.hs:331)。 列構造・+-- 順序が訓練と完全一致するので @confidenceBandAt@ / 'predictGlmMuWithCI' がそのまま使える。+-- 型で単/多変量を分離し ('SingleVarModel' / @MultiVarModel@)、 along 忘れをコンパイル時に弾く。+-- ===========================================================================++-- | [日本語]: along を必須引数に持つ多変量モデル。 'statModelMulti' が要求する能力。+--   [English]: A multivariate model that requires along as a mandatory+--   argument. The capability required by 'statModelMulti'.+class MultiVarModel m where+  -- | [日本語]: 訓練 'ModelFrame' (along の range と他変数の集約元)。+  --   [English]: The training 'ModelFrame' (the source of along's range and+  --   of other variables' aggregate values).+  mvFrame     :: m -> ModelFrame+  -- | [日本語]: 評価点 'ModelFrame' から (中心 μ̂, CI 帯 @(lo, hi)@) を評価する。+  --   設計行列が組めない場合は空 + 'Nothing'。+  --   [English]: Evaluates (center μ̂, CI band @(lo, hi)@) from an+  --   evaluation-point 'ModelFrame'. Returns empty + 'Nothing' if the design+  --   matrix cannot be built.+  mvEvalFrame :: m -> Double -> ModelFrame -> ([Double], Maybe ([Double], [Double]))+  -- | [日本語]: 評価点での予測区間 (PI)。 既定 'Nothing' (PI 非提供)。 closed-form PI を持つ+  --   モデル ('MultiLMModel' = 多変量 OLS) のみ override する (@svGridPI@ と同じ方針)。+  --   [English]: The prediction interval (PI) at the evaluation points.+  --   Defaults to 'Nothing' (PI not provided). Only overridden by models+  --   with a closed-form PI ('MultiLMModel' = multivariate OLS), following+  --   the same policy as @svGridPI@.+  mvEvalFramePI :: m -> Double -> ModelFrame -> Maybe ([Double], [Double])+  mvEvalFramePI _ _ _ = Nothing++-- | [日本語]: 学習済の多変量モデルと along 変数から effect plot の 'ModelSpec' を作る。+--   along は __必須引数__ (型で単/多変量を分離し誤用を弾く)。+--   @df |>> (layer (scatter \"x1\" \"y\") <> toPlot (statModelMulti m (along \"x1\") <> holdAt Median))@。+--   [English]: Builds an effect-plot 'ModelSpec' from a fitted multivariate+--   model and an along variable. along is a __mandatory argument__ (the type+--   separates single/multivariate to catch misuse at compile time).+--   @df |>> (layer (scatter \"x1\" \"y\") <> toPlot (statModelMulti m (along \"x1\") <> holdAt Median))@.+statModelMulti :: MultiVarModel m => m -> AlongSpec -> ModelSpec+statModelMulti m (AlongSpec v) = mempty { msRender = Just (renderGridMulti m v) }++-- | [日本語]: effect plot の @VisualSpec@ を組む。 along を grid で動かし他変数を 'HoldAgg' で固定。+--   byVar があれば第2変数の各値で曲線を色分け重畳する。 'statModelMulti' がクロージャ化。+--   [English]: Builds the effect-plot @VisualSpec@. Sweeps along over the+--   grid while holding other variables fixed via 'HoldAgg'. If byVar is+--   present, overlays one color-coded curve per value of the second+--   variable. Closed over by 'statModelMulti'.+renderGridMulti :: MultiVarModel m => m -> Text -> GridOpts -> VisualSpec+renderGridMulti m alongV opts =+  let mf         = mvFrame m+      (lo0, hi0) = alongRange mf alongV+      (lo, hi)   = fromMaybe (lo0, hi0) (goRange opts)+      n          = max 2 (goN opts)+      gxs        = linspace lo hi n+      level      = goLevel opts+      hold       = goHoldAt opts+      -- 1 曲線分 (override = byVar 固定, mCol = 線色)。+      oneCurve override mCol =+        case hold of+          Marginalize -> marginalizeCurve opts m alongV level gxs override mCol+          _ ->+            let ef         = evalFrame mf alongV hold override gxs+                (mu, mbCI) = mvEvalFrame m level ef+                mbPI       = mvEvalFramePI m level ef+                lineL      = layer (goLineDeco opts mCol n (line (inline gxs) (inline mu)))+                bL deco mb = case mb of+                  Just (los, his) -> layer (deco (band (inline gxs) (inline los) (inline his)))+                  Nothing         -> mempty+                ciDeco = goBandDeco opts mCol+                piA    = maybe 0.10 (* 0.5) (goAlpha opts)+                piDeco = goBandDeco (opts { goAlpha = Just piA }) mCol+                bands  = case goBandMode opts of+                  BandOff  -> mempty+                  BandCI   -> bL ciDeco mbCI+                  BandPI   -> case mbPI of+                                Just _  -> bL ciDeco mbPI+                                Nothing -> bL ciDeco mbCI       -- PI 非提供 → CI+                  BandCIPI -> case mbPI of+                                Just _  -> bL piDeco mbPI <> bL ciDeco mbCI+                                Nothing -> bL ciDeco mbCI       -- PI 非提供 → CI のみ+            in bands <> lineL+  in case goByVar opts of+       Nothing          -> oneCurve [] Nothing <> labelLegend opts+       Just (v2, vals)  ->+         foldMap+           (\(i, val) ->+              let col = fromHex (effectPalette !! (i `mod` length effectPalette))+              in oneCurve [(v2, val)] (Just col))+           (zip [0 :: Int ..] vals)++-- | [日本語]: Marginalize (PDP/AME): 各 grid 点で along=gx に固定し他変数は __観測分布のまま__、+--   μ̂ を全観測行で平均する (band なし・曲線のみ。 全観測行 × grid で重い)。+--   [English]: Marginalize (PDP/AME): at each grid point, fixes along=gx+--   while leaving other variables at __their observed distribution__, and+--   averages μ̂ over all observation rows (no band, curve only; heavy since+--   it's all observation rows × grid).+marginalizeCurve :: MultiVarModel m+                 => GridOpts -> m -> Text -> Double -> [Double] -> [(Text, Double)] -> Maybe Color -> VisualSpec+marginalizeCurve opts m alongV level gxs override mCol =+  let mf   = mvFrame m+      nObs = mfNRows mf+      base = mf { mfRoles = [ (nm, baseRole nm r) | (nm, r) <- mfRoles mf ] }+      baseRole nm r+        | isResponseRole r              = RoleResponse (V.replicate nObs 0)+        | Just fv <- lookup nm override = fixedRole r nObs fv+        | otherwise                     = r                       -- 観測分布のまま+      muAt gx =+        let (mu, _) = mvEvalFrame m level (setAlong base alongV gx)+        in sum mu / fromIntegral (max 1 (length mu))+      mus  = map muAt gxs+  in layer (goLineDeco opts mCol (length gxs) (line (inline gxs) (inline mus)))++-- | [日本語]: along 変数の観測範囲 (effect grid の既定範囲)。 along が連続でなければ退避 @(0,1)@。+--   [English]: The observed range of the along variable (the effect grid's+--   default range). Falls back to @(0,1)@ if along is not continuous.+alongRange :: ModelFrame -> Text -> (Double, Double)+alongRange mf v = case lookup v (mfRoles mf) of+  Just (RoleContinuous xs) | not (V.null xs) -> (V.minimum xs, V.maximum xs)+  _                                          -> (0, 1)++-- | [日本語]: 各説明変数を 'HoldAgg' で固定値の定数列に差し替えた評価点 'ModelFrame' を合成する。+--   along 変数は grid (gxs)、 応答列はダミー (@designMatrixF@ は応答を使わない)。+--   override は byVar 等の明示固定で 'HoldAgg' より優先する。+--   [English]: Composes an evaluation-point 'ModelFrame' by replacing each+--   predictor with a constant column fixed via 'HoldAgg'. The along variable+--   becomes the grid (gxs); the response column is a dummy (@designMatrixF@+--   doesn't use the response). override (explicit fixes like byVar) takes+--   precedence over 'HoldAgg'.+evalFrame :: ModelFrame -> Text -> HoldAgg -> [(Text, Double)] -> [Double] -> ModelFrame+evalFrame mf alongV hold override gxs =+  let n = length gxs+      adjust (nm, role)+        | isResponseRole role           = (nm, RoleResponse (V.replicate n 0))+        | nm == alongV                  = (nm, RoleContinuous (V.fromList gxs))+        | Just fv <- lookup nm override = (nm, fixedRole role n fv)+        | otherwise                     = (nm, holdRole hold n nm role)+  in mf { mfRoles = map adjust (mfRoles mf), mfNRows = n }++-- | [日本語]: frame の along 列だけを定数 gx に差し替える (行数据え置き、 Marginalize 用)。+--   [English]: Replaces only the frame's along column with the constant gx+--   (row count unchanged; for Marginalize).+setAlong :: ModelFrame -> Text -> Double -> ModelFrame+setAlong mf alongV gx =+  let n = mfNRows mf+      adj (nm, role)+        | nm == alongV = (nm, RoleContinuous (V.replicate n gx))+        | otherwise    = (nm, role)+  in mf { mfRoles = map adj (mfRoles mf) }++isResponseRole :: VarRole -> Bool+isResponseRole (RoleResponse _) = True+isResponseRole _                = False++-- | [日本語]: 1 変数を 'HoldAgg' で固定した定数列にする (連続は集約値、 factor は固定水準 index)。+--   factor は Mean\/Median\/Mode\/Fixed すべて最頻水準に振替 (Reference のみ参照=index 0)。+--   [English]: Turns one variable into a constant column fixed via+--   'HoldAgg' (an aggregate value for continuous, a fixed-level index for+--   factor). For factor, Mean\/Median\/Mode\/Fixed all redirect to the most+--   common level (only Reference uses the reference level = index 0).+holdRole :: HoldAgg -> Int -> Text -> VarRole -> VarRole+holdRole hold n nm role = case role of+  RoleContinuous xs ->+    let v = case hold of+              Mean        -> meanV xs+              Median      -> medianV xs+              Mode        -> modeV xs+              Reference   -> meanV xs              -- 連続に参照水準は無し → 平均で代替+              Marginalize -> meanV xs              -- (Marginalize は別経路。 安全側に平均)+              Fixed fm    -> fromMaybe (meanV xs) (lookup nm fm)+    in RoleContinuous (V.replicate n v)+  RoleFactor levels idx ->+    let fixIdx = case hold of+                   Reference -> 0+                   Fixed fm  -> maybe (modeIdx idx) (clampIdx levels . round) (lookup nm fm)+                   _         -> modeIdx idx+    in RoleFactor levels (V.replicate n fixIdx)+  RoleResponse _ -> RoleResponse (V.replicate n 0)++-- | [日本語]: 明示値 (byVar / Fixed override) で 1 変数を定数列にする。+--   [English]: Turns one variable into a constant column using an explicit+--   value (byVar / Fixed override).+fixedRole :: VarRole -> Int -> Double -> VarRole+fixedRole role n fv = case role of+  RoleContinuous _    -> RoleContinuous (V.replicate n fv)+  RoleFactor levels _ -> RoleFactor levels (V.replicate n (clampIdx levels (round fv)))+  RoleResponse _      -> RoleResponse (V.replicate n fv)++clampIdx :: [Text] -> Int -> Int+clampIdx levels i = max 0 (min (length levels - 1) i)++-- | [日本語]: byVar 曲線の固定色パレット (層別の値ごとに 1 色)。+--   [English]: Fixed color palette for byVar curves (one color per+--   stratification value).+effectPalette :: [Text]+effectPalette =+  [ "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2" ]++-- ===========================================================================+-- 応答曲面 3D 直結 — plot Phase 24 A3 (fit 済み多変量モデル → surface)+--+-- JMP Surface Profiler 同型: 2 因子 (v1, v2) を grid で動かし他変数を 'HoldAgg'+-- で固定、 μ̂ を 3D surface (z colormap 既定 ON) で描く。 effect plot+-- ('statModelMulti') の 2 因子版で、 評価核は同じ 'mvEvalFrame'。+-- ===========================================================================++-- | [日本語]: 2 因子 grid + 'HoldAgg' の評価点 frame ('evalFrame' の 2 変数版)。+--   行 = v2 (外側)、 列 = v1 (内側) — 'P3.surface3D' の grid 規約+--   (row = y 方向) に一致させる。+--   [English]: The evaluation-point frame for a two-factor grid + 'HoldAgg'+--   (the two-variable version of 'evalFrame'). Rows = v2 (outer), columns =+--   v1 (inner) — matching 'P3.surface3D''s grid convention (row = y+--   direction).+evalFrame2 :: ModelFrame -> Text -> Text -> HoldAgg -> [Double] -> [Double] -> ModelFrame+evalFrame2 mf v1 v2 hold gxs gys =+  let n   = length gxs * length gys+      x1s = [ gx | _  <- gys, gx <- gxs ]+      x2s = [ gy | gy <- gys, _  <- gxs ]+      adjust (nm, role)+        | isResponseRole role = (nm, RoleResponse (V.replicate n 0))+        | nm == v1            = (nm, RoleContinuous (V.fromList x1s))+        | nm == v2            = (nm, RoleContinuous (V.fromList x2s))+        | otherwise           = (nm, holdRole hold n nm role)+  in mf { mfRoles = map adjust (mfRoles mf), mfNRows = n }++-- | [日本語]: 応答曲面の数値核: @(gxs, gys, grid)@。 @grid !! j !! i = μ̂(gxs!!i, gys!!j)@。+--   [English]: The numerical core of the response surface: @(gxs, gys,+--   grid)@. @grid !! j !! i = μ̂(gxs!!i, gys!!j)@.+surfaceGrid :: MultiVarModel m+            => m -> Text -> Text -> SurfaceOpts -> ([Double], [Double], [[Double]])+surfaceGrid m v1 v2 opts =+  let mf         = mvFrame m+      (xlo, xhi) = fromMaybe (alongRange mf v1) (soXRange opts)+      (ylo, yhi) = fromMaybe (alongRange mf v2) (soYRange opts)+      n          = max 2 (soN opts)+      gxs        = linspace xlo xhi n+      gys        = linspace ylo yhi n+      ef         = evalFrame2 mf v1 v2 (soHoldAt opts) gxs gys+      (mu, _)    = mvEvalFrame m 0.95 ef+  in (gxs, gys, chunkRows n mu)++chunkRows :: Int -> [a] -> [[a]]+chunkRows k = go+  where go [] = []+        go xs = let (h, t) = splitAt k xs in h : go t++-- | [日本語]: fit 済み多変量モデル → 3D 応答曲面 (z colormap 既定 ON・colorbar 自動)。+--   @saveSVG3D path (surfaceOf m "x1" "x2" <> dataScatter3DOf m "x1" "x2")@。+--   [English]: Fitted multivariate model → 3D response surface (z colormap+--   on by default, colorbar automatic).+--   @saveSVG3D path (surfaceOf m "x1" "x2" <> dataScatter3DOf m "x1" "x2")@.+surfaceOf :: MultiVarModel m => m -> Text -> Text -> P3.VisualSpec3D+surfaceOf m v1 v2 = surfaceOfWith m v1 v2 defaultSurfaceOpts++-- | [日本語]: オプション付き ('SurfaceOpts': grid 点数・hold・範囲)。+--   [English]: The variant with options ('SurfaceOpts': grid point count,+--   hold, range).+surfaceOfWith :: MultiVarModel m => m -> Text -> Text -> SurfaceOpts -> P3.VisualSpec3D+surfaceOfWith m v1 v2 opts =+  let (gxs, gys, grid') = surfaceGrid m v1 v2 opts+  in P3.layer3D ( P3.surface3DGrid grid'+               <> P3.xRange3D (head gxs, last gxs)+               <> P3.yRange3D (head gys, last gys)+               <> P3.colormap3D )++-- | [日本語]: 実測点の 3D overlay: 訓練データの @(v1, v2, y)@ を scatter3D で重畳。+--   [English]: A 3D overlay of the observed points: overlays the training+--   data's @(v1, v2, y)@ via scatter3D.+dataScatter3DOf :: MultiVarModel m => m -> Text -> Text -> P3.VisualSpec3D+dataScatter3DOf m v1 v2 =+  let mf = mvFrame m+      contOf nm = case lookup nm (mfRoles mf) of+        Just (RoleContinuous xs) -> V.toList xs+        _                        -> []+      ys = case [ v | (_, RoleResponse v) <- mfRoles mf ] of+        (v : _) -> V.toList v+        []      -> []+      pts = zipWith3 Point3 (contOf v1) (contOf v2) ys+  in P3.layer3D (P3.scatter3DPoints pts <> P3.color3D (fromHex "#d62728") <> P3.size3D 4)++meanV :: V.Vector Double -> Double+meanV xs | V.null xs = 0+         | otherwise = V.sum xs / fromIntegral (V.length xs)++medianV :: V.Vector Double -> Double+medianV xs+  | null ys   = 0+  | odd k     = ys !! (k `div` 2)+  | otherwise = (ys !! (k `div` 2 - 1) + ys !! (k `div` 2)) / 2+  where ys = sort (V.toList xs)+        k  = length ys++-- | [日本語]: 連続列の最頻 (観測値の完全一致でグループ化。 繰り返しのない真の連続では任意)。+--   [English]: The mode of a continuous column (grouped by exact value+--   match; arbitrary for genuinely continuous data with no repeats).+modeV :: V.Vector Double -> Double+modeV xs | V.null xs = 0+         | otherwise = mostCommon (V.toList xs)++-- | [日本語]: factor の最頻水準 index。+--   [English]: The index of the most common factor level.+modeIdx :: V.Vector Int -> Int+modeIdx idx | V.null idx = 0+            | otherwise  = mostCommon (V.toList idx)++mostCommon :: Ord a => [a] -> a+mostCommon = fst . maximumBy (comparing snd)+           . map (\g -> (head g, length g)) . group . sort++-- ===========================================================================+-- 共有描画 helper (複数のモデル族が利用)+-- ===========================================================================++-- | [日本語]: CI band の既定 level (95%)。+--   [English]: The default level for a CI band (95%).+defaultCILevel :: Double+defaultCILevel = 0.95++-- | [日本語]: 分位線の色パレット (τ 昇順に割当て。 必要数を循環)。+--   [English]: Color palette for quantile lines (assigned in ascending τ+--   order; cycles if more are needed).+quantilePalette :: [T.Text]+quantilePalette =+  [ "#4575b4", "#d73027", "#1a9850", "#984ea3", "#ff7f00", "#377eb8" ]++-- | [日本語]: 階段関数の頂点列を作る。 開始値 @s0@ (= t=0 での値) から、 各 @(tᵢ, sᵢ)@ について+--   直前の高さで @tᵢ@ まで水平に来てから @sᵢ@ に垂直に跳ぶ 2 頂点を出す。+--   [English]: Builds the vertex list of a step function. Starting from+--   @s0@ (= the value at t=0), for each @(tᵢ, sᵢ)@ emits two vertices: a+--   horizontal run to @tᵢ@ at the previous height, then a vertical jump to+--   @sᵢ@.+stepVerts :: Double -> [(Double, Double)] -> [(Double, Double)]+stepVerts s0 pts = (0, s0) : go s0 pts+  where+    go _    []            = []+    go prev ((t, s) : rest) = (t, prev) : (t, s) : go s rest++-- | [日本語]: grid index を x として複数曲線を色分け重畳する内部 helper。+--   [English]: An internal helper that overlays multiple color-coded curves+--   using the grid index as x.+gridCurves :: [(Text, [Double])] -> VisualSpec+gridCurves named =+  let mkLine (lbl, ys) =+        let xs = [ fromIntegral i | i <- [1 .. length ys] ] :: [Double]+        in layer ( line (inline xs) (inline ys)+                 <> colorBy (inlineCat (replicate (length ys) lbl)) )+  in mconcat (map mkLine named)++-- | [日本語]: 特徴重要度 → bar layer ("f1", "f2", … をカテゴリ軸に・値=重要度)。+--   [English]: Feature importances → bar layer ("f1", "f2", … as the+--   category axis; value = importance).+importanceBar :: [Double] -> VisualSpec+importanceBar imps =+  let labels = [ "f" <> T.pack (show k) | k <- [1 .. length imps] ]+  in layer (bar (inlineCat labels) (inline imps))++-- | [日本語]: 行列の第 @i@/@j@ 列を (xs, ys) として取り出す (列不足は 0 埋め)。+--   [English]: Extracts a matrix's @i@-th\/@j@-th columns as (xs, ys)+--   (missing columns are zero-filled).+matCols2 :: LA.Matrix Double -> Int -> Int -> ([Double], [Double])+matCols2 m i j =+  let cols = LA.toColumns m+      colAt k = if k < length cols then LA.toList (cols !! k) else replicate (LA.rows m) 0+  in (colAt i, colAt j)++-- | [日本語]: クラス代表点 (平均) をクラス色 ✚ で散布する (第 0/1 特徴)。 Discriminant /+--   NaiveBayes(Gaussian) の data-free 代表図。+--   [English]: Scatters each class's representative point (mean) as a+--   class-colored ✚ (features 0/1). A data-free representative figure for+--   Discriminant \/ NaiveBayes(Gaussian).+classMeansScatter :: [[Double]] -> [Int] -> VisualSpec+classMeansScatter rows cids = classMeansScatterNamed rows cids []++-- | [日本語]: 'classMeansScatter' の __クラス名つき__版。 @names@ があれば凡例をクラス名 (levels)+--   に、 無ければ整数へフォールバック (@names !! k@・範囲外は show)。 df|-> 経路が+--   levels を載せた分類モデルの代表図で使う。+--   [English]: The __class-named__ variant of 'classMeansScatter'. If+--   @names@ is present, the legend uses the class names (levels); otherwise+--   falls back to integers (@names !! k@; out-of-range uses show). Used by+--   the representative figure of classification models where the df|->+--   path attaches levels.+classMeansScatterNamed :: [[Double]] -> [Int] -> [Text] -> VisualSpec+classMeansScatterNamed rows cids names+  | null rows = mempty+  | otherwise =+      let xs   = [ if not (null r) then head r else 0 | r <- rows ]+          ys   = [ if length r >= 2 then r !! 1 else 0 | r <- rows ]+          nameOf k | k >= 0 && k < length names = names !! k+                   | otherwise                  = T.pack (show k)+          labs = map nameOf cids+      in layer ( scatter (inline xs) (inline ys)+               <> colorBy (inlineCat labs)+               <> shape MShCross )++-- | [日本語]: chain index → 色 (effectPalette を巡回)。+--   [English]: chain index → color (cycles through effectPalette).+chainColor :: Int -> Text+chainColor k = effectPalette !! (k `mod` length effectPalette)++-- ===========================================================================+-- 分類器抽象 (Discriminant / NaiveBayes / KNN 共通) — Phase 68 A3+-- ===========================================================================++-- | [日本語]: 学習済分類器を評価点行列で走らせ、 各行の予測クラスを返す共通インターフェース。+--   (@decisionBoundaryOf@ / @confusionOf@ が分類器種に依らず動くための薄い抽象)。+--   [English]: A common interface that runs a fitted classifier over an+--   evaluation-point matrix and returns each row's predicted class. (A thin+--   abstraction letting @decisionBoundaryOf@ \/ @confusionOf@ work+--   regardless of classifier type.)+class ClassPredict c where+  predictClasses :: c -> LA.Matrix Double -> [Int]+  -- | [日本語]: クラス番号 0..K-1 に対応する __クラス名 (levels)__。 高レベル @df |->@ 経路が+  --   fit 時に載せる (factor 列なら levels 名・数値列なら数値)。 既定は空 = 名前を+  --   持たないモデル (@confusionOf@ 等は空なら整数ラベルにフォールバック)。+  --   [English]: The __class names (levels)__ corresponding to class+  --   numbers 0..K-1. Attached by the high-level @df |->@ path at fit time+  --   (level names for a factor column, numbers for a numeric column).+  --   Defaults to empty = a model with no names (@confusionOf@ etc. fall+  --   back to integer labels when empty).+  classNamesOf :: c -> [Text]+  classNamesOf _ = []++-- ===========================================================================+-- 回帰診断の可視化 (係数 forest / 実測vs予測) — Phase 72.4/72.5+--+-- 係数表 (@coefSummary@・'Hanalyze.Diagnostics') と各モデルの実測/予測ペアを+-- 図に落とす薄い玄関。 数値層 (係数統計・予測) は別パッケージに依存しない+-- 'Diagnostics' / 各 fit が持ち、 ここ (別パッケージ hanalyze-plot 側) では+-- @VisualSpec@ 化だけを担う。+-- ===========================================================================++-- | [日本語]: fit 済モデルから (実測値, 予測値) の対を取り出せる能力。 実測値は+--   @fitted + residual@ で復元する (回帰一般で成り立つ)。 instance は各モデル族の+--   'Plottable' と同じ Plot.* 側に置く (orphan・クラス=Core / instance=族 module)。+--   [English]: The capability to extract (observed, predicted) pairs from a+--   fitted model. The observed value is reconstructed as @fitted ++--   residual@ (holds for regression in general). Instances live on the same+--   Plot.* side as each model family's 'Plottable' (orphan instances; class+--   in Core, instance in the family module).+class HasObsPred m where+  -- | [日本語]: @(observed, predicted)@。 長さは観測数 n で一致する。+  --   [English]: @(observed, predicted)@. Both have length equal to the+  --   number of observations n.+  obsPredPairs :: m -> ([Double], [Double])++-- | [日本語]: 実測 vs 予測プロット。 x=実測値・y=予測値の散布に @y = x@ の参照線 (灰の破線) を+--   重ねる。 点が参照線に近いほど当てはまりが良い (残差が小さい)。+--   [English]: Observed-vs-predicted plot. Overlays a @y = x@ reference line+--   (gray dashed) on a scatter of x=observed, y=predicted. The closer the+--   points are to the reference line, the better the fit (smaller+--   residuals).+obsVsPred :: HasObsPred m => m -> VisualSpec+obsVsPred m = let (obs, prd) = obsPredPairs m in obsPredSpec obs prd++-- | [日本語]: (実測, 予測) のリストから実測 vs 予測 spec を組む。 'obsVsPred' の純データ版+--   (テスト・任意のペアからの作図に再利用)。 空入力は空図。+--   [English]: Builds an observed-vs-predicted spec from lists of+--   (observed, predicted). The pure-data variant of 'obsVsPred' (reusable+--   for tests or plotting arbitrary pairs). Empty input yields an empty+--   figure.+obsPredSpec :: [Double] -> [Double] -> VisualSpec+obsPredSpec obs prd+  | null obs  = mempty+  | otherwise =+      let lo = minimum (obs ++ prd)+          hi = maximum (obs ++ prd)+      in  layer ( line (inline [lo, hi]) (inline [lo, hi])+                <> linetype LtDashed+                <> color (fromHex "#888888") )+       <> layer (scatter (inline obs) (inline prd))+       <> xLabel "observed"+       <> yLabel "predicted"++-- | [日本語]: 係数 forest plot。 各係数の点推定 ('crEstimate') を中心、 95% CI ('crCI95') の+--   半幅を誤差バーとして 1 行ずつ水平に並べ、 0 (= 効果なし) に参照線を引く。 解析+--   Wald CI (@coefSummary@) を持つ線形系で使う (CI は左右対称なので半幅で表せる)。+--   bootstrap 由来の非対称 CI を図にしたい場合は @coefSummaryBoot@ の行から個別に組む。+--   [English]: Coefficient forest plot. Lays out each coefficient's point+--   estimate ('crEstimate') as the center, one row per coefficient, with the+--   half-width of the 95% CI ('crCI95') as the error bar, and draws a+--   reference line at 0 (= no effect). Used for linear systems with an+--   analytic Wald CI (@coefSummary@) (since the CI is symmetric and can be+--   expressed as a half-width). To plot asymmetric bootstrap-derived CIs,+--   build the figure manually from 'coefSummaryBoot''s rows instead.+coefForest :: HasCoefSummary m => m -> VisualSpec+coefForest m =+  let rows  = coefSummary m+      names = [ crTerm r | r <- rows ]+      ests  = [ crEstimate r | r <- rows ]+      errs  = [ (hi - lo) / 2 | r <- rows, let (lo, hi) = crCI95 r ]+  in if null rows+       then mempty+       else layer (forest (inlineCat names) (inline ests) (inline errs) <> forestNull 0)
+ src/Hanalyze/Plot/Linear.hs view
@@ -0,0 +1,306 @@+-- |+-- Module      : Hanalyze.Plot.Linear+-- Description : hgg 連携層 — 線形モデル族の図化 instance+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- [日本語]: hgg 連携層 — __線形モデル族__ の図化 instance。+--+-- ⚠ 親 'Hanalyze.Plot' と同じく別パッケージ @hanalyze-plot@ に属し、+-- @cabal build --project-file=cabal.project.plot@ で build される。 共通基盤 (class / ModelSpec / grid 評価核) は+-- 'Hanalyze.Plot.Core' を import して取り込む (orphan instance を許容:+-- クラス=Core・instance=ここ・型=Wrappers)。+--+-- 担当する型 (= LM 系・GLM 系・WLS):+--   LMModel / MultiLMModel / WeightedLMModel / GLMModel / MultiGLMModel。+--+-- [English]: hgg integration layer — plotting instances for the+-- __linear-model family__.+--+-- ⚠ Lives in the same separate package @hanalyze-plot@ as its parent+-- 'Hanalyze.Plot', built via @cabal build --project-file=cabal.project.plot@.+-- The shared foundation+-- (class \/ ModelSpec \/ grid evaluation core) is pulled in via+-- 'Hanalyze.Plot.Core' (orphan instances allowed: class in Core,+-- instances here, types in Wrappers).+--+-- Types covered (= LM family, GLM family, WLS):+--   LMModel \/ MultiLMModel \/ WeightedLMModel \/ GLMModel \/ MultiGLMModel.+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleContexts #-}+module Hanalyze.Plot.Linear+  ( familyObsDist+  ) where++import           Data.List             (sortBy, zip4)+import           Data.Ord              (comparing)+import qualified Hanalyze.Model.HBM.Distribution as BD+import qualified Numeric.LinearAlgebra as LA++import           Graphics.Hgg.Spec     ( layer, inline+                                       , scatter, line, band )++import           Hanalyze.Model.Wrappers+import           Hanalyze.Plot.Core+import           Hanalyze.Fit            (weightedR2)+import           Hanalyze.Model.Core     (FitResult, coefficientsV, fittedV, residualsV, rSquared1)+import           Hanalyze.Model.GLM      ( Family (..), LinkFn (..), GlmPredictCI (..)+                                                , predictGlmMuWithCI )+import           Hanalyze.Model.LM       ( CIBand (..), confidenceBand, confidenceBandAt+                                                , predictionBandAt )+import           Hanalyze.Model.Formula.Design  (designMatrixF)++-- ===========================================================================+-- 多変量モデル型 (effect plot 用、 新規 fit)+--+-- 既存の単変数 'LMModel' / 'GLMModel' (設計行列が @[1, x]@ 固定) とは別型。+-- formula 文字列 + @DataFrame@ で多変量 fit し、 formula を保持して評価点設計行列を+-- 組む (HoldAgg 固定 + along grid)。 ★GLM は formula 経路が未整備なので+-- 'designMatrixF' で設計行列を作り 'fitGLMFull' を直接呼ぶ。+-- ===========================================================================++instance MultiVarModel MultiLMModel where+  mvFrame = mlmFrame+  mvEvalFrame m level ef =+    case designMatrixF (mlmFormula m) ef of+      Left _        -> ([], Nothing)+      Right (xe, _) ->+        let cib = confidenceBandAt (mlmDesign m) (mlmResult m) level xe+            los = lowerBound cib+            his = upperBound cib+            mu  = zipWith (\l h -> (l + h) / 2) los his+        in (mu, Just (los, his))+  -- 多変量 OLS の closed-form PI (評価点設計行列 → predictionBandAt)。+  mvEvalFramePI m level ef =+    case designMatrixF (mlmFormula m) ef of+      Left _        -> Nothing+      Right (xe, _) ->+        let pib = predictionBandAt (mlmDesign m) (mlmResult m) level xe+        in Just (lowerBound pib, upperBound pib)++instance MultiVarModel MultiGLMModel where+  mvFrame = mglmFrame+  mvEvalFrame m level ef =+    case designMatrixF (mglmFormula m) ef of+      Left _        -> ([], Nothing)+      Right (xe, _) ->+        let beta = coefficientsV (mglmResult m)+            cis  = [ predictGlmMuWithCI (mglmLink m) level beta (mglmSigma m) r+                   | r <- LA.toRows xe ]+        in (map gpMu cis, Just (map gpLo cis, map gpHi cis))++-- ===========================================================================+-- 線形モデル (描画可能)+--+-- 'FitResult' (数値核) は設計行列 X を保持しないが、 回帰線・CI band を描くには+-- X が要る (@confidenceBand@ は X 引数)。 そこで X と生 predictor を束ねた+-- 「描画可能なモデル」 を別型にする (= plot Phase 15 §2.1 の開放論点を (i) で確定)。+-- ===========================================================================+++instance Plottable LMModel where+  -- 散布図に重ねる回帰線 + CI band。 @confidenceBand@ は **訓練点**で評価し+  -- @yHats ± se@ を返す (= grid を渡すと fitted と不整合)。 ゆえに合成 grid を+  -- 使わず、 訓練 x を昇順ソートして直線を結ぶ (= 単回帰なら直線で grid と同形、+  -- かつ @confidenceBand@ を無改修で再利用できる)。 ± 半幅 errorY = se。+  toPlot m =+    let res    = lmResult m+        xs     = LA.toList (lmXraw m)+        yhat   = LA.toList (fittedV res)+        cib    = confidenceBand (lmDesign m) res defaultCILevel+        se     = zipWith (-) (upperBound cib) yhat   -- upper - ŷ = 片側半幅+        sorted = sortBy (comparing (\(x, _, _) -> x)) (zip3 xs yhat se)+        xsS    = [ x | (x, _, _) <- sorted ]+        yhatS  = [ y | (_, y, _) <- sorted ]+        seS    = [ e | (_, _, e) <- sorted ]+    in layer (band (inline xsS) (inline (zipWith (-) yhatS seS)) (inline (zipWith (+) yhatS seS)))+         <> layer (line (inline xsS) (inline yhatS))++  -- 残差診断 (代表回帰線 + 残差 vs fitted)。+  diagnosticPlots m =+    let res  = lmResult m+        yhat = LA.toList (fittedV res)+        resd = LA.toList (residualsV res)+    in [ toPlot m+       , layer (scatter (inline yhat) (inline resd))+       ]++-- | [日本語]: grid 評価。 grid x で設計行列 @[1, x]@ を再構築し、+--   訓練の分散核を流用する @confidenceBandAt@ で滑らかな曲線 + 対称 CI 帯を出す。+--   [English]: Grid evaluation. Rebuilds the design matrix @[1, x]@ at the+--   grid x, reusing the training variance kernel via @confidenceBandAt@ to+--   produce a smooth curve + symmetric CI band.+instance SingleVarModel LMModel where+  svRange m = let xs = LA.toList (lmXraw m) in (minimum xs, maximum xs)+  svGrid m level gxs =+    let xEval = LA.fromColumns [ LA.konst 1 (length gxs), LA.fromList gxs ]+        cib   = confidenceBandAt (lmDesign m) (lmResult m) level xEval+        los   = lowerBound cib+        his   = upperBound cib+        mu    = zipWith (\l h -> (l + h) / 2) los his+    in (mu, Just (los, his))+  -- PI = closed form σ̂²(1 + xᵀ(XᵀX)⁻¹x) (statsmodels obs_ci と一致)。+  svGridPI m level gxs =+    let xEval = LA.fromColumns [ LA.konst 1 (length gxs), LA.fromList gxs ]+        pib   = predictionBandAt (lmDesign m) (lmResult m) level xEval+    in Just (lowerBound pib, upperBound pib)+  -- A8: 係数 [β₀, β₁] と R² (式/R² 凡例注釈用)。+  svCoefR2 m = Just (LA.toList (coefficientsV (lmResult m)), rSquared1 (lmResult m))+  -- ブートストラップ: 加法誤差ゆえ obsDist=Nothing (μ + 再標本化残差)。+  svBootKit m = Just BootKit+    { bkX = LA.toList (lmXraw m)+    , bkY = zipWith (+) (LA.toList (fittedV (lmResult m))) (LA.toList (residualsV (lmResult m)))+    , bkRefit = \xs ys -> lmModel (LA.fromList xs) (LA.fromList ys)+    , bkObsDist = Nothing }++-- ===========================================================================+-- 一般化線形モデル (描画可能)+--+-- GLM の不確実性帯は **μ (応答) スケールで非対称** (線形予測子 η の対称 Wald CI を+-- 逆リンク gInv で μ に写すため、 Logit/Log 等では下側・上側の半幅が異なる)。 ゆえに+-- LMModel/GPResult の対称 band (ŷ±se) では忠実に描けない。 そこで+-- 下境界 lo / 上境界 hi を別々に持てる 'band' layer (= MBand area fill) を使い、 μ 曲線は+-- 'line' で重ねる。 帯は **訓練点での Wald CI** を 'predictGlmMuWithCI' で評価する+-- (= grid 補間でなく fit と整合)。 'fitGLMFull' が返す逆 Fisher 情報 Σ=(XᵀWX)⁻¹ が要る。+-- ===========================================================================++instance Plottable GLMModel where+  -- μ 曲線 + 非対称 Wald CI 帯。 各訓練点 (設計行列の行) で 'predictGlmMuWithCI' を+  -- 評価し、 x 昇順にソートして band (lo→hi の area) と μ 折れ線を重ねる。 帯を先に+  -- 置いて μ 線を上に描く。+  toPlot m =+    let beta  = coefficientsV (glmResult m)+        rows  = LA.toRows (glmDesign m)+        cis   = [ predictGlmMuWithCI (glmLink m) defaultCILevel beta (glmSigma m) r+                | r <- rows ]+        quads = sortBy (comparing (\(x, _, _, _) -> x))+                  (zip4 (LA.toList (glmXraw m))+                        (map gpMu cis) (map gpLo cis) (map gpHi cis))+        xsS = [ x | (x, _, _, _) <- quads ]+        muS = [ u | (_, u, _, _) <- quads ]+        loS = [ l | (_, _, l, _) <- quads ]+        hiS = [ h | (_, _, _, h) <- quads ]+    in layer (band (inline xsS) (inline loS) (inline hiS))+         <> layer (line (inline xsS) (inline muS))++  -- 残差診断 (μ 曲線 + 帯、 残差 vs fitted μ̂)。+  diagnosticPlots m =+    let res  = glmResult m+        yhat = LA.toList (fittedV res)+        resd = LA.toList (residualsV res)+    in [ toPlot m+       , layer (scatter (inline yhat) (inline resd))+       ]++-- | [日本語]: grid 評価。 grid x の行 @[1, x]@ を 'predictGlmMuWithCI' に渡し、+--   μ スケールの非対称 Wald CI 帯を滑らかに評価する (band lo/hi は別々に保持)。+--   [English]: Grid evaluation. Passes each grid-x row @[1, x]@ to+--   'predictGlmMuWithCI', smoothly evaluating the asymmetric Wald CI band+--   on the μ scale (band lo\/hi are kept separately).+instance SingleVarModel GLMModel where+  svRange m = let xs = LA.toList (glmXraw m) in (minimum xs, maximum xs)+  svGrid m level gxs =+    let beta = coefficientsV (glmResult m)+        cis  = [ predictGlmMuWithCI (glmLink m) level beta (glmSigma m)+                   (LA.fromList [1, gx])+               | gx <- gxs ]+    in (map gpMu cis, Just (map gpLo cis, map gpHi cis))+  -- PI は **Gaussian + Identity のみ** = LM の closed form に帰着 (μ̂ = Xβ・W=I)。+  -- 非 Gaussian (Poisson/Binomial) は予測区間が応答分布の離散/非対称分位を要し+  -- closed form で出ないため 'Nothing' (over-claim しない・CI 帯と同じ部分集合方針)。+  svGridPI m level gxs = case (glmFamily m, glmLink m) of+    (Gaussian, Identity) ->+      let xEval = LA.fromColumns [ LA.konst 1 (length gxs), LA.fromList gxs ]+          pib   = predictionBandAt (glmDesign m) (glmResult m) level xEval+      in Just (lowerBound pib, upperBound pib)+    _ -> Nothing+  -- ブートストラップ: 新規観測は Family(μ) から parametric にドロー (Poisson/Bernoulli)。+  -- これにより closed form PI を持たない非 Gaussian GLM でも PI を出せる。+  svBootKit m = Just BootKit+    { bkX = LA.toList (glmXraw m)+    , bkY = zipWith (+) (LA.toList (fittedV (glmResult m))) (LA.toList (residualsV (glmResult m)))+    , bkRefit = \xs ys -> glmModel (glmFamily m) (glmLink m) (LA.fromList xs) (LA.fromList ys)+    , bkObsDist = familyObsDist (glmFamily m) }++-- ===========================================================================+-- 重み付き最小二乗 (WLS)+-- ===========================================================================++-- | [日本語]: grid 経路に委譲 (内側 LM の svGrid/PI は非スケール xEval × スケール設計で正しい+--   WLS CI を出す)。 'svRange' は元 x ('lmXraw') から。 @svCoefR2@ のみ override し、+--   R² は statsmodels WLS と一致する weighted R² を返す (β̂ は内側のスケール OLS が WLS)。+--   [English]: Delegates to the grid path (the inner LM's svGrid\/PI produces+--   the correct WLS CI from the unscaled xEval × scaled design). 'svRange'+--   comes from the raw x ('lmXraw'). Only @svCoefR2@ is overridden, returning+--   a weighted R² matching statsmodels WLS (β̂ itself is the inner scaled+--   OLS, which is the WLS estimate).+instance SingleVarModel WeightedLMModel where+  svRange  (WeightedLMModel m _ _)  = svRange m+  svGrid   (WeightedLMModel m _ _)  = svGrid m+  svGridPI (WeightedLMModel m _ _)  = svGridPI m+  svCoefR2 (WeightedLMModel m ws ys) =+    let coefs = LA.toList (coefficientsV (lmResult m))+        yhats = case coefs of                                  -- ŷ = β₀ + β₁x (元スケール)+          (b0 : b1 : _) -> [ b0 + b1 * x | x <- LA.toList (lmXraw m) ]+          [b0]          -> [ b0 | _ <- LA.toList (lmXraw m) ]+          _             -> ys+    in Just (coefs, weightedR2 ws ys yhats)++-- | [日本語]: ★訓練点経路 ('LMModel' の素の @toPlot@) を__使わず__ grid 経路 ('statModel') に+--   固定する。 これで WLS 線+CI が元 x スケールで出て、 元データ散布図と整合する。+--   [English]: ★Deliberately does __not__ use the training-point path+--   (plain 'LMModel' @toPlot@), fixing instead on the grid path+--   ('statModel'). This makes the WLS line+CI come out on the original x+--   scale, matching the original data scatter.+instance Plottable WeightedLMModel where+  toPlot = toPlot . statModel++-- ===========================================================================+-- GLM family → 観測分布 (ブートストラップ PI 用)+-- ===========================================================================++-- | [日本語]: GLM family → 新規観測の分布関数 (μ ↦ 分布。 ブートストラップ PI の parametric ドロー用)。+--   Gaussian は加法残差で扱うため 'Nothing' (σ̂ を別途要さない)。 'svBootKit' が使う。+--   [English]: GLM family → distribution function for new observations+--   (μ ↦ distribution; used for parametric draws in bootstrap PI). Gaussian+--   is handled via additive residuals, so it returns 'Nothing' (no separate+--   σ̂ needed). Used by 'svBootKit'.+familyObsDist :: Family -> Maybe (Double -> BD.Distribution Double)+familyObsDist Poisson  = Just (\mu -> BD.Poisson  (max 1e-9 mu))+familyObsDist Binomial = Just (\mu -> BD.Bernoulli (min (1 - 1e-12) (max 1e-12 mu)))+familyObsDist Gaussian = Nothing++-- ===========================================================================+-- 実測 vs 予測 (HasObsPred) — Phase 72.4+--+-- 実測値 = fitted + residual で復元する (回帰一般)。 WLS は内側 fit が √w スケール+-- なので予測を 1/√w で元スケールへ戻し、 実測は保持した元 y ('wlmY') を使う。+-- ===========================================================================++-- | [日本語]: FitResult から (実測, 予測) を復元する共通ヘルパ。+--   [English]: A shared helper that recovers (observed, predicted) from a+--   FitResult.+obsPredFromFit :: FitResult -> ([Double], [Double])+obsPredFromFit r =+  let f = LA.toList (fittedV r)+      e = LA.toList (residualsV r)+  in (zipWith (+) f e, f)++instance HasObsPred LMModel where+  obsPredPairs = obsPredFromFit . lmResult++instance HasObsPred MultiLMModel where+  obsPredPairs = obsPredFromFit . mlmResult++instance HasObsPred GLMModel where+  obsPredPairs = obsPredFromFit . glmResult++instance HasObsPred MultiGLMModel where+  obsPredPairs = obsPredFromFit . mglmResult++instance HasObsPred WeightedLMModel where+  obsPredPairs m =+    let fScaled = LA.toList (fittedV (lmResult (wlmInner m)))+        prd     = zipWith (\f w -> if w > 0 then f / sqrt w else f) fScaled (wlmWeights m)+    in (wlmY m, prd)
+ src/Hanalyze/Plot/ML.hs view
@@ -0,0 +1,2161 @@+-- |+-- Module      : Hanalyze.Plot.ML+-- Description : hgg 連携層 — ML / 統計モデル連携族の図化 instance + 抽出子+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- [日本語]: hgg 連携層 — __ML / 統計モデル連携族__ の図化 instance + 抽出子。+--+-- ⚠ 親 'Hanalyze.Plot' と同じく別パッケージ @hanalyze-plot@ に属し、+-- @cabal build --project-file=cabal.project.plot@ で build される。 共通基盤 (class / ModelSpec / grid 評価核) は+-- 'Hanalyze.Plot.Core' を import して取り込む (orphan instance を許容:+-- クラス=Core・instance=ここ・型=Wrappers/各 Model module)。+--+-- 担当する型・ヘルパ:+--   クラスタリング (KMeans) / 木・アンサンブル (PCA/RF/GB/DT) / 分類+--   (Discriminant/NaiveBayes/KNN) / 次元圧縮 (PLS) / 時系列・生存・FDA+--   (Forecast/GARCH/AFT/FunctionalPCA/FLM) / 罰則回帰・因果探索 (Reg/LiNGAM) /+--   記述統計・検定 (TestResult)。 新規 plot mark は不要 (既存 mark の組合せ)。+--+-- [English]: hgg integration layer — __plotting instances and extractors__+-- for the ML \/ statistical-model family.+--+-- ⚠ Lives in the same separate package @hanalyze-plot@ as the parent+-- 'Hanalyze.Plot', built via @cabal build --project-file=cabal.project.plot@.+-- It imports the common+-- foundation (class \/ ModelSpec \/ grid evaluation core) from+-- 'Hanalyze.Plot.Core' (orphan instances are permitted: class =+-- Core, instance = here, type = Wrappers \/ each Model module).+--+-- Types and helpers covered:+--   clustering (KMeans) \/ trees and ensembles (PCA\/RF\/GB\/DT) \/+-- classification (Discriminant\/NaiveBayes\/KNN) \/ dimensionality reduction+-- (PLS) \/ time series, survival, FDA (Forecast\/GARCH\/AFT\/FunctionalPCA\/FLM)+-- \/ penalized regression and causal discovery (Reg\/LiNGAM) \/ descriptive+-- statistics and testing (TestResult). No new plot marks are needed+-- (combinations of existing marks suffice).+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleContexts #-}+module Hanalyze.Plot.ML+  ( -- * クラスタリング+    clusterScatterOf+  , centroidsOf+    -- * クラスタを囲む (凸包輪郭 / 95% 共分散楕円)+  , clusterHullOf+  , clusterEllipseOf+    -- * DOE prediction profiler+  , ResidualMode (..)+  , ProfilerSpec (..)+  , profiler+  , profilerResidual+  , contourOf+    -- * 階層クラスタリング dendrogram+  , DendroOpts (..)+  , defaultDendroOpts+  , dendrogramOf+  , dendrogramOf'+    -- * 木/アンサンブル+  , treeImportances+    -- * 決定木 樹形図 (rpart.plot 流・annotation ベース)+  , treePlot+  , treePlotRaw+    -- * 分類+  , decisionBoundaryOf+  , confusionOf+    -- * MDS 埋め込み (モデル型 + 群色オプション)+  , MDSView+  , mdsView+  , mdsGroupBy+    -- * NN 可視化+  , nnLossOf+    -- * カーネル SVM サポートベクタ可視化+  , svmSupportVectorsOf+    -- * 決定境界を線で描く (等高線)+  , ScorePredict (..)+  , decisionLineOf+    -- * 部分従属図 (PDP / ICE)+  , RegPredict (..)+    -- ** Plottable 中間型 (HBM 抽出子と同型・toPlot で描画)+  , PDPView+  , pdp+  , pdpIce+  , pdpOf+  , pdpIceOf+  , pdpPlot+  , pdpIcePlot+  , partialDependencePlot+  , partialDependenceIcePlot+    -- * 次元圧縮 (PLS 診断ビュー)+  , PLSView (..)+  , PLSViewKind (..)+  , scoreView+  , loadingView+  , vipView+    -- * 時系列・生存・FDA+  , garchVolatility+  , aftSurvivalAt+    -- * 罰則回帰・因果探索+  , regPathPlot+  , lingamDag+  , lingamDagNamed+  , varLagDagNamed+  , bootstrapEdgeProbOf+    -- * 記述統計・検定+  , testForest+  , testForestLabeled+  , describeBox+  ) where++import           Control.Applicative   ((<|>))+import           Data.Maybe            (fromMaybe)+import           Data.List             (nub, sort, elemIndex, sortBy, foldl')+import           Data.Ord              (comparing)+import qualified Data.Map.Strict       as Map+import qualified Data.Vector           as V+import qualified Data.Vector.Unboxed    as VU+import qualified Numeric.LinearAlgebra as LA++import           Data.Text             (Text)+import qualified Data.Text             as T++import           Graphics.Hgg.Spec     ( VisualSpec, layer, inline, inlineCat+                                       , fromHex+                                       , scatter, line, band+                                       , shape, MarkShape (..)+                                       , heatmap, contour, contourFilled, contourLevels+                                       , label, color, colorBy, bar, boxplot, forest, forestNull+                                       , legendOff+                                       , title, coordFlip, coordCartesian, subplots, subplotCols+                                       , scaleXDiscreteLimits+                                       , xLabel, yLabel+                                       , annotTextP, annotRectP+                                       , annotate, Annotation (..)+                                       , theme, ThemeName (..), themeGrid, themeAxisLine, panelBorder+                                       , tickColor+                                       , xAxis, yAxis, hideTicks+                                       , axisBreaksLabeled, axisRotate+                                       , scaleColorManual+                                       , themeLegendFont, fontSize+                                       , alpha+                                       , dagFromListsWithPlates+                                       , DAGNode (..), DAGEdge (..)+                                       , DAGNodeKind (..), DAGLayoutAlgorithm (..) )+import           Graphics.Hgg.Unit     (Pos (..))+import           Graphics.Hgg.Palette  (ggplotHue)+import           Graphics.Hgg.Custom.Dendrogram (DendroSeg (..), DendroPayload (..), dendrogramMark)  -- Phase 48+import           Graphics.Hgg.DAG      (layoutHierarchicalFullWithPlates)+import           Graphics.Hgg.Render.Special (bakeDAGRoutesInSpec)++import           Numeric               (showFFloat)++import           Hanalyze.Data.ColumnSource     (ColumnSource (..))+import           Hanalyze.Model.Formula.Frame   (ModelFrame (..), VarRole (..))+import           Hanalyze.Model.Formula.Design  (designMatrixF)+import           Hanalyze.Fit                   (DesignHBMFit (..))+import           Hanalyze.Model.Wrappers+import           Hanalyze.Plot.Core+import           Hanalyze.Model.LM     (linspace)+import           Hanalyze.Model.GP     (gpNoiseVar)+import           Hanalyze.Model.Weibull (quantileNormal)+import           Hanalyze.Model.PLS    (predictPLS)+import           Hanalyze.Model.Cluster (KMeansResult (..))+import           Hanalyze.Model.HierarchicalCluster+                   (HClusterFit (..), cutTree)+import           Hanalyze.Model.RandomForest (RandomForest (..), featureImportance, rfPermutationImportance, defaultFeatureNames, Tree)+import qualified Hanalyze.Model.RandomForest as RF+import           Hanalyze.Model.GradientBoosting (GBRegressor (..), GBClassifier (..), predictGBR)+import           Hanalyze.Model.PartialDependence+                   (PDPResult, partialDependence, pdpGrid, pdpMean)   -- pdpIce 欄は PD. で参照 (関数名と衝突回避)+import qualified Hanalyze.Model.PartialDependence as PD+import           Hanalyze.Model.RandomForestClassifier (RFClassifierFit (..))+import           Hanalyze.Model.DecisionTree (DTree (..), DTFit (..))+import           Hanalyze.Model.Discriminant (DiscriminantFit (..), predictDiscriminant)+import           Hanalyze.Model.NaiveBayes (NBModel (..), GaussianNB (..)+                                       , MultinomialNB (..), predictNB)+import           Hanalyze.Model.KNN (KNNClassifier (..), predictKNNC)+import           Hanalyze.Model.NeuralNetwork (MLPFit (..), predictMLPClass)+import           Hanalyze.Model.SVM (SVM (..), SVMMulti (..)+                                       , predictSVM, predictSVMMulti, predictSVMScore)+import           Hanalyze.Model.MDS (MDSResult (..))+import           Hanalyze.DataIO.Convert (getTextVec, getDoubleVec)+import qualified DataFrame.Internal.DataFrame  as DXD+import           Hanalyze.Model.PLS (PLSFit (..))+import           Hanalyze.Model.GARCH (GARCHFit (..))+import           Hanalyze.Model.AFT (AFTFit (..), logS, predictAFT)+import           Hanalyze.Model.FDA (FunctionalPCA (..), FLMResult (..))+import           Hanalyze.Model.Regularized (RegFit (..))+import           Hanalyze.Model.LiNGAM.Direct (DirectLiNGAMFit (..))+import           Hanalyze.Model.LiNGAM.Parce (ParceFit (..))+import           Hanalyze.Model.LiNGAM.MultiGroup (MultiGroupFit (..))+import           Hanalyze.Model.LiNGAM.VAR (VARLiNGAMFit (..))+import           Hanalyze.Model.LiNGAM.Pairwise (PairwiseResult (..), PairwiseDirection (..))+import           Hanalyze.Model.LiNGAM.Bootstrap (BootstrapResult (..))+import           Hanalyze.Model.LiNGAM.ICA (ICALiNGAMFit (..))+import           Hanalyze.Stat.CorrelationNetwork (CorrelationGraph (..))+import           Hanalyze.Stat.Test (TestResult (..))+import           Hanalyze.Model.PCA     (PCAResult (..))+import           Hanalyze.Model.Survival (KMResult (..))+import           Hanalyze.Model.CompetingRisks (CRFit (..))+import           Hanalyze.Model.TimeSeries (ARFit (..), forecastAR)++-- ===========================================================================+-- クラスタリング (KMeans) の図 — Phase 68 A1+--+-- KMeans の分野定番の図は「クラスタ別散布 (色=ラベル)」。 ただし+-- 'KMeansResult' は centroids + labels + inertia のみ保持し **生データ座標を+-- 持たない**。 そこで 'surfaceOf' <> 'dataScatter3DOf' と同じ **model 層 / data+-- 層の二層イディオム**に分ける:+--+--   * 'Plottable' 'KMeansResult' の @toPlot@ = centroid 散布のみ (データ不要・+--     クラス契約 @m -> VisualSpec@ を満たす)。 既定は centroid 行列の第 0/1 次元。+--   * 'clusterScatterOf' = データ点をラベル色で散布 (要データ源・列名指定)。+--   * 'centroidsOf' = centroid を任意 2 次元で重畳 (✚ マーカー・次元 index 明示)。+--+-- 定番図 = @df |>> (clusterScatterOf df res \"x\" \"y\" <> centroidsOf res 0 1)@。+-- ⚠ centroid 行列は **学習時の特徴量列順**のみで列名を持たない。 重畳時は+-- データ列 (@xn@, @yn@) と centroid 次元 (@i@, @j@) の対応をユーザが揃える。+-- ===========================================================================++-- | [日本語]: KMeans クラスタの代表図 = centroid 散布 (第 0/1 次元・クラスタ色・✚ マーカー)。+--   生データ点は 'clusterScatterOf' で別 layer に重ねる+--   (cf. 'surfaceOf' (model) <> 'dataScatter3DOf' (data) の二層イディオム)。+--   [English]: The representative KMeans cluster plot = a centroid scatter+--   (dimensions 0\/1, cluster color, cross marker). Raw data points are+--   overlaid as a separate layer via 'clusterScatterOf' (cf. the two-layer+--   idiom of 'surfaceOf' (model) <> 'dataScatter3DOf' (data)).+instance Plottable KMeansResult where+  toPlot res = centroidsOf res 0 1++-- | [日本語]: データ点をラベル色で散布する (= KMeans の定番「クラスタ別散布」)。+--   @d@ は 'ColumnSource' (DataFrame / assoc / Map 等)、 @xn@\/@yn@ は描く列名。+--   色はクラスタラベル ('kmrLabels') の categorical (= 点と同順)。+--   列が無ければ空 ('mempty')。+--   [English]: Scatters the data points colored by cluster label (the+--   classic KMeans "scatter colored by cluster"). @d@ is a 'ColumnSource'+--   (DataFrame \/ assoc \/ Map, etc.), and @xn@\/@yn@ are the column names to+--   plot. Color is the categorical cluster label ('kmrLabels', in the same+--   order as the points). Empty ('mempty') if the columns don't exist.+clusterScatterOf :: ColumnSource d => d -> KMeansResult -> Text -> Text -> VisualSpec+clusterScatterOf d res xn yn =+  case (lookupCol xn d, lookupCol yn d) of+    (Just xs, Just ys) ->+      layer ( scatter (inline xs) (inline ys)+            <> colorBy (inlineCat (map (T.pack . show) (kmrLabels res))) )+    _ -> mempty++-- | [日本語]: centroid を任意 2 次元 (@i@, @j@) で散布 (クラスタ色・✚ マーカーで点と区別)。+--   index が centroid 次元数を超える / 負なら空 ('mempty')。+--   [English]: Scatters the centroids over any two dimensions (@i@, @j@)+--   (cluster color, cross marker to distinguish from the data points).+--   Empty ('mempty') if an index exceeds the centroid dimension count or is+--   negative.+centroidsOf :: KMeansResult -> Int -> Int -> VisualSpec+centroidsOf res i j+  | i < 0 || j < 0 || i >= d || j >= d = mempty+  | otherwise =+      layer ( scatter (inline xs) (inline ys)+            <> colorBy (inlineCat cids)+            <> shape MShCross )+  where+    cs   = kmrCentroids res+    d    = LA.cols cs+    k    = LA.rows cs+    cols = LA.toColumns cs+    xs   = LA.toList (cols !! i)+    ys   = LA.toList (cols !! j)+    cids = map (T.pack . show) [0 .. k - 1 :: Int]++-- | [日本語]: render の categorical 群色 (colorBy → @sort.nub@ 順 → 'ggplotHue') を analyze 側で+--   再現し、 カテゴリ名 → 色(hex) の辞書を返す。 annotation の色は spec 時に確定するため+--   ('clusterScatterOf'/@toPlot@ の凡例色と一致させる用)。+--   [English]: Reproduces render's categorical group color (colorBy →+--   @sort.nub@ order → 'ggplotHue') on the analyze side and returns a+--   category name → color (hex) dictionary. Since annotation colors are+--   fixed at spec time, this keeps them matching 'clusterScatterOf'\/@toPlot@+--   legend colors.+hueColorMap :: [Text] -> Map.Map Text Text+hueColorMap labels =+  let cats = sort (nub labels)+  in Map.fromList (zip cats (ggplotHue (length cats) ++ repeat "#cccccc"))++-- | [日本語]: 色・太さ指定の線分注釈 (@annotLineP@ は色固定なので 'AnnLine' を直接構築)。+--   [English]: A line-segment annotation with a specified color and width+--   (built directly as 'AnnLine' since @annotLineP@ has a fixed color).+annotLineC :: Text -> Double -> (Double, Double) -> (Double, Double) -> VisualSpec+annotLineC col w (x1, y1) (x2, y2) = annotate AnnLine+  { anX1 = PNative x1, anY1 = PNative y1, anX2 = PNative x2, anY2 = PNative y2+  , anColor = col, anWidth = w }++-- | [日本語]: 頂点列を閉じた折れ線 (最後→最初も結ぶ) として色付き線分で描く。+--   [English]: Draws a vertex sequence as a closed polyline (also connecting+--   the last vertex back to the first) using colored line segments.+closedPolyline :: Text -> Double -> [(Double, Double)] -> VisualSpec+closedPolyline _   _ []  = mempty+closedPolyline _   _ [_] = mempty+closedPolyline col w vs  =+  mconcat [ annotLineC col w p q | (p, q) <- zip vs (tail vs ++ [head vs]) ]++-- | [日本語]: 2D 凸包 (Andrew monotone chain)・反時計回り頂点列。 3 点未満は入力そのまま。+--   [English]: 2D convex hull (Andrew's monotone chain); returns a+--   counter-clockwise vertex sequence. Fewer than 3 points are returned as-is.+convexHull :: [(Double, Double)] -> [(Double, Double)]+convexHull ps0 =+  let ps = sort (nub ps0)                 -- lexicographic (x, y)+  in if length ps <= 2 then ps+     else let lower = half ps+              upper = half (reverse ps)+          in init lower ++ init upper      -- 端点重複を除いて連結+  where+    -- 単調鎖: 直近 2 点と p が右回り (cross<=0) の間は pop。 stack は head=最新。+    half = reverse . foldl step []+    step acc p = p : popRight acc p+    popRight (b : a : rest) p+      | cross a b p <= 0 = popRight (a : rest) p+    popRight acc _ = acc+    cross (ox, oy) (ax, ay) (bx, by) = (ax - ox) * (by - oy) - (ay - oy) * (bx - ox)++-- | [日本語]: クラスタ点群をラベルごとにグルーピング (色は 'clusterScatterOf' と一致)。+--   d が xn/yn 列を持たなければ空。+--   [English]: Groups the cluster point cloud by label (colors match+--   'clusterScatterOf'). Empty if @d@ lacks the xn\/yn columns.+clusterGroups+  :: ColumnSource d => d -> KMeansResult -> Text -> Text+  -> [(Text, [(Double, Double)])]         -- (群色 hex, 点列)+clusterGroups d res xn yn =+  case (lookupCol xn d, lookupCol yn d) of+    (Just xs, Just ys) ->+      let labs = kmrLabels res+          cmap = hueColorMap (map (T.pack . show) labs)+          gmap = Map.fromListWith (flip (++))+                   [ (l, [(x, y)]) | (l, x, y) <- zip3 labs xs ys ]+      in [ (Map.findWithDefault "#cccccc" (T.pack (show l)) cmap, ps)+         | (l, ps) <- Map.toList gmap ]+    _ -> []++-- | [日本語]: 各クラスタを __凸包の輪郭線__ で囲む (ggplot @geom_encircle@ 相当・塗りなし)。+--   群色は 'clusterScatterOf' と一致。 定番 = @cdf |>> (clusterScatterOf … \<\> clusterHullOf …)@。+--   ⚠ annotation は軸平行矩形しか塗れないため __輪郭線のみ__ (半透明塗りは将来 @MPolygon@ 移譲)。+--   [English]: Encircles each cluster with its __convex-hull outline__+--   (equivalent to ggplot's @geom_encircle@; unfilled). Group colors match+--   'clusterScatterOf'. Typical usage = @cdf |>> (clusterScatterOf … \<\> clusterHullOf …)@.+--   ⚠ Since annotations can only fill axis-aligned rectangles, this draws+--   __only the outline__ (semi-transparent fill is deferred to a future+--   @MPolygon@ hand-off).+clusterHullOf :: ColumnSource d => d -> KMeansResult -> Text -> Text -> VisualSpec+clusterHullOf d res xn yn =+  mconcat [ closedPolyline col 1.5 (convexHull ps)+          | (col, ps) <- clusterGroups d res xn yn ]++-- | [日本語]: 各クラスタを __95% 共分散楕円__ (χ²(0.95, 2)=5.991) の輪郭で囲む (ggplot @stat_ellipse@+--   相当・正規分布仮定)。 群平均 μ・共分散 Σ を固有分解 ('LA.eigSH') し、 固有軸方向へ+--   半径 √5.991·√λ の楕円点列を折れ線で近似。 群色は 'clusterScatterOf' と一致。+--   点数 3 未満の群は描かない (共分散が定義できないため)。+--   [English]: Encircles each cluster with its __95% covariance ellipse__+--   outline (χ²(0.95, 2) = 5.991; equivalent to ggplot's @stat_ellipse@,+--   assuming normality). Eigendecomposes ('LA.eigSH') the group mean μ and+--   covariance Σ, then approximates the ellipse with a polyline of points at+--   radius √5.991·√λ along the eigenvector axes. Group colors match+--   'clusterScatterOf'. Groups with fewer than 3 points are not drawn+--   (covariance is undefined).+clusterEllipseOf :: ColumnSource d => d -> KMeansResult -> Text -> Text -> VisualSpec+clusterEllipseOf d res xn yn =+  let ellipses = [ (col, ellipse95 ps) | (col, ps) <- clusterGroups d res xn yn ]+      outlines = mconcat [ closedPolyline col 1.5 pts | (col, pts) <- ellipses ]+      allPts   = concatMap snd ellipses+      -- annotation は軸ドメインを駆動しないため、 95% 楕円 (データ点より外へ広がる) が+      -- フレームをはみ出す。 楕円点を alpha=0 の不可視散布で載せ軸を広げる (colorBy 無し=+      -- 凡例に出ない)。 決定境界の coordCartesian と違い、 ここは重畳データ点も含めて+      -- auto-fit させたいので固定でなく anchor 方式。+      anchor+        | null allPts = mempty+        | otherwise   = layer ( scatter (inline (map fst allPts)) (inline (map snd allPts))+                                <> alpha 0 )+  in outlines <> anchor+  where+    seg   = 64 :: Int+    scl   = sqrt 5.991                       -- χ²(0.95, 2)+    ellipse95 ps+      | n < 3     = []+      | otherwise =+          [ ( mux + a * (v1 !! 0) + b * (v2 !! 0)+            , muy + a * (v1 !! 1) + b * (v2 !! 1) )+          | t <- [ 2 * pi * fromIntegral k / fromIntegral seg | k <- [0 .. seg - 1] ]+          , let a = scl * sqrt (max 0 l1) * cos t+                b = scl * sqrt (max 0 l2) * sin t ]+      where+        n   = length ps+        xs  = map fst ps; ys = map snd ps+        mux = sum xs / fromIntegral n+        muy = sum ys / fromIntegral n+        sxx = sum [ (x - mux) ^ (2 :: Int) | x <- xs ] / fromIntegral (n - 1)+        syy = sum [ (y - muy) ^ (2 :: Int) | y <- ys ] / fromIntegral (n - 1)+        sxy = sum [ (x - mux) * (y - muy) | (x, y) <- ps ] / fromIntegral (n - 1)+        sigma = LA.fromLists [[sxx, sxy], [sxy, syy]]+        (vals, vecs) = LA.eigSH (LA.trustSym sigma)   -- λ 降順・列=固有ベクトル+        l1 = vals `LA.atIndex` 0+        l2 = vals `LA.atIndex` 1+        cols = LA.toColumns vecs+        v1 = LA.toList (cols !! 0)+        v2 = LA.toList (cols !! 1)++-- | [日本語]: dendrogram の描画オプション。+--   [English]: Rendering options for the dendrogram.+data DendroOpts = DendroOpts+  { doLineColor      :: !Text            -- ^ [日本語]: 閾値超 (または閾値未指定) の線色。 [English]: Line color above the threshold (or when no threshold is set).+  , doWidth          :: !Double          -- ^ [日本語]: 線幅。 [English]: Line width.+  , doColorThreshold :: !(Maybe Double)  -- ^ [日本語]: @Just t@ で高さ @t@ 未満のサブツリーをクラスタ色分け+                                         --   (scipy @color_threshold@ 流)。 @Nothing@ で単色。+                                         --   [English]: With @Just t@, subtrees below height @t@ are+                                         --   colored by cluster (scipy's @color_threshold@ convention).+                                         --   @Nothing@ renders a single color.+  } deriving (Show)++-- | [日本語]: 既定 = 単色 (grey20 相当・閾値なし)。+--   [English]: The default: a single color (equivalent to grey20, no threshold).+defaultDendroOpts :: DendroOpts+defaultDendroOpts = DendroOpts "#4C4C4C" 1.2 Nothing++instance Plottable HClusterFit where+  toPlot = dendrogramOf++-- | [日本語]: 階層クラスタリング結果を __dendrogram__ で描く (scipy @dendrogram@ / ggdendro 流)。+--   マージ列 ('hcMerges') と高さ ('hcHeights') から U 字リンク (縦 2 + 横 1) を 'AnnLine' で+--   描画。 葉は x 軸に等間隔・各マージノードの x = 子の中点・y = マージ高。 リーフに元サンプル+--   ID ラベル。 plot core は触らず annotation で描く (将来 plot 正式 mark 移譲予定)。+--   [English]: Renders a hierarchical clustering result as a __dendrogram__+--   (following scipy's @dendrogram@ \/ ggdendro convention). Draws U-shaped+--   links (2 vertical + 1 horizontal) via 'AnnLine' from the merge sequence+--   ('hcMerges') and heights ('hcHeights'). Leaves are equally spaced on the+--   x axis; each merge node's x = the midpoint of its children, y = the+--   merge height. Leaves are labeled with the original sample ID. Drawn via+--   annotation without touching the plot core (a future hand-off to a+--   proper plot mark is planned).+dendrogramOf :: HClusterFit -> VisualSpec+dendrogramOf = dendrogramOf' defaultDendroOpts++-- | [日本語]: 色閾値・線色等を指定できる版。+--   [English]: A variant that lets you specify the color threshold, line+--   color, etc.+dendrogramOf' :: DendroOpts -> HClusterFit -> VisualSpec+dendrogramOf' opts fit+  | n <= 1 || null merges = mempty+  | otherwise =+      -- R base / scipy 同様 grid・軸線・枠なし (theme_minimal + grid off)。+      theme ThemeMinimal <> themeGrid False <> themeAxisLine False <> panelBorder False+        <> tickColor "transparent"      -- 目盛マーク (短線) を消す。 数字ラベルは残る。+        -- 葉ラベルは x 軸目盛 (slot 位置・縦書き) で。 軸ラベルは margin を予約するので+        -- リンク根と被らない (annotText と違い R と同挙動)。+        -- ★ axisRotate は CCW 正 (R/matplotlib/ggplot 準拠・hgg Phase 50 A1)。+        --   90 = CCW 90 = 下→上読みで R base / scipy dendrogram の既定向きと一致。+        <> xAxis (axisBreaksLabeled leafTicks <> axisRotate 90)+        <> layer (dendrogramMark payload)  -- ★ Phase 48: U字リンクを custom mark で描く (焼き込み)。+                                           --   encX/encY で軸 range を束ねる (旧 anchor 不要)。+        <> yAxisLine                    -- 軸線は 2 辺一括制御しか無いので y 軸線だけ自前描画。+        <> yLabel "height"              -- y = マージ高 (結合時の非類似度・Ward 増分)。+  where+    n       = hcNumOriginals fit+    merges  = hcMerges fit+    heights = hcHeights fit+    root    = 2 * n - 2                                 -- 最終マージ = 根ノード+    childrenOf node = merges !! (node - n)+    leavesOf node+      | node < n  = [node]+      | otherwise = let (a, b) = childrenOf node in leavesOf a ++ leavesOf b+    order   = leavesOf root                             -- 葉 ID を左→右の並びで+    slotOf  = Map.fromList (zip order [0 :: Int ..])+    -- ノードの x (子の中点)・高さ・代表葉を fold で確定 (子は id が小さく先に入る)。+    (nodeX, nodeH, leafRep) = foldl' step (x0, h0, r0) (zip [0 :: Int ..] merges)+      where+        x0 = Map.fromList [ (l, fromIntegral (slotOf Map.! l)) | l <- [0 .. n - 1] ]+        h0 = Map.fromList [ (l, 0 :: Double) | l <- [0 .. n - 1] ]+        r0 = Map.fromList [ (l, l) | l <- [0 .. n - 1] ]+        step (mx, mh, mr) (i, (a, b)) =+          let node = n + i+          in ( Map.insert node ((mx Map.! a + mx Map.! b) / 2) mx+             , Map.insert node (heights !! i) mh+             , Map.insert node (mr Map.! a) mr )+    maxH    = maximum heights+    -- 葉ラベル = x 軸目盛 (slot 位置に元サンプル ID)。 縦書きは axisRotate 90。+    leafTicks = [ (fromIntegral slot, T.pack (show leaf))+                | (leaf, slot) <- zip order [0 :: Int ..] ]+    -- 色閾値: t 未満マージ数だけ切って各葉のクラスタ ID を得る (hcMerges は高さ昇順)。+    thrInf     = maybe (1 / 0) id (doColorThreshold opts)+    kCut       = n - length (filter (< thrInf) heights)+    clusterIds = cutTree fit kCut+    distinctCs = foldr (\c acc -> if c `elem` acc then acc else acc ++ [c])+                       [] (V.toList clusterIds)         -- 出現順+    cmap       = Map.fromList (zip distinctCs+                   (ggplotHue (length distinctCs) ++ repeat "#999999"))+    linkColor i = case doColorThreshold opts of+      Just t | heights !! i < t ->+        Map.findWithDefault (doLineColor opts)+                            (clusterIds V.! (leafRep Map.! (n + i))) cmap+      _ -> doLineColor opts+    -- U 字リンク (子の高さ→マージ高の縦線 2 本 + マージ高の横線 1 本) を焼き込み線分に。+    -- 座標系は従来の annotLine 版と同一 (x=葉 slot/node 中点、 y=height)。+    payload = DendroPayload+      { dpSegments = concat+          [ [ DendroSeg xa ha  xa hgt col w+            , DendroSeg xa hgt xb hgt col w+            , DendroSeg xb hgt xb hb  col w ]+          | (i, (a, b)) <- zip [0 :: Int ..] merges+          , let xa  = nodeX Map.! a; xb = nodeX Map.! b+                ha  = nodeH Map.! a; hb = nodeH Map.! b+                hgt = heights !! i+                col = linkColor i+                w   = doWidth opts ]+      , dpXRange = (-0.6, fromIntegral n - 0.4)   -- 旧 anchor と同じ range+      , dpYRange = (0, maxH * 1.05)+      }+    -- 左辺 (panel npc x=0) に y 軸線を 1 本 (下辺 x 軸線は出さない = R 流)。+    yAxisLine = annotate AnnLine+      { anX1 = PNpc 0, anY1 = PNpc 0, anX2 = PNpc 0, anY2 = PNpc 1+      , anColor = "#333333", anWidth = 1 }++-- ===========================================================================+-- 時系列予測 (描画可能)+--+-- AR(p) の点予測 'forecastAR' は将来値の中心のみを返す。 予測の不確実性帯は **h-step+-- 予測分散** から得る: AR の MA(∞) 表現の ψ-weights (ψ₀=1, ψⱼ=Σφᵢψⱼ₋ᵢ) を用いて+-- @Var(ŷ_{n+k}) = σ² Σ_{j=0}^{k-1} ψⱼ²@ (σ² = 革新分散 'arResidVar')。 これは Gaussian+-- 革新の下での正統な予測区間 (地平 k とともに単調に広がる)。 対称ゆえ band は+-- @中心 ± z·se@。 @toPlot@ は履歴折れ線 + 予測折れ線 + 予測区間 band を 1 枚に重ねる。+-- ===========================================================================++-- | [日本語]: AR(p) の MA(∞) 表現の ψ-weights ψ₀..ψ_{h-1} (ψ₀=1, ψⱼ=Σ_{i=1}^{min j p} φᵢ ψⱼ₋ᵢ)。+--   [English]: The ψ-weights ψ₀..ψ_{h-1} of the AR(p) model's MA(∞)+--   representation (ψ₀=1, ψⱼ=Σ_{i=1}^{min j p} φᵢ ψⱼ₋ᵢ).+arPsiWeights :: [Double] -> Int -> [Double]+arPsiWeights phi h = go [1.0]+  where+    p = length phi+    go ps+      | length ps >= h = take h ps+      | otherwise =+          let j  = length ps+              pj = sum [ (phi !! (i - 1)) * (ps !! (j - i)) | i <- [1 .. min j p] ]+          in go (ps ++ [pj])++-- | [日本語]: k-step (k=1..h) 予測標準誤差 se_k = sqrt(σ² Σ_{j<k} ψⱼ²)。+--   [English]: The k-step (k=1..h) forecast standard error se_k =+--   sqrt(σ² Σ_{j<k} ψⱼ²).+arForecastSE :: ARFit -> Int -> [Double]+arForecastSE fit h =+  let phi  = LA.toList (arPhi fit)+      s2   = arResidVar fit+      psis = arPsiWeights phi h+  in [ sqrt (s2 * sum (map (^ (2 :: Int)) (take k psis))) | k <- [1 .. h] ]++instance Plottable ForecastModel where+  -- 履歴折れ線 + 予測折れ線 + 予測区間 band (中心 ± 1.96·se)。 x = 時刻 index+  -- (履歴 1..n、 予測 n+1..n+h)。 予測線は履歴末尾点から繋げる。 帯を先・線を後に重ねる。+  toPlot m =+    let fit  = fmFit m+        hist = LA.toList (fmHistory m)+        n    = length hist+        h    = fmHorizon m+        fc   = LA.toList (forecastAR fit (fmHistory m) h)+        se   = arForecastSE fit h+        fx   = [ fromIntegral (n + k) | k <- [1 .. h] ] :: [Double]+        lo   = zipWith (\f s -> f - 1.96 * s) fc se+        hi   = zipWith (\f s -> f + 1.96 * s) fc se+        histX = [ fromIntegral i | i <- [1 .. n] ] :: [Double]+        -- 予測線は履歴末尾 (n, hist[n-1]) から始めて連続させる。+        lineX = fromIntegral n : fx+        lineY = last hist : fc+    in layer (band (inline fx) (inline lo) (inline hi))+         <> layer (line (inline histX) (inline hist))+         <> layer (line (inline lineX) (inline lineY))++-- ===========================================================================+-- 生存解析 (描画可能)+--+-- KM 生存曲線・CIF (競合リスク) はいずれも階段関数。 'stepVerts' (Core) で階段頂点を+-- 明示展開して line で結ぶ。 KM は s0=1 で下降、 CIF は s0=0 で上昇。+-- ===========================================================================++instance Plottable KMResult where+  -- KM 生存曲線 (階段、 S=1 から下降)。+  toPlot km =+    let pts   = zip (kmrTimes km) (kmrSurvival km)+        verts = stepVerts 1.0 pts+    in layer (line (inline (map fst verts)) (inline (map snd verts)))++instance Plottable CRFit where+  -- 競合リスク CIF (cause ごとに 0 から上昇する階段、 色分け重畳)。+  toPlot cr =+    let ts = LA.toList (crfTimes cr)+        mkCause (i, (_cause, cifV)) =+          let pts   = zip ts (LA.toList cifV)+              verts = stepVerts 0.0 pts+              col   = quantilePalette !! (i `mod` length quantilePalette)+          in layer (line (inline (map fst verts)) (inline (map snd verts))+                      <> color (fromHex col))+    in foldMap mkCause (zip [0 ..] (crfCIF cr))++-- ===========================================================================+-- 多変量・木 (描画可能)+--+-- PCA の代表図は **scree plot** (各主成分の寄与率 'pcaExplainedRatio' を棒で)、 木 (RF) の+-- 代表図は **特徴重要度バー** ('featureImportance')。 いずれも自己完結ゆえそのまま+-- 'Plottable'。 棒の x 軸はラベル ("PC1".. / "f1"..) なので 'inlineCat' (categorical) で渡す+-- (heatmap A9 と同じく 'bar' も categorical 軸が必要)。 優先低 (§3.5 A14) ゆえ scree/重要度+-- の 1 枚ずつに絞る (biplot や木構造図は将来拡張)。+-- ===========================================================================++instance Plottable PCAResult where+  -- scree plot: 各主成分 (PC1, PC2, …) の寄与率を棒で。+  toPlot res =+    let ratios = LA.toList (pcaExplainedRatio res)+        labels = [ "PC" <> T.pack (show k) | k <- [1 .. length ratios] ]+    in layer (bar (inlineCat labels) (inline ratios))++instance Plottable RandomForest where+  -- R @varImpPlot@ 流の 2 パネル: 左 = impurity (IncNodePurity)、 右 = permutation+  -- (%IncMSE)。 各パネルは降順ソート + 実列名 + 横棒 ('coordFlip')。+  toPlot rf =+    let n     = V.length (featureImportance rf)+        names = case rfFeatureNames rf of+                  [] -> defaultFeatureNames n+                  ns -> ns+        imp   = V.toList (featureImportance rf)+        perm  = V.toList (rfPermutationImportance rf)+    in subplots+         [ importanceBarNamed "IncNodePurity (impurity)" names imp+         , importanceBarNamed "%IncMSE (permutation)"    names perm ]+       <> subplotCols 2++-- | [日本語]: 名前つき importance を横棒 ('coordFlip') で描く (R @varImpPlot@ 流)。 重要度で+--   ソートするため 'scaleXDiscreteLimits' でカテゴリ順を明示する (bar 軸は既定+--   アルファベット順ゆえデータ並びでは効かない)。 coordFlip 後は limits 順が下→上+--   なので、 昇順 limits を渡して最重要を上端に置く。 タイトル付き。+--   [English]: Draws named importances as a horizontal bar ('coordFlip',+--   following R's @varImpPlot@). Since the bar axis defaults to alphabetical+--   order (unaffected by data order), 'scaleXDiscreteLimits' is used to make+--   the category order explicit for sorting. After @coordFlip@ the limits+--   order runs bottom→top, so ascending limits are passed to place the most+--   important feature at the top. Includes a title.+importanceBarNamed :: T.Text -> [T.Text] -> [Double] -> VisualSpec+importanceBarNamed ttl names vals =+  let ascByVal = map fst (sortBy (comparing snd) (zip names vals))  -- 昇順 → 最大が末尾 = 上端+  in layer (bar (inlineCat names) (inline vals))+       <> scaleXDiscreteLimits ascByVal+       <> coordFlip <> title ttl++-- ===========================================================================+-- 木/アンサンブル — Phase 68 A2+--+-- 各モデルの分野定番図を **既存 mark のみ**で描く (新規 plot mark 不要):+--+--   * GradientBoosting (回帰/分類)・RandomForestClassifier = **特徴重要度 bar**。+--     GBM は重要度フィールドを持たないので弱学習器 ('Tree') の split 使用回数から+--     純粋計算する ('treeImportances'・RF.'featureImportance' と同方式・正規化)。+--   * DecisionTree = **樹形図**。 決定木は DAG の特殊形 (二分木) ゆえ、 HBM の+--     ModelGraph と同じ MDAG (Sugiyama 階層 layout) を **再利用**して node-link で描く+--     (split ノード = "f{j} ≤ {thr}"、 葉 = "y={class}")。+--+-- ⚠ DecisionTree の edge True/False ラベル・gini・サンプル数表示 (sklearn plot_tree+-- 相当) は DAGNode/DAGEdge が持たないため v1 では描かない。 必要なら専用 mark を+-- plot 側 Phase として起こす (= dendrogram Phase 48 と同型の判断)。+-- ===========================================================================++-- | [日本語]: 弱学習器 ('Tree') 列の split 使用回数による特徴重要度 (RF と同方式・合計 1 に正規化)。+--   特徴数は出現した最大 index + 1 (= 木で一度も使われない末尾特徴は現れない)。+--   [English]: Feature importance from the split-usage count of the weak+--   learner ('Tree') sequence (the same method as RF, normalized to sum 1).+--   The feature count is the max observed index + 1 (a trailing feature+--   never used in any tree simply doesn't appear).+treeImportances :: [Tree] -> [Double]+treeImportances trees =+  let counts = foldr walk Map.empty trees+      walk (RF.Leaf _)       m = m+      walk (RF.Node j _ l r) m = walk l (walk r (Map.insertWith (+) j (1 :: Double) m))+      d   = if Map.null counts then 0 else maximum (Map.keys counts) + 1+      raw = [ Map.findWithDefault 0 j counts | j <- [0 .. d - 1] ]+      tot = sum raw+  in if tot <= 0 then raw else map (/ tot) raw++instance Plottable GBRegressor where+  -- 弱学習器の split 使用回数による特徴重要度 bar。+  toPlot gb = importanceBar (treeImportances (gbrTrees gb))++instance Plottable GBClassifier where+  toPlot gb = importanceBar (treeImportances (gbcTrees gb))++instance Plottable RFClassifierFit where+  -- R @varImpPlot@ 流の 2 パネル: 左 = permutation (MeanDecreaseAccuracy)、+  -- 右 = gini 減少 (MeanDecreaseGini・MDI)。 各パネル降順・実列名・横棒。+  toPlot fit =+    let perm  = LA.toList (rfcImportance fit)+        gini  = LA.toList (rfcGiniImportance fit)+        names = case rfcFeatureNames fit of+                  [] -> defaultFeatureNames (length perm)+                  ns -> ns+    in subplots+         [ importanceBarNamed "MeanDecreaseAccuracy" names perm+         , importanceBarNamed "MeanDecreaseGini"     names gini ]+       <> subplotCols 2++instance Plottable DTree where+  -- 決定木 → node-link 樹形図 (MDAG 再利用・Sugiyama 階層 layout)。+  toPlot t =+    let (dnodes, dedges)     = dtreeToDag t+        (positioned, routed) = layoutHierarchicalFullWithPlates dnodes dedges []+    in bakeDAGRoutesInSpec $+         layer (dagFromListsWithPlates positioned routed LayoutHierarchical [])++-- | [日本語]: 学習済み 'DTFit' → __rpart.plot 流__ の樹形図 ('treePlot' と同じ)。 @df |-> decisionTree@+--   の返り値をそのまま @toPlot@ に渡せる。 素の node-link 図は 'DTree' の 'Plottable'。+--   [English]: A trained 'DTFit' → the __rpart.plot style__ tree diagram+--   (same as 'treePlot'). The return value of @df |-> decisionTree@ can be+--   passed straight to @toPlot@. The plain node-link diagram is 'DTree'\'s+--   'Plottable'.+instance Plottable DTFit where+  toPlot = treePlot++-- | [日本語]: 'DTree' を MDAG の node/edge 列へ変換する。 ノード id は根から L/R を辿る経路+--   ("n" / "nL" / "nLR" …) で一意。 split ノードは @NodeOther@、 葉は @NodeObserved@+--   (色で区別)。 左 child = 条件成立 (≤)・右 = 不成立 (>) の慣例で並べる。+--   [English]: Converts a 'DTree' into MDAG node\/edge lists. Node ids are+--   unique paths from the root following L\/R ("n" \/ "nL" \/ "nLR" …). Split+--   nodes are @NodeOther@, leaves are @NodeObserved@ (distinguished by+--   color). By convention, the left child = the condition holds (≤), the+--   right = it doesn't (>).+dtreeToDag :: DTree -> ([DAGNode], [DAGEdge])+dtreeToDag = go "n"+  where+    mkNode nid lbl kind = DAGNode+      { dnId = nid, dnLabel = lbl, dnKind = kind, dnDist = Nothing, dnX = 0, dnY = 0 }+    go nid DLeaf{dlMajority = maj} =+      ( [ mkNode nid ("y=" <> T.pack (show maj)) NodeObserved ], [] )+    go nid DNode{dnFeature = f, dnThr = thr, dnLeft = l, dnRight = r} =+      let self     = mkNode nid ("f" <> T.pack (show f) <> " ≤ " <> fmt2 thr) NodeOther+          lid      = nid <> "L"+          rid      = nid <> "R"+          (ln, le) = go lid l+          (rn, re) = go rid r+          edges    = [ DAGEdge nid lid Nothing Nothing+                     , DAGEdge nid rid Nothing Nothing ]+      in (self : ln ++ rn, edges ++ le ++ re)+    fmt2 x = T.pack (showFFloat (Just 2) x "")++-- ---------------------------------------------------------------------------+-- Phase 75.26: 決定木 樹形図 (rpart.plot 流・annotation ベース)+-- ---------------------------------------------------------------------------++-- | [日本語]: 位置付け済みの決定木ノード (annotation 描画用の中間表現)。 @tpU@ は葉単位の+--   水平座標 (葉 = 0,1,2,…・内部 = 子の中点)、 @tpDepth@ は根からの深さ。+--   [English]: A positioned decision-tree node (an intermediate+--   representation for annotation-based rendering). @tpU@ is the+--   per-leaf horizontal coordinate (leaves = 0,1,2,…; internal nodes = the+--   midpoint of their children), and @tpDepth@ is the depth from the root.+data TPNode = TPNode+  { tpU     :: !Double                -- ^ [日本語]: 葉単位の水平座標。 [English]: The per-leaf horizontal coordinate.+  , tpDepth :: !Int                   -- ^ [日本語]: 根からの深さ (根 = 0)。 [English]: Depth from the root (root = 0).+  , tpMaj   :: !Int                   -- ^ [日本語]: 多数決 (予測) クラス。 [English]: The majority-vote (predicted) class.+  , tpN     :: !Int                   -- ^ [日本語]: ノードのサンプル数。 [English]: The node's sample count.+  , tpProbs :: !(Map.Map Int Double)  -- ^ [日本語]: クラス割合。 [English]: Class proportions.+  , tpSplit :: !(Maybe (Int, Double)) -- ^ [日本語]: 分岐なら (特徴 index, 閾値)。 葉は Nothing。 [English]: @(feature index, threshold)@ for a split node; @Nothing@ for a leaf.+  , tpKids  :: [TPNode]               -- ^ [日本語]: [] = 葉、 [左, 右] = 分岐。 [English]: @[]@ = leaf, @[left, right]@ = split.+  }++-- | [日本語]: 決定木を __rpart.plot 流__ の樹形図で描く (analyze 側 annotation ベース)。+--+-- 各ノードを矩形で表し、 内部に __予測クラス / 全クラス確率 / サンプル割合__ を 3 行で+-- 書く (rpart.plot @type=2@ 既定に相当)。 配線は R と同じく __親→バスの縦線を引かず__、+-- 分割条件 @feat < thr@ を親の少し下の水平バス上に置き、 枝はその両端から出て子の真上で+-- 折れる。 条件の両脇 (__根の分岐のみ__) に枠付き白箱で @yes@ (左=成立)・@no@ (右) を添える。+--+-- 塗り色は rpart.plot @box.palette="auto"@ 準拠で、 クラスごとに ColorBrewer 連番+-- パレット (Reds/Greys/Greens/…) を割当て、 __濃淡で予測クラスの確率 (確信度)__ を表す+-- (淡=低・濃=高)。 暗い塗りには白文字を自動選択。 右上にクラス色の凡例を出す。+--+-- 第 1 = 特徴量名、 第 2 = クラス名 (@printRpart@ と同型・長さ不足は @f{i}@/整数へ+-- フォールバック)。 木レイアウトは葉を左→右へ等間隔・深さ→縦位置で配置し、 座標は+-- panel 正規化 (PNpc) で算術する。 plot core の型は触らず annotation だけで描く+-- (図が固まれば plot 正式 mark へ移譲予定・PS parity は移譲時に対応)。+--+-- ⚠ 文字幅は annotation では実測できないため npc で概算する (@wpc@)。 既定は図幅+-- 〜680px 前提に調律してあり、 極端なサイズでは箱幅/マスク幅が僅かにズレる。+--+-- 高レベル 'treePlot' は 'DTFit' 一つを取り (@df |-> decisionTree@ の返り値をそのまま+-- 渡せる)、 内部に載った特徴量名・クラス名を使う。 名前を手渡ししたい行列 fit 用は+-- 'treePlotRaw'。 'DTFit' は 'Plottable' なので @toPlot@ でも同じ図が出る。+--+-- [English]: Draws a decision tree as an __rpart.plot-style__ tree diagram+-- (annotation-based, on the analyze side).+--+-- Each node is drawn as a rectangle, with+-- __the predicted class \/ all class probabilities \/ sample proportion__+-- written inside on three lines (equivalent to rpart.plot's default+-- @type=2@). Wiring follows R:+-- __no vertical line is drawn from the parent to the bus__;+-- the split condition @feat < thr@ is placed on a horizontal bus just+-- below the parent, and the branches leave from its two ends and bend down+-- directly above each child. Bordered white boxes with @yes@ (left = holds)+-- \/ @no@ (right) flank the condition (root split only).+--+-- Fill colors follow rpart.plot's @box.palette="auto"@: each class is+-- assigned a sequential ColorBrewer palette (Reds\/Greys\/Greens\/…), with+-- __shade encoding the predicted class's probability (confidence)__+-- (light = low, dark = high). Dark fills automatically get white text. A+-- class-color legend is shown in the top right.+--+-- The 1st argument is the feature names, the 2nd is the class names (same+-- form as @printRpart@; falls back to @f{i}@\/integers when too short). The+-- tree layout spaces leaves evenly left→right, maps depth to the vertical+-- position, and computes coordinates in panel-normalized units (PNpc). It+-- is drawn purely via annotation without touching the plot core types (a+-- hand-off to a proper plot mark is planned once the diagram stabilizes;+-- PS parity will be handled at hand-off time).+--+-- ⚠ Since annotation cannot measure text width, it is approximated in npc+-- units (@wpc@). The default is tuned for a figure width of ~680px; box \/+-- mask widths drift slightly at extreme sizes.+--+-- The high-level 'treePlot' takes a single 'DTFit' (the return value of+-- @df |-> decisionTree@ can be passed straight through), using the feature+-- \/ class names carried inside it. For matrix fits where you want to pass+-- names explicitly, use 'treePlotRaw'. Since 'DTFit' is 'Plottable', @toPlot@+-- produces the same diagram.+treePlot :: DTFit -> VisualSpec+treePlot (DTFit tree feats classes) = treePlotRaw feats classes tree++-- | [日本語]: 行列 fit 用の低レベル版 — 特徴量名・クラス名を明示的に渡す (名無しは @f{i}@/整数へ+--   フォールバック)。+--   [English]: The low-level variant for matrix fits — pass feature \/ class+--   names explicitly (falls back to @f{i}@\/integers when unnamed).+treePlotRaw :: [Text] -> [Text] -> DTree -> VisualSpec+treePlotRaw featNames classNames tree =+  theme ThemeVoid+    <> xAxis hideTicks <> yAxis hideTicks       -- 目盛線・目盛ラベルを消す (樹形図は座標軸不要)。+    <> legendLayer                              -- クラス色の凡例 (標準機構・他マークと同じ)。+    <> themeLegendFont (fontSize 11)            -- 凡例文字をノード (class 11pt) に揃える。+    <> mconcat (concatMap edgesOf allNodes)     -- 枝を先に (ノード矩形の下敷き)。+    <> mconcat (concatMap nodeAnns allNodes)+  where+    (nLeaves, root) = assign 0 0 tree+    allNodes        = flatten root+    total           = tpN root+    maxD            = maximum (map tpDepth allNodes)+    classes         = Map.keys (Map.fromList+                        [ (c, ()) | t <- allNodes+                        , c <- tpMaj t : Map.keys (tpProbs t) ])+    nClasses        = length classes+    colorIx         = Map.fromList (zip classes [0 :: Int ..])++    -- ---- 配色: rpart.plot box.palette="auto" 準拠 --------------------------+    --   クラスごとに ColorBrewer 連番パレット (Reds/Greys/Greens/…) を割当て、+    --   塗りの **濃淡で予測クラスの確率 (確信度)** を表す。 R iris 実測と一致:+    --   setosa=Reds・versicolor=Greys・virginica=Greens、 淡=低確率・濃=高確率。+    nodeFill t =+      let pi_  = maybe 0 id (Map.lookup (tpMaj t) colorIx)+          pal9 = ix greysP brewerPals (pi_ `mod` length brewerPals)+          p    = Map.findWithDefault 0 (tpMaj t) (tpProbs t)+      in ix "#cccccc" pal9 (shadeIx p)+    -- 予測確率 p∈[1/K,1] を 9 段 palette の index (概ね 1..5) へ (R 実測に fit)。+    shadeIx p =+      let k = fromIntegral (max 2 nClasses) :: Double+      in max 0 (min 8 (round (1 + (p - 1 / k) / (1 - 1 / k) * 4) :: Int))+    -- 塗りが暗いときは白文字 (簡易輝度判定)。+    textColorFor hex = if luminance hex < 0.5 then "#ffffff" else "#111111"++    -- ---- npc 座標変換 -----------------------------------------------------+    leftM = 0.04; rightM = 0.04; topM = 0.85; botM = 0.16+    spanX = 1 - leftM - rightM+    xNpc u = leftM + (u + 0.5) / fromIntegral nLeaves * spanX+    yNpc d | maxD <= 0 = topM+           | otherwise = topM - fromIntegral d / fromIntegral maxD * (topM - botM)+    colW = spanX / fromIntegral nLeaves+    -- 箱は中身 (最長のクラス名 / 確率行) に合わせて締める (スカスカ回避)。 フォントは+    -- **凡例 (themeLegendFont 11pt) と揃える** (class 11 / 数値 10)。 font を膨らませず+    -- 箱側を締めて詰めて見せる (凡例とノードのサイズを統一)。+    contentW = maximum (0.06 : [ wpc 10 (plineOf t) | t <- allNodes ]+                            ++ [ wpc 11 (classLabel (tpMaj t)) | t <- allNodes ])+    hw   = min (colW * 0.47) (contentW / 2 + 0.016)  -- 矩形半幅。+    hh   = 0.054                      -- 矩形半高。+    dy   = 0.030                      -- 3 行ラベルの行間 (npc)。+    bc   = -0.011                     -- ベースライン補正 (npc・下げて上下中央に見せる)。+    plineOf t = T.intercalate "  "+                  [ fmtP (Map.findWithDefault 0 c (tpProbs t)) | c <- classes ]++    -- ---- ノード矩形 + 3 行ラベル (rpart.plot type=2 相当・上下中央) ---------+    --   1 行目 = 予測クラス、 2 行目 = 全クラス確率 (.34 .30 .35 形式)、+    --   3 行目 = 全体に占めるサンプル割合 (%)。+    nodeAnns t =+      let x    = xNpc (tpU t); y = yNpc (tpDepth t)+          fill = nodeFill t+          tc   = textColorFor fill+          pct  = 100 * fromIntegral (tpN t) / fromIntegral total :: Double+          box  = rectA fill "#404040" 0.7 (x - hw) (y - hh) (x + hw) (y + hh)+          l1   = textC tc x (y + dy + bc) 11 (classLabel (tpMaj t))+          l2   = textC tc x (y      + bc) 10 (plineOf t)+          l3   = textC tc x (y - dy + bc) 10 (fmt0 pct <> "%")+      in [box, l1, l2, l3]++    -- ---- 凡例 (標準機構) --------------------------------------------------+    --   手描き annotation は中央アンカーで文字が揃わないため、 **他マークと同じ+    --   凡例機構**に載せる: 不可視 (alpha 0) の colorBy 散布レイヤを 1 枚足し、+    --   'scaleColorManual' で各クラス名→代表色 (ColorBrewer index 4) を固定する。+    --   凡例スウォッチは layer alpha 非適用ゆえ満色で出る (グリフだけ不可視)。+    reprColor i = ix "#888888" (ix greysP brewerPals (i `mod` length brewerPals)) 4+    legendLayer =+      let cats = [ classLabel c | c <- classes ] :: [Text]+          xs   = [ fromIntegral i | i <- [0 .. nClasses - 1] ] :: [Double]+          dict = [ (classLabel c, reprColor i) | (i, c) <- zip [0 :: Int ..] classes ]+      in layer (scatter (inline xs) (inline xs) <> colorBy (inlineCat cats) <> alpha 0)+           <> scaleColorManual dict++    -- ---- 枝 = rpart.plot type=2 の配線 -----------------------------------+    --   ★親→バスの縦線は引かない (R 準拠)。 分割ラベルを親の少し下に置き、 枝は+    --   ラベル両端から水平に出て子の真上で下へ折れる。 中央 (ラベル/yes-no) 部分は+    --   線を描かないことで枝線をマスクする。 yes/no は **根の分岐のみ**・枠付き白箱。+    edgesOf t = case (tpKids t, tpSplit t) of+      ([l, r], Just (f, thr)) ->+        let px   = xNpc (tpU t); pBot = yNpc (tpDepth t) - hh+            lx   = xNpc (tpU l); rx   = xNpc (tpU r)+            cTop = yNpc (tpDepth l) + hh          -- 子上端 (左右子は同じ深さ)。+            busY = pBot - 0.03                    -- バスは親の少し下 (縦線なし)。+            condTxt = featName f <> " < " <> fmt2 thr+            lw    = wpc 11 condTxt+            isRoot = tpDepth t == 0+            -- 中央の非描画幅 (ラベル + 根なら yes/no 箱ぶん)。+            clr   = lw / 2 + (if isRoot then 0.075 else 0.008)+            branch = [ lineA lx busY (px - clr) busY  -- 左枝 (水平)。+                     , lineA (px + clr) busY rx busY  -- 右枝 (水平)。+                     , lineA lx busY lx cTop          -- 左子へ縦。+                     , lineA rx busY rx cTop ]        -- 右子へ縦。+            cond = textA px (busY - 0.004) 11 condTxt+            yn   = if isRoot+                     then labelBox (px - lw / 2 - 0.03) busY "yes"+                       ++ labelBox (px + lw / 2 + 0.026) busY "no"+                     else []+        in branch ++ cond : yn+      _ -> []++    -- yes/no の枠付き白箱 (中央にテキスト)。+    labelBox cx cy txt =+      let w = wpc 10 txt + 0.014; h = 0.03+      in [ rectA "#ffffff" "#555555" 0.7 (cx - w / 2) (cy - h / 2) (cx + w / 2) (cy + h / 2)+         , textA cx (cy - 0.004) 10 txt ]++    -- ---- annotation プリミティブ (PNpc 固定) ----------------------------+    rectA fill stroke sw x1 y1 x2 y2 = annotate $+      AnnRect (PNpc x1) (PNpc y1) (PNpc x2) (PNpc y2) fill stroke sw 1.0+    textA = textC "#111111"+    textC col x y sz t = annotate $+      AnnText (PNpc x) (PNpc y) t col sz+    lineA x1 y1 x2 y2 = annotate $+      AnnLine (PNpc x1) (PNpc y1) (PNpc x2) (PNpc y2) "#606060" 0.8++    -- 文字列の描画幅を npc で概算 (font px と文字数から線形近似・図幅 ~680px 前提)。+    -- annotation は実測不可ゆえの heuristic。 doc/demo は size を指定して調律に合わせる。+    wpc fs t = 0.00095 * fs * fromIntegral (T.length t)++    -- ---- 名前解決 (@printRpart@ と同じ規則) ------------------------------+    featName i   = pick i featNames  ("f" <> tShowI i)+    classLabel i = pick i classNames (tShowI i)+    pick i xs d  = case drop i xs of+      (nm : _) | not (T.null nm) -> nm+      _                          -> d++    tShowI = T.pack . show :: Int -> Text+    fmt2 x = T.pack (showFFloat (Just 2) x "")+    fmt0 x = T.pack (showFFloat (Just 0) x "")+    -- rpart.plot 流の確率表記 (先頭 0 を落として ".34"、 1.00 は据置き)。+    fmtP x = let s = T.pack (showFFloat (Just 2) x "")+             in maybe s id (T.stripPrefix "0" s)+    ix d xs i = if i >= 0 && i < length xs then xs !! i else d++-- | [日本語]: 'DTree' を葉単位で位置付けした 'TPNode' へ変換する。 葉に左→右で連番 (slot) を+--   振り、 内部ノードは左右子の中点を水平座標にする。 戻りは (葉総数, 根ノード)。+--   [English]: Converts a 'DTree' into a leaf-positioned 'TPNode'. Assigns+--   leaves consecutive left→right numbers (slots), and gives internal nodes+--   the midpoint of their left\/right children as horizontal coordinate.+--   Returns @(total leaf count, root node)@.+assign :: Int -> Int -> DTree -> (Int, TPNode)+assign depth k node = case node of+  DLeaf p m n _ ->+    (k + 1, TPNode (fromIntegral k) depth m n p Nothing [])+  DNode f thr l r n _ p m ->+    let (k1, lp) = assign (depth + 1) k  l+        (k2, rp) = assign (depth + 1) k1 r+        u        = (tpU lp + tpU rp) / 2+    in (k2, TPNode u depth m n p (Just (f, thr)) [lp, rp])++-- | [日本語]: 'TPNode' 木を前順で平坦化する。+--   [English]: Flattens a 'TPNode' tree in pre-order.+flatten :: TPNode -> [TPNode]+flatten t = t : concatMap flatten (tpKids t)++-- | [日本語]: ColorBrewer 9 段連番パレット (rpart.plot box.palette="auto" の per-class 割当)。+--   クラス index 0,1,2,… に Reds, Greys, Greens, Blues, Purples, Oranges を循環割当。+--   [English]: The 9-step sequential ColorBrewer palettes (rpart.plot's+--   @box.palette="auto"@ per-class assignment). Class indices 0,1,2,… cycle+--   through Reds, Greys, Greens, Blues, Purples, Oranges.+brewerPals :: [[Text]]+brewerPals = [redsP, greysP, greensP, bluesP, purplesP, orangesP]++redsP, greysP, greensP, bluesP, purplesP, orangesP :: [Text]+redsP    = ["#fff5f0","#fee0d2","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#a50f15","#67000d"]+greysP   = ["#ffffff","#f0f0f0","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525","#000000"]+greensP  = ["#f7fcf5","#e5f5e0","#c7e9c0","#a1d99b","#74c476","#41ab5d","#238b45","#006d2c","#00441b"]+bluesP   = ["#f7fbff","#deebf7","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#08519c","#08306b"]+purplesP = ["#fcfbfd","#efedf5","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#54278f","#3f007d"]+orangesP = ["#fff5eb","#fee6ce","#fdd0a2","#fdae6b","#fd8d3c","#f16913","#d94801","#a63603","#7f2704"]++-- | [日本語]: @#rrggbb@ の相対輝度 (0..1・Rec.601 加重和)。 塗りの明暗で文字色を切替える用。+--   [English]: The relative luminance of a @#rrggbb@ color (0..1, Rec.601+--   weighted sum). Used to switch text color based on fill lightness.+luminance :: Text -> Double+luminance hex =+  let s = T.dropWhile (== '#') hex+      hx a b = fromIntegral (16 * hv a + hv b) :: Double+      hv c | c >= '0' && c <= '9' = fromEnum c - fromEnum '0'+           | c >= 'a' && c <= 'f' = fromEnum c - fromEnum 'a' + 10+           | c >= 'A' && c <= 'F' = fromEnum c - fromEnum 'A' + 10+           | otherwise            = 0+  in case T.unpack s of+       (r1:r2:g1:g2:b1:b2:_) ->+         (0.299 * hx r1 r2 + 0.587 * hx g1 g2 + 0.114 * hx b1 b2) / 255+       _ -> 1++-- ===========================================================================+-- 分類 (Discriminant / NaiveBayes / KNN) — Phase 68 A3+--+-- 代表図は **決定境界** と **confusion 行列**。 いずれも「学習済モデルを評価点で+-- 走らせる」 図ゆえ、 KMeans (A1) と同じく **データ/範囲を取るヘルパ**で提供する+-- (新規 plot mark 不要):+--+--   * @decisionBoundaryOf@ = 2D grid を予測しクラス色で塗る (= 連続軸の散布を+--     四角マーカー・低 alpha で「領域」表現。 ★renderHeatmap はカテゴリ軸なので+--     連続 grid には不適 → 'MScatter' + 'colorBy' (離散色) を採用)。 2 特徴前提。+--   * 'confusionOf' = テストデータの真値×予測の件数を @MHeatmap@ で (カテゴリ軸が適合)。+--+-- 'Plottable' の @toPlot@ (データ非保持で描ける代表 1 枚):+--   * KNN は訓練データ ('knnCX'/'knnCY') を保持 → **ラベル色の訓練点散布**。+--   * Discriminant / NaiveBayes(Gaussian) は **クラス平均散布** (✚)、+--     NaiveBayes(Multinomial) は **クラス事前確率 bar**。+-- ===========================================================================+++instance ClassPredict DiscriminantFit where+  predictClasses fit m = V.toList (fst (predictDiscriminant fit m))++instance ClassPredict NBModel where+  predictClasses nb m = VU.toList (predictNB nb m)+  classNamesOf (NBGaussian g)    = gnbClassNames g+  classNamesOf (NBMultinomial g) = mnbClassNames g++instance ClassPredict KNNClassifier where+  predictClasses knn m = VU.toList (predictKNNC knn m)+  classNamesOf = knnCClassNames++-- Phase 75.5: 分類 NN も同様に decisionBoundaryOf / confusionOf 対応。+instance ClassPredict MLPFit where+  predictClasses fit m = V.toList (predictMLPClass fit m)+  classNamesOf = mlpClassNames++-- Phase 75.12: カーネル SVM (真の SV) も decisionBoundaryOf (非線形境界) / confusionOf 対応。+instance ClassPredict SVM where+  predictClasses m x = VU.toList (predictSVM m x)++instance ClassPredict SVMMulti where+  predictClasses m x = VU.toList (predictSVMMulti m x)+  classNamesOf = svmmClassNames++-- | [日本語]: 決定境界 (2 特徴) の __領域塗り__ (annotation ベース)。+--+-- @res×res@ の格子セルを中心で予測し、 各セルを予測クラス色の塗り矩形 ('annotRectP')+-- で敷き詰める (sklearn @DecisionBoundaryDisplay@ の pcolormesh 相当)。 点散布でなく+-- __実矩形__ をセル境界ぴったりに敷くので、 旧実装 (半透明の四角散布) の __縞模様__ が出ない。+--+-- クラス色は @toPlot@ の凡例 (@colorBy@ → ggplot @hue_pal()@) と一致させる。 render が+-- categorical を @sort.nub@ 順に並べ 'ggplotHue' を割り当てるのと同順で再現する。+-- 訓練点・クラス平均は呼び出し側で上に重ねる (@decisionBoundaryOf c xr yr res \<\> toPlot c@)。+--+-- ⚠ __annotation の制約__: 塗りは 'annotRectP' 固定の @fill-opacity=0.2@ (薄塗り)。+-- また annotation は layer の __後__ に描かれるため、 塗りは重ねた訓練点の __上__ に来る+-- (0.2 の薄塗りなので点は透けて見える)。 「点が上・塗りが下」 の厳密な重ね順や+-- 半透明でない濃淡は将来 plot 正式 mark (@MTile@/@MRaster@) 移譲時に対応する。+-- クラス色は既定 hue パレット前提 (theme series palette を差し替えた場合、 塗り色は+-- 追従しない — annotation 色は spec 時に確定するため)。+--+-- [English]: __Filled region__ rendering of the decision boundary (2+-- features; annotation-based).+--+-- Predicts at the center of each @res×res@ grid cell and tiles each cell as+-- a fill rectangle ('annotRectP') colored by the predicted class+-- (equivalent to sklearn's @DecisionBoundaryDisplay@ pcolormesh). Since it+-- tiles __actual rectangles__ flush with cell boundaries rather than point+-- scatter, it doesn't show the __striping__ of the old implementation+-- (semi-transparent square scatter).+--+-- Class colors are kept in sync with @toPlot@\'s legend (@colorBy@ → ggplot+-- @hue_pal()@): the same procedure render uses — sorting categoricals in+-- @sort.nub@ order and assigning 'ggplotHue' — is reproduced here. Training+-- points and class means are overlaid by the caller+-- (@decisionBoundaryOf c xr yr res \<\> toPlot c@).+--+-- ⚠ __Annotation constraints__: fills use 'annotRectP'\'s fixed+-- @fill-opacity=0.2@ (light fill). Also, since annotations are drawn+-- __after__ layers, the fill ends up __on top of__ any overlaid training+-- points (points still show through the 0.2 light fill). Strict "points on+-- top, fill below" ordering, or non-transparent shading, is deferred to a+-- future hand-off to a proper plot mark (@MTile@\/@MRaster@). Class colors+-- assume the default hue palette (if the theme's series palette is+-- swapped, the fill colors won't follow — annotation colors are fixed at+-- spec time).+decisionBoundaryOf+  :: ClassPredict c => c -> (Double, Double) -> (Double, Double) -> Int -> VisualSpec+decisionBoundaryOf c (x0, x1) (y0, y1) res+  | res <= 0 || x1 <= x0 || y1 <= y0 = mempty+  | otherwise =+      -- 軸ドメインをグリッド範囲へ正確に固定 (expand=FALSE)。 annotation は軸を駆動しない+      -- ため、 これが無いと軸がデータ点範囲に縮み塗りがフレーム外へはみ出す (sklearn は+      -- 軸 = グリッド範囲)。 範囲外の重畳点は panel に clip される。+      coordCartesian x0 x1 y0 y1 <> mconcat+      [ annotRectP (PNative cx0) (PNative cy0) (PNative cx1) (PNative cy1) (colorFor k)+      | (idx, k) <- zip [0 :: Int ..] preds+      , let (i, j) = idx `divMod` res+            cx0 = x0 + fromIntegral i * dx+            cx1 = cx0 + dx+            cy0 = y0 + fromIntegral j * dy+            cy1 = cy0 + dy ]+  where+    dx = (x1 - x0) / fromIntegral res+    dy = (y1 - y0) / fromIntegral res+    -- セル中心 (行 = i*res + j・列 = [x, y]) をまとめて 1 回でバッチ予測する。+    centers = [ [ x0 + (fromIntegral i + 0.5) * dx, y0 + (fromIntegral j + 0.5) * dy ]+              | i <- [0 .. res - 1], j <- [0 .. res - 1] ]+    preds = predictClasses c (LA.fromLists centers)+    -- クラス色の対応: render は colorBy の categorical を sort.nub 順に並べ ggplotHue を+    -- 割り当てる。 同じ手順を再現し、 予測クラス k → クラス名 → cats 内 index → 色。+    names     = classNamesOf c+    labelOf k = classNameByIx names k+    classK    = if null names then sort (nub preds) else [0 .. length names - 1]+    cmap      = hueColorMap (map labelOf classK)+    colorFor k = Map.findWithDefault "#cccccc" (labelOf k) cmap++-- | [日本語]: confusion 行列のヒートマップ: テストデータ @X@ を予測し、 真値 @yTrue@ との件数を+--   x=予測 / y=真値 のセルに集計する (@MHeatmap@・色 = 件数)。+--   [English]: A confusion-matrix heatmap: predicts on test data @X@ and+--   tallies counts against the true value @yTrue@ into cells with+--   x=predicted \/ y=true (@MHeatmap@; color = count).+-- | [日本語]: クラス番号 k → 名前 (levels があれば @names !! k@・範囲外/空なら整数 show)。+--   分類 toPlot / confusion がクラス名を出す共通ヘルパ。+--   [English]: Class number k → name (@names !! k@ if levels exist;+--   otherwise the integer's @show@). A shared helper used by classification+--   toPlot \/ confusion to render class names.+classNameByIx :: [Text] -> Int -> Text+classNameByIx names k+  | k >= 0 && k < length names = names !! k+  | otherwise                  = T.pack (show k)++confusionOf :: ClassPredict c => c -> LA.Matrix Double -> [Int] -> VisualSpec+confusionOf c x yTrue =+  let yPred   = predictClasses c x+      classes = sort (nub (yTrue ++ yPred))+      -- クラス番号 → クラス名 (levels があれば名前・無ければ整数)。 対角は t==p→同名→+      -- 同 index ゆえ、 名前順が整数順とずれても混同行列は正しい (対角=正解が保たれる)。+      nameOf  = classNameByIx (classNamesOf c)+      counts  = Map.fromListWith (+) [ ((t, p), 1 :: Int) | (t, p) <- zip yTrue yPred ]+      cells   = [ (t, p, Map.findWithDefault 0 (t, p) counts) | t <- classes, p <- classes ]+      xs = [ nameOf p | (_, p, _) <- cells ]+      ys = [ nameOf t | (t, _, _) <- cells ]+      vs = [ fromIntegral nC | (_, _, nC) <- cells ] :: [Double]+      -- セル件数の数値注釈 (sklearn ConfusionMatrixDisplay 同型)。 heatmap の categorical+      -- 軸は label を 'orderedCats' (= sort.nub) の index 位置に置くので、 text は同じ+      -- index 位置 (数値座標) に重ねる (任意クラス数で整合)。 背景 box 付き ('label') ゆえ+      -- viridis のどのセル色 (暗紫〜黄) でも読める。+      axisLabels = sort (nub xs)                       -- x/y 同 classes ゆえ共通・軸順と一致+      idxOf lbl  = maybe 0 fromIntegral (elemIndex lbl axisLabels) :: Double+      txIdx  = [ idxOf (nameOf p) | (_, p, _) <- cells ]+      tyIdx  = [ idxOf (nameOf t) | (t, _, _) <- cells ]+      cntTxt = [ T.pack (show nC) | (_, _, nC) <- cells ]+  in layer (heatmap (inlineCat xs) (inlineCat ys) (inline vs))+       <> layer (label (inline txIdx) (inline tyIdx) (inlineCat cntTxt))+       <> xLabel "predicted" <> yLabel "true"++-- ===========================================================================+-- MDS 埋め込み (モデル型 'MDSResult' + 群色オプション) — Phase 75.21+--+-- 'MDSResult' は @df |-> mds cfg cols@ の結果 (PCAResult 同格のモデル型)。+-- 既定は単色散布 ('Plottable' 'MDSResult' の @toPlot m@)、 群色は元データの列名を+-- 指定する 'mdsGroupBy' を @<>@ で合成する (regression の @statModel <> statColor@ と+-- 同形。 ただし 'statColor' は 'Color' 専用ゆえ「列名で群色」は別オプション)。+--+-- > m = df |-> mds defaultMDS ["x1","x2","x3"]+-- > noDf |>> toPlot m                              -- 単色+-- > noDf |>> toPlot (mdsView m <> mdsGroupBy "species")  -- species で群色+--+-- MDS は反転・回転自由度があるので軸の向きは本質でない (相対配置を見る)。+-- ===========================================================================++-- | [日本語]: MDS 埋め込みの描画オプション束 (Monoid)。 'mdsView' で結果を載せ、+-- 'mdsGroupBy' で群色列を足して @<>@ で合成する。+-- [English]: A bundle of MDS embedding rendering options (Monoid). Load the+-- result via 'mdsView', then compose with @<>@ to add a group-color column+-- via 'mdsGroupBy'.+data MDSView = MDSView+  { mvResult   :: !(Maybe MDSResult)  -- ^ [日本語]: 描く埋め込み (後勝ち)。 [English]: The embedding to draw (last write wins).+  , mvGroupCol :: !(Maybe Text)       -- ^ [日本語]: 群色に使う元データの列名 (後勝ち)。 [English]: The source data column name used for group color (last write wins).+  }++instance Semigroup MDSView where+  a <> b = MDSView (orElse (mvResult b) (mvResult a))+                   (orElse (mvGroupCol b) (mvGroupCol a))+    where orElse (Just x) _ = Just x+          orElse Nothing  y = y++instance Monoid MDSView where+  mempty = MDSView Nothing Nothing++-- | [日本語]: MDS 結果を描画オプションに載せる (@<>@ の起点)。+-- [English]: Loads an MDS result into rendering options (the @<>@ starting+-- point).+mdsView :: MDSResult -> MDSView+mdsView m = mempty { mvResult = Just m }++-- | [日本語]: 元データの列名で群色を付ける (factor/数値どちらでも categorical 色に)。+-- @toPlot (mdsView m <> mdsGroupBy "species")@。+-- [English]: Adds group color from a source-data column name (factor or+-- numeric alike become categorical color). @toPlot (mdsView m <> mdsGroupBy "species")@.+mdsGroupBy :: Text -> MDSView+mdsGroupBy c = mempty { mvGroupCol = Just c }++instance Plottable MDSResult where+  -- 単色の埋め込み散布。+  toPlot m = toPlot (mdsView m)++instance Plottable MDSView where+  toPlot v = case mvResult v of+    Nothing -> mempty+    Just m  ->+      let cols = LA.toColumns (mdsEmbedding m)+          xs   = if not (null cols)  then LA.toList (head cols) else []+          ys   = if length cols >= 2 then LA.toList (cols !! 1) else replicate (length xs) 0+          base = scatter (inline xs) (inline ys)+          withColor = case mvGroupCol v >>= \gc -> groupLabels gc (mdsSourceFrame m) of+            Just labs -> base <> colorBy (inlineCat labs)+            Nothing   -> base+      in layer withColor <> xLabel "MDS1" <> yLabel "MDS2"++-- | [日本語]: 元データの列を categorical な群ラベル ('[Text]') に変換する。 text 列+-- ('getTextVec') を優先し、 無ければ数値列 ('getDoubleVec') を整数寄せで文字列化。+-- [English]: Converts a source-data column into categorical group labels+-- ('[Text]'). Prefers a text column ('getTextVec'); otherwise falls back to+-- a numeric column ('getDoubleVec'), stringified with integer rounding.+groupLabels :: Text -> DXD.DataFrame -> Maybe [Text]+groupLabels gc frame =+  case getTextVec gc frame of+    Just tv -> Just (V.toList tv)+    Nothing -> case getDoubleVec gc frame of+      Just dv -> Just (map numLabel (V.toList dv))+      Nothing -> Nothing+  where+    -- 整数値は小数点を出さない (0.0 → "0")。+    numLabel x = let r = round x :: Int+                 in if fromIntegral r == x then T.pack (show r) else T.pack (show x)++-- | [日本語]: NN 学習損失曲線。 'mlpLossHist' (エポックごとの損失) を epoch (x) 対+-- loss (y) の line で描く。 損失が単調減少して平坦化すれば収束 (keras @history@ 同型)。+-- [English]: The NN training loss curve. Draws 'mlpLossHist' (per-epoch+-- loss) as a line of epoch (x) against loss (y). Monotonic decrease+-- flattening out indicates convergence (equivalent to keras's @history@).+nnLossOf :: MLPFit -> VisualSpec+nnLossOf fit =+  let losses = mlpLossHist fit+      epochs = [ fromIntegral i | i <- [1 .. length losses] ] :: [Double]+  in layer (line (inline epochs) (inline losses))+       <> xLabel "epoch" <> yLabel "loss"++-- | [日本語]: カーネル SVM のサポートベクタ (α>0 の点) を強調散布する。 第 0/1 特徴を+-- __そのクラスの色のまま ✚ (cross) マーカー__ で打つ (通常点 ○ と形で区別・色はクラスで一致)。+-- 決定境界に重ねて「SV が境界を定義する」 様子を見る。 凡例は通常点散布側に任せる+-- ('legendOff')。 SV が無い/1 次元なら空。+-- [English]: Emphasis-scatters the kernel SVM's support vectors (points+-- with α>0). Features 0\/1 are plotted with a+-- __cross marker in the same color as the class__+-- (distinguished from ordinary points' circle shape, with matching class+-- color). Overlaid on the decision boundary to show how+-- SVs define it. The legend is left to the ordinary point scatter+-- ('legendOff'). Empty if there are no SVs or the data is 1-dimensional.+svmSupportVectorsOf :: SVM -> VisualSpec+svmSupportVectorsOf m =+  let cols = LA.toColumns (svmSVx m)+      xs   = if not (null cols)  then LA.toList (head cols) else []+      ys   = if length cols >= 2 then LA.toList (cols !! 1) else []+      -- svmSVy は ±1 (+1 = 正クラス=1・-1 = クラス 0)。 散布の colorBy "cls" と同綴りに+      -- "0"/"1" の categorical 色で合わせる (= 同グループ同色)。+      labs = [ if y > 0 then "1" else "0" | y <- VU.toList (svmSVy m) ] :: [Text]+  in if null xs || null ys then mempty+     else layer ( scatter (inline xs) (inline ys)+                  <> colorBy (inlineCat labs) <> shape MShCross )++-- | [日本語]: 連続な決定スコアを持つ分類器 (decisionLineOf 用)。 score ≥ 0 が片クラス、 < 0 が他。+-- [English]: A classifier with a continuous decision score (for+-- decisionLineOf). @score ≥ 0@ is one class, @< 0@ is the other.+class ScorePredict c where+  decisionScore :: c -> LA.Matrix Double -> [Double]++instance ScorePredict SVM where+  decisionScore m x = VU.toList (predictSVMScore m x)++-- | [日本語]: 決定境界を __線 (等高線)__ で描く。 @decisionBoundaryOf@ が領域を色で+-- 塗り分けるのに対し、 こちらは決定スコア = 0 の等値線を marching squares で引く+-- (sklearn の @contour(…, levels=[0])@ 相当)。 スコアベースなので滑らかな曲線になる。+-- @res@ = grid 解像度 (大きいほど滑らか)。 2 特徴前提。+-- [English]: Draws the decision boundary as a __line (contour)__. Whereas+-- @decisionBoundaryOf@ fills regions with color, this draws the+-- score-equals-zero level curve via marching squares (equivalent to+-- sklearn's @contour(…, levels=[0])@). Being score-based, it produces a+-- smooth curve. @res@ = grid resolution (larger = smoother); assumes 2+-- features.+decisionLineOf :: ScorePredict c+               => c -> (Double, Double) -> (Double, Double) -> Int -> VisualSpec+decisionLineOf c (xlo, xhi) (ylo, yhi) res0 =+  let res = max 2 res0+      ax i = xlo + (xhi - xlo) * fromIntegral i / fromIntegral (res - 1)+      ay j = ylo + (yhi - ylo) * fromIntegral j / fromIntegral (res - 1)+      xsV  = V.generate res ax+      ysV  = V.generate res ay+      grid = LA.fromLists [ [xsV V.! i, ysV V.! j] | j <- [0 .. res - 1], i <- [0 .. res - 1] ]+      zV   = V.fromList (decisionScore c grid)     -- row-major: index = j*res + i+      z i j = zV V.! (j * res + i)+      lvl = 0 :: Double+      straddle a b = (a < lvl) /= (b < lvl)+      interp (px, py) (qx, qy) va vb =+        let t = (lvl - va) / (vb - va) in (px + t * (qx - px), py + t * (qy - py))+      cellSegs i j =+        let p00 = (xsV V.! i, ysV V.! j);       v00 = z i j+            p10 = (xsV V.! (i+1), ysV V.! j);   v10 = z (i+1) j+            p01 = (xsV V.! i, ysV V.! (j+1));   v01 = z i (j+1)+            p11 = (xsV V.! (i+1), ysV V.! (j+1)); v11 = z (i+1) (j+1)+            cross = concat+              [ [ interp p00 p10 v00 v10 | straddle v00 v10 ]+              , [ interp p10 p11 v10 v11 | straddle v10 v11 ]+              , [ interp p01 p11 v01 v11 | straddle v01 v11 ]+              , [ interp p00 p01 v00 v01 | straddle v00 v01 ] ]+        in case cross of+             [a, b]       -> [(a, b)]+             [a, b, d, e] -> [(a, b), (d, e)]   -- saddle (近似ペアリング)+             _            -> []+      segs = concat [ cellSegs i j | i <- [0 .. res - 2], j <- [0 .. res - 2] ]+  in mconcat+       [ layer ( line (inline [x1, x2]) (inline [y1, y2])+                 <> color (fromHex "#333333") )+       | ((x1, y1), (x2, y2)) <- segs ]++++-- ===========================================================================+-- 部分従属図 (PDP / ICE) — Phase 75.27+--+-- 純粋エンジン 'partialDependence' ('Model.PartialDependence') を VisualSpec に落とす+-- 玄関。 回帰モデルは 'RegPredict' instance で短く (@pdpPlot rf trainX 0 "age"@)、 未対応の+-- モデルや分類確率は predict 閉包を直接渡す escape hatch (@partialDependencePlot@) で描く。+-- R @pdp::partial@ / sklearn @PartialDependenceDisplay@ 相当。+-- ===========================================================================++-- | [日本語]: 学習済モデルを評価点行列で走らせ、 各行の __連続予測値__ を返す共通インターフェース+--   (回帰モデルの PDP を種に依らず組むための薄い抽象)。 分類確率など instance の無い+--   ものは 'partialDependencePlot' に predict 閉包を直接渡す。+--   [English]: A common interface that runs a trained model on an+--   evaluation-point matrix and returns a __continuous prediction__ per+--   row (a thin abstraction letting regression-model PDPs be built without+--   caring about model kind). Models lacking an instance — such as+--   classification probabilities — pass a predict closure directly to+--   'partialDependencePlot'.+class RegPredict m where+  predictReg :: m -> LA.Matrix Double -> [Double]++instance RegPredict RandomForest where+  predictReg rf x = map (RF.predictRF rf) (LA.toLists x)++instance RegPredict GBRegressor where+  predictReg gb x = VU.toList (predictGBR gb x)++-- | [日本語]: 高レベル PDP: 訓練 df ('ColumnSource') と __列名__ で部分従属図を描く。+--   @featCols@ = fit に使った特徴列 (順序込み)、 @target@ = 部分従属を見る列。 注目特徴を+--   観測範囲の grid で振り、 他特徴は訓練分布のまま各行予測して平均した曲線を描く+--   (R pdp / sklearn @kind=@average@@ 相当)。 列が引けない / target が featCols に無いときは空図。+--   [English]: The high-level PDP: draws a partial-dependence plot from a+--   training df ('ColumnSource') and __column names__. @featCols@ = the+--   feature columns used to fit (in order), @target@ = the column to view+--   partial dependence for. Sweeps the feature of interest over a grid+--   spanning its observed range, predicts each row with the other features+--   held at their training distribution, and draws the averaged curve+--   (equivalent to R's pdp \/ sklearn's @kind=@average@@). Yields an empty+--   plot if the columns can't be resolved or @target@ isn't in @featCols@.+pdpOf :: (RegPredict m, ColumnSource d) => m -> d -> [Text] -> Text -> VisualSpec+pdpOf model d featCols target =+  case (reqColsM featCols d, elemIndex target featCols) of+    (Right x, Just j) -> partialDependencePlot x (predictReg model) j target+    _                 -> mempty++-- | [日本語]: 高レベル PDP + ICE 重畳 (sklearn @kind=@both@@)。 個体条件付き期待 (ICE) を薄灰で観測数+--   ぶん重ね、 平均 (PDP) を上描きする。 'pdpOf' の ICE 版。+--   [English]: The high-level PDP + ICE overlay (sklearn @kind=@both@@).+--   Overlays individual conditional expectation (ICE) curves in light gray,+--   one per observation, then draws the average (PDP) on top. The ICE+--   variant of 'pdpOf'.+pdpIceOf :: (RegPredict m, ColumnSource d) => m -> d -> [Text] -> Text -> VisualSpec+pdpIceOf model d featCols target =+  case (reqColsM featCols d, elemIndex target featCols) of+    (Right x, Just j) -> partialDependenceIcePlot x (predictReg model) j target+    _                 -> mempty++-- | [日本語]: 低レベル PDP: 訓練特徴 __行列__ と列 index を直接取る ('pdpOf' の実体)。+--   [English]: The low-level PDP: takes the training feature __matrix__ and+--   column index directly (the implementation behind 'pdpOf').+pdpPlot :: RegPredict m => m -> LA.Matrix Double -> Int -> Text -> VisualSpec+pdpPlot m x j name = partialDependencePlot x (predictReg m) j name++-- | [日本語]: 低レベル PDP + ICE (行列・列 index 版)。+--   [English]: The low-level PDP + ICE (matrix and column-index variant).+pdpIcePlot :: RegPredict m => m -> LA.Matrix Double -> Int -> Text -> VisualSpec+pdpIcePlot m x j name = partialDependenceIcePlot x (predictReg m) j name++-- | [日本語]: 任意モデル用 PDP。 predict 閉包 (行列 → 予測値) を直接受ける escape hatch。+--   分類の部分従属 (あるクラスの予測確率) 等、 'RegPredict' instance の無いモデルに使う。+--   [English]: PDP for any model. An escape hatch that takes a predict+--   closure (matrix → predictions) directly. Used for models without a+--   'RegPredict' instance, such as classification partial dependence (a+--   given class's predicted probability).+partialDependencePlot+  :: LA.Matrix Double -> (LA.Matrix Double -> [Double]) -> Int -> Text -> VisualSpec+partialDependencePlot x predict j name =+  let r = partialDependence x predict j 40+  in if null (pdpGrid r)+       then mempty+       else layer ( line (inline (pdpGrid r)) (inline (pdpMean r))+                    <> color (fromHex "#1f77b4") )+            <> xLabel name <> yLabel "partial dependence"++-- | [日本語]: 任意モデル用 PDP+ICE。 'partialDependencePlot' の ICE 重畳版 (predict 閉包版)。+--   [English]: PDP+ICE for any model. The ICE-overlay variant of+--   'partialDependencePlot' (predict-closure version).+partialDependenceIcePlot+  :: LA.Matrix Double -> (LA.Matrix Double -> [Double]) -> Int -> Text -> VisualSpec+partialDependenceIcePlot x predict j name =+  let r  = partialDependence x predict j 40+      g  = pdpGrid r+  in if null g+       then mempty+       else mconcat+              [ layer ( line (inline g) (inline curve)+                        <> color (fromHex "#bbbbbb") <> alpha 0.35 )+              | curve <- PD.pdpIce r ]+            <> layer ( line (inline g) (inline (pdpMean r))+                       <> color (fromHex "#1f77b4") )+            <> xLabel name <> yLabel "partial dependence"++-- ---------------------------------------------------------------------------+-- Phase 76.D: PDP を HBM 抽出子と同型に (Plottable 中間型 + toPlot・<> で合成)+--+-- @pdpOf model d featCols target@ は @VisualSpec@ を直に返すが、 demo は @[] |>> (…)@ の+-- ダミー束ねが要り不格好だった。 HBM の @forestOf@/@epred@ と同じく **Plottable 中間型**+-- ('PDPView') にし、 @toPlot@ で描画・@<>@ で装飾を合成する:+--+-- > noDf |>> (toPlot (pdp rf trainDf featCols target) <> title \"…\")+--+-- ★HBM 抽出子は fit が事後分布を内包し自己完結だが、 RF/GBM は訓練データを保持しないため+--   PDP は訓練 df ('ColumnSource') を受け取る (周辺化に訓練分布が要る)。 予測は 'RegPredict'。+-- ---------------------------------------------------------------------------++data PDPKind = PDPAverage | PDPBoth++-- | [日本語]: PDP の Plottable 中間型。 特徴行列・予測子・注目列 index を捕捉し、+--   @toPlot@ で PDP (平均) / PDP+ICE 曲線に描く。 'pdp' / 'pdpIce' で作る。+--   [English]: The Plottable intermediate type for PDP. Captures the+--   feature matrix, predictor, and the column index of interest, and draws+--   a PDP (average) \/ PDP+ICE curve via @toPlot@. Built with 'pdp' \/+--   'pdpIce'.+data PDPView = PDPView+  { pvX       :: !(LA.Matrix Double)              -- 訓練特徴行列 (周辺化の分布)+  , pvPredict :: LA.Matrix Double -> [Double]     -- モデルの連続予測 (RegPredict 由来)+  , pvJ       :: !Int                             -- 注目特徴の列 index+  , pvName    :: !Text                            -- 注目特徴名 (x 軸)+  , pvKind    :: !PDPKind+  }++-- | [日本語]: 訓練 df + 特徴列から (特徴行列, 注目列 index) を解く。 引けない / target が featCols に+--   無いときは 0×0 行列 (toPlot が 'mempty' にする)。+--   [English]: Resolves @(feature matrix, target column index)@ from a+--   training df and feature columns. Yields a 0×0 matrix when the columns+--   can't be resolved or @target@ isn't in @featCols@ (which @toPlot@ turns+--   into 'mempty').+pdpXJ :: ColumnSource d => d -> [Text] -> Text -> (LA.Matrix Double, Int)+pdpXJ d feats target =+  case (reqColsM feats d, elemIndex target feats) of+    (Right x, Just j) -> (x, j)+    _                 -> (LA.fromLists [], 0)++-- | [日本語]: 平均部分従属 (PDP)。 @noDf |>> (toPlot (pdp model trainDf featCols target) <> …)@。+--   [English]: Average partial dependence (PDP).+--   @noDf |>> (toPlot (pdp model trainDf featCols target) <> …)@.+pdp :: (RegPredict m, ColumnSource d) => m -> d -> [Text] -> Text -> PDPView+pdp model d feats target =+  let (x, j) = pdpXJ d feats target in PDPView x (predictReg model) j target PDPAverage++-- | [日本語]: PDP + ICE 重畳 (sklearn @kind=@both@@)。 個体曲線 (薄灰) + 平均 (青)。+--   [English]: PDP + ICE overlay (sklearn @kind=@both@@). Individual curves+--   (light gray) + average (blue).+pdpIce :: (RegPredict m, ColumnSource d) => m -> d -> [Text] -> Text -> PDPView+pdpIce model d feats target =+  let (x, j) = pdpXJ d feats target in PDPView x (predictReg model) j target PDPBoth++instance Plottable PDPView where+  toPlot (PDPView x predict j name k)+    | LA.rows x == 0 = mempty+    | otherwise = case k of+        PDPAverage -> partialDependencePlot    x predict j name+        PDPBoth    -> partialDependenceIcePlot x predict j name++instance Plottable KNNClassifier where+  -- 訓練データをラベル色で散布 (第 0/1 特徴)。 KNN は X/Y を保持するので data-rich。+  -- 凡例は df|-> が載せた knnCClassNames があればクラス名・無ければ整数 ('classNameByIx')。+  toPlot knn =+    let cols = LA.toColumns (knnCX knn)+        xs   = if not (null cols)      then LA.toList (head cols)   else []+        ys   = if length cols >= 2     then LA.toList (cols !! 1)   else []+        labs = map (classNameByIx (knnCClassNames knn)) (VU.toList (knnCY knn))+    in layer (scatter (inline xs) (inline ys) <> colorBy (inlineCat labs))++instance Plottable DiscriminantFit where+  toPlot fit =+    classMeansScatter (LA.toLists (dfMeans fit))+                      (map round (LA.toList (dfClasses fit)))++instance Plottable NBModel where+  toPlot (NBGaussian m)    =+    classMeansScatterNamed (map LA.toList (gnbMeans m)) (gnbClasses m) (gnbClassNames m)+  toPlot (NBMultinomial m) =+    let labels = [ classNameByIx (mnbClassNames m) cl | cl <- mnbClasses m ]+    in layer (bar (inlineCat labels) (inline (map exp (mnbLogPrior m))))++-- ===========================================================================+-- 次元圧縮 (PLS / MultiGP) — Phase 68 A4+--+-- どちらも結果が自己完結 ('PCAResult' 同様) なので外部データ不要で 'Plottable':+--+--   * 'PLSFit' = 潜在空間の **score plot** (標本 T) を代表図に、 'loading plot' (変数 P)+--     と **VIP bar** を診断図束に。 いずれも既存 'MScatter'/'bar'。+--   * 'MultiGPResult' = **多出力の予測曲線 + 95% band** (出力ごとに色分け・x=index)。+--     'MLine' + 'MBand' を出力数ぶん重畳。+--+-- ※ 'Hanalyze.Model.MultiOutput' は変換+メトリクスの **ユーティリティ**で+-- fit 結果型を持たないため 'Plottable' 対象外 (多出力の「相関」図は既存+-- 'MultiFit' = 残差相関 heatmap が担当)。 新規 plot mark は不要。+-- ===========================================================================+++-- | [日本語]: PLS 診断ビューの種別 (score / loading / VIP)。+--   [English]: The PLS diagnostic view kind (score \/ loading \/ VIP).+data PLSViewKind = ScoreView | LoadingView | VipView+  deriving (Show, Eq)++-- | [日本語]: PLS の中間 Plottable Spec (HBM 式統一)。 終端 @VisualSpec@ を+-- 直返ししていた旧 @plsScorePlot@ 系を、 forest/trace 等と同じく+-- __'Plottable' な中間 Spec__ に揃える (@toPlot@ 境界でオプション合成可・診断束を型で表現)。+-- [English]: The PLS intermediate Plottable Spec (unified with the HBM+-- style). Brings the old @plsScorePlot@ family — which directly returned a+-- terminal @VisualSpec@ — in line with forest\/trace etc. as an+-- __intermediate 'Plottable' Spec__+-- (options can be composed at the @toPlot@ boundary; the diagnostic bundle+-- is expressed as a type).+data PLSView = PLSView !PLSFit !PLSViewKind++-- | [日本語]: score ビュー: 標本を潜在空間の第 1/2 成分 (T[:,0] vs T[:,1]) で散布。+--   [English]: The score view: scatters samples over latent-space+--   components 1\/2 (T[:,0] vs T[:,1]).+scoreView :: PLSFit -> PLSView+scoreView fit = PLSView fit ScoreView++-- | [日本語]: loading ビュー: 変数を潜在空間の第 1/2 成分 (P[:,0] vs P[:,1]) で散布。+--   [English]: The loading view: scatters variables over latent-space+--   components 1\/2 (P[:,0] vs P[:,1]).+loadingView :: PLSFit -> PLSView+loadingView fit = PLSView fit LoadingView++-- | [日本語]: VIP ビュー: 変数重要度 (Variable Importance in Projection) bar。+--   [English]: The VIP view: a Variable Importance in Projection bar chart.+vipView :: PLSFit -> PLSView+vipView fit = PLSView fit VipView++instance Plottable PLSView where+  toPlot (PLSView fit ScoreView) =+    let (xs, ys) = matCols2 (plsScoresT fit) 0 1+    in layer (scatter (inline xs) (inline ys))+         <> xLabel "comp 1" <> yLabel "comp 2"+  toPlot (PLSView fit LoadingView) =+    let (xs, ys) = matCols2 (plsLoadingsP fit) 0 1+    in layer (scatter (inline xs) (inline ys))+         <> xLabel "loading 1" <> yLabel "loading 2"+  toPlot (PLSView fit VipView) =+    let vips   = LA.toList (plsVIP fit)+        labels = [ "f" <> T.pack (show k) | k <- [1 .. length vips] ]+    in layer (bar (inlineCat labels) (inline vips))++instance Plottable PLSFit where+  -- 代表図 = score ビュー (標本の潜在空間布置)。+  toPlot = toPlot . scoreView+  -- 診断束 = score / loading / VIP の 3 枚。+  diagnosticPlots fit = map toPlot [ scoreView fit, loadingView fit, vipView fit ]++-- ===========================================================================+-- 時系列・生存・FDA (GARCH / AFT / FDA) — Phase 68 A5+--+-- 新規 plot mark は不要 (既存 line/band の重畳):+--+--   * 'GARCHFit'      = 系列 (μ + ε_t) + 条件付き volatility 帯 (μ ± 2σ_t) の帯付き線。+--   * 'AFTFit'        = パラメトリック生存曲線 S(t|x)。 fit は観測時刻を持たないので+--                       代表図 (@toPlot@) は **基準共変量** (intercept のみ) の曲線、+--                       任意共変量は 'aftSurvivalAt' ヘルパ。 t 範囲は予測平均寿命から導出。+--   * 'FunctionalPCA' = 平均関数 + 上位固有関数を grid 上に重畳 (x = grid index)。+--   * 'FLMResult'     = 関数回帰係数 β(t) の曲線。+-- ===========================================================================++-- | [日本語]: GARCH の条件付き volatility 帯付き線: 系列 @y_t = μ + ε_t@ の line に、+--   @μ ± 2σ_t@ (σ_t = √σ²_t) の帯を重ねる。 x = 時刻 index。+--   [English]: A banded line of GARCH conditional volatility: overlays a+--   band of @μ ± 2σ_t@ (σ_t = √σ²_t) on the line of series+--   @y_t = μ + ε_t@. x = time index.+garchVolatility :: GARCHFit -> VisualSpec+garchVolatility fit =+  let eps   = LA.toList (gResiduals fit)+      s2    = LA.toList (gSigma2 fit)+      mu    = gMu fit+      n     = min (length eps) (length s2)+      xs    = [ fromIntegral i | i <- [1 .. n] ] :: [Double]+      ys    = [ mu + e | e <- take n eps ]+      sig   = [ sqrt (max 0 v) | v <- take n s2 ]+      lo    = zipWith (\_ s -> mu - 2 * s) xs sig+      hi    = zipWith (\_ s -> mu + 2 * s) xs sig+  in layer (band (inline xs) (inline lo) (inline hi) <> alpha 0.25)+       <> layer (line (inline xs) (inline ys))+       <> xLabel "t" <> yLabel "y"++instance Plottable GARCHFit where+  toPlot = garchVolatility++-- | [日本語]: AFT 生存曲線 S(t|x): 共変量 @x@ の線形予測子 @lp = x·β@ から+--   @z(t) = (log t − lp)/σ@・@S = exp(logS dist z)@ を t-grid 上で評価する。+--   t 範囲は予測平均寿命の @(0.01, 3×mean)@、 grid 120 点。+--   [English]: The AFT survival curve S(t|x): evaluates+--   @z(t) = (log t − lp)/σ@ \/ @S = exp(logS dist z)@ over a t-grid from+--   covariate @x@'s linear predictor @lp = x·β@. The t range is derived+--   from the predicted mean lifetime as @(0.01, 3×mean)@, with a 120-point+--   grid.+aftSurvivalAt :: AFTFit -> [Double] -> VisualSpec+aftSurvivalAt fit x =+  let beta   = LA.toList (aftBeta fit)+      lp     = sum (zipWith (*) x beta)+      sigma  = aftScale fit+      dist   = aftDistribution fit+      meanL  = let v = predictAFT fit (LA.fromLists [x]) in head (LA.toList v)+      tMax   = if meanL > 0 && not (isInfinite meanL) then 3 * meanL else 10+      tMin   = max 1e-3 (tMax / 200)+      ts     = linspace tMin tMax 120+      surv t = exp (logS dist ((log t - lp) / sigma))+      ss     = map surv ts+  in layer (line (inline ts) (inline ss))+       <> xLabel "t" <> yLabel "S(t)"++instance Plottable AFTFit where+  -- 代表図 = 基準共変量 (intercept 列のみ = [1,0,…,0]) の生存曲線。+  toPlot fit =+    let p = LA.size (aftBeta fit)+        xRef = if p <= 0 then [] else 1 : replicate (p - 1) 0+    in aftSurvivalAt fit xRef+++instance Plottable FunctionalPCA where+  -- 平均関数 + 上位 (最大 3) 固有関数を grid 上に重畳。+  toPlot fpca =+    let meanFn = LA.toList (fpcaMeanFn fpca)+        eigs   = LA.toRows (fpcaEigenfn fpca)+        eigNs  = [ ("PC" <> T.pack (show k), LA.toList e)+                 | (k, e) <- zip [1 :: Int ..] (take 3 eigs) ]+    in gridCurves (("mean", meanFn) : eigNs)++instance Plottable FLMResult where+  -- 関数回帰係数 β(t) の曲線 (x = grid index)。+  toPlot flm =+    let betaFn = LA.toList (flmBetaFn flm)+        xs     = [ fromIntegral i | i <- [1 .. length betaFn] ] :: [Double]+    in layer (line (inline xs) (inline betaFn))+         <> xLabel "t" <> yLabel "beta(t)"++-- ===========================================================================+-- 罰則回帰・因果探索 (Regularized / LiNGAM) — Phase 68 A6+--+-- 新規 plot mark は不要:+--+--   * 'RegFit'          = 単一 λ の係数 ('rfBeta') を bar (代表図)。+--   * 'regPathPlot'     = 正則化パス @[(λ, [β_j])]@ ('regularizationPath' 出力) を、+--                         係数ごとに 1 本の line で λ-横軸に重畳 (= LASSO 係数パス図)。+--   * @DirectLiNGAMFit@ = 推定した因果構造を **MDAG** で描く (B 行列 → node/edge、+--                         決定木と同じ MDAG 再利用)。 edge j→i は @|adjacency[i,j]|>0@。+-- ===========================================================================++instance Plottable RegFit where+  -- 係数 bar (b1, b2, … = rfBeta)。 intercept 含む並びをそのまま描く。+  toPlot fit =+    let bs     = LA.toList (rfBeta fit)+        labels = [ "b" <> T.pack (show k) | k <- [0 .. length bs - 1] ]+    in layer (bar (inlineCat labels) (inline bs))++-- | [日本語]: 正則化パス図: @[(λ, [β_j])]@ を係数ごとに 1 本の line で重畳。 横軸は __log₁₀λ__+--   (glmnet の係数パス図と同じ慣例・小 λ=full model が左、 大 λ=sparse が右)。 色=係数 index。+--   λ は正を仮定する (パスの λ グリッドは常に @> 0@)。+--   [English]: The regularization-path plot: overlays @[(λ, [β_j])]@ as one+--   line per coefficient. The x axis is __log₁₀λ__ (following glmnet's+--   coefficient-path convention: small λ = full model on the left, large+--   λ = sparse on the right). Color = coefficient index. Assumes λ is+--   positive (the path's λ grid is always @> 0@).+regPathPlot :: [(Double, [Double])] -> VisualSpec+regPathPlot path+  | null path = mempty+  | otherwise =+      let logLams = map (logBase 10 . fst) path   -- x = log₁₀λ+          rows = map snd path           -- λ ごとの [β_j]+          p    = minimum (map length rows)+          mkCoef j =+            let ys  = [ r !! j | r <- rows ]+                lbl = "b" <> T.pack (show j)+            in layer ( line (inline logLams) (inline ys)+                     <> colorBy (inlineCat (replicate (length logLams) lbl)) )+      in mconcat [ mkCoef j | j <- [0 .. p - 1] ]+           <> xLabel "log10(lambda)" <> yLabel "coef"++-- | [日本語]: 隣接行列 + 変数名から因果 DAG (MDAG) を描く低レベル関数。+--   edge @j→i@ は @|adj[i,j]| > 0@ (= x_i が x_j に依存)。 @names@ が列数と一致しなければ+--   @x0..@ フォールバック。 全 LiNGAM variant の Plottable が共有する。+--   [English]: The low-level function that draws a causal DAG (MDAG) from+--   an adjacency matrix and variable names. Edge @j→i@ means+--   @|adj[i,j]| > 0@ (i.e. x_i depends on x_j). Falls back to @x0..@ if+--   @names@ doesn't match the column count. Shared by every LiNGAM+--   variant's Plottable instance.+lingamDagNamed :: [Text] -> LA.Matrix Double -> VisualSpec+lingamDagNamed rawNames adj =+  let p     = LA.rows adj+      names = if length rawNames == p && p > 0+                then rawNames+                else [ "x" <> T.pack (show j) | j <- [0 .. p - 1] ]+      dnodes = [ DAGNode { dnId = nm, dnLabel = nm, dnKind = NodeObserved+                         , dnDist = Nothing, dnX = 0, dnY = 0 } | nm <- names ]+      dedges = [ DAGEdge (names !! j) (names !! i) Nothing Nothing+               | i <- [0 .. p - 1], j <- [0 .. p - 1]+               , abs (adj `LA.atIndex` (i, j)) > 0 ]+      (positioned, routed) = layoutHierarchicalFullWithPlates dnodes dedges []+  in bakeDAGRoutesInSpec $+       layer (dagFromListsWithPlates positioned routed LayoutHierarchical [])++-- | [日本語]: 推定因果構造 (DirectLiNGAM) を MDAG で描く。 ノード = @x0..x_{p-1}@ (変数名は+--   高レベル @df |-> directLingam@ 経由で付く・'LiNGAMFitted' の Plottable 参照)。+--   [English]: Draws the estimated causal structure (DirectLiNGAM) as an+--   MDAG. Nodes are @x0..x_{p-1}@ (variable names come from the high-level+--   @df |-> directLingam@; see 'LiNGAMFitted'\'s Plottable instance).+lingamDag :: DirectLiNGAMFit -> VisualSpec+lingamDag fit = lingamDagNamed [] (dlAdjacency fit)++instance Plottable DirectLiNGAMFit where+  toPlot = lingamDag++-- | [日本語]: 高レベル @df |-> directLingam cols@ の結果 = __実変数名__ の因果 DAG。+--   [English]: The result of the high-level @df |-> directLingam cols@ =+--   the causal DAG with __actual variable names__.+instance Plottable (LiNGAMFitted DirectLiNGAMFit) where+  toPlot (LiNGAMFitted fit names) = lingamDagNamed names (dlAdjacency fit)++-- | [日本語]: ParceLiNGAM の名前付き DAG (pcAdjacency)。+--   [English]: ParceLiNGAM's named DAG (pcAdjacency).+instance Plottable (LiNGAMFitted ParceFit) where+  toPlot (LiNGAMFitted fit names) = lingamDagNamed names (pcAdjacency fit)++-- | [日本語]: MultiGroupLiNGAM の __共通__ DAG (多数決 mgCommonAdj・名前付き)。+--   [English]: MultiGroupLiNGAM's __common__ DAG (majority-vote+--   mgCommonAdj, named).+instance Plottable (LiNGAMFitted MultiGroupFit) where+  toPlot (LiNGAMFitted fit names) = lingamDagNamed names (mgCommonAdj fit)++-- | [日本語]: VARLiNGAM の __時間ラグ DAG__。 ノード = 各変数の @name[t]@ / @name[t-l]@、+--   辺 = 同時刻 (@B0@: x_j[t]→x_i[t]) + ラグ (@structuralLags[l]@: x_j[t-l]→x_i[t])。+--   @thr@ 未満の係数は辺を出さない。 孤立したラグノード (辺に現れない) は省く。+--   [English]: VARLiNGAM's __time-lag DAG__. Nodes are each variable's+--   @name[t]@ \/ @name[t-l]@; edges are contemporaneous (@B0@:+--   x_j[t]→x_i[t]) plus lagged (@structuralLags[l]@: x_j[t-l]→x_i[t]).+--   Coefficients below @thr@ yield no edge. Isolated lag nodes (not+--   appearing in any edge) are omitted.+varLagDagNamed :: [Text] -> LA.Matrix Double -> [LA.Matrix Double] -> Double -> VisualSpec+varLagDagNamed rawNames b0 lags thr =+  let k    = LA.rows b0+      base = if length rawNames == k && k > 0+               then rawNames else [ "x" <> T.pack (show j) | j <- [0 .. k - 1] ]+      p    = length lags+      nm i 0 = base !! i <> "[t]"+      nm i l = base !! i <> "[t-" <> T.pack (show l) <> "]"+      contempEdges = [ DAGEdge (nm j 0) (nm i 0) Nothing Nothing+                     | i <- [0 .. k - 1], j <- [0 .. k - 1]+                     , abs (b0 `LA.atIndex` (i, j)) > thr ]+      lagEdges = [ DAGEdge (nm j l) (nm i 0) Nothing Nothing+                 | l <- [1 .. p], i <- [0 .. k - 1], j <- [0 .. k - 1]+                 , abs ((lags !! (l - 1)) `LA.atIndex` (i, j)) > thr ]+      dedges = contempEdges ++ lagEdges+      refIds = concatMap (\(DAGEdge a b _ _) -> [a, b]) dedges+      allNodes = [ (i, l) | l <- [0 .. p], i <- [0 .. k - 1] ]+      keep (i, l) = l == 0 || nm i l `elem` refIds        -- 現時刻は常に・ラグは辺があるものだけ+      dnodes = [ DAGNode { dnId = nm i l, dnLabel = nm i l, dnKind = NodeObserved+                         , dnDist = Nothing, dnX = 0, dnY = 0 }+               | (i, l) <- allNodes, keep (i, l) ]+      (positioned, routed) = layoutHierarchicalFullWithPlates dnodes dedges []+  in bakeDAGRoutesInSpec $+       layer (dagFromListsWithPlates positioned routed LayoutHierarchical [])++-- | [日本語]: VARLiNGAM の高レベル結果 = 時間ラグ DAG (辺閾値 0.1・同時刻 + ラグ)。+--   [English]: VARLiNGAM's high-level result = the time-lag DAG (edge+--   threshold 0.1; contemporaneous + lagged).+instance Plottable (LiNGAMFitted VARLiNGAMFit) where+  toPlot (LiNGAMFitted fit names) =+    varLagDagNamed names (vlB0 fit) (vlStructuralLags fit) 0.1++-- | [日本語]: PairwiseLiNGAM の 2 変数向き図。 検出向きの矢印 1 本 (Inconclusive は無向)。+--   2×2 隣接に落として 'lingamDagNamed' を再利用する。+--   [English]: PairwiseLiNGAM's two-variable direction diagram. A single+--   arrow in the detected direction (undirected for Inconclusive). Reduced+--   to a 2×2 adjacency and reuses 'lingamDagNamed'.+instance Plottable (LiNGAMFitted PairwiseResult) where+  toPlot (LiNGAMFitted r names) =+    let adj = case prDirection r of+          XtoY         -> LA.fromLists [[0, 0], [1, 0]]   -- x(0) → y(1): adj[1,0]=1+          YtoX         -> LA.fromLists [[0, 1], [0, 0]]   -- y(1) → x(0)+          Inconclusive -> LA.fromLists [[0, 0], [0, 0]]   -- 無向 (2 ノードのみ)+    in lingamDagNamed names adj++-- | [日本語]: ICA-LiNGAM の名前付き DAG (ilAdjacency)。+--   [English]: ICA-LiNGAM's named DAG (ilAdjacency).+instance Plottable (LiNGAMFitted ICALiNGAMFit) where+  toPlot (LiNGAMFitted fit names) = lingamDagNamed names (ilAdjacency fit)++-- | [日本語]: 相関ネットワークのグラフ。 @|r| > cgThreshold@ の対を辺にする (無向・向きは+--   index 順の便宜配置で __因果でない__ )。 LiNGAM DAG と対比すると間接相関の過剰さが分かる。+--   下三角のみ辺にして重複/自己ループを避ける (相関は対称ゆえ)。+--   [English]: The correlation-network graph. Pairs with @|r| > cgThreshold@+--   become edges (undirected; the index-order layout is a convenience and+--   __is not causal__). Contrasting this with the LiNGAM DAG reveals the+--   excess of indirect correlations. Only the lower triangle is edged, to+--   avoid duplicates \/ self-loops (correlation is symmetric).+instance Plottable CorrelationGraph where+  toPlot (CorrelationGraph corr names thr) =+    let p   = LA.rows corr+        adj = LA.build (p, p)+                (\i j -> let (ii, jj) = (round i, round j)+                         in if ii > jj && abs (corr `LA.atIndex` (ii, jj)) > thr+                              then 1 else 0 :: Double)+    in lingamDagNamed names adj++-- | [日本語]: BootstrapLiNGAM の __確信度 DAG__。 出現確率 ≥ 0.5 のエッジだけ描く+--   (= 過半数の bootstrap で現れた信頼できる因果構造)。 全確率は 'bootstrapEdgeProbOf' で。+--   [English]: BootstrapLiNGAM's __confidence DAG__. Draws only edges with+--   occurrence probability ≥ 0.5 (i.e. causal structure trusted enough to+--   appear in a majority of bootstrap resamples). See 'bootstrapEdgeProbOf'+--   for the full probabilities.+instance Plottable (LiNGAMFitted BootstrapResult) where+  toPlot (LiNGAMFitted res names) =+    let prob = brEdgeProbability res+        p    = LA.rows prob+        adj  = LA.build (p, p)+                 (\i j -> if prob `LA.atIndex` (round i, round j) >= 0.5 then 1 else 0)+    in lingamDagNamed names adj++-- | [日本語]: BootstrapLiNGAM の __エッジ出現確率ヒートマップ__。 行=結果 i・列=原因 j、+--   セル = P(j→i) (0..1)。 確信度の全体像を DAG と別に見せる (python lingam の確率行列相当)。+--   [English]: BootstrapLiNGAM's __edge-occurrence-probability heatmap__.+--   Rows = effect i, columns = cause j; cells = P(j→i) (0..1). Shows the+--   full picture of confidence separately from the DAG (equivalent to+--   python lingam's probability matrix).+bootstrapEdgeProbOf :: LiNGAMFitted BootstrapResult -> VisualSpec+bootstrapEdgeProbOf (LiNGAMFitted res rawNames) =+  let prob  = brEdgeProbability res+      p     = LA.rows prob+      names = if length rawNames == p && p > 0+                then rawNames else [ "x" <> T.pack (show j) | j <- [0 .. p - 1] ]+      cells = [ (names !! j, names !! i, prob `LA.atIndex` (i, j))+              | i <- [0 .. p - 1], j <- [0 .. p - 1] ]+      xs = [ c | (c, _, _) <- cells ]      -- 原因 j (x 軸)+      ys = [ r | (_, r, _) <- cells ]      -- 結果 i (y 軸)+      vs = [ v | (_, _, v) <- cells ]+  in layer (heatmap (inlineCat xs) (inlineCat ys) (inline vs))+       <> xLabel "cause (j)" <> yLabel "effect (i)"++-- ===========================================================================+-- DOE prediction profiler — Phase 78.C/D/F+--+-- JMP の Prediction Profiler 相当 = **応答 × 各因子**のパネルをグリッドに並べる+-- (行=応答・列=因子)。 各パネル = 予測線 + 95% CI 帯 (他因子は中央値固定) + 打点。+-- 打点は 'Raw' (実測 y) か 'Partial' (偏残差 = 部分効果 + 全モデル残差) を @<>@ で選ぶ。+-- 既存 effect plot ('statModelMulti' + 'along' + 'holdAt') を再利用する。+--+-- 中間 Plottable 型 ('ProfilerSpec') にして @toPlot@ で描画・@<>@ でオプション合成+-- (HBM @epred@ / 'PDPView' と同じ流儀)。 打点はモデル ('mvFrame') の観測値から算出+-- するので @noDf@ で束ねられる。 複数応答は @df |-> @multiOutput@ ys (designModel plan)@+-- が返す @[(応答名, モデル)]@ をそのまま渡す。+--+-- > let model = df |-> multiOutput ["strength","yield"] (designModel plan)+-- > noDf |>> toPlot (profiler model ["temp","time"] <> profilerResidual Partial)+-- ===========================================================================++-- | [日本語]: 打点の種別。 'Raw' = 実測 y (他因子が動くぶん予測線から縦に散る = 多変量の正しい挙動)。+--   'Partial' = __偏残差__ @fⱼ(xⱼ) + (全モデル残差)@ で他因子の寄与を除き点を予測線に乗せる+--   (R @termplot(partial.resid=TRUE)@ / @car::crPlots@ 相当)。+--   [English]: The kind of plotted points. 'Raw' = observed y (scatters+--   vertically off the prediction line as other factors vary — the correct+--   multivariate behavior). 'Partial' = removes other factors'+--   contributions via the __partial residual__+--   @fⱼ(xⱼ) + (whole-model residual)@, placing points on the prediction+--   line (equivalent to R's @termplot(partial.resid=TRUE)@ \/+--   @car::crPlots@).+data ResidualMode = Raw | Partial+  deriving (Eq, Show)++-- | [日本語]: prediction profiler の中間 Plottable Spec。 @(応答名, モデル)@ のリスト+--   (複数応答)・因子名・打点モード ('ResidualMode') を捕捉し、 @toPlot@ で「行=応答 ×+--   列=因子」 のグリッドに描く。 'profiler' で作り、 @<> 'profilerResidual' Partial@ で+--   モードを合成する。+--   [English]: The prediction profiler's intermediate Plottable Spec.+--   Captures a list of @(response name, model)@ (multiple responses),+--   factor names, and the plotted-point mode ('ResidualMode'), and draws a+--   "rows = responses × columns = factors" grid via @toPlot@. Built with+--   'profiler'; compose the mode with @<> 'profilerResidual' Partial@.+data ProfilerSpec m = ProfilerSpec+  { psModels   :: [(Text, m)]        -- ^ [日本語]: (応答ラベル, 学習済モデル)。 行になる。 [English]: @(response label, trained model)@; becomes a row.+  , psFactors  :: [Text]             -- ^ [日本語]: 説明因子名。 列になる。 [English]: Explanatory factor names; becomes a column.+  , psResidual :: Maybe ResidualMode -- ^ [日本語]: 打点モード (合成後 'Nothing' は 'Raw' 既定)。 [English]: The plotted-point mode (@Nothing@ after composition defaults to 'Raw').+  }++-- | [日本語]: 右バイアス合成 (option-only 片は models\/factors が空)。 mode は後勝ち。+--   [English]: Right-biased composition (an option-only side has empty+--   models\/factors). @mode@ follows last-write-wins.+instance Semigroup (ProfilerSpec m) where+  a <> b = ProfilerSpec+    { psModels   = psModels a  <> psModels b+    , psFactors  = if null (psFactors b) then psFactors a else psFactors b+    , psResidual = psResidual b <|> psResidual a }++instance Monoid (ProfilerSpec m) where+  mempty = ProfilerSpec [] [] Nothing++-- | [日本語]: @profiler models factors@ — 応答×因子の profiler。 @models@ は+--   @df |-> @multiOutput@ ys (designModel plan)@ が返す @[(応答名, モデル)]@。 既定は 'Raw'。+--   [English]: @profiler models factors@ — a response × factor profiler.+--   @models@ is the @[(response name, model)]@ returned by+--   @df |-> @multiOutput@ ys (designModel plan)@. Defaults to 'Raw'.+profiler :: [(Text, m)] -> [Text] -> ProfilerSpec m+profiler models factors = ProfilerSpec models factors Nothing++-- | [日本語]: 打点モードを差す option (@<>@ で合成)。 @profiler … <> profilerResidual Partial@。+--   [English]: An option that sets the plotted-point mode (composed via+--   @<>@). @profiler … <> profilerResidual Partial@.+profilerResidual :: ResidualMode -> ProfilerSpec m+profilerResidual mode = mempty { psResidual = Just mode }++instance MultiVarModel m => Plottable (ProfilerSpec m) where+  toPlot (ProfilerSpec models factors mMode)+    | null models || null factors = mempty+    | otherwise =+        subplots [ panel lbl m f | (lbl, m) <- models, f <- factors ]+          <> subplotCols (length factors)+    where+      mode = fromMaybe Raw mMode+      -- 1 パネル = 予測線 + CI + 打点 (Raw: 実測 y / Partial: 偏残差)。他因子は中央値固定。+      panel lbl m f =+        let mf    = mvFrame m+            contOf nm = case lookup nm (mfRoles mf) of+              Just (RoleContinuous xs) -> V.toList xs+              _                        -> []+            xsf   = contOf f+            (pts, ylab) = case mode of+              Raw ->+                let ysObs = case [ v | (_, RoleResponse v) <- mfRoles mf ] of+                              (v : _) -> V.toList v+                              []      -> []+                in (ysObs, lbl)+              Partial ->+                let ysObs = case [ v | (_, RoleResponse v) <- mfRoles mf ] of+                              (v : _) -> V.toList v+                              []      -> []+                    (muFull, _) = mvEvalFrame m 0.95 mf+                    resid       = zipWith (-) ysObs muFull+                    -- 部分効果 fⱼ(xⱼ): f=観測値・他因子=中央値固定 (予測線と同じ hold)。+                    ef          = evalFrame mf f Median [] xsf+                    (muPart, _) = mvEvalFrame m 0.95 ef+                in (zipWith (+) muPart resid, "partial: " <> lbl)+        in layer (scatter (inline xsf) (inline pts))+             <> toPlot (statModelMulti m (along f) <> holdAt Median <> grid 60)+             <> xLabel f <> yLabel ylab++-- | [日本語]: RSM __等高線 / 応答曲面__。 2 因子 (v1, v2) を grid で動かし他因子を+--   中央値固定して応答 μ̂ を評価し、 __塗り等値帯 ('contourFilled') + 等高線 ('contour')__ で+--   描く (R @rsm::contour@ / matplotlib @contourf+contour@ 相当・応答面を平面で俯瞰)。+--   3D の応答曲面は 'surfaceOf' (別途 @saveSVG3D@)。 評価はモデル観測範囲なので+--   @noDf |>> contourOf model "temp" "time"@ で描ける。+--   [English]: The RSM __contour \/ response-surface plot__. Sweeps two+--   factors (v1, v2) on a grid, holds other factors at their median, and+--   evaluates the response μ̂, drawing it as a+--   __filled contour band ('contourFilled') + contour lines ('contour')__+--   (equivalent to R's @rsm::contour@ \/ matplotlib's @contourf+contour@; a+--   flat-plane overview of the response surface). The 3D response surface+--   is 'surfaceOf' (separate @saveSVG3D@). Since evaluation uses the+--   model's observed range, it can be drawn simply as+--   @noDf |>> contourOf model "temp" "time"@.+contourOf :: MultiVarModel m => m -> Text -> Text -> VisualSpec+contourOf m v1 v2 =+  let (gxs, gys, grid') = surfaceGrid m v1 v2 (defaultSurfaceOpts { soHoldAt = Median })+      -- grid' !! j !! i = μ̂(gxs!!i, gys!!j)。 (x, y, z) へ平坦化。+      pts = concat (zipWith (\gy row -> zipWith (\gx z -> (gx, gy, z)) gxs row) gys grid')+      xs  = [ x | (x, _, _) <- pts ]+      ys  = [ y | (_, y, _) <- pts ]+      zs  = [ z | (_, _, z) <- pts ]+  in layer (contourFilled (inline xs) (inline ys) (inline zs))+       <> layer (contour (inline xs) (inline ys) (inline zs) <> contourLevels 10)+       <> xLabel v1 <> yLabel v2++-- ===========================================================================+-- 記述統計・検定 (Stat.*) — Phase 68 A7+--+-- 新規 plot mark は不要:+--+--   * 'TestResult'  = 効果量 + 95% CI の **forest** (検定パラメータの区間 + 0 基準線)。+--                     代表図 (@toPlot@) は 1 行 forest、 複数検定は 'testForest'。+--   * 'describeBox' = 生データ列の **box plot** (= describe の分布図・5 数要約を可視化)。+-- ===========================================================================++-- | [日本語]: 検定結果の forest plot: 各検定の 95% CI ('trCI') を区間、 中心を点推定として+--   1 行に並べ、 0 の基準線を引く。 CI を持たない検定は除外する。 行ラベルは+--   'trMethod'。 同種検定を群間で並べるなど+--   __ラベルを区別したい場合は 'testForestLabeled'__ を使う。+--+-- ⚠ 0 基準線は __平均差・効果量__ (null = 0) 向け。 生の平均など null ≠ 0 の量を+-- 混在させると軸ドメインが歪むので、 同一スケールの量だけを 1 枚に並べること。+--+-- [English]: A forest plot of test results: lays out each test's 95% CI+-- ('trCI') as an interval, with the point estimate as the center, one per+-- row, and draws a reference line at 0. Tests without a CI are excluded.+-- Row labels come from 'trMethod'. When you need to __distinguish labels__+-- — e.g. laying out the same test kind across groups — use+-- __'testForestLabeled'__.+--+-- ⚠ The 0 reference line targets __mean differences \/ effect sizes__+-- (null = 0). Mixing in quantities whose null ≠ 0, such as raw means, would+-- distort the axis domain, so only lay out quantities on the same scale in+-- one plot.+testForest :: [TestResult] -> VisualSpec+testForest = testForestLabeled . map (\r -> (trMethod r, r))++-- | [日本語]: ラベル指定版 'testForest' (= 行ラベルを呼び出し側で与える)。 同じ検定種を+--   群ごとに並べる (= 同名衝突を避ける) 用途に使う。+--   [English]: A label-specified variant of 'testForest' (the caller+--   supplies the row labels). Used for cases like laying out the same test+--   kind across groups (avoiding name collisions).+testForestLabeled :: [(Text, TestResult)] -> VisualSpec+testForestLabeled labeled =+  let rows  = [ (nm, lo, hi) | (nm, r) <- labeled, Just (lo, hi) <- [trCI r] ]+      names = [ nm            | (nm, _,  _ ) <- rows ]+      ests  = [ (lo + hi) / 2 | (_,  lo, hi) <- rows ]+      errs  = [ (hi - lo) / 2 | (_,  lo, hi) <- rows ]+  in if null rows+       then mempty+       else layer (forest (inlineCat names) (inline ests) (inline errs) <> forestNull 0)++instance Plottable TestResult where+  -- 代表図 = 単一検定の 1 行 forest (effect/CI)。+  toPlot r = testForest [r]++-- | [日本語]: describe の分布図: 生データ列の box plot (5 数要約を可視化)。+--   [English]: The describe distribution plot: a box plot of a raw data+--   column (visualizes the five-number summary).+describeBox :: [Double] -> VisualSpec+describeBox xs = layer (boxplot (inline xs))++-- ===========================================================================+-- 次元圧縮 (PLS effect plot) — Phase 70.B2/B3+-- ===========================================================================++instance MultiVarModel PLSModel where+  mvFrame = plsmFrame+  -- PLS は閉形式 CI を持たない → band 非提供 (曲線のみ・GAM と同じ honest 方針)。+  mvEvalFrame m _level ef =+    let n      = mfNRows ef+        colOf nm = case lookup nm (mfRoles ef) of+          Just (RoleContinuous v) -> LA.fromList (V.toList v)+          _                       -> LA.fromList (replicate n 0)+        xMat  = LA.fromColumns (map colOf (plsmXNames m))   -- n × p (xNames 順)+        yPred = predictPLS (plsmFit m) xMat                 -- n × q+        ycols = LA.toColumns yPred+        idx   = plsmOutIdx m+        mu    = if idx < length ycols then LA.toList (ycols !! idx)+                                      else replicate n 0+    in (mu, Nothing)++-- Phase 78.G-e: 多変量カーネル回帰 (GP/RFF) を effect plot / profiler / contour で使う+-- (DOE の非 LM 化)。 mvEvalFrame は ef から予測子を 'gprnNames' 順に取り 'gprnPredict'+-- に渡す ('PLSModel' と同型)。 帯 = **事後予測帯** (潜在分散 + 観測 noise σ_n²) で、+-- 分布あり象限 (Gp/GpRff) のみ Just、 mean のみ象限 (Krr/KrrRff) は帯なし。 'gpmvVar' は+-- σ_n² を含まない ('GP.hs' の diagKss=σ_f²) ので noise を足して予測帯にする。+instance MultiVarModel GPRegModelN where+  mvFrame m =+    let n     = LA.size (gprnYraw m)+        roles = ("__gp_resp", RoleResponse (V.fromList (LA.toList (gprnYraw m))))+              : [ (nm, RoleContinuous (V.fromList (LA.toList xv)))+                | (nm, xv) <- zip (gprnNames m) (gprnXraws m) ]+    in ModelFrame { mfRoles = roles, mfNRows = n }+  mvEvalFrame m level ef =+    let n        = mfNRows ef+        colOf nm = case lookup nm (mfRoles ef) of+          Just (RoleContinuous v) -> V.toList v+          _                       -> replicate n 0+        xMat        = LA.fromColumns (map (LA.fromList . colOf) (gprnNames m))  -- n × p+        (mu, mbVar) = gprnPredict m xMat+        z           = quantileNormal (1 - (1 - level) / 2)+        sn2         = max 0 (gpNoiseVar (gprnParams m))+    in case mbVar of+         Just vs -> let sds = map (\v -> sqrt (max 0 v + sn2)) vs+                    in ( mu, Just ( zipWith (\u s -> u - z * s) mu sds+                                  , zipWith (\u s -> u + z * s) mu sds ) )+         Nothing -> (mu, Nothing)++-- | [日本語]: DOE 階層ベイズ fit の effect plot 対応。固定効果 β の事後 draw で+--   評価点の μ を計算し、事後予測帯 (μ の分散 + 観測 noise σ²) を CI slot に載せる。+--   ランダム効果は集団平均で marginalize (profiler = 代表条件の予測)。+--   [English]: Effect-plot support for the DOE hierarchical Bayesian fit.+--   Computes μ at evaluation points from the fixed effect β's posterior+--   draws, and loads the posterior predictive band (variance of μ ++--   observation noise σ²) into the CI slot. Random effects are marginalized+--   over the population average (the profiler predicts a representative+--   condition).+instance MultiVarModel DesignHBMFit where+  mvFrame = dhfFrame+  mvEvalFrame m level ef =+    case designMatrixF (dhfFormula m) ef of+      Left _          -> ([], Nothing)+      Right (xMat, _) ->+        let rows  = map LA.toList (LA.toRows xMat)   -- 評価点 × p+            draws = dhfBetaDraws m                   -- draws × p+            muAt row = [ sum (zipWith (*) row bd) | bd <- draws ]+            perPoint = map muAt rows                 -- 評価点ごとの draw 列+            z     = quantileNormal (1 - (1 - level) / 2)+            s2bar = let ss = dhfSigmaDraws m+                    in if null ss then 0 else sum (map (^ (2::Int)) ss) / fromIntegral (length ss)+            center = map mean0L perPoint+            sds    = map (\ds -> sqrt (varL ds + s2bar)) perPoint+        in ( center+           , Just ( zipWith (\c s -> c - z * s) center sds+                  , zipWith (\c s -> c + z * s) center sds ) )+    where+      mean0L xs = if null xs then 0 else sum xs / fromIntegral (length xs)+      varL   xs = let mu = mean0L xs+                  in if null xs then 0 else sum (map (\x -> (x - mu) ^ (2::Int)) xs) / fromIntegral (length xs)+
+ src/Hanalyze/Plot/Robust.hs view
@@ -0,0 +1,233 @@+-- |+-- Module      : Hanalyze.Plot.Robust+-- Description : hgg 連携層 — ロバスト・分位点回帰族の図化 instance+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- [日本語]: hgg 連携層 — __ロバスト・分位点回帰族__ の図化 instance。+--+-- ⚠ 親 'Hanalyze.Plot' と同じく別パッケージ @hanalyze-plot@ に属し、+-- @cabal build --project-file=cabal.project.plot@ で build される。 共通基盤 (class / ModelSpec / grid 評価核) は+-- 'Hanalyze.Plot.Core' を import して取り込む (orphan instance を許容)。+--+-- 担当する型 (= M 推定ロバスト回帰・分位点回帰):+--   RobustModel / MultiRobustModel / QuantileModel / MultiQuantileModel。+--+-- [English]: hgg integration layer — plotting instances for the+-- __robust and quantile regression family__.+--+-- ⚠ Lives in the same separate package @hanalyze-plot@ as the parent+-- 'Hanalyze.Plot', built via @cabal build --project-file=cabal.project.plot@.+-- It imports the common+-- foundation (class \/ ModelSpec \/ grid evaluation core) from+-- 'Hanalyze.Plot.Core' (permitting orphan instances).+--+-- Types covered (M-estimator robust regression and quantile regression):+--   RobustModel \/ MultiRobustModel \/ QuantileModel \/ MultiQuantileModel.+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleContexts #-}+module Hanalyze.Plot.Robust+  ( robustBand+  ) where++import           Data.List             (sortBy, minimumBy)+import           Data.Ord              (comparing)+import qualified Data.Vector           as V+import qualified Numeric.LinearAlgebra as LA++import           Graphics.Hgg.Spec     ( layer, inline, fromHex+                                       , scatter, line, band+                                       , sizeBy, color )++import           Hanalyze.Model.Wrappers+import           Hanalyze.Plot.Core+import           Hanalyze.Model.LM       (designMatrix)+import           Hanalyze.Model.Robust   (RobustFit (..), robustCovBeta)+import           Hanalyze.Model.Weibull  (quantileNormal)+import           Hanalyze.Model.Quantile (QRFit (..))+import           Hanalyze.Model.Formula.Design  (designMatrixF)++-- ===========================================================================+-- 多変量ロバスト回帰 (effect plot + 係数サマリ) — Phase 70.D+--+-- ロバスト回帰は formula 経路を持たない (単回帰 'RobustModel' のみだった) ので、+-- 'MultiLMModel' と同型の frame-carrying ラッパを新設する。 設計行列は+-- 'additiveFormula' 由来 ('designMatrixF' で @[1, x1,…,xp]@)、 fit は 'fitRobustLM'、+-- CI 帯は M 推定量サンドイッチ共分散 ('robustCovBeta'・statsmodels RLM 一致)。+-- ===========================================================================++instance MultiVarModel MultiRobustModel where+  mvFrame = mrmFrame+  mvEvalFrame m level ef =+    case designMatrixF (mrmFormula m) ef of+      Left _        -> ([], Nothing)+      Right (xe, _) ->+        let fit  = mrmFit m+            beta = rfCoef fit+            cov  = robustCovBeta (rfEstimator fit) (rfScale fit)+                                 (rfResiduals fit) (mrmDesign m)+            z    = quantileNormal ((1 + level) / 2)+            rows = LA.toRows xe+            mu   = [ r `LA.dot` beta | r <- rows ]+            se   = [ sqrt (max 0 (r `LA.dot` (cov LA.#> r))) | r <- rows ]+        in ( mu, Just ( zipWith (\mu' s -> mu' - z * s) mu se+                      , zipWith (\mu' s -> mu' + z * s) mu se ) )++-- ===========================================================================+-- ロバスト回帰 (描画可能)+--+-- 'RobustFit' (Hanalyze.Model.Robust) は M-estimator IRLS の係数 'rfCoef' / fitted+-- 'rfFitted' / 最終重み 'rfWeights' (≤ 1、 外れ値ほど小) を持つ。 代表図 (@toPlot@) は+-- ロバスト直線 + サンドイッチ CI 帯。 「どの点がダウンウェイトされたか」 は+-- 'diagnosticPlots' 側で **点サイズ = IRLS 重み** の散布図に encode して見せる。+-- ===========================================================================++instance Plottable RobustModel where+  -- ロバスト直線 + CI 帯 ('robustBand' = M 推定量サンドイッチ共分散)。 LM と揃え、+  -- 訓練点の ŷ='rfFitted' を x 昇順に結ぶ (= 単回帰なので直線) + 帯を重ねる。+  toPlot m =+    let fit        = rmFit m+        xs         = LA.toList (rmXraw m)+        yhat       = LA.toList (rfFitted fit)+        sorted     = sortBy (comparing fst) (zip xs yhat)+        xsS        = map fst sorted+        yhatS      = map snd sorted+        (los, his) = robustBand m defaultCILevel xsS+    in layer (band (inline xsS) (inline los) (inline his))+         <> layer (line (inline xsS) (inline yhatS))++  -- 診断束: ロバスト直線 + 残差 vs fitted + **重み encode 散布図** (点サイズ = IRLS+  -- 重み、 小さい点 = ダウンウェイトされた外れ値)。 y は ŷ + 残差で復元。+  diagnosticPlots m =+    let fit  = rmFit m+        xs   = LA.toList (rmXraw m)+        yhat = LA.toList (rfFitted fit)+        resd = LA.toList (rfResiduals fit)+        ys   = zipWith (+) yhat resd+        ws   = LA.toList (rfWeights fit)+    in [ toPlot m+       , layer (scatter (inline yhat) (inline resd))+       , layer (scatter (inline xs) (inline ys) <> sizeBy (inline ws))+       ]++-- | [日本語]: ロバスト回帰の CI 帯。 M 推定量 β̂ の漸近共分散 ('robustCovBeta'・サンドイッチ・+--   statsmodels RLM 一致) から、 評価点 x での @se(ŷ) = √([1,x]·Cov·[1,x]ᵀ)@、+--   帯 = @μ̂ ∓ z·se@ (z = 正規分位点・RLM は正規で Wald CI)。+--   [English]: The robust regression CI band. From the M-estimator β̂'s+--   asymptotic covariance ('robustCovBeta'; sandwich estimator, matching+--   statsmodels RLM), compute @se(ŷ) = √([1,x]·Cov·[1,x]ᵀ)@ at evaluation+--   point x; the band is @μ̂ ∓ z·se@ (z = the normal quantile; RLM uses the+--   normal distribution for its Wald CI).+robustBand :: RobustModel -> Double -> [Double] -> ([Double], [Double])+robustBand m level gxs =+  let fit  = rmFit m+      xd   = designMatrix (V.fromList (LA.toList (rmXraw m)))   -- [1, x]+      cov  = robustCovBeta (rfEstimator fit) (rfScale fit) (rfResiduals fit) xd+      z    = quantileNormal ((1 + level) / 2)+      beta = rfCoef fit+      b0   = LA.atIndex beta 0+      b1   = if LA.size beta > 1 then LA.atIndex beta 1 else 0+      muAt gx = b0 + b1 * gx+      seAt gx = let v = LA.fromList [1, gx]+                in sqrt (max 0 (v `LA.dot` (cov LA.#> v)))+  in ( [ muAt gx - z * seAt gx | gx <- gxs ]+     , [ muAt gx + z * seAt gx | gx <- gxs ] )++-- | [日本語]: grid 評価。 grid x で β̂·[1, x] を評価しロバスト直線を滑らかに描く。+--   band は 'robustBand' (サンドイッチ CI) を返す (LM と揃えた)。+--   [English]: Grid evaluation. Evaluates β̂·[1, x] at grid x to draw a+--   smooth robust regression line. The band comes from 'robustBand'+--   (sandwich CI), consistent with LM.+instance SingleVarModel RobustModel where+  svRange m = let xs = LA.toList (rmXraw m) in (minimum xs, maximum xs)+  svGrid m level gxs =+    let beta = rfCoef (rmFit m)+        mu   = [ LA.atIndex beta 0+                 + (if LA.size beta > 1 then LA.atIndex beta 1 * gx else 0)+               | gx <- gxs ]+        (los, his) = robustBand m level gxs+    in (mu, Just (los, his))+  -- ブートストラップ: 加法誤差 (残差再標本化)。 refit は同じ estimator で再 fit。+  svBootKit m =+    let fit = rmFit m+    in Just BootKit+       { bkX = LA.toList (rmXraw m)+       , bkY = zipWith (+) (LA.toList (rfFitted fit)) (LA.toList (rfResiduals fit))+       , bkRefit = \xs ys -> robustModel (rfEstimator fit) (LA.fromList xs) (LA.fromList ys)+       , bkObsDist = Nothing }++-- ===========================================================================+-- 分位点回帰 (描画可能)+--+-- 'QRFit' (Hanalyze.Model.Quantile) は 1 つの分位 τ に対する係数 + fitted 'qfYHat' を+-- 持つ。 複数の τ (例 0.1/0.5/0.9) の fit を重ねると **予測区間そのものを線群で** 表現+-- できる (= heteroscedastic データで帯より直接的)。 各線は 'color' ('fromHex') で固定色。+-- ===========================================================================++instance Plottable QuantileModel where+  -- 各 τ-fit を x 昇順に結んだ折れ線を、 固定色で重畳 (分位ごとに 1 layer)。+  toPlot m =+    let xs = LA.toList (qmXraw m)+        mkLine (i, (_tau, fit)) =+          let yhat   = LA.toList (qfYHat fit)+              sorted = sortBy (comparing fst) (zip xs yhat)+              col    = quantilePalette !! (i `mod` length quantilePalette)+          in layer (line (inline (map fst sorted)) (inline (map snd sorted))+                      <> color (fromHex col))+    in foldMap mkLine (zip [0 ..] (qmFits m))++-- | [日本語]: 多変量分位点回帰の代表図 = __第 1 予測子に沿った effect plot__ (他予測子は+--   訓練平均に固定)。 各 τ を 1 本の線で色分け重畳する (単変量 'QuantileModel' の τ 別+--   線群の一般化)。 分位点回帰は閉形式 CI を持たないため帯はなし。+--   [English]: The representative plot for multivariate quantile regression+--   = __an effect plot along the first predictor__ (other predictors held+--   fixed at their training mean). Each τ is overlaid as a separate+--   color-coded line (a generalization of the single-variable+--   'QuantileModel''s per-τ line group). Quantile regression has no+--   closed-form CI, so there is no band.+instance Plottable MultiQuantileModel where+  toPlot m =+    case LA.toColumns (mqmX m) of                    -- [1, x₁, …, xₚ]+      (_ : x1 : rest) ->+        let xs1   = LA.toList x1+            means = [ LA.sumElements c / fromIntegral (max 1 (LA.size c)) | c <- rest ]  -- x₂..xₚ の平均+            (lo, hi) = (minimum xs1, maximum xs1)+            gn    = 100 :: Int+            grid' = [ lo + (hi - lo) * fromIntegral i / fromIntegral (gn - 1) | i <- [0 .. gn - 1] ]+            evalX = LA.fromRows [ LA.fromList (1 : gx : means) | gx <- grid' ]+            mkLine (i, (_t, fit)) =+              let yhat = LA.toList (evalX LA.#> qfBeta fit)+                  col  = quantilePalette !! (i `mod` length quantilePalette)+              in layer (line (inline grid') (inline yhat) <> color (fromHex col))+        in foldMap mkLine (zip [0 :: Int ..] (mqmFits m))+      _ -> mempty   -- 予測子が無い (設計行列が intercept のみ) = 描画不能++-- ===========================================================================+-- 実測 vs 予測 (HasObsPred) — Phase 72.4+--+-- ロバスト/分位点 fit は ŷ と残差を直接持つので 実測 = ŷ + residual。 分位点回帰は+-- 0.5 (中央値) に最も近い τ の fit を代表予測に使う (中央値回帰 = 条件付き中央値)。+-- ===========================================================================++instance HasObsPred RobustModel where+  obsPredPairs m =+    let f = LA.toList (rfFitted (rmFit m))+        e = LA.toList (rfResiduals (rmFit m))+    in (zipWith (+) f e, f)++instance HasObsPred MultiRobustModel where+  obsPredPairs m =+    let f = LA.toList (rfFitted (mrmFit m))+        e = LA.toList (rfResiduals (mrmFit m))+    in (zipWith (+) f e, f)++instance HasObsPred QuantileModel where+  obsPredPairs m =+    case qmFits m of+      [] -> ([], [])+      fs ->+        let (_, fit) = minimumBy (comparing (\(t, _) -> abs (t - 0.5))) fs+            f = LA.toList (qfYHat fit)+            e = LA.toList (qfResid fit)+        in (zipWith (+) f e, f)
+ src/Hanalyze/Plot/Smooth.hs view
@@ -0,0 +1,371 @@+-- |+-- Module      : Hanalyze.Plot.Smooth+-- Description : hgg 連携層 — 平滑化・カーネル法族の図化 instance+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- [日本語]: hgg 連携層 — __平滑化・カーネル法族__ の図化 instance。+--+-- ⚠ 親 'Hanalyze.Plot' と同じく別パッケージ @hanalyze-plot@ に属し、+-- @cabal build --project-file=cabal.project.plot@ で build される。 共通基盤 (class / ModelSpec / grid 評価核) は+-- 'Hanalyze.Plot.Core' を import して取り込む (orphan instance を許容)。+--+-- 担当する型 (= spline / GAM / GP / kernel 法):+--   SplineModel / GAMModel / GAMModelN / GPResult / GPRegModel / GPRegModelN /+--   MultiGPResult。+--+-- [English]: hgg integration layer — plotting instances for the+-- __smoothing/kernel-method family__.+--+-- ⚠ Lives in the same separate package @hanalyze-plot@ as the parent+-- 'Hanalyze.Plot', built via @cabal build --project-file=cabal.project.plot@.+-- The shared foundation+-- (class \/ ModelSpec \/ grid evaluation core) is pulled in by importing+-- 'Hanalyze.Plot.Core' (allowing orphan instances).+--+-- Types covered (= spline \/ GAM \/ GP \/ kernel methods):+--   SplineModel, GAMModel, GAMModelN, GPResult, GPRegModel, GPRegModelN,+--   MultiGPResult.+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleContexts #-}+module Hanalyze.Plot.Smooth+  ( splineBasisAt+  , gamGridCI+  , multiGpCurves+  ) where++import           Data.List             (sortBy)+import           Data.Ord              (comparing)+import qualified Data.Vector           as V+import qualified Numeric.LinearAlgebra as LA++import           Data.Text             (Text)+import qualified Data.Text             as T++import           Graphics.Hgg.Spec     ( VisualSpec, layer, inline, inlineCat+                                       , scatter, line, band+                                       , colorBy, alpha )++import           Hanalyze.Model.Wrappers+import           Hanalyze.Plot.Core+import           Hanalyze.Model.Core     (fittedV, residualsV)+import           Hanalyze.Model.GP       (GPResult (..), gpNoiseVar)+import           Hanalyze.Model.LM       ( CIBand (..), confidenceBand, confidenceBandAt+                                                , predictionBandAt )+import           Hanalyze.Model.Spline   ( SplineKind (..), SplineFit (..)+                                                , bsplineBasis, naturalSplineBasis )+import           Hanalyze.Model.GAM      (GAMFit (..), predictGAMSE)+import           Hanalyze.Model.Weibull  (quantileNormal)+import           Hanalyze.Model.MultiGP  (MultiGPResult (..))+import qualified Statistics.Distribution         as SD+import           Statistics.Distribution.StudentT (studentT)++-- ===========================================================================+-- ガウス過程 (描画可能)+--+-- 'GPResult' (Hanalyze.Model.GP) は予測 grid (gpTestX) + 事後平均 (gpMean) ++-- credible band (gpLower/gpUpper) を **自己完結** で保持する。 ゆえに LMModel の+-- ように X を別途束ねる必要がなく、 結果型をそのまま 'Plottable' にできる。+-- ===========================================================================++instance Plottable GPResult where+  -- 事後平均 (曲線) + credible band。 予測 grid をソートして 'line' に渡せば GP の曲線が+  -- そのまま描ける。 band 半幅は対称 (mean ± 2σ) ゆえ es = gpUpper − gpMean とし、+  -- 'band' に [mean−es, mean+es] を、 'line' に mean を載せる。+  toPlot res =+    let triples = sortBy (comparing (\(x, _, _) -> x))+                    (zip3 (gpTestX res) (gpMean res)+                          (zipWith (-) (gpUpper res) (gpMean res)))+        xs = [ x | (x, _, _) <- triples ]+        ys = [ y | (_, y, _) <- triples ]+        es = [ e | (_, _, e) <- triples ]+    in layer (band (inline xs) (inline (zipWith (-) ys es)) (inline (zipWith (+) ys es)))+         <> layer (line (inline xs) (inline ys))++-- ===========================================================================+-- スプライン回帰 (描画可能)+--+-- 'SplineFit' (Hanalyze.Model.Spline) は基底係数 'sfBeta' と、 基底行列で fit した+-- 線形モデル核 'sfResult' (= 'FitResult') を保持する。 ゆえに **基底行列を設計行列と+-- みなせば** LMModel と同じ @confidenceBand@ (= X (XᵀX)⁻¹ Xᵀ の対角) がそのまま使える。+-- 違いは「曲線」 である点だけ: 単回帰の直線でなく、 訓練点を x 昇順に結ぶと基底展開に+-- よる平滑曲線になる。 帯は LM と同じ **線形モデルの対称 Wald CI** (基底空間での予測分散)。+-- ===========================================================================++-- | [日本語]: 'SplineFit' を訓練 x で評価したときの基底行列 (= confidenceBand の設計行列)。+--   [English]: The basis matrix obtained by evaluating a 'SplineFit' at the+--   training x values (= the design matrix for confidenceBand).+splineBasisAt :: SplineFit -> LA.Vector Double -> LA.Matrix Double+splineBasisAt fit xs =+  let xsV = V.fromList (LA.toList xs)+  in case sfKind fit of+       BSpline k    -> bsplineBasis k (sfKnots fit) xsV+       NaturalCubic -> naturalSplineBasis (sfKnots fit) xsV++instance Plottable SplineModel where+  -- 平滑曲線 + CI band。 基底行列を設計行列とみなして @confidenceBand@ を訓練点で+  -- 評価し (LMModel と同じ Wald CI)、 x 昇順にソートして折れ線で結ぶ (= 平滑曲線)。+  -- ± 半幅 errorY = se (帯は基底空間の予測分散 = 対称)。+  toPlot m =+    let fit    = splFit m+        res    = sfResult fit+        xs     = LA.toList (splXraw m)+        yhat   = LA.toList (fittedV res)+        basis  = splineBasisAt fit (splXraw m)+        cib    = confidenceBand basis res defaultCILevel+        se     = zipWith (-) (upperBound cib) yhat   -- upper - ŷ = 片側半幅+        sorted = sortBy (comparing (\(x, _, _) -> x)) (zip3 xs yhat se)+        xsS    = [ x | (x, _, _) <- sorted ]+        yhatS  = [ y | (_, y, _) <- sorted ]+        seS    = [ e | (_, _, e) <- sorted ]+    in layer (band (inline xsS) (inline (zipWith (-) yhatS seS)) (inline (zipWith (+) yhatS seS)))+         <> layer (line (inline xsS) (inline yhatS))++  -- 残差診断 (平滑曲線 + 残差 vs fitted)。+  diagnosticPlots m =+    let res  = sfResult (splFit m)+        yhat = LA.toList (fittedV res)+        resd = LA.toList (residualsV res)+    in [ toPlot m+       , layer (scatter (inline yhat) (inline resd))+       ]++-- | [日本語]: grid 評価。 grid x で基底行列を再構築し、 それを設計行列とみなして+--   @confidenceBandAt@ を評価する (基底空間の対称 Wald CI = 訓練 @confidenceBand@ と同核)。+--   [English]: Grid evaluation. Rebuilds the basis matrix at the grid x+--   values, treats it as a design matrix, and evaluates @confidenceBandAt@+--   (the symmetric Wald CI in basis space — the same core as the training+--   @confidenceBand@).+instance SingleVarModel SplineModel where+  svRange m = let xs = LA.toList (splXraw m) in (minimum xs, maximum xs)+  svGrid m level gxs =+    let fit        = splFit m+        basisTrain = splineBasisAt fit (splXraw m)+        basisGrid  = splineBasisAt fit (LA.fromList gxs)+        cib        = confidenceBandAt basisTrain (sfResult fit) level basisGrid+        los        = lowerBound cib+        his        = upperBound cib+        mu         = zipWith (\l h -> (l + h) / 2) los his+    in (mu, Just (los, his))+  -- PI = closed form σ̂²(1 + xᵀ(XᵀX)⁻¹x) (基底空間 OLS ゆえ LM と同型・statsmodels obs_ci 相当)。+  svGridPI m level gxs =+    let fit        = splFit m+        basisTrain = splineBasisAt fit (splXraw m)+        basisGrid  = splineBasisAt fit (LA.fromList gxs)+        pib        = predictionBandAt basisTrain (sfResult fit) level basisGrid+    in Just (lowerBound pib, upperBound pib)+  -- ブートストラップ: 加法誤差。 refit は同じ kind/knots で再 fit。+  svBootKit m =+    let fit = splFit m+        res = sfResult fit+    in Just BootKit+       { bkX = LA.toList (splXraw m)+       , bkY = zipWith (+) (LA.toList (fittedV res)) (LA.toList (residualsV res))+       , bkRefit = \xs ys -> splineModel (sfKind fit) (sfKnots fit) (LA.fromList xs) (LA.fromList ys)+       , bkObsDist = Nothing }++-- ===========================================================================+-- 一般化加法モデル (描画可能)+--+-- 'GAMFit' (Hanalyze.Model.GAM) は各特徴の基底係数 + fitted 'gamYHat' を保持する。+-- 本 Phase では mgcv 流 Bayesian CI を実装した平滑曲線 + CI 帯を描く。+-- ===========================================================================++instance Plottable GAMModel where+  -- 平滑曲線 + CI 帯 (Phase 70.6 G で mgcv 流 Bayesian CI を実装)。 grid 経路+  -- ('statModel') に固定し、 LM/spline と同様 band + line を出す。+  toPlot = toPlot . statModel++  -- 残差診断 (平滑曲線 + 残差 vs fitted)。+  diagnosticPlots m =+    let fit  = gamFit m+        yhat = LA.toList (gamYHat fit)+        resd = LA.toList (gamResid fit)+    in [ toPlot m+       , layer (scatter (inline yhat) (inline resd))+       ]++-- | [日本語]: GAM の grid 評価 (中心 μ̂ + __mgcv 流 Bayesian 信頼帯__)。 'predictGAMSE' の+--   pointwise se に t_{n−edf} 臨界値を掛けて帯にする (Vβ='gamCov')。+--   [English]: GAM grid evaluation (center μ̂ + an __mgcv-style Bayesian confidence band__).+--   Multiplies the pointwise se from 'predictGAMSE' by the t_{n−edf}+--   critical value to form the band (Vβ='gamCov').+gamGridCI :: GAMFit -> Double -> [V.Vector Double] -> ([Double], Maybe ([Double], [Double]))+gamGridCI fit level cols =+  let (muV, seV) = predictGAMSE fit cols+      mu   = V.toList muV+      se   = V.toList seV+      df   = fromIntegral (LA.size (gamResid fit)) - gamEdf fit+      tVal = SD.quantile (studentT (max 1 df)) ((1 + level) / 2)+      lo   = zipWith (\u s -> u - tVal * s) mu se+      hi   = zipWith (\u s -> u + tVal * s) mu se+  in (mu, Just (lo, hi))++-- | [日本語]: grid 評価。 grid x を 'predictGAMSE' に通し平滑曲線 + CI 帯を評価する+--   (mgcv 流 Bayesian CI を実装)。+--   [English]: Grid evaluation. Runs the grid x through 'predictGAMSE' to+--   evaluate the smooth curve and CI band (implements an mgcv-style+--   Bayesian CI).+instance SingleVarModel GAMModel where+  svRange m = let xs = LA.toList (gamXraw m) in (minimum xs, maximum xs)+  svGrid m level gxs = gamGridCI (gamFit m) level [V.fromList gxs]+++-- | [日本語]: 第1予測子を描画軸に、 他予測子は訓練平均に固定して偏依存曲線を評価する。+--   [English]: Evaluates a partial-dependence curve using the first+--   predictor as the plotting axis, with the other predictors held fixed+--   at their training means.+instance SingleVarModel GAMModelN where+  svRange m = case gamNXraws m of+    (x:_) -> let xs = LA.toList x in (minimum xs, maximum xs)+    []    -> (0, 1)+  svGrid m level gxs =+    let n          = length gxs+        others     = drop 1 (gamNXraws m)+        holdMean v = V.replicate n (LA.sumElements v / fromIntegral (LA.size v))+        cols       = V.fromList gxs : map holdMean others+    in gamGridCI (gamNFit m) level cols+  svCoefR2 m = Just ([gamIntercept (gamNFit m)], gamR2 (gamNFit m))++instance Plottable GAMModelN where+  -- 平滑曲線 + CI 帯 (Phase 70.6 G)。 grid 経路 ('statModel') に固定。 多予測子では+  -- 第1予測子を軸に他を訓練平均で固定した偏依存曲線 + その点の CI。+  toPlot = toPlot . statModel++-- ===========================================================================+-- カーネル回帰 (GP / KRR / RFF) の描画可能ラッパ+-- ===========================================================================++-- | [日本語]: grid 評価 (E2)。 予測子 'gprPredict' を grid x に当て、 分布あり象限+--   (Gp/GpRff) は事後分散→正規 credible 帯 (μ̂ ± z·σ)、 点象限 (Ridge/RidgeRff) は+--   帯なし ('Nothing')。 信頼水準 @level@ → @z = Φ⁻¹(1 − (1−level)/2)@+--   ('quantileNormal')。 'WeightedLMModel' と同じく @toPlot@ を grid 経路+--   ('statModel') に固定する (元データ散布図と整合)。+--   [English]: Grid evaluation (E2). Applies the predictor 'gprPredict' to+--   the grid x. For distribution-bearing quadrants (Gp\/GpRff), converts+--   the posterior variance into a normal credible band (μ̂ ± z·σ); for+--   point quadrants (Ridge\/RidgeRff), there is no band ('Nothing'). The+--   confidence level @level@ maps to @z = Φ⁻¹(1 − (1−level)/2)@+--   ('quantileNormal'). Like 'WeightedLMModel', @toPlot@ is fixed to the+--   grid path ('statModel') to stay consistent with the raw-data scatter+--   plot.+instance SingleVarModel GPRegModel where+  svRange m = let xs = LA.toList (gprXraw m) in (minimum xs, maximum xs)+  svGrid m level gxs =+    let (mu, mbVar) = gprPredict m gxs+        z           = quantileNormal (1 - (1 - level) / 2)+    in case mbVar of+         Just vs -> let sds = map (sqrt . max 0) vs+                        los = zipWith (\u s -> u - z * s) mu sds+                        his = zipWith (\u s -> u + z * s) mu sds+                    in (mu, Just (los, his))+         Nothing -> (mu, Nothing)               -- Ridge 系 = 帯なし+  -- 予測区間 (PI) = 事後予測分散 (f の分散 + 観測ノイズ σ_n²) の正規帯。 分布あり象限のみ。+  svGridPI m level gxs =+    let (mu, mbVar) = gprPredict m gxs+        z           = quantileNormal (1 - (1 - level) / 2)+        sn2         = max 0 (gpNoiseVar (gprParams m))+    in case mbVar of+         Just vs -> let sds = map (\v -> sqrt (max 0 v + sn2)) vs+                    in Just ( zipWith (\u s -> u - z * s) mu sds+                            , zipWith (\u s -> u + z * s) mu sds )+         Nothing -> Nothing+  -- カーネル回帰は β₀+β₁x の線形「式」を持たないため式/R² 注釈は出さない。+  svCoefR2 _ = Nothing++-- | [日本語]: ★訓練点経路ではなく grid 経路 ('statModel') に固定 (元データ散布図と整合)。+--   分布あり象限は曲線 + credible 帯、 点象限は曲線のみ。+--   [English]: ★Fixed to the grid path ('statModel') rather than the+--   training-point path (to stay consistent with the raw-data scatter+--   plot). Distribution-bearing quadrants get a curve + credible band;+--   point quadrants get only a curve.+instance Plottable GPRegModel where+  toPlot = toPlot . statModel+++-- | [日本語]: 第1予測子を描画軸に、 他予測子を訓練平均に固定した偏依存曲線 (band は+--   分布あり象限のみ)。+--   [English]: A partial-dependence curve using the first predictor as+--   the plotting axis, with the other predictors held fixed at their+--   training means (the band appears only for distribution-bearing+--   quadrants).+instance SingleVarModel GPRegModelN where+  svRange m = case gprnXraws m of+    (x:_) -> let xs = LA.toList x in (minimum xs, maximum xs)+    []    -> (0, 1)+  svGrid m level gxs =+    let n          = length gxs+        others     = drop 1 (gprnXraws m)+        holdMean v = LA.konst (LA.sumElements v / fromIntegral (LA.size v)) n+        testX      = LA.fromColumns (LA.fromList gxs : map holdMean others)+        (mu, mbVar) = gprnPredict m testX+        z          = quantileNormal (1 - (1 - level) / 2)+    in case mbVar of+         Just vs -> let sds = map (sqrt . max 0) vs+                    in (mu, Just ( zipWith (\u s -> u - z * s) mu sds+                                 , zipWith (\u s -> u + z * s) mu sds ))+         Nothing -> (mu, Nothing)+  svGridPI m level gxs =+    let n          = length gxs+        others     = drop 1 (gprnXraws m)+        holdMean v = LA.konst (LA.sumElements v / fromIntegral (LA.size v)) n+        testX      = LA.fromColumns (LA.fromList gxs : map holdMean others)+        (mu, mbVar) = gprnPredict m testX+        z          = quantileNormal (1 - (1 - level) / 2)+        sn2        = max 0 (gpNoiseVar (gprnParams m))+    in case mbVar of+         Just vs -> let sds = map (\v -> sqrt (max 0 v + sn2)) vs+                    in Just ( zipWith (\u s -> u - z * s) mu sds+                            , zipWith (\u s -> u + z * s) mu sds )+         Nothing -> Nothing+  svCoefR2 _ = Nothing++instance Plottable GPRegModelN where+  toPlot = toPlot . statModel++-- ===========================================================================+-- 多出力 GP (描画可能)+-- ===========================================================================++-- | [日本語]: 多出力 GP の予測曲線 + 95% band (出力ごとに色分け・x = 予測点 index)。+--   [English]: Multi-output GP prediction curves + 95% band (colored per+--   output; x = prediction-point index).+multiGpCurves :: MultiGPResult -> VisualSpec+multiGpCurves res =+  let outs = zip3 (mgpMean res) (mgpLower res) (mgpUpper res)+      mkOut k (m, lo, hi) =+        let xs  = [ fromIntegral i | i <- [1 .. length m] ] :: [Double]+            lbl = "y" <> T.pack (show (k :: Int))+            grp = inlineCat (replicate (length m) lbl)+        in layer (band (inline xs) (inline lo) (inline hi) <> colorBy grp <> alpha 0.2)+             <> layer (line (inline xs) (inline m) <> colorBy grp)+  in mconcat (zipWith mkOut [0 ..] outs)++instance Plottable MultiGPResult where+  toPlot = multiGpCurves++-- ===========================================================================+-- 実測 vs 予測 (HasObsPred) — Phase 72.4+--+-- spline は内側の線形 fit ('sfResult') から、 GAM は保持する ŷ/残差から復元する。+-- ===========================================================================++instance HasObsPred SplineModel where+  obsPredPairs m =+    let r = sfResult (splFit m)+        f = LA.toList (fittedV r)+        e = LA.toList (residualsV r)+    in (zipWith (+) f e, f)++instance HasObsPred GAMModel where+  obsPredPairs m =+    let f = LA.toList (gamYHat (gamFit m))+        e = LA.toList (gamResid (gamFit m))+    in (zipWith (+) f e, f)++instance HasObsPred GAMModelN where+  obsPredPairs m =+    let f = LA.toList (gamYHat (gamNFit m))+        e = LA.toList (gamResid (gamNFit m))+    in (zipWith (+) f e, f)
+ src/Hanalyze/Plot/Wrappers.hs view
@@ -0,0 +1,339 @@+-- |+-- Module      : Hanalyze.Plot.Wrappers+-- Description : hgg 連携層 — 汎用ラッパの Plottable / SingleVarModel 連携 instance+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- [日本語]: hgg 連携層 — __汎用ラッパ (どの族にも属さない) の Plottable / SingleVarModel__ 連携 instance + 専用 helper。+--+-- ⚠ 親 'Hanalyze.Plot' と同じく別パッケージ @hanalyze-plot@ に属し、+-- @cabal build --project-file=cabal.project.plot@ で build される。 共通基盤 (class / ModelSpec / grid 評価核) は+-- 'Hanalyze.Plot.Core' を import して取り込む (orphan instance を許容:+-- クラス=Core・instance=ここ・型=Wrappers/各 Model module)。+--+-- 担当する型・ヘルパ (= 特定の ML / ベイズ族に属さない汎用ラッパ):+--   多出力線形回帰 'MultiFit' の残差相関 heatmap・k-NN 回帰の単変量描画+--   ('KNNRegressor')・透過標準化ラッパ 'StandardizedModel'・罰則回帰結果+--   'RegModel' の係数 bar・群別フィット 'GroupedFit' の N 曲線重畳・plot ColData+--   源の 'ColumnSource'・LM 係数診断アクセサ ('lmDiag' / 'groupedLmDiag')。+--+-- [English]: hgg integration layer — __Plottable\/SingleVarModel instances for generic wrappers (those not belonging to any specific family)__, plus dedicated helpers.+--+-- ⚠ Lives in the same separate package @hanalyze-plot@ as the parent+-- 'Hanalyze.Plot', built via @cabal build --project-file=cabal.project.plot@.+-- The shared foundation+-- (class \/ ModelSpec \/ grid-evaluation core) is pulled in by importing+-- 'Hanalyze.Plot.Core' (orphan instances are allowed by design:+-- class = Core, instance = here, type = Wrappers\/each model module).+--+-- Types and helpers covered here (= generic wrappers not belonging to any+-- specific ML\/Bayesian family):+--   the residual-correlation heatmap for multi-output linear regression+--   'MultiFit'; univariate plotting for k-NN regression ('KNNRegressor');+--   the transparent standardization wrapper 'StandardizedModel'; the+--   coefficient bar chart for penalized regression results 'RegModel'; the+--   N-curve overlay for grouped fits 'GroupedFit'; the 'ColumnSource'+--   instance for plot ColData sources; and the LM coefficient diagnostic+--   accessors ('lmDiag' \/ 'groupedLmDiag').+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE UndecidableInstances #-}+module Hanalyze.Plot.Wrappers+  ( -- ** 係数診断の薄アクセサ — A9+    lmDiag+  , groupedLmDiag+    -- ** 群別フィットの fullrange レンダラ — A4 / A7+  , groupedFullrange+  ) where++import qualified Data.Map.Strict       as Map+import qualified Data.Vector           as V+import qualified Data.Vector.Unboxed    as VU+import qualified Numeric.LinearAlgebra as LA++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           Hanalyze.Data.ColumnSource     (ColumnSource (..))++import           Graphics.Hgg.Spec     ( VisualSpec, layer, inline, inlineCat+                                       , ColData (..)+                                       , scatter, line+                                       , heatmap, colorBy+                                       , scaleColorManual, legend+                                       , bar, title )++import           Hanalyze.Model.Wrappers+import           Hanalyze.Plot.Core+import           Hanalyze.Fit+import           Hanalyze.Model.LM.Diagnostics (CoefStats (..), lmCoefStats)+import           Hanalyze.Model.LM     (linspace)+import           Hanalyze.Model.MultiLM (MultiFit (..))+import           Hanalyze.Stat.Standardize+                   ( Standardizer (..)+                   , applyStandardizerCol )+import           Hanalyze.Model.KNN (KNNRegressor (..), predictKNNR)++-- ===========================================================================+-- 多出力線形回帰 (描画可能)+--+-- 'MultiFit' (Hanalyze.Model.MultiLM) は q 個の応答を共通の予測子で同時回帰し、+-- 固有の成果物として **出力間の残差相関 'mfResidCor' (q×q)** を保持する。 q 本の回帰+-- 関係を単一図に素直に載せる方法は一意でない (出力ごとスケールが異なり得る) ため、+-- 代表図 (@toPlot@) は **残差相関 heatmap** とする (= 多出力回帰固有の図。 user 決定+-- 2026-06-04)。 'MultiFit' は heatmap に必要な相関行列を自己完結で持つので、 'GPResult'+-- 同様 X を別途束ねず結果型をそのまま 'Plottable' にできる。 個別の出力 j の回帰線は+-- 'predictMultiLM' で別途描ける (本 instance の対象外)。+--+-- ⚠ 'heatmap' (geom_tile) は **categorical 軸専用** (renderHeatmap が x/y をラベルとして+-- カテゴリ軸の index に引く。 実測: Render/Statistical.hs)。 ゆえに格子座標は数値でなく+-- **出力名ラベル** ("y1", "y2", …) を 'inlineCat' で渡す (数値だとカテゴリ軸が立たず+-- 全セルが drop されてタイルが描かれない = 計測で確認)。+-- ===========================================================================++instance Plottable MultiFit where+  -- 残差相関 q×q を heatmap に。 行 i・列 j のセル (x=yⱼ, y=yᵢ) に相関値 mfResidCor[i,j]+  -- を割り当てて 'heatmap' (= geom_tile) layer を 1 枚返す。 軸は出力名ラベル (categorical)。+  toPlot mf =+    let cor   = LA.toLists (mfResidCor mf)+        q     = length cor+        lbl k = "y" <> T.pack (show (k + 1 :: Int))   -- 出力名ラベル+        cells = [ (lbl j, lbl i, (cor !! i) !! j)+                | i <- [0 .. q - 1], j <- [0 .. q - 1] ]+        xs = [ x | (x, _, _) <- cells ]+        ys = [ y | (_, y, _) <- cells ]+        vs = [ v | (_, _, v) <- cells ]+    in layer (heatmap (inlineCat xs) (inlineCat ys) (inline vs))++-- C2: 元スケール逆変換 instance (Phase 70.3 項目 C) -------------------------+--+-- 内側モデルは標準化空間で学習されている。 ここで予測子 x を入力時に標準化し、+-- (@standardizedY@ なら) 応答 y を出力時に逆変換することで、 図・予測を**元スケール**で+-- 返す。 単変量 (1 特徴) 描画が対象 (smXStd の 0 次元を使う)。++-- | [日本語]: k-NN 回帰の単変量描画 (透過標準化の内側として要る)。 1 特徴 ('knnRX' が+--   1 列) を仮定し grid 点を予測曲線にする。 局所平均ゆえ band は持たない (Nothing)。+--   [English]: Univariate plotting for k-NN regression (needed as the inner+--   model of transparent standardization). Assumes a single feature+--   ('knnRX' has 1 column) and turns grid points into the predicted curve.+--   Since it's a local average, there's no band (Nothing).+instance SingleVarModel KNNRegressor where+  svRange m =+    let c0 = LA.toList (head (LA.toColumns (knnRX m)))+    in (minimum c0, maximum c0)+  svGrid m _ gxs =+    let xEval = LA.fromColumns [LA.fromList gxs]    -- n × 1 (単一特徴)+    in (VU.toList (predictKNNR m xEval), Nothing)   -- 帯なし++-- | [日本語]: 0 次元 (単変量描画の予測子) の (μ, σ)。+--   [English]: The (μ, σ) of dimension 0 (the univariate-plotting predictor).+stMu1, stSd1 :: Standardizer -> Double+stMu1 = head . stMu+stSd1 = head . stSd++-- | [日本語]: 応答 y の逆変換 (@smYStd = Just@ のみ実施。 @Nothing@ は元 y スケールのまま)。+--   [English]: Inverse-transforms the response y (only performed when+--   @smYStd = Just@; @Nothing@ leaves it in the original y scale).+unstdY :: Maybe (Double, Double) -> Double -> Double+unstdY (Just (muY, sdY)) v = v * sdY + muY+unstdY Nothing           v = v++-- | [日本語]: 透過標準化ラッパの単変量描画 (元スケール)。 入力 x を標準化 → 内側を評価 →+--   (@standardizedY@ なら) 出力 y を逆変換する。 内側 'svRange' (標準化空間) は+--   smXStd の 0 次元で元スケールへ戻す。 band/PI も同様に y を逆変換。+--   [English]: Univariate plotting for the transparent standardization+--   wrapper (in the original scale). Standardizes input x → evaluates the+--   inner model → (if @standardizedY@) inverse-transforms output y. The+--   inner 'svRange' (in standardized space) is converted back to the+--   original scale via dimension 0 of smXStd. The band\/PI likewise have y+--   inverse-transformed.+instance SingleVarModel m => SingleVarModel (StandardizedModel m) where+  svRange (StandardizedModel inner sx _ _) =+    let (zlo, zhi) = svRange inner+        unX z = z * stSd1 sx + stMu1 sx+    in (unX zlo, unX zhi)+  svGrid (StandardizedModel inner sx mY _) level xs =+    let zs            = map (applyStandardizerCol sx 0) xs+        (muZ, mbBand) = svGrid inner level zs+    in ( map (unstdY mY) muZ+       , fmap (\(lo, hi) -> (map (unstdY mY) lo, map (unstdY mY) hi)) mbBand )+  svGridPI (StandardizedModel inner sx mY _) level xs =+    let zs = map (applyStandardizerCol sx 0) xs+    in fmap (\(lo, hi) -> (map (unstdY mY) lo, map (unstdY mY) hi))+            (svGridPI inner level zs)+  -- 線形内側のみ式注釈 (係数を元スケールへ逆変換・R² はスケール不変で透過)。+  --   X のみ標準化: y = β₀ + β₁·(x−μₓ)/σₓ = (β₀ − β₁μₓ/σₓ) + (β₁/σₓ)·x。+  --   X+y 標準化:   y = σ_y·(β₀ + β₁·(x−μₓ)/σₓ) + μ_y。+  svCoefR2 (StandardizedModel inner sx mY _) =+    case svCoefR2 inner of+      Just ([b0, b1], r2) ->+        let mux = stMu1 sx; sdx = stSd1 sx+            (a0, a1) = case mY of+              Nothing         -> (b0 - b1 * mux / sdx, b1 / sdx)+              Just (muY, sdY) -> (b0 * sdY + muY - b1 * sdY * mux / sdx, b1 * sdY / sdx)+        in Just ([a0, a1], r2)+      _ -> Nothing   -- 非線形 (kNN 等) は式注釈なし++-- | [日本語]: 透過標準化ラッパの代表図 = 元スケールの予測曲線 (+ 単変量散布 'smTrain')。+--   内側 @toPlot@ (標準化軸) には依存せず、 ラッパ自身の 'SingleVarModel' を+--   'statModel' grid 機構へ流す。+--   [English]: The transparent standardization wrapper's representative+--   plot = the predicted curve in the original scale (+ the univariate+--   scatter 'smTrain'). Doesn't depend on the inner @toPlot@ (standardized+--   axes); instead feeds the wrapper's own 'SingleVarModel' into the+--   'statModel' grid mechanism.+instance SingleVarModel m => Plottable (StandardizedModel m) where+  toPlot sm = case smTrain sm of+    Just (xs, ys) -> layer (scatter (inline xs) (inline ys)) <> toPlot (statModel sm)+    Nothing       -> toPlot (statModel sm)++-- | [日本語]: 係数 bar (特徴名ラベル・元スケール) を代表図に。 CV パスがあれば診断束に+--   λ-MSE 図。+--   [English]: Uses the coefficient bar chart (feature-name labels, original+--   scale) as the representative plot. If a CV path exists, adds a λ-MSE+--   plot to the diagnostic bundle.+instance Plottable RegModel where+  toPlot m =+    layer (bar (inlineCat (rmgNames m)) (inline (rmgCoefs m)))+      <> title (regMethodName (rmgMethod m) <> " coefficients (\955="+                <> T.pack (show (roundTo 4 (rmgLambda m))) <> ")")+  diagnosticPlots m = toPlot m : case rmgCVPath m of+    Just (lams, scores) ->+      [ layer (line (inline lams) (inline scores)) <> title "CV/LOOCV score path" ]+    Nothing -> []++-- | [日本語]: RegMethod の表示名 (図タイトル用)。+--   [English]: The display name of a RegMethod (for figure titles).+regMethodName :: RegMethod -> Text+regMethodName Ridge            = "Ridge"+regMethodName Lasso            = "Lasso"+regMethodName (ElasticNet _)   = "Elastic Net"+regMethodName (MCP _)          = "MCP"+regMethodName (SCAD _)         = "SCAD"+regMethodName (AdaptiveLasso _) = "Adaptive Lasso"+regMethodName (GroupLasso _)   = "Group Lasso"++-- | [日本語]: 小数 n 桁丸め (タイトル表示用)。+--   [English]: Rounds to n decimal places (for title display).+roundTo :: Int -> Double -> Double+roundTo n v = let f = 10 ^^ n in fromIntegral (round (v * f) :: Integer) / f++-- | [日本語]: A9: 'LMModel' の係数診断 (SE / t値 / p値) を一発取得する薄アクセサ。+--   数値核は 'Hanalyze.Model.LM.Diagnostics.lmCoefStats'。 描画用に X を束ねた+--   'LMModel' から設計行列 ('lmDesign') と fit 結果 ('lmResult') を渡すだけ。+--   返りは係数順 (@[(Intercept), x]@) の 'CoefStats' リスト。+--   [English]: A9: A thin accessor to get 'LMModel' coefficient diagnostics+--   (SE \/ t-value \/ p-value) in one call. The numeric core is+--   'Hanalyze.Model.LM.Diagnostics.lmCoefStats'; it just passes the+--   design matrix ('lmDesign') and fit result ('lmResult') from the+--   plotting-bundled 'LMModel'. Returns a 'CoefStats' list in coefficient+--   order (@[(Intercept), x]@).+lmDiag :: LMModel -> [CoefStats]+lmDiag m = lmCoefStats (lmDesign m) (lmResult m)++-- | [日本語]: A9: 群別 LM フィット ('grouped "g" (lm …)' の結果) の各群係数診断を取り出す。+--   @[(群ラベル, [係数の CoefStats])]@。 群間で傾き SE/有意性を比較する用途。+--   ★@Fitted spec ~ LMModel@ に特殊化 (LM 群フィット専用)。+--   [English]: A9: Extracts the per-group coefficient diagnostics from a+--   grouped LM fit (the result of @grouped "g" (lm …)@). Returns+--   @[(group label, [coefficient CoefStats])]@, for comparing slope SE\/+--   significance across groups.+--   ★Specialized to @Fitted spec ~ LMModel@ (LM grouped-fit only).+groupedLmDiag :: (Fitted spec ~ LMModel) => GroupedFit spec -> [(Text, [CoefStats])]+groupedLmDiag = map (fmap lmDiag) . groupModels++-- | [日本語]: 群別フィットを N 曲線で重畳する (@toPlot@ = 各群 'svGrid' の μ̂ 曲線・群色 + 凡例)。+--   ★A3 の凡例機構を N 群へ一般化: 各曲線を @ColorByCol@ (群ラベル) に載せ+--   'scaleColorManual' で群色を固定し 'legend' を出す (固定色だと凡例が出ない罠を回避)。+--   grid 点数 100・帯なし (A1 既定 OFF) 固定。 群色は 'effectPalette' の循環。+--   [English]: Overlays a grouped fit as N curves (@toPlot@ = the μ̂ curve of+--   each group's 'svGrid', with per-group color + legend).+--   ★Generalizes the A3 legend mechanism to N groups: each curve is placed+--   on @ColorByCol@ (the group label), fixed with 'scaleColorManual' to a+--   per-group color, and 'legend' is shown (avoiding the trap where a fixed+--   color makes no legend appear). Fixed at 100 grid points, no band+--   (A1 default OFF). Group colors cycle through 'effectPalette'.+instance SingleVarModel (Fitted spec) => Plottable (GroupedFit spec) where+  toPlot = renderGrouped++-- | [日本語]: 群別フィットを __各群の x 範囲のみ__で描く (既定。 @toPlot@ = これ)。+--   [English]: Plots a grouped fit using __only each group's own x range__+--   (the default; @toPlot@ = this).+renderGrouped :: SingleVarModel (Fitted spec) => GroupedFit spec -> VisualSpec+renderGrouped = renderGroupedWith False++-- | [日本語]: 群別フィットを __データ全幅__ (全群 x の union 範囲) へ延ばして描く (A7 fullrange)。+--   ggplot @geom_smooth(fullrange = TRUE)@ 相当: 各群の回帰線を、 その群の x 範囲だけでなく+--   __全群を合わせた x の min/max__ まで延長して評価する (群間の傾き差を全域で比較しやすい)。+--   ★単一モデルでは「データ全幅 = 訓練 x」 ゆえ意味を持たない (range 拡張は grouped 固有)。+--   @toPlot@ とは別経路 (結果型 'GroupedFit' に描画 flag を持たせない・別レンダラとして提供)。+--   [English]: Plots a grouped fit extended to __the full data width__ (the+--   union range of all groups' x) — the A7 fullrange variant.+--   Equivalent to ggplot's @geom_smooth(fullrange = TRUE)@: each group's+--   regression line is evaluated not just over its own x range but extended+--   to __the min\/max of x across all groups combined__ (making it easier to+--   compare slope differences across the whole domain).+--   ★Meaningless for a single model, since "full data width = training x"+--   there (range extension is specific to grouped fits).+--   Reached via a separate path from @toPlot@ (the result type 'GroupedFit'+--   carries no plotting flag; this is offered as a separate renderer).+groupedFullrange :: SingleVarModel (Fitted spec) => GroupedFit spec -> VisualSpec+groupedFullrange = renderGroupedWith True++-- | [日本語]: 群別フィットの共通レンダラ。 @full@ で評価 x 範囲を切替える+--   (@False@ = 各群自範囲、 @True@ = 全群 union 範囲 = A7 fullrange)。+--   [English]: The shared renderer for grouped fits. @full@ switches the+--   evaluated x range (@False@ = each group's own range, @True@ = the union+--   range of all groups = A7 fullrange).+renderGroupedWith :: SingleVarModel (Fitted spec) => Bool -> GroupedFit spec -> VisualSpec+renderGroupedWith full gf =+  let pairs   = zip [0 :: Int ..] (gfGroups gf)+      n       = 100+      colOf i = effectPalette !! (i `mod` length effectPalette)+      -- fullrange = 全群 svRange の union (lo = 最小, hi = 最大)。 群が無ければ使われない。+      ranges  = [ svRange m | (_, (_, m)) <- pairs ]+      unionLo = minimum (map fst ranges)+      unionHi = maximum (map snd ranges)+      curveOf (_, (lbl, m)) =+        let (lo, hi) = if full then (unionLo, unionHi) else svRange m+            gxs      = linspace lo hi n+            (mu, _)  = svGrid m defaultCILevel gxs+        in layer (line (inline gxs) (inline mu)+                    <> colorBy (inlineCat (replicate n lbl)))+      legendSpec+        | null pairs = mempty+        | otherwise  = scaleColorManual [ (lbl, colOf i) | (i, (lbl, _)) <- pairs ]+                         <> legend+  in foldMap curveOf pairs <> legendSpec++-- --- plot ColData 源の ColumnSource instance (flag 配下・非 portable) -------+--+-- hgg の @[(Text, ColData)]@ (= df 中立表現)。 'NumData' は数値列、+-- 'TxtData' は factor 列。 'lookupCol' は数値列のみ返し、 'toFrame' は+-- 数値・文字列の両方を @DX.DataFrame@ に詰めて formula 経路で factor を温存する。+instance ColumnSource [(Text, ColData)] where+  lookupCol n cs = case lookup n cs of+    Just (NumData v) -> Just (V.toList v)+    _                -> Nothing+  columnNames = map fst+  toFrame cs  = DX.fromNamedColumns (concatMap toCol cs)+    where+      toCol (n, NumData v) = [(n, DX.fromList (V.toList v))]+      toCol (n, TxtData v) = [(n, DX.fromList (V.toList v))]++-- ===========================================================================+-- 実測 vs 予測 (HasObsPred) — Phase 72.4+--+-- 罰則回帰 ('RegModel') は元スケール係数 (rmgIntercept + rmgCoefs) と生設計+-- 'rmgXraw' から予測を再構成し、 実測は生応答 'rmgYraw' を使う。+-- ===========================================================================++instance HasObsPred RegModel where+  obsPredPairs m =+    let beta = LA.fromList (rmgCoefs m)+        prd  = map (+ rmgIntercept m) (LA.toList (rmgXraw m LA.#> beta))+    in (LA.toList (rmgYraw m), prd)
+ test-plot/Spec.hs view
@@ -0,0 +1,3749 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Phase 46 (hgg 統合) の数値・構造テスト。+--+-- 別パッケージ hanalyze-plot の一部として build/run される (= Hanalyze.Plot が必要)。+--   cabal test --project-file=cabal.project.plot hanalyze-plot-test+module Main (main) where++import           Data.Monoid           (First (..), Last (..))+import           Data.Text             (Text)+import qualified Data.Vector           as V+import qualified Data.Vector.Unboxed    as VU+import qualified Numeric.LinearAlgebra as LA+import           Test.Hspec++import           Graphics.Hgg.Spec     (ColorEnc (..), ColRef (..), Layer (..),+                                        MarkKind (..), MarkShape (..), VisualSpec (..),+                                        Annotation (..), CustomMark (..),+                                        ColData (..), LineType (..), fromHex,+                                        DAGSpec (..), DAGNode (..), DAGEdge (..),+                                        DAGNodeKind (..), DAGPlate (..))+import           Graphics.Hgg.Custom.Dendrogram (DendroPayload (..), DendroSeg (..))+import           Data.Aeson            (fromJSON, Result (..))+import qualified Data.Map.Strict       as Map+import qualified System.Random.MWC     as MWC+import           Hanalyze.MCMC.Core    (Chain (..))+import           Hanalyze.Model.PCA    (PCAStandardize (..))+import qualified Hanalyze.Model.PCA    as PCALow+import           Hanalyze.Model.RandomForest (defaultRandomForest, fitRF+                                       , fitRFVPure, predictRF, RandomForest (..))+import           Hanalyze.Model.CompetingRisks (CRSample (..), fitCompetingRisks)+import           Hanalyze.Model.Quantile (QRFit (..))+import           Hanalyze.Model.Survival (Event (..), SurvSample (..),+                                          kaplanMeier)+import           Hanalyze.Model.MultiLM (fitMultiLM)+import           Hanalyze.Model.NeuralNetwork (fitMLPClassifier, fitMLPClassifierPure, defaultMLP, MLPFit (..))+import           Hanalyze.Model.GP (Kernel (..), GPParams (..), defaultGPParams)+import           Hanalyze.Model.Kernel (defaultKernelParams)+import           Hanalyze.Model.Robust (RobustEstimator (..), RobustFit (..),+                                        defaultHuberK, fitRobustLM, robustCovBeta)+import           Hanalyze.Model.Core   (coefficientsV, fittedV, predictAt)+import           Hanalyze.Model.GLM    (Family (..), LinkFn (..))+import           Hanalyze.Model.GP     (GPResult (..))+import           Hanalyze.Model.GAM    (GAMFit (..), fitGAM)+import           Hanalyze.Model.Spline (SplineFit (..), SplineKind (..))+import           Hanalyze.Plot         (GAMModel (..), GLMModel (..),+                                        GAMBasis (..), GAMLambda (..),+                                        GAMConfig (..), defaultGAMConfig, gam, gamMulti,+                                        GAMModelN (..), fitGAMWith,+                                        GPConfig (..), defaultGP, gp, GPMethod (..),+                                        HyperStrategy (..), GPRegModel (..),+                                        gpMulti, GPRegModelN (..),+                                        RegMethod (..), LambdaStrat (..), RegConfig (..),+                                        defaultRidge, defaultLasso, regularized,+                                        ridge, lasso, elasticNet, RegModel (..), regPredict,+                                        fitEither,+                                        Kernel (..), GPParams (..), defaultGPParams,+                                        LMModel (..), QuantileModel (..),+                                        RobustModel (..), SplineModel (..),+                                        chainModel, diagnosticPlots,+                                        forecastModel, gamModel, glmModel,+                                        lmModel, quantileModel, robustModel,+                                        splineModel, toPlot,+                                        Coef (..), modelCoefficients,+                                        predictPoint, describeModel,+                                        statModel, grid, gridRange,+                                        BandMode (..), bandMode,+                                        statColor, statFill, statLinetype,+                                        statLinewidth, statAlpha, statLabel,+                                        statEquation, statR2,+                                        SingleVarModel (..),+                                        predAt,+                                        MultiLMModel (..), multiLMModel,+                                        MultiGLMModel (..), multiGLMModel,+                                        along, statModelMulti,+                                        HoldAgg (..), holdAt, byVar,+                                        plsModel, selectOutput,+                                        SurfaceOpts (..), defaultSurfaceOpts,+                                        surfaceGrid, surfaceOf, surfaceOfWith,+                                        dataScatter3DOf,+                                        epredSurfaceOf, epredSurfaceOfWith,+                                        HBMConfig (..), defaultHBM,+                                        HBMModel (..), hbmModel, hbmModelPure,+                                        hbmModelIO,+                                        ppcOfWithIO,+                                        hbmParamNames, marginalsOf,+                                        TraceOpts (..), defaultTraceOpts,+                                        tracesOf, tracesOfWith,+                                        ForestSpec (..), forestOf,+                                        statLevel, epred, epredAt,+                                        PPCConfig (..), defaultPPC,+                                        PPCSpec (..), ppcOf, ppcOfWith,+                                        DagSpec (..), dagOf, dagOfRaw,+                                        dagOfModel, dagOfModelWith,+                                        dashboardOf, dashboardFullOf, traceDensityOf,+                                        divergencesOf,+                                        pairOf, energyOf, autocorrOf, autocorrOfLag,+                                        rankOf, rankOfBins,+                                        Fit (..), (|->), (|->!),+                                        lm, glm, rq, rlm,+                                        piMethod, PIMethod (..),+                                        lmF, glmF, glmmF,+                                        hbm, dataScatterOf,+                                        grouped, groupModels, groupLabels,+                                        groupedFullrange,+                                        weighted, WeightedLMModel (..),+                                        CoefStats (..), lmDiag, groupedLmDiag,+                                        clusterScatterOf, centroidsOf,+                                        clusterHullOf, clusterEllipseOf,+                                        dendrogramOf, dendrogramOf', defaultDendroOpts, DendroOpts (..),+                                        GBRegressor (..), GBClassifier (..),+                                        RFClassifierFit (..), DTree (..),+                                        treeImportances,+                                        ClassPredict (..), decisionBoundaryOf,+                                        confusionOf, mdsView, mdsGroupBy, mds, defaultMDS, nnLossOf,+                                        mlpCls, mlpReg,+                                        svmCls, defaultSVM,+                                        SVMConfig (..), SVMMulti (..),+                                        numSupportVectors, svmSupportVectorsOf, decisionLineOf,+                                        RegPredict (..), pdp, pdpIce, pdpOf, pdpIceOf, pdpPlot, pdpIcePlot,+                                        partialDependencePlot, partialDependenceIcePlot,+                                        KNNClassifier (..),+                                        PLSFit (..), scoreView, vipView,+                                        MultiGPResult (..), multiGpCurves,+                                        GARCHFit (..), garchVolatility,+                                        AFTFit (..), aftSurvivalAt,+                                        FunctionalPCA (..), FLMResult (..),+                                        RegFit (..), regPathPlot,+                                        DirectLiNGAMFit (..), lingamDag,+                                        directLingam, parceLingam, multiGroupLingam,+                                        varLingam, pairwiseLingam,+                                        bootstrapLingam, icaLingam, bootstrapEdgeProbOf, LiNGAMFitted (..),+                                        correlationOf, CorrelationGraph (..),+                                        factorialDesign, centralCompositeDesign, designTable, designModel, multiOutput,+                                        contFactor, catFactor,+                                        profiler, profilerResidual, ProfilerSpec (..), ResidualMode (..), contourOf,+                                        TestResult (..), testForest,+                                        testForestLabeled, describeBox,+                                        pca, pls, lda, ccaOf, CCAFit (..),+                                        gbmReg, gbmCls, defaultGBM,+                                        decisionTree, defaultDecisionTree, knnCls, knnReg, naiveBayes,+                                        kmeans, randomForestReg, randomForestCls,+                                        lmMulti, glmMulti, rlmMulti,+                                        CoefRow (..), coefSummary,+                                        obsVsPred, obsPredPairs, coefForest,+                                        MultiRobustModel (..),+                                        standardized, standardizedY,+                                        StandardizedModel (..),+                                        predictorCols, responseCol,+                                        rqMulti, MultiQuantileModel (..))+import           Hanalyze.Model.Multivariate (cca)+import           Control.Exception (evaluate)+import           Hanalyze.Stat.Test (Alternative (..))+import           Hanalyze.Model.AFT (AFTDistribution (..))+import           Hanalyze.Model.Regularized (Penalty (NoPen))+import           Hanalyze.Model.Cluster (KMeansResult (..)+                                       , KMeansConfig (..), defaultKMeans, kMeansPure, kMeans)+import           Hanalyze.Model.HierarchicalCluster (fitHierarchical, Linkage (..), HClusterFit (..))+import           Hanalyze.Model.LiNGAM.Direct (fitDirectLiNGAM, defaultDirectLiNGAMConfig)+import           Hanalyze.Model.LiNGAM.Parce (defaultParceConfig)+import           Hanalyze.Model.LiNGAM.MultiGroup (defaultMultiGroupConfig)+import           Hanalyze.Model.LiNGAM.VAR (defaultVARLiNGAMConfig)+import           Hanalyze.Model.LiNGAM.Bootstrap (defaultBootstrapConfig, BootstrapConfig (..)+                                       , BootstrapResult (..), fitBootstrapLiNGAM, fitBootstrapLiNGAMPure)+import           Hanalyze.Model.LiNGAM.ICA (defaultICALiNGAMConfig, ICALiNGAMFit (..)+                                       , fitICALiNGAM, fitICALiNGAMPure)+import qualified Hanalyze.Model.RandomForest as RF+import           Hanalyze.Model.RandomForestClassifier (defaultRFCConfig, fitRFClassifierPure)+import           Hanalyze.Model.PLS (fitPLS, defaultPLS, PLSConfig (..))+import           Hanalyze.Model.KNN (predictKNNR, fitKNNR)+import           Hanalyze.Stat.Standardize (Standardizer (..), fitStandardizer, applyStandardizer)+import qualified Graphics.Hgg.ThreeD.Spec     as P3+import           Graphics.Hgg.ThreeD.Types    (Point3 (..))+import           Hanalyze.Model.HBM    ( Distribution (Normal, HalfNormal)+                                       , sample, dataNamedX, dataNamedObs, dataNamedIx, (!!!)+                                       , deterministic+                                       , observeColumns, observe, withData+                                       , plate, ModelP )+import           Hanalyze.Data.ColumnSource (ColumnSource (lookupCol))+import           Control.Monad         (forM_)+import qualified Data.Text             as T+import           Hanalyze.MCMC.Core    (chainVals)+import qualified DataFrame.Internal.Column    as DX+import qualified DataFrame.Internal.DataFrame  as DX+-- Phase 106.4: WorkflowSpec (umbrella test) から移行した plot 連携診断テスト用。+import           Data.Maybe            (isJust)+import           Hanalyze.Fit   (designModelHBM, DesignHBMFit (..), ranIntercept)+import           Hanalyze.Model.Formula.Frame (ModelFrame (..), VarRole (..))+import           Hanalyze.Plot.Core (MultiVarModel (..))+import           Hanalyze.Plot.ML ()  -- instance MultiVarModel DesignHBMFit++-- y = 2x + 1 (完全線形) を入れて係数を検証する。+xs, ys :: LA.Vector Double+xs = LA.fromList [1, 2, 3, 4, 5]+ys = LA.fromList [3, 5, 7, 9, 11]++m :: LMModel+m = lmModel xs ys++allClose :: [Double] -> [Double] -> Bool+allClose a b = length a == length b && and (zipWith (\x y -> abs (x - y) < 1e-9) a b)++-- Phase 49 A1: 線形 HBM (y ~ Normal(a + b·x, s))。 data は placeholder ([]) で書き、+-- hbmModel が列名で withData 自動 bind する (PyMC set_data 同型)。+hbmLinModel :: ModelP ()+hbmLinModel = do+  x <- dataNamedX "x" []+  y <- dataNamedObs "y" []+  a <- sample "a" (Normal 0 10)+  b <- sample "b" (Normal 0 10)+  s <- sample "s" (HalfNormal 1)+  observeColumns "obs"+    [ (Normal (a + b * xi) s, [yi]) | (xi, yi) <- zip x y ]++posteriorMeanOf :: Text -> [Chain] -> Double+posteriorMeanOf name chains =+  let vals = concatMap (chainVals name) chains+  in sum vals / fromIntegral (length vals)++-- Phase 49 A3: epred 用 O1 規約モデル。 学習 likelihood は per-point inline mu のまま、+-- 平均 μ を deterministic "mu" として 1 点スカラで併存公開する。 epred は grid を+-- withData "x" [xi] で 1 点に差し替えて mu を読む (head x = xi)。 訓練時は x が full data+-- ゆえ head x は安全 (deterministic 値は thunk で構築時に head [] を踏まない)。+hbmEpredModel :: ModelP ()+hbmEpredModel = do+  x <- dataNamedX "x" []+  y <- dataNamedObs "y" []+  a <- sample "a" (Normal 0 10)+  b <- sample "b" (Normal 0 10)+  s <- sample "s" (HalfNormal 1)+  _ <- deterministic "mu" (a + b * head x)+  observeColumns "obs"+    [ (Normal (a + b * xi) s, [yi]) | (xi, yi) <- zip x y ]++-- plot Phase 24 A3: epred 応答曲面用の 2 予測子 O1 規約モデル。+hbmEpred2Model :: ModelP ()+hbmEpred2Model = do+  x1 <- dataNamedX "x1" []+  x2 <- dataNamedX "x2" []+  y  <- dataNamedObs "y" []+  a  <- sample "a" (Normal 0 10)+  b  <- sample "b" (Normal 0 10)+  c  <- sample "c" (Normal 0 10)+  s  <- sample "s" (HalfNormal 1)+  _  <- deterministic "mu" (a + b * head x1 + c * head x2)+  observeColumns "obs"+    [ (Normal (a + b * xi + c * zi) s, [yi])+    | (xi, (zi, yi)) <- zip x1 (zip x2 y) ]++-- Phase 60.3: dataNamedIx で群 index を受ける 2 群モデル (factor 自動コード化の検証)。+-- mus !! g に round が消えるのが新 DSL の眼目。 sort 順 levels なら code 0 = "A"。+hbmIxModel :: ModelP ()+hbmIxModel = do+  gs <- dataNamedIx  "g" []+  y  <- dataNamedObs "y" []+  mu0 <- sample "mu0" (Normal 0 5)+  mu1 <- sample "mu1" (Normal 0 5)+  s   <- sample "s" (HalfNormal 1)+  let mus = [mu0, mu1]+  observeColumns "obs" [ (Normal (mus !!! g) s, [yi]) | (g, yi) <- zip gs y ]++-- Phase 49 A5: plate を使う階層モデル (dagOf の DAGPlate 変換検証用)。 group g の+-- 各メンバ eta_j を plate "g" 4 で囲い、 mu/tau は plate 外に置く (8-schools 風)。+hbmPlateModel :: ModelP ()+hbmPlateModel = do+  mu  <- sample "mu"  (Normal 0 5)+  tau <- sample "tau" (HalfNormal 5)+  _ <- plate "g" 4 $ forM_ [0 .. 3 :: Int] $ \j -> do+    eta <- sample ("eta_" <> T.pack (show j)) (Normal 0 1)+    observe ("y_" <> T.pack (show j)) (Normal (mu + tau * eta) 1) [realToFrac j]+  pure ()++main :: IO ()+main = hspec $ do+  describe "Phase 46 A4: LMModel + toPlot" $ do++    it "係数 ≈ [1, 2] (intercept, slope) = fitLM 直計算と一致" $ do+      let cs = LA.toList (coefficientsV (lmResult m))+      cs `shouldSatisfy` allClose [1, 2]++    it "predictAt (lmResult) X == fitted (PredictiveModel 整合)" $ do+      let yhat = LA.toList (LA.flatten (predictAt (lmResult m) (lmDesign m)))+      yhat `shouldSatisfy` allClose (LA.toList (fittedV (lmResult m)))++    it "toPlot は band + line の 2 layer を inline encY 付きで返す" $ do+      let ls = vsLayers (toPlot m)+      length ls `shouldBe` 2+      getFirst (lyKind (ls !! 0)) `shouldBe` Just MBand+      getFirst (lyKind (ls !! 1)) `shouldBe` Just MLine+      case getLast (lyEncY (ls !! 1)) of+        Just (ColNum v) -> V.length v `shouldBe` 5+        _               -> expectationFailure "line encY が inline ColNum でない"++    it "line layer の inline encY ≈ 予測 ŷ = Xβ (回帰線が fit と一致)" $ do+      let l = vsLayers (toPlot m) !! 1                          -- line layer+          yhatFit = LA.toList (fittedV (lmResult m))           -- x 昇順入力ゆえ既に整列+      case getLast (lyEncY l) of+        Just (ColNum v) -> V.toList v `shouldSatisfy` allClose yhatFit+        _               -> expectationFailure "line encY が inline ColNum でない"++    it "CI band 半幅 (encY2 − encY) は全点 ≥ 0" $ do+      let b = head (vsLayers (toPlot m))                        -- band layer+      case (getLast (lyEncY b), getLast (lyEncY2 b)) of+        (Just (ColNum vlo), Just (ColNum vhi)) ->+          zipWith (-) (V.toList vhi) (V.toList vlo) `shouldSatisfy` all (>= 0)+        _ -> expectationFailure "band encY/encY2 が inline ColNum でない"++  describe "Phase 70.A: df |-> pca / pls (行列入力モデルの高レベル化)" $ do+    -- 3 列の df と、 同じ並びの行列。 列名 spec が低レベル行列 fit と一致するか。+    let c1 = [ 5   * sin (fromIntegral i * 0.3) | i <- [1 .. 30 :: Int] ]+        c2 = [ 1.2 * cos (fromIntegral i * 0.5) | i <- [1 .. 30 :: Int] ]+        c3 = [ 0.3 * sin (fromIntegral i)       | i <- [1 .. 30 :: Int] ]+        df = [ ("x1", NumData (V.fromList c1))+             , ("x2", NumData (V.fromList c2))+             , ("x3", NumData (V.fromList c3)) ] :: [(Text, ColData)]+        xmat = LA.fromColumns (map LA.fromList [c1, c2, c3])+        encY r = case getLast (lyEncY (head (vsLayers (toPlot r)))) of+                   Just (ColNum v) -> V.toList v+                   _               -> []++    it "df |-> pca == 低レベル pca (toPlot 寄与率が一致)" $ do+      let resHi = df |-> pca CenterScale Nothing ["x1", "x2", "x3"]+          resLo = PCALow.pca CenterScale Nothing xmat+      encY resHi `shouldSatisfy` allClose (encY resLo)++    it "df |-> pls == 低レベル fitPLS (回帰係数 plsBeta が一致)" $ do+      let y    = [ 2 * a + 0.5 * b | (a, b) <- zip c1 c2 ]+          df'  = df ++ [ ("y", NumData (V.fromList y)) ] :: [(Text, ColData)]+          ymat = LA.fromColumns [LA.fromList y]+          mHi  = df' |-> pls defaultPLS ["x1", "x2"] ["y"]+      case fitPLS defaultPLS (LA.fromColumns (map LA.fromList [c1, c2])) ymat of+        Right mLo -> concat (LA.toLists (plsCoef mHi))+                       `shouldSatisfy` allClose (concat (LA.toLists (plsCoef mLo)))+        Left e    -> expectationFailure (T.unpack e)++    it "df |-> ccaOf == 低レベル cca (正準相関 ccaCorr が一致)" $ do+      let y1 = [ a + 0.1 * b | (a, b) <- zip c1 c2 ]+          y2 = [ b - 0.2 * a | (a, b) <- zip c1 c2 ]+          df' = df ++ [ ("y1", NumData (V.fromList y1))+                      , ("y2", NumData (V.fromList y2)) ] :: [(Text, ColData)]+          mHi = df' |-> ccaOf ["x1", "x2"] ["y1", "y2"]+          mLo = cca (LA.fromColumns (map LA.fromList [c1, c2]))+                    (LA.fromColumns (map LA.fromList [y1, y2]))+      LA.toList (ccaCorr mHi) `shouldSatisfy` allClose (LA.toList (ccaCorr mLo))++    it "df |-> lda は DiscriminantFit を当て描画可能 (クラス列の整数化が効く)" $ do+      let cls = [ if a > 0 then 1 else 0 | a <- c1 ] :: [Int]   -- 2 クラス+          df' = df ++ [ ("cls", NumData (V.fromList (map fromIntegral cls))) ]+                  :: [(Text, ColData)]+          m   = df' |-> lda ["x1", "x2"] "cls"+      length (vsLayers (toPlot m)) `shouldSatisfy` (> 0)++    -- 教師あり ML 分類器/回帰器 (純粋 fit)。 ラベル/応答列を足した df で fit→描画可。+    let yreg = [ 2 * a + 0.5 * b | (a, b) <- zip c1 c2 ]+        cls2 = [ fromIntegral (if a > 0 then 1 else 0 :: Int) | a <- c1 ] :: [Double]+        dfML = df ++ [ ("y", NumData (V.fromList yreg))+                     , ("cls", NumData (V.fromList cls2)) ] :: [(Text, ColData)]+    it "df |-> gbmReg / decisionTree / knnCls / naiveBayes が当てて描画可 (toPlot レイヤ > 0)" $ do+      let mGB = dfML |-> gbmReg defaultGBM ["x1", "x2"] "y"+          mDT = dfML |-> decisionTree defaultDecisionTree ["x1", "x2"] "cls"+          mKN = dfML |-> knnCls 3 ["x1", "x2"] "cls"+          mNB = dfML |-> naiveBayes ["x1", "x2"] "cls"+      length (vsLayers (toPlot mGB)) `shouldSatisfy` (> 0)+      length (vsLayers (toPlot mDT)) `shouldSatisfy` (> 0)+      length (vsLayers (toPlot mKN)) `shouldSatisfy` (> 0)+      length (vsLayers (toPlot mNB)) `shouldSatisfy` (> 0)+    it "df |-> gbmCls / knnReg は error なく当たる (fitEither が Right)" $ do+      let mGBC = dfML |-> gbmCls defaultGBM ["x1", "x2"] "cls"+          mKNR = dfML |-> knnReg 3 ["x1", "x2"] "y"+      _ <- evaluate (length (vsLayers (toPlot mGBC)))   -- GBClassifier は Plottable+      _ <- evaluate mKNR                                 -- KNNRegressor は WHNF へ強制+      pure ()++  describe "Phase 76: mark 拡充 (決定領域塗り / クラスタ囲み / dendrogram) の primitive" $ do+    let isAnnLine a = case a of AnnLine{} -> True; _ -> False+        -- Phase 48: dendrogram の U 字リンクは custom mark の焼き込み payload に載る。+        -- 先頭 layer の lyCustom (Last CustomMark) → cmOptions (JSON) を DendroPayload へ+        -- decode し、 その線分列 (dpSegments) を取り出す。+        dendroSegs vs = case vsLayers vs of+          (ly:_) -> case getLast (lyCustom ly) of+            Just cm -> case fromJSON (cmOptions cm) of+              Success p -> dpSegments p+              _         -> []+            Nothing -> []+          _ -> []+        -- 2 群 (各 3 点・非共線の三角形) の決定的データ + KMeansResult。+        hdf  = [ ("x", NumData (V.fromList [0, 1, 0.5, 5, 6, 5.5]))+               , ("y", NumData (V.fromList [0, 0, 1.0, 5, 5, 6.0])) ] :: [(Text, ColData)]+        kres = KMeansResult+                 { kmrCentroids = LA.fromLists [[0.5, 0.33], [5.5, 5.33]]+                 , kmrLabels    = [0, 0, 0, 1, 1, 1]+                 , kmrInertia   = 0, kmrIters = 1, kmrConverged = True }++    it "clusterHullOf: 各群の凸包 = 三角形 (3 点) → 群ごと 3 辺・全て AnnLine・layer 無し" $ do+      let vs = clusterHullOf hdf kres "x" "y"+      vsLayers vs `shouldBe` []+      length (vsAnnotations vs) `shouldBe` 6            -- 2 群 × 3 辺+      all isAnnLine (vsAnnotations vs) `shouldBe` True++    it "clusterHullOf: 列が無ければ空" $+      clusterHullOf hdf kres "nope" "y" `shouldBe` mempty++    it "clusterEllipseOf: 群ごと 64 辺の楕円折れ線 + 不可視 anchor layer 1" $ do+      let vs = clusterEllipseOf hdf kres "x" "y"+      length (vsAnnotations vs) `shouldBe` 128          -- 2 群 × 64 辺+      all isAnnLine (vsAnnotations vs) `shouldBe` True+      length (vsLayers vs) `shouldBe` 1                 -- 軸 auto-fit 用の alpha=0 散布++    it "dendrogramOf: U 字リンク 3*(n-1) 本 (custom mark 焼き込み) + y 軸線 1 本 (AnnLine)" $ do+      let xm = LA.fromLists [[0,0],[0.2,0.1],[5,5],[5.1,4.9]]   -- n=4・2 群+          hc = fitHierarchical Ward xm+          vs = dendrogramOf hc+          n  = 4 :: Int+      -- Phase 48: U 字リンクは 1 layer (MCustom) に焼き込み・segments = 3*(n-1) 本。+      length (vsLayers vs) `shouldBe` 1+      getFirst (lyKind (head (vsLayers vs))) `shouldBe` Just MCustom+      length (dendroSegs vs) `shouldBe` 3 * (n - 1)+      -- annotation は y 軸線 1 本のみ。+      length (vsAnnotations vs) `shouldBe` 1+      all isAnnLine (vsAnnotations vs) `shouldBe` True++    it "dendrogramOf': 色閾値で葉クラスタが色分け (閾値超は既定線色・複数色出る)" $ do+      let xm = LA.fromLists [[0,0],[0.2,0.1],[5,5],[5.1,4.9]]+          hc = fitHierarchical Ward xm+          hs = hcHeights hc+          thr = (hs !! (length hs - 2) + hs !! (length hs - 1)) / 2+          vs = dendrogramOf' defaultDendroOpts { doColorThreshold = Just thr } hc+          cols = map segColor (dendroSegs vs)             -- 焼き込み線分の色+      any (/= head cols) cols `shouldBe` True           -- 群色 + 閾値超色 = 2 色以上++    -- Phase 76.D: PDP を HBM 同型の Plottable 中間型 (toPlot) に。 pdpOf と同一 primitive。+    it "toPlot (pdp …) == pdpOf (同一 layer 数)" $ do+      let pdf = [ ("a", NumData (V.fromList [0,1,2,3,4,5,6,7]))+                , ("b", NumData (V.fromList [1,0,1,0,1,0,1,0])) ] :: [(Text, ColData)]+          xm  = LA.fromColumns [ LA.fromList [0,1,2,3,4,5,6,7], LA.fromList [1,0,1,0,1,0,1,0] ]+          yv  = VU.fromList [1,2,3,4,5,6,7,8 :: Double]+          rf  = fitRFVPure defaultRandomForest xm yv 7+          viaView   = toPlot (pdp rf pdf ["a","b"] "a")+          viaDirect = pdpOf rf pdf ["a","b"] "a"+      length (vsLayers viaView) `shouldBe` length (vsLayers viaDirect)+      length (vsLayers viaView) `shouldSatisfy` (> 0)++    it "toPlot (pdpIce …) は ICE 曲線 + 平均で pdp より layer が多い" $ do+      let pdf = [ ("a", NumData (V.fromList [0,1,2,3,4,5,6,7]))+                , ("b", NumData (V.fromList [1,0,1,0,1,0,1,0])) ] :: [(Text, ColData)]+          xm  = LA.fromColumns [ LA.fromList [0,1,2,3,4,5,6,7], LA.fromList [1,0,1,0,1,0,1,0] ]+          yv  = VU.fromList [1,2,3,4,5,6,7,8 :: Double]+          rf  = fitRFVPure defaultRandomForest xm yv 7+          nP  = length (vsLayers (toPlot (pdp    rf pdf ["a","b"] "a")))+          nI  = length (vsLayers (toPlot (pdpIce rf pdf ["a","b"] "a")))+      nI `shouldSatisfy` (> nP)++  describe "Phase 70.A: KMeans / RandomForest の seed 純粋化 (df |-> + 決定性)" $ do+    -- 2 クラスタ (中心 (1,1) と (5,5)・各 20 点) の決定的データ。+    let pts  = [ (1 + 0.1 * sin (fromIntegral i), 1 + 0.1 * cos (fromIntegral i))+               | i <- [1 .. 20 :: Int] ]+            ++ [ (5 + 0.1 * sin (fromIntegral i), 5 + 0.1 * cos (fromIntegral i))+               | i <- [1 .. 20 :: Int] ]+        as    = map fst pts+        bs    = map snd pts+        xmatK = LA.fromColumns [LA.fromList as, LA.fromList bs]+        dfK   = [ ("a", NumData (V.fromList as))+                , ("b", NumData (V.fromList bs)) ] :: [(Text, ColData)]+        cfgK  = defaultKMeans 2++    it "kMeansPure は決定的 (同 seed → inertia/centroids/labels がビット一致)" $ do+      let r1 = kMeansPure cfgK xmatK 42+          r2 = kMeansPure cfgK xmatK 42+      kmrInertia r1 `shouldBe` kmrInertia r2+      LA.toList (LA.flatten (kmrCentroids r1))+        `shouldBe` LA.toList (LA.flatten (kmrCentroids r2))+      kmrLabels r1 `shouldBe` kmrLabels r2++    it "kMeansPure seed==42 は IO kMeans(initialize 42) とビット一致 (ST/IO 同コード)" $ do+      gen <- MWC.initialize (V.singleton 42)+      rIO <- kMeans cfgK xmatK gen+      let rST = kMeansPure cfgK xmatK 42+      kmrInertia rIO `shouldBe` kmrInertia rST+      kmrLabels  rIO `shouldBe` kmrLabels  rST+      LA.toList (LA.flatten (kmrCentroids rIO))+        `shouldBe` LA.toList (LA.flatten (kmrCentroids rST))++    it "df |-> kmeans == kMeansPure (列名経路と行列経路が一致・描画可)" $ do+      let r = dfK |-> kmeans cfgK 42 ["a", "b"]+      kmrInertia r `shouldBe` kmrInertia (kMeansPure cfgK xmatK 42)+      length (vsLayers (toPlot r)) `shouldSatisfy` (> 0)++    -- RandomForest 回帰: y = 2a + 3b。+    let yR    = [ 2 * a + 3 * b | (a, b) <- pts ]+        yvR   = VU.fromList yR+        dfR   = dfK ++ [ ("y", NumData (V.fromList yR)) ] :: [(Text, ColData)]+        cfgR  = defaultRandomForest++    it "fitRFVPure は決定的 (同 seed → importance/予測がビット一致)" $ do+      let f1 = fitRFVPure cfgR xmatK yvR 7+          f2 = fitRFVPure cfgR xmatK yvR 7+      V.toList (rfImportance f1) `shouldBe` V.toList (rfImportance f2)+      predictRF f1 [1, 1] `shouldBe` predictRF f2 [1, 1]++    it "df |-> randomForestReg == fitRFVPure (同 seed・特徴重要度バー描画可)" $ do+      let r = dfR |-> randomForestReg cfgR 7 ["a", "b"] "y"+      V.toList (rfImportance r)+        `shouldBe` V.toList (rfImportance (fitRFVPure cfgR xmatK yvR 7))+      -- toPlot は 2 パネル (impurity + permutation) の subplots (75.24)。+      length (vsSubplots (toPlot r)) `shouldBe` 2++    -- RandomForest 分類 (75.24d): 2 クラスタ → クラス 0/1。df|-> で実列名・決定的。+    let clsC = replicate 20 0 ++ replicate 20 1 :: [Int]+        dfC  = dfK ++ [ ("cls", NumData (V.fromList (map fromIntegral clsC))) ] :: [(Text, ColData)]++    it "df |-> randomForestCls == fitRFClassifierPure (同 seed・実列名・2 パネル)" $ do+      let r   = dfC |-> randomForestCls defaultRFCConfig 7 ["a", "b"] "cls"+          ref = fitRFClassifierPure defaultRFCConfig xmatK (VU.fromList clsC) 7+      -- 決定性: gini/permutation がビット一致 (df|-> = 行列経路)+      LA.toList (rfcGiniImportance r) `shouldBe` LA.toList (rfcGiniImportance ref)+      LA.toList (rfcImportance r)     `shouldBe` LA.toList (rfcImportance ref)+      -- df|-> は実列名を載せる (行列経路の f1.. でなく)+      rfcFeatureNames r `shouldBe` ["a", "b"]+      length (vsSubplots (toPlot r)) `shouldBe` 2++  describe "Phase 70.D: 重回帰 統一 API (lmMulti/glmMulti/robustMulti + coefSummary)" $ do+    -- 固定データ (12 行・x1,x2,x3)。 末尾 y=40 が外れ値 (ロバストの効きを見る)。+    -- 期待値は statsmodels 0.14.6 で生成 (experiments/phase-70d-coefsummary/ref_statsmodels.py)。+    let x1 = [1,2,3,4,5,6,7,8,9,10,11,12] :: [Double]+        x2 = [2,1,4,3,6,5,8,7,10,9,12,11] :: [Double]+        x3 = [0.5,1.5,1,2.5,2,3.5,3,4.5,4,5.5,5,6.5] :: [Double]+        yv = [3.1,4,7.2,8.1,11,12.3,15.1,16,19.2,20.1,23,40] :: [Double]+        dfM = [ ("x1", NumData (V.fromList x1)), ("x2", NumData (V.fromList x2))+              , ("x3", NumData (V.fromList x3)), ("y",  NumData (V.fromList yv)) ]+              :: [(Text, ColData)]+        near a b = abs (a - b) < 1e-6+        nearV xs ys = and (zipWith near xs ys)+        -- GLM は IRLS 収束点が statsmodels とごく僅か異なり SE が ~1e-5 ずれる+        -- (β は 1e-9 一致。 独立実装間として 5 桁一致は良好)。 GLM のみ緩い許容。+        nearV4 xs ys = and (zipWith (\a b -> abs (a - b) < 1e-4) xs ys)+        rowsOf m = ( map crEstimate m, map crStdErr m, map crStat m+                   , map crPValue m, concatMap (\r -> [fst (crCI95 r), snd (crCI95 r)]) m )++    it "lmMulti coefSummary (t) == statsmodels OLS .summary()" $ do+      let m  = dfM |-> lmMulti ["x1", "x2", "x3"] "y" :: MultiLMModel+          cs = coefSummary m+          (est, se, tv, pv, ci) = rowsOf cs+      -- 名前 = (Intercept) + 列名+      map crTerm cs `shouldBe` ["(Intercept)", "x1", "x2", "x3"]+      nearV est [-3.121363636363598,-5.105000000000107,3.5004545454546294,8.650909090909133]+        `shouldBe` True+      nearV se  [3.2114157547937565,10.36170010985987,5.21966108203359,10.83050421206717]+        `shouldBe` True+      nearV tv  [-0.97195874800834,-0.49267976740055897,0.6706287037492568,0.798754048890026]+        `shouldBe` True+      nearV pv  [0.35953762982419396,0.6354752397806847,0.5213433571725514,0.4474952370377395]+        `shouldBe` True+      nearV ci  [-10.526901646229312,4.284174373502117,-28.999123299312696,18.789123299312482+                ,-8.536105493187584,15.537014584096845,-16.3242784066141,33.62609658843236]+        `shouldBe` True++    it "robustMulti coefSummary (z, Huber 1.345) == statsmodels RLM (cov=H1)" $ do+      let m  = dfM |-> rlmMulti (Huber 1.345) ["x1", "x2", "x3"] "y" :: MultiRobustModel+          cs = coefSummary m+          (est, se, zv, pv, ci) = rowsOf cs+      map crTerm cs `shouldBe` ["(Intercept)", "x1", "x2", "x3"]+      nearV est [0.5497573872869811,1.4082792355091573,0.5398730747685647,0.12443445533346953]+        `shouldBe` True+      nearV se  [0.1120721724104495,0.3616032086297613,0.18215603377936052,0.3779635612533277]+        `shouldBe` True+      nearV zv  [4.905387086399712,3.8945429739011743,2.9637946301712677,0.3292234175189923]+        `shouldBe` True+      nearV pv  [9.324326853300844e-7,9.838404940536295e-5,0.0030387101129149083,0.7419868240281127]+        `shouldBe` True+      nearV ci  [0.33009996569333655,0.7694148088806256,0.699549969900702,2.1170085011176125+                ,0.1828538089943566,0.8968923405427729,-0.6163605121915514,0.8652294228584905]+        `shouldBe` True++    -- GLM は scale=1 の族 (Poisson/Binomial) が本来用途。 z 経路 (正規) で statsmodels+    -- GLM と一致する (Gaussian は分散スケール推定が入り別経路 → 連続応答は lmMulti を使う)。+    it "glmMulti Poisson/log coefSummary (z) == statsmodels GLM Poisson" $ do+      let yc  = [1,2,2,4,5,7,10,14,19,26,35,48] :: [Double]+          dfP = [ ("x1", NumData (V.fromList x1)), ("x2", NumData (V.fromList x2))+                , ("x3", NumData (V.fromList x3)), ("yc", NumData (V.fromList yc)) ]+                :: [(Text, ColData)]+          cs = coefSummary (dfP |-> glmMulti Poisson Log ["x1", "x2", "x3"] "yc")+          (est, se, zv, pv, ci) = rowsOf cs+      nearV  est [0.03218678346706216,1.0213360876134985,-0.35313387540543173,-0.6945102404086394]+        `shouldBe` True+      nearV4 se  [0.31691453822410903,2.071111308008677,1.0400997867517308,2.081045612659335]+        `shouldBe` True+      nearV4 zv  [0.1015629754552345,0.4931343301850291,-0.3395192268121519,-0.33373138780996536]+        `shouldBe` True+      nearV4 pv  [0.9191035687400753,0.6219176751774244,0.7342186154504002,0.7385822621359937]+        `shouldBe` True+      nearV4 ci  [-0.5889542976293338,0.6533278645634581,-3.037967484057151,5.080639659284148+                ,-2.3916919977666145,1.6854242469557508,-4.773284691406028,3.3842642105887486]+        `shouldBe` True++    it "lmMulti は effect plot (statModelMulti + along) が即使える" $ do+      let m = dfM |-> lmMulti ["x1", "x2", "x3"] "y" :: MultiLMModel+          spec = statModelMulti m (along "x1") <> holdAt Mean+      length (vsLayers (toPlot spec)) `shouldSatisfy` (> 0)++    it "robustMulti も effect plot (band + line) が即使える" $ do+      let m = dfM |-> rlmMulti (Huber 1.345) ["x1", "x2", "x3"] "y" :: MultiRobustModel+          spec = statModelMulti m (along "x1") <> holdAt Mean+      length (vsLayers (toPlot spec)) `shouldSatisfy` (>= 2)++  describe "Phase 46 A8: GLMModel + toPlot (非対称 μ-CI 帯)" $ do+    -- Poisson 回帰 (log link)。 count が単調増加するデータ。+    let gxs = LA.fromList [1, 2, 3, 4, 5, 6]+        gys = LA.fromList [1, 2, 4, 7, 12, 20]+        gm  = glmModel Poisson Log gxs gys+        layersOf = vsLayers (toPlot gm)+        bandL = head [ l | l <- layersOf, getFirst (lyKind l) == Just MBand ]+        lineL = head [ l | l <- layersOf, getFirst (lyKind l) == Just MLine ]+        numOf f l = case getLast (f l) of+          Just (ColNum v) -> V.toList v+          _               -> error "encoding が inline ColNum でない"++    it "toPlot は band (MBand) + μ 線 (MLine) の 2 layer を返す" $ do+      length layersOf `shouldBe` 2+      map (getFirst . lyKind) layersOf `shouldMatchList`+        [Just MBand, Just MLine]++    it "band の x は昇順、 点数は n=6" $ do+      let xb = numOf lyEncX bandL+      length xb `shouldBe` 6+      xb `shouldSatisfy` \v -> and (zipWith (<=) v (drop 1 v))++    it "band 上境界 (encY2) ≥ 下境界 (encY) = 帯が潰れない" $ do+      let lo = numOf lyEncY  bandL+          hi = numOf lyEncY2 bandL+      and (zipWith (<=) lo hi) `shouldBe` True++    it "μ 線 (MLine encY) は band の [lo, hi] 内 = 中心が帯内" $ do+      let lo = numOf lyEncY  bandL+          hi = numOf lyEncY2 bandL+          mu = numOf lyEncY  lineL+      and (zipWith3 (\l u c -> l - 1e-9 <= c && c <= u + 1e-9) lo hi mu)+        `shouldBe` True++    it "μ 線は fitted μ̂ と一致 (= predictGlmMuWithCI が fit と整合)" $ do+      let mu = numOf lyEncY lineL+      mu `shouldSatisfy` allClose (LA.toList (fittedV (glmResult gm)))++  describe "Phase 46 A6: GPResult + toPlot (protocol 汎用性)" $ do+    -- 予測 grid をわざと降順にして、 toPlot がソートして折れ線を作ることを検証。+    -- 値は手組み (GP 数値計算に非依存・決定的): mean = grid, band 半幅 = 0.5。+    let gres = GPResult+          { gpTestX = [3, 1, 2]+          , gpMean  = [30, 10, 20]+          , gpVar   = [0.25, 0.25, 0.25]+          , gpLower = [29.5, 9.5, 19.5]+          , gpUpper = [30.5, 10.5, 20.5]+          }++    it "toPlot は band + line の 2 layer を返す (FitResult 系と別型でも成立)" $ do+      let ls = vsLayers (toPlot gres)+      length ls `shouldBe` 2+      getFirst (lyKind (ls !! 0)) `shouldBe` Just MBand+      getFirst (lyKind (ls !! 1)) `shouldBe` Just MLine++    it "line の encX/encY は x 昇順にソートされる (= 折れ線が交差しない)" $ do+      let l = vsLayers (toPlot gres) !! 1                        -- line layer+      case (getLast (lyEncX l), getLast (lyEncY l)) of+        (Just (ColNum vx), Just (ColNum vy)) -> do+          V.toList vx `shouldSatisfy` allClose [1, 2, 3]+          V.toList vy `shouldSatisfy` allClose [10, 20, 30]+        _ -> expectationFailure "encX/encY が inline ColNum でない"++    it "band は gpMean ± (gpUpper − gpMean) = [low, high] を encY/encY2 に持つ" $ do+      let b = head (vsLayers (toPlot gres))                      -- band layer+      case (getLast (lyEncY b), getLast (lyEncY2 b)) of+        (Just (ColNum vlo), Just (ColNum vhi)) -> do+          V.toList vlo `shouldSatisfy` allClose [9.5, 19.5, 29.5]+          V.toList vhi `shouldSatisfy` allClose [10.5, 20.5, 30.5]+        _ -> expectationFailure "band encY/encY2 が inline ColNum でない"++  describe "Phase 46 A9: SplineModel + toPlot (平滑曲線 + 対称 CI band)" $ do+    -- なめらかな非線形データ。 x は意図的に降順で渡し、 toPlot が昇順整列することも確認。+    let sxs = LA.fromList [6, 5, 4, 3, 2, 1, 0]+        sys = LA.fromList [36, 25, 16, 9, 4, 1, 0]  -- y = x²+        sm  = splineModel (BSpline 3) [0, 2, 4, 6] sxs sys+        sBand = head (vsLayers (toPlot sm))           -- band layer (CI)+        sLine = vsLayers (toPlot sm) !! 1             -- line layer (ŷ 曲線)+        numBand f = case getLast (f sBand) of+          Just (ColNum v) -> V.toList v+          _               -> error "band encoding が inline ColNum でない"+        numLine f = case getLast (f sLine) of+          Just (ColNum v) -> V.toList v+          _               -> error "line encoding が inline ColNum でない"++    it "toPlot は band + line の 2 layer を返す (曲線 + band)" $ do+      let ls = vsLayers (toPlot sm)+      length ls `shouldBe` 2+      getFirst (lyKind sBand) `shouldBe` Just MBand+      getFirst (lyKind sLine) `shouldBe` Just MLine++    it "encX は x 昇順にソートされる (= 平滑曲線が交差しない)" $ do+      let xb = numLine lyEncX+      and (zipWith (<=) xb (drop 1 xb)) `shouldBe` True++    it "line encY (ŷ) は fitted と一致 (= 基底空間 fit と整合)" $ do+      -- fittedV は入力順 (降順)、 encY は昇順整列ゆえ reverse して突合。+      let yhatFit = reverse (LA.toList (fittedV (sfResult (splFit sm))))+      numLine lyEncY `shouldSatisfy` allClose yhatFit++    it "CI band 半幅 (encY2 − encY) は全点 ≥ 0 (基底空間 Wald CI)" $ do+      zipWith (-) (numBand lyEncY2) (numBand lyEncY) `shouldSatisfy` all (>= 0)++    it "Phase 70.G: svGridPI (PI) ⊃ svGrid (CI) — 基底空間 closed-form PI" $ do+      let gx = [1.0, 3.0, 5.0]+      case (svGrid sm 0.95 gx, svGridPI sm 0.95 gx) of+        ((_, Just (clo, chi)), Just (plo, phi)) -> do+          and (zipWith (<=) plo clo) `shouldBe` True   -- PI 下限 ≤ CI 下限+          and (zipWith (>=) phi chi) `shouldBe` True   -- PI 上限 ≥ CI 上限+        _ -> expectationFailure "spline の CI/PI が出ない"++    it "Phase 70.G: bandMode BandCIPI で CI+PI 入れ子 (MBand 2 + MLine 1)" $ do+      let ls = vsLayers (toPlot (statModel sm <> grid 10 <> bandMode BandCIPI))+      map (getFirst . lyKind) ls `shouldMatchList` [Just MBand, Just MBand, Just MLine]++  describe "Phase 70.6 G: GAMModel CI 帯 (mgcv 流 Bayesian)" $ do+    -- 非線形データ。 GAM は CI 実装後、 grid 経路で band + line を出す。+    let gmxs = LA.fromList [0, 1, 2, 3, 4, 5, 6, 7, 8]+        gmys = LA.fromList [ sin x + 0.1 * x | x <- LA.toList gmxs ]+        gmm  = gamModel 3 5 0.0 gmxs gmys++    it "toPlot は band(MBand) + line(MLine) の 2 layer を返す" $ do+      let ls = vsLayers (toPlot gmm)+      length ls `shouldBe` 2+      getFirst (lyKind (ls !! 0)) `shouldBe` Just MBand+      getFirst (lyKind (ls !! 1)) `shouldBe` Just MLine++    it "svGrid は Just の CI 帯を返し lo ≤ μ ≤ hi (帯幅 > 0)" $ do+      let (mu, mb) = svGrid gmm 0.95 [1.0, 4.0, 7.0]+      length mu `shouldBe` 3+      case mb of+        Just (lo, hi) -> do+          and (zipWith (<=) lo mu) `shouldBe` True+          and (zipWith (<=) mu hi) `shouldBe` True+          and (zipWith (\l h -> h - l > 0) lo hi) `shouldBe` True+        Nothing -> expectationFailure "CI 帯 (Just) を期待"++    it "★厳密検証: GAM(PolyB 1, λ=0) の CI 帯は LM と一致 (基底が {1,x} と同一スパン)" $ do+      -- y = 2x + 1 + ノイズ。 PolyB 1・λ=0 の列空間は {1, x} = OLS と同一ゆえ、+      -- 予測も予測分散も (したがって CI 帯も) LMModel と厳密一致するはず。+      let xsC = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] :: [Double]+          ysC = [3.1, 4.8, 7.2, 8.9, 11.3, 12.7, 15.1, 16.8, 19.2, 20.9]+          dfC = [ ("x", xsC), ("y", ysC) ] :: [(Text, [Double])]+          gmP = dfC |-> gam (GAMConfig (PolyB 1) (FixedL 0)) "x" "y"   -- GAMModelN+          lmM = lmModel (LA.fromList xsC) (LA.fromList ysC)            -- LMModel (OLS)+          gridC = [2.0, 4.5, 6.0, 8.5]+          (gmu, Just (glo, ghi)) = svGrid gmP 0.95 gridC+          (lmu, Just (llo, lhi)) = svGrid lmM 0.95 gridC+      gmu `shouldSatisfy` allClose lmu+      glo `shouldSatisfy` allClose llo+      ghi `shouldSatisfy` allClose lhi++  describe "Phase 70.6: GAM 基底一般化 + GCV + gam/gamMulti (df|->)" $ do+    -- 非線形 y = sin x + 0.1 x を x∈[0,8] で 21 点。+    let gx70 = [ 0.4 * fromIntegral i | i <- [0 .. 20 :: Int] ] :: [Double]+        gy70 = [ sin x + 0.1 * x | x <- gx70 ]+        xv70 = V.fromList gx70+        yv70 = V.fromList gy70+        df70 = [ ("x", gx70), ("y", gy70) ] :: [(Text, [Double])]++    it "F1: fitGAMWith [BSplineB] が fitGAM とビット一致 (後方互換)" $ do+      let a = fitGAMWith [BSplineB 3 5] 0.0 [xv70] yv70+          b = fitGAM 3 5 0.0 [xv70] yv70+      LA.toList (gamYHat a) `shouldSatisfy` allClose (LA.toList (gamYHat b))++    it "F1: 自然3次基底で非線形を高 R² でフィット" $ do+      gamR2 (fitGAMWith [NaturalCubicB 6] 0.0 [xv70] yv70) `shouldSatisfy` (> 0.9)++    it "F1: Fourier 基底で非線形をフィット (周期=レンジゆえ sin の周期と不一致で 0.8 台)" $ do+      gamR2 (fitGAMWith [FourierB 4] 0.0 [xv70] yv70) `shouldSatisfy` (> 0.8)++    it "F1: RBF 基底で非線形を高 R² でフィット" $ do+      gamR2 (fitGAMWith [RBFB 8 1.0] 0.0 [xv70] yv70) `shouldSatisfy` (> 0.9)++    it "F1: 多項基底でも非線形をある程度フィット (R² > 0.7)" $ do+      gamR2 (fitGAMWith [PolyB 5] 0.0 [xv70] yv70) `shouldSatisfy` (> 0.7)++    it "F2: GCV が有限な λ を選び高 R² (gam 経由)" $ do+      let m = df70 |-> gam (GAMConfig (BSplineB 3 8) GCV) "x" "y"+          f = gamNFit m+      gamLambda f `shouldSatisfy` (\l -> not (isNaN l) && not (isInfinite l) && l >= 0)+      gamEdf f    `shouldSatisfy` (< 21)   -- edf < n+      gamR2 f     `shouldSatisfy` (> 0.95)++    it "F3: gam の toPlot は band(MBand) + line(MLine) の 2 layer を返す (CI 実装後)" $ do+      let m  = df70 |-> gam defaultGAMConfig "x" "y"+          ls = vsLayers (toPlot m)+      length ls `shouldBe` 2+      getFirst (lyKind (ls !! 0)) `shouldBe` Just MBand+      getFirst (lyKind (ls !! 1)) `shouldBe` Just MLine++    it "F3: gam の svGrid が grid 点数分の有限値 + CI 帯 (Just) を返す" $ do+      let m       = df70 |-> gam defaultGAMConfig "x" "y"+          (mu, b) = svGrid m 0.95 [1.0, 4.0, 7.0]+      length mu `shouldBe` 3+      all (\v -> not (isNaN v)) mu `shouldBe` True+      case b of+        Just (lo, hi) -> and (zipWith3 (\l u h -> l <= u && u <= h) lo mu hi) `shouldBe` True+        Nothing       -> expectationFailure "CI 帯 (Just) を期待"++    it "F3: 多予測子は第1予測子を軸に・他を訓練平均で固定して評価できる" $ do+      let dfMV = [ ("x1", gx70)+                 , ("x2", reverse gx70)+                 , ("y",  zipWith (\a c -> sin a + 0.05 * c) gx70 (reverse gx70)) ]+                 :: [(Text, [Double])]+          m       = dfMV |-> gamMulti (GAMConfig (BSplineB 3 5) (FixedL 0.1)) ["x1", "x2"] "y"+          (lo, hi) = svRange m+          (mu, _) = svGrid m 0.95 [lo, (lo + hi) / 2, hi]+      gamNNames m `shouldBe` ["x1", "x2"]+      length mu `shouldBe` 3+      all (\v -> not (isNaN v)) mu `shouldBe` True++  describe "Phase 70.5 項目 E (E1): GP/KRR/RFF 統合 (gp + GPConfig・df|->)" $ do+    -- 非線形 y = sin x を x∈[0,6] で 21 点 (帯/予測の数値検証用)。+    let ex = [ 0.3 * fromIntegral i | i <- [0 .. 20 :: Int] ] :: [Double]+        ey = map sin ex+        dfE = [ ("x", ex), ("y", ey) ] :: [(Text, [Double])]+        q   = [1.0, 3.0, 5.0]+        truth = map sin q+        -- 予測 RMSE (μ̂ vs 真値)。+        rmse cfg =+          let (mu, _) = gprPredict (dfE |-> gp cfg "x" "y") q+          in sqrt (sum (zipWith (\a b -> (a - b) ^ (2 :: Int)) mu truth) / 3)++    it "E1: defaultGP (厳密 GP・周辺尤度) が sin を高精度予測 (RMSE < 0.1)" $ do+      rmse defaultGP `shouldSatisfy` (< 0.1)++    it "E1: Gp 象限は事後分散 (Just) を返す・Ridge 象限は帯なし (Nothing)" $ do+      let (_, vGp)    = gprPredict (dfE |-> gp (GPConfig RBF Gp    AutoMarginalLik) "x" "y") q+          (_, vRidge) = gprPredict (dfE |-> gp (GPConfig RBF Krr AutoMarginalLik) "x" "y") q+      vRidge `shouldBe` Nothing+      case vGp of+        Just vs -> do length vs `shouldBe` 3+                      all (\v -> not (isNaN v) && v >= 0) vs `shouldBe` True+        Nothing -> expectationFailure "Gp 象限は Just 分散を返すべき"++    it "E1: KRR (Ridge) の μ̂ ≡ GP 事後平均 (同ハイパラでビット一致)" $ do+      let p   = defaultGPParams { gpLengthScale = 1.0, gpSignalVar = 1.0, gpNoiseVar = 0.05 }+          cfgG = GPConfig RBF Gp    (FixedHyper p)+          cfgR = GPConfig RBF Krr (FixedHyper p)+          (muG, _) = gprPredict (dfE |-> gp cfgG "x" "y") q+          (muR, _) = gprPredict (dfE |-> gp cfgR "x" "y") q+      muR `shouldBe` muG          -- KRR は GP の mean のみ (帯を捨てるだけ)++    it "E1: RFF 近似 (GpRff) が厳密 GP に概ね一致 (RMSE < 0.15)" $ do+      rmse (GPConfig RBF (GpRff 500 12345) AutoMarginalLik) `shouldSatisfy` (< 0.15)++    it "E1: RFF seed 純粋化 = 同 seed で完全再現・別 seed で別結果" $ do+      let pull s = fst (gprPredict (dfE |-> gp (GPConfig RBF (GpRff 300 s) AutoMarginalLik) "x" "y") q)+      pull 7 `shouldBe` pull 7+      pull 7 `shouldNotBe` pull 99++    it "E1: AutoCV (Gram-LOOCV) が有限な予測を返し高精度 (RMSE < 0.12)" $ do+      let (mu, _) = gprPredict (dfE |-> gp (GPConfig RBF Gp AutoCV) "x" "y") q+      all (\v -> not (isNaN v) && not (isInfinite v)) mu `shouldBe` True+      rmse (GPConfig RBF Gp AutoCV) `shouldSatisfy` (< 0.12)++    it "E1: Periodic + RFF 近似指定 → 厳密象限へフォールバック (gprMethod = Gp)" $ do+      let m = dfE |-> gp (GPConfig Periodic (GpRff 200 7) AutoMarginalLik) "x" "y"+      gprMethod m `shouldBe` Gp+      let (mu, v) = gprPredict m q+      all (\val -> not (isNaN val)) mu `shouldBe` True+      v `shouldSatisfy` (/= Nothing)   -- Gp へ落ちたので分布あり++    it "E1: Matern52 厳密 GP も sin を高精度予測" $ do+      rmse (GPConfig Matern52 Gp AutoMarginalLik) `shouldSatisfy` (< 0.1)++    -- E2: SingleVarModel + Plottable+    it "E2: Gp 象限の svGrid は credible 帯 (Just) を返す" $ do+      let m       = dfE |-> gp defaultGP "x" "y"+          (mu, b) = svGrid m 0.95 [1.0, 3.0, 5.0]+      length mu `shouldBe` 3+      case b of+        Just (los, his) -> do+          length los `shouldBe` 3+          and (zipWith3 (\l u h -> l <= u && u <= h) los mu his) `shouldBe` True+        Nothing -> expectationFailure "Gp 象限は帯を返すべき"++    it "E2: Ridge 象限の svGrid は帯なし (Nothing)" $ do+      let m      = dfE |-> gp (GPConfig RBF Krr AutoMarginalLik) "x" "y"+          (_, b) = svGrid m 0.95 [1.0, 3.0, 5.0]+      b `shouldBe` Nothing++    it "E2: Gp の toPlot は band + line の 2 layer・Ridge は line のみ 1 layer" $ do+      let mGp = dfE |-> gp defaultGP "x" "y"+          mRi = dfE |-> gp (GPConfig RBF Krr AutoMarginalLik) "x" "y"+      length (vsLayers (toPlot mGp)) `shouldBe` 2+      length (vsLayers (toPlot mRi)) `shouldBe` 1+      getFirst (lyKind (last (vsLayers (toPlot mGp)))) `shouldBe` Just MLine++    it "E2: svGridPI (事後予測分散) は CI より広い帯 (Gp 象限)" $ do+      let m         = dfE |-> gp defaultGP "x" "y"+          (_, mbCI) = svGrid m 0.95 [1.0, 3.0, 5.0]+          mbPI      = svGridPI m 0.95 [1.0, 3.0, 5.0]+      case (mbCI, mbPI) of+        (Just (lci, hci), Just (lpi, hpi)) ->+          and (zipWith (\(lc, hc) (lp, hp) -> (hp - lp) >= (hc - lc))+                       (zip lci hci) (zip lpi hpi))+            `shouldBe` True+        _ -> expectationFailure "Gp 象限は CI/PI 両帯を返すべき"++  describe "Phase 70.5 項目 E (E3): gpMulti 多変量 (df|->)" $ do+    -- y = sin x1 + 0.5 x2 を 2 予測子で。 第1予測子を軸に偏依存曲線を評価。+    let ex1 = [ 0.4 * fromIntegral i | i <- [0 .. 20 :: Int] ] :: [Double]+        ex2 = [ 0.2 * fromIntegral i | i <- [0 .. 20 :: Int] ] :: [Double]+        ey3 = zipWith (\a c -> sin a + 0.5 * c) ex1 ex2+        dfM = [ ("x1", ex1), ("x2", ex2), ("y", ey3) ] :: [(Text, [Double])]++    it "E3: gpMulti Gp は予測子名を保持し svGrid が credible 帯を返す" $ do+      let m       = dfM |-> gpMulti defaultGP ["x1", "x2"] "y"+          (lo, hi) = svRange m+          (mu, b) = svGrid m 0.95 [lo, (lo + hi) / 2, hi]+      gprnNames m `shouldBe` ["x1", "x2"]+      length mu `shouldBe` 3+      all (\v -> not (isNaN v)) mu `shouldBe` True+      b `shouldSatisfy` (/= Nothing)++    it "E3: gpMulti Ridge は帯なし (Nothing)" $ do+      let m      = dfM |-> gpMulti (GPConfig RBF Krr AutoMarginalLik) ["x1", "x2"] "y"+          (_, b) = svGrid m 0.95 [1.0, 3.0, 5.0]+      b `shouldBe` Nothing++    it "E3: gpMulti GpRff (MV RFF GP) も帯を返し有限・同 seed 再現" $ do+      let mk s = dfM |-> gpMulti (GPConfig RBF (GpRff 300 s) AutoMarginalLik) ["x1", "x2"] "y"+          (mu1, b1) = svGrid (mk 7) 0.95 [1.0, 3.0, 5.0]+          (mu2, _)  = svGrid (mk 7) 0.95 [1.0, 3.0, 5.0]+      all (\v -> not (isNaN v)) mu1 `shouldBe` True+      b1 `shouldSatisfy` (/= Nothing)+      mu1 `shouldBe` mu2++    it "E3: gpMulti の偏依存曲線が sin(x1) の山谷を捉える (x2 平均固定)" $ do+      let m       = dfM |-> gpMulti defaultGP ["x1", "x2"] "y"+          (mu, _) = svGrid m 0.95 [1.5, 4.5]   -- sin の山 (≈1) と谷 (≈−1)+      -- x2 を平均で固定すれば PD 曲線 ≈ sin(x1) + const ゆえ μ(1.5) > μ(4.5)。+      (mu !! 0 > mu !! 1) `shouldBe` True++    it "E3: gpMulti AutoCV も有限な多変量予測を返す" $ do+      let m       = dfM |-> gpMulti (GPConfig RBF Gp AutoCV) ["x1", "x2"] "y"+          (mu, _) = svGrid m 0.95 [1.0, 4.0, 7.0]+      all (\v -> not (isNaN v) && not (isInfinite v)) mu `shouldBe` True++  describe "Phase 70.7 項目 G (G1): 罰則付き回帰の高レベル df|-> 化" $ do+    -- y = 2 x1 + 0 x2 + 3 x3 + 小ノイズ。 x2 は無関係 (Lasso が 0 にすべき)。+    let n      = 60 :: Int+        x1d    = [ sin (0.3 * fromIntegral i) | i <- [0 .. n - 1] ] :: [Double]+        x2d    = [ cos (0.21 * fromIntegral i) | i <- [0 .. n - 1] ]   -- 無関係+        x3d    = [ sin (0.13 * fromIntegral i + 1.0) | i <- [0 .. n - 1] ]+        noised = [ 0.02 * sin (3.1 * fromIntegral i) | i <- [0 .. n - 1] ]+        yd     = zipWith3 (\a c e -> 2 * a + 3 * c + e) x1d x3d noised+        dfR    = [ ("x1", x1d), ("x2", x2d), ("x3", x3d), ("y", yd) ] :: [(Text, [Double])]+        rows   = [ [a, b, c] | (a, b, c) <- zip3 x1d x2d x3d ]+        rmseOf m = sqrt (sum (zipWith (\p t -> (p - t) ^ (2 :: Int)) (regPredict m rows) yd)+                          / fromIntegral n)++    it "G1: ridge (df|->) が名前を保持し元スケールで高精度予測 (RMSE 小)" $ do+      let m = dfR |-> ridge ["x1", "x2", "x3"] "y"+      rmgNames m `shouldBe` ["x1", "x2", "x3"]+      length (rmgCoefs m) `shouldBe` 3+      rmseOf m `shouldSatisfy` (< 0.3)++    it "G1: lasso が無関係な x2 を縮約 (|β₂| < |β₁|,|β₃|)" $ do+      let m = dfR |-> lasso ["x1", "x2", "x3"] "y"+          [b1, b2, b3] = map abs (rmgCoefs m)+      b2 `shouldSatisfy` (< b1)+      b2 `shouldSatisfy` (< b3)++    it "G1: FixedLambda 経路 — Ridge/Lasso/EN/MCP/SCAD/Adaptive すべて有限係数" $ do+      let mk meth = dfR |-> regularized (RegConfig meth (FixedLambda 0.1)) ["x1", "x2", "x3"] "y"+          finite m = all (\v -> not (isNaN v) && not (isInfinite v)) (rmgCoefs m)+      all finite [ mk Ridge, mk Lasso, mk (ElasticNet 0.5)+                 , mk (MCP 3.0), mk (SCAD 3.7), mk (AdaptiveLasso 1.0) ] `shouldBe` True++    it "G1: LambdaLOOCV は Ridge で成功・Lasso では Left (線形平滑器専用)" $ do+      let okRidge = fitEither (regularized (RegConfig Ridge LambdaLOOCV) ["x1","x2","x3"] "y") dfR+          noLasso = fitEither (regularized (RegConfig Lasso LambdaLOOCV) ["x1","x2","x3"] "y") dfR+      (case okRidge of Right _ -> True; Left _ -> False) `shouldBe` True+      (case (noLasso :: Either String RegModel) of Left _ -> True; Right _ -> False) `shouldBe` True++    it "G1: LambdaCV seed 再現性 (同 seed → 同 λ・別 seed で変わりうる)" $ do+      let mk s = dfR |-> regularized (RegConfig Lasso (LambdaCV 5 s)) ["x1","x2","x3"] "y"+      rmgLambda (mk 7) `shouldBe` rmgLambda (mk 7)++    it "G1: LambdaCV1SE の λ は LambdaCV(best) 以上 (より保守的=スパース)" $ do+      let best = rmgLambda (dfR |-> regularized (RegConfig Lasso (LambdaCV    5 7)) ["x1","x2","x3"] "y")+          one  = rmgLambda (dfR |-> regularized (RegConfig Lasso (LambdaCV1SE 5 7)) ["x1","x2","x3"] "y")+      one `shouldSatisfy` (>= best)++    it "G1: Group Lasso の群 ID 長さ不一致は Left" $ do+      let bad = fitEither (regularized (RegConfig (GroupLasso [0,0]) (FixedLambda 0.1)) ["x1","x2","x3"] "y") dfR+      (case (bad :: Either String RegModel) of Left _ -> True; Right _ -> False) `shouldBe` True++    it "G1: Group Lasso (群指定) も有限な係数で当てはまる" $ do+      let m = dfR |-> regularized (RegConfig (GroupLasso [0,1,0]) (FixedLambda 0.05)) ["x1","x2","x3"] "y"+      all (\v -> not (isNaN v)) (rmgCoefs m) `shouldBe` True++  describe "Phase 46 A9 + 70.C: RobustModel + toPlot (ロバスト直線 + CI 帯)" $ do+    -- y = 2x + 1 にして、 1 点だけ大きな外れ値を入れる。 ロバスト傾きが外れ値に+    -- 引っ張られず ≈ 2 に留まることを確認。+    let rxs = LA.fromList [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]+        rys = LA.fromList [3, 5, 7, 9, 11, 13, 15, 17, 19, 100]  -- 末尾が外れ値 (本来 21)+        rm  = robustModel (Huber defaultHuberK) rxs rys+        rLine = head [ l | l <- vsLayers (toPlot rm), getFirst (lyKind l) == Just MLine ]++    it "toPlot は MBand + MLine の 2 layer (CI 帯付き・Phase 70.C で揃えた)" $ do+      let ls = vsLayers (toPlot rm)+      map (getFirst . lyKind) ls `shouldBe` [Just MBand, Just MLine]++    it "ロバスト傾き ≈ 2 (外れ値に引っ張られない)" $ do+      -- 直線 2 点から傾きを復元 (encX/encY は昇順)。+      case (getLast (lyEncX rLine), getLast (lyEncY rLine)) of+        (Just (ColNum vx), Just (ColNum vy)) -> do+          let xs = V.toList vx; ys = V.toList vy+              slope = (last ys - head ys) / (last xs - head xs)+          abs (slope - 2) `shouldSatisfy` (< 0.2)+        _ -> expectationFailure "encX/encY が inline ColNum でない"++    it "外れ値 (末尾) の IRLS 重みは健全点より小さい" $ do+      let ws = LA.toList (rfWeights (rmFit rm))+      last ws `shouldSatisfy` (< minimum (init ws) + 1e-9)++    -- 検証: Huber の k を巨大にすると全点 inlier (ψ'=1, ψ(u)=u) ゆえ、 サンドイッチ+    -- 共分散は σ が打ち消えて OLS 共分散 (Σr²/(n-p)·(XᵀX)⁻¹) に厳密一致する。+    -- = statsmodels RLM cov="H1" の OLS 極限。 これを数値で実測する。+    it "robustCovBeta(Huber 巨大k) の SE は OLS SE に厳密一致 (サンドイッチ→OLS極限)" $ do+      let n      = LA.rows xX+          xX     = LA.fromColumns [ LA.konst 1 (LA.size rxs), rxs ]   -- [1, x]+          xtxInv = LA.inv (LA.tr xX LA.<> xX)+          betaO  = xtxInv LA.#> (LA.tr xX LA.#> rys)+          residO = rys - (xX LA.#> betaO)+          sigma2 = (residO LA.<.> residO) / fromIntegral (n - 2)+          olsSE  = map sqrt (LA.toList (LA.takeDiag (LA.scale sigma2 xtxInv)))+          rfit   = fitRobustLM (Huber 1e6) xX rys 50 1e-9+          rcov   = robustCovBeta (Huber 1e6) (rfScale rfit) (rfResiduals rfit) xX+          rSE    = map sqrt (LA.toList (LA.takeDiag rcov))+      rSE `shouldSatisfy` allClose olsSE++    -- ★statsmodels RLM (HuberT t=1.345, scale=mad, cov="H1") との実測突合。+    -- 参照値 (statsmodels 0.14.6):+    --   params = [0.83418235, 2.06340481]、 scale = 0.64004295、 bse = [0.41150687, 0.06632034]+    it "robust SE/係数/scale が statsmodels RLM (HuberT, cov=H1) に一致" $ do+      let vx   = LA.fromList [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]+          vy   = LA.fromList [3.4, 4.6, 7.5, 8.7, 11.3, 12.6, 15.4, 16.8, 19.2, 45]+          xX   = LA.fromColumns [ LA.konst 1 (LA.size vx), vx ]+          rfit = fitRobustLM (Huber 1.345) xX vy 100 1e-10+          se   = map sqrt (LA.toList (LA.takeDiag+                   (robustCovBeta (Huber 1.345) (rfScale rfit) (rfResiduals rfit) xX)))+          near tol a b = abs (a - b) <= tol * (1 + abs b)+          closeTo tol ref xs = length xs == length ref && and (zipWith (near tol) xs ref)+      LA.toList (rfCoef rfit) `shouldSatisfy` closeTo 1e-3 [0.83418235, 2.06340481]+      rfScale rfit            `shouldSatisfy` near    1e-3 0.64004295+      se                      `shouldSatisfy` closeTo 1e-3 [0.41150687, 0.06632034]++    it "diagnosticPlots は 3 枚 (直線 + 残差 + 重み encode 散布図)" $ do+      let ds = diagnosticPlots rm+      length ds `shouldBe` 3+      -- 3 枚目に size encoding (lySizeBy) が乗っている。+      let wLayer = head (vsLayers (ds !! 2))+      getLast (lySizeBy wLayer) `shouldSatisfy` \x -> case x of+        Just (ColNum _) -> True+        _               -> False++  describe "Phase 46 A9: MultiFit + toPlot (残差相関 heatmap)" $ do+    -- 3 出力。 各出力に残差を残すため決定的な wiggle を加える (完全 fit だと残差分散 0+    -- → 相関が NaN になる)。 共有 wiggle で out1/out2 は正相関、 out3 は逆相関にする。+    let nObs = 20+        xcol = [ fromIntegral i | i <- [1 .. nObs] ] :: [Double]+        wig  = [ sin (fromIntegral i) | i <- [1 .. nObs] ] :: [Double]+        xmat = LA.fromColumns [LA.konst 1 nObs, LA.fromList xcol]   -- n×2 (intercept+x)+        y1   = zipWith (\x w -> 2*x + w)      xcol wig+        y2   = zipWith (\x w -> x   + 0.8*w)  xcol wig             -- wig 共有 → +相関+        y3   = zipWith (\x w -> -x  - w)      xcol wig             -- -wig → 逆相関+        ymat = LA.fromColumns (map LA.fromList [y1, y2, y3])        -- n×3+        mf   = fitMultiLM xmat ymat+        hLayer = head (vsLayers (toPlot mf))+        -- heatmap は categorical 軸: encX/encY は出力名ラベル (ColTxt inline)。+        txtH f = case getLast (f hLayer) of+          Just (ColTxt v) -> V.toList v+          _               -> error "encoding が inline ColTxt でない"+        -- value は lyColor = ColorByContinuous (inline) に入る。+        valsH = case getLast (lyColor hLayer) of+          Just (ColorByContinuous (ColNum v)) -> V.toList v+          _ -> error "color が ColorByContinuous inline でない"++    it "toPlot は MHeatmap layer を 1 つ返す" $ do+      length (vsLayers (toPlot mf)) `shouldBe` 1+      getFirst (lyKind hLayer) `shouldBe` Just MHeatmap++    it "heatmap は q×q = 9 セル・軸は出力名ラベル (q=3 出力)" $ do+      length (txtH lyEncX) `shouldBe` 9+      txtH lyEncX `shouldSatisfy` all (`elem` ["y1", "y2", "y3"])++    it "対角は相関 = 1 (自己相関)" $ do+      -- セルは (x=yⱼ, y=yᵢ)。 対角 = x==y のラベル一致セル。+      let xs = txtH lyEncX; ys = txtH lyEncY; vs = valsH+          diag = [ v | (x, y, v) <- zip3 xs ys vs, x == y ]+      diag `shouldSatisfy` allClose [1, 1, 1]++    it "out1-out2 は正相関、 out1-out3 は逆相関 (符号検証)" $ do+      let xs = txtH lyEncX; ys = txtH lyEncY; vs = valsH+          at i j = head [ v | (x, y, v) <- zip3 xs ys vs, x == j, y == i ]+      at "y1" "y2" `shouldSatisfy` (> 0)   -- out1 vs out2+      at "y1" "y3" `shouldSatisfy` (< 0)   -- out1 vs out3++  describe "Phase 46 A10: QuantileModel + toPlot (複数分位線・色分け重畳)" $ do+    -- heteroscedastic データ: 分散が x で増える → 分位線が末広がりになる。+    let qxs = LA.fromList [ fromIntegral i | i <- [1 .. 40 :: Int] ]+        qys = LA.fromList [ 2 * x + (x / 8) * sin (fromIntegral i)+                          | (i, x) <- zip [1 :: Int ..] (LA.toList qxs) ]+        qm  = quantileModel [0.1, 0.5, 0.9] qxs qys+        qLayers = vsLayers (toPlot qm)++    it "toPlot は分位数ぶんの MLine layer を返す (3 本)" $ do+      length qLayers `shouldBe` 3+      map (getFirst . lyKind) qLayers `shouldSatisfy` all (== Just MLine)++    it "各 line layer に固定色 (ColorStatic) が付く" $ do+      qLayers `shouldSatisfy` all (\l -> case getLast (lyColor l) of+        Just (ColorStatic _) -> True+        _                    -> False)++    it "分位は単調: 全 x で τ=0.1 の ŷ ≤ τ=0.9 の ŷ" $ do+      let yhatOf t = head [ V.toList (V.fromList (LA.toList (qfYHat f)))+                          | (tt, f) <- qmFits qm, tt == t ]+          lo = yhatOf 0.1; hi = yhatOf 0.9+      and (zipWith (<=) lo hi) `shouldBe` True++    it "中央値 (τ=0.5) の傾き ≈ 2" $ do+      let med = head [ f | (tt, f) <- qmFits qm, tt == 0.5 ]+          b   = LA.toList (qfBeta med)+      abs (b !! 1 - 2) `shouldSatisfy` (< 0.3)++  describe "Phase 46 A11: ChainModel + toPlot (trace + 周辺事後密度)" $ do+    -- AR(1) 風の決定的な draw 列 (平均 5 まわりに揺れる)。 1 パラメータ "mu"。+    let draws = take 100 (iterate (\v -> 5 + 0.7 * (v - 5) + 0.5 * sin v) 4.0)+        ch    = Chain { chainSamples     = [ Map.singleton "mu" v | v <- draws ]+                      , chainAccepted    = 100+                      , chainTotal       = 120+                      , chainEnergy      = []+                      , chainDivergences = []+                      , chainTreeDepths  = [] }+        cm     = chainModel "mu" ch+        tLayer = head (vsLayers (toPlot cm))++    it "toPlot は trace (MTrace) layer を 1 つ返す" $ do+      length (vsLayers (toPlot cm)) `shouldBe` 1+      getFirst (lyKind tLayer) `shouldBe` Just MTrace++    it "trace の encX は draw index 1..N、 encY は draw 値" $ do+      case (getLast (lyEncX tLayer), getLast (lyEncY tLayer)) of+        (Just (ColNum vx), Just (ColNum vy)) -> do+          V.toList vx `shouldSatisfy` allClose [ fromIntegral i | i <- [1 .. length draws] ]+          V.toList vy `shouldSatisfy` allClose draws+        _ -> expectationFailure "encX/encY が inline ColNum でない"++    it "diagnosticPlots は 2 枚 (trace + density)" $ do+      let ds = diagnosticPlots cm+      length ds `shouldBe` 2+      getFirst (lyKind (head (vsLayers (ds !! 0)))) `shouldBe` Just MTrace+      getFirst (lyKind (head (vsLayers (ds !! 1)))) `shouldBe` Just MDensity++  describe "Phase 46 A12: KMResult + toPlot (KM 生存曲線・階段)" $ do+    let samples = [ SurvSample t e+                  | (t, e) <- [ (1, Observed), (2, Observed), (3, Censored)+                              , (4, Observed), (5, Observed), (6, Censored)+                              , (7, Observed), (8, Observed) ] ]+        km  = kaplanMeier samples+        kLayer = head (vsLayers (toPlot km))+        kY  = case getLast (lyEncY kLayer) of+          Just (ColNum v) -> V.toList v+          _               -> error "encY が ColNum でない"++    it "toPlot は MLine layer を 1 つ返す (階段)" $ do+      length (vsLayers (toPlot km)) `shouldBe` 1+      getFirst (lyKind kLayer) `shouldBe` Just MLine++    it "生存曲線は S=1 から始まり単調非増加 (∈ [0,1])" $ do+      head kY `shouldBe` 1.0+      and (zipWith (>=) kY (drop 1 kY)) `shouldBe` True+      kY `shouldSatisfy` all (\s -> s >= 0 && s <= 1)++  describe "Phase 46 A12: CRFit + toPlot (競合リスク CIF・色分け階段)" $ do+    -- 2 cause + 打ち切り (cause 0)。+    let crs = [ CRSample t c+              | (t, c) <- [ (1, 1), (2, 2), (3, 1), (4, 0), (5, 2)+                          , (6, 1), (7, 0), (8, 2), (9, 1), (10, 2) ] ]+        cr  = fitCompetingRisks crs+        crLayers = vsLayers (toPlot cr)+        firstY = case getLast (lyEncY (head crLayers)) of+          Just (ColNum v) -> V.toList v+          _               -> error "encY が ColNum でない"++    it "toPlot は cause 数ぶんの MLine layer を返す (2 本)" $ do+      length crLayers `shouldBe` 2+      map (getFirst . lyKind) crLayers `shouldSatisfy` all (== Just MLine)++    it "各 cause CIF は 0 から始まり単調非減少" $ do+      head firstY `shouldBe` 0.0+      and (zipWith (<=) firstY (drop 1 firstY)) `shouldBe` True++    it "各 line layer に固定色 (ColorStatic) が付く" $ do+      crLayers `shouldSatisfy` all (\l -> case getLast (lyColor l) of+        Just (ColorStatic _) -> True+        _                    -> False)++  describe "Phase 46 A13: ForecastModel + toPlot (AR 予測 + 予測区間 band)" $ do+    -- 定常 AR(1) 風の系列。 h=8 step 予測。+    let series = LA.fromList [ 10 + 3 * sin (fromIntegral i * 0.4) + 0.5 * cos (fromIntegral i)+                             | i <- [1 .. 60 :: Int] ]+        fm    = forecastModel 2 8 series+        layers = vsLayers (toPlot fm)+        bandL = head [ l | l <- layers, getFirst (lyKind l) == Just MBand ]+        bandLo f = case getLast (f bandL) of+          Just (ColNum v) -> V.toList v+          _               -> error "band encoding が ColNum でない"++    it "toPlot は band (MBand) + line 2 本 (履歴 + 予測) を返す" $ do+      length layers `shouldBe` 3+      length [ l | l <- layers, getFirst (lyKind l) == Just MBand ] `shouldBe` 1+      length [ l | l <- layers, getFirst (lyKind l) == Just MLine ] `shouldBe` 2++    it "band は h=8 点、 上境界 ≥ 下境界" $ do+      let lo = bandLo lyEncY; hi = bandLo lyEncY2+      length lo `shouldBe` 8+      and (zipWith (<=) lo hi) `shouldBe` True++    it "予測区間幅は地平とともに単調増加 (se が h で広がる)" $ do+      let lo = bandLo lyEncY; hi = bandLo lyEncY2+          widths = zipWith (-) hi lo+      and (zipWith (<=) widths (drop 1 widths)) `shouldBe` True++  describe "Phase 46 A14: PCAResult + toPlot (scree plot)" $ do+    -- 第 1 軸に強い分散・第 2 軸に弱い分散を持たせた 3 次元データ。+    let rows = [ [ 5 * sin (fromIntegral i * 0.3)+                 , 1.2 * cos (fromIntegral i * 0.5)+                 , 0.3 * sin (fromIntegral i) ]+               | i <- [1 .. 40 :: Int] ]+        xmat = LA.fromLists rows+        res  = PCALow.pca CenterScale Nothing xmat+        pLayer = head (vsLayers (toPlot res))++    it "toPlot は bar (MBar) layer を 1 つ返す" $ do+      length (vsLayers (toPlot res)) `shouldBe` 1+      getFirst (lyKind pLayer) `shouldBe` Just MBar++    it "棒は PC ラベル軸・寄与率は降順 (PCA は分散順)" $ do+      case getLast (lyEncY pLayer) of+        Just (ColNum v) -> do+          let ratios = V.toList v+          and (zipWith (>=) ratios (drop 1 ratios)) `shouldBe` True+          abs (sum ratios - 1) `shouldSatisfy` (< 1e-6)   -- 全成分で和=1+        _ -> expectationFailure "encY が ColNum でない"++  describe "Phase 46 A14 / 75.24: RandomForest + toPlot (2 パネル importance)" $ do+    it "toPlot は 2 パネル (impurity/permutation)・impurity は特徴数ぶんの bar・非負" $ do+      -- y は主に x0 で決まる → x0 の重要度が高いはず (構造のみ検証)。+      let xss = [ [ fromIntegral i, sin (fromIntegral i) ] | i <- [1 .. 50 :: Int] ]+          ys  = [ 2 * fromIntegral i | i <- [1 .. 50 :: Int] ]+      gen <- MWC.createSystemRandom+      rf  <- fitRF defaultRandomForest xss ys gen+      let panels = vsSubplots (toPlot rf)+      length panels `shouldBe` 2                          -- R varImpPlot 流 2 パネル+      let impLayer = head (vsLayers (head panels))        -- 左 = impurity+      getFirst (lyKind impLayer) `shouldBe` Just MBar+      case getLast (lyEncY impLayer) of+        Just (ColNum v) -> do+          V.length v `shouldBe` 2                         -- 2 特徴+          V.toList v `shouldSatisfy` all (>= 0)           -- impurity 重要度 ≥ 0+        _ -> expectationFailure "encY が ColNum でない"++  describe "Phase 16 §3-D: モデル API 層 (predict / describe / coefficients)" $ do+    -- m は冒頭の LMModel (y = 2x + 1)。+    it "modelCoefficients == coefficientsV (LM)" $ do+      modelCoefficients m `shouldSatisfy` allClose (LA.toList (coefficientsV (lmResult m)))++    it "predictPoint LM (訓練点) == fitted (線形予測の整合)" $ do+      let preds = map (predictPoint m) (LA.toList xs)+      preds `shouldSatisfy` allClose (LA.toList (fittedV (lmResult m)))++    it "describeModel: 係数値=modelCoefficients・SE≥0・CI が値を含む" $ do+      let cs = describeModel m+      map coefValue cs `shouldSatisfy` allClose (modelCoefficients m)+      cs `shouldSatisfy` all (\c -> coefSE c >= 0)+      cs `shouldSatisfy` all (\c -> let (lo, hi) = coefCI c+                                    in lo <= coefValue c && coefValue c <= hi)++    it "predictPoint GLM (訓練点) == 逆リンク後の μ̂ (Poisson/Log)" $ do+      let gxs = LA.fromList [1, 2, 3, 4, 5, 6]+          gys = LA.fromList [1, 2, 4, 7, 12, 20]+          gm  = glmModel Poisson Log gxs gys+          preds = map (predictPoint gm) (LA.toList gxs)+      preds `shouldSatisfy` allClose (LA.toList (fittedV (glmResult gm)))++  describe "Phase 16 §3 C1: grid 評価 (滑らかな曲線・ModelSpec)" $ do+    -- m = 冒頭の LMModel (y = 2x + 1, x ∈ [1..5])。 grid 評価は訓練点数と独立。+    let lineLayerOf spec = head [ l | l <- vsLayers (toPlot spec)+                                    , getFirst (lyKind l) == Just MLine ]+        numX l = case getLast (lyEncX l) of+                   Just (ColNum v) -> V.toList v+                   _               -> error "encX が ColNum でない"+        numY l = case getLast (lyEncY l) of+                   Just (ColNum v) -> V.toList v+                   _               -> error "encY が ColNum でない"++    it "grid n で曲線の頂点数 = n (訓練点数 5 と独立)" $ do+      length (numX (lineLayerOf (statModel m <> grid 25))) `shouldBe` 25++    it "既定 grid = 100 点" $ do+      length (numX (lineLayerOf (statModel m))) `shouldBe` 100++    it "grid 範囲の既定は説明変数 min/max (= [1, 5])" $ do+      let xg = numX (lineLayerOf (statModel m <> grid 50))+      head xg `shouldSatisfy` (\v -> abs (v - 1) < 1e-9)+      last xg `shouldSatisfy` (\v -> abs (v - 5) < 1e-9)++    it "gridRange lo hi が評価範囲を上書きする" $ do+      let xg = numX (lineLayerOf (statModel m <> grid 11 <> gridRange 0 10))+      head xg `shouldSatisfy` (\v -> abs v < 1e-9)+      last xg `shouldSatisfy` (\v -> abs (v - 10) < 1e-9)++    it "grid 上の μ̂ = predictPoint (= fit と整合・補間でない)" $ do+      let l  = lineLayerOf (statModel m <> grid 7)+      numY l `shouldSatisfy` allClose (map (predictPoint m) (numX l))++    it "LM grid: 曲線は 2x+1 (完全線形データを正しく外挿)" $ do+      let l = lineLayerOf (statModel m <> grid 9)+      numY l `shouldSatisfy` allClose (map (\x -> 2 * x + 1) (numX l))++    it "既定 (帯 ON, Phase 70.E) は MBand + MLine の 2 layer" $ do+      let ls = vsLayers (toPlot (statModel m <> grid 20))+      map (getFirst . lyKind) ls `shouldMatchList` [Just MBand, Just MLine]++    it "bandMode BandOff で帯が消え MLine 1 layer のみ" $ do+      let ls = vsLayers (toPlot (statModel m <> grid 20 <> bandMode BandOff))+      map (getFirst . lyKind) ls `shouldBe` [Just MLine]++    it "bandMode BandCIPI で CI+PI 入れ子 (MBand 2 + MLine 1)・PI ⊃ CI" $ do+      let ls    = vsLayers (toPlot (statModel m <> grid 20 <> bandMode BandCIPI))+          bands = [ l | l <- ls, getFirst (lyKind l) == Just MBand ]+          loY l = case getLast (lyEncY l) of+            Just (ColNum v) -> V.toList v+            _               -> error "encY が ColNum でない"+      map (getFirst . lyKind) ls `shouldMatchList` [Just MBand, Just MBand, Just MLine]+      -- 1 本目が PI (下に描く=広い)、 2 本目が CI。 PI 下限 < CI 下限。+      case bands of+        [pib, cib] -> and (zipWith (<=) (loY pib) (loY cib)) `shouldBe` True+        _          -> expectationFailure "MBand が 2 本でない"++    it "オプションのみ (モデル無し) は空図" $ do+      vsLayers (toPlot (grid 50 <> bandMode BandOff)) `shouldBe` []++    it "GAM grid は CI 帯 + 線 (Phase 70.6 G・grid 点数反映)" $ do+      let gm = gamModel 3 5 0.0+                 (LA.fromList [0, 1, 2, 3, 4, 5, 6, 7, 8])+                 (LA.fromList [0, 1, 4, 9, 16, 25, 36, 49, 64])+          ls = vsLayers (toPlot (statModel gm <> grid 30))+      map (getFirst . lyKind) ls `shouldMatchList` [Just MBand, Just MLine]+      length (numX (head ls)) `shouldBe` 30   -- band も line も grid 30 点++    -- Phase 52 A2: 線/帯 aes setter (color/fill/linetype/linewidth/alpha)+    let bandLayerOf spec = head [ l | l <- vsLayers (toPlot spec)+                                    , getFirst (lyKind l) == Just MBand ]++    it "A2 statColor: 線レイヤに固定色 (ColorStatic)" $ do+      getLast (lyColor (lineLayerOf (statModel m <> statColor (fromHex "#ff0000"))))+        `shouldBe` Just (ColorStatic "#ff0000")++    it "A2 statLinetype: 線レイヤに固定線種" $ do+      getLast (lyLinetype (lineLayerOf (statModel m <> statLinetype LtDashed)))+        `shouldBe` Just LtDashed++    it "A2 statLinewidth: 線レイヤに stroke 幅" $ do+      getLast (lyStroke (lineLayerOf (statModel m <> statLinewidth 2.5)))+        `shouldBe` Just 2.5++    it "A2 statFill + statAlpha: 帯レイヤに塗り色 + 透明度" $ do+      let spec = statModel m <> statFill (fromHex "#4682b4") <> statAlpha 0.2+          b    = bandLayerOf spec+      getLast (lyColor b) `shouldBe` Just (ColorStatic "#4682b4")+      getLast (lyAlpha b) `shouldBe` Just 0.2++    it "A2 statColor は帯 fill に漏れない (線のみ・帯は statFill)" $ do+      let spec = statModel m <> statColor (fromHex "#ff0000")+      getLast (lyColor (lineLayerOf spec)) `shouldBe` Just (ColorStatic "#ff0000")+      getLast (lyColor (bandLayerOf spec)) `shouldBe` Nothing++    -- Phase 52 A3: statLabel (単線命名・凡例)+    it "A3 statLabel: 線は ColorByCol (固定色でなく凡例が出る encoding)" $ do+      let l = lineLayerOf (statModel m <> statLabel "OLS")+      case getLast (lyColor l) of+        Just (ColorByCol _) -> pure ()+        other               -> expectationFailure ("ColorByCol を期待: " ++ show other)++    it "A3 statLabel: scaleColorManual で色固定 (既定パレット先頭) + legend 有効" $ do+      let vs = toPlot (statModel m <> statLabel "OLS")+      getLast (vsColorManual vs) `shouldBe` Just [("OLS", "#1f77b4")]+      getLast (vsLegend vs) `shouldSatisfy` \x -> case x of+        Just _  -> True+        Nothing -> False++    it "A3 statLabel + statColor: scaleColorManual の色は statColor" $ do+      let vs = toPlot (statModel m <> statLabel "OLS" <> statColor (fromHex "#aa0000"))+      getLast (vsColorManual vs) `shouldBe` Just [("OLS", "#aa0000")]++    it "A3 statLabel 無し: scaleColorManual も legend も付かない" $ do+      let vs = toPlot (statModel m)+      getLast (vsColorManual vs) `shouldBe` Nothing+      getLast (vsLegend vs) `shouldBe` Nothing++    -- Phase 52 A8: statEquation / statR2 (回帰式/R² 凡例注釈)。+    -- m = y = 2x + 1 (intercept 1・slope 2・R²=1)。 A3 と同じ ColorByCol+scaleColorManual 経路。+    it "A8 statEquation: 凡例ラベルが回帰式 'y = 1.000 + 2.000x'" $ do+      let vs = toPlot (statModel m <> statEquation)+      getLast (vsColorManual vs) `shouldBe` Just [("y = 1.000 + 2.000x", "#1f77b4")]+      getLast (vsLegend vs) `shouldSatisfy` \x -> case x of Just _ -> True; Nothing -> False++    it "A8 statR2: 凡例ラベルが 'R² = 1.000'" $ do+      let vs = toPlot (statModel m <> statR2)+      getLast (vsColorManual vs) `shouldBe` Just [("R² = 1.000", "#1f77b4")]++    it "A8 statEquation + statR2: 1 ラベルに連結 'y = … , R² = …'" $ do+      let vs = toPlot (statModel m <> statEquation <> statR2)+      getLast (vsColorManual vs)+        `shouldBe` Just [("y = 1.000 + 2.000x, R² = 1.000", "#1f77b4")]++    it "A8 statLabel 明示は autoLabel より優先 (式に上書きされない)" $ do+      let vs = toPlot (statModel m <> statEquation <> statLabel "OLS")+      getLast (vsColorManual vs) `shouldBe` Just [("OLS", "#1f77b4")]++    it "A8 注釈無し: scaleColorManual は付かない (オプトイン)" $ do+      getLast (vsColorManual (toPlot (statModel m))) `shouldBe` Nothing++    it "A8 svCoefR2 を持たないモデル (GAM) は式注釈が出ない" $ do+      -- GAM は svCoefR2 = Nothing ゆえ statEquation を付けても凡例ラベルなし。+      let gamM = gamModel 3 5 0.0 (LA.fromList [1, 2, 3, 4, 5])+                                  (LA.fromList [1, 4, 9, 16, 25])+          vs   = toPlot (statModel gamM <> statEquation)+      getLast (vsColorManual vs) `shouldBe` Nothing++  describe "Phase 16 §3 C2: predAt (予測点・点 + CI エラーバー)" $ do+    -- m = 冒頭の LMModel (y = 2x + 1)。 band ありモデルは lineRange + scatter。+    let kindsOf spec = map (getFirst . lyKind) (vsLayers (toPlot spec))+        layerOfKind k spec = head [ l | l <- vsLayers (toPlot spec)+                                      , getFirst (lyKind l) == Just k ]+        numOf f l = case getLast (f l) of+          Just (ColNum v) -> V.toList v+          _               -> error "encoding が ColNum でない"++    it "predAt 1 点: band + line に lineRange + scatter が加わる (LM)" $ do+      kindsOf (statModel m <> grid 20 <> predAt 3)+        `shouldMatchList` [Just MBand, Just MLine, Just MLineRange, Just MScatter]++    it "predAt はリスト累積 (<> で複数点): scatter に 3 点" $ do+      let sc = layerOfKind MScatter (statModel m <> predAt 1 <> predAt 3 <> predAt 5)+      numOf lyEncX sc `shouldSatisfy` allClose [1, 3, 5]++    it "予測点 μ̂ = predictPoint (= D の点予測と一致・LM 2x+1)" $ do+      let sc = layerOfKind MScatter (statModel m <> predAt 2 <> predAt 4)+      numOf lyEncY sc `shouldSatisfy` allClose [5, 9]  -- 2·2+1, 2·4+1++    it "CI エラーバー (lineRange) の中心は区間中点・半幅 ≥ 0 (LM=対称)" $ do+      let lr = layerOfKind MLineRange (statModel m <> predAt 3)+      numOf lyErrorY lr `shouldSatisfy` all (>= 0)+      -- LM は対称ゆえ中点 = μ̂ = 7+      numOf lyEncY  lr `shouldSatisfy` allClose [7]++    it "GLM predAt: μ̂ は逆リンク後 μ̂、 lineRange 区間は μ̂ を内包 (非対称可)" $ do+      let gxs = LA.fromList [1, 2, 3, 4, 5, 6]+          gys = LA.fromList [1, 2, 4, 7, 12, 20]+          gm  = glmModel Poisson Log gxs gys+          spec = statModel gm <> predAt 4+          sc  = layerOfKind MScatter   spec+          lr  = layerOfKind MLineRange spec+          mu  = head (numOf lyEncY sc)+          mid = head (numOf lyEncY lr)+          haf = head (numOf lyErrorY lr)+      -- μ̂ は区間 [mid-haf, mid+haf] = [lo, hi] 内+      (mid - haf - 1e-9 <= mu && mu <= mid + haf + 1e-9) `shouldBe` True++    it "GAM predAt: CI 帯ありゆえ band + line + lineRange + scatter (Phase 70.6 G)" $ do+      let gm = gamModel 3 5 0.0+                 (LA.fromList [0, 1, 2, 3, 4, 5, 6, 7, 8])+                 (LA.fromList [0, 1, 4, 9, 16, 25, 36, 49, 64])+      kindsOf (statModel gm <> predAt 4)   -- GAM に CI 実装後は LM と同形+        `shouldMatchList` [Just MBand, Just MLine, Just MLineRange, Just MScatter]++  describe "Phase 16 §3 C3: 多変量 effect plot (statModelMulti + holdAt + byVar)" $ do+    -- y = 1 + 2·x1 + 3·x2 + 4·x3 (厳密線形・無誤差)。 12 行 = x1∈{1,2,3} × x2∈{0,1} × x3∈{0,1}。+    -- 設計フルランクゆえ OLS は β=[1,2,3,4] を厳密復元 → 評価点 μ̂ も厳密。+    let dfMV = DX.fromNamedColumns+          [ ("y",  DX.fromList ([3,7,6,10, 5,9,8,12, 7,11,10,14] :: [Double]))+          , ("x1", DX.fromList ([1,1,1,1, 2,2,2,2, 3,3,3,3] :: [Double]))+          , ("x2", DX.fromList ([0,0,1,1, 0,0,1,1, 0,0,1,1] :: [Double]))+          , ("x3", DX.fromList ([0,1,0,1, 0,1,0,1, 0,1,0,1] :: [Double]))+          ]+        mlm = either error id (multiLMModel "y ~ x1 + x2 + x3" dfMV)+        lineLayersOf spec = [ l | l <- vsLayers (toPlot spec)+                                , getFirst (lyKind l) == Just MLine ]+        bandLayersOf spec = [ l | l <- vsLayers (toPlot spec)+                                , getFirst (lyKind l) == Just MBand ]+        firstLine spec = head (lineLayersOf spec)+        numXof l = case getLast (lyEncX l) of+                     Just (ColNum v) -> V.toList v+                     _               -> error "encX が ColNum でない"+        numYof l = case getLast (lyEncY l) of+                     Just (ColNum v) -> V.toList v+                     _               -> error "encY が ColNum でない"+        kindsMV spec = map (getFirst . lyKind) (vsLayers (toPlot spec))++    it "along x1 (既定 帯 ON, 既定 holdAt Mean): MBand + MLine の 2 layer・頂点 100" $ do+      let spec = statModelMulti mlm (along "x1")+      kindsMV spec `shouldMatchList` [Just MBand, Just MLine]+      length (numXof (firstLine spec)) `shouldBe` 100++    it "Phase 70.G: 重回帰 effect plot も BandCIPI で CI+PI 入れ子 (PI ⊃ CI)" $ do+      -- σ̂²>0 のノイズ入り重回帰 (exact-linear だと PI=CI になるため別データ)。+      let nz   = cycle [0.4,-0.5,0.3,-0.2,0.6,-0.4,0.2,-0.3]+          ys'  = zipWith3 (\a b e -> 1 + 2*a + 1.5*b + e)+                          ([1..12] :: [Double]) (cycle [0,1,2]) (take 12 nz)+          dfN  = DX.fromNamedColumns+                   [ ("y",  DX.fromList ys')+                   , ("x1", DX.fromList ([1..12] :: [Double]))+                   , ("x2", DX.fromList (take 12 (cycle [0,1,2]) :: [Double])) ]+          mN   = either error id (multiLMModel "y ~ x1 + x2" dfN)+          spec = statModelMulti mN (along "x1") <> grid 5 <> bandMode BandCIPI+          bands = bandLayersOf spec+          halfOf l = case (getLast (lyEncY l), getLast (lyEncY2 l)) of+            (Just (ColNum lo), Just (ColNum hi)) -> zipWith (-) (V.toList hi) (V.toList lo)+            _ -> error "band encoding が ColNum でない"+      length bands `shouldBe` 2          -- PI (外) + CI (内)+      -- bands[0]=PI が bands[1]=CI より各点で広い (PI ⊃ CI)。+      case bands of+        [pib, cib] -> and (zipWith (>=) (halfOf pib) (halfOf cib)) `shouldBe` True+        _          -> expectationFailure "MBand が 2 本でない"++    it "along grid 範囲は x1 の観測 min/max = [1, 3]" $ do+      let xg = numXof (firstLine (statModelMulti mlm (along "x1") <> grid 5))+      head xg `shouldSatisfy` (\v -> abs (v - 1) < 1e-9)+      last xg `shouldSatisfy` (\v -> abs (v - 3) < 1e-9)++    it "holdAt Mean: 曲線 = 4.5 + 2·x1 (x2,x3 を平均 0.5 で固定)" $ do+      let l = firstLine (statModelMulti mlm (along "x1") <> grid 9)+      numYof l `shouldSatisfy` allClose (map (\x -> 4.5 + 2 * x) (numXof l))++    it "holdAt (Fixed x2=1, x3=1): 曲線 = 8 + 2·x1" $ do+      let l = firstLine (statModelMulti mlm (along "x1") <> grid 7+                          <> holdAt (Fixed [("x2", 1), ("x3", 1)]))+      numYof l `shouldSatisfy` allClose (map (\x -> 8 + 2 * x) (numXof l))++    it "holdAt (Fixed x2=0) 部分指定: x3 は Mean 0.5 のまま → 曲線 = 3 + 2·x1" $ do+      let l = firstLine (statModelMulti mlm (along "x1") <> grid 7+                          <> holdAt (Fixed [("x2", 0)]))+      numYof l `shouldSatisfy` allClose (map (\x -> 3 + 2 * x) (numXof l))++    it "byVar x2 [0,1] (既定 帯 ON): 2 曲線 (MLine 2 本・MBand 2 本)" $ do+      let spec = statModelMulti mlm (along "x1") <> byVar "x2" [0, 1]+      length (lineLayersOf spec) `shouldBe` 2+      length (bandLayersOf spec) `shouldBe` 2++    it "byVar x2 [0,1]: 曲線は x2=0→3+2x1, x2=1→6+2x1 (x3 は Mean・順序保持)" $ do+      let spec    = statModelMulti mlm (along "x1") <> grid 5 <> byVar "x2" [0, 1]+          ls      = lineLayersOf spec+          [l0, l1] = ls+      length ls `shouldBe` 2+      numYof l0 `shouldSatisfy` allClose (map (\x -> 3 + 2 * x) (numXof l0))+      numYof l1 `shouldSatisfy` allClose (map (\x -> 6 + 2 * x) (numXof l1))++    it "holdAt Marginalize: band 無し (MLine のみ)・PDP = 4.5+2x1 (線形ゆえ Mean と一致)" $ do+      let spec = statModelMulti mlm (along "x1") <> grid 6 <> holdAt Marginalize+      kindsMV spec `shouldBe` [Just MLine]+      numYof (firstLine spec)+        `shouldSatisfy` allClose (map (\x -> 4.5 + 2 * x) (numXof (firstLine spec)))++    it "bandMode BandOff: MLine のみ" $ do+      kindsMV (statModelMulti mlm (along "x1") <> bandMode BandOff) `shouldBe` [Just MLine]++    it "多変量 GLM effect (Poisson/Log, 既定 帯 ON): μ 曲線 + 非対称帯 (MBand + MLine)・μ̂ > 0 単調増" $ do+      let glm  = either error id (multiGLMModel Poisson Log "y ~ x1 + x2" dfMV)+          spec = statModelMulti glm (along "x1") <> grid 8+          l    = firstLine spec+          ys'  = numYof l+      kindsMV spec `shouldMatchList` [Just MBand, Just MLine]+      all (> 0) ys' `shouldBe` True+      -- Log リンク + 正係数 → x1 に対し μ̂ 単調増+      and (zipWith (<=) ys' (tail ys')) `shouldBe` True++  describe "Phase 49 A1: hbmModel (HBM 学習 = 列名 bind + 並列 multi-chain)" $ do+    -- y = 1 + 2x + 小さな決定論的ゆらぎ を生成し、 線形 HBM で a≈1, b≈2 を復元する。+    -- (完全無ノイズだと σ→0 で尤度が発散し NUTS が荒れるため微小ゆらぎを足す)。+    let xdat  = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] :: [Double]+        noise = [0.12, -0.08, 0.05, -0.15, 0.10, 0.03, -0.07, 0.14, -0.04, 0.06]+        ydat  = zipWith (\x e -> 1 + 2 * x + e) xdat noise+        cfg   = defaultHBM { hbmChains = 2, hbmSamples = 500, hbmWarmup = 500+                           , hbmSeed = Just 20260605 }++    it "列名で withData 自動 bind し multi-chain で学習・posterior が真値を復元" $ do+      studied <- hbmModel cfg hbmLinModel [("x", xdat), ("y", ydat)]+      -- chain 数 = config 通り。+      length (hbmChainsR studied) `shouldBe` 2+      -- bind 済みデータが保持されている。+      hbmData studied `shouldBe` [("x", xdat), ("y", ydat)]+      -- 事後平均が真値 a=1, b=2 を概ね復元 (緩い許容)。+      let aHat = posteriorMeanOf "a" (hbmChainsR studied)+          bHat = posteriorMeanOf "b" (hbmChainsR studied)+      abs (aHat - 1) `shouldSatisfy` (< 0.5)+      abs (bHat - 2) `shouldSatisfy` (< 0.2)++    it "各 chain の draw 数 = hbmSamples (post-warmup)" $ do+      studied <- hbmModel cfg hbmLinModel [("x", xdat), ("y", ydat)]+      map (length . chainSamples) (hbmChainsR studied)+        `shouldBe` [500, 500]++  describe "Phase 49 A2 / 74: tracesOf + forestOf (HBM 出力抽出子)" $ do+    let xdat = [1, 2, 3, 4, 5, 6, 7, 8] :: [Double]+        ydat = zipWith (\x e -> 1 + 2 * x + e) xdat+                 [0.1, -0.05, 0.08, -0.1, 0.05, -0.03, 0.07, -0.06]+        -- A2 は構造検証ゆえ軽い設定で十分。+        cfgS = defaultHBM { hbmChains = 2, hbmSamples = 60, hbmWarmup = 60+                          , hbmSeed = Just 49 }++    it "tracesOf: latent パラメータ数 (a,b,s = 3) 個の VisualSpec" $ do+      studied <- hbmModel cfgS hbmLinModel [("x", xdat), ("y", ydat)]+      hbmParamNames studied `shouldMatchList` ["a", "b", "s"]+      length (tracesOf studied) `shouldBe` 3++    it "tracesOf: merged trace の頂点数 = 全 chain 連結の draw 総数 (2×60=120)" $ do+      studied <- hbmModel cfgS hbmLinModel [("x", xdat), ("y", ydat)]+      -- 既定は merged + rug。trace 層は先頭 (rug は分岐後)。divergence 無しでも先頭は trace。+      let vs      = head (tracesOf studied)+          tLayer  = head (vsLayers vs)+      case getLast (lyEncX tLayer) of+        Just (ColNum vx) -> V.length vx `shouldBe` 120+        _                -> expectationFailure "trace encX が inline ColNum でない"+      getFirst (lyKind tLayer) `shouldBe` Just MTrace++    it "forestOf: MForest layer・点 3 個 (= パラメータ数)・誤差半幅 ≥ 0" $ do+      studied <- hbmModel cfgS hbmLinModel [("x", xdat), ("y", ydat)]+      let fLayer = head (vsLayers (toPlot (forestOf studied)))+      getFirst (lyKind fLayer) `shouldBe` Just MForest+      case (getLast (lyEncX fLayer), getLast (lyErrorX fLayer)) of+        (Just (ColNum ests), Just (ColNum errs)) -> do+          V.length ests `shouldBe` 3+          V.length errs `shouldBe` 3+          V.toList errs `shouldSatisfy` all (>= 0)+        _ -> expectationFailure "forest encX/errorX が inline ColNum でない"++    -- Phase 52.B2: marginalsOf = 周辺事後密度を per-param で list 返し。+    it "marginalsOf: latent 数 (3) 個・各図 density layer 1 枚 + title=param 名" $ do+      studied <- hbmModel cfgS hbmLinModel [("x", xdat), ("y", ydat)]+      let ms = marginalsOf studied+      length ms `shouldBe` 3+      -- 各図のタイトル = パラメータ名 (a,b,s)+      [ t | s <- ms, Just t <- [getLast (vsTitle s)] ]+        `shouldMatchList` ["a", "b", "s"]+      -- 各図は density (MDensity) layer のみ+      [ getFirst (lyKind l) | s <- ms, l <- vsLayers s ]+        `shouldSatisfy` all (== Just MDensity)++  describe "Phase 49 A3: epred (事後予測平均 + HDI band・O1 規約)" $ do+    -- y = 1 + 2x + 微小ゆらぎ。 epred の事後平均線が真値 1+2x を復元するか検証。+    let xdat = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] :: [Double]+        ydat = zipWith (\x e -> 1 + 2 * x + e) xdat+                 [0.12, -0.08, 0.05, -0.15, 0.10, 0.03, -0.07, 0.14, -0.04, 0.06]+        cfg  = defaultHBM { hbmChains = 2, hbmSamples = 400, hbmWarmup = 400+                          , hbmSeed = Just 4903 }+        lineLayerOf spec = head [ l | l <- vsLayers (toPlot spec)+                                    , getFirst (lyKind l) == Just MLine ]+        numX l = case getLast (lyEncX l) of+                   Just (ColNum v) -> V.toList v+                   _               -> error "encX が ColNum でない"+        numY l = case getLast (lyEncY l) of+                   Just (ColNum v) -> V.toList v+                   _               -> error "encY が ColNum でない"++    it "既定は band (MBand) + 事後平均線 (MLine) の 2 layer・grid 100 点" $ do+      studied <- hbmModel cfg hbmEpredModel [("x", xdat), ("y", ydat)]+      let ls = vsLayers (toPlot (epred studied "x" "mu"))+      map (getFirst . lyKind) ls `shouldMatchList` [Just MBand, Just MLine]+      length (numX (lineLayerOf (epred studied "x" "mu"))) `shouldBe` 100++    it "grid n / gridRange が grid を制御 (既定範囲 = 予測子 min/max = [1,10])" $ do+      studied <- hbmModel cfg hbmEpredModel [("x", xdat), ("y", ydat)]+      let xg = numX (lineLayerOf (epred studied "x" "mu" <> grid 21))+      length xg `shouldBe` 21+      head xg `shouldSatisfy` (\v -> abs (v - 1) < 1e-9)+      last xg `shouldSatisfy` (\v -> abs (v - 10) < 1e-9)++    -- epred は HDI 帯を本体として焼き込む (帯既定 ON・bandOff でも消えない)。++    it "事後平均線が真値 1+2x を概ね復元 (grid 上で平均絶対誤差 < 0.4)" $ do+      studied <- hbmModel cfg hbmEpredModel [("x", xdat), ("y", ydat)]+      let l    = lineLayerOf (epred studied "x" "mu" <> grid 10)+          errs = zipWith (\x mu -> abs (mu - (1 + 2 * x))) (numX l) (numY l)+          mae  = sum errs / fromIntegral (length errs)+      mae `shouldSatisfy` (< 0.4)++    it "epredAt: HDI 幅は level とともに単調増 (0.5 < 0.94 < 0.99)" $ do+      studied <- hbmModel cfg hbmEpredModel [("x", xdat), ("y", ydat)]+      let widthAt lvl =+            let (_, (lo, hi)) = epredAt studied "x" "mu" lvl 5.0 in hi - lo+          w50 = widthAt 0.50+          w94 = widthAt 0.94+          w99 = widthAt 0.99+      w50 `shouldSatisfy` (< w94)+      w94 `shouldSatisfy` (< w99)++    it "epredAt: 事後平均は線層の対応点と一致 (= 同じ評価核)" $ do+      studied <- hbmModel cfg hbmEpredModel [("x", xdat), ("y", ydat)]+      let (mean5, _) = epredAt studied "x" "mu" 0.94 5.0+      abs (mean5 - (1 + 2 * 5)) `shouldSatisfy` (< 0.4)++  -- Phase 74: epred の多予測子 hold (holdAt / byVar)。 mu = a + b*x1 + c*x2 で+  -- 非軸 x2 を固定値/水準別に動かし、 既存 holdAt/byVar (頻度論と同綴り) が効くか検証。+  describe "Phase 74: epred holdAt / byVar (多予測子 hold)" $ do+    let x1d = [1, 2, 3, 4, 5, 6, 7, 8] :: [Double]+        x2d = [2, 1, 3, 2, 4, 3, 5, 4] :: [Double]+        y2d = zipWith3 (\a b e -> 1 + 2 * a + 3 * b + e) x1d x2d+                [0.05, -0.04, 0.03, -0.06, 0.02, 0.01, -0.03, 0.04]+        cfg2 = defaultHBM { hbmChains = 2, hbmSamples = 400, hbmWarmup = 400+                          , hbmSeed = Just 7402 }+        lineLayerOf spec = head [ l | l <- vsLayers (toPlot spec)+                                    , getFirst (lyKind l) == Just MLine ]+        mlines spec = [ l | l <- vsLayers (toPlot spec), getFirst (lyKind l) == Just MLine ]+        numY l = case getLast (lyEncY l) of+                   Just (ColNum v) -> V.toList v+                   _               -> error "encY が ColNum でない"++    it "holdAt (Fixed): x2 を Δ=4 上げると曲線が c*Δ ≈ 12 だけ一様に上シフト" $ do+      m <- hbmModel cfg2 hbmEpred2Model [("x1", x1d), ("x2", x2d), ("y", y2d)]+      let muAt v = numY (lineLayerOf (epred m "x1" "mu" <> holdAt (Fixed [("x2", v)]) <> grid 6))+          diffs  = zipWith (-) (muAt 4) (muAt 0)+      -- c ≈ 3・Δx2 = 4 ゆえ全 grid 点で ≈ 12 の一様シフト (傾きは x1 のまま不変)。+      diffs `shouldSatisfy` all (\d -> abs (d - 12) < 1.5)++    it "holdAt 既定 (Mean) = Fixed mean(x2) (非軸の既定は head でなく中央化された Mean)" $ do+      m <- hbmModel cfg2 hbmEpred2Model [("x1", x1d), ("x2", x2d), ("y", y2d)]+      let meanX2 = sum x2d / fromIntegral (length x2d)+          dflt = numY (lineLayerOf (epred m "x1" "mu" <> grid 6))+          fixd = numY (lineLayerOf (epred m "x1" "mu" <> holdAt (Fixed [("x2", meanX2)]) <> grid 6))+      zip dflt fixd `shouldSatisfy` all (\(a, b) -> abs (a - b) < 1e-9)++    it "byVar: x2 の水準数だけ曲線 (MLine) が出て、 水準が高いほど mu 大" $ do+      m <- hbmModel cfg2 hbmEpred2Model [("x1", x1d), ("x2", x2d), ("y", y2d)]+      let spec = epred m "x1" "mu" <> byVar "x2" [0, 4] <> grid 6+          ls   = mlines spec+          midOf l = numY l !! 3+      length ls `shouldBe` 2+      midOf (ls !! 1) `shouldSatisfy` (> midOf (head ls))++  -- Phase 74.5: epred の予測区間 (PI) 帯。 bandMode で CI (μ HDI) / PI (観測ノイズ込み) /+  -- CIPI (入れ子) を切替 (頻度論 statModel と同綴り)。 PI は観測分布サンプルゆえ固定 seed で+  -- 決定的。 noise を大きめにして PI が CI を有意に上回る (s が効く) ことを検証可能にする。+  describe "Phase 74.5: epred bandMode (CI / PI / CIPI)" $ do+    let xdat = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] :: [Double]+        ydat = zipWith (\x e -> 1 + 2 * x + e) xdat+                 [0.4, -0.3, 0.5, -0.5, 0.3, 0.2, -0.4, 0.5, -0.2, 0.3]+        cfg  = defaultHBM { hbmChains = 2, hbmSamples = 400, hbmWarmup = 400+                          , hbmSeed = Just 4903 }+        bandsOf spec = [ l | l <- vsLayers (toPlot spec), getFirst (lyKind l) == Just MBand ]+        kindsOf spec = map (getFirst . lyKind) (vsLayers (toPlot spec))+        loY l = case getLast (lyEncY l)  of { Just (ColNum v) -> V.toList v; _ -> error "encY" }+        hiY l = case getLast (lyEncY2 l) of { Just (ColNum v) -> V.toList v; _ -> error "encY2" }+        widthsOf l = zipWith (-) (hiY l) (loY l)++    it "既定 (bandMode 無し) = bandMode BandCI とバイト一致 (後方互換)" $ do+      m <- hbmModel cfg hbmEpredModel [("x", xdat), ("y", ydat)]+      let dflt = bandsOf (epred m "x" "mu" <> grid 8)+          ci   = bandsOf (epred m "x" "mu" <> grid 8 <> bandMode BandCI)+      map loY dflt `shouldBe` map loY ci+      map hiY dflt `shouldBe` map hiY ci++    it "BandPI: PI 帯は CI 帯を全 grid 点で包含し、 幅が広い (観測 σ ぶん)" $ do+      m <- hbmModel cfg hbmEpredModel [("x", xdat), ("y", ydat)]+      let [ciB] = bandsOf (epred m "x" "mu" <> grid 8 <> bandMode BandCI)+          [piB] = bandsOf (epred m "x" "mu" <> grid 8 <> bandMode BandPI)+      and (zipWith (<=) (loY piB) (loY ciB)) `shouldBe` True+      and (zipWith (>=) (hiY piB) (hiY ciB)) `shouldBe` True+      and (zipWith (>)  (widthsOf piB) (widthsOf ciB)) `shouldBe` True++    it "BandCIPI: MBand 2 (外 PI + 内 CI) + MLine 1、 PI が外側 (包含)" $ do+      m <- hbmModel cfg hbmEpredModel [("x", xdat), ("y", ydat)]+      let spec  = epred m "x" "mu" <> grid 8 <> bandMode BandCIPI+          bands = bandsOf spec+      kindsOf spec `shouldMatchList` [Just MBand, Just MBand, Just MLine]+      case bands of+        [pib, cib] -> do+          and (zipWith (<=) (loY pib) (loY cib)) `shouldBe` True+          and (zipWith (>=) (hiY pib) (hiY cib)) `shouldBe` True+        _ -> expectationFailure "MBand が 2 本でない"++    it "BandOff: 帯が消え MLine 1 layer のみ" $ do+      m <- hbmModel cfg hbmEpredModel [("x", xdat), ("y", ydat)]+      kindsOf (epred m "x" "mu" <> grid 8 <> bandMode BandOff) `shouldBe` [Just MLine]++    it "BandPI: PI 帯は妥当な区間 (全 grid 点で lo < hi・grid 点数一致)" $ do+      m <- hbmModel cfg hbmEpredModel [("x", xdat), ("y", ydat)]+      let [piB] = bandsOf (epred m "x" "mu" <> grid 6 <> bandMode BandPI)+      length (widthsOf piB) `shouldBe` 6+      widthsOf piB `shouldSatisfy` all (> 0)++  -- Phase 74.8: 診断ダッシュボード (抽出子を subplots で束ねる便宜関数)。+  describe "Phase 74.8: dashboardOf / dashboardFullOf" $ do+    let xdat = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] :: [Double]+        ydat = zipWith (\x e -> 1 + 2 * x + e) xdat+                 [0.4, -0.3, 0.5, -0.5, 0.3, 0.2, -0.4, 0.5, -0.2, 0.3]+        cfg  = defaultHBM { hbmChains = 2, hbmSamples = 300, hbmWarmup = 300+                          , hbmSeed = Just 4903 }++    it "dashboardOf: 2×2 = 4 パネル (構造 / 推定値 / 当てはまり / サンプラ健全性)" $ do+      m <- hbmModel cfg hbmEpredModel [("x", xdat), ("y", ydat)]+      length (vsSubplots (dashboardOf m "obs")) `shouldBe` 4++    it "dashboardFullOf: 健全性 4 + param ごと [事後分布,trace] 2×3 = 10 パネル" $ do+      m <- hbmModel cfg hbmEpredModel [("x", xdat), ("y", ydat)]+      -- a,b,s の 3 param ゆえ 4 + 2*3 = 10。 係数が増えると下に行 (2 パネル) ずつ増える。+      length (vsSubplots (dashboardFullOf m "obs")) `shouldBe` 10++    it "traceDensityOf: param ごと [事後分布,trace] = 2*3 = 6 パネル" $ do+      m <- hbmModel cfg hbmEpredModel [("x", xdat), ("y", ydat)]+      length (vsSubplots (traceDensityOf m)) `shouldBe` 6++  describe "Phase 49 A4: ppcOf (事後予測チェック = ArviZ kde overlay)" $ do+    -- y = 1 + 2x + 微小ゆらぎ。 ppc は観測 (黒) + 各 draw の y_rep 群 (青) を重ねる+    -- (Phase 74.10: プール赤線は KDE バンド幅が n 依存で誤解を招くため削除)。+    let xdat = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] :: [Double]+        ydat = zipWith (\x e -> 1 + 2 * x + e) xdat+                 [0.12, -0.08, 0.05, -0.15, 0.10, 0.03, -0.07, 0.14, -0.04, 0.06]+        cfg  = defaultHBM { hbmChains = 2, hbmSamples = 300, hbmWarmup = 300+                          , hbmSeed = Just 7711 }+        ppcCfg = defaultPPC { ppcReps = 10, ppcSeed = Just 2024 }+        numX l = case getLast (lyEncX l) of+                   Just (ColNum v) -> V.toList v+                   _               -> error "encX が ColNum でない"+        meanOf zs = sum zs / fromIntegral (length zs)+        varOf zs  = let m = meanOf zs+                    in sum [ (z - m) ^ (2 :: Int) | z <- zs ]+                         / fromIntegral (length zs)++    it "層 = y_rep 10 本 + 観測 = 11 層・全て MDensity (プール線なし)" $ do+      studied <- hbmModel cfg hbmLinModel [("x", xdat), ("y", ydat)]+      sp <- ppcOfWithIO ppcCfg studied "obs"+      let ls = vsLayers (toPlot sp)+      length ls `shouldBe` 11+      map (getFirst . lyKind) ls `shouldSatisfy` all (== Just MDensity)++    it "ppcCumulative で全層 MEcdf に切り替わる" $ do+      studied <- hbmModel cfg hbmLinModel [("x", xdat), ("y", ydat)]+      sp <- ppcOfWithIO ppcCfg { ppcCumulative = True } studied "obs"+      let ls = vsLayers (toPlot sp)+      map (getFirst . lyKind) ls `shouldSatisfy` all (== Just MEcdf)++    it "観測層 (最後) の encX = 実 y データ (10 点)" $ do+      studied <- hbmModel cfg hbmLinModel [("x", xdat), ("y", ydat)]+      sp <- ppcOfWithIO ppcCfg studied "obs"+      let ls  = vsLayers (toPlot sp)+          obs = numX (last ls)+      obs `shouldSatisfy` (\v -> length v == 10+                                   && all (\(a, b) -> abs (a - b) < 1e-9) (zip v ydat))++    it "各 draw の y_rep 層 (先頭 = 1 draw) の平均/分散が観測と整合" $ do+      studied <- hbmModel cfg hbmLinModel [("x", xdat), ("y", ydat)]+      sp <- ppcOfWithIO ppcCfg studied "obs"+      let ls   = vsLayers (toPlot sp)+          yrep = numX (head ls)   -- 先頭 = 1 つの draw の y_rep 層 (n=n_obs・観測と同条件)+          obs  = numX (last ls)+      -- 事後予測が観測の中心・広がりを再現 (緩い許容)。+      abs (meanOf yrep - meanOf obs) `shouldSatisfy` (< 2.0)+      (varOf yrep / varOf obs) `shouldSatisfy` (\r -> r > 0.5 && r < 2.0)++    it "ppcReps が draw 総数を超えても全 draw 数で頭打ち (層 = draw+1)" $ do+      let smallCfg = defaultHBM { hbmChains = 1, hbmSamples = 5, hbmWarmup = 50+                                , hbmSeed = Just 5 }+      studied <- hbmModel smallCfg hbmLinModel [("x", xdat), ("y", ydat)]+      sp <- ppcOfWithIO defaultPPC { ppcReps = 1000, ppcSeed = Just 1 } studied "obs"+      length (vsLayers (toPlot sp)) `shouldBe` 5 + 1++  describe "Phase 49 A5: dagOf (モデル構造 DAG = buildModelGraph 橋渡し)" $ do+    -- dagOf は構造のみ (学習不要)。 data を withData で bind した spec を手で組み、+    -- chains 空の HBMModel を作って DAG を取り出す (NUTS を回さず高速)。+    let xdat = [1, 2, 3, 4] :: [Double]+        ydat = [3, 5, 7, 9] :: [Double]+        boundLin :: ModelP ()+        boundLin = withData "x" xdat (withData "y" ydat hbmLinModel)+        linDagM  = HBMModel { hbmModelSpec = boundLin+                            , hbmChainsR = [], hbmData = []+                            , hbmFactorLevels = [] }+        plateDagM = HBMModel { hbmModelSpec = hbmPlateModel+                             , hbmChainsR = [], hbmData = []+                             , hbmFactorLevels = [] }+        -- Phase 59.3: dagOf は collapse 済が既定、 旧挙動 (indexed 個別) は dagOfRaw+        dsOf hm = case getLast (lyDAG (head (vsLayers (toPlot (dagOf hm))))) of+                    Just ds -> ds+                    Nothing -> error "dagOf に lyDAG が無い"+        dsOfRaw hm = case getLast (lyDAG (head (vsLayers (toPlot (dagOfRaw hm))))) of+                       Just ds -> ds+                       Nothing -> error "dagOfRaw に lyDAG が無い"++    it "MDAG layer 1 枚 (mark = MDAG)" $ do+      let ls = vsLayers (toPlot (dagOf linDagM))+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MDAG++    -- Phase 60.4: dataNamedX/dataNamedObs slot (x, y) が NodeData として DAG に+    -- 出るようになった (pm.Data parity・既定 ON)。 x は mu 経由で obs への+    -- エッジを持つが、 y (dataNamedObs = 生 [Double] view) はエッジなし。+    it "線形モデル (collapsed 既定): a,b,s,obs + data x,y = 6 node" $ do+      let ds    = dsOf linDagM+          names = map dnId (dsNodes ds)+      length (dsNodes ds) `shouldBe` 6+      names `shouldSatisfy` (\ns -> all (`elem` ns) ["a", "b", "s", "obs", "x", "y"])+      length (filter (\n -> dnKind n == NodeLatent) (dsNodes ds)) `shouldBe` 3+      length (filter (\n -> dnKind n == NodeObserved) (dsNodes ds)) `shouldBe` 1+      length (filter (\n -> dnKind n == NodeData) (dsNodes ds)) `shouldBe` 2++    it "線形モデル (collapsed 既定): edge は (a,b,s,x) → obs の 4 本" $ do+      let ds  = dsOf linDagM+          es  = [ (deFrom e, deTo e) | e <- dsEdges ds ]+      length es `shouldBe` 4+      es `shouldSatisfy` (("a", "obs") `elem`)+      es `shouldSatisfy` (("s", "obs") `elem`)+      es `shouldSatisfy` (("x", "obs") `elem`)++    it "線形モデル (dagOfRaw): latent 3 + obs_0..3 + data 2 = 9 node" $ do+      let ds    = dsOfRaw linDagM+          names = map dnId (dsNodes ds)+      length (dsNodes ds) `shouldBe` 9+      names `shouldSatisfy` (\ns -> all (`elem` ns) ["a", "b", "s", "x", "y"])+      length (filter (\n -> dnKind n == NodeLatent) (dsNodes ds)) `shouldBe` 3+      length (filter (\n -> dnKind n == NodeObserved) (dsNodes ds)) `shouldBe` 4+      length (filter (\n -> dnKind n == NodeData) (dsNodes ds)) `shouldBe` 2++    it "線形モデル (dagOfRaw): edge は (a,b,s,x) → 各 obs (4×4 = 16 本)" $ do+      let ds  = dsOfRaw linDagM+          es  = [ (deFrom e, deTo e) | e <- dsEdges ds ]+      length es `shouldBe` 16+      es `shouldSatisfy` (("a", "obs_0") `elem`)+      es `shouldSatisfy` (("s", "obs_3") `elem`)+      es `shouldSatisfy` (("x", "obs_0") `elem`)++    it "線形モデル (plate 無し): dsPlates は空 (collapsed/raw とも)" $ do+      dsPlates (dsOf linDagM) `shouldBe` []+      dsPlates (dsOfRaw linDagM) `shouldBe` []++    it "plate モデル (collapsed 既定): eta_0..3/y_0..3 が eta/y に畳まれる" $ do+      let ds    = dsOf plateDagM+          names = map dnId (dsNodes ds)+      length (dsNodes ds) `shouldBe` 4   -- mu, tau, eta, y+      names `shouldSatisfy` (\ns -> all (`elem` ns) ["mu", "tau", "eta", "y"])+      length (dsPlates ds) `shouldBe` 1+      let p = head (dsPlates ds)+      dpLabel p `shouldBe` "g (4)"+      dpNodeIds p `shouldSatisfy` ("eta" `elem`)++    it "plate モデル (dagOfRaw): dsPlates に \"g (4)\" が 1 個・eta_0 を含む" $ do+      let ds = dsOfRaw plateDagM+      length (dsPlates ds) `shouldBe` 1+      let p = head (dsPlates ds)+      dpLabel p `shouldBe` "g (4)"+      dpNodeIds p `shouldSatisfy` ("eta_0" `elem`)++    it "plate モデル: 分布名が dnDist に入る (mu ~ Normal)" $ do+      let ds = dsOf plateDagM+      case filter (\n -> dnId n == "mu") (dsNodes ds) of+        [n] -> dnDist n `shouldBe` Just "Normal"+        _   -> expectationFailure "mu node not found"++  -- Phase 74.9: 学習前 DAG (ModelP 直接・サンプリングなし)。+  describe "Phase 74.9: dagOfModel / dagOfModelWith" $ do+    let xdat = [1, 2, 3, 4] :: [Double]+        ydat = [3, 5, 7, 9] :: [Double]+        dsD d = case getLast (lyDAG (head (vsLayers (toPlot d)))) of+                  Just ds -> ds+                  Nothing -> error "DAG に lyDAG が無い"++    it "dagOfModelWith: データ束ねで data 駆動 plate の全ノードが出る (6 node・NUTS なし)" $ do+      let ds    = dsD (dagOfModelWith [("x", xdat), ("y", ydat)] hbmLinModel)+          names = map dnId (dsNodes ds)+      length (dsNodes ds) `shouldBe` 6+      names `shouldSatisfy` (\ns -> all (`elem` ns) ["a", "b", "s", "obs", "x", "y"])++    it "dagOfModel: 既に withData 束ね済みモデルなら dagOf と同一構造 (6 node)" $ do+      let bound :: ModelP ()+          bound = withData "x" xdat (withData "y" ydat hbmLinModel)+          ds    = dsD (dagOfModel bound)+      length (dsNodes ds) `shouldBe` 6++    it "dagOfModel: 未束縛 (slot []) は data 駆動 plate 本体が出ない (caveat: obs 無し)" $ do+      let ds    = dsD (dagOfModel hbmLinModel)+          names = map dnId (dsNodes ds)+      -- a,b,s + data x,y は出るが、 ループ本体 (obs) は 0 反復ゆえ出ない。+      names `shouldSatisfy` (\ns -> notElem "obs" ns)+      names `shouldSatisfy` (\ns -> all (`elem` ns) ["a", "b", "s"])++  describe "Phase 59.4 / 74: divergencesOf / tracesOfWith byChain+divergence (divergence 診断)" $ do+    -- fake chain で offset 規約を決定的に検証 (chainDivergences = chain 内 0-based+    -- post-burn-in index、 root: request/255 §4。 NUTS を回さない)+    let muModel :: ModelP ()+        muModel = do+          _ <- sample "mu" (Normal 0 1)+          pure ()+        mkCh vals divs = Chain { chainSamples     = [ Map.singleton "mu" v | v <- vals ]+                               , chainAccepted    = 0+                               , chainTotal       = length vals+                               , chainEnergy      = map (* 2) vals+                               , chainDivergences = divs+                               , chainTreeDepths  = [] }+        ch1 = mkCh [1, 2, 3] [0, 2]   -- 3 draws・div = 0,2+        ch2 = mkCh [4, 5] [1]         -- 2 draws・div = 1+        divM   = HBMModel { hbmModelSpec = muModel+                          , hbmChainsR = [ch1, ch2], hbmData = []+                          , hbmFactorLevels = [] }+        cleanM = HBMModel { hbmModelSpec = muModel+                          , hbmChainsR = [mkCh [1, 2, 3] [], mkCh [4, 5] []]+                          , hbmData = [], hbmFactorLevels = [] }++    it "divergencesOf: chain offset 加算の通し index ([0,2] ++ map (+3) [1] = [0,2,4])" $ do+      divergencesOf divM `shouldBe` [0, 2, 4]++    it "divergencesOf: divergence 無しなら空" $ do+      divergencesOf cleanM `shouldBe` []++    -- Phase 74: 旧 tracesWithDivergencesOf = tracesOfWith (byChain + divergence ON)。+    let byChainDiv = tracesOfWith defaultTraceOpts { toByChain = True }++    it "tracesOfWith byChain+div: trace 2 層 (chain 別) + rug 1 層 (MLineRange 縦棒)" $ do+      let [vs] = byChainDiv divM+          ls   = vsLayers vs+      length ls `shouldBe` 3+      -- Phase 60.5: rug は scatter の点から lineRange の縦棒へ (ArviZ tick 同型)+      map (getFirst . lyKind) ls `shouldBe` [Just MTrace, Just MTrace, Just MLineRange]++    it "tracesOfWith byChain+div: rug の x = chain 内 1-based iteration・縦棒 = 下端から値域 2%" $ do+      let [vs] = byChainDiv divM+          rug  = last (vsLayers vs)+      case (getLast (lyEncX rug), getLast (lyEncY rug), getLast (lyErrorY rug)) of+        (Just (ColNum vx), Just (ColNum vy), Just (ColNum ve)) -> do+          V.toList vx `shouldBe` [1, 3, 2]   -- ch1: 0,2 → 1,3 / ch2: 1 → 2+          -- lineRange は (x, 中心 y, ±err)。 divM の mu は 1..5 → 値域 4・+          -- tick = 4*0.02 = 0.08。 中心 = 1 + 0.04、 err = 0.04+          V.toList vy `shouldSatisfy` all (\v -> abs (v - 1.04) < 1e-9)+          V.toList ve `shouldSatisfy` all (\v -> abs (v - 0.04) < 1e-9)+        _ -> expectationFailure "rug の encX/encY/errorY が inline ColNum でない"++    it "tracesOfWith byChain+div: divergence 無しなら rug 層なし" $ do+      let [vs] = byChainDiv cleanM+      map (getFirst . lyKind) (vsLayers vs) `shouldBe` [Just MTrace, Just MTrace]++    it "tracesOf 既定 (merged + div): merged trace 1 層 + rug 1 層" $ do+      let [vs] = tracesOf divM+          ls   = vsLayers vs+      -- merged は単線 trace 1 層 + rug 1 層+      map (getFirst . lyKind) ls `shouldBe` [Just MTrace, Just MLineRange]++    it "tracesOfWith divergence OFF: rug 層なし (merged)" $ do+      let [vs] = tracesOfWith defaultTraceOpts { toShowDivergences = False } divM+      map (getFirst . lyKind) (vsLayers vs) `shouldBe` [Just MTrace]++  describe "Phase 59.5: pairOf (joint 散布 + 発散強調)" $ do+    let abModel :: ModelP ()+        abModel = do+          _ <- sample "a" (Normal 0 1)+          _ <- sample "b" (Normal 0 1)+          pure ()+        mkCh2 avs bvs divs = Chain+          { chainSamples     = [ Map.fromList [("a", av), ("b", bv)]+                               | (av, bv) <- zip avs bvs ]+          , chainAccepted    = 0+          , chainTotal       = length avs+          , chainEnergy      = []+          , chainDivergences = divs }+        ch1 = mkCh2 [10, 11, 12] [20, 21, 22] [2]   -- div: chain 内 2 → 通し 2+        ch2 = mkCh2 [13, 14] [23, 24] [0]           -- div: chain 内 0 → 通し 3+        m2  = HBMModel { hbmModelSpec = abModel+                       , hbmChainsR = [ch1, ch2], hbmData = []+                       , hbmFactorLevels = [] }++    it "pairOf: 図数 = ペア数・layer = base + 強調の MScatter 2 層" $ do+      let vss = pairOf m2 [("a", "b")]+      length vss `shouldBe` 1+      map (getFirst . lyKind) (vsLayers (head vss))+        `shouldBe` [Just MScatter, Just MScatter]++    it "pairOf: 強調点 = 通し index の draw (chain 跨ぎ x=[12,13] y=[22,23])" $ do+      let [vs] = pairOf m2 [("a", "b")]+          ov   = last (vsLayers vs)+      case (getLast (lyEncX ov), getLast (lyEncY ov)) of+        (Just (ColNum vx), Just (ColNum vy)) -> do+          V.toList vx `shouldBe` [12, 13]+          V.toList vy `shouldBe` [22, 23]+        _ -> expectationFailure "強調層の encX/encY が inline ColNum でない"++    it "pairOf: divergence 無しなら base 層のみ" $ do+      let m0   = HBMModel { hbmModelSpec = abModel+                          , hbmChainsR = [mkCh2 [1] [2] []], hbmData = []+                          , hbmFactorLevels = [] }+          [vs] = pairOf m0 [("a", "b")]+      map (getFirst . lyKind) (vsLayers vs) `shouldBe` [Just MScatter]++  describe "Phase 59.6: energyOf (marginal vs ΔE energy 密度)" $ do+    let muModel :: ModelP ()+        muModel = do+          _ <- sample "mu" (Normal 0 1)+          pure ()+        mkEnCh es = Chain { chainSamples     = [ Map.singleton "mu" e | e <- es ]+                          , chainAccepted    = 0+                          , chainTotal       = length es+                          , chainEnergy      = es+                          , chainDivergences = []+                          , chainTreeDepths  = [] }+        es1 = [10, 13, 11, 15, 12, 14, 16, 11, 13, 12]+        enM = HBMModel { hbmModelSpec = muModel+                       , hbmChainsR = [mkEnCh es1, mkEnCh (map (+ 1) es1)]+                       , hbmData = [], hbmFactorLevels = [] }+        noEnM = HBMModel { hbmModelSpec = muModel+                         , hbmChainsR =+                             [ Chain [Map.singleton "mu" 1] 0 1 [] [] [] ]+                         , hbmData = [], hbmFactorLevels = [] }++    it "energyOf: marginal + ΔE の MLine 2 層 (KDE 200 点)" $ do+      let vs = energyOf enM+          ls = vsLayers vs+      map (getFirst . lyKind) ls `shouldBe` [Just MLine, Just MLine]+      case getLast (lyEncX (head ls)) of+        Just (ColNum vx) -> V.length vx `shouldBe` 200+        _ -> expectationFailure "encX が inline ColNum でない"++    it "energyOf: energy 記録なし (MH 等) なら layer 0 (空図)" $ do+      length (vsLayers (energyOf noEnM)) `shouldBe` 0++  describe "Phase 73.1: autocorrOf (自己相関・ArviZ plot_autocorr)" $ do+    let muModel :: ModelP ()+        muModel = sample "mu" (Normal 0 1) >> pure ()+        mkAcCh xs = Chain { chainSamples     = [ Map.singleton "mu" x | x <- xs ]+                          , chainAccepted    = 0+                          , chainTotal       = length xs+                          , chainEnergy      = []+                          , chainDivergences = []+                          , chainTreeDepths  = [] }+        acXs = [ sin (0.3 * fromIntegral i) | i <- [0 .. 49 :: Int] ]   -- 自己相関のある系列+        acM  = HBMModel { hbmModelSpec = muModel+                        , hbmChainsR = [mkAcCh acXs, mkAcCh (map (* 0.9) acXs)]+                        , hbmData = [], hbmFactorLevels = [] }++    it "autocorrOf: param ごと 1 図・MBar 層" $ do+      let specs = autocorrOf acM+      length specs `shouldBe` 1                                   -- "mu" の 1 つ+      map (getFirst . lyKind) (vsLayers (head specs)) `shouldBe` [Just MBar]++    it "autocorrOf: lag 0 の ACF == 1.0 (自己相関の定義・chain 平均)" $ do+      let l = head (vsLayers (head (autocorrOf acM)))+      case getLast (lyEncY l) of+        Just (ColNum v) -> V.head v `shouldSatisfy` (\a -> abs (a - 1.0) < 1e-9)+        _               -> expectationFailure "encY が inline ColNum でない"++    it "autocorrOfLag: 最大ラグ k なら bar は k+1 本 (lag 0..k)" $ do+      let l = head (vsLayers (head (autocorrOfLag 8 acM)))+      case getLast (lyEncX l) of+        Just (ColNum v) -> V.length v `shouldBe` 9+        _               -> expectationFailure "encX が inline ColNum でない"++  describe "Phase 73.2: rankOf (rank plot・ArviZ plot_rank)" $ do+    let muModel :: ModelP ()+        muModel = sample "mu" (Normal 0 1) >> pure ()+        mkRkCh xs = Chain { chainSamples     = [ Map.singleton "mu" x | x <- xs ]+                          , chainAccepted    = 0+                          , chainTotal       = length xs+                          , chainEnergy      = []+                          , chainDivergences = []+                          , chainTreeDepths  = [] }+        rkXs1 = [ fromIntegral i * 0.5 | i <- [0 .. 39 :: Int] ]+        rkXs2 = map (+ 0.25) rkXs1+        rk2M  = HBMModel { hbmModelSpec = muModel+                         , hbmChainsR = [mkRkCh rkXs1, mkRkCh rkXs2]+                         , hbmData = [], hbmFactorLevels = [] }+        rk1M  = HBMModel { hbmModelSpec = muModel+                         , hbmChainsR = [mkRkCh rkXs1]   -- chain 1 本+                         , hbmData = [], hbmFactorLevels = [] }++    it "rankOf: 2 chain を横並び (dodge) した MBar 1 層" $ do+      let ls = vsLayers (head (rankOf rk2M))+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MBar++    it "rankOf: count 総和 = 全 chain の総標本数 (2 chain × 40)" $ do+      let l0 = head (vsLayers (head (rankOf rk2M)))+      case getLast (lyEncY l0) of+        Just (ColNum v) -> round (V.sum v) `shouldBe` (80 :: Int)+        _               -> expectationFailure "encY が inline ColNum でない"++    it "rankOf: chain 1 本なら空図 (rank が自明に一様)" $ do+      length (vsLayers (head (rankOf rk1M))) `shouldBe` 0++    it "rankOfBins: ビン数 k・chain c なら long-form bar は k×c 本" $ do+      let l0 = head (vsLayers (head (rankOfBins 10 rk2M)))+      case getLast (lyEncY l0) of+        Just (ColNum v) -> V.length v `shouldBe` 20   -- 10 bins × 2 chains+        _               -> expectationFailure "encY が inline ColNum でない"++  describe "Phase 50.4: hbmModelPure / ppcOf (HBM 純粋版・正本)" $ do+    let xdat = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] :: [Double]+        ydat = zipWith (\x e -> 1 + 2 * x + e) xdat+                 [0.12,-0.08,0.05,-0.15,0.10,0.03,-0.07,0.14,-0.04,0.06]+        cfg  = defaultHBM { hbmChains = 2, hbmSamples = 300, hbmWarmup = 300+                          , hbmSeed = Just 314 }+        dat  = [("x", xdat), ("y", ydat)]+        chainsData m = map chainSamples (hbmChainsR m)++    it "hbmModelPure: IO 無しで学習でき posterior が真値 (a≈1,b≈2) を復元" $ do+      let m = hbmModelPure cfg hbmLinModel dat+      length (hbmChainsR m) `shouldBe` 2+      abs (posteriorMeanOf "a" (hbmChainsR m) - 1) `shouldSatisfy` (< 0.5)+      abs (posteriorMeanOf "b" (hbmChainsR m) - 2) `shouldSatisfy` (< 0.2)++    it "hbmModelPure: 同 config なら chainSamples がビット同一 (再現性)" $ do+      let m1 = hbmModelPure cfg hbmLinModel dat+          m2 = hbmModelPure cfg hbmLinModel dat+      chainsData m1 `shouldBe` chainsData m2++    -- Phase 61.3: IO + 進捗表示版は bind + seed 規約 (chainSeeds 共有) が+    -- 同一ゆえ純粋版とビット一致するのが設計の柱 (進捗は stderr に出る)。+    it "hbmModelIO: hbmModelPure と chainSamples ビット同一 (Phase 61.3)" $ do+      mIO <- hbmModelIO cfg hbmLinModel dat+      let mP = hbmModelPure cfg hbmLinModel dat+      chainsData mIO `shouldBe` chainsData mP++    -- Phase 61.4: IO 動詞 (|->!) = fitIO。 HBM は進捗つき学習 (ビット一致)、+    -- 純粋 spec は既定実装 (pure . fitWith) で挙動不変。+    it "df |->! hbm == df |-> hbm (chainSamples ビット同一・Phase 61.4)" $ do+      mIO <- dat |->! hbm cfg hbmLinModel+      let mP = dat |-> hbm cfg hbmLinModel+      chainsData mIO `shouldBe` chainsData mP++    it "(|->!): 純粋 spec (lm) は既定 fitIO = pure . fitWith (Phase 61.4)" $ do+      let dat61 = [("x", [1, 2, 3, 4, 5]), ("y", [3, 5, 7, 9, 11])]+                    :: [(Text, [Double])]+      m1 <- dat61 |->! lm "x" "y"+      let m2 = dat61 |-> lm "x" "y"+      LA.toList (coefficientsV (lmResult m1))+        `shouldBe` LA.toList (coefficientsV (lmResult m2))++    it "ppcOf (純粋): 層 = y_rep + 観測・全て MDensity (純粋・プール線なし)" $ do+      let m  = hbmModelPure cfg hbmLinModel dat+          sp = ppcOfWith defaultPPC { ppcReps = 8, ppcSeed = Just 1 } m "obs"+          ls = vsLayers (toPlot sp)+      length ls `shouldBe` 8 + 1+      map (getFirst . lyKind) ls `shouldSatisfy` all (== Just MDensity)++    it "ppcOf (純粋): 同 seed なら y_rep がビット同一 (再現性)" $ do+      let m   = hbmModelPure cfg hbmLinModel dat+          c   = defaultPPC { ppcReps = 8, ppcSeed = Just 7 }+          dataOf sp = [ case getLast (lyEncX l) of+                          Just (ColNum v) -> V.toList v+                          _               -> []+                      | l <- vsLayers (toPlot sp) ] :: [[Double]]+      dataOf (ppcOfWith c m "obs") `shouldBe` dataOf (ppcOfWith c m "obs")++  describe "Phase 51.2: df |-> spec (二変量近道・ColumnSource)" $ do+    -- y = 2x + 1 を assoc データ源 ([(Text,[Double])] = core instance) で渡す。+    let dat51 = [ ("x", [1, 2, 3, 4, 5])+                , ("y", [3, 5, 7, 9, 11]) ] :: [(Text, [Double])]++    it "df |-> lm \"x\" \"y\" == lmModel xs ys (係数一致)" $ do+      let m1 = dat51 |-> lm "x" "y"+          m2 = lmModel xs ys+      LA.toList (coefficientsV (lmResult m1))+        `shouldSatisfy` allClose (LA.toList (coefficientsV (lmResult m2)))++    it "df |-> lm の係数 ≈ [1, 2] (intercept, slope)" $ do+      let m1 = dat51 |-> lm "x" "y"+      LA.toList (coefficientsV (lmResult m1)) `shouldSatisfy` allClose [1, 2]++    it "fitWith == (|->) (演算子は fitWith のラッパ)" $ do+      let m1 = dat51 |-> lm "x" "y"+          m2 = fitWith (lm "x" "y") dat51+      LA.toList (coefficientsV (lmResult m1))+        `shouldSatisfy` allClose (LA.toList (coefficientsV (lmResult m2)))++    it "df |-> glm Gauss Identity == glmModel (係数一致)" $ do+      let m1 = dat51 |-> glm Gaussian Identity "x" "y"+          m2 = glmModel Gaussian Identity xs ys+      LA.toList (coefficientsV (glmResult m1))+        `shouldSatisfy` allClose (LA.toList (coefficientsV (glmResult m2)))++    it "fitEither: 列が存在すれば Right" $ do+      case fitEither (lm "x" "y") dat51 of+        Right m1 -> LA.toList (coefficientsV (lmResult m1))+                      `shouldSatisfy` allClose [1, 2]+        Left e   -> expectationFailure ("Right を期待したが Left: " <> e)++    it "fitEither: 欠落列は Left (total・error を投げない)" $ do+      case fitEither (lm "nope" "y") dat51 of+        Left _  -> pure ()+        Right _ -> expectationFailure "欠落列で Left を期待"++    it "df |-> rq [0.5] == 中央値回帰 (傾き ≈ 2)" $ do+      let m1 = dat51 |-> rq [0.5] "x" "y"+      case qmFits m1 of+        [(t, qf)] -> do+          t `shouldBe` 0.5+          (LA.toList (qfBeta qf) !! 1) `shouldSatisfy` (\b -> abs (b - 2) < 1e-6)+        _         -> expectationFailure "分位 fit が 1 本でない"++  describe "Phase 70.3 項目 C: 透過標準化ラッパ (standardized / standardizedY)" $ do+    -- y = 2x + 1 (μx=3, σx=√2.5≈1.58114)。 X のみ標準化した内側 LM の係数は+    -- β1 = a1·σx = 2·1.58114 = 3.16228, β0 = a0 + a1·μx = 1 + 6 = 7 になる。+    let datC = [ ("x", [1, 2, 3, 4, 5])+               , ("y", [3, 5, 7, 9, 11]) ] :: [(Text, [Double])]+        -- スケール差の大きい 2 特徴 (x1 ~ O(1), x2 ~ O(1000))。 距離は x2 が支配。+        datKNN = [ ("x1", [0.1, 0.2, 0.3, 0.4, 0.5, 0.6])+                 , ("x2", [1000, 2000, 3000, 4000, 5000, 6000])+                 , ("y",  [1, 2, 3, 4, 5, 6]) ] :: [(Text, [Double])]++    it "predictorCols / responseCol が spec から正しく出る (列名を二重に書かない)" $ do+      predictorCols (knnReg 3 ["x1", "x2"] "y") `shouldBe` ["x1", "x2"]+      responseCol   (knnReg 3 ["x1", "x2"] "y") `shouldBe` Just "y"+      predictorCols (knnCls 3 ["x1", "x2"] "c") `shouldBe` ["x1", "x2"]+      responseCol   (knnCls 3 ["x1", "x2"] "c") `shouldBe` Nothing  -- 分類+      responseCol   (glm Poisson Log "x" "y")     `shouldBe` Nothing  -- family/link 拘束++    it "standardized (lm): 内側 LM は標準化空間の係数 [7, 3.16228]" $ do+      let StandardizedModel { smInner = inner, smXStd = sx, smYStd = sy } =+            datC |-> standardized (lm "x" "y")+      LA.toList (coefficientsV (lmResult inner)) `shouldSatisfy` allClose [7, sqrt 2.5 * 2]+      stMu sx `shouldSatisfy` allClose [3]+      stSd sx `shouldSatisfy` allClose [sqrt 2.5]+      sy `shouldBe` Nothing                                  -- X のみ → y 標準化なし++    it "standardizedY (lm): X+y 標準化で内側係数 ≈ [0, 1] (完全相関)" $ do+      let StandardizedModel { smInner = inner, smYStd = sy } =+            datC |-> standardizedY (lm "x" "y")+      LA.toList (coefficientsV (lmResult inner))+        `shouldSatisfy` (\cs -> allClose [0, 1] (map (\v -> if abs v < 1e-9 then 0 else v) cs))+      case sy of+        Just (muY, sdY) -> do+          muY `shouldSatisfy` (\v -> abs (v - 7) < 1e-9)        -- ȳ = 7+          sdY `shouldSatisfy` (\v -> abs (v - sqrt 10) < 1e-9)  -- σy = √10+        Nothing -> expectationFailure "standardizedY は smYStd = Just を期待"++    it "smTrain: 単変量 (予測子1列) では元スケール (x,y) を保持" $ do+      let StandardizedModel { smTrain = tr } = datC |-> standardized (lm "x" "y")+      tr `shouldBe` Just ([1, 2, 3, 4, 5], [3, 5, 7, 9, 11])++    it "standardized (knnReg): 内側は手動標準化 df の fit とビット一致" $ do+      -- 手動: 特徴行列を fitStandardizer/applyStandardizer で標準化 → 同じ列名 df を組む。+      let feats   = ["x1", "x2"]+          col n   = LA.fromList (maybe (error "col") id (lookup n datKNN))+          xm      = LA.fromColumns (map col feats)        -- n × p+          sx      = fitStandardizer xm+          xmZ     = applyStandardizer sx xm+          datZ    = zip feats (map LA.toList (LA.toColumns xmZ))+                      ++ [("y", [1, 2, 3, 4, 5, 6])] :: [(Text, [Double])]+          mWrap   = datKNN |-> standardized (knnReg 3 feats "y")+          mManual = datZ   |-> knnReg 3 feats "y"+          -- 標準化空間の query (訓練点) で予測を突合。+          q       = xmZ+      VU.toList (predictKNNR (smInner mWrap) q)+        `shouldBe` VU.toList (predictKNNR mManual q)++    it "ガード: 内部標準化済 spec (regularized) に standardized → Left" $ do+      case fitEither (standardized (ridge ["x1", "x2"] "y")) datKNN of+        Left _  -> pure ()+        Right _ -> expectationFailure "predictorCols=[] の spec は Left を期待"++    it "ガード: スケール不変 spec (randomForestReg) に standardized → Left" $ do+      case fitEither (standardized (randomForestReg defaultRandomForest 42 ["x1", "x2"] "y")) datKNN of+        Left _  -> pure ()+        Right _ -> expectationFailure "木系は predictorCols=[] ゆえ Left を期待"++    it "ガード: standardizedY を分類 spec (knnCls) に付けると Left" $ do+      case fitEither (standardizedY (knnCls 3 ["x1", "x2"] "y")) datKNN of+        Left _  -> pure ()+        Right _ -> expectationFailure "responseCol=Nothing への standardizedY は Left を期待"++    it "ガード: standardizedY を GLM (family/link 拘束) に付けると Left" $ do+      case fitEither (standardizedY (glm Gaussian Identity "x" "y")) datC of+        Left _  -> pure ()+        Right _ -> expectationFailure "GLM の responseCol=Nothing ゆえ standardizedY は Left を期待"++    -- --- C2: 元スケール逆変換 (SingleVarModel / Plottable) ---+    let plainLM = lmModel (LA.fromList [1, 2, 3, 4, 5]) (LA.fromList [3, 5, 7, 9, 11])+        gC      = [1.5, 2.5, 3.5, 4.5]++    it "C2 svRange: standardized ラッパは元スケールの x 範囲 (1,5) を返す" $ do+      let wrap = datC |-> standardized (lm "x" "y")+      svRange wrap `shouldSatisfy` (\(lo, hi) -> abs (lo - 1) < 1e-9 && abs (hi - 5) < 1e-9)++    it "C2 svGrid: standardized (lm) の元スケール予測が plain lm と一致 (X 標準化は ŷ 不変)" $ do+      let wrap     = datC |-> standardized (lm "x" "y")+          (muW, _) = svGrid wrap 0.95 gC+          (muP, _) = svGrid plainLM 0.95 gC+      muW `shouldSatisfy` allClose muP++    it "C2 svGrid: standardizedY (lm) も round-trip で plain lm 予測に一致" $ do+      let wrap     = datC |-> standardizedY (lm "x" "y")+          (muW, _) = svGrid wrap 0.95 gC+          (muP, _) = svGrid plainLM 0.95 gC+      muW `shouldSatisfy` allClose muP++    it "C2 svGrid CI band: standardized (lm) の帯も plain lm と一致" $ do+      let wrap = datC |-> standardized (lm "x" "y")+      case (svGrid wrap 0.95 gC, svGrid plainLM 0.95 gC) of+        ((_, Just (loW, hiW)), (_, Just (loP, hiP))) -> do+          loW `shouldSatisfy` allClose loP+          hiW `shouldSatisfy` allClose hiP+        _ -> expectationFailure "両者とも CI band (Just) を期待"++    it "C2 svCoefR2: 元スケール係数が plain lm [1,2] に一致 (standardized / standardizedY)" $ do+      let plainCs = LA.toList (coefficientsV (lmResult plainLM))+      case (svCoefR2 (datC |-> standardized  (lm "x" "y")),+            svCoefR2 (datC |-> standardizedY (lm "x" "y"))) of+        (Just (csX, _), Just (csXY, _)) -> do+          csX  `shouldSatisfy` allClose plainCs+          csXY `shouldSatisfy` allClose plainCs+          csX  `shouldSatisfy` allClose [1, 2]+        _ -> expectationFailure "両ラッパとも線形ゆえ svCoefR2 = Just を期待"++    it "C2 svGrid: standardized (knnReg・1 特徴) は plain kNN と一致 (単調変換で近傍不変)" $ do+      let dat1   = [ ("x", [1, 2, 3, 4, 5, 6, 7, 8])+                   , ("y", [1, 3, 2, 5, 4, 7, 6, 9]) ] :: [(Text, [Double])]+          wrap   = dat1 |-> standardized (knnReg 2 ["x"] "y")+          plainK = fitKNNR 2 (LA.fromColumns [LA.fromList [1,2,3,4,5,6,7,8]])+                             (VU.fromList [1,3,2,5,4,7,6,9])+          g      = [1.5, 3.0, 5.5, 7.0]+          (muW, mbBand) = svGrid wrap 0.95 g+          muP    = VU.toList (predictKNNR plainK (LA.fromColumns [LA.fromList g]))+      muW `shouldSatisfy` allClose muP+      mbBand `shouldBe` Nothing                       -- kNN は band なし++    it "C2 toPlot: standardized ラッパは散布 + 曲線の 2 レイヤ以上を出す" $ do+      let wrap = datC |-> standardized (lm "x" "y")+      length (vsLayers (toPlot wrap)) `shouldSatisfy` (>= 2)++  describe "Phase 70.4?: quantileMulti (多変量分位点回帰)" $ do+    -- 厳密線形 (無誤差) y = 1 + 2·x1 + 3·x2。 無誤差なら全 τ が同じ β=[1,2,3] を復元。+    let dfQM = [ ("x1", [1, 2, 3, 1, 2, 3, 1, 2, 3])+               , ("x2", [0, 0, 0, 1, 1, 1, 2, 2, 2])+               , ("y",  [ 1 + 2*a + 3*b+                        | (a, b) <- zip [1,2,3,1,2,3,1,2,3] [0,0,0,1,1,1,2,2,2] ]) ]+              :: [(Text, [Double])]++    it "predictorCols / responseCol が正しい" $ do+      predictorCols (rqMulti [0.5] ["x1", "x2"] "y") `shouldBe` ["x1", "x2"]+      responseCol   (rqMulti [0.5] ["x1", "x2"] "y") `shouldBe` Just "y"++    it "★無誤差線形: 全 τ が β=[1,2,3] を復元 (intercept, x1, x2)" $ do+      let m = dfQM |-> rqMulti [0.25, 0.5, 0.75] ["x1", "x2"] "y"+      length (mqmFits m) `shouldBe` 3+      mqmNames m `shouldBe` ["x1", "x2"]+      -- QR は MM-IRLS (eps=1e-6) ゆえ厳密一致でなく 1e-5 許容で突合。+      mapM_ (\(_, qf) -> LA.toList (qfBeta qf)+                `shouldSatisfy` (\bs -> and (zipWith (\a b -> abs (a - b) < 1e-5) bs [1, 2, 3])))+            (mqmFits m)++    it "toPlot は τ ごとに 1 本ずつ線を出す (3 τ → 3 MLine)" $ do+      let m  = dfQM |-> rqMulti [0.1, 0.5, 0.9] ["x1", "x2"] "y"+          ls = vsLayers (toPlot m)+      length ls `shouldBe` 3+      all (\l -> getFirst (lyKind l) == Just MLine) ls `shouldBe` True++  describe "Phase 51.3: df |-> formula spec (R 流多変量)" $ do+    -- y = 1 + 2 x1 + 3 x2 (完全線形・OLS が係数を厳密復元)。+    let dfMV = DX.fromNamedColumns+                 [ ("x1", DX.fromList ([1, 2, 3, 4, 5] :: [Double]))+                 , ("x2", DX.fromList ([2, 1, 4, 3, 6] :: [Double]))+                 , ("y",  DX.fromList ([9, 8, 19, 18, 29] :: [Double])) ]+        assocMV = [ ("x1", [1, 2, 3, 4, 5])+                  , ("x2", [2, 1, 4, 3, 6])+                  , ("y",  [9, 8, 19, 18, 29]) ] :: [(Text, [Double])]+        -- glmmF 用 (text factor 列 = toFrame=id で温存する canonical 経路)。+        dfRE = DX.fromNamedColumns+                 [ ("x",     DX.fromList ([1,2,3,4, 1,2,3,4, 1,2,3,4] :: [Double]))+                 , ("y",     DX.fromList ([7.1,6.9,7.0,7.0, 5.0,4.9,5.1,5.0, 3.0,2.9,3.1,3.0] :: [Double]))+                 , ("group", DX.fromList (["A","A","A","A","B","B","B","B","C","C","C","C"] :: [T.Text])) ]++    it "df |-> lmF == multiLMModel (係数一致)" $ do+      let m1 = dfMV |-> lmF "y ~ x1 + x2"+      case multiLMModel "y ~ x1 + x2" dfMV of+        Right m2 -> LA.toList (coefficientsV (mlmResult m1))+                      `shouldSatisfy` allClose (LA.toList (coefficientsV (mlmResult m2)))+        Left e   -> expectationFailure e++    it "df |-> lmF の係数 ≈ [1, 2, 3] (完全線形を復元)" $ do+      let m1 = dfMV |-> lmF "y ~ x1 + x2"+      LA.toList (coefficientsV (mlmResult m1)) `shouldSatisfy` allClose [1, 2, 3]++    it "assoc 源 (toFrame 数値再構築) でも同じ係数" $ do+      let m1 = assocMV |-> lmF "y ~ x1 + x2"+      LA.toList (coefficientsV (mlmResult m1)) `shouldSatisfy` allClose [1, 2, 3]++    it "df |-> glmF Gaussian Identity == multiGLMModel (係数一致)" $ do+      let m1 = dfMV |-> glmF Gaussian Identity "y ~ x1 + x2"+      case multiGLMModel Gaussian Identity "y ~ x1 + x2" dfMV of+        Right m2 -> LA.toList (coefficientsV (mglmResult m1))+                      `shouldSatisfy` allClose (LA.toList (coefficientsV (mglmResult m2)))+        Left e   -> expectationFailure e++    it "fitEither: formula parse 失敗は Left (total)" $ do+      case fitEither (lmF "garbage") dfMV of+        Left _  -> pure ()+        Right _ -> expectationFailure "parse 失敗で Left を期待"++    it "df |-> glmmF (1|group): Right で固定効果 2 個 (toFrame=id で factor 温存)" $ do+      case fitEither (glmmF "y ~ x + (1|group)") dfRE of+        Right (_, labels) -> length labels `shouldBe` 2+        Left e            -> expectationFailure e++  describe "Phase 51.4: df |-> hbm + dataScatterOf (ColumnSource→HBM)" $ do+    let xdat4 = [1,2,3,4,5,6,7,8,9,10] :: [Double]+        ydat4 = zipWith (\x e -> 1 + 2 * x + e) xdat4+                  [0.12,-0.08,0.05,-0.15,0.10,0.03,-0.07,0.14,-0.04,0.06]+        cfg4  = defaultHBM { hbmChains = 2, hbmSamples = 200, hbmWarmup = 200+                           , hbmSeed = Just 314 }+        assoc4 = [("x", xdat4), ("y", ydat4)] :: [(Text, [Double])]+        df4    = DX.fromNamedColumns [ ("x", DX.fromList xdat4)+                                     , ("y", DX.fromList ydat4) ]+        coldata4 = [ ("x", NumData (V.fromList xdat4))+                   , ("y", NumData (V.fromList ydat4)) ] :: [(Text, ColData)]+        sams m = map chainSamples (hbmChainsR m)++    it "assoc |-> hbm == hbmModelPure (chainSamples ビット同一)" $ do+      let m1 = assoc4 |-> hbm cfg4 hbmLinModel+          m2 = hbmModelPure cfg4 hbmLinModel assoc4+      sams m1 `shouldBe` sams m2++    it "DataFrame 源でも posterior が真値 (a≈1, b≈2)" $ do+      let m = df4 |-> hbm cfg4 hbmLinModel+      abs (posteriorMeanOf "a" (hbmChainsR m) - 1) `shouldSatisfy` (< 0.5)+      abs (posteriorMeanOf "b" (hbmChainsR m) - 2) `shouldSatisfy` (< 0.2)++    it "ColData 源 (NumData) でも assoc と同じ結果 (ビット同一)" $ do+      let m1 = coldata4 |-> hbm cfg4 hbmLinModel+          m2 = assoc4   |-> hbm cfg4 hbmLinModel+      sams m1 `shouldBe` sams m2++    it "dataScatterOf: hbmData から scatter 層 1 枚 (10 点・MScatter)" $ do+      let m  = assoc4 |-> hbm cfg4 hbmLinModel+          ls = vsLayers (dataScatterOf m "x" "y")+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MScatter+      case getLast (lyEncX (head ls)) of+        Just (ColNum v) -> V.length v `shouldBe` 10+        _               -> expectationFailure "encX が inline ColNum でない"++    it "dataScatterOf: 欠落列なら空 (mempty)" $ do+      let m = assoc4 |-> hbm cfg4 hbmLinModel+      vsLayers (dataScatterOf m "nope" "y") `shouldBe` []++  describe "Phase 60.3: DataIx 束縛 + Integer 許容 + 突合 loud error" $ do+    let cfg60 = defaultHBM { hbmChains = 1, hbmSamples = 100, hbmWarmup = 100+                           , hbmSeed = Just 60 }+        -- 群 A ≈ 1、 群 B ≈ 5。 行順は B,A,B,A,... (sort 順コード化の検証:+        -- 出現順なら B=0 になるが、 sort 順なら A=0)。+        gTxt  = ["B","A","B","A","B","A","B","A"] :: [T.Text]+        yMix  = [5.1, 0.9, 4.9, 1.1, 5.0, 1.0, 5.2, 0.8] :: [Double]+        dfFac = DX.fromNamedColumns [ ("g", DX.fromList gTxt)+                                    , ("y", DX.fromList yMix) ]++    it "Text factor 列 → sort 順コード化 (levels=[A,B]・mu0=A群≈1, mu1=B群≈5)" $ do+      let m = dfFac |-> hbm cfg60 hbmIxModel+      hbmFactorLevels m `shouldBe` [("g", ["A", "B"])]+      abs (posteriorMeanOf "mu0" (hbmChainsR m) - 1) `shouldSatisfy` (< 0.5)+      abs (posteriorMeanOf "mu1" (hbmChainsR m) - 5) `shouldSatisfy` (< 0.5)++    it "Int 数値列でも DataIx slot に直結 (levels は空)" $ do+      let dfNum = DX.fromNamedColumns+                    [ ("g", DX.fromList ([1,0,1,0,1,0,1,0] :: [Int]))+                    , ("y", DX.fromList yMix) ]+          m = dfNum |-> hbm cfg60 hbmIxModel+      hbmFactorLevels m `shouldBe` []+      abs (posteriorMeanOf "mu0" (hbmChainsR m) - 1) `shouldSatisfy` (< 0.5)+      abs (posteriorMeanOf "mu1" (hbmChainsR m) - 5) `shouldSatisfy` (< 0.5)++    it "Integer 列が dataNamedX (連続) で黙殺されず通る (60.3a 根治の確認)" $ do+      let dfInt = DX.fromNamedColumns+                    [ ("x", DX.fromList ([1..10] :: [Integer]))+                    , ("y", DX.fromList (zipWith (\x e -> 1 + 2 * x + e)+                                          ([1..10] :: [Double])+                                          [0.12,-0.08,0.05,-0.15,0.10+                                          ,0.03,-0.07,0.14,-0.04,0.06])) ]+      lookupCol "x" dfInt `shouldBe` Just (map fromIntegral [1..10 :: Int])+      let m = dfInt |-> hbm cfg60 hbmLinModel+      abs (posteriorMeanOf "b" (hbmChainsR m) - 2) `shouldSatisfy` (< 0.3)++    it "空 placeholder dataNamed の列欠落は fitEither Left (loud)" $ do+      let dfNoX = DX.fromNamedColumns [ ("y", DX.fromList yMix) ]+      case fitEither (hbm cfg60 hbmLinModel) dfNoX of+        Left e  -> e `shouldSatisfy` (("dataNamed" `T.isInfixOf`) . T.pack)+        Right _ -> expectationFailure "列欠落 (x) で Left を期待"++    it "空 placeholder dataNamedIx の列欠落も Left (loud)" $ do+      let dfNoG = DX.fromNamedColumns [ ("y", DX.fromList yMix) ]+      case fitEither (hbm cfg60 hbmIxModel) dfNoG of+        Left e  -> e `shouldSatisfy` (("dataNamedIx" `T.isInfixOf`) . T.pack)+        Right _ -> expectationFailure "列欠落 (g) で Left を期待"++    it "非整数の数値列を DataIx slot に bind すると Left" $ do+      let dfBad = DX.fromNamedColumns+                    [ ("g", DX.fromList ([0.5, 1.5, 0.5, 1.5, 0.5, 1.5, 0.5, 1.5] :: [Double]))+                    , ("y", DX.fromList yMix) ]+      case fitEither (hbm cfg60 hbmIxModel) dfBad of+        Left e  -> e `shouldSatisfy` (("非整数" `T.isInfixOf`) . T.pack)+        Right _ -> expectationFailure "非整数列で Left を期待"++  describe "Phase 52.A4: grouped (群別フィット・HBM 整合)" $ do+    -- 群 0 = y=2x (傾き 2)、 群 1 = y=5x (傾き 5)。 数値群列 "g"。+    let datG = [ ("x", [1, 2, 3, 4,  1, 2, 3, 4])+               , ("y", [2, 4, 6, 8,  5, 10, 15, 20])+               , ("g", [0, 0, 0, 0,  1, 1, 1, 1]) ] :: [(Text, [Double])]+        gfG  = datG |-> grouped "g" (lm "x" "y")+        slopeOf m = LA.toList (coefficientsV (lmResult m)) !! 1+        lineLayers vs = [ l | l <- vsLayers vs, getFirst (lyKind l) == Just MLine ]++    it "groupLabels: 群が 2 つ (出現順 [\"0\", \"1\"])" $ do+      groupLabels gfG `shouldBe` ["0", "1"]++    it "groupModels: 各群を別々に fit (群 0 傾き≈2・群 1 傾き≈5)" $ do+      case map snd (groupModels gfG) of+        [m0, m1] -> do+          slopeOf m0 `shouldSatisfy` (\b -> abs (b - 2) < 1e-9)+          slopeOf m1 `shouldSatisfy` (\b -> abs (b - 5) < 1e-9)+        _        -> expectationFailure "群モデルが 2 本でない"++    it "toPlot: 群数ぶんの MLine layer (2 本)・各線は ColorByCol" $ do+      let ls = lineLayers (toPlot gfG)+      length ls `shouldBe` 2+      let colorEncs = map (getLast . lyColor) ls+      colorEncs `shouldSatisfy` all (\x -> case x of+        Just (ColorByCol _) -> True+        _                   -> False)++    it "toPlot: scaleColorManual で群色固定 (effectPalette) + legend" $ do+      let vs = toPlot gfG+      getLast (vsColorManual vs)+        `shouldBe` Just [("0", "#1f77b4"), ("1", "#ff7f0e")]+      getLast (vsLegend vs) `shouldSatisfy` \x -> case x of+        Just _  -> True+        Nothing -> False++    it "factor (文字) 群列でも分割できる (DataFrame 源・getTextVec 経路)" $ do+      let dfG = DX.fromNamedColumns+                  [ ("x", DX.fromList ([1, 2, 3, 4, 1, 2, 3, 4] :: [Double]))+                  , ("y", DX.fromList ([2, 4, 6, 8, 5, 10, 15, 20] :: [Double]))+                  , ("g", DX.fromList (["a", "a", "a", "a", "b", "b", "b", "b"] :: [Text])) ]+          gf = dfG |-> grouped "g" (lm "x" "y")+      groupLabels gf `shouldBe` ["a", "b"]+      case map snd (groupModels gf) of+        [m0, m1] -> do+          slopeOf m0 `shouldSatisfy` (\b -> abs (b - 2) < 1e-9)+          slopeOf m1 `shouldSatisfy` (\b -> abs (b - 5) < 1e-9)+        _        -> expectationFailure "群モデルが 2 本でない"++    it "fitEither: 群列が無ければ Left (total)" $ do+      case fitEither (grouped "nope" (lm "x" "y")) datG of+        Left _  -> pure ()+        Right _ -> expectationFailure "欠落群列で Left を期待"++  describe "Phase 52.A7: groupedFullrange (回帰線をデータ全幅へ延長)" $ do+    -- 群 0 = x∈[1..4]・y=2x、 群 1 = x∈[10..13]・y=5x。 x 範囲が群間で重ならない。+    let datF = [ ("x", [1, 2, 3, 4,   10, 11, 12, 13])+               , ("y", [2, 4, 6, 8,   50, 55, 60, 65])+               , ("g", [0, 0, 0, 0,    1,  1,  1,  1]) ] :: [(Text, [Double])]+        gfF  = datF |-> grouped "g" (lm "x" "y")+        lineLayers' vs = [ l | l <- vsLayers vs, getFirst (lyKind l) == Just MLine ]+        xRangeOf l = case getLast (lyEncX l) of+          Just (ColNum vx) -> let xs = V.toList vx in (minimum xs, maximum xs)+          _                -> error "encX が inline ColNum でない"++    it "既定 toPlot: 各群線は自群の x 範囲のみ ([1,4] と [10,13])" $ do+      case lineLayers' (toPlot gfF) of+        [l0, l1] -> do+          xRangeOf l0 `shouldSatisfy` \(lo, hi) -> abs (lo - 1) < 1e-9 && abs (hi - 4) < 1e-9+          xRangeOf l1 `shouldSatisfy` \(lo, hi) -> abs (lo - 10) < 1e-9 && abs (hi - 13) < 1e-9+        _        -> expectationFailure "群線が 2 本でない"++    it "groupedFullrange: 各群線が全群 union 範囲 [1,13] へ延長される" $ do+      case lineLayers' (groupedFullrange gfF) of+        [l0, l1] -> do+          xRangeOf l0 `shouldSatisfy` \(lo, hi) -> abs (lo - 1) < 1e-9 && abs (hi - 13) < 1e-9+          xRangeOf l1 `shouldSatisfy` \(lo, hi) -> abs (lo - 1) < 1e-9 && abs (hi - 13) < 1e-9+        _        -> expectationFailure "群線が 2 本でない"++    it "groupedFullrange: 傾き・凡例は toPlot と不変 (range のみ拡張)" $ do+      let vsF = groupedFullrange gfF+      -- 凡例 (scaleColorManual) は既定と同じ群色固定。+      getLast (vsColorManual vsF) `shouldBe` Just [("0", "#1f77b4"), ("1", "#ff7f0e")]+      -- 延長端でも各線は当該群の傾きを保つ (群 1 は y=5x ゆえ x=1 で μ≈5)。+      case lineLayers' vsF of+        [_, l1] -> case (getLast (lyEncX l1), getLast (lyEncY l1)) of+          (Just (ColNum vx), Just (ColNum vy)) -> do+            -- linspace は昇順 (lo=1 が先頭) ゆえ先頭が x=1 での μ̂。+            V.head vx `shouldSatisfy` (\x -> abs (x - 1) < 1e-9)+            V.head vy `shouldSatisfy` (\y -> abs (y - 5) < 1e-6)+          _ -> expectationFailure "encX/encY が inline ColNum でない"+        _       -> expectationFailure "群線が 2 本でない"++  describe "Phase 52.A9: lmDiag (係数診断の薄アクセサ)" $ do+    -- statsmodels OLS と突合: x=[1..5], y=[2.1,3.9,6.2,7.8,10.1]。+    -- intercept: SE=0.19807406 t=0.25243083 p=0.81701518+    -- slope    : SE=0.05972158 t=33.32129066 p=5.94153911e-5+    let m9 = lmModel (LA.fromList [1, 2, 3, 4, 5])+                     (LA.fromList [2.1, 3.9, 6.2, 7.8, 10.1])++    it "lmDiag: 係数 2 つ ([(Intercept), slope]) の CoefStats" $ do+      length (lmDiag m9) `shouldBe` 2++    it "lmDiag: SE/t/p が statsmodels と一致 (intercept)" $ do+      case lmDiag m9 of+        (c0 : _) -> do+          csSE c0     `shouldSatisfy` \v -> abs (v - 0.19807406) < 1e-6+          csTValue c0 `shouldSatisfy` \v -> abs (v - 0.25243083) < 1e-6+          csPValue c0 `shouldSatisfy` \v -> abs (v - 0.81701518) < 1e-6+        _ -> expectationFailure "係数が空"++    it "lmDiag: SE/t/p が statsmodels と一致 (slope)" $ do+      case lmDiag m9 of+        (_ : c1 : _) -> do+          csSE c1     `shouldSatisfy` \v -> abs (v - 0.05972158) < 1e-6+          csTValue c1 `shouldSatisfy` \v -> abs (v - 33.32129066) < 1e-5+          csPValue c1 `shouldSatisfy` \v -> abs (v - 5.94153911e-5) < 1e-9+        _ -> expectationFailure "slope が無い"++    it "groupedLmDiag: 各群の係数診断を群ラベル付きで取り出す" $ do+      -- 群 0 = y=2x, 群 1 = y=5x (傾きは厳密ゆえ SE≈0・t は大)。+      let datG = [ ("x", [1, 2, 3, 4,  1, 2, 3, 4])+                 , ("y", [2, 4, 6, 8,  5, 10, 15, 20])+                 , ("g", [0, 0, 0, 0,  1, 1, 1, 1]) ] :: [(Text, [Double])]+          gf   = datG |-> grouped "g" (lm "x" "y")+          diags = groupedLmDiag gf+      map fst diags `shouldBe` ["0", "1"]+      map (length . snd) diags `shouldBe` [2, 2]   -- 各群 intercept+slope++  describe "Phase 52.A6: weighted (WLS 露出)" $ do+    -- statsmodels WLS と突合: x=[1..5], y=[2.1,3.9,6.2,7.8,10.1], w=[1..5]。+    -- beta=[0.00285714, 2.00285714], rsquared=0.99619888,+    -- mean_ci @x=3,5: lower=[5.68995295,9.60211961] upper=[6.33290419,10.4321661]+    let datW6 = [ ("x", [1, 2, 3, 4, 5])+                , ("y", [2.1, 3.9, 6.2, 7.8, 10.1])+                , ("w", [1, 2, 3, 4, 5])      -- 重み列 (列名で参照)+                , ("w1", [1, 1, 1, 1, 1])     -- 全重み 1 (OLS 一致確認用)+                , ("wbad", [1, 2, 3]) ] :: [(Text, [Double])]  -- 長さ不一致 (Left 確認用)+        wm6   = datW6 |-> weighted "w" (lm "x" "y")+        nearW a b = abs (a - b) < 1e-6+        allNearW xs ys = and (zipWith nearW xs ys)++    it "weighted β̂ が statsmodels WLS と一致" $ do+      LA.toList (coefficientsV (lmResult (wlmInner wm6)))+        `shouldSatisfy` allNearW [0.00285714, 2.00285714]++    it "weighted svGrid (CI) が statsmodels WLS mean_ci と一致 (x=3,5)" $ do+      case svGrid wm6 0.95 [3.0, 5.0] of+        (_, Just (los, his)) -> do+          los `shouldSatisfy` allNearW [5.68995295, 9.60211961]+          his `shouldSatisfy` allNearW [6.33290419, 10.4321661]+        _ -> expectationFailure "WLS CI 帯が出ない"++    it "weighted svCoefR2 の R² が statsmodels WLS rsquared と一致 (weighted R²)" $ do+      case svCoefR2 wm6 of+        Just (_, r2) -> r2 `shouldSatisfy` nearW 0.9961988825728395+        Nothing      -> expectationFailure "svCoefR2 が Nothing"++    it "全重み 1 の WLS は OLS と一致" $ do+      let wm1 = datW6 |-> weighted "w1" (lm "x" "y")+          ols = datW6 |-> lm "x" "y"+      LA.toList (coefficientsV (lmResult (wlmInner wm1)))+        `shouldSatisfy` allNearW (LA.toList (coefficientsV (lmResult ols)))++    it "重み列長が観測数と不一致なら Left" $ do+      case fitEither (weighted "wbad" (lm "x" "y")) datW6 of+        Left _  -> pure ()+        Right _ -> expectationFailure "長さ不一致が Left にならない"++    it "toPlot は grid 経路 (line layer) を返す (訓練点経路を使わない)" $ do+      length (vsLayers (toPlot wm6)) `shouldSatisfy` (>= 1)++  describe "Phase 52.A5 / 70.F: bandMode CI/PI (予測区間)" $ do+    -- statsmodels OLS の mean_ci / obs_ci と突合する固定データ (σ̂²>0)。+    let xsA5 = LA.fromList [1, 2, 3, 4, 5]+        ysA5 = LA.fromList [2.1, 3.9, 6.2, 7.8, 10.1]+        mA5  = lmModel xsA5 ysA5+        gxA5 = [3.0, 5.0]+        near a b = abs (a - b) < 1e-6+        allNear xs ys = and (zipWith near xs ys)++    it "svGrid (CI) が statsmodels mean_ci と一致 (x=3,5)" $ do+      case svGrid mA5 0.95 gxA5 of+        (_, Just (los, his)) -> do+          los `shouldSatisfy` allNear [5.75121357, 9.53444824]+          his `shouldSatisfy` allNear [6.28878643, 10.46555176]+        _ -> expectationFailure "CI 帯が出ない"++    it "svGridPI (PI) が statsmodels obs_ci と一致 (x=3,5)" $ do+      case svGridPI mA5 0.95 gxA5 of+        Just (los, his) -> do+          los `shouldSatisfy` allNear [5.36161039, 9.23975716]+          his `shouldSatisfy` allNear [6.67838961, 10.76024284]+        Nothing -> expectationFailure "PI 帯が出ない"++    it "PI ⊃ CI (各点で PI の方が広い)" $ do+      case (svGrid mA5 0.95 gxA5, svGridPI mA5 0.95 gxA5) of+        ((_, Just (clo, chi)), Just (plo, phi)) -> do+          and (zipWith (<) plo clo) `shouldBe` True   -- PI 下限 < CI 下限+          and (zipWith (>) phi chi) `shouldBe` True   -- PI 上限 > CI 上限+        _ -> expectationFailure "帯が揃わない"++    it "GLM Gaussian/Identity の PI は LM と一致 (closed form 帰着)" $ do+      let gm = glmModel Gaussian Identity xsA5 ysA5+      case (svGridPI gm 0.95 gxA5, svGridPI mA5 0.95 gxA5) of+        (Just (glo, ghi), Just (llo, lhi)) -> do+          allNear glo llo `shouldBe` True+          allNear ghi lhi `shouldBe` True+        _ -> expectationFailure "GLM/LM PI が揃わない"++    it "非 Gaussian GLM は PI=Nothing (over-claim しない)" $ do+      let gp = glmModel Poisson Log xsA5 (LA.fromList [1, 2, 3, 5, 8])+      svGridPI gp 0.95 gxA5 `shouldBe` Nothing++    it "GAM は PI=Nothing (既定実装)" $ do+      let gm = gamModel 3 5 0.0 xsA5 ysA5+      svGridPI gm 0.95 gxA5 `shouldBe` Nothing++    it "renderGrid: bandMode BandPI の帯は CI より広い" $ do+      let bandLayerOf spec = head [ l | l <- vsLayers (toPlot spec)+                                      , getFirst (lyKind l) == Just MBand ]+          loY l = case getLast (lyEncY l) of+            Just (ColNum v) -> V.toList v+            _               -> error "encY が ColNum でない"+          spec k = statModel mA5 <> gridRange 3 5 <> grid 2 <> bandMode k+          ciLo = loY (bandLayerOf (spec BandCI))+          piLo = loY (bandLayerOf (spec BandPI))+      and (zipWith (<) piLo ciLo) `shouldBe` True   -- PI 帯下限 < CI 帯下限+      head piLo `shouldSatisfy` near 5.36161039     -- PI 下限 = obs_ci 下限++    it "renderGrid: BandPI は GAM (PI 非提供・CI 提供) で CI へフォールバックし帯が出る (Phase 70.6 G)" $ do+      let gm   = gamModel 3 5 0.0 xsA5 ysA5+          spec = statModel gm <> gridRange 1 5 <> grid 5 <> bandMode BandPI+          bands = [ l | l <- vsLayers (toPlot spec), getFirst (lyKind l) == Just MBand ]+      -- GAM は CI (svGrid Just) を持つが PI (svGridPI) は Nothing。BandPI は CI へフォールバック。+      length bands `shouldBe` 1++  describe "Phase 70.H: ブートストラップ CI/PI (piMethod PIBootstrap)" $ do+    let bxs = [1,2,3,4,5,6,7,8,9,10,11,12] :: [Double]+        bys = [2.4,3.6,6.5,7.4,11.1,11.9,15.2,15.8,19.0,20.4,22.7,24.1] :: [Double]+        bdf = [ ("x", NumData (V.fromList bxs)), ("y", NumData (V.fromList bys)) ]+              :: [(Text, ColData)]+        ycount = [1,2,2,4,5,8,11,15,20,27,36,49] :: [Double]+        bdfP = [ ("x", NumData (V.fromList bxs)), ("y", NumData (V.fromList ycount)) ]+               :: [(Text, ColData)]+        boot = PIBootstrap 7 400      -- seed 7・400 draws+        -- statModel model <> bandMode <> piMethod (PIBootstrap …) の第 1 MBand 層の (lo, hi)。+        bandBoundsP model mode pm =+          let ls = vsLayers (toPlot (statModel model <> grid 100 <> bandMode mode <> piMethod pm))+              b  = head [ l | l <- ls, getFirst (lyKind l) == Just MBand ]+              col f = case getLast (f b) of+                Just (ColNum v) -> V.toList v+                _               -> error "band encoding が ColNum でない"+          in (col lyEncY, col lyEncY2)+        lmM   = bdf  |-> lm "x" "y"                          :: LMModel+        glmM  = bdfP |-> glm Poisson Log "x" "y"             :: GLMModel+        robM  = bdf  |-> rlm (Huber defaultHuberK) "x" "y" :: RobustModel++    it "決定的: 同 seed → ビット同一の帯 (PI)" $ do+      bandBoundsP lmM BandPI boot `shouldBe` bandBoundsP lmM BandPI boot++    it "別 seed → 異なる帯 (実際に確率的)" $ do+      (bandBoundsP lmM BandPI boot == bandBoundsP lmM BandPI (PIBootstrap 99 400))+        `shouldBe` False++    it "LM: bootstrap PI ⊃ CI かつ grid 100 点" $ do+      let (clo, chi) = bandBoundsP lmM BandCI boot+          (plo, phi) = bandBoundsP lmM BandPI boot+      length clo `shouldBe` 100+      and (zipWith (<=) plo clo) `shouldBe` True+      and (zipWith (>=) phi chi) `shouldBe` True++    it "非 Gaussian GLM (Poisson/Log) でも bootstrap PI が出る (closed-form 非対応)" $ do+      let (clo, chi) = bandBoundsP glmM BandCI boot+          (plo, phi) = bandBoundsP glmM BandPI boot+      length plo `shouldBe` 100+      and (zipWith (<=) plo clo) `shouldBe` True   -- PI 下限 ≤ CI 下限+      and (zipWith (>=) phi chi) `shouldBe` True++    it "ロバスト (Huber) でも bootstrap PI が出る" $ do+      let (clo, chi) = bandBoundsP robM BandCI boot+          (plo, phi) = bandBoundsP robM BandPI boot+      and (zipWith (<=) plo clo) `shouldBe` True+      and (zipWith (>=) phi chi) `shouldBe` True++    it "piMethod 既定 (PIClosedForm) は closed-form 帯と一致" $ do+      let viaDefault = vsLayers (toPlot (statModel lmM <> grid 20 <> bandMode BandCI))+          viaClosed  = vsLayers (toPlot (statModel lmM <> grid 20 <> bandMode BandCI+                                          <> piMethod PIClosedForm))+      map (getFirst . lyKind) viaDefault `shouldBe` map (getFirst . lyKind) viaClosed++  describe "Phase 52.D3: GLMMResultRE + toPlot (混合効果 caterpillar)" $ do+    -- 3 群 (A,B,C) の random intercept。 群ごとに水準が違う (A≈7, B≈5, C≈3)。+    let dfRE3 = DX.fromNamedColumns+                  [ ("x",     DX.fromList ([1,2,3,4, 1,2,3,4, 1,2,3,4] :: [Double]))+                  , ("y",     DX.fromList ([7.1,6.9,7.0,7.0, 5.0,4.9,5.1,5.0, 3.0,2.9,3.1,3.0] :: [Double]))+                  , ("group", DX.fromList (["A","A","A","A","B","B","B","B","C","C","C","C"] :: [T.Text])) ]+        reOf = case fitEither (glmmF "y ~ x + (1|group)") dfRE3 of+                 Right (re, _) -> re+                 Left e        -> error e++    it "toPlot: MForest layer・点 3 個 (= 群数)・誤差半幅 0 (CI 帯なし)" $ do+      let fLayer = head (vsLayers (toPlot reOf))+      getFirst (lyKind fLayer) `shouldBe` Just MForest+      case (getLast (lyEncX fLayer), getLast (lyErrorX fLayer)) of+        (Just (ColNum ests), Just (ColNum errs)) -> do+          V.length ests `shouldBe` 3+          V.toList errs `shouldSatisfy` all (== 0)   -- conditional variance 未格納ゆえ点のみ+        _ -> expectationFailure "forest encX/errorX が inline ColNum でない"++    it "toPlot: BLUP が値で昇順ソート (caterpillar の並び順)" $ do+      let fLayer = head (vsLayers (toPlot reOf))+      case getLast (lyEncX fLayer) of+        Just (ColNum ests) ->+          let es = V.toList ests+          in and (zipWith (<=) es (drop 1 es)) `shouldBe` True+        _ -> expectationFailure "encX が inline ColNum でない"++    it "diagnosticPlots: random intercept のみ (r=1) ゆえ 1 枚" $ do+      length (diagnosticPlots reOf) `shouldBe` 1++  describe "plot Phase 24 A3: 応答曲面 3D 直結 (surfaceGrid / surfaceOf / epredSurfaceOf)" $ do+    -- y = 1 + 2·x1 + 3·x2 + 4·x3 (厳密線形・無誤差、 C3 と同じ design)。+    let dfRS = DX.fromNamedColumns+          [ ("y",  DX.fromList ([3,7,6,10, 5,9,8,12, 7,11,10,14] :: [Double]))+          , ("x1", DX.fromList ([1,1,1,1, 2,2,2,2, 3,3,3,3] :: [Double]))+          , ("x2", DX.fromList ([0,0,1,1, 0,0,1,1, 0,0,1,1] :: [Double]))+          , ("x3", DX.fromList ([0,1,0,1, 0,1,0,1, 0,1,0,1] :: [Double]))+          ]+        mrs   = either error id (multiLMModel "y ~ x1 + x2 + x3" dfRS)+        opts5 = defaultSurfaceOpts { soN = 5 }++    it "surfaceGrid: 範囲 = 観測 min/max・寸法 n×n" $ do+      let (gxs, gys, grd) = surfaceGrid mrs "x1" "x2" opts5+      (head gxs, last gxs) `shouldBe` (1, 3)+      (head gys, last gys) `shouldBe` (0, 1)+      (length grd, length (head grd)) `shouldBe` (5, 5)++    it "surfaceGrid: grid[j][i] = 3 + 2·gxs[i] + 3·gys[j] (x3 hold Mean 0.5) を厳密復元" $ do+      let (gxs, gys, grd) = surfaceGrid mrs "x1" "x2" opts5+          expectAt j i = 1 + 2 * (gxs !! i) + 3 * (gys !! j) + 4 * 0.5+      sequence_ [ (grd !! j !! i) `shouldSatisfy`+                    (\v -> abs (v - expectAt j i) < 1e-8)+                | j <- [0 .. 4], i <- [0 .. 4] ]++    it "surfaceGrid: holdAt (Fixed x3=1) で面全体が +2 (vs Mean 0.5)" $ do+      let (_, _, g0) = surfaceGrid mrs "x1" "x2" opts5+          (_, _, g1) = surfaceGrid mrs "x1" "x2" opts5 { soHoldAt = Fixed [("x3", 1)] }+      (g1 !! 2 !! 2 - g0 !! 2 !! 2) `shouldSatisfy` (\d -> abs (d - 2) < 1e-8)++    it "surfaceOf: M3Surface 1 layer・colormap ON・x/y range 焼き込み" $ do+      let ls = P3.vs3Layers (surfaceOf mrs "x1" "x2")+      length ls `shouldBe` 1+      let l = head ls+      getFirst (P3.lyr3Kind l) `shouldBe` Just P3.M3Surface+      getLast (P3.lyr3Colormap l) `shouldBe` Just P3.viridisStops3D+      getLast (P3.lyr3XRange l) `shouldBe` Just (1, 3)+      getLast (P3.lyr3YRange l) `shouldBe` Just (0, 1)++    it "dataScatter3DOf: 訓練 12 点の M3Scatter (z = 実測 y)" $ do+      let ls = P3.vs3Layers (dataScatter3DOf mrs "x1" "x2")+          l  = head ls+      getFirst (P3.lyr3Kind l) `shouldBe` Just P3.M3Scatter+      case getLast (P3.lyr3Points l) of+        Just pts -> do+          length pts `shouldBe` 12+          case head pts of Point3 px py pz -> (px, py, pz) `shouldBe` (1, 0, 3)+        Nothing -> expectationFailure "scatter3D の points が無い"++    it "epredSurfaceOf: 事後平均面が 1 + 2·x1 + 1.5·x2 に近い (無誤差データ)" $ do+      let xs1  = [0, 0.5, 1, 1.5, 2, 0, 0.5, 1, 1.5, 2]+          xs2  = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]+          ysE  = [ 1 + 2 * a + 1.5 * b | (a, b) <- zip xs1 xs2 ]+          cfg  = defaultHBM { hbmChains = 2, hbmSamples = 400, hbmWarmup = 400 }+      studied <- hbmModel cfg hbmEpred2Model+                   [("x1", xs1), ("x2", xs2), ("y", ysE)]+      let spec = epredSurfaceOfWith studied "x1" "x2" "mu" defaultSurfaceOpts { soN = 3 }+          l    = head (P3.vs3Layers spec)+      getFirst (P3.lyr3Kind l) `shouldBe` Just P3.M3Surface+      case getLast (P3.lyr3Grid l) of+        Just grd -> do+          (length grd, length (head grd)) `shouldBe` (3, 3)+          -- 角 (x1=2, x2=1) の真値 = 6.5、 原点 = 1+          (grd !! 0 !! 0) `shouldSatisfy` (\v -> abs (v - 1) < 0.5)+          (grd !! 2 !! 2) `shouldSatisfy` (\v -> abs (v - 6.5) < 0.5)+        Nothing -> expectationFailure "surface3D の grid が無い"++  -- =====================================================================+  -- Phase 68 A1: KMeans クラスタリングの図 (Plottable + ヘルパ)+  -- 二層イディオム: clusterScatterOf (data 層・色=ラベル) <> centroidsOf+  -- (model 層・✚ centroid)。 KMeansResult を直接構築して構造を検証する。+  -- =====================================================================+  describe "Phase 68 A1: KMeans + Plottable / clusterScatterOf / centroidsOf" $ do+    let kres = KMeansResult+                 { kmrCentroids = LA.fromLists [[1, 1], [5, 5], [1, 5]]   -- 3×2+                 , kmrLabels    = [0, 0, 1, 1, 2]+                 , kmrInertia   = 0+                 , kmrIters     = 1+                 , kmrConverged = True+                 }+        kdf  = [ ("x", [0.9, 1.1, 5.2, 4.8, 1.0])+               , ("y", [1.2, 0.8, 5.1, 4.9, 5.3]) ] :: [(Text, [Double])]++    it "centroidsOf: 第 0/1 次元 centroid を MScatter + ✚ shape + クラスタ色で 1 layer" $ do+      let ls = vsLayers (centroidsOf kres 0 1)+      length ls `shouldBe` 1+      let l = head ls+      getFirst (lyKind l)  `shouldBe` Just MScatter+      getLast  (lyShape l) `shouldBe` Just MShCross+      -- x = centroid 第0次元 [1,5,1]、 y = 第1次元 [1,5,5]+      case (getLast (lyEncX l), getLast (lyEncY l)) of+        (Just (ColNum vx), Just (ColNum vy)) -> do+          V.toList vx `shouldBe` [1, 5, 1]+          V.toList vy `shouldBe` [1, 5, 5]+        _ -> expectationFailure "centroid encX/encY が inline ColNum でない"+      -- 色 = クラスタ id "0","1","2" の categorical+      case getLast (lyColor l) of+        Just (ColorByCol (ColTxt v)) -> V.toList v `shouldBe` ["0", "1", "2"]+        _ -> expectationFailure "centroid の colorBy が ColTxt クラスタ id でない"++    it "centroidsOf: 範囲外 index は mempty (layer ゼロ)" $ do+      vsLayers (centroidsOf kres 0 2) `shouldBe` []   -- d=2 ゆえ index 2 は範囲外+      vsLayers (centroidsOf kres (-1) 0) `shouldBe` []++    it "toPlot (KMeansResult) == centroidsOf res 0 1 (代表図 = centroid 散布)" $ do+      let a = vsLayers (toPlot kres)+          b = vsLayers (centroidsOf kres 0 1)+      length a `shouldBe` 1+      map (getFirst . lyKind)  a `shouldBe` map (getFirst . lyKind)  b+      map (getLast  . lyShape) a `shouldBe` map (getLast  . lyShape) b++    it "clusterScatterOf: データ点を MScatter + ラベル色で 1 layer (点と同順)" $ do+      let ls = vsLayers (clusterScatterOf kdf kres "x" "y")+      length ls `shouldBe` 1+      let l = head ls+      getFirst (lyKind l) `shouldBe` Just MScatter+      case (getLast (lyEncX l), getLast (lyEncY l)) of+        (Just (ColNum vx), Just (ColNum vy)) -> do+          V.toList vx `shouldBe` [0.9, 1.1, 5.2, 4.8, 1.0]+          V.length vy `shouldBe` 5+        _ -> expectationFailure "data 散布の encX/encY が inline ColNum でない"+      -- 色 = kmrLabels を文字列化した categorical (5 点ぶん)+      case getLast (lyColor l) of+        Just (ColorByCol (ColTxt v)) -> V.toList v `shouldBe` ["0", "0", "1", "1", "2"]+        _ -> expectationFailure "data 散布の colorBy が ColTxt ラベルでない"++    it "clusterScatterOf: 存在しない列名は mempty" $ do+      vsLayers (clusterScatterOf kdf kres "nope" "y") `shouldBe` []++  -- =====================================================================+  -- Phase 68 A2: 木/アンサンブル (重要度 bar / 決定木 樹形図)+  -- 結果型を直接構築して構造を検証 (IO/学習不要)。+  -- =====================================================================+  describe "Phase 68 A2: GBM/RFClassifier 重要度 bar + DecisionTree 樹形図" $ do+    -- feature 0 を 2 回・feature 1 を 1 回 split に使う木+    let tree1 = RF.Node 0 0.5 (RF.Leaf 1) (RF.Node 1 0.5 (RF.Leaf 0) (RF.Leaf 1))+        tree2 = RF.Node 0 0.5 (RF.Leaf 0) (RF.Leaf 1)++    it "treeImportances: split 使用回数を正規化 (feat0 2回 / feat1 1回 → [2/3, 1/3])" $ do+      treeImportances [tree1, tree2] `shouldSatisfy`+        (\xs -> length xs == 2+                && abs (xs !! 0 - 2/3) < 1e-9+                && abs (xs !! 1 - 1/3) < 1e-9)++    it "treeImportances: 空入力は []" $ do+      treeImportances [] `shouldBe` []++    it "GBRegressor toPlot: 1 bar layer・重要度は合計 1 に正規化" $ do+      let gb = GBRegressor { gbrInit = 0, gbrTrees = [tree1, tree2], gbrLR = 0.1 }+          ls = vsLayers (toPlot gb)+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MBar+      case getLast (lyEncY (head ls)) of+        Just (ColNum v) -> do+          V.length v `shouldBe` 2+          abs (V.head v - 2/3) `shouldSatisfy` (< 1e-9)+          sum (V.toList v) `shouldSatisfy` (\s -> abs (s - 1) < 1e-9)+        _ -> expectationFailure "GBM importance bar の encY が inline ColNum でない"++    it "GBClassifier toPlot: 1 bar layer (MBar)" $ do+      let gb = GBClassifier { gbcInit = 0, gbcTrees = [tree2], gbcLR = 0.1 }+          ls = vsLayers (toPlot gb)+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MBar++    it "RFClassifierFit toPlot: 2 パネル (permutation/gini)・実列名 (75.24b)" $ do+      let fit = RFClassifierFit+                  { rfcTrees          = []+                  , rfcOOBSamples     = []+                  , rfcClasses        = [0, 1]+                  , rfcOOBError       = 0+                  , rfcImportance     = LA.fromList [1, 3]+                  , rfcGiniImportance = LA.fromList [0.25, 0.75]+                  , rfcFeatureNames   = ["a", "b"]+                  , rfcConfig         = defaultRFCConfig+                  }+          panels = vsSubplots (toPlot fit)+      length panels `shouldBe` 2                          -- permutation + gini+      let permLayer = head (vsLayers (head panels))       -- 左 = permutation+      getFirst (lyKind permLayer) `shouldBe` Just MBar+      case getLast (lyEncY permLayer) of+        Just (ColNum v) -> V.toList v `shouldBe` [1, 3]   -- permutation raw (データ順・sort は limits 側)+        _ -> expectationFailure "RFClassifier importance bar の encY が inline ColNum でない"++    it "DTree toPlot: MDAG 樹形図を 1 layer (split=矩形/葉)" $ do+      let dt = DNode 1 3.5 (DLeaf (Map.fromList [(0,1)]) 0 5 0)+                           (DLeaf (Map.fromList [(1,1)]) 1 4 0)+                           9 0.49 (Map.fromList [(0,0.55),(1,0.45)]) 0+          ls = vsLayers (toPlot dt)+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MDAG++  -- =====================================================================+  -- Phase 75.27: 部分従属図 (PDP / ICE)+  -- 純粋エンジンは PartialDependenceSpec で検証済。 ここは VisualSpec への+  -- 落とし込み (line layer 数・grid・encY 値) を検証。+  -- =====================================================================+  describe "Phase 75.27: PDP / ICE (pdpPlot / partialDependencePlot)" $ do+    -- 3 行 × 2 列。 列 0 = {0,1,2}, 列 1 = {10,20,30}。+    let trainX  = LA.fromLists [[0, 10], [1, 20], [2, 30]]+        -- 加法 predict f = 2*x0 + x1 → 特徴 0 の PDP = 2*grid + mean(x1) = 2*grid + 20。+        predict m = [ 2 * (row LA.! 0) + (row LA.! 1) | row <- LA.toRows m ]++    it "partialDependencePlot (閉包): 1 line layer・grid 40 点・PDP = 2*grid+20" $ do+      let ls = vsLayers (partialDependencePlot trainX predict 0 "x0")+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MLine+      case getLast (lyEncY (head ls)) of+        Just (ColNum v) -> do+          V.length v `shouldBe` 40+          abs (V.head v - 20) `shouldSatisfy` (< 1e-9)   -- g=0 → 20+          abs (V.last v - 24) `shouldSatisfy` (< 1e-9)   -- g=2 → 24+        _ -> expectationFailure "PDP line の encY が inline ColNum でない"++    it "partialDependenceIcePlot (閉包): ICE n 本 + PDP 1 本 = n+1 line layer" $ do+      let ls = vsLayers (partialDependenceIcePlot trainX predict 0 "x0")+      length ls `shouldBe` 4                              -- 3 ICE + 1 PDP+      all ((== Just MLine) . getFirst . lyKind) ls `shouldBe` True++    it "pdpPlot (RegPredict GBRegressor instance): line layer を出す" $ do+      let t  = RF.Node 0 0.5 (RF.Leaf 0) (RF.Leaf 1)+          gb = GBRegressor { gbrInit = 0, gbrTrees = [t], gbrLR = 1 }+          ls = vsLayers (pdpPlot gb trainX 0 "x0")+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MLine++    it "pdpOf (高レベル・df + 列名): 行列版 pdpPlot と一致" $ do+      let t   = RF.Node 0 0.5 (RF.Leaf 0) (RF.Leaf 1)+          gb  = GBRegressor { gbrInit = 0, gbrTrees = [t], gbrLR = 1 }+          -- trainX = [[0,10],[1,20],[2,30]] を df 化。+          df  = [ ("x0", NumData (V.fromList [0, 1, 2]))+                , ("x1", NumData (V.fromList [10, 20, 30])) ] :: [(Text, ColData)]+          encOf spec = getLast (lyEncY (head (vsLayers spec)))+      encOf (pdpOf gb df ["x0","x1"] "x0") `shouldBe` encOf (pdpPlot gb trainX 0 "x0")++    it "pdpOf: target が featCols に無ければ空 spec" $ do+      let t  = RF.Node 0 0.5 (RF.Leaf 0) (RF.Leaf 1)+          gb = GBRegressor { gbrInit = 0, gbrTrees = [t], gbrLR = 1 }+          df = [ ("x0", NumData (V.fromList [0, 1, 2])) ] :: [(Text, ColData)]+      vsLayers (pdpOf gb df ["x0"] "nope") `shouldBe` []++    it "列外 index は空 spec" $+      vsLayers (partialDependencePlot trainX predict 9 "x9") `shouldBe` []++  -- =====================================================================+  -- Phase 68 A3: 分類 (決定境界 + confusion + 代表散布)+  -- KNNClassifier を直接構築 (1-NN・2 点) して構造を検証。+  -- =====================================================================+  describe "Phase 68 A3: ClassPredict / decisionBoundaryOf / confusionOf" $ do+    let knn = KNNClassifier+                { knnCK          = 1+                , knnCX          = LA.fromLists [[0, 0], [4, 4]]+                , knnCY          = VU.fromList [0, 1]+                , knnCClasses    = [0, 1]+                , knnCClassNames = []+                }++    it "predictClasses (1-NN): 近い訓練点のクラスを返す" $ do+      predictClasses knn (LA.fromLists [[0, 0], [4, 4], [0.5, 0.5], [3.6, 3.6]])+        `shouldBe` [0, 1, 0, 1]++    it "decisionBoundaryOf: res×res の annotRect grid で領域塗り (Phase 76.A)" $ do+      let vs = decisionBoundaryOf knn (0, 4) (0, 4) 3+      -- layer は持たず (塗りは全て annotation)、 res² = 9 個の塗り矩形を敷き詰める。+      vsLayers vs `shouldBe` []+      length (vsAnnotations vs) `shouldBe` 9+      -- 全 annotation が AnnRect (塗り矩形) であること。+      let isAnnRect a = case a of AnnRect{} -> True; _ -> False+      all isAnnRect (vsAnnotations vs) `shouldBe` True+      -- 軸ドメインをグリッド範囲へ固定 (expand=FALSE・はみ出し防止)。+      getLast (vsCoordXLim vs) `shouldBe` Just (0, 4)+      getLast (vsCoordYLim vs) `shouldBe` Just (0, 4)++    it "confusionOf: MHeatmap + MLabel (件数注釈)・セル数 = クラス数² (完全予測で対角)" $ do+      let spec = confusionOf knn (LA.fromLists [[0, 0], [4, 4]]) [0, 1]+          ls   = vsLayers spec+      map (getFirst . lyKind) ls `shouldBe` [Just MHeatmap, Just MLabel]   -- Phase 75.2+      case getLast (lyEncY (head ls)) of+        Just (ColTxt v) -> V.length v `shouldBe` 4      -- 2 クラス × 2+        _               -> expectationFailure "confusion の encY が inlineCat でない"+      -- 件数注釈 (MLabel) は cells 数 (=4) の数値ラベルを持ち、 値は counts と一致+      -- (完全予測ゆえ対角 = [1,0,0,1] = (t,p)∈{(0,0),(0,1),(1,0),(1,1)} の件数)。+      case getLast (lyLabel (ls !! 1)) of+        Just (ColTxt v) -> V.toList v `shouldBe` ["1", "0", "0", "1"]+        _               -> expectationFailure "confusion 件数注釈の lyLabel が ColTxt でない"++    -- Phase 75.21: MDS モデル型 (df |-> mds) + 単色/群色 toPlot。+    it "df |-> mds: 埋め込みの散布点数 = n (toPlot m・単色)" $ do+      let df = [ ("x1", NumData (V.fromList [0,1,5,6]))+               , ("x2", NumData (V.fromList [0,1,5,6]))+               , ("x3", NumData (V.fromList [0,1,5,6]))+               , ("cls", NumData (V.fromList [0,0,1,1])) ] :: [(Text, ColData)]+          m  = df |-> mds defaultMDS ["x1","x2","x3"]+          sl = head [ l | l <- vsLayers (toPlot m), getFirst (lyKind l) == Just MScatter ]+      case getLast (lyEncX sl) of+        Just (ColNum v) -> V.length v `shouldBe` 4+        _               -> expectationFailure "mds toPlot encX が ColNum でない"++    it "toPlot (mdsView m <> mdsGroupBy cls): 群色列が colorBy に乗る" $ do+      let df = [ ("x1", NumData (V.fromList [0,1,5,6]))+               , ("x2", NumData (V.fromList [0,1,5,6]))+               , ("x3", NumData (V.fromList [0,1,5,6]))+               , ("cls", TxtData (V.fromList ["a","a","b","b"])) ] :: [(Text, ColData)]+          m  = df |-> mds defaultMDS ["x1","x2","x3"]+          sl = head [ l | l <- vsLayers (toPlot (mdsView m <> mdsGroupBy "cls"))+                        , getFirst (lyKind l) == Just MScatter ]+      -- 群色列があれば lyColor (categorical) が入る。+      case getLast (lyColor sl) of+        Just _  -> pure ()+        Nothing -> expectationFailure "mdsGroupBy で lyColor が入らない"++    it "nnLossOf: 損失曲線の点数 = epoch 数 (mlpLossHist)・MLPFit decisionBoundaryOf" $ do+      let xMat = LA.fromLists [[0, 0], [0, 1], [4, 4], [4, 5]]+          yLab = VU.fromList [0, 0, 1, 1]+      gen <- MWC.initialize (V.fromList [7])+      fit <- fitMLPClassifier defaultMLP xMat yLab gen+      let ln = head [ l | l <- vsLayers (nnLossOf fit), getFirst (lyKind l) == Just MLine ]+      length (mlpLossHist fit) `shouldSatisfy` (> 0)+      case getLast (lyEncX ln) of+        Just (ColNum v) -> V.length v `shouldBe` length (mlpLossHist fit)+        _               -> expectationFailure "nnLoss encX が ColNum でない"+      length (vsAnnotations (decisionBoundaryOf fit (0, 4) (0, 5) 8)) `shouldBe` 64++    -- Phase 75.8: 乱数純粋化 (NN は seed でビット一致)。+    it "fitMLPClassifierPure ≡ fitMLPClassifier (同 seed)・損失ビット一致" $ do+      let xMat = LA.fromLists [[0, 0], [0, 1], [4, 4], [4, 5]]+          yLab = VU.fromList [0, 0, 1, 1]+      gen <- MWC.initialize (V.fromList [99])+      ioFit <- fitMLPClassifier defaultMLP xMat yLab gen+      let pFit = fitMLPClassifierPure defaultMLP xMat yLab 99+      mlpLossHist pFit `shouldBe` mlpLossHist ioFit++    -- Phase 75.9: 高レベル df |-> (mlpCls)。+    it "df |-> mlpCls: MLPFit を返し損失曲線/決定境界が出る" $ do+      let df2 = [ ("x1", NumData (V.fromList [0,0,4,4]))+                , ("x2", NumData (V.fromList [0,1,4,5]))+                , ("cls", NumData (V.fromList [0,0,1,1])) ] :: [(Text, ColData)]+          m  = df2 |-> mlpCls defaultMLP 7 ["x1","x2"] "cls"+      length (mlpLossHist m) `shouldSatisfy` (> 0)+      length (vsAnnotations (decisionBoundaryOf m (0,4) (0,5) 8)) `shouldBe` 64++    -- Phase 75.11/75.12: カーネル SVM の高レベル + SV 可視化。+    it "df |-> svmCls: SVMMulti・非線形 decisionBoundaryOf・SV 強調" $ do+      let df = [ ("x1", NumData (V.fromList [0,0.2,-0.2, 3,-3,0,0,2.1,-2.1]))+               , ("x2", NumData (V.fromList [0,0.1,0.1, 0,0,3,-3,2.1,-2.1]))+               , ("cls", NumData (V.fromList [0,0,0, 1,1,1,1,1,1])) ] :: [(Text, ColData)]+          -- RBF・γ=0.5 ⇔ ℓ=1 (γ=1/(2ℓ²)・Phase 75.15 共有 Kernel)。+          m  = df |-> svmCls defaultSVM+                        { svmKernel = RBF, svmParams = defaultKernelParams, svmC = 10 }+                        ["x1","x2"] "cls"+      length (svmmClasses m) `shouldBe` 2+      length (vsAnnotations (decisionBoundaryOf m (-4,4) (-4,4) 8)) `shouldBe` 64+      let bin = head (svmmBinaries m)+          sv  = vsLayers (svmSupportVectorsOf bin)+      numSupportVectors bin `shouldSatisfy` (> 0)+      length sv `shouldSatisfy` (> 0)+      -- 決定境界を線 (スコア=0 等高線) で: 内外を分ける曲線ゆえ segment が出る。+      length (vsLayers (decisionLineOf bin (-4,4) (-4,4) 40)) `shouldSatisfy` (> 0)++    it "KNNClassifier toPlot: 訓練点をラベル色で散布 (MScatter)" $ do+      let ls = vsLayers (toPlot knn)+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MScatter+      case getLast (lyColor (head ls)) of+        Just (ColorByCol (ColTxt v)) -> V.toList v `shouldBe` ["0", "1"]+        _ -> expectationFailure "KNN 訓練散布の colorBy が ColTxt ラベルでない"++  -- =====================================================================+  -- Phase 68 A4: 次元圧縮 (PLS score/loading/VIP, MultiGP 多出力曲線)+  -- =====================================================================+  describe "Phase 68 A4: PLS + MultiGP" $ do+    let xP   = LA.fromLists [[1,0],[0,1],[2,1],[1,2],[3,2]]   -- 5×2+        yP   = LA.fromLists [[1],[2],[3],[4],[5]]             -- 5×1+        plsFit = either (error . T.unpack) id+                   (fitPLS defaultPLS { plsN_Components = 2 } xP yP)++    it "scoreView: 1 MScatter layer・点数 = 標本数 n" $ do+      let ls = vsLayers (toPlot (scoreView plsFit))+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MScatter+      case getLast (lyEncX (head ls)) of+        Just (ColNum v) -> V.length v `shouldBe` 5+        _               -> expectationFailure "score plot の encX が inline ColNum でない"++    it "vipView: 1 MBar layer・本数 = 特徴数 p" $ do+      let ls = vsLayers (toPlot (vipView plsFit))+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MBar+      case getLast (lyEncY (head ls)) of+        Just (ColNum v) -> V.length v `shouldBe` 2+        _               -> expectationFailure "VIP bar の encY が inline ColNum でない"++    it "multiGpCurves: 出力ごとに band+line の 2 layer (2 出力 → 4 layer)" $ do+      let res = MultiGPResult+                  { mgpMean  = [[1, 2, 3], [3, 2, 1]]+                  , mgpLower = [[0, 1, 2], [2, 1, 0]]+                  , mgpUpper = [[2, 3, 4], [4, 3, 2]]+                  , mgpModels = []+                  }+          ls  = vsLayers (multiGpCurves res)+      length ls `shouldBe` 4+      map (getFirst . lyKind) ls+        `shouldBe` [Just MBand, Just MLine, Just MBand, Just MLine]++    it "MultiGPResult toPlot == multiGpCurves" $ do+      let res = MultiGPResult+                  { mgpMean = [[1, 2]], mgpLower = [[0, 1]]+                  , mgpUpper = [[2, 3]], mgpModels = [] }+      length (vsLayers (toPlot res)) `shouldBe` length (vsLayers (multiGpCurves res))++  -- =====================================================================+  -- Phase 70.B2/B3: PLS effect plot (plsModel + statModelMulti + selectOutput)+  -- =====================================================================+  describe "Phase 70.B2/B3: PLS effect plot + 出力セレクタ" $ do+    -- 2 入力・2 出力。 y1 = 2*x1、 y2 = -3*x2 (出力で傾きの符号が違う = セレクタ確認用)。+    let n    = 30+        x1   = [ fromIntegral i * 0.2       | i <- [1 .. n :: Int] ]+        x2   = [ 1 + sin (fromIntegral i)   | i <- [1 .. n :: Int] ]+        y1   = [ 2 * a            | a <- x1 ]+        y2   = [ negate (3 * b)   | b <- x2 ]+        dfP  = [ ("x1", NumData (V.fromList x1)), ("x2", NumData (V.fromList x2))+               , ("y1", NumData (V.fromList y1)), ("y2", NumData (V.fromList y2)) ]+               :: [(Text, ColData)]+        cfgP = defaultPLS { plsN_Components = 2 }+        m    = either error id (plsModel cfgP ["x1", "x2"] ["y1", "y2"] dfP)+        -- effect plot の MLine layer の encY (μ 曲線) を取り出す+        -- (ModelSpec は Plottable・データはモデル frame 内ゆえ df |>> 不要)。+        effLine mdl =+          let ls = vsLayers (toPlot (statModelMulti mdl (along "x1")))+              ln = head [ l | l <- ls, getFirst (lyKind l) == Just MLine ]+          in case getLast (lyEncY ln) of+               Just (ColNum v) -> V.toList v+               _               -> []++    it "plsModel + statModelMulti: along 曲線 (MLine) を 1 本・band 非提供" $ do+      let ls = vsLayers (toPlot (statModelMulti m (along "x1")))+      map (getFirst . lyKind) ls `shouldBe` [Just MLine]   -- band 無し (PLS CI 非提供)++    it "selectOutput: 第0出力 (y1) は x1 増で増加・既定と一致" $ do+      let muDefault = effLine m+          muY1      = effLine (selectOutput "y1" m)+      muDefault `shouldSatisfy` allClose muY1                  -- 既定 = 第0出力+      head muY1 `shouldSatisfy` (< last muY1)                  -- y1 ∝ +x1 → 単調増++    it "selectOutput: 第1出力 (y2) は第0出力と異なる曲線 (出力選択が効く)" $ do+      let muY1 = effLine (selectOutput "y1" m)+          muY2 = effLine (selectOutput "y2" m)+      (muY1 == muY2) `shouldBe` False                         -- 出力で曲線が変わる++    it "selectOutput: 未知の出力名は無変更 (既定 = 第0出力のまま)" $ do+      effLine (selectOutput "nope" m) `shouldSatisfy` allClose (effLine m)++  -- =====================================================================+  -- Phase 68 A5: 時系列・生存・FDA (GARCH / AFT / FunctionalPCA / FLM)+  -- =====================================================================+  describe "Phase 68 A5: GARCH / AFT / FDA" $ do+    it "GARCHFit toPlot: band + line の 2 layer・hi ≥ lo" $ do+      let gf = GARCHFit { gOmega = 0.1, gAlpha = 0.1, gBeta = 0.8, gMu = 0+                        , gSigma2 = LA.fromList [1, 4, 1]+                        , gResiduals = LA.fromList [0.5, -1, 0.2], gLogLik = 0 }+          ls = vsLayers (garchVolatility gf)+      length ls `shouldBe` 2+      map (getFirst . lyKind) ls `shouldBe` [Just MBand, Just MLine]+      case (getLast (lyEncY (head ls)), getLast (lyEncY2 (head ls))) of+        (Just (ColNum lo), Just (ColNum hi)) ->+          zipWith (-) (V.toList hi) (V.toList lo) `shouldSatisfy` all (>= 0)+        _ -> expectationFailure "GARCH band の encY/encY2 が ColNum でない"++    it "AFTFit toPlot: 生存曲線 S(t) は [0,1]・単調非増加・始点 ≈ 1" $ do+      let af = AFTFit { aftBeta = LA.fromList [1, 0.5], aftScale = 1+                      , aftLogLik = 0, aftDistribution = AFTWeibull, aftIters = 1 }+          ls = vsLayers (toPlot af)+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MLine+      case getLast (lyEncY (head ls)) of+        Just (ColNum v) -> do+          let ss = V.toList v+          all (\s -> s >= -1e-9 && s <= 1 + 1e-9) ss `shouldBe` True+          and (zipWith (>=) ss (drop 1 ss)) `shouldBe` True   -- 単調非増加+          head ss `shouldSatisfy` (> 0.9)+        _ -> expectationFailure "AFT survival の encY が ColNum でない"++    it "aftSurvivalAt: 共変量を変えると線形予測子で生存が変わる" $ do+      let af = AFTFit { aftBeta = LA.fromList [1, 0.5], aftScale = 1+                      , aftLogLik = 0, aftDistribution = AFTWeibull, aftIters = 1 }+      length (vsLayers (aftSurvivalAt af [1, 2])) `shouldBe` 1++    it "FunctionalPCA toPlot: 平均 + 上位固有関数 (≤3) を line 重畳 (4 layer)" $ do+      let fpca = FunctionalPCA+                   { fpcaScores      = LA.fromLists [[1, 0, 0]]+                   , fpcaEigenfn     = LA.fromLists [[0,1,2],[2,1,0],[1,1,1],[0,0,1]]  -- 4 PC+                   , fpcaEigenvalues = LA.fromList [4, 2, 1, 0.5]+                   , fpcaMeanFn      = LA.fromList [1, 2, 3] }+          ls = vsLayers (toPlot fpca)+      length ls `shouldBe` 4    -- mean + 上位 3 PC+      all (\l -> getFirst (lyKind l) == Just MLine) ls `shouldBe` True++    it "FLMResult toPlot: β(t) を 1 MLine" $ do+      let flm = FLMResult { flmAlpha = 0, flmBetaFn = LA.fromList [0.1, 0.3, 0.2]+                          , flmFitted = LA.fromList [1, 2], flmR2 = 0.8 }+          ls = vsLayers (toPlot flm)+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MLine++  -- =====================================================================+  -- Phase 68 A6: 罰則回帰・因果 (RegFit bar / 係数パス / LiNGAM DAG)+  -- =====================================================================+  describe "Phase 68 A6: Regularized + LiNGAM" $ do+    it "RegFit toPlot: 係数 bar (本数 = β の長さ)" $ do+      let rf = RegFit { rfBeta = LA.fromList [0.5, 1.2, 0]+                      , rfYHat = LA.fromList [1], rfResid = LA.fromList [0]+                      , rfR2 = 0.9, rfPenalty = NoPen, rfNonZero = 2, rfIters = 0 }+          ls = vsLayers (toPlot rf)+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MBar+      case getLast (lyEncY (head ls)) of+        Just (ColNum v) -> V.length v `shouldBe` 3+        _               -> expectationFailure "RegFit bar の encY が ColNum でない"++    it "regPathPlot: 係数ごとに 1 line (2 係数 → 2 layer)" $ do+      let path = [ (0.1, [3.0, 1.0]), (0.5, [2.0, 0.5]), (1.0, [0.0, 0.0]) ]+          ls   = vsLayers (regPathPlot path)+      length ls `shouldBe` 2+      all (\l -> getFirst (lyKind l) == Just MLine) ls `shouldBe` True++    it "regPathPlot: 空パスは mempty" $ do+      vsLayers (regPathPlot []) `shouldBe` []++    it "DirectLiNGAMFit toPlot: 因果 DAG を MDAG 1 layer (x0->x1->x2)" $ do+      let adj = LA.fromLists [[0,0,0],[1,0,0],[0,1,0]]   -- edge 0->1, 1->2+          fit = DirectLiNGAMFit { dlOrder = [0,1,2], dlB = adj, dlAdjacency = adj+                                , dlResiduals = LA.fromLists [[0,0,0]] }+          ls  = vsLayers (toPlot fit)+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MDAG++    -- Phase 77.A: 高レベル df |-> directLingam (変数名保持・名前付き DAG)。+    it "df |-> directLingam: 変数名保持・低レベル fit と一致・toPlot は名前付き MDAG" $ do+      let xs0 = [0.5, -0.3, 0.8, -0.6, 0.2, -0.9, 0.7, -0.1]+          xs1 = [1.1, -0.5, 1.7, -1.3, 0.3, -1.9, 1.5, -0.4]   -- ≈ 2·xs0 + 揺らぎ+          xs2 = [-1.5, 0.8, -2.4, 2.0, -0.6, 2.7, -2.1, 0.5]   -- ≈ -1.5·xs1 + 揺らぎ+          df  = [ ("smoking", NumData (V.fromList xs0))+                , ("tar",     NumData (V.fromList xs1))+                , ("cancer",  NumData (V.fromList xs2)) ] :: [(Text, ColData)]+          mat = LA.fromColumns [LA.fromList xs0, LA.fromList xs1, LA.fromList xs2]+          fitted = df |-> directLingam defaultDirectLiNGAMConfig ["smoking","tar","cancer"]+      lfNames fitted `shouldBe` ["smoking","tar","cancer"]+      -- df|-> は低レベル fitDirectLiNGAM と同一 (列名経路と行列経路が一致)+      LA.toLists (dlAdjacency (lfFit fitted))+        `shouldBe` LA.toLists (dlAdjacency (fitDirectLiNGAM defaultDirectLiNGAMConfig mat))+      -- toPlot は名前付き DAG (1 MDAG layer)+      let ls2 = vsLayers (toPlot fitted)+      length ls2 `shouldBe` 1+      getFirst (lyKind (head ls2)) `shouldBe` Just MDAG++    -- Phase 77.B: Parce / MultiGroup も高レベル df|-> + 名前付き DAG。+    it "df |-> parceLingam: 変数名保持・toPlot は名前付き MDAG" $ do+      let xs0 = [0.5,-0.3,0.8,-0.6,0.2,-0.9,0.7,-0.1,0.4,-0.5]+          xs1 = [1.1,-0.5,1.7,-1.3,0.3,-1.9,1.5,-0.4,0.9,-1.0]+          xs2 = [-1.5,0.8,-2.4,2.0,-0.6,2.7,-2.1,0.5,-1.2,1.4]+          df  = [ ("a", NumData (V.fromList xs0)), ("b", NumData (V.fromList xs1))+                , ("c", NumData (V.fromList xs2)) ] :: [(Text, ColData)]+          fitted = df |-> parceLingam defaultParceConfig ["a","b","c"]+      lfNames fitted `shouldBe` ["a","b","c"]+      getFirst (lyKind (head (vsLayers (toPlot fitted)))) `shouldBe` Just MDAG++    it "df |-> multiGroupLingam: group 列で分割・共通 DAG (名前付き MDAG)" $ do+      let g0x0 = [0.5,-0.3,0.8,-0.6,0.2,-0.9]; g0x1 = [1.1,-0.5,1.7,-1.3,0.3,-1.9]+          g1x0 = [0.4,-0.5,0.7,-0.2,0.6,-0.8]; g1x1 = [0.9,-1.0,1.5,-0.4,1.2,-1.6]+          df  = [ ("a",   NumData (V.fromList (g0x0 ++ g1x0)))+                , ("b",   NumData (V.fromList (g0x1 ++ g1x1)))+                , ("grp", NumData (V.fromList (replicate 6 0 ++ replicate 6 1))) ] :: [(Text, ColData)]+          fitted = df |-> multiGroupLingam defaultMultiGroupConfig ["a","b"] "grp"+      lfNames fitted `shouldBe` ["a","b"]+      getFirst (lyKind (head (vsLayers (toPlot fitted)))) `shouldBe` Just MDAG++    it "df |-> varLingam: 時系列因果・toPlot は時間ラグ MDAG" $ do+      let n = 40 :: Int+          ea = [ sin (fromIntegral i * 0.7) | i <- [1..n] ]+          eb = [ cos (fromIntegral i * 0.9) | i <- [1..n] ]+          go (aP,bP) (x,y) = let a = 0.5*aP + x; b = 0.6*a + 0.3*bP + 0.3*y in (a,b)+          ps = tail (scanl go (0,0) (zip ea eb))+          df = [ ("a", NumData (V.fromList (map fst ps)))+               , ("b", NumData (V.fromList (map snd ps))) ] :: [(Text, ColData)]+          fitted = df |-> varLingam defaultVARLiNGAMConfig ["a","b"]+      lfNames fitted `shouldBe` ["a","b"]+      getFirst (lyKind (head (vsLayers (toPlot fitted)))) `shouldBe` Just MDAG++    it "df |-> pairwiseLingam: 2 変数の向き・toPlot は 2 ノード MDAG" $ do+      let xs = [0.5,-0.3,0.8,-0.6,0.2,-0.9,0.7,-0.1,0.4,-0.5]+          ys = map (\v -> 2.0*v) xs   -- x → y (関数従属で向き明確)+          df = [ ("x", NumData (V.fromList xs)), ("y", NumData (V.fromList ys)) ] :: [(Text, ColData)]+          fitted = df |-> pairwiseLingam 0.0 "x" "y"+      lfNames fitted `shouldBe` ["x","y"]+      getFirst (lyKind (head (vsLayers (toPlot fitted)))) `shouldBe` Just MDAG++    -- Phase 77.C: Bootstrap / ICA の seed 純粋版 (IO とビット一致) + 高レベル + plot。+    let semABC = let xs0 = [0.5,-0.3,0.8,-0.6,0.2,-0.9,0.7,-0.1,0.4,-0.5,0.3,-0.7]+                     xs1 = zipWith (\a i -> 2.0*a + 0.2*sin (fromIntegral i)) xs0 [0..]+                     xs2 = zipWith (\b i -> -1.5*b + 0.2*cos (fromIntegral i)) xs1 [0..]+                 in (xs0, xs1, xs2)+        (sa, sb, sc) = semABC+        semMat = LA.fromColumns [LA.fromList sa, LA.fromList sb, LA.fromList sc]+        semDF  = [ ("a", NumData (V.fromList sa)), ("b", NumData (V.fromList sb))+                 , ("c", NumData (V.fromList sc)) ] :: [(Text, ColData)]+        bcfg = defaultBootstrapConfig { bcNumBootstraps = 20 }++    it "fitBootstrapLiNGAMPure ≡ fitBootstrapLiNGAM (同 seed・edge 確率ビット一致)" $ do+      ioRes <- fitBootstrapLiNGAM bcfg semMat+      let pRes = fitBootstrapLiNGAMPure bcfg semMat+      LA.toLists (brEdgeProbability pRes) `shouldBe` LA.toLists (brEdgeProbability ioRes)++    it "df |-> bootstrapLingam: 確信度 DAG (MDAG) + edge 確率ヒートマップ (MHeatmap)" $ do+      let fitted = semDF |-> bootstrapLingam bcfg ["a","b","c"]+      lfNames fitted `shouldBe` ["a","b","c"]+      getFirst (lyKind (head (vsLayers (toPlot fitted)))) `shouldBe` Just MDAG+      getFirst (lyKind (head (vsLayers (bootstrapEdgeProbOf fitted)))) `shouldBe` Just MHeatmap++    it "fitICALiNGAMPure ≡ fitICALiNGAM (同 seed・adjacency ビット一致)" $ do+      ioFit <- fitICALiNGAM defaultICALiNGAMConfig semMat+      let pFit = fitICALiNGAMPure defaultICALiNGAMConfig semMat+      LA.toLists (ilAdjacency pFit) `shouldBe` LA.toLists (ilAdjacency ioFit)++    it "df |-> icaLingam: 名前付き DAG (MDAG)" $ do+      let fitted = semDF |-> icaLingam defaultICALiNGAMConfig ["a","b","c"]+      lfNames fitted `shouldBe` ["a","b","c"]+      getFirst (lyKind (head (vsLayers (toPlot fitted)))) `shouldBe` Just MDAG++    -- Phase 77: 相関ネットワーク (高レベル df|-> correlationOf → toPlot)。+    it "df |-> correlationOf: 相関行列 (対角=1) + toPlot は相関グラフ (MDAG)" $ do+      let cg = semDF |-> correlationOf 0.3 ["a","b","c"]+      cgNames cg `shouldBe` ["a","b","c"]+      cgThreshold cg `shouldBe` 0.3+      -- 相関行列の対角は 1 (自己相関)+      [ cgCorr cg `LA.atIndex` (i,i) | i <- [0..2] ] `shouldSatisfy` all ((< 1e-9) . abs . subtract 1)+      getFirst (lyKind (head (vsLayers (toPlot cg)))) `shouldBe` Just MDAG++    -- Phase 78.C/D/F: DOE prediction profiler (行=応答 × 列=因子・toPlot + <> オプション)。+    it "multiOutput + profiler: 応答×因子ぶんの subplots (2 応答 × 2 因子 = 4)" $ do+      let plan   = factorialDesign [contFactor "a" (0,1), contFactor "b" (0,1)]+          rs     = designTable plan+          av     = maybe [] id (lookup "a" rs); bv = maybe [] id (lookup "b" rs)+          y1     = zipWith (\x y -> 1 + 2*x + 3*y) av bv+          y2     = zipWith (\x y -> 5 + x - y) av bv+          df     = [ (n, NumData (V.fromList v))+                   | (n,v) <- ("y1",y1):("y2",y2):rs ] :: [(Text,ColData)]+          models = df |-> multiOutput ["y1","y2"] (designModel plan)+          vs     = toPlot (profiler models ["a","b"])+      length models `shouldBe` 2                          -- multiOutput = [(応答名, モデル)]+      map fst models `shouldBe` ["y1","y2"]+      length (vsSubplots vs) `shouldBe` 4                 -- 2 応答 × 2 因子+      all (not . null . vsLayers) (vsSubplots vs) `shouldBe` True++    it "profiler: 空 models / 空 factors は mempty (subplots なし)" $ do+      let plan   = factorialDesign [contFactor "a" (0,1)]+          rs     = designTable plan+          av     = maybe [] id (lookup "a" rs)+          df     = [ (n, NumData (V.fromList v)) | (n,v) <- ("y",av):rs ] :: [(Text,ColData)]+          models = df |-> multiOutput ["y"] (designModel plan)+      vsSubplots (toPlot (profiler models []))              `shouldBe` []+      vsSubplots (toPlot (profiler [] ["a"] :: ProfilerSpec MultiLMModel)) `shouldBe` []++    it "profilerResidual: <> で打点モードが後勝ち合成 (Semigroup)" $ do+      psResidual (profiler [] [] <> profilerResidual Partial :: ProfilerSpec MultiLMModel)+        `shouldBe` Just Partial+      psResidual (profilerResidual Raw <> profilerResidual Partial :: ProfilerSpec MultiLMModel)+        `shouldBe` Just Partial      -- 右 (後) 勝ち+      psResidual (profiler [] [] :: ProfilerSpec MultiLMModel)+        `shouldBe` Nothing           -- 既定 (= Raw)++    -- Phase 78.G-e: FRR/HBM 化 (GP/RFF)。MultiVarModel GPRegModelN で profiler/contour に+    -- GP 事後予測帯を出す。分布あり象限 (Gp/GpRff) は帯、mean のみ象限 (Krr) は帯なし。+    it "profiler (GP): 応答×因子の subplots + Gp は予測帯・Krr は帯なし" $ do+      let plan = factorialDesign [contFactor "a" (0,1), contFactor "b" (0,1)]+          rs   = designTable plan+          av   = maybe [] id (lookup "a" rs); bv = maybe [] id (lookup "b" rs)+          y    = zipWith (\x z -> 1 + 2*x + 3*z) av bv+          df   = [ (n, NumData (V.fromList v)) | (n,v) <- ("y",y):rs ] :: [(Text,ColData)]+          mGp  = df |-> gpMulti (GPConfig RBF Gp  AutoMarginalLik) ["a","b"] "y"+          mKrr = df |-> gpMulti (GPConfig RBF Krr AutoMarginalLik) ["a","b"] "y"+          panelLayers mm =+            length (vsLayers (head (vsSubplots (toPlot (profiler [("y", mm)] ["a"])))))+      length (vsSubplots (toPlot (profiler [("y", mGp)] ["a","b"]))) `shouldBe` 2+      panelLayers mGp `shouldSatisfy` (> panelLayers mKrr)   -- Gp = 帯レイヤぶん多い++    -- Phase 78.E: RSM 等高線 / 応答曲面 (contourFilled + contour の 2 層)。+    it "contourOf: 塗り等値帯 (MContourFilled) + 等高線 (MContour) の 2 層" $ do+      let plan = centralCompositeDesign [contFactor "a" (0,1), contFactor "b" (0,1)]+          rs   = designTable plan+          av   = maybe [] id (lookup "a" rs); bv = maybe [] id (lookup "b" rs)+          ys   = zipWith (\x y -> 1 + 2*x + 3*y + x*y) av bv+          df   = [ (n, NumData (V.fromList v)) | (n,v) <- ("y",ys):rs ] :: [(Text,ColData)]+          m    = df |-> designModel plan "y"+          ls   = vsLayers (contourOf m "a" "b")+      length ls `shouldBe` 2+      map (getFirst . lyKind) ls `shouldBe` [Just MContourFilled, Just MContour]++  -- =====================================================================+  -- Phase 68 A7: 記述統計・検定 (test forest / describe box)+  -- =====================================================================+  describe "Phase 68 A7: TestResult forest + describeBox" $ do+    let mkTest nm ci = TestResult+                         { trMethod = nm, trStatistic = 2.0+                         , trDf = Just (10, Nothing), trPValue = 0.04+                         , trEffect = Just ("d", 0.8), trCI = ci+                         , trAlternative = TwoSided, trNote = Nothing }++    it "TestResult toPlot: CI 付き検定を 1 行 forest (MForest)" $ do+      let ls = vsLayers (toPlot (mkTest "t-test" (Just (0.2, 1.4))))+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MForest++    it "testForest: CI を持たない検定は除外 (全滅なら mempty)" $ do+      vsLayers (testForest [mkTest "a" Nothing, mkTest "b" Nothing]) `shouldBe` []++    it "testForestLabeled: CI 中心=点推定・半幅=誤差 (1 行 forest)" $ do+      let ls = vsLayers (testForestLabeled [("A vs B", mkTest "t" (Just (-1.0, 0.2)))])+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MForest++    it "describeBox: 生データ列を box plot (MBox)" $ do+      let ls = vsLayers (describeBox [1, 2, 3, 4, 5, 6, 7])+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MBox++  -- =====================================================================+  -- Phase 72.4/72.5: 回帰診断の可視化 (実測vs予測 / 係数 forest)+  -- =====================================================================+  describe "Phase 72.4/72.5: obsVsPred + coefForest (回帰診断の可視化)" $ do++    it "obsVsPred: y=x 参照線 (MLine) + 散布 (MScatter) の 2 layer" $ do+      let ls = vsLayers (obsVsPred m)+      length ls `shouldBe` 2+      getFirst (lyKind (ls !! 0)) `shouldBe` Just MLine     -- 参照線が下+      getFirst (lyKind (ls !! 1)) `shouldBe` Just MScatter  -- 散布が上++    it "obsVsPred: 散布の (x=実測, y=予測) は obsPredPairs と一致" $ do+      let (obs, prd) = obsPredPairs m+          sl         = vsLayers (obsVsPred m) !! 1+      case (getLast (lyEncX sl), getLast (lyEncY sl)) of+        (Just (ColNum vx), Just (ColNum vy)) -> do+          V.toList vx `shouldSatisfy` allClose obs+          V.toList vy `shouldSatisfy` allClose prd+        _ -> expectationFailure "scatter encX/encY が inline ColNum でない"++    it "obsPredPairs: 完全線形 fit は 実測 == 予測" $ do+      let (obs, prd) = obsPredPairs m+      prd `shouldSatisfy` allClose obs++    it "obsVsPred: y=x 参照線は (lo,hi)→(lo,hi) の 2 点 (傾き 1)" $ do+      let l = head (vsLayers (obsVsPred m))+      case (getLast (lyEncX l), getLast (lyEncY l)) of+        (Just (ColNum vx), Just (ColNum vy)) -> do+          V.length vx `shouldBe` 2+          V.toList vx `shouldBe` V.toList vy   -- y = x+        _ -> expectationFailure "参照線 encX/encY が inline ColNum でない"++    it "coefForest: 係数表を 1 行 forest (MForest)" $ do+      let ls = vsLayers (coefForest m)+      length ls `shouldBe` 1+      getFirst (lyKind (head ls)) `shouldBe` Just MForest++    it "coefForest: 点推定 (encX) は coefSummary の crEstimate と一致 ([1,2])" $ do+      let l = head (vsLayers (coefForest m))+      case getLast (lyEncX l) of+        Just (ColNum v) -> V.toList v `shouldSatisfy` allClose (map crEstimate (coefSummary m))+        _               -> expectationFailure "forest encX が inline ColNum でない"++    it "coefForest: 誤差 (errorX) は CI 半幅 = (hi-lo)/2 と一致" $ do+      let l       = head (vsLayers (coefForest m))+          halfCIs = [ (hi - lo) / 2 | r <- coefSummary m, let (lo, hi) = crCI95 r ]+      case getLast (lyErrorX l) of+        Just (ColNum v) -> V.toList v `shouldSatisfy` allClose halfCIs+        _               -> expectationFailure "forest errorX が inline ColNum でない"++  -- ==========================================================================+  -- Phase 106.4: umbrella の WorkflowSpec から移行した plot 連携診断テスト+  -- (旧 #ifdef PLOT_INTEGRATION 節。 umbrella component は hanalyze-plot に+  --  依存できない = package 循環のため、 本 suite が正式な置き場)。+  describe "Design.Workflow x plot 連携 (Phase 78.J / 78.G-f Task 4)" $ do++    -- Phase 78.J: designModelHBM の学習済 HBM を dhfModel で露出し、 診断抽出子+    -- (tracesOf / dagOf 等) に渡せる (DesignHBMFit から診断が出せる)。+    it "designModelHBM: dhfModel で診断 (tracesOf) が出せる" $ do+      let temps  = [-1, 1, -1, 1, -1, 1, -1, 1] :: [Double]+          lots   = ["A","A","A","A","B","B","B","B"] :: [T.Text]+          yv     = [ 2 + 3 * t + (if l == "A" then -1 else 1) | (t, l) <- zip temps lots ]+          df     = DX.insertColumn "lot"  (DX.fromList lots)+                 $ DX.insertColumn "temp" (DX.fromList temps)+                 $ DX.insertColumn "y"    (DX.fromList yv)+                 $ DX.empty+          plan   = factorialDesign [contFactor "temp" (-1, 1)]+          mWk      = df |-> designModelHBM defaultHBM plan [ranIntercept "lot"] "y"+      length (tracesOf (dhfModel mWk)) `shouldSatisfy` (> 0)   -- param ごとの trace が出る++    -- Phase 78.G-f Task 4: profiler/contour が designModelHBM に載る事後予測帯。+    -- designMatrixF (dhfFormula mWk) ef を直接叩き、 fit 時と同じ列順の設計行列を作る+    -- ( evalFrameAt 相当の helper は無いので eval ModelFrame を手組みする)。+    -- ★mfParams (合成パラメータ名 "_p0"/"_p1"…) は formula 内部表現なので手で推測せず、+    --   訓練済 'dhfFrame' (= mvFrame mWk) を土台に mfRoles/mfNRows だけ差し替える+    --   (本番の Core.hs evalFrame と同じ据え置き方)。+    it "MultiVarModel DesignHBMFit は事後予測帯を返す" $ do+      let temps  = [-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1] :: [Double]+          lots   = ["A","A","A","A","A","A","B","B","B","B","B","B"] :: [T.Text]+          noise  = [0.05, -0.03, 0.02, -0.04, 0.01, -0.02, 0.03, -0.01, 0.04, -0.05, 0.02, -0.03]+          lotShift l = if l == "A" then (-1.0) else 1.0+          yv     = [ 2 + 3 * t + lotShift l + e+                   | (t, l, e) <- zip3 temps lots noise ]+          mkFrameHBM = DX.insertColumn "lot"  (DX.fromList lots)+                     $ DX.insertColumn "temp" (DX.fromList temps)+                     $ DX.insertColumn "y"    (DX.fromList yv)+                     $ DX.empty+          plan = factorialDesign [contFactor "temp" (-1, 1)]+          mWk    = mkFrameHBM |-> designModelHBM defaultHBM plan [ranIntercept "lot"] "y"+          ef   = (mvFrame mWk)+                   { mfRoles  = [ ("y",    RoleResponse (V.fromList [0, 0, 0]))+                                , ("temp", RoleContinuous (V.fromList [-1, 0, 1]))+                                ]+                   , mfNRows  = 3+                   }+          (mu, band) = mvEvalFrame mWk 0.95 ef+      length mu `shouldBe` 3+      band `shouldSatisfy` isJust+      -- 中心 μ は temp とともに増加 (真の傾き ≈ 3)。+      (last mu - head mu) `shouldSatisfy` (> 3)