packages feed

dataframe-3.0.0.0: tests/Learn/TypedModel.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

{- | 'fit'/'predict' are frame-aware. Training on a 'TypedDataFrame' with a typed
target ('DT.col') returns a 'Fitted' carrying the schema, and 'predict' on it
yields a typed 'TExpr'; training on an untyped 'DataFrame' with an untyped target
('F.col') returns the bare model and an 'Expr'. Both paths must agree numerically.
-}
module Learn.TypedModel (tests) where

import Data.Maybe (fromJust)
import qualified Data.Vector.Unboxed as VU

import qualified DataFrame as D
import qualified DataFrame.Functions as F
import DataFrame.Internal.Column (TypedColumn (..), toVector)
import qualified DataFrame.Internal.Column as DI
import DataFrame.Internal.Expression (Expr)
import DataFrame.Internal.Interpreter (interpret)

import DataFrame.LinearModel.Regression (
    LinearConfig (..),
    LinearRegressor (..),
    Penalty (..),
    defaultLinearConfig,
 )
import DataFrame.Model (Fitted (..), fit, predict)
import qualified DataFrame.Typed as DT

import Test.HUnit

-- | All-Double schema: two features and a Double target.
type Houses =
    '[ DT.Column "x1" Double
     , DT.Column "x2" Double
     , DT.Column "y" Double
     ]

regDF :: D.DataFrame
regDF =
    D.fromNamedColumns
        [ ("x1", DI.fromList xs1)
        , ("x2", DI.fromList xs2)
        , ("y", DI.fromList [2 * a - 3 * b + 1 | (a, b) <- zip xs1 xs2])
        ]
  where
    xs1 = [1, 2, 3, 4, 5, 6, 7, 8] :: [Double]
    xs2 = [2, 1, 4, 3, 6, 5, 8, 7] :: [Double]

housesTDF :: DT.TypedDataFrame Houses
housesTDF = fromJust (DT.freeze @Houses regDF)

interpD :: D.DataFrame -> Expr Double -> [Double]
interpD df e = case interpret @Double df e of
    Right (TColumn c) -> either (const []) VU.toList (toVector @Double @VU.Vector c)
    Left err -> error (show err)

close :: Double -> Double -> Double -> Bool
close tol a b = abs (a - b) <= tol

sameModel :: LinearRegressor -> LinearRegressor -> Bool
sameModel a b =
    and (zipWith (close 1e-12) (VU.toList (regCoef a)) (VU.toList (regCoef b)))
        && close 1e-12 (regIntercept a) (regIntercept b)

{- | 'fit' on a 'TypedDataFrame' returns a 'Fitted' whose model matches the
bare model from 'fit' on the equivalent 'DataFrame'.
-}
testTypedFitParity :: Test
testTypedFitParity = TestCase $ do
    let onTyped = fit defaultLinearConfig (DT.col @"y") housesTDF
        onUntyped = fit defaultLinearConfig (F.col @Double "y") regDF
    assertBool
        "fittedModel of typed fit == untyped fit"
        (sameModel (fittedModel onTyped) onUntyped)

{- | 'predict' on the typed fit yields a 'TExpr Houses Double'; unwrapped and
interpreted it matches 'predict' on the untyped fit.
-}
testTypedPredict :: Test
testTypedPredict = TestCase $ do
    let onTyped = fit defaultLinearConfig (DT.col @"y") housesTDF
        onUntyped = fit defaultLinearConfig (F.col @Double "y") regDF
        typedExpr = DT.unTExpr (predict onTyped) -- :: Expr Double via TExpr Houses Double
        untypedExpr = predict onUntyped -- :: Expr Double
        truth = interpD regDF (F.col @Double "y")
    assertBool
        "typed predict matches untyped predict"
        ( and
            (zipWith (close 1e-12) (interpD regDF typedExpr) (interpD regDF untypedExpr))
        )
    assertBool
        "typed predict recovers the target"
        (and (zipWith (close 1e-6) (interpD regDF typedExpr) truth))

-- | The frame-awareness holds for a regularized fit too.
testTypedRidge :: Test
testTypedRidge = TestCase $ do
    let cfg = defaultLinearConfig{lcPenalty = Ridge 0.01}
        onTyped = fit cfg (DT.col @"y") housesTDF
        onUntyped = fit cfg (F.col @Double "y") regDF
    assertBool
        "ridge: fittedModel of typed fit == untyped fit"
        (sameModel (fittedModel onTyped) onUntyped)

tests :: [Test]
tests =
    [ TestLabel "typed fit returns Fitted matching untyped" testTypedFitParity
    , TestLabel "typed predict yields TExpr matching untyped" testTypedPredict
    , TestLabel "typed ridge fit parity" testTypedRidge
    ]