packages feed

hanalyze-0.1.0.0: src/Hanalyze/Model/LM.hs

-- | Ordinary linear regression by least squares.
--
-- Solves @β = (XᵀX)⁻¹ Xᵀ y@ via hmatrix's @\\\\@ (LAPACK). Provides
-- confidence and prediction bands using
-- @t × √(s² xᵢᵀ(XᵀX)⁻¹xᵢ)@ and convenient adapters from a
-- @DataFrame@ for use from the CLI and report builder.
module Hanalyze.Model.LM
  ( LinearModel (..)
  , CIBand (..)
  , SmoothFit (..)
    -- * Matrix-canonical fit
  , fitLM
  , predictLM
    -- * Vector wrapper (1-output convenience)
  , fitLMVec
  , predictLMVec
    -- * Design matrices
  , designMatrix
  , polyDesignMatrix
  , multiPolyDesignMatrix
  , linspace
    -- * DataFrame helpers
  , fitDataFrameLM
  , confidenceBand
  , fitWithCI
  , fitPolyWithSmooth
  ) where

import qualified DataFrame.Internal.DataFrame as DXD
import Hanalyze.DataIO.Convert (getDoubleVec)
import Hanalyze.Model.Core (FitResult (..), Model (..), Band (..),
                   coefficientsV, residualsV, fittedList)

import Data.Text (Text)
import qualified Data.Vector as V
import qualified Numeric.LinearAlgebra as LA
import Statistics.Distribution (quantile)
import Statistics.Distribution.StudentT (studentT)

data LinearModel = LinearModel
  deriving (Show)

instance Model LinearModel where
  fit     _ = fitLM
  predict _ = predictLM

-- | Ordinary Least Squares (Matrix canonical, 多出力対応):
-- B = (XᵀX)⁻¹ Xᵀ Y、各列を独立に解く。
fitLM :: LA.Matrix Double -> LA.Matrix Double -> FitResult
fitLM x y =
  let beta  = x LA.<\> y                   -- p × q
      yHat  = x LA.<> beta                 -- n × q
      resid = y - yHat
      r2    = computeR2Multi y yHat
  in FitResult beta yHat resid r2

predictLM :: LA.Matrix Double -> LA.Matrix Double -> LA.Matrix Double
predictLM beta xNew = xNew LA.<> beta

-- | 単一出力 (Vector y) の便利ラッパ。@asColumn@ で 1 列行列に変換。
fitLMVec :: LA.Matrix Double -> LA.Vector Double -> FitResult
fitLMVec x y = fitLM x (LA.asColumn y)

-- | 1 出力での予測 (β は Vector)。
predictLMVec :: LA.Vector Double -> LA.Matrix Double -> LA.Vector Double
predictLMVec beta xNew = xNew LA.#> beta

-- | Build intercept + single predictor design matrix  [1, x].
designMatrix :: V.Vector Double -> LA.Matrix Double
designMatrix xs = LA.fromColumns
  [ LA.konst 1.0 n
  , LA.fromList (V.toList xs)
  ]
  where n = V.length xs

-- | Convenience: fit a simple LM directly from a DataFrame.
fitDataFrameLM :: DXD.DataFrame -> Text -> Text -> Maybe FitResult
fitDataFrameLM df xCol yCol = do
  xVec <- getDoubleVec xCol df
  yVec <- getDoubleVec yCol df
  let dm = designMatrix xVec
      y  = LA.fromList (V.toList yVec)
  return (fitLMVec dm y)

data CIBand = CIBand
  { lowerBound :: [Double]
  , upperBound :: [Double]
  , ciLevel    :: Double
  } deriving (Show)

