packages feed

dataframe 0.3.3.7 → 0.3.3.8

raw patch · 18 files changed

+996/−370 lines, 18 filesdep +aesondep +cassavadep +regex-tdfaPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: aeson, cassava, regex-tdfa, scientific, unordered-containers

API changes (from Hackage documentation)

- DataFrame: [chunkSize] :: ReadOptions -> Int
- DataFrame.IO.CSV: DoubleType :: InferredType
- DataFrame.IO.CSV: GrowingDouble :: !GrowingUnboxedVector Double -> !IORef [Int] -> GrowingColumn
- DataFrame.IO.CSV: GrowingInt :: !GrowingUnboxedVector Int -> !IORef [Int] -> GrowingColumn
- DataFrame.IO.CSV: GrowingText :: !GrowingVector Text -> !IORef [Int] -> GrowingColumn
- DataFrame.IO.CSV: GrowingUnboxedVector :: !IORef (IOVector a) -> !IORef Int -> !IORef Int -> GrowingUnboxedVector a
- DataFrame.IO.CSV: GrowingVector :: !IORef (IOVector a) -> !IORef Int -> !IORef Int -> GrowingVector a
- DataFrame.IO.CSV: IntType :: InferredType
- DataFrame.IO.CSV: TextType :: InferredType
- DataFrame.IO.CSV: [chunkSize] :: ReadOptions -> Int
- DataFrame.IO.CSV: [guvCapacity] :: GrowingUnboxedVector a -> !IORef Int
- DataFrame.IO.CSV: [guvData] :: GrowingUnboxedVector a -> !IORef (IOVector a)
- DataFrame.IO.CSV: [guvSize] :: GrowingUnboxedVector a -> !IORef Int
- DataFrame.IO.CSV: [gvCapacity] :: GrowingVector a -> !IORef Int
- DataFrame.IO.CSV: [gvData] :: GrowingVector a -> !IORef (IOVector a)
- DataFrame.IO.CSV: [gvSize] :: GrowingVector a -> !IORef Int
- DataFrame.IO.CSV: appendGrowingUnboxedVector :: Unbox a => GrowingUnboxedVector a -> a -> IO ()
- DataFrame.IO.CSV: appendGrowingVector :: GrowingVector a -> a -> IO ()
- DataFrame.IO.CSV: data GrowingColumn
- DataFrame.IO.CSV: data GrowingUnboxedVector a
- DataFrame.IO.CSV: data GrowingVector a
- DataFrame.IO.CSV: data InferredType
- DataFrame.IO.CSV: endOfLine :: Parser ()
- DataFrame.IO.CSV: field :: Char -> Parser ByteString
- DataFrame.IO.CSV: freezeGrowingColumn :: GrowingColumn -> IO Column
- DataFrame.IO.CSV: freezeGrowingUnboxedVector :: Unbox a => GrowingUnboxedVector a -> IO (Vector a)
- DataFrame.IO.CSV: freezeGrowingVector :: GrowingVector a -> IO (Vector a)
- DataFrame.IO.CSV: inferType :: Text -> InferredType
- DataFrame.IO.CSV: newGrowingUnboxedVector :: Unbox a => Int -> IO (GrowingUnboxedVector a)
- DataFrame.IO.CSV: newGrowingVector :: Int -> IO (GrowingVector a)
- DataFrame.IO.CSV: parseLine :: Char -> ByteString -> [ByteString]
- DataFrame.IO.CSV: parseRow :: Char -> Parser [ByteString]
- DataFrame.IO.CSV: processFile :: Handle -> Char -> [GrowingColumn] -> Int -> Int -> IO ()
- DataFrame.IO.CSV: quotedField :: Parser ByteString
- DataFrame.IO.CSV: record :: Char -> Parser [ByteString]
- DataFrame.IO.CSV: unquotedField :: Char -> Parser ByteString
+ DataFrame: columnAsList :: Columnable a => Text -> DataFrame -> [a]
+ DataFrame: imputeWith :: Columnable b => (Expr b -> Expr b) -> Expr (Maybe b) -> DataFrame -> DataFrame
+ DataFrame: insert :: (Columnable a, Foldable t) => Text -> t a -> DataFrame -> DataFrame
+ DataFrame: insertWithDefault :: (Columnable a, Foldable t) => a -> Text -> t a -> DataFrame -> DataFrame
+ DataFrame: nColumns :: DataFrame -> Int
+ DataFrame: nRows :: DataFrame -> Int
+ DataFrame.Functions: bind :: forall a b m. (Columnable a, Columnable (m a), Monad m, Columnable b, Columnable (m b)) => (a -> m b) -> Expr (m a) -> Expr (m b)
+ DataFrame.Functions: daysBetween :: Expr Day -> Expr Day -> Expr Int
+ DataFrame.Functions: firstOrNothing :: Columnable a => Expr [a] -> Expr (Maybe a)
+ DataFrame.Functions: lastOrNothing :: Columnable a => Expr [a] -> Expr (Maybe a)
+ DataFrame.Functions: match :: Text -> Expr Text -> Expr (Maybe Text)
+ DataFrame.Functions: matchAll :: Text -> Expr Text -> Expr [Text]
+ DataFrame.Functions: parseDate :: Text -> Expr Text -> Expr (Maybe Day)
+ DataFrame.Functions: recodeWithDefault :: (Columnable a, Columnable b) => b -> [(a, b)] -> Expr a -> Expr b
+ DataFrame.Functions: splitOn :: Text -> Expr Text -> Expr [Text]
+ DataFrame.IO.CSV: BuilderDouble :: !PagedUnboxedVector Double -> !PagedUnboxedVector Word8 -> BuilderColumn
+ DataFrame.IO.CSV: BuilderInt :: !PagedUnboxedVector Int -> !PagedUnboxedVector Word8 -> BuilderColumn
+ DataFrame.IO.CSV: BuilderText :: !PagedVector Text -> !PagedUnboxedVector Word8 -> BuilderColumn
+ DataFrame.IO.CSV: PagedUnboxedVector :: !IORef [Vector a] -> !IORef (IOVector a) -> !IORef Int -> PagedUnboxedVector a
+ DataFrame.IO.CSV: PagedVector :: !IORef [Vector a] -> !IORef (IOVector a) -> !IORef Int -> PagedVector a
+ DataFrame.IO.CSV: [puvActive] :: PagedUnboxedVector a -> !IORef (IOVector a)
+ DataFrame.IO.CSV: [puvChunks] :: PagedUnboxedVector a -> !IORef [Vector a]
+ DataFrame.IO.CSV: [puvCount] :: PagedUnboxedVector a -> !IORef Int
+ DataFrame.IO.CSV: [pvActive] :: PagedVector a -> !IORef (IOVector a)
+ DataFrame.IO.CSV: [pvChunks] :: PagedVector a -> !IORef [Vector a]
+ DataFrame.IO.CSV: [pvCount] :: PagedVector a -> !IORef Int
+ DataFrame.IO.CSV: appendPagedUnboxedVector :: Unbox a => PagedUnboxedVector a -> a -> IO ()
+ DataFrame.IO.CSV: appendPagedVector :: PagedVector a -> a -> IO ()
+ DataFrame.IO.CSV: chunkSize :: Int
+ DataFrame.IO.CSV: constructOptional :: (Unbox a, Columnable a) => Vector a -> Vector Word8 -> IO Column
+ DataFrame.IO.CSV: constructOptionalBoxed :: Vector Text -> Vector Word8 -> IO Column
+ DataFrame.IO.CSV: data BuilderColumn
+ DataFrame.IO.CSV: data PagedUnboxedVector a
+ DataFrame.IO.CSV: data PagedVector a
+ DataFrame.IO.CSV: freezeBuilderColumn :: BuilderColumn -> IO Column
+ DataFrame.IO.CSV: freezePagedUnboxedVector :: Unbox a => PagedUnboxedVector a -> IO (Vector a)
+ DataFrame.IO.CSV: freezePagedVector :: PagedVector a -> IO (Vector a)
+ DataFrame.IO.CSV: newPagedUnboxedVector :: Unbox a => IO (PagedUnboxedVector a)
+ DataFrame.IO.CSV: newPagedVector :: IO (PagedVector a)
+ DataFrame.IO.CSV: processStream :: Records (Vector ByteString) -> [BuilderColumn] -> IO ()
+ DataFrame.IO.CSV: schemaTypes :: TypeSpec -> [SchemaType]
+ DataFrame.IO.JSON: readJSON :: FilePath -> IO DataFrame
+ DataFrame.IO.JSON: readJSONEither :: ByteString -> Either String DataFrame
+ DataFrame.Internal.Column: allMissing :: Column -> Bool
+ DataFrame.Internal.DataFrame: columnAsList :: Columnable a => Text -> DataFrame -> [a]
+ DataFrame.Internal.Parsing: isFalseish :: Text -> Bool
+ DataFrame.Internal.Parsing: isTrueish :: Text -> Bool
+ DataFrame.Internal.Parsing: readBool :: HasCallStack => Text -> Maybe Bool
+ DataFrame.Operations.Core: insert :: (Columnable a, Foldable t) => Text -> t a -> DataFrame -> DataFrame
+ DataFrame.Operations.Core: insertWithDefault :: (Columnable a, Foldable t) => a -> Text -> t a -> DataFrame -> DataFrame
+ DataFrame.Operations.Core: nColumns :: DataFrame -> Int
+ DataFrame.Operations.Core: nRows :: DataFrame -> Int
+ DataFrame.Operations.Statistics: imputeWith :: Columnable b => (Expr b -> Expr b) -> Expr (Maybe b) -> DataFrame -> DataFrame
+ DataFrame.Operations.Typing: BoolAssumption :: ParsingAssumption
+ DataFrame.Operations.Typing: handleBoolAssumption :: Vector (Maybe Text) -> Column
+ DataFrame.Operations.Typing: parseWithTypes :: [SchemaType] -> DataFrame -> DataFrame
- DataFrame: ReadOptions :: HeaderSpec -> TypeSpec -> Bool -> Int -> String -> ReadOptions
+ DataFrame: ReadOptions :: HeaderSpec -> TypeSpec -> Bool -> String -> ReadOptions
- DataFrame.IO.CSV: ReadOptions :: HeaderSpec -> TypeSpec -> Bool -> Int -> String -> ReadOptions
+ DataFrame.IO.CSV: ReadOptions :: HeaderSpec -> TypeSpec -> Bool -> String -> ReadOptions
- DataFrame.IO.CSV: initializeColumns :: [ByteString] -> ReadOptions -> IO [GrowingColumn]
+ DataFrame.IO.CSV: initializeColumns :: [ByteString] -> ReadOptions -> IO [BuilderColumn]
- DataFrame.IO.CSV: processRow :: Int -> [ByteString] -> [GrowingColumn] -> IO ()
+ DataFrame.IO.CSV: processRow :: Vector ByteString -> [BuilderColumn] -> IO ()

