packages feed

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

{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module      : Hanalyze.Model.MultiLM
-- Description : Multivariate (multi-output) linear regression — 列別 OLS + 残差共分散推定
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- Multivariate (multi-output) linear regression.
--
-- @Y = XB + E@ with @Y@ of shape @n × q@ (@q@ outputs), @X@ of shape
-- @n × p@, @B@ of shape @p × q@ and @E@ of shape @n × q@.
--
-- Solves each column independently by OLS (column-wise OLS) and
-- additionally estimates the residual covariance matrix @Σ@, which is
-- used for joint multi-output predictive intervals.
--
-- The API matches 'Hanalyze.Model.LM', so @fitLM@ can be called directly; this
-- module merely exposes the additional multi-output information
-- (@Σ@, correlation matrix).
module Hanalyze.Model.MultiLM
  ( MultiFit (..)
  , fitMultiLM
  , predictMultiLM
  , residualCovariance
  , residualCorrelation
  ) where

import qualified Numeric.LinearAlgebra as LA
import Hanalyze.Model.Core (FitResult (..))
import qualified Hanalyze.Model.LM as LM

-- | Augmented result for multi-output linear regression.
data MultiFit = MultiFit
  { mfFit         :: FitResult        -- ^ Underlying matrix-based fit.
  , mfResidCov    :: LA.Matrix Double -- ^ Residual covariance @Σ@ (@q × q@).
  , mfResidCor    :: LA.Matrix Double -- ^ Residual correlation matrix (@q × q@).
  , mfNumOutputs  :: Int              -- ^ Number of responses @q@.
  , mfNumPredict  :: Int              -- ^ Number of predictors @p@.
  , mfNumSamples  :: Int              -- ^ Number of observations @n@.
  } deriving (Show)

-- | Multi-output linear regression: @Y = XB + E@.
-- Delegates to 'LM.fitLM' and additionally returns the residual
-- covariance.
fitMultiLM :: LA.Matrix Double  -- ^ Design matrix @X@ (@n × p@).
           -> LA.Matrix Double  -- ^ Response @Y@ (@n × q@).
           -> MultiFit
fitMultiLM x y =
  let fit = LM.fitLM x y
      res = residuals fit
      n   = LA.rows y
      q   = LA.cols y
      p   = LA.cols x
      df  = max 1 (n - p)   -- 自由度補正
      -- Σ = (1/(n-p)) * Eᵀ E
      sigma = LA.scale (1 / fromIntegral df)
                       (LA.tr res LA.<> res)
      -- 相関行列: D⁻¹ Σ D⁻¹ where D = diag(sqrt(diag(Σ)))
      diagS = [ sqrt (sigma `LA.atIndex` (i, i))
              | i <- [0 .. q - 1] ]
      corr  = LA.fromLists
        [ [ if di == 0 || dj == 0 then 0
            else (sigma `LA.atIndex` (i, j)) / (di * dj)
          | j <- [0 .. q - 1]
          , let dj = diagS !! j ]
        | i <- [0 .. q - 1]
        , let di = diagS !! i ]
  in MultiFit fit sigma corr q p n

-- | Predict @Ŷ@ (@m × q@) for new inputs @X_new@ (@m × p@). A thin
-- wrapper around 'LM.predictLM'.
predictMultiLM :: MultiFit -> LA.Matrix Double -> LA.Matrix Double
predictMultiLM mf xNew =
  LM.predictLM (coefficients (mfFit mf)) xNew

-- | Residual covariance matrix (alias for 'mfResidCov').
residualCovariance :: MultiFit -> LA.Matrix Double
residualCovariance = mfResidCov

-- | Residual correlation matrix.
residualCorrelation :: MultiFit -> LA.Matrix Double
residualCorrelation = mfResidCor