-- | Pointwise confidence band for the mean response (1 出力前提)。
-- Formula: ŷᵢ ± t_{α/2, n−p} × sqrt(s² × xᵢᵀ (XᵀX)⁻¹ xᵢ)
confidenceBand :: LA.Matrix Double -> FitResult -> Double -> CIBand
confidenceBand x res level =
  let df    = fromIntegral (LA.rows x - LA.cols x)
      resV  = residualsV res
      s2    = (resV `LA.dot` resV) / df
      xtxi  = LA.inv (LA.tr x LA.<> x)
      tVal  = quantile (studentT df) ((1.0 + level) / 2.0)
      se xi = tVal * sqrt (s2 * (xi `LA.dot` (xtxi LA.#> xi)))
      rs    = LA.toRows x
      yHats = fittedList res
      los   = zipWith (\yh xi -> yh - se xi) yHats rs
      his   = zipWith (\yh xi -> yh + se xi) yHats rs
  in CIBand los his level

-- | Fit LM and compute confidence band in one step.
fitWithCI :: Double -> DXD.DataFrame -> Text -> Text -> Maybe (FitResult, CIBand)
fitWithCI level df xCol yCol = do
  xVec <- getDoubleVec xCol df
  yVec <- getDoubleVec yCol df
  let dm  = designMatrix xVec
      y   = LA.fromList (V.toList yVec)
      res = fitLMVec dm y
  return (res, confidenceBand dm res level)

-- | Polynomial design matrix [1, x, x², …, xᵈ].
polyDesignMatrix :: Int -> V.Vector Double -> LA.Matrix Double
polyDesignMatrix degree xs = LA.fromColumns
  [ LA.fromList [ x ^ k | x <- V.toList xs ]
  | k <- [0 .. degree]
  ]

-- | Multi-column polynomial design matrix.
-- Builds [1, x1, x1², …, x1^d1, x2, …, x2^d2, …] from a list of (column, degree) pairs.
multiPolyDesignMatrix :: [(V.Vector Double, Int)] -> LA.Matrix Double
multiPolyDesignMatrix [] = error "multiPolyDesignMatrix: empty predictor list"
multiPolyDesignMatrix colDegs@((firstXs, _) : _) =
  LA.fromColumns (intercept : concatMap polyExpand colDegs)
  where
    n           = V.length firstXs
    intercept   = LA.konst 1.0 n
    polyExpand (xs, deg) =
      [ LA.fromList [ x ^ k | x <- V.toList xs ] | k <- [1 .. deg] ]

-- | Grid of evenly spaced values from lo to hi.
linspace :: Double -> Double -> Int -> [Double]
linspace lo hi n
  | n <= 1    = [lo]
  | otherwise = [ lo + fromIntegral i * (hi - lo) / fromIntegral (n - 1)
                | i <- [0 .. n - 1] ]

-- | Pre-computed smooth curve data for plotting (evaluated on a fine grid).
data SmoothFit = SmoothFit
  { sfX       :: [Double]
  , sfFit     :: [Double]
  , sfLower   :: [Double]
  , sfUpper   :: [Double]
  , sfHasBand :: Bool
  } deriving (Show)

-- | Fit polynomial LM of given degree and compute a smooth curve with optional band
-- on a fine grid of nGrid points for clean visualisation.
fitPolyWithSmooth
  :: Band
  -> Int
  -> DXD.DataFrame
  -> Text
  -> Text
  -> Maybe (FitResult, SmoothFit)
fitPolyWithSmooth band nGrid df xCol yCol = do
  xVec <- getDoubleVec xCol df
  yVec <- getDoubleVec yCol df
  let degree = 1
      dm     = polyDesignMatrix degree xVec
      y      = LA.fromList (V.toList yVec)
      res    = fitLMVec dm y
      beta   = coefficientsV res

      xLa    = LA.fromList (V.toList xVec)
      xGrid  = V.fromList (linspace (LA.minElement xLa) (LA.maxElement xLa) nGrid)
      dmG    = polyDesignMatrix degree xGrid
      yGrid  = LA.toList (dmG LA.#> beta)

      dfStat = fromIntegral (LA.rows dm - LA.cols dm) :: Double
      resV   = residualsV res
      s2     = (resV `LA.dot` resV) / dfStat
      xtxi   = LA.inv (LA.tr dm LA.<> dm)
      gRows  = LA.toRows dmG

      computeBand level isPI =
        let tVal   = quantile (studentT dfStat) ((1.0 + level) / 2.0)
            extra  = if isPI then 1.0 else 0.0
            halfW xi = tVal * sqrt (s2 * (extra + xi `LA.dot` (xtxi LA.#> xi)))
            los    = zipWith (\yh xi -> yh - halfW xi) yGrid gRows
            his    = zipWith (\yh xi -> yh + halfW xi) yGrid gRows
        in (los, his)

  case band of
    NoBand ->
      return (res, SmoothFit (V.toList xGrid) yGrid yGrid yGrid False)
    CI level ->
      let (los, his) = computeBand level False
      in return (res, SmoothFit (V.toList xGrid) yGrid los his True)
    PI level ->
      let (los, his) = computeBand level True
      in return (res, SmoothFit (V.toList xGrid) yGrid los his True)

-- | 各列ごとに R² を計算 (多出力対応)。
computeR2Multi :: LA.Matrix Double -> LA.Matrix Double -> LA.Vector Double
computeR2Multi y yHat =
  let q = LA.cols y
  in LA.fromList
       [ let yj    = LA.flatten (y    LA.¿ [j])
             yhj   = LA.flatten (yHat LA.¿ [j])
             resid = yj - yhj
             yMean = LA.sumElements yj / fromIntegral (LA.size yj)
             dev   = LA.cmap (subtract yMean) yj
             ssRes = resid `LA.dot` resid
             ssTot = dev   `LA.dot` dev
         in if ssTot == 0 then 0
              else 1.0 - ssRes / ssTot
       | j <- [0 .. q - 1] ]