Files

CHANGELOG.md view
@@ -1,5 +1,16 @@ # Revision history for dataframe +## 0.3.3.8+* More efficient inner joins using hashmaps.+* Initial JSON lines implementation+* More robust logic when specifying CSV types.+* Strip spaces from titles and rows in CSV reading.+* Auto parsing bools in CSV.+* Add `imputeWith`, `bind`, `nRows`, `nColumns`, `recodeWitDefault` function that takes +* Better support for proper markdown+* Fix bug with full outer join.+* Unify `insertVector` and `insertList` functions into insert.+ ## 0.3.3.7 * Many functions how rely on expressions (not strings). * full, left, and right join now implemented.
README.md view
@@ -16,7 +16,7 @@ <p align="center">   <a href="https://dataframe.readthedocs.io/en/latest/">User guide</a>   |-  <a href="https://discord.gg/XJE5wKT2kb">Discord</a>+  <a href="https://discord.gg/8u8SCWfrNC">Discord</a> </p>  # DataFrame
dataframe.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               dataframe-version:            0.3.3.7+version:            0.3.3.8  synopsis: A fast, safe, and intuitive DataFrame library. @@ -68,6 +68,7 @@                     DataFrame.Display,                     DataFrame.Display.Terminal.Plot,                     DataFrame.IO.CSV,+                    DataFrame.IO.JSON,                     DataFrame.IO.Unstable.CSV,                     DataFrame.IO.Parquet,                     DataFrame.IO.Parquet.Binary@@ -83,10 +84,12 @@                     DataFrame.Lazy.IO.CSV,                     DataFrame.Lazy.Internal.DataFrame     build-depends:    base >= 4 && <5,+                      aeson >= 0.11.0.0 && < 3,                       array >= 0.5.4.0 && < 0.6,                       attoparsec >= 0.12 && < 0.15,                       bytestring >= 0.11 && < 0.13,                       bytestring-lexing >= 0.5 && < 0.6,+                      cassava >= 0.1 && < 1,                       containers >= 0.6.7 && < 0.9,                       directory >= 1.3.0.0 && < 2,                       granite ^>= 0.3,@@ -94,9 +97,12 @@                       process ^>= 1.6,                       snappy-hs ^>= 0.1,                       random >= 1 && < 2,+                      regex-tdfa >= 1.3.0 && < 2,+                      scientific >=0.3.1 && <0.4,                       template-haskell >= 2.0 && < 3,                       text >= 2.0 && < 3,                       time >= 1.12 && < 2,+                      unordered-containers >= 0.1 && < 1,                       vector ^>= 0.13,                       vector-algorithms ^>= 0.9,                       zstd >= 0.1.2.0 && < 0.2,
src/DataFrame.hs view
@@ -273,6 +273,7 @@     columnAsDoubleVector,     columnAsFloatVector,     columnAsIntVector,+    columnAsList,     columnAsVector,     empty,     null,@@ -313,6 +314,7 @@ import DataFrame.Operations.Statistics as Statistics (     correlation,     frequencies,+    imputeWith,     interQuartileRange,     mean,     median,
src/DataFrame/Display/Terminal/PrettyPrint.hs view
@@ -65,10 +65,10 @@         lines =             if properMarkdown                 then-                    border-                        : fillCols colTitleFill consolidatedHeader-                        : separator-                        : map (fillCols colValueFill) rows+                    T.concat ["  ", border, "  "]+                        : T.concat ["| ", fillCols colTitleFill consolidatedHeader, " |"]+                        : T.concat ["| ", separator, " |"]+                        : map ((\t -> T.concat ["| ", t, " |"]) . fillCols colValueFill) rows                 else                     border                         : fillCols colTitleFill consolidatedHeader
src/DataFrame/Functions.hs view
@@ -39,9 +39,10 @@ import Data.Function import qualified Data.List as L import qualified Data.Map as M-import Data.Maybe (listToMaybe)+import Data.Maybe (fromMaybe, listToMaybe) import qualified Data.Set as S import qualified Data.Text as T+import Data.Time import Data.Type.Equality import qualified Data.Vector as V import qualified Data.Vector.Generic as VG@@ -51,6 +52,7 @@ import Debug.Trace (trace, traceShow) import Language.Haskell.TH import qualified Language.Haskell.TH.Syntax as TH+import Text.Regex.TDFA import Type.Reflection (typeRep) import Prelude hiding (maximum, minimum, sum) @@ -220,6 +222,39 @@     forall a b.     (Columnable a, Columnable b) => [(a, b)] -> Expr a -> Expr (Maybe b) recode mapping = UnaryOp (T.pack ("recode " ++ show mapping)) (`lookup` mapping)++recodeWithDefault ::+    forall a b.+    (Columnable a, Columnable b) => b -> [(a, b)] -> Expr a -> Expr b+recodeWithDefault d mapping =+    UnaryOp (T.pack ("recode " ++ show mapping)) (fromMaybe d . (`lookup` mapping))++firstOrNothing :: (Columnable a) => Expr [a] -> Expr (Maybe a)+firstOrNothing = lift listToMaybe++lastOrNothing :: (Columnable a) => Expr [a] -> Expr (Maybe a)+lastOrNothing = lift (listToMaybe . reverse)++splitOn :: T.Text -> Expr T.Text -> Expr [T.Text]+splitOn delim = lift (T.splitOn delim)++match :: T.Text -> Expr T.Text -> Expr (Maybe T.Text)+match regex = lift ((\r -> if T.null r then Nothing else Just r) . (=~ regex))++matchAll :: T.Text -> Expr T.Text -> Expr [T.Text]+matchAll regex = lift (getAllTextMatches . (=~ regex))++parseDate :: T.Text -> Expr T.Text -> Expr (Maybe Day)+parseDate format = lift (parseTimeM True defaultTimeLocale (T.unpack format) . T.unpack)++daysBetween :: Expr Day -> Expr Day -> Expr Int+daysBetween d1 d2 = lift fromIntegral (lift2 diffDays d1 d2)++bind ::+    forall a b m.+    (Columnable a, Columnable (m a), Monad m, Columnable b, Columnable (m b)) =>+    (a -> m b) -> Expr (m a) -> Expr (m b)+bind f = lift (>>= f)  generateConditions ::     TypedColumn Double -> [Expr Bool] -> [Expr Double] -> DataFrame -> [Expr Bool]
src/DataFrame/IO/CSV.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-}-{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-}@@ -10,9 +10,7 @@  module DataFrame.IO.CSV where -import qualified Data.ByteString as BS-import qualified Data.ByteString.Builder as Builder-import qualified Data.ByteString.Char8 as C8+import qualified Data.ByteString.Lazy as BL import qualified Data.List as L import qualified Data.Map.Strict as M import qualified Data.Proxy as P@@ -24,18 +22,20 @@ import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Unboxed.Mutable as VUM -import Control.Applicative (many, (<|>))-import Control.Monad (forM_, unless, zipWithM, zipWithM_)-import Data.Attoparsec.ByteString.Char8 hiding (endOfLine)-import Data.Bits (shiftL)+import Data.Csv.Streaming (Records (..))+import qualified Data.Csv.Streaming as CsvStream++import Control.Monad import Data.Char+import qualified Data.Csv as Csv import Data.Either import Data.Function (on) import Data.Functor import Data.IORef import Data.Maybe import Data.Type.Equality (TestEquality (testEquality))-import DataFrame.Internal.Column (Column (..), columnLength)+import Data.Word (Word8)+import DataFrame.Internal.Column import DataFrame.Internal.DataFrame (DataFrame (..)) import DataFrame.Internal.Parsing import DataFrame.Internal.Schema@@ -44,45 +44,105 @@ import Type.Reflection import Prelude hiding (concat, takeWhile) -data GrowingVector a = GrowingVector-    { gvData :: !(IORef (VM.IOVector a))-    , gvSize :: !(IORef Int)-    , gvCapacity :: !(IORef Int)+chunkSize :: Int+chunkSize = 16_384++data PagedVector a = PagedVector+    { pvChunks :: !(IORef [V.Vector a])+    -- ^ Finished chunks (reverse order)+    , pvActive :: !(IORef (VM.IOVector a))+    -- ^ Current mutable chunk+    , pvCount :: !(IORef Int)+    -- ^ Items written in current chunk     } -data GrowingUnboxedVector a = GrowingUnboxedVector-    { guvData :: !(IORef (VUM.IOVector a))-    , guvSize :: !(IORef Int)-    , guvCapacity :: !(IORef Int)+data PagedUnboxedVector a = PagedUnboxedVector+    { puvChunks :: !(IORef [VU.Vector a])+    , puvActive :: !(IORef (VUM.IOVector a))+    , puvCount :: !(IORef Int)     } -data GrowingColumn-    = GrowingInt !(GrowingUnboxedVector Int) !(IORef [Int])-    | GrowingDouble !(GrowingUnboxedVector Double) !(IORef [Int])-    | GrowingText !(GrowingVector T.Text) !(IORef [Int])+data BuilderColumn+    = BuilderInt !(PagedUnboxedVector Int) !(PagedUnboxedVector Word8)+    | BuilderDouble !(PagedUnboxedVector Double) !(PagedUnboxedVector Word8)+    | BuilderText !(PagedVector T.Text) !(PagedUnboxedVector Word8) -data HeaderSpec-    = -- | File has no header row-      NoHeader-    | -- | Use first row as column names-      UseFirstRow-    | -- | Supply names for a no-header file-      ProvideNames [T.Text]-    deriving (Eq, Show)+newPagedVector :: IO (PagedVector a)+newPagedVector = do+    active <- VM.unsafeNew chunkSize+    PagedVector <$> newIORef [] <*> newIORef active <*> newIORef 0 -data TypeSpec-    = InferFromSample Int-    | SpecifyTypes [SchemaType]-    | NoInference+newPagedUnboxedVector :: (VUM.Unbox a) => IO (PagedUnboxedVector a)+newPagedUnboxedVector = do+    active <- VUM.unsafeNew chunkSize+    PagedUnboxedVector <$> newIORef [] <*> newIORef active <*> newIORef 0 -shouldInferFromSample :: TypeSpec -> Bool-shouldInferFromSample (InferFromSample _) = True-shouldInferFromSample _ = False+appendPagedVector :: PagedVector a -> a -> IO ()+appendPagedVector (PagedVector chunksRef activeRef countRef) !val = do+    count <- readIORef countRef+    active <- readIORef activeRef -typeInferenceSampleSize :: TypeSpec -> Int-typeInferenceSampleSize (InferFromSample n) = n-typeInferenceSampleSize _ = 0+    if count < chunkSize+        then do+            VM.unsafeWrite active count val+            writeIORef countRef $! count + 1+        else do+            frozen <- V.freeze active+            modifyIORef' chunksRef (frozen :) +            newActive <- VM.unsafeNew chunkSize+            VM.unsafeWrite newActive 0 val++            writeIORef activeRef newActive+            writeIORef countRef 1+{-# INLINE appendPagedVector #-}++appendPagedUnboxedVector :: (VUM.Unbox a) => PagedUnboxedVector a -> a -> IO ()+appendPagedUnboxedVector (PagedUnboxedVector chunksRef activeRef countRef) !val = do+    count <- readIORef countRef+    active <- readIORef activeRef++    if count < chunkSize+        then do+            VUM.unsafeWrite active count val+            writeIORef countRef $! count + 1+        else do+            frozen <- VU.freeze active+            modifyIORef' chunksRef (frozen :)++            newActive <- VUM.unsafeNew chunkSize+            VUM.unsafeWrite newActive 0 val++            writeIORef activeRef newActive+            writeIORef countRef 1+{-# INLINE appendPagedUnboxedVector #-}++freezePagedVector :: PagedVector a -> IO (V.Vector a)+freezePagedVector (PagedVector chunksRef activeRef countRef) = do+    count <- readIORef countRef+    active <- readIORef activeRef+    chunks <- readIORef chunksRef++    lastChunk <- V.freeze (VM.slice 0 count active)++    return $! V.concat (reverse (lastChunk : chunks))++freezePagedUnboxedVector ::+    (VUM.Unbox a) => PagedUnboxedVector a -> IO (VU.Vector a)+freezePagedUnboxedVector (PagedUnboxedVector chunksRef activeRef countRef) = do+    count <- readIORef countRef+    active <- readIORef activeRef+    chunks <- readIORef chunksRef++    lastChunk <- VU.freeze (VUM.slice 0 count active)+    return $! VU.concat (reverse (lastChunk : chunks))++-- | STANDARD CONFIG TYPES+data HeaderSpec = NoHeader | UseFirstRow | ProvideNames [T.Text]+    deriving (Eq, Show)++data TypeSpec = InferFromSample Int | SpecifyTypes [SchemaType] | NoInference+ -- | CSV read parameters. data ReadOptions = ReadOptions     { headerSpec :: HeaderSpec@@ -91,8 +151,6 @@     -- ^ Whether/how to infer types. (default: InferFromSample 100)     , safeRead :: Bool     -- ^ Whether to partially parse values into `Maybe`/`Either`. (default: True)-    , chunkSize :: Int-    -- ^ Default chunk size (in bytes) for csv reader. (default: 512'000)     , dateFormat :: String     {- ^ Format of date fields as recognized by the Data.Time.Format module. @@ -107,78 +165,27 @@     -}     } +shouldInferFromSample :: TypeSpec -> Bool+shouldInferFromSample (InferFromSample _) = True+shouldInferFromSample _ = False++schemaTypes :: TypeSpec -> [SchemaType]+schemaTypes (SpecifyTypes xs) = xs+schemaTypes _ = []++typeInferenceSampleSize :: TypeSpec -> Int+typeInferenceSampleSize (InferFromSample n) = n+typeInferenceSampleSize _ = 0+ defaultReadOptions :: ReadOptions defaultReadOptions =     ReadOptions         { headerSpec = UseFirstRow         , typeSpec = InferFromSample 100         , safeRead = True-        , chunkSize = 512_000         , dateFormat = "%Y-%m-%d"         } -newGrowingVector :: Int -> IO (GrowingVector a)-newGrowingVector !initCap = do-    vec <- VM.unsafeNew initCap-    GrowingVector <$> newIORef vec <*> newIORef 0 <*> newIORef initCap--newGrowingUnboxedVector :: (VUM.Unbox a) => Int -> IO (GrowingUnboxedVector a)-newGrowingUnboxedVector !initCap = do-    vec <- VUM.unsafeNew initCap-    GrowingUnboxedVector <$> newIORef vec <*> newIORef 0 <*> newIORef initCap--appendGrowingVector :: GrowingVector a -> a -> IO ()-appendGrowingVector (GrowingVector vecRef sizeRef capRef) !val = do-    size <- readIORef sizeRef-    cap <- readIORef capRef-    vec <- readIORef vecRef--    vec' <--        if size >= cap-            then do-                let !newCap = cap `shiftL` 1-                newVec <- VM.unsafeGrow vec newCap-                writeIORef vecRef newVec-                writeIORef capRef newCap-                return newVec-            else return vec--    VM.unsafeWrite vec' size val-    writeIORef sizeRef $! size + 1--appendGrowingUnboxedVector ::-    (VUM.Unbox a) => GrowingUnboxedVector a -> a -> IO ()-appendGrowingUnboxedVector (GrowingUnboxedVector vecRef sizeRef capRef) !val = do-    size <- readIORef sizeRef-    cap <- readIORef capRef-    vec <- readIORef vecRef--    vec' <--        if size >= cap-            then do-                let !newCap = cap `shiftL` 1-                newVec <- VUM.unsafeGrow vec newCap-                writeIORef vecRef newVec-                writeIORef capRef newCap-                return newVec-            else return vec--    VUM.unsafeWrite vec' size val-    writeIORef sizeRef $! size + 1--freezeGrowingVector :: GrowingVector a -> IO (V.Vector a)-freezeGrowingVector (GrowingVector vecRef sizeRef _) = do-    vec <- readIORef vecRef-    size <- readIORef sizeRef-    V.freeze (VM.slice 0 size vec)--freezeGrowingUnboxedVector ::-    (VUM.Unbox a) => GrowingUnboxedVector a -> IO (VU.Vector a)-freezeGrowingUnboxedVector (GrowingUnboxedVector vecRef sizeRef _) = do-    vec <- readIORef vecRef-    size <- readIORef sizeRef-    VU.freeze (VUM.slice 0 size vec)- {- | Read CSV file from path and load it into a dataframe.  ==== __Example__@@ -221,35 +228,45 @@ @ -} readSeparated :: Char -> ReadOptions -> FilePath -> IO DataFrame-readSeparated !sep !opts !path = withFile path ReadMode $ \handle -> do-    hSetBuffering handle (BlockBuffering (Just (chunkSize opts)))--    firstLine <- C8.hGetLine handle-    let firstRow = parseLine sep firstLine-        columnNames = case headerSpec opts of-            NoHeader -> map (T.pack . show) [0 .. length firstRow - 1]-            UseFirstRow -> map (stripQuotes . TE.decodeUtf8Lenient) firstRow-            ProvideNames ns -> ns--    unless (headerSpec opts == UseFirstRow) $ hSeek handle AbsoluteSeek 0+readSeparated !sep !opts !path = do+    csvData <- BL.readFile path+    let decodeOpts = Csv.defaultDecodeOptions{Csv.decDelimiter = fromIntegral (ord sep)}+    let stream = CsvStream.decodeWith decodeOpts Csv.NoHeader csvData -    dataLine <- C8.hGetLine handle-    let dataRow = parseLine sep dataLine-    growingCols <- initializeColumns dataRow opts+    let peekStream (Cons (Right row) rest) = return (row, rest)+        peekStream (Cons (Left err) _) = error $ "Error parsing CSV header: " ++ err+        peekStream (Nil Nothing _) = error "Empty CSV file"+        peekStream (Nil (Just err) _) = error err -    processRow 0 dataRow growingCols+    (firstRowRaw, dataStream) <- peekStream stream -    processFile handle sep growingCols (chunkSize opts) 1+    let (columnNames, rowsToProcess) = case headerSpec opts of+            NoHeader ->+                ( map (T.pack . show) [0 .. V.length firstRowRaw - 1]+                , Cons (Right firstRowRaw) dataStream+                )+            UseFirstRow ->+                ( map (T.strip . TE.decodeUtf8Lenient . BL.toStrict) (V.toList firstRowRaw)+                , dataStream+                )+            ProvideNames ns ->+                ( ns ++ drop (length ns) (map (T.pack . show) [0 .. V.length firstRowRaw - 1])+                , Cons (Right firstRowRaw) dataStream+                ) -    frozenCols <- V.fromList <$> mapM freezeGrowingColumn growingCols+    (sampleRow, _) <- peekStream rowsToProcess+    builderCols <- initializeColumns (V.toList sampleRow) opts+    processStream rowsToProcess builderCols +    frozenCols <- V.fromList <$> mapM freezeBuilderColumn builderCols     let numRows = maybe 0 columnLength (frozenCols V.!? 0)-        df =++    let df =             DataFrame-                { columns = frozenCols-                , columnIndices = M.fromList (zip columnNames [0 ..])-                , dataframeDimensions = (numRows, V.length frozenCols)-                }+                frozenCols+                (M.fromList (zip columnNames [0 ..]))+                (numRows, V.length frozenCols)+     return $         if shouldInferFromSample (typeSpec opts)             then@@ -258,159 +275,95 @@                     (safeRead opts)                     (dateFormat opts)                     df-            else df+            else+                if not (null (schemaTypes (typeSpec opts)))+                    then parseWithTypes (schemaTypes (typeSpec opts)) df+                    else df -initializeColumns :: [BS.ByteString] -> ReadOptions -> IO [GrowingColumn]+initializeColumns :: [BL.ByteString] -> ReadOptions -> IO [BuilderColumn] initializeColumns row opts = case typeSpec opts of     NoInference -> zipWithM initColumn row (expandTypes [])     InferFromSample _ -> zipWithM initColumn row (expandTypes [])     SpecifyTypes ts -> zipWithM initColumn row (expandTypes ts)   where     expandTypes xs = xs ++ replicate (length row - length xs) (schemaType @T.Text)-    initColumn :: BS.ByteString -> SchemaType -> IO GrowingColumn-    initColumn bs t = do-        nullsRef <- newIORef []+    initColumn _ t = do+        validityRef <- newPagedUnboxedVector         case t of             SType (_ :: P.Proxy a) -> case testEquality (typeRep @a) (typeRep @Int) of-                Just Refl -> GrowingInt <$> newGrowingUnboxedVector 1_024 <*> pure nullsRef+                Just Refl -> BuilderInt <$> newPagedUnboxedVector <*> pure validityRef                 Nothing -> case testEquality (typeRep @a) (typeRep @Double) of-                    Just Refl -> GrowingDouble <$> newGrowingUnboxedVector 1_024 <*> pure nullsRef-                    Nothing -> GrowingText <$> newGrowingVector 1_024 <*> pure nullsRef--data InferredType = IntType | DoubleType | TextType+                    Just Refl -> BuilderDouble <$> newPagedUnboxedVector <*> pure validityRef+                    Nothing -> BuilderText <$> newPagedVector <*> pure validityRef -inferType :: T.Text -> InferredType-inferType !t-    | T.null t = TextType-    | isJust (readInt t) = IntType-    | isJust (readDouble t) = DoubleType-    | otherwise = TextType+processStream ::+    CsvStream.Records (V.Vector BL.ByteString) -> [BuilderColumn] -> IO ()+processStream (Cons (Right row) rest) cols = processRow row cols >> processStream rest cols+processStream (Cons (Left err) _) _ = error ("CSV Parse Error: " ++ err)+processStream (Nil _ _) _ = return () -processRow :: Int -> [BS.ByteString] -> [GrowingColumn] -> IO ()-processRow !rowIdx !vals !cols = zipWithM_ (processValue rowIdx) vals cols+processRow :: V.Vector BL.ByteString -> [BuilderColumn] -> IO ()+processRow !vals !cols = V.zipWithM_ processValue vals (V.fromList cols)   where-    processValue :: Int -> BS.ByteString -> GrowingColumn -> IO ()-    processValue !idx !bs !col = do-        let !val = (stripQuotes . TE.decodeUtf8Lenient) bs+    processValue !bs !col = do+        let bs' = BL.toStrict bs         case col of-            GrowingInt gv nulls ->-                case readByteStringInt bs of-                    Just !i -> appendGrowingUnboxedVector gv i-                    Nothing -> do-                        appendGrowingUnboxedVector gv 0-                        modifyIORef' nulls (idx :)-            GrowingDouble gv nulls ->-                case readByteStringDouble bs of-                    Just !d -> appendGrowingUnboxedVector gv d-                    Nothing -> do-                        appendGrowingUnboxedVector gv 0.0-                        modifyIORef' nulls (idx :)-            GrowingText gv nulls ->+            BuilderInt gv valid -> case readByteStringInt bs' of+                Just !i -> appendPagedUnboxedVector gv i >> appendPagedUnboxedVector valid 1+                Nothing -> appendPagedUnboxedVector gv 0 >> appendPagedUnboxedVector valid 0+            BuilderDouble gv valid -> case readByteStringDouble bs' of+                Just !d -> appendPagedUnboxedVector gv d >> appendPagedUnboxedVector valid 1+                Nothing -> appendPagedUnboxedVector gv 0.0 >> appendPagedUnboxedVector valid 0+            BuilderText gv valid -> do+                let !val = T.strip (TE.decodeUtf8Lenient bs')                 if isNull val-                    then do-                        appendGrowingVector gv T.empty-                        modifyIORef' nulls (idx :)-                    else appendGrowingVector gv val+                    then appendPagedVector gv T.empty >> appendPagedUnboxedVector valid 0+                    else appendPagedVector gv val >> appendPagedUnboxedVector valid 1  isNull :: T.Text -> Bool isNull t = T.null t || t == "NA" || t == "NULL" || t == "null" -processFile :: Handle -> Char -> [GrowingColumn] -> Int -> Int -> IO ()-processFile !handle !sep !cols !chunk r = do-    let go remain !rowIdx =-            parseWith (C8.hGetNonBlocking handle chunk) (parseRow sep) remain >>= \case-                Fail unconsumed ctx er -> do-                    erpos <- hTell handle-                    fail $-                        "Failed to parse CSV file around "-                            <> show erpos-                            <> " byte; due: "-                            <> show er-                            <> "; context: "-                            <> show ctx-                Partial c -> do-                    fail "Partial handler is called"-                Done (unconsumed :: C8.ByteString) (row :: [C8.ByteString]) -> do-                    processRow rowIdx row cols-                    unless (null row || unconsumed == mempty) $ go unconsumed $! rowIdx + 1-    go "" r--parseLine :: Char -> BS.ByteString -> [BS.ByteString]-parseLine !sep = fromRight [] . parseOnly (record sep)--parseRow :: Char -> Parser [C8.ByteString]-parseRow sep = record sep <* (endOfLine <|> endOfInput) <?> "CSV row"--record :: Char -> Parser [BS.ByteString]-record sep = field sep `sepBy` char sep <?> "CSV record"--field :: Char -> Parser BS.ByteString-field sep = quotedField <|> unquotedField sep <?> "CSV field"--unquotedField :: Char -> Parser BS.ByteString-unquotedField sep = takeWhile (\c -> c /= sep && c /= '\n' && c /= '\r') <?> "unquoted field"--quotedField :: Parser BS.ByteString-quotedField =-    do-        char '"'-        contents <- Builder.toLazyByteString <$> parseQuotedContents-        char '"'-        return $ BS.toStrict contents-        <?> "quoted field"-  where-    parseQuotedContents = mconcat <$> many quotedChar-    quotedChar =-        Builder.byteString <$> takeWhile1 (/= '"')-            <|> ((char '"' *> char '"') Data.Functor.$> Builder.char8 '"')-            <?> "quoted field content"--endOfLine :: Parser ()-endOfLine =-    (void (string "\r\n") <|> void (char '\n') <|> void (char '\r'))-        <?> "line ending"--freezeGrowingColumn :: GrowingColumn -> IO Column-freezeGrowingColumn (GrowingInt gv nullsRef) = do-    vec <- freezeGrowingUnboxedVector gv-    nulls <- readIORef nullsRef-    if null nulls+freezeBuilderColumn :: BuilderColumn -> IO Column+freezeBuilderColumn (BuilderInt gv validRef) = do+    vec <- freezePagedUnboxedVector gv+    valid <- freezePagedUnboxedVector validRef+    if VU.all (== 1) valid         then return $ UnboxedColumn vec-        else do-            let size = VU.length vec-            mvec <- VM.new size-            forM_ [0 .. size - 1] $ \i -> do-                if i `elem` nulls-                    then VM.write mvec i Nothing-                    else VM.write mvec i (Just (vec VU.! i))-            OptionalColumn <$> V.freeze mvec-freezeGrowingColumn (GrowingDouble gv nullsRef) = do-    vec <- freezeGrowingUnboxedVector gv-    nulls <- readIORef nullsRef-    if null nulls+        else constructOptional vec valid+freezeBuilderColumn (BuilderDouble gv validRef) = do+    vec <- freezePagedUnboxedVector gv+    valid <- freezePagedUnboxedVector validRef+    if VU.all (== 1) valid         then return $ UnboxedColumn vec-        else do-            let size = VU.length vec-            mvec <- VM.new size-            forM_ [0 .. size - 1] $ \i -> do-                if i `elem` nulls-                    then VM.write mvec i Nothing-                    else VM.write mvec i (Just (vec VU.! i))-            OptionalColumn <$> V.freeze mvec-freezeGrowingColumn (GrowingText gv nullsRef) = do-    vec <- freezeGrowingVector gv-    nulls <- readIORef nullsRef-    if null nulls+        else constructOptional vec valid+freezeBuilderColumn (BuilderText gv validRef) = do+    vec <- freezePagedVector gv+    valid <- freezePagedUnboxedVector validRef+    if VU.all (== 1) valid         then return $ BoxedColumn vec-        else do-            let size = V.length vec-            mvec <- VM.new size-            forM_ [0 .. size - 1] $ \i -> do-                if i `elem` nulls-                    then VM.write mvec i Nothing-                    else VM.write mvec i (Just (vec V.! i))-            OptionalColumn <$> V.freeze mvec+        else constructOptionalBoxed vec valid +constructOptional ::+    (VU.Unbox a, Columnable a) => VU.Vector a -> VU.Vector Word8 -> IO Column+constructOptional vec valid = do+    let size = VU.length vec+    mvec <- VM.new size+    forM_ [0 .. size - 1] $ \i ->+        if (valid VU.! i) == 0+            then VM.write mvec i Nothing+            else VM.write mvec i (Just (vec VU.! i))+    OptionalColumn <$> V.freeze mvec++constructOptionalBoxed :: V.Vector T.Text -> VU.Vector Word8 -> IO Column+constructOptionalBoxed vec valid = do+    let size = V.length vec+    mvec <- VM.new size+    forM_ [0 .. size - 1] $ \i ->+        if (valid VU.! i) == 0+            then VM.write mvec i Nothing+            else VM.write mvec i (Just (vec V.! i))+    OptionalColumn <$> V.freeze mvec+ writeCsv :: FilePath -> DataFrame -> IO () writeCsv = writeSeparated ',' @@ -427,7 +380,7 @@     TIO.hPutStrLn handle (T.intercalate ", " headers)     forM_ [0 .. (rows - 1)] $ \i -> do         let row = getRowAsText df i-        TIO.hPutStrLn handle (T.intercalate ", " row)+        TIO.hPutStrLn handle (T.intercalate "," row)  getRowAsText :: DataFrame -> Int -> [T.Text] getRowAsText df i = V.ifoldr go [] (columns df)
+ src/DataFrame/IO/JSON.hs view
@@ -0,0 +1,133 @@+{-# LANGUAGE TypeApplications #-}++module DataFrame.IO.JSON (+    readJSON,+    readJSONEither,+) where++import Control.Monad (forM)+import Data.Aeson+import qualified Data.Aeson.Key as K+import qualified Data.Aeson.KeyMap as KM+import qualified Data.ByteString.Lazy as LBS+import Data.Maybe (catMaybes)+import Data.Scientific (toRealFloat)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Vector as V++import qualified DataFrame.Internal.Column as D+import qualified DataFrame.Internal.DataFrame as D+import qualified DataFrame.Operations.Core as D++readJSONEither :: LBS.ByteString -> Either String D.DataFrame+readJSONEither bs = do+    v <- note "Could not decode JSON" (decode @Value bs)+    rows <- toArrayOfObjects v+    let cols :: [Text]+        cols =+            uniq+                . concatMap (map K.toText . KM.keys)+                . V.toList+                $ rows++    columns <- forM cols $ \c -> do+        let col = buildColumn rows c+        pure (c, col)++    pure $ D.fromNamedColumns columns++readJSON :: FilePath -> IO D.DataFrame+readJSON path = do+    contents <- LBS.readFile path+    case readJSONEither contents of+        Left err -> fail $ "readJSON: " <> err+        Right df -> pure df++toArrayOfObjects :: Value -> Either String (V.Vector Object)+toArrayOfObjects (Array xs)+    | V.null xs = Left "Top-level JSON array is empty"+    | otherwise = traverse asObject xs+toArrayOfObjects _ =+    Left "Top-level JSON value must be a JSON array of objects"++asObject :: Value -> Either String Object+asObject (Object o) = Right o+asObject _ = Left "Expected each element of the array to be an object"++uniq :: (Ord a) => [a] -> [a]+uniq = go mempty+  where+    go _ [] = []+    go seen (x : xs)+        | x `elem` seen = go seen xs+        | otherwise = x : go (x : seen) xs++note :: e -> Maybe a -> Either e a+note e = maybe (Left e) Right++data ColType+    = CTString+    | CTNumber+    | CTBool+    | CTArray+    | CTMixed++buildColumn :: V.Vector Object -> Text -> D.Column+buildColumn rows colName =+    let key = K.fromText colName+        values :: V.Vector (Maybe Value)+        values = V.map (KM.lookup key) rows+        colType = detectColType values+     in case colType of+            CTString ->+                D.fromVector (fmap (fmap asText) values)+            CTNumber ->+                D.fromVector (fmap (fmap asDouble) values)+            CTBool ->+                D.fromVector (fmap (fmap asBool) values)+            CTArray ->+                D.fromVector (fmap (fmap asArray) values)+            CTMixed ->+                D.fromVector values++detectColType :: V.Vector (Maybe Value) -> ColType+detectColType vals =+    case nonMissing of+        [] -> CTMixed+        vs+            | all isString vs -> CTString+            | all isNumber vs -> CTNumber+            | all isBool vs -> CTBool+            | all isArray vs -> CTArray+            | otherwise -> CTMixed+  where+    nonMissing = catMaybes (V.toList vals)++    isString (String _) = True+    isString _ = False++    isNumber (Number _) = True+    isNumber _ = False++    isBool (Bool _) = True+    isBool _ = False++    isArray (Array _) = True+    isArray _ = False++asText :: Value -> Text+asText (String s) = s+asText v = T.pack (show v)++asDouble :: Value -> Double+asDouble (Number s) = toRealFloat @Double s+asDouble v = error $ "asDouble: non-number value: " <> show v++asBool :: Value -> Bool+asBool (Bool b) = b+asBool v = error $ "asBool: non-bool value: " <> show v++asArray :: Value -> V.Vector Value+asArray (Array a) = a+asArray v = error $ "asArray: non-array value: " <> show v
src/DataFrame/Internal/Column.hs view
@@ -70,6 +70,11 @@ hasMissing (OptionalColumn column) = True hasMissing _ = False +-- | Checks if a column contains only missing values.+allMissing :: Column -> Bool+allMissing (OptionalColumn column) = VB.length (VB.filter isNothing column) == VB.length column+allMissing _ = False+ -- | Checks if a column contains numeric values. isNumeric :: Column -> Bool isNumeric (UnboxedColumn (vec :: VU.Vector a)) = case sNumeric @a of
src/DataFrame/Internal/DataFrame.hs view
@@ -303,3 +303,22 @@     (Columnable a, VU.Unbox a) =>     T.Text -> DataFrame -> Either DataFrameException (VU.Vector a) columnAsUnboxedVector name df = toUnboxedVector @a (unsafeGetColumn name df)++{- | Get a specific column as a list.++You must specify the type via type applications.++==== __Examples__++>>> columnAsList @Int "age" df+[25, 30, 35, ...]++>>> columnAsList @Text "name" df+["Alice", "Bob", "Charlie", ...]++==== __Throws__++* 'error' - if the column type doesn't match the requested type+-}+columnAsList :: forall a. (Columnable a) => T.Text -> DataFrame -> [a]+columnAsList name df = V.toList (columnAsVector name df)
src/DataFrame/Internal/Parsing.hs view
@@ -19,10 +19,22 @@             ["Nothing", "NULL", "", " ", "nan", "null", "N/A", "NaN", "NAN", "NA"]     ) +isTrueish :: T.Text -> Bool+isTrueish t = t `elem` ["True", "true", "TRUE"]++isFalseish :: T.Text -> Bool+isFalseish t = t `elem` ["False", "false", "FALSE"]+ readValue :: (HasCallStack, Read a) => T.Text -> a readValue s = case readMaybe (T.unpack s) of     Nothing -> error $ "Could not read value: " ++ T.unpack s     Just value -> value++readBool :: (HasCallStack) => T.Text -> Maybe Bool+readBool s+    | isTrueish s = Just True+    | isFalseish s = Just False+    | otherwise = Nothing  readInteger :: (HasCallStack) => T.Text -> Maybe Integer readInteger s = case signed decimal (T.strip s) of
src/DataFrame/Operations/Core.hs view
@@ -18,6 +18,7 @@  import Control.Exception (throw) import Data.Either+import qualified Data.Foldable as Fold import Data.Function (on, (&)) import Data.Maybe import Data.Type.Equality (TestEquality (..))@@ -42,7 +43,10 @@  ==== __Example__ @-ghci> D.dimensions df+>>> :set -XOverloadedStrings+>>> import qualified DataFrame as D+>>> df = D.fromNamedColumns [("a", D.fromList [1..100]), ("b", D.fromList [1..100]), ("c", D.fromList [1..100])]+>>> D.dimensions df  (100, 3) @@@ -51,13 +55,44 @@ dimensions = dataframeDimensions {-# INLINE dimensions #-} +{- | O(1) Get number of rows in a dataframe.++==== __Example__+@+>>> :set -XOverloadedStrings+>>> import qualified DataFrame as D+>>> df = D.fromNamedColumns [("a", D.fromList [1..100]), ("b", D.fromList [1..100]), ("c", D.fromList [1..100])]+>>> D.nRows df+100+@+-}+nRows :: DataFrame -> Int+nRows = fst . dataframeDimensions++{- | O(1) Get number of columns in a dataframe.++==== __Example__+@+>>> :set -XOverloadedStrings+>>> import qualified DataFrame as D+>>> df = D.fromNamedColumns [("a", D.fromList [1..100]), ("b", D.fromList [1..100]), ("c", D.fromList [1..100])]+>>> D.nColumns df+3+@+-}+nColumns :: DataFrame -> Int+nColumns = snd . dataframeDimensions+ {- | O(k) Get column names of the DataFrame in order of insertion.  ==== __Example__ @-ghci> D.columnNames df+>>> :set -XOverloadedStrings+>>> import qualified DataFrame as D+>>> df = D.fromNamedColumns [("a", D.fromList [1..100]), ("b", D.fromList [1..100]), ("c", D.fromList [1..100])]+>>> D.columnNames df -["col_a", "col_b", "col_c"]+["a", "b", "c"] @ -} columnNames :: DataFrame -> [T.Text]@@ -71,9 +106,10 @@  ==== __Example__ @-ghci> import qualified Data.Vector as V--ghci> D.insertVector "numbers" (V.fromList [1..10]) D.empty+>>> :set -XOverloadedStrings+>>> import qualified DataFrame as D+>>> import qualified Data.Vector as V+>>> D.insertVector "numbers" (V.fromList [(1 :: Int)..10]) D.empty  --------  numbers@@ -106,11 +142,79 @@ insertVector name xs = insertColumn name (fromVector xs) {-# INLINE insertVector #-} -{- | /O(k)/ Add a column to the dataframe providing a default.-This constructs a new vector and also may convert it-to an unboxed vector if necessary. Since columns are usually-large the runtime is dominated by the length of the list, k.+{- | Adds a foldable collection to the dataframe. If the collection has less elements than the+dataframe and the dataframe is not empty+the collection is converted to type `Maybe a` filled with `Nothing` to match the size of the dataframe. Similarly,+if the collection has more elements than what's currently in the dataframe, the other columns in the dataframe are+change to `Maybe <Type>` and filled with `Nothing`.++Be careful not to insert infinite collections with this function as that will crash the program.++==== __Example__+@+>>> :set -XOverloadedStrings+>>> import qualified DataFrame as D+>>> D.insert "numbers" [(1 :: Int)..10] D.empty++--------+ numbers+--------+   Int+--------+ 1+ 2+ 3+ 4+ 5+ 6+ 7+ 8+ 9+ 10++@ -}+insert ::+    forall a t.+    (Columnable a, Foldable t) =>+    -- | Column Name+    T.Text ->+    -- | Sequence to add to dataframe+    t a ->+    -- | DataFrame to add column to+    DataFrame ->+    DataFrame+insert name xs = insertColumn name (fromList (Fold.foldr' (:) [] xs)) -- TODO: Do reflection on container type so we can sometimes avoid the list construction.+{-# INLINE insert #-}++{- | Adds a vector to the dataframe and pads it with a default value if it has less elements than the number of rows.++==== __Example__+@+>>> :set -XOverloadedStrings+>>> import qualified Data.Vector as V+>>> import qualified DataFrame as D+>>> df = D.fromNamedColumns [("x", D.fromList [(1 :: Int)..10])]+>>> D.insertVectorWithDefault 0 "numbers" (V.fromList [(1 :: Int),2,3]) df++-------------+ x  | numbers+----|--------+Int |   Int+----|--------+1   | 1+2   | 2+3   | 3+4   | 0+5   | 0+6   | 0+7   | 0+8   | 0+9   | 0+10  | 0++@+-} insertVectorWithDefault ::     forall a.     (Columnable a) =>@@ -128,6 +232,51 @@         values = xs V.++ V.replicate (rows - V.length xs) defaultValue      in insertColumn name (fromVector values) d +{- | Adds a list to the dataframe and pads it with a default value if it has less elements than the number of rows.++==== __Example__+@+>>> :set -XOverloadedStrings+>>> import qualified DataFrame as D+>>> df = D.fromNamedColumns [("x", D.fromList [(1 :: Int)..10])]+>>> D.insertWithDefault 0 "numbers" [(1 :: Int),2,3] df++-------------+ x  | numbers+----|--------+Int |   Int+----|--------+1   | 1+2   | 2+3   | 3+4   | 0+5   | 0+6   | 0+7   | 0+8   | 0+9   | 0+10  | 0++@+-}+insertWithDefault ::+    forall a t.+    (Columnable a, Foldable t) =>+    -- | Default Value+    a ->+    -- | Column name+    T.Text ->+    -- | Data to add to column+    t a ->+    -- | DataFrame to add the column to+    DataFrame ->+    DataFrame+insertWithDefault defaultValue name xs d =+    let (rows, _) = dataframeDimensions d+        xs' = Fold.foldr' (:) [] xs+        values = xs' ++ replicate (rows - length xs') defaultValue+     in insertColumn name (fromList values) d+ {- | /O(n)/ Adds an unboxed vector to the dataframe.  Same as insertVector but takes an unboxed vector. If you insert a vector of numbers through insertVector it will either way be converted@@ -149,7 +298,9 @@  ==== __Example__ @-ghci> D.insertColumn "numbers" (D.fromList [1..10]) D.empty+>>> :set -XOverloadedStrings+>>> import qualified DataFrame as D+>>> D.insertColumn "numbers" (D.fromList [(1 :: Int)..10]) D.empty  --------  numbers@@ -198,11 +349,10 @@  ==== __Example__ @-ghci> import qualified Data.Vector as V--ghci> df = insertVector "numbers" (V.fromList [1..10]) D.empty--ghci> D.cloneColumn "numbers" "others" df+>>> :set -XOverloadedStrings+>>> import qualified Data.Vector as V+>>> df = insertVector "numbers" (V.fromList [1..10]) D.empty+>>> D.cloneColumn "numbers" "others" df  -----------------  numbers | others@@ -235,11 +385,11 @@  ==== __Example__ @-ghci> import qualified Data.Vector as V--ghci> df = insertVector "numbers" (V.fromList [1..10]) D.empty--ghci> D.rename "numbers" "others" df+>>> :set -XOverloadedStrings+>>> import qualified DataFrame as D+>>> import qualified Data.Vector as V+>>> df = insertVector "numbers" (V.fromList [1..10]) D.empty+>>> D.rename "numbers" "others" df  -------  others@@ -266,11 +416,11 @@  ==== __Example__ @-ghci> import qualified Data.Vector as V--ghci> df = D.insertVector "others" (V.fromList [11..20]) (D.insertVector "numbers" (V.fromList [1..10]) D.empty)--ghci> df+>>> :set -XOverloadedStrings+>>> import qualified DataFrame as D+>>> import qualified Data.Vector as V+>>> df = D.insertVector "others" (V.fromList [11..20]) (D.insertVector "numbers" (V.fromList [1..10]) D.empty)+>>> df  -----------------  numbers | others@@ -288,7 +438,7 @@  9       | 19  10      | 20 -ghci> D.renameMany [("numbers", "first_10"), ("others", "next_10")] df+>>> D.renameMany [("numbers", "first_10"), ("others", "next_10")] df  -------------------  first_10 | next_10@@ -332,11 +482,9 @@  ==== __Example__ @-ghci> import qualified Data.Vector as V--ghci> df = D.insertVector "others" (V.fromList [11..20]) (D.insertVector "numbers" (V.fromList [1..10]) D.empty)--ghci> D.describeColumns df+>>> import qualified Data.Vector as V+>>> df = D.insertVector "others" (V.fromList [11..20]) (D.insertVector "numbers" (V.fromList [1..10]) D.empty)+>>> D.describeColumns df  --------------------------------------------------------  Column Name | # Non-null Values | # Null Values | Type@@ -428,10 +576,8 @@  ==== __Example__ @-ghci> df = D.fromNamedColumns [("numbers", D.fromList [1..10]), ("others", D.fromList [11..20])]--ghci> df-+>>> df = D.fromNamedColumns [("numbers", D.fromList [1..10]), ("others", D.fromList [11..20])]+>>> df -----------------  numbers | others ---------|-------@@ -459,10 +605,8 @@  ==== __Example__ @-ghci> df = D.fromUnnamedColumns [D.fromList [1..10], D.fromList [11..20]]--ghci> df-+>>> df = D.fromUnnamedColumns [D.fromList [1..10], D.fromList [11..20]]+>>> df -----------------   0  |  1 -----|----@@ -488,9 +632,9 @@  ==== __Example__ @-ghci> df = D.fromRows ["A", "B"] [[D.toAny 1, D.toAny 11], [D.toAny 2, D.toAny 12], [D.toAny 3, D.toAny 13]]+>>> df = D.fromRows ["A", "B"] [[D.toAny 1, D.toAny 11], [D.toAny 2, D.toAny 12], [D.toAny 3, D.toAny 13]] -ghci> df+>>> df  ----------   A  |  B@@ -514,9 +658,9 @@  ==== __Example__ @-ghci> df = D.fromUnnamedColumns [D.fromList [1..10], D.fromList [11..20]]+>>> df = D.fromUnnamedColumns [D.fromList [1..10], D.fromList [11..20]] -ghci> D.valueCounts @Int "0" df+>>> D.valueCounts @Int "0" df  [(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,1),(10,1)] @@ -583,7 +727,7 @@  ==== __Example__ @-ghci> D.fold (const id) [1..5] df+>>> D.fold (const id) [1..5] df  ----------   0  |  1
src/DataFrame/Operations/Join.hs view
@@ -1,17 +1,25 @@ {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}  module DataFrame.Operations.Join where +import Control.Applicative (asum)+import qualified Data.HashMap.Strict as HM import qualified Data.Map.Strict as M+import Data.Maybe (fromMaybe) import qualified Data.Text as T+import Data.Type.Equality (TestEquality (..)) import qualified Data.Vector as VB import qualified Data.Vector.Unboxed as VU import DataFrame.Internal.Column as D import DataFrame.Internal.DataFrame as D import DataFrame.Operations.Aggregation as D import DataFrame.Operations.Core as D+import Type.Reflection  -- | Equivalent to SQL join types. data JoinType@@ -55,64 +63,73 @@  @ -}-innerJoin ::-    [T.Text] -> DataFrame -> DataFrame -> DataFrame+innerJoin :: [T.Text] -> DataFrame -> DataFrame -> DataFrame innerJoin cs right left =     let-        leftIndicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` cs) (D.columnIndices left)-        leftRowRepresentations = VU.generate (fst (D.dimensions left)) (D.mkRowRep leftIndicesToGroup left)-        -- key -> [index0, index1]-        leftKeyCountsAndIndices =-            VU.foldr-                (\(i, v) acc -> M.insertWith (++) v [i] acc)-                M.empty-                (VU.indexed leftRowRepresentations)-        -- key -> [index0, index1]-        rightIndicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` cs) (D.columnIndices right)-        rightRowRepresentations = VU.generate (fst (D.dimensions right)) (D.mkRowRep rightIndicesToGroup right)-        rightKeyCountsAndIndices =-            VU.foldr-                (\(i, v) acc -> M.insertWith (++) v [i] acc)-                M.empty-                (VU.indexed rightRowRepresentations)-        -- key -> [(left_indexes0, right_indexes1)]-        mergedKeyCountsAndIndices =-            M.foldrWithKey-                ( \k v m ->-                    if k `M.member` rightKeyCountsAndIndices-                        then M.insert k (VU.fromList v, VU.fromList (rightKeyCountsAndIndices M.! k)) m-                        else m-                )-                M.empty-                leftKeyCountsAndIndices-        -- [(ints, ints)]-        leftAndRightIndicies = M.elems mergedKeyCountsAndIndices-        -- [(ints, ints)] (expanded to n * m)-        expandedIndices =-            map-                ( \(l, r) -> (mconcat (replicate (VU.length r) l), mconcat (replicate (VU.length l) r))+        -- Prepare Keys for the Right DataFrame+        rightIndicesToGroup =+            [c | (k, c) <- M.toList (D.columnIndices right), k `elem` cs]++        rightRowRepresentations :: VU.Vector Int+        rightRowRepresentations =+            VU.generate (fst (D.dimensions right)) (D.mkRowRep rightIndicesToGroup right)++        -- Build the Hash Map: Int -> Vector of Indices+        -- We use ifoldr to efficiently insert (index, key) without intermediate allocations.+        rightKeyMap :: HM.HashMap Int (VU.Vector Int)+        rightKeyMap =+            let accumulator =+                    VU.ifoldr+                        (\i key acc -> HM.insertWith (++) key [i] acc)+                        HM.empty+                        rightRowRepresentations+             in HM.map (VU.fromList . reverse) accumulator++        -- Prepare Keys for Left DataFrame+        leftIndicesToGroup =+            [c | (k, c) <- M.toList (D.columnIndices left), k `elem` cs]++        leftRowRepresentations :: VU.Vector Int+        leftRowRepresentations =+            VU.generate (fst (D.dimensions left)) (D.mkRowRep leftIndicesToGroup left)++        -- Perform the Join+        (leftIndexChunks, rightIndexChunks) =+            VU.ifoldr+                ( \lIdx key (lAcc, rAcc) ->+                    case HM.lookup key rightKeyMap of+                        Nothing -> (lAcc, rAcc)+                        Just rIndices ->+                            let len = VU.length rIndices+                                -- Replicate the Left Index to match the number of Right matches+                                lChunk = VU.replicate len lIdx+                             in (lChunk : lAcc, rIndices : rAcc)                 )-                leftAndRightIndicies-        expandedLeftIndicies = mconcat (map fst expandedIndices)-        expandedRightIndicies = mconcat (map snd expandedIndices)-        -- df+                ([], [])+                leftRowRepresentations++        -- Flatten chunks+        expandedLeftIndicies = VU.concat leftIndexChunks+        expandedRightIndicies = VU.concat rightIndexChunks++        resultLen = VU.length expandedLeftIndicies++        -- Construct Result DataFrames         expandedLeft =             left                 { columns = VB.map (D.atIndicesStable expandedLeftIndicies) (D.columns left)-                , dataframeDimensions =-                    (VU.length expandedLeftIndicies, snd (D.dataframeDimensions left))+                , dataframeDimensions = (resultLen, snd (D.dataframeDimensions left))                 }-        -- df+         expandedRight =             right                 { columns = VB.map (D.atIndicesStable expandedRightIndicies) (D.columns right)-                , dataframeDimensions =-                    (VU.length expandedRightIndicies, snd (D.dataframeDimensions right))+                , dataframeDimensions = (resultLen, snd (D.dataframeDimensions right))                 }-        -- [string]+         leftColumns = D.columnNames left         rightColumns = D.columnNames right-        initDf = expandedLeft+         insertIfPresent _ Nothing df = df         insertIfPresent name (Just c) df = D.insertColumn name c df      in@@ -127,7 +144,7 @@                         )             )             rightColumns-            initDf+            expandedLeft  {- | Performs a left join on two dataframes using the specified key columns. Returns all rows from the left dataframe, with matching rows from the right dataframe.@@ -346,12 +363,22 @@         D.fold             ( \name df ->                 if name `elem` cs-                    then df+                    then case (D.unsafeGetColumn name expandedRight, D.unsafeGetColumn name expandedLeft) of+                        ( OptionalColumn (left :: VB.Vector (Maybe a))+                            , OptionalColumn (right :: VB.Vector (Maybe b))+                            ) -> case testEquality (typeRep @a) (typeRep @b) of+                                Nothing -> error "Cannot join columns of different types"+                                Just Refl ->+                                    D.insert+                                        name+                                        (VB.map (fromMaybe undefined) (VB.zipWith (\l r -> asum [l, r]) left right))+                                        df+                        _ -> error "Join should have optional keys."                     else                         ( if name `elem` leftColumns                             then insertIfPresent ("Right_" <> name) (D.getColumn name expandedRight) df                             else insertIfPresent name (D.getColumn name expandedRight) df                         )-            )+            ) -- ???             rightColumns             initDf
src/DataFrame/Operations/Statistics.hs view
@@ -35,6 +35,7 @@ import DataFrame.Internal.Types import DataFrame.Operations.Core import DataFrame.Operations.Subset (filterJust)+import DataFrame.Operations.Transformations (impute) import Text.Printf (printf) import Type.Reflection (typeRep) @@ -198,6 +199,60 @@         Left e -> throw e         Right xs -> VG.sum xs +{- | /O(n)/ Impute missing values in a column using a derived scalar.++Given++* an expression @f :: 'Expr' b -> 'Expr' b@ that, when interpreted over a+  non-nullable column, produces the same value in every row (for example a+  mean, median, or other aggregate), and+* a nullable column @'Expr' ('Maybe' b)@++this function:++1. Drops all @Nothing@ values from the target column.+2. Interprets @f@ on the remaining non-null values.+3. Checks that the resulting column contains a single repeated value.+4. Uses that value to impute all @Nothing@s in the original column.++==== __Throws__++* 'DataFrameException' - if the column does not exist, is empty,++==== __Example__+@+>>> :set -XOverloadedStrings+>>> import qualified DataFrame as D+>>> let df =+...       D.fromNamedColumns+...         [ ("age", D.fromList [Just 10, Nothing, Just 20 :: Maybe Int]) ]+>>>+>>> -- Impute missing ages with the mean of the observed ages+>>> D.imputeWith F.mean "age" df+-- age+-- ----+-- 10+-- 15+-- 20+@+-}+imputeWith ::+    forall b.+    (Columnable b) =>+    (Expr b -> Expr b) ->+    Expr (Maybe b) ->+    DataFrame ->+    DataFrame+imputeWith f col@(Col columnName) df = case interpret @b (filterJust columnName df) (f (Col @b columnName)) of+    Left e -> throw e+    Right (TColumn value) -> case headColumn @b value of+        Left e -> throw e+        Right h ->+            if all (== h) (toList @b value)+                then impute col h df+                else error "Impute expression returned more than one value"+imputeWith _ _ df = df+ applyStatistic ::     (VU.Vector Double -> Double) -> T.Text -> DataFrame -> Maybe Double applyStatistic f name df = apply =<< _getColumnAsDouble name (filterJust name df)@@ -259,17 +314,18 @@             quartile3 = flip (VG.!) 3 <$> quantiles             max' = flip (VG.!) 4 <$> quantiles             iqr = (-) <$> quartile3 <*> quartile1+            doubleColumn col = _getColumnAsDouble col (filterJust col df)          in             [ count-            , mean' <$> _getColumnAsDouble name df+            , mean' <$> doubleColumn name             , min'             , quartile1             , median'             , quartile3             , max'-            , sqrt . variance' <$> _getColumnAsDouble name df+            , sqrt . variance' <$> doubleColumn name             , iqr-            , skewness' <$> _getColumnAsDouble name df+            , skewness' <$> doubleColumn name             ]  -- | Round a @Double@ to Specified Precision
src/DataFrame/Operations/Typing.hs view
@@ -10,11 +10,14 @@ import qualified Data.Vector as V  import Data.Maybe (fromMaybe)+import qualified Data.Proxy as P import Data.Time import Data.Type.Equality (TestEquality (..), type (:~:) (Refl)) import DataFrame.Internal.Column (Column (..), fromVector) import DataFrame.Internal.DataFrame (DataFrame (..)) import DataFrame.Internal.Parsing+import DataFrame.Internal.Schema+import Text.Read import Type.Reflection (typeRep)  type DateFormat = String@@ -45,12 +48,22 @@         asMaybeText = V.map converter cols      in         case makeParsingAssumption dateFormat examples of+            BoolAssumption -> handleBoolAssumption asMaybeText             IntAssumption -> handleIntAssumption asMaybeText             DoubleAssumption -> handleDoubleAssumption asMaybeText             TextAssumption -> handleTextAssumption asMaybeText             DateAssumption -> handleDateAssumption dateFormat asMaybeText             NoAssumption -> handleNoAssumption dateFormat asMaybeText +handleBoolAssumption :: V.Vector (Maybe T.Text) -> Column+handleBoolAssumption asMaybeText+    | parsableAsBool =+        maybe (fromVector asMaybeBool) fromVector (sequenceA asMaybeBool)+    | otherwise = maybe (fromVector asMaybeText) fromVector (sequenceA asMaybeText)+  where+    asMaybeBool = V.map (>>= readBool) asMaybeText+    parsableAsBool = vecSameConstructor asMaybeText asMaybeBool+ handleIntAssumption :: V.Vector (Maybe T.Text) -> Column handleIntAssumption asMaybeText     | parsableAsInt =@@ -93,14 +106,17 @@     -- means that the examples consisted only of null values, so we can     -- confidently know that this column must be an OptionalColumn     | V.all (== Nothing) asMaybeText = fromVector asMaybeText+    | parsableAsBool = fromVector asMaybeBool     | parsableAsInt = fromVector asMaybeInt     | parsableAsDouble = fromVector asMaybeDouble     | parsableAsDate = fromVector asMaybeDate     | otherwise = fromVector asMaybeText   where+    asMaybeBool = V.map (>>= readBool) asMaybeText     asMaybeInt = V.map (>>= readInt) asMaybeText     asMaybeDouble = V.map (>>= readDouble) asMaybeText     asMaybeDate = V.map (>>= parseTimeOpt dateFormat) asMaybeText+    parsableAsBool = vecSameConstructor asMaybeText asMaybeBool     parsableAsInt =         vecSameConstructor asMaybeText asMaybeInt             && vecSameConstructor asMaybeText asMaybeDouble@@ -149,6 +165,7 @@     -- After accounting for nulls, parsing for Ints and Doubles results in the     -- same corresponding positions of Justs and Nothings, so we assume     -- that the best way to parse is Int+    | vecSameConstructor asMaybeText asMaybeBool = BoolAssumption     | vecSameConstructor asMaybeText asMaybeInt         && vecSameConstructor asMaybeText asMaybeDouble =         IntAssumption@@ -160,13 +177,32 @@     | vecSameConstructor asMaybeText asMaybeDate = DateAssumption     | otherwise = TextAssumption   where+    asMaybeBool = V.map (>>= readBool) asMaybeText     asMaybeInt = V.map (>>= readInt) asMaybeText     asMaybeDouble = V.map (>>= readDouble) asMaybeText     asMaybeDate = V.map (>>= parseTimeOpt dateFormat) asMaybeText  data ParsingAssumption-    = IntAssumption+    = BoolAssumption+    | IntAssumption     | DoubleAssumption     | DateAssumption     | NoAssumption     | TextAssumption++parseWithTypes :: [SchemaType] -> DataFrame -> DataFrame+parseWithTypes ts df = df{columns = go 0 ts (columns df)}+  where+    go :: Int -> [SchemaType] -> V.Vector Column -> V.Vector Column+    go n [] xs = xs+    go n (t : rest) xs+        | n >= V.length xs = xs+        | otherwise =+            go (n + 1) rest (V.update xs (V.fromList [(n, asType t (xs V.! n))]))+    asType :: SchemaType -> Column -> Column+    asType (SType (_ :: P.Proxy a)) c@(BoxedColumn (col :: V.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of+        Just Refl -> c+        Nothing -> case testEquality (typeRep @T.Text) (typeRep @b) of+            Just Refl -> fromVector (V.map ((readMaybe @a) . T.unpack) col)+            Nothing -> fromVector (V.map ((readMaybe @a) . show) col)+    asType _ c = c
tests/Main.hs view
@@ -52,6 +52,21 @@  -- PARSING TESTS ------- 1. SIMPLE CASES+parseBools :: Test+parseBools =+    let afterParse :: [Bool]+        afterParse = [True, True, True] ++ [False, False, False]+        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+     in TestCase+            ( assertEqual+                "Correctly parses Bools without missing values as UnboxedColumn of Bools"+                expected+                actual+            )+ parseInts :: Test parseInts =     let afterParse :: [Int]@@ -217,6 +232,21 @@             )  --- 2. COMBINATION CASES+parseBoolsAndIntsAsTexts :: Test+parseBoolsAndIntsAsTexts =+    let afterParse :: [T.Text]+        afterParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"] ++ ["1", "0", "1"]+        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+     in TestCase+            ( assertEqual+                "Correctly parses mixture of Bools and Ints as Text"+                expected+                actual+            )+ parseIntsAndDoublesAsDoubles :: Test parseIntsAndDoublesAsDoubles =     let afterParse :: [Double]@@ -733,6 +763,21 @@  -- 3A. PARSING WITH SAFEREAD OFF +parseBoolsWithoutSafeRead :: Test+parseBoolsWithoutSafeRead =+    let afterParse :: [Bool]+        afterParse = replicate 10 True ++ replicate 10 False+        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+     in TestCase+            ( assertEqual+                "Correctly parses Bools without missing values as UnboxedColumn of Bools, when safeRead is off"+                expected+                actual+            )+ parseIntsWithoutSafeRead :: Test parseIntsWithoutSafeRead =     let afterParse :: [Int]@@ -1109,6 +1154,21 @@                 actual             ) +parseBoolsAndEmptyStringsWithoutSafeRead :: Test+parseBoolsAndEmptyStringsWithoutSafeRead =+    let afterParse :: [Maybe Bool]+        afterParse = replicate 10 Nothing ++ replicate 10 (Just True) ++ replicate 10 (Just False)+        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+     in TestCase+            ( assertEqual+                "Correctly parses Bools and empty Strings as OptionalColumn of Bools, when safeRead is off"+                expected+                actual+            )+ parseIntsAndEmptyStringsWithoutSafeRead :: Test parseIntsAndEmptyStringsWithoutSafeRead =     let afterParse :: [Maybe Int]@@ -1529,6 +1589,21 @@                 actual             ) +parseBoolsAndNullishStringsWithoutSafeRead :: Test+parseBoolsAndNullishStringsWithoutSafeRead =+    let afterParse :: [T.Text]+        afterParse = replicate 10 "N/A" ++ replicate 10 "True" ++ replicate 10 "False"+        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+     in TestCase+            ( assertEqual+                "Correctly parses Bools with nullish values as BoxedColumn of Texts, when safeRead is off"+                expected+                actual+            )+ parseIntsAndNullishStringsWithoutSafeRead :: Test parseIntsAndNullishStringsWithoutSafeRead =     let afterParse :: [T.Text]@@ -2038,6 +2113,21 @@             )  -- 3B. PARSING WITH SAFEREAD ON+parseBoolsAndEmptyStringsWithSafeRead :: Test+parseBoolsAndEmptyStringsWithSafeRead =+    let afterParse :: [Maybe Bool]+        afterParse = replicate 10 Nothing ++ replicate 10 (Just True)+        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+     in TestCase+            ( assertEqual+                "Correctly parses Bools and empty strings as OptionalColumn of Bools, when safeRead is on"+                expected+                actual+            )+ parseIntsAndEmptyStringsWithSafeRead :: Test parseIntsAndEmptyStringsWithSafeRead =     let afterParse :: [Maybe Int]@@ -3064,6 +3154,36 @@             )  -- 4. PARSING SHOULD NOT DEPEND ON THE NUMBER OF EXAMPLES.+parseBoolsWithOneExample :: Test+parseBoolsWithOneExample =+    let afterParse :: [Bool]+        afterParse = False : replicate 50 True+        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+     in TestCase+            ( assertEqual+                "Correctly parses Bools without missing values as UnboxedColumn of Ints with only one example"+                expected+                actual+            )++parseBoolsWithManyExamples :: Test+parseBoolsWithManyExamples =+    let afterParse :: [Bool]+        afterParse = False : replicate 50 True+        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+     in TestCase+            ( assertEqual+                "Correctly parses Bools without missing values as UnboxedColumn of Ints with only one example"+                expected+                actual+            )+ parseIntsWithOneExample :: Test parseIntsWithOneExample =     let afterParse :: [Int]@@ -4854,21 +4974,27 @@ parseTests :: [Test] parseTests =     [ -- 1. SIMPLE CASES-      TestLabel "parseInts" parseInts+      TestLabel "parseBools" parseBools+    , TestLabel "parseInts" parseInts     , TestLabel "parseDoubles" parseDoubles     , TestLabel "parseDates" parseDates     , TestLabel "parseTexts" parseTexts     , -- 2. COMBINATION CASES-      TestLabel "parseIntsAndDoublesAsDoubles" parseIntsAndDoublesAsDoubles+      TestLabel "parseBoolsAndIntsAsTexts" parseBoolsAndIntsAsTexts+    , TestLabel "parseIntsAndDoublesAsDoubles" parseIntsAndDoublesAsDoubles     , TestLabel "parseIntsAndDatesAsTexts" parseIntsAndDatesAsTexts     , TestLabel "parseTextsAndDoublesAsTexts" parseTextsAndDoublesAsTexts     , TestLabel "parseDatesAndTextsAsTexts" parseDatesAndTextsAsTexts     , -- 3A. PARSING WITH SAFEREAD OFF-      TestLabel "parseIntsWithoutSafeRead" parseIntsWithoutSafeRead+      TestLabel "parseBoolsWithoutSafeRead" parseBoolsWithoutSafeRead+    , TestLabel "parseIntsWithoutSafeRead" parseIntsWithoutSafeRead     , TestLabel "parseDoublesWithoutSafeRead" parseDoublesWithoutSafeRead     , TestLabel "parseDatesWithoutSafeRead" parseDatesWithoutSafeRead     , TestLabel "parseTextsWithoutSafeRead" parseTextsWithoutSafeRead     , TestLabel+        "parseBoolsAndEmptyStringsWithoutSafeRead"+        parseBoolsAndEmptyStringsWithoutSafeRead+    , TestLabel         "parseIntsAndEmptyStringsWithoutSafeRead"         parseIntsAndEmptyStringsWithoutSafeRead     , TestLabel@@ -4881,6 +5007,9 @@         "parseTextsAndEmptyStringsWithoutSafeRead"         parseTextsAndEmptyStringsWithoutSafeRead     , TestLabel+        "parseBoolsAndNullishStringsWithoutSafeRead"+        parseBoolsAndNullishStringsWithoutSafeRead+    , TestLabel         "parseIntsAndNullishStringsWithoutSafeRead"         parseIntsAndNullishStringsWithoutSafeRead     , TestLabel@@ -4894,6 +5023,9 @@         parseTextsAndEmptyAndNullishStringsWithoutSafeRead     , -- 3B. PARSING WITH SAFEREAD ON       TestLabel+        "parseBoolsAndEmptyStringsWithSafeRead"+        parseBoolsAndEmptyStringsWithSafeRead+    , TestLabel         "parseIntsAndEmptyStringsWithSafeRead"         parseIntsAndEmptyStringsWithSafeRead     , TestLabel@@ -4920,8 +5052,10 @@     , TestLabel         "parseTextsAndEmptyAndNullishStringsWithSafeRead"         parseTextsAndEmptyAndNullishStringsWithSafeRead-    , -- 4A. PARSING MUST NOT DEPEND ON THE NUMBER OF EXAMPLES-      TestLabel "parseIntsWithOneExample" parseIntsWithOneExample+    , -- 4. PARSING MUST NOT DEPEND ON THE NUMBER OF EXAMPLES+      TestLabel "parseBoolsWithOneExample" parseBoolsWithOneExample+    , TestLabel "parseBoolsWithManyExamples" parseBoolsWithManyExamples+    , TestLabel "parseIntsWithOneExample" parseIntsWithOneExample     , TestLabel "parseIntsWithTwentyFiveExamples" parseIntsWithTwentyFiveExamples     , TestLabel "parseIntsWithFortyNineExamples" parseIntsWithFortyNineExamples     , TestLabel "parseDatesWithOneExample" parseDatesWithOneExample
tests/Operations/Join.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}  module Operations.Join where @@ -63,18 +64,51 @@             (D.sortBy D.Ascending ["key"] (rightJoin ["key"] df2 df1))         ) +staffDf :: D.DataFrame+staffDf =+    D.fromRows+        ["Name", "Role"]+        [ [D.toAny @Text "Kelly", D.toAny @Text "Director of HR"]+        , [D.toAny @Text "Sally", D.toAny @Text "Course liasion"]+        , [D.toAny @Text "James", D.toAny @Text "Grader"]+        ]++studentDf :: D.DataFrame+studentDf =+    D.fromRows+        ["Name", "School"]+        [ [D.toAny @Text "James", D.toAny @Text "Business"]+        , [D.toAny @Text "Mike", D.toAny @Text "Law"]+        , [D.toAny @Text "Sally", D.toAny @Text "Engineering"]+        ]+ testFullOuterJoin :: Test testFullOuterJoin =     TestCase         ( assertEqual             "Test full outer join with single key"             ( D.fromNamedColumns-                [ ("key", D.fromList ["K0" :: Text, "K1", "K2", "K3", "K4", "K5"])-                , ("A", D.fromList ["A0" :: Text, "A1", "A2", "A3", "A4", "A5"])-                , ("B", D.fromList [Just "B0", Just "B1" :: Maybe Text, Just "B2"])+                [+                    ( "Name"+                    , D.fromList ["James" :: Text, "Kelly", "Mike", "Sally"]+                    )+                ,+                    ( "Role"+                    , D.fromList+                        [ Just "Grader" :: Maybe Text+                        , Just "Director of HR"+                        , Nothing+                        , Just "Course liasion"+                        ]+                    )+                ,+                    ( "School"+                    , D.fromList+                        [Just "Business" :: Maybe Text, Nothing, Just "Law", Just "Engineering"]+                    )                 ]             )-            (D.sortBy D.Ascending ["key"] (fullOuterJoin ["key"] df2 df1))+            (D.sortBy D.Ascending ["Name"] (fullOuterJoin ["Name"] studentDf staffDf))         )  tests :: [Test]@@ -82,4 +116,5 @@     [ TestLabel "innerJoin" testInnerJoin     , TestLabel "leftJoin" testLeftJoin     , TestLabel "rightJoin" testRightJoin+    , TestLabel "fullOuterJoin" testFullOuterJoin     ]
tests/Operations/Statistics.hs view
@@ -180,6 +180,23 @@             (print $ D.quantiles' (VU.fromList [5]) 4 (VU.fromList [1 :: Int, 2, 3, 4, 5]))         ) +summarizeOptional :: Test+summarizeOptional =+    TestCase+        ( assertEqual+            "Summarizes `Num a => Maybe a` column"+            3 -- The three columns should be Statistics, A, and B+            ( D.nColumns+                ( D.summarize+                    ( D.fromNamedColumns+                        [ ("A", D.fromList [1 :: Int, 2])+                        , ("B", D.fromList [Just (1 :: Int), Nothing])+                        ]+                    )+                )+            )+        )+ tests :: [Test] tests =     [ TestLabel "medianOfOddLengthDataSet" medianOfOddLengthDataSet@@ -202,4 +219,5 @@         interQuartileRangeOfEvenLengthDataSet     , TestLabel "wrongQuantileNumber" wrongQuantileNumber     , TestLabel "wrongQuantileIndex" wrongQuantileIndex+    , TestLabel "summarizeOptional" summarizeOptional     ]