packages feed

dataframe-3.8.0.0: tests/Typed/Parity.hs

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

module Typed.Parity (tests) where

import Data.Either (fromRight)
import qualified Data.Text as T
import System.Random (mkStdGen)

import qualified DataFrame as D
import DataFrame.Expression.Operators ((./=.))
import qualified DataFrame.Functions as F
import qualified DataFrame.Internal.Column as DI
import DataFrame.Internal.Expression (eqExpr)
import qualified DataFrame.Typed as DT
import qualified DataFrame.Typed.Functions as DTF
import qualified DataFrame.Typed.Statistics as TS

import Test.HUnit

type S =
    '[ '("x", Int)
     , '("y", Double)
     , '("g", T.Text)
     ]

baseDF :: D.DataFrame
baseDF =
    D.fromColumns
        [ ("x", DI.fromList [1, 2, 3, 4, 5, 6 :: Int])
        , ("y", DI.fromList [10.0, 25.0, 30.0, 5.0, 50.0, 12.0 :: Double])
        , ("g", DI.fromList ["a", "b", "a", "b", "a", "b" :: T.Text])
        ]

tdf :: DT.TypedDataFrame S
tdf = either (error . show) id (DT.freezeWithError baseDF)

-- Statistics reducers should match the untyped ones exactly (same code path).
statParity :: Test
statParity =
    TestList
        [ eq "mean" (TS.mean (DT.col @"y") tdf) (D.mean (F.double "y") baseDF)
        , eq "median" (TS.median (DT.col @"y") tdf) (D.median (F.double "y") baseDF)
        , eq
            "variance"
            (TS.variance (DT.col @"y") tdf)
            (D.variance (F.double "y") baseDF)
        , eq
            "stddev"
            (TS.standardDeviation (DT.col @"y") tdf)
            (D.standardDeviation (F.double "y") baseDF)
        , eq "sum" (TS.sum (DT.col @"x") tdf) (D.sum (F.int "x") baseDF)
        , eq
            "percentile"
            (TS.percentile 50 (DT.col @"y") tdf)
            (D.percentile 50 (F.double "y") baseDF)
        , eq
            "iqr"
            (TS.interQuartileRange (DT.col @"y") tdf)
            (D.interQuartileRange (F.double "y") baseDF)
        , eq
            "skewness"
            (TS.skewness (DT.col @"y") tdf)
            (D.skewness (F.double "y") baseDF)
        , eq
            "correlation"
            (TS.correlation @"x" @"y" tdf)
            (D.correlation "x" "y" baseDF)
        ]
  where
    eq :: (Eq a, Show a) => String -> a -> a -> Test
    eq lbl typed untyped = TestCase (assertEqual ("typed stat " ++ lbl) untyped typed)

-- Expression combinators ported in Expr.Extra: derive a column both ways.
exprParity :: Test
exprParity =
    TestList
        [ deriveEq "pow" (DTF.pow (DT.col @"x") 2) (F.pow (F.int "x") 2)
        , deriveEq "relu" (DTF.relu (DT.col @"x")) (F.relu (F.int "x"))
        , deriveEq "toMaybe" (DTF.toMaybe (DT.col @"x")) (F.toMaybe (F.int "x"))
        , deriveEq
            "min"
            (DTF.min (DT.col @"x") (DT.lit 3))
            (F.min (F.int "x") (F.lit @Int 3))
        , deriveEq
            "splitOn"
            (DTF.splitOn "a" (DT.col @"g"))
            (F.splitOn "a" (F.text "g"))
        , deriveEq
            "recodeWithDefault"
            (DTF.recodeWithDefault 0 [(1 :: Int, 100 :: Int)] (DT.col @"x"))
            (F.recodeWithDefault 0 [(1 :: Int, 100 :: Int)] (F.int "x"))
        , deriveEq
            "eq"
            (DTF.eq (DT.col @"x") (DT.lit 3))
            (F.eq (F.int "x") (F.lit @Int 3))
        , deriveEq
            "and"
            (DTF.and (DTF.lt (DT.col @"x") (DT.lit 5)) (DTF.geq (DT.col @"x") (DT.lit 2)))
            (F.and (F.lt x (F.lit @Int 5)) (F.geq x (F.lit @Int 2)))
        , deriveEq "contains" (DTF.contains "a" (DT.col @"g")) (F.contains "a" g)
        , deriveEq "length" (DTF.length (DT.col @"g")) (F.length g)
        , deriveEq
            "isNull"
            (DTF.isNull (DTF.toMaybe (DT.col @"x")))
            (F.isNull (F.toMaybe x))
        , deriveEq "cast" (DTF.cast @"x" @Int) (F.cast @Int "x")
        , deriveEq
            "castWithDefault"
            (DTF.castWithDefault @"g" (0 :: Int))
            (F.castWithDefault (0 :: Int) "g")
        ]
  where
    x = F.int "x"
    g = F.text "g"
    deriveEq ::
        (DI.Columnable a) =>
        String -> DT.TExpr S a -> D.Expr a -> Test
    deriveEq lbl te ue =
        TestCase
            ( assertEqual
                ("typed derive " ++ lbl)
                (D.derive "r" ue baseDF)
                (DT.thaw (DT.derive @"r" te tdf))
            )

