packages feed

hanalyze-0.2.0.0: src/Hanalyze/Model/LiNGAM/Pairwise.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BangPatterns      #-}
-- |
-- Module      : Hanalyze.Model.LiNGAM.Pairwise
-- Description : Pairwise LiNGAM (Hyvärinen-Smith 2013、2 変数間の因果方向推定)
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- Pairwise LiNGAM: 2 変数間の因果方向 (x → y か y → x か) 推定。
--
-- ## アルゴリズム (Hyvärinen-Smith 2013)
--
-- 標準化された (x, y) について、 非ガウシアン独立性に基づき:
--
--   R(x → y) = - Cov(x³, y) · sign(Cov(x, y)) + Cov(x, y³)
--
-- の符号で方向を決定する近似的測度 (LIM, likelihood ratio approximation)。
--
-- * R > 0 → x → y
-- * R < 0 → y → x
-- * |R| 小 → 判定不能 (ガウシアン近接 or 弱依存)
--
-- 軽量で 2 変数の方向推定に直接使える。 3 変数以上には 'DirectLiNGAM' を使う。
--
-- ## リファレンス
--
-- Hyvärinen, A. & Smith, S. M. (2013) "Pairwise likelihood ratios for
-- estimation of non-Gaussian structural equation models", JMLR 14.
-- Python 実装は cdt15/lingam の `lingam/lim.py` (LIM = Likelihood-based
-- Independence Measure)。
module Hanalyze.Model.LiNGAM.Pairwise
  ( PairwiseDirection (..)
  , PairwiseResult (..)
  , pairwiseLiNGAM
  , pairwiseScore
  ) where

import qualified Numeric.LinearAlgebra as LA

-- ===========================================================================
-- 型
-- ===========================================================================

data PairwiseDirection
  = XtoY        -- ^ x → y
  | YtoX        -- ^ y → x
  | Inconclusive  -- ^ |score| < threshold
  deriving (Show, Eq)

data PairwiseResult = PairwiseResult
  { prScore     :: !Double             -- ^ R(x → y) の値、 符号で方向決定
  , prDirection :: !PairwiseDirection
  , prMagnitude :: !Double             -- ^ |score|、 confidence の代理
  } deriving (Show)

-- ===========================================================================
-- 実装
-- ===========================================================================

-- | Pairwise LiNGAM の主関数。 threshold 未満は Inconclusive。
pairwiseLiNGAM
  :: Double               -- threshold (default 0.0 = 符号だけで判定)
  -> LA.Vector Double     -- x
  -> LA.Vector Double     -- y
  -> PairwiseResult
pairwiseLiNGAM thr x y =
  let !s = pairwiseScore x y
      !mag = abs s
      !dir
        | mag < thr = Inconclusive
        | s > 0     = XtoY
        | otherwise = YtoX
  in PairwiseResult { prScore = s, prDirection = dir, prMagnitude = mag }

-- | スコア R = -Cov(x³, y)·sign(Cov(x,y)) + Cov(x, y³)
--   x, y は内部で標準化される (zero-mean、 unit-variance)。
pairwiseScore :: LA.Vector Double -> LA.Vector Double -> Double
pairwiseScore xRaw yRaw =
  let !x = standardize xRaw
      !y = standardize yRaw
      !x3 = x * x * x
      !y3 = y * y * y
      !cov_x_y   = covar x  y
      !cov_x3_y  = covar x3 y
      !cov_x_y3  = covar x  y3
      !sgn = if cov_x_y >= 0 then 1.0 else (-1.0 :: Double)
  in - cov_x3_y * sgn + cov_x_y3

-- ===========================================================================
-- 内部
-- ===========================================================================

standardize :: LA.Vector Double -> LA.Vector Double
standardize v =
  let !n  = fromIntegral (LA.size v) :: Double
      !mu = LA.sumElements v / n
      !c  = v - LA.scalar mu
      !s  = sqrt (c `LA.dot` c / n)
      !sd = if s > 1e-12 then s else 1.0
  in LA.scale (1 / sd) c

covar :: LA.Vector Double -> LA.Vector Double -> Double
covar a b =
  let !n  = fromIntegral (LA.size a) :: Double
      !ma = LA.sumElements a / n
      !mb = LA.sumElements b / n
      !ca = a - LA.scalar ma
      !cb = b - LA.scalar mb
  in ca `LA.dot` cb / n