dataframe-learn-2.5.0.0: tests-internal/NullSplits.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
-- | Tree learners on nullable columns route null rows as fitting did.
module NullSplits (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.Boosting.AdaBoost (AdaBoostConfig (..), defaultAdaBoostConfig)
import DataFrame.DecisionTree.Cart (buildCartTree, cartFeatures)
import DataFrame.DecisionTree.Fit (treeToExpr)
import DataFrame.DecisionTree.Regression (
RegFit (..),
RegTreeConfig (..),
defaultRegTreeConfig,
fitRegTree,
)
import DataFrame.DecisionTree.Types (Tree, TreeConfig (..), defaultTreeConfig)
import qualified DataFrame.Internal.Column as DI
import DataFrame.Internal.Expression (Expr (..))
import DataFrame.Internal.Interpreter (interpret)
import DataFrame.Model (Fit (..), Predict (..))
import qualified DataFrameApi as D
tests :: [Test]
tests =
[ TestLabel
"null splits: in-sample == interpreted (mixed nullable frame)"
inSampleMatchesInterpret
, TestLabel "null splits: nulls join the side they fit" learnedDirection
, TestLabel "null splits: null-indicator split" nullIndicator
, TestLabel "null splits: unseen nulls still route" unseenNullsRoute
, TestLabel "null splits: CART null indicator" cartNullIndicator
, TestLabel "null splits: AdaBoost null indicator" adaBoostNullIndicator
]
fitAll ::
Int -> D.DataFrame -> VU.Vector Double -> (Tree Double, VU.Vector Double)
fitAll depth df y = (rfTree fitted, rfFitted fitted)
where
fitted =
fitRegTree
defaultRegTreeConfig{rtMaxDepth = depth}
(V.fromList (cartFeatures "y" df))
y
Nothing
interpreted :: D.DataFrame -> Tree Double -> VU.Vector Double
interpreted df = interpretedExpr df . treeToExpr
interpretedExpr :: D.DataFrame -> Expr Double -> VU.Vector Double
interpretedExpr df e = case interpret @Double df e of
Right (DI.TColumn c) -> fromRight VU.empty (DI.toVector @Double @VU.Vector c)
Left e -> error (show e)
withTarget :: [Double] -> [(T.Text, DI.Column)] -> D.DataFrame
withTarget y cols = D.fromColumns (("y", DI.fromList y) : cols)
maybeCol :: (DI.Columnable a) => [Maybe a] -> DI.Column
maybeCol = DI.fromVector . V.fromList
inSampleMatchesInterpret :: Test
inSampleMatchesInterpret = TestCase $ do
let n = 60 :: Int
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]
df =
withTarget
y
[ ("md", maybeCol md)
, ("mi", maybeCol mi)
, ("mt", maybeCol mt)
, ("z", DI.fromList z)
]
(t, inSample) = fitAll 6 df (VU.fromList y)
assertEqual
"in-sample predictions equal the interpreted tree"
(interpreted df t)
inSample
-- Nulls share the low x values' target, so an exact split sends nulls left.
learnedDirection :: Test
learnedDirection = TestCase $ do
let y = [0, 0, 0, 1, 1, 1, 0, 0]
x = map Just [1, 2, 3, 4, 5, 6] ++ [Nothing, Nothing] :: [Maybe Double]
df = withTarget y [("x", maybeCol x)]
(t, inSample) = fitAll 1 df (VU.fromList y)
assertEqual "fits exactly" (VU.fromList y) inSample
assertEqual "interpreted agrees" (VU.fromList y) (interpreted df t)
nullIndicator :: Test
nullIndicator = TestCase $ do
let y = [0, 0, 0, 1, 1]
x = [Just 1, Just 2, Just 3, Nothing, Nothing] :: [Maybe Int]
df = withTarget y [("x", maybeCol x)]
(t, inSample) = fitAll 1 df (VU.fromList y)
assertEqual "fits exactly" (VU.fromList y) inSample
assertEqual "interpreted agrees" (VU.fromList y) (interpreted df t)
unseenNullsRoute :: Test
unseenNullsRoute = TestCase $ do
let y = [0, 0, 0, 1, 1, 1]
x = map Just [1, 2, 3, 4, 5, 6] :: [Maybe Int]
(t, inSample) = fitAll 1 (withTarget y [("x", maybeCol x)]) (VU.fromList y)
testDf = withTarget y [("x", maybeCol (take 4 x ++ [Nothing, Nothing]))]
scored = interpreted testDf t
assertEqual "fits exactly" (VU.fromList y) inSample
assertEqual "present rows unchanged" (VU.take 4 inSample) (VU.take 4 scored)
assertBool
"null rows land in a leaf"
(VU.all (`elem` [0, 1]) (VU.drop 4 scored))
-- The target depends only on whether x is null.
indicatorFrame :: ([Double], D.DataFrame)
indicatorFrame = (y, withTarget y [("x", maybeCol x)])
where
y = [0, 0, 0, 1, 1]
x = [Just 1, Just 2, Just 3, Nothing, Nothing] :: [Maybe Int]
cartNullIndicator :: Test
cartNullIndicator = TestCase $ do
let (y, df) = indicatorFrame
t =
buildCartTree @Double
defaultTreeConfig{maxTreeDepth = 1, minLeafSize = 1}
"y"
df
assertEqual "fits exactly" (VU.fromList y) (interpreted df t)
adaBoostNullIndicator :: Test
adaBoostNullIndicator = TestCase $ do
let (y, df) = indicatorFrame
cfg = defaultAdaBoostConfig{abNEstimators = 1, abMaxDepth = 1}
model = fit cfg (Col @Double "y") df
assertEqual "fits exactly" (VU.fromList y) (interpretedExpr df (predict model))