dataframe-core-2.6.0.0: src-internal/DataFrame/Internal/Interpreter.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module DataFrame.Internal.Interpreter (
-- * New core API
Value (..),
Ctx (..),
eval,
materialize,
-- * Backward-compatible API
interpret,
interpretAggregation,
AggregationResult (..),
) where
import Data.Bifunctor (first)
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Data.Text as T
import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))
import qualified Data.Vector as V
import qualified Data.Vector.Generic as VG
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM
import DataFrame.Errors
import DataFrame.Internal.Aggregation.Kernel.Distinct (
countDistinctColumn,
countDistinctGroups,
)
import DataFrame.Internal.Column
import DataFrame.Internal.DataFrame
import DataFrame.Internal.Expression
import qualified DataFrame.Internal.Grouping as G
import qualified DataFrame.Internal.Interpreter.Kernels as K
import Type.Reflection (
SomeTypeRep (..),
Typeable,
typeRep,
)
import Data.Int (Int16, Int32, Int64, Int8)
import Data.Word (Word64)
import GHC.Float (castDoubleToWord64)
-- Specializations for common aggregation types to avoid dictionary overhead.
{-# SPECIALIZE foldLinearGroups ::
((Double, Int) -> Double -> (Double, Int)) ->
(Double, Int) ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
((Double, Int) -> Float -> (Double, Int)) ->
(Double, Int) ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
((Double, Int) -> Int -> (Double, Int)) ->
(Double, Int) ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
((Double, Int) -> Int8 -> (Double, Int)) ->
(Double, Int) ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
((Double, Int) -> Int16 -> (Double, Int)) ->
(Double, Int) ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
((Double, Int) -> Int32 -> (Double, Int)) ->
(Double, Int) ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
((Double, Int) -> Int64 -> (Double, Int)) ->
(Double, Int) ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
-- foldLinearGroups: count accumulator
{-# SPECIALIZE foldLinearGroups ::
(Int -> Double -> Int) ->
Int ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
(Int -> Float -> Int) ->
Int ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
(Int -> Int -> Int) ->
Int ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
(Int -> Int8 -> Int) ->
Int ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
(Int -> Int16 -> Int) ->
Int ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
(Int -> Int32 -> Int) ->
Int ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
(Int -> Int64 -> Int) ->
Int ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
-- foldLinearGroups: sum/min/max (acc == elem)
{-# SPECIALIZE foldLinearGroups ::
(Double -> Double -> Double) ->
Double ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
(Float -> Float -> Float) ->
Float ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
(Int8 -> Int8 -> Int8) ->
Int8 ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
(Int16 -> Int16 -> Int16) ->
Int16 ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
(Int32 -> Int32 -> Int32) ->
Int32 ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
{-# SPECIALIZE foldLinearGroups ::
(Int64 -> Int64 -> Int64) ->
Int64 ->
Column ->
VU.Vector Int ->
Int ->
Either DataFrameException Column
#-}
-- mapColumn: finalize
{-# SPECIALIZE mapColumn ::
((Double, Int) -> Double) -> Column -> Either DataFrameException Column
#-}
{-# SPECIALIZE mapColumn ::
(Double -> Double) -> Column -> Either DataFrameException Column
#-}
{-# SPECIALIZE mapColumn ::
(Float -> Float) -> Column -> Either DataFrameException Column
#-}
{-# SPECIALIZE mapColumn ::
(Int -> Int) -> Column -> Either DataFrameException Column
#-}
-- toDouble on an Int column (hot path for derived arithmetic)
{-# SPECIALIZE mapColumn ::
(Int -> Double) -> Column -> Either DataFrameException Column
#-}
-- Bool-mapping unary ops (e.g. 'not')
{-# SPECIALIZE mapColumn ::
(Bool -> Bool) -> Column -> Either DataFrameException Column
#-}
-- | The result of interpreting an expression; literals stay scalar.
data Value a where
-- | A single value, not yet broadcast to any length.
Scalar ::
(Columnable a) =>
!a ->
-- | A flat column, one element per row (or per group once aggregated).
Value a
Flat ::
(Columnable a) =>
!Column ->
-- | A grouped column: one 'Column' slice per group.
Value a
Group :: (Columnable a) => !(V.Vector Column) -> Value a
instance (Show a) => Show (Value a) where
show (Scalar v) = show v
show (Flat v) = show v
show (Group v) = show v
-- | The interpretation context.
data Ctx
= FlatCtx DataFrame
| GroupCtx GroupedDataFrame
-- | Force a 'Value' into a flat 'Column' of the given length.
materialize :: forall a. (Columnable a) => Int -> Value a -> Column
materialize n (Scalar v) = broadcastScalar @a n v
materialize _ (Flat c) = c
materialize _ (Group _) =
error "materialize: cannot flatten a grouped value to a single column"
-- | Replicate a scalar to a column of length @n@ in the cheapest form.
broadcastScalar :: forall a. (Columnable a) => Int -> a -> Column
broadcastScalar n v = case sUnbox @a of
STrue -> fromUnboxedVector (VU.replicate n v)
SFalse -> fromVector (V.replicate n v)
-- | Apply a pure function to a 'Value'.
liftValue ::
(Columnable b, Columnable a) =>
(b -> a) -> Value b -> Either DataFrameException (Value a)
liftValue f (Scalar v) = Right (Scalar (f v))
liftValue f (Flat col) = Flat <$> mapColumn f col
liftValue f (Group gs) = Group <$> V.mapM (mapColumn f) gs
{-# INLINEABLE liftValue #-}
-- | Apply a binary function to two 'Value's, degenerating when one is scalar.
liftValue2 ::
(Columnable c, Columnable b, Columnable a) =>
(c -> b -> a) ->
Value c ->
Value b ->
Either DataFrameException (Value a)
liftValue2 f (Scalar l) (Scalar r) = Right (Scalar (f l r))
liftValue2 f (Scalar l) v = liftValue (f l) v
liftValue2 f v (Scalar r) = liftValue (`f` r) v
liftValue2 f (Flat l) (Flat r) = Flat <$> zipWithColumns f l r
liftValue2 f (Group ls) (Group rs)
| V.length ls == V.length rs =
Group <$> V.zipWithM (zipWithColumns f) ls rs
liftValue2 _ (Flat _) (Group _) =
Left $ AggregatedAndNonAggregatedException "aggregated" "non-aggregated"
liftValue2 _ (Group _) (Flat _) =
Left $ AggregatedAndNonAggregatedException "non-aggregated" "aggregated"
liftValue2 _ (Group _) (Group _) =
Left $ InternalException "Group count mismatch in binary operation"
{-# INLINEABLE liftValue2 #-}
-- | If-then-else over values already computed for every row.
branchValue ::
forall a.
(Columnable a) =>
Value Bool ->
Value a ->
Value a ->
Either DataFrameException (Value a)
branchValue (Scalar True) l _ = Right l
branchValue (Scalar False) _ r = Right r
branchValue (Flat cc) l r = do
lc <- alongside cc l
rc <- alongside cc r
Flat <$> chooseRows cc lc rc
branchValue (Group cgs) l r =
Group
<$> V.imapM
( \i cc -> do
lc <- alongside cc =<< groupAt i l
rc <- alongside cc =<< groupAt i r
chooseRows cc lc rc
)
cgs
shapeMismatch :: DataFrameException
shapeMismatch =
AggregatedAndNonAggregatedException "if-then-else branches" "mismatched shapes"
-- | A branch as a column with one value per row of the condition column.
alongside ::
forall a.
(Columnable a) => Column -> Value a -> Either DataFrameException Column
alongside cc (Scalar x) = Right (broadcastScalar (columnLength cc) x)
alongside _ (Flat c) = Right c
alongside _ (Group _) = Left shapeMismatch
-- | The part of a branch that belongs to group @i@.
groupAt :: Int -> Value a -> Either DataFrameException (Value a)
groupAt _ v@(Scalar _) = Right v
groupAt i (Group gs) | i < V.length gs = Right (Flat (gs V.! i))
groupAt _ _ = Left shapeMismatch
chooseRows :: Column -> Column -> Column -> Either DataFrameException Column
chooseRows cc lc rc = do
cs <- conditionVector cc
let n = VU.length cs
both <- mappendColumns lc rc
pure (atIndicesStable (VU.imap (\i c -> if c then i else n + i) cs) both)
-- | A condition with nulls is an error.
conditionVector :: Column -> Either DataFrameException (VU.Vector Bool)
conditionVector (UnboxedColumn bm (v :: VU.Vector x))
| Just Refl <- testEquality (typeRep @x) (typeRep @Bool)
, maybe True (not . bitmapHasNulls (VU.length v)) bm =
Right v
conditionVector c = toVector @Bool @VU.Vector c
-- | A value as a kernel operand: a literal or a null-free unboxed column.
operand :: forall a. (Columnable a) => Value a -> Maybe (K.Operand a)
operand (Scalar s) = Just (K.Literal s)
operand (Flat (UnboxedColumn Nothing (v :: VU.Vector x)))
| Just Refl <- testEquality (typeRep @x) (typeRep @a) = Just (K.Column v)
operand _ = Nothing
fromOperand :: (Columnable a, VU.Unbox a) => K.Operand a -> Value a
fromOperand (K.Literal x) = Scalar x
fromOperand (K.Column v) = Flat (UnboxedColumn Nothing v)
-- | A column loop for a built-in binary operator, if it can run.
builtinBinary ::
forall c b a.
(Columnable c, Columnable b, Columnable a) =>
T.Text -> Maybe (Value c -> Value b -> Maybe (Value a))
builtinBinary name = do
Refl <- testEquality (typeRep @c) (typeRep @b)
case (comparison, arithmetic, integral, textEquality, textContains) of
(Just run, _, _, _, _) -> Just run
(_, Just run, _, _, _) -> Just run
(_, _, Just run, _, _) -> Just run
(_, _, _, Just run, _) -> Just run
(_, _, _, _, found) -> found
where
-- Int-only integer division and remainder.
integral :: (c ~ b) => Maybe (Value c -> Value c -> Maybe (Value a))
integral = do
Refl <- testEquality (typeRep @a) (typeRep @Int)
Refl <- testEquality (typeRep @c) (typeRep @Int)
op <- case name of
"div" -> Just div
"mod" -> Just mod
_ -> Nothing
-- Only non-zero literal divisors: lazy evaluation might skip a zero.
Just $ \l r -> case r of
Scalar k | k /= 0 -> fromOperand <$> (K.zipOperands op <$> operand l <*> operand r)
_ -> Nothing
textContains :: (c ~ b) => Maybe (Value c -> Value c -> Maybe (Value a))
textContains = do
Refl <- testEquality (typeRep @c) (typeRep @T.Text)
Refl <- testEquality (typeRep @a) (typeRep @Bool)
case name of
"contains" -> Just packedContains
_ -> Nothing
comparison :: (c ~ b) => Maybe (Value c -> Value c -> Maybe (Value a))
comparison = do
Refl <- testEquality (typeRep @a) (typeRep @Bool)
op <- K.cmpOpByName name
run <- K.compareOperands @c
Just (\l r -> fromOperand <$> (run op <$> operand l <*> operand r))
arithmetic :: (c ~ b) => Maybe (Value c -> Value c -> Maybe (Value a))
arithmetic = do
Refl <- testEquality (typeRep @a) (typeRep @c)
op <- K.arithOpByName name
run <- K.arithOperands @c
case sUnbox @c of
STrue -> Just (\l r -> fromOperand <$> (run op <$> operand l <*> operand r))
SFalse -> Nothing
textEquality :: (c ~ b) => Maybe (Value c -> Value c -> Maybe (Value a))
textEquality = do
Refl <- testEquality (typeRep @c) (typeRep @T.Text)
Refl <- testEquality (typeRep @a) (typeRep @Bool)
wantEq <- case name of
"eq" -> Just True
"neq" -> Just False
_ -> Nothing
Just (packedEquals wantEq)
-- | Column on the left, needle on the right; not used if needle has U+FFFD.
packedContains :: Value T.Text -> Value T.Text -> Maybe (Value Bool)
packedContains (Flat (PackedText Nothing p)) (Scalar t)
| not (T.any (== '\xFFFD') t) =
Just (Flat (UnboxedColumn Nothing (K.packedContainsLit p t)))
packedContains _ _ = Nothing
-- | Not used when the literal contains U+FFFD (see 'K.packedEqLit').
packedEquals :: Bool -> Value T.Text -> Value T.Text -> Maybe (Value Bool)
packedEquals wantEq l r = do
(p, t) <- case (l, r) of
(Flat (PackedText Nothing p), Scalar t) -> Just (p, t)
(Scalar t, Flat (PackedText Nothing p)) -> Just (p, t)
_ -> Nothing
if T.any (== '\xFFFD') t
then Nothing
else Just (Flat (UnboxedColumn Nothing (K.packedEqLit wantEq p t)))
-- | As 'builtinBinary', for unary operators.
builtinUnary ::
forall b a.
(Columnable b, Columnable a) => T.Text -> Maybe (Value b -> Maybe (Value a))
builtinUnary "toDouble"
| Just Refl <- testEquality (typeRep @b) (typeRep @Int)
, Just Refl <- testEquality (typeRep @a) (typeRep @Double) =
Just $ \v -> case operand v of
Just (K.Column x) -> Just (Flat (UnboxedColumn Nothing (K.intToDouble x)))
_ -> Nothing
builtinUnary "textLength"
| Just Refl <- testEquality (typeRep @b) (typeRep @T.Text)
, Just Refl <- testEquality (typeRep @a) (typeRep @Int) =
Just $ \case
Flat (PackedText Nothing p) -> Just (Flat (UnboxedColumn Nothing (K.packedCharCount p)))
_ -> Nothing
builtinUnary _ = Nothing
applyUnary ::
forall op b a.
(UnaryOp op, Columnable b, Columnable a) =>
op b a -> Value b -> Either DataFrameException (Value a)
applyUnary op v = case builtinUnary @b @a (unaryName op) >>= ($ v) of
Just out -> Right out
Nothing -> liftValue (fastUnaryFn @b @a (unaryName op) (unaryFn op)) v
applyBinary ::
forall op c b a.
(BinaryOp op, Columnable c, Columnable b, Columnable a) =>
op c b a -> Value c -> Value b -> Either DataFrameException (Value a)
applyBinary op l r = case builtinBinary @c @b @a (binaryName op) >>= (\run -> run l r) of
Just out -> Right out
Nothing -> liftValue2 (binaryFn op) l r
-- | The rows of the frame an evaluation covers.
data Rows = AllRows | Rows !(VU.Vector Int)
data Env = Env {envFrame :: DataFrame, envRows :: Rows}
frameRows :: Env -> Int
frameRows env = fst (dataframeDimensions (envFrame env))
rowCount :: Env -> Int
rowCount env = case envRows env of
AllRows -> frameRows env
Rows sel -> VU.length sel
-- | Keep only the covered rows of a value computed for the whole frame.
restrict :: Rows -> Value a -> Value a
restrict (Rows sel) (Flat col) = Flat (atIndicesStable sel col)
restrict _ v = v
-- | The rows at these positions of the rows already covered.
narrow :: Env -> VU.Vector Int -> Env
narrow env positions = env{envRows = Rows rows}
where
rows = case envRows env of
AllRows -> positions
Rows sel -> VU.unsafeBackpermute sel positions
-- | Identifies an expression built only from columns, literals and built-ins.
data Key
= KeyColumn T.Text SomeTypeRep
| KeyLiteral Literal
| KeyUnary T.Text SomeTypeRep Key
| KeyBinary T.Text SomeTypeRep Key Key
deriving (Eq, Ord)
data Literal
= LitDouble Word64
| LitInt Int
| LitBool Bool
| LitText T.Text
deriving (Eq, Ord)
-- | Keys are only built for expressions up to this many nodes.
maxKeyNodes :: Int
maxKeyNodes = 32
keyOf :: (Columnable a) => Expr a -> Maybe Key
keyOf expr = fst <$> go maxKeyNodes expr
where
-- Returns the key and the node budget left.
go :: forall x. (Columnable x) => Int -> Expr x -> Maybe (Key, Int)
go budget _ | budget <= 0 = Nothing
go budget e = case e of
Col name -> Just (KeyColumn name (SomeTypeRep (typeRep @x)), budget - 1)
Lit v -> (\l -> (KeyLiteral l, budget - 1)) <$> literalKey v
Unary op (inner :: Expr b) -> do
_ <- builtinUnary @b @x (unaryName op)
(k, left) <- go (budget - 1) inner
Just (KeyUnary (unaryName op) (SomeTypeRep (typeRep @b)) k, left)
Binary op (l :: Expr c) (r :: Expr b) -> do
_ <- builtinBinary @c @b @x (binaryName op)
-- A division is shareable only with a non-zero literal divisor.
if binaryName op `elem` ["div", "mod"]
then case r of
Lit (k :: b)
| Just Refl <- testEquality (typeRep @b) (typeRep @Int)
, k /= 0 ->
Just ()
_ -> Nothing
else Just ()
(kl, afterLeft) <- go (budget - 1) l
(kr, left) <- go afterLeft r
Just (KeyBinary (binaryName op) (SomeTypeRep (typeRep @c)) kl kr, left)
_ -> Nothing
literalKey :: forall a. (Columnable a) => a -> Maybe Literal
literalKey v
| Just Refl <- testEquality (typeRep @a) (typeRep @Double) =
Just (LitDouble (castDoubleToWord64 v))
| Just Refl <- testEquality (typeRep @a) (typeRep @Int) = Just (LitInt v)
| Just Refl <- testEquality (typeRep @a) (typeRep @Bool) = Just (LitBool v)
| Just Refl <- testEquality (typeRep @a) (typeRep @T.Text) = Just (LitText v)
| otherwise = Nothing
-- | The keys of operator nodes that occur more than once in an expression.
repeatedKeys :: (Columnable a) => Expr a -> S.Set Key
repeatedKeys expr = M.keysSet (M.filter (> 1) (count expr M.empty))
where
count :: forall x. (Columnable x) => Expr x -> M.Map Key Int -> M.Map Key Int
count e seen = case e of
Unary _ inner -> count inner (note e seen)
Binary _ l r -> count r (count l (note e seen))
If c l r -> count r (count l (count c seen))
_ -> seen
note :: forall x. (Columnable x) => Expr x -> M.Map Key Int -> M.Map Key Int
note e seen = maybe seen (\k -> M.insertWith (+) k (1 :: Int) seen) (keyOf e)
data Sharing = Sharing
{ repeated :: !(S.Set Key)
, rowsAsked :: !(M.Map Key Int)
-- ^ How many rows each repeated key has been computed for so far.
, computed :: !(M.Map Key Column)
-- ^ Results kept for the whole frame.
, bytesKept :: !Int
}
-- | Results kept for reuse during one evaluation may total this many bytes.
maxBytesKept :: Int
maxBytesKept = 256 * 1024 * 1024
newtype Eval a = Eval
{runEval :: Sharing -> Either DataFrameException (a, Sharing)}
instance Functor Eval where
fmap f (Eval m) = Eval (fmap (first f) . m)
instance Applicative Eval where
pure x = Eval (\s -> Right (x, s))
Eval mf <*> Eval mx = Eval $ \s -> do
(f, s') <- mf s
(x, s'') <- mx s'
Right (f x, s'')
instance Monad Eval where
Eval m >>= k = Eval $ \s -> do
(x, s') <- m s
runEval (k x) s'
failing :: Either DataFrameException a -> Eval a
failing r = Eval (\s -> fmap (,s) r)
inContext :: (Show e) => Expr e -> Eval a -> Eval a
inContext expr (Eval m) = Eval (addContext expr . m)
evalFlat ::
(Columnable a) => DataFrame -> Expr a -> Either DataFrameException (Value a)
evalFlat df expr =
fst
<$> runEval
(evalRows (Env df AllRows) expr)
(Sharing (repeatedKeys expr) M.empty M.empty 0)
evalRows :: forall a. (Columnable a) => Env -> Expr a -> Eval (Value a)
evalRows env expr = case expr of
Lit v -> pure (Scalar v)
Unary{} -> evalShared env expr
Binary{} -> evalShared env expr
If cond l r -> inContext expr $ do
c <- evalRows env cond
case c of
Scalar holds -> do
-- The other branch runs on no rows; it can only report columns.
let (chosen, other) = if holds then (l, r) else (r, l)
_ <- evalRows (narrow env VU.empty) other
evalRows env chosen
Flat cc -> do
cs <- failing (conditionVector cc)
let (yes, no) = K.partitionRows cs
lv <- evalArm env yes l
rv <- evalArm env no r
failing (mergeArms cs yes no lv rv)
Group _ -> failing (Left (InternalException "grouped condition in a flat frame"))
-- Aggregations and windows need every row; then keep the covered ones.
_ -> failing (restrict (envRows env) <$> eval (FlatCtx (envFrame env)) expr)
-- | Evaluate an operator node, reusing a result kept for the whole frame.
evalShared :: forall a. (Columnable a) => Env -> Expr a -> Eval (Value a)
evalShared env expr = Eval $ \s -> case keyOf expr of
Just key
| S.member key (repeated s) -> case M.lookup key (computed s) of
Just col -> Right (restrict (envRows env) (Flat col), s)
Nothing -> runEval (askFor key) s
_ -> runEval (evalNode env expr) s
where
total = frameRows env
askFor key = Eval $ \s ->
let asked = M.findWithDefault 0 key (rowsAsked s) + rowCount env
in if asked >= total && bytesKept s + 8 * total <= maxBytesKept
then do
(full, s') <- runEval (evalNode env{envRows = AllRows} expr) s
Right (restrict (envRows env) full, keep key full s')
else runEval (evalNode env expr) s{rowsAsked = M.insert key asked (rowsAsked s)}
keep :: Key -> Value a -> Sharing -> Sharing
keep key (Flat col) s =
s
{ computed = M.insert key col (computed s)
, bytesKept = bytesKept s + columnBytes col
}
keep _ _ s = s
evalNode :: forall a. (Columnable a) => Env -> Expr a -> Eval (Value a)
evalNode env expr = inContext expr $ case expr of
Unary op inner -> evalRows env inner >>= failing . applyUnary op
Binary op l r -> do
lv <- evalRows env l
rv <- evalRows env r
failing (applyBinary op lv rv)
_ -> evalRows env expr
columnBytes :: Column -> Int
columnBytes (UnboxedColumn _ (v :: VU.Vector x))
| Just Refl <- testEquality (typeRep @x) (typeRep @Bool) = VU.length v
| otherwise = 8 * VU.length v
columnBytes c = 16 * columnLength c
-- | A branch computed for every covered row, or only for the rows choosing it.
data Arm a = ForEveryRow (Value a) | ForChosenRows (Value a)
-- | A column or literal is already there for every row; else compute lazily.
evalArm :: (Columnable a) => Env -> VU.Vector Int -> Expr a -> Eval (Arm a)
evalArm env chosen expr = case expr of
Col _ -> ForEveryRow <$> evalRows env expr
Lit _ -> ForEveryRow <$> evalRows env expr
_ -> ForChosenRows <$> evalRows (narrow env chosen) expr
-- | Each row's value from the branch its condition chooses.
mergeArms ::
forall a.
(Columnable a) =>
VU.Vector Bool ->
VU.Vector Int ->
VU.Vector Int ->
Arm a ->
Arm a ->
Either DataFrameException (Value a)
mergeArms cs yes no l r = case (K.mergeBranches @a, branch l, branch r, sUnbox @a) of
(Just run, Just lb, Just rb, STrue) -> Right (Flat (UnboxedColumn Nothing (run cs lb rb)))
_
| VU.null no -> Right (Flat lc)
| VU.null yes -> Right (Flat rc)
| otherwise -> do
both <- mappendColumns lc rc
-- Row i reads the next unused value of its branch.
let place (y, f) c = if c then (y + 1, f) else (y, f + 1)
taken = VU.prescanl' place (0, VU.length yes) cs
source = VU.zipWith (\c (y, f) -> if c then y else f) cs taken
Right (Flat (atIndicesStable source both))
where
branch :: Arm a -> Maybe (K.Branch a)
branch arm = case arm of
ForEveryRow v ->
(\case K.Literal x -> K.Constant x; K.Column xs -> K.EveryRow xs)
<$> operand v
ForChosenRows v ->
(\case K.Literal x -> K.Constant x; K.Column xs -> K.ChosenRows xs)
<$> operand v
-- The generic path wants each branch as one value per row choosing it.
chosenColumn :: VU.Vector Int -> Arm a -> Column
chosenColumn rows arm = case arm of
ForEveryRow v -> materialize @a (VU.length rows) (restrict (Rows rows) v)
ForChosenRows v -> materialize @a (VU.length rows) v
lc = chosenColumn yes l
rc = chosenColumn no r
-- | Annotate any 'TypeMismatchException' with the expression being evaluated.
addContext ::
(Show a) => Expr a -> Either DataFrameException b -> Either DataFrameException b
addContext expr = first (enrichError (show expr))
enrichError :: String -> DataFrameException -> DataFrameException
enrichError loc (TypeMismatchException ctx) =
TypeMismatchException
ctx
{ callingFunctionName =
callingFunctionName ctx <|+> Just "eval"
, errorColumnName =
errorColumnName ctx <|+> Just loc
}
where
Nothing <|+> b = b
a <|+> _ = a
enrichError _ e = e
-- | Given a flat column and grouping metadata, produce one 'Column' per group.
sliceGroups :: Column -> VU.Vector Int -> VU.Vector Int -> V.Vector Column
sliceGroups col os indices = case col of
PackedText _ _ -> sliceGroups (materializePacked col) os indices
MergedColumn _ _ -> sliceGroups (materializeMerged col) os indices
BoxedColumn bm vec ->
let !sorted =
V.generate
(VU.length indices)
((vec `V.unsafeIndex`) . (indices `VU.unsafeIndex`))
!sortedBm = permuteBitmap bm
in V.generate nGroups $ \i ->
BoxedColumn
(fmap (bitmapSlice (start i) (len i)) sortedBm)
(V.unsafeSlice (start i) (len i) sorted)
UnboxedColumn bm vec ->
let !sorted = VU.unsafeBackpermute vec indices
!sortedBm = permuteBitmap bm
in V.generate nGroups $ \i ->
UnboxedColumn
(fmap (bitmapSlice (start i) (len i)) sortedBm)
(VU.unsafeSlice (start i) (len i) sorted)
where
!nGroups = VU.length os - 1
start i = os `VU.unsafeIndex` i
len i = os `VU.unsafeIndex` (i + 1) - start i
permuteBitmap = fmap $ \bm ->
buildBitmapFromValid $
VU.map
(\r -> if bitmapTestBit bm r then 1 else 0)
indices
{-# INLINE sliceGroups #-}
numGroups :: GroupedDataFrame -> Int
numGroups gdf = VU.length (offsets gdf) - 1
-- | Build the inverse of a permutation vector.
invertPermutation :: VU.Vector Int -> VU.Vector Int
invertPermutation perm = VU.create $ do
let !n = VU.length perm
inv <- VUM.new n
VU.imapM_ (flip (VUM.unsafeWrite inv)) perm
return inv
{-# INLINE invertPermutation #-}
-- | Coerce a column to type @a@, then apply @onResult@ to each element.
promoteColumnWith ::
forall a b.
(Columnable a, Columnable b, Read a) =>
(Either String a -> b) -> Column -> Either DataFrameException Column
promoteColumnWith onResult col
| hasElemType @b col = Right col
| hasElemType @a col = mapColumn @a (onResult . Right) col
| Just result <- tryMaybeWrap @a @b onResult col = result
| otherwise =
case testEquality (typeRep @a) (typeRep @Double) of
Just Refl -> promoteToDoubleWith onResult col
Nothing ->
case testEquality (typeRep @a) (typeRep @Float) of
Just Refl -> promoteToFloatWith onResult col
Nothing ->
case testEquality (typeRep @a) (typeRep @Int) of
Just Refl -> promoteToIntWith onResult col
Nothing -> tryParseWith @a onResult col
promoteToDoubleWith ::
forall b.
(Columnable b) =>
(Either String Double -> b) -> Column -> Either DataFrameException Column
promoteToDoubleWith onResult col = case col of
UnboxedColumn Nothing (v :: VU.Vector c) ->
case sFloating @c of
STrue ->
Right $
fromVector @b
(V.map (onResult . Right . (realToFrac :: c -> Double)) (VG.convert v))
SFalse -> case sIntegral @c of
STrue ->
Right $
fromVector @b
(V.map (onResult . Right . (fromIntegral :: c -> Double)) (VG.convert v))
SFalse -> castMismatch @c @b
UnboxedColumn (Just bm) (v :: VU.Vector c) ->
case sFloating @c of
STrue ->
Right $
fromVector @b
( V.generate (VU.length v) $ \i ->
if bitmapTestBit bm i
then onResult (Right (realToFrac (VU.unsafeIndex v i) :: Double))
else onResult (Left "null")
)
SFalse -> case sIntegral @c of
STrue ->
Right $
fromVector @b
( V.generate (VU.length v) $ \i ->
if bitmapTestBit bm i
then onResult (Right (fromIntegral (VU.unsafeIndex v i) :: Double))
else onResult (Left "null")
)
SFalse -> castMismatch @c @b
BoxedColumn _ _ -> tryParseWith @Double onResult col
PackedText _ _ -> promoteToDoubleWith onResult (materializePacked col)
MergedColumn _ _ -> promoteToDoubleWith onResult (materializeMerged col)
promoteToFloatWith ::
forall b.
(Columnable b) =>
(Either String Float -> b) -> Column -> Either DataFrameException Column
promoteToFloatWith onResult col = case col of
UnboxedColumn Nothing (v :: VU.Vector c) ->
case sFloating @c of
STrue ->
Right $
fromVector @b
(V.map (onResult . Right . (realToFrac :: c -> Float)) (VG.convert v))
SFalse -> case sIntegral @c of
STrue ->
Right $
fromVector @b
(V.map (onResult . Right . (fromIntegral :: c -> Float)) (VG.convert v))
SFalse -> castMismatch @c @b
UnboxedColumn (Just bm) (v :: VU.Vector c) ->
case sFloating @c of
STrue ->
Right $
fromVector @b
( V.generate (VU.length v) $ \i ->
if bitmapTestBit bm i
then onResult (Right (realToFrac (VU.unsafeIndex v i) :: Float))
else onResult (Left "null")
)
SFalse -> case sIntegral @c of
STrue ->
Right $
fromVector @b
( V.generate (VU.length v) $ \i ->
if bitmapTestBit bm i
then onResult (Right (fromIntegral (VU.unsafeIndex v i) :: Float))
else onResult (Left "null")
)
SFalse -> castMismatch @c @b
BoxedColumn _ _ -> tryParseWith @Float onResult col
PackedText _ _ -> promoteToFloatWith onResult (materializePacked col)
MergedColumn _ _ -> promoteToFloatWith onResult (materializeMerged col)
promoteToIntWith ::
forall b.
(Columnable b) =>
(Either String Int -> b) -> Column -> Either DataFrameException Column
promoteToIntWith onResult col = case col of
UnboxedColumn Nothing (v :: VU.Vector c) ->
case sFloating @c of
STrue ->
Right $
fromVector @b
(V.map (onResult . Right . (round . (realToFrac :: c -> Double))) (VG.convert v))
SFalse -> case sIntegral @c of
STrue ->
Right $
fromVector @b
(V.map (onResult . Right . (fromIntegral :: c -> Int)) (VG.convert v))
SFalse -> castMismatch @c @b
UnboxedColumn (Just bm) (v :: VU.Vector c) ->
case sFloating @c of
STrue ->
Right $
fromVector @b
( V.generate (VU.length v) $ \i ->
if bitmapTestBit bm i
then onResult (Right (round (realToFrac (VU.unsafeIndex v i) :: Double)))
else onResult (Left "null")
)
SFalse -> case sIntegral @c of
STrue ->
Right $
fromVector @b
( V.generate (VU.length v) $ \i ->
if bitmapTestBit bm i
then onResult (Right (fromIntegral (VU.unsafeIndex v i) :: Int))
else onResult (Left "null")
)
SFalse -> castMismatch @c @b
BoxedColumn _ _ -> tryParseWith @Int onResult col
PackedText _ _ -> promoteToIntWith onResult (materializePacked col)
MergedColumn _ _ -> promoteToIntWith onResult (materializeMerged col)
-- | Single parse primitive: apply @onResult@ to the result of 'reads'.
parseWith :: (Read a) => (Either String a -> b) -> String -> b
parseWith f s = case reads s of
[(x, "")] -> f (Right x)
_ -> case reads (show s) of
[(x, "")] -> f (Right x)
_ -> f (Left s)
tryParseWith ::
forall a b.
(Columnable a, Columnable b, Read a) =>
(Either String a -> b) -> Column -> Either DataFrameException Column
tryParseWith onResult col = case col of
PackedText _ _ -> tryParseWith onResult (materializePacked col)
MergedColumn _ _ -> tryParseWith onResult (materializeMerged col)
BoxedColumn bm (v :: V.Vector c) ->
case testEquality (typeRep @c) (typeRep @String) of
Just Refl -> case bm of
Nothing -> Right $ fromVector @b $ V.map (parseWith onResult) v
Just bitmap ->
Right $
fromVector @b $
V.imap
( \i x ->
if bitmapTestBit bitmap i then parseWith onResult x else onResult (Left "null")
)
v
Nothing ->
case testEquality (typeRep @c) (typeRep @T.Text) of
Just Refl -> case bm of
Nothing -> Right $ fromVector @b $ V.map (parseWith onResult . T.unpack) v
Just bitmap ->
Right $
fromVector @b $
V.imap
( \i x ->
if bitmapTestBit bitmap i
then parseWith onResult (T.unpack x)
else onResult (Left "null")
)
v
Nothing -> castMismatch @c @b
UnboxedColumn bm (v :: VU.Vector c) -> case bm of
Nothing -> Right $ fromVector @b $ V.map (parseWith onResult . show) (V.convert v)
Just bitmap ->
Right $
fromVector @b $
V.imap
( \i x ->
if bitmapTestBit bitmap i
then parseWith onResult (show x)
else onResult (Left "null")
)
(V.convert v)
-- | Wrap each element in 'Just' when the output type is @Maybe c@.
tryMaybeWrap ::
forall a b.
(Columnable a, Columnable b) =>
(Either String a -> b) -> Column -> Maybe (Either DataFrameException Column)
tryMaybeWrap _onResult col = case col of
UnboxedColumn Nothing (v :: VU.Vector c) ->
let wrapped = V.map Just (VG.convert v) :: V.Vector (Maybe c)
in case testEquality (typeRep @b) (typeRep @(Maybe c)) of
Just Refl -> Just $ Right $ fromVector @b wrapped
Nothing ->
case testEquality (typeRep @b) (typeRep @(Maybe (Maybe c))) of
Just _ -> Just $ Right $ fromVector @(Maybe c) wrapped
Nothing -> Nothing
BoxedColumn Nothing (v :: V.Vector c) ->
let wrapped = V.map Just v :: V.Vector (Maybe c)
in case testEquality (typeRep @b) (typeRep @(Maybe c)) of
Just Refl -> Just $ Right $ fromVector @b wrapped
Nothing ->
case testEquality (typeRep @b) (typeRep @(Maybe (Maybe c))) of
Just _ -> Just $ Right $ fromVector @(Maybe c) wrapped
Nothing -> Nothing
_ -> Nothing
castMismatch ::
forall src tgt.
(Typeable src, Typeable tgt) =>
Either DataFrameException Column
castMismatch =
Left $
TypeMismatchException
MkTypeErrorContext
{ userType = Right (typeRep @tgt)
, expectedType = Right (typeRep @src)
, callingFunctionName = Just "cast"
, errorColumnName = Nothing
}
-- | Evaluate an expression in a given context, producing a 'Value'.
eval ::
forall a.
(Columnable a) =>
Ctx -> Expr a -> Either DataFrameException (Value a)
eval _ (Lit v) = Right (Scalar v)
eval (FlatCtx df) (Col name) =
case getColumn name df of
Nothing ->
Left $ ColumnsNotFoundException [name] "" (M.keys $ columnIndices df)
Just c
| hasElemType @a c -> Right (Flat c)
| otherwise ->
Left $
TypeMismatchException
( MkTypeErrorContext
{ userType = Right (typeRep @a)
, expectedType = Left (columnTypeString c)
, errorColumnName = Just (T.unpack name)
, callingFunctionName = Just "col"
} ::
TypeErrorContext a ()
)
eval (GroupCtx gdf) (Col name) =
case getColumn name (fullDataframe gdf) of
Nothing ->
Left $
ColumnsNotFoundException
[name]
""
(M.keys $ columnIndices $ fullDataframe gdf)
Just c
| hasElemType @a c ->
Right (Group (sliceGroups c (offsets gdf) (valueIndices gdf)))
| otherwise ->
Left $
TypeMismatchException
( MkTypeErrorContext
{ userType = Right (typeRep @a)
, expectedType = Left (columnTypeString c)
, errorColumnName = Just (T.unpack name)
, callingFunctionName = Just "col"
} ::
TypeErrorContext a ()
)
eval (FlatCtx df) (CastWith name _tag onResult) =
case getColumn name df of
Nothing ->
Left $
ColumnsNotFoundException [name] "" (M.keys $ columnIndices df)
Just c -> Flat <$> promoteColumnWith onResult c
eval (GroupCtx gdf) (CastWith name _tag onResult) =
case getColumn name (fullDataframe gdf) of
Nothing ->
Left $
ColumnsNotFoundException
[name]
""
(M.keys $ columnIndices $ fullDataframe gdf)
Just c -> do
promoted <- promoteColumnWith onResult c
Right $ Group (sliceGroups promoted (offsets gdf) (valueIndices gdf))
eval ctx (CastExprWith _tag onResult (inner :: Expr src)) = do
v <- eval @src ctx inner
case v of
Scalar s ->
Flat <$> promoteColumnWith onResult (fromList @src [s])
Flat col ->
Flat <$> promoteColumnWith onResult col
Group gs ->
Group <$> V.mapM (promoteColumnWith onResult) gs
eval (FlatCtx df) expr@(Unary{}) = evalFlat df expr
eval (FlatCtx df) expr@(Binary{}) = evalFlat df expr
eval (FlatCtx df) expr@(If{}) = evalFlat df expr
eval ctx expr@(Unary op (inner :: Expr b)) = addContext expr $ do
v <- eval @b ctx inner
applyUnary op v
eval ctx expr@(Binary op (left :: Expr c) (right :: Expr b)) =
addContext expr $ do
l <- eval @c ctx left
r <- eval @b ctx right
applyBinary op l r
eval ctx expr@(If cond l r) = addContext expr $ do
c <- eval @Bool ctx cond
lv <- eval @a ctx l
rv <- eval @a ctx r
branchValue c lv rv
eval (FlatCtx df) expr@(Over keys inner) = addContext expr $ do
let gdf = G.groupBy keys df
v <- eval (GroupCtx gdf) inner
case v of
Scalar s ->
Right (Scalar s)
Flat groupCol ->
Right (Flat (atIndicesStable (rowToGroup gdf) groupCol))
Group groupCols -> do
sorted <- V.fold1M' mappendColumns groupCols
let inv = invertPermutation (valueIndices gdf)
Right (Flat (atIndicesStable inv sorted))
eval (GroupCtx _) expr@(Over _ _) =
addContext expr $
Left
( InternalException
"Over (window function) is not supported inside a grouped context"
)
-- Fast path: seeded FoldAgg on a bare Col folds over permuted indices.
eval (GroupCtx gdf) expr@(Agg (FoldAgg _ (Just seed) (f :: a -> b -> a)) (Col name :: Expr b)) =
addContext expr $
case getColumn name (fullDataframe gdf) of
Nothing ->
Left $
ColumnsNotFoundException
[name]
""
(M.keys $ columnIndices $ fullDataframe gdf)
Just col ->
Flat <$> foldLinearGroups @b @a f seed col (rowToGroup gdf) (numGroups gdf)
-- Fast path: FoldAgg (seedless) on a bare Col in GroupCtx.
eval (GroupCtx gdf) expr@(Agg (FoldAgg _ Nothing (f :: a -> b -> a)) (Col name :: Expr b)) =
addContext expr $
case testEquality (typeRep @a) (typeRep @b) of
Nothing ->
Left $
InternalException
"Type mismatch in seedless fold: \
\accumulator and element types must match"
Just Refl ->
case getColumn name (fullDataframe gdf) of
Nothing ->
Left $
ColumnsNotFoundException
[name]
""
(M.keys $ columnIndices $ fullDataframe gdf)
Just col ->
Flat <$> foldl1DirectGroups @b f col (valueIndices gdf) (offsets gdf)
-- Fast path: countDistinct on a bare Col goes to the partitioned hash kernel.
eval ctx (Agg (MergeAgg "countDistinct" _ _ _ _) (Col name))
| Just Refl <- testEquality (typeRep @a) (typeRep @Int)
, Just col <- getColumn name (ctxFrame ctx)
, Just v <- distinctValue col =
Right v
where
ctxFrame (FlatCtx df) = df
ctxFrame (GroupCtx gdf) = fullDataframe gdf
distinctValue col = case ctx of
FlatCtx _ -> Scalar <$> countDistinctColumn col
GroupCtx gdf ->
Flat . fromUnboxedVector
<$> countDistinctGroups (rowToGroup gdf) (numGroups gdf) col
-- Fast path: MergeAgg on a bare Col in GroupCtx.
eval
(GroupCtx gdf)
expr@( Agg
(MergeAgg _ seed (step :: acc -> b -> acc) _ (finalize :: acc -> a))
(Col name :: Expr b)
) =
addContext expr $
case getColumn name (fullDataframe gdf) of
Nothing ->
Left $
ColumnsNotFoundException
[name]
""
(M.keys $ columnIndices $ fullDataframe gdf)
Just col ->
Flat
<$> ( foldLinearGroups @b step seed col (rowToGroup gdf) (numGroups gdf)
>>= mapColumn finalize
)
eval ctx expr@(Agg (CollectAgg _ (f :: v b -> a)) inner) =
addContext expr $ do
v <- eval @b ctx inner
case v of
Scalar _ ->
Left $
InternalException
"Cannot apply a collection aggregation to a scalar"
Flat col ->
Scalar <$> applyCollect @v @b @a f col
Group gs ->
Flat . fromVector
<$> V.mapM (applyCollect @v @b @a f) gs
eval ctx expr@(Agg (FoldAgg _ (Just seed) (f :: a -> b -> a)) inner) =
addContext expr $ do
v <- eval @b ctx inner
case v of
Scalar x -> Right (broadcastFold ctx seed f x)
Flat col ->
Scalar <$> foldlColumn @b @a f seed col
Group gs ->
Flat . fromVector
<$> V.mapM (foldlColumn @b @a f seed) gs
eval (FlatCtx df) (Agg (MergeAgg "mean" _ _ _ (_ :: acc -> a)) (Col name))
| Just Refl <- testEquality (typeRep @a) (typeRep @Double)
, Just v <- fusedMean df name =
Right (Scalar v)
eval
ctx
expr@( Agg
(MergeAgg _ seed (step :: acc -> b -> acc) _ (finalize :: acc -> a))
(inner :: Expr b)
) =
addContext expr $ do
v <- eval @b ctx inner
case v of
Scalar x -> case broadcastFold ctx seed step x of
Scalar acc -> Right (Scalar (finalize acc))
Flat col -> Flat <$> mapColumn @acc @a finalize col
Group _ ->
Left
( InternalException
"broadcastFold unexpectedly produced a Group value"
)
Flat col ->
Scalar . finalize <$> foldlColumn @b step seed col
Group gs ->
Flat . fromVector
<$> V.mapM (fmap finalize . foldlColumn @b step seed) gs
-- Fast path: whole-frame sum of a clean Int/Double column, one parallel loop.
eval (FlatCtx df) (Agg (FoldAgg "sum" Nothing (_ :: a -> b -> a)) inner)
| Just Refl <- testEquality (typeRep @a) (typeRep @b)
, Just v <- fusedSum df inner =
Right (Scalar v)
eval ctx expr@(Agg (FoldAgg _ Nothing (f :: a -> b -> a)) inner) =
addContext expr $
case testEquality (typeRep @a) (typeRep @b) of
Nothing ->
Left $
InternalException
"Type mismatch in seedless fold: \
\accumulator and element types must match"
Just Refl -> do
v <- eval @b ctx inner
case v of
Scalar _ ->
Left $
InternalException
"fold1 requires at least one element"
Flat col ->
Scalar <$> foldl1Column @a f col
Group gs ->
Flat . fromVector
<$> V.mapM (foldl1Column @a f) gs
-- | The op's element function, with a fast path for @toDouble@.
fastUnaryFn ::
forall b a.
(Columnable b, Columnable a) =>
T.Text -> (b -> a) -> (b -> a)
fastUnaryFn name f
| name == "toDouble"
, Just Refl <- testEquality (typeRep @a) (typeRep @Double) =
integralToDouble @b f
| otherwise = f
integralToDouble :: forall b. (Columnable b) => (b -> Double) -> b -> Double
integralToDouble f
| Just Refl <- testEquality rb (typeRep @Int) = fromIntegral
| Just Refl <- testEquality rb (typeRep @Int8) = fromIntegral
| Just Refl <- testEquality rb (typeRep @Int16) = fromIntegral
| Just Refl <- testEquality rb (typeRep @Int32) = fromIntegral
| Just Refl <- testEquality rb (typeRep @Int64) = fromIntegral
| Just Refl <- testEquality rb (typeRep @Word) = fromIntegral
| otherwise = f
where
rb = typeRep @b
broadcastFold ::
forall acc b.
(Columnable acc) =>
Ctx -> acc -> (acc -> b -> acc) -> b -> Value acc
broadcastFold (FlatCtx df) seed step x =
let n = fst (dataframeDimensions df)
in Scalar (iterateStep n step seed x)
broadcastFold (GroupCtx gdf) seed step x =
let offs = offsets gdf
ng = VU.length offs - 1
results =
V.generate ng $ \i ->
let sz = offs VU.! (i + 1) - offs VU.! i
in iterateStep sz step seed x
in Flat (fromVector results)
iterateStep :: Int -> (acc -> b -> acc) -> acc -> b -> acc
iterateStep n step = go n
where
go 0 !acc _ = acc
go k !acc x = go (k - 1) (step acc x) x
-- | Apply a 'CollectAgg' function to a single column.
applyCollect ::
forall v b a.
(VG.Vector v b, Typeable v, Columnable b, Columnable a) =>
(v b -> a) -> Column -> Either DataFrameException a
applyCollect f col = f <$> toVector @b @v col
-- | Result of interpreting an expression in a grouped context.
data AggregationResult a
= UnAggregated Column
| Aggregated (TypedColumn a)
-- | Interpret an expression against a flat 'DataFrame', producing a column.
interpret ::
forall a.
(Columnable a) =>
DataFrame -> Expr a -> Either DataFrameException (TypedColumn a)
interpret df expr = do
v <- eval (FlatCtx df) expr
pure $ TColumn $ materialize @a (fst (dataframeDimensions df)) v
-- | Interpret an expression against a 'GroupedDataFrame'.
interpretAggregation ::
forall a.
(Columnable a) =>
GroupedDataFrame ->
Expr a ->
Either DataFrameException (AggregationResult a)
interpretAggregation gdf expr = do
v <- eval (GroupCtx gdf) expr
case v of
Scalar a ->
Right $
Aggregated $
TColumn $
broadcastScalar @a (numGroups gdf) a
Flat col ->
Right $ Aggregated $ TColumn col
Group _ ->
Right $ UnAggregated $ BoxedColumn @T.Text Nothing V.empty
-- | The mean of a clean Int or Double column. Empty gives NaN, like 'mean'.
fusedMean :: DataFrame -> T.Text -> Maybe Double
fusedMean df name = do
UnboxedColumn Nothing (v :: VU.Vector t) <- getColumn name df
total <- case testEquality (typeRep @t) (typeRep @Int) of
Just Refl -> Just (K.parSumBy fromIntegral v)
Nothing -> case testEquality (typeRep @t) (typeRep @Double) of
Just Refl -> Just (K.parSumBy id v)
Nothing -> Nothing
pure $! if VU.null v then 0 / 0 else total / fromIntegral (VU.length v)
-- | The sum of a column, or of an arithmetic combination with a literal.
fusedSum :: forall a. (Columnable a) => DataFrame -> Expr a -> Maybe a
fusedSum df expr = case expr of
Col name -> withClean name Nothing
Binary op (Col name :: Expr c) (Lit (k :: b))
| Just Refl <- testEquality (typeRep @c) (typeRep @a)
, Just Refl <- testEquality (typeRep @b) (typeRep @a) ->
withClean name (Just (binaryName op, k, False))
Binary op (Lit (k :: c)) (Col name :: Expr b)
| Just Refl <- testEquality (typeRep @c) (typeRep @a)
, Just Refl <- testEquality (typeRep @b) (typeRep @a) ->
withClean name (Just (binaryName op, k, True))
_ -> Nothing
where
-- The operator is picked by name at a concrete element type.
withClean :: T.Text -> Maybe (T.Text, a, Bool) -> Maybe a
withClean name combo = do
UnboxedColumn Nothing (v :: VU.Vector t) <- getColumn name df
Refl <- testEquality (typeRep @t) (typeRep @a)
case testEquality (typeRep @a) (typeRep @Int) of
Just Refl -> sumInt combo v
Nothing -> case testEquality (typeRep @a) (typeRep @Double) of
Just Refl -> sumDouble combo v
Nothing -> Nothing
sumInt :: Maybe (T.Text, Int, Bool) -> VU.Vector Int -> Maybe Int
sumInt combo v = case combo of
Nothing -> Just (K.parSumBy id v)
Just ("add", k, _) -> Just (K.parSumBy (+ k) v)
Just ("mult", k, _) -> Just (K.parSumBy (* k) v)
Just ("sub", k, False) -> Just (K.parSumBy (subtract k) v)
Just ("sub", k, True) -> Just (K.parSumBy (k -) v)
_ -> Nothing
sumDouble :: Maybe (T.Text, Double, Bool) -> VU.Vector Double -> Maybe Double
sumDouble combo v = case combo of
Nothing -> Just (K.parSumBy id v)
Just ("add", k, _) -> Just (K.parSumBy (+ k) v)
Just ("mult", k, _) -> Just (K.parSumBy (* k) v)
Just ("sub", k, False) -> Just (K.parSumBy (subtract k) v)
Just ("sub", k, True) -> Just (K.parSumBy (k -) v)
_ -> Nothing