-- Typed aggregations build the same expression as their untyped forms.
aggParity :: Test
aggParity =
    TestList
        [ same "countDistinct" (DTF.countDistinct (DT.col @"x")) (F.countDistinct x)
        , same "topK" (DTF.topK 2 (DT.col @"x")) (F.topK 2 x)
        , same "bottomK" (DTF.bottomK 2 (DT.col @"x")) (F.bottomK 2 x)
        , same "not" (DTF.not (DTF.eq (DT.col @"x") 1)) (F.not (F.eq x 1))
        ]
  where
    x = F.int "x"
    same :: (DI.Columnable a) => String -> DT.TExpr S a -> D.Expr a -> Test
    same lbl te ue = TestCase (assertBool lbl (eqExpr (DT.unTExpr te) ue))

-- apply / valueCounts / horizontal merge.
applyParity :: Test
applyParity =
    TestList
        [ TestCase
            ( assertEqual
                "apply type-change"
                (D.apply (show :: Int -> String) "x" baseDF)
                (DT.thaw (DT.apply @"x" (show :: Int -> String) tdf))
            )
        , TestCase
            ( assertEqual
                "applyMany type-preserving"
                (D.applyMany (+ (1 :: Int)) ["x"] baseDF)
                (DT.thaw (DT.applyMany @'["x"] (+ (1 :: Int)) tdf))
            )
        , TestCase
            ( assertEqual
                "valueCounts"
                (D.valueCounts (F.text "g") baseDF)
                (DT.valueCounts (DT.col @"g") tdf)
            )
        , TestCase
            ( assertEqual
                "horizontal merge (|||)"
                (leftDF D.||| rightDF)
                (DT.thaw ((DT.|||) leftT rightT))
            )
        ]

-- Sampling/splitting determinism: same seed => same rows as the untyped form.
samplingParity :: Test
samplingParity =
    TestList
        [ TestCase
            ( assertEqual
                "randomSplit"
                (D.randomSplit (mkStdGen 7) 0.5 baseDF)
                (let (a, b) = DT.randomSplit (mkStdGen 7) 0.5 tdf in (DT.thaw a, DT.thaw b))
            )
        , TestCase
            ( assertEqual
                "kFolds"
                (D.kFolds (mkStdGen 7) 3 baseDF)
                (map DT.thaw (DT.kFolds (mkStdGen 7) 3 tdf))
            )
        ]

-- Matrix / numeric-vector extraction.
accessParity :: Test
accessParity =
    TestList
        [ TestCase
            ( assertEqual
                "columnAsDoubleVector"
                (extract (D.columnAsDoubleVector (F.double "y") baseDF))
                (DT.columnAsDoubleVector @"y" tdf)
            )
        , TestCase
            ( assertEqual
                "toDoubleMatrix"
                (extract (D.toDoubleMatrix numericDF))
                (DT.toDoubleMatrix numericT)
            )
        ]
  where
    extract :: Either e a -> a
    extract = fromRight (error "extraction failed")

-- Disjoint frames for the (|||) test.
leftDF :: D.DataFrame
leftDF = D.fromColumns [("x", DI.fromList [1, 2 :: Int])]

rightDF :: D.DataFrame
rightDF = D.fromColumns [("y", DI.fromList [1.0, 2.0 :: Double])]

leftT :: DT.TypedDataFrame '[ '("x", Int)]
leftT = either (error . show) id (DT.freezeWithError leftDF)

rightT :: DT.TypedDataFrame '[ '("y", Double)]
rightT = either (error . show) id (DT.freezeWithError rightDF)

