dataframe-learn-2.5.0.0: tests-internal/HistogramTrees.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
-- | Histogram trees: compiled routing must match fitting, and exact trees.
module HistogramTrees (tests) where
import Data.Either (fromRight)
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as VU
import Test.HUnit
import DataFrame.DecisionTree.Cart (cartFeatures)
import DataFrame.DecisionTree.Fit (treeToExpr)
import DataFrame.DecisionTree.Histogram (
TreeLimits (..),
binFeatures,
fitBinnedTree,
)
import DataFrame.DecisionTree.Regression (
RegFit (..),
RegTreeConfig (..),
defaultRegTreeConfig,
fitRegTree,
)
import DataFrame.DecisionTree.Types (Tree)
import qualified DataFrame.Internal.Column as DI
import DataFrame.Internal.Interpreter (interpret)
import qualified DataFrameApi as D
tests :: [Test]
tests =
[ TestLabel
"histogram tree: in-sample == interpreted (mixed nullable frame)"
inSampleMatchesInterpret
, TestLabel
"histogram tree: in-sample == interpreted (more values than bins)"
coarseBinsMatchInterpret
, TestLabel "histogram tree: same partition as the exact tree" matchesExactTree
]
limits :: Int -> TreeLimits
limits depth = TreeLimits depth 2 1 0 0 0
fitBinned ::
Int -> Int -> D.DataFrame -> VU.Vector Double -> (Tree Double, VU.Vector Double)
fitBinned bins depth df y =
fitBinnedTree
(limits depth)
(binFeatures bins (V.fromList (cartFeatures "y" df)))
y
Nothing
interpreted :: D.DataFrame -> Tree Double -> VU.Vector Double
interpreted df t = case interpret @Double df (treeToExpr t) of
Right (DI.TColumn c) -> fromRight VU.empty (DI.toVector @Double @VU.Vector c)
Left e -> error (show e)
maybeCol :: (DI.Columnable a) => [Maybe a] -> DI.Column
maybeCol = DI.fromVector . V.fromList
-- Nullable Double, Int and Text columns plus a plain one, as in NullSplits.
mixedFrame :: Int -> ([Double], D.DataFrame)
mixedFrame n = (y, D.fromColumns (("y", DI.fromList y) : cols))
where
rows = [0 .. n - 1]
md =
[ if i `mod` 7 == 0
then Nothing
else Just (fromIntegral ((i * 37) `mod` 23) :: Double)
| i <- rows
]
mi =
[if i `mod` 5 == 2 then Nothing else Just ((i * 11) `mod` 9 :: Int) | i <- rows]
mt =
[ if i `mod` 6 == 4
then Nothing
else Just (["a", "b", "c"] !! (i `mod` 3) :: T.Text)
| i <- rows
]
z = [fromIntegral ((i * 13) `mod` 17) :: Double | i <- rows]
y = [fromIntegral ((i * 29) `mod` 31) / 7 :: Double | i <- rows]
cols =
[ ("md", maybeCol md)
, ("mi", maybeCol mi)
, ("mt", maybeCol mt)
, ("z", DI.fromList z)
]
inSampleMatchesInterpret :: Test
inSampleMatchesInterpret = TestCase $ do
let (y, df) = mixedFrame 60
(t, inSample) = fitBinned 1024 6 df (VU.fromList y)
assertEqual
"in-sample predictions equal the interpreted tree"
(interpreted df t)
inSample
-- 23 distinct md values squeezed into 4 bins must still route consistently.
coarseBinsMatchInterpret :: Test
coarseBinsMatchInterpret = TestCase $ do
let (y, df) = mixedFrame 200
(t, inSample) = fitBinned 4 5 df (VU.fromList y)
assertEqual
"in-sample predictions equal the interpreted tree"
(interpreted df t)
inSample
matchesExactTree :: Test
matchesExactTree = TestCase $ do
let (y, df) = mixedFrame 120
yv = VU.fromList y
(_, binned) = fitBinned 1024 4 df yv
exact =
rfFitted
( fitRegTree
defaultRegTreeConfig{rtMaxDepth = 4}
(V.fromList (cartFeatures "y" df))
yv
Nothing
)
gap = VU.maximum (VU.zipWith (\a b -> abs (a - b)) binned exact)
assertBool ("same leaves up to rounding, max gap " ++ show gap) (gap < 1e-9)