hanalyze-0.2.0.0: test/Hanalyze/Model/DecisionTreeSpec.hs
{-# OPTIONS_GHC -Wno-unused-imports #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Hanalyze.Model.DecisionTreeSpec (spec) where
import Test.Hspec
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck
import Hanalyze.Model.Formula
import Hanalyze.Model.Formula.Frame
import Hanalyze.Model.Formula.Design
import Hanalyze.Model.Formula.RFormula
import Hanalyze.Model.Formula.Nonlinear
import Hanalyze.Model.Formula.Mixed
import Hanalyze.Model.GLMM
import Hanalyze.Model.GLM (Family (..), LinkFn (..))
import Hanalyze.Stat.Distribution (Transform)
import Data.List (sort, nub)
import Control.Monad (forM, forM_)
import System.IO.Temp (withSystemTempFile)
import System.IO (hPutStr, hClose)
import Hanalyze.Model.HBM.Ast (Expr (..), Lit (..), DoStmt (..), Err)
import Data.IORef (newIORef, readIORef, modifyIORef')
import qualified Hanalyze.Model.DecisionTree as DT
import qualified Data.Text as T
import SpecHelper
spec :: Spec
spec = do
describe "Hanalyze.Model.DecisionTree" $ do
it "fitDT: 線形分離可能なデータで perfect train accuracy" $ do
-- y = 0 if x[0] < 5, else 1
let xs = [[fromIntegral x] | x <- [1..10::Int]]
ys = [if x < 5 then 0 else 1 | x <- [1..10::Int]]
tree = DT.fitDT DT.defaultDecisionTree xs ys
preds = map (DT.predictDT tree) xs
preds `shouldBe` ys
it "fitDT: 2D XOR-like パターン" $ do
let xs = [[0, 0], [0, 1], [1, 0], [1, 1]]
ys = [0, 1, 1, 0] -- XOR
tree = DT.fitDT DT.defaultDecisionTree xs ys
preds = map (DT.predictDT tree) xs
preds `shouldBe` ys
it "predictDT: 混在葉は多数派 (argmax) を予測する (argmin 回帰バグ防止)" $ do
-- depth=0 に制限し root 自身を混在葉にする。 x に依らずクラス 1 が多数 (7:3)。
-- 旧バグ (argmin) だと少数派クラス 0 を返していた。
let cfg = DT.defaultDecisionTree { DT.dtMaxDepth = Just 0 }
xs = [[fromIntegral i] | i <- [1..10::Int]]
ys = [0,0,0, 1,1,1,1,1,1,1] -- class 1 が 7、 class 0 が 3。
tree = DT.fitDT cfg xs ys
map (DT.predictDT tree) xs `shouldBe` replicate 10 1
let xs = [[1.0], [2.0], [3.0]]
ys = [0, 0, 1]
tree = DT.fitDT DT.defaultDecisionTree xs ys
probs = DT.predictDTProbs tree [1.5]
-- x=1.5 should reach a leaf where most samples are class 0
probs `shouldSatisfy` (\m -> length m >= 1)
it "giniImpurity: 純粋クラスで 0、均等で 0.5 (2 クラス)" $ do
DT.giniImpurity [0, 0, 0, 0] `shouldBe` 0.0
DT.giniImpurity [1, 1, 1, 1] `shouldBe` 0.0
DT.giniImpurity [0, 0, 1, 1] `shouldBe` 0.5
it "maxDepth=1: shallow tree、underfit に近い" $ do
let cfg = DT.defaultDecisionTree { DT.dtMaxDepth = Just 1 }
xs = [[fromIntegral x, fromIntegral y]
| x <- [1..5::Int], y <- [1..5::Int]]
ys = [if x + y > 5 then 1 else 0
| x <- [1..5::Int], y <- [1..5::Int]]
tree = DT.fitDT cfg xs ys
-- maxDepth=1 → 1 split, root = decision node
case tree of
DT.DNode {} -> True `shouldBe` True
_ -> expectationFailure "Expected DNode at root"
it "printRpart: R print.rpart 形式 (node#/split/n/loss/yval/yprob/*)" $ do
-- 3 クラス・特徴 1 (petal_width 風) で完全分離する木。
let xs = [[1.4,0.2],[1.3,0.2],[1.5,0.2],[1.4,0.3]
,[4.5,1.5],[4.7,1.4],[4.9,1.5],[4.0,1.3]
,[6.0,2.5],[5.8,2.2],[6.3,1.8],[5.5,2.1]]
ys = [0,0,0,0, 1,1,1,1, 2,2,2,2]
tree = DT.fitDT DT.defaultDecisionTree xs ys
out = DT.printRpartRaw ["petal_length","petal_width"]
["setosa","versicolor","virginica"] tree
ls = lines (T.unpack out)
-- ヘッダ (n= / legend / * denotes)。
ls `shouldContain` ["n= 12"]
ls `shouldContain` ["node), split, n, loss, yval, (yprob)"]
-- root は node# 1・split=root・n=12・loss=8・yval=setosa (全クラス同数の tie)。
ls `shouldContain` ["1) root 12 8 setosa (0.3333 0.3333 0.3333)"]
-- 左枝 (≤・条件成立) = "name< thr"・終端 * 付き・純粋クラス確率。
ls `shouldContain`
[" 2) petal_width< 0.80 4 0 setosa (1.0000 0.0000 0.0000) *"]
-- 右枝 = "name>=thr"・内部ノード (* 無し)。
ls `shouldContain` [" 3) petal_width>=0.80 8 4 versicolor (0.0000 0.5000 0.5000)"]
-- node# は R 慣例 root=1・子 2k/2k+1 → node 3 の子は 6/7。
ls `shouldContain` [" 6) petal_width< 1.65 4 0 versicolor (0.0000 1.0000 0.0000) *"]
ls `shouldContain` [" 7) petal_width>=1.65 4 0 virginica (0.0000 0.0000 1.0000) *"]
it "printRpart: 列名・クラス名なしは f{i}/整数へフォールバック" $ do
let xs = [[1.4,0.2],[1.3,0.2],[4.5,1.5],[4.7,1.4]]
ys = [0,0,1,1]
tree = DT.fitDT DT.defaultDecisionTree xs ys
out = DT.printRpartRaw [] [] tree
-- 特徴 index 1 → f1、 クラス → 整数のまま。
out `shouldSatisfy` \t -> "f" `T.isInfixOf` t
&& "root 4 2 0" `T.isInfixOf` t
-- ===========================================================================
-- Hanalyze.Model.TimeSeries (Phase 11)
-- ===========================================================================