packages feed

dataframe-3.7.0.0: tests/Operations/RowWise.hs

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

-- | An expression means what it computes for one row at a time.
module Operations.RowWise (tests) where

import Control.Exception (throw)
import qualified Data.Text as T
import qualified Data.Vector as V

import qualified DataFrame as D
import DataFrame.Expression.Operators ((./=.), (.<=.), (.>.))
import qualified DataFrame.Functions as F
import DataFrame.Internal.Column (Columnable, TypedColumn (..), toVector)
import DataFrame.Internal.Expression (Expr (..))
import DataFrame.Internal.Interpreter (interpret)

import Test.HUnit

-- | The second size is above the 200k threshold where loops run in parallel.
sizes :: [Int]
sizes = [60, 200003]

xs, ys :: Int -> [Int]
xs n = take n (cycle [7, -3, 0, 12, 5, -8, 1])
ys n = take n (cycle [2, 0, -1, 0, 3])

ds :: Int -> [Double]
ds n = take n (cycle [0.5, -1.5, 2.25, 100, -0.0, 3])

names :: Int -> [T.Text]
names n = take n (cycle ["Eco", "Business", "", "Eco Plus"])

maybes :: Int -> [Maybe Int]
maybes n = take n (cycle [Just 1, Nothing, Just 3, Nothing, Just 5, Just 6])

frame :: Int -> D.DataFrame
frame n =
    D.fromColumns
        [ "x" D.=: xs n
        , "y" D.=: ys n
        , "d" D.=: ds n
        , "name" D.=: names n
        , "m" D.=: maybes n
        , "flag" D.=: map (fmap (> 2)) (maybes n)
        ]

eval :: forall a. (Columnable a) => Int -> Expr a -> Either String [a]
eval n e = case interpret @a (frame n) e of
    Left err -> Left (show err)
    Right (TColumn col) -> Right (V.toList (either throw id (toVector @a @V.Vector col)))

x, y :: Expr Int
x = F.col "x"
y = F.col "y"

d :: Expr Double
d = F.col "d"

-- | The expression's rows equal the reference rows, at every size.
rows :: (Columnable a) => String -> Expr a -> (Int -> [a]) -> [Test]
rows label e expected =
    [ TestCase
        (assertEqual (label ++ " n=" ++ show n) (Right (expected n)) (eval n e))
    | n <- sizes
    ]

tests :: [Test]
tests =
    concat
        [ rows
            "a guard protects its branch"
            (If (y ./=. F.lit 0) (F.div x y) (F.lit 0))
            (\n -> [if b /= 0 then a `div` b else 0 | (a, b) <- zip (xs n) (ys n)])
        , rows
            "a guarded branch may repeat a failing operation"
            (If (y ./=. F.lit 0) (F.div x y + F.div x y) (F.lit 0))
            (\n -> [if b /= 0 then 2 * (a `div` b) else 0 | (a, b) <- zip (xs n) (ys n)])
        , rows
            "a null in the branch not chosen is ignored"
            (If (x .>. F.lit 0) (F.col @(Maybe Int) "m") (F.lit (Just 0)))
            (\n -> [if a > 0 then m else Just 0 | (a, m) <- zip (xs n) (maybes n)])
        , rows
            "nested conditions"
            ( If
                (x .>. F.lit 0)
                (If (y .>. F.lit 0) (x + y) (x - y))
                (If (y .>. F.lit 1) (x * y) x)
            )
            ( \n ->
                [ if a > 0 then (if b > 0 then a + b else a - b) else (if b > 1 then a * b else a)
                | (a, b) <- zip (xs n) (ys n)
                ]
            )
        , rows
            "an aggregate inside a branch sees every row"
            (If (d .<=. F.lit 1) (d - F.mean d) (F.lit 0))
            ( \n ->
                let mean = sum (ds n) / fromIntegral n
                 in [if v <= 1 then v - mean else 0 | v <- ds n]
            )
        , rows
            "text branches"
            (If (x .>. F.lit 0) (F.lift T.toUpper (F.col @T.Text "name")) (F.lit "none"))
            (\n -> [if a > 0 then T.toUpper t else "none" | (a, t) <- zip (xs n) (names n)])
        , rows
            "nullable branches"
            ( If
                (x .>. F.lit 0)
                (F.lift (fmap (+ 1)) (F.col @(Maybe Int) "m"))
                (F.col @(Maybe Int) "m")
            )
            (\n -> [if a > 0 then fmap (+ 1) m else m | (a, m) <- zip (xs n) (maybes n)])
        , rows
            "a repeated subexpression"
            ((x + y) * (x + y) - (x + y))
            (\n -> [(a + b) * (a + b) - (a + b) | (a, b) <- zip (xs n) (ys n)])
        , rows
            "a subexpression repeated across branches"
            (If (y .>. F.lit 0) ((x + y) * F.lit 2) ((x + y) * F.lit 3))
            (\n -> [if b > 0 then (a + b) * 2 else (a + b) * 3 | (a, b) <- zip (xs n) (ys n)])
        , rows
            "different functions under one name are not merged"
            (F.lift (+ 1) x + F.lift (* 2) x)
            (\n -> [(a + 1) + (a * 2) | a <- xs n])
        ,
            [ TestCase
                ( assertBool
                    "a missing column is reported even in a branch never chosen"
                    ( either
                        (const True)
                        (const False)
                        (eval 60 (If (F.lit True) x (F.col @Int "absent")))
                    )
                )
            , TestCase
                ( assertBool
                    "a null condition is an error"
                    ( either
                        (const True)
                        (const False)
                        (eval 60 (If (F.col @Bool "flag") x y))
                    )
                )
            ]
        ]