packages feed

hanalyze-0.2.0.0: src/Hanalyze/Model/PartialDependence.hs

{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module      : Hanalyze.Model.PartialDependence
-- Description : 任意モデル対応の Partial Dependence / ICE 純粋計算エンジン (model 非依存・非ゲート層)
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- 部分従属 (Partial Dependence) / ICE の純粋計算エンジン — 任意モデル対応 (Phase 75.27)。
--
-- R @pdp::partial@ / sklearn @sklearn.inspection.partial_dependence@ 相当。 学習済モデルの
-- predict を「注目特徴を grid で振り、 他の特徴は訓練データの観測分布のまま」評価し、 全観測
-- 行で平均したものが PDP、 行ごとの曲線が ICE (individual conditional expectation)。
--
-- model 非依存 (predict 閉包のみを受ける) ゆえ **非ゲート層** に置き、 図化は
-- 'Hanalyze.Plot.ML' がゲート (@plot-integration@) 配下で担う。
--
-- @
-- import Hanalyze.Model.PartialDependence
--
-- -- 任意モデルの predict 閉包を渡す (R pdp の pred.fun 流)。
-- let r = partialDependence trainX (\\m -> map (predictRF rf) (LA.toLists m)) 0 40
-- in  (pdpGrid r, pdpMean r)          -- 特徴 0 の PDP 曲線
-- @
module Hanalyze.Model.PartialDependence
  ( -- * 結果型
    PDPResult (..)
    -- * 計算
  , partialDependence
  , partialDependenceGrid
    -- * 変換
  , centerICE
  ) where

import qualified Numeric.LinearAlgebra as LA
import           Data.List             (transpose)

-- ===========================================================================
-- 結果型
-- ===========================================================================

-- | 部分従属の計算結果。 grid・PDP 平均曲線・ICE 個体曲線群をまとめて返す。
data PDPResult = PDPResult
  { pdpGrid :: ![Double]      -- ^ 注目特徴の grid 値 (長さ = grid 数)。
  , pdpMean :: ![Double]      -- ^ PDP: 各 grid 値で全観測行の予測を平均 (長さ = grid 数)。
  , pdpIce  :: ![[Double]]    -- ^ ICE: 観測行ごとの曲線 (n 本・各長さ = grid 数)。
  } deriving (Eq, Show)

-- ===========================================================================
-- 計算
-- ===========================================================================

-- | 注目特徴 j の観測 @[min,max]@ を等間隔 grid にして PDP/ICE を計算する。
--   grid 数 <2 は 2 に切り上げ。 空データ・列外 index は空結果 ('PDPResult' [] [] [])。
partialDependence
  :: LA.Matrix Double                 -- ^ 訓練特徴行列 X (n 行 × p 列)。
  -> (LA.Matrix Double -> [Double])   -- ^ predict: 行列の各行 → 予測値 (長さ = 行数)。
  -> Int                              -- ^ 注目特徴の列 index j (0 始まり)。
  -> Int                              -- ^ grid 数。
  -> PDPResult
partialDependence x predict j n
  | LA.rows x == 0 || j < 0 || j >= LA.cols x = PDPResult [] [] []
  | otherwise =
      let col  = LA.toList (LA.toColumns x !! j)
          lo   = minimum col
          hi   = maximum col
          m    = max 2 n
          grid = [ lo + (hi - lo) * fromIntegral i / fromIntegral (m - 1)
                 | i <- [0 .. m - 1] ]
      in partialDependenceGrid x predict j grid

-- | grid を明示指定する版。 分位点 grid や任意評価点を渡したいときに使う。
--   空 grid・空データ・列外 index は空結果。
partialDependenceGrid
  :: LA.Matrix Double
  -> (LA.Matrix Double -> [Double])
  -> Int
  -> [Double]                         -- ^ 注目特徴の評価 grid。
  -> PDPResult
partialDependenceGrid x predict j grid
  | LA.rows x == 0 || j < 0 || j >= LA.cols x || null grid = PDPResult [] [] []
  | otherwise =
      let nrows  = LA.rows x
          cols   = LA.toColumns x
          -- 各 grid 値 g で X の j 列を定数 g に置換 → 全行 predict (長さ nrows)。
          predsAtG g =
            let xg = LA.fromColumns
                       [ if c == j then LA.konst g nrows else col
                       | (c, col) <- zip [0 ..] cols ]
            in predict xg
          byGrid = [ predsAtG g | g <- grid ]              -- grid × n
          means  = [ sum ps / fromIntegral nrows | ps <- byGrid ]
          ice    = transpose byGrid                        -- n × grid (行ごとの曲線)
      in PDPResult grid means ice

-- ===========================================================================
-- 変換
-- ===========================================================================

-- | 中心化 ICE (c-ICE)。 各 ICE 曲線を **左端 (grid[0]) の値が 0** になるよう平行移動し、
--   PDP 平均も中心化後の ICE から取り直す。 個体間の傾き差を見やすくする
--   (sklearn @centered=True@ / R @ice()@ centered 相当)。 空結果はそのまま。
centerICE :: PDPResult -> PDPResult
centerICE r
  | null (pdpGrid r) || null (pdpIce r) = r
  | otherwise =
      let ice'   = [ case curve of
                       (c0 : _) -> map (subtract c0) curve
                       []       -> curve
                   | curve <- pdpIce r ]
          nrows  = length ice'
          means' = case ice' of
                     [] -> []
                     _  -> map (\col -> sum col / fromIntegral nrows) (transpose ice')
      in r { pdpMean = means', pdpIce = ice' }