packages feed

dataframe 0.4.0.10 → 0.4.1.0

raw patch · 18 files changed

+1103/−566 lines, 18 filesdep +QuickCheckdep +unixPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: QuickCheck, unix

API changes (from Hackage documentation)

- DataFrame.DecisionTree: type CondGen = TreeConfig -> DataFrame -> [Expr Bool]
- DataFrame.Internal.Column: ifilterColumn :: Columnable a => (Int -> a -> Bool) -> Column -> Either DataFrameException Column
- DataFrame.Internal.Column: ifoldlColumn :: (Columnable a, Columnable b) => (b -> Int -> a -> b) -> b -> Column -> Either DataFrameException b
- DataFrame.Internal.Column: reduceColumn :: (Columnable a, Columnable b) => (a -> b) -> Column -> Either DataFrameException b
+ DataFrame: [missingIndicators] :: ReadOptions -> [Text]
+ DataFrame.DecisionTree: Branch :: !Expr Bool -> !Tree a -> !Tree a -> Tree a
+ DataFrame.DecisionTree: CarePoint :: !Int -> !Direction -> CarePoint
+ DataFrame.DecisionTree: GoLeft :: Direction
+ DataFrame.DecisionTree: GoRight :: Direction
+ DataFrame.DecisionTree: Leaf :: !a -> Tree a
+ DataFrame.DecisionTree: [cpCorrectDir] :: CarePoint -> !Direction
+ DataFrame.DecisionTree: [cpIndex] :: CarePoint -> !Int
+ DataFrame.DecisionTree: [taoConvergenceTol] :: TreeConfig -> Double
+ DataFrame.DecisionTree: [taoIterations] :: TreeConfig -> Int
+ DataFrame.DecisionTree: buildGreedyTree :: Columnable a => TreeConfig -> Int -> Text -> [Expr Bool] -> DataFrame -> Tree a
+ DataFrame.DecisionTree: computeTreeLoss :: Columnable a => Text -> DataFrame -> Vector Int -> Tree a -> Double
+ DataFrame.DecisionTree: countCarePointErrors :: Expr Bool -> DataFrame -> [CarePoint] -> Int
+ DataFrame.DecisionTree: data CarePoint
+ DataFrame.DecisionTree: data Direction
+ DataFrame.DecisionTree: data Tree a
+ DataFrame.DecisionTree: findBestGreedySplit :: Columnable a => TreeConfig -> Text -> [Expr Bool] -> DataFrame -> Maybe (Expr Bool)
+ DataFrame.DecisionTree: findBestSplitTAO :: Columnable a => TreeConfig -> Text -> [Expr Bool] -> DataFrame -> Vector Int -> Tree a -> Tree a -> Expr Bool -> Expr Bool
+ DataFrame.DecisionTree: identifyCarePoints :: Columnable a => Text -> DataFrame -> Vector Int -> Tree a -> Tree a -> [CarePoint]
+ DataFrame.DecisionTree: instance GHC.Classes.Eq DataFrame.DecisionTree.CarePoint
+ DataFrame.DecisionTree: instance GHC.Classes.Eq DataFrame.DecisionTree.Direction
+ DataFrame.DecisionTree: instance GHC.Classes.Eq a => GHC.Classes.Eq (DataFrame.DecisionTree.Tree a)
+ DataFrame.DecisionTree: instance GHC.Show.Show DataFrame.DecisionTree.CarePoint
+ DataFrame.DecisionTree: instance GHC.Show.Show DataFrame.DecisionTree.Direction
+ DataFrame.DecisionTree: instance GHC.Show.Show a => GHC.Show.Show (DataFrame.DecisionTree.Tree a)
+ DataFrame.DecisionTree: majorityValueFromIndices :: Columnable a => Text -> DataFrame -> Vector Int -> a
+ DataFrame.DecisionTree: optimizeAtDepth :: Columnable a => TreeConfig -> Text -> [Expr Bool] -> DataFrame -> Vector Int -> Tree a -> Int -> Int -> Tree a
+ DataFrame.DecisionTree: optimizeDepthLevel :: Columnable a => TreeConfig -> Text -> [Expr Bool] -> DataFrame -> Vector Int -> Tree a -> Int -> Tree a
+ DataFrame.DecisionTree: optimizeNode :: Columnable a => TreeConfig -> Text -> [Expr Bool] -> DataFrame -> Vector Int -> Tree a -> Tree a
+ DataFrame.DecisionTree: partitionIndices :: Expr Bool -> DataFrame -> Vector Int -> (Vector Int, Vector Int)
+ DataFrame.DecisionTree: percentile :: Int -> Expr Double -> DataFrame -> Double
+ DataFrame.DecisionTree: predictWithTree :: Columnable a => Text -> DataFrame -> Int -> Tree a -> a
+ DataFrame.DecisionTree: pruneDead :: Tree a -> Tree a
+ DataFrame.DecisionTree: pruneExpr :: (Columnable a, Eq a) => Expr a -> Expr a
+ DataFrame.DecisionTree: taoIteration :: Columnable a => TreeConfig -> Text -> [Expr Bool] -> DataFrame -> Vector Int -> Tree a -> Tree a
+ DataFrame.DecisionTree: taoOptimize :: Columnable a => TreeConfig -> Text -> [Expr Bool] -> DataFrame -> Vector Int -> Tree a -> Tree a
+ DataFrame.DecisionTree: treeDepth :: Tree a -> Int
+ DataFrame.DecisionTree: treeToExpr :: Columnable a => Tree a -> Expr a
+ DataFrame.IO.CSV: [missingIndicators] :: ReadOptions -> [Text]
+ DataFrame.IO.Parquet.Thrift: createParquetSchema :: DataFrame -> [SchemaElement]
+ DataFrame.IO.Parquet.Thrift: haskellToTType :: Typeable a => TType
+ DataFrame.Internal.Column: generalLEQ :: (Typeable a, Typeable b, Ord a, Ord b) => a -> b -> Bool
+ DataFrame.Internal.Column: throwTypeMismatch :: (Typeable a, Typeable b) => Either DataFrameException Column
+ DataFrame.Monad: sampleM :: RandomGen g => g -> Double -> FrameM ()
+ DataFrame.Monad: takeM :: Int -> FrameM ()
- DataFrame: ReadOptions :: HeaderSpec -> TypeSpec -> Bool -> String -> Char -> Maybe Int -> ReadOptions
+ DataFrame: ReadOptions :: HeaderSpec -> TypeSpec -> Bool -> String -> Char -> Maybe Int -> [Text] -> ReadOptions
- DataFrame: columnAsDoubleVector :: Expr Double -> DataFrame -> Either DataFrameException (Vector Double)
+ DataFrame: columnAsDoubleVector :: (Columnable a, Num a) => Expr a -> DataFrame -> Either DataFrameException (Vector Double)
- DataFrame: columnAsFloatVector :: Expr Float -> DataFrame -> Either DataFrameException (Vector Float)
+ DataFrame: columnAsFloatVector :: (Columnable a, Num a) => Expr a -> DataFrame -> Either DataFrameException (Vector Float)
- DataFrame: columnAsIntVector :: Expr Int -> DataFrame -> Either DataFrameException (Vector Int)
+ DataFrame: columnAsIntVector :: (Columnable a, Num a) => Expr a -> DataFrame -> Either DataFrameException (Vector Int)
- DataFrame.DecisionTree: TreeConfig :: Int -> Int -> Int -> [Int] -> Int -> SynthConfig -> TreeConfig
+ DataFrame.DecisionTree: TreeConfig :: Int -> Int -> Int -> [Int] -> Int -> SynthConfig -> Int -> Double -> TreeConfig
- DataFrame.DecisionTree: numericConditions :: CondGen
+ DataFrame.DecisionTree: numericConditions :: TreeConfig -> DataFrame -> [Expr Bool]
- DataFrame.IO.CSV: ReadOptions :: HeaderSpec -> TypeSpec -> Bool -> String -> Char -> Maybe Int -> ReadOptions
+ DataFrame.IO.CSV: ReadOptions :: HeaderSpec -> TypeSpec -> Bool -> String -> Char -> Maybe Int -> [Text] -> ReadOptions
- DataFrame.IO.CSV: processRow :: Vector ByteString -> [BuilderColumn] -> IO ()
+ DataFrame.IO.CSV: processRow :: [Text] -> Vector ByteString -> [BuilderColumn] -> IO ()
- DataFrame.IO.CSV: processStream :: Records (Vector ByteString) -> [BuilderColumn] -> Maybe Int -> IO ()
+ DataFrame.IO.CSV: processStream :: [Text] -> Records (Vector ByteString) -> [BuilderColumn] -> Maybe Int -> IO ()
- DataFrame.Internal.Column: mapColumn :: (Columnable b, Columnable c, UnboxIf c) => (b -> c) -> Column -> Either DataFrameException Column
+ DataFrame.Internal.Column: mapColumn :: (Columnable b, Columnable c) => (b -> c) -> Column -> Either DataFrameException Column
- DataFrame.Operations.Core: columnAsDoubleVector :: Expr Double -> DataFrame -> Either DataFrameException (Vector Double)
+ DataFrame.Operations.Core: columnAsDoubleVector :: (Columnable a, Num a) => Expr a -> DataFrame -> Either DataFrameException (Vector Double)
- DataFrame.Operations.Core: columnAsFloatVector :: Expr Float -> DataFrame -> Either DataFrameException (Vector Float)
+ DataFrame.Operations.Core: columnAsFloatVector :: (Columnable a, Num a) => Expr a -> DataFrame -> Either DataFrameException (Vector Float)
- DataFrame.Operations.Core: columnAsIntVector :: Expr Int -> DataFrame -> Either DataFrameException (Vector Int)
+ DataFrame.Operations.Core: columnAsIntVector :: (Columnable a, Num a) => Expr a -> DataFrame -> Either DataFrameException (Vector Int)
- DataFrame.Operations.Typing: convertNullish :: Text -> Maybe Text
+ DataFrame.Operations.Typing: convertNullish :: [Text] -> Text -> Maybe Text
- DataFrame.Operations.Typing: parseDefault :: Int -> Bool -> DateFormat -> Column -> Column
+ DataFrame.Operations.Typing: parseDefault :: [Text] -> Int -> Bool -> DateFormat -> Column -> Column
- DataFrame.Operations.Typing: parseDefaults :: Int -> Bool -> DateFormat -> DataFrame -> DataFrame
+ DataFrame.Operations.Typing: parseDefaults :: [Text] -> Int -> Bool -> DateFormat -> DataFrame -> DataFrame
- DataFrame.Operations.Typing: parseFromExamples :: Int -> Bool -> DateFormat -> Vector Text -> Column
+ DataFrame.Operations.Typing: parseFromExamples :: [Text] -> Int -> Bool -> DateFormat -> Vector Text -> Column

Files

