packages feed

dataframe-2.3.0.0: tests/Learn/Synthesis.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

{- | Feature-synthesis enumerator: it should recover small exact features
(@x²@, @a/b@) from the example rows, score them near-perfectly, and be
deterministic.
-}
module Learn.Synthesis (tests) where

import qualified DataFrame as D
import DataFrame.Model (fit)
import DataFrame.Synthesis

import Test.HUnit

quad :: D.DataFrame
quad =
    D.fromNamedColumns
        [ ("x", D.fromList xs)
        , ("y", D.fromList (map (\x -> x * x) xs))
        ]
  where
    xs = map fromIntegral [1 .. 12 :: Int] :: [Double]

ratio :: D.DataFrame
ratio =
    D.fromNamedColumns
        [ ("a", D.fromList ([2, 6, 12, 20, 30, 42] :: [Double]))
        , ("b", D.fromList ([1, 2, 3, 4, 5, 6] :: [Double]))
        , ("y", D.fromList ([2, 3, 4, 5, 6, 7] :: [Double]))
        ]

-- | Pearson r²: enumeration should find x·x and score it ~1.
recoversQuadratic :: Test
recoversQuadratic = TestCase $ do
    let sf = fit defaultSynthesisConfig (D.col @Double "y") quad
    assertBool
        ("quadratic r2 = " ++ show (sfScore sf))
        (sfScore sf > 0.999 && sfScore sf <= 1.0001)

-- | MSE: the best feature reproduces the target exactly, so -MSE ~ 0.
exactRecoveryMSE :: Test
exactRecoveryMSE = TestCase $ do
    let sf =
            fit defaultSynthesisConfig{synLoss = MeanSquaredError} (D.col @Double "y") quad
    assertBool ("exact -mse = " ++ show (sfScore sf)) (sfScore sf > -1.0e-6)

-- | Division is enumerated (with the denominator guard): a/b is recovered.
recoversRatio :: Test
recoversRatio = TestCase $ do
    let sf = fit defaultSynthesisConfig (D.col @Double "y") ratio
    assertBool
        ("ratio r2 = " ++ show (sfScore sf))
        (sfScore sf > 0.999 && sfScore sf <= 1.0001)

-- | The bank is non-trivial and its ranked features are distinct expressions.
distinctFeatures :: Test
distinctFeatures = TestCase $ do
    let sf = fit defaultSynthesisConfig (D.col @Double "y") quad
        names = [D.prettyPrint e | (e, _) <- sfFeatures sf]
    assertBool "synthesizes more than one feature" (length names > 1)
    assertBool "ranked features are distinct" (length names == length (dedup names))
  where
    dedup = foldr (\x acc -> if x `elem` acc then acc else x : acc) []

-- | Same config and data give the same best expression.
deterministic :: Test
deterministic = TestCase $ do
    let a = fit defaultSynthesisConfig (D.col @Double "y") quad
        b = fit defaultSynthesisConfig (D.col @Double "y") quad
    assertEqual
        "same best expression"
        (D.prettyPrint (sfExpr a))
        (D.prettyPrint (sfExpr b))

tests :: [Test]
tests =
    [ recoversQuadratic
    , exactRecoveryMSE
    , recoversRatio
    , distinctFeatures
    , deterministic
    ]