dataframe-operations 2.5.0.1 → 2.6.0.0
raw patch · 26 files changed
+921/−1557 lines, 26 filesdep ~dataframe-core
Dependency ranges changed: dataframe-core
Files
- dataframe-operations.cabal +3/−2
- src-internal/DataFrame/Internal/Statistics.hs +1/−2
- src/DataFrame/Expression/Parse.hs +477/−0
- src/DataFrame/Functions.hs +41/−59
- src/DataFrame/Monad.hs +8/−12
- src/DataFrame/Operations/Aggregation.hs +9/−61
- src/DataFrame/Operations/Aggregation/Run.hs +17/−118
- src/DataFrame/Operations/Core.hs +39/−505
- src/DataFrame/Operations/Inference.hs +3/−6
- src/DataFrame/Operations/Join.hs +28/−163
- src/DataFrame/Operations/Join/Parallel.hs +13/−69
- src/DataFrame/Operations/Merge.hs +1/−3
- src/DataFrame/Operations/Permutation.hs +153/−12
- src/DataFrame/Operations/SetOps.hs +5/−23
- src/DataFrame/Operations/Statistics.hs +8/−61
- src/DataFrame/Operations/Subset.hs +29/−117
- src/DataFrame/Operations/Transformations.hs +10/−29
- src/DataFrame/Operations/Typing.hs +16/−45
- src/DataFrame/Typed/Access.hs +8/−18
- src/DataFrame/Typed/Aggregate.hs +3/−57
- src/DataFrame/Typed/Apply.hs +6/−32
- src/DataFrame/Typed/Expr.hs +24/−73
- src/DataFrame/Typed/Expr/Extra.hs +2/−6
- src/DataFrame/Typed/Operations.hs +9/−42
- src/DataFrame/Typed/Sampling.hs +2/−8
- src/DataFrame/Typed/Statistics.hs +6/−34
dataframe-operations.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.4 name: dataframe-operations-version: 2.5.0.1+version: 2.6.0.0 synopsis: Column operations, expression DSL, and statistics for the dataframe ecosystem. description: Untyped column operations (select, filter, sort, join, groupBy,@@ -31,6 +31,7 @@ import: warnings exposed-modules: DataFrame.Internal.Statistics+ DataFrame.Expression.Parse DataFrame.Functions DataFrame.Monad DataFrame.Operations.Aggregation.Run@@ -58,7 +59,7 @@ build-depends: base >= 4 && < 5, bytestring >= 0.11 && < 0.14, containers >= 0.6.7 && < 0.10,- dataframe-core >= 2.5 && < 2.6,+ dataframe-core >= 2.6 && < 2.7, dataframe-parsing >= 2.2 && < 2.3, random >= 1.2 && < 2, regex-tdfa >= 1.3.0 && < 2,
src-internal/DataFrame/Internal/Statistics.hs view
@@ -186,8 +186,7 @@ let !p = fromIntegral i / fromIntegral q :: Double !position = p * fromIntegral (n - 1) !index = floor position :: Int- -- This is not exact for Ord instances.- -- Figure out how to make it so.+ -- Not exact for Ord instances; TODO make it so. VM.read mutableSamp index ) (V.convert qs)
+ src/DataFrame/Expression/Parse.hs view
@@ -0,0 +1,477 @@+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++-- | Reload expressions from their 'Show' and 'prettyPrint' printed forms.+module DataFrame.Expression.Parse (+ ColType (..),+ Schema,+ schemaOf,+ parseShown,+ parsePretty,+ parseShownAs,+ parsePrettyAs,+) where++import Data.Char (isAlphaNum, isDigit, isSpace)+import Data.List (isPrefixOf, sortBy)+import qualified Data.Map.Strict as M+import Data.Ord (Down (..), comparing)+import qualified Data.Text as T++import Data.Type.Equality (testEquality, (:~:) (Refl))+import DataFrame.Internal.Column (Columnable)+import DataFrame.Internal.DataFrame (DataFrame, columnNames)+import Type.Reflection (typeRep)++import qualified DataFrame.Functions as F+import DataFrame.Internal.Expression (Expr (..), SomeExpr (..), fromSomeExpr)+import DataFrame.Internal.Expression.Operators (+ (.&&.),+ (./=.),+ (.<.),+ (.<=.),+ (.==.),+ (.>.),+ (.>=.),+ (.||.),+ )+import DataFrame.Internal.Expression.Reflect (+ Dict (..),+ fracDict,+ integralDict,+ numDict,+ ordDict,+ realDict,+ )+import DataFrame.Operations.Core (columnAsVector)++-- | Column types the parser can build expressions over.+data ColType = TInt | TDouble | TText | TBool+ deriving (Eq, Show)++type Schema = M.Map T.Text ColType++-- | Column types of a frame, for the columns whose type the parser supports.+schemaOf :: DataFrame -> Schema+schemaOf df = M.fromList [(c, t) | c <- columnNames df, Just t <- [typeOf c]]+ where+ typeOf c+ | ok (F.col @Int c) = Just TInt+ | ok (F.col @Double c) = Just TDouble+ | ok (F.col @T.Text c) = Just TText+ | ok (F.col @Bool c) = Just TBool+ | otherwise = Nothing+ ok :: forall a. (Columnable a) => Expr a -> Bool+ ok e = either (const False) (const True) (columnAsVector e df)++-- | The untyped syntax both printers share.+data Ast+ = ACol T.Text (Maybe ColType)+ | -- | The value, and whether it was a Double (point or exponent).+ ANum Double Bool+ | AStr T.Text+ | ABool Bool+ | AIf Ast Ast Ast+ | ACall T.Text [Ast]+ deriving (Show)++-- | Two expressions of one type (the result of making operands agree).+data Same where+ Same :: (Columnable t) => Expr t -> Expr t -> Same++typeName :: SomeExpr -> String+typeName (SomeExpr (_ :: Expr t)) = show (typeRep @t)++-- | Use evidence for a class on an expression's type, or name the operator.+need :: String -> Maybe (Dict c) -> ((c) => Either String r) -> Either String r+need what d k = maybe (Left (what ++ ": operand type not supported")) (\Dict -> k) d++elab :: Schema -> Ast -> Either String SomeExpr+elab schema ast = case ast of+ ACol c (Just t) -> pure (colOf c t)+ ACol c Nothing ->+ maybe (Left ("unknown column " ++ show c)) (pure . colOf c) (M.lookup c schema)+ ANum v True -> pure (SomeExpr (F.lit v))+ ANum v False -> pure (SomeExpr (F.lit (round v :: Int)))+ AStr s -> pure (SomeExpr (F.lit s))+ ABool b -> pure (SomeExpr (F.lit b))+ AIf c t e -> do+ c' <- elab schema c+ cb <-+ maybe+ (Left ("if: condition has type " ++ typeName c'))+ pure+ (fromSomeExpr @Bool c')+ t' <- elab schema t+ e' <- elab schema e+ Same a b <- unify t' e'+ pure (SomeExpr (F.ifThenElse cb a b))+ ACall name args -> do+ args' <- mapM (elab schema) args+ call name args'+ where+ colOf c t = case t of+ TInt -> SomeExpr (F.col @Int c)+ TDouble -> SomeExpr (F.col @Double c)+ TText -> SomeExpr (F.col @T.Text c)+ TBool -> SomeExpr (F.col @Bool c)++-- | Make two operands the same type; a numeric literal adopts the other's.+unify :: SomeExpr -> SomeExpr -> Either String Same+unify a@(SomeExpr (x :: Expr p)) b@(SomeExpr (y :: Expr q))+ | Just Refl <- testEquality (typeRep @p) (typeRep @q) = pure (Same x y)+ | Just (Lit i) <- fromSomeExpr @Int a+ , Just yd <- fromSomeExpr @Double b =+ pure (Same (F.lit (fromIntegral i)) yd)+ | Just xd <- fromSomeExpr @Double a+ , Just (Lit i) <- fromSomeExpr @Int b =+ pure (Same xd (F.lit (fromIntegral i)))+ | Just (Lit d) <- fromSomeExpr @Double a+ , Just yi <- fromSomeExpr @Int b+ , whole d =+ pure (Same (F.lit (round d)) yi)+ | Just xi <- fromSomeExpr @Int a+ , Just (Lit d) <- fromSomeExpr @Double b+ , whole d =+ pure (Same xi (F.lit (round d)))+ | otherwise =+ Left ("operand types differ: " ++ typeName a ++ " vs " ++ typeName b)+ where+ whole d = fromIntegral (round d :: Int) == (d :: Double)++call :: T.Text -> [SomeExpr] -> Either String SomeExpr+call name args = case (name, args) of+ ("toDouble", [SomeExpr (e :: Expr t)]) -> need "toDouble" (realDict @t) (pure (SomeExpr (F.toDouble e)))+ ("not", [a]) ->+ maybe+ (Left "not needs a Bool operand")+ (pure . SomeExpr . F.not)+ (fromSomeExpr @Bool a)+ ("negate", [SomeExpr (e :: Expr t)]) -> need "negate" (numDict @t) (pure (SomeExpr (negate e)))+ ("abs", [SomeExpr (e :: Expr t)]) -> need "abs" (numDict @t) (pure (SomeExpr (abs e)))+ (_, [a, b]) -> do+ Same x y <- unify a b+ binary name x y+ _ ->+ Left+ ( "unsupported call "+ ++ T.unpack name+ ++ " with "+ ++ show (length args)+ ++ " arguments"+ )++binary ::+ forall t. (Columnable t) => T.Text -> Expr t -> Expr t -> Either String SomeExpr+binary name x y = case name of+ "add" -> need "add" (numDict @t) (pure (SomeExpr (x + y)))+ "sub" -> need "sub" (numDict @t) (pure (SomeExpr (x - y)))+ "mult" -> need "mult" (numDict @t) (pure (SomeExpr (x * y)))+ "divide" -> need "divide" (fracDict @t) (pure (SomeExpr (x / y)))+ "div" -> need "div" (integralDict @t) (pure (SomeExpr (F.div x y)))+ "mod" -> need "mod" (integralDict @t) (pure (SomeExpr (F.mod x y)))+ "min" -> need "min" (ordDict @t) (pure (SomeExpr (F.min x y)))+ "max" -> need "max" (ordDict @t) (pure (SomeExpr (F.max x y)))+ "eq" -> pure (SomeExpr (x .==. y))+ "neq" -> pure (SomeExpr (x ./=. y))+ "lt" -> need "lt" (ordDict @t) (pure (SomeExpr (x .<. y)))+ "gt" -> need "gt" (ordDict @t) (pure (SomeExpr (x .>. y)))+ "leq" -> need "leq" (ordDict @t) (pure (SomeExpr (x .<=. y)))+ "geq" -> need "geq" (ordDict @t) (pure (SomeExpr (x .>=. y)))+ "and" -> bool (.&&.)+ "or" -> bool (.||.)+ _ -> Left ("unsupported operator " ++ T.unpack name)+ where+ bool f = case testEquality (typeRep @t) (typeRep @Bool) of+ Just Refl -> pure (SomeExpr (f x y))+ Nothing -> Left (T.unpack name ++ " needs Bool operands")++data SExp = SAtom String | SList [SExp]+ deriving (Show)++sexp :: String -> Either String (SExp, String)+sexp s = case dropWhile isSpace s of+ '(' : rest -> list rest []+ '"' : _ -> case reads (dropWhile isSpace s) :: [(String, String)] of+ [(str, rest)] -> pure (SAtom (show str), rest)+ _ -> Left "bad string literal"+ rest ->+ let (tok, more) = span (\c -> not (isSpace c) && c /= '(' && c /= ')') rest+ in if null tok+ then Left ("unexpected input near " ++ take 20 rest)+ else pure (SAtom tok, more)+ where+ list str acc = case dropWhile isSpace str of+ ')' : rest -> pure (SList (reverse acc), rest)+ "" -> Left "unbalanced parentheses"+ more -> do+ (e, rest) <- sexp more+ list rest (e : acc)++fromSExp :: SExp -> Either String Ast+fromSExp e = case e of+ SList [SAtom "col", SAtom ('@' : ty), SAtom name] -> ACol <$> strLit name <*> (Just <$> colType ty)+ SList [SAtom "col", SAtom "@Maybe", SAtom _, SAtom _] -> Left "nullable columns are not supported"+ SList [SAtom "lit", inner] -> literal inner+ SList [SAtom "ifThenElse", c, t, f] -> AIf <$> fromSExp c <*> fromSExp t <*> fromSExp f+ SList (SAtom fn : args) -> ACall (T.pack fn) <$> mapM fromSExp args+ SAtom a -> Left ("bare atom " ++ a)+ SList [] -> Left "empty list"+ SList (SList _ : _) -> Left "list head must be an atom"+ where+ strLit s = case reads s :: [(String, String)] of+ [(v, "")] -> pure (T.pack v)+ _ -> Left ("bad name " ++ s)+ colType ty = case ty of+ "Int" -> pure TInt+ "Double" -> pure TDouble+ "Text" -> pure TText+ "Bool" -> pure TBool+ _ -> Left ("unsupported column type " ++ ty)+ -- (lit (3.5)), (lit ("x")); the inner parentheses come from show.+ literal inner = case inner of+ SList [SAtom v] -> atomLit v+ SAtom v -> atomLit v+ SList [SAtom "-", SAtom v] -> atomLit ('-' : v)+ _ -> Left ("bad literal " ++ show inner)+ atomLit v+ | v == "True" = pure (ABool True)+ | v == "False" = pure (ABool False)+ | take 1 v == "\"" = AStr <$> strLit v+ | otherwise = case reads v :: [(Double, String)] of+ [(d, "")] -> pure (ANum d (any (`elem` (".eE" :: String)) v))+ _ -> Left ("bad literal " ++ v)++-- | Parse the output of 'show' on an 'Expr'; columns carry their own types.+parseShown :: String -> Either String SomeExpr+parseShown s = do+ (e, rest) <- sexp s+ if all isSpace rest then pure () else Left ("trailing input: " ++ take 30 rest)+ ast <- fromSExp e+ elab M.empty ast++data Tok+ = TNum Double Bool+ | TStr T.Text+ | TName T.Text+ | TFun T.Text+ | TOp String+ | TLParen+ | TRParen+ | TComma+ | TIf+ | TElse+ deriving (Eq, Show)++operators :: [String]+-- Dotted forms come from Operators, bare forms from Functions; same names.+operators =+ sortBy+ (comparing (Down . length))+ [ ".<=."+ , ".>=."+ , ".==."+ , "./=."+ , ".&&."+ , ".||."+ , ".<."+ , ".>."+ , "<="+ , ">="+ , "=="+ , "/="+ , "<"+ , ">"+ , "//"+ , "+"+ , "-"+ , "*"+ , "/"+ , "~"+ ]++functions :: [T.Text]+functions = ["toDouble", "min", "max", "mod", "abs", "negate", "not", "isJust"]++lexPretty :: Schema -> String -> Either String [Tok]+lexPretty schema = go+ where+ names = sortBy (comparing (Down . T.length)) (M.keys schema)+ go [] = pure []+ go s@(c : rest)+ | isSpace c = go rest+ | c == '(' = (TLParen :) <$> go rest+ | c == ')' = (TRParen :) <$> go rest+ | c == ',' = (TComma :) <$> go rest+ | c == '"' = case reads s :: [(String, String)] of+ [(str, more)] -> (TStr (T.pack str) :) <$> go more+ _ -> Left "bad string literal"+ | Just (n, more) <- keyword s = (n :) <$> go more+ | Just (n, more) <- longest names s = (TName n :) <$> go more+ | Just (f, more) <- longest functions s+ , take 1 (dropWhile isSpace more) == "(" =+ (TFun f :) <$> go more+ | isDigit c = number s+ -- A '-' touching a digit is a negative literal; binary '-' is spaced.+ | c == '-', (d : _) <- rest, isDigit d = number s+ | Just op <- firstPrefix operators s = (TOp op :) <$> go (drop (length op) s)+ | otherwise = Left ("cannot lex near: " ++ take 30 s)+ keyword s+ | "else" `isPrefixOf` s && boundary (drop 4 s) = Just (TElse, drop 4 s)+ | "if" `isPrefixOf` s && boundary (drop 2 s) = Just (TIf, drop 2 s)+ | otherwise = Nothing+ boundary more = case more of+ [] -> True+ (c : _) -> not (isAlphaNum c)+ longest cands s = case [n | n <- cands, T.unpack n `isPrefixOf` s] of+ (n : _) -> Just (n, drop (T.length n) s)+ [] -> Nothing+ firstPrefix cands s = case [op | op <- cands, op `isPrefixOf` s] of+ (op : _) -> Just op+ [] -> Nothing+ number s0 =+ let (sign, s) = case s0 of+ '-' : more0 -> ("-", more0)+ _ -> ("", s0)+ (body0, more) =+ span (\ch -> isDigit ch || ch == '.' || ch == 'e' || ch == 'E' || ch == '-') s+ body = sign ++ body0+ -- A trailing '-' belongs to the next token, not an exponent.+ (num, more') =+ if not (null body) && last body == '-'+ then (init body, '-' : more)+ else (body, more)+ in case reads num :: [(Double, String)] of+ [(d, "")] -> (TNum d (any (`elem` (".eE" :: String)) num) :) <$> go more'+ _ -> Left ("bad number " ++ num)++-- Precedence climbing over the printer's precedences.+type P a = [Tok] -> Either String (a, [Tok])++parseExpr :: P Ast+parseExpr = parseOr++parseOr+ , parseAnd+ , parseCmp+ , parseAdd+ , parseMul+ , parseUnary+ , parsePrimary ::+ P Ast+parseOr ts = do+ (l, rest) <- parseAnd ts+ case rest of+ TOp ".||." : more -> do+ (r, rest') <- parseOr more+ pure (ACall "or" [l, r], rest')+ _ -> pure (l, rest)+parseAnd ts = do+ (l, rest) <- parseCmp ts+ case rest of+ TOp ".&&." : more -> do+ (r, rest') <- parseAnd more+ pure (ACall "and" [l, r], rest')+ _ -> pure (l, rest)+parseCmp ts = do+ (l, rest) <- parseAdd ts+ case rest of+ TOp op : more | Just name <- lookup op cmpNames -> do+ (r, rest') <- parseAdd more+ pure (ACall name [l, r], rest')+ _ -> pure (l, rest)+ where+ cmpNames =+ [ (".<=.", "leq")+ , (".>=.", "geq")+ , (".==.", "eq")+ , ("./=.", "neq")+ , (".<.", "lt")+ , (".>.", "gt")+ , ("<=", "leq")+ , (">=", "geq")+ , ("==", "eq")+ , ("/=", "neq")+ , ("<", "lt")+ , (">", "gt")+ ]+parseAdd ts = parseMul ts >>= uncurry loop+ where+ loop l (TOp "+" : more) = parseMul more >>= \(r, rest) -> loop (ACall "add" [l, r]) rest+ loop l (TOp "-" : more) = parseMul more >>= \(r, rest) -> loop (ACall "sub" [l, r]) rest+ loop l rest = pure (l, rest)+parseMul ts = parseUnary ts >>= uncurry loop+ where+ loop l (TOp "*" : more) = parseUnary more >>= \(r, rest) -> loop (ACall "mult" [l, r]) rest+ loop l (TOp "/" : more) = parseUnary more >>= \(r, rest) -> loop (ACall "divide" [l, r]) rest+ loop l (TOp "//" : more) = parseUnary more >>= \(r, rest) -> loop (ACall "div" [l, r]) rest+ loop l rest = pure (l, rest)+parseUnary (TOp "~" : more) = do+ (e, rest) <- parseUnary more+ pure (ACall "not" [e], rest)+parseUnary (TOp "-" : more) = do+ (e, rest) <- parseUnary more+ pure (ACall "negate" [e], rest)+parseUnary ts = parsePrimary ts+parsePrimary ts = case ts of+ TNum v isD : rest -> pure (ANum v isD, rest)+ TStr s : rest -> pure (AStr s, rest)+ TName n : rest -> pure (ACol n Nothing, rest)+ TFun f : TLParen : rest -> do+ (args, rest') <- parseArgs rest []+ pure (ACall f args, rest')+ TLParen : rest -> do+ (e, rest') <- parseExpr rest+ case rest' of+ TRParen : more -> pure (e, more)+ _ -> Left "expected )"+ TIf : rest -> parseIf rest+ _ -> Left ("unexpected token " ++ show (take 3 ts))+ where+ parseArgs toks acc = do+ (e, rest) <- parseExpr toks+ case rest of+ TComma : more -> parseArgs more (e : acc)+ TRParen : more -> pure (reverse (e : acc), more)+ _ -> Left "expected , or ) in call"++-- if C T else if C2 T2 else E+parseIf :: P Ast+parseIf ts = do+ (c, rest) <- parseExpr ts+ (t, rest') <- parseExpr rest+ case rest' of+ TElse : TIf : more -> do+ (e, rest'') <- parseIf more+ pure (AIf c t e, rest'')+ TElse : more -> do+ (e, rest'') <- parseExpr more+ pure (AIf c t e, rest'')+ _ -> Left "expected else"++-- | Parse the output of 'prettyPrint', given the frame's column types.+parsePretty :: Schema -> String -> Either String SomeExpr+parsePretty schema s = do+ toks <- lexPretty schema s+ (ast, rest) <- parseExpr toks+ if null rest then pure () else Left ("trailing tokens: " ++ show (take 5 rest))+ elab schema ast++parseShownAs :: forall a. (Columnable a) => String -> Either String (Expr a)+parseShownAs s = parseShown s >>= asType++parsePrettyAs ::+ forall a. (Columnable a) => Schema -> String -> Either String (Expr a)+parsePrettyAs schema s = parsePretty schema s >>= asType++asType :: forall a. (Columnable a) => SomeExpr -> Either String (Expr a)+asType u =+ maybe+ (Left "expression has a different type from the one requested")+ pure+ (fromSomeExpr u)
src/DataFrame/Functions.hs view
@@ -34,6 +34,7 @@ import qualified Data.List as L import qualified Data.Map as M import qualified Data.Maybe as Maybe+import qualified Data.Set as S import qualified Data.Text as T import Data.Time import Data.Type.Equality (testEquality, type (:~:) (Refl))@@ -74,12 +75,7 @@ } ) -{- | Lift a unary function over a nullable or non-nullable column expression.-When the input is @Maybe a@, 'Nothing' short-circuits (like 'fmap').-When the input is plain @a@, the function is applied directly.--The return type is inferred via 'NullLift1Result': no annotation needed.--}+-- | Lift a unary function over a nullable or non-nullable column expression. nullLift :: (NullLift1Op a r (NullLift1Result a r), Columnable (NullLift1Result a r)) => (BaseType a -> r) ->@@ -89,11 +85,7 @@ Unary (MkUnaryOp{unaryFn = applyNull1 f, unaryName = "nullLift", unarySymbol = Nothing}) -{- | Lift a binary function over nullable or non-nullable column expressions.-Any 'Nothing' operand short-circuits to 'Nothing' in the result.--The return type is inferred via 'NullLift2Result': no annotation needed.--}+-- | Lift a binary function over nullable or non-nullable column expressions. nullLift2 :: (NullLift2Op a b r (NullLift2Result a b r), Columnable (NullLift2Result a b r)) => (BaseType a -> BaseType b -> r) ->@@ -111,38 +103,21 @@ } ) -{- | Lenient numeric \/ text coercion returning @Maybe a@. Looks up column-@name@ and coerces its values to @a@. Values that cannot be converted-(parse failures, type mismatches) become 'Nothing'; successfully converted-values are wrapped in 'Just'. Existing 'Nothing' in optional source columns-stays as 'Nothing'.--}+-- | Lenient coercion of a column to @Maybe a@; failures become 'Nothing'. cast :: forall a. (Columnable a, Read a) => T.Text -> Expr (Maybe a) cast colName = CastWith colName "cast" (either (const Nothing) Just) -{- | Lenient coercion that substitutes a default for unconvertible values.-Looks up column @name@, coerces its values to @a@, and uses @def@ wherever-conversion fails or the source value is 'Nothing'.--}+-- | Lenient coercion that substitutes a default for unconvertible values. castWithDefault :: forall a. (Columnable a, Read a) => a -> T.Text -> Expr a castWithDefault def colName = CastWith colName ("castWithDefault:" <> T.pack (show def)) (fromRight def) -{- | Lenient coercion returning @Either T.Text a@. Successfully converted-values are 'Right'; values that cannot be parsed are kept as 'Left' with-their original string representation, so the caller can inspect or handle-them downstream. Existing 'Nothing' in optional source columns becomes-@Left \"null\"@.--}+-- | Lenient coercion to @Either T.Text a@, keeping unparsed values as 'Left'. castEither :: forall a. (Columnable a, Read a) => T.Text -> Expr (Either T.Text a) castEither colName = CastWith colName "castEither" (either (Left . T.pack) Right) -{- | Lenient coercion for assertedly non-nullable columns.-Substitutes @error@ for @Nothing@, so it will crash at evaluation time if-any @Nothing@ is actually encountered. For non-nullable and-fully-populated nullable columns no cost is paid.--}+-- | Lenient coercion that crashes at evaluation if a @Nothing@ is found. unsafeCast :: forall a. (Columnable a, Read a) => T.Text -> Expr a unsafeCast colName = CastWith@@ -237,6 +212,25 @@ {-# SPECIALIZE count :: Expr Int64 -> Expr Int #-} {-# INLINEABLE count #-} +-- | Number of distinct values, the equivalent of SQL's @COUNT(DISTINCT x)@.+countDistinct :: forall a. (Columnable a, Ord a) => Expr a -> Expr Int+countDistinct =+ Agg+ ( MergeAgg+ "countDistinct"+ (S.empty :: S.Set a)+ (flip S.insert)+ S.union+ S.size+ )+{-# SPECIALIZE countDistinct :: Expr Double -> Expr Int #-}+{-# SPECIALIZE countDistinct :: Expr Float -> Expr Int #-}+{-# SPECIALIZE countDistinct :: Expr Int -> Expr Int #-}+{-# SPECIALIZE countDistinct :: Expr Int8 -> Expr Int #-}+{-# SPECIALIZE countDistinct :: Expr Int16 -> Expr Int #-}+{-# SPECIALIZE countDistinct :: Expr Int32 -> Expr Int #-}+{-# SPECIALIZE countDistinct :: Expr Int64 -> Expr Int #-}+ -- | Row count, the equivalent of SQL's @COUNT(*)@. countAll :: Expr Int countAll = count (Lit (0 :: Int))@@ -342,11 +336,7 @@ bottomK = kExtremes "bottomK" (<) {-# INLINEABLE bottomK #-} -{- | Top k values after applying an ordering function. Floating-point NaNs-are dropped: they have no place in a total order, and admitting them would-make the merge non-associative (results would depend on chunk boundaries in-the lazy executor).--}+-- | Top k values after applying an ordering function, dropping NaNs. kExtremes :: forall a. (Columnable a, Ord a) =>@@ -460,7 +450,7 @@ zScore c = (c - mean c) / stddev c pow :: (Columnable a, Num a) => Expr a -> Int -> Expr a-pow expr i = lift2Decorated (^) "pow" (Just "^") True 8 expr (Lit i)+pow expr i = lift2Decorated (^) "pow" (Just "^") False 8 expr (Lit i) {-# SPECIALIZE pow :: Expr Double -> Int -> Expr Double #-} {-# SPECIALIZE pow :: Expr Float -> Int -> Expr Float #-} {-# SPECIALIZE pow :: Expr Int -> Int -> Expr Int #-}@@ -614,6 +604,15 @@ lastOrNothing :: (Columnable a) => Expr [a] -> Expr (Maybe a) lastOrNothing = liftDecorated (Maybe.listToMaybe . reverse) "lastOrNothing" Nothing +-- | Whether the text contains the literal substring.+contains :: T.Text -> Expr T.Text -> Expr Bool+contains needle expr =+ lift2Decorated (flip T.isInfixOf) "contains" Nothing False 4 expr (Lit needle)++-- | Character count.+length :: Expr T.Text -> Expr Int+length = liftDecorated T.length "textLength" Nothing+ splitOn :: T.Text -> Expr T.Text -> Expr [T.Text] splitOn delim = liftDecorated (T.splitOn delim) "splitOn" Nothing @@ -645,7 +644,7 @@ (\d1 d2 -> fromIntegral (diffDays d1 d2)) "daysBetween" Nothing- True+ False 2 bind ::@@ -656,24 +655,11 @@ Expr (m b) bind f = liftDecorated (>>= f) "bind" Nothing -{- | Window function: evaluate an expression partitioned by the given columns.--Each partition computes the inner expression independently, and the result-is broadcast back to every row in that partition. This is analogous to-Polars' @.over()@ or SQL @OVER (PARTITION BY ...)@.--@--- Per-country median, broadcast to every row:-F.over [\"country\"] (F.median (F.col \@Double \"amount\"))---- Deviation from group mean:-F.col \@Double \"amount\" - F.over [\"group\"] (F.mean (F.col \@Double \"amount\"))-@--}+-- | Window function: evaluate an expression partitioned by the given columns. over :: (Columnable a) => [T.Text] -> Expr a -> Expr a over = Over --- See Section 2.4 of the Haskell Report https://www.haskell.org/definition/haskell2010.pdf+-- Haskell Report 2.4: https://www.haskell.org/definition/haskell2010.pdf isReservedId :: T.Text -> Bool isReservedId t = case t of "case" -> True@@ -702,11 +688,7 @@ isVarId :: T.Text -> Bool isVarId t = case T.uncons t of- -- We might want to check c == '_' || Char.isLower c- -- since the haskell report considers '_' a lowercase character- -- However, to prevent an edge case where a user may have a- -- "Name" and an "_Name_" in the same scope, wherein we'd end up- -- with duplicate "_Name_"s, we eschew the check for '_' here.+ -- '_' is skipped on purpose so "Name" and "_Name_" cannot collide. Just (c, _) -> Char.isLower c && Char.isAlpha c Nothing -> False
src/DataFrame/Monad.hs view
@@ -44,7 +44,7 @@ import Control.Monad (void) import DataFrame.Internal.Column (Columnable) import DataFrame.Internal.DataFrame (DataFrame)-import DataFrame.Internal.Expression (Expr (..), UExpr (..), prettyPrint)+import DataFrame.Internal.Expression (Expr (..), SomeExpr (..), prettyPrint) import DataFrame.Internal.Expression.Operators.Nullable (BaseType) import qualified DataFrame.Operations.Core as D import DataFrame.Operations.Permutation (SortOrder)@@ -57,9 +57,7 @@ import Data.Tuple (swap) import System.Random --- A re-implementation of the state monad.--- `mtl` might be too heavy a dependency just to get--- a single monad instance.+-- A re-implementation of the state monad, avoiding an `mtl` dependency. newtype FrameM a = FrameM {runFrameM_ :: DataFrame -> (DataFrame, a)} instance Functor FrameM where@@ -153,7 +151,7 @@ execFrameM :: DataFrame -> FrameM a -> DataFrame execFrameM df m = snd (runFrameM df m) -newtype Pipeline a = Pipeline {unPipeline :: Int -> (Int, [(T.Text, UExpr)], a)}+newtype Pipeline a = Pipeline {unPipeline :: Int -> (Int, [(T.Text, SomeExpr)], a)} instance Functor Pipeline where fmap f (Pipeline g) = Pipeline $ \n -> let (n', w, a) = g n in (n', w, f a)@@ -173,18 +171,18 @@ -- | Derive a column under an explicit name, returning a reference to it. letAs :: (Columnable a) => T.Text -> Expr a -> Pipeline (Expr a)-letAs nm e = Pipeline (,[(nm, UExpr e)],Col nm)+letAs nm e = Pipeline (,[(nm, SomeExpr e)],Col nm) -- | Derive a column under a fresh generated name. letExpr :: (Columnable a) => Expr a -> Pipeline (Expr a) letExpr e = Pipeline $ \n ->- let nm = T.pack ('_' : 'v' : show n) in (n + 1, [(nm, UExpr e)], Col nm)+ let nm = T.pack ('_' : 'v' : show n) in (n + 1, [(nm, SomeExpr e)], Col nm) instance Show (Pipeline (Expr a)) where show p = let (_, steps, res) = unPipeline p 0 in concatMap- (\(nm, UExpr e) -> T.unpack nm ++ " = " ++ prettyPrint e ++ "\n")+ (\(nm, SomeExpr e) -> T.unpack nm ++ " = " ++ prettyPrint e ++ "\n") steps ++ "return " ++ prettyPrint res@@ -197,10 +195,8 @@ toFrameM :: Pipeline (Expr a) -> FrameM (Expr a) toFrameM p = let (_, steps, res) = unPipeline p 0- in mapM_ (\(nm, UExpr e) -> void (deriveM nm e)) steps >> pure res+ in mapM_ (\(nm, SomeExpr e) -> void (deriveM nm e)) steps >> pure res -{- | Run a pipeline over a frame: derive its columns and return the result-expression (a reference to the final column) and the resulting frame.--}+-- | Run a pipeline over a frame, returning the result expression and frame. runPipeline :: DataFrame -> Pipeline (Expr a) -> (Expr a, DataFrame) runPipeline df = runFrameM df . toFrameM
src/DataFrame/Operations/Aggregation.hs view
@@ -62,11 +62,7 @@ import DataFrame.Operations.Subset import System.IO.Unsafe (unsafePerformIO) -{- | Per-row key hash over the selected key columns. Delegates to the shared-'computeRowHashesIO' kernel, which forks over contiguous row ranges for large-frames (the hashing of a wide 1e7-row text/factor join key dominates that join)-and is bit-for-bit identical to a single sequential pass at any capability count.--}+-- | Per-row key hash over the selected key columns, computed in parallel. computeRowHashes :: [Int] -> DataFrame -> VU.Vector Int computeRowHashes indices df = let n = fst (dimensions df)@@ -74,30 +70,14 @@ in unsafePerformIO (computeRowHashesIO n selectedCols) {-# NOINLINE computeRowHashes #-} -{- | Aggregate a grouped dataframe using the expressions given.-All ungrouped columns will be dropped.--NOTE: this function deliberately never pattern-matches or strictly binds the-'Grouped' per-row fields (this module is compiled with @-XStrict@, whose strict-patterns and bindings would force them): on direct-grouped frames BOTH-'valueIndices' (the placement permutation) and 'rowToGroup' are deferred-thunks, and each aggregation path needs at most one of them — always passed as-un-forced argument expressions. Key columns materialize through 'groupRepRows'-(one representative row per group) instead of gathering-@valueIndices[offsets[g]]@.--}+-- | Aggregate a grouped dataframe using the expressions given. aggregate :: [NamedExpr] -> GroupedDataFrame -> DataFrame aggregate aggs gdf = let df = fullDataframe gdf offs = offsets gdf - {- Key columns materialize through ONE fused parallel gather over the- representative rows ('atIndicesStableMulti'): the 1e8-group Q10 result- was six sequential latency-bound random-gather passes as per-column- 'selectIndices'. Each result column is still identical to (and as- deferred as) the per-column gather; the row-count field uses the eager- @offsets@ so nothing here forces 'groupRepRows' early. -}+ -- Key columns: one fused parallel gather over the representative rows. df' = let sub = select (groupedColumns gdf) df in sub@@ -113,21 +93,7 @@ !nGroups = VU.length offs - 1 !nRows' = fst (dataframeDimensions df) - {- Fused multi-reduction fast path (Q3/Q4/Q5-shaped aggregates): every- recognised simple scatter reduction (sum/mean/count/min/max over a clean- unboxed Int/Double column) in this aggregate runs in ONE pass instead of- one full pass per expression. At or below 'streamGroupCap' groups that- pass streams over (rowToGroup, columns) with per-worker accumulators —- never touching the (possibly lazy) valueIndices; above it (necessarily a- hash-path grouping, whose valueIndices is already eager) the accumulator- arrays would thrash, so the pass gathers by disjoint group range instead- (register accumulators, bit-identical to the unfused gather kernels,- one traversal instead of one per expression). Only taken when at- least two reductions fuse; non-fusable expressions (median, var/std,- max-min, arbitrary DSL) keep their per-expression path below. -}- {- This binding is strict (-XStrict), so it must stay empty-and-cheap- whenever the moment path below already covers the aggregate — otherwise- the pass would run redundantly before the moment result is consulted. -}+ -- Under -XStrict this must stay cheap when the moment path covers it. fusedScatterCols :: MS.Map T.Text Column fusedScatterCols = case fusedMoments of Just _ -> MS.empty@@ -154,13 +120,7 @@ , Just ga <- [mkGatherAgg nGroups (valueIndices gdf) offs red c] ]- in {- Unlike the stream branch, a SINGLE candidate also- takes this path: the per-expression fallback is a- scatter over rowToGroup, which on a hash-path grouping- is now a deferred thunk — the gather kernel (documented- bit-identical to the unfused kernels) works off the- already-eager valueIndices instead and skips that whole- random-write pass. -}+ in -- Even a single candidate uses the gather path. if not (null cands) then MS.fromList@@ -175,12 +135,7 @@ ) else MS.empty - {- Fused median + std/var over one column (the Q6 shape): both are- holistic gathers over the same values, so one shared gather serves the- Welford fold and the median selection ('runMedianVarFused',- bit-identical to the separate kernels). Only built when a median and a- std/var on the same column appear together; empty-and-cheap otherwise- (same strictness caveat as 'fusedScatterCols'). -}+ -- Median and std/var on one column share a single gather. medianVarCols :: MS.Map T.Text Column medianVarCols = case fusedMoments of Just _ -> MS.empty@@ -202,9 +157,6 @@ ] -- Fast path: a recognised reduction scatters in one unboxed pass.- -- Anything 'planAgg' rejects keeps the existing interpreter, so the- -- general typed + DSL aggregate API stays correct for arbitrary- -- expressions. f ne@(name, uexpr) d = let value = case MS.lookup name medianVarCols of Just c -> c@@ -215,10 +167,7 @@ Nothing -> interpretNamed gdf ne in insertColumn name value d - -- Fused fast path: the Q9 regression family (count + five moment sums- -- of two base columns) becomes one scatter over the base columns,- -- dropping the derived product columns and the six separate folds.- -- 'planMoments' returns 'Nothing' on any other set, falling back below.+ -- Fused fast path: the Q9 regression family becomes one scatter. fusedMoments = do mp <- planMoments gdf aggs :: Maybe MomentPlan runMomentPlan gdf nGroups mp@@ -229,7 +178,7 @@ -- | The fall-back path: evaluate one named aggregation via the interpreter. interpretNamed :: GroupedDataFrame -> NamedExpr -> Column-interpretNamed gdf (_, UExpr (expr :: Expr a)) =+interpretNamed gdf (_, SomeExpr (expr :: Expr a)) = case interpretAggregation @a gdf expr of Left e -> throw e Right (UnAggregated _) -> throw $ UnaggregatedException (T.pack $ show expr)@@ -246,6 +195,5 @@ distinct :: DataFrame -> DataFrame distinct df = selectIndices (VU.map (indices VU.!) (VU.init os)) df where- -- The trailing field stays a wildcard: under -XStrict a named pattern- -- variable would force the (possibly deferred) rowToGroup thunk.+ -- Wildcard last field: a named pattern would force the deferred thunk. (Grouped _ _ indices os _) = groupBy (columnNames df) df
src/DataFrame/Operations/Aggregation/Run.hs view
@@ -4,27 +4,7 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} -{- | Execute a recognised aggregation plan ('AggPlan'), producing one result-column (length @nGroups@, canonical group order).--This is the dispatch layer: it owns the policy of WHICH kernel runs — the dense-direct-indexed one ('DataFrame.Internal.Aggregation.Kernel.Dense') when the-group domain is small enough, otherwise the group-range scatter-('DataFrame.Internal.Aggregation.Kernel.Scatter') — and handles the compound-@max - min@ combine and the holistic grouped median itself. The kernels carry no-policy of their own. A plan only reaches here once 'planAgg' verified the value-columns are clean unboxed Int/Double, so the @error@ branches are unreachable.--Every reduction takes the Round-5 grouping layout @(valueIndices, offsets)@ so-the parallel kernel can split the group-id range across capabilities with no-cross-worker merge. Each group's rows stay in original-row order within one-worker's range, so results are byte-identical to the sequential path at any @-N@.-(Two exceptions, both deterministic at a fixed @-N@: the direct streaming-Double sum/mean above the small-group cutoff, whose chunked partials change the-float summation order, and the direct var/std, which finalize from-(count, sum, sumsq) partials rather than the gather kernel's row-order Welford-recurrence; see 'DataFrame.Internal.AggKernelDirect'.)--}+-- | Execute a recognised aggregation plan, producing one result column. module DataFrame.Operations.Aggregation.Run ( runPlan, runMomentPlan,@@ -42,6 +22,7 @@ denseMaxMinusMin, denseReduce, )+import DataFrame.Internal.Aggregation.Kernel.Distinct (countDistinctGroups) import DataFrame.Internal.Aggregation.Kernel.Moments ( Moments (..), momentScatterPar,@@ -66,11 +47,7 @@ import System.IO.Unsafe (unsafePerformIO) import Type.Reflection (typeRep) -{- | Group-domain size at or below which the dense direct-indexed kernel is-chosen; wider domains go to the group-range scatter kernel. A @2^18@-slot-accumulator is replicated per worker there, which is what bounds this. Dispatch-policy, so it lives with the dispatcher rather than inside the kernel.--}+-- | Group-domain size at or below which the dense kernel is chosen. denseThreshold :: Int denseThreshold = 262144 @@ -78,15 +55,7 @@ runPlan gdf rtg nGroups plan = case plan of PlanScatter red name -> scatterColumn red name PlanMaxMinusMin a b ->- {- min/max are order-independent, so both fused single-pass kernels- (the direct streaming one up to 'streamGroupCap', the group-range- gather one above it) are exactly the two gather extrema they replace;- anything they reject (mixed/unclean columns, small inputs) keeps the- two-pass gather path. The streaming cap extends past 'denseThreshold'- for the same reason as the fused multi-reduction pass: on a- direct-grouped frame it works off the eager @rowToGroup@ and skips the- deferred @valueIndices@ placement entirely (measured at 1e6 groups /- 1e8 rows on -N16: stream 1.1s against placement 0.7s + gather 0.7s). -}+ -- min/max are order-independent, so the fused kernels are exact. let ca = col a cb = col b direct@@ -96,32 +65,17 @@ Just out -> out Nothing -> maxMinusMin vis offs nGroups ca cb PlanMedian name -> groupedMedian vis offs nGroups (col name)+ PlanCountDistinct name -> case countDistinctGroups rtg nGroups (col name) of+ Just counts -> fromUnboxedVector counts+ Nothing -> error "runPlan: countDistinctGroups rejected a planned column" where vis = valueIndices gdf offs = offsets gdf- {- The low-cardinality DENSE fast path: for a small dense domain the grouping- layer's @rowToGroup@ already maps row -> group, so we scatter straight off it- (no @valueIndices@ gather). 'denseReduce' admits the order-independent- reductions (exact partial merge, byte-identical to -N1) plus the streaming- Double sum/mean/var/std variants (byte-identical sequential row order at- small group counts; deterministic chunked partials for the large-domain- sum/mean — see "DataFrame.Internal.Aggregation.Kernel.Dense"); anything it- rejects keeps the order-preserving group-range kernel. -}+ -- Dense fast path: scatter straight off the eager @rowToGroup@. scatterColumn red name = let c = col name dense | nGroups <= denseThreshold = denseReduce red rtg nGroups c- {- Top-2 selection merges exactly (a multiset selection, no- float adds until finalize), so like the fused passes it streams- off @rowToGroup@ up to 'streamGroupCap': on a direct-grouped- frame that skips the deferred @valueIndices@ placement, which- costs more than the accumulator cache misses it saves- (measured at 1e6 groups / 1e8 rows on -N16: stream 1.0s- against placement 0.7s + gather 0.4s). RTop2Snd shares the- same accumulator machinery and merge-exactness. -}- | red == RTop2Sum || red == RTop2Snd- , nGroups <= streamGroupCap =- denseReduce red rtg nGroups c | otherwise = Nothing in case dense of Just out -> out@@ -132,26 +86,13 @@ Just c -> c Nothing -> error ("runPlan: planned column missing: " ++ T.unpack name) -{- | Run a recognised moment (Q9 regression) plan as one fused scatter over the-two base columns, returning each output name bound to its moment field. The six-sufficient statistics (count, Sx, Sy, Sxx, Syy, Sxy) come out of a single pass,-replacing the three derive passes and six independent scatters of the-per-expression path. The streaming kernel's count is exact; its five Double-sums accumulate per worker chunk in original row order and merge in fixed-worker order — deterministic at a fixed @-N@, float summation order chunk-major-rather than per-group (see 'momentStreamPar'). The gather fallback remains-byte-identical to the sequential kernel at any @-N@.--}+-- | Run a recognised moment plan as one fused scatter over two columns. runMomentPlan :: GroupedDataFrame -> Int -> MomentPlan -> Maybe [(T.Text, Column)] runMomentPlan gdf nGroups mp = do let cx = col (mpColX mp) cy = col (mpColY mp)- {- Preferred: the streaming kernel — one fused pass over rowToGroup and- the TYPED base columns (no sequential Int->Double materialization, no- valueIndices gather, so a direct-grouped frame never runs its placement- pass). Falls back to the gather kernel above 'streamGroupCap' or on- unclean columns. -}+ -- Preferred: the streaming kernel, one fused pass over rowToGroup. streamed = momentStreamPar (rowToGroup gdf) nGroups cx cy ms <- case streamed of Just m -> Just m@@ -171,10 +112,7 @@ Just c -> c Nothing -> error ("runMomentPlan: planned column missing: " ++ T.unpack name) -{- | @max a - min b@ on the small @nGroups@ arrays. Preserves the Int element-type of the source columns (matching the interpreter), falling back to a Double-combine otherwise.--}+-- | @max a - min b@ over the small per-group arrays. maxMinusMin :: VU.Vector Int -> VU.Vector Int -> Int -> Column -> Column -> Column maxMinusMin vis offs nGroups ca cb =@@ -208,24 +146,7 @@ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) -> VU.map fromIntegral v _ -> error "scatterExtremaDbl" ----------------------------------------------------------------------------------- Fused holistic median + var/std over one shared gather----------------------------------------------------------------------------------{- | Fused grouped median and variance family over the SAME column: one gather-into the shared scratch buffer serves both. Returns-@(median, variance, stddev)@ columns, or 'Nothing' on a non-numeric column-(the caller keeps the separate per-expression kernels).--The Welford fold runs over each gathered slice in ascending original-row order-— exactly the recurrence, order and finalize of the var/std kernels-('DataFrame.Internal.AggKernelPar.varPar' and the sequential @varScatter@,-which agree bit-for-bit) — BEFORE the in-place median selection permutes the-slice, and the selection then proceeds exactly as 'groupedMedian'. Both-outputs are therefore bit-identical to the unfused paths; the second full-gather pass is what the fusion saves (measured ~35% off the median+sd pair at-1e4 groups / 1e8 rows on -N16).--}+-- | Fused grouped median and variance family over the same column. runMedianVarFused :: GroupedDataFrame -> Int -> Column -> Maybe (Column, Column, Column) runMedianVarFused gdf nGroups c = do@@ -277,7 +198,7 @@ welford (pos + 1) c' mu' mm' var <- welford s (0 :: Int) 0 0 VUM.unsafeWrite varOut g var- -- Median selection, as in 'medianByGroup' (permutes the slice).+ -- Median selection, as in 'medianByGroup'. let slice = VUM.unsafeSlice s len buf !mid = len `div` 2 VA.select slice (mid + 1)@@ -298,20 +219,7 @@ pure (med, var) {-# NOINLINE medianVarByGroup #-} ----------------------------------------------------------------------------------- Parallel holistic median----------------------------------------------------------------------------------{- | Holistic per-group median over a single unboxed Int/Double column. The-@valueIndices@/@offsets@ layout already places each group's rows in a contiguous-run, so we copy each group's values into a scratch buffer at its own offset and-select the median-rank order statistics in that slice in place (O(len) per-group rather than the O(len log len) full sort) — each group's slice is-independent, so the per-group selections split across capabilities by group-range with no merge. Order statistics are value-determined, so the result is-identical to the sorting variant. Empty groups never occur, so the result is-total.--}+-- | Holistic per-group median over a single unboxed Int or Double column. groupedMedian :: VU.Vector Int -> VU.Vector Int -> Int -> Column -> Column groupedMedian vis offs nGroups c = case cleanDoubleVector c of Nothing -> error "groupedMedian: non-numeric planned column"@@ -325,8 +233,7 @@ out <- VUM.new (max 1 nGroups) caps <- getNumCapabilities let !bounds = groupRangeBounds offs nGroups caps- -- Each worker fills+sorts the buffer slices of its own group range, then- -- writes that range's medians. Disjoint ranges => safe to parallelise.+ -- Each worker sorts and takes medians of its own disjoint group range. parallelBounds_ caps bounds $ \gs ge -> let grp !g | g >= ge = pure ()@@ -342,9 +249,7 @@ fill s let slice = VUM.unsafeSlice s len buf !mid = len `div` 2- {- Move the least mid+1 values to the front (in no- particular order); the two largest of those are the order- statistics at sorted positions mid and mid-1. -}+ -- Move the least mid+1 values to the front, unordered. VA.select slice (mid + 1) let scan !i !hi !lo | i > mid = pure (hi, lo)@@ -361,13 +266,7 @@ VU.unsafeFreeze (VUM.unsafeSlice 0 nGroups out) {-# NOINLINE medianByGroup #-} ----------------------------------------------------------------------------------- Group-range partitioning (shared with the median path)----------------------------------------------------------------------------------{- | Split @[0, nGroups)@ into @caps@ contiguous group ranges balanced by row-count. Identical policy to 'DataFrame.Internal.Aggregation.Kernel.Scatter.groupRangeBounds'.--}+-- | Split the groups into contiguous ranges balanced by row count. groupRangeBounds :: VU.Vector Int -> Int -> Int -> VU.Vector Int groupRangeBounds offs nGroups caps = VU.create $ do b <- VUM.new (caps + 1)
src/DataFrame/Operations/Core.hs view
@@ -86,7 +86,7 @@ columnNames, derivingExpressions, empty,- fromNamedColumns,+ fromColumns, getColumn, insertColumn, null,@@ -98,94 +98,22 @@ import Type.Reflection import Prelude hiding (null) -{- | O(1) Get DataFrame dimensions i.e. (rows, columns)--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> df = D.fromNamedColumns [("a", D.fromList [1..100]), ("b", D.fromList [1..100]), ("c", D.fromList [1..100])]->>> D.dimensions df--(100, 3)-@--}+-- | O(1) Get DataFrame dimensions, i.e. (rows, columns). dimensions :: DataFrame -> (Int, Int) dimensions = dataframeDimensions {-# INLINE dimensions #-} -{- | O(1) Get number of rows in a dataframe.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> df = D.fromNamedColumns [("a", D.fromList [1..100]), ("b", D.fromList [1..100]), ("c", D.fromList [1..100])]->>> D.nRows df-100-@--}+-- | O(1) Get the number of rows in a dataframe. nRows :: DataFrame -> Int nRows = fst . dataframeDimensions -{- | O(1) Get number of columns in a dataframe.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> df = D.fromNamedColumns [("a", D.fromList [1..100]), ("b", D.fromList [1..100]), ("c", D.fromList [1..100])]->>> D.nColumns df-3-@--}+-- | O(1) Get the number of columns in a dataframe. nColumns :: DataFrame -> Int nColumns = snd . dataframeDimensions -{- | O(k) Get column names of the DataFrame in order of insertion.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> df = D.fromNamedColumns [("a", D.fromList [1..100]), ("b", D.fromList [1..100]), ("c", D.fromList [1..100])]->>> D.columnNames df--["a", "b", "c"]-@--}---- 'columnNames' is now defined in "DataFrame.Internal.DataFrame" and--- re-exported from "DataFrame" at the top level.--{- | Adds a vector as a named column. Size mismatches are reconciled by making-the shorter side nullable (`Maybe a`) and padding with `Nothing`.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> import qualified Data.Vector as V->>> D.insertVector "numbers" (V.fromList [(1 :: Int)..10]) D.empty----------- numbers---------- Int---------- 1- 2- 3- 4- 5- 6- 7- 8- 9- 10+-- 'columnNames' lives in "DataFrame.Internal.DataFrame" (re-exported). -@--}+-- | Add a vector as a named column; size mismatches pad with Nothing. insertVector :: forall a. (Columnable a) =>@@ -199,34 +127,7 @@ insertVector name xs = insertColumn name (fromVector xs) {-# INLINE insertVector #-} -{- | Adds a foldable collection as a named column. Size mismatches are reconciled by-making the shorter side nullable (`Maybe a`) and padding with `Nothing`.-Do not pass infinite collections: they are fully forced.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> D.insert "numbers" [(1 :: Int)..10] D.empty----------- numbers---------- Int---------- 1- 2- 3- 4- 5- 6- 7- 8- 9- 10--@--}+-- | Add a foldable as a named column; do not pass infinite collections. insert :: forall a t. (Columnable a, Foldable t) =>@@ -240,34 +141,7 @@ insert name xs = insertColumn name (fromList (Fold.foldr' (:) [] xs)) {-# INLINE insert #-} -{- | Adds a vector to the dataframe and pads it with a default value if it has less elements than the number of rows.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified Data.Vector as V->>> import qualified DataFrame as D->>> df = D.fromNamedColumns [("x", D.fromList [(1 :: Int)..10])]->>> D.insertVectorWithDefault 0 "numbers" (V.fromList [(1 :: Int),2,3]) df---------------- x | numbers-----|---------Int | Int-----|---------1 | 1-2 | 2-3 | 3-4 | 0-5 | 0-6 | 0-7 | 0-8 | 0-9 | 0-10 | 0--@--}+-- | Add a vector, padding with a default if it has fewer elements than rows. insertVectorWithDefault :: forall a. (Columnable a) =>@@ -285,33 +159,7 @@ values = xs V.++ V.replicate (rows - V.length xs) defaultValue in insertColumn name (fromVector values) d -{- | Adds a list to the dataframe and pads it with a default value if it has less elements than the number of rows.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> df = D.fromNamedColumns [("x", D.fromList [(1 :: Int)..10])]->>> D.insertWithDefault 0 "numbers" [(1 :: Int),2,3] df---------------- x | numbers-----|---------Int | Int-----|---------1 | 1-2 | 2-3 | 3-4 | 0-5 | 0-6 | 0-7 | 0-8 | 0-9 | 0-10 | 0--@--}+-- | Add a list, padding with a default if it has fewer elements than rows. insertWithDefault :: forall a t. (Columnable a, Foldable t) =>@@ -330,9 +178,7 @@ values = xs' ++ replicate (rows - length xs') defaultValue in insertColumn name (fromList values) d -{- | /O(n)/ Like 'insertVector' but takes an already-unboxed vector,-skipping the boxed-to-unboxed conversion 'insertVector' would do for numbers.--}+-- | /O(n)/ Like 'insertVector' but takes an already-unboxed vector. insertUnboxedVector :: forall a. (Columnable a, VU.Unbox a) =>@@ -345,62 +191,9 @@ DataFrame insertUnboxedVector name xs = insertColumn name (UnboxedColumn Nothing xs) -{- | /O(n)/ Add a column to the dataframe.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> D.insertColumn "numbers" (D.fromList [(1 :: Int)..10]) D.empty----------- numbers---------- Int---------- 1- 2- 3- 4- 5- 6- 7- 8- 9- 10--@--}- -- 'insertColumn' is now defined in "DataFrame.Internal.DataFrame". -{- | /O(n)/ Clones a column and places it under a new name in the dataframe.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified Data.Vector as V->>> df = insertVector "numbers" (V.fromList [1..10]) D.empty->>> D.cloneColumn "numbers" "others" df-------------------- numbers | others----------|-------- Int | Int----------|-------- 1 | 1- 2 | 2- 3 | 3- 4 | 4- 5 | 5- 6 | 6- 7 | 7- 8 | 8- 9 | 9- 10 | 10--@--}+-- | /O(n)/ Clone a column and place it under a new name. cloneColumn :: T.Text -> T.Text -> DataFrame -> DataFrame cloneColumn original new df | null df = throw (EmptyDataSetException "cloneColumn")@@ -412,83 +205,11 @@ column <- getColumn original df return $ insertColumn new column df -{- | /O(n)/ Renames a single column.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> import qualified Data.Vector as V->>> df = insertVector "numbers" (V.fromList [1..10]) D.empty->>> D.rename "numbers" "others" df---------- others--------- Int--------- 1- 2- 3- 4- 5- 6- 7- 8- 9- 10--@--}+-- | /O(n)/ Rename a single column. rename :: T.Text -> T.Text -> DataFrame -> DataFrame rename orig new df = either throw id (renameSafe orig new df) -{- | /O(n)/ Renames many columns.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> import qualified Data.Vector as V->>> df = D.insertVector "others" (V.fromList [11..20]) (D.insertVector "numbers" (V.fromList [1..10]) D.empty)->>> df-------------------- numbers | others----------|-------- Int | Int----------|-------- 1 | 11- 2 | 12- 3 | 13- 4 | 14- 5 | 15- 6 | 16- 7 | 17- 8 | 18- 9 | 19- 10 | 20-->>> D.renameMany [("numbers", "first_10"), ("others", "next_10")] df---------------------- first_10 | next_10-----------|--------- Int | Int-----------|--------- 1 | 11- 2 | 12- 3 | 13- 4 | 14- 5 | 15- 6 | 16- 7 | 17- 8 | 18- 9 | 19- 10 | 20--@--}+-- | /O(n)/ Rename many columns. renameMany :: [(T.Text, T.Text)] -> DataFrame -> DataFrame renameMany = fold (uncurry rename) @@ -511,24 +232,7 @@ , typeOfColumn :: !T.Text } -{- | O(n * k ^ 2) Returns the number of non-null columns in the dataframe and the type associated with each column.--==== __Example__-@->>> import qualified Data.Vector as V->>> df = D.insertVector "others" (V.fromList [11..20]) (D.insertVector "numbers" (V.fromList [1..10]) D.empty)->>> D.describeColumns df----------------------------------------------------------- Column Name | # Non-null Values | # Null Values | Type--------------|-------------------|---------------|------ Text | Int | Int | Text--------------|-------------------|---------------|------ others | 10 | 0 | Int- numbers | 10 | 0 | Int--@--}+-- | O(n * k ^ 2) Count non-null values and the type of each column. describeColumns :: DataFrame -> DataFrame describeColumns df = empty@@ -590,81 +294,13 @@ nulls c@(PackedText _ _) = nulls (materializePacked c) nulls _ = 0 -{- | Creates a dataframe from a list of tuples with name and column.--==== __Example__-@->>> df = D.fromNamedColumns [("numbers", D.fromList [1..10]), ("others", D.fromList [11..20])]->>> df------------------- numbers | others----------|-------- Int | Int----------|-------- 1 | 11- 2 | 12- 3 | 13- 4 | 14- 5 | 15- 6 | 16- 7 | 17- 8 | 18- 9 | 19- 10 | 20--@--}---- 'fromNamedColumns' is now defined in "DataFrame.Internal.DataFrame".--{- | Create a dataframe from a list of columns. The column names are "0", "1"... etc.-Useful for quick exploration but you should probably always rename the columns after-or drop the ones you don't want.--==== __Example__-@->>> df = D.fromUnnamedColumns [D.fromList [1..10], D.fromList [11..20]]->>> df------------------- 0 | 1------|----- Int | Int------|----- 1 | 11- 2 | 12- 3 | 13- 4 | 14- 5 | 15- 6 | 16- 7 | 17- 8 | 18- 9 | 19- 10 | 20+-- 'fromColumns' is now defined in "DataFrame.Internal.DataFrame". -@--}+-- | Create a dataframe from a list of columns named "0", "1", and so on. fromUnnamedColumns :: [Column] -> DataFrame-fromUnnamedColumns = fromNamedColumns . zip (map (T.pack . show) [(0 :: Int) ..])--{- | Create a dataframe from a list of column names and rows.--==== __Example__-@->>> df = D.fromRows ["A", "B"] [[D.toAny 1, D.toAny 11], [D.toAny 2, D.toAny 12], [D.toAny 3, D.toAny 13]]-->>> df------------- A | B------|----- Int | Int------|----- 1 | 11- 2 | 12- 3 | 13+fromUnnamedColumns = fromColumns . zip (map (T.pack . show) [(0 :: Int) ..]) -@--}+-- | Create a dataframe from a list of column names and rows. fromRows :: [T.Text] -> [[Any]] -> DataFrame fromRows names rows = L.foldl'@@ -672,18 +308,7 @@ empty (zip [0 ..] names) -{- | O (k * n) Counts the occurences of each value in a given column.--==== __Example__-@->>> df = D.fromUnnamedColumns [D.fromList [1..10], D.fromList [11..20]]-->>> D.valueCounts @Int "0" df--[(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,1),(10,1)]--@--}+-- | O (k * n) Count the occurrences of each value in a given column. valueCounts :: forall a. (Ord a, Columnable a) => Expr a -> DataFrame -> [(a, Int)] valueCounts expr df@@ -696,18 +321,7 @@ in M.toAscList column -{- | O (k * n) Shows the proportions of each value in a given column.--==== __Example__-@->>> df = D.fromUnnamedColumns [D.fromList [1..10], D.fromList [11..20]]-->>> D.valueCounts @Int "0" df--[(1,0.1),(2,0.1),(3,0.1),(4,0.1),(5,0.1),(6,0.1),(7,0.1),(8,0.1),(9,0.1),(10,0.1)]--@--}+-- | O (k * n) Show the proportion of each value in a given column. valueProportions :: forall a. (Ord a, Columnable a) => Expr a -> DataFrame -> [(a, Double)] valueProportions expr df@@ -723,50 +337,11 @@ in map (fmap ((/ total) . fromIntegral)) counts -{- | A left fold for dataframes that takes the dataframe as the last object.-This makes it easier to chain operations.--==== __Example__-@->>> df = D.fromNamedColumns [("x", D.fromList [1..100]), ("y", D.fromList [11..110])]->>> D.fold D.dropLast [1..5] df------------ x | y-----|-----Int | Int-----|-----1 | 11-2 | 12-3 | 13-4 | 14-5 | 15-6 | 16-7 | 17-8 | 18-9 | 19-10 | 20-11 | 21-12 | 22-13 | 23-14 | 24-15 | 25-16 | 26-17 | 27-18 | 28-19 | 29-20 | 30--Showing 20 rows out of 85--@--}+-- | A left fold for dataframes that takes the dataframe as the last argument. fold :: (a -> DataFrame -> DataFrame) -> [a] -> DataFrame -> DataFrame fold f xs acc = L.foldl' (flip f) acc xs -{- | The dataframe as a row-major matrix of floats, for handing data to ML systems.-'Left' if any column cannot be converted to floats.--}+-- | The dataframe as a row-major float matrix, 'Left' if conversion fails. toFloatMatrix :: DataFrame -> Either DataFrameException (V.Vector (VU.Vector Float)) toFloatMatrix df = case V.foldl'@@ -780,9 +355,7 @@ (fst (dataframeDimensions df)) (\i -> VU.generate (V.length m) (\j -> (m VG.! j) VG.! i)) -{- | The dataframe as a row-major matrix of doubles, for handing data to ML systems.-'Left' if any column cannot be converted to doubles.--}+-- | The dataframe as a row-major double matrix, 'Left' if conversion fails. toDoubleMatrix :: DataFrame -> Either DataFrameException (V.Vector (VU.Vector Double)) toDoubleMatrix df = case V.foldl'@@ -796,9 +369,7 @@ (fst (dataframeDimensions df)) (\i -> VU.generate (V.length m) (\j -> (m VG.! j) VG.! i)) -{- | The dataframe as a row-major matrix of ints, for handing data to ML systems.-'Left' if any column cannot be converted to ints.--}+-- | The dataframe as a row-major int matrix, 'Left' if conversion fails. toIntMatrix :: DataFrame -> Either DataFrameException (V.Vector (VU.Vector Int)) toIntMatrix df = case V.foldl' (\acc c -> V.snoc <$> acc <*> toIntVector c)@@ -821,18 +392,7 @@ orFallback Nothing = Just (T.unpack name) withColumnName _ result = result -{- | Get a specific column as a vector.--You must specify the type via type applications.--==== __Examples__-->>> columnAsVector (F.col @Int "age") df-Right [25, 30, 35, ...]-->>> columnAsVector (F.col @Text "name") df-Right ["Alice", "Bob", "Charlie", ...]--}+-- | Get a specific column as a vector; the type comes from type application. columnAsVector :: forall a. (Columnable a) => Expr a -> DataFrame -> Either DataFrameException (V.Vector a)@@ -848,9 +408,7 @@ Left e -> throw e Right (TColumn col) -> toVector col -{- | A column as an unboxed vector of 'Int' values.-'Left' if the column cannot be converted to ints (non-numeric or out of range).--}+-- | A column as an unboxed vector of 'Int', 'Left' if not convertible. columnAsIntVector :: (Columnable a, Num a) => Expr a -> DataFrame -> Either DataFrameException (VU.Vector Int)@@ -863,9 +421,7 @@ Left e -> throw e Right (TColumn col) -> toIntVector col -{- | A column as an unboxed vector of 'Double' values.-'Left' if the column cannot be converted to doubles (e.g. non-numeric data).--}+-- | A column as an unboxed vector of 'Double', 'Left' if not convertible. columnAsDoubleVector :: (Columnable a, Num a) => Expr a -> DataFrame -> Either DataFrameException (VU.Vector Double)@@ -881,9 +437,7 @@ Left e -> throw e Right (TColumn col) -> toDoubleVector col -{- | A column as an unboxed vector of 'Float' values.-'Left' if the column cannot be converted to floats (e.g. non-numeric data).--}+-- | A column as an unboxed vector of 'Float', 'Left' if not convertible. columnAsFloatVector :: (Columnable a, Num a) => Expr a -> DataFrame -> Either DataFrameException (VU.Vector Float)@@ -919,30 +473,10 @@ #-} {-# INLINE columnAsUnboxedVector #-} -{- | Get a specific column as a list.--You must specify the type via type applications.--==== __Examples__-->>> columnAsList @Int "age" df-[25, 30, 35, ...]-->>> columnAsList @Text "name" df-["Alice", "Bob", "Charlie", ...]--==== __Throws__--* 'error' - if the column type doesn't match the requested type--}+-- | Get a specific column as a list; the type comes from type application. columnAsList :: forall a. (Columnable a) => Expr a -> DataFrame -> [a] columnAsList expr df = either throw V.toList (columnAsVector expr df) -{- | Returns the provenance of all columns in the DataFrame as a list of-@(name, expression)@ pairs. Derived columns show their expression;-raw columns show an identity @col \@type name@ expression.--}- -- TODO: mchavinda - Expand out these expressions if possible. showDerivedExpressions :: DataFrame -> [NamedExpr] showDerivedExpressions df =@@ -950,19 +484,19 @@ names = columnNames df toNamedExpr name = case M.lookup name exprs of Just uexpr -> (name, uexpr)- Nothing -> (name, identityUExpr name)+ Nothing -> (name, identityExpr name) in map toNamedExpr names where- identityUExpr name = case getColumn name df of- Just (BoxedColumn (Just _) (_ :: V.Vector a)) -> UExpr (Col @(Maybe a) name)- Just (BoxedColumn Nothing (_ :: V.Vector a)) -> UExpr (Col @a name)- Just (UnboxedColumn (Just _) (_ :: VU.Vector a)) -> UExpr (Col @(Maybe a) name)- Just (UnboxedColumn Nothing (_ :: VU.Vector a)) -> UExpr (Col @a name)- Just (PackedText (Just _) _) -> UExpr (Col @(Maybe T.Text) name)- Just (PackedText Nothing _) -> UExpr (Col @T.Text name)+ identityExpr name = case getColumn name df of+ Just (BoxedColumn (Just _) (_ :: V.Vector a)) -> SomeExpr (Col @(Maybe a) name)+ Just (BoxedColumn Nothing (_ :: V.Vector a)) -> SomeExpr (Col @a name)+ Just (UnboxedColumn (Just _) (_ :: VU.Vector a)) -> SomeExpr (Col @(Maybe a) name)+ Just (UnboxedColumn Nothing (_ :: VU.Vector a)) -> SomeExpr (Col @a name)+ Just (PackedText (Just _) _) -> SomeExpr (Col @(Maybe T.Text) name)+ Just (PackedText Nothing _) -> SomeExpr (Col @T.Text name) Just c@(MergedColumn _ _) -> case mergedHead c of- BoxedColumn (Just _) (_ :: V.Vector a) -> UExpr (Col @(Maybe a) name)- BoxedColumn Nothing (_ :: V.Vector a) -> UExpr (Col @a name)+ BoxedColumn (Just _) (_ :: V.Vector a) -> SomeExpr (Col @(Maybe a) name)+ BoxedColumn Nothing (_ :: V.Vector a) -> SomeExpr (Col @a name) _ -> error $ "showDerivedExpressions: merged column did not materialize boxed: "
src/DataFrame/Operations/Inference.hs view
@@ -44,7 +44,7 @@ parseTimeOpt :: DateFormat -> T.Text -> Maybe Day parseTimeOpt dateFormat s =- parseTimeM {- Accept leading/trailing whitespace -}+ parseTimeM True defaultTimeLocale dateFormat@@ -127,9 +127,7 @@ (parseD . V.unsafeIndex vec) {-# INLINE promoteIntColumn #-} -{- | 'promoteIntColumn' over row indices instead of a materialized cell-vector (used by readers that keep cells as buffer offsets).--}+-- | 'promoteIntColumn' over row indices instead of a materialized cell vector. promoteIntColumnIndexed :: Int -> (Int -> Bool) ->@@ -156,8 +154,7 @@ Just dv -> promote i anyNull dv Nothing -> return Nothing - -- Switch to Double at row i: convert the Int prefix, then- -- continue parsing the remaining cells as Double.+ -- Switch to Double at row i: convert the Int prefix, then keep going. promote !i !anyNull !dv = do dvalues <- VUM.unsafeNew n let copy !j
src/DataFrame/Operations/Join.hs view
@@ -21,9 +21,7 @@ -- * Low-level join kernels - {- | Reused by the lazy executor and the parallel-join tests; not a- stable public API (candidates for a future @Join.Internal@ split).- -}+ -- | Reused by the lazy executor and tests; not a stable public API. buildHashColumn, buildCompactIndex, hashProbeKernel,@@ -101,26 +99,16 @@ join RIGHT xs right = rightJoin xs right join FULL_OUTER xs right = fullOuterJoin xs right -{- | Build-side row count above which the single-threaded hash join gives way-to sort-merge (single-threaded) or the parallel chunked-probe hash join when-multiple capabilities are available (see 'shouldParallelizeJoin').--}+-- | Build-side row count above which the hash join switches strategy. joinStrategyThreshold :: Int joinStrategyThreshold = 500_000 -{- | Maps hash values to contiguous slices of original row indices. Indices-live in one hash-sorted vector; an open-addressing linear-probe table (keyed-by hash) records each run's offset and length. @-1@ starts mark empty slots.--}+-- | Maps hash values to contiguous slices of original row indices. data CompactIndex = CompactIndex { ciSortedIndices :: {-# UNPACK #-} !(VU.Vector Int) , ciKeys :: {-# UNPACK #-} !(VU.Vector Int) , ciRuns :: {-# UNPACK #-} !(VU.Vector Int)- {- ^ @(start, len)@ of each run packed 32/32 into one 'Int'; @-1@ = empty- slot. One vector instead of separate starts\/lens halves the table's- memory (a 1e8-row build side is a 2^28-slot table: 2.1GB instead of- 4.3GB) and saves a random cache-line read per lookup hit.- -}+ -- ^ @(start, len)@ of each run packed 32/32 into one 'Int'; @-1@ = empty. , ciMask :: {-# UNPACK #-} !Int } @@ -139,12 +127,7 @@ ciRunLen !w = w .&. 0xFFFF_FFFF {-# INLINE ciRunLen #-} -{- | Home slot of a hash: the top @log2 cap@ bits of a Fibonacci multiply.-The row hash's final FxHash step is a multiply, which leaves its LOW bits-poorly diffused (text keys cluster catastrophically: contiguous-pileup chains-in the hundreds), so the table must never index by @h .&. mask@ directly.-@shift@ is @64 - log2 cap@.--}+-- | Home slot of a hash: top bits of a Fibonacci multiply, not low bits. ciSlot :: Int -> Int -> Int ciSlot !shift !h = fromIntegral@@ -156,9 +139,7 @@ ciShiftFor !mask = 64 - popCount mask {-# INLINE ciShiftFor #-} -{- | Look up a hash in the open-addressing table.-Returns @(start, len)@ of the matching run, or @(-1, 0)@ on a miss.--}+-- | Look up a hash, returning the run's (start, len) or (-1, 0) on a miss. ciLookup :: CompactIndex -> Int -> (Int, Int) ciLookup ci !h = go (ciSlot shift h) where@@ -176,9 +157,7 @@ else go ((slot + 1) .&. mask) {-# INLINE ciLookup #-} -{- | Smallest power of two strictly greater than @n@, at least 2.-Used to size the open-addressing table so the load factor stays below ~0.5.--}+-- | Smallest power of two strictly greater than @n@, at least 2. nextPow2Above :: Int -> Int nextPow2Above n = go 2 where@@ -186,10 +165,7 @@ | p > n = p | otherwise = go (p * 2) -{- | Build a compact index from a vector of row hashes: sort by hash, scan for-contiguous runs, insert each run into an open-addressing table. Capacity is-sized for the worst case (every row distinct) so building never resizes.--}+-- | Build a compact index from row hashes by sorting and scanning for runs. buildCompactIndex :: VU.Vector Int -> CompactIndex buildCompactIndex hashes | VU.length hashes > 0x7FFF_FFFF =@@ -232,9 +208,7 @@ | otherwise = j {-# INLINE findGroupEnd #-} -{- | Sort a hash vector, returning sorted hashes and their original indices.-Sorts an index array keyed by hash, avoiding an intermediate pair vector.--}+-- | Sort a hash vector, returning sorted hashes and their original indices. sortWithIndices :: VU.Vector Int -> (VU.Vector Int, VU.Vector Int) sortWithIndices hashes = runST $ do let n = VU.length hashes@@ -287,38 +261,13 @@ [] -> M.elems $ M.restrictKeys columnIdxs csSet _ -> throw (ColumnsNotFoundException missingKeys callPoint (M.keys columnIdxs)) --- ============================================================--- Inner Join--- ============================================================--{- | Performs an inner join on two dataframes using the specified key columns.-Returns only rows where the key values exist in both dataframes.--==== __Example__-@-ghci> df = D.fromNamedColumns [("key", D.fromList ["K0", "K1", "K2", "K3"]), ("A", D.fromList ["A0", "A1", "A2", "A3"])]-ghci> other = D.fromNamedColumns [("key", D.fromList ["K0", "K1", "K2"]), ("B", D.fromList ["B0", "B1", "B2"])]-ghci> D.innerJoin ["key"] df other-------------------- key | A | B-------|-----|----- Text | Text| Text-------|-----|----- K0 | A0 | B0- K1 | A1 | B1- K2 | A2 | B2--@--}+-- | Inner join on two dataframes using the specified key columns. innerJoin :: [T.Text] -> DataFrame -> DataFrame -> DataFrame innerJoin cs left right | D.null right || D.null left = D.empty | otherwise = innerJoinNonEmpty cs left right --- Build the hash index on the smaller side and probe with the larger one;--- the probe side is partitioned across cores, so probing the larger maximises--- the parallel win.+-- Build the hash index on the smaller side and probe with the larger one. innerJoinNonEmpty :: [T.Text] -> DataFrame -> DataFrame -> DataFrame innerJoinNonEmpty cs left right = let@@ -347,20 +296,14 @@ in assembleInner csSet left right leftIxs rightIxs -{- | Select the inner-join kernel: parallel chunked-probe hash join when the-caller decided the probe side is large enough and multi-core, otherwise the-sequential single-threaded hash kernel.--}+-- | Select the inner-join kernel: parallel probe when large, else sequential. innerKernel :: Bool -> VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int) innerKernel True = parInnerKernel innerKernel False = hashInnerKernel {-# INLINE innerKernel #-} -{- | Parallel inner-join kernel: build the 'CompactIndex' on @buildHashes@ once,-then probe @probeHashes@ in parallel. Output is bit-for-bit identical to-'hashInnerKernel' (probe-row order preserved).--}+-- | Parallel inner-join kernel: build the index once, probe in parallel. parInnerKernel :: VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int) parInnerKernel probeHashes buildHashes =@@ -370,9 +313,7 @@ in (pf, bf) {-# NOINLINE parInnerKernel #-} -{- | The raw table fields of a 'CompactIndex', in the closure-free shape the-parallel probe kernels consume.--}+-- | The raw table fields of a 'CompactIndex' for the parallel probe kernels. ciProbeTable :: CompactIndex -> ProbeTable ciProbeTable ci = ProbeTable@@ -389,10 +330,7 @@ keyIdxs = validatedKeyColIndices "buildHashColumn" csSet df in D.computeRowHashes keyIdxs df -{- | Probe one batch of rows against a pre-built 'CompactIndex', returning-@(probeExpandedIxs, buildExpandedIxs)@. Unlike 'hashInnerKernel' it neither-builds the index nor guards cross-product size — the caller sizes batches.--}+-- | Probe a batch of rows against a pre-built 'CompactIndex'. hashProbeKernel :: -- | Built once from the full right\/build side. CompactIndex ->@@ -456,14 +394,7 @@ <*> VU.unsafeFreeze (VUM.slice 0 total bv) in (VU.force pFrozen, VU.force bFrozen) -{- | Hash-based inner join kernel: build the index on @buildHashes@, probe with-@probeHashes@, return @(probeExpandedIndices, buildExpandedIndices)@. Grows its-output buffer dynamically rather than pre-allocating the full cross product.--}--{- | Output-row ceiling for a join kernel; exceeding it signals a cross-product-explosion (e.g. low-cardinality keys).--}+-- | Output-row ceiling for a join kernel, guarding cross-product explosion. maxJoinOutputRows :: Int maxJoinOutputRows = 500_000_000 @@ -534,10 +465,7 @@ <*> VU.unsafeFreeze (VUM.slice 0 total bv) in (VU.force pFrozen, VU.force bFrozen) -{- | Sort-merge inner join kernel: sort both sides by hash, walk in lockstep,-return @(leftExpandedIndices, rightExpandedIndices)@. Grows its output buffer-dynamically so low-cardinality cross products don't OOM.--}+-- | Sort-merge inner join kernel returning left and right expanded indices. sortMergeInnerKernel :: VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int) sortMergeInnerKernel leftHashes rightHashes =@@ -623,7 +551,7 @@ <*> VU.unsafeFreeze (VUM.slice 0 total rv) in (VU.force lFrozen, VU.force rFrozen) --- | Assemble the result DataFrame for an inner join from expanded index vectors.+-- | Assemble the inner join result DataFrame from expanded index vectors. assembleInner :: S.Set T.Text -> DataFrame ->@@ -673,32 +601,7 @@ rightColNames baseDf --- ============================================================--- Left Join--- ============================================================--{- | Performs a left join on two dataframes using the specified key columns.-Returns all rows from the left dataframe, with matching rows from the right dataframe.-Non-matching rows will have Nothing/null values for columns from the right dataframe.--==== __Example__-@-ghci> df = D.fromNamedColumns [("key", D.fromList ["K0", "K1", "K2", "K3"]), ("A", D.fromList ["A0", "A1", "A2", "A3"])]-ghci> other = D.fromNamedColumns [("key", D.fromList ["K0", "K1", "K2"]), ("B", D.fromList ["B0", "B1", "B2"])]-ghci> D.leftJoin ["key"] df other--------------------------- key | A | B-------|-----|----------- Text | Text| Maybe Text-------|-----|----------- K0 | A0 | Just "B0"- K1 | A1 | Just "B1"- K2 | A2 | Just "B2"- K3 | A3 | Nothing--@--}+-- | Left join on two dataframes, keeping every row from the left dataframe. leftJoin :: [T.Text] -> DataFrame -> DataFrame -> DataFrame leftJoin = leftJoinWithCallPoint "leftJoin" @@ -709,8 +612,7 @@ | D.null left || D.nRows left == 0 = D.empty | otherwise = leftJoinNonEmpty callPoint cs left right --- The right side is always the build side; the left is probed (and drives--- parallelism). Unmatched right rows are marked with a @-1@ sentinel.+-- The right side is the build side; unmatched right rows get a @-1@ sentinel. leftJoinNonEmpty :: T.Text -> [T.Text] -> DataFrame -> DataFrame -> DataFrame leftJoinNonEmpty callPoint cs left right = let@@ -736,10 +638,7 @@ in assembleLeft csSet left right leftIxs rightIxs -{- | Parallel left-join kernel: build the 'CompactIndex' on @rightHashes@ once,-then probe @leftHashes@ in parallel. Bit-for-bit identical output to-'hashLeftKernel' (left-row order preserved, @-1@ sentinel for misses).--}+-- | Parallel left-join kernel: build the index once, probe in parallel. parLeftKernel :: VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int) parLeftKernel leftHashes rightHashes =@@ -747,10 +646,7 @@ in unsafePerformIO (parLeftProbe (ciProbeTable ci) leftHashes) {-# NOINLINE parLeftKernel #-} -{- | Hash-based left join kernel, returning @(leftExpandedIndices,-rightExpandedIndices)@ with @-1@ marking unmatched right rows. Grows its output-buffer dynamically rather than pre-allocating the full cross product.--}+-- | Hash-based left join kernel with @-1@ marking unmatched right rows. hashLeftKernel :: VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int) hashLeftKernel leftHashes rightHashes = runST $ do@@ -816,10 +712,7 @@ <$> VU.unsafeFreeze (VUM.slice 0 total lv) <*> VU.unsafeFreeze (VUM.slice 0 total rv) -{- | Sort-merge left join kernel, returning @(leftExpandedIndices,-rightExpandedIndices)@ with a @-1@ sentinel. Grows its output buffer-dynamically so low-cardinality cross products don't OOM.--}+-- | Sort-merge left join kernel with a @-1@ sentinel for unmatched rows. sortMergeLeftKernel :: VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int) sortMergeLeftKernel leftHashes rightHashes = runST $ do@@ -919,9 +812,7 @@ <$> VU.unsafeFreeze (VUM.slice 0 total lv) <*> VU.unsafeFreeze (VUM.slice 0 total rv) -{- | Assemble the result DataFrame for a left join.-Right index vectors use @-1@ sentinel, gathered via 'gatherWithSentinel'.--}+-- | Assemble the result DataFrame for a left join. assembleLeft :: S.Set T.Text -> DataFrame ->@@ -970,26 +861,7 @@ rightColNames baseDf -{- | Performs a right join on two dataframes using the specified key columns.-Returns all rows from the right dataframe, with matching rows from the left dataframe.-Non-matching rows will have Nothing/null values for columns from the left dataframe.--==== __Example__-@-ghci> df = D.fromNamedColumns [("key", D.fromList ["K0", "K1", "K2", "K3"]), ("A", D.fromList ["A0", "A1", "A2", "A3"])]-ghci> other = D.fromNamedColumns [("key", D.fromList ["K0", "K1"]), ("B", D.fromList ["B0", "B1"])]-ghci> D.rightJoin ["key"] df other-------------------- key | A | B-------|-----|----- Text | Text| Text-------|-----|----- K0 | A0 | B0- K1 | A1 | B1--@--}+-- | Right join on two dataframes, keeping every row from the right dataframe. rightJoin :: [T.Text] -> DataFrame -> DataFrame -> DataFrame rightJoin cs left right = leftJoinWithCallPoint "rightJoin" cs right left@@ -1021,10 +893,7 @@ in assembleFullOuter csSet left right leftIxs rightIxs -{- | Hash-based full outer join kernel.-Builds compact indices on both sides.-Returns @(leftExpandedIndices, rightExpandedIndices)@ with @-1@ sentinels.--}+-- | Hash-based full outer join kernel with @-1@ sentinels. hashFullOuterKernel :: VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int) hashFullOuterKernel leftHashes rightHashes = runST $ do@@ -1134,9 +1003,7 @@ (,) <$> VU.unsafeFreeze lv <*> VU.unsafeFreeze rv -{- | Sort-merge full outer join kernel.-Returns @(leftExpandedIndices, rightExpandedIndices)@ with @-1@ sentinels.--}+-- | Sort-merge full outer join kernel with @-1@ sentinels. sortMergeFullOuterKernel :: VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int) sortMergeFullOuterKernel leftHashes rightHashes = runST $ do@@ -1202,9 +1069,7 @@ fill 0 0 0 (,) <$> VU.unsafeFreeze lv <*> VU.unsafeFreeze rv -{- | Assemble the result DataFrame for a full outer join. Both index vectors-use a @-1@ sentinel; key columns are coalesced (first non-null wins).--}+-- | Assemble the result DataFrame for a full outer join. assembleFullOuter :: S.Set T.Text -> DataFrame ->
src/DataFrame/Operations/Join/Parallel.hs view
@@ -1,18 +1,7 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE ScopedTypeVariables #-} -{- |-Parallel chunked-probe join kernels. The build side is indexed once into a-shared, read-only 'CompactIndex' (open-addressing, from-"DataFrame.Operations.Join"); the probe side is split into @caps@ /contiguous/-row ranges and probed in parallel by 'forkFinally' workers (no sparks). Each worker-makes two passes over its range — a count pass to size its slice, then a fill-pass — and writes into the single shared output buffers at a precomputed-prefix-sum offset. Because ranges are contiguous and laid out in range order,-the produced @(probeIxs, buildIxs)@ vectors are /bit-for-bit identical/ to the-sequential 'hashInnerKernel' \/ 'hashLeftKernel': probe rows appear in original-order and, within a probe row, build matches in @ciSortedIndices@ order.--}+-- | Parallel chunked-probe join kernels over a shared 'CompactIndex'. module DataFrame.Operations.Join.Parallel ( ProbeTable (..), parInnerProbe,@@ -31,26 +20,15 @@ import Data.Word (Word64) import DataFrame.Internal.Control.Concurrent (capabilities, forkJoin_) -{- | Below this many probe rows the fork/coordination overhead is not worth it;-the caller uses its sequential 'ST' kernel instead.--}+-- | Below this many probe rows the fork overhead outweighs parallel probing. parJoinThreshold :: Int parJoinThreshold = 200000 -{- | Below this many build rows the shared 'CompactIndex' is small and hot, so-the sequential hash probe is already memory-bound-fast and the fork overhead-loses (measured: a 1e4-row Text-key build probed by 1e7 rows is /slower/ in-parallel). Parallelism only pays once the build index is large enough to spill-cache — exactly the regime where sort-merge used to be chosen.--}+-- | Below this many build rows the sequential hash probe beats forking. parBuildThreshold :: Int parBuildThreshold = 500000 -{- | Whether a join should take the parallel probe path: more than one-capability, a probe side of at least 'parJoinThreshold' rows, and a build side-of at least 'parBuildThreshold' rows (a small/hot index is faster probed-sequentially).--}+-- | Whether a join should take the parallel probe path. shouldParallelizeJoin :: Int -> Int -> Bool shouldParallelizeJoin probeRows buildRows = probeRows >= parJoinThreshold@@ -58,38 +36,18 @@ && capabilities > 1 {-# NOINLINE shouldParallelizeJoin #-} -{- | Above this many probe rows the probe-side row hashing and table lookups-dominate the join, so partitioning the probe across cores wins even when the-build side is small and cache-resident (the regime 'shouldParallelizeJoin'-deliberately leaves sequential). Sized at 1e6: below it the per-question gain is-swamped by 'forkFinally'/coordination overhead, so small/medium-inner joins stay-sequential (measured). This is the small-build large-probe lever closing the-medium-factor 1e7 join (1e7 probe x ~1e4 build).--}+-- | Probe-row count above which parallel probing wins even for small builds. parProbeThreshold :: Int parProbeThreshold = 1000000 -{- | Whether a /small-build/ join (build below 'parBuildThreshold', so radix-partitioning / sort-merge is not used) should take the parallel probe path: a-very large probe side (at least 'parProbeThreshold') and more than one-capability. The shared build index is read-only across threads, so probing it in-parallel needs no synchronization. Independent of build size on purpose: the-build is already tiny; the cost is the 1e7-row probe hashing, which parallelizes-cleanly.--}+-- | Whether a small-build join should take the parallel probe path. shouldParallelizeSmallBuildProbe :: Int -> Bool shouldParallelizeSmallBuildProbe probeRows = probeRows >= parProbeThreshold && capabilities > 1 {-# NOINLINE shouldParallelizeSmallBuildProbe #-} -{- | A read-only view of the build-side index needed by the probe: the raw-open-addressing table vectors of the @CompactIndex@ (which lives in-"DataFrame.Operations.Join"; passing the fields avoids an import cycle).-Passing concrete vectors instead of a lookup closure keeps the per-row probe-loop free of unknown calls and boxed-tuple allocation — the lookup is inlined-into the count and fill loops.--}+-- | A read-only view of the build-side index fields needed by the probe. data ProbeTable = ProbeTable { ptSorted :: !(VU.Vector Int) , ptKeys :: !(VU.Vector Int)@@ -98,30 +56,21 @@ , ptMask :: {-# UNPACK #-} !Int } -{- | Parallel inner-join probe. @parInnerProbe table probeHashes@ returns-@(probeIxs, buildIxs)@ identical to a sequential probe of the same index. The-build index must already be constructed from the build side.--}+-- | Parallel inner-join probe returning (probeIxs, buildIxs). parInnerProbe :: ProbeTable -> VU.Vector Int -> IO (VU.Vector Int, VU.Vector Int) parInnerProbe = runProbe False -{- | Parallel left-join probe. Like 'parInnerProbe' but every probe row emits at-least one output row; unmatched rows carry a @-1@ sentinel in the build column.--}+-- | Parallel left-join probe; unmatched rows carry a @-1@ build sentinel. parLeftProbe :: ProbeTable -> VU.Vector Int -> IO (VU.Vector Int, VU.Vector Int) parLeftProbe = runProbe True -{- | Shared two-pass parallel probe. @keepUnmatched@ selects left- vs-inner-join semantics. Splits @[0, probeN)@ into @caps@ contiguous ranges, counts-each range's output, prefix-sums to global offsets, then fills the single output-buffers in parallel.--}+-- | Shared two-pass parallel probe covering inner and left join semantics. runProbe :: Bool -> ProbeTable ->@@ -136,9 +85,7 @@ !runs = ptRuns pt !mask = ptMask pt !shift = 64 - popCount mask- -- Packed (start,len) run for hash @h@, or -1 on a miss. Home slot is- -- the top log2(cap) bits of a Fibonacci multiply (the row hash's low- -- bits are poorly diffused); must match the build-side ciSlot exactly.+ -- Packed (start,len) run for hash @h@, or -1 on a miss. findRun !h = go ( fromIntegral@@ -171,7 +118,7 @@ forkRanges nChunks $ \k -> let (lo, hi) = chunkBounds k in VUM.unsafeWrite chunkCounts k (countRange lo hi)- -- Exclusive prefix sum -> per-chunk global start offsets; total at [nChunks].+ -- Exclusive prefix sum gives per-chunk start offsets; total at [nChunks]. let scan !k !acc | k > nChunks = pure acc | otherwise = do@@ -214,9 +161,6 @@ bf <- VU.unsafeFreeze (VUM.slice 0 total bv) pure (pf, bf) -{- | Run @body k@ for @k@ in @[0, nChunks)@, one chunk per task, on @nChunks@-forked threads; rethrow the first failure. Chunk @k@ is owned by exactly one-thread, so concurrent writes to disjoint output regions are race-free.--}+-- | Run @body k@ for each chunk on its own forked thread; rethrow failures. forkRanges :: Int -> (Int -> IO ()) -> IO () forkRanges nChunks body = forkJoin_ (map body [0 .. nChunks - 1])
src/DataFrame/Operations/Merge.hs view
@@ -15,9 +15,7 @@ import Data.Maybe -{- | Vertically merge two dataframes using shared columns.-Columns that exist in only one dataframe are padded with Nothing.--}+-- | Vertically merge two dataframes, padding unshared columns with Nothing. instance Semigroup D.DataFrame where (<>) :: D.DataFrame -> D.DataFrame -> D.DataFrame (<>) a b =
src/DataFrame/Operations/Permutation.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE InstanceSigs #-}@@ -9,6 +10,7 @@ -- * Sorting SortOrder (..), sortBy,+ topBy, -- * Shuffling shuffle,@@ -19,20 +21,35 @@ import qualified Data.Text as T import qualified Data.Vector as V import qualified Data.Vector.Algorithms.Merge as VA+import qualified Data.Vector.Algorithms.Radix as Radix import qualified Data.Vector.Generic as VG import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Unboxed.Mutable as VUM import Control.Exception (throw)+import Control.Monad (forM_, when) import Control.Monad.ST (runST)+import Data.Bits (+ complement,+ countLeadingZeros,+ testBit,+ unsafeShiftR,+ xor,+ (.&.),+ (.|.),+ )+import Data.Int (Int16, Int32, Int64, Int8) import Data.Type.Equality (testEquality, (:~:) (Refl))+import Data.Typeable (Typeable) import Data.Vector.Internal.Check (HasCallStack)+import Data.Word (Word16, Word32, Word64, Word8) import DataFrame.Errors (DataFrameException (..)) import DataFrame.Internal.Column ( Column (..), Columnable, atIndicesStable, materializeMerged,+ sliceColumn, ) import DataFrame.Internal.Data.PackedText (packedSlice, sliceCmpBytes) import DataFrame.Internal.DataFrame (@@ -43,6 +60,7 @@ import DataFrame.Internal.Expression (Expr (Col), getColumns) import DataFrame.Operations.Core (dimensions) import DataFrame.Operations.Transformations (derive)+import GHC.Float (castDoubleToWord64) import System.Random (Random (randomR), RandomGen) import Type.Reflection (typeRep) @@ -65,10 +83,7 @@ mustFlipCompare (Asc _) = True mustFlipCompare (Desc _) = False -{- | Materialize any compound sort expressions into synthetic columns on-a working dataframe, returning rewritten 'SortOrder's that reference-those columns by name.--}+-- | Materialize compound sort expressions into synthetic columns. prepareSortColumns :: [SortOrder] -> DataFrame -> ([SortOrder], DataFrame) prepareSortColumns = go 0 where@@ -91,10 +106,7 @@ syntheticName :: Int -> T.Text syntheticName i = "__sortBy_synthetic_" <> T.pack (show i) <> "__" -{- | O(k log n) Sorts the dataframe by a given row.--> sortBy Ascending ["Age"] df--}+-- | O(k log n) Sort the dataframe by the given sort orders. sortBy :: [SortOrder] -> DataFrame ->@@ -112,16 +124,88 @@ comparators = map (`sortOrderComparator` df') sortOrds' compositeCompare i j = mconcat [c i j | c <- comparators] nRows = fst (dataframeDimensions df')- indexes = sortIndices compositeCompare nRows+ indexes = case sortOrds' of+ [ord] | Just ixs <- radixSortIndices ord df' -> ixs+ _ -> sortIndices compositeCompare nRows in df{columns = V.map (atIndicesStable indexes) (columns df)} where referenced = L.nub (concatMap sortOrderColumns sortOrds) missing = referenced L.\\ columnNames df -{- | Build a row-index comparator from a SortOrder and a DataFrame.-The Ord dictionary is recovered from the SortOrder GADT.--}+-- | The rows a 'sortBy' would place at positions @[offset, offset + count)@.+topBy :: Int -> Int -> [SortOrder] -> DataFrame -> DataFrame+topBy offset count sortOrds df+ | not (null missing) =+ throw $+ ColumnsNotFoundException+ missing+ "topBy"+ (columnNames df)+ | k >= nRows || k <= 0 =+ let sorted = sortBy sortOrds df+ in sorted+ { columns = V.map (sliceColumn offset' (k - offset')) (columns sorted)+ , dataframeDimensions = (k - offset', snd (dataframeDimensions df))+ }+ | otherwise =+ let+ (sortOrds', df') = prepareSortColumns sortOrds df+ comparators = map (`sortOrderComparator` df') sortOrds'+ -- Ties keep their original order, as in the stable full sort.+ cmp i j = mconcat [c i j | c <- comparators] <> compare i j+ leading = smallestIndices cmp k nRows+ window = VU.slice offset' (k - offset') (sortIndicesOf cmp leading)+ in+ df+ { columns = V.map (atIndicesStable window) (columns df)+ , dataframeDimensions = (VU.length window, snd (dataframeDimensions df))+ }+ where+ nRows = fst (dataframeDimensions df)+ k = min nRows (max 0 offset + max 0 count)+ offset' = min k (max 0 offset)+ referenced = L.nub (concatMap sortOrderColumns sortOrds)+ missing = referenced L.\\ columnNames df++-- | The @k@ smallest of @[0, n)@ under @cmp@, unordered (a bounded max-heap).+smallestIndices :: (Int -> Int -> Ordering) -> Int -> Int -> VU.Vector Int+smallestIndices cmp k n = runST $ do+ heap <- VUM.generate k id+ let siftDown !i = do+ let l = 2 * i + 1+ r = l + 1+ if l >= k+ then pure ()+ else do+ vi <- VUM.unsafeRead heap i+ vl <- VUM.unsafeRead heap l+ (c, vc) <-+ if r < k+ then do+ vr <- VUM.unsafeRead heap r+ pure (if cmp vr vl == GT then (r, vr) else (l, vl))+ else pure (l, vl)+ when (cmp vc vi == GT) $ do+ VUM.unsafeWrite heap i vc+ VUM.unsafeWrite heap c vi+ siftDown c+ forM_ [k `div` 2 - 1, k `div` 2 - 2 .. 0] siftDown+ forM_ [k .. n - 1] $ \i -> do+ top <- VUM.unsafeRead heap 0+ when (cmp i top == LT) $ do+ VUM.unsafeWrite heap 0 i+ siftDown 0+ VU.unsafeFreeze heap++-- | 'sortIndices' over an explicit index set.+sortIndicesOf :: (Int -> Int -> Ordering) -> VU.Vector Int -> VU.Vector Int+sortIndicesOf cmp ixs = runST $ do+ mv <- VG.thaw (VU.convert ixs :: V.Vector Int)+ VA.sortBy cmp mv+ VU.convert <$> VG.unsafeFreeze mv++-- | Build a row-index comparator from a SortOrder and a DataFrame. sortOrderComparator :: SortOrder -> DataFrame -> Int -> Int -> Ordering sortOrderComparator (Asc (Col name :: Expr a)) df = case unsafeGetColumn name df of@@ -164,6 +248,63 @@ sortOrderComparator _ _ = error "Sorting on compound column" -- | Sort row indices using a comparator function.++-- | Stable LSD radix sort by one clean numeric column, else 'Nothing'.+radixSortIndices :: SortOrder -> DataFrame -> Maybe (VU.Vector Int)+radixSortIndices ord df = case ord of+ Asc (Col name) -> keysOf name False+ Desc (Col name) -> keysOf name True+ _ -> Nothing+ where+ keysOf name desc = case unsafeGetColumn name df of+ UnboxedColumn Nothing (v :: VU.Vector b) -> do+ key <- orderKey @b+ -- NaN has no 'compare' order; leave it to the comparison sort.+ case testEquality (typeRep @b) (typeRep @Double) of+ Just Refl | VU.any isNaN v -> Nothing+ _ -> Just ()+ let flipIf k = if desc then complement k else k+ Just (radixByKey (VU.map (flipIf . key) v))+ _ -> Nothing++-- | An unsigned key whose order matches the type's 'compare' order.+orderKey :: forall b. (Typeable b) => Maybe (b -> Word64)+orderKey+ | Just Refl <- testEquality (typeRep @b) (typeRep @Int) = Just signed+ | Just Refl <- testEquality (typeRep @b) (typeRep @Int64) = Just signed+ | Just Refl <- testEquality (typeRep @b) (typeRep @Int32) = Just signed+ | Just Refl <- testEquality (typeRep @b) (typeRep @Int16) = Just signed+ | Just Refl <- testEquality (typeRep @b) (typeRep @Int8) = Just signed+ | Just Refl <- testEquality (typeRep @b) (typeRep @Word64) = Just fromIntegral+ | Just Refl <- testEquality (typeRep @b) (typeRep @Word32) = Just fromIntegral+ | Just Refl <- testEquality (typeRep @b) (typeRep @Word16) = Just fromIntegral+ | Just Refl <- testEquality (typeRep @b) (typeRep @Word8) = Just fromIntegral+ | Just Refl <- testEquality (typeRep @b) (typeRep @Double) = Just double+ | otherwise = Nothing+ where+ signed :: (Integral a) => a -> Word64+ signed x = fromIntegral (fromIntegral x :: Int64) `xor` 0x8000000000000000+ -- -0.0 maps to 0.0's key, as 'compare' treats them equal.+ double d =+ let w = castDoubleToWord64 (if d == 0 then 0 else d)+ in if testBit w 63 then complement w else w `xor` 0x8000000000000000++-- | Row indices ordered by key, ties in row order; skips constant bytes.+radixByKey :: VU.Vector Word64 -> VU.Vector Int+radixByKey keys+ | VU.null keys = VU.empty+ | otherwise = runST $ do+ let !k0 = VU.unsafeHead keys+ !varying = VU.foldl' (\acc k -> acc .|. (k `xor` k0)) 0 keys+ !passes = (64 - countLeadingZeros varying + 7) `div` 8+ pairs <- VU.thaw (VU.imap (\i k -> (k, i)) keys)+ Radix.sortBy+ passes+ 256+ (\p (k, _) -> fromIntegral ((k `unsafeShiftR` (8 * p)) .&. 255))+ pairs+ VU.map snd <$> VU.unsafeFreeze pairs+ sortIndices :: (Int -> Int -> Ordering) -> Int -> VU.Vector Int sortIndices cmp nRows = runST $ do withIndexes <- VG.thaw (V.generate nRows id :: V.Vector Int)
src/DataFrame/Operations/SetOps.hs view
@@ -20,41 +20,23 @@ import DataFrame.Operations.Aggregation (distinct, groupBy, selectIndices) import DataFrame.Operations.Merge () -{- | All rows that appear in either dataframe, deduplicated.--@union a b@ is @distinct (a <> b)@: the set union of the two row sets.--}+-- | All rows that appear in either dataframe, deduplicated. union :: DataFrame -> DataFrame -> DataFrame union a b = distinct (a <> b) -{- | Rows that appear in both dataframes, deduplicated.--A row survives iff an equal row is present in each input.--}+-- | Rows that appear in both dataframes, deduplicated. intersect :: DataFrame -> DataFrame -> DataFrame intersect = setOp (&&) -{- | Rows present in the left dataframe but absent from the right, deduplicated-(the relational @EXCEPT@; the subobject complement of @a@ by @b@).--}+-- | Rows in the left dataframe but not the right, deduplicated. difference :: DataFrame -> DataFrame -> DataFrame difference = setOp (\inLeft inRight -> inLeft && not inRight) -{- | Rows present in exactly one of the two dataframes, deduplicated.--@symmetricDifference a b@ is @union (difference a b) (difference b a)@.--}+-- | Rows present in exactly one of the two dataframes, deduplicated. symmetricDifference :: DataFrame -> DataFrame -> DataFrame symmetricDifference a b = difference a b `union` difference b a -{- | Core engine for 'intersect' and 'difference'.--Concatenate the inputs, group by every column, and decide each group from-whether it has a member on the left side (original-row index @< nRows a@) and/or-the right side. The first member of a qualifying group is emitted as the-representative row, which keeps the result deduplicated and, because group-members come out in ascending row order, prefers the left dataframe's row.--}+-- | Core engine for 'intersect' and 'difference'. setOp :: (Bool -> Bool -> Bool) -> DataFrame -> DataFrame -> DataFrame setOp keep a b = selectIndices (VU.fromList chosen) combined
src/DataFrame/Operations/Statistics.hs view
@@ -53,7 +53,7 @@ DataFrame (..), columnNames, empty,- fromNamedColumns,+ fromColumns, getColumn, ) import DataFrame.Internal.Expression@@ -67,24 +67,7 @@ import Text.Printf (printf) import Type.Reflection (typeRep) -{- | Show a frequency table for a categorical feaure.--__Examples:__--@-ghci> df <- D.readCsv ".\/data\/housing.csv"--ghci> D.frequencies "ocean_proximity" df------------------------------------------------------------------------ Statistic | <1H OCEAN | INLAND | ISLAND | NEAR BAY | NEAR OCEAN-----------------|-----------|--------|--------|----------|------------ Text | Any | Any | Any | Any | Any-----------------|-----------|--------|--------|----------|------------ Count | 9136 | 6551 | 5 | 2290 | 2658- Percentage (%) | 44.26% | 31.74% | 0.02% | 11.09% | 12.88%-@--}+-- | Show a frequency table for a categorical feature. frequencies :: forall a. (Columnable a, Ord a) => Expr a -> DataFrame -> DataFrame frequencies expr df =@@ -144,7 +127,7 @@ Left e -> throw e Right xs -> median' xs --- | Calculates the median of a given column (containing optional values) as a standalone value.+-- | The median of a column of optional values, as a standalone value. medianMaybe :: forall a. (Columnable a, Real a) => Expr (Maybe a) -> DataFrame -> Double medianMaybe (Col name) df =@@ -218,7 +201,7 @@ Left e -> throw e Right xs -> variance' xs --- | Calculates the inter-quartile range of a given column as a standalone value.+-- | The inter-quartile range of a column as a standalone value. interQuartileRange :: forall a. (Columnable a, Real a, VU.Unbox a) => Expr a -> DataFrame -> Double interQuartileRange (Col name) df = case columnAsUnboxedVector (Col @a name) df of@@ -230,7 +213,7 @@ Left e -> throw e Right xs -> interQuartileRange' xs --- | Calculates the Pearson's correlation coefficient between two given columns as a standalone value.+-- | Pearson's correlation coefficient of two columns, as a standalone value. correlation :: T.Text -> T.Text -> DataFrame -> Maybe Double correlation first second df = do f <- _getColumnAsDouble first df@@ -271,50 +254,14 @@ Just Refl -> VG.sum column Nothing -> 0 Just (PackedText _ _) -> 0- Just (MergedColumn _ _) -> 0 -- matches the old eager These column (type never Num)+ Just (MergedColumn _ _) -> 0 -- matches the old eager These column sum expr df = case interpret df expr of Left e -> throw e Right (TColumn xs) -> case toVector @a @V.Vector xs of Left e -> throw e Right xs' -> VG.sum xs' -{- | /O(n)/ Impute missing values in a column using a derived scalar.--Given--* an expression @f :: 'Expr' b -> 'Expr' b@ that, when interpreted over a- non-nullable column, produces the same value in every row (for example a- mean, median, or other aggregate), and-* a nullable column @'Expr' ('Maybe' b)@--this function:--1. Drops all @Nothing@ values from the target column.-2. Interprets @f@ on the remaining non-null values.-3. Checks that the resulting column contains a single repeated value.-4. Uses that value to impute all @Nothing@s in the original column.--==== __Throws__--* 'DataFrameException' - if the column does not exist, is empty,--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> let df =-... D.fromNamedColumns-... [ ("age", D.fromList [Just 10, Nothing, Just 20 :: Maybe Int]) ]->>>->>> -- Impute missing ages with the mean of the observed ages->>> D.imputeWith F.mean "age" df--- age--- ------- 10--- 15--- 20-@--}+-- | /O(n)/ Impute missing values in a column using a derived scalar. instance {-# OVERLAPPING #-} (Columnable b) => ImputeOp (Maybe b) where runImpute = imputeCore @@ -362,7 +309,7 @@ fold columnStats (columnNames df)- ( fromNamedColumns+ ( fromColumns [ ( "Statistic" , fromList
src/DataFrame/Operations/Subset.hs view
@@ -90,6 +90,7 @@ columnToTextVec, findIndices, hasMissing,+ maskIndices, materializeMerged, materializePacked, mkRandom,@@ -182,18 +183,14 @@ where (r, c) = dataframeDimensions d start' = clip start 0 r- -- Clamp both endpoints before subtracting: end - start' on an unclamped- -- end wraps for very negative values and reopens the range.+ -- Clamp both endpoints first: an unclamped very negative end wraps. end' = clip end start' r n' = end' - start' clip :: Int -> Int -> Int -> Int clip n left right = min right $ max n left -{- | O(n * k) Filter rows by a given condition.--> filter "x" even df--}+-- | O(n * k) Filter rows by a given condition. filter :: forall a. (Columnable a) =>@@ -276,26 +273,23 @@ , dataframeDimensions = (VG.length ixs, snd (dataframeDimensions df)) } -{- | O(k) a version of filter where the predicate comes first.--> filterBy even "x" df--}+-- | O(k) A version of filter where the predicate comes first. filterBy :: (Columnable a) => (a -> Bool) -> Expr a -> DataFrame -> DataFrame filterBy = flip filter -{- | O(k) filters the dataframe with a boolean expression.--> filterWhere (F.col @Int x + F.col y F.> 5) df--}+-- | O(k) Filter the dataframe with a boolean expression. filterWhere :: Expr Bool -> DataFrame -> DataFrame filterWhere expr df = let (TColumn col') = case interpret @Bool df (normalize expr) of Left e -> throw e Right c -> c- indexes = case findIndices id col' of- Right ixs -> ixs- Left e -> throw e+ indexes = case col' of+ UnboxedColumn Nothing (mask :: VU.Vector b)+ | Just Refl <- testEquality (typeRep @b) (typeRep @Bool) -> maskIndices mask+ _ -> case findIndices id col' of+ Right ixs -> ixs+ Left e -> throw e c' = snd $ dataframeDimensions df in df@@ -303,10 +297,7 @@ , dataframeDimensions = (VU.length indexes, c') } -{- | O(k) removes all rows with `Nothing` in a given column from the dataframe.--> filterJust "col" df--}+-- | O(k) Remove all rows with Nothing in a given column. filterJust :: T.Text -> DataFrame -> DataFrame filterJust colName df = case getColumn colName df of Nothing ->@@ -322,10 +313,7 @@ _ -> df Just _ -> df -{- | O(k) returns all rows with `Nothing` in a give column.--> filterNothing "col" df--}+-- | O(k) Return all rows with Nothing in a given column. filterNothing :: T.Text -> DataFrame -> DataFrame filterNothing colName df = case getColumn colName df of Nothing ->@@ -339,32 +327,19 @@ _ -> df _ -> df -{- | O(n * k) removes all rows with `Nothing` from the dataframe.--> filterAllJust df--}+-- | O(n * k) Remove all rows with Nothing from the dataframe. filterAllJust :: DataFrame -> DataFrame filterAllJust df = foldr filterJust df (columnNames df) -{- | O(n * k) keeps any row with a null value.--> filterAllNothing df--}+-- | O(n * k) Keep any row with a null value. filterAllNothing :: DataFrame -> DataFrame filterAllNothing df = foldr filterNothing df (columnNames df) -{- | O(k) cuts the dataframe in a cube of size (a, b) where- a is the length and b is the width.--> cube (10, 5) df--}+-- | O(k) Cut the dataframe to a cube of size (length, width). cube :: (Int, Int) -> DataFrame -> DataFrame cube (len, width) = take len . selectBy [ColumnIndexRange (0, width - 1)] -{- | O(n) Selects a number of columns in a given dataframe.--> select ["name", "age"] df--}+-- | O(n) Select a number of columns in a given dataframe. select :: [T.Text] -> DataFrame ->@@ -393,42 +368,23 @@ | ColumnIndexRange (Int, Int) | ColumnName T.Text -{- | Criteria for selecting a column by name.--> selectBy [byName "Age"] df--equivalent to:--> select ["Age"] df--}+-- | Criteria for selecting a column by name. byName :: T.Text -> SelectionCriteria byName = ColumnName -{- | Criteria for selecting columns whose property satisfies given predicate.--> selectBy [byProperty isNumeric] df--}+-- | Criteria for selecting columns whose property satisfies a predicate. byProperty :: (Column -> Bool) -> SelectionCriteria byProperty = ColumnProperty -{- | Criteria for selecting columns whose name satisfies given predicate.--> selectBy [byNameProperty (T.isPrefixOf "weight")] df--}+-- | Criteria for selecting columns whose name satisfies a predicate. byNameProperty :: (T.Text -> Bool) -> SelectionCriteria byNameProperty = ColumnNameProperty -{- | Criteria for selecting columns whose names are in the given lexicographic range (inclusive).--> selectBy [byNameRange ("a", "c")] df--}+-- | Criteria for selecting columns whose names are in a lexicographic range. byNameRange :: (T.Text, T.Text) -> SelectionCriteria byNameRange = ColumnTextRange -{- | Criteria for selecting columns whose indices are in the given (inclusive) range.--> selectBy [byIndexRange (0, 5)] df--}+-- | Criteria for selecting columns whose indices are in an inclusive range. byIndexRange :: (Int, Int) -> SelectionCriteria byIndexRange = ColumnIndexRange @@ -451,10 +407,7 @@ where ixs = V.ifoldl' (\acc' i c -> if f c then i : acc' else acc') [] (columns df) -{- | O(k * n) select rows by index--> selectRows [0, 2, 4] df--}+-- | O(k * n) Select rows by index. selectRows :: [Int] -> DataFrame -> DataFrame selectRows ixs df = df@@ -464,10 +417,7 @@ where ixs' = VU.fromList ixs -{- | O(n) inverse of select--> exclude ["Name"] df--}+-- | O(n) Inverse of select. exclude :: [T.Text] -> DataFrame ->@@ -476,15 +426,7 @@ let keysToKeep = columnNames df L.\\ cs in select keysToKeep df -{- | Sample a dataframe. The double parameter must be between 0 and 1 (inclusive).--==== __Example__-@-ghci> import System.Random-ghci> D.sample (mkStdGen 137) 0.1 df--@--}+-- | Sample a dataframe; the fraction must be between 0 and 1 inclusive. sample :: (RandomGen g) => g -> Double -> DataFrame -> DataFrame sample pureGen p df = let@@ -496,15 +438,7 @@ & filterWhere (cRand .>=. Lit (1 - p)) & exclude [name cRand] -{- | Split a dataset into two. The first in the tuple gets a sample of p (0 <= p <= 1) and the second gets (1 - p). This is useful for creating test and train splits.--==== __Example__-@-ghci> import System.Random-ghci> D.randomSplit (mkStdGen 137) 0.9 df--@--}+-- | Split a dataset in two: a sample of fraction p and the remainder. randomSplit :: (RandomGen g) => g -> Double -> DataFrame -> (DataFrame, DataFrame) randomSplit pureGen p df =@@ -522,15 +456,7 @@ & exclude [name cRand] ) -{- | Creates n folds of a dataframe.--==== __Example__-@-ghci> import System.Random-ghci> D.kFolds (mkStdGen 137) 5 df--@--}+-- | Create n folds of a dataframe. kFolds :: (RandomGen g) => g -> Int -> DataFrame -> [DataFrame] kFolds pureGen folds df = let@@ -569,14 +495,7 @@ , dataframeDimensions = (VU.length ixs, snd (dataframeDimensions df)) } -{- | Sample a dataframe, preserving per-stratum proportions.--==== __Example__-@-ghci> import System.Random-ghci> D.stratifiedSample (mkStdGen 42) 0.8 "label" df-@--}+-- | Sample a dataframe, preserving per-stratum proportions. stratifiedSample :: forall a g. (SplittableGen g, Columnable a) =>@@ -595,14 +514,7 @@ in sample g1 p stratum <> go g2 rest in go gen groups -{- | Split a dataframe into two, preserving per-stratum proportions.--==== __Example__-@-ghci> import System.Random-ghci> D.stratifiedSplit (mkStdGen 42) 0.8 "label" df-@--}+-- | Split a dataframe into two, preserving per-stratum proportions. stratifiedSplit :: forall a g. (SplittableGen g, Columnable a) =>
src/DataFrame/Operations/Transformations.hs view
@@ -75,7 +75,7 @@ Left exception -> throw exception Right df -> df --- | O(k) Safe version of the apply function. Returns (instead of throwing) the error.+-- | O(k) Safe version of 'apply'. Returns the error instead of throwing. safeApply :: forall b c. (Columnable b, Columnable c) =>@@ -93,25 +93,16 @@ column' <- mapColumn f column pure $ insertColumn columnName column' d -{- | O(k) Apply a function to an expression in a dataframe and-add the result into `alias` column.--}+-- | O(k) Derive a new column named alias from an expression. derive :: forall a. (Columnable a) => T.Text -> Expr a -> DataFrame -> DataFrame derive name expr df = case interpret @a df (normalize expr) of Left e -> throw e Right (TColumn value) -> (insertColumn name value df)- { derivingExpressions = M.insert name (UExpr expr) (derivingExpressions df)+ { derivingExpressions = M.insert name (SomeExpr expr) (derivingExpressions df) } -{- | O(k) Apply a function to an expression in a dataframe and-add the result into `alias` column but--==== __Examples__-->>> (z, df') = deriveWithExpr "z" (F.col @Int "x" + F.col "y") df->>> filterWhere (z .>= 50)--}+-- | O(k) Like 'derive' but also returns a reference to the new column. deriveWithExpr :: forall a. (Columnable a) => T.Text -> Expr a -> DataFrame -> (Expr a, DataFrame) deriveWithExpr name expr df = case interpret @a df (normalize expr) of@@ -119,14 +110,14 @@ Right (TColumn value) -> ( Col name , (insertColumn name value df)- { derivingExpressions = M.insert name (UExpr expr) (derivingExpressions df)+ { derivingExpressions = M.insert name (SomeExpr expr) (derivingExpressions df) } ) deriveMany :: [NamedExpr] -> DataFrame -> DataFrame deriveMany exprs df = let- f (name, UExpr (expr :: Expr a)) d =+ f (name, SomeExpr (expr :: Expr a)) d = case interpret @a df expr of Left e -> throw e Right (TColumn value) -> insertColumn name value d@@ -166,11 +157,7 @@ DataFrame applyDouble = apply -{- | O(k * n) Apply a function to a column only if there is another column-value that matches the given criterion.--> applyWhere (<20) "Age" (const "Gen-Z") "Generation" df--}+-- | O(k * n) Apply a function to a column where another column matches. applyWhere :: forall a b. (Columnable a, Columnable b) =>@@ -223,9 +210,7 @@ Left e -> throw e Right column' -> insertColumn columnName column' df -{- | Core impute implementation for nullable columns. A column with no null-bitmap cannot hold a missing value, so imputing it is a mistake, not a no-op.--}+-- | Core impute for nullable columns; a column with no nulls is an error. imputeCore :: forall b. (Columnable b) =>@@ -246,9 +231,7 @@ (nonNullableColumnError columnName ("Maybe " ++ show (typeRep @b)) col) imputeCore expr _ _ = throw (NonColumnReferenceException (T.pack (show expr))) -{- | 'impute' was handed a column that carries no null bitmap, so there is-nothing it could replace.--}+-- | 'impute' was handed a column with no null bitmap, so nothing to replace. nonNullableColumnError :: T.Text -> String -> Column -> DataFrameException nonNullableColumnError columnName wanted col = TypeMismatchException @() @()@@ -284,9 +267,7 @@ runImpute = errorWithoutStackTrace "impute: unreachable" runImputeWith = errorWithoutStackTrace "impute: unreachable" -{- | Replace all instances of `Nothing` in a column with the given value.-Throws when the column carries no nulls to replace.--}+-- | Replace all instances of Nothing in a column with the given value. impute :: forall a. (ImputeOp a) =>
src/DataFrame/Operations/Typing.hs view
@@ -6,9 +6,7 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} --- The inference lattice (sample classification, 'ParsingAssumption',--- Int -> Double promotion) lives in "DataFrame.Operations.Inference"--- and is re-exported here for backwards compatibility.+-- The inference lattice lives in "DataFrame.Operations.Inference", re-exported. module DataFrame.Operations.Typing ( module DataFrame.Operations.Typing, module DataFrame.Operations.Inference,@@ -49,10 +47,7 @@ import Text.Read import Type.Reflection -{- | How parse failures are surfaced: 'NoSafeRead' throws, 'MaybeRead' yields-@Nothing@ (column wrapped @Maybe a@), 'EitherRead' yields @Left rawText@-(column wrapped @Either Text a@, preserving the original input).--}+-- | How parse failures are surfaced: throw, 'Nothing', or 'Left' raw text. data SafeReadMode = NoSafeRead | MaybeRead@@ -64,20 +59,16 @@ { missingValues :: [T.Text] -- ^ Values to treat as @Nothing@ when the effective mode is 'MaybeRead'. , sampleSize :: Int- -- ^ Number of rows to inspect when inferring a column's type (0 = all rows).+ -- ^ Rows to inspect when inferring a column's type (0 = all rows). , parseSafe :: SafeReadMode- -- ^ Default 'SafeReadMode' for columns without a 'parseSafeOverrides' entry.+ -- ^ Default 'SafeReadMode' for columns with no 'parseSafeOverrides' entry. , parseSafeOverrides :: [(T.Text, SafeReadMode)]- {- ^ Per-column overrides taking precedence over 'parseSafe' — e.g. strict- IDs (@NoSafeRead@) alongside lenient fields (@MaybeRead@/@EitherRead@).- -}+ -- ^ Per-column overrides taking precedence over 'parseSafe'. , parseDateFormat :: DateFormat- -- ^ Date format string as accepted by "Data.Time.Format" (e.g. @\"%Y-%m-%d\"@).+ -- ^ Date format as accepted by "Data.Time.Format" (e.g. @\"%Y-%m-%d\"@). } -{- | Sensible out-of-the-box parse options: infer from the first 100 rows,- treat common nullish strings as missing, and expect ISO 8601 dates.--}+-- | Sensible out-of-the-box parse options: infer from 100 rows, ISO dates. defaultParseOptions :: ParseOptions defaultParseOptions = ParseOptions@@ -88,9 +79,7 @@ , parseDateFormat = "%Y-%m-%d" } -{- | Resolve a column's effective 'SafeReadMode': the override if present,-otherwise the default.--}+-- | Resolve a column's effective 'SafeReadMode': override if present. effectiveSafeRead :: SafeReadMode -> [(T.Text, SafeReadMode)] -> T.Text -> SafeReadMode effectiveSafeRead def overrides name = fromMaybe def (lookup name overrides)@@ -150,10 +139,7 @@ where classify p t = if p t then Nothing else Just t -{- | For 'EitherRead' mode: parse under the chosen assumption into an-@Either Text a@ column. Successful parses become @Right@; failures (including-null/missing cells) become @Left@ carrying the raw input verbatim.--}+-- | For 'EitherRead' mode: parse into an @Either Text a@ column. handleEitherAssumption :: DateFormat -> ParsingAssumption -> V.Vector T.Text -> Column handleEitherAssumption dfmt assumption raw = case assumption of@@ -215,10 +201,7 @@ (parseUnboxedColumnWithPred False isNull readBool cols) (handleTextAssumption isNull cols) -{- | Int columns: one fused pass with in-place Int -> Double promotion; a cell-parsing as neither demotes the column to Text. 'readIntStrict' rejects overflow-so a huge integer promotes to 'Double' rather than wrapping.--}+-- | Int columns: one fused pass with in-place Int to Double promotion. handleIntAssumption :: (T.Text -> Bool) -> V.Vector T.Text -> Column handleIntAssumption isNull cols = case promoteIntColumn (\_ t -> isNull t) readIntStrict readDouble cols of@@ -231,9 +214,7 @@ (parseUnboxedColumnWithPred 0 isNull readDouble cols) (handleTextAssumption isNull cols) -{- | Text columns: no parse, just null-marking. An all-non-null column stays a-plain @V.Vector T.Text@; otherwise it becomes @V.Vector (Maybe T.Text)@.--}+-- | Text columns: no parse, just null-marking. handleTextAssumption :: (T.Text -> Bool) -> V.Vector T.Text -> Column handleTextAssumption isNull cols | V.any isNull cols =@@ -241,10 +222,7 @@ (V.map (\t -> if isNull t then Nothing else Just t) cols) | otherwise = fromVector cols -{- | Date: single boxed parse pass ('Day' is not unboxable). Bails to-'handleTextAssumption' the moment a non-null cell fails to parse as a 'Day'.-A column with no nulls keeps type 'Day' rather than 'Maybe Day'.--}+-- | Date columns: one boxed parse pass, falling back to Text on failure. handleDateAssumption :: DateFormat -> (T.Text -> Bool) -> V.Vector T.Text -> Column handleDateAssumption dateFormat isNull cols =@@ -279,8 +257,7 @@ Nothing -> return Nothing loop 0 False --- Reached only when the sample was all-null: try each concrete type in turn,--- falling back to Text. A column with no nulls keeps type 'Day', not 'Maybe Day'.+-- Reached only for an all-null sample: try each concrete type, else Text. handleNoAssumption :: DateFormat -> (T.Text -> Bool) -> V.Vector T.Text -> Column handleNoAssumption dateFormat isNull cols@@ -298,9 +275,7 @@ | otherwise -> fromVector (V.mapMaybe id vec) Nothing -> handleTextAssumption isNull cols -{- | True for nullish or explicitly-listed missing strings. ('convertNullish'-and 'convertOnlyEmpty' below are kept only for external callers.)--}+-- | True for nullish or explicitly-listed missing strings. isNullishOrMissing :: [T.Text] -> T.Text -> Bool isNullishOrMissing missing v = isNullish v || v `elem` missing @@ -329,9 +304,7 @@ hasSameConstructor Nothing Nothing = True hasSameConstructor _ _ = False -{- | Re-type columns of a 'DataFrame' according to a schema map. @resolveMode@-maps a column name to its 'SafeReadMode' (typically via 'effectiveSafeRead').--}+-- | Re-type columns of a 'DataFrame' according to a schema map. parseWithTypes :: (T.Text -> SafeReadMode) -> M.Map T.Text SchemaType ->@@ -398,9 +371,7 @@ where s = if null v then "\"\"" else v -{- | Try 'readMaybe'; on failure return @Left raw@ where @raw@ is the original-input text. Used by 'parseWithTypes' under 'EitherRead'.--}+-- | Try 'readMaybe'; on failure return @Left raw@ with the original text. readEitherRaw :: forall a. (Read a) => String -> Either T.Text a readEitherRaw s = case readMaybe s of Just v -> Right v
src/DataFrame/Typed/Access.hs view
@@ -41,9 +41,7 @@ ) import DataFrame.Typed.Types (TypedDataFrame (..)) -{- | Retrieve a column as a boxed 'Vector', with the type determined by-the schema. The column must exist (enforced at compile time).--}+-- | Retrieve a column as a boxed 'Vector', typed by the schema. columnAsVector :: forall name cols a. ( KnownSymbol name@@ -71,10 +69,7 @@ where colName = T.pack (symbolVal (Proxy @name)) -{- | Retrieve a column coerced to an unboxed 'Int' vector, named by type-application. The column must exist and be numeric — both are compile-time-checks via 'SafeLookup', so this is total (no 'Either', no runtime throw).--}+-- | Retrieve a column as an unboxed 'Int' vector, checked at compile time. columnAsIntVector :: forall name cols a. ( KnownSymbol name@@ -90,7 +85,7 @@ where colName = T.pack (symbolVal (Proxy @name)) --- | Retrieve a column coerced to an unboxed 'Double' vector. See 'columnAsIntVector'.+-- | Retrieve a column as an unboxed 'Double' vector. See 'columnAsIntVector'. columnAsDoubleVector :: forall name cols a. ( KnownSymbol name@@ -107,7 +102,7 @@ where colName = T.pack (symbolVal (Proxy @name)) --- | Retrieve a column coerced to an unboxed 'Float' vector. See 'columnAsIntVector'.+-- | Retrieve a column as an unboxed 'Float' vector. See 'columnAsIntVector'. columnAsFloatVector :: forall name cols a. ( KnownSymbol name@@ -124,9 +119,7 @@ where colName = T.pack (symbolVal (Proxy @name)) -{- | Retrieve a column as an unboxed vector of its own element type. The column-must exist and be unboxable — both compile-time checks, so this is total.--}+-- | Retrieve a column as an unboxed vector of its own element type. columnAsUnboxedVector :: forall name cols a. ( KnownSymbol name@@ -141,22 +134,19 @@ where colName = T.pack (symbolVal (Proxy @name)) -{- | Convert every column to 'Double' and transpose into a row-major matrix.-Total: 'AllColumnsReal' proves at compile time that every column is numeric and-unboxed, so the conversion cannot fail.--}+-- | Convert every column to 'Double' as a row-major matrix; total. toDoubleMatrix :: (AllColumnsReal "toDoubleMatrix" cols) => TypedDataFrame cols -> V.Vector (VU.Vector Double) toDoubleMatrix (TDF df) = either throw id (D.toDoubleMatrix df) --- | Convert every column to 'Float' and transpose into a row-major matrix. See 'toDoubleMatrix'.+-- | Every column as 'Float', row-major. See 'toDoubleMatrix'. toFloatMatrix :: (AllColumnsReal "toFloatMatrix" cols) => TypedDataFrame cols -> V.Vector (VU.Vector Float) toFloatMatrix (TDF df) = either throw id (D.toFloatMatrix df) --- | Convert every column to 'Int' and transpose into a row-major matrix. See 'toDoubleMatrix'.+-- | Every column as 'Int', row-major. See 'toDoubleMatrix'. toIntMatrix :: (AllColumnsReal "toIntMatrix" cols) => TypedDataFrame cols -> V.Vector (VU.Vector Int)
src/DataFrame/Typed/Aggregate.hs view
@@ -34,50 +34,14 @@ import DataFrame.Typed.Schema import DataFrame.Typed.Types -{- | Group a typed DataFrame by one or more key columns.--@-grouped = groupBy \@'[\"department\"] employees-@--}+-- | Group a typed DataFrame by one or more key columns. groupBy :: forall (keys :: [Symbol]) cols. (AllKnownSymbol keys, AssertAllPresent keys cols) => TypedDataFrame cols -> TypedGrouped keys cols groupBy (TDF df) = TGD (DA.groupBy (symbolVals @keys) df) -{- | Build a named aggregation entry. The result column name is supplied via-@TypeApplications@; the underlying expression is validated against the-source schema at compile time.--@as@ produces a /transformer/ on the aggregation chain — entries compose-with plain @(.)@ from Prelude (or via @(|>)@ for SQL-like postfix-reading). 'aggregate' applies the composed transformer to the empty chain-internally, so no terminator is needed.--==== __Prefix form__--@-result = grouped |> aggregate- ( as \@\"total\" (sum (col \@\"amount\"))- . as \@\"orders\" (count (col \@\"order_id\"))- . as \@\"avg\" (mean (col \@\"amount\"))- )-@--==== __Postfix form (SQL-like)__--@-result = grouped |> aggregate- ( (sum (col \@\"amount\") |> as \@\"total\")- . (count (col \@\"order_id\") |> as \@\"orders\")- . (mean (col \@\"amount\") |> as \@\"avg\")- )-@--Per-entry parentheses are required in the postfix form because-@(.)@ binds tighter than @(|>)@.--}+-- | Build a named aggregation entry, validated against the schema. as :: forall name a keys cols aggs. (KnownSymbol name, Columnable a) =>@@ -86,25 +50,7 @@ TAgg keys cols ('(name, a) ': aggs) as = TAggCons (T.pack (symbolVal (Proxy @name))) -{- | Run a typed aggregation against a grouped DataFrame.--The first argument is a chain of 'as' entries composed with @(.)@. The-empty composition (@id@) yields just the group keys. The result schema is-the group-key columns followed by the aggregation columns in declaration-order.--@-result = grouped |> aggregate- ( as \@\"total\" (sum (col \@\"amount\"))- . as \@\"orders\" (count (col \@\"order_id\"))- )--- result :: TypedDataFrame--- '[ '(\"region\", Text)--- , '(\"total\", Double)--- , '(\"orders\", Int)--- ]-@--}+-- | Run a typed aggregation against a grouped DataFrame. aggregate :: forall keys cols aggs. (TAgg keys cols '[] -> TAgg keys cols aggs) ->
src/DataFrame/Typed/Apply.hs view
@@ -6,11 +6,7 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} -{- | Typed column transformations: the @apply@\/@derive@ family and-default-valued inserts, plus the horizontal merge @('|||')@. Schema changes are-tracked at the type level (e.g. 'applyColumn' rewrites a column's element type-via 'SetColumnType').--}+-- | Typed column transformations that track schema changes in the types. module DataFrame.Typed.Apply ( applyColumn, applyMany,@@ -50,14 +46,7 @@ ) import DataFrame.Typed.Types (TExpr (..), TypedDataFrame (..)) -{- | Map a function over a column, rewriting its element type from @a@ to @b@.-The schema's entry for @name@ is updated via 'SetColumnType'.--@-df' = applyColumn \@\"age\" (show :: Int -> String) df--- the \"age\" column is now String-typed-@--}+-- | Map a function over a column, rewriting its element type from @a@ to @b@. applyColumn :: forall name a b cols. ( KnownSymbol name@@ -89,10 +78,7 @@ where colName = T.pack (symbolVal (Proxy @name)) -{- | Apply a type-preserving function to several columns at once. Every named-column must already share the element type @a@ (enforced by-'AssertAllColumnsHaveType').--}+-- | Apply a type-preserving function to several columns at once. applyMany :: forall (names :: [Symbol]) a cols. (AllKnownSymbol names, Columnable a, AssertAllColumnsHaveType names a cols) =>@@ -101,14 +87,7 @@ TypedDataFrame cols applyMany f (TDF df) = TDF (D.applyMany f (symbolVals @names) df) -{- | Apply a function to a target column only on rows where a condition holds on-a filter column. Both columns are named by type application; the target keeps-its type.--@-applyWhere \@\"flagged\" \@\"score\" id (* 2) df-@--}+-- | Apply a function to a target column on rows passing a condition. applyWhere :: forall filterName targetName a b cols. ( KnownSymbol filterName@@ -145,9 +124,7 @@ where colName = T.pack (symbolVal (Proxy @name)) -{- | Derive a new column and also return a typed reference to it. The returned-expression lives in the extended schema, so it can feed later operations.--}+-- | Derive a new column and also return a typed reference to it. deriveWithExpr :: forall name a cols. ( KnownSymbol name@@ -206,10 +183,7 @@ where colName = T.pack (symbolVal (Proxy @name)) -{- | Horizontal merge: place two DataFrames side by side. The schemas must be-disjoint (no shared column names), enforced by 'AssertDisjoint'; the result-schema is their concatenation.--}+-- | Horizontal merge: place two DataFrames with disjoint schemas side by side. (|||) :: (AssertDisjoint left right) => TypedDataFrame left ->
src/DataFrame/Typed/Expr.hs view
@@ -14,32 +14,7 @@ {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -Wno-orphans #-} -{- | Type-safe expression construction for typed DataFrames.--Unlike the untyped @Expr a@ where column references are unchecked strings,-'TExpr' ensures at compile time that:--* Referenced columns exist in the schema-* Column types match the expression type--== Example--@-type Schema = '[ '(\"age\", Int), '(\"salary\", Double)]---- This compiles:-goodExpr :: TExpr Schema Double-goodExpr = col \@\"salary\"---- This gives a compile-time error (column not found):-badExpr :: TExpr Schema Double-badExpr = col \@\"nonexistent\"---- This gives a compile-time error (type mismatch):-wrongType :: TExpr Schema Int-wrongType = col \@\"salary\" -- salary is Double, not Int-@--}+-- | Type-safe expression construction for typed DataFrames. module DataFrame.Typed.Expr ( -- * Core typed expression type (re-exported from Types) TExpr (..),@@ -52,6 +27,8 @@ -- * Conditional ifThenElse,+ caseWhen,+ orElse, -- * Unary / binary lifting lift,@@ -172,16 +149,7 @@ import DataFrame.Typed.Types (TExpr (..), TSortOrder (..)) import Prelude hiding (maximum, minimum, sum) -{- | Create a typed column reference. This is the key type-safety entry point.--The column name must exist in @cols@ and its type must match @a@.-Both checks happen at compile time via type families.--@-salary :: TExpr '[(\"salary\", Double)] Double-salary = col \@\"salary\"-@--}+-- | Create a typed column reference, checked against the schema. col :: forall (name :: Symbol) cols a. ( KnownSymbol name@@ -192,13 +160,7 @@ TExpr cols a col = TExpr (Col (T.pack (symbolVal (Proxy @name)))) -{- | Use a column name as an @OverloadedLabels@ label: @#age@ is sugar for-@col \@\"age\"@. Enable @OverloadedLabels@ at the use site.--@-adults = filterWhere (#age .>=. lit 18) people-@--}+-- | Use a column name as an @OverloadedLabels@ label such as @#age@. instance ( KnownSymbol name , a ~ SafeLookup name cols@@ -209,9 +171,7 @@ where fromLabel = col @name -{- | Create a literal expression. Valid for any schema since it-references no columns.--}+-- | Create a literal expression, valid for any schema. lit :: (Columnable a) => a -> TExpr cols a lit = TExpr . Lit @@ -221,6 +181,16 @@ TExpr cols Bool -> TExpr cols a -> TExpr cols a -> TExpr cols a ifThenElse (TExpr c) (TExpr t) (TExpr e) = TExpr (If c t e) +-- | One branch of a chained conditional; the fallback goes last.+caseWhen ::+ (Columnable a) =>+ TExpr cols Bool -> TExpr cols a -> TExpr cols a -> TExpr cols a+caseWhen (TExpr c) (TExpr t) (TExpr e) = TExpr (If c t e)++-- | Closes a 'caseWhen' chain with its fallback value.+orElse :: TExpr cols a -> TExpr cols a+orElse = id+ instance (Num a, Columnable a) => Num (TExpr cols a) where (TExpr a) + (TExpr b) = TExpr (a + b) (TExpr a) - (TExpr b) = TExpr (a - b)@@ -267,10 +237,7 @@ (a -> b -> c) -> TExpr cols a -> TExpr cols b -> TExpr cols c lift2 f (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp f "binaryUdf" Nothing False 0) a b) -{- | Typed 'nullLift': lift a unary function with nullable propagation.-When the input is @Maybe a@, 'Nothing' short-circuits; when plain @a@, applies directly.-The return type is inferred via 'NullLift1Result': no annotation needed.--}+-- | Typed 'nullLift': lift a unary function with nullable propagation. nullLift :: (NullLift1Op a r (NullLift1Result a r), Columnable (NullLift1Result a r)) => (BaseType a -> r) ->@@ -278,10 +245,7 @@ TExpr cols (NullLift1Result a r) nullLift f (TExpr e) = TExpr (Unary (MkUnaryOp (applyNull1 f) "nullLift" Nothing) e) -{- | Typed 'nullLift2': lift a binary function with nullable propagation.-Any 'Nothing' operand short-circuits to 'Nothing' in the result.-The return type is inferred via 'NullLift2Result': no annotation needed.--}+-- | Typed 'nullLift2': lift a binary function with nullable propagation. nullLift2 :: (NullLift2Op a b r (NullLift2Result a b r), Columnable (NullLift2Result a b r)) => (BaseType a -> BaseType b -> r) ->@@ -355,7 +319,7 @@ (.||.) :: TExpr cols Bool -> TExpr cols Bool -> TExpr cols Bool (.||.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (||) "or" (Just ".||.") True 2) a b) --- | Nullable-aware logical AND. Returns @Maybe Bool@ when either operand is nullable.+-- | Nullable-aware logical AND; @Maybe Bool@ when either operand is nullable. (.&&) :: (NullableCmpOp a b (NullCmpResult a b), BaseType a ~ Bool) => TExpr cols a ->@@ -364,7 +328,7 @@ (.&&) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (nullCmpOp (&&)) "nulland" (Just ".&&") True 3) a b) --- | Nullable-aware logical OR. Returns @Maybe Bool@ when either operand is nullable.+-- | Nullable-aware logical OR; @Maybe Bool@ when either operand is nullable. (.||) :: (NullableCmpOp a b (NullCmpResult a b), BaseType a ~ Bool) => TExpr cols a ->@@ -376,9 +340,7 @@ infixl 6 .+, .- infixl 7 .*, ./ -{- | Nullable-aware addition. Works for all combinations of nullable\/non-nullable operands.-@col \@\"x\" '.+' col \@\"y\" -- :: TExpr cols (Maybe Int) when y :: Maybe Int@--}+-- | Nullable-aware addition for nullable and non-nullable operands. (.+) :: ( NumericWidenOp (BaseType a) (BaseType b) , NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b)@@ -474,10 +436,7 @@ (.^) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (applyNull2 (^)) "pow" (Just ".^") False 8) a b) -{- | Nullable-aware equality. Widens numeric operands to their common type,-so @TExpr cols Double .== TExpr cols Int@ typechecks. Returns @Maybe Bool@-when either operand is nullable.--}+-- | Nullable-aware equality that widens numeric operands to a common type. (.==) :: ( NumericWidenOp (BaseType a) (BaseType b) , NullLift2Op a b Bool (NullCmpResult a b)@@ -516,7 +475,7 @@ TExpr (Binary (MkBinaryOp (applyNull2 (widenCmpOp (<))) "lt" (Just "<") False 4) a b) --- | Nullable-aware less-than-or-equal. Widens numeric operands to their common type.+-- | Nullable-aware <=. Widens numeric operands to their common type. (.<=) :: ( NumericWidenOp (BaseType a) (BaseType b) , NullLift2Op a b Bool (NullCmpResult a b)@@ -529,7 +488,7 @@ TExpr (Binary (MkBinaryOp (applyNull2 (widenCmpOp (<=))) "leq" (Just "<=") False 4) a b) --- | Nullable-aware greater-than-or-equal. Widens numeric operands to their common type.+-- | Nullable-aware >=. Widens numeric operands to their common type. (.>=) :: ( NumericWidenOp (BaseType a) (BaseType b) , NullLift2Op a b Bool (NullCmpResult a b)@@ -558,10 +517,6 @@ not :: TExpr cols Bool -> TExpr cols Bool not (TExpr e) = TExpr (Unary (MkUnaryOp Prelude.not "not" (Just "!")) e) ----------------------------------------------------------------------------------- Aggregation combinators--------------------------------------------------------------------------------- sum :: (Columnable a, Num a) => TExpr cols a -> TExpr cols a sum (TExpr e) = TExpr (F.sum e) @@ -593,10 +548,6 @@ (Columnable a, AllKnownSymbol names, AssertAllPresent names cols) => TExpr cols a -> TExpr cols a over (TExpr e) = TExpr{unTExpr = F.over (symbolVals @names) e}------------------------------------------------------------------------------------ Cast / coercion expressions-------------------------------------------------------------------------------- castExpr :: forall b cols src.
src/DataFrame/Typed/Expr/Extra.hs view
@@ -2,11 +2,7 @@ {-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE ScopedTypeVariables #-} -{- | Typed counterparts of the remaining "DataFrame.Functions" expression-combinators not already provided by "DataFrame.Typed.Expr". Each wraps the-untyped combinator 1:1, replacing @Expr@ with @'TExpr' cols@ so column-references stay schema-checked. Re-exported from "DataFrame.Typed.Expr".--}+-- | Typed counterparts of the remaining "DataFrame.Functions" combinators. module DataFrame.Typed.Expr.Extra ( div, mod,@@ -119,7 +115,7 @@ max :: (Columnable a, Ord a) => TExpr cols a -> TExpr cols a -> TExpr cols a max (TExpr a) (TExpr b) = TExpr (F.max a b) --- | Fold a column into a single value with a seed and step function (aggregation).+-- | Fold a column into a single value with a seed and step function. reduce :: (Columnable a, Columnable b) => TExpr cols b -> a -> (a -> b -> a) -> TExpr cols a
src/DataFrame/Typed/Operations.hs view
@@ -86,9 +86,7 @@ import DataFrame.Typed.Schema import DataFrame.Typed.Types (TExpr (..), TSortOrder (..), TypedDataFrame (..)) -{- | Filter rows where a boolean expression evaluates to True.-The expression is validated against the schema at compile time.--}+-- | Filter rows where a boolean expression evaluates to True. filterWhere :: TExpr cols Bool -> TypedDataFrame cols -> TypedDataFrame cols filterWhere (TExpr expr) (TDF df) = TDF (D.filterWhere expr df) @@ -104,24 +102,11 @@ (a -> Bool) -> TExpr cols a -> TypedDataFrame cols -> TypedDataFrame cols filterBy pred' (TExpr expr) (TDF df) = TDF (D.filterBy pred' expr df) -{- | Keep only rows where ALL Optional columns have Just values.-Strips 'Maybe' from all column types in the result schema.--@-df :: TDF '[ '(\"x\", Maybe Double), '(\"y\", Int)]-filterAllJust df :: TDF '[ '(\"x\", Double), '(\"y\", Int)]-@--}+-- | Keep only rows where all Optional columns have Just values. filterAllJust :: TypedDataFrame cols -> TypedDataFrame (StripAllMaybe cols) filterAllJust (TDF df) = unsafeFreeze (D.filterAllJust df) -{- | Keep only rows where the named column has Just values.-Strips 'Maybe' from that column's type in the result schema.--@-filterJust \@\"x\" df-@--}+-- | Keep only rows where the named column has Just values. filterJust :: forall name cols. ( KnownSymbol name@@ -132,9 +117,7 @@ where colName = T.pack (symbolVal (Proxy @name)) -{- | Keep only rows where the named column has Nothing.-Schema is preserved (column types unchanged, just fewer rows).--}+-- | Keep only rows where the named column has Nothing. filterNothing :: forall name cols. ( KnownSymbol name@@ -145,15 +128,11 @@ where colName = T.pack (symbolVal (Proxy @name)) -{- | Keep only rows where every nullable column has Nothing.-Schema is preserved.--}+-- | Keep only rows where every nullable column has Nothing. filterAllNothing :: TypedDataFrame cols -> TypedDataFrame cols filterAllNothing (TDF df) = TDF (D.filterAllNothing df) -{- | Sort by the given typed sort orders.-Sort orders reference columns that are validated against the schema.--}+-- | Sort by the given typed sort orders, validated against the schema. sortBy :: [TSortOrder cols] -> TypedDataFrame cols -> TypedDataFrame cols sortBy ords (TDF df) = TDF (D.sortBy (map toUntypedSort ords) df) where@@ -198,15 +177,7 @@ shuffle :: (RandomGen g) => g -> TypedDataFrame cols -> TypedDataFrame cols shuffle g (TDF df) = TDF (D.shuffle g df) -{- | Derive a new column from a typed expression. The column name must NOT-already exist in the schema (enforced at compile time via 'AssertAbsent').-The expression is validated against the current schema.--@-df' = derive \@\"total\" (col \@\"price\" * col \@\"qty\") df--- df' :: TDF ('(\"total\", Double ': originalCols))-@--}+-- | Derive a new column from a typed expression; its name must be absent. derive :: forall name a cols. ( KnownSymbol name@@ -331,9 +302,7 @@ where colName = T.pack (symbolVal (Proxy @name)) -{- | Replace an existing column with new values derived from a typed expression.-The column must already exist and the new type must match.--}+-- | Replace an existing column with values from a typed expression. replaceColumn :: forall name a cols. ( KnownSymbol name@@ -358,9 +327,7 @@ intersect :: TypedDataFrame cols -> TypedDataFrame cols -> TypedDataFrame cols intersect (TDF a) (TDF b) = TDF (DS.intersect a b) -{- | Rows in the left DataFrame but not the right, deduplicated-(relational @EXCEPT@; the subobject complement).--}+-- | Rows in the left DataFrame but not the right, deduplicated. difference :: TypedDataFrame cols -> TypedDataFrame cols -> TypedDataFrame cols difference (TDF a) (TDF b) = TDF (DS.difference a b)
src/DataFrame/Typed/Sampling.hs view
@@ -1,9 +1,6 @@ {-# LANGUAGE FlexibleContexts #-} -{- | Typed sampling and splitting. All operations are schema-preserving: they-change which rows are present, never the columns, so every result reuses the-input schema @cols@.--}+-- | Typed sampling and splitting; all operations preserve the schema. module DataFrame.Typed.Sampling ( randomSplit, kFolds,@@ -30,10 +27,7 @@ (RandomGen g) => g -> Int -> TypedDataFrame cols -> [TypedDataFrame cols] kFolds g k (TDF df) = map TDF (D.kFolds g k df) -{- | Select rows by index.-| This may fail if the indices are out of bounds;-| use with caution or use 'filter' to select rows by a predicate instead.--}+-- | Select rows by index; may fail if the indices are out of bounds. selectRows :: [Int] -> TypedDataFrame cols -> TypedDataFrame cols selectRows ixs (TDF df) = TDF (D.selectRows ixs df)
src/DataFrame/Typed/Statistics.hs view
@@ -6,19 +6,7 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} -{- | Typed statistical reducers over a 'TypedDataFrame'.--These mirror the untyped reducers in "DataFrame.Operations.Statistics", taking-a schema-checked 'TExpr' instead of a raw @Expr@. The names (@sum@, @mean@,-@median@, …) deliberately collide with the aggregation-expression combinators-in "DataFrame.Typed.Expr", so this module is meant to be imported qualified:--@-import qualified DataFrame.Typed.Statistics as TS--avg = TS.mean (col \@\"salary\") employees-@--}+-- | Typed statistical reducers over a 'TypedDataFrame'; import qualified. module DataFrame.Typed.Statistics ( mean, meanMaybe,@@ -117,14 +105,7 @@ sum :: (Columnable a, Num a) => TExpr cols a -> TypedDataFrame cols -> a sum (TExpr e) (TDF df) = Stats.sum e df -{- | Pearson's correlation coefficient between two columns, named by type-application. Both columns must exist in the schema and be numeric — these are-checked at compile time via 'SafeLookup' on each name.--@-TS.correlation \@\"height\" \@\"weight\" people-@--}+-- | Pearson's correlation coefficient between two named numeric columns. correlation :: forall c1 c2 a b cols. ( KnownSymbol c1@@ -147,17 +128,12 @@ (T.pack (symbolVal (Proxy @c2))) df -{- | Frequency table for a column. The result schema is data-dependent-(one column per distinct value), so an untyped 'D.DataFrame' is returned.--}+-- | Frequency table for a column, returned as an untyped 'D.DataFrame'. frequencies :: (Columnable a, Ord a) => TExpr cols a -> TypedDataFrame cols -> D.DataFrame frequencies (TExpr e) (TDF df) = Stats.frequencies e df -{- | Impute missing values in a column using a derived scalar (e.g. the mean).-Schema-preserving: the imputed column keeps its type-level @Maybe@ even though-its runtime values are now fully populated.--}+-- | Impute missing values in a column using a derived scalar. imputeWith :: (ImputeOp a, Columnable (BaseType a)) => (TExpr cols (BaseType a) -> TExpr cols (BaseType a)) ->@@ -167,14 +143,10 @@ imputeWith f (TExpr e) (TDF df) = TDF (Stats.imputeWith (unTExpr . f . TExpr) e df) -{- | Descriptive statistics of the numeric columns. Returns an untyped-'D.DataFrame' (the result is a fixed set of statistic rows, not the input schema).--}+-- | Descriptive statistics of the numeric columns as an untyped 'D.DataFrame'. summarize :: TypedDataFrame cols -> D.DataFrame summarize (TDF df) = Stats.summarize df -{- | Per-column summary (non-null\/null counts, unique values, type). Returns an-untyped 'D.DataFrame'.--}+-- | Per-column summary of counts, unique values and type, untyped. describeColumns :: TypedDataFrame cols -> D.DataFrame describeColumns (TDF df) = Core.describeColumns df