CHANGELOG.md view
@@ -1,5 +1,12 @@ # Revision history for dataframe +## 0.4.1.0+* Improve signal handling of dataframe repl.+* `writeCsv` not correctly writes `Maybe` values (thanks to @mcoady).+* Create a boilerplate package in cache that will be used to start repl.+* Tree implementation is now a TAO tree instead of greedy cart trees.+* Add `sampleM` and `takeM` functions.+ ## 0.4.0.10 * License in cabal was wrong. * Remove ollama-haskell dependencies.
app/Main.hs view
@@ -1,32 +1,51 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ScopedTypeVariables #-}+ module Main where +import Control.Exception (bracket) import Control.Monad import Data.Time (NominalDiffTime, diffUTCTime, getCurrentTime) import System.Directory (     XdgDirectory (..),     createDirectoryIfMissing,     doesFileExist,+    getCurrentDirectory,     getModificationTime,     getXdgDirectory,  )+import System.Exit (ExitCode (..)) import System.FilePath ((</>)) import System.Process +#ifndef mingw32_HOST_OS+import System.Posix.Signals (Handler (..), installHandler, sigINT)+#endif+ main :: IO () main = do     cacheDir <- getXdgDirectory XdgCache "dataframe_repl"+    currDir <- getCurrentDirectory+    print currDir     createDirectoryIfMissing True cacheDir -    let filepath = cacheDir </> "dataframe.ghci"-    shouldDownload <- needsUpdate filepath+    let ghciScript = cacheDir </> "dataframe.ghci"+        projectFile = cacheDir </> "cabal.project"+        cabalFile = cacheDir </> "df-repl.cabal"+        mainFile = cacheDir </> "Main.hs" +    -- Ensure the directory is a valid Cabal project with a local target to repl into.+    ensureBoilerplate projectFile cabalFile mainFile++    -- Keep your ghci script updated (weekly).+    shouldDownload <- needsUpdate ghciScript     when shouldDownload $ do         putStrLn "\ESC[92mDownloading latest version of dataframe config...\ESC[0m"         output <-             readProcess                 "curl"                 [ "--output"-                , filepath+                , ghciScript                 , "https://raw.githubusercontent.com/mchav/dataframe/refs/heads/main/dataframe.ghci"                 ]                 ""@@ -37,15 +56,82 @@     let command = "cabal"         args =             [ "repl"+            , "exe:df-repl"+            , "--project-file=" ++ projectFile             , "-O2"-            , "--build-depends"-            , "dataframe"-            , "--repl-option=-ghci-script=" ++ filepath+            , "--repl-option=-ghci-script=" ++ ghciScript             ]-    (_, _, _, processHandle) <- createProcess (proc command args) -    exitCode <- waitForProcess processHandle-    pure ()+        baseCp =+            (proc command args)+                { cwd = Just currDir+                , std_in = Inherit+                , std_out = Inherit+                , std_err = Inherit+                }++#ifdef mingw32_HOST_OS+        cp = baseCp { delegate_ctlc = True }+#else+        cp = baseCp+#endif++#ifndef mingw32_HOST_OS+    -- Unix: ignore Ctrl-C in the wrapper so the child handles it.+    bracket+        (installHandler sigINT Ignore Nothing)+        (\old -> installHandler sigINT old Nothing)+        (\_ -> runChild cp)+#else+    -- Windows: delegate Ctrl-C handling to the child.+    runChild cp+#endif++ensureBoilerplate :: FilePath -> FilePath -> FilePath -> IO ()+ensureBoilerplate projectFile cabalFile mainFile = do+    -- cabal.project+    writeIfMissing+        projectFile+        $ unlines+            [ "packages: ."+            ]++    writeIfMissing+        cabalFile+        $ unlines+            [ "cabal-version:      3.0"+            , "name:               df-repl"+            , "version:            0.1.0.0"+            , "build-type:         Simple"+            , ""+            , "executable df-repl"+            , "  main-is:          Main.hs"+            , "  hs-source-dirs:   ."+            , "  default-language: Haskell2010"+            , "  build-depends:    base, dataframe, text, time, random"+            ]++    writeIfMissing+        mainFile+        $ unlines+            [ "module Main where"+            , ""+            , "main :: IO ()"+            , "main = putStrLn \"df-repl\""+            ]++writeIfMissing :: FilePath -> String -> IO ()+writeIfMissing fp contents = do+    exists <- doesFileExist fp+    unless exists $ writeFile fp contents++runChild :: CreateProcess -> IO ()+runChild cp = do+    (_, _, _, ph) <- createProcess cp+    ec <- waitForProcess ph+    case ec of+        ExitSuccess -> pure ()+        ExitFailure n -> fail ("cabal repl failed with exit code " <> show n)  oneWeek :: NominalDiffTime oneWeek = 7 * 24 * 60 * 60
app/Synthesis.hs view
@@ -43,7 +43,7 @@                 (D.nRows train)                 combined                 |> D.filterJust (F.name survived)-                |> D.randomSplit (mkStdGen 4232) 0.8+                |> D.randomSplit (mkStdGen 4232) 0.7         -- Split the test out again.         test' =             D.drop@@ -54,14 +54,18 @@             fitDecisionTree                 ( defaultTreeConfig                     { maxTreeDepth = 5-                    , minSamplesSplit = 25-                    , minLeafSize = 15+                    , minSamplesSplit = 10+                    , minLeafSize = 3+                    , taoIterations = 100                     , synthConfig =                         defaultSynthConfig-                            { complexityPenalty = 0+                            { complexityPenalty = 0.00                             , maxExprDepth = 2                             , disallowedCombinations =-                                [(F.name age, F.name fare)]+                                [ (F.name age, F.name fare)+                                , ("passenger_class", "number_of_siblings_and_spouses")+                                , ("passenger_class", "number_of_parents_and_children")+                                ]                             }                     }                 )
dataframe.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               dataframe-version:            0.4.0.10+version:            0.4.1.0  synopsis: A fast, safe, and intuitive DataFrame library. @@ -157,7 +157,8 @@                       directory >= 1.3.0.0 && < 2,                       filepath >= 1.4 && < 2,                       process >= 1.6 && < 2,-                      time >= 1.12 && < 2+                      time >= 1.12 && < 2,+                      unix >= 2 && < 3     hs-source-dirs:   app     default-language: Haskell2010     ghc-options: -rtsopts -threaded -with-rtsopts=-N@@ -183,6 +184,7 @@     main-is: Main.hs     other-modules: Assertions,                    Functions,+                   GenDataFrame,                    Operations.Aggregations,                    Operations.Apply,                    Operations.Core,@@ -194,6 +196,7 @@                    Operations.Merge,                    Operations.ReadCsv,                    Operations.Sort,+                   Operations.Subset,                    Operations.Statistics,                    Operations.Take,                    Parquet@@ -201,6 +204,7 @@                     dataframe ^>= 0.4,                     directory >= 1.3.0.0 && < 2,                     HUnit ^>= 1.6,+                    QuickCheck >= 2 && < 3,                     random >= 1 && < 2,                     random-shuffle >= 0.0.4 && < 1,                     random >= 1 && < 2,
src/DataFrame/DecisionTree.hs view
@@ -1,11 +1,7 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExplicitNamespaces #-} {-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} @@ -14,19 +10,18 @@ import qualified DataFrame.Functions as F import DataFrame.Internal.Column import DataFrame.Internal.DataFrame (DataFrame (..), unsafeGetColumn)-import DataFrame.Internal.Expression (Expr (..), eSize, getColumns, prettyPrint)+import DataFrame.Internal.Expression (Expr (..), eSize, getColumns) import DataFrame.Internal.Interpreter (interpret) import DataFrame.Internal.Statistics (percentile', percentileOrd') import DataFrame.Internal.Types import DataFrame.Operations.Core (columnNames, nRows)-import DataFrame.Operations.Statistics (percentile) import DataFrame.Operations.Subset (exclude, filterWhere) -import Control.Exception (SomeException, throw, try)+import Control.Exception (throw) import Control.Monad (guard) import Data.Containers.ListUtils (nubOrd) import Data.Function (on)-import Data.List (foldl', maximumBy, sort, sortBy)+import Data.List (foldl', maximumBy, minimumBy, sort, sortBy) import qualified Data.Map.Strict as M import Data.Maybe import qualified Data.Text as T@@ -35,19 +30,17 @@ import qualified Data.Vector.Unboxed as VU import Type.Reflection (typeRep) -import Data.Text.Read import DataFrame.Functions ((./=), (.<), (.<=), (.==), (.>), (.>=))-import Debug.Trace (trace)-import GHC.IO.Unsafe (unsafePerformIO) -data TreeConfig-    = TreeConfig+data TreeConfig = TreeConfig     { maxTreeDepth :: Int     , minSamplesSplit :: Int     , minLeafSize :: Int     , percentiles :: [Int]     , expressionPairs :: Int     , synthConfig :: SynthConfig+    , taoIterations :: Int+    , taoConvergenceTol :: Double     }     deriving (Eq, Show) @@ -83,8 +76,25 @@         , percentiles = [0, 10 .. 100]         , expressionPairs = 10         , synthConfig = defaultSynthConfig+        , taoIterations = 10+        , taoConvergenceTol = 1e-6         } +data Tree a+    = Leaf !a+    | Branch !(Expr Bool) !(Tree a) !(Tree a)+    deriving (Eq, Show)++treeDepth :: Tree a -> Int+treeDepth (Leaf _) = 0+treeDepth (Branch _ l r) = 1 + max (treeDepth l) (treeDepth r)++treeToExpr :: (Columnable a) => Tree a -> Expr a+treeToExpr (Leaf v) = Lit v+treeToExpr (Branch cond left right) =+    F.ifThenElse cond (treeToExpr left) (treeToExpr right)++-- | Fit a TAO decision tree fitDecisionTree ::     forall a.     (Columnable a) =>@@ -93,70 +103,455 @@     DataFrame ->     Expr a fitDecisionTree cfg (Col target) df =-    buildTree @a-        cfg-        (maxTreeDepth cfg)-        target-        ( nubOrd $-            numericConditions cfg (exclude [target] df)-                ++ generateConditionsOld cfg (exclude [target] df)-        )-        df+    let+        conds =+            nubOrd $+                numericConditions cfg (exclude [target] df)+                    ++ generateConditionsOld cfg (exclude [target] df)++        initialTree = buildGreedyTree @a cfg (maxTreeDepth cfg) target conds df++        indices = V.enumFromN 0 (nRows df)++        optimizedTree = taoOptimize @a cfg target conds df indices initialTree+     in+        pruneExpr (treeToExpr optimizedTree) fitDecisionTree _ expr _ = error $ "Cannot create tree for compound expression: " ++ show expr -buildTree ::+taoOptimize ::     forall a.     (Columnable a) =>     TreeConfig ->+    T.Text -> -- Target column name+    [Expr Bool] -> -- Candidate conditions+    DataFrame -> -- Full dataset+    V.Vector Int -> -- Indices of points reaching the root+    Tree a -> -- Current tree+    Tree a+taoOptimize cfg target conds df rootIndices initialTree =+    go 0 initialTree (computeTreeLoss @a target df rootIndices initialTree)+  where+    go :: Int -> Tree a -> Double -> Tree a+    go iter tree prevLoss+        | iter >= taoIterations cfg = pruneDead tree+        | otherwise =+            let+                tree' = taoIteration @a cfg target conds df rootIndices tree++                newLoss = computeTreeLoss @a target df rootIndices tree'+                improvement = prevLoss - newLoss+             in+                if improvement < taoConvergenceTol cfg+                    then pruneDead tree'+                    else go (iter + 1) tree' newLoss++taoIteration ::+    forall a.+    (Columnable a) =>+    TreeConfig ->+    T.Text ->+    [Expr Bool] ->+    DataFrame ->+    V.Vector Int ->+    Tree a ->+    Tree a+taoIteration cfg target conds df rootIndices tree =+    let depth = treeDepth tree+     in foldl'+            (optimizeDepthLevel @a cfg target conds df rootIndices)+            tree+            [depth, depth - 1 .. 0] -- Bottom to top++optimizeDepthLevel ::+    forall a.+    (Columnable a) =>+    TreeConfig ->+    T.Text ->+    [Expr Bool] ->+    DataFrame ->+    V.Vector Int ->+    Tree a ->+    Int -> -- Target depth+    Tree a+optimizeDepthLevel cfg target conds df rootIndices tree = optimizeAtDepth @a cfg target conds df rootIndices tree 0++optimizeAtDepth ::+    forall a.+    (Columnable a) =>+    TreeConfig ->+    T.Text ->+    [Expr Bool] ->+    DataFrame ->+    V.Vector Int ->+    Tree a ->     Int ->+    Int ->+    Tree a+optimizeAtDepth cfg target conds df indices tree currentDepth targetDepth+    | currentDepth == targetDepth =+        optimizeNode @a cfg target conds df indices tree+    | otherwise = case tree of+        Leaf v -> Leaf v+        Branch cond left right ->+            let+                (indicesL, indicesR) = partitionIndices cond df indices+                left' =+                    optimizeAtDepth @a+                        cfg+                        target+                        conds+                        df+                        indicesL+                        left+                        (currentDepth + 1)+                        targetDepth+                right' =+                    optimizeAtDepth @a+                        cfg+                        target+                        conds+                        df+                        indicesR+                        right+                        (currentDepth + 1)+                        targetDepth+             in+                Branch cond left' right'++optimizeNode ::+    forall a.+    (Columnable a) =>+    TreeConfig ->     T.Text ->     [Expr Bool] ->     DataFrame ->-    Expr a-buildTree cfg depth target conds df-    | depth <= 0 || nRows df <= minSamplesSplit cfg =-        Lit (majorityValue @a target df)+    V.Vector Int ->+    Tree a ->+    Tree a+optimizeNode cfg target conds df indices tree+    | V.null indices = tree+    | otherwise = case tree of+        Leaf _ -> Leaf (majorityValueFromIndices @a target df indices)+        Branch oldCond left right ->+            let+                newCond = findBestSplitTAO @a cfg target conds df indices left right oldCond++                (newIndicesL, newIndicesR) = partitionIndices newCond df indices+             in+                if V.length newIndicesL < minLeafSize cfg+                    || V.length newIndicesR < minLeafSize cfg+                    then Leaf (majorityValueFromIndices @a target df indices)+                    else Branch newCond left right++findBestSplitTAO ::+    forall a.+    (Columnable a) =>+    TreeConfig ->+    T.Text ->+    [Expr Bool] ->+    DataFrame ->+    V.Vector Int ->+    Tree a -> -- Left subtree (FIXED)+    Tree a -> -- Right subtree (FIXED)+    Expr Bool -> -- Current condition (fallback)+    Expr Bool+findBestSplitTAO cfg target conds df indices leftTree rightTree currentCond+    | V.null indices = currentCond+    | null validConds = currentCond     | otherwise =-        case findBestSplit @a cfg target conds df of-            Nothing -> Lit (majorityValue @a target df)-            Just bestCond ->-                let (dfTrue, dfFalse) = partitionDataFrame bestCond df-                 in if nRows dfTrue == 0 || nRows dfFalse == 0-                        then Lit (majorityValue @a target df)-                        else-                            pruneTree-                                ( F.ifThenElse-                                    bestCond-                                    (buildTree @a cfg (depth - 1) target conds dfTrue)-                                    (buildTree @a cfg (depth - 1) target conds dfFalse)-                                )+        let+            carePoints = identifyCarePoints @a target df indices leftTree rightTree+         in+            if null carePoints+                then currentCond+                else+                    let+                        evalSplit :: Expr Bool -> Int+                        evalSplit cond = countCarePointErrors cond df carePoints -pruneTree :: forall a. (Columnable a, Eq a) => Expr a -> Expr a-pruneTree (If cond trueBranch falseBranch) =+                        evalWithPenalty c =+                            let errors = evalSplit c+                                penalty =+                                    floor+                                        ( complexityPenalty (synthConfig cfg)+                                            * fromIntegral (eSize c)+                                        )+                             in errors + penalty++                        sortedConds =+                            take (expressionPairs cfg) $+                                sortBy (compare `on` evalWithPenalty) validConds++                        expandedConds =+                            boolExprs+                                df+                                sortedConds+                                sortedConds+                                0+                                (boolExpansion (synthConfig cfg))+                     in+                        if null expandedConds+                            then currentCond+                            else minimumBy (compare `on` evalWithPenalty) expandedConds+  where+    validConds = filter isValidSplit conds+    isValidSplit c =+        let (t, f) = partitionIndices c df indices+         in V.length t >= minLeafSize cfg && V.length f >= minLeafSize cfg++-- | A care point with its index and which direction leads to correct classification+data CarePoint = CarePoint+    { cpIndex :: !Int+    , cpCorrectDir :: !Direction -- Which child classifies this point correctly+    }+    deriving (Eq, Show)++data Direction = GoLeft | GoRight+    deriving (Eq, Show)++{- | Identify care points: points where exactly one subtree classifies correctly++   For each point reaching the node:+   1. Compute what label the left subtree would predict+   2. Compute what label the right subtree would predict+   3. If exactly one matches the true label, it's a care point+   4. Record which direction leads to correct classification+-}+identifyCarePoints ::+    forall a.+    (Columnable a) =>+    T.Text ->+    DataFrame ->+    V.Vector Int ->+    Tree a -> -- Left subtree+    Tree a -> -- Right subtree+    [CarePoint]+identifyCarePoints target df indices leftTree rightTree =+    case interpret @a df (Col target) of+        Left _ -> []+        Right (TColumn col) ->+            case toVector @a col of+                Left _ -> []+                Right targetVals ->+                    V.toList $ V.mapMaybe (checkPoint targetVals) indices+  where+    checkPoint :: V.Vector a -> Int -> Maybe CarePoint+    checkPoint targetVals idx =+        let+            trueLabel = targetVals V.! idx+            leftPred = predictWithTree @a target df idx leftTree+            rightPred = predictWithTree @a target df idx rightTree+            leftCorrect = leftPred == trueLabel+            rightCorrect = rightPred == trueLabel+         in+            case (leftCorrect, rightCorrect) of+                (True, False) -> Just $ CarePoint idx GoLeft+                (False, True) -> Just $ CarePoint idx GoRight+                _ -> Nothing -- Don't-care point (both correct or both wrong)++-- | Predict the label for a single point using a fixed tree+predictWithTree ::+    forall a.+    (Columnable a) =>+    T.Text ->+    DataFrame ->+    Int -> -- Row index+    Tree a ->+    a+predictWithTree target df idx (Leaf v) = v+predictWithTree target df idx (Branch cond left right) =+    case interpret @Bool df cond of+        Left _ -> predictWithTree @a target df idx left -- Default to left on error+        Right (TColumn col) ->+            case toVector @Bool col of+                Left _ -> predictWithTree @a target df idx left+                Right boolVals ->+                    if boolVals V.! idx+                        then predictWithTree @a target df idx left+                        else predictWithTree @a target df idx right++countCarePointErrors :: Expr Bool -> DataFrame -> [CarePoint] -> Int+countCarePointErrors cond df carePoints =+    case interpret @Bool df cond of+        Left _ -> length carePoints+        Right (TColumn col) ->+            case toVector @Bool col of+                Left _ -> length carePoints+                Right boolVals ->+                    length $ filter (isMisclassified boolVals) carePoints+  where+    isMisclassified :: V.Vector Bool -> CarePoint -> Bool+    isMisclassified boolVals cp =+        let goesLeft = boolVals V.! cpIndex cp+            shouldGoLeft = cpCorrectDir cp == GoLeft+         in goesLeft /= shouldGoLeft++partitionIndices ::+    Expr Bool -> DataFrame -> V.Vector Int -> (V.Vector Int, V.Vector Int)+partitionIndices cond df indices =+    case interpret @Bool df cond of+        Left _ -> (indices, V.empty)+        Right (TColumn col) ->+            case toVector @Bool col of+                Left _ -> (indices, V.empty)+                Right boolVals ->+                    V.partition (boolVals V.!) indices++majorityValueFromIndices ::+    forall a.+    (Columnable a) =>+    T.Text ->+    DataFrame ->+    V.Vector Int ->+    a+majorityValueFromIndices target df indices =+    case interpret @a df (Col target) of+        Left e -> throw e+        Right (TColumn col) ->+            case toVector @a col of+                Left e -> throw e+                Right vals ->+                    let counts =+                            V.foldl'+                                (\acc i -> M.insertWith (+) (vals V.! i) 1 acc)+                                M.empty+                                indices+                     in if M.null counts+                            then error "Empty indices in majorityValueFromIndices"+                            else fst $ maximumBy (compare `on` snd) (M.toList counts)++computeTreeLoss ::+    forall a.+    (Columnable a) =>+    T.Text ->+    DataFrame ->+    V.Vector Int ->+    Tree a ->+    Double+computeTreeLoss target df indices tree+    | V.null indices = 0+    | otherwise =+        case interpret @a df (Col target) of+            Left _ -> 1.0+            Right (TColumn col) ->+                case toVector @a col of+                    Left _ -> 1.0+                    Right targetVals ->+                        let+                            n = V.length indices+                            errors =+                                V.length $+                                    V.filter+                                        (\i -> targetVals V.! i /= predictWithTree @a target df i tree)+                                        indices+                         in+                            fromIntegral errors / fromIntegral n++pruneDead :: Tree a -> Tree a+pruneDead (Leaf v) = Leaf v+pruneDead (Branch cond left right) =     let-        t = pruneTree trueBranch-        f = pruneTree falseBranch+        left' = pruneDead left+        right' = pruneDead right      in-        if t == f+        Branch cond left' right'++pruneExpr :: forall a. (Columnable a, Eq a) => Expr a -> Expr a+pruneExpr (If cond trueBranch falseBranch) =+    let t = pruneExpr trueBranch+        f = pruneExpr falseBranch+     in if t == f             then t             else case (t, f) of-                -- Nested simplification: `if C1 then (if C1 then X else Y) else Z`-                -- becomes:     if C1 then X else Z`-                -- Generalize this with hegg later.-                (If condInner tInner fInner, _) | cond == condInner -> If cond tInner f-                (_, If condInner tInner fInner) | cond == condInner -> If cond t fInner+                (If condInner tInner _, _) | cond == condInner -> If cond tInner f+                (_, If condInner _ fInner) | cond == condInner -> If cond t fInner                 _ -> If cond t f-pruneTree (UnaryOp name op e) = UnaryOp name op (pruneTree e)-pruneTree (BinaryOp name op l r) = BinaryOp name op (pruneTree l) (pruneTree r)-pruneTree e = e+pruneExpr (UnaryOp name op e) = UnaryOp name op (pruneExpr e)+pruneExpr (BinaryOp name op l r) = BinaryOp name op (pruneExpr l) (pruneExpr r)+pruneExpr e = e -type CondGen = TreeConfig -> DataFrame -> [Expr Bool]+buildGreedyTree ::+    forall a.+    (Columnable a) =>+    TreeConfig ->+    Int ->+    T.Text ->+    [Expr Bool] ->+    DataFrame ->+    Tree a+buildGreedyTree cfg depth target conds df+    | depth <= 0 || nRows df <= minSamplesSplit cfg =+        Leaf (majorityValue @a target df)+    | otherwise =+        case findBestGreedySplit @a cfg target conds df of+            Nothing -> Leaf (majorityValue @a target df)+            Just bestCond ->+                let (dfTrue, dfFalse) = partitionDataFrame bestCond df+                 in if nRows dfTrue < minLeafSize cfg || nRows dfFalse < minLeafSize cfg+                        then Leaf (majorityValue @a target df)+                        else+                            Branch+                                bestCond+                                (buildGreedyTree @a cfg (depth - 1) target conds dfTrue)+                                (buildGreedyTree @a cfg (depth - 1) target conds dfFalse) -numericConditions :: CondGen+findBestGreedySplit ::+    forall a.+    (Columnable a) =>+    TreeConfig -> T.Text -> [Expr Bool] -> DataFrame -> Maybe (Expr Bool)+findBestGreedySplit cfg target conds df =+    let+        initialImpurity = calculateGini @a target df+        calculateComplexity c = complexityPenalty (synthConfig cfg) * fromIntegral (eSize c)++        evalGain :: Expr Bool -> (Double, Int)+        evalGain cond =+            let (t, f) = partitionDataFrame cond df+                n = fromIntegral @Int @Double (nRows df)+                weightT = fromIntegral @Int @Double (nRows t) / n+                weightF = fromIntegral @Int @Double (nRows f) / n+                newImpurity =+                    weightT * calculateGini @a target t+                        + weightF * calculateGini @a target f+             in ( (initialImpurity - newImpurity) - calculateComplexity cond+                , negate (eSize cond)+                )++        validConds =+            filter+                ( \c ->+                    let (t, f) = partitionDataFrame c df+                     in nRows t >= minLeafSize cfg && nRows f >= minLeafSize cfg+                )+                conds++        sortedConditions =+            map fst $+                take+                    (expressionPairs cfg)+                    ( filter+                        (\(c, v) -> ((> negate (calculateComplexity c)) . fst) v)+                        (sortBy (flip compare `on` snd) (map (\c -> (c, evalGain c)) validConds))+                    )+     in+        if null sortedConditions+            then Nothing+            else+                Just $+                    maximumBy+                        (compare `on` evalGain)+                        ( boolExprs+                            df+                            sortedConditions+                            sortedConditions+                            0+                            (boolExpansion (synthConfig cfg))+                        )++numericConditions :: TreeConfig -> DataFrame -> [Expr Bool] numericConditions = generateNumericConds -generateNumericConds ::-    TreeConfig -> DataFrame -> [Expr Bool]+generateNumericConds :: TreeConfig -> DataFrame -> [Expr Bool] generateNumericConds cfg df = do     expr <- numericExprsWithTerms (synthConfig cfg) df     let thresholds = map (\p -> percentile p expr df) (percentiles cfg)@@ -167,8 +562,7 @@         , expr .> F.lit threshold         ] -numericExprsWithTerms ::-    SynthConfig -> DataFrame -> [Expr Double]+numericExprsWithTerms :: SynthConfig -> DataFrame -> [Expr Double] numericExprsWithTerms cfg df =     concatMap (numericExprs cfg df [] 0) [0 .. maxExprDepth cfg] @@ -230,18 +624,14 @@         genConds :: T.Text -> [Expr Bool]         genConds colName = case unsafeGetColumn colName df of             (BoxedColumn (col :: V.Vector a)) ->-                let-                    percentiles = map (Lit . (`percentileOrd'` col)) [1, 25, 75, 99]-                 in-                    map (Col @a colName .==) percentiles+                let ps = map (Lit . (`percentileOrd'` col)) [1, 25, 75, 99]+                 in map (Col @a colName .==) ps             (OptionalColumn (col :: V.Vector (Maybe a))) -> case sFloating @a of                 STrue ->-                    let-                        doubleCol =+                    let doubleCol =                             VU.convert                                 (V.map fromJust (V.filter isJust (V.map (fmap (realToFrac @a @Double)) col)))-                     in-                        zipWith+                     in zipWith                             ($)                             [ (Col @(Maybe a) colName .==)                             , (Col @(Maybe a) colName .<=)@@ -249,21 +639,15 @@                             ]                             ( Lit Nothing                                 : map-                                    ( Lit-                                        . Just-                                        . realToFrac-                                        . (`percentile'` doubleCol)-                                    )+                                    (Lit . Just . realToFrac . (`percentile'` doubleCol))                                     (percentiles cfg)                             )                 SFalse -> case sIntegral @a of                     STrue ->-                        let-                            doubleCol =+                        let doubleCol =                                 VU.convert                                     (V.map fromJust (V.filter isJust (V.map (fmap (fromIntegral @a @Double)) col)))-                         in-                            zipWith+                         in zipWith                                 ($)                                 [ (Col @(Maybe a) colName .==)                                 , (Col @(Maybe a) colName .<=)@@ -271,18 +655,15 @@                                 ]                                 ( Lit Nothing                                     : map-                                        ( Lit-                                            . Just-                                            . round-                                            . (`percentile'` doubleCol)-                                        )+                                        (Lit . Just . round . (`percentile'` doubleCol))                                         (percentiles cfg)                                 )                     SFalse ->                         map                             ((Col @(Maybe a) colName .==) . Lit . (`percentileOrd'` col))                             [1, 25, 75, 99]-            (UnboxedColumn (col :: VU.Vector a)) -> []+            (UnboxedColumn (_ :: VU.Vector a)) -> []+         columnConds =             concatMap                 colConds@@ -297,12 +678,13 @@                 ]           where             colConds (!l, !r) = case (unsafeGetColumn l df, unsafeGetColumn r df) of-                (BoxedColumn (col1 :: V.Vector a), BoxedColumn (col2 :: V.Vector b)) -> case testEquality (typeRep @a) (typeRep @b) of-                    Nothing -> []-                    Just Refl -> [Col @a l .== Col @a r]-                (UnboxedColumn (col1 :: VU.Vector a), UnboxedColumn (col2 :: VU.Vector b)) -> []-                ( OptionalColumn (col1 :: V.Vector (Maybe a))-                    , OptionalColumn (col2 :: V.Vector (Maybe b))+                (BoxedColumn (col1 :: V.Vector a), BoxedColumn (_ :: V.Vector b)) ->+                    case testEquality (typeRep @a) (typeRep @b) of+                        Nothing -> []+                        Just Refl -> [Col @a l .== Col @a r]+                (UnboxedColumn (_ :: VU.Vector a), UnboxedColumn (_ :: VU.Vector b)) -> []+                ( OptionalColumn (_ :: V.Vector (Maybe a))+                    , OptionalColumn (_ :: V.Vector (Maybe b))                     ) -> case testEquality (typeRep @a) (typeRep @b) of                         Nothing -> []                         Just Refl -> case testEquality (typeRep @a) (typeRep @T.Text) of@@ -315,67 +697,7 @@ partitionDataFrame :: Expr Bool -> DataFrame -> (DataFrame, DataFrame) partitionDataFrame cond df = (filterWhere cond df, filterWhere (F.not cond) df) -findBestSplit ::-    forall a.-    (Columnable a) =>-    TreeConfig -> T.Text -> [Expr Bool] -> DataFrame -> Maybe (Expr Bool)-findBestSplit cfg target conds df =-    let-        initialImpurity = calculateGini @a target df-        calculateComplexity c = complexityPenalty (synthConfig cfg) * fromIntegral (eSize c)-        evalGain :: Expr Bool -> (Double, Int)-        evalGain cond =-            let (t, f) = partitionDataFrame cond df-                n = fromIntegral @Int @Double (nRows df)-                weightT = fromIntegral @Int @Double (nRows t) / n-                weightF = fromIntegral @Int @Double (nRows f) / n-                newImpurity =-                    (weightT * calculateGini @a target t)-                        + (weightF * calculateGini @a target f)-             in ( (initialImpurity - newImpurity)-                    - calculateComplexity cond-                , negate (eSize cond)-                )--        validConds =-            filter-                ( \c ->-                    let-                        (t, f) = partitionDataFrame c df-                     in-                        nRows t >= minLeafSize cfg && nRows f >= minLeafSize cfg-                )-                conds-        sortedConditions =-            map fst $-                take-                    (expressionPairs cfg)-                    ( filter-                        ( \(c, v) ->-                            ((> negate (calculateComplexity c)) . fst)-                                v-                        )-                        (sortBy (flip compare `on` snd) (map (\c -> (c, evalGain c)) validConds))-                    )-     in-        if null sortedConditions-            then Nothing-            else-                Just $-                    maximumBy-                        (compare `on` evalGain)-                        ( boolExprs-                            df-                            sortedConditions-                            sortedConditions-                            0-                            (boolExpansion (synthConfig cfg))-                        )--calculateGini ::-    forall a.-    (Columnable a) =>-    T.Text -> DataFrame -> Double+calculateGini :: forall a. (Columnable a) => T.Text -> DataFrame -> Double calculateGini target df =     let n = fromIntegral $ nRows df         counts = getCounts @a target df@@ -383,20 +705,14 @@         probs = map (\c -> (fromIntegral c + 1) / (n + numClasses)) (M.elems counts)      in if n == 0 then 0 else 1 - sum (map (^ 2) probs) -majorityValue ::-    forall a.-    (Columnable a) =>-    T.Text -> DataFrame -> a+majorityValue :: forall a. (Columnable a) => T.Text -> DataFrame -> a majorityValue target df =     let counts = getCounts @a target df      in if M.null counts             then error "Empty DataFrame in leaf"             else fst $ maximumBy (compare `on` snd) (M.toList counts) -getCounts ::-    forall a.-    (Columnable a) =>-    T.Text -> DataFrame -> M.Map a Int+getCounts :: forall a. (Columnable a) => T.Text -> DataFrame -> M.Map a Int getCounts target df =     case interpret @a df (Col target) of         Left e -> throw e@@ -404,3 +720,42 @@             case toVector @a col of                 Left e -> throw e                 Right vals -> foldl' (\acc x -> M.insertWith (+) x 1 acc) M.empty (V.toList vals)++percentile :: Int -> Expr Double -> DataFrame -> Double+percentile p expr df =+    case interpret @Double df expr of+        Left _ -> 0+        Right (TColumn col) ->+            case toVector @Double col of+                Left _ -> 0+                Right vals ->+                    let sorted = V.fromList $ sort $ V.toList vals+                        n = V.length sorted+                        idx = min (n - 1) $ max 0 $ (p * n) `div` 100+                     in if n == 0 then 0 else sorted V.! idx++buildTree ::+    forall a.+    (Columnable a) =>+    TreeConfig ->+    Int ->+    T.Text ->+    [Expr Bool] ->+    DataFrame ->+    Expr a+buildTree cfg depth target conds df =+    let+        tree = buildGreedyTree @a cfg depth target conds df+        indices = V.enumFromN 0 (nRows df)+        optimized = taoOptimize @a cfg target conds df indices tree+     in+        pruneExpr (treeToExpr optimized)++findBestSplit ::+    forall a.+    (Columnable a) =>+    TreeConfig -> T.Text -> [Expr Bool] -> DataFrame -> Maybe (Expr Bool)+findBestSplit = findBestGreedySplit @a++pruneTree :: forall a. (Columnable a, Eq a) => Expr a -> Expr a+pruneTree = pruneExpr
src/DataFrame/IO/CSV.hs view
@@ -167,6 +167,8 @@     -- ^ Character that separates column values.     , numColumns :: Maybe Int     -- ^ Number of columns to read.+    , missingIndicators :: [T.Text]+    -- ^ Values that should be read as `Nothing`.     }  shouldInferFromSample :: TypeSpec -> Bool@@ -190,6 +192,7 @@         , dateFormat = "%Y-%m-%d"         , columnSeparator = ','         , numColumns = Nothing+        , missingIndicators = []         }  {- | Read CSV file from path and load it into a dataframe.@@ -236,7 +239,8 @@ readSeparated :: ReadOptions -> FilePath -> IO DataFrame readSeparated !opts !path = do     let sep = columnSeparator opts-    csvData <- BL.readFile path+    let stripUtf8Bom bs = fromMaybe bs (BL.stripPrefix "\xEF\xBB\xBF" bs)+    csvData <- stripUtf8Bom <$> BL.readFile path     let decodeOpts = Csv.defaultDecodeOptions{Csv.decDelimiter = fromIntegral (ord sep)}     let stream = CsvStream.decodeWith decodeOpts Csv.NoHeader csvData @@ -263,7 +267,11 @@      (sampleRow, _) <- peekStream rowsToProcess     builderCols <- initializeColumns (V.toList sampleRow) opts-    processStream rowsToProcess builderCols (numColumns opts)+    processStream+        (missingIndicators opts)+        rowsToProcess+        builderCols+        (numColumns opts)      frozenCols <- V.fromList <$> mapM freezeBuilderColumn builderCols     let numRows = maybe 0 columnLength (frozenCols V.!? 0)@@ -278,6 +286,7 @@         if shouldInferFromSample (typeSpec opts)             then                 parseDefaults+                    (missingIndicators opts)                     (typeInferenceSampleSize (typeSpec opts))                     (safeRead opts)                     (dateFormat opts)@@ -305,17 +314,20 @@                     Nothing -> BuilderText <$> newPagedVector <*> pure validityRef  processStream ::+    [T.Text] ->     CsvStream.Records (V.Vector BL.ByteString) ->     [BuilderColumn] ->     Maybe Int ->     IO ()-processStream _ _ (Just 0) = return ()-processStream (Cons (Right row) rest) cols n = processRow row cols >> processStream rest cols (fmap (flip (-) 1) n)-processStream (Cons (Left err) _) _ _ = error ("CSV Parse Error: " ++ err)-processStream (Nil _ _) _ _ = return ()+processStream _ _ _ (Just 0) = return ()+processStream missing (Cons (Right row) rest) cols n =+    processRow missing row cols+        >> processStream missing rest cols (fmap (flip (-) 1) n)+processStream missing (Cons (Left err) _) _ _ = error ("CSV Parse Error: " ++ err)+processStream missing (Nil _ _) _ _ = return () -processRow :: V.Vector BL.ByteString -> [BuilderColumn] -> IO ()-processRow !vals !cols = V.zipWithM_ processValue vals (V.fromList cols)+processRow :: [T.Text] -> V.Vector BL.ByteString -> [BuilderColumn] -> IO ()+processRow missing !vals !cols = V.zipWithM_ processValue vals (V.fromList cols)   where     processValue !bs !col = do         let bs' = BL.toStrict bs@@ -328,7 +340,7 @@                 Nothing -> appendPagedUnboxedVector gv 0.0 >> appendPagedUnboxedVector valid 0             BuilderText gv valid -> do                 let !val = T.strip (TE.decodeUtf8Lenient bs')-                if isNullish val+                if isNullish val || val `elem` missing                     then appendPagedVector gv T.empty >> appendPagedUnboxedVector valid 0                     else appendPagedVector gv val >> appendPagedUnboxedVector valid 1 @@ -428,11 +440,9 @@                     ++ "the other columns at index "                     ++ show i     go k (OptionalColumn (c :: V.Vector (Maybe a))) acc = case c V.!? i of-        Just e -> textRep : acc-          where-            textRep = case testEquality (typeRep @a) (typeRep @T.Text) of-                Just Refl -> fromMaybe "Nothing" e-                Nothing -> (T.pack . show) e+        Just e -> case testEquality (typeRep @a) (typeRep @T.Text) of+            Just Refl -> fromMaybe T.empty e : acc+            Nothing -> maybe T.empty (T.pack . show) e : acc         Nothing ->             error $                 "Column "
src/DataFrame/IO/Parquet/Thrift.hs view
@@ -1,4 +1,7 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StrictData #-} {-# LANGUAGE TypeApplications #-} @@ -13,9 +16,20 @@ import qualified Data.Map as M import Data.Maybe import qualified Data.Text as T+import Data.Typeable (Typeable)+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU import Data.Word import DataFrame.IO.Parquet.Binary import DataFrame.IO.Parquet.Types+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.DataFrame (DataFrame, unsafeGetColumn)+import qualified DataFrame.Operations.Core as DI+import Type.Reflection (+    eqTypeRep,+    typeRep,+    (:~~:) (HRefl),+ )  data SchemaElement = SchemaElement     { elementName :: T.Text@@ -31,6 +45,39 @@     }     deriving (Show, Eq) +createParquetSchema :: DataFrame -> [SchemaElement]+createParquetSchema df = schemaDef : map toSchemaElement (DI.columnNames df)+  where+    -- The schema always contains an initial element+    -- indicating the group of fields.+    schemaDef =+        SchemaElement+            { elementName = "schema"+            , elementType = STOP+            , typeLength = 0+            , numChildren = fromIntegral (snd (DI.dimensions df))+            , fieldId = -1+            , repetitionType = UNKNOWN_REPETITION_TYPE+            , convertedType = 0+            , scale = 0+            , precision = 0+            , logicalType = LOGICAL_TYPE_UNKNOWN+            }+    toSchemaElement colName =+        let+            colType :: TType+            colType = case unsafeGetColumn colName df of+                (DI.BoxedColumn (col :: V.Vector a)) -> haskellToTType @a+                (DI.UnboxedColumn (col :: VU.Vector a)) -> haskellToTType @a+                (DI.OptionalColumn (col :: V.Vector (Maybe a))) -> haskellToTType @a+            lType =+                if DI.hasElemType @T.Text (unsafeGetColumn colName df)+                    || DI.hasElemType @(Maybe T.Text) (unsafeGetColumn colName df)+                    then STRING_TYPE+                    else LOGICAL_TYPE_UNKNOWN+         in+            SchemaElement colName colType 0 0 (-1) OPTIONAL 0 0 0 lType+ data KeyValue = KeyValue     { key :: String     , value :: String@@ -67,6 +114,29 @@     | STRUCT     | UUID     deriving (Show, Eq)++haskellToTType :: forall a. (Typeable a) => TType+haskellToTType+    | is @Bool = BOOL+    | is @Int8 = BYTE+    | is @Word8 = BYTE+    | is @Int16 = I16+    | is @Word16 = I16+    | is @Int32 = I32+    | is @Word32 = I32+    | is @Int64 = I64+    | is @Word64 = I64+    | is @Float = FLOAT+    | is @Double = DOUBLE+    | is @String = STRING+    | is @T.Text = STRING+    | is @BS.ByteString = STRING+    | otherwise = STOP+  where+    is :: forall x. (Typeable x) => Bool+    is = case eqTypeRep (typeRep @a) (typeRep @x) of+        Just HRefl -> True+        Nothing -> False  defaultMetadata :: FileMetadata defaultMetadata =
src/DataFrame/IO/Unstable/CSV.hs view
@@ -124,6 +124,7 @@                         then typeInferenceSampleSize (typeSpec opts)                         else 0              in parseFromExamples+                    (missingIndicators opts)                     n                     (safeRead opts)                     (dateFormat opts)
src/DataFrame/Internal/Column.hs view
@@ -28,6 +28,7 @@  import Control.Exception (throw) import Control.Monad.ST (runST)+import Data.Kind (Type) import Data.Maybe import Data.Type.Equality (TestEquality (..)) import DataFrame.Errors@@ -139,20 +140,18 @@             Just Refl -> a == b     (==) _ _ = False +-- Generalised LEQ that does reflection.+generalLEQ ::+    forall a b. (Typeable a, Typeable b, Ord a, Ord b) => a -> b -> Bool+generalLEQ x y = case testEquality (typeRep @a) (typeRep @b) of+    Nothing -> False+    Just Refl -> x <= y+ instance Ord Column where     (<=) :: Column -> Column -> Bool-    (<=) (BoxedColumn (a :: VB.Vector t1)) (BoxedColumn (b :: VB.Vector t2)) =-        case testEquality (typeRep @t1) (typeRep @t2) of-            Nothing -> False-            Just Refl -> a <= b-    (<=) (OptionalColumn (a :: VB.Vector t1)) (OptionalColumn (b :: VB.Vector t2)) =-        case testEquality (typeRep @t1) (typeRep @t2) of-            Nothing -> False-            Just Refl -> a <= b-    (<=) (UnboxedColumn (a :: VU.Vector t1)) (UnboxedColumn (b :: VU.Vector t2)) =-        case testEquality (typeRep @t1) (typeRep @t2) of-            Nothing -> False-            Just Refl -> a <= b+    (<=) (BoxedColumn (a :: VB.Vector t1)) (BoxedColumn (b :: VB.Vector t2)) = generalLEQ a b+    (<=) (OptionalColumn (a :: VB.Vector t1)) (OptionalColumn (b :: VB.Vector t2)) = generalLEQ a b+    (<=) (UnboxedColumn (a :: VU.Vector t1)) (UnboxedColumn (b :: VU.Vector t2)) = generalLEQ a b     (<=) _ _ = False  {- | A class for converting a vector to a column of the appropriate type.@@ -239,63 +238,71 @@     [a] -> Column fromList = toColumnRep @(KindOf a) . VB.fromList +throwTypeMismatch ::+    forall (a :: Type) (b :: Type).+    (Typeable a, Typeable b) => Either DataFrameException Column+throwTypeMismatch =+    Left $+        TypeMismatchException+            MkTypeErrorContext+                { userType = Right (typeRep @b)+                , expectedType = Right (typeRep @a)+                , callingFunctionName = Just "mapColumn"+                , errorColumnName = Nothing+                }+ -- | An internal function to map a function over the values of a column. mapColumn ::     forall b c.-    ( Columnable b-    , Columnable c-    , UnboxIf c-    ) =>-    (b -> c) ->-    Column ->-    Either DataFrameException Column+    (Columnable b, Columnable c) =>+    (b -> c) -> Column -> Either DataFrameException Column mapColumn f = \case-    BoxedColumn (col :: VB.Vector a)-        | Just Refl <- testEquality (typeRep @a) (typeRep @b) ->-            Right (fromVector @c (VB.map f col))-        | otherwise ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @b)-                        , expectedType = Right (typeRep @a)-                        , callingFunctionName = Just "mapColumn"-                        , errorColumnName = Nothing-                        }-                    )-    OptionalColumn (col :: VB.Vector a)-        | Just Refl <- testEquality (typeRep @a) (typeRep @b) ->-            Right (fromVector @c (VB.map f col))-        | otherwise ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @b)-                        , expectedType = Right (typeRep @a)-                        , callingFunctionName = Just "mapColumn"-                        , errorColumnName = Nothing-                        }-                    )-    UnboxedColumn (col :: VU.Vector a)-        | Just Refl <- testEquality (typeRep @a) (typeRep @b) ->-            Right $ case sUnbox @c of-                STrue -> UnboxedColumn (VU.map f col)-                SFalse -> fromVector @c (VB.generate (VU.length col) (f . VU.unsafeIndex col))-        | otherwise ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @b)-                        , expectedType = Right (typeRep @a)-                        , callingFunctionName = Just "mapColumn"-                        , errorColumnName = Nothing-                        }-                    )+    BoxedColumn (col :: VB.Vector a) -> run col+    OptionalColumn (col :: VB.Vector a) -> run col+    UnboxedColumn (col :: VU.Vector a) -> runUnboxed col+  where+    run :: forall a. (Typeable a) => VB.Vector a -> Either DataFrameException Column+    run col = case testEquality (typeRep @a) (typeRep @b) of+        Just Refl -> Right (fromVector @c (VB.map f col))+        Nothing -> throwTypeMismatch @a @b++    runUnboxed ::+        forall a.+        (Typeable a, VU.Unbox a) => VU.Vector a -> Either DataFrameException Column+    runUnboxed col = case testEquality (typeRep @a) (typeRep @b) of+        Just Refl -> Right $ case sUnbox @c of+            STrue -> UnboxedColumn (VU.map f col)+            SFalse -> fromVector @c (VB.generate (VU.length col) (f . VU.unsafeIndex col))+        Nothing -> throwTypeMismatch @a @b {-# SPECIALIZE mapColumn ::     (Double -> Double) -> Column -> Either DataFrameException Column     #-} {-# INLINEABLE mapColumn #-} +-- | Applies a function that returns an unboxed result to an unboxed vector, storing the result in a column.+imapColumn ::+    forall b c.+    (Columnable b, Columnable c) =>+    (Int -> b -> c) -> Column -> Either DataFrameException Column+imapColumn f = \case+    BoxedColumn (col :: VB.Vector a) -> run col+    OptionalColumn (col :: VB.Vector a) -> run col+    UnboxedColumn (col :: VU.Vector a) -> runUnboxed col+  where+    run :: forall a. (Typeable a) => VB.Vector a -> Either DataFrameException Column+    run col = case testEquality (typeRep @a) (typeRep @b) of+        Just Refl -> Right (fromVector @c (VB.imap f col))+        Nothing -> throwTypeMismatch @a @b++    runUnboxed ::+        forall a.+        (Typeable a, VU.Unbox a) => VU.Vector a -> Either DataFrameException Column+    runUnboxed col = case testEquality (typeRep @a) (typeRep @b) of+        Just Refl -> Right $ case sUnbox @c of+            STrue -> UnboxedColumn (VU.imap f col)+            SFalse -> BoxedColumn (VB.imap f (VG.convert col))+        Nothing -> throwTypeMismatch @a @b+ -- | O(1) Gets the number of elements in the column. columnLength :: Column -> Int columnLength (BoxedColumn xs) = VG.length xs@@ -373,158 +380,46 @@     (a -> Bool) ->     Column ->     Either DataFrameException (VU.Vector Int)-findIndices pred (BoxedColumn (column :: VB.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of-    Just Refl -> pure $ VG.convert (VG.findIndices pred column)-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @b)-                    , callingFunctionName = Just "findIndices"-                    , errorColumnName = Nothing-                    }-                )-findIndices pred (UnboxedColumn (column :: VU.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of-    Just Refl -> pure $ VG.findIndices pred column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @b)-                    , callingFunctionName = Just "findIndices"-                    , errorColumnName = Nothing-                    }-                )-findIndices pred (OptionalColumn (column :: VB.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of-    Just Refl -> pure $ VG.convert (VG.findIndices pred column)-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @b)-                    , callingFunctionName = Just "findIndices"-                    , errorColumnName = Nothing-                    }-                )---- | An internal function that returns a vector of how indexes change after a column is sorted.-sortedIndexes :: Bool -> Column -> VU.Vector Int-sortedIndexes asc (BoxedColumn column) = runST $ do-    withIndexes <- VG.thaw $ VG.indexed column-    VA.sortBy-        (\(a, b) (a', b') -> (if asc then compare else flip compare) b b')-        withIndexes-    sorted <- VG.unsafeFreeze withIndexes-    return $ VU.generate (VG.length column) (\i -> fst (sorted VG.! i))-sortedIndexes asc (UnboxedColumn column) = runST $ do-    withIndexes <- VG.thaw $ VG.indexed column-    VA.sortBy-        (\(a, b) (a', b') -> (if asc then compare else flip compare) b b')-        withIndexes-    sorted <- VG.unsafeFreeze withIndexes-    return $ VU.generate (VG.length column) (\i -> fst (sorted VG.! i))-sortedIndexes asc (OptionalColumn column) = runST $ do-    withIndexes <- VG.thaw $ VG.indexed column-    VA.sortBy-        (\(a, b) (a', b') -> (if asc then compare else flip compare) b b')-        withIndexes-    sorted <- VG.unsafeFreeze withIndexes-    return $ VU.generate (VG.length column) (\i -> fst (sorted VG.! i))-{-# INLINE sortedIndexes #-}---- | Applies a function that returns an unboxed result to an unboxed vector, storing the result in a column.-imapColumn ::-    forall b c.-    (Columnable b, Columnable c) =>-    (Int -> b -> c) -> Column -> Either DataFrameException Column-imapColumn f = \case-    BoxedColumn (col :: VB.Vector a)-        | Just Refl <- testEquality (typeRep @a) (typeRep @b) ->-            pure (fromVector @c (VB.imap f col))-        | otherwise ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @b)-                        , expectedType = Right (typeRep @a)-                        , callingFunctionName = Just "imapColumn"-                        , errorColumnName = Nothing-                        }-                    )-    UnboxedColumn (col :: VU.Vector a)-        | Just Refl <- testEquality (typeRep @a) (typeRep @b) ->-            pure $-                case sUnbox @c of-                    STrue -> UnboxedColumn (VU.imap f col)-                    SFalse -> fromVector @c (VB.imap f (VB.convert col))-        | otherwise ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @b)-                        , expectedType = Right (typeRep @a)-                        , callingFunctionName = Just "imapColumn"-                        , errorColumnName = Nothing-                        }-                    )-    OptionalColumn (col :: VB.Vector a)-        | Just Refl <- testEquality (typeRep @a) (typeRep @b) ->-            pure (fromVector @c (VB.imap f col))-        | otherwise ->+findIndices pred = \case+    BoxedColumn (v :: VB.Vector b) -> run v VG.convert+    OptionalColumn (v :: VB.Vector b) -> run v VG.convert+    UnboxedColumn (v :: VU.Vector b) -> run v id+  where+    run ::+        forall b v.+        (Typeable b, VG.Vector v b, VG.Vector v Int) =>+        v b ->+        (v Int -> VU.Vector Int) ->+        Either DataFrameException (VU.Vector Int)+    run column finalize = case testEquality (typeRep @a) (typeRep @b) of+        Just Refl -> Right . finalize $ VG.findIndices pred column+        Nothing ->             Left $                 TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @b)-                        , expectedType = Right (typeRep @a)-                        , callingFunctionName = Just "imapColumn"+                    MkTypeErrorContext+                        { userType = Right (typeRep @a)+                        , expectedType = Right (typeRep @b)+                        , callingFunctionName = Just "findIndices"                         , errorColumnName = Nothing                         }-                    ) --- | Filter column with index.-ifilterColumn ::-    forall a.-    (Columnable a) =>-    (Int -> a -> Bool) -> Column -> Either DataFrameException Column-ifilterColumn f c@(BoxedColumn (column :: VB.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of-    Just Refl -> pure $ BoxedColumn $ VG.ifilter f column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @b)-                    , callingFunctionName = Just "ifilterColumn"-                    , errorColumnName = Nothing-                    }-                )-ifilterColumn f c@(UnboxedColumn (column :: VU.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of-    Just Refl -> pure $ UnboxedColumn $ VG.ifilter f column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @b)-                    , callingFunctionName = Just "ifilterColumn"-                    , errorColumnName = Nothing-                    }-                )-ifilterColumn f c@(OptionalColumn (column :: VB.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of-    Just Refl -> pure $ OptionalColumn $ VG.ifilter f column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @b)-                    , callingFunctionName = Just "ifilterColumn"-                    , errorColumnName = Nothing-                    }-                )+-- | An internal function that returns a vector of how indexes change after a column is sorted.+sortedIndexes :: Bool -> Column -> VU.Vector Int+sortedIndexes asc = \case+    BoxedColumn column -> sortWorker column+    UnboxedColumn column -> sortWorker column+    OptionalColumn column -> sortWorker column+  where+    sortWorker ::+        (VG.Vector v a, Ord a, VG.Vector v (Int, a), VG.Vector v Int) =>+        v a -> VU.Vector Int+    sortWorker column = runST $ do+        withIndexes <- VG.thaw $ VG.indexed column+        let cmp = if asc then compare else flip compare+        VA.sortBy (\(_, b) (_, b') -> cmp b b') withIndexes+        sorted <- VG.unsafeFreeze withIndexes+        return $ VG.convert $ VG.map fst sorted+{-# INLINE sortedIndexes #-}  -- | Fold (right) column with index. ifoldrColumn ::@@ -568,48 +463,6 @@                     }                 ) --- | Fold (left) column with index.-ifoldlColumn ::-    forall a b.-    (Columnable a, Columnable b) =>-    (b -> Int -> a -> b) -> b -> Column -> Either DataFrameException b-ifoldlColumn f acc c@(BoxedColumn (column :: VB.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of-    Just Refl -> pure $ VG.ifoldl' f acc column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @d)-                    , callingFunctionName = Just "ifoldlColumn"-                    , errorColumnName = Nothing-                    }-                )-ifoldlColumn f acc c@(OptionalColumn (column :: VB.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of-    Just Refl -> pure $ VG.ifoldl' f acc column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @d)-                    , callingFunctionName = Just "ifoldlColumn"-                    , errorColumnName = Nothing-                    }-                )-ifoldlColumn f acc c@(UnboxedColumn (column :: VU.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of-    Just Refl -> pure $ VG.ifoldl' f acc column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @d)-                    , callingFunctionName = Just "ifoldlColumn"-                    , errorColumnName = Nothing-                    }-                )- foldlColumn ::     forall a b.     (Columnable a, Columnable b) =>@@ -738,55 +591,6 @@                     , errorColumnName = Nothing                     }                 )---- | Generic reduce function for all Column types.-reduceColumn ::-    forall a b.-    (Columnable a, Columnable b) =>-    (a -> b) -> Column -> Either DataFrameException b-{-# SPECIALIZE reduceColumn ::-    (VU.Vector (Double, Double) -> Double) ->-    Column ->-    Either DataFrameException Double-    , (VU.Vector Double -> Double) -> Column -> Either DataFrameException Double-    #-}-reduceColumn f (BoxedColumn (column :: c)) = case testEquality (typeRep @c) (typeRep @a) of-    Just Refl -> pure $ f column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @b)-                    , callingFunctionName = Just "reduceColumn"-                    , errorColumnName = Nothing-                    }-                )-reduceColumn f (UnboxedColumn (column :: c)) = case testEquality (typeRep @c) (typeRep @a) of-    Just Refl -> pure $ f column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @b)-                    , callingFunctionName = Just "reduceColumn"-                    , errorColumnName = Nothing-                    }-                )-reduceColumn f (OptionalColumn (column :: c)) = case testEquality (typeRep @c) (typeRep @a) of-    Just Refl -> pure $ f column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @b)-                    , callingFunctionName = Just "reduceColumn"-                    , errorColumnName = Nothing-                    }-                )-{-# INLINE reduceColumn #-}  -- | An internal, column version of zip. zipColumns :: Column -> Column -> Column
src/DataFrame/Lazy/IO/CSV.hs view
@@ -383,11 +383,9 @@                     ++ "the other columns at index "                     ++ show i     go k (OptionalColumn (c :: V.Vector (Maybe a))) acc = case c V.!? i of-        Just e -> textRep : acc-          where-            textRep = case testEquality (typeRep @a) (typeRep @T.Text) of-                Just Refl -> fromMaybe "Nothing" e-                Nothing -> (T.pack . show) e+        Just e -> case testEquality (typeRep @a) (typeRep @T.Text) of+            Just Refl -> fromMaybe T.empty e : acc+            Nothing -> maybe T.empty (T.pack . show) e : acc         Nothing ->             error $                 "Column "
src/DataFrame/Monad.hs view
@@ -14,6 +14,7 @@ import DataFrame.Internal.Expression (Expr (..))  import qualified Data.Text as T+import System.Random  -- A re-implementation of the state monad. -- `mtl` might be too heavy a dependency just to get@@ -60,6 +61,12 @@  filterWhereM :: Expr Bool -> FrameM () filterWhereM p = modifyM (D.filterWhere p)++sampleM :: (RandomGen g) => g -> Double -> FrameM ()+sampleM pureGen p = modifyM (D.sample pureGen p)++takeM :: Int -> FrameM ()+takeM n = modifyM (D.take n)  filterJustM :: (Columnable a) => Expr (Maybe a) -> FrameM (Expr a) filterJustM (Col name) = FrameM $ \df ->
src/DataFrame/Operations/Core.hs view
@@ -864,7 +864,8 @@ This may occur if the column contains non-numeric data or values outside the 'Int' range. -} columnAsIntVector ::-    Expr Int -> DataFrame -> Either DataFrameException (VU.Vector Int)+    (Columnable a, Num a) =>+    Expr a -> DataFrame -> Either DataFrameException (VU.Vector Int) columnAsIntVector (Col name) df = case getColumn name df of     Just col -> toIntVector col     Nothing ->@@ -880,7 +881,8 @@ This may occur if the column contains non-numeric data. -} columnAsDoubleVector ::-    Expr Double -> DataFrame -> Either DataFrameException (VU.Vector Double)+    (Columnable a, Num a) =>+    Expr a -> DataFrame -> Either DataFrameException (VU.Vector Double) columnAsDoubleVector (Col name) df = case getColumn name df of     Just col -> toDoubleVector col     Nothing ->@@ -896,7 +898,8 @@ This may occur if the column contains non-numeric data. -} columnAsFloatVector ::-    Expr Float -> DataFrame -> Either DataFrameException (VU.Vector Float)+    (Columnable a, Num a) =>+    Expr a -> DataFrame -> Either DataFrameException (VU.Vector Float) columnAsFloatVector (Col name) df = case getColumn name df of     Just col -> toFloatVector col     Nothing ->
src/DataFrame/Operations/Subset.hs view
@@ -18,11 +18,23 @@  import Control.Exception (throw) import Data.Function ((&))-import Data.Maybe (fromJust, fromMaybe, isJust, isNothing)+import Data.Maybe (+    fromJust,+    fromMaybe,+    isJust,+    isNothing,+ ) import Data.Type.Equality (TestEquality (..))-import DataFrame.Errors (DataFrameException (..), TypeErrorContext (..))+import DataFrame.Errors (+    DataFrameException (..),+    TypeErrorContext (..),+ ) import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (DataFrame (..), empty, getColumn)+import DataFrame.Internal.DataFrame (+    DataFrame (..),+    empty,+    getColumn,+ ) import DataFrame.Internal.Expression import DataFrame.Internal.Interpreter import DataFrame.Operations.Core
src/DataFrame/Operations/Typing.hs view
@@ -22,28 +22,35 @@  type DateFormat = String -parseDefaults :: Int -> Bool -> DateFormat -> DataFrame -> DataFrame-parseDefaults n safeRead dateFormat df = df{columns = V.map (parseDefault n safeRead dateFormat) (columns df)}+parseDefaults :: [T.Text] -> Int -> Bool -> DateFormat -> DataFrame -> DataFrame+parseDefaults missing n safeRead dateFormat df = df{columns = V.map (parseDefault missing n safeRead dateFormat) (columns df)} -parseDefault :: Int -> Bool -> DateFormat -> Column -> Column-parseDefault n safeRead dateFormat (BoxedColumn (c :: V.Vector a)) =+parseDefault :: [T.Text] -> Int -> Bool -> DateFormat -> Column -> Column+parseDefault missing n safeRead dateFormat (BoxedColumn (c :: V.Vector a)) =     case (typeRep @a) `testEquality` (typeRep @T.Text) of         Nothing -> case (typeRep @a) `testEquality` (typeRep @String) of-            Just Refl -> parseFromExamples n safeRead dateFormat (V.map T.pack c)+            Just Refl -> parseFromExamples missing n safeRead dateFormat (V.map T.pack c)             Nothing -> BoxedColumn c-        Just Refl -> parseFromExamples n safeRead dateFormat c-parseDefault n safeRead dateFormat (OptionalColumn (c :: V.Vector (Maybe a))) =+        Just Refl -> parseFromExamples missing n safeRead dateFormat c+parseDefault missing n safeRead dateFormat (OptionalColumn (c :: V.Vector (Maybe a))) =     case (typeRep @a) `testEquality` (typeRep @T.Text) of         Nothing -> case (typeRep @a) `testEquality` (typeRep @String) of-            Just Refl -> parseFromExamples n safeRead dateFormat (V.map (T.pack . fromMaybe "") c)+            Just Refl ->+                parseFromExamples+                    missing+                    n+                    safeRead+                    dateFormat+                    (V.map (T.pack . fromMaybe "") c)             Nothing -> BoxedColumn c-        Just Refl -> parseFromExamples n safeRead dateFormat (V.map (fromMaybe "") c)-parseDefault _ _ _ column = column+        Just Refl -> parseFromExamples missing n safeRead dateFormat (V.map (fromMaybe "") c)+parseDefault _ _ _ _ column = column -parseFromExamples :: Int -> Bool -> DateFormat -> V.Vector T.Text -> Column-parseFromExamples n safeRead dateFormat cols =+parseFromExamples ::+    [T.Text] -> Int -> Bool -> DateFormat -> V.Vector T.Text -> Column+parseFromExamples missing n safeRead dateFormat cols =     let-        converter = if safeRead then convertNullish else convertOnlyEmpty+        converter = if safeRead then convertNullish missing else convertOnlyEmpty         examples = V.map converter (V.take n cols)         asMaybeText = V.map converter cols      in@@ -123,8 +130,8 @@     parsableAsDouble = vecSameConstructor asMaybeText asMaybeDouble     parsableAsDate = vecSameConstructor asMaybeText asMaybeDate -convertNullish :: T.Text -> Maybe T.Text-convertNullish v = if isNullish v then Nothing else Just v+convertNullish :: [T.Text] -> T.Text -> Maybe T.Text+convertNullish missing v = if isNullish v || v `elem` missing then Nothing else Just v  convertOnlyEmpty :: T.Text -> Maybe T.Text convertOnlyEmpty v = if v == "" then Nothing else Just v
src/DataFrame/Synthesis.hs view
@@ -266,7 +266,9 @@ percentiles df =     let         doubleColumns =-            map (either throw id . ((`columnAsDoubleVector` df) . Col)) (D.columnNames df)+            map+                (either throw id . ((`columnAsDoubleVector` df) . Col @Double))+                (D.columnNames df)      in         concatMap             (\c -> map (Lit . roundTo2SigDigits . (`percentile'` c)) [1, 25, 75, 99])
+ tests/GenDataFrame.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE GADTs #-}+{-# LANGUAGE TypeApplications #-}++module GenDataFrame where++import qualified Data.Map as M+import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import DataFrame.Internal.Column+import DataFrame.Internal.DataFrame+import Test.QuickCheck++genColumn :: Int -> Gen Column+genColumn len =+    oneof+        [ BoxedColumn . V.fromList <$> vectorOf len (arbitrary @Int)+        , UnboxedColumn . VU.fromList <$> vectorOf len (arbitrary @Double)+        , OptionalColumn . V.fromList <$> vectorOf len (arbitrary @(Maybe Int))+        ]++genDataFrame :: Gen DataFrame+genDataFrame = do+    numRows <- choose (0, 100)+    numCols <- choose (0, 10)+    colNames <- V.fromList <$> vectorOf numCols genUniqueColName+    cols <- V.fromList <$> vectorOf numCols (genColumn numRows)+    let indices = M.fromList $ zip (V.toList colNames) [0 ..]+    pure $+        DataFrame+            { columns = cols+            , columnIndices = indices+            , dataframeDimensions = (numRows, numCols)+            , derivingExpressions = M.empty+            }++genUniqueColName :: Gen T.Text+genUniqueColName = T.pack <$> listOf1 (elements ['a' .. 'z'])++instance Arbitrary DataFrame where+    arbitrary = genDataFrame+    shrink df = []
tests/Main.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-}  module Main where@@ -12,7 +13,9 @@ import qualified System.Exit as Exit  import Data.Time+import GenDataFrame () import Test.HUnit+import Test.QuickCheck  import qualified Functions import qualified Operations.Aggregations@@ -27,6 +30,7 @@ import qualified Operations.ReadCsv import qualified Operations.Sort import qualified Operations.Statistics+import qualified Operations.Subset import qualified Operations.Take import qualified Parquet @@ -59,7 +63,7 @@         beforeParse :: [T.Text]         beforeParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Bools without missing values as UnboxedColumn of Bools"@@ -74,7 +78,7 @@         beforeParse :: [T.Text]         beforeParse = T.pack . show <$> [1 .. 50]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints without missing values as UnboxedColumn of Ints"@@ -91,7 +95,7 @@             T.pack . show                 <$> [1.0 .. 50.0] ++ [3.14, 2.22, 8.55, 23.3, 12.22222235049450945049504950]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Doubles without missing values as UnboxedColumn of Doubles"@@ -138,7 +142,7 @@             , "2020-02-26"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Dates without missing values as BoxedColumn of Days"@@ -223,7 +227,7 @@             , "Meowth, that's right!"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Text without missing values as BoxedColumn of Text"@@ -239,7 +243,7 @@         beforeParse :: [T.Text]         beforeParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"] ++ ["1", "0", "1"]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses mixture of Bools and Ints as Text"@@ -466,7 +470,7 @@             , "12.22222235049451"             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles"@@ -561,7 +565,7 @@             , "2020-02-20"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints and Dates as BoxedColumn of Texts"@@ -700,7 +704,7 @@             , "12.22222235049451"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Texts and Doubles as BoxedColumn of Texts"@@ -753,7 +757,7 @@             , "Meowth"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Dates and Texts as BoxedColumn of Texts"@@ -770,7 +774,8 @@         beforeParse :: [T.Text]         beforeParse = replicate 10 "true" ++ replicate 10 "false"         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Bools without missing values as UnboxedColumn of Bools, when safeRead is off"@@ -887,7 +892,8 @@             , "50"             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints without missing values as UnboxedColumn of Ints, when safeRead is off"@@ -1014,7 +1020,8 @@             , "12.22222235049451"             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Doubles without missing values as UnboxedColumn of Doubles, when safeRead is off"@@ -1061,7 +1068,8 @@             , "2020-02-26"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Dates without missing values as BoxedColumn of Days"@@ -1146,7 +1154,8 @@             , "Meowth, that's right!"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Text without missing values as BoxedColumn of Text"@@ -1161,7 +1170,8 @@         beforeParse :: [T.Text]         beforeParse = replicate 10 "" ++ replicate 10 "true" ++ replicate 10 "false"         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Bools and empty Strings as OptionalColumn of Bools, when safeRead is off"@@ -1288,7 +1298,8 @@             , "50"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is off"@@ -1425,7 +1436,8 @@             , "12.22222235049451"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"@@ -1482,7 +1494,8 @@             , ""             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead off"@@ -1581,7 +1594,8 @@             , "Meowth, that's right!"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"@@ -1596,7 +1610,8 @@         beforeParse :: [T.Text]         beforeParse = replicate 10 "N/A" ++ replicate 10 "True" ++ replicate 10 "False"         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Bools with nullish values as BoxedColumn of Texts, when safeRead is off"@@ -1723,7 +1738,8 @@             , "50"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints with nullish values as BoxedColumn of Texts, when safeRead is off"@@ -1860,7 +1876,8 @@             , "12.22222235049451"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"@@ -1999,7 +2016,8 @@             , ""             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints with nullish values AND empty strings as OptionalColumn of Texts, when safeRead is off"@@ -2104,7 +2122,8 @@             , "N/A"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"@@ -2120,7 +2139,7 @@         beforeParse :: [T.Text]         beforeParse = replicate 10 "" ++ replicate 10 "true"         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Bools and empty strings as OptionalColumn of Bools, when safeRead is on"@@ -2247,7 +2266,7 @@             , "50"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is on"@@ -2384,7 +2403,7 @@             , "12.22222235049451"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is on"@@ -2441,7 +2460,7 @@             , ""             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead on"@@ -2540,7 +2559,7 @@             , "Meowth, that's right!"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead on"@@ -2667,7 +2686,7 @@             , "50"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints with nullish values as OptionalColumn of Ints, when safeRead is on"@@ -2804,7 +2823,7 @@             , "12.03"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"@@ -2943,7 +2962,7 @@             , ""             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints with nullish values AND empty strings as OptionalColumn of Ints, when safeRead is on"@@ -3040,7 +3059,7 @@             , "3.14"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints and Doubles with nullish values AND empty strings as OptionalColumn of Doubles, when safeRead is on"@@ -3145,7 +3164,7 @@             , "N/A"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"@@ -3161,7 +3180,7 @@         beforeParse :: [T.Text]         beforeParse = "false" : replicate 50 "true"         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Bools without missing values as UnboxedColumn of Ints with only one example"@@ -3176,7 +3195,7 @@         beforeParse :: [T.Text]         beforeParse = "false" : replicate 50 "true"         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault 49 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 49 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Bools without missing values as UnboxedColumn of Ints with only one example"@@ -3293,7 +3312,7 @@             , "50"             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints without missing values as UnboxedColumn of Ints with only one example"@@ -3410,7 +3429,7 @@             , "50"             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault 25 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 25 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints without missing values as UnboxedColumn of Ints with some examples"@@ -3527,7 +3546,7 @@             , "50"             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault 49 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 49 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints without missing values as UnboxedColumn of Ints with many examples"@@ -3574,7 +3593,7 @@             , "2020-02-26"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Dates without missing values as BoxedColumn of Days with only one example"@@ -3621,7 +3640,7 @@             , "2020-02-26"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault 15 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 15 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Dates without missing values as BoxedColumn of Days with many examples"@@ -3848,7 +3867,7 @@             , "12.22222235049451"             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"@@ -4075,7 +4094,7 @@             , "12.22222235049451"             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault 50 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 50 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"@@ -4322,7 +4341,7 @@             , "12.22222235049451"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 1 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 1 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"@@ -4570,7 +4589,8 @@             , "12.22222235049451"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 30 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 30 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"@@ -4738,7 +4758,7 @@             , "12.22222235049451"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 1 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 1 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints, Doubles, empty strings, nullish as OptionalColumn of Text with just one example, when safeRead is off"@@ -4906,7 +4926,8 @@             , "12.22222235049451"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 30 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault [] 30 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints, Doubles, empty strings, nullish as OptionalColumn of Text with many examples, when safeRead is off"@@ -4923,7 +4944,7 @@         beforeParse :: [T.Text]         beforeParse = replicate 100 "NaN" ++ ["100000"]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             (assertEqual "Correctly parses many Nulls followed by one Int" expected actual) @@ -4934,7 +4955,7 @@         beforeParse :: [T.Text]         beforeParse = replicate 100 "NaN" ++ ["3.14"]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             (assertEqual "Correctly parses many Nulls followed by one Int" expected actual) @@ -4945,7 +4966,7 @@         beforeParse :: [T.Text]         beforeParse = replicate 100 "NaN" ++ ["2024-12-25"]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             (assertEqual "Correctly parses many Nulls followed by one Int" expected actual) @@ -4956,7 +4977,7 @@         beforeParse :: [T.Text]         beforeParse = replicate 100 "NaN" ++ ["2024-12-25", "2024-12-w6"]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             (assertEqual "Correctly parses many Nulls followed by one Int" expected actual) @@ -4967,7 +4988,7 @@         beforeParse :: [T.Text]         beforeParse = replicate 100 "NaN"         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse      in TestCase             (assertEqual "Correctly parses many Nulls followed by one Int" expected actual) @@ -5105,9 +5126,18 @@             ++ Parquet.tests             ++ parseTests +isSuccessful :: Result -> Bool+isSuccessful (Success{..}) = True+isSuccessful _ = False+ main :: IO () main = do     result <- runTestTT tests-    if failures result > 0 || errors result > 0+    -- Property tests+    propRes <-+        mapM+            (quickCheckWithResult stdArgs)+            Operations.Subset.tests+    if failures result > 0 || errors result > 0 || not (all isSuccessful propRes)         then Exit.exitFailure         else Exit.exitSuccess
+ tests/Operations/Subset.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE GADTs #-}++module Operations.Subset where++import DataFrame.Internal.DataFrame+import qualified DataFrame.Operations.Core as D+import qualified DataFrame.Operations.Subset as D+import System.Random++prop_dropZero :: DataFrame -> Bool+prop_dropZero df = D.drop 0 df == df++prop_takeZero :: DataFrame -> Bool+prop_takeZero df = fst (dataframeDimensions (D.take 0 df)) == 0++prop_takeAll :: DataFrame -> Bool+prop_takeAll df =+    let n = fst (dataframeDimensions df)+     in D.take n df == df++prop_dropAll :: DataFrame -> Bool+prop_dropAll df =+    let n = fst (dataframeDimensions df)+     in fst (dataframeDimensions (D.drop n df)) == 0++prop_takeLastZero :: DataFrame -> Bool+prop_takeLastZero df = fst (dataframeDimensions (D.takeLast 0 df)) == 0++prop_dropLastZero :: DataFrame -> Bool+prop_dropLastZero df = D.dropLast 0 df == df++prop_takeLastAll :: DataFrame -> Bool+prop_takeLastAll df =+    let n = fst (dataframeDimensions df)+     in D.takeLast n df == df++prop_dropLastAll :: DataFrame -> Bool+prop_dropLastAll df =+    let n = fst (dataframeDimensions df)+     in fst (dataframeDimensions (D.dropLast n df)) == 0++prop_rangeEmpty :: DataFrame -> Bool+prop_rangeEmpty df =+    fst (dataframeDimensions (D.range (5, 5) df)) == 0++prop_rangeFull :: DataFrame -> Bool+prop_rangeFull df =+    let rows = fst (dataframeDimensions df)+     in D.range (0, rows) df == df++prop_selectAll :: DataFrame -> Bool+prop_selectAll df = D.select (D.columnNames df) df == df++prop_selectEmpty :: DataFrame -> Bool+prop_selectEmpty df =+    let result = D.select [] df+     in dataframeDimensions result == (0, 0)++prop_excludeEmpty :: DataFrame -> Bool+prop_excludeEmpty df = D.exclude [] df == df++prop_excludeAll :: DataFrame -> Bool+prop_excludeAll df =+    let result = D.exclude (D.columnNames df) df+     in snd (dataframeDimensions result) == 0++prop_cubePreservesSmall :: DataFrame -> Bool+prop_cubePreservesSmall df =+    let (rows, cols) = dataframeDimensions df+     in D.cube (rows + 100, cols + 100) df == df++prop_sampleEmptyApprox :: DataFrame -> Bool+prop_sampleEmptyApprox df =+    let gen = mkStdGen 42+        sampled = D.sample gen 0.0 df+     in fst (dataframeDimensions sampled) == 0++tests =+    [ prop_dropZero+    , prop_takeZero+    , prop_takeAll+    , prop_dropAll+    , prop_takeLastZero+    , prop_dropLastZero+    , prop_takeLastAll+    , prop_dropLastAll+    , prop_rangeEmpty+    , prop_rangeFull+    , prop_selectAll+    , prop_selectEmpty+    , prop_excludeEmpty+    , prop_excludeAll+    , prop_cubePreservesSmall+    , prop_sampleEmptyApprox+    ]