-- Numeric-only frame for the matrix test.
numericDF :: D.DataFrame
numericDF =
    D.fromColumns
        [ ("x", DI.fromList [1, 2, 3 :: Int])
        , ("y", DI.fromList [1.5, 2.5, 3.5 :: Double])
        ]

numericT :: DT.TypedDataFrame '[ '("x", Int), '("y", Double)]
numericT = either (error . show) id (DT.freezeWithError numericDF)

-- Ops added for import-swap parity: same result through either API.
newOpsParity :: Test
newOpsParity =
    TestList
        [ same
            "topBy"
            (D.topBy 1 2 [D.Desc (F.double "y")] baseDF)
            (DT.topBy 1 2 [DT.Desc (DT.col @"y")] tdf)
        , same
            "deriveMany"
            ( D.deriveMany
                [ "x2" D..= (F.int "x" * 2)
                , "y2" D..= (F.double "y" + 1)
                ]
                baseDF
            )
            ( DT.deriveMany
                (DT.as @"y2" (DT.col @"y" + 1) . DT.as @"x2" (DT.col @"x" * 2))
                tdf
            )
        , same
            "join INNER"
            (D.join D.INNER ["x"] leftDF numericDF)
            (DT.join @'DT.INNER @'["x"] leftT numericT)
        , same
            "join LEFT"
            (D.join D.LEFT ["x"] numericDF leftDF)
            (DT.join @'DT.LEFT @'["x"] numericT leftT)
        , same
            "applyInt"
            (D.applyInt (* 10) "x" baseDF)
            (DT.applyInt @"x" (* 10) tdf)
        , same
            "applyDouble"
            (D.applyDouble (> 20) "y" baseDF)
            (DT.applyDouble @"y" (> 20) tdf)
        , same
            "fold"
            (D.fold (\n -> D.filter (F.int "x" ./=. F.lit n)) [2, 4] baseDF)
            (DT.fold (\n -> DT.filter (DT.col @"x" DT../=. DT.lit n)) [2, 4] tdf)
        , TestCase (assertBool "empty is null" (DT.null DT.empty))
        , TestCase (assertEqual "null" (D.null baseDF) (DT.null tdf))
        , TestCase (assertEqual "toCsv" (D.toCsv baseDF) (DT.toCsv tdf))
        , TestCase
            (assertEqual "toMarkdown" (D.toMarkdown baseDF) (DT.toMarkdown tdf))
        , TestCase
            ( assertEqual
                "frame stats via DataFrame.Typed"
                (D.mean (F.double "y") baseDF)
                (DT.mean (DT.col @"y") tdf)
            )
        , TestCase
            ( assertEqual
                "readJSONEither"
                (D.toCsv <$> D.readJSONEither jsonBytes)
                (DT.toCsv <$> DT.readJSONEither @'[ '("a", Maybe Double)] jsonBytes)
            )
        ]
  where
    same :: String -> D.DataFrame -> DT.TypedDataFrame cols -> Test
    same lbl untyped typed = TestCase (assertEqual lbl untyped (DT.thaw typed))
    jsonBytes = "[{\"a\": 1}, {\"a\": 2}]"

tests :: [Test]
tests =
    [ newOpsParity
    , statParity
    , exprParity
    , aggParity
    , applyParity
    , samplingParity
    , accessParity
    ]