dataframe 0.3.0.2 → 0.3.0.3
raw patch · 52 files changed
+6670/−4465 lines, 52 filesdep +bytestring-lexingdep ~granitedep ~randomdep ~textPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: bytestring-lexing
Dependency ranges changed: granite, random, text, time
API changes (from Hackage documentation)
- DataFrame: Bar' :: PlotType
- DataFrame: BoxPlot' :: PlotType
- DataFrame: ColumnInfo :: !Text -> !Int -> !Int -> !Int -> !Int -> !Text -> ColumnInfo
- DataFrame: DataFrame :: Vector Column -> Map Text Int -> (Int, Int) -> DataFrame
- DataFrame: Grouped :: DataFrame -> [Text] -> Vector Int -> Vector Int -> GroupedDataFrame
- DataFrame: Heatmap' :: PlotType
- DataFrame: Histogram' :: PlotType
- DataFrame: Line' :: PlotType
- DataFrame: Pie' :: PlotType
- DataFrame: RBoxed :: Rep
- DataFrame: ROptional :: Rep
- DataFrame: RUnboxed :: Rep
- DataFrame: Scatter' :: PlotType
- DataFrame: StackedBar' :: PlotType
- DataFrame: VarAcc :: !Int -> !Double -> !Double -> VarAcc
- DataFrame: [Apply] :: forall a b. (Columnable a, Columnable b) => Text -> (b -> a) -> Expr b -> Expr a
- DataFrame: [BinOp] :: forall c b a. (Columnable c, Columnable b, Columnable a) => Text -> (c -> b -> a) -> Expr c -> Expr b -> Expr a
- DataFrame: [BoxedColumn] :: forall a. Columnable a => Vector a -> Column
- DataFrame: [Col] :: forall a. Columnable a => Text -> Expr a
- DataFrame: [GeneralAggregate] :: forall a. Columnable a => Text -> Text -> (forall (v :: Type -> Type) b. (Vector v b, Columnable b) => v b -> a) -> Expr a
- DataFrame: [Lit] :: forall a. Columnable a => a -> Expr a
- DataFrame: [MBoxedColumn] :: forall a. Columnable a => IOVector a -> MutableColumn
- DataFrame: [MUnboxedColumn] :: forall a. (Columnable a, Unbox a) => IOVector a -> MutableColumn
- DataFrame: [NumericAggregate] :: forall a b. (Columnable a, Columnable b, Unbox a, Unbox b, Num a, Num b) => Text -> Text -> (Vector b -> a) -> Expr a
- DataFrame: [OptionalColumn] :: forall a. Columnable a => Vector (Maybe a) -> Column
- DataFrame: [ReductionAggregate] :: forall a. Columnable a => Text -> Text -> (forall a1. Columnable a1 => a1 -> a1 -> a1) -> Expr a
- DataFrame: [SFalse] :: SBool 'False
- DataFrame: [STrue] :: SBool 'True
- DataFrame: [TColumn] :: forall a. Columnable a => Column -> TypedColumn a
- DataFrame: [UnboxedColumn] :: forall a. (Columnable a, Unbox a) => Vector a -> Column
- DataFrame: [Value] :: forall a. Columnable' a => a -> Any
- DataFrame: [Wrap] :: forall a. Columnable a => Expr a -> UExpr
- DataFrame: [fullDataframe] :: GroupedDataFrame -> DataFrame
- DataFrame: [groupedColumns] :: GroupedDataFrame -> [Text]
- DataFrame: [nameOfColumn] :: ColumnInfo -> !Text
- DataFrame: [nonNullValues] :: ColumnInfo -> !Int
- DataFrame: [nullValues] :: ColumnInfo -> !Int
- DataFrame: [offsets] :: GroupedDataFrame -> Vector Int
- DataFrame: [partiallyParsedValues] :: ColumnInfo -> !Int
- DataFrame: [plotTitle] :: PlotConfig -> String
- DataFrame: [typeOfColumn] :: ColumnInfo -> !Text
- DataFrame: [uniqueValues] :: ColumnInfo -> !Int
- DataFrame: [valueIndices] :: GroupedDataFrame -> Vector Int
- DataFrame: _getColumnAsDouble :: Text -> DataFrame -> Maybe (Vector Double)
- DataFrame: applyStatistic :: (Vector Double -> Double) -> Text -> DataFrame -> Maybe Double
- DataFrame: applyStatistics :: (Vector Double -> Vector Double) -> Text -> DataFrame -> Maybe (Vector Double)
- DataFrame: asText :: DataFrame -> Bool -> Text
- DataFrame: atIndices :: Set Int -> Column -> Column
- DataFrame: atIndicesStable :: Vector Int -> Column -> Column
- DataFrame: changingPoints :: (Eq a, Unbox a) => Vector (Int, a) -> [Int]
- DataFrame: class ColumnifyRep (r :: Rep) a
- DataFrame: class SBoolI (b :: Bool)
- DataFrame: clip :: Int -> Int -> Int -> Int
- DataFrame: columnLength :: Column -> Int
- DataFrame: columnSize :: Text -> DataFrame -> Maybe Int
- DataFrame: columnTypeString :: Column -> String
- DataFrame: columnVersionString :: Column -> String
- DataFrame: computeVariance :: VarAcc -> Double
- DataFrame: concatColumns :: Column -> Column -> Maybe Column
- DataFrame: countRows :: Char -> FilePath -> IO Int
- DataFrame: data Any
- DataFrame: data ColumnInfo
- DataFrame: data MutableColumn
- DataFrame: data Rep
- DataFrame: data SBool (b :: Bool)
- DataFrame: data TypedColumn a
- DataFrame: data UExpr
- DataFrame: data VarAcc
- DataFrame: expandColumn :: Int -> Column -> Column
- DataFrame: field :: Char -> Parser Text
- DataFrame: fillColumns :: Int -> Char -> IOVector MutableColumn -> IOVector [(Int, Text)] -> Handle -> IO ()
- DataFrame: filterByVector :: forall a b v. (Vector v b, Columnable a, Columnable b) => Text -> v b -> (a -> Bool) -> DataFrame -> DataFrame
- DataFrame: findIndices :: Columnable a => (a -> Bool) -> Column -> Maybe (Vector Int)
- DataFrame: freezeColumn :: IOVector MutableColumn -> Vector [(Int, Text)] -> ReadOptions -> Int -> IO Column
- DataFrame: freezeColumn' :: [(Int, Text)] -> MutableColumn -> IO Column
- DataFrame: getColumn :: Text -> DataFrame -> Maybe Column
- DataFrame: getIndices :: Vector Int -> Vector a -> Vector a
- DataFrame: getIndicesUnboxed :: Unbox a => Vector Int -> Vector a -> Vector a
- DataFrame: getInitialDataVectors :: Int -> IOVector MutableColumn -> [Text] -> IO ()
- DataFrame: getRowAsText :: DataFrame -> Int -> [Text]
- DataFrame: hash' :: Columnable a => a -> Int
- DataFrame: headColumn :: Columnable a => Column -> Maybe a
- DataFrame: ifilterColumn :: Columnable a => (Int -> a -> Bool) -> Column -> Maybe Column
- DataFrame: ifoldlColumn :: (Columnable a, Columnable b) => (b -> Int -> a -> b) -> b -> Column -> Maybe b
- DataFrame: ifoldrColumn :: (Columnable a, Columnable b) => (Int -> a -> b -> b) -> b -> Column -> Maybe b
- DataFrame: imapColumn :: (Columnable b, Columnable c) => (Int -> b -> c) -> Column -> Maybe Column
- DataFrame: indexes :: Vector v a => (a -> Bool) -> v a -> Vector Int
- DataFrame: inferValueType :: Text -> Text
- DataFrame: interpret :: Columnable a => DataFrame -> Expr a -> TypedColumn a
- DataFrame: interpretAggregation :: Columnable a => GroupedDataFrame -> Expr a -> TypedColumn a
- DataFrame: isNullish :: Text -> Bool
- DataFrame: isOptional :: Column -> Bool
- DataFrame: leftExpandColumn :: Int -> Column -> Column
- DataFrame: lineEnd :: Parser ()
- DataFrame: mapColumn :: (Columnable b, Columnable c, UnboxIf c) => (b -> c) -> Column -> Maybe Column
- DataFrame: mean' :: Vector Double -> Double
- DataFrame: mkGroupedColumns :: Vector Int -> DataFrame -> DataFrame -> Text -> DataFrame
- DataFrame: mkRowFromArgs :: [Text] -> DataFrame -> Int -> Row
- DataFrame: mkRowRep :: [Int] -> DataFrame -> Int -> Int
- DataFrame: null :: DataFrame -> Bool
- DataFrame: nulls :: Column -> Int
- DataFrame: numElements :: Column -> Int
- DataFrame: parseDefault :: Bool -> Column -> Column
- DataFrame: parseDefaults :: Bool -> DataFrame -> DataFrame
- DataFrame: parseRow :: Char -> Parser [Text]
- DataFrame: parseSep :: Char -> Text -> [Text]
- DataFrame: partiallyParsed :: Column -> Int
- DataFrame: printSessionSchema :: DataFrame -> IO ()
- DataFrame: quickPlot :: HasCallStack => [Text] -> DataFrame -> IO ()
- DataFrame: quotedField :: Parser Text
- DataFrame: readByteStringInt :: HasCallStack => ByteString -> Maybe Int
- DataFrame: readDouble :: HasCallStack => Text -> Maybe Double
- DataFrame: readDoubleEither :: HasCallStack => Text -> Either Text Double
- DataFrame: readInt :: HasCallStack => Text -> Maybe Int
- DataFrame: readIntEither :: HasCallStack => Text -> Either Text Int
- DataFrame: readInteger :: HasCallStack => Text -> Maybe Integer
- DataFrame: readIntegerEither :: HasCallStack => Text -> Either Text Integer
- DataFrame: readValue :: (HasCallStack, Read a) => Text -> a
- DataFrame: readWithDefault :: (HasCallStack, Read a) => a -> Text -> a
- DataFrame: record :: Char -> Parser [Text]
- DataFrame: reduceColumn :: Columnable a => (a -> b) -> Column -> Maybe b
- DataFrame: renameSafe :: Text -> Text -> DataFrame -> Either DataFrameException DataFrame
- DataFrame: roundTo :: Int -> Double -> Double
- DataFrame: sNumeric :: SBoolI (Numeric a) => SBool (Numeric a)
- DataFrame: sUnbox :: SBoolI (Unboxable a) => SBool (Unboxable a)
- DataFrame: safeReadValue :: Read a => Text -> Maybe a
- DataFrame: sbool :: SBoolI b => SBool b
- DataFrame: selectIndices :: Vector Int -> DataFrame -> DataFrame
- DataFrame: showValue :: Columnable' a => a -> Text
- DataFrame: sliceColumn :: Int -> Int -> Column -> Column
- DataFrame: sortedIndexes :: Bool -> Column -> Vector Int
- DataFrame: sortedIndexes' :: Bool -> Vector Row -> Vector Int
- DataFrame: step :: VarAcc -> Double -> VarAcc
- DataFrame: takeColumn :: Int -> Column -> Column
- DataFrame: takeLastColumn :: Int -> Column -> Column
- DataFrame: toAny :: Columnable' a => a -> Any
- DataFrame: toColumnRep :: ColumnifyRep r a => Vector a -> Column
- DataFrame: toPct2dp :: Double -> String
- DataFrame: toRowList :: [Text] -> DataFrame -> [Row]
- DataFrame: toRowVector :: [Text] -> DataFrame -> Vector Row
- DataFrame: toVectorSafe :: forall a v. (Vector v a, Columnable a) => Column -> Either DataFrameException (v a)
- DataFrame: type Columnable a = (Columnable' a, ColumnifyRep KindOf a a, UnboxIf a, SBoolI Unboxable a, SBoolI Numeric a)
- DataFrame: type Columnable' a = (Typeable a, Show a, Ord a, Eq a, Read a)
- DataFrame: type Row = Vector Any
- DataFrame: type UnboxIf a = When Unboxable a Unbox a
- DataFrame: type family When (flag :: Bool) c
- DataFrame: unquotedField :: Char -> Parser Text
- DataFrame: unquotedTerminators :: Char -> Set Char
- DataFrame: unsafeGetColumn :: Text -> DataFrame -> Column
- DataFrame: unwrapTypedColumn :: TypedColumn a -> Column
- DataFrame: variance' :: Vector Double -> Double
- DataFrame: writeColumn :: Int -> Text -> MutableColumn -> IO (Either Text Bool)
- DataFrame: writeCsv :: String -> DataFrame -> IO ()
- DataFrame: writeSeparated :: Char -> String -> DataFrame -> IO ()
- DataFrame: writeValue :: IOVector MutableColumn -> IOVector [(Int, Text)] -> Int -> Int -> Text -> IO ()
- DataFrame: zipColumns :: Column -> Column -> Column
- DataFrame: zipWithColumns :: (Columnable a, Columnable b, Columnable c) => (a -> b -> c) -> Column -> Column -> Maybe Column
+ DataFrame: Bar :: PlotType
+ DataFrame: BoxPlot :: PlotType
+ DataFrame: Heatmap :: PlotType
+ DataFrame: Histogram :: PlotType
+ DataFrame: Line :: PlotType
+ DataFrame: Pie :: PlotType
+ DataFrame: Scatter :: PlotType
+ DataFrame: StackedBar :: PlotType
+ DataFrame: [EmptyDataSetException] :: Text -> DataFrameException
+ DataFrame: [chunkSize] :: ReadOptions -> Int
+ DataFrame: emptyDataSetError :: Text -> String
+ DataFrame: plotGroupedBarsWithN :: HasCallStack => Int -> Text -> Text -> PlotConfig -> DataFrame -> IO ()
+ DataFrame: plotScatterBy :: HasCallStack => Text -> Text -> Text -> DataFrame -> IO ()
+ DataFrame: plotScatterByWith :: HasCallStack => Text -> Text -> Text -> PlotConfig -> DataFrame -> IO ()
- DataFrame: PlotConfig :: PlotType -> String -> Plot -> PlotConfig
+ DataFrame: PlotConfig :: PlotType -> Plot -> PlotConfig
- DataFrame: ReadOptions :: Bool -> Bool -> Bool -> ReadOptions
+ DataFrame: ReadOptions :: Bool -> Bool -> Bool -> Int -> ReadOptions
- DataFrame: extractStringColumn :: HasCallStack => Text -> DataFrame -> [String]
+ DataFrame: extractStringColumn :: HasCallStack => Text -> DataFrame -> [Text]
- DataFrame: getCategoricalCounts :: HasCallStack => Text -> DataFrame -> Maybe [(String, Double)]
+ DataFrame: getCategoricalCounts :: HasCallStack => Text -> DataFrame -> Maybe [(Text, Double)]
- DataFrame: groupWithOther :: Int -> [(String, Double)] -> [(String, Double)]
+ DataFrame: groupWithOther :: Int -> [(Text, Double)] -> [(Text, Double)]
- DataFrame: groupWithOtherForPie :: Int -> [(String, Double)] -> [(String, Double)]
+ DataFrame: groupWithOtherForPie :: Int -> [(Text, Double)] -> [(Text, Double)]
- DataFrame: plotLines :: HasCallStack => [Text] -> DataFrame -> IO ()
+ DataFrame: plotLines :: HasCallStack => Text -> [Text] -> DataFrame -> IO ()
- DataFrame: plotLinesWith :: HasCallStack => [Text] -> PlotConfig -> DataFrame -> IO ()
+ DataFrame: plotLinesWith :: HasCallStack => Text -> [Text] -> PlotConfig -> DataFrame -> IO ()
Files
- CHANGELOG.md +6/−0
- README.md +20/−1
- app/Main.hs +6/−5
- benchmark/Main.hs +34/−28
- dataframe.cabal +78/−15
- src/DataFrame.hs +254/−36
- src/DataFrame/Display/Terminal/Plot.hs +638/−561
- src/DataFrame/Display/Terminal/PrettyPrint.hs +16/−14
- src/DataFrame/Errors.hs +70/−58
- src/DataFrame/Functions.hs +107/−100
- src/DataFrame/IO/CSV.hs +344/−210
- src/DataFrame/IO/Parquet.hs +250/−1560
- src/DataFrame/IO/Parquet/Binary.hs +130/−0
- src/DataFrame/IO/Parquet/ColumnStatistics.hs +10/−10
- src/DataFrame/IO/Parquet/Compression.hs +10/−10
- src/DataFrame/IO/Parquet/Dictionary.hs +176/−0
- src/DataFrame/IO/Parquet/Encoding.hs +94/−24
- src/DataFrame/IO/Parquet/Levels.hs +106/−0
- src/DataFrame/IO/Parquet/Page.hs +336/−13
- src/DataFrame/IO/Parquet/Thrift.hs +774/−0
- src/DataFrame/IO/Parquet/Time.hs +56/−0
- src/DataFrame/IO/Parquet/Types.hs +278/−10
- src/DataFrame/Internal/Column.hs +285/−244
- src/DataFrame/Internal/DataFrame.hs +91/−68
- src/DataFrame/Internal/Expression.hs +255/−175
- src/DataFrame/Internal/Parsing.hs +35/−26
- src/DataFrame/Internal/Row.hs +55/−50
- src/DataFrame/Internal/Schema.hs +15/−13
- src/DataFrame/Internal/Types.hs +56/−54
- src/DataFrame/Lazy/IO/CSV.hs +169/−133
- src/DataFrame/Lazy/Internal/DataFrame.hs +60/−47
- src/DataFrame/Operations/Aggregation.hs +80/−64
- src/DataFrame/Operations/Core.hs +462/−157
- src/DataFrame/Operations/Join.hs +35/−24
- src/DataFrame/Operations/Merge.hs +20/−13
- src/DataFrame/Operations/Sorting.hs +20/−16
- src/DataFrame/Operations/Statistics.hs +175/−89
- src/DataFrame/Operations/Subset.hs +124/−99
- src/DataFrame/Operations/Transformations.hs +141/−121
- src/DataFrame/Operations/Typing.hs +62/−51
- tests/Assertions.hs +20/−11
- tests/Functions.hs +51/−31
- tests/Main.hs +54/−36
- tests/Operations/Apply.hs +157/−76
- tests/Operations/Derive.hs +28/−16
- tests/Operations/Filter.hs +73/−42
- tests/Operations/GroupBy.hs +40/−24
- tests/Operations/InsertColumn.hs +129/−76
- tests/Operations/Sort.hs +50/−28
- tests/Operations/Statistics.hs +47/−0
- tests/Operations/Take.hs +24/−18
- tests/Parquet.hs +64/−8
CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for dataframe +## 0.3.0.3+* Improved parquet reader. The reader now supports most parquet files found in the wild.+* Improve CSV parsing: Parse bytestring and convert to text only at the end. Remove some redundancies in parsing with suggestions from @Jhingon.+* Faster correlation computation.+* Update version of granite that ships with dataframe and add new scatterBy plot.+ ## 0.3.0.2 * Re-enable Parquet. * Change columnInfo to describeColumns
README.md view
@@ -117,7 +117,26 @@ ## Supported input formats * CSV-* Apache Parquet (still buggy and experimental)+* Apache Parquet+ * Supports all primitive parquet types plain and uncompressed.+ * Can decode both v1 and v2 data pages.+ * Supports Snappy and ZSTD compression.+ * Supports RLE/bitpacking encoding for primitive types+ * Backward compatible with INT96 type.+ * From the parquet-testing repo we can successfully read the following:+ * alltypes_dictionary.parquet+ * alltypes_plain.parquet+ * alltypes_plain.snappy.parquet+ * alltypes_tiny_pages_plain.parquet+ * binary_truncated_min_max.parquet+ * datapage_v1-corrupt-checksum.parquet+ * datapage_v1-snappy-compressed-checksum.parquet+ * datapage_v1-uncompressed-checksum.parquet+ * We will continue adding functionality/coverage as needed.+++## Supported output formats+* CSV ## Future work * Apache arrow compatability
app/Main.hs view
@@ -1,17 +1,18 @@---- Useful Haskell extensions.-{-# LANGUAGE OverloadedStrings #-} -- Allow string literal to be interpreted as any other string type.-{-# LANGUAGE TypeApplications #-} -- Convenience syntax for specifiying the type `sum a b :: Int` vs `sum @Int a b'. {-# LANGUAGE NumericUnderscores #-}+-- Useful Haskell extensions.+-- Allow string literal to be interpreted as any other string type.+{-# LANGUAGE OverloadedStrings #-}+-- Convenience syntax for specifiying the type `sum a b :: Int` vs `sum @Int a b'.+{-# LANGUAGE TypeApplications #-} import qualified DataFrame as D -- import for general functionality. import qualified DataFrame.Functions as F -- import for column expressions. import DataFrame ((|>)) -- import chaining operator with unqualified. -import qualified Data.Vector.Unboxed as VU import Control.Monad (replicateM) import Data.Time+import qualified Data.Vector.Unboxed as VU import System.Random.Stateful main :: IO ()
benchmark/Main.hs view
@@ -2,59 +2,65 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} -import qualified DataFrame as D-import qualified DataFrame.Functions as F import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Unboxed.Mutable as VUM+import qualified DataFrame as D+import qualified DataFrame.Functions as F import Control.Monad (replicateM) import Criterion.Main-import DataFrame ((|>)) import Data.Time+import DataFrame ((|>)) import System.Process import System.Random.Stateful haskell :: IO () haskell = do- output <- readProcess "cabal" ["run", "dataframe"] ""- putStrLn output+ output <- readProcess "cabal" ["run", "dataframe", "-O2"] ""+ putStrLn output polars :: IO () polars = do- output <- readProcess "./benchmark/dataframe_benchmark/bin/python3" ["./benchmark/polars/polars_benchmark.py"] ""- putStrLn output+ output <- readProcess "./benchmark/dataframe_benchmark/bin/python3" ["./benchmark/polars/polars_benchmark.py"] ""+ putStrLn output pandas :: IO () pandas = do- output <- readProcess "./benchmark/dataframe_benchmark/bin/python3" ["./benchmark/pandas/pandas_benchmark.py"] ""- putStrLn output+ output <- readProcess "./benchmark/dataframe_benchmark/bin/python3" ["./benchmark/pandas/pandas_benchmark.py"] ""+ putStrLn output groupByHaskell :: IO () groupByHaskell = do- df <- D.readCsv "./data/housing.csv"- print $ df |> D.groupBy ["ocean_proximity"]- |> D.aggregate [ (F.minimum (F.col @Double "median_house_value")) `F.as` "minimum_median_house_value"- , (F.maximum (F.col @Double "median_house_value")) `F.as` "maximum_median_house_value"]+ df <- D.readCsv "./data/housing.csv"+ print $+ df+ |> D.groupBy ["ocean_proximity"]+ |> D.aggregate+ [ (F.minimum (F.col @Double "median_house_value")) `F.as` "minimum_median_house_value"+ , (F.maximum (F.col @Double "median_house_value")) `F.as` "maximum_median_house_value"+ ] groupByPolars :: IO () groupByPolars = do- output <- readProcess "./benchmark/dataframe_benchmark/bin/python3" ["./benchmark/polars/group_by.py"] ""- putStrLn output+ output <- readProcess "./benchmark/dataframe_benchmark/bin/python3" ["./benchmark/polars/group_by.py"] ""+ putStrLn output groupByPandas :: IO () groupByPandas = do- output <- readProcess "./benchmark/dataframe_benchmark/bin/python3" ["./benchmark/pandas/group_by.py"] ""- putStrLn output+ output <- readProcess "./benchmark/dataframe_benchmark/bin/python3" ["./benchmark/pandas/group_by.py"] ""+ putStrLn output main = do- output <- readProcess "cabal" ["build", "-O2"] ""- putStrLn output- defaultMain [- bgroup "stats" [ bench "simpleStatsHaskell" $ nfIO haskell- , bench "simpleStatsPandas" $ nfIO pandas- , bench "simpleStatsPolars" $ nfIO polars- , bench "groupByHaskell" $ nfIO groupByHaskell- , bench "groupByPolars" $ nfIO groupByPolars- , bench "groupByPandas" $ nfIO groupByPandas- ]- ]+ output <- readProcess "cabal" ["build", "-O2"] ""+ putStrLn output+ defaultMain+ [ bgroup+ "stats"+ [ bench "simpleStatsHaskell" $ nfIO haskell+ , bench "simpleStatsPandas" $ nfIO pandas+ , bench "simpleStatsPolars" $ nfIO polars+ , bench "groupByHaskell" $ nfIO groupByHaskell+ , bench "groupByPolars" $ nfIO groupByPolars+ , bench "groupByPandas" $ nfIO groupByPandas+ ]+ ]
dataframe.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: dataframe-version: 0.3.0.2+version: 0.3.0.3 synopsis: A fast, safe, and intuitive DataFrame library. @@ -22,7 +22,7 @@ location: https://github.com/mchav/dataframe library- -- default-extensions: StrictData+ default-extensions: Strict exposed-modules: DataFrame, DataFrame.Lazy, DataFrame.Functions@@ -48,10 +48,15 @@ DataFrame.Display.Terminal.Plot, DataFrame.IO.CSV, DataFrame.IO.Parquet,+ DataFrame.IO.Parquet.Binary+ DataFrame.IO.Parquet.Dictionary+ DataFrame.IO.Parquet.Levels+ DataFrame.IO.Parquet.Thrift DataFrame.IO.Parquet.ColumnStatistics, DataFrame.IO.Parquet.Compression, DataFrame.IO.Parquet.Encoding, DataFrame.IO.Parquet.Page,+ DataFrame.IO.Parquet.Time, DataFrame.IO.Parquet.Types, DataFrame.Lazy.IO.CSV, DataFrame.Lazy.Internal.DataFrame@@ -59,10 +64,11 @@ array ^>= 0.5, attoparsec >= 0.12 && <= 0.14.4, bytestring >= 0.11 && <= 0.12.2.0,+ bytestring-lexing >= 0.5 && < 0.6, containers >= 0.6.7 && < 0.8, directory >= 1.3.0.0 && <= 1.3.9.0, filepath >= 1.0.0.0 && <= 1.5.4.0,- granite ^>= 0.1.0.2,+ granite ^>= 0.2, hashable >= 1.2 && <= 1.5.0.0, snappy-hs ^>= 0.1, statistics >= 0.16.2.1 && <= 0.16.3.0,@@ -97,10 +103,15 @@ DataFrame.Display.Terminal.Plot, DataFrame.IO.CSV, DataFrame.IO.Parquet,+ DataFrame.IO.Parquet.Binary+ DataFrame.IO.Parquet.Dictionary+ DataFrame.IO.Parquet.Levels+ DataFrame.IO.Parquet.Thrift DataFrame.IO.Parquet.ColumnStatistics, DataFrame.IO.Parquet.Compression, DataFrame.IO.Parquet.Encoding, DataFrame.IO.Parquet.Page,+ DataFrame.IO.Parquet.Time, DataFrame.IO.Parquet.Types, DataFrame.Operations.Join, DataFrame.Operations.Merge,@@ -110,9 +121,10 @@ array ^>= 0.5, attoparsec >= 0.12 && <= 0.14.4, bytestring >= 0.11 && <= 0.12.2.0,+ bytestring-lexing >= 0.5 && < 0.6, containers >= 0.6.7 && < 0.8, directory >= 1.3.0.0 && <= 1.3.9.0,- granite ^>= 0.1.0.2,+ granite ^>= 0.2, hashable >= 1.2 && <= 1.5.0.0, random >= 1 && <= 1.3.1, snappy-hs ^>= 0.1,@@ -152,16 +164,67 @@ Operations.Filter, Operations.GroupBy, Operations.InsertColumn,- Operations.Sort,+ Operations.Sort, + Operations.Statistics, Operations.Take,- Parquet- build-depends: base >= 4.17.2.0 && < 4.22,- HUnit ^>= 1.6,- random >= 1,- random-shuffle >= 0.0.4,- text >= 2.0,- time >= 1.12,- vector ^>= 0.13,- dataframe- hs-source-dirs: tests+ Parquet,++ -- Dataframe modules.+ DataFrame,+ DataFrame.Internal.Types,+ DataFrame.Internal.Expression,+ DataFrame.Internal.Parsing,+ DataFrame.Internal.Column,+ DataFrame.Display.Terminal.PrettyPrint,+ DataFrame.Display.Terminal.Colours,+ DataFrame.Internal.DataFrame,+ DataFrame.Internal.Row,+ DataFrame.Errors,+ DataFrame.Operations.Core,+ DataFrame.Operations.Subset,+ DataFrame.Operations.Sorting,+ DataFrame.Operations.Statistics,+ DataFrame.Operations.Transformations,+ DataFrame.Operations.Typing,+ DataFrame.Operations.Aggregation,+ DataFrame.Display.Terminal.Plot,+ DataFrame.IO.CSV,+ DataFrame.IO.Parquet,+ DataFrame.IO.Parquet.Binary+ DataFrame.IO.Parquet.Dictionary+ DataFrame.IO.Parquet.Levels+ DataFrame.IO.Parquet.Thrift+ DataFrame.IO.Parquet.ColumnStatistics,+ DataFrame.IO.Parquet.Compression,+ DataFrame.IO.Parquet.Encoding,+ DataFrame.IO.Parquet.Page,+ DataFrame.IO.Parquet.Time,+ DataFrame.IO.Parquet.Types,+ DataFrame.Operations.Join,+ DataFrame.Operations.Merge,+ DataFrame.Lazy.IO.CSV,+ DataFrame.Functions+ build-depends: base >= 4.17.2.0 && < 4.22,+ array ^>= 0.5,+ attoparsec >= 0.12 && <= 0.14.4,+ bytestring >= 0.11 && <= 0.12.2.0,+ bytestring-lexing >= 0.5 && < 0.6,+ containers >= 0.6.7 && < 0.8,+ directory >= 1.3.0.0 && <= 1.3.9.0,+ granite ^>= 0.2,+ hashable >= 1.2 && <= 1.5.0.0,+ HUnit ^>= 1.6,+ random >= 1,+ random-shuffle >= 0.0.4,+ random >= 1 && <= 1.3.1,+ snappy-hs ^>= 0.1,+ statistics >= 0.16.2.1 && <= 0.16.3.0,+ template-haskell >= 2.0 && <= 2.30,+ text >= 2.0 && <= 2.1.2,+ time >= 1.12 && <= 1.14,+ vector ^>= 0.13,+ vector-algorithms ^>= 0.9,+ zstd >= 0.1.2.0 && <= 0.1.3.0+ hs-source-dirs: tests,+ src default-language: Haskell2010
src/DataFrame.hs view
@@ -1,30 +1,261 @@ {-# LANGUAGE OverloadedStrings #-}-module DataFrame- ( module D,++{- |+Module : DataFrame+Copyright : (c) 2025+License : GPL-3.0+Maintainer : mschavinda@gmail.com+Stability : experimental+Portability : POSIX++Batteries-included entry point for the DataFrame library.++This module re-exports the most commonly used pieces of the @dataframe@ library so you+can get productive fast in GHCi, IHaskell, or scripts.++__Naming convention__+* Use the @D.@ (\"DataFrame\") prefix for core table operations.+* Use the @F.@ (\"Functions\") prefix for the expression DSL (columns, math, aggregations).++Example session:++@+-- GHCi quality-of-life:+ghci> :set -XOverloadedStrings -XTypeApplications+ghci> :module + DataFrame as D, DataFrame.Functions as F, Data.Text (Text)+@++= Quick start+Load a CSV, select a few columns, filter, derive a column, then group + aggregate:++@+-- 1) Load data+ghci> df0 <- D.readCsv "data/housing.csv"+ghci> D.describeColumns df0+--------------------------------------------------------------------------------------------------------------------+index | Column Name | # Non-null Values | # Null Values | # Partially parsed | # Unique Values | Type+------|--------------------|-------------------|---------------|--------------------|-----------------|-------------+ Int | Text | Int | Int | Int | Int | Text+------|--------------------|-------------------|---------------|--------------------|-----------------|-------------+0 | ocean_proximity | 20640 | 0 | 0 | 5 | Text+1 | median_house_value | 20640 | 0 | 0 | 3842 | Double+2 | median_income | 20640 | 0 | 0 | 12928 | Double+3 | households | 20640 | 0 | 0 | 1815 | Double+4 | population | 20640 | 0 | 0 | 3888 | Double+5 | total_bedrooms | 20640 | 0 | 0 | 1924 | Maybe Double+6 | total_rooms | 20640 | 0 | 0 | 5926 | Double+7 | housing_median_age | 20640 | 0 | 0 | 52 | Double+8 | latitude | 20640 | 0 | 0 | 862 | Double+9 | longitude | 20640 | 0 | 0 | 844 | Double++-- 2) Project & filter+ghci> let df1 = df1 = D.filter @Text "ocean_proximity" (== "ISLAND") df0 D.|> D.select ["median_house_value", "median_income", "ocean_proximity"]++-- 3) Add a derived column using the expression DSL+-- (col types are explicit via TypeApplications)+ghci> df2 = D.derive "rooms_per_household" (F.col @Double "total_rooms" / F.col @Double "households") df0++-- 4) Group + aggregate+ghci> let grouped = D.groupBy ["ocean_proximity"] df0+ghci> let summary =+ D.aggregate+ [ F.maximum (F.col @Double "median_house_value") `F.as` "max_house_value"]+ grouped+ghci> D.take 5 summary+-----------------------------------------+index | ocean_proximity | max_house_value+------|-----------------|----------------+ Int | Text | Double+------|-----------------|----------------+0 | <1H OCEAN | 500001.0+1 | INLAND | 500001.0+2 | ISLAND | 450000.0+3 | NEAR BAY | 500001.0+4 | NEAR OCEAN | 500001.0+@++== Simple operations (cheat sheet)+Most users only need a handful of verbs:++__I/O__++ * @D.readCsv :: FilePath -> IO DataFrame@+ * @D.writeCsv :: FilePath -> DataFrame -> IO ()@+ * @D.readParquet :: FilePath -> IO DataFrame@++__Exploration__++ * @D.take :: Int -> DataFrame -> DataFrame@+ * @D.takeLast :: Int -> DataFrame -> DataFrame@+ * @D.describeColumns :: DataFrame -> DataFrame@+ * @D.summarize :: DataFrame -> DataFrame@++__Row ops__++ * @D.filter :: Columnable a => Text -> (a -> Bool) -> DataFrame -> DataFrame@+ * @D.sortBy :: SortOrder -> [Text] -> DataFrame -> DataFrame@++__Column ops__++ * @D.select :: [Text] -> DataFrame -> DataFrame@+ * @D.exclude :: [Text] -> DataFrame -> DataFrame@+ * @D.rename :: [(Text,Text)] -> DataFrame -> DataFrame@+ * @D.derive :: Text -> D.Expr a -> DataFrame -> DataFrame@++__Group & aggregate__++ * @D.groupBy :: [Text] -> DataFrame -> GroupedDataFrame@+ * @D.aggregate :: [(Text, F.UExpr)] -> GroupedDataFrame -> DataFrame@++__Joins__++ * @D.innerJoin / D.leftJoin / D.rightJoin / D.fullJoin@++== Expression DSL (F.*) at a glance+Columns (typed):++@+F.col @Text "ocean_proximity"+F.col @Double "total_rooms"+F.lit @Double 1.0+@++Math & comparisons (overloaded by type):++@+(+), (-), (*), (/), abs, log, exp, round+(F.eq), (F.gt), (F.geq), (F.lt), (F.leq)+@++Aggregations (for 'D.aggregate'):++@+F.count @a (F.col @a "c")+F.sum @Double (F.col @Double "x")+F.mean @Double (F.col @Double "x")+F.min @t (F.col @t "x")+F.max @t (F.col @t "x")+@++== REPL power-tool: ':exposeColumns'++Use @:exposeColumns <df>@ in GHCi/IHaskell to turn each column of a bound 'DataFrame'+into a local binding with the same (mangled if needed) name and the column's concrete+vector type. This is great for quick ad-hoc analysis, plotting, or hand-rolled checks.++@+-- Suppose df has columns: "passengers" :: Int, "fare" :: Double, "payment" :: Text+ghci> :set -XTemplateHaskell+ghci> :exposeColumns df++-- Now you have in scope:+ghci> :type passengers+passengers :: Expr Int++ghci> :type fare+fare :: Expr Double++ghci> :type payment+payment :: Expr Text++-- You can use them directly:+ghci> D.derive "fare_with_tip" (fare * F.lit 1.2)+@++Notes:++* Name mangling: spaces and non-identifier characters are replaced (e.g. @"trip id"@ -> @trip_id@).+* Optional/nullable columns are exposed as @Expr (Maybe a)@.+-}+module DataFrame (+ -- * Core data structures+ module Dataframe,+ module Column,+ module Expression,++ -- * Core dataframe operations+ module Core,++ -- * I/O+ module CSV,+ module Parquet,++ -- * Operations+ module Subset,+ module Transformations,+ module Aggregation,+ module Sorting,+ module Merge,+ module Join,+ module Statistics,++ -- * Errors+ module Errors,++ -- * Plotting+ module Plot,++ -- * Convenience functions (|>),- printSessionSchema- )+) where -import DataFrame.Internal.Types as D-import DataFrame.Internal.Expression as D-import DataFrame.Internal.Parsing as D-import DataFrame.Internal.Column as D-import DataFrame.Internal.DataFrame as D hiding (columnIndices, columns)-import DataFrame.Internal.Row as D hiding (mkRowRep)-import DataFrame.Errors as D-import DataFrame.Operations.Core as D-import DataFrame.Operations.Merge as D-import DataFrame.Operations.Join as D-import DataFrame.Operations.Subset as D-import DataFrame.Operations.Sorting as D-import DataFrame.Operations.Statistics as D-import DataFrame.Operations.Transformations as D-import DataFrame.Operations.Typing as D-import DataFrame.Operations.Aggregation as D-import DataFrame.Display.Terminal.Plot as D-import DataFrame.IO.CSV as D-import DataFrame.IO.Parquet as D+import DataFrame.Display.Terminal.Plot as Plot+import DataFrame.Errors as Errors+import DataFrame.IO.CSV as CSV (ReadOptions (..), defaultOptions, readCsv, readSeparated, readTsv)+import DataFrame.IO.Parquet as Parquet (readParquet)+import DataFrame.Internal.Column as Column (+ Column,+ fromList,+ fromUnboxedVector,+ fromVector,+ toList,+ toVector,+ )+import DataFrame.Internal.DataFrame as Dataframe (+ DataFrame,+ GroupedDataFrame,+ columnAsVector,+ empty,+ toMatrix,+ )+import DataFrame.Internal.Expression as Expression (Expr)+import DataFrame.Operations.Aggregation as Aggregation (aggregate, distinct, groupBy)+import DataFrame.Operations.Core as Core hiding (ColumnInfo (..), nulls, partiallyParsed, renameSafe)+import DataFrame.Operations.Join as Join+import DataFrame.Operations.Merge as Merge+import DataFrame.Operations.Sorting as Sorting+import DataFrame.Operations.Statistics as Statistics (+ correlation,+ frequencies,+ interQuartileRange,+ mean,+ median,+ skewness,+ standardDeviation,+ sum,+ summarize,+ variance,+ )+import DataFrame.Operations.Subset as Subset (+ cube,+ drop,+ dropLast,+ exclude,+ filter,+ filterAllJust,+ filterBy,+ filterJust,+ filterWhere,+ range,+ select,+ selectBy,+ selectIntRange,+ selectRange,+ take,+ takeLast,+ )+import DataFrame.Operations.Transformations as Transformations import Data.Function import Data.List@@ -32,16 +263,3 @@ import qualified Data.Text.IO as T (|>) = (&)--printSessionSchema :: D.DataFrame -> IO ()-printSessionSchema df = T.putStrLn $ let- (lhs, rhs) = foldr go ([], []) (D.columnNames df)- columnRep name = let- colType = T.pack (D.columnTypeString (D.unsafeGetColumn name df))- in "F.col @(" <> colType <> ") \"" <> name <> "\""- go name (l, r) = (T.toLower name:l, columnRep name:r)- in T.unlines [":{", "{-# LANGUAGE TypeApplications #-}",- "import qualified DataFrame.Functions as F",- "import Data.Text (Text)",- "(" <> T.intercalate "," lhs <> ")" <> " = " <> "(" <> T.intercalate "," rhs <> ")",- ":}"]
src/DataFrame/Display/Terminal/Plot.hs view
@@ -1,563 +1,640 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE GADTs #-}--module DataFrame.Display.Terminal.Plot where--import Control.Monad-import qualified Data.List as L-import qualified Data.Map as M-import qualified Data.Text as T-import qualified Data.Vector as V-import qualified Data.Vector.Generic as VG-import qualified Data.Vector.Unboxed as VU-import Data.Maybe (fromMaybe, catMaybes)-import Data.Typeable (Typeable)-import Data.Type.Equality (type (:~:)(Refl), TestEquality(testEquality))-import Type.Reflection (typeRep)-import GHC.Stack (HasCallStack)--import DataFrame.Internal.Column (Column(..), Columnable)-import DataFrame.Internal.DataFrame (DataFrame(..))-import DataFrame.Operations.Core-import Granite--data PlotConfig = PlotConfig- { plotType :: PlotType- , plotTitle :: String- , plotSettings :: Plot- }--data PlotType - = Histogram'- | Scatter'- | Line'- | Bar'- | BoxPlot'- | Pie'- | StackedBar'- | Heatmap'- deriving (Eq, Show)--defaultPlotConfig :: PlotType -> PlotConfig-defaultPlotConfig ptype = PlotConfig- { plotType = ptype- , plotTitle = ""- , plotSettings = defPlot- }--plotHistogram :: HasCallStack => T.Text -> DataFrame -> IO ()-plotHistogram colName df = plotHistogramWith colName (defaultPlotConfig Histogram') df--plotHistogramWith :: HasCallStack => T.Text -> PlotConfig -> DataFrame -> IO ()-plotHistogramWith colName config df = do- let values = extractNumericColumn colName df- (minVal, maxVal) = if null values then (0, 1) else (minimum values, maximum values)- putStrLn $ histogram (plotTitle config) (bins 30 minVal maxVal) values (plotSettings config)--plotScatter :: HasCallStack => T.Text -> T.Text -> DataFrame -> IO ()-plotScatter xCol yCol df = plotScatterWith xCol yCol (defaultPlotConfig Scatter') df--plotScatterWith :: HasCallStack => T.Text -> T.Text -> PlotConfig -> DataFrame -> IO ()-plotScatterWith xCol yCol config df = do- let xVals = extractNumericColumn xCol df- yVals = extractNumericColumn yCol df- points = zip xVals yVals- putStrLn $ scatter (plotTitle config) [(T.unpack xCol ++ " vs " ++ T.unpack yCol, points)] (plotSettings config)--plotLines :: HasCallStack => [T.Text] -> DataFrame -> IO ()-plotLines colNames df = plotLinesWith colNames (defaultPlotConfig Line') df--plotLinesWith :: HasCallStack => [T.Text] -> PlotConfig -> DataFrame -> IO ()-plotLinesWith colNames config df = do- seriesData <- forM colNames $ \col -> do- let values = extractNumericColumn col df- indices = map fromIntegral [0..length values - 1]- return (T.unpack col, zip indices values)- putStrLn $ lineGraph (plotTitle config) seriesData (plotSettings config)--plotBoxPlots :: HasCallStack => [T.Text] -> DataFrame -> IO ()-plotBoxPlots colNames df = plotBoxPlotsWith colNames (defaultPlotConfig BoxPlot') df--plotBoxPlotsWith :: HasCallStack => [T.Text] -> PlotConfig -> DataFrame -> IO ()-plotBoxPlotsWith colNames config df = do- boxData <- forM colNames $ \col -> do- let values = extractNumericColumn col df- return (T.unpack col, values)- putStrLn $ boxPlot (plotTitle config) boxData (plotSettings config)--plotStackedBars :: HasCallStack => T.Text -> [T.Text] -> DataFrame -> IO ()-plotStackedBars categoryCol valueColumns df = - plotStackedBarsWith categoryCol valueColumns (defaultPlotConfig StackedBar') df--plotStackedBarsWith :: HasCallStack => T.Text -> [T.Text] -> PlotConfig -> DataFrame -> IO ()-plotStackedBarsWith categoryCol valueColumns config df = do- let categories = extractStringColumn categoryCol df- uniqueCategories = L.nub categories- - stackData <- forM uniqueCategories $ \cat -> do- let indices = [i | (i, c) <- zip [0..] categories, c == cat]- seriesData <- forM valueColumns $ \col -> do- let allValues = extractNumericColumn col df- values = [allValues !! i | i <- indices, i < length allValues]- return (T.unpack col, sum values)- return (cat, seriesData)- - putStrLn $ stackedBars (plotTitle config) stackData (plotSettings config)--plotHeatmap :: HasCallStack => DataFrame -> IO ()-plotHeatmap df = plotHeatmapWith (defaultPlotConfig Heatmap') df--plotHeatmapWith :: HasCallStack => PlotConfig -> DataFrame -> IO ()-plotHeatmapWith config df = do- let numericCols = filter (isNumericColumn df) (columnNames df)- matrix = map (\col -> extractNumericColumn col df) numericCols- putStrLn $ heatmap (plotTitle config) matrix (plotSettings config)--isNumericColumn :: DataFrame -> T.Text -> Bool-isNumericColumn df colName = - case M.lookup colName (columnIndices df) of- Nothing -> False- Just idx -> - let col = columns df V.! idx- in case col of- BoxedColumn (vec :: V.Vector a) -> - case testEquality (typeRep @a) (typeRep @Double) of- Just _ -> True- Nothing -> case testEquality (typeRep @a) (typeRep @Int) of- Just _ -> True- Nothing -> case testEquality (typeRep @a) (typeRep @Float) of- Just _ -> True- Nothing -> False- UnboxedColumn (vec :: VU.Vector a) -> - case testEquality (typeRep @a) (typeRep @Double) of- Just _ -> True- Nothing -> case testEquality (typeRep @a) (typeRep @Int) of- Just _ -> True - Nothing -> case testEquality (typeRep @a) (typeRep @Float) of- Just _ -> True- Nothing -> False- -- Haven't dealt with optionals yet.- _ -> False--plotAllHistograms :: HasCallStack => DataFrame -> IO ()-plotAllHistograms df = do- let numericCols = filter (isNumericColumn df) (columnNames df)- forM_ numericCols $ \col -> do- plotHistogram col df--plotCorrelationMatrix :: HasCallStack => DataFrame -> IO ()-plotCorrelationMatrix df = do- let numericCols = filter (isNumericColumn df) (columnNames df)- let correlations = map (\col1 ->- map (\col2 -> let- vals1 = extractNumericColumn col1 df- vals2 = extractNumericColumn col2 df- in correlation vals1 vals2) numericCols) numericCols- putStrLn $ heatmap "Correlation Matrix" correlations defPlot- where- correlation xs ys = - let n = fromIntegral $ length xs- meanX = sum xs / n- meanY = sum ys / n- covXY = sum [(x - meanX) * (y - meanY) | (x,y) <- zip xs ys] / n- stdX = sqrt $ sum [(x - meanX)^2 | x <- xs] / n- stdY = sqrt $ sum [(y - meanY)^2 | y <- ys] / n- in covXY / (stdX * stdY)--quickPlot :: HasCallStack => [T.Text] -> DataFrame -> IO ()-quickPlot [] df = plotAllHistograms df >> putStrLn "Plotted all numeric columns"-quickPlot [col] df = plotHistogram col df-quickPlot [col1, col2] df = plotScatter col1 col2 df-quickPlot cols df = plotLines cols df--plotBars :: HasCallStack => T.Text -> DataFrame -> IO ()-plotBars colName df = plotBarsWith colName Nothing (defaultPlotConfig Bar') df--plotBarsWith :: HasCallStack => T.Text -> Maybe T.Text -> PlotConfig -> DataFrame -> IO ()-plotBarsWith colName groupByCol config df = - case groupByCol of- Nothing -> plotSingleBars colName config df- Just grpCol -> plotGroupedBarsWith grpCol colName config df--plotSingleBars :: HasCallStack => T.Text -> PlotConfig -> DataFrame -> IO ()-plotSingleBars colName config df = do- let barData = getCategoricalCounts colName df- case barData of- Just counts -> do- let grouped = groupWithOther 10 counts- putStrLn $ bars (plotTitle config) grouped (plotSettings config)- Nothing -> do- let values = extractNumericColumn colName df- if length values > 20- then do- let labels = map (\i -> "Item " ++ show i) [1..length values]- paired = zip labels values- grouped = groupWithOther 10 paired- putStrLn $ bars (plotTitle config) grouped (plotSettings config)- else do- let labels = map (\i -> "Item " ++ show i) [1..length values]- putStrLn $ bars (plotTitle config) (zip labels values) (plotSettings config)--plotBarsTopN :: HasCallStack => Int -> T.Text -> DataFrame -> IO ()-plotBarsTopN n colName df = plotBarsTopNWith n colName (defaultPlotConfig Bar') df--plotBarsTopNWith :: HasCallStack => Int -> T.Text -> PlotConfig -> DataFrame -> IO ()-plotBarsTopNWith n colName config df = do- let barData = getCategoricalCounts colName df- case barData of- Just counts -> do- let grouped = groupWithOther n counts- putStrLn $ bars (plotTitle config) grouped (plotSettings config)- Nothing -> do- let values = extractNumericColumn colName df- labels = map (\i -> "Item " ++ show i) [1..length values]- paired = zip labels values- grouped = groupWithOther n paired- putStrLn $ bars (plotTitle config) grouped (plotSettings config)--plotGroupedBarsWith :: HasCallStack => T.Text -> T.Text -> PlotConfig -> DataFrame -> IO ()-plotGroupedBarsWith groupCol valCol config df = do- let isNumeric = isNumericColumnCheck valCol df- - if isNumeric- then do- let groups = extractStringColumn groupCol df- values = extractNumericColumn valCol df- grouped = M.toList $ M.fromListWith (+) (zip groups values)- finalGroups = groupWithOther 10 grouped- putStrLn $ bars (plotTitle config) finalGroups (plotSettings config)- else do- let groups = extractStringColumn groupCol df- vals = extractStringColumn valCol df- pairs = zip groups vals- counts = M.toList $ M.fromListWith (+) - [(g ++ " - " ++ v, 1) | (g, v) <- pairs]- finalCounts = groupWithOther 15 [(k, fromIntegral v) | (k, v) <- counts]- putStrLn $ bars (plotTitle config) finalCounts (plotSettings config)--plotValueCounts :: HasCallStack => T.Text -> DataFrame -> IO ()-plotValueCounts colName df = plotValueCountsWith colName 10 (defaultPlotConfig Bar') df--plotValueCountsWith :: HasCallStack => T.Text -> Int -> PlotConfig -> DataFrame -> IO ()-plotValueCountsWith colName maxBars config df = do- let counts = getCategoricalCounts colName df- case counts of- Just c -> do- let grouped = groupWithOther maxBars c- config' = config { plotTitle = if null (plotTitle config) - then "Value counts for " ++ T.unpack colName- else plotTitle config }- putStrLn $ bars (T.unpack colName) grouped (plotSettings config')- Nothing -> error $ "Could not get value counts for column " ++ T.unpack colName--plotBarsWithPercentages :: HasCallStack => T.Text -> DataFrame -> IO ()-plotBarsWithPercentages colName df = do- let counts = getCategoricalCounts colName df- case counts of- Just c -> do- let total = sum (map snd c)- percentages = [(label ++ " (" ++ show (round (100 * val / total) :: Int) ++ "%)", val) - | (label, val) <- c]- grouped = groupWithOther 10 percentages- putStrLn $ bars ("Distribution of " ++ T.unpack colName) grouped defPlot- Nothing -> error $ "Could not get value counts for column " ++ T.unpack colName--smartPlotBars :: HasCallStack => T.Text -> DataFrame -> IO ()-smartPlotBars colName df = do- let counts = getCategoricalCounts colName df- case counts of- Just c -> do- let numUnique = length c- config = (defaultPlotConfig Bar') { - plotTitle = T.unpack colName ++ " (" ++ show numUnique ++ " unique values)"- }- if numUnique <= 12- then putStrLn $ bars (plotTitle config) c (plotSettings config)- else if numUnique <= 20- then do- let grouped = groupWithOther 12 c- putStrLn $ bars (plotTitle config ++ " - Top 12 + Other") grouped (plotSettings config)- else do- let grouped = groupWithOther 10 c- otherCount = numUnique - 10- putStrLn $ bars (plotTitle config ++ " - Top 10 + Other (" ++ show otherCount ++ " items)") - grouped (plotSettings config)- Nothing -> plotBars colName df--plotCategoricalSummary :: HasCallStack => DataFrame -> IO ()-plotCategoricalSummary df = do- let cols = columnNames df- forM_ cols $ \col -> do- let counts = getCategoricalCounts col df- case counts of- Just c -> when (length c > 1) $ do- let numUnique = length c- putStrLn $ "\n=== " ++ T.unpack col ++ " (" ++ show numUnique ++ " unique values) ==="- if numUnique > 15 then plotBarsTopN 10 col df else plotBars col df- Nothing -> return ()--getCategoricalCounts :: HasCallStack => T.Text -> DataFrame -> Maybe [(String, Double)]-getCategoricalCounts colName df = - case M.lookup colName (columnIndices df) of- Nothing -> error $ "Column " ++ T.unpack colName ++ " not found"- Just idx -> - let col = columns df V.! idx- in case col of- BoxedColumn vec -> let counts = countValues vec- in Just [(show k, fromIntegral v) | (k, v) <- counts]- UnboxedColumn vec -> let counts = countValuesUnboxed vec- in Just [(show k, fromIntegral v) | (k, v) <- counts]- where- countValues :: (Ord a, Show a) => V.Vector a -> [(a, Int)]- countValues vec = M.toList $ V.foldr' (\x acc -> M.insertWith (+) x 1 acc) M.empty vec- - countValuesUnboxed :: (Ord a, Show a, VU.Unbox a) => VU.Vector a -> [(a, Int)]- countValuesUnboxed vec = M.toList $ VU.foldr' (\x acc -> M.insertWith (+) x 1 acc) M.empty vec--isNumericColumnCheck :: T.Text -> DataFrame -> Bool-isNumericColumnCheck colName df = - case M.lookup colName (columnIndices df) of- Nothing -> False- Just idx -> - let col = columns df V.! idx- in case col of- BoxedColumn (vec :: V.Vector a) -> isNumericType @a- UnboxedColumn (vec :: VU.Vector a) -> isNumericType @a--isNumericType :: forall a. Typeable a => Bool-isNumericType = - case testEquality (typeRep @a) (typeRep @Double) of- Just _ -> True- Nothing -> case testEquality (typeRep @a) (typeRep @Int) of- Just _ -> True- Nothing -> case testEquality (typeRep @a) (typeRep @Float) of- Just _ -> True- Nothing -> case testEquality (typeRep @a) (typeRep @Integer) of- Just _ -> True- Nothing -> False--extractStringColumn :: HasCallStack => T.Text -> DataFrame -> [String]-extractStringColumn colName df = - case M.lookup colName (columnIndices df) of- Nothing -> error $ "Column " ++ T.unpack colName ++ " not found"- Just idx -> - let col = columns df V.! idx- in case col of- BoxedColumn vec -> V.toList $ V.map show vec- UnboxedColumn vec -> V.toList $ VG.map show (VG.convert vec)--extractNumericColumn :: HasCallStack => T.Text -> DataFrame -> [Double]-extractNumericColumn colName df = - case M.lookup colName (columnIndices df) of- Nothing -> error $ "Column " ++ T.unpack colName ++ " not found"- Just idx -> - let col = columns df V.! idx- in case col of- BoxedColumn vec -> vectorToDoubles vec- UnboxedColumn vec -> unboxedVectorToDoubles vec--vectorToDoubles :: forall a. (Typeable a, Show a) => V.Vector a -> [Double]-vectorToDoubles vec = - case testEquality (typeRep @a) (typeRep @Double) of- Just Refl -> V.toList vec- Nothing -> case testEquality (typeRep @a) (typeRep @Int) of- Just Refl -> V.toList $ V.map fromIntegral vec- Nothing -> case testEquality (typeRep @a) (typeRep @Integer) of- Just Refl -> V.toList $ V.map fromIntegral vec- Nothing -> case testEquality (typeRep @a) (typeRep @Float) of- Just Refl -> V.toList $ V.map realToFrac vec- Nothing -> error $ "Column is not numeric (type: " ++ show (typeRep @a) ++ ")"--unboxedVectorToDoubles :: forall a. (Typeable a, VU.Unbox a, Show a) => VU.Vector a -> [Double]-unboxedVectorToDoubles vec = - case testEquality (typeRep @a) (typeRep @Double) of- Just Refl -> VU.toList vec- Nothing -> case testEquality (typeRep @a) (typeRep @Int) of- Just Refl -> VU.toList $ VU.map fromIntegral vec- Nothing -> case testEquality (typeRep @a) (typeRep @Float) of- Just Refl -> VU.toList $ VU.map realToFrac vec- Nothing -> error $ "Column is not numeric (type: " ++ show (typeRep @a) ++ ")"--groupWithOther :: Int -> [(String, Double)] -> [(String, Double)]-groupWithOther n items = - let sorted = L.sortOn (negate . snd) items- (topN, rest) = splitAt n sorted- otherSum = sum (map snd rest)- result = if null rest || otherSum == 0- then topN- else topN ++ [("Other (" ++ show (length rest) ++ " items)", otherSum)]- in result--plotPie :: HasCallStack => T.Text -> Maybe T.Text -> DataFrame -> IO ()-plotPie valCol labelCol df = plotPieWith valCol labelCol (defaultPlotConfig Pie') df--plotPieWith :: HasCallStack => T.Text -> Maybe T.Text -> PlotConfig -> DataFrame -> IO ()-plotPieWith valCol labelCol config df = do- let categoricalData = getCategoricalCounts valCol df- case categoricalData of- Just counts -> do- let grouped = groupWithOtherForPie 8 counts- putStrLn $ pie (plotTitle config) grouped (plotSettings config)- Nothing -> do- let values = extractNumericColumn valCol df- labels = case labelCol of- Nothing -> map (\i -> "Item " ++ show i) [1..length values]- Just lCol -> extractStringColumn lCol df- let pieData = zip labels values- grouped = if length pieData > 10- then groupWithOtherForPie 8 pieData- else pieData- putStrLn $ pie (plotTitle config) grouped (plotSettings config)--groupWithOtherForPie :: Int -> [(String, Double)] -> [(String, Double)]-groupWithOtherForPie n items = - let total = sum (map snd items)- sorted = L.sortOn (negate . snd) items- (topN, rest) = splitAt n sorted- otherSum = sum (map snd rest)- otherPct = round (100 * otherSum / total) :: Int- result = if null rest || otherSum == 0- then topN- else topN ++ [("Other (" ++ show (length rest) ++ " items, " ++ - show otherPct ++ "%)", otherSum)]- in result--plotPieWithPercentages :: HasCallStack => T.Text -> DataFrame -> IO ()-plotPieWithPercentages colName df = plotPieWithPercentagesConfig colName (defaultPlotConfig Pie') df--plotPieWithPercentagesConfig :: HasCallStack => T.Text -> PlotConfig -> DataFrame -> IO ()-plotPieWithPercentagesConfig colName config df = do- let counts = getCategoricalCounts colName df- case counts of- Just c -> do- let total = sum (map snd c)- withPct = [(label ++ " (" ++ show (round (100 * val / total) :: Int) ++ "%)", val) - | (label, val) <- c]- grouped = groupWithOtherForPie 8 withPct- putStrLn $ pie (plotTitle config) grouped (plotSettings config)- Nothing -> do- let values = extractNumericColumn colName df- total = sum values- labels = map (\i -> "Item " ++ show i) [1..length values]- withPct = [(label ++ " (" ++ show (round (100 * val / total) :: Int) ++ "%)", val)- | (label, val) <- zip labels values]- grouped = groupWithOtherForPie 8 withPct- putStrLn $ pie (plotTitle config) grouped (plotSettings config)--plotPieTopN :: HasCallStack => Int -> T.Text -> DataFrame -> IO ()-plotPieTopN n colName df = plotPieTopNWith n colName (defaultPlotConfig Pie') df--plotPieTopNWith :: HasCallStack => Int -> T.Text -> PlotConfig -> DataFrame -> IO ()-plotPieTopNWith n colName config df = do- let counts = getCategoricalCounts colName df- case counts of- Just c -> do- let grouped = groupWithOtherForPie n c- putStrLn $ pie (plotTitle config) grouped (plotSettings config)- Nothing -> do- let values = extractNumericColumn colName df- labels = map (\i -> "Item " ++ show i) [1..length values]- paired = zip labels values- grouped = groupWithOtherForPie n paired- putStrLn $ pie (plotTitle config) grouped (plotSettings config)--smartPlotPie :: HasCallStack => T.Text -> DataFrame -> IO ()-smartPlotPie colName df = do- let counts = getCategoricalCounts colName df- case counts of- Just c -> do- let numUnique = length c- total = sum (map snd c)- significant = filter (\(_, v) -> v / total >= 0.01) c- config = (defaultPlotConfig Pie') { - plotTitle = T.unpack colName ++ " Distribution"- }- if length significant <= 6- then putStrLn $ pie (plotTitle config) significant (plotSettings config)- else if length significant <= 10- then do- let grouped = groupWithOtherForPie 8 c- putStrLn $ pie (plotTitle config) grouped (plotSettings config)- else do- let grouped = groupWithOtherForPie 6 c- putStrLn $ pie (plotTitle config) grouped (plotSettings config)- Nothing -> plotPie colName Nothing df--plotPieGrouped :: HasCallStack => T.Text -> T.Text -> DataFrame -> IO ()-plotPieGrouped groupCol valCol df = plotPieGroupedWith groupCol valCol (defaultPlotConfig Pie') df--plotPieGroupedWith :: HasCallStack => T.Text -> T.Text -> PlotConfig -> DataFrame -> IO ()-plotPieGroupedWith groupCol valCol config df = do- let isNumeric = isNumericColumnCheck valCol df- - if isNumeric- then do- let groups = extractStringColumn groupCol df- values = extractNumericColumn valCol df- grouped = M.toList $ M.fromListWith (+) (zip groups values)- finalGroups = groupWithOtherForPie 8 grouped- putStrLn $ pie (plotTitle config) finalGroups (plotSettings config)- else do- let groups = extractStringColumn groupCol df- vals = extractStringColumn valCol df- combined = zipWith (\g v -> g ++ " - " ++ v) groups vals- counts = M.toList $ M.fromListWith (+) [(c, 1) | c <- combined]- finalCounts = groupWithOtherForPie 10 [(k, fromIntegral v) | (k, v) <- counts]- putStrLn $ pie (plotTitle config) finalCounts (plotSettings config)--plotPieComparison :: HasCallStack => [T.Text] -> DataFrame -> IO ()-plotPieComparison cols df = do- forM_ cols $ \col -> do- let counts = getCategoricalCounts col df- case counts of- Just c -> when (length c > 1 && length c <= 20) $ do- putStrLn $ "\n=== " ++ T.unpack col ++ " Distribution ==="- smartPlotPie col df- Nothing -> return ()--plotBinaryPie :: HasCallStack => T.Text -> DataFrame -> IO ()-plotBinaryPie colName df = do- let counts = getCategoricalCounts colName df- case counts of- Just c -> - if length c == 2- then do- let total = sum (map snd c)- withPct = [(label ++ " (" ++ show (round (100 * val / total) :: Int) ++ "%)", val) - | (label, val) <- c]- putStrLn $ pie (T.unpack colName ++ " Proportion") withPct defPlot- else error $ "Column " ++ T.unpack colName ++ " is not binary (has " ++ - show (length c) ++ " unique values)"- Nothing -> error $ "Column " ++ T.unpack colName ++ " is not categorical"--plotMarketShare :: HasCallStack => T.Text -> DataFrame -> IO ()-plotMarketShare colName df = plotMarketShareWith colName (defaultPlotConfig Pie') df--plotMarketShareWith :: HasCallStack => T.Text -> PlotConfig -> DataFrame -> IO ()-plotMarketShareWith colName config df = do- let counts = getCategoricalCounts colName df- case counts of- Just c -> do- let total = sum (map snd c)- sorted = L.sortOn (negate . snd) c- significantShares = takeWhile (\(_, v) -> v / total >= 0.02) sorted- otherSum = sum [v | (_, v) <- c, v `notElem` map snd significantShares]- - formatShare (label, val) = - let pct = round (100 * val / total) :: Int- in (label ++ " (" ++ show pct ++ "%)", val)- - shares = map formatShare significantShares- finalShares = if otherSum > 0 && otherSum / total >= 0.01- then shares ++ [("Others (<2% each)", otherSum)]- else shares- - let config' = config { plotTitle = if null (plotTitle config)- then T.unpack colName ++ " Market Share"- else plotTitle config }- putStrLn $ pie (plotTitle config') finalShares (plotSettings config')- Nothing -> error $ "Column " ++ T.unpack colName ++ " is not categorical"+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++module DataFrame.Display.Terminal.Plot where++import Control.Monad+import qualified Data.List as L+import qualified Data.Map as M+import Data.Maybe (catMaybes, fromMaybe)+import qualified Data.Text as T+import qualified Data.Text.IO as T+import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))+import Data.Typeable (Typeable)+import qualified Data.Vector as V+import qualified Data.Vector.Generic as VG+import qualified Data.Vector.Unboxed as VU+import GHC.Stack (HasCallStack)+import Type.Reflection (typeRep)++import DataFrame.Internal.Column (Column (..), Columnable)+import qualified DataFrame.Internal.Column as D+import DataFrame.Internal.DataFrame (DataFrame (..))+import DataFrame.Operations.Core+import qualified DataFrame.Operations.Subset as D+import Granite++data PlotConfig = PlotConfig+ { plotType :: PlotType+ , plotSettings :: Plot+ }++data PlotType+ = Histogram+ | Scatter+ | Line+ | Bar+ | BoxPlot+ | Pie+ | StackedBar+ | Heatmap+ deriving (Eq, Show)++defaultPlotConfig :: PlotType -> PlotConfig+defaultPlotConfig ptype =+ PlotConfig+ { plotType = ptype+ , plotSettings = defPlot+ }++plotHistogram :: (HasCallStack) => T.Text -> DataFrame -> IO ()+plotHistogram colName df = plotHistogramWith colName (defaultPlotConfig Histogram) df++plotHistogramWith :: (HasCallStack) => T.Text -> PlotConfig -> DataFrame -> IO ()+plotHistogramWith colName config df = do+ let values = extractNumericColumn colName df+ (minVal, maxVal) = if null values then (0, 1) else (minimum values, maximum values)+ T.putStrLn $ histogram (bins 30 minVal maxVal) values (plotSettings config)++plotScatter :: (HasCallStack) => T.Text -> T.Text -> DataFrame -> IO ()+plotScatter xCol yCol df = plotScatterWith xCol yCol (defaultPlotConfig Scatter) df++plotScatterWith :: (HasCallStack) => T.Text -> T.Text -> PlotConfig -> DataFrame -> IO ()+plotScatterWith xCol yCol config df = do+ let xVals = extractNumericColumn xCol df+ yVals = extractNumericColumn yCol df+ points = zip xVals yVals+ T.putStrLn $ scatter [(xCol <> " vs " <> yCol, points)] (plotSettings config)++plotScatterBy :: (HasCallStack) => T.Text -> T.Text -> T.Text -> DataFrame -> IO ()+plotScatterBy xCol yCol grouping df = plotScatterByWith xCol yCol grouping (defaultPlotConfig Scatter) df++plotScatterByWith :: (HasCallStack) => T.Text -> T.Text -> T.Text -> PlotConfig -> DataFrame -> IO ()+plotScatterByWith xCol yCol grouping config df = do+ let vals = extractStringColumn grouping df+ let df' = insertColumn grouping (D.fromList vals) df+ xs <- forM (L.nub vals) $ \col -> do+ let filtered = D.filter grouping (== col) df'+ xVals = extractNumericColumn xCol filtered+ yVals = extractNumericColumn yCol filtered+ points = zip xVals yVals+ pure (col, points)+ T.putStrLn $ scatter xs (plotSettings config)++plotLines :: (HasCallStack) => T.Text -> [T.Text] -> DataFrame -> IO ()+plotLines xAxis colNames df = plotLinesWith xAxis colNames (defaultPlotConfig Line) df++plotLinesWith :: (HasCallStack) => T.Text -> [T.Text] -> PlotConfig -> DataFrame -> IO ()+plotLinesWith xAxis colNames config df = do+ seriesData <- forM colNames $ \col -> do+ let values = extractNumericColumn col df+ indices = extractNumericColumn xAxis df+ return (col, zip indices values)+ T.putStrLn $ lineGraph seriesData (plotSettings config)++plotBoxPlots :: (HasCallStack) => [T.Text] -> DataFrame -> IO ()+plotBoxPlots colNames df = plotBoxPlotsWith colNames (defaultPlotConfig BoxPlot) df++plotBoxPlotsWith :: (HasCallStack) => [T.Text] -> PlotConfig -> DataFrame -> IO ()+plotBoxPlotsWith colNames config df = do+ boxData <- forM colNames $ \col -> do+ let values = extractNumericColumn col df+ return (col, values)+ T.putStrLn $ boxPlot boxData (plotSettings config)++plotStackedBars :: (HasCallStack) => T.Text -> [T.Text] -> DataFrame -> IO ()+plotStackedBars categoryCol valueColumns df =+ plotStackedBarsWith categoryCol valueColumns (defaultPlotConfig StackedBar) df++plotStackedBarsWith :: (HasCallStack) => T.Text -> [T.Text] -> PlotConfig -> DataFrame -> IO ()+plotStackedBarsWith categoryCol valueColumns config df = do+ let categories = extractStringColumn categoryCol df+ uniqueCategories = L.nub categories++ stackData <- forM uniqueCategories $ \cat -> do+ let indices = [i | (i, c) <- zip [0 ..] categories, c == cat]+ seriesData <- forM valueColumns $ \col -> do+ let allValues = extractNumericColumn col df+ values = [allValues !! i | i <- indices, i < length allValues]+ return (col, sum values)+ return (cat, seriesData)++ T.putStrLn $ stackedBars stackData (plotSettings config)++plotHeatmap :: (HasCallStack) => DataFrame -> IO ()+plotHeatmap df = plotHeatmapWith (defaultPlotConfig Heatmap) df++plotHeatmapWith :: (HasCallStack) => PlotConfig -> DataFrame -> IO ()+plotHeatmapWith config df = do+ let numericCols = filter (isNumericColumn df) (columnNames df)+ matrix = map (\col -> extractNumericColumn col df) numericCols+ T.putStrLn $ heatmap matrix (plotSettings config)++isNumericColumn :: DataFrame -> T.Text -> Bool+isNumericColumn df colName =+ case M.lookup colName (columnIndices df) of+ Nothing -> False+ Just idx ->+ let col = columns df V.! idx+ in case col of+ BoxedColumn (vec :: V.Vector a) ->+ case testEquality (typeRep @a) (typeRep @Double) of+ Just _ -> True+ Nothing -> case testEquality (typeRep @a) (typeRep @Int) of+ Just _ -> True+ Nothing -> case testEquality (typeRep @a) (typeRep @Float) of+ Just _ -> True+ Nothing -> False+ UnboxedColumn (vec :: VU.Vector a) ->+ case testEquality (typeRep @a) (typeRep @Double) of+ Just _ -> True+ Nothing -> case testEquality (typeRep @a) (typeRep @Int) of+ Just _ -> True+ Nothing -> case testEquality (typeRep @a) (typeRep @Float) of+ Just _ -> True+ Nothing -> False+ -- Haven't dealt with optionals yet.+ _ -> False++plotAllHistograms :: (HasCallStack) => DataFrame -> IO ()+plotAllHistograms df = do+ let numericCols = filter (isNumericColumn df) (columnNames df)+ forM_ numericCols $ \col -> do+ T.putStrLn col+ plotHistogram col df++plotCorrelationMatrix :: (HasCallStack) => DataFrame -> IO ()+plotCorrelationMatrix df = do+ let numericCols = filter (isNumericColumn df) (columnNames df)+ let correlations =+ map+ ( \col1 ->+ map+ ( \col2 ->+ let+ vals1 = extractNumericColumn col1 df+ vals2 = extractNumericColumn col2 df+ in+ correlation vals1 vals2+ )+ numericCols+ )+ numericCols+ print (zip [0 ..] numericCols)+ T.putStrLn $ heatmap correlations (defPlot{plotTitle = "Correlation Matrix"})+ where+ correlation xs ys =+ let n = fromIntegral $ length xs+ meanX = sum xs / n+ meanY = sum ys / n+ covXY = sum [(x - meanX) * (y - meanY) | (x, y) <- zip xs ys] / n+ stdX = sqrt $ sum [(x - meanX) ^ 2 | x <- xs] / n+ stdY = sqrt $ sum [(y - meanY) ^ 2 | y <- ys] / n+ in covXY / (stdX * stdY)++plotBars :: (HasCallStack) => T.Text -> DataFrame -> IO ()+plotBars colName df = plotBarsWith colName Nothing (defaultPlotConfig Bar) df++plotBarsWith :: (HasCallStack) => T.Text -> Maybe T.Text -> PlotConfig -> DataFrame -> IO ()+plotBarsWith colName groupByCol config df =+ case groupByCol of+ Nothing -> plotSingleBars colName config df+ Just grpCol -> plotGroupedBarsWith grpCol colName config df++plotSingleBars :: (HasCallStack) => T.Text -> PlotConfig -> DataFrame -> IO ()+plotSingleBars colName config df = do+ let barData = getCategoricalCounts colName df+ case barData of+ Just counts -> do+ let grouped = groupWithOther 10 counts+ T.putStrLn $ bars grouped (plotSettings config)+ Nothing -> do+ let values = extractNumericColumn colName df+ if length values > 20+ then do+ let labels = map (\i -> "Item " <> T.pack (show i)) [1 .. length values]+ paired = zip labels values+ grouped = groupWithOther 10 paired+ T.putStrLn $ bars grouped (plotSettings config)+ else do+ let labels = map (\i -> "Item " <> T.pack (show i)) [1 .. length values]+ T.putStrLn $ bars (zip labels values) (plotSettings config)++plotBarsTopN :: (HasCallStack) => Int -> T.Text -> DataFrame -> IO ()+plotBarsTopN n colName df = plotBarsTopNWith n colName (defaultPlotConfig Bar) df++plotBarsTopNWith :: (HasCallStack) => Int -> T.Text -> PlotConfig -> DataFrame -> IO ()+plotBarsTopNWith n colName config df = do+ let barData = getCategoricalCounts colName df+ case barData of+ Just counts -> do+ let grouped = groupWithOther n counts+ T.putStrLn $ bars grouped (plotSettings config)+ Nothing -> do+ let values = extractNumericColumn colName df+ labels = map (\i -> "Item " <> T.pack (show i)) [1 .. length values]+ paired = zip labels values+ grouped = groupWithOther n paired+ T.putStrLn $ bars grouped (plotSettings config)++plotGroupedBarsWith :: (HasCallStack) => T.Text -> T.Text -> PlotConfig -> DataFrame -> IO ()+plotGroupedBarsWith = plotGroupedBarsWithN 10++plotGroupedBarsWithN :: (HasCallStack) => Int -> T.Text -> T.Text -> PlotConfig -> DataFrame -> IO ()+plotGroupedBarsWithN n groupCol valCol config df = do+ let isNumeric = isNumericColumnCheck valCol df++ if isNumeric+ then do+ let groups = extractStringColumn groupCol df+ values = extractNumericColumn valCol df+ grouped = M.toList $ M.fromListWith (+) (zip groups values)+ finalGroups = groupWithOther n grouped+ T.putStrLn $ bars finalGroups (plotSettings config)+ else do+ let groups = extractStringColumn groupCol df+ vals = extractStringColumn valCol df+ pairs = zip groups vals+ counts =+ M.toList $+ M.fromListWith+ (+)+ [(g <> " - " <> v, 1) | (g, v) <- pairs]+ finalCounts = groupWithOther n [(k, fromIntegral v) | (k, v) <- counts]+ T.putStrLn $ bars finalCounts (plotSettings config)++plotValueCounts :: (HasCallStack) => T.Text -> DataFrame -> IO ()+plotValueCounts colName df = plotValueCountsWith colName 10 (defaultPlotConfig Bar) df++plotValueCountsWith :: (HasCallStack) => T.Text -> Int -> PlotConfig -> DataFrame -> IO ()+plotValueCountsWith colName maxBars config df = do+ let counts = getCategoricalCounts colName df+ case counts of+ Just c -> do+ let grouped = groupWithOther maxBars c+ config' =+ config+ { plotSettings =+ (plotSettings config)+ { plotTitle =+ if T.null (plotTitle (plotSettings config))+ then "Value counts for " <> colName+ else plotTitle (plotSettings config)+ }+ }+ T.putStrLn $ bars grouped (plotSettings config')+ Nothing -> error $ "Could not get value counts for column " ++ T.unpack colName++plotBarsWithPercentages :: (HasCallStack) => T.Text -> DataFrame -> IO ()+plotBarsWithPercentages colName df = do+ let counts = getCategoricalCounts colName df+ case counts of+ Just c -> do+ let total = sum (map snd c)+ percentages =+ [ (label <> " (" <> T.pack (show (round (100 * val / total) :: Int)) <> "%)", val)+ | (label, val) <- c+ ]+ grouped = groupWithOther 10 percentages+ T.putStrLn $ bars grouped (defPlot{plotTitle = "Distribution of " <> colName})+ Nothing -> error $ "Could not get value counts for column " ++ T.unpack colName++smartPlotBars :: (HasCallStack) => T.Text -> DataFrame -> IO ()+smartPlotBars colName df = do+ let counts = getCategoricalCounts colName df+ case counts of+ Just c -> do+ let numUnique = length c+ config =+ (defaultPlotConfig Bar)+ { plotSettings = (plotSettings (defaultPlotConfig Bar)){plotTitle = colName <> " (" <> T.pack (show numUnique) <> " unique values)"}+ }+ if numUnique <= 12+ then T.putStrLn $ bars c (plotSettings config)+ else+ if numUnique <= 20+ then do+ let grouped = groupWithOther 12 c+ T.putStrLn $ bars grouped (plotSettings config)+ else do+ let grouped = groupWithOther 10 c+ otherCount = numUnique - 10+ T.putStrLn $+ bars+ grouped+ (plotSettings config)+ Nothing -> plotBars colName df++plotCategoricalSummary :: (HasCallStack) => DataFrame -> IO ()+plotCategoricalSummary df = do+ let cols = columnNames df+ forM_ cols $ \col -> do+ let counts = getCategoricalCounts col df+ case counts of+ Just c -> when (length c > 1) $ do+ let numUnique = length c+ putStrLn $ "\n=== " ++ T.unpack col ++ " (" ++ show numUnique ++ " unique values) ==="+ if numUnique > 15 then plotBarsTopN 10 col df else plotBars col df+ Nothing -> return ()++getCategoricalCounts :: (HasCallStack) => T.Text -> DataFrame -> Maybe [(T.Text, Double)]+getCategoricalCounts colName df =+ case M.lookup colName (columnIndices df) of+ Nothing -> error $ "Column " ++ T.unpack colName ++ " not found"+ Just idx ->+ let col = columns df V.! idx+ in case col of+ BoxedColumn vec ->+ let counts = countValues vec+ in Just [(T.pack (show k), fromIntegral v) | (k, v) <- counts]+ UnboxedColumn vec ->+ let counts = countValuesUnboxed vec+ in Just [(T.pack (show k), fromIntegral v) | (k, v) <- counts]+ _ -> Nothing+ where+ countValues :: (Ord a, Show a) => V.Vector a -> [(a, Int)]+ countValues vec = M.toList $ V.foldr' (\x acc -> M.insertWith (+) x 1 acc) M.empty vec++ countValuesUnboxed :: (Ord a, Show a, VU.Unbox a) => VU.Vector a -> [(a, Int)]+ countValuesUnboxed vec = M.toList $ VU.foldr' (\x acc -> M.insertWith (+) x 1 acc) M.empty vec++isNumericColumnCheck :: T.Text -> DataFrame -> Bool+isNumericColumnCheck colName df =+ case M.lookup colName (columnIndices df) of+ Nothing -> False+ Just idx ->+ let col = columns df V.! idx+ in case col of+ BoxedColumn (vec :: V.Vector a) -> isNumericType @a+ UnboxedColumn (vec :: VU.Vector a) -> isNumericType @a+ _ -> False++isNumericType :: forall a. (Typeable a) => Bool+isNumericType =+ case testEquality (typeRep @a) (typeRep @Double) of+ Just _ -> True+ Nothing -> case testEquality (typeRep @a) (typeRep @Int) of+ Just _ -> True+ Nothing -> case testEquality (typeRep @a) (typeRep @Float) of+ Just _ -> True+ Nothing -> case testEquality (typeRep @a) (typeRep @Integer) of+ Just _ -> True+ Nothing -> False++extractStringColumn :: (HasCallStack) => T.Text -> DataFrame -> [T.Text]+extractStringColumn colName df =+ case M.lookup colName (columnIndices df) of+ Nothing -> error $ "Column " ++ T.unpack colName ++ " not found"+ Just idx ->+ let col = columns df V.! idx+ in case col of+ BoxedColumn vec -> V.toList $ V.map (T.pack . show) vec+ UnboxedColumn vec -> V.toList $ VG.map (T.pack . show) (VG.convert vec)+ OptionalColumn vec -> V.toList $ V.map (T.pack . show) vec++extractNumericColumn :: (HasCallStack) => T.Text -> DataFrame -> [Double]+extractNumericColumn colName df =+ case M.lookup colName (columnIndices df) of+ Nothing -> error $ "Column " ++ T.unpack colName ++ " not found"+ Just idx ->+ let col = columns df V.! idx+ in case col of+ BoxedColumn vec -> vectorToDoubles vec+ UnboxedColumn vec -> unboxedVectorToDoubles vec+ _ -> []++vectorToDoubles :: forall a. (Typeable a, Show a) => V.Vector a -> [Double]+vectorToDoubles vec =+ case testEquality (typeRep @a) (typeRep @Double) of+ Just Refl -> V.toList vec+ Nothing -> case testEquality (typeRep @a) (typeRep @Int) of+ Just Refl -> V.toList $ V.map fromIntegral vec+ Nothing -> case testEquality (typeRep @a) (typeRep @Integer) of+ Just Refl -> V.toList $ V.map fromIntegral vec+ Nothing -> case testEquality (typeRep @a) (typeRep @Float) of+ Just Refl -> V.toList $ V.map realToFrac vec+ Nothing -> error $ "Column is not numeric (type: " ++ show (typeRep @a) ++ ")"++unboxedVectorToDoubles :: forall a. (Typeable a, VU.Unbox a, Show a) => VU.Vector a -> [Double]+unboxedVectorToDoubles vec =+ case testEquality (typeRep @a) (typeRep @Double) of+ Just Refl -> VU.toList vec+ Nothing -> case testEquality (typeRep @a) (typeRep @Int) of+ Just Refl -> VU.toList $ VU.map fromIntegral vec+ Nothing -> case testEquality (typeRep @a) (typeRep @Float) of+ Just Refl -> VU.toList $ VU.map realToFrac vec+ Nothing -> error $ "Column is not numeric (type: " ++ show (typeRep @a) ++ ")"++groupWithOther :: Int -> [(T.Text, Double)] -> [(T.Text, Double)]+groupWithOther n items =+ let sorted = L.sortOn (negate . snd) items+ (topN, rest) = splitAt n sorted+ otherSum = sum (map snd rest)+ result =+ if null rest || otherSum == 0+ then topN+ else topN ++ [("Other (" <> T.pack (show (length rest)) <> " items)", otherSum)]+ in result++plotPie :: (HasCallStack) => T.Text -> Maybe T.Text -> DataFrame -> IO ()+plotPie valCol labelCol df = plotPieWith valCol labelCol (defaultPlotConfig Pie) df++plotPieWith :: (HasCallStack) => T.Text -> Maybe T.Text -> PlotConfig -> DataFrame -> IO ()+plotPieWith valCol labelCol config df = do+ let categoricalData = getCategoricalCounts valCol df+ case categoricalData of+ Just counts -> do+ let grouped = groupWithOtherForPie 8 counts+ T.putStrLn $ pie grouped (plotSettings config)+ Nothing -> do+ let values = extractNumericColumn valCol df+ labels = case labelCol of+ Nothing -> map (\i -> "Item " <> T.pack (show i)) [1 .. length values]+ Just lCol -> extractStringColumn lCol df+ let pieData = zip labels values+ grouped =+ if length pieData > 10+ then groupWithOtherForPie 8 pieData+ else pieData+ T.putStrLn $ pie grouped (plotSettings config)++groupWithOtherForPie :: Int -> [(T.Text, Double)] -> [(T.Text, Double)]+groupWithOtherForPie n items =+ let total = sum (map snd items)+ sorted = L.sortOn (negate . snd) items+ (topN, rest) = splitAt n sorted+ otherSum = sum (map snd rest)+ otherPct = round (100 * otherSum / total) :: Int+ result =+ if null rest || otherSum == 0+ then topN+ else+ topN+ ++ [+ ( "Other ("+ <> T.pack (show (length rest))+ <> " items, "+ <> T.pack (show otherPct)+ <> "%)"+ , otherSum+ )+ ]+ in result++plotPieWithPercentages :: (HasCallStack) => T.Text -> DataFrame -> IO ()+plotPieWithPercentages colName df = plotPieWithPercentagesConfig colName (defaultPlotConfig Pie) df++plotPieWithPercentagesConfig :: (HasCallStack) => T.Text -> PlotConfig -> DataFrame -> IO ()+plotPieWithPercentagesConfig colName config df = do+ let counts = getCategoricalCounts colName df+ case counts of+ Just c -> do+ let total = sum (map snd c)+ withPct =+ [ (label <> " (" <> T.pack (show (round (100 * val / total) :: Int)) <> "%)", val)+ | (label, val) <- c+ ]+ grouped = groupWithOtherForPie 8 withPct+ T.putStrLn $ pie grouped (plotSettings config)+ Nothing -> do+ let values = extractNumericColumn colName df+ total = sum values+ labels = map (\i -> "Item " <> T.pack (show i)) [1 .. length values]+ withPct =+ [ (label <> " (" <> T.pack (show (round (100 * val / total) :: Int)) <> "%)", val)+ | (label, val) <- zip labels values+ ]+ grouped = groupWithOtherForPie 8 withPct+ T.putStrLn $ pie grouped (plotSettings config)++plotPieTopN :: (HasCallStack) => Int -> T.Text -> DataFrame -> IO ()+plotPieTopN n colName df = plotPieTopNWith n colName (defaultPlotConfig Pie) df++plotPieTopNWith :: (HasCallStack) => Int -> T.Text -> PlotConfig -> DataFrame -> IO ()+plotPieTopNWith n colName config df = do+ let counts = getCategoricalCounts colName df+ case counts of+ Just c -> do+ let grouped = groupWithOtherForPie n c+ T.putStrLn $ pie grouped (plotSettings config)+ Nothing -> do+ let values = extractNumericColumn colName df+ labels = map (\i -> "Item " <> T.pack (show i)) [1 .. length values]+ paired = zip labels values+ grouped = groupWithOtherForPie n paired+ T.putStrLn $ pie grouped (plotSettings config)++smartPlotPie :: (HasCallStack) => T.Text -> DataFrame -> IO ()+smartPlotPie colName df = do+ let counts = getCategoricalCounts colName df+ case counts of+ Just c -> do+ let numUnique = length c+ total = sum (map snd c)+ significant = filter (\(_, v) -> v / total >= 0.01) c+ config =+ (defaultPlotConfig Pie)+ { plotSettings = (plotSettings (defaultPlotConfig Pie)){plotTitle = colName <> " Distribution"}+ }+ if length significant <= 6+ then T.putStrLn $ pie significant (plotSettings config)+ else+ if length significant <= 10+ then do+ let grouped = groupWithOtherForPie 8 c+ T.putStrLn $ pie grouped (plotSettings config)+ else do+ let grouped = groupWithOtherForPie 6 c+ T.putStrLn $ pie grouped (plotSettings config)+ Nothing -> plotPie colName Nothing df++plotPieGrouped :: (HasCallStack) => T.Text -> T.Text -> DataFrame -> IO ()+plotPieGrouped groupCol valCol df = plotPieGroupedWith groupCol valCol (defaultPlotConfig Pie) df++plotPieGroupedWith :: (HasCallStack) => T.Text -> T.Text -> PlotConfig -> DataFrame -> IO ()+plotPieGroupedWith groupCol valCol config df = do+ let isNumeric = isNumericColumnCheck valCol df++ if isNumeric+ then do+ let groups = extractStringColumn groupCol df+ values = extractNumericColumn valCol df+ grouped = M.toList $ M.fromListWith (+) (zip groups values)+ finalGroups = groupWithOtherForPie 8 grouped+ T.putStrLn $ pie finalGroups (plotSettings config)+ else do+ let groups = extractStringColumn groupCol df+ vals = extractStringColumn valCol df+ combined = zipWith (\g v -> g <> " - " <> v) groups vals+ counts = M.toList $ M.fromListWith (+) [(c, 1) | c <- combined]+ finalCounts = groupWithOtherForPie 10 [(k, fromIntegral v) | (k, v) <- counts]+ T.putStrLn $ pie finalCounts (plotSettings config)++plotPieComparison :: (HasCallStack) => [T.Text] -> DataFrame -> IO ()+plotPieComparison cols df = do+ forM_ cols $ \col -> do+ let counts = getCategoricalCounts col df+ case counts of+ Just c -> when (length c > 1 && length c <= 20) $ do+ putStrLn $ "\n=== " ++ T.unpack col ++ " Distribution ==="+ smartPlotPie col df+ Nothing -> return ()++plotBinaryPie :: (HasCallStack) => T.Text -> DataFrame -> IO ()+plotBinaryPie colName df = do+ let counts = getCategoricalCounts colName df+ case counts of+ Just c ->+ if length c == 2+ then do+ let total = sum (map snd c)+ withPct =+ [ (label <> " (" <> T.pack (show (round (100 * val / total) :: Int)) <> "%)", val)+ | (label, val) <- c+ ]+ T.putStrLn $ pie withPct defPlot+ else+ error $+ "Column "+ ++ T.unpack colName+ ++ " is not binary (has "+ ++ show (length c)+ ++ " unique values)"+ Nothing -> error $ "Column " ++ T.unpack colName ++ " is not categorical"++plotMarketShare :: (HasCallStack) => T.Text -> DataFrame -> IO ()+plotMarketShare colName df = plotMarketShareWith colName (defaultPlotConfig Pie) df++plotMarketShareWith :: (HasCallStack) => T.Text -> PlotConfig -> DataFrame -> IO ()+plotMarketShareWith colName config df = do+ let counts = getCategoricalCounts colName df+ case counts of+ Just c -> do+ let total = sum (map snd c)+ sorted = L.sortOn (negate . snd) c+ significantShares = takeWhile (\(_, v) -> v / total >= 0.02) sorted+ otherSum = sum [v | (_, v) <- c, v `notElem` map snd significantShares]++ formatShare (label, val) =+ let pct = round (100 * val / total) :: Int+ in (label <> " (" <> T.pack (show pct) <> "%)", val)++ shares = map formatShare significantShares+ finalShares =+ if otherSum > 0 && otherSum / total >= 0.01+ then shares <> [("Others (<2% each)", otherSum)]+ else shares++ let config' =+ config+ -- { plotSettings = (plotSettings config) {+ -- plotTitle = colName <> ": market share"+ -- }+ -- }+ T.putStrLn $ pie finalShares (plotSettings config')+ Nothing -> error $ "Column " ++ T.unpack colName ++ " is not categorical"
src/DataFrame/Display/Terminal/PrettyPrint.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}+ module DataFrame.Display.Terminal.PrettyPrint where import qualified Data.Text as T@@ -13,10 +14,10 @@ -- a type for describing table columns data ColDesc t = ColDesc- { colTitleFill :: Filler,- colTitle :: T.Text,- colValueFill :: Filler- }+ { colTitleFill :: Filler+ , colTitle :: T.Text+ , colValueFill :: Filler+ } -- functions that fill a string (s) to a given width (n) by adding pad -- character (c) to align left, right, or center@@ -45,13 +46,14 @@ showTable :: Bool -> [T.Text] -> [T.Text] -> [[T.Text]] -> T.Text showTable properMarkdown header types rows =- let consolidatedHeader = if properMarkdown then zipWith (\h t -> h <> "<br>" <> t) header types else header- cs = map (\h -> ColDesc center h left) consolidatedHeader- widths = [maximum $ map T.length col | col <- transpose $ consolidatedHeader : types : rows]- border = T.intercalate "---" [T.replicate width (T.singleton '-') | width <- widths]- separator = T.intercalate "-|-" [T.replicate width (T.singleton '-') | width <- widths]- fillCols fill cols = T.intercalate " | " [fill c width col | (c, width, col) <- zip3 cs widths cols]- lines = if properMarkdown- then border : fillCols colTitleFill consolidatedHeader : separator : map (fillCols colValueFill) rows- else border : fillCols colTitleFill consolidatedHeader : separator : fillCols colTitleFill types : separator : map (fillCols colValueFill) rows- in T.unlines lines+ let consolidatedHeader = if properMarkdown then zipWith (\h t -> h <> "<br>" <> t) header types else header+ cs = map (\h -> ColDesc center h left) consolidatedHeader+ widths = [maximum $ map T.length col | col <- transpose $ consolidatedHeader : types : rows]+ border = T.intercalate "---" [T.replicate width (T.singleton '-') | width <- widths]+ separator = T.intercalate "-|-" [T.replicate width (T.singleton '-') | width <- widths]+ fillCols fill cols = T.intercalate " | " [fill c width col | (c, width, col) <- zip3 cs widths cols]+ lines =+ if properMarkdown+ then border : fillCols colTitleFill consolidatedHeader : separator : map (fillCols colValueFill) rows+ else border : fillCols colTitleFill consolidatedHeader : separator : fillCols colTitleFill types : separator : map (fillCols colValueFill) rows+ in T.unlines lines
src/DataFrame/Errors.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-} module DataFrame.Errors where @@ -11,87 +11,99 @@ import Control.Exception import Data.Array import Data.Either-import DataFrame.Display.Terminal.Colours import Data.Typeable (Typeable)+import DataFrame.Display.Terminal.Colours import Type.Reflection (TypeRep) data TypeErrorContext a b = MkTypeErrorContext- { userType :: Either String (TypeRep a)- , expectedType :: Either String (TypeRep b)- , errorColumnName :: Maybe String- , callingFunctionName :: Maybe String- }+ { userType :: Either String (TypeRep a)+ , expectedType :: Either String (TypeRep b)+ , errorColumnName :: Maybe String+ , callingFunctionName :: Maybe String+ } data DataFrameException where- TypeMismatchException :: forall a b. (Typeable a, Typeable b)- => TypeErrorContext a b- -> DataFrameException+ TypeMismatchException ::+ forall a b.+ (Typeable a, Typeable b) =>+ TypeErrorContext a b ->+ DataFrameException ColumnNotFoundException :: T.Text -> T.Text -> [T.Text] -> DataFrameException+ EmptyDataSetException :: T.Text -> DataFrameException deriving (Exception) instance Show DataFrameException where show :: DataFrameException -> String- show (TypeMismatchException context) = let- errorString = typeMismatchError (either id show (userType context)) (either id show (expectedType context))- in addCallPointInfo (errorColumnName context) (callingFunctionName context) errorString+ show (TypeMismatchException context) =+ let+ errorString = typeMismatchError (either id show (userType context)) (either id show (expectedType context))+ in+ addCallPointInfo (errorColumnName context) (callingFunctionName context) errorString show (ColumnNotFoundException columnName callPoint availableColumns) = columnNotFound columnName callPoint availableColumns+ show (EmptyDataSetException callPoint) = emptyDataSetError callPoint columnNotFound :: T.Text -> T.Text -> [T.Text] -> String columnNotFound name callPoint columns =- red "\n\n[ERROR] "- ++ "Column not found: "- ++ T.unpack name- ++ " for operation "- ++ T.unpack callPoint- ++ "\n\tDid you mean "- ++ T.unpack (guessColumnName name columns)- ++ "?\n\n"+ red "\n\n[ERROR] "+ ++ "Column not found: "+ ++ T.unpack name+ ++ " for operation "+ ++ T.unpack callPoint+ ++ "\n\tDid you mean "+ ++ T.unpack (guessColumnName name columns)+ ++ "?\n\n" typeMismatchError :: String -> String -> String typeMismatchError givenType expectedType =- red $- red "\n\n[Error]: Type Mismatch"- ++ "\n\tWhile running your code I tried to "- ++ "get a column of type: "- ++ red (show givenType)- ++ " but the column in the dataframe was actually of type: "- ++ green (show expectedType)+ red $+ red "\n\n[Error]: Type Mismatch"+ ++ "\n\tWhile running your code I tried to "+ ++ "get a column of type: "+ ++ red (show givenType)+ ++ " but the column in the dataframe was actually of type: "+ ++ green (show expectedType) +emptyDataSetError :: T.Text -> String+emptyDataSetError callPoint =+ red "\n\n[ERROR] "+ ++ T.unpack callPoint+ ++ " cannot be called on empty data sets"+ addCallPointInfo :: Maybe String -> Maybe String -> String -> String addCallPointInfo (Just name) (Just cp) err =- err- ++ ( "\n\tThis happened when calling function "- ++ brightGreen cp- ++ " on the column "- ++ brightGreen name- ++ "\n\n"- ++ typeAnnotationSuggestion cp- )+ err+ ++ ( "\n\tThis happened when calling function "+ ++ brightGreen cp+ ++ " on the column "+ ++ brightGreen name+ ++ "\n\n"+ ++ typeAnnotationSuggestion cp+ ) addCallPointInfo Nothing (Just cp) err = err ++ "\n" ++ typeAnnotationSuggestion cp addCallPointInfo (Just name) Nothing err =- err- ++ ( "\n\tOn the column "- ++ name- ++ "\n\n"- )+ err+ ++ ( "\n\tOn the column "+ ++ name+ ++ "\n\n"+ ) addCallPointInfo Nothing Nothing err = err typeAnnotationSuggestion :: String -> String typeAnnotationSuggestion cp =- "\n\n\tTry adding a type at the end of the function e.g "- ++ "change\n\t\t"- ++ red (cp ++ " ...")- ++ " to \n\t\t"- ++ green ("(" ++ cp ++ " ... :: <Type>)")- ++ "\n\tor add "- ++ "{-# LANGUAGE TypeApplications #-} to the top of your "- ++ "file then change the call to \n\t\t"- ++ brightGreen (cp ++ " @<Type> ....")+ "\n\n\tTry adding a type at the end of the function e.g "+ ++ "change\n\t\t"+ ++ red (cp ++ " ...")+ ++ " to \n\t\t"+ ++ green ("(" ++ cp ++ " ... :: <Type>)")+ ++ "\n\tor add "+ ++ "{-# LANGUAGE TypeApplications #-} to the top of your "+ ++ "file then change the call to \n\t\t"+ ++ brightGreen (cp ++ " @<Type> ....") guessColumnName :: T.Text -> [T.Text] -> T.Text guessColumnName userInput columns = case map (\k -> (editDistance userInput k, k)) columns of- [] -> ""- res -> (snd . minimum) res+ [] -> ""+ res -> (snd . minimum) res editDistance :: T.Text -> T.Text -> Int editDistance xs ys = table ! (m, n)@@ -107,8 +119,8 @@ dist (0, j) = j dist (i, 0) = i dist (i, j) =- minimum- [ table ! (i - 1, j) + 1,- table ! (i, j - 1) + 1,- if x ! i == y ! j then table ! (i - 1, j - 1) else 1 + table ! (i - 1, j - 1)- ]+ minimum+ [ table ! (i - 1, j) + 1+ , table ! (i, j - 1) + 1+ , if x ! i == y ! j then table ! (i - 1, j - 1) else 1 + table ! (i - 1, j - 1)+ ]
src/DataFrame/Functions.hs view
@@ -1,49 +1,50 @@+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE UndecidableInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE TemplateHaskell #-}+ module DataFrame.Functions where import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (DataFrame(..), unsafeGetColumn)-import DataFrame.Internal.Expression (Expr(..), UExpr(..))+import DataFrame.Internal.DataFrame (DataFrame (..), unsafeGetColumn)+import DataFrame.Internal.Expression (Expr (..), UExpr (..)) -import Control.Monad-import Data.Function+import Control.Monad+import qualified Data.Char as Char+import Data.Function import qualified Data.List as L import qualified Data.Map as M import qualified Data.Text as T+import qualified Data.Vector as VB import qualified Data.Vector.Generic as VG import qualified Data.Vector.Unboxed as VU-import qualified Data.Vector as VB-import Language.Haskell.TH-import qualified Language.Haskell.TH.Syntax as TH-import qualified Data.Char as Char import Debug.Trace (traceShow)+import Language.Haskell.TH+import qualified Language.Haskell.TH.Syntax as TH import Type.Reflection (typeRep) -col :: Columnable a => T.Text -> Expr a+col :: (Columnable a) => T.Text -> Expr a col = Col -as :: Columnable a => Expr a -> T.Text -> (T.Text, UExpr)+as :: (Columnable a) => Expr a -> T.Text -> (T.Text, UExpr) as expr name = (name, Wrap expr) -lit :: Columnable a => a -> Expr a+lit :: (Columnable a) => a -> Expr a lit = Lit lift :: (Columnable a, Columnable b) => (a -> b) -> Expr a -> Expr b lift = Apply "udf" -lift2 :: (Columnable c, Columnable b, Columnable a) => (c -> b -> a) -> Expr c -> Expr b -> Expr a +lift2 :: (Columnable c, Columnable b, Columnable a) => (c -> b -> a) -> Expr c -> Expr b -> Expr a lift2 = BinOp "udf" eq :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool@@ -61,120 +62,126 @@ geq :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool geq = BinOp "geq" (>=) -count :: Columnable a => Expr a -> Expr Int+count :: (Columnable a) => Expr a -> Expr Int count (Col name) = GeneralAggregate name "count" VG.length count _ = error "Argument can only be a column reference not an unevaluated expression" -minimum :: Columnable a => Expr a -> Expr a+minimum :: (Columnable a) => Expr a -> Expr a minimum (Col name) = ReductionAggregate name "minimum" min -maximum :: Columnable a => Expr a -> Expr a+maximum :: (Columnable a) => Expr a -> Expr a maximum (Col name) = ReductionAggregate name "maximum" max -sum :: forall a . (Columnable a, Num a, VU.Unbox a) => Expr a -> Expr a+sum :: forall a. (Columnable a, Num a, VU.Unbox a) => Expr a -> Expr a sum (Col name) = NumericAggregate name "sum" VG.sum mean :: (Columnable a, Num a, VU.Unbox a) => Expr a -> Expr Double-mean (Col name) = let- mean' samp = let- (!total, !n) = VG.foldl' (\(!total, !n) v -> (total + v, n + 1)) (0 :: Double, 0 :: Int) samp- in total / fromIntegral n- in NumericAggregate name "mean" mean'+mean (Col name) =+ let+ mean' samp =+ let+ (!total, !n) = VG.foldl' (\(!total, !n) v -> (total + v, n + 1)) (0 :: Double, 0 :: Int) samp+ in+ total / fromIntegral n+ in+ NumericAggregate name "mean" mean' -- See Section 2.4 of the Haskell Report https://www.haskell.org/definition/haskell2010.pdf isReservedId :: T.Text -> Bool isReservedId t = case t of- "case" -> True- "class" -> True- "data" -> True- "default" -> True- "deriving" -> True- "do" -> True- "else" -> True- "foreign" -> True- "if" -> True- "import" -> True- "in" -> True- "infix" -> True- "infixl" -> True- "infixr" -> True- "instance" -> True- "let" -> True- "module" -> True- "newtype" -> True- "of" -> True- "then" -> True- "type" -> True- "where" -> True- _ -> False+ "case" -> True+ "class" -> True+ "data" -> True+ "default" -> True+ "deriving" -> True+ "do" -> True+ "else" -> True+ "foreign" -> True+ "if" -> True+ "import" -> True+ "in" -> True+ "infix" -> True+ "infixl" -> True+ "infixr" -> True+ "instance" -> True+ "let" -> True+ "module" -> True+ "newtype" -> True+ "of" -> True+ "then" -> True+ "type" -> True+ "where" -> True+ _ -> False isVarId :: T.Text -> Bool isVarId t = case T.uncons t of--- We might want to check c == '_' || Char.isLower c--- since the haskell report considers '_' a lowercase character--- However, to prevent an edge case where a user may have a--- "Name" and an "_Name_" in the same scope, wherein we'd end up--- with duplicate "_Name_"s, we eschew the check for '_' here.- Just (c, _) -> Char.isLower c && Char.isAlpha c- Nothing -> False+ -- We might want to check c == '_' || Char.isLower c+ -- since the haskell report considers '_' a lowercase character+ -- However, to prevent an edge case where a user may have a+ -- "Name" and an "_Name_" in the same scope, wherein we'd end up+ -- with duplicate "_Name_"s, we eschew the check for '_' here.+ Just (c, _) -> Char.isLower c && Char.isAlpha c+ Nothing -> False isHaskellIdentifier :: T.Text -> Bool-isHaskellIdentifier t = not (isVarId t) || isReservedId t+isHaskellIdentifier t = not (isVarId t) || isReservedId t sanitize :: T.Text -> T.Text sanitize t- | isValid = t- | isHaskellIdentifier t' = "_" <> t' <> "_"- | otherwise = t'+ | isValid = t+ | isHaskellIdentifier t' = "_" <> t' <> "_"+ | otherwise = t' where- isValid- = not (isHaskellIdentifier t)- && isVarId t- && T.all Char.isAlphaNum t+ isValid =+ not (isHaskellIdentifier t)+ && isVarId t+ && T.all Char.isAlphaNum t t' = T.map replaceInvalidCharacters . T.filter (not . parentheses) $ t replaceInvalidCharacters c- | Char.isUpper c = Char.toLower c- | Char.isSpace c = '_'- | Char.isPunctuation c = '_' -- '-' will also become a '_'- | Char.isSymbol c = '_'- | Char.isAlphaNum c = c -- Blanket condition- | otherwise = '_' -- If we're unsure we'll default to an underscore+ | Char.isUpper c = Char.toLower c+ | Char.isSpace c = '_'+ | Char.isPunctuation c = '_' -- '-' will also become a '_'+ | Char.isSymbol c = '_'+ | Char.isAlphaNum c = c -- Blanket condition+ | otherwise = '_' -- If we're unsure we'll default to an underscore parentheses c = case c of- '(' -> True- ')' -> True- '{' -> True- '}' -> True- '[' -> True- ']' -> True- _ -> False+ '(' -> True+ ')' -> True+ '{' -> True+ '}' -> True+ '[' -> True+ ']' -> True+ _ -> False typeFromString :: [String] -> Q Type-typeFromString [] = fail "No type specified"+typeFromString [] = fail "No type specified" typeFromString [t] = do- maybeType <- lookupTypeName t- case maybeType of- Just name -> return (ConT name)- Nothing -> fail $ "Unsupported type: " ++ t-typeFromString [tycon,t1] = do- outer <- typeFromString [tycon]- inner <- typeFromString [t1]- return (AppT outer inner)-typeFromString [tycon,t1,t2] = do- outer <- typeFromString [tycon]- lhs <- typeFromString [t1]- rhs <- typeFromString [t2]- return (AppT (AppT outer lhs) rhs)+ maybeType <- lookupTypeName t+ case maybeType of+ Just name -> return (ConT name)+ Nothing -> fail $ "Unsupported type: " ++ t+typeFromString [tycon, t1] = do+ outer <- typeFromString [tycon]+ inner <- typeFromString [t1]+ return (AppT outer inner)+typeFromString [tycon, t1, t2] = do+ outer <- typeFromString [tycon]+ lhs <- typeFromString [t1]+ rhs <- typeFromString [t2]+ return (AppT (AppT outer lhs) rhs) typeFromString s = fail $ "Unsupported type: " ++ (unwords s) declareColumns :: DataFrame -> DecsQ-declareColumns df = let- names = (map fst . L.sortBy (compare `on` snd). M.toList . columnIndices) df+declareColumns df =+ let+ names = (map fst . L.sortBy (compare `on` snd) . M.toList . columnIndices) df types = map (columnTypeString . (`unsafeGetColumn` df)) names specs = zipWith (\name type_ -> (sanitize name, type_)) names types- in fmap concat $ forM specs $ \(nm, tyStr) -> do- traceShow nm (pure ())- ty <- typeFromString (words tyStr)- let n = mkName (T.unpack nm)- sig <- sigD n [t| Expr $(pure ty) |]- val <- valD (varP n) (normalB [| col $(TH.lift nm) |]) []- pure [sig, val]+ in+ fmap concat $ forM specs $ \(nm, tyStr) -> do+ traceShow nm (pure ())+ ty <- typeFromString (words tyStr)+ let n = mkName (T.unpack nm)+ sig <- sigD n [t|Expr $(pure ty)|]+ val <- valD (varP n) (normalB [|col $(TH.lift nm)|]) []+ pure [sig, val]
src/DataFrame/IO/CSV.hs view
@@ -1,245 +1,376 @@+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE RankNTypes #-}+ module DataFrame.IO.CSV where -import qualified Data.ByteString.Char8 as C+import qualified Data.ByteString as BS+import qualified Data.ByteString.Builder as Builder+import qualified Data.ByteString.Char8 as C8 import qualified Data.List as L-import qualified Data.Map as M-import qualified Data.Set as S+import qualified Data.Map.Strict as M import qualified Data.Text as T-import qualified Data.Text.Lazy as TL-import qualified Data.Text.Lazy.IO as TLIO+import qualified Data.Text.Encoding as TE import qualified Data.Text.IO as TIO import qualified Data.Vector as V-import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Mutable as VM+import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Unboxed.Mutable as VUM -import Control.Applicative ((<$>), (<|>), (<*>), (<*), (*>), many)-import Control.Monad (forM_, zipWithM_, unless, void)-import Data.Attoparsec.Text+import Control.Applicative (many, (*>), (<$>), (<*), (<*>), (<|>))+import Control.Monad (forM_, unless, void, when, zipWithM_)+import Control.Monad.ST (runST)+import Data.Attoparsec.ByteString.Char8 hiding (endOfLine)+import Data.Bits (shiftL) import Data.Char-import DataFrame.Internal.Column (Column(..), MutableColumn(..), freezeColumn', writeColumn, columnLength)-import DataFrame.Internal.DataFrame (DataFrame(..))-import DataFrame.Internal.Parsing-import DataFrame.Operations.Typing-import Data.Foldable (fold)+import Data.Either import Data.Function (on) import Data.IORef import Data.Maybe-import Data.Text.Encoding (decodeUtf8Lenient)-import Data.Type.Equality- ( TestEquality (testEquality),- type (:~:) (Refl)- )+import Data.Type.Equality (+ TestEquality (testEquality),+ type (:~:) (Refl),+ )+import DataFrame.Internal.Column (Column (..), MutableColumn (..), columnLength, freezeColumn', writeColumn)+import DataFrame.Internal.DataFrame (DataFrame (..))+import DataFrame.Internal.Parsing+import DataFrame.Operations.Typing import GHC.IO.Handle (Handle)-import Prelude hiding (concat, takeWhile) import System.IO import Type.Reflection+import Prelude hiding (concat, takeWhile) --- | Record for CSV read options.-data ReadOptions = ReadOptions {- hasHeader :: Bool,- inferTypes :: Bool,- safeRead :: Bool-}+data GrowingVector a = GrowingVector+ { gvData :: !(IORef (VM.IOVector a))+ , gvSize :: !(IORef Int)+ , gvCapacity :: !(IORef Int)+ } --- | By default we assume the file has a header, we infer the types on read--- and we convert any rows with nullish objects into Maybe (safeRead).+data GrowingUnboxedVector a = GrowingUnboxedVector+ { guvData :: !(IORef (VUM.IOVector a))+ , guvSize :: !(IORef Int)+ , guvCapacity :: !(IORef Int)+ }++data GrowingColumn+ = GrowingInt !(GrowingUnboxedVector Int) !(IORef [Int])+ | GrowingDouble !(GrowingUnboxedVector Double) !(IORef [Int])+ | GrowingText !(GrowingVector T.Text) !(IORef [Int])++-- | CSV read parameters.+data ReadOptions = ReadOptions+ { hasHeader :: Bool+ -- ^ Whether or not the CSV file has a header. (default: True)+ , inferTypes :: Bool+ -- ^ Whether to try and infer types. (default: True)+ , 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)+ }+ defaultOptions :: ReadOptions-defaultOptions = ReadOptions { hasHeader = True, inferTypes = True, safeRead = True }+defaultOptions =+ ReadOptions+ { hasHeader = True+ , inferTypes = True+ , safeRead = True+ , chunkSize = 512_000+ } --- | Reads a CSV file from the given path.--- Note this file stores intermediate temporary files--- while converting the CSV from a row to a columnar format.+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__+@+ghci> D.readCsv "./data/taxi.csv" df++@+-} readCsv :: String -> IO DataFrame readCsv = readSeparated ',' defaultOptions --- | Reads a tab separated file from the given path.--- Note this file stores intermediate temporary files--- while converting the CSV from a row to a columnar format.+{- | Read TSV (tab separated) file from path and load it into a dataframe.++==== __Example__+@+ghci> D.readTsv "./data/taxi.tsv" df++@+-} readTsv :: String -> IO DataFrame readTsv = readSeparated '\t' defaultOptions --- | Reads a character separated file into a dataframe using mutable vectors.+{- | Read text file with specified delimiter into a dataframe.++==== __Example__+@+ghci> D.readSeparated ';' D.defaultOptions "./data/taxi.txt" df++@+-} readSeparated :: Char -> ReadOptions -> String -> IO DataFrame-readSeparated c opts path = do- totalRows <- countRows c path+readSeparated !sep !opts !path = do withFile path ReadMode $ \handle -> do- firstRow <- map T.strip . parseSep c <$> TIO.hGetLine handle- let columnNames = if hasHeader opts- then map (T.filter (/= '\"')) firstRow- else map (T.singleton . intToDigit) [0..(length firstRow - 1)]- -- If there was no header rewind the file cursor.+ hSetBuffering handle (BlockBuffering (Just (chunkSize opts)))++ firstLine <- C8.hGetLine handle+ let firstRow = parseLine sep firstLine+ columnNames =+ if hasHeader opts+ then map (T.filter (/= '\"') . TE.decodeUtf8Lenient) firstRow+ else map (T.singleton . intToDigit) [0 .. length firstRow - 1]+ unless (hasHeader opts) $ hSeek handle AbsoluteSeek 0 - -- Initialize mutable vectors for each column- let numColumns = length columnNames- let numRows = if hasHeader opts then totalRows - 1 else totalRows- -- Use this row to infer the types of the rest of the column.- -- TODO: this isn't robust but in so far as this is a guess anyway- -- it's probably fine. But we should probably sample n rows and pick- -- the most likely type from the sample.- dataRow <- map T.strip . parseSep c <$> TIO.hGetLine handle+ dataLine <- C8.hGetLine handle+ let dataRow = parseLine sep dataLine+ growingCols <- initializeColumns dataRow opts - -- This array will track the indices of all null values for each column.- -- If any exist then the column will be an optional type.- nullIndices <- VM.unsafeNew numColumns- VM.set nullIndices []- mutableCols <- VM.unsafeNew numColumns- getInitialDataVectors numRows mutableCols dataRow+ processRow 0 dataRow growingCols - -- Read rows into the mutable vectors- fillColumns numRows c mutableCols nullIndices handle+ processFile handle sep growingCols (chunkSize opts) 1 - -- Freeze the mutable vectors into immutable ones- nulls' <- V.unsafeFreeze nullIndices- cols <- V.mapM (freezeColumn mutableCols nulls' opts) (V.generate numColumns id)- return $ DataFrame {- columns = cols,- columnIndices = M.fromList (zip columnNames [0..]),- dataframeDimensions = (maybe 0 columnLength (cols V.!? 0), V.length cols)- }-{-# INLINE readSeparated #-}+ frozenCols <- V.fromList <$> mapM freezeGrowingColumn growingCols -getInitialDataVectors :: Int -> VM.IOVector MutableColumn -> [T.Text] -> IO ()-getInitialDataVectors n mCol xs = do- forM_ (zip [0..] xs) $ \(i, x) -> do- col <- case inferValueType x of- "Int" -> MUnboxedColumn <$> ((VUM.unsafeNew n :: IO (VUM.IOVector Int)) >>= \c -> VUM.unsafeWrite c 0 (fromMaybe 0 $ readInt x) >> return c)- "Double" -> MUnboxedColumn <$> ((VUM.unsafeNew n :: IO (VUM.IOVector Double)) >>= \c -> VUM.unsafeWrite c 0 (fromMaybe 0 $ readDouble x) >> return c)- _ -> MBoxedColumn <$> ((VM.unsafeNew n :: IO (VM.IOVector T.Text)) >>= \c -> VM.unsafeWrite c 0 x >> return c)- VM.unsafeWrite mCol i col-{-# INLINE getInitialDataVectors #-}+ let numRows = maybe 0 columnLength (frozenCols V.!? 0) -inferValueType :: T.Text -> T.Text-inferValueType s = let- example = s- in case readInt example of- Just _ -> "Int"- Nothing -> case readDouble example of- Just _ -> "Double"- Nothing -> "Other"-{-# INLINE inferValueType #-}+ return $+ DataFrame+ { columns = frozenCols+ , columnIndices = M.fromList (zip columnNames [0 ..])+ , dataframeDimensions = (numRows, V.length frozenCols)+ } --- | Reads rows from the handle and stores values in mutable vectors.-fillColumns :: Int -> Char -> VM.IOVector MutableColumn -> VM.IOVector [(Int, T.Text)] -> Handle -> IO ()-fillColumns n c mutableCols nullIndices handle = do- input <- newIORef (mempty :: T.Text)- forM_ [1..n] $ \i -> do- isEOF <- hIsEOF handle- input' <- readIORef input- unless (isEOF && input' == mempty) $ do- parseWith (TIO.hGetChunk handle) (parseRow c) input' >>= \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 :: T.Text) (row :: [T.Text]) -> do- writeIORef input unconsumed- zipWithM_ (writeValue mutableCols nullIndices i) [0..] row-{-# INLINE fillColumns #-}+initializeColumns :: [BS.ByteString] -> ReadOptions -> IO [GrowingColumn]+initializeColumns row opts = mapM initColumn row+ where+ initColumn :: BS.ByteString -> IO GrowingColumn+ initColumn bs = do+ nullsRef <- newIORef []+ let val = TE.decodeUtf8Lenient bs+ if inferTypes opts+ then case inferType val of+ IntType -> GrowingInt <$> newGrowingUnboxedVector 1_024 <*> pure nullsRef+ DoubleType -> GrowingDouble <$> newGrowingUnboxedVector 1_024 <*> pure nullsRef+ TextType -> GrowingText <$> newGrowingVector 1_024 <*> pure nullsRef+ else GrowingText <$> newGrowingVector 1_024 <*> pure nullsRef --- | Writes a value into the appropriate column, resizing the vector if necessary.-writeValue :: VM.IOVector MutableColumn -> VM.IOVector [(Int, T.Text)] -> Int -> Int -> T.Text -> IO ()-writeValue mutableCols nullIndices count colIndex value = do- col <- VM.unsafeRead mutableCols colIndex- res <- writeColumn count value col- let modify value = VM.unsafeModify nullIndices ((count, value) :) colIndex- either modify (const (return ())) res-{-# INLINE writeValue #-}+data InferredType = IntType | DoubleType | TextType --- | Freezes a mutable vector into an immutable one, trimming it to the actual row count.-freezeColumn :: VM.IOVector MutableColumn -> V.Vector [(Int, T.Text)] -> ReadOptions -> Int -> IO Column-freezeColumn mutableCols nulls opts colIndex = do- col <- VM.unsafeRead mutableCols colIndex- freezeColumn' (nulls V.! colIndex) col-{-# INLINE freezeColumn #-}+inferType :: T.Text -> InferredType+inferType !t+ | T.null t = TextType+ | isJust (readInt t) = IntType+ | isJust (readDouble t) = DoubleType+ | otherwise = TextType -parseSep :: Char -> T.Text -> [T.Text]-parseSep c s = either error id (parseOnly (record c) s)-{-# INLINE parseSep #-}+processRow :: Int -> [BS.ByteString] -> [GrowingColumn] -> IO ()+processRow !rowIdx !vals !cols = zipWithM_ (processValue rowIdx) vals cols+ where+ processValue :: Int -> BS.ByteString -> GrowingColumn -> IO ()+ processValue !idx !bs !col = do+ let !val = TE.decodeUtf8Lenient 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 ->+ if isNull val+ then do+ appendGrowingVector gv T.empty+ modifyIORef' nulls (idx :)+ else appendGrowingVector gv val -record :: Char -> Parser [T.Text]-record c =- field c `sepBy1` char c- <?> "record"-{-# INLINE record #-}+isNull :: T.Text -> Bool+isNull t = T.null t || t == "NA" || t == "NULL" || t == "null" -parseRow :: Char -> Parser [T.Text]-parseRow c = (record c <* lineEnd) <?> "record-new-line"+processFile :: Handle -> Char -> [GrowingColumn] -> Int -> Int -> IO ()+processFile !handle !sep !cols !chunk r = do+ let go remain !rowIdx = do+ 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 (row == [] || unconsumed == mempty) $ go unconsumed $! rowIdx + 1+ return ()+ go "" r -field :: Char -> Parser T.Text-field c =- quotedField <|> unquotedField c- <?> "field"-{-# INLINE field #-}+parseLine :: Char -> BS.ByteString -> [BS.ByteString]+parseLine !sep = fromRight [] . parseOnly (record sep) -unquotedTerminators :: Char -> S.Set Char-unquotedTerminators sep = S.fromList [sep, '\n', '\r', '"']+parseRow :: Char -> Parser [C8.ByteString]+parseRow sep = record sep <* (endOfLine <|> endOfInput) <?> "CSV row" -unquotedField :: Char -> Parser T.Text-unquotedField sep =- takeWhile (not . (`S.member` terminators)) <?> "unquoted field"- where terminators = unquotedTerminators sep-{-# INLINE unquotedField #-}+record :: Char -> Parser [BS.ByteString]+record sep = field sep `sepBy` char sep <?> "CSV record" -quotedField :: Parser T.Text-quotedField = char '"' *> contents <* char '"' <?> "quoted field"- where- contents = fold <$> many (unquote <|> unescape)- where- unquote = takeWhile1 (notInClass "\"\\")- unescape = char '\\' *> do- T.singleton <$> do- char '\\' <|> char '"'-{-# INLINE quotedField #-}+field :: Char -> Parser BS.ByteString+field sep = quotedField <|> unquotedField sep <?> "CSV field" -lineEnd :: Parser ()-lineEnd =- (endOfLine <|> endOfInput)- <?> "end of line"-{-# INLINE lineEnd #-}+unquotedField :: Char -> Parser BS.ByteString+unquotedField sep = takeWhile (\c -> c /= sep && c /= '\n' && c /= '\r') <?> "unquoted field" --- | First pass to count rows for exact allocation-countRows :: Char -> FilePath -> IO Int-countRows c path = withFile path ReadMode $! go 0 ""- where- go n input h = do- isEOF <- hIsEOF h- if isEOF && input == mempty- then pure n- else- parseWith (TIO.hGetChunk h) (parseRow c) input >>= \case- Fail unconsumed ctx er -> do- erpos <- hTell h- fail $ "Failed to parse CSV file around " <> show erpos <> " byte; due: "- <> show er <> "; context: " <> show ctx <> " " <> show unconsumed- Partial c -> do- fail $ "Partial handler is called; n = " <> show n- Done (unconsumed :: T.Text) _ ->- go (n + 1) unconsumed h-{-# INLINE countRows #-}+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 '"' *> pure (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+ 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))+ BoxedColumn <$> V.freeze mvec+freezeGrowingColumn (GrowingDouble gv nullsRef) = do+ vec <- freezeGrowingUnboxedVector gv+ nulls <- readIORef nullsRef+ if null nulls+ 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))+ BoxedColumn <$> V.freeze mvec+freezeGrowingColumn (GrowingText gv nullsRef) = do+ vec <- freezeGrowingVector gv+ nulls <- readIORef nullsRef+ if null nulls+ 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))+ BoxedColumn <$> V.freeze mvec+ writeCsv :: String -> DataFrame -> IO () writeCsv = writeSeparated ',' -writeSeparated :: Char -- ^ Separator- -> String -- ^ Path to write to- -> DataFrame- -> IO ()-writeSeparated c filepath df = withFile filepath WriteMode $ \handle ->do+writeSeparated ::+ -- | Separator+ Char ->+ -- | Path to write to+ String ->+ DataFrame ->+ IO ()+writeSeparated c filepath df = withFile filepath WriteMode $ \handle -> do let (rows, columns) = dataframeDimensions df let headers = map fst (L.sortBy (compare `on` snd) (M.toList (columnIndices df))) TIO.hPutStrLn handle (T.intercalate ", " headers)- forM_ [0..(rows - 1)] $ \i -> do+ forM_ [0 .. (rows - 1)] $ \i -> do let row = getRowAsText df i TIO.hPutStrLn handle (T.intercalate ", " row) @@ -249,43 +380,46 @@ indexMap = M.fromList (map (\(a, b) -> (b, a)) $ M.toList (columnIndices df)) go k (BoxedColumn (c :: V.Vector a)) acc = case c V.!? i of Just e -> textRep : acc- where textRep = case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl -> e- Nothing -> case typeRep @a of- App t1 t2 -> case eqTypeRep t1 (typeRep @Maybe) of- Just HRefl -> case testEquality t2 (typeRep @T.Text) of- Just Refl -> fromMaybe "null" e- Nothing -> (fromOptional . (T.pack . show)) e- where fromOptional s- | T.isPrefixOf "Just " s = T.drop (T.length "Just ") s- | otherwise = "null"- Nothing -> (T.pack . show) e- _ -> (T.pack . show) e+ where+ textRep = case testEquality (typeRep @a) (typeRep @T.Text) of+ Just Refl -> e+ Nothing -> case typeRep @a of+ App t1 t2 -> case eqTypeRep t1 (typeRep @Maybe) of+ Just HRefl -> case testEquality t2 (typeRep @T.Text) of+ Just Refl -> fromMaybe "null" e+ Nothing -> (fromOptional . (T.pack . show)) e+ where+ fromOptional s+ | T.isPrefixOf "Just " s = T.drop (T.length "Just ") s+ | otherwise = "null"+ Nothing -> (T.pack . show) e+ _ -> (T.pack . show) e Nothing -> error $ "Column "- ++ T.unpack (indexMap M.! k)- ++ " has less items than "- ++ "the other columns at index "- ++ show i+ ++ T.unpack (indexMap M.! k)+ ++ " has less items than "+ ++ "the other columns at index "+ ++ show i go k (UnboxedColumn c) acc = case c VU.!? i of Just e -> T.pack (show e) : acc Nothing -> error $ "Column "- ++ T.unpack (indexMap M.! k)- ++ " has less items than "- ++ "the other columns at index "- ++ show i+ ++ T.unpack (indexMap M.! k)+ ++ " has less items than "+ ++ "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+ where+ textRep = case testEquality (typeRep @a) (typeRep @T.Text) of+ Just Refl -> fromMaybe "Nothing" e+ Nothing -> (T.pack . show) e Nothing -> error $ "Column "- ++ T.unpack (indexMap M.! k)- ++ " has less items than "- ++ "the other columns at index "- ++ show i+ ++ T.unpack (indexMap M.! k)+ ++ " has less items than "+ ++ "the other columns at index "+ ++ show i
src/DataFrame/IO/Parquet.hs view
@@ -1,1560 +1,250 @@-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE RecordWildCards #-}--module DataFrame.IO.Parquet (- readParquet- ) where--import Codec.Compression.Zstd.Streaming-import Control.Monad-import qualified Data.ByteString as BSO-import qualified Data.ByteString.Char8 as BS-import Data.Char-import Data.Foldable-import qualified Data.Vector.Unboxed as VU-import Data.IORef-import qualified Data.Map as M-import Data.Maybe-import qualified Data.Text as T-import DataFrame.Internal.DataFrame (DataFrame)-import qualified DataFrame.Internal.DataFrame as DI-import qualified DataFrame.Internal.Column as DI-import qualified DataFrame.Operations.Core as DI-import qualified Snappy as Snappy-import Foreign-import GHC.Float-import GHC.IO (unsafePerformIO)-import System.IO--import DataFrame.IO.Parquet.ColumnStatistics-import DataFrame.IO.Parquet.Compression-import DataFrame.IO.Parquet.Encoding-import DataFrame.IO.Parquet.Page-import DataFrame.IO.Parquet.Types--footerSize :: Integer-footerSize = 8--data PageEncodingStats = PageEncodingStats- { pageEncodingPageType :: PageType,- pageEncoding :: ParquetEncoding,- pagesWithEncoding :: Int32- }- deriving (Show, Eq)--emptyPageEncodingStats :: PageEncodingStats-emptyPageEncodingStats = PageEncodingStats PAGE_TYPE_UNKNOWN PARQUET_ENCODING_UNKNOWN 0--data SizeStatistics = SizeStatisics- { unencodedByteArrayDataTypes :: Int64,- repetitionLevelHistogram :: [Int64],- definitionLevelHistogram :: [Int64]- }- deriving (Show, Eq)--emptySizeStatistics :: SizeStatistics-emptySizeStatistics = SizeStatisics 0 [] []--data BoundingBox = BoundingBox- { xmin :: Double,- xmax :: Double,- ymin :: Double,- ymax :: Double,- zmin :: Double,- zmax :: Double,- mmin :: Double,- mmax :: Double- }- deriving (Show, Eq)--emptyBoundingBox :: BoundingBox-emptyBoundingBox = BoundingBox 0 0 0 0 0 0 0 0--data GeospatialStatistics = GeospatialStatistics- { bbox :: BoundingBox,- geospatialTypes :: [Int32]- }- deriving (Show, Eq)--emptyGeospatialStatistics :: GeospatialStatistics-emptyGeospatialStatistics = GeospatialStatistics emptyBoundingBox []--emptyKeyValue :: KeyValue-emptyKeyValue = KeyValue {key = "", value = ""}--data ColumnMetaData = ColumnMetaData- { columnType :: ParquetType,- columnEncodings :: [ParquetEncoding],- columnPathInSchema :: [String],- columnCodec :: CompressionCodec,- columnNumValues :: Int64,- columnTotalUncompressedSize :: Int64,- columnTotalCompressedSize :: Int64,- columnKeyValueMetadata :: [KeyValue],- columnDataPageOffset :: Int64,- columnIndexPageOffset :: Int64,- columnDictionaryPageOffset :: Int64,- columnStatistics :: ColumnStatistics,- columnEncodingStats :: [PageEncodingStats],- bloomFilterOffset :: Int64,- bloomFilterLength :: Int32,- columnSizeStatistics :: SizeStatistics,- columnGeospatialStatistics :: GeospatialStatistics- }- deriving (Show, Eq)--emptyColumnMetadata :: ColumnMetaData-emptyColumnMetadata = ColumnMetaData PARQUET_TYPE_UNKNOWN [] [] COMPRESSION_CODEC_UNKNOWN 0 0 0 [] 0 0 0 emptyColumnStatistics [] 0 0 emptySizeStatistics emptyGeospatialStatistics--data ColumnCryptoMetadata- = COLUMN_CRYPTO_METADATA_UNKNOWN- | ENCRYPTION_WITH_FOOTER_KEY- | EncryptionWithColumnKey- { columnCryptPathInSchema :: [String],- columnKeyMetadata :: [Word8]- }- deriving (Show, Eq)--data ColumnChunk = ColumnChunk- { columnChunkFilePath :: String,- columnChunkMetadataFileOffset :: Int64,- columnMetaData :: ColumnMetaData,- columnChunkOffsetIndexOffset :: Int64,- columnChunkOffsetIndexLength :: Int32,- columnChunkColumnIndexOffset :: Int64,- columnChunkColumnIndexLength :: Int32,- cryptoMetadata :: ColumnCryptoMetadata,- encryptedColumnMetadata :: [Word8]- }- deriving (Show, Eq)--emptyColumnChunk :: ColumnChunk-emptyColumnChunk = ColumnChunk "" 0 emptyColumnMetadata 0 0 0 0 COLUMN_CRYPTO_METADATA_UNKNOWN []--data SortingColumn = SortingColumn- { columnIndex :: Int32,- columnOrderDescending :: Bool,- nullFirst :: Bool- }- deriving (Show, Eq)--emptySortingColumn :: SortingColumn-emptySortingColumn = SortingColumn 0 False False--data RowGroup = RowGroup- { rowGroupColumns :: [ColumnChunk],- totalByteSize :: Int64,- rowGroupNumRows :: Int64,- rowGroupSortingColumns :: [SortingColumn],- fileOffset :: Int64,- totalCompressedSize :: Int64,- ordinal :: Int16- }- deriving (Show, Eq)--emptyRowGroup :: RowGroup-emptyRowGroup = RowGroup [] 0 0 [] 0 0 0--data ColumnOrder- = TYPE_ORDER- | COLUMN_ORDER_UNKNOWN- deriving (Show, Eq)--data EncryptionAlgorithm- = ENCRYPTION_ALGORITHM_UNKNOWN- | AesGcmV1- { aadPrefix :: [Word8],- aadFileUnique :: [Word8],- supplyAadPrefix :: Bool- }- | AesGcmCtrV1- { aadPrefix :: [Word8],- aadFileUnique :: [Word8],- supplyAadPrefix :: Bool- }- deriving (Show, Eq)--data KeyValue = KeyValue- { key :: String,- value :: String- }- deriving (Show, Eq)--data FileMetadata = FileMetaData- { version :: Int32,- schema :: [SchemaElement],- numRows :: Integer,- rowGroups :: [RowGroup],- keyValueMetadata :: [KeyValue],- createdBy :: Maybe String,- columnOrders :: [ColumnOrder],- encryptionAlgorithm :: EncryptionAlgorithm,- footerSigningKeyMetadata :: [Word8]- }- deriving (Show, Eq)--defaultMetadata :: FileMetadata-defaultMetadata =- FileMetaData- { version = 0,- schema = [],- numRows = 0,- rowGroups = [],- keyValueMetadata = [],- createdBy = Nothing,- columnOrders = [],- encryptionAlgorithm = ENCRYPTION_ALGORITHM_UNKNOWN,- footerSigningKeyMetadata = []- }--readParquet :: String -> IO DataFrame-readParquet path = withBinaryFile path ReadMode $ \handle -> do- (size, magicString) <- readMetadataSizeFromFooter handle- when (magicString /= "PAR1") $ error "Invalid Parquet file"-- colMap <- newIORef (M.empty :: (M.Map T.Text DI.Column))- colNames <- newIORef ([] :: [T.Text])-- fileMetadata <- readMetadata handle size- forM_ (rowGroups fileMetadata) $ \r -> do- forM_ (rowGroupColumns r) $ \c -> do- let metadata = columnMetaData c- let colDataPageOffset = columnDataPageOffset metadata- let colDictionaryPageOffset = columnDictionaryPageOffset metadata- let colStart = if colDictionaryPageOffset > 0 && colDataPageOffset > colDictionaryPageOffset- then colDictionaryPageOffset- else colDataPageOffset- let colLength = columnTotalCompressedSize metadata- columnBytes <-readBytes handle colStart colLength- (maybePage, res) <- readPage (columnCodec metadata) columnBytes- case maybePage of- Just p -> if isDictionaryPage p- then do- (maybePage', res') <- readPage (columnCodec metadata) res- let p' = fromMaybe (error "Empty page") maybePage'- let schemaElem = filter (\se -> (elementName se) == (T.pack $ head (columnPathInSchema metadata))) (schema fileMetadata)- let rep = if null schemaElem then UNKNOWN_REPETITION_TYPE else ((repetitionType . head) schemaElem)- when (rep == REPEATED || rep == UNKNOWN_REPETITION_TYPE) (error $ "REPETITION TYPE NOT SUPPORTED: " ++ show rep)- - case ((definitionLevelEncoding . pageTypeHeader . pageHeader ) p') of- ERLE -> do- let rleColumn = case columnType metadata of- PBYTE_ARRAY -> readByteArrayColumn (pageBytes p)- PDOUBLE -> readDoubleColumn (pageBytes p)- PINT32 -> readInt32Column (pageBytes p)- t -> error $ "UNKNOWN TYPE: " ++ (show t)- let nbytes = littleEndianInt32 (take 4 (pageBytes p'))- let rleDecoder = MkRleDecoder (drop 4 (pageBytes p')) 1 0 0-- let bytesAfterLevels =- case rep of- REQUIRED -> pageBytes p' -- no def-levels to skip- OPTIONAL -> -- skip def-level RLE block (len + payload)- let nbytes = littleEndianInt32 (take 4 (pageBytes p'))- in drop (fromIntegral nbytes + 4) (pageBytes p')- REPEATED -> error "REPEATED not supported"- let bitWidth = head bytesAfterLevels- let indexDecoder = MkRleDecoder (tail bytesAfterLevels) (fromIntegral bitWidth) 0 0-- let finalCol = DI.takeColumn ((fromIntegral . dataPageHeaderNumValues . pageTypeHeader . pageHeader) p') (decodeDictionary rleColumn rleDecoder indexDecoder)- let colName = T.pack $ head (columnPathInSchema metadata)-- modifyIORef' colNames (++[colName]) - modifyIORef' colMap (\m -> M.insertWith (\l r -> fromMaybe (error "UNEXPECTED") (DI.concatColumns l r)) colName finalCol m)- other -> error $ "UNSUPPORTED ENCODING: " ++ (show other)- else do- -- p is the first page we read; since it’s not a dictionary page, it must be a Data Page- let schemaElem = filter (\se -> (elementName se) == (T.pack $ head (columnPathInSchema metadata))) (schema fileMetadata)- let rep = if null schemaElem then UNKNOWN_REPETITION_TYPE else ((repetitionType . head) schemaElem)- when (rep == REPEATED || rep == UNKNOWN_REPETITION_TYPE) (error $ "REPETITION TYPE NOT SUPPORTED: " ++ show rep)-- -- Determine how many values to read from this page- let nVals = case pageTypeHeader (pageHeader p) of- DataPageHeader{..} -> fromIntegral dataPageHeaderNumValues- DataPageHeaderV2{..} -> fromIntegral dataPageHeaderV2NumValues- _ -> error "Unexpected page header for data page"-- -- For REQUIRED fields there are no level streams to skip in Data Page v1.- -- For Data Page v2, def/rep sections are at the front; REQUIRED => both lengths are 0.- let bytesAfterLevels = case pageTypeHeader (pageHeader p) of- DataPageHeader{..} -> pageBytes p -- v1, REQUIRED => no level bytes- DataPageHeaderV2{..} -> drop (fromIntegral (definitionLevelByteLength + repetitionLevelByteLength)) (pageBytes p)- _ -> pageBytes p-- -- Check the value encoding; support PLAIN here- let enc = case pageTypeHeader (pageHeader p) of- DataPageHeader{..} -> dataPageHeaderEncoding- DataPageHeaderV2{..} -> dataPageHeaderV2Encoding- _ -> PARQUET_ENCODING_UNKNOWN- when (enc /= EPLAIN) (error $ "Unsupported non-dictionary encoding: " ++ show enc)-- -- Read values in PLAIN format- let ptype = columnType metadata- finalCol <-- case ptype of- PINT32 -> let (col, _) = readPlainColumn PINT32 nVals bytesAfterLevels in pure col- PDOUBLE -> let (col, _) = readPlainColumn PDOUBLE nVals bytesAfterLevels in pure col- PBYTE_ARRAY ->- let (col, _) = readPlainColumn PBYTE_ARRAY nVals bytesAfterLevels in pure col- PFIXED_LEN_BYTE_ARRAY ->- let len = fromIntegral (typeLength (head schemaElem))- (col, _) = readPlainFixedLenColumn len nVals bytesAfterLevels- in pure col- other -> error $ "PLAIN not implemented for: " ++ show other-- let colName = T.pack $ head (columnPathInSchema metadata)- modifyIORef' colNames (++[colName])- modifyIORef' colMap (\m -> M.insertWith (\l r -> fromMaybe (error "UNEXPECTED") (DI.concatColumns l r)) colName finalCol m)- Nothing -> pure ()-- c' <- readIORef colMap- colNames' <- readIORef colNames- let asscList = map (\name -> (name, c' M.! name)) colNames'- pure $ DI.fromNamedColumns asscList---- Reads exactly `n` values in PLAIN encoding and returns (column, remainingBytes)-readPlainColumn :: ParquetType -> Int -> [Word8] -> (DI.Column, [Word8])-readPlainColumn PINT32 n bs =- let (vals, rest) = readNInt32 n bs- in (DI.fromList vals, rest)--readPlainColumn PDOUBLE n bs =- let (vals, rest) = readNDouble n bs- in (DI.fromList vals, rest)---- PLAIN BYTE_ARRAY: 4-byte little-endian length followed by bytes-readPlainColumn PBYTE_ARRAY n bs =- let (vals, rest) = readNByteArrays n bs- in (DI.fromList (map (T.pack . map (chr . fromIntegral)) vals), rest)---- PLAIN FIXED_LEN_BYTE_ARRAY: each value is `len` bytes (from schema element)-readPlainColumn PFIXED_LEN_BYTE_ARRAY n bs =- error "FIXED_LEN_BYTE_ARRAY requires typeLength from schema; use readPlainFixedLenColumn len"--readPlainFixedLenColumn :: Int -> Int -> [Word8] -> (DI.Column, [Word8])-readPlainFixedLenColumn len n bs =- let (vals, rest) = splitFixed n len bs- in (DI.fromList (map (T.pack . map (chr . fromIntegral)) vals), rest)---- Helpers-readNInt32 :: Int -> [Word8] -> ([Int32],[Word8])-readNInt32 0 bs = ([], bs)-readNInt32 k bs =- let x = littleEndianInt32 (take 4 bs)- bs' = drop 4 bs- (xs, rest) = readNInt32 (k-1) bs'- in (x:xs, rest)--readNDouble :: Int -> [Word8] -> ([Double],[Word8])-readNDouble 0 bs = ([], bs)-readNDouble k bs =- let x = castWord64ToDouble (littleEndianWord64 (take 8 bs))- bs' = drop 8 bs- (xs, rest) = readNDouble (k-1) bs'- in (x:xs, rest)--readNByteArrays :: Int -> [Word8] -> ([[Word8]],[Word8])-readNByteArrays 0 bs = ([], bs)-readNByteArrays k bs =- let len = fromIntegral (littleEndianInt32 (take 4 bs)) :: Int- body = take len (drop 4 bs)- bs' = drop (4 + len) bs- (xs, rest) = readNByteArrays (k-1) bs'- in (body:xs, rest)--splitFixed :: Int -> Int -> [Word8] -> ([[Word8]],[Word8])-splitFixed 0 _ bs = ([], bs)-splitFixed k len bs =- let body = take len bs- bs' = drop len bs- (xs, rest) = splitFixed (k-1) len bs'- in (body:xs, rest)--decodeDictionary :: DI.Column -> RleDecoder -> RleDecoder -> DI.Column-decodeDictionary col rleDecoder indexDecoder- | repCount indexDecoder > 0 = error "UNIMPLEMENTED: Repetition not supported"- | litCount indexDecoder > 0 = decodeDictionary (DI.atIndicesStable (VU.map fromIntegral (getIndices indexDecoder)) col) rleDecoder (indexDecoder { litCount = 0 })- | otherwise = let- (finished, indexDecoder') = advance indexDecoder- in if finished then col else decodeDictionary col rleDecoder indexDecoder'--advance :: RleDecoder -> (Bool, RleDecoder)-advance indexDecoder - | (rleDecoderData indexDecoder) == [] = (True, indexDecoder)- | otherwise = let- (indicator, remaining) = readUVarInt (rleDecoderData indexDecoder)- isLiteral = (indicator .&. 1) /= 0- countValues = (fromIntegral (indicator `shiftR` 1) :: Int32)- litCount = if isLiteral then (countValues * 8) else 0- in if isLiteral then (False, indexDecoder { rleDecoderData = remaining, litCount = litCount }) else (True, indexDecoder) -- (error "NON-LITERAL TYPES NOT YET SUPPORTED")--getIndices :: RleDecoder -> VU.Vector Word32-getIndices indexDecoder- | rleBitWidth indexDecoder == 5 = unpackWidth5 (rleDecoderData indexDecoder)- | rleBitWidth indexDecoder == 1 = unpackWidth1 (rleDecoderData indexDecoder)- | rleBitWidth indexDecoder == 2 = unpackWidth2 (rleDecoderData indexDecoder)- | rleBitWidth indexDecoder == 3 = unpackWidth3 (rleDecoderData indexDecoder)- | otherwise = error $ "Unsupported bit width: " ++ (show (rleBitWidth indexDecoder))--unpackWidth5 :: [Word8] -> VU.Vector Word32-unpackWidth5 [] = VU.empty-unpackWidth5 bytes = let- n0 = littleEndianWord32 $ take 4 bytes- n1 = littleEndianWord32 $ take 4 $ drop 4 bytes- n2 = littleEndianWord32 $ take 4 $ drop 8 bytes- n3 = littleEndianWord32 $ take 4 $ drop 12 bytes- n4 = littleEndianWord32 $ take 4 $ drop 16 bytes- out0 = (n0 .>>. 0) `mod` (1 .<<. 5)- out1 = (n0 .>>. 5) `mod` (1 .<<. 5)- out2 = (n0 .>>. 10) `mod` (1 .<<. 5)- out3 = (n0 .>>. 15) `mod` (1 .<<. 5)- out4 = (n0 .>>. 20) `mod` (1 .<<. 5)- out5 = (n0 .>>. 25) `mod` (1 .<<. 5)- out6 = (n0 .>>. 30) .|. ((n1 `mod` (1 .<<. 3)) .<<. (5 - 3))- out7 = (n1 .>>. 3) `mod` (1 .<<. 5)- out8 = (n1 .>>. 8) `mod` (1 .<<. 5)- out9 = (n1 .>>. 13) `mod` (1 .<<. 5)- out10 = (n1 .>>. 18) `mod` (1 .<<. 5)- out11 = (n1 .>>. 23) `mod` (1 .<<. 5)- out12 = (n1 .>>. 28) .|. (n2 `mod` (1 .<<. 1)) .<<. (5 - 1)- out13 = (n2 .>>. 1) `mod` (1 .<<. 5)- out14 = (n2 .>>. 6) `mod` (1 .<<. 5)- out15 = (n2 .>>. 11) `mod` (1 .<<. 5)- out16 = (n2 .>>. 16) `mod` (1 .<<. 5)- out17 = (n2 .>>. 21) `mod` (1 .<<. 5)- out18 = (n2 .>>. 26) `mod` (1 .<<. 5)- out19 = (n2 .>>. 31) .|. (n3 `mod` (1 .<<. 4)) .<<. (5 - 4)- out20 = (n3 .>>. 4) `mod` (1 .<<. 5)- out21 = (n3 .>>. 9) `mod` (1 .<<. 5)- out22 = (n3 .>>. 14) `mod` (1 .<<. 5)- out23 = (n3 .>>. 19) `mod` (1 .<<. 5)- out24 = (n3 .>>. 24) `mod` (1 .<<. 5)- out25 = (n3 .>>. 29) .|. (n4 `mod` (1 .<<. 2)) .<<. (5 - 2)- out26 = (n4 .>>. 2) `mod` (1 .<<. 5)- out27 = (n4 .>>. 7) `mod` (1 .<<. 5)- out28 = (n4 .>>. 12) `mod` (1 .<<. 5)- out29 = (n4 .>>. 17) `mod` (1 .<<. 5)- out30 = (n4 .>>. 22) `mod` (1 .<<. 5)- out31 = (n4 .>>. 27)- in (VU.fromList [out0,out1,out2,out3,out4,out5,out6,out7,out8,out9,out10,out11,out12,out13,out14,out15,out16,out17,out18,out19,out20,out21,out22,out23,out24,out25,out26,out27,out28,out29,out30,out31]) VU.++ (unpackWidth5 (drop 20 bytes))--unpackWidth2, unpackWidth1, unpackWidth3 :: [Word8] -> VU.Vector Word32-unpackWidth1 [] = VU.empty-unpackWidth1 bytes = let- n = littleEndianWord32 $ take 4 bytes- in VU.fromList (map (\i -> (n .>>. i) .&. 1) [0..31]) VU.++ (unpackWidth1 (drop 4 bytes))-unpackWidth2 [] = VU.empty-unpackWidth2 bytes = let- n = littleEndianWord32 $ take 4 bytes- in VU.fromList (map (\i -> (n .>>. (i * 2)) `mod` (1 .<<. 2)) [0..14] ++ [n .>>. 30]) VU.++ (unpackWidth2 (drop 4 bytes))-unpackWidth3 [] = VU.empty-unpackWidth3 bytes = let- n0 = littleEndianWord32 $ take 4 bytes- n1 = littleEndianWord32 $ take 4 $ drop 4 bytes- n2 = littleEndianWord32 $ take 4 $ drop 8 bytes- out0 = (n0 .>>. 0) `mod` (1 .<<. 3)- out1 = (n0 .>>. 3) `mod` (1 .<<. 3)- out2 = (n0 .>>. 6) `mod` (1 .<<. 3)- out3 = (n0 .>>. 9) `mod` (1 .<<. 3)- out4 = (n0 .>>. 12) `mod` (1 .<<. 3)- out5 = (n0 .>>. 15) `mod` (1 .<<. 3)- out6 = (n0 .>>. 18) `mod` (1 .<<. 3)- out7 = (n0 .>>. 21) `mod` (1 .<<. 3)- out8 = (n0 .>>. 24) `mod` (1 .<<. 3)- out9 = (n0 .>>. 27) `mod` (1 .<<. 3)- out10 = (n0 .>>. 30) .|. (n1 `mod` (1 .<<. 1)) .<<. (3 - 1)- out11 = (n1 .>>. 1) `mod` (1 .<<. 3)- out12 = (n1 .>>. 4) `mod` (1 .<<. 3)- out13 = (n1 .>>. 7) `mod` (1 .<<. 3)- out14 = (n1 .>>. 10) `mod` (1 .<<. 3)- out15 = (n1 .>>. 13) `mod` (1 .<<. 3)- out16 = (n1 .>>. 16) `mod` (1 .<<. 3)- out17 = (n1 .>>. 19) `mod` (1 .<<. 3)- out18 = (n1 .>>. 22) `mod` (1 .<<. 3)- out19 = (n1 .>>. 25) `mod` (1 .<<. 3)- out20 = (n1 .>>. 28) `mod` (1 .<<. 3)- out21 = ((n1 .>>. 31) `mod` (1 .<<. 3)) .|. (n2 `mod` (1 .<<. 2)) .<<. (3 - 2)- out22 = (n2 .>>. 2) `mod` (1 .<<. 3)- out23 = (n2 .>>. 5) `mod` (1 .<<. 3)- out24 = (n2 .>>. 8) `mod` (1 .<<. 3)- out25 = (n2 .>>. 11) `mod` (1 .<<. 3)- out26 = (n2 .>>. 14) `mod` (1 .<<. 3)- out27 = (n2 .>>. 17) `mod` (1 .<<. 3)- out28 = (n2 .>>. 20) `mod` (1 .<<. 3)- out29 = (n2 .>>. 23) `mod` (1 .<<. 3)- out30 = (n2 .>>. 26) `mod` (1 .<<. 3)- out31 = (n2 .>>. 29)- in (VU.fromList [out0,out1,out2,out3,out4,out5,out6,out7,out8,out9,out10,out11,out12,out13,out14,out15,out16,out17,out18,out19,out20,out21,out22,out23,out24,out25,out26,out27,out28,out29,out30,out31]) VU.++ (unpackWidth3 (drop 12 bytes))--data RleDecoder = MkRleDecoder { rleDecoderData :: [Word8]- , rleBitWidth :: Int32- , repCount :: Int32- , litCount :: Int32- } deriving (Show, Eq)--expandDictionary :: [Word8] -> [Word8]-expandDictionary (bitWidth:rest) = rest--readInt32Column :: [Word8] -> DI.Column-readInt32Column = DI.fromList . readPageInt32--readDoubleColumn :: [Word8] -> DI.Column-readDoubleColumn = DI.fromList . readPageWord64--readByteArrayColumn :: [Word8] -> DI.Column-readByteArrayColumn = DI.fromList .readPageBytes--readPageInt32 :: [Word8] -> [Int32]-readPageInt32 [] = []-readPageInt32 xs = (fromIntegral (littleEndianInt32 (take 4 xs))) : readPageInt32 (drop 4 xs)--readPageWord64 :: [Word8] -> [Double]-readPageWord64 [] = []-readPageWord64 xs = (castWord64ToDouble (littleEndianWord64 (take 8 xs))) : readPageWord64 (drop 8 xs)--readPageBytes :: [Word8] -> [T.Text]-readPageBytes [] = []-readPageBytes xs = let- lenBytes = fromIntegral (littleEndianInt32 $ take 4 xs)- totalBytesRead = lenBytes + 4- in T.pack (map (chr . fromIntegral) $ take lenBytes (drop 4 xs)) : readPageBytes (drop totalBytesRead xs)--readPage :: CompressionCodec -> [Word8] -> IO (Maybe Page, [Word8])-readPage c [] = pure (Nothing, [])-readPage c columnBytes = do- let (hdr, rem) = readPageHeader emptyPageHeader columnBytes 0- let compressed = take (fromIntegral $ compressedPageSize hdr) rem- - -- Weird round about way to uncompress zstd files compressed using the- -- streaming API- fullData <- case c of- ZSTD -> do- Consume dFunc <- decompress- Consume dFunc' <- dFunc (BSO.pack compressed)- Done res <- dFunc' BSO.empty- pure res- SNAPPY -> case Snappy.decompress (BSO.pack compressed) of- Left e -> error (show e)- Right res -> pure res- UNCOMPRESSED -> pure (BSO.pack compressed)- comp -> error ("UNSUPPORTED_COMPRESSION TYPE: " ++ (show comp))- pure $ (Just $ Page hdr (BSO.unpack fullData), drop (fromIntegral $ compressedPageSize hdr) rem)--data Page = Page { pageHeader :: PageHeader- , pageBytes :: [Word8] } deriving (Show, Eq)--data PageHeader = PageHeader { pageHeaderPageType :: PageType- , uncompressedPageSize :: Int32- , compressedPageSize ::Int32- , pageHeaderCrcChecksum :: Int32- , pageTypeHeader :: PageTypeHeader- } deriving (Show, Eq)---emptyPageHeader = PageHeader PAGE_TYPE_UNKNOWN 0 0 0 PAGE_TYPE_HEADER_UNKNOWN--isDataPage :: Page -> Bool-isDataPage page = case pageTypeHeader (pageHeader page) of- DataPageHeader {..} -> True- DataPageHeaderV2 {..} -> True- _ -> False--isDictionaryPage :: Page -> Bool-isDictionaryPage page = case pageTypeHeader (pageHeader page) of- DictionaryPageHeader {..} -> True- _ -> False--data PageTypeHeader = DataPageHeader { dataPageHeaderNumValues :: Int32- , dataPageHeaderEncoding :: ParquetEncoding- , definitionLevelEncoding :: ParquetEncoding- , repetitionLevelEncoding :: ParquetEncoding- , dataPageHeaderStatistics :: ColumnStatistics- }- | DataPageHeaderV2 { dataPageHeaderV2NumValues :: Int32- , dataPageHeaderV2NumNulls :: Int32- , dataPageHeaderV2NumRows :: Int32- , dataPageHeaderV2Encoding :: ParquetEncoding- , definitionLevelByteLength :: Int32- , repetitionLevelByteLength :: Int32- , dataPageHeaderV2IsCompressed :: Bool- , dataPageHeaderV2Statistics :: ColumnStatistics- }- | DictionaryPageHeader { dictionaryPageHeaderNumValues :: Int32- , dictionaryPageHeaderEncoding :: ParquetEncoding- , dictionaryPageIsSorted :: Bool - }- | INDEX_PAGE_HEADER- | PAGE_TYPE_HEADER_UNKNOWN deriving (Show, Eq)--emptyDictionaryPageHeader = DictionaryPageHeader 0 PARQUET_ENCODING_UNKNOWN False-emptyDataPageHeader = DataPageHeader 0 PARQUET_ENCODING_UNKNOWN PARQUET_ENCODING_UNKNOWN PARQUET_ENCODING_UNKNOWN emptyColumnStatistics-emptyDataPageHeaderV2 = DataPageHeaderV2 0 0 0 PARQUET_ENCODING_UNKNOWN 0 0 False emptyColumnStatistics --readPageHeader :: PageHeader -> [Word8] -> Int16 -> (PageHeader, [Word8])-readPageHeader hdr [] _ = (hdr, [])-readPageHeader hdr xs lastFieldId = let- fieldContents = readField' xs lastFieldId- in case fieldContents of- Nothing -> (hdr, tail xs)- Just (rem, elemType, identifier) -> case identifier of- 1 -> let- (pType, rem') = readInt32FromBytes rem- in readPageHeader (hdr {pageHeaderPageType = pageTypeFromInt pType}) rem' identifier- 2 -> let- (uncompressedPageSize, rem') = readInt32FromBytes rem- in readPageHeader (hdr {uncompressedPageSize = uncompressedPageSize}) rem' identifier- 3 -> let- (compressedPageSize, rem') = readInt32FromBytes rem- in readPageHeader (hdr {compressedPageSize = compressedPageSize}) rem' identifier- 5 -> let- (dataPageHeader, rem') = readPageTypeHeader emptyDataPageHeader rem 0- in readPageHeader (hdr {pageTypeHeader = dataPageHeader}) rem' identifier- 7 -> let- (dictionaryPageHeader, rem') = readPageTypeHeader emptyDictionaryPageHeader rem 0- in readPageHeader (hdr {pageTypeHeader = dictionaryPageHeader}) rem' identifier- n -> error $ show n--readPageTypeHeader :: PageTypeHeader -> [Word8] -> Int16 -> (PageTypeHeader, [Word8])-readPageTypeHeader hdr [] _ = (hdr, [])-readPageTypeHeader hdr@(DictionaryPageHeader {..}) xs lastFieldId = let- fieldContents = readField' xs lastFieldId- in case fieldContents of- Nothing -> (hdr, tail xs)- Just (rem, elemType, identifier) -> case identifier of- 1 -> let- (numValues, rem') = readInt32FromBytes rem- in readPageTypeHeader (hdr {dictionaryPageHeaderNumValues = numValues}) rem' identifier- 2 -> let- (enc, rem') = readInt32FromBytes rem- in readPageTypeHeader (hdr {dictionaryPageHeaderEncoding = parquetEncodingFromInt enc}) rem' identifier- 3 -> let- (isSorted: rem') = rem- in readPageTypeHeader (hdr {dictionaryPageIsSorted = isSorted == compactBooleanTrue}) rem' identifier- n -> error $ show n-readPageTypeHeader hdr@(DataPageHeader {..}) xs lastFieldId = let- fieldContents = readField' xs lastFieldId- in case fieldContents of- Nothing -> (hdr, tail xs)- Just (rem, elemType, identifier) -> case identifier of- 1 -> let- (numValues, rem') = readInt32FromBytes rem- in readPageTypeHeader (hdr {dataPageHeaderNumValues = numValues}) rem' identifier- 2 -> let- (enc, rem') = readInt32FromBytes rem- in readPageTypeHeader (hdr {dataPageHeaderEncoding = parquetEncodingFromInt enc}) rem' identifier- 3 -> let- (enc, rem') = readInt32FromBytes rem- in readPageTypeHeader (hdr {definitionLevelEncoding = parquetEncodingFromInt enc}) rem' identifier- 4 -> let- (enc, rem') = readInt32FromBytes rem- in readPageTypeHeader (hdr {repetitionLevelEncoding = parquetEncodingFromInt enc}) rem' identifier- 5 -> let- (stats, rem') = readStatisticsFromBytes emptyColumnStatistics rem 0- in readPageTypeHeader (hdr {dataPageHeaderStatistics = stats}) rem' identifier- n -> error $ show n--readStatisticsFromBytes :: ColumnStatistics -> [Word8] -> Int16 -> (ColumnStatistics, [Word8])-readStatisticsFromBytes cs xs lastFieldId = let- fieldContents = readField' xs lastFieldId- in case fieldContents of- Nothing -> (cs, tail xs)- Just (rem, elemType, identifier) -> case identifier of- 1 -> let- (maxInBytes, rem') = readByteStringFromBytes rem- in readStatisticsFromBytes (cs {columnMax = maxInBytes}) rem' identifier- 2 -> let- (minInBytes, rem') = readByteStringFromBytes rem- in readStatisticsFromBytes (cs {columnMin = minInBytes}) rem' identifier- 3 -> let- (nullCount, rem') = readIntFromBytes @Int64 rem- in readStatisticsFromBytes (cs {columnNullCount = nullCount}) rem' identifier- 4 -> let- (distinctCount, rem') = readIntFromBytes @Int64 rem- in readStatisticsFromBytes (cs {columnDistictCount = distinctCount}) rem' identifier- 5 -> let- (maxInBytes, rem') = readByteStringFromBytes rem- in readStatisticsFromBytes (cs {columnMaxValue = maxInBytes}) rem' identifier- 6 -> let- (minInBytes, rem') = readByteStringFromBytes rem- in readStatisticsFromBytes (cs {columnMinValue = minInBytes}) rem' identifier- 7 -> let- (isMaxValueExact: rem') = rem- in readStatisticsFromBytes (cs {isColumnMaxValueExact = isMaxValueExact == compactBooleanTrue}) rem' identifier- 8 -> let- (isMinValueExact: rem') = rem- in readStatisticsFromBytes (cs {isColumnMinValueExact = isMinValueExact == compactBooleanTrue}) rem' identifier- n -> error $ show n--readBytes :: Handle -> Int64 -> Int64 -> IO [Word8]-readBytes handle colStart colLen = do- buf <- mallocBytes (fromIntegral colLen) :: IO (Ptr Word8)- hSeek handle AbsoluteSeek (fromIntegral colStart)- _ <- hGetBuf handle buf (fromIntegral colLen) - columnBytes <- readByteString' buf colLen- free buf- pure columnBytes--numBytesInFile :: Handle -> IO Integer-numBytesInFile handle = do- hSeek handle SeekFromEnd 0- hTell handle--readMetadataSizeFromFooter :: Handle -> IO (Integer, BS.ByteString)-readMetadataSizeFromFooter handle = do- footerOffSet <- numBytesInFile handle-- buf <- mallocBytes (fromIntegral footerSize) :: IO (Ptr Word8)-- hSeek handle AbsoluteSeek (fromIntegral $! footerOffSet - footerSize)- n <- hGetBuf handle buf (fromIntegral footerSize)-- -- The bytes that store the metadata size.- sizeBytes <- mapM (\i -> fromIntegral <$> (peekElemOff buf i :: IO Word8) :: IO Int32) [0 .. 3]- let size = fromIntegral $ foldl' (.|.) 0 $! zipWith shift sizeBytes [0, 8, 16, 24]-- magicStringBytes <- mapM (\i -> peekElemOff buf i :: IO Word8) [4 .. 7]- let magicString = BSO.pack magicStringBytes- free buf- return (size, magicString)--readMetadata :: Handle -> Integer -> IO FileMetadata-readMetadata handle size = do- metaDataBuf <- mallocBytes (fromIntegral size) :: IO (Ptr Word8)- footerOffSet <- numBytesInFile handle-- hSeek handle AbsoluteSeek (fromIntegral $! footerOffSet - footerSize - size)-- metadataBytesRead <- hGetBuf handle metaDataBuf (fromIntegral size)- let lastFieldId = 0- let fieldStack = []- bufferPos <- newIORef (0 :: Int)- metadata <- readFileMetaData defaultMetadata metaDataBuf bufferPos lastFieldId fieldStack- free metaDataBuf- return metadata--readFileMetaData :: FileMetadata -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO FileMetadata-readFileMetaData metadata metaDataBuf bufferPos lastFieldId fieldStack = do- fieldContents <- readField metaDataBuf bufferPos lastFieldId fieldStack- case fieldContents of- Nothing -> return metadata- Just (elemType, identifier) -> case identifier of- 1 -> do- version <- readIntFromBuffer @Int32 metaDataBuf bufferPos- readFileMetaData (metadata {version = version}) metaDataBuf bufferPos identifier fieldStack- 2 -> do- -- We can do some type checking/exception handling here.- -- Check elemType == List- sizeAndType <- readAndAdvance bufferPos metaDataBuf- let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int- -- type of the contents of the list.- let elemType = toTType sizeAndType- schemaElements <- replicateM sizeOnly (readSchemaElement defaultSchemaElement metaDataBuf bufferPos 0 [])- readFileMetaData (metadata {schema = schemaElements}) metaDataBuf bufferPos identifier fieldStack- 3 -> do- numRows <- readIntFromBuffer @Int64 metaDataBuf bufferPos- readFileMetaData (metadata {numRows = fromIntegral numRows}) metaDataBuf bufferPos identifier fieldStack- 4 -> do- -- We can do some type checking/exception handling here.- -- Check elemType == List- sizeAndType <- readAndAdvance bufferPos metaDataBuf- let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int- -- type of the contents of the list.- let elemType = toTType sizeAndType- rowGroups <- replicateM sizeOnly (readRowGroup emptyRowGroup metaDataBuf bufferPos 0 [])- readFileMetaData (metadata {rowGroups = rowGroups}) metaDataBuf bufferPos identifier fieldStack- 5 -> do- -- We can do some type checking/exception handling here.- -- Check elemType == List- sizeAndType <- readAndAdvance bufferPos metaDataBuf- let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int- -- type of the contents of the list.- let elemType = toTType sizeAndType- keyValueMetadata <- replicateM sizeOnly (readKeyValue emptyKeyValue metaDataBuf bufferPos 0 [])- readFileMetaData (metadata {keyValueMetadata = keyValueMetadata}) metaDataBuf bufferPos identifier fieldStack- 6 -> do- createdBy <- readString metaDataBuf bufferPos- readFileMetaData (metadata {createdBy = Just createdBy}) metaDataBuf bufferPos identifier fieldStack- 7 -> do- sizeAndType <- readAndAdvance bufferPos metaDataBuf- let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int- let elemType = toTType sizeAndType- columnOrders <- replicateM sizeOnly (readColumnOrder metaDataBuf bufferPos 0 [])- readFileMetaData (metadata {columnOrders = columnOrders}) metaDataBuf bufferPos identifier fieldStack- 8 -> do- encryptionAlgorithm <- readEncryptionAlgorithm metaDataBuf bufferPos 0 []- readFileMetaData (metadata {encryptionAlgorithm = encryptionAlgorithm}) metaDataBuf bufferPos identifier fieldStack- 9 -> do- footerSigningKeyMetadata <- readByteString metaDataBuf bufferPos- readFileMetaData (metadata {footerSigningKeyMetadata = footerSigningKeyMetadata}) metaDataBuf bufferPos identifier fieldStack- n -> return $ error $ "UNIMPLEMENTED " ++ show n--readEncryptionAlgorithm :: Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO EncryptionAlgorithm-readEncryptionAlgorithm buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return ENCRYPTION_ALGORITHM_UNKNOWN- Just (elemType, identifier) -> case identifier of- 1 -> do- readAesGcmV1 (AesGcmV1 {aadPrefix = [], aadFileUnique = [], supplyAadPrefix = False}) buf pos 0 []- 2 -> do- readAesGcmCtrV1 (AesGcmCtrV1 {aadPrefix = [], aadFileUnique = [], supplyAadPrefix = False}) buf pos 0 []- n -> return ENCRYPTION_ALGORITHM_UNKNOWN--readAesGcmV1 :: EncryptionAlgorithm -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO EncryptionAlgorithm-readAesGcmV1 v@(AesGcmV1 aadPrefix aadFileUnique supplyAadPrefix) buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return v- Just (elemType, identifier) -> case identifier of- 1 -> do- aadPrefix <- readByteString buf pos- readAesGcmV1 (v {aadPrefix = aadPrefix}) buf pos lastFieldId fieldStack- 2 -> do- aadFileUnique <- readByteString buf pos- readAesGcmV1 (v {aadFileUnique = aadFileUnique}) buf pos lastFieldId fieldStack- 3 -> do- supplyAadPrefix <- readAndAdvance pos buf- readAesGcmV1 (v {supplyAadPrefix = supplyAadPrefix == compactBooleanTrue}) buf pos lastFieldId fieldStack- _ -> return ENCRYPTION_ALGORITHM_UNKNOWN--readAesGcmCtrV1 :: EncryptionAlgorithm -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO EncryptionAlgorithm-readAesGcmCtrV1 v@(AesGcmCtrV1 aadPrefix aadFileUnique supplyAadPrefix) buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return v- Just (elemType, identifier) -> case identifier of- 1 -> do- aadPrefix <- readByteString buf pos- readAesGcmCtrV1 (v {aadPrefix = aadPrefix}) buf pos lastFieldId fieldStack- 2 -> do- aadFileUnique <- readByteString buf pos- readAesGcmCtrV1 (v {aadFileUnique = aadFileUnique}) buf pos lastFieldId fieldStack- 3 -> do- supplyAadPrefix <- readAndAdvance pos buf- readAesGcmCtrV1 (v {supplyAadPrefix = supplyAadPrefix == compactBooleanTrue}) buf pos lastFieldId fieldStack- _ -> return ENCRYPTION_ALGORITHM_UNKNOWN--readColumnOrder :: Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO ColumnOrder-readColumnOrder buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return COLUMN_ORDER_UNKNOWN- Just (elemType, identifier) -> case identifier of- 1 -> do- _ <- replicateM_ 2 (readTypeOrder buf pos 0 [])- return TYPE_ORDER- _ -> return COLUMN_ORDER_UNKNOWN--readTypeOrder :: Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO ColumnOrder-readTypeOrder buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return TYPE_ORDER- Just (elemType, identifier) -> if elemType == STOP- then return TYPE_ORDER - else readTypeOrder buf pos identifier fieldStack--data SchemaElement = SchemaElement- { elementName :: T.Text,- elementType :: TType,- typeLength :: Int32,- numChildren :: Int32,- fieldId :: Int32,- repetitionType :: RepetitionType,- convertedType :: Int32,- scale :: Int32,- precision :: Int32,- logicalType :: LogicalType- }- deriving (Show, Eq)--data RepetitionType = REQUIRED | OPTIONAL | REPEATED | UNKNOWN_REPETITION_TYPE deriving (Eq, Show)--data LogicalType- = STRING_TYPE- | MAP_TYPE- | LIST_TYPE- | ENUM_TYPE- | DECIMAL_TYPE- | DATE_TYPE- | DecimalType {decimalTypePrecision :: Int32, decimalTypeScale :: Int32}- | TimeType {isAdjustedToUTC :: Bool, unit :: TimeUnit}- | -- This should probably have a different, more constrained TimeUnit type.- TimestampType {isAdjustedToUTC :: Bool, unit :: TimeUnit}- | IntType {bitWidth :: Int8, intIsSigned :: Bool}- | LOGICAL_TYPE_UNKNOWN- | JSON_TYPE- | BSON_TYPE- | UUID_TYPE- | FLOAT16_TYPE- | VariantType {specificationVersion :: Int8}- | GeometryType {crs :: T.Text}- | GeographyType {crs :: T.Text, algorithm :: EdgeInterpolationAlgorithm}- deriving (Eq, Show)--data TimeUnit- = MILLISECONDS- | MICROSECONDS- | NANOSECONDS- | TIME_UNIT_UNKNOWN- deriving (Eq, Show)--data EdgeInterpolationAlgorithm- = SPHERICAL- | VINCENTY- | THOMAS- | ANDOYER- | KARNEY- deriving (Eq, Show)--repetitionTypeFromInt :: Int32 -> RepetitionType-repetitionTypeFromInt 0 = REQUIRED-repetitionTypeFromInt 1 = OPTIONAL-repetitionTypeFromInt 2 = REPEATED-repetitionTypeFromInt _ = UNKNOWN_REPETITION_TYPE--defaultSchemaElement :: SchemaElement-defaultSchemaElement = SchemaElement "" STOP 0 0 (-1) UNKNOWN_REPETITION_TYPE 0 0 0 LOGICAL_TYPE_UNKNOWN--toIntegralType :: Int32 -> TType-toIntegralType n- | n == 0 = BOOL- | n == 1 = I32- | n == 2 = I64- | n == 3 = I64- | n == 4 = DOUBLE- | n == 5 = DOUBLE- | n == 6 = STRING- | n == 7 = STRING- | otherwise = error $ "Unknown integral type: " ++ show n--readSchemaElement :: SchemaElement -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO SchemaElement-readSchemaElement schemaElement buf pos lastFieldId fieldStack = do- t <- readAndAdvance pos buf- if t .&. 0x0f == 0- then return schemaElement- else do- let modifier = fromIntegral ((t .&. 0xf0) `shiftR` 4) :: Int16- identifier <-- if modifier == 0- then readIntFromBuffer @Int16 buf pos- else return (lastFieldId + modifier)- let elemType = toTType (t .&. 0x0f)- case identifier of- 1 -> do- schemaElemType <- toIntegralType <$> readInt32FromBuffer buf pos- readSchemaElement (schemaElement {elementType = schemaElemType}) buf pos identifier fieldStack- 2 -> do- typeLength <- readInt32FromBuffer buf pos- readSchemaElement (schemaElement {typeLength = typeLength}) buf pos identifier fieldStack- 3 -> do- fieldRepetitionType <- readInt32FromBuffer buf pos- readSchemaElement (schemaElement {repetitionType = repetitionTypeFromInt fieldRepetitionType}) buf pos identifier fieldStack- 4 -> do- nameSize <- readVarIntFromBuffer @Int buf pos- contents <- replicateM nameSize (readAndAdvance pos buf)- readSchemaElement (schemaElement {elementName = T.pack (map (chr . fromIntegral) contents)}) buf pos identifier fieldStack- 5 -> do- numChildren <- readInt32FromBuffer buf pos- readSchemaElement (schemaElement {numChildren = numChildren}) buf pos identifier fieldStack- 6 -> do- convertedType <- readInt32FromBuffer buf pos- readSchemaElement (schemaElement {convertedType = convertedType}) buf pos identifier fieldStack- 7 -> do- scale <- readInt32FromBuffer buf pos- readSchemaElement (schemaElement {scale = scale}) buf pos identifier fieldStack- 8 -> do- precision <- readInt32FromBuffer buf pos- readSchemaElement (schemaElement {precision = precision}) buf pos identifier fieldStack- 9 -> do- fieldId <- readInt32FromBuffer buf pos- readSchemaElement (schemaElement {fieldId = fieldId}) buf pos identifier fieldStack- 10 -> do- logicalType <- readLogicalType buf pos 0 []- readSchemaElement (schemaElement {logicalType = logicalType}) buf pos identifier fieldStack- _ -> error $ show identifier -- return schemaElement--readLogicalType :: Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO LogicalType-readLogicalType buf pos lastFieldId fieldStack = do- t <- readAndAdvance pos buf- if t .&. 0x0f == 0- then return LOGICAL_TYPE_UNKNOWN- else do- let modifier = fromIntegral ((t .&. 0xf0) `shiftR` 4) :: Int16- identifier <-- if modifier == 0- then readIntFromBuffer @Int16 buf pos- else return (lastFieldId + modifier)- let elemType = toTType (t .&. 0x0f)- case identifier of- 1 -> do- replicateM_ 2 (readField buf pos 0 [])- return STRING_TYPE- 2 -> do- replicateM_ 2 (readField buf pos 0 [])- return MAP_TYPE- 3 -> do- replicateM_ 2 (readField buf pos 0 [])- return LIST_TYPE- 4 -> do- replicateM_ 2 (readField buf pos 0 [])- return ENUM_TYPE- 5 -> do- _ <- readField buf pos 0 []- readDecimalType (DecimalType {decimalTypeScale = 0, decimalTypePrecision = 0}) buf pos 0 []- 6 -> do- replicateM_ 2 (readField buf pos 0 [])- return DATE_TYPE- 7 -> do- _ <- readField buf pos 0 []- readTimeType (TimeType {isAdjustedToUTC = False, unit = MILLISECONDS}) buf pos 0 []- 8 -> do- _ <- readField buf pos 0 []- readTimeType (TimestampType {isAdjustedToUTC = False, unit = MILLISECONDS}) buf pos 0 []- -- Apparently reserved for interval types- 9 -> return LOGICAL_TYPE_UNKNOWN- 10 -> do- _ <- readField buf pos 0 []- readIntType (IntType {intIsSigned = False, bitWidth = 0}) buf pos 0 []- 11 -> do- replicateM_ 2 (readField buf pos 0 [])- return LOGICAL_TYPE_UNKNOWN- 12 -> do- replicateM_ 2 (readField buf pos 0 [])- return JSON_TYPE- 13 -> do- replicateM_ 2 (readField buf pos 0 [])- return BSON_TYPE- 14 -> do- replicateM_ 2 (readField buf pos 0 [])- return UUID_TYPE- 15 -> do- replicateM_ 2 (readField buf pos 0 [])- return FLOAT16_TYPE- 16 -> do- _ <- readField buf pos 0 []- return VariantType {specificationVersion = 1}- 17 -> do- _ <- readField buf pos 0 []- return GeometryType {crs = ""}- 18 -> do- _ <- readField buf pos 0 []- return GeographyType {crs = "", algorithm = SPHERICAL}- _ -> return LOGICAL_TYPE_UNKNOWN--readIntType :: LogicalType -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO LogicalType-readIntType v@(IntType bitWidth intIsSigned) buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return v- Just (elemType, identifier) -> case identifier of- 1 -> do- bitWidth <- readAndAdvance pos buf- readIntType (v {bitWidth = fromIntegral bitWidth}) buf pos lastFieldId fieldStack- 2 -> do- -- TODO: Check for empty- intIsSigned <- readAndAdvance pos buf- readIntType (v {intIsSigned = intIsSigned == compactBooleanTrue}) buf pos lastFieldId fieldStack- _ -> error $ "UNKNOWN field ID for IntType" ++ show identifier--readDecimalType :: LogicalType -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO LogicalType-readDecimalType v@(DecimalType p s) buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return v- Just (elemType, identifier) -> case identifier of- 1 -> do- s' <- readInt32FromBuffer buf pos- readDecimalType (v {decimalTypeScale = s'}) buf pos lastFieldId fieldStack- 2 -> do- p' <- readInt32FromBuffer buf pos- readDecimalType (v {decimalTypePrecision = p'}) buf pos lastFieldId fieldStack- _ -> error $ "UNKNOWN field ID for DecimalType" ++ show identifier--readTimeType :: LogicalType -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO LogicalType-readTimeType v@(TimeType _ _) buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return v- Just (elemType, identifier) -> case identifier of- 1 -> do- -- TODO: Check for empty- isAdjustedToUTC <- readAndAdvance pos buf- readTimeType (v {isAdjustedToUTC = isAdjustedToUTC == compactBooleanTrue}) buf pos lastFieldId fieldStack- 2 -> do- u <- readUnit buf pos 0 []- readTimeType (v {unit = u}) buf pos lastFieldId fieldStack- _ -> error $ "UNKNOWN field ID for TimeType" ++ show identifier-readTimeType v@(TimestampType _ _) buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return v- Just (elemType, identifier) -> case identifier of- 1 -> do- -- TODO: Check for empty- isAdjustedToUTC <- readAndAdvance pos buf- readTimeType (v {isAdjustedToUTC = isAdjustedToUTC == compactBooleanTrue}) buf pos lastFieldId fieldStack- 2 -> do- u <- readUnit buf pos 0 []- readTimeType (v {unit = u}) buf pos lastFieldId fieldStack- _ -> error $ "UNKNOWN field ID for TimestampType" ++ show identifier--readUnit :: Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO TimeUnit-readUnit buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return TIME_UNIT_UNKNOWN- Just (elemType, identifier) -> case identifier of- 1 -> do- _ <- readField buf pos 0 []- return MILLISECONDS- 2 -> do- _ <- readField buf pos 0 []- return MICROSECONDS- 3 -> do- _ <- readField buf pos 0 []- return NANOSECONDS- _ -> return TIME_UNIT_UNKNOWN--readRowGroup :: RowGroup -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO RowGroup-readRowGroup r buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return r- Just (elemType, identifier) -> case identifier of- 1 -> do- sizeAndType <- readAndAdvance pos buf- let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int- -- type of the contents of the list.- let elemType = toTType sizeAndType- columnChunks <- replicateM sizeOnly (readColumnChunk emptyColumnChunk buf pos 0 [])- readRowGroup (r {rowGroupColumns = columnChunks}) buf pos identifier fieldStack- 2 -> do- totalBytes <- readIntFromBuffer @Int64 buf pos- readRowGroup (r {totalByteSize = totalBytes}) buf pos identifier fieldStack- 3 -> do- nRows <- readIntFromBuffer @Int64 buf pos- readRowGroup (r {rowGroupNumRows = nRows}) buf pos identifier fieldStack- 4 -> return r- 5 -> do- offset <- readIntFromBuffer @Int64 buf pos- readRowGroup (r {fileOffset = offset}) buf pos identifier fieldStack- 6 -> do- compressedSize <- readIntFromBuffer @Int64 buf pos- readRowGroup (r {totalCompressedSize = compressedSize}) buf pos identifier fieldStack- 7 -> do- ordinal <- readIntFromBuffer @Int16 buf pos- readRowGroup (r {ordinal = ordinal}) buf pos identifier fieldStack- _ -> error $ "Unknown row group field: " ++ show identifier--readColumnChunk :: ColumnChunk -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO ColumnChunk-readColumnChunk c buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return c- Just (elemType, identifier) -> case identifier of- 1 -> do- stringSize <- readVarIntFromBuffer @Int buf pos- contents <- map (chr . fromIntegral) <$> replicateM stringSize (readAndAdvance pos buf)- readColumnChunk (c {columnChunkFilePath = contents}) buf pos identifier fieldStack- 2 -> do- columnChunkMetadataFileOffset <- readIntFromBuffer @Int64 buf pos- readColumnChunk (c {columnChunkMetadataFileOffset = columnChunkMetadataFileOffset}) buf pos identifier fieldStack- 3 -> do- columnMetadata <- readColumnMetadata emptyColumnMetadata buf pos 0 []- readColumnChunk (c {columnMetaData = columnMetadata}) buf pos identifier fieldStack - 4 -> do- columnOffsetIndexOffset <- readIntFromBuffer @Int64 buf pos- readColumnChunk (c {columnChunkOffsetIndexOffset = columnOffsetIndexOffset}) buf pos identifier fieldStack- 5 -> do- columnOffsetIndexLength <- readInt32FromBuffer buf pos- readColumnChunk (c {columnChunkOffsetIndexLength = columnOffsetIndexLength}) buf pos identifier fieldStack- 6 -> do- columnChunkColumnIndexOffset <- readIntFromBuffer @Int64 buf pos- readColumnChunk (c {columnChunkColumnIndexOffset = columnChunkColumnIndexOffset}) buf pos identifier fieldStack- 7 -> do- columnChunkColumnIndexLength <- readInt32FromBuffer buf pos- readColumnChunk (c {columnChunkColumnIndexLength = columnChunkColumnIndexLength}) buf pos identifier fieldStack- _ -> return c--readColumnMetadata :: ColumnMetaData -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO ColumnMetaData-readColumnMetadata cm buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return cm- Just (elemType, identifier) -> case identifier of- 1 -> do- cType <- parquetTypeFromInt <$> readInt32FromBuffer buf pos- readColumnMetadata (cm {columnType = cType}) buf pos identifier []- 2 -> do- sizeAndType <- readAndAdvance pos buf- let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int- let elemType = toTType sizeAndType- encodings <- replicateM sizeOnly (readParquetEncoding buf pos 0 [])- readColumnMetadata (cm {columnEncodings = encodings}) buf pos identifier fieldStack- 3 -> do- sizeAndType <- readAndAdvance pos buf- let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int- let elemType = toTType sizeAndType- paths <- replicateM sizeOnly (readString buf pos)- readColumnMetadata (cm {columnPathInSchema = paths}) buf pos identifier fieldStack- 4 -> do- cType <- compressionCodecFromInt <$> readInt32FromBuffer buf pos- readColumnMetadata (cm {columnCodec = cType}) buf pos identifier []- 5 -> do- numValues <- readIntFromBuffer @Int64 buf pos- readColumnMetadata (cm {columnNumValues = numValues}) buf pos identifier []- 6 -> do- columnTotalUncompressedSize <- readIntFromBuffer @Int64 buf pos- readColumnMetadata (cm {columnTotalUncompressedSize = columnTotalUncompressedSize}) buf pos identifier []- 7 -> do- columnTotalCompressedSize <- readIntFromBuffer @Int64 buf pos- readColumnMetadata (cm {columnTotalCompressedSize = columnTotalCompressedSize}) buf pos identifier []- 8 -> do- sizeAndType <- readAndAdvance pos buf- let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int- let elemType = toTType sizeAndType- columnKeyValueMetadata <- replicateM sizeOnly (readKeyValue emptyKeyValue buf pos 0 [])- readColumnMetadata (cm {columnKeyValueMetadata = columnKeyValueMetadata}) buf pos identifier fieldStack- 9 -> do- columnDataPageOffset <- readIntFromBuffer @Int64 buf pos- readColumnMetadata (cm {columnDataPageOffset = columnDataPageOffset}) buf pos identifier []- 10 -> do- columnIndexPageOffset <- readIntFromBuffer @Int64 buf pos- readColumnMetadata (cm {columnIndexPageOffset = columnIndexPageOffset}) buf pos identifier []- 11 -> do- columnDictionaryPageOffset <- readIntFromBuffer @Int64 buf pos- readColumnMetadata (cm {columnDictionaryPageOffset = columnDictionaryPageOffset}) buf pos identifier []- 12 -> do- stats <- readStatistics emptyColumnStatistics buf pos 0 []- readColumnMetadata (cm {columnStatistics = stats}) buf pos identifier fieldStack- 13 -> do- sizeAndType <- readAndAdvance pos buf- let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int- let elemType = toTType sizeAndType- pageEncodingStats <- replicateM sizeOnly (readPageEncodingStats emptyPageEncodingStats buf pos 0 [])- readColumnMetadata (cm {columnEncodingStats = pageEncodingStats}) buf pos identifier fieldStack- 14 -> do- bloomFilterOffset <- readIntFromBuffer @Int64 buf pos- readColumnMetadata (cm {bloomFilterOffset = bloomFilterOffset}) buf pos identifier []- 15 -> do- bloomFilterLength <- readInt32FromBuffer buf pos- readColumnMetadata (cm {bloomFilterLength = bloomFilterLength}) buf pos identifier []- 16 -> do- stats <- readSizeStatistics emptySizeStatistics buf pos 0 []- readColumnMetadata (cm {columnSizeStatistics = stats}) buf pos identifier fieldStack- 17 -> return $ error "UNIMPLEMENTED"- _ -> return cm--readParquetEncoding :: Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO ParquetEncoding-readParquetEncoding buf pos lastFieldId fieldStack = parquetEncodingFromInt <$> readInt32FromBuffer buf pos--readPageEncodingStats :: PageEncodingStats -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO PageEncodingStats-readPageEncodingStats pes buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return pes- Just (elemType, identifier) -> case identifier of- 1 -> do- pType <- pageTypeFromInt <$> readInt32FromBuffer buf pos- readPageEncodingStats (pes {pageEncodingPageType = pType}) buf pos identifier []- 2 -> do- pEnc <- parquetEncodingFromInt <$> readInt32FromBuffer buf pos- readPageEncodingStats (pes {pageEncoding = pEnc}) buf pos identifier []- 3 -> do- encodedCount <- readInt32FromBuffer buf pos- readPageEncodingStats (pes {pagesWithEncoding = encodedCount}) buf pos identifier []- _ -> pure pes--readStatistics :: ColumnStatistics -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO ColumnStatistics-readStatistics cs buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return cs- Just (elemType, identifier) -> case identifier of- 1 -> do- maxInBytes <- readByteString buf pos- readStatistics (cs {columnMax = maxInBytes}) buf pos identifier fieldStack- 2 -> do- minInBytes <- readByteString buf pos- readStatistics (cs {columnMin = minInBytes}) buf pos identifier fieldStack- 3 -> do- nullCount <- readIntFromBuffer @Int64 buf pos- readStatistics (cs {columnNullCount = nullCount}) buf pos identifier fieldStack- 4 -> do- distinctCount <- readIntFromBuffer @Int64 buf pos- readStatistics (cs {columnDistictCount = distinctCount}) buf pos identifier fieldStack- 5 -> do- maxInBytes <- readByteString buf pos- readStatistics (cs {columnMaxValue = maxInBytes}) buf pos identifier fieldStack- 6 -> do- minInBytes <- readByteString buf pos- readStatistics (cs {columnMinValue = minInBytes}) buf pos identifier fieldStack- 7 -> do- isMaxValueExact <- readAndAdvance pos buf- readStatistics (cs {isColumnMaxValueExact = isMaxValueExact == compactBooleanTrue}) buf pos identifier fieldStack- 8 -> do- isMinValueExact <- readAndAdvance pos buf- readStatistics (cs {isColumnMinValueExact = isMinValueExact == compactBooleanTrue}) buf pos identifier fieldStack- _ -> pure cs--readSizeStatistics :: SizeStatistics -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO SizeStatistics-readSizeStatistics ss buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return ss- Just (elemType, identifier) -> case identifier of- 1 -> do- unencodedByteArrayDataTypes <- readIntFromBuffer @Int64 buf pos- readSizeStatistics (ss {unencodedByteArrayDataTypes = unencodedByteArrayDataTypes}) buf pos identifier fieldStack- 2 -> do- sizeAndType <- readAndAdvance pos buf- let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int- let elemType = toTType sizeAndType- repetitionLevelHistogram <- replicateM sizeOnly (readIntFromBuffer @Int64 buf pos)- readSizeStatistics (ss {repetitionLevelHistogram = repetitionLevelHistogram}) buf pos identifier fieldStack- 3 -> do- sizeAndType <- readAndAdvance pos buf- let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int- let elemType = toTType sizeAndType- definitionLevelHistogram <- replicateM sizeOnly (readIntFromBuffer @Int64 buf pos)- readSizeStatistics (ss {definitionLevelHistogram = definitionLevelHistogram}) buf pos identifier fieldStack- _ -> pure ss--readKeyValue :: KeyValue -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO KeyValue-readKeyValue kv buf pos lastFieldId fieldStack = do- fieldContents <- readField buf pos lastFieldId fieldStack- case fieldContents of- Nothing -> return kv- Just (elemType, identifier) -> case identifier of- 1 -> do- k <- readString buf pos- readKeyValue (kv {key = k}) buf pos identifier fieldStack- 2 -> do- v <- readString buf pos- readKeyValue (kv {key = v}) buf pos identifier fieldStack- _ -> return kv--readString :: Ptr Word8 -> IORef Int -> IO String-readString buf pos = do- nameSize <- readVarIntFromBuffer @Int buf pos- map (chr . fromIntegral) <$> replicateM nameSize (readAndAdvance pos buf)--readByteStringFromBytes :: [Word8] -> ([Word8], [Word8])-readByteStringFromBytes xs = let- (size, rem) = readVarIntFromBytes @Int xs- in (take size rem, drop size rem)--readByteString :: Ptr Word8 -> IORef Int -> IO [Word8]-readByteString buf pos = do- size <- readVarIntFromBuffer @Int buf pos- replicateM size (readAndAdvance pos buf)--readByteString' :: Ptr Word8 -> Int64 -> IO [Word8]-readByteString' buf size = mapM (`readSingleByte` buf) [0..(size - 1)]--readField :: Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO (Maybe (TType, Int16))-readField buf pos lastFieldId fieldStack = do- t <- readAndAdvance pos buf- if t .&. 0x0f == 0- then return Nothing- else do- let modifier = fromIntegral ((t .&. 0xf0) `shiftR` 4) :: Int16- identifier <-- if modifier == 0- then readIntFromBuffer @Int16 buf pos- else return (lastFieldId + modifier)- let elemType = toTType (t .&. 0x0f)- pure $ Just (elemType, identifier)--readField' :: [Word8] -> Int16 -> Maybe ([Word8], TType, Int16)-readField' [] _ = Nothing-readField' (x:xs) lastFieldId- | x .&. 0x0f == 0 = Nothing- | otherwise = let- modifier = fromIntegral ((x .&. 0xf0) `shiftR` 4) :: Int16- (identifier, rem) = if modifier == 0 then readIntFromBytes @Int16 xs else (lastFieldId + modifier, xs)- elemType = toTType (x .&. 0x0f)- in Just (rem, elemType, identifier)--readAndAdvance :: IORef Int -> Ptr b -> IO Word8-readAndAdvance bufferPos buffer = do- pos <- readIORef bufferPos- b <- peekByteOff buffer pos :: IO Word8- modifyIORef bufferPos (+ 1)- return b--readSingleByte :: Int64 -> Ptr b -> IO Word8-readSingleByte pos buffer = peekByteOff buffer (fromIntegral pos)--readNoAdvance :: IORef Int -> Ptr b -> IO Word8-readNoAdvance bufferPos buffer = do- pos <- readIORef bufferPos- peekByteOff buffer pos :: IO Word8--compactBooleanTrue :: Word8-compactBooleanTrue = 0x01--compactBooleanFalse :: Word8-compactBooleanFalse = 0x02--compactByte :: Word8-compactByte = 0x03--compactI16 :: Word8-compactI16 = 0x04--compactI32 :: Word8-compactI32 = 0x05--compactI64 :: Word8-compactI64 = 0x06--compactDouble :: Word8-compactDouble = 0x07--compactBinary :: Word8-compactBinary = 0x08--compactList :: Word8-compactList = 0x09--compactSet :: Word8-compactSet = 0x0A--compactMap :: Word8-compactMap = 0x0B--compactStruct :: Word8-compactStruct = 0x0C--compactUuid :: Word8-compactUuid = 0x0D--data TType- = STOP- | BOOL- | BYTE- | I16- | I32- | I64- | DOUBLE- | STRING- | LIST- | SET- | MAP- | STRUCT- | UUID- deriving (Show, Eq)--toTType :: Word8 -> TType-toTType t =- fromMaybe STOP $- M.lookup (t .&. 0x0f) $- M.fromList- [ (compactBooleanTrue, BOOL),- (compactBooleanFalse, BOOL),- (compactByte, BYTE),- (compactI16, I16),- (compactI32, I32),- (compactI64, I64),- (compactDouble, DOUBLE),- (compactBinary, STRING),- (compactList, LIST),- (compactSet, SET),- (compactMap, MAP),- (compactStruct, STRUCT),- (compactUuid, UUID)- ]--readIntFromBuffer :: (Integral a) => Ptr b -> IORef Int -> IO a-readIntFromBuffer buf bufferPos = do- n <- readVarIntFromBuffer buf bufferPos- let u = fromIntegral n :: Word32- return $ fromIntegral $ (fromIntegral (u `shiftR` 1) :: Int32) .^. (-(n .&. 1))--readIntFromBytes :: (Integral a) => [Word8] -> (a, [Word8])-readIntFromBytes bs = let- (n, rem) = readVarIntFromBytes bs- u = fromIntegral n :: Word32- in (fromIntegral $ (fromIntegral (u `shiftR` 1) :: Int32) .^. (-(n .&. 1)), rem)--readInt32FromBuffer :: Ptr b -> IORef Int -> IO Int32-readInt32FromBuffer buf bufferPos = do- n <- (fromIntegral <$> readVarIntFromBuffer @Int64 buf bufferPos) :: IO Int32- let u = fromIntegral n :: Word32- return $ (fromIntegral (u `shiftR` 1) :: Int32) .^. (-(n .&. 1))--readInt32FromBytes :: [Word8] -> (Int32, [Word8])-readInt32FromBytes bs = let- (n', rem) = readVarIntFromBytes @Int64 bs- n = fromIntegral n' :: Int32- u = fromIntegral n :: Word32- in ((fromIntegral (u `shiftR` 1) :: Int32) .^. (-(n .&. 1)), rem)--readVarIntFromBuffer :: (Integral a) => Ptr b -> IORef Int -> IO a-readVarIntFromBuffer buf bufferPos = do- start <- readIORef bufferPos- let loop i shift result = do- b <- readAndAdvance bufferPos buf- let res = result .|. ((fromIntegral (b .&. 0x7f) :: Integer) `shiftL` shift)- if (b .&. 0x80) /= 0x80- then return res- else loop (i + 1) (shift + 7) res- fromIntegral <$> loop start 0 0--readVarIntFromBytes :: (Integral a) => [Word8] -> (a, [Word8])-readVarIntFromBytes bs = (fromIntegral n, rem)- where- (n, rem) = loop 0 0 bs- loop _ result [] = (result, [])- loop shift result (x:xs) = let- res = result .|. ((fromIntegral (x .&. 0x7f) :: Integer) `shiftL` shift)- in if (x .&. 0x80) /= 0x80 then (res, xs) else loop (shift + 7) res xs--littleEndianWord32 :: [Word8] -> Word32-littleEndianWord32 bytes- | length bytes == 4 = foldr (\v acc -> acc .|. v) 0 (zipWith (\b i -> (fromIntegral b) `shiftL` i) bytes [0,8..])- | length bytes < 4 = littleEndianWord32 (take 4 $ bytes ++ (cycle[0]))- | otherwise = error $ "Expected exactly 4 bytes for Word32 but got " ++ (show bytes)--littleEndianWord64 :: [Word8] -> Word64-littleEndianWord64 bytes- | length bytes == 8 = foldr (\v acc -> acc .|. v) 0 (zipWith (\b i -> (fromIntegral b) `shiftL` i) bytes [0,8..])- | otherwise = error "Expected exactly 8 bytes"--littleEndianInt32 :: [Word8] -> Int32-littleEndianInt32 bytes- | length bytes == 4 = foldr (\v acc -> acc .|. v) 0 (zipWith (\b i -> (fromIntegral b) `shiftL` i) bytes [0,8..])- | otherwise = error "Expected exactly 4 bytes for Int32"--readUVarInt :: [Word8] -> (Word64, [Word8])-readUVarInt xs = loop xs 0 0 0- where loop bs x _ 10 = (x, bs)- loop (b:bs) x s i- | b < 0x80 = (x .|. ((fromIntegral b) `shiftL` s), bs)- | otherwise = loop bs (x .|. (fromIntegral ((b .&. 0x7f) `shiftL` s))) (s + 7) (i + 1)--bitStream :: [Word8] -> [[Word8]]-bitStream xs = map (reverse . toBits) xs--toBits :: Word8 -> [Word8]-toBits b = go 1 b- where- go 8 n = [(n .&. 1)]- go i n = (n .&. 1) : go (i + 1) (n .>>. 1)--bitStreamToInt :: Word8 -> [Word8] -> [Int32]-bitStreamToInt _ [] = []-bitStreamToInt bitWidth bits = let- currBits = take (fromIntegral bitWidth) bits- remaining = drop (fromIntegral bitWidth) bits- in bitsToInt32 bitWidth currBits : bitStreamToInt bitWidth remaining--bitsToInt32 :: Word8 -> [Word8] -> Int32-bitsToInt32 bitWidth bits = fromIntegral $ foldr (.|.) 0 (zipWith (\s b -> b .<<. s) [0..] (reverse bits))+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module DataFrame.IO.Parquet (+ readParquet,+) where++import Control.Monad+import qualified Data.ByteString as BSO+import Data.Char+import Data.Foldable+import Data.IORef+import Data.List+import qualified Data.Map as M+import Data.Maybe+import qualified Data.Text as T+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.DataFrame (DataFrame)+import qualified DataFrame.Internal.DataFrame as DI+import qualified DataFrame.Operations.Core as DI+import Foreign+import System.IO++import DataFrame.IO.Parquet.Binary+import DataFrame.IO.Parquet.Dictionary+import DataFrame.IO.Parquet.Levels+import DataFrame.IO.Parquet.Page+import DataFrame.IO.Parquet.Thrift+import DataFrame.IO.Parquet.Types++{- | Read a parquet file from path and load it into a dataframe.++==== __Example__+@+ghci> D.readParquet "./data/mtcars.parquet" df++@+-}+readParquet :: String -> IO DataFrame+readParquet path = withBinaryFile path ReadMode $ \handle -> do+ (size, magicString) <- readMetadataSizeFromFooter handle+ when (magicString /= "PAR1") $ error "Invalid Parquet file"++ fileMetadata <- readMetadata handle size++ let columnPaths = getColumnPaths (drop 1 $ schema fileMetadata)+ let columnNames = map fst columnPaths++ colMap <- newIORef (M.empty :: M.Map T.Text DI.Column)++ let schemaElements = schema fileMetadata+ let getTypeLength :: [String] -> Maybe Int32+ getTypeLength path = findTypeLength schemaElements path 0+ where+ findTypeLength [] _ _ = Nothing+ findTypeLength (s : ss) targetPath depth+ | map T.unpack (pathToElement s ss depth) == targetPath+ && elementType s == STRING+ && typeLength s > 0 =+ Just (typeLength s)+ | otherwise = findTypeLength ss targetPath (if numChildren s > 0 then depth + 1 else depth)++ pathToElement _ _ _ = []++ forM_ (rowGroups fileMetadata) $ \rowGroup -> do+ forM_ (zip (rowGroupColumns rowGroup) [0 ..]) $ \(colChunk, colIdx) -> do+ let metadata = columnMetaData colChunk+ let colPath = columnPathInSchema metadata+ let colName =+ if null colPath+ then T.pack $ "col_" ++ show colIdx+ else T.pack $ last colPath++ let colDataPageOffset = columnDataPageOffset metadata+ let colDictionaryPageOffset = columnDictionaryPageOffset metadata+ let colStart =+ if colDictionaryPageOffset > 0 && colDataPageOffset > colDictionaryPageOffset+ then colDictionaryPageOffset+ else colDataPageOffset+ let colLength = columnTotalCompressedSize metadata++ columnBytes <- readBytes handle colStart colLength++ pages <- readAllPages (columnCodec metadata) columnBytes++ let maybeTypeLength =+ if columnType metadata == PFIXED_LEN_BYTE_ARRAY+ then getTypeLength colPath+ else Nothing++ let primaryEncoding = fromMaybe EPLAIN (fmap fst (uncons (columnEncodings metadata)))++ let schemaTail = drop 1 (schema fileMetadata)+ let colPath = columnPathInSchema (columnMetaData colChunk)+ let (maxDef, maxRep) = levelsForPath schemaTail colPath+ column <- processColumnPages (maxDef, maxRep) pages (columnType metadata) primaryEncoding maybeTypeLength++ modifyIORef colMap (M.insert colName column)++ finalColMap <- readIORef colMap+ let orderedColumns =+ map+ (\name -> (name, finalColMap M.! name))+ (filter (`M.member` finalColMap) columnNames)++ pure $ DI.fromNamedColumns orderedColumns++readMetadataSizeFromFooter :: Handle -> IO (Integer, BSO.ByteString)+readMetadataSizeFromFooter handle = do+ footerOffSet <- numBytesInFile handle+ buf <- mallocBytes 8 :: IO (Ptr Word8)+ hSeek handle AbsoluteSeek (fromIntegral $ footerOffSet - 8)+ _ <- hGetBuf handle buf 8++ sizeBytes <- mapM (\i -> fromIntegral <$> (peekElemOff buf i :: IO Word8) :: IO Int32) [0 .. 3]+ let size = fromIntegral $ foldl' (.|.) 0 $ zipWith shift sizeBytes [0, 8, 16, 24]++ magicStringBytes <- mapM (\i -> peekElemOff buf i :: IO Word8) [4 .. 7]+ let magicString = BSO.pack magicStringBytes+ free buf+ return (size, magicString)++getColumnPaths :: [SchemaElement] -> [(T.Text, Int)]+getColumnPaths schema = extractLeafPaths schema 0 []+ where+ extractLeafPaths :: [SchemaElement] -> Int -> [T.Text] -> [(T.Text, Int)]+ extractLeafPaths [] _ _ = []+ extractLeafPaths (s : ss) idx path+ | numChildren s == 0 =+ let fullPath = T.intercalate "." (path ++ [elementName s])+ in (fullPath, idx) : extractLeafPaths ss (idx + 1) path+ | otherwise =+ let newPath = if T.null (elementName s) then path else path ++ [elementName s]+ childrenCount = fromIntegral (numChildren s)+ (children, remaining) = splitAt childrenCount ss+ childResults = extractLeafPaths children idx newPath+ in childResults ++ extractLeafPaths remaining (idx + length childResults) path++processColumnPages :: (Int, Int) -> [Page] -> ParquetType -> ParquetEncoding -> Maybe Int32 -> IO DI.Column+processColumnPages (maxDef, maxRep) pages pType _ maybeTypeLength = do+ let dictPages = filter isDictionaryPage pages+ let dataPages = filter isDataPage pages++ let dictValsM =+ case dictPages of+ [] -> Nothing+ (dictPage : _) ->+ case pageTypeHeader (pageHeader dictPage) of+ DictionaryPageHeader{..} ->+ let countForBools =+ if pType == PBOOLEAN+ then error "is bool" Just dictionaryPageHeaderNumValues+ else maybeTypeLength+ in Just (readDictVals pType (pageBytes dictPage) countForBools)+ _ -> Nothing++ cols <- forM dataPages $ \page -> do+ case pageTypeHeader (pageHeader page) of+ DataPageHeader{..} -> do+ let n = fromIntegral dataPageHeaderNumValues+ let bs0 = pageBytes page+ let (defLvls, _repLvls, afterLvls) = readLevelsV1 n maxDef maxRep bs0+ let nPresent = length (filter (== maxDef) defLvls)++ case dataPageHeaderEncoding of+ EPLAIN ->+ case pType of+ PBOOLEAN ->+ let (vals, _) = readNBool nPresent afterLvls+ in pure (toMaybeBool maxDef defLvls vals)+ PINT32 ->+ let (vals, _) = readNInt32 nPresent afterLvls+ in pure (toMaybeInt32 maxDef defLvls vals)+ PINT64 ->+ let (vals, _) = readNInt64 nPresent afterLvls+ in pure (toMaybeInt64 maxDef defLvls vals)+ PINT96 ->+ let (vals, _) = readNInt96Times nPresent afterLvls+ in pure (toMaybeUTCTime maxDef defLvls vals)+ PFLOAT ->+ let (vals, _) = readNFloat nPresent afterLvls+ in pure (toMaybeFloat maxDef defLvls vals)+ PDOUBLE ->+ let (vals, _) = readNDouble nPresent afterLvls+ in pure (toMaybeDouble maxDef defLvls vals)+ PBYTE_ARRAY ->+ let (raws, _) = readNByteArrays nPresent afterLvls+ texts = map (T.pack . map (chr . fromIntegral)) raws+ in pure (toMaybeText maxDef defLvls texts)+ PFIXED_LEN_BYTE_ARRAY ->+ case maybeTypeLength of+ Just len ->+ let (raws, _) = splitFixed nPresent (fromIntegral len) afterLvls+ texts = map (T.pack . map (chr . fromIntegral)) raws+ in pure (toMaybeText maxDef defLvls texts)+ Nothing -> error "FIXED_LEN_BYTE_ARRAY requires type length"+ PARQUET_TYPE_UNKNOWN -> error "Cannot read unknown Parquet type"+ ERLE_DICTIONARY -> decodeDictV1 dictValsM maxDef defLvls nPresent afterLvls+ EPLAIN_DICTIONARY -> decodeDictV1 dictValsM maxDef defLvls nPresent afterLvls+ other -> error ("Unsupported v1 encoding: " ++ show other)+ DataPageHeaderV2{..} -> do+ let n = fromIntegral dataPageHeaderV2NumValues+ let bs0 = pageBytes page+ let (defLvls, _repLvls, afterLvls) =+ readLevelsV2 n maxDef maxRep definitionLevelByteLength repetitionLevelByteLength bs0+ let nPresent =+ if dataPageHeaderV2NumNulls > 0+ then fromIntegral (dataPageHeaderV2NumValues - dataPageHeaderV2NumNulls)+ else length (filter (== maxDef) defLvls)++ case dataPageHeaderV2Encoding of+ EPLAIN ->+ case pType of+ PBOOLEAN ->+ let (vals, _) = readNBool nPresent afterLvls+ in pure (toMaybeBool maxDef defLvls vals)+ PINT32 ->+ let (vals, _) = readNInt32 nPresent afterLvls+ in pure (toMaybeInt32 maxDef defLvls vals)+ PINT64 ->+ let (vals, _) = readNInt64 nPresent afterLvls+ in pure (toMaybeInt64 maxDef defLvls vals)+ PINT96 ->+ let (vals, _) = readNInt96Times nPresent afterLvls+ in pure (toMaybeUTCTime maxDef defLvls vals)+ PFLOAT ->+ let (vals, _) = readNFloat nPresent afterLvls+ in pure (toMaybeFloat maxDef defLvls vals)+ PDOUBLE ->+ let (vals, _) = readNDouble nPresent afterLvls+ in pure (toMaybeDouble maxDef defLvls vals)+ PBYTE_ARRAY ->+ let (raws, _) = readNByteArrays nPresent afterLvls+ texts = map (T.pack . map (chr . fromIntegral)) raws+ in pure (toMaybeText maxDef defLvls texts)+ PFIXED_LEN_BYTE_ARRAY ->+ case maybeTypeLength of+ Just len ->+ let (raws, _) = splitFixed nPresent (fromIntegral len) afterLvls+ texts = map (T.pack . map (chr . fromIntegral)) raws+ in pure (toMaybeText maxDef defLvls texts)+ Nothing -> error "FIXED_LEN_BYTE_ARRAY requires type length"+ PARQUET_TYPE_UNKNOWN -> error "Cannot read unknown Parquet type"+ ERLE_DICTIONARY -> decodeDictV1 dictValsM maxDef defLvls nPresent afterLvls+ EPLAIN_DICTIONARY -> decodeDictV1 dictValsM maxDef defLvls nPresent afterLvls+ other -> error ("Unsupported v2 encoding: " ++ show other)++ case cols of+ [] -> pure $ DI.fromList ([] :: [Maybe Int])+ (c : cs) -> pure $ foldl' (\l r -> fromMaybe (error "concat failed") (DI.concatColumns l r)) c cs
+ src/DataFrame/IO/Parquet/Binary.hs view
@@ -0,0 +1,130 @@+{-# LANGUAGE TypeApplications #-}++module DataFrame.IO.Parquet.Binary where++import Control.Monad+import Data.Bits+import Data.Char+import Data.IORef+import Data.Word+import Foreign+import System.IO++littleEndianWord32 :: [Word8] -> Word32+littleEndianWord32 bytes+ | length bytes >= 4 = foldr (.|.) 0 (zipWith (\b i -> (fromIntegral b) `shiftL` i) (take 4 bytes) [0, 8, 16, 24])+ | otherwise = littleEndianWord32 (take 4 $ bytes ++ repeat 0)++littleEndianWord64 :: [Word8] -> Word64+littleEndianWord64 bytes = foldr (.|.) 0 (zipWith (\b i -> (fromIntegral b) `shiftL` i) (take 8 bytes) [0, 8 ..])++littleEndianInt32 :: [Word8] -> Int32+littleEndianInt32 = fromIntegral . littleEndianWord32++word64ToLittleEndian :: Word64 -> [Word8]+word64ToLittleEndian w = map (\i -> fromIntegral (w `shiftR` i)) [0, 8, 16, 24, 32, 40, 48, 56]++word32ToLittleEndian :: Word32 -> [Word8]+word32ToLittleEndian w = map (\i -> fromIntegral (w `shiftR` i)) [0, 8, 16, 24]++readUVarInt :: [Word8] -> (Word64, [Word8])+readUVarInt xs = loop xs 0 0 0+ where+ loop bs x _ 10 = (x, bs)+ loop (b : bs) x s i+ | b < 0x80 = (x .|. ((fromIntegral b) `shiftL` s), bs)+ | otherwise = loop bs (x .|. (fromIntegral ((b .&. 0x7f) `shiftL` s))) (s + 7) (i + 1)++readVarIntFromBytes :: (Integral a) => [Word8] -> (a, [Word8])+readVarIntFromBytes bs = (fromIntegral n, rem)+ where+ (n, rem) = loop 0 0 bs+ loop _ result [] = (result, [])+ loop shift result (x : xs) =+ let res = result .|. ((fromIntegral (x .&. 0x7f) :: Integer) `shiftL` shift)+ in if (x .&. 0x80) /= 0x80 then (res, xs) else loop (shift + 7) res xs++readIntFromBytes :: (Integral a) => [Word8] -> (a, [Word8])+readIntFromBytes bs =+ let (n, rem) = readVarIntFromBytes bs+ u = fromIntegral n :: Word32+ in (fromIntegral $ (fromIntegral (u `shiftR` 1) :: Int32) .^. (-(n .&. 1)), rem)++readInt32FromBytes :: [Word8] -> (Int32, [Word8])+readInt32FromBytes bs =+ let (n', rem) = readVarIntFromBytes @Int64 bs+ n = fromIntegral n' :: Int32+ u = fromIntegral n :: Word32+ in ((fromIntegral (u `shiftR` 1) :: Int32) .^. (-(n .&. 1)), rem)++readAndAdvance :: IORef Int -> Ptr b -> IO Word8+readAndAdvance bufferPos buffer = do+ pos <- readIORef bufferPos+ b <- peekByteOff buffer pos :: IO Word8+ modifyIORef bufferPos (+ 1)+ return b++readVarIntFromBuffer :: (Integral a) => Ptr b -> IORef Int -> IO a+readVarIntFromBuffer buf bufferPos = do+ start <- readIORef bufferPos+ let loop i shift result = do+ b <- readAndAdvance bufferPos buf+ let res = result .|. ((fromIntegral (b .&. 0x7f) :: Integer) `shiftL` shift)+ if (b .&. 0x80) /= 0x80+ then return res+ else loop (i + 1) (shift + 7) res+ fromIntegral <$> loop start 0 0++readIntFromBuffer :: (Integral a) => Ptr b -> IORef Int -> IO a+readIntFromBuffer buf bufferPos = do+ n <- readVarIntFromBuffer buf bufferPos+ let u = fromIntegral n :: Word32+ return $ fromIntegral $ (fromIntegral (u `shiftR` 1) :: Int32) .^. (-(n .&. 1))++readInt32FromBuffer :: Ptr b -> IORef Int -> IO Int32+readInt32FromBuffer buf bufferPos = do+ n <- (fromIntegral <$> readVarIntFromBuffer @Int64 buf bufferPos) :: IO Int32+ let u = fromIntegral n :: Word32+ return $ (fromIntegral (u `shiftR` 1) :: Int32) .^. (-(n .&. 1))++readString :: Ptr Word8 -> IORef Int -> IO String+readString buf pos = do+ nameSize <- readVarIntFromBuffer @Int buf pos+ map (chr . fromIntegral) <$> replicateM nameSize (readAndAdvance pos buf)++readBytes :: Handle -> Int64 -> Int64 -> IO [Word8]+readBytes handle colStart colLen = do+ buf <- mallocBytes (fromIntegral colLen) :: IO (Ptr Word8)+ hSeek handle AbsoluteSeek (fromIntegral colStart)+ _ <- hGetBuf handle buf (fromIntegral colLen)+ columnBytes <- mapM (peekByteOff buf) [0 .. fromIntegral colLen - 1]+ free buf+ pure columnBytes++numBytesInFile :: Handle -> IO Integer+numBytesInFile handle = do+ hSeek handle SeekFromEnd 0+ hTell handle++readByteStringFromBytes :: [Word8] -> ([Word8], [Word8])+readByteStringFromBytes xs =+ let+ (size, rem) = readVarIntFromBytes @Int xs+ in+ (take size rem, drop size rem)++readByteString :: Ptr Word8 -> IORef Int -> IO [Word8]+readByteString buf pos = do+ size <- readVarIntFromBuffer @Int buf pos+ replicateM size (readAndAdvance pos buf)++readByteString' :: Ptr Word8 -> Int64 -> IO [Word8]+readByteString' buf size = mapM (`readSingleByte` buf) [0 .. (size - 1)]++readSingleByte :: Int64 -> Ptr b -> IO Word8+readSingleByte pos buffer = peekByteOff buffer (fromIntegral pos)++readNoAdvance :: IORef Int -> Ptr b -> IO Word8+readNoAdvance bufferPos buffer = do+ pos <- readIORef bufferPos+ peekByteOff buffer pos :: IO Word8
src/DataFrame/IO/Parquet/ColumnStatistics.hs view
@@ -4,16 +4,16 @@ import Data.Word data ColumnStatistics = ColumnStatistics- { columnMin :: [Word8],- columnMax :: [Word8],- columnNullCount :: Int64,- columnDistictCount :: Int64,- columnMinValue :: [Word8],- columnMaxValue :: [Word8],- isColumnMaxValueExact :: Bool,- isColumnMinValueExact :: Bool- }- deriving (Show, Eq)+ { columnMin :: [Word8]+ , columnMax :: [Word8]+ , columnNullCount :: Int64+ , columnDistictCount :: Int64+ , columnMinValue :: [Word8]+ , columnMaxValue :: [Word8]+ , isColumnMaxValueExact :: Bool+ , isColumnMinValueExact :: Bool+ }+ deriving (Show, Eq) emptyColumnStatistics :: ColumnStatistics emptyColumnStatistics = ColumnStatistics [] [] 0 0 [] [] False False
src/DataFrame/IO/Parquet/Compression.hs view
@@ -3,16 +3,16 @@ import Data.Int data CompressionCodec- = UNCOMPRESSED- | SNAPPY- | GZIP- | LZO- | BROTLI- | LZ4- | ZSTD- | LZ4_RAW- | COMPRESSION_CODEC_UNKNOWN- deriving (Show, Eq)+ = UNCOMPRESSED+ | SNAPPY+ | GZIP+ | LZO+ | BROTLI+ | LZ4+ | ZSTD+ | LZ4_RAW+ | COMPRESSION_CODEC_UNKNOWN+ deriving (Show, Eq) compressionCodecFromInt :: Int32 -> CompressionCodec compressionCodecFromInt 0 = UNCOMPRESSED
+ src/DataFrame/IO/Parquet/Dictionary.hs view
@@ -0,0 +1,176 @@+{-# LANGUAGE OverloadedStrings #-}++module DataFrame.IO.Parquet.Dictionary where++import Control.Monad+import Data.Char+import Data.Int+import Data.Maybe+import qualified Data.Text as T+import Data.Time+import Data.Word+import DataFrame.IO.Parquet.Binary+import DataFrame.IO.Parquet.Encoding+import DataFrame.IO.Parquet.Levels+import DataFrame.IO.Parquet.Time+import DataFrame.IO.Parquet.Types+import qualified DataFrame.Internal.Column as DI+import Foreign+import GHC.Float+import GHC.IO (unsafePerformIO)+import Text.Printf++dictCardinality :: DictVals -> Int+dictCardinality (DBool ds) = length ds+dictCardinality (DInt32 ds) = length ds+dictCardinality (DInt64 ds) = length ds+dictCardinality (DInt96 ds) = length ds+dictCardinality (DFloat ds) = length ds+dictCardinality (DDouble ds) = length ds+dictCardinality (DText ds) = length ds++readDictVals :: ParquetType -> [Word8] -> Maybe Int32 -> DictVals+readDictVals PBOOLEAN bs (Just count) = DBool (take (fromIntegral count) $ readPageBool bs)+readDictVals PINT32 bs _ = DInt32 (readPageInt32 bs)+readDictVals PINT64 bs _ = DInt64 (readPageInt64 bs)+readDictVals PINT96 bs _ = DInt96 (readPageInt96Times bs)+readDictVals PFLOAT bs _ = DFloat (readPageFloat bs)+readDictVals PDOUBLE bs _ = DDouble (readPageWord64 bs)+readDictVals PBYTE_ARRAY bs _ = DText (readPageBytes bs)+readDictVals PFIXED_LEN_BYTE_ARRAY bs (Just len) = DText (readPageFixedBytes bs (fromIntegral len))+readDictVals t _ _ = error $ "Unsupported dictionary type: " ++ show t++readPageInt32 :: [Word8] -> [Int32]+readPageInt32 [] = []+readPageInt32 xs = littleEndianInt32 (take 4 xs) : readPageInt32 (drop 4 xs)++readPageWord64 :: [Word8] -> [Double]+readPageWord64 [] = []+readPageWord64 xs = castWord64ToDouble (littleEndianWord64 (take 8 xs)) : readPageWord64 (drop 8 xs)++readPageBytes :: [Word8] -> [T.Text]+readPageBytes [] = []+readPageBytes xs =+ let lenBytes = fromIntegral (littleEndianInt32 $ take 4 xs)+ totalBytesRead = lenBytes + 4+ in T.pack (map (chr . fromIntegral) $ take lenBytes (drop 4 xs)) : readPageBytes (drop totalBytesRead xs)++readPageBool :: [Word8] -> [Bool]+readPageBool [] = []+readPageBool bs =+ let totalBits = length bs * 8+ bits = concatMap (\b -> map (\i -> (b `shiftR` i) .&. 1 == 1) [0 .. 7]) bs+ in bits++readPageInt64 :: [Word8] -> [Int64]+readPageInt64 [] = []+readPageInt64 xs = fromIntegral (littleEndianWord64 (take 8 xs)) : readPageInt64 (drop 8 xs)++readPageFloat :: [Word8] -> [Float]+readPageFloat [] = []+readPageFloat xs = castWord32ToFloat (littleEndianWord32 (take 4 xs)) : readPageFloat (drop 4 xs)++readNInt96Times :: Int -> [Word8] -> ([UTCTime], [Word8])+readNInt96Times 0 bs = ([], bs)+readNInt96Times k bs =+ let timestamp96 = take 12 bs+ utcTime = int96ToUTCTime timestamp96+ bs' = drop 12 bs+ (times, rest) = readNInt96Times (k - 1) bs'+ in (utcTime : times, rest)++readPageInt96Times :: [Word8] -> [UTCTime]+readPageInt96Times [] = []+readPageInt96Times bs =+ let (times, rest) = readNInt96Times (length bs `div` 12) bs+ in times++readPageFixedBytes :: [Word8] -> Int -> [T.Text]+readPageFixedBytes [] _ = []+readPageFixedBytes xs len =+ let chunk = take len xs+ text = T.pack (map (chr . fromIntegral) chunk)+ in text : readPageFixedBytes (drop len xs) len++decodeDictV1 :: Maybe DictVals -> Int -> [Int] -> Int -> [Word8] -> IO DI.Column+decodeDictV1 dictValsM maxDef defLvls nPresent bytes =+ case dictValsM of+ Nothing -> error "Dictionary-encoded page but dictionary is missing"+ Just dictVals ->+ let (idxs, _rest) = decodeDictIndicesV1 nPresent (dictCardinality dictVals) bytes+ in do+ when (length idxs /= nPresent) $+ error $+ "dict index count mismatch: got " ++ show (length idxs) ++ ", expected " ++ show nPresent+ case dictVals of+ DBool ds -> do+ let values = [ds !! i | i <- idxs]+ pure (toMaybeBool maxDef defLvls values)+ DInt32 ds -> do+ let values = [ds !! i | i <- idxs]+ pure (toMaybeInt32 maxDef defLvls values)+ DInt64 ds -> do+ let values = [ds !! i | i <- idxs]+ pure (toMaybeInt64 maxDef defLvls values)+ DInt96 ds -> do+ let values = [ds !! i | i <- idxs]+ pure (toMaybeUTCTime maxDef defLvls values)+ DFloat ds -> do+ let values = [ds !! i | i <- idxs]+ pure (toMaybeFloat maxDef defLvls values)+ DDouble ds -> do+ let values = [ds !! i | i <- idxs]+ pure (toMaybeDouble maxDef defLvls values)+ DText ds -> do+ let values = [ds !! i | i <- idxs]+ pure (toMaybeText maxDef defLvls values)++toMaybeInt32 :: Int -> [Int] -> [Int32] -> DI.Column+toMaybeInt32 maxDef def xs =+ let filled = stitchNullable maxDef def xs+ in if all isJust filled+ then DI.fromList (map (fromMaybe 0) filled)+ else DI.fromList filled++toMaybeDouble :: Int -> [Int] -> [Double] -> DI.Column+toMaybeDouble maxDef def xs =+ let filled = stitchNullable maxDef def xs+ in if all isJust filled+ then DI.fromList (map (fromMaybe 0) filled)+ else DI.fromList filled++toMaybeText :: Int -> [Int] -> [T.Text] -> DI.Column+toMaybeText maxDef def xs =+ let filled = stitchNullable maxDef def xs+ in if all isJust filled+ then DI.fromList (map (fromMaybe "") filled)+ else DI.fromList filled++toMaybeBool :: Int -> [Int] -> [Bool] -> DI.Column+toMaybeBool maxDef def xs =+ let filled = stitchNullable maxDef def xs+ in if all isJust filled+ then DI.fromList (map (fromMaybe False) filled)+ else DI.fromList filled++toMaybeInt64 :: Int -> [Int] -> [Int64] -> DI.Column+toMaybeInt64 maxDef def xs =+ let filled = stitchNullable maxDef def xs+ in if all isJust filled+ then DI.fromList (map (fromMaybe 0) filled)+ else DI.fromList filled++toMaybeFloat :: Int -> [Int] -> [Float] -> DI.Column+toMaybeFloat maxDef def xs =+ let filled = stitchNullable maxDef def xs+ in if all isJust filled+ then DI.fromList (map (fromMaybe 0.0) filled)+ else DI.fromList filled++toMaybeUTCTime :: Int -> [Int] -> [UTCTime] -> DI.Column+toMaybeUTCTime maxDef def times =+ let filled = stitchNullable maxDef def times+ defaultTime = UTCTime (fromGregorian 1970 1 1) (secondsToDiffTime 0)+ in if all isJust filled+ then DI.fromList (map (fromMaybe defaultTime) filled)+ else DI.fromList filled
src/DataFrame/IO/Parquet/Encoding.hs view
@@ -1,28 +1,98 @@ module DataFrame.IO.Parquet.Encoding where -import Data.Int+import Data.Bits+import Data.Foldable+import Data.List (mapAccumL)+import Data.Word+import DataFrame.IO.Parquet.Binary -data ParquetEncoding- = EPLAIN- | EPLAIN_DICTIONARY- | ERLE- | EBIT_PACKED- | EDELTA_BINARY_PACKED- | EDELTA_LENGTH_BYTE_ARRAY- | EDELTA_BYTE_ARRAY- | ERLE_DICTIONARY- | EBYTE_STREAM_SPLIT- | PARQUET_ENCODING_UNKNOWN- deriving (Show, Eq)+ceilLog2 :: Int -> Int+ceilLog2 x+ | x <= 1 = 0+ | otherwise = 1 + ceilLog2 ((x + 1) `div` 2) -parquetEncodingFromInt :: Int32 -> ParquetEncoding-parquetEncodingFromInt 0 = EPLAIN-parquetEncodingFromInt 2 = EPLAIN_DICTIONARY-parquetEncodingFromInt 3 = ERLE-parquetEncodingFromInt 4 = EBIT_PACKED-parquetEncodingFromInt 5 = EDELTA_BINARY_PACKED-parquetEncodingFromInt 6 = EDELTA_LENGTH_BYTE_ARRAY-parquetEncodingFromInt 7 = EDELTA_BYTE_ARRAY-parquetEncodingFromInt 8 = ERLE_DICTIONARY-parquetEncodingFromInt 9 = EBYTE_STREAM_SPLIT-parquetEncodingFromInt _ = PARQUET_ENCODING_UNKNOWN+bitWidthForMaxLevel :: Int -> Int+bitWidthForMaxLevel maxLevel = ceilLog2 (maxLevel + 1)++bytesForBW :: Int -> Int+bytesForBW bw = (bw + 7) `div` 8++unpackBitPacked :: Int -> Int -> [Word8] -> ([Word32], [Word8])+unpackBitPacked bw count bs+ | count <= 0 = ([], bs)+ | null bs = ([], bs)+ | otherwise =+ let totalBits = bw * count+ totalBytes = (totalBits + 7) `div` 8+ chunk = take totalBytes bs+ rest = drop totalBytes bs+ in if length chunk < totalBytes+ then error $ "Not enough bytes: need " ++ show totalBytes ++ ", got " ++ show (length chunk)+ else+ let bits = concatMap (\b -> map (\i -> (b `shiftR` i) .&. 1) [0 .. 7]) chunk+ toN xs = foldl' (\a (i, b) -> a .|. (b `shiftL` i)) 0 (zip [0 ..] xs)++ extractValues _ [] = []+ extractValues n bitsLeft+ | n <= 0 = []+ | length bitsLeft < bw = []+ | otherwise =+ let (this, bitsLeft') = splitAt bw bitsLeft+ in toN this : extractValues (n - 1) bitsLeft'++ vals = extractValues count bits+ in (map fromIntegral vals, rest)++decodeRLEBitPackedHybrid :: Int -> Int -> [Word8] -> ([Word32], [Word8])+decodeRLEBitPackedHybrid bw need bs+ | bw == 0 = (replicate need 0, bs)+ | otherwise = go need bs []+ where+ mask :: Word32+ mask = if bw == 32 then maxBound else (1 `shiftL` bw) - 1+ go 0 rest acc = (reverse acc, rest)+ go n rest acc+ | null rest = error $ "Premature end of stream: need " ++ show n ++ " more values"+ | otherwise =+ let (hdr64, afterHdr) = readUVarInt rest+ isPacked = (hdr64 .&. 1) == 1+ in if isPacked+ then+ let groups = fromIntegral (hdr64 `shiftR` 1) :: Int+ totalVals = groups * 8+ (valsAll, afterRun) = unpackBitPacked bw totalVals afterHdr+ takeN = min n totalVals+ actualTaken = take takeN valsAll+ in go (n - takeN) afterRun (reverse actualTaken ++ acc)+ else+ let runLen = fromIntegral (hdr64 `shiftR` 1) :: Int+ nbytes = bytesForBW bw+ word32 = littleEndianWord32 (take 4 afterHdr)+ afterV = drop nbytes afterHdr+ val = word32 .&. mask+ takeN = min n runLen+ in go (n - takeN) afterV (replicate takeN val ++ acc)++decodeDictIndicesV1 :: Int -> Int -> [Word8] -> ([Int], [Word8])+decodeDictIndicesV1 need dictCard bs =+ case bs of+ [] -> error "empty dictionary index stream"+ (w0 : rest0) ->+ let widthFromDict = ceilLog2 dictCard+ looksLikeWidth = w0 <= 32 && (w0 /= 0 || dictCard <= 1)+ tryWithWidthByte =+ let bw = fromIntegral w0+ (u32s, rest1) = decodeRLEBitPackedHybrid bw need rest0+ in (map fromIntegral u32s, rest1)+ tryWithoutWidthByte =+ let bw = widthFromDict+ (u32s, rest1) = decodeRLEBitPackedHybrid bw need bs+ in (map fromIntegral u32s, rest1)+ (idxs, rest') =+ if looksLikeWidth+ then+ let (xs, r) = tryWithWidthByte+ in if length xs == need then (xs, r) else tryWithoutWidthByte+ else+ tryWithoutWidthByte+ in (idxs, rest')
+ src/DataFrame/IO/Parquet/Levels.hs view
@@ -0,0 +1,106 @@+module DataFrame.IO.Parquet.Levels where++import Data.Int+import Data.List+import qualified Data.Text as T+import Data.Word++import DataFrame.IO.Parquet.Binary+import DataFrame.IO.Parquet.Encoding+import DataFrame.IO.Parquet.Thrift+import DataFrame.IO.Parquet.Types++readLevelsV1 :: Int -> Int -> Int -> [Word8] -> ([Int], [Int], [Word8])+readLevelsV1 n maxDef maxRep bs =+ let bwDef = bitWidthForMaxLevel maxDef+ bwRep = bitWidthForMaxLevel maxRep++ (repLvlsU32, afterRep) =+ if bwRep == 0+ then (replicate n 0, bs)+ else+ let repLength = littleEndianWord32 (take 4 bs)+ repData = take (fromIntegral repLength) (drop 4 bs)+ afterRepData = drop (4 + fromIntegral repLength) bs+ (repVals, _) = decodeRLEBitPackedHybrid bwRep n repData+ in (repVals, afterRepData)++ (defLvlsU32, afterDef) =+ if bwDef == 0+ then (replicate n 0, afterRep)+ else+ let defLength = littleEndianWord32 (take 4 afterRep)+ defData = take (fromIntegral defLength) (drop 4 afterRep)+ afterDefData = drop (4 + fromIntegral defLength) afterRep+ (defVals, _) = decodeRLEBitPackedHybrid bwDef n defData+ in (defVals, afterDefData)+ in (map fromIntegral defLvlsU32, map fromIntegral repLvlsU32, afterDef)++readLevelsV2 :: Int -> Int -> Int -> Int32 -> Int32 -> [Word8] -> ([Int], [Int], [Word8])+readLevelsV2 n maxDef maxRep defLen repLen bs =+ let (repBytes, afterRepBytes) = splitAt (fromIntegral repLen) bs+ (defBytes, afterDefBytes) = splitAt (fromIntegral defLen) afterRepBytes+ bwDef = bitWidthForMaxLevel maxDef+ bwRep = bitWidthForMaxLevel maxRep+ (repLvlsU32, _) =+ if bwRep == 0+ then (replicate n 0, repBytes)+ else decodeRLEBitPackedHybrid bwRep n repBytes+ (defLvlsU32, _) =+ if bwDef == 0+ then (replicate n 0, defBytes)+ else decodeRLEBitPackedHybrid bwDef n defBytes+ in (map fromIntegral defLvlsU32, map fromIntegral repLvlsU32, afterDefBytes)++stitchNullable :: Int -> [Int] -> [a] -> [Maybe a]+stitchNullable maxDef defLvls vals = go defLvls vals+ where+ go [] _ = []+ go (d : ds) vs+ | d == maxDef = case vs of+ (v : vs') -> Just v : go ds vs'+ [] -> error "value stream exhausted"+ | otherwise = Nothing : go ds vs++data SNode = SNode+ { sName :: String+ , sRep :: RepetitionType+ , sChildren :: [SNode]+ }+ deriving (Show, Eq)++parseOne :: [SchemaElement] -> (SNode, [SchemaElement])+parseOne [] = error "parseOne: empty schema list"+parseOne (se : rest) =+ let childCount = fromIntegral (numChildren se)+ (kids, rest') = parseMany childCount rest+ in ( SNode+ { sName = T.unpack (elementName se)+ , sRep = repetitionType se+ , sChildren = kids+ }+ , rest'+ )++parseMany :: Int -> [SchemaElement] -> ([SNode], [SchemaElement])+parseMany 0 xs = ([], xs)+parseMany n xs =+ let (node, xs') = parseOne xs+ (nodes, xs'') = parseMany (n - 1) xs'+ in (node : nodes, xs'')++parseAll :: [SchemaElement] -> [SNode]+parseAll [] = []+parseAll xs = let (n, xs') = parseOne xs in n : parseAll xs'++levelsForPath :: [SchemaElement] -> [String] -> (Int, Int)+levelsForPath schemaTail path = go 0 0 (parseAll schemaTail) path+ where+ go defC repC _ [] = (defC, repC)+ go defC repC nodes (p : ps) =+ case find (\n -> sName n == p) nodes of+ Nothing -> (defC, repC)+ Just n ->+ let defC' = defC + (if sRep n == OPTIONAL then 1 else 0)+ repC' = repC + (if sRep n == REPEATED then 1 else 0)+ in go defC' repC' (sChildren n) ps
src/DataFrame/IO/Parquet/Page.hs view
@@ -1,19 +1,342 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeApplications #-}+ module DataFrame.IO.Parquet.Page where +import Codec.Compression.Zstd.Streaming+import Control.Monad+import Data.Bits+import qualified Data.ByteString as BSO+import Data.Char+import Data.Foldable import Data.Int+import Data.List+import Data.Maybe+import qualified Data.Text as T+import Data.Word+import DataFrame.IO.Parquet.Binary+import DataFrame.IO.Parquet.Dictionary+import DataFrame.IO.Parquet.Levels+import DataFrame.IO.Parquet.Thrift+import DataFrame.IO.Parquet.Types+import qualified DataFrame.Internal.Column as DI+import GHC.Float+import qualified Snappy as Snappy+import Text.Printf -data PageType- = DATA_PAGE- | INDEX_PAGE- | DICTIONARY_PAGE- | DATA_PAGE_V2- | PAGE_TYPE_UNKNOWN- deriving (Show, Eq)+isDataPage :: Page -> Bool+isDataPage page = case pageTypeHeader (pageHeader page) of+ DataPageHeader{..} -> True+ DataPageHeaderV2{..} -> True+ _ -> False -pageTypeFromInt :: Int32 -> PageType-pageTypeFromInt 0 = DATA_PAGE-pageTypeFromInt 1 = INDEX_PAGE-pageTypeFromInt 2 = DICTIONARY_PAGE-pageTypeFromInt 3 = DATA_PAGE_V2-pageTypeFromInt _ = PAGE_TYPE_UNKNOWN+isDictionaryPage :: Page -> Bool+isDictionaryPage page = case pageTypeHeader (pageHeader page) of+ DictionaryPageHeader{..} -> True+ _ -> False +readPage :: CompressionCodec -> [Word8] -> IO (Maybe Page, [Word8])+readPage c [] = pure (Nothing, [])+readPage c columnBytes = do+ let (hdr, rem) = readPageHeader emptyPageHeader columnBytes 0+ let compressed = take (fromIntegral $ compressedPageSize hdr) rem++ fullData <- case c of+ ZSTD -> do+ Consume dFunc <- decompress+ Consume dFunc' <- dFunc (BSO.pack compressed)+ Done res <- dFunc' BSO.empty+ pure res+ SNAPPY -> case Snappy.decompress (BSO.pack compressed) of+ Left e -> error (show e)+ Right res -> pure res+ UNCOMPRESSED -> pure (BSO.pack compressed)+ other -> error ("Unsupported compression type: " ++ show other)+ pure $ (Just $ Page hdr (BSO.unpack fullData), drop (fromIntegral $ compressedPageSize hdr) rem)++readPageHeader :: PageHeader -> [Word8] -> Int16 -> (PageHeader, [Word8])+readPageHeader hdr [] _ = (hdr, [])+readPageHeader hdr xs lastFieldId =+ let+ fieldContents = readField' xs lastFieldId+ in+ case fieldContents of+ Nothing -> (hdr, drop 1 xs)+ Just (rem, elemType, identifier) -> case identifier of+ 1 ->+ let+ (pType, rem') = readInt32FromBytes rem+ in+ readPageHeader (hdr{pageHeaderPageType = pageTypeFromInt pType}) rem' identifier+ 2 ->+ let+ (uncompressedPageSize, rem') = readInt32FromBytes rem+ in+ readPageHeader (hdr{uncompressedPageSize = uncompressedPageSize}) rem' identifier+ 3 ->+ let+ (compressedPageSize, rem') = readInt32FromBytes rem+ in+ readPageHeader (hdr{compressedPageSize = compressedPageSize}) rem' identifier+ 4 ->+ let+ (crc, rem') = readInt32FromBytes rem+ in+ readPageHeader (hdr{pageHeaderCrcChecksum = crc}) rem' identifier+ 5 ->+ let+ (dataPageHeader, rem') = readPageTypeHeader emptyDataPageHeader rem 0+ in+ readPageHeader (hdr{pageTypeHeader = dataPageHeader}) rem' identifier+ 6 -> error "Index page header not supported"+ 7 ->+ let+ (dictionaryPageHeader, rem') = readPageTypeHeader emptyDictionaryPageHeader rem 0+ in+ readPageHeader (hdr{pageTypeHeader = dictionaryPageHeader}) rem' identifier+ 8 ->+ let+ (dataPageHeaderV2, rem') = readPageTypeHeader emptyDataPageHeaderV2 rem 0+ in+ readPageHeader (hdr{pageTypeHeader = dataPageHeaderV2}) rem' identifier+ n -> error $ "Unknown page header field" ++ show n++readPageTypeHeader :: PageTypeHeader -> [Word8] -> Int16 -> (PageTypeHeader, [Word8])+readPageTypeHeader hdr [] _ = (hdr, [])+readPageTypeHeader hdr@(DictionaryPageHeader{..}) xs lastFieldId =+ let+ fieldContents = readField' xs lastFieldId+ in+ case fieldContents of+ Nothing -> (hdr, drop 1 xs)+ Just (rem, elemType, identifier) -> case identifier of+ 1 ->+ let+ (numValues, rem') = readInt32FromBytes rem+ in+ readPageTypeHeader (hdr{dictionaryPageHeaderNumValues = numValues}) rem' identifier+ 2 ->+ let+ (enc, rem') = readInt32FromBytes rem+ in+ readPageTypeHeader (hdr{dictionaryPageHeaderEncoding = parquetEncodingFromInt enc}) rem' identifier+ 3 ->+ let+ (isSorted : rem') = rem+ in+ readPageTypeHeader (hdr{dictionaryPageIsSorted = isSorted == compactBooleanTrue}) rem' identifier+ n -> error $ show n+readPageTypeHeader hdr@(DataPageHeader{..}) xs lastFieldId =+ let+ fieldContents = readField' xs lastFieldId+ in+ case fieldContents of+ Nothing -> (hdr, drop 1 xs)+ Just (rem, elemType, identifier) -> case identifier of+ 1 ->+ let+ (numValues, rem') = readInt32FromBytes rem+ in+ readPageTypeHeader (hdr{dataPageHeaderNumValues = numValues}) rem' identifier+ 2 ->+ let+ (enc, rem') = readInt32FromBytes rem+ in+ readPageTypeHeader (hdr{dataPageHeaderEncoding = parquetEncodingFromInt enc}) rem' identifier+ 3 ->+ let+ (enc, rem') = readInt32FromBytes rem+ in+ readPageTypeHeader (hdr{definitionLevelEncoding = parquetEncodingFromInt enc}) rem' identifier+ 4 ->+ let+ (enc, rem') = readInt32FromBytes rem+ in+ readPageTypeHeader (hdr{repetitionLevelEncoding = parquetEncodingFromInt enc}) rem' identifier+ 5 ->+ let+ (stats, rem') = readStatisticsFromBytes emptyColumnStatistics rem 0+ in+ readPageTypeHeader (hdr{dataPageHeaderStatistics = stats}) rem' identifier+ n -> error $ show n+readPageTypeHeader hdr@(DataPageHeaderV2{..}) xs lastFieldId =+ let+ fieldContents = readField' xs lastFieldId+ in+ case fieldContents of+ Nothing -> (hdr, drop 1 xs)+ Just (rem, elemType, identifier) -> case identifier of+ 1 ->+ let+ (numValues, rem') = readInt32FromBytes rem+ in+ readPageTypeHeader (hdr{dataPageHeaderV2NumValues = numValues}) rem' identifier+ 2 ->+ let+ (numNulls, rem') = readInt32FromBytes rem+ in+ readPageTypeHeader (hdr{dataPageHeaderV2NumNulls = numNulls}) rem' identifier+ 3 ->+ let+ (numRows, rem') = readInt32FromBytes rem+ in+ readPageTypeHeader (hdr{dataPageHeaderV2NumRows = numRows}) rem' identifier+ 4 ->+ let+ (enc, rem') = readInt32FromBytes rem+ in+ readPageTypeHeader (hdr{dataPageHeaderV2Encoding = parquetEncodingFromInt enc}) rem' identifier+ 5 ->+ let+ (n, rem') = readInt32FromBytes rem+ in+ readPageTypeHeader (hdr{definitionLevelByteLength = n}) rem' identifier+ 6 ->+ let+ (n, rem') = readInt32FromBytes rem+ in+ readPageTypeHeader (hdr{repetitionLevelByteLength = n}) rem' identifier+ 7 ->+ let+ isCompressed = fromMaybe True $ fmap ((== compactBooleanTrue) . (.&. 0x0f)) (safeHead xs)+ in+ readPageTypeHeader (hdr{dataPageHeaderV2IsCompressed = dataPageHeaderV2IsCompressed}) rem identifier+ 8 ->+ let+ (stats, rem') = readStatisticsFromBytes emptyColumnStatistics rem 0+ in+ readPageTypeHeader (hdr{dataPageHeaderV2Statistics = stats}) rem' identifier+ n -> error $ show n++safeHead :: [a] -> Maybe a+safeHead [] = Nothing+safeHead (x : _) = Just x++readField' :: [Word8] -> Int16 -> Maybe ([Word8], TType, Int16)+readField' [] _ = Nothing+readField' (x : xs) lastFieldId+ | x .&. 0x0f == 0 = Nothing+ | otherwise =+ let modifier = fromIntegral ((x .&. 0xf0) `shiftR` 4) :: Int16+ (identifier, rem) = if modifier == 0 then readIntFromBytes @Int16 xs else (lastFieldId + modifier, xs)+ elemType = toTType (x .&. 0x0f)+ in Just (rem, elemType, identifier)++readAllPages :: CompressionCodec -> [Word8] -> IO [Page]+readAllPages codec bytes = go bytes []+ where+ go [] acc = return (reverse acc)+ go bs acc = do+ (maybePage, remaining) <- readPage codec bs+ case maybePage of+ Nothing -> return (reverse acc)+ Just page -> go remaining (page : acc)++readNInt32 :: Int -> [Word8] -> ([Int32], [Word8])+readNInt32 0 bs = ([], bs)+readNInt32 k bs =+ let x = littleEndianInt32 (take 4 bs)+ bs' = drop 4 bs+ (xs, rest) = readNInt32 (k - 1) bs'+ in (x : xs, rest)++readNDouble :: Int -> [Word8] -> ([Double], [Word8])+readNDouble 0 bs = ([], bs)+readNDouble k bs =+ let x = castWord64ToDouble (littleEndianWord64 (take 8 bs))+ bs' = drop 8 bs+ (xs, rest) = readNDouble (k - 1) bs'+ in (x : xs, rest)++readNByteArrays :: Int -> [Word8] -> ([[Word8]], [Word8])+readNByteArrays 0 bs = ([], bs)+readNByteArrays k bs =+ let len = fromIntegral (littleEndianInt32 (take 4 bs)) :: Int+ body = take len (drop 4 bs)+ bs' = drop (4 + len) bs+ (xs, rest) = readNByteArrays (k - 1) bs'+ in (body : xs, rest)++readNBool :: Int -> [Word8] -> ([Bool], [Word8])+readNBool 0 bs = ([], bs)+readNBool count bs =+ let totalBytes = (count + 7) `div` 8+ chunk = take totalBytes bs+ rest = drop totalBytes bs+ bits = concatMap (\b -> map (\i -> (b `shiftR` i) .&. 1 == 1) [0 .. 7]) chunk+ bools = take count bits+ in (bools, rest)++readNInt64 :: Int -> [Word8] -> ([Int64], [Word8])+readNInt64 0 bs = ([], bs)+readNInt64 k bs =+ let x = fromIntegral (littleEndianWord64 (take 8 bs))+ bs' = drop 8 bs+ (xs, rest) = readNInt64 (k - 1) bs'+ in (x : xs, rest)++readNFloat :: Int -> [Word8] -> ([Float], [Word8])+readNFloat 0 bs = ([], bs)+readNFloat k bs =+ let x = castWord32ToFloat (littleEndianWord32 (take 4 bs))+ bs' = drop 4 bs+ (xs, rest) = readNFloat (k - 1) bs'+ in (x : xs, rest)++splitFixed :: Int -> Int -> [Word8] -> ([[Word8]], [Word8])+splitFixed 0 _ bs = ([], bs)+splitFixed k len bs =+ let body = take len bs+ bs' = drop len bs+ (xs, rest) = splitFixed (k - 1) len bs'+ in (body : xs, rest)++readStatisticsFromBytes :: ColumnStatistics -> [Word8] -> Int16 -> (ColumnStatistics, [Word8])+readStatisticsFromBytes cs xs lastFieldId =+ let+ fieldContents = readField' xs lastFieldId+ in+ case fieldContents of+ Nothing -> (cs, drop 1 xs)+ Just (rem, elemType, identifier) -> case identifier of+ 1 ->+ let+ (maxInBytes, rem') = readByteStringFromBytes rem+ in+ readStatisticsFromBytes (cs{columnMax = maxInBytes}) rem' identifier+ 2 ->+ let+ (minInBytes, rem') = readByteStringFromBytes rem+ in+ readStatisticsFromBytes (cs{columnMin = minInBytes}) rem' identifier+ 3 ->+ let+ (nullCount, rem') = readIntFromBytes @Int64 rem+ in+ readStatisticsFromBytes (cs{columnNullCount = nullCount}) rem' identifier+ 4 ->+ let+ (distinctCount, rem') = readIntFromBytes @Int64 rem+ in+ readStatisticsFromBytes (cs{columnDistictCount = distinctCount}) rem' identifier+ 5 ->+ let+ (maxInBytes, rem') = readByteStringFromBytes rem+ in+ readStatisticsFromBytes (cs{columnMaxValue = maxInBytes}) rem' identifier+ 6 ->+ let+ (minInBytes, rem') = readByteStringFromBytes rem+ in+ readStatisticsFromBytes (cs{columnMinValue = minInBytes}) rem' identifier+ 7 ->+ let+ (isMaxValueExact : rem') = rem+ in+ readStatisticsFromBytes (cs{isColumnMaxValueExact = isMaxValueExact == compactBooleanTrue}) rem' identifier+ 8 ->+ let+ (isMinValueExact : rem') = rem+ in+ readStatisticsFromBytes (cs{isColumnMinValueExact = isMinValueExact == compactBooleanTrue}) rem' identifier+ n -> error $ show n
+ src/DataFrame/IO/Parquet/Thrift.hs view
@@ -0,0 +1,774 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module DataFrame.IO.Parquet.Thrift where++import Control.Monad+import Data.Bits+import Data.Char+import Data.IORef+import Data.Int+import qualified Data.Map as M+import Data.Maybe+import qualified Data.Text as T+import Data.Word+import DataFrame.IO.Parquet.Binary+import DataFrame.IO.Parquet.Types+import Foreign+import System.IO++data SchemaElement = SchemaElement+ { elementName :: T.Text+ , elementType :: TType+ , typeLength :: Int32+ , numChildren :: Int32+ , fieldId :: Int32+ , repetitionType :: RepetitionType+ , convertedType :: Int32+ , scale :: Int32+ , precision :: Int32+ , logicalType :: LogicalType+ }+ deriving (Show, Eq)++data KeyValue = KeyValue+ { key :: String+ , value :: String+ }+ deriving (Show, Eq)++data FileMetadata = FileMetaData+ { version :: Int32+ , schema :: [SchemaElement]+ , numRows :: Integer+ , rowGroups :: [RowGroup]+ , keyValueMetadata :: [KeyValue]+ , createdBy :: Maybe String+ , columnOrders :: [ColumnOrder]+ , encryptionAlgorithm :: EncryptionAlgorithm+ , footerSigningKeyMetadata :: [Word8]+ }+ deriving (Show, Eq)++data TType+ = STOP+ | BOOL+ | BYTE+ | I16+ | I32+ | I64+ | DOUBLE+ | STRING+ | LIST+ | SET+ | MAP+ | STRUCT+ | UUID+ deriving (Show, Eq)++defaultMetadata :: FileMetadata+defaultMetadata =+ FileMetaData+ { version = 0+ , schema = []+ , numRows = 0+ , rowGroups = []+ , keyValueMetadata = []+ , createdBy = Nothing+ , columnOrders = []+ , encryptionAlgorithm = ENCRYPTION_ALGORITHM_UNKNOWN+ , footerSigningKeyMetadata = []+ }++data ColumnMetaData = ColumnMetaData+ { columnType :: ParquetType+ , columnEncodings :: [ParquetEncoding]+ , columnPathInSchema :: [String]+ , columnCodec :: CompressionCodec+ , columnNumValues :: Int64+ , columnTotalUncompressedSize :: Int64+ , columnTotalCompressedSize :: Int64+ , columnKeyValueMetadata :: [KeyValue]+ , columnDataPageOffset :: Int64+ , columnIndexPageOffset :: Int64+ , columnDictionaryPageOffset :: Int64+ , columnStatistics :: ColumnStatistics+ , columnEncodingStats :: [PageEncodingStats]+ , bloomFilterOffset :: Int64+ , bloomFilterLength :: Int32+ , columnSizeStatistics :: SizeStatistics+ , columnGeospatialStatistics :: GeospatialStatistics+ }+ deriving (Show, Eq)++data ColumnChunk = ColumnChunk+ { columnChunkFilePath :: String+ , columnChunkMetadataFileOffset :: Int64+ , columnMetaData :: ColumnMetaData+ , columnChunkOffsetIndexOffset :: Int64+ , columnChunkOffsetIndexLength :: Int32+ , columnChunkColumnIndexOffset :: Int64+ , columnChunkColumnIndexLength :: Int32+ , cryptoMetadata :: ColumnCryptoMetadata+ , encryptedColumnMetadata :: [Word8]+ }+ deriving (Show, Eq)++data RowGroup = RowGroup+ { rowGroupColumns :: [ColumnChunk]+ , totalByteSize :: Int64+ , rowGroupNumRows :: Int64+ , rowGroupSortingColumns :: [SortingColumn]+ , fileOffset :: Int64+ , totalCompressedSize :: Int64+ , ordinal :: Int16+ }+ deriving (Show, Eq)++defaultSchemaElement :: SchemaElement+defaultSchemaElement = SchemaElement "" STOP 0 0 (-1) UNKNOWN_REPETITION_TYPE 0 0 0 LOGICAL_TYPE_UNKNOWN++emptyColumnMetadata :: ColumnMetaData+emptyColumnMetadata = ColumnMetaData PARQUET_TYPE_UNKNOWN [] [] COMPRESSION_CODEC_UNKNOWN 0 0 0 [] 0 0 0 emptyColumnStatistics [] 0 0 emptySizeStatistics emptyGeospatialStatistics++emptyColumnChunk :: ColumnChunk+emptyColumnChunk = ColumnChunk "" 0 emptyColumnMetadata 0 0 0 0 COLUMN_CRYPTO_METADATA_UNKNOWN []++emptyKeyValue :: KeyValue+emptyKeyValue = KeyValue{key = "", value = ""}++emptyRowGroup :: RowGroup+emptyRowGroup = RowGroup [] 0 0 [] 0 0 0++compactBooleanTrue, compactI32, compactI64, compactDouble, compactBinary, compactList, compactStruct :: Word8+compactBooleanTrue = 0x01+compactI32 = 0x05+compactI64 = 0x06+compactDouble = 0x07+compactBinary = 0x08+compactList = 0x09+compactStruct = 0x0C++toTType :: Word8 -> TType+toTType t =+ fromMaybe STOP $+ M.lookup (t .&. 0x0f) $+ M.fromList+ [ (compactBooleanTrue, BOOL)+ , (compactI32, I32)+ , (compactI64, I64)+ , (compactDouble, DOUBLE)+ , (compactBinary, STRING)+ , (compactList, LIST)+ , (compactStruct, STRUCT)+ ]++readField :: Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO (Maybe (TType, Int16))+readField buf pos lastFieldId fieldStack = do+ t <- readAndAdvance pos buf+ if t .&. 0x0f == 0+ then return Nothing+ else do+ let modifier = fromIntegral ((t .&. 0xf0) `shiftR` 4) :: Int16+ identifier <-+ if modifier == 0+ then readIntFromBuffer @Int16 buf pos+ else return (lastFieldId + modifier)+ let elemType = toTType (t .&. 0x0f)+ pure $ Just (elemType, identifier)++skipToStructEnd :: Ptr Word8 -> IORef Int -> IO ()+skipToStructEnd buf pos = do+ t <- readAndAdvance pos buf+ if t .&. 0x0f == 0+ then return ()+ else do+ let modifier = fromIntegral ((t .&. 0xf0) `shiftR` 4) :: Int16+ identifier <-+ if modifier == 0+ then readIntFromBuffer @Int16 buf pos+ else return 0+ let elemType = toTType (t .&. 0x0f)+ skipFieldData elemType buf pos+ skipToStructEnd buf pos++skipFieldData :: TType -> Ptr Word8 -> IORef Int -> IO ()+skipFieldData fieldType buf pos = case fieldType of+ BOOL -> return ()+ I32 -> readIntFromBuffer @Int32 buf pos >> pure ()+ I64 -> readIntFromBuffer @Int64 buf pos >> pure ()+ DOUBLE -> readIntFromBuffer @Int64 buf pos >> pure ()+ STRING -> readByteString buf pos >> pure ()+ LIST -> skipList buf pos+ STRUCT -> skipToStructEnd buf pos+ _ -> return ()++skipList :: Ptr Word8 -> IORef Int -> IO ()+skipList buf pos = do+ sizeAndType <- readAndAdvance pos buf+ let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int+ let elemType = toTType sizeAndType+ replicateM_ sizeOnly (skipFieldData elemType buf pos)++readMetadata :: Handle -> Integer -> IO FileMetadata+readMetadata handle size = do+ metaDataBuf <- mallocBytes (fromIntegral size) :: IO (Ptr Word8)+ footerOffSet <- numBytesInFile handle++ hSeek handle AbsoluteSeek (fromIntegral $! footerOffSet - footerSize - size)++ metadataBytesRead <- hGetBuf handle metaDataBuf (fromIntegral size)+ let lastFieldId = 0+ let fieldStack = []+ bufferPos <- newIORef (0 :: Int)+ metadata <- readFileMetaData defaultMetadata metaDataBuf bufferPos lastFieldId fieldStack+ free metaDataBuf+ return metadata++readFileMetaData :: FileMetadata -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO FileMetadata+readFileMetaData metadata metaDataBuf bufferPos lastFieldId fieldStack = do+ fieldContents <- readField metaDataBuf bufferPos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return metadata+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ version <- readIntFromBuffer @Int32 metaDataBuf bufferPos+ readFileMetaData (metadata{version = version}) metaDataBuf bufferPos identifier fieldStack+ 2 -> do+ sizeAndType <- readAndAdvance bufferPos metaDataBuf+ listSize <-+ if (sizeAndType `shiftR` 4) .&. 0x0f == 15+ then readVarIntFromBuffer @Int metaDataBuf bufferPos+ else return $ fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f)++ let elemType = toTType sizeAndType+ schemaElements <- replicateM listSize (readSchemaElement defaultSchemaElement metaDataBuf bufferPos 0 [])+ readFileMetaData (metadata{schema = schemaElements}) metaDataBuf bufferPos identifier fieldStack+ 3 -> do+ numRows <- readIntFromBuffer @Int64 metaDataBuf bufferPos+ readFileMetaData (metadata{numRows = fromIntegral numRows}) metaDataBuf bufferPos identifier fieldStack+ 4 -> do+ sizeAndType <- readAndAdvance bufferPos metaDataBuf+ listSize <-+ if (sizeAndType `shiftR` 4) .&. 0x0f == 15+ then readVarIntFromBuffer @Int metaDataBuf bufferPos+ else return $ fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f)++ let elemType = toTType sizeAndType+ rowGroups <- replicateM listSize (readRowGroup emptyRowGroup metaDataBuf bufferPos 0 [])+ readFileMetaData (metadata{rowGroups = rowGroups}) metaDataBuf bufferPos identifier fieldStack+ 5 -> do+ sizeAndType <- readAndAdvance bufferPos metaDataBuf+ listSize <-+ if (sizeAndType `shiftR` 4) .&. 0x0f == 15+ then readVarIntFromBuffer @Int metaDataBuf bufferPos+ else return $ fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f)++ let elemType = toTType sizeAndType+ keyValueMetadata <- replicateM listSize (readKeyValue emptyKeyValue metaDataBuf bufferPos 0 [])+ readFileMetaData (metadata{keyValueMetadata = keyValueMetadata}) metaDataBuf bufferPos identifier fieldStack+ 6 -> do+ createdBy <- readString metaDataBuf bufferPos+ readFileMetaData (metadata{createdBy = Just createdBy}) metaDataBuf bufferPos identifier fieldStack+ 7 -> do+ sizeAndType <- readAndAdvance bufferPos metaDataBuf+ listSize <-+ if (sizeAndType `shiftR` 4) .&. 0x0f == 15+ then readVarIntFromBuffer @Int metaDataBuf bufferPos+ else return $ fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f)++ let elemType = toTType sizeAndType+ columnOrders <- replicateM listSize (readColumnOrder metaDataBuf bufferPos 0 [])+ readFileMetaData (metadata{columnOrders = columnOrders}) metaDataBuf bufferPos identifier fieldStack+ 8 -> do+ encryptionAlgorithm <- readEncryptionAlgorithm metaDataBuf bufferPos 0 []+ readFileMetaData (metadata{encryptionAlgorithm = encryptionAlgorithm}) metaDataBuf bufferPos identifier fieldStack+ 9 -> do+ footerSigningKeyMetadata <- readByteString metaDataBuf bufferPos+ readFileMetaData (metadata{footerSigningKeyMetadata = footerSigningKeyMetadata}) metaDataBuf bufferPos identifier fieldStack+ n -> return $ error $ "UNIMPLEMENTED " ++ show n++readSchemaElement :: SchemaElement -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO SchemaElement+readSchemaElement schemaElement buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return schemaElement+ Just (STOP, _) -> return schemaElement+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ schemaElemType <- toIntegralType <$> readInt32FromBuffer buf pos+ readSchemaElement (schemaElement{elementType = schemaElemType}) buf pos identifier fieldStack+ 2 -> do+ typeLength <- readInt32FromBuffer buf pos+ readSchemaElement (schemaElement{typeLength = typeLength}) buf pos identifier fieldStack+ 3 -> do+ fieldRepetitionType <- readInt32FromBuffer buf pos+ readSchemaElement (schemaElement{repetitionType = repetitionTypeFromInt fieldRepetitionType}) buf pos identifier fieldStack+ 4 -> do+ nameSize <- readVarIntFromBuffer @Int buf pos+ if nameSize <= 0+ then readSchemaElement schemaElement buf pos identifier fieldStack+ else do+ contents <- replicateM nameSize (readAndAdvance pos buf)+ readSchemaElement (schemaElement{elementName = T.pack (map (chr . fromIntegral) contents)}) buf pos identifier fieldStack+ 5 -> do+ numChildren <- readInt32FromBuffer buf pos+ readSchemaElement (schemaElement{numChildren = numChildren}) buf pos identifier fieldStack+ 6 -> do+ convertedType <- readInt32FromBuffer buf pos+ readSchemaElement (schemaElement{convertedType = convertedType}) buf pos identifier fieldStack+ 7 -> do+ scale <- readInt32FromBuffer buf pos+ readSchemaElement (schemaElement{scale = scale}) buf pos identifier fieldStack+ 8 -> do+ precision <- readInt32FromBuffer buf pos+ readSchemaElement (schemaElement{precision = precision}) buf pos identifier fieldStack+ 9 -> do+ fieldId <- readInt32FromBuffer buf pos+ readSchemaElement (schemaElement{fieldId = fieldId}) buf pos identifier fieldStack+ 10 -> do+ logicalType <- readLogicalType buf pos 0 []+ readSchemaElement (schemaElement{logicalType = logicalType}) buf pos identifier fieldStack+ _ -> do+ skipFieldData elemType buf pos+ readSchemaElement schemaElement buf pos identifier fieldStack++readRowGroup :: RowGroup -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO RowGroup+readRowGroup r buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return r+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ sizeAndType <- readAndAdvance pos buf+ let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int+ -- type of the contents of the list.+ let elemType = toTType sizeAndType+ columnChunks <- replicateM sizeOnly (readColumnChunk emptyColumnChunk buf pos 0 [])+ readRowGroup (r{rowGroupColumns = columnChunks}) buf pos identifier fieldStack+ 2 -> do+ totalBytes <- readIntFromBuffer @Int64 buf pos+ readRowGroup (r{totalByteSize = totalBytes}) buf pos identifier fieldStack+ 3 -> do+ nRows <- readIntFromBuffer @Int64 buf pos+ readRowGroup (r{rowGroupNumRows = nRows}) buf pos identifier fieldStack+ 4 -> return r+ 5 -> do+ offset <- readIntFromBuffer @Int64 buf pos+ readRowGroup (r{fileOffset = offset}) buf pos identifier fieldStack+ 6 -> do+ compressedSize <- readIntFromBuffer @Int64 buf pos+ readRowGroup (r{totalCompressedSize = compressedSize}) buf pos identifier fieldStack+ 7 -> do+ ordinal <- readIntFromBuffer @Int16 buf pos+ readRowGroup (r{ordinal = ordinal}) buf pos identifier fieldStack+ _ -> error $ "Unknown row group field: " ++ show identifier++readColumnChunk :: ColumnChunk -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO ColumnChunk+readColumnChunk c buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return c+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ stringSize <- readVarIntFromBuffer @Int buf pos+ contents <- map (chr . fromIntegral) <$> replicateM stringSize (readAndAdvance pos buf)+ readColumnChunk (c{columnChunkFilePath = contents}) buf pos identifier fieldStack+ 2 -> do+ columnChunkMetadataFileOffset <- readIntFromBuffer @Int64 buf pos+ readColumnChunk (c{columnChunkMetadataFileOffset = columnChunkMetadataFileOffset}) buf pos identifier fieldStack+ 3 -> do+ columnMetadata <- readColumnMetadata emptyColumnMetadata buf pos 0 []+ readColumnChunk (c{columnMetaData = columnMetadata}) buf pos identifier fieldStack+ 4 -> do+ columnOffsetIndexOffset <- readIntFromBuffer @Int64 buf pos+ readColumnChunk (c{columnChunkOffsetIndexOffset = columnOffsetIndexOffset}) buf pos identifier fieldStack+ 5 -> do+ columnOffsetIndexLength <- readInt32FromBuffer buf pos+ readColumnChunk (c{columnChunkOffsetIndexLength = columnOffsetIndexLength}) buf pos identifier fieldStack+ 6 -> do+ columnChunkColumnIndexOffset <- readIntFromBuffer @Int64 buf pos+ readColumnChunk (c{columnChunkColumnIndexOffset = columnChunkColumnIndexOffset}) buf pos identifier fieldStack+ 7 -> do+ columnChunkColumnIndexLength <- readInt32FromBuffer buf pos+ readColumnChunk (c{columnChunkColumnIndexLength = columnChunkColumnIndexLength}) buf pos identifier fieldStack+ _ -> return c++readColumnMetadata :: ColumnMetaData -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO ColumnMetaData+readColumnMetadata cm buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return cm+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ cType <- parquetTypeFromInt <$> readInt32FromBuffer buf pos+ readColumnMetadata (cm{columnType = cType}) buf pos identifier []+ 2 -> do+ sizeAndType <- readAndAdvance pos buf+ let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int+ let elemType = toTType sizeAndType+ encodings <- replicateM sizeOnly (readParquetEncoding buf pos 0 [])+ readColumnMetadata (cm{columnEncodings = encodings}) buf pos identifier fieldStack+ 3 -> do+ sizeAndType <- readAndAdvance pos buf+ let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int+ let elemType = toTType sizeAndType+ paths <- replicateM sizeOnly (readString buf pos)+ readColumnMetadata (cm{columnPathInSchema = paths}) buf pos identifier fieldStack+ 4 -> do+ cType <- compressionCodecFromInt <$> readInt32FromBuffer buf pos+ readColumnMetadata (cm{columnCodec = cType}) buf pos identifier []+ 5 -> do+ numValues <- readIntFromBuffer @Int64 buf pos+ readColumnMetadata (cm{columnNumValues = numValues}) buf pos identifier []+ 6 -> do+ columnTotalUncompressedSize <- readIntFromBuffer @Int64 buf pos+ readColumnMetadata (cm{columnTotalUncompressedSize = columnTotalUncompressedSize}) buf pos identifier []+ 7 -> do+ columnTotalCompressedSize <- readIntFromBuffer @Int64 buf pos+ readColumnMetadata (cm{columnTotalCompressedSize = columnTotalCompressedSize}) buf pos identifier []+ 8 -> do+ sizeAndType <- readAndAdvance pos buf+ let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int+ let elemType = toTType sizeAndType+ columnKeyValueMetadata <- replicateM sizeOnly (readKeyValue emptyKeyValue buf pos 0 [])+ readColumnMetadata (cm{columnKeyValueMetadata = columnKeyValueMetadata}) buf pos identifier fieldStack+ 9 -> do+ columnDataPageOffset <- readIntFromBuffer @Int64 buf pos+ readColumnMetadata (cm{columnDataPageOffset = columnDataPageOffset}) buf pos identifier []+ 10 -> do+ columnIndexPageOffset <- readIntFromBuffer @Int64 buf pos+ readColumnMetadata (cm{columnIndexPageOffset = columnIndexPageOffset}) buf pos identifier []+ 11 -> do+ columnDictionaryPageOffset <- readIntFromBuffer @Int64 buf pos+ readColumnMetadata (cm{columnDictionaryPageOffset = columnDictionaryPageOffset}) buf pos identifier []+ 12 -> do+ stats <- readStatistics emptyColumnStatistics buf pos 0 []+ readColumnMetadata (cm{columnStatistics = stats}) buf pos identifier fieldStack+ 13 -> do+ sizeAndType <- readAndAdvance pos buf+ let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int+ let elemType = toTType sizeAndType+ pageEncodingStats <- replicateM sizeOnly (readPageEncodingStats emptyPageEncodingStats buf pos 0 [])+ readColumnMetadata (cm{columnEncodingStats = pageEncodingStats}) buf pos identifier fieldStack+ 14 -> do+ bloomFilterOffset <- readIntFromBuffer @Int64 buf pos+ readColumnMetadata (cm{bloomFilterOffset = bloomFilterOffset}) buf pos identifier []+ 15 -> do+ bloomFilterLength <- readInt32FromBuffer buf pos+ readColumnMetadata (cm{bloomFilterLength = bloomFilterLength}) buf pos identifier []+ 16 -> do+ stats <- readSizeStatistics emptySizeStatistics buf pos 0 []+ readColumnMetadata (cm{columnSizeStatistics = stats}) buf pos identifier fieldStack+ 17 -> return $ error "UNIMPLEMENTED"+ _ -> return cm++readEncryptionAlgorithm :: Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO EncryptionAlgorithm+readEncryptionAlgorithm buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return ENCRYPTION_ALGORITHM_UNKNOWN+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ readAesGcmV1 (AesGcmV1{aadPrefix = [], aadFileUnique = [], supplyAadPrefix = False}) buf pos 0 []+ 2 -> do+ readAesGcmCtrV1 (AesGcmCtrV1{aadPrefix = [], aadFileUnique = [], supplyAadPrefix = False}) buf pos 0 []+ n -> return ENCRYPTION_ALGORITHM_UNKNOWN++readColumnOrder :: Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO ColumnOrder+readColumnOrder buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return COLUMN_ORDER_UNKNOWN+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ _ <- replicateM_ 2 (readTypeOrder buf pos 0 [])+ return TYPE_ORDER+ _ -> return COLUMN_ORDER_UNKNOWN++readAesGcmCtrV1 :: EncryptionAlgorithm -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO EncryptionAlgorithm+readAesGcmCtrV1 v@(AesGcmCtrV1 aadPrefix aadFileUnique supplyAadPrefix) buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return v+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ aadPrefix <- readByteString buf pos+ readAesGcmCtrV1 (v{aadPrefix = aadPrefix}) buf pos lastFieldId fieldStack+ 2 -> do+ aadFileUnique <- readByteString buf pos+ readAesGcmCtrV1 (v{aadFileUnique = aadFileUnique}) buf pos lastFieldId fieldStack+ 3 -> do+ supplyAadPrefix <- readAndAdvance pos buf+ readAesGcmCtrV1 (v{supplyAadPrefix = supplyAadPrefix == compactBooleanTrue}) buf pos lastFieldId fieldStack+ _ -> return ENCRYPTION_ALGORITHM_UNKNOWN++readAesGcmV1 :: EncryptionAlgorithm -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO EncryptionAlgorithm+readAesGcmV1 v@(AesGcmV1 aadPrefix aadFileUnique supplyAadPrefix) buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return v+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ aadPrefix <- readByteString buf pos+ readAesGcmV1 (v{aadPrefix = aadPrefix}) buf pos lastFieldId fieldStack+ 2 -> do+ aadFileUnique <- readByteString buf pos+ readAesGcmV1 (v{aadFileUnique = aadFileUnique}) buf pos lastFieldId fieldStack+ 3 -> do+ supplyAadPrefix <- readAndAdvance pos buf+ readAesGcmV1 (v{supplyAadPrefix = supplyAadPrefix == compactBooleanTrue}) buf pos lastFieldId fieldStack+ _ -> return ENCRYPTION_ALGORITHM_UNKNOWN++readTypeOrder :: Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO ColumnOrder+readTypeOrder buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return TYPE_ORDER+ Just (elemType, identifier) ->+ if elemType == STOP+ then return TYPE_ORDER+ else readTypeOrder buf pos identifier fieldStack++readKeyValue :: KeyValue -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO KeyValue+readKeyValue kv buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return kv+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ k <- readString buf pos+ readKeyValue (kv{key = k}) buf pos identifier fieldStack+ 2 -> do+ v <- readString buf pos+ readKeyValue (kv{value = v}) buf pos identifier fieldStack+ _ -> return kv++readPageEncodingStats :: PageEncodingStats -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO PageEncodingStats+readPageEncodingStats pes buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return pes+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ pType <- pageTypeFromInt <$> readInt32FromBuffer buf pos+ readPageEncodingStats (pes{pageEncodingPageType = pType}) buf pos identifier []+ 2 -> do+ pEnc <- parquetEncodingFromInt <$> readInt32FromBuffer buf pos+ readPageEncodingStats (pes{pageEncoding = pEnc}) buf pos identifier []+ 3 -> do+ encodedCount <- readInt32FromBuffer buf pos+ readPageEncodingStats (pes{pagesWithEncoding = encodedCount}) buf pos identifier []+ _ -> pure pes++readParquetEncoding :: Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO ParquetEncoding+readParquetEncoding buf pos lastFieldId fieldStack = parquetEncodingFromInt <$> readInt32FromBuffer buf pos++readStatistics :: ColumnStatistics -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO ColumnStatistics+readStatistics cs buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return cs+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ maxInBytes <- readByteString buf pos+ readStatistics (cs{columnMax = maxInBytes}) buf pos identifier fieldStack+ 2 -> do+ minInBytes <- readByteString buf pos+ readStatistics (cs{columnMin = minInBytes}) buf pos identifier fieldStack+ 3 -> do+ nullCount <- readIntFromBuffer @Int64 buf pos+ readStatistics (cs{columnNullCount = nullCount}) buf pos identifier fieldStack+ 4 -> do+ distinctCount <- readIntFromBuffer @Int64 buf pos+ readStatistics (cs{columnDistictCount = distinctCount}) buf pos identifier fieldStack+ 5 -> do+ maxInBytes <- readByteString buf pos+ readStatistics (cs{columnMaxValue = maxInBytes}) buf pos identifier fieldStack+ 6 -> do+ minInBytes <- readByteString buf pos+ readStatistics (cs{columnMinValue = minInBytes}) buf pos identifier fieldStack+ 7 -> do+ isMaxValueExact <- readAndAdvance pos buf+ readStatistics (cs{isColumnMaxValueExact = isMaxValueExact == compactBooleanTrue}) buf pos identifier fieldStack+ 8 -> do+ isMinValueExact <- readAndAdvance pos buf+ readStatistics (cs{isColumnMinValueExact = isMinValueExact == compactBooleanTrue}) buf pos identifier fieldStack+ _ -> pure cs++readSizeStatistics :: SizeStatistics -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO SizeStatistics+readSizeStatistics ss buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return ss+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ unencodedByteArrayDataTypes <- readIntFromBuffer @Int64 buf pos+ readSizeStatistics (ss{unencodedByteArrayDataTypes = unencodedByteArrayDataTypes}) buf pos identifier fieldStack+ 2 -> do+ sizeAndType <- readAndAdvance pos buf+ let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int+ let elemType = toTType sizeAndType+ repetitionLevelHistogram <- replicateM sizeOnly (readIntFromBuffer @Int64 buf pos)+ readSizeStatistics (ss{repetitionLevelHistogram = repetitionLevelHistogram}) buf pos identifier fieldStack+ 3 -> do+ sizeAndType <- readAndAdvance pos buf+ let sizeOnly = fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f) :: Int+ let elemType = toTType sizeAndType+ definitionLevelHistogram <- replicateM sizeOnly (readIntFromBuffer @Int64 buf pos)+ readSizeStatistics (ss{definitionLevelHistogram = definitionLevelHistogram}) buf pos identifier fieldStack+ _ -> pure ss++footerSize :: Integer+footerSize = 8++toIntegralType :: Int32 -> TType+toIntegralType n+ | n == 1 = I32+ | n == 2 = I64+ | n == 4 = DOUBLE+ | n == 6 = STRING+ | otherwise = STRING++readLogicalType :: Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO LogicalType+readLogicalType buf pos lastFieldId fieldStack = do+ t <- readAndAdvance pos buf+ if t .&. 0x0f == 0+ then return LOGICAL_TYPE_UNKNOWN+ else do+ let modifier = fromIntegral ((t .&. 0xf0) `shiftR` 4) :: Int16+ identifier <-+ if modifier == 0+ then readIntFromBuffer @Int16 buf pos+ else return (lastFieldId + modifier)+ let elemType = toTType (t .&. 0x0f)+ case identifier of+ 1 -> do+ replicateM_ 2 (readField buf pos 0 [])+ return STRING_TYPE+ 2 -> do+ replicateM_ 2 (readField buf pos 0 [])+ return MAP_TYPE+ 3 -> do+ replicateM_ 2 (readField buf pos 0 [])+ return LIST_TYPE+ 4 -> do+ replicateM_ 2 (readField buf pos 0 [])+ return ENUM_TYPE+ 5 -> do+ readDecimalType (DecimalType{decimalTypeScale = 0, decimalTypePrecision = 0}) buf pos 0 []+ 6 -> do+ replicateM_ 2 (readField buf pos 0 [])+ return DATE_TYPE+ 7 -> do+ readTimeType (TimeType{isAdjustedToUTC = False, unit = MILLISECONDS}) buf pos 0 []+ 8 -> do+ readTimeType (TimestampType{isAdjustedToUTC = False, unit = MILLISECONDS}) buf pos 0 []+ -- Apparently reserved for interval types+ 9 -> return LOGICAL_TYPE_UNKNOWN+ 10 -> do+ intType <- readIntType (IntType{intIsSigned = False, bitWidth = 0}) buf pos 0 []+ _ <- readField buf pos 0 []+ pure intType+ 11 -> do+ replicateM_ 2 (readField buf pos 0 [])+ return LOGICAL_TYPE_UNKNOWN+ 12 -> do+ replicateM_ 2 (readField buf pos 0 [])+ return JSON_TYPE+ 13 -> do+ replicateM_ 2 (readField buf pos 0 [])+ return BSON_TYPE+ 14 -> do+ replicateM_ 2 (readField buf pos 0 [])+ return UUID_TYPE+ 15 -> do+ replicateM_ 2 (readField buf pos 0 [])+ return FLOAT16_TYPE+ 16 -> do+ return VariantType{specificationVersion = 1}+ 17 -> do+ return GeometryType{crs = ""}+ 18 -> do+ return GeographyType{crs = "", algorithm = SPHERICAL}+ _ -> return LOGICAL_TYPE_UNKNOWN++readIntType :: LogicalType -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO LogicalType+readIntType v@(IntType bitWidth intIsSigned) buf pos lastFieldId fieldStack = do+ t <- readAndAdvance pos buf+ if t .&. 0x0f == 0+ then return v+ else do+ let modifier = fromIntegral ((t .&. 0xf0) `shiftR` 4) :: Int16+ identifier <-+ if modifier == 0+ then readIntFromBuffer @Int16 buf pos+ else return (lastFieldId + modifier)++ case identifier of+ 1 -> do+ bitWidthValue <- readAndAdvance pos buf+ readIntType (v{bitWidth = fromIntegral bitWidthValue}) buf pos identifier fieldStack+ 2 -> do+ let isSigned = (t .&. 0x0f) == compactBooleanTrue+ readIntType (v{intIsSigned = isSigned}) buf pos identifier fieldStack+ _ -> error $ "UNKNOWN field ID for IntType: " ++ show identifier++readDecimalType :: LogicalType -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO LogicalType+readDecimalType v@(DecimalType p s) buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return v+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ s' <- readInt32FromBuffer buf pos+ readDecimalType (v{decimalTypeScale = s'}) buf pos lastFieldId fieldStack+ 2 -> do+ p' <- readInt32FromBuffer buf pos+ readDecimalType (v{decimalTypePrecision = p'}) buf pos lastFieldId fieldStack+ _ -> error $ "UNKNOWN field ID for DecimalType" ++ show identifier++readTimeType :: LogicalType -> Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO LogicalType+readTimeType v@(TimeType _ _) buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return v+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ -- TODO: Check for empty+ isAdjustedToUTC <- readAndAdvance pos buf+ readTimeType (v{isAdjustedToUTC = isAdjustedToUTC == compactBooleanTrue}) buf pos lastFieldId fieldStack+ 2 -> do+ u <- readUnit buf pos 0 []+ readTimeType (v{unit = u}) buf pos lastFieldId fieldStack+ _ -> error $ "UNKNOWN field ID for TimeType" ++ show identifier+readTimeType v@(TimestampType _ _) buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return v+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ -- TODO: Check for empty+ isAdjustedToUTC <- readAndAdvance pos buf+ readTimeType (v{isAdjustedToUTC = isAdjustedToUTC == compactBooleanTrue}) buf pos lastFieldId fieldStack+ 2 -> do+ u <- readUnit buf pos 0 []+ readTimeType (v{unit = u}) buf pos lastFieldId fieldStack+ _ -> error $ "UNKNOWN field ID for TimestampType" ++ show identifier++readUnit :: Ptr Word8 -> IORef Int -> Int16 -> [Int16] -> IO TimeUnit+readUnit buf pos lastFieldId fieldStack = do+ fieldContents <- readField buf pos lastFieldId fieldStack+ case fieldContents of+ Nothing -> return TIME_UNIT_UNKNOWN+ Just (elemType, identifier) -> case identifier of+ 1 -> do+ _ <- readField buf pos 0 []+ return MILLISECONDS+ 2 -> do+ _ <- readField buf pos 0 []+ return MICROSECONDS+ 3 -> do+ _ <- readField buf pos 0 []+ return NANOSECONDS+ _ -> return TIME_UNIT_UNKNOWN
+ src/DataFrame/IO/Parquet/Time.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE NumericUnderscores #-}++module DataFrame.IO.Parquet.Time where++import Data.Int+import Data.Time+import Data.Word++import DataFrame.IO.Parquet.Binary++int96ToUTCTime :: [Word8] -> UTCTime+int96ToUTCTime bytes+ | length bytes /= 12 = error "INT96 must be exactly 12 bytes"+ | otherwise =+ let (nanosBytes, julianBytes) = splitAt 8 bytes+ nanosSinceMidnight = littleEndianWord64 nanosBytes+ julianDay = littleEndianWord32 julianBytes+ in julianDayAndNanosToUTCTime (fromIntegral julianDay) nanosSinceMidnight++julianDayAndNanosToUTCTime :: Integer -> Word64 -> UTCTime+julianDayAndNanosToUTCTime julianDay nanosSinceMidnight =+ let day = julianDayToDay julianDay+ secondsSinceMidnight = fromIntegral nanosSinceMidnight / 1_000_000_000+ diffTime = secondsToDiffTime (floor secondsSinceMidnight)+ in UTCTime day diffTime++julianDayToDay :: Integer -> Day+julianDayToDay julianDay =+ let a = julianDay + 32044+ b = (4 * a + 3) `div` 146097+ c = a - (146097 * b) `div` 4+ d = (4 * c + 3) `div` 1461+ e = c - (1461 * d) `div` 4+ m = (5 * e + 2) `div` 153+ day = e - (153 * m + 2) `div` 5 + 1+ month = m + 3 - 12 * (m `div` 10)+ year = 100 * b + d - 4800 + m `div` 10+ in fromGregorian year (fromIntegral month) (fromIntegral day)++-- I include this here even though it's unused because we'll likely use+-- it for the writer. Since int96 is deprecated this is only included for completeness anyway.+utcTimeToInt96 :: UTCTime -> [Word8]+utcTimeToInt96 (UTCTime day diffTime) =+ let julianDay = dayToJulianDay day+ nanosSinceMidnight = floor (realToFrac diffTime * 1_000_000_000)+ nanosBytes = word64ToLittleEndian nanosSinceMidnight+ julianBytes = word32ToLittleEndian (fromIntegral julianDay)+ in nanosBytes ++ julianBytes++dayToJulianDay :: Day -> Integer+dayToJulianDay day =+ let (year, month, dayOfMonth) = toGregorian day+ a = fromIntegral $ (14 - (fromIntegral month)) `div` 12+ y = fromIntegral $ year + 4800 - a+ m = fromIntegral $ month + 12 * (fromIntegral a) - 3+ in fromIntegral dayOfMonth + (153 * m + 2) `div` 5 + 365 * y + y `div` 4 - y `div` 100 + y `div` 400 - 32045
src/DataFrame/IO/Parquet/Types.hs view
@@ -1,18 +1,23 @@+{-# LANGUAGE OverloadedStrings #-}+ module DataFrame.IO.Parquet.Types where import Data.Int+import qualified Data.Text as T+import Data.Time+import Data.Word data ParquetType- = PBOOLEAN- | PINT32- | PINT64- | PINT96- | PFLOAT- | PDOUBLE- | PBYTE_ARRAY- | PFIXED_LEN_BYTE_ARRAY- | PARQUET_TYPE_UNKNOWN- deriving (Show, Eq)+ = PBOOLEAN+ | PINT32+ | PINT64+ | PINT96+ | PFLOAT+ | PDOUBLE+ | PBYTE_ARRAY+ | PFIXED_LEN_BYTE_ARRAY+ | PARQUET_TYPE_UNKNOWN+ deriving (Show, Eq) parquetTypeFromInt :: Int32 -> ParquetType parquetTypeFromInt 0 = PBOOLEAN@@ -24,3 +29,266 @@ parquetTypeFromInt 6 = PBYTE_ARRAY parquetTypeFromInt 7 = PFIXED_LEN_BYTE_ARRAY parquetTypeFromInt _ = PARQUET_TYPE_UNKNOWN++data PageType+ = DATA_PAGE+ | INDEX_PAGE+ | DICTIONARY_PAGE+ | DATA_PAGE_V2+ | PAGE_TYPE_UNKNOWN+ deriving (Show, Eq)++pageTypeFromInt :: Int32 -> PageType+pageTypeFromInt 0 = DATA_PAGE+pageTypeFromInt 1 = INDEX_PAGE+pageTypeFromInt 2 = DICTIONARY_PAGE+pageTypeFromInt 3 = DATA_PAGE_V2+pageTypeFromInt _ = PAGE_TYPE_UNKNOWN++data ParquetEncoding+ = EPLAIN+ | EPLAIN_DICTIONARY+ | ERLE+ | EBIT_PACKED+ | EDELTA_BINARY_PACKED+ | EDELTA_LENGTH_BYTE_ARRAY+ | EDELTA_BYTE_ARRAY+ | ERLE_DICTIONARY+ | EBYTE_STREAM_SPLIT+ | PARQUET_ENCODING_UNKNOWN+ deriving (Show, Eq)++parquetEncodingFromInt :: Int32 -> ParquetEncoding+parquetEncodingFromInt 0 = EPLAIN+parquetEncodingFromInt 2 = EPLAIN_DICTIONARY+parquetEncodingFromInt 3 = ERLE+parquetEncodingFromInt 4 = EBIT_PACKED+parquetEncodingFromInt 5 = EDELTA_BINARY_PACKED+parquetEncodingFromInt 6 = EDELTA_LENGTH_BYTE_ARRAY+parquetEncodingFromInt 7 = EDELTA_BYTE_ARRAY+parquetEncodingFromInt 8 = ERLE_DICTIONARY+parquetEncodingFromInt 9 = EBYTE_STREAM_SPLIT+parquetEncodingFromInt _ = PARQUET_ENCODING_UNKNOWN++data CompressionCodec+ = UNCOMPRESSED+ | SNAPPY+ | GZIP+ | LZO+ | BROTLI+ | LZ4+ | ZSTD+ | LZ4_RAW+ | COMPRESSION_CODEC_UNKNOWN+ deriving (Show, Eq)++data PageEncodingStats = PageEncodingStats+ { pageEncodingPageType :: PageType+ , pageEncoding :: ParquetEncoding+ , pagesWithEncoding :: Int32+ }+ deriving (Show, Eq)++emptyPageEncodingStats :: PageEncodingStats+emptyPageEncodingStats = PageEncodingStats PAGE_TYPE_UNKNOWN PARQUET_ENCODING_UNKNOWN 0++data SizeStatistics = SizeStatisics+ { unencodedByteArrayDataTypes :: Int64+ , repetitionLevelHistogram :: [Int64]+ , definitionLevelHistogram :: [Int64]+ }+ deriving (Show, Eq)++emptySizeStatistics :: SizeStatistics+emptySizeStatistics = SizeStatisics 0 [] []++data BoundingBox = BoundingBox+ { xmin :: Double+ , xmax :: Double+ , ymin :: Double+ , ymax :: Double+ , zmin :: Double+ , zmax :: Double+ , mmin :: Double+ , mmax :: Double+ }+ deriving (Show, Eq)++emptyBoundingBox :: BoundingBox+emptyBoundingBox = BoundingBox 0 0 0 0 0 0 0 0++data GeospatialStatistics = GeospatialStatistics+ { bbox :: BoundingBox+ , geospatialTypes :: [Int32]+ }+ deriving (Show, Eq)++emptyGeospatialStatistics :: GeospatialStatistics+emptyGeospatialStatistics = GeospatialStatistics emptyBoundingBox []++data ColumnStatistics = ColumnStatistics+ { columnMin :: [Word8]+ , columnMax :: [Word8]+ , columnNullCount :: Int64+ , columnDistictCount :: Int64+ , columnMinValue :: [Word8]+ , columnMaxValue :: [Word8]+ , isColumnMaxValueExact :: Bool+ , isColumnMinValueExact :: Bool+ }+ deriving (Show, Eq)++emptyColumnStatistics :: ColumnStatistics+emptyColumnStatistics = ColumnStatistics [] [] 0 0 [] [] False False++data ColumnCryptoMetadata+ = COLUMN_CRYPTO_METADATA_UNKNOWN+ | ENCRYPTION_WITH_FOOTER_KEY+ | EncryptionWithColumnKey+ { columnCryptPathInSchema :: [String]+ , columnKeyMetadata :: [Word8]+ }+ deriving (Show, Eq)++data SortingColumn = SortingColumn+ { columnIndex :: Int32+ , columnOrderDescending :: Bool+ , nullFirst :: Bool+ }+ deriving (Show, Eq)++emptySortingColumn :: SortingColumn+emptySortingColumn = SortingColumn 0 False False++data ColumnOrder+ = TYPE_ORDER+ | COLUMN_ORDER_UNKNOWN+ deriving (Show, Eq)++data EncryptionAlgorithm+ = ENCRYPTION_ALGORITHM_UNKNOWN+ | AesGcmV1+ { aadPrefix :: [Word8]+ , aadFileUnique :: [Word8]+ , supplyAadPrefix :: Bool+ }+ | AesGcmCtrV1+ { aadPrefix :: [Word8]+ , aadFileUnique :: [Word8]+ , supplyAadPrefix :: Bool+ }+ deriving (Show, Eq)++data DictVals+ = DBool [Bool]+ | DInt32 [Int32]+ | DInt64 [Int64]+ | DInt96 [UTCTime]+ | DFloat [Float]+ | DDouble [Double]+ | DText [T.Text]+ deriving (Show, Eq)++data Page = Page+ { pageHeader :: PageHeader+ , pageBytes :: [Word8]+ }+ deriving (Show, Eq)++data PageHeader = PageHeader+ { pageHeaderPageType :: PageType+ , uncompressedPageSize :: Int32+ , compressedPageSize :: Int32+ , pageHeaderCrcChecksum :: Int32+ , pageTypeHeader :: PageTypeHeader+ }+ deriving (Show, Eq)++emptyPageHeader = PageHeader PAGE_TYPE_UNKNOWN 0 0 0 PAGE_TYPE_HEADER_UNKNOWN++data PageTypeHeader+ = DataPageHeader+ { dataPageHeaderNumValues :: Int32+ , dataPageHeaderEncoding :: ParquetEncoding+ , definitionLevelEncoding :: ParquetEncoding+ , repetitionLevelEncoding :: ParquetEncoding+ , dataPageHeaderStatistics :: ColumnStatistics+ }+ | DataPageHeaderV2+ { dataPageHeaderV2NumValues :: Int32+ , dataPageHeaderV2NumNulls :: Int32+ , dataPageHeaderV2NumRows :: Int32+ , dataPageHeaderV2Encoding :: ParquetEncoding+ , definitionLevelByteLength :: Int32+ , repetitionLevelByteLength :: Int32+ , dataPageHeaderV2IsCompressed :: Bool+ , dataPageHeaderV2Statistics :: ColumnStatistics+ }+ | DictionaryPageHeader+ { dictionaryPageHeaderNumValues :: Int32+ , dictionaryPageHeaderEncoding :: ParquetEncoding+ , dictionaryPageIsSorted :: Bool+ }+ | INDEX_PAGE_HEADER+ | PAGE_TYPE_HEADER_UNKNOWN+ deriving (Show, Eq)++emptyDictionaryPageHeader = DictionaryPageHeader 0 PARQUET_ENCODING_UNKNOWN False+emptyDataPageHeader = DataPageHeader 0 PARQUET_ENCODING_UNKNOWN PARQUET_ENCODING_UNKNOWN PARQUET_ENCODING_UNKNOWN emptyColumnStatistics+emptyDataPageHeaderV2 = DataPageHeaderV2 0 0 0 PARQUET_ENCODING_UNKNOWN 0 0 {- default for v2 is compressed -} True emptyColumnStatistics++data RepetitionType = REQUIRED | OPTIONAL | REPEATED | UNKNOWN_REPETITION_TYPE deriving (Eq, Show)++data LogicalType+ = STRING_TYPE+ | MAP_TYPE+ | LIST_TYPE+ | ENUM_TYPE+ | DECIMAL_TYPE+ | DATE_TYPE+ | DecimalType {decimalTypePrecision :: Int32, decimalTypeScale :: Int32}+ | TimeType {isAdjustedToUTC :: Bool, unit :: TimeUnit}+ | -- This should probably have a different, more constrained TimeUnit type.+ TimestampType {isAdjustedToUTC :: Bool, unit :: TimeUnit}+ | IntType {bitWidth :: Int8, intIsSigned :: Bool}+ | LOGICAL_TYPE_UNKNOWN+ | JSON_TYPE+ | BSON_TYPE+ | UUID_TYPE+ | FLOAT16_TYPE+ | VariantType {specificationVersion :: Int8}+ | GeometryType {crs :: T.Text}+ | GeographyType {crs :: T.Text, algorithm :: EdgeInterpolationAlgorithm}+ deriving (Eq, Show)++data TimeUnit+ = MILLISECONDS+ | MICROSECONDS+ | NANOSECONDS+ | TIME_UNIT_UNKNOWN+ deriving (Eq, Show)++data EdgeInterpolationAlgorithm+ = SPHERICAL+ | VINCENTY+ | THOMAS+ | ANDOYER+ | KARNEY+ deriving (Eq, Show)++repetitionTypeFromInt :: Int32 -> RepetitionType+repetitionTypeFromInt 0 = REQUIRED+repetitionTypeFromInt 1 = OPTIONAL+repetitionTypeFromInt 2 = REPEATED+repetitionTypeFromInt _ = UNKNOWN_REPETITION_TYPE++compressionCodecFromInt :: Int32 -> CompressionCodec+compressionCodecFromInt 0 = UNCOMPRESSED+compressionCodecFromInt 1 = SNAPPY+compressionCodecFromInt 2 = GZIP+compressionCodecFromInt 3 = LZO+compressionCodecFromInt 4 = BROTLI+compressionCodecFromInt 5 = LZ4+compressionCodecFromInt 6 = ZSTD+compressionCodecFromInt 7 = LZ4_RAW+compressionCodecFromInt _ = COMPRESSION_CODEC_UNKNOWN
src/DataFrame/Internal/Column.hs view
@@ -1,72 +1,76 @@+{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE LambdaCase #-}+ module DataFrame.Internal.Column where import qualified Data.ByteString.Char8 as C import qualified Data.List as L import qualified Data.Set as S import qualified Data.Text as T+import qualified Data.Vector as VB import qualified Data.Vector.Algorithms.Merge as VA import qualified Data.Vector.Generic as VG-import qualified Data.Vector as VB import qualified Data.Vector.Mutable as VBM import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Unboxed.Mutable as VUM +import Control.Exception (throw) import Control.Monad.ST (runST)-import DataFrame.Internal.Types-import DataFrame.Internal.Parsing import Data.Int+import Data.Kind (Constraint, Type) import Data.Maybe import Data.Proxy import Data.Text.Encoding (decodeUtf8Lenient)-import Data.Type.Equality (type (:~:)(Refl), TestEquality (..))+import Data.Type.Equality (TestEquality (..), type (:~:) (Refl)) import Data.Typeable (Typeable, cast) import Data.Word+import DataFrame.Errors+import DataFrame.Internal.Parsing+import DataFrame.Internal.Types import Type.Reflection import Unsafe.Coerce (unsafeCoerce)-import DataFrame.Errors-import Control.Exception (throw)-import Data.Kind (Type, Constraint) --- | Our representation of a column is a GADT that can store data based on the underlying data.--- --- This allows us to pattern match on data kinds and limit some operations to only some--- kinds of vectors. E.g. operations for missing data only happen in an OptionalColumn.+{- | Our representation of a column is a GADT that can store data based on the underlying data.++This allows us to pattern match on data kinds and limit some operations to only some+kinds of vectors. E.g. operations for missing data only happen in an OptionalColumn.+-} data Column where- BoxedColumn :: Columnable a => VB.Vector a -> Column- UnboxedColumn :: (Columnable a, VU.Unbox a) => VU.Vector a -> Column- OptionalColumn :: Columnable a => VB.Vector (Maybe a) -> Column+ BoxedColumn :: (Columnable a) => VB.Vector a -> Column+ UnboxedColumn :: (Columnable a, VU.Unbox a) => VU.Vector a -> Column+ OptionalColumn :: (Columnable a) => VB.Vector (Maybe a) -> Column data MutableColumn where- MBoxedColumn :: Columnable a => VBM.IOVector a -> MutableColumn- MUnboxedColumn :: (Columnable a, VU.Unbox a) => VUM.IOVector a -> MutableColumn+ MBoxedColumn :: (Columnable a) => VBM.IOVector a -> MutableColumn+ MUnboxedColumn :: (Columnable a, VU.Unbox a) => VUM.IOVector a -> MutableColumn --- | A TypedColumn is a wrapper around our type-erased column.--- It is used to type check expressions on columns.+{- | A TypedColumn is a wrapper around our type-erased column.+It is used to type check expressions on columns.+-} data TypedColumn a where- TColumn :: Columnable a => Column -> TypedColumn a+ TColumn :: (Columnable a) => Column -> TypedColumn a -- | Gets the underlying value from a TypedColumn. unwrapTypedColumn :: TypedColumn a -> Column unwrapTypedColumn (TColumn value) = value --- | An internal function that checks if a column can--- be used in missing value operations.+{- | An internal function that checks if a column can+be used in missing value operations.+-} isOptional :: Column -> Bool isOptional (OptionalColumn column) = True isOptional _ = False@@ -74,62 +78,70 @@ -- | An internal/debugging function to get the column type of a column. columnVersionString :: Column -> String columnVersionString column = case column of- BoxedColumn _ -> "Boxed"- UnboxedColumn _ -> "Unboxed"- OptionalColumn _ -> "Optional"+ BoxedColumn _ -> "Boxed"+ UnboxedColumn _ -> "Unboxed"+ OptionalColumn _ -> "Optional" --- | An internal/debugging function to get the type stored in the outermost vector--- of a column.+{- | An internal/debugging function to get the type stored in the outermost vector+of a column.+-} columnTypeString :: Column -> String columnTypeString column = case column of- BoxedColumn (column :: VB.Vector a) -> show (typeRep @a)- UnboxedColumn (column :: VU.Vector a) -> show (typeRep @a)- OptionalColumn (column :: VB.Vector a) -> show (typeRep @a)+ BoxedColumn (column :: VB.Vector a) -> show (typeRep @a)+ UnboxedColumn (column :: VU.Vector a) -> show (typeRep @a)+ OptionalColumn (column :: VB.Vector a) -> show (typeRep @a) instance (Show a) => Show (TypedColumn a) where- show (TColumn col) = show col+ show (TColumn col) = show col instance Show Column where- show :: Column -> String- show (BoxedColumn column) = show column- show (UnboxedColumn column) = show column- show (OptionalColumn column) = show column+ show :: Column -> String+ show (BoxedColumn column) = show column+ show (UnboxedColumn column) = show column+ show (OptionalColumn column) = show column instance Eq 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- (==) _ _ = False+ (==) :: 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+ (==) _ _ = False --- | A class for converting a vector to a column of the appropriate type.--- Given each Rep we tell the `toColumnRep` function which Column type to pick.+{- | A class for converting a vector to a column of the appropriate type.+Given each Rep we tell the `toColumnRep` function which Column type to pick.+-} class ColumnifyRep (r :: Rep) a where- toColumnRep :: VB.Vector a -> Column+ toColumnRep :: VB.Vector a -> Column -- | Constraint synonym for what we can put into columns.-type Columnable a = (Columnable' a, ColumnifyRep (KindOf a) a, UnboxIf a, SBoolI (Unboxable a), SBoolI (Numeric a) )+type Columnable a = (Columnable' a, ColumnifyRep (KindOf a) a, UnboxIf a, SBoolI (Unboxable a), SBoolI (Numeric a)) -instance (Columnable a, VU.Unbox a)- => ColumnifyRep 'RUnboxed a where- toColumnRep = UnboxedColumn . VU.convert+instance+ (Columnable a, VU.Unbox a) =>+ ColumnifyRep 'RUnboxed a+ where+ toColumnRep = UnboxedColumn . VU.convert -instance Columnable a- => ColumnifyRep 'RBoxed a where- toColumnRep = BoxedColumn+instance+ (Columnable a) =>+ ColumnifyRep 'RBoxed a+ where+ toColumnRep = BoxedColumn -instance Columnable a- => ColumnifyRep 'ROptional (Maybe a) where- toColumnRep = OptionalColumn+instance+ (Columnable a) =>+ ColumnifyRep 'ROptional (Maybe a)+ where+ toColumnRep = OptionalColumn {- | O(n) Convert a vector to a column. Automatically picks the best representation of a vector to store the underlying data in. @@ -142,8 +154,9 @@ @ -} fromVector ::- forall a. (Columnable a, ColumnifyRep (KindOf a) a)- => VB.Vector a -> Column+ forall a.+ (Columnable a, ColumnifyRep (KindOf a) a) =>+ VB.Vector a -> Column fromVector = toColumnRep @(KindOf a) {- | O(n) Convert an unboxed vector to a column. This avoids the extra conversion if you already have the data in an unboxed vector.@@ -169,35 +182,36 @@ @ -} fromList ::- forall a. (Columnable a, ColumnifyRep (KindOf a) a)- => [a] -> Column+ forall a.+ (Columnable a, ColumnifyRep (KindOf a) a) =>+ [a] -> Column fromList = toColumnRep @(KindOf a) . VB.fromList -- | 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- -> Maybe Column+mapColumn ::+ forall b c.+ ( Columnable b+ , Columnable c+ , UnboxIf c+ ) =>+ (b -> c) ->+ Column ->+ Maybe Column mapColumn f = \case- BoxedColumn (col :: VB.Vector a)- | Just Refl <- testEquality (typeRep @a) (typeRep @b)- -> Just (fromVector @c (VB.map f col))- | otherwise -> Nothing- OptionalColumn (col :: VB.Vector a)- | Just Refl <- testEquality (typeRep @a) (typeRep @b)- -> Just (fromVector @c (VB.map f col))- | otherwise -> Nothing- UnboxedColumn (col :: VU.Vector a)- | Just Refl <- testEquality (typeRep @a) (typeRep @b)- -> Just $ case sUnbox @c of- STrue -> UnboxedColumn (VU.map f col)+ BoxedColumn (col :: VB.Vector a)+ | Just Refl <- testEquality (typeRep @a) (typeRep @b) ->+ Just (fromVector @c (VB.map f col))+ | otherwise -> Nothing+ OptionalColumn (col :: VB.Vector a)+ | Just Refl <- testEquality (typeRep @a) (typeRep @b) ->+ Just (fromVector @c (VB.map f col))+ | otherwise -> Nothing+ UnboxedColumn (col :: VU.Vector a)+ | Just Refl <- testEquality (typeRep @a) (typeRep @b) ->+ Just $ case sUnbox @c of+ STrue -> UnboxedColumn (VU.map f col) SFalse -> fromVector @c (VB.map f (VB.convert col))- | otherwise -> Nothing-+ | otherwise -> Nothing -- | O(1) Gets the number of elements in the column. columnLength :: Column -> Int@@ -242,7 +256,7 @@ -- | O(n) Selects the elements at a given set of indices. Does not change the order. atIndicesStable :: VU.Vector Int -> Column -> Column atIndicesStable indexes (BoxedColumn column) = BoxedColumn $ indexes `getIndices` column-atIndicesStable indexes (UnboxedColumn column) = UnboxedColumn $ indexes `getIndicesUnboxed` column+atIndicesStable indexes (UnboxedColumn column) = UnboxedColumn $ VU.unsafeBackpermute column indexes atIndicesStable indexes (OptionalColumn column) = OptionalColumn $ indexes `getIndices` column {-# INLINE atIndicesStable #-} @@ -256,119 +270,123 @@ getIndicesUnboxed indices xs = VU.generate (VU.length indices) (\i -> xs VU.! (indices VU.! i)) {-# INLINE getIndicesUnboxed #-} -findIndices :: forall a. (Columnable a)- => (a -> Bool)- -> Column- -> Maybe (VU.Vector Int)+findIndices ::+ forall a.+ (Columnable a) =>+ (a -> Bool) ->+ Column ->+ Maybe (VU.Vector Int) findIndices pred (BoxedColumn (column :: VB.Vector b)) = do- Refl <- testEquality (typeRep @a) (typeRep @b)- pure $ VG.convert (VG.findIndices pred column)+ Refl <- testEquality (typeRep @a) (typeRep @b)+ pure $ VG.convert (VG.findIndices pred column) findIndices pred (UnboxedColumn (column :: VU.Vector b)) = do- Refl <- testEquality (typeRep @a) (typeRep @b)- pure $ VG.findIndices pred column+ Refl <- testEquality (typeRep @a) (typeRep @b)+ pure $ VG.findIndices pred column findIndices pred (OptionalColumn (column :: VB.Vector (Maybe b))) = do- Refl <- testEquality (typeRep @a) (typeRep @(Maybe b))- pure $ VG.convert (VG.findIndices pred column)+ Refl <- testEquality (typeRep @a) (typeRep @(Maybe b))+ pure $ VG.convert (VG.findIndices pred column) -- | 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 (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))+ 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 -> Maybe Column+imapColumn ::+ forall b c.+ (Columnable b, Columnable c) =>+ (Int -> b -> c) -> Column -> Maybe Column imapColumn f = \case- BoxedColumn (col :: VB.Vector a)- | Just Refl <- testEquality (typeRep @a) (typeRep @b)- -> Just (fromVector @c (VB.imap f col))- | otherwise -> Nothing- UnboxedColumn (col :: VU.Vector a)- | Just Refl <- testEquality (typeRep @a) (typeRep @b)- -> Just $- case sUnbox @c of- STrue -> UnboxedColumn (VU.imap f col)- SFalse -> fromVector @c (VB.imap f (VB.convert col))- | otherwise -> Nothing- OptionalColumn (col :: VB.Vector a)- | Just Refl <- testEquality (typeRep @a) (typeRep @b)- -> Just (fromVector @c (VB.imap f col))- | otherwise -> Nothing+ BoxedColumn (col :: VB.Vector a)+ | Just Refl <- testEquality (typeRep @a) (typeRep @b) ->+ Just (fromVector @c (VB.imap f col))+ | otherwise -> Nothing+ UnboxedColumn (col :: VU.Vector a)+ | Just Refl <- testEquality (typeRep @a) (typeRep @b) ->+ Just $+ case sUnbox @c of+ STrue -> UnboxedColumn (VU.imap f col)+ SFalse -> fromVector @c (VB.imap f (VB.convert col))+ | otherwise -> Nothing+ OptionalColumn (col :: VB.Vector a)+ | Just Refl <- testEquality (typeRep @a) (typeRep @b) ->+ Just (fromVector @c (VB.imap f col))+ | otherwise -> Nothing -- | Filter column with index.-ifilterColumn :: forall a . (Columnable a) => (Int -> a -> Bool) -> Column -> Maybe Column+ifilterColumn :: forall a. (Columnable a) => (Int -> a -> Bool) -> Column -> Maybe Column ifilterColumn f c@(BoxedColumn (column :: VB.Vector b)) = do- Refl <- testEquality (typeRep @a) (typeRep @b)- return $ BoxedColumn $ VG.ifilter f column+ Refl <- testEquality (typeRep @a) (typeRep @b)+ return $ BoxedColumn $ VG.ifilter f column ifilterColumn f c@(UnboxedColumn (column :: VU.Vector b)) = do- Refl <- testEquality (typeRep @a) (typeRep @b)- return $ UnboxedColumn $ VG.ifilter f column+ Refl <- testEquality (typeRep @a) (typeRep @b)+ return $ UnboxedColumn $ VG.ifilter f column ifilterColumn _ _ = Nothing -- | Fold (right) column with index. ifoldrColumn :: forall a b. (Columnable a, Columnable b) => (Int -> a -> b -> b) -> b -> Column -> Maybe b ifoldrColumn f acc c@(BoxedColumn (column :: VB.Vector d)) = do- Refl <- testEquality (typeRep @a) (typeRep @d)- return $ VG.ifoldr f acc column+ Refl <- testEquality (typeRep @a) (typeRep @d)+ return $ VG.ifoldr f acc column ifoldrColumn f acc c@(OptionalColumn (column :: VB.Vector d)) = do- Refl <- testEquality (typeRep @a) (typeRep @d)- return $ VG.ifoldr f acc column+ Refl <- testEquality (typeRep @a) (typeRep @d)+ return $ VG.ifoldr f acc column ifoldrColumn f acc c@(UnboxedColumn (column :: VU.Vector d)) = do- Refl <- testEquality (typeRep @a) (typeRep @d)- return $ VG.ifoldr f acc column+ Refl <- testEquality (typeRep @a) (typeRep @d)+ return $ VG.ifoldr f acc column -- | Fold (left) column with index.-ifoldlColumn :: forall a b . (Columnable a, Columnable b) => (b -> Int -> a -> b) -> b -> Column -> Maybe b+ifoldlColumn :: forall a b. (Columnable a, Columnable b) => (b -> Int -> a -> b) -> b -> Column -> Maybe b ifoldlColumn f acc c@(BoxedColumn (column :: VB.Vector d)) = do- Refl <- testEquality (typeRep @a) (typeRep @d)- return $ VG.ifoldl' f acc column+ Refl <- testEquality (typeRep @a) (typeRep @d)+ return $ VG.ifoldl' f acc column ifoldlColumn f acc c@(OptionalColumn (column :: VB.Vector d)) = do- Refl <- testEquality (typeRep @a) (typeRep @d)- return $ VG.ifoldl' f acc column+ Refl <- testEquality (typeRep @a) (typeRep @d)+ return $ VG.ifoldl' f acc column ifoldlColumn f acc c@(UnboxedColumn (column :: VU.Vector d)) = do- Refl <- testEquality (typeRep @a) (typeRep @d)- return $ VG.ifoldl' f acc column+ Refl <- testEquality (typeRep @a) (typeRep @d)+ return $ VG.ifoldl' f acc column -headColumn :: forall a . Columnable a => Column -> Maybe a+headColumn :: forall a. (Columnable a) => Column -> Maybe a headColumn (BoxedColumn (col :: VB.Vector b)) = do- Refl <- testEquality (typeRep @a) (typeRep @b)- pure (VG.head col)+ Refl <- testEquality (typeRep @a) (typeRep @b)+ pure (VG.head col) headColumn (UnboxedColumn (col :: VU.Vector b)) = do- Refl <- testEquality (typeRep @a) (typeRep @b)- pure (VG.head col)+ Refl <- testEquality (typeRep @a) (typeRep @b)+ pure (VG.head col) headColumn (OptionalColumn (col :: VB.Vector b)) = do- Refl <- testEquality (typeRep @a) (typeRep @b)- pure (VG.head col)+ Refl <- testEquality (typeRep @a) (typeRep @b)+ pure (VG.head col) -- | Generic reduce function for all Column types.-reduceColumn :: forall a b. Columnable a => (a -> b) -> Column -> Maybe b+reduceColumn :: forall a b. (Columnable a) => (a -> b) -> Column -> Maybe b {-# SPECIALIZE reduceColumn ::- (VU.Vector (Double, Double) -> Double) -> Column -> Maybe Double,- (VU.Vector Double -> Double) -> Column -> Maybe Double #-}+ (VU.Vector (Double, Double) -> Double) -> Column -> Maybe Double+ , (VU.Vector Double -> Double) -> Column -> Maybe Double+ #-} reduceColumn f (BoxedColumn (column :: c)) = do- Refl <- testEquality (typeRep @c) (typeRep @a)- pure $ f column+ Refl <- testEquality (typeRep @c) (typeRep @a)+ pure $ f column reduceColumn f (UnboxedColumn (column :: c)) = do- Refl <- testEquality (typeRep @c) (typeRep @a)- pure $ f column+ Refl <- testEquality (typeRep @c) (typeRep @a)+ pure $ f column reduceColumn f (OptionalColumn (column :: c)) = do- Refl <- testEquality (typeRep @c) (typeRep @a)- pure $ f column+ Refl <- testEquality (typeRep @c) (typeRep @a)+ pure $ f column {-# INLINE reduceColumn #-} -- | An internal, column version of zip.@@ -376,95 +394,98 @@ zipColumns (BoxedColumn column) (BoxedColumn other) = BoxedColumn (VG.zip column other) zipColumns (BoxedColumn column) (UnboxedColumn other) = BoxedColumn (VB.generate (min (VG.length column) (VG.length other)) (\i -> (column VG.! i, other VG.! i))) zipColumns (BoxedColumn column) (OptionalColumn optcolumn) = BoxedColumn (VG.zip (VB.convert column) optcolumn)- zipColumns (UnboxedColumn column) (BoxedColumn other) = BoxedColumn (VB.generate (min (VG.length column) (VG.length other)) (\i -> (column VG.! i, other VG.! i))) zipColumns (UnboxedColumn column) (UnboxedColumn other) = UnboxedColumn (VG.zip column other) zipColumns (UnboxedColumn column) (OptionalColumn optcolumn) = BoxedColumn (VG.zip (VB.convert column) optcolumn)- zipColumns (OptionalColumn optcolumn) (BoxedColumn column) = BoxedColumn (VG.zip optcolumn (VB.convert column)) zipColumns (OptionalColumn optcolumn) (UnboxedColumn column) = BoxedColumn (VG.zip optcolumn (VB.convert column)) zipColumns (OptionalColumn optcolumn) (OptionalColumn optother) = BoxedColumn (VG.zip optcolumn optother) {-# INLINE zipColumns #-} -- | An internal, column version of zipWith.-zipWithColumns :: forall a b c . (Columnable a, Columnable b, Columnable c) => (a -> b -> c) -> Column -> Column -> Maybe Column+zipWithColumns :: forall a b c. (Columnable a, Columnable b, Columnable c) => (a -> b -> c) -> Column -> Column -> Maybe Column zipWithColumns f (UnboxedColumn (column :: VU.Vector d)) (UnboxedColumn (other :: VU.Vector e)) = case testEquality (typeRep @a) (typeRep @d) of- Just Refl -> case testEquality (typeRep @b) (typeRep @e) of- Just Refl -> pure $ case sUnbox @c of- STrue -> fromUnboxedVector (VU.zipWith f column other)- SFalse -> fromVector $ VB.zipWith f (VG.convert column) (VG.convert other)+ Just Refl -> case testEquality (typeRep @b) (typeRep @e) of+ Just Refl -> pure $ case sUnbox @c of+ STrue -> fromUnboxedVector (VU.zipWith f column other)+ SFalse -> fromVector $ VB.zipWith f (VG.convert column) (VG.convert other)+ Nothing -> Nothing Nothing -> Nothing- Nothing -> Nothing-zipWithColumns f left right = let- left' = toVector @a left- right' = toVector @b right- in pure $ fromVector $ VB.zipWith f left' right' +zipWithColumns f left right =+ let+ left' = toVector @a left+ right' = toVector @b right+ in+ pure $ fromVector $ VB.zipWith f left' right' {-# INLINE zipWithColumns #-} -- Functions for mutable columns (intended for IO). writeColumn :: Int -> T.Text -> MutableColumn -> IO (Either T.Text Bool)-writeColumn i value (MBoxedColumn (col :: VBM.IOVector a)) = let- in case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl -> (if isNullish value+writeColumn i value (MBoxedColumn (col :: VBM.IOVector a)) =+ let+ in case testEquality (typeRep @a) (typeRep @T.Text) of+ Just Refl ->+ ( if isNullish value then VBM.unsafeWrite col i "" >> return (Left $! value)- else VBM.unsafeWrite col i value >> return (Right True))- Nothing -> return (Left value)+ else VBM.unsafeWrite col i value >> return (Right True)+ )+ Nothing -> return (Left value) writeColumn i value (MUnboxedColumn (col :: VUM.IOVector a)) =- case testEquality (typeRep @a) (typeRep @Int) of- Just Refl -> case readInt value of- Just v -> VUM.unsafeWrite col i v >> return (Right True)- Nothing -> VUM.unsafeWrite col i 0 >> return (Left value)- Nothing -> case testEquality (typeRep @a) (typeRep @Double) of- Nothing -> return (Left $! value)- Just Refl -> case readDouble value of+ case testEquality (typeRep @a) (typeRep @Int) of+ Just Refl -> case readInt value of Just v -> VUM.unsafeWrite col i v >> return (Right True)- Nothing -> VUM.unsafeWrite col i 0 >> return (Left $! value)+ Nothing -> VUM.unsafeWrite col i 0 >> return (Left value)+ Nothing -> case testEquality (typeRep @a) (typeRep @Double) of+ Nothing -> return (Left $! value)+ Just Refl -> case readDouble value of+ Just v -> VUM.unsafeWrite col i v >> return (Right True)+ Nothing -> VUM.unsafeWrite col i 0 >> return (Left $! value) {-# INLINE writeColumn #-} freezeColumn' :: [(Int, T.Text)] -> MutableColumn -> IO Column freezeColumn' nulls (MBoxedColumn col)- | null nulls = BoxedColumn <$> VB.unsafeFreeze col- | all (isNullish . snd) nulls = OptionalColumn . VB.imap (\i v -> if i `elem` map fst nulls then Nothing else Just v) <$> VB.unsafeFreeze col- | otherwise = BoxedColumn . VB.imap (\i v -> if i `elem` map fst nulls then Left (fromMaybe (error "") (lookup i nulls)) else Right v) <$> VB.unsafeFreeze col+ | null nulls = BoxedColumn <$> VB.unsafeFreeze col+ | all (isNullish . snd) nulls = OptionalColumn . VB.imap (\i v -> if i `elem` map fst nulls then Nothing else Just v) <$> VB.unsafeFreeze col+ | otherwise = BoxedColumn . VB.imap (\i v -> if i `elem` map fst nulls then Left (fromMaybe (error "") (lookup i nulls)) else Right v) <$> VB.unsafeFreeze col freezeColumn' nulls (MUnboxedColumn col)- | null nulls = UnboxedColumn <$> VU.unsafeFreeze col- | all (isNullish . snd) nulls = VU.unsafeFreeze col >>= \c -> return $ OptionalColumn $ VB.generate (VU.length c) (\i -> if i `elem` map fst nulls then Nothing else Just (c VU.! i))- | otherwise = VU.unsafeFreeze col >>= \c -> return $ BoxedColumn $ VB.generate (VU.length c) (\i -> if i `elem` map fst nulls then Left (fromMaybe (error "") (lookup i nulls)) else Right (c VU.! i))+ | null nulls = UnboxedColumn <$> VU.unsafeFreeze col+ | all (isNullish . snd) nulls = VU.unsafeFreeze col >>= \c -> return $ OptionalColumn $ VB.generate (VU.length c) (\i -> if i `elem` map fst nulls then Nothing else Just (c VU.! i))+ | otherwise = VU.unsafeFreeze col >>= \c -> return $ BoxedColumn $ VB.generate (VU.length c) (\i -> if i `elem` map fst nulls then Left (fromMaybe (error "") (lookup i nulls)) else Right (c VU.! i)) {-# INLINE freezeColumn' #-} -- | Fills the end of a column, up to n, with Nothing. Does nothing if column has length greater than n. expandColumn :: Int -> Column -> Column expandColumn n (OptionalColumn col) = OptionalColumn $ col <> VB.replicate (n - VG.length col) Nothing expandColumn n column@(BoxedColumn col)- | n > VG.length col = OptionalColumn $ VB.map Just col <> VB.replicate (n - VG.length col) Nothing- | otherwise = column+ | n > VG.length col = OptionalColumn $ VB.map Just col <> VB.replicate (n - VG.length col) Nothing+ | otherwise = column expandColumn n column@(UnboxedColumn col)- | n > VG.length col = OptionalColumn $ VB.map Just (VU.convert col) <> VB.replicate (n - VG.length col) Nothing- | otherwise = column+ | n > VG.length col = OptionalColumn $ VB.map Just (VU.convert col) <> VB.replicate (n - VG.length col) Nothing+ | otherwise = column -- | Fills the beginning of a column, up to n, with Nothing. Does nothing if column has length greater than n. leftExpandColumn :: Int -> Column -> Column leftExpandColumn n column@(OptionalColumn col)- | n > VG.length col = OptionalColumn $ VG.replicate (n - VG.length col) Nothing <> col- | otherwise = column+ | n > VG.length col = OptionalColumn $ VG.replicate (n - VG.length col) Nothing <> col+ | otherwise = column leftExpandColumn n column@(BoxedColumn col)- | n > VG.length col = OptionalColumn $ VG.replicate (n - VG.length col) Nothing <> VG.map Just col- | otherwise = column+ | n > VG.length col = OptionalColumn $ VG.replicate (n - VG.length col) Nothing <> VG.map Just col+ | otherwise = column leftExpandColumn n column@(UnboxedColumn col)- | n > VG.length col = OptionalColumn $ VG.replicate (n - VG.length col) Nothing <> VG.map Just (VU.convert col)- | otherwise = column+ | n > VG.length col = OptionalColumn $ VG.replicate (n - VG.length col) Nothing <> VG.map Just (VU.convert col)+ | otherwise = column -- | Concatenates two columns. concatColumns :: Column -> Column -> Maybe Column concatColumns (OptionalColumn left) (OptionalColumn right) = case testEquality (typeOf left) (typeOf right) of- Nothing -> Nothing- Just Refl -> Just (OptionalColumn $ left <> right)+ Nothing -> Nothing+ Just Refl -> Just (OptionalColumn $ left <> right) concatColumns (BoxedColumn left) (BoxedColumn right) = case testEquality (typeOf left) (typeOf right) of- Nothing -> Nothing- Just Refl -> Just (BoxedColumn $ left <> right)+ Nothing -> Nothing+ Just Refl -> Just (BoxedColumn $ left <> right) concatColumns (UnboxedColumn left) (UnboxedColumn right) = case testEquality (typeOf left) (typeOf right) of- Nothing -> Nothing- Just Refl -> Just (UnboxedColumn $ left <> right)+ Nothing -> Nothing+ Just Refl -> Just (UnboxedColumn $ left <> right) concatColumns _ _ = Nothing {- | O(n) Converts a column to a boxed vector. Throws an exception if the wrong type is specified.@@ -477,11 +498,12 @@ [1,2,3,4] > toVector @Double column exception: ...+@ -}-toVector :: forall a . Columnable a => Column -> VB.Vector a+toVector :: forall a. (Columnable a) => Column -> VB.Vector a toVector xs = case toVectorSafe xs of- Left err -> throw err- Right val -> val+ Left err -> throw err+ Right val -> val {- | O(n) Converts a column to a list. Throws an exception if the wrong type is specified. @@ -493,32 +515,51 @@ [1,2,3,4] > toList @Double column exception: ...+@ -}-toList :: forall a . Columnable a => Column -> [a]+toList :: forall a. (Columnable a) => Column -> [a] toList xs = case toVectorSafe @a xs of- Left err -> throw err- Right val -> VB.toList val+ Left err -> throw err+ Right val -> VB.toList val -- | A safe version of toVector that returns an Either type.-toVectorSafe :: forall a v . (VG.Vector v a, Columnable a) => Column -> Either DataFrameException (v a)+toVectorSafe :: forall a v. (VG.Vector v a, Columnable a) => Column -> Either DataFrameException (v a) toVectorSafe column@(OptionalColumn (col :: VB.Vector b)) =- case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> Right $ VG.convert col- Nothing -> Left $ TypeMismatchException (MkTypeErrorContext { userType = Right (typeRep @a)- , expectedType = Right (typeRep @b)- , callingFunctionName = Just "toVectorSafe"- , errorColumnName = Nothing})+ case testEquality (typeRep @a) (typeRep @b) of+ Just Refl -> Right $ VG.convert col+ Nothing ->+ Left $+ TypeMismatchException+ ( MkTypeErrorContext+ { userType = Right (typeRep @a)+ , expectedType = Right (typeRep @b)+ , callingFunctionName = Just "toVectorSafe"+ , errorColumnName = Nothing+ }+ ) toVectorSafe (BoxedColumn (col :: VB.Vector b)) =- case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> Right $ VG.convert col- Nothing -> Left $ TypeMismatchException (MkTypeErrorContext { userType = Right (typeRep @a)- , expectedType = Right (typeRep @b)- , callingFunctionName = Just "toVectorSafe"- , errorColumnName = Nothing})+ case testEquality (typeRep @a) (typeRep @b) of+ Just Refl -> Right $ VG.convert col+ Nothing ->+ Left $+ TypeMismatchException+ ( MkTypeErrorContext+ { userType = Right (typeRep @a)+ , expectedType = Right (typeRep @b)+ , callingFunctionName = Just "toVectorSafe"+ , errorColumnName = Nothing+ }+ ) toVectorSafe (UnboxedColumn (col :: VU.Vector b)) =- case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> Right $ VG.convert col- Nothing -> Left $ TypeMismatchException (MkTypeErrorContext { userType = Right (typeRep @a)- , expectedType = Right (typeRep @b)- , callingFunctionName = Just "toVectorSafe"- , errorColumnName = Nothing})+ case testEquality (typeRep @a) (typeRep @b) of+ Just Refl -> Right $ VG.convert col+ Nothing ->+ Left $+ TypeMismatchException+ ( MkTypeErrorContext+ { userType = Right (typeRep @a)+ , expectedType = Right (typeRep @b)+ , callingFunctionName = Just "toVectorSafe"+ , errorColumnName = Nothing+ }+ )
src/DataFrame/Internal/DataFrame.hs view
@@ -1,98 +1,110 @@ {-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE FlexibleContexts #-}+ module DataFrame.Internal.DataFrame 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 qualified Data.Vector.Generic as VG+import qualified Data.Vector.Unboxed as VU import Control.Monad (join)-import DataFrame.Display.Terminal.PrettyPrint-import DataFrame.Internal.Column import Data.Function (on) import Data.List (sortBy, transpose, (\\)) import Data.Maybe (isJust)-import Data.Type.Equality (type (:~:)(Refl), TestEquality (testEquality))+import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))+import DataFrame.Display.Terminal.PrettyPrint+import DataFrame.Internal.Column import Text.Printf import Type.Reflection (typeRep) data DataFrame = DataFrame- { -- | Our main data structure stores a dataframe as- -- a vector of columns. This improv- columns :: V.Vector Column,- -- | Keeps the column names in the order they were inserted in.- columnIndices :: M.Map T.Text Int,- dataframeDimensions :: (Int, Int)- }+ { columns :: V.Vector Column+ {- ^ Our main data structure stores a dataframe as+ a vector of columns. This improv+ -}+ , columnIndices :: M.Map T.Text Int+ -- ^ Keeps the column names in the order they were inserted in.+ , dataframeDimensions :: (Int, Int)+ } -data GroupedDataFrame = Grouped {- fullDataframe :: DataFrame,- groupedColumns :: [T.Text],- valueIndices :: VU.Vector Int,- offsets :: VU.Vector Int-}+{- | A record that contains information about how and what+rows are grouped in the dataframe. This can only be used with+`aggregate`.+-}+data GroupedDataFrame = Grouped+ { fullDataframe :: DataFrame+ , groupedColumns :: [T.Text]+ , valueIndices :: VU.Vector Int+ , offsets :: VU.Vector Int+ } instance Show GroupedDataFrame where- show (Grouped df cols indices os) = printf "{ keyColumns: %s groupedColumns: %s }" - (show cols)- (show ((M.keys (columnIndices df)) \\ cols))+ show (Grouped df cols indices os) =+ printf+ "{ keyColumns: %s groupedColumns: %s }"+ (show cols)+ (show ((M.keys (columnIndices df)) \\ cols)) instance Eq GroupedDataFrame where- (==) (Grouped df cols indices os) (Grouped df' cols' indices' os') = (df == df') && (cols == cols') -+ (==) (Grouped df cols indices os) (Grouped df' cols' indices' os') = (df == df') && (cols == cols') instance Eq DataFrame where- (==) :: DataFrame -> DataFrame -> Bool- a == b = map fst (M.toList $ columnIndices a) == map fst (M.toList $ columnIndices b) &&- foldr (\(name, index) acc -> acc && (columns a V.!? index == (columns b V.!? (columnIndices b M.! name)))) True (M.toList $ columnIndices a)+ (==) :: DataFrame -> DataFrame -> Bool+ a == b =+ map fst (M.toList $ columnIndices a) == map fst (M.toList $ columnIndices b)+ && foldr (\(name, index) acc -> acc && (columns a V.!? index == (columns b V.!? (columnIndices b M.! name)))) True (M.toList $ columnIndices a) instance Show DataFrame where- show :: DataFrame -> String- show d = T.unpack (asText d False)+ show :: DataFrame -> String+ show d = T.unpack (asText d False) asText :: DataFrame -> Bool -> T.Text asText d properMarkdown =- let header = "index" : map fst (sortBy (compare `on` snd) $ M.toList (columnIndices d))- types = V.toList $ V.filter (/= "") $ V.map getType (columns d)- getType :: Column -> T.Text- getType (BoxedColumn (column :: V.Vector a)) = T.pack $ show (typeRep @a)- getType (UnboxedColumn (column :: VU.Vector a)) = T.pack $ show (typeRep @a)- getType (OptionalColumn (column :: V.Vector a)) = T.pack $ show (typeRep @a)- -- Separate out cases dynamically so we don't end up making round trip string- -- copies.- get :: Maybe Column -> V.Vector T.Text- get (Just (BoxedColumn (column :: V.Vector a))) = case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl -> column- Nothing -> case testEquality (typeRep @a) (typeRep @String) of+ let header = "index" : map fst (sortBy (compare `on` snd) $ M.toList (columnIndices d))+ types = V.toList $ V.filter (/= "") $ V.map getType (columns d)+ getType :: Column -> T.Text+ getType (BoxedColumn (column :: V.Vector a)) = T.pack $ show (typeRep @a)+ getType (UnboxedColumn (column :: VU.Vector a)) = T.pack $ show (typeRep @a)+ getType (OptionalColumn (column :: V.Vector a)) = T.pack $ show (typeRep @a)+ -- Separate out cases dynamically so we don't end up making round trip string+ -- copies.+ get :: Maybe Column -> V.Vector T.Text+ get (Just (BoxedColumn (column :: V.Vector a))) = case testEquality (typeRep @a) (typeRep @T.Text) of+ Just Refl -> column+ Nothing -> case testEquality (typeRep @a) (typeRep @String) of Just Refl -> V.map T.pack column Nothing -> V.map (T.pack . show) column- get (Just (UnboxedColumn column)) = V.map (T.pack . show) (V.convert column)- get (Just (OptionalColumn column)) = V.map (T.pack . show) column- getTextColumnFromFrame df (i, name) = if i == 0- then V.fromList (map (T.pack . show) [0..(fst (dataframeDimensions df) - 1)])- else get $ (V.!?) (columns d) ((M.!) (columnIndices d) name)- rows =- transpose $- zipWith (curry (V.toList . getTextColumnFromFrame d)) [0..] header- in showTable properMarkdown header ("Int":types) rows+ get (Just (UnboxedColumn column)) = V.map (T.pack . show) (V.convert column)+ get (Just (OptionalColumn column)) = V.map (T.pack . show) column+ getTextColumnFromFrame df (i, name) =+ if i == 0+ then V.fromList (map (T.pack . show) [0 .. (fst (dataframeDimensions df) - 1)])+ else get $ (V.!?) (columns d) ((M.!) (columnIndices d) name)+ rows =+ transpose $+ zipWith (curry (V.toList . getTextColumnFromFrame d)) [0 ..] header+ in showTable properMarkdown header ("Int" : types) rows -- | O(1) Creates an empty dataframe empty :: DataFrame-empty = DataFrame {columns = V.empty,- columnIndices = M.empty,- dataframeDimensions = (0, 0) }+empty =+ DataFrame+ { columns = V.empty+ , columnIndices = M.empty+ , dataframeDimensions = (0, 0)+ } getColumn :: T.Text -> DataFrame -> Maybe Column getColumn name df = do- i <- columnIndices df M.!? name- columns df V.!? i+ i <- columnIndices df M.!? name+ columns df V.!? i unsafeGetColumn :: T.Text -> DataFrame -> Column unsafeGetColumn name df = columns df V.! (columnIndices df M.! name)@@ -100,19 +112,30 @@ null :: DataFrame -> Bool null df = V.null (columns df) +{- | Returns a dataframe as a two dimentions vector of floats.++All entries in the dataframe must be doubles.+This is useful for handing data over into ML systems.+-} toMatrix :: DataFrame -> V.Vector (VU.Vector Float)-toMatrix df = let- m = V.map (toVector @Double) (columns df)- in V.generate (fst (dataframeDimensions df)) (\i -> foldl (\acc j -> acc `VU.snoc` (realToFrac ((m V.! j) V.! i))) VU.empty [0..(V.length m - 1)])+toMatrix df =+ let+ m = V.map (toVector @Double) (columns df)+ in+ V.generate (fst (dataframeDimensions df)) (\i -> foldl (\acc j -> acc `VU.snoc` (realToFrac ((m V.! j) V.! i))) VU.empty [0 .. (V.length m - 1)]) -columnAsVector :: forall a . Columnable a => T.Text -> DataFrame -> V.Vector a+{- | Get a specific column as a vector.++You must specify the type via type applications.+-}+columnAsVector :: forall a. (Columnable a) => T.Text -> DataFrame -> V.Vector a columnAsVector name df = case unsafeGetColumn name df of- (BoxedColumn (col :: V.Vector b)) -> case testEquality (typeRep @a) (typeRep @b) of- Nothing -> error "Type error"- Just Refl -> col- (OptionalColumn (col :: V.Vector b)) -> case testEquality (typeRep @a) (typeRep @b) of- Nothing -> error "Type error"- Just Refl -> col- (UnboxedColumn (col :: VU.Vector b)) -> case testEquality (typeRep @a) (typeRep @b) of- Nothing -> error "Type error"- Just Refl -> VG.convert col+ (BoxedColumn (col :: V.Vector b)) -> case testEquality (typeRep @a) (typeRep @b) of+ Nothing -> error "Type error"+ Just Refl -> col+ (OptionalColumn (col :: V.Vector b)) -> case testEquality (typeRep @a) (typeRep @b) of+ Nothing -> error "Type error"+ Just Refl -> col+ (UnboxedColumn (col :: VU.Vector b)) -> case testEquality (typeRep @a) (typeRep @b) of+ Nothing -> error "Type error"+ Just Refl -> VG.convert col
src/DataFrame/Internal/Expression.hs view
@@ -1,221 +1,302 @@+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE UndecidableInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE BangPatterns #-}+ module DataFrame.Internal.Expression where -import qualified Data.Map as M-import Data.Type.Equality (type (:~:)(Refl), TestEquality (testEquality))+import Control.Exception (throw) import Data.Data (Typeable)-import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame-import DataFrame.Internal.Types+import qualified Data.Map as M+import Data.Maybe (fromMaybe) import qualified Data.Text as T+import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl)) import qualified Data.Vector as V-import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Generic as VG+import qualified Data.Vector.Unboxed as VU+import DataFrame.Errors (DataFrameException (ColumnNotFoundException))+import DataFrame.Internal.Column+import DataFrame.Internal.DataFrame+import DataFrame.Internal.Types import Type.Reflection (typeRep)-import DataFrame.Errors (DataFrameException(ColumnNotFoundException))-import Control.Exception (throw)-import Data.Maybe (fromMaybe) data Expr a where- Col :: Columnable a => T.Text -> Expr a - Lit :: Columnable a => a -> Expr a- Apply :: (Columnable a,- Columnable b)- => T.Text -- Operation name- -> (b -> a)- -> Expr b- -> Expr a- BinOp :: (Columnable c, - Columnable b, - Columnable a)- => T.Text -- operation name- -> (c -> b -> a)- -> Expr c- -> Expr b - -> Expr a- GeneralAggregate :: (Columnable a)- => T.Text -- Column name- -> T.Text -- Operation name- -> (forall v b. (VG.Vector v b, Columnable b) => v b -> a)- -> Expr a- ReductionAggregate :: (Columnable a)- => T.Text -- Column name- -> T.Text -- Operation name- -> (forall a . Columnable a => a -> a -> a)- -> Expr a- NumericAggregate :: (Columnable a,- Columnable b,- VU.Unbox a,- VU.Unbox b,- Num a,- Num b)- => T.Text -- Column name- -> T.Text -- Operation name- -> (VU.Vector b -> a) - -> Expr a+ Col :: (Columnable a) => T.Text -> Expr a+ Lit :: (Columnable a) => a -> Expr a+ Apply ::+ ( Columnable a+ , Columnable b+ ) =>+ T.Text -> -- Operation name+ (b -> a) ->+ Expr b ->+ Expr a+ BinOp ::+ ( Columnable c+ , Columnable b+ , Columnable a+ ) =>+ T.Text -> -- operation name+ (c -> b -> a) ->+ Expr c ->+ Expr b ->+ Expr a+ GeneralAggregate ::+ (Columnable a) =>+ T.Text -> -- Column name+ T.Text -> -- Operation name+ (forall v b. (VG.Vector v b, Columnable b) => v b -> a) ->+ Expr a+ ReductionAggregate ::+ (Columnable a) =>+ T.Text -> -- Column name+ T.Text -> -- Operation name+ (forall a. (Columnable a) => a -> a -> a) ->+ Expr a+ NumericAggregate ::+ ( Columnable a+ , Columnable b+ , VU.Unbox a+ , VU.Unbox b+ , Num a+ , Num b+ ) =>+ T.Text -> -- Column name+ T.Text -> -- Operation name+ (VU.Vector b -> a) ->+ Expr a data UExpr where- Wrap :: Columnable a => Expr a -> UExpr+ Wrap :: (Columnable a) => Expr a -> UExpr -interpret :: forall a . (Columnable a) => DataFrame -> Expr a -> TypedColumn a+interpret :: forall a. (Columnable a) => DataFrame -> Expr a -> TypedColumn a interpret df (Lit value) = TColumn $ fromVector $ V.replicate (fst $ dataframeDimensions df) value interpret df (Col name) = case getColumn name df of Nothing -> throw $ ColumnNotFoundException name "" (map fst $ M.toList $ columnIndices df) Just col -> TColumn col-interpret df (Apply _ (f :: c -> d) value) = let+interpret df (Apply _ (f :: c -> d) value) =+ let (TColumn value') = interpret @c df value- -- TODO: Handle this gracefully.- in TColumn $ fromMaybe (error "mapColumn returned nothing") (mapColumn f value')-interpret df (BinOp _ (f :: c -> d -> e) left right) = let+ in+ -- TODO: Handle this gracefully.+ TColumn $ fromMaybe (error "mapColumn returned nothing") (mapColumn f value')+interpret df (BinOp _ (f :: c -> d -> e) (Lit left) right) =+ let+ (TColumn right') = interpret @d df right+ in+ TColumn $ fromMaybe (error "mapColumn returned nothing") (mapColumn (f left) right')+interpret df (BinOp _ (f :: c -> d -> e) left (Lit right)) =+ let (TColumn left') = interpret @c df left+ in+ TColumn $ fromMaybe (error "mapColumn returned nothing") (mapColumn (flip f right) left')+interpret df (BinOp _ (f :: c -> d -> e) left right) =+ let+ (TColumn left') = interpret @c df left (TColumn right') = interpret @d df right- in TColumn $ fromMaybe (error "mapColumn returned nothing") (zipWithColumns f left' right')-interpret df (ReductionAggregate name op (f :: forall a . Columnable a => a -> a -> a)) = let+ in+ TColumn $ fromMaybe (error "mapColumn returned nothing") (zipWithColumns f left' right')+interpret df (ReductionAggregate name op (f :: forall a. (Columnable a) => a -> a -> a)) =+ let (TColumn column) = interpret @a df (Col name)- in case headColumn @a column of- Nothing -> error "Invalid operation"- Just h -> case ifoldlColumn (\acc _ v -> f acc v) h column of- Nothing -> error "Invalid operation"- Just value -> TColumn $ fromVector $ V.replicate (fst $ dataframeDimensions df) value+ in+ case headColumn @a column of+ Nothing -> error "Invalid operation"+ Just h -> case ifoldlColumn (\acc _ v -> f acc v) h column of+ Nothing -> error "Invalid operation"+ Just value -> TColumn $ fromVector $ V.replicate (fst $ dataframeDimensions df) value interpret _ expr = error ("Invalid operation for dataframe: " ++ show expr) -interpretAggregation :: forall a . (Columnable a) => GroupedDataFrame -> Expr a -> TypedColumn a+interpretAggregation :: forall a. (Columnable a) => GroupedDataFrame -> Expr a -> TypedColumn a interpretAggregation gdf (Lit value) = TColumn $ fromVector $ V.replicate (VG.length (offsets gdf) - 1) value interpretAggregation gdf@(Grouped df names indices os) (Col name) = case getColumn name df of Nothing -> throw $ ColumnNotFoundException name "" (map fst $ M.toList $ columnIndices df) Just col -> TColumn $ atIndicesStable (VG.map (indices `VG.unsafeIndex`) (VG.init os)) col-interpretAggregation gdf (Apply _ (f :: c -> d) expr) = let+interpretAggregation gdf (Apply _ (f :: c -> d) expr) =+ let (TColumn value) = interpretAggregation @c gdf expr- in case mapColumn f value of- Nothing -> error "Type error in interpretation"- Just col -> TColumn col-interpretAggregation gdf (BinOp _ (f :: c -> d -> e) left right) = let+ in+ case mapColumn f value of+ Nothing -> error "Type error in interpretation"+ Just col -> TColumn col+interpretAggregation gdf (BinOp _ (f :: c -> d -> e) left right) =+ let (TColumn left') = interpretAggregation @c gdf left (TColumn right') = interpretAggregation @d gdf right- in case zipWithColumns f left' right' of- Nothing -> error "Type error in binary operation"- Just col -> TColumn col+ in+ case zipWithColumns f left' right' of+ Nothing -> error "Type error in binary operation"+ Just col -> TColumn col interpretAggregation gdf@(Grouped df names indices os) (GeneralAggregate name op (f :: forall v b. (VG.Vector v b, Columnable b) => v b -> c)) = case getColumn name df of Nothing -> throw $ ColumnNotFoundException name "" (map fst $ M.toList $ columnIndices df)- Just (BoxedColumn col) -> TColumn $ fromVector $- V.generate (VG.length os - 1)- (\i -> f (V.generate (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))- (\j -> col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i))))- )- )+ Just (BoxedColumn col) ->+ TColumn $+ fromVector $+ V.generate+ (VG.length os - 1)+ ( \i ->+ f+ ( V.generate+ (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))+ (\j -> col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i))))+ )+ ) Just (UnboxedColumn col) -> case sUnbox @c of- SFalse -> TColumn $ fromVector $- V.generate (VG.length os - 1)- (\i -> f (VU.generate (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))- (\j -> col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i))))- )- )- STrue -> TColumn $ fromUnboxedVector $- VU.generate (VG.length os - 1)- (\i -> f (VU.generate (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))- (\j -> col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i))))- )- )- Just (OptionalColumn col) -> TColumn $ fromVector $- V.generate (VG.length os - 1)- (\i -> f (V.generate (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))- (\j -> col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i))))- )- )-interpretAggregation gdf@(Grouped df names indices os) (ReductionAggregate name op (f :: forall a . Columnable a => a -> a -> a)) = case getColumn name df of+ SFalse ->+ TColumn $+ fromVector $+ V.generate+ (VG.length os - 1)+ ( \i ->+ f+ ( VU.generate+ (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))+ (\j -> col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i))))+ )+ )+ STrue ->+ TColumn $+ fromUnboxedVector $+ VU.generate+ (VG.length os - 1)+ ( \i ->+ f+ ( VU.generate+ (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))+ (\j -> col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i))))+ )+ )+ Just (OptionalColumn col) ->+ TColumn $+ fromVector $+ V.generate+ (VG.length os - 1)+ ( \i ->+ f+ ( V.generate+ (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))+ (\j -> col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i))))+ )+ )+interpretAggregation gdf@(Grouped df names indices os) (ReductionAggregate name op (f :: forall a. (Columnable a) => a -> a -> a)) = case getColumn name df of Nothing -> throw $ ColumnNotFoundException name "" (map fst $ M.toList $ columnIndices df)- Just (BoxedColumn col) -> TColumn $ fromVector $- VG.generate (VG.length os - 1) $ \g ->- let !start = os `VG.unsafeIndex` g- !end = os `VG.unsafeIndex` (g+1)- in go (col `VG.unsafeIndex` (indices `VG.unsafeIndex` start)) (start + 1) end- where- {-# INLINE go #-}- go !acc j e- | j == e = acc- | otherwise =- let !x = col `VG.unsafeIndex` (indices `VG.unsafeIndex` j)- in go (f acc x) (j + 1) e+ Just (BoxedColumn col) -> TColumn $+ fromVector $+ VG.generate (VG.length os - 1) $ \g ->+ let !start = os `VG.unsafeIndex` g+ !end = os `VG.unsafeIndex` (g + 1)+ in go (col `VG.unsafeIndex` (indices `VG.unsafeIndex` start)) (start + 1) end+ where+ {-# INLINE go #-}+ go !acc j e+ | j == e = acc+ | otherwise =+ let !x = col `VG.unsafeIndex` (indices `VG.unsafeIndex` j)+ in go (f acc x) (j + 1) e Just (UnboxedColumn col) -> case sUnbox @a of- SFalse -> TColumn $ fromVector $- VG.generate (VG.length os - 1) $ \g ->- let !start = os `VG.unsafeIndex` g- !end = os `VG.unsafeIndex` (g+1)- in go (col `VG.unsafeIndex` (indices `VG.unsafeIndex` start)) (start + 1) end- where- {-# INLINE go #-}- go !acc j e- | j == e = acc- | otherwise =- let !x = col `VG.unsafeIndex` (indices `VG.unsafeIndex` j)- in go (f acc x) (j + 1) e- STrue -> TColumn $ fromVector $- VG.generate (VG.length os - 1) $ \g ->- let !start = os `VG.unsafeIndex` g- !end = os `VG.unsafeIndex` (g+1)- in go (col `VG.unsafeIndex` (indices `VG.unsafeIndex` start)) (start + 1) end- where- {-# INLINE go #-}- go !acc j e- | j == e = acc- | otherwise =- let !x = col `VG.unsafeIndex` (indices `VG.unsafeIndex` j)- in go (f acc x) (j + 1) e- Just (OptionalColumn col) -> TColumn $ fromVector $- VG.generate (VG.length os - 1) $ \g ->- let !start = os `VG.unsafeIndex` g- !end = os `VG.unsafeIndex` (g+1)- in go (col `VG.unsafeIndex` (indices `VG.unsafeIndex` start)) (start + 1) end- where- {-# INLINE go #-}- go !acc j e- | j == e = acc- | otherwise =- let !x = col `VG.unsafeIndex` (indices `VG.unsafeIndex` j)- in go (f acc x) (j + 1) e+ SFalse -> TColumn $+ fromVector $+ VG.generate (VG.length os - 1) $ \g ->+ let !start = os `VG.unsafeIndex` g+ !end = os `VG.unsafeIndex` (g + 1)+ in go (col `VG.unsafeIndex` (indices `VG.unsafeIndex` start)) (start + 1) end+ where+ {-# INLINE go #-}+ go !acc j e+ | j == e = acc+ | otherwise =+ let !x = col `VG.unsafeIndex` (indices `VG.unsafeIndex` j)+ in go (f acc x) (j + 1) e+ STrue -> TColumn $+ fromVector $+ VG.generate (VG.length os - 1) $ \g ->+ let !start = os `VG.unsafeIndex` g+ !end = os `VG.unsafeIndex` (g + 1)+ in go (col `VG.unsafeIndex` (indices `VG.unsafeIndex` start)) (start + 1) end+ where+ {-# INLINE go #-}+ go !acc j e+ | j == e = acc+ | otherwise =+ let !x = col `VG.unsafeIndex` (indices `VG.unsafeIndex` j)+ in go (f acc x) (j + 1) e+ Just (OptionalColumn col) -> TColumn $+ fromVector $+ VG.generate (VG.length os - 1) $ \g ->+ let !start = os `VG.unsafeIndex` g+ !end = os `VG.unsafeIndex` (g + 1)+ in go (col `VG.unsafeIndex` (indices `VG.unsafeIndex` start)) (start + 1) end+ where+ {-# INLINE go #-}+ go !acc j e+ | j == e = acc+ | otherwise =+ let !x = col `VG.unsafeIndex` (indices `VG.unsafeIndex` j)+ in go (f acc x) (j + 1) e interpretAggregation gdf@(Grouped df names indices os) (NumericAggregate name op (f :: VU.Vector b -> c)) = case getColumn name df of Nothing -> throw $ ColumnNotFoundException name "" (map fst $ M.toList $ columnIndices df) Just (UnboxedColumn (col :: VU.Vector d)) -> case testEquality (typeRep @b) (typeRep @d) of- Nothing -> case testEquality (typeRep @d) (typeRep @Int) of+ Nothing -> case testEquality (typeRep @d) (typeRep @Int) of Just Refl -> case sUnbox @c of- SFalse -> TColumn $ fromVector $- V.generate (VG.length os - 1)- (\i -> f (VU.generate (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))- (\j -> fromIntegral (col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i)))))- )- )- STrue -> TColumn $ fromUnboxedVector $- VU.generate (VG.length os - 1)- (\i -> f (VU.generate (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))- (\j -> fromIntegral (col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i)))))- )- )+ SFalse ->+ TColumn $+ fromVector $+ V.generate+ (VG.length os - 1)+ ( \i ->+ f+ ( VU.generate+ (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))+ (\j -> fromIntegral (col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i)))))+ )+ )+ STrue ->+ TColumn $+ fromUnboxedVector $+ VU.generate+ (VG.length os - 1)+ ( \i ->+ f+ ( VU.generate+ (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))+ (\j -> fromIntegral (col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i)))))+ )+ ) Just Refl -> case sNumeric @d of SFalse -> error $ "Cannot apply numeric aggregation to non-numeric column: " ++ (T.unpack name)- STrue -> case sUnbox @c of- SFalse -> TColumn $ fromVector $- V.generate (VG.length os - 1)- (\i -> f (VU.generate (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))- (\j -> col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i))))- )+ STrue -> case sUnbox @c of+ SFalse ->+ TColumn $+ fromVector $+ V.generate+ (VG.length os - 1)+ ( \i ->+ f+ ( VU.generate+ (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))+ (\j -> col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i)))) )- STrue -> TColumn $ fromUnboxedVector $- VU.generate (VG.length os - 1)- (\i -> f (VU.generate (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))- (\j -> col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i))))- )+ )+ STrue ->+ TColumn $+ fromUnboxedVector $+ VU.generate+ (VG.length os - 1)+ ( \i ->+ f+ ( VU.generate+ (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))+ (\j -> col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i)))) )+ ) _ -> error $ "Cannot apply numeric aggregation to non-numeric column: " ++ (T.unpack name) instance (Num a, Columnable a) => Num (Expr a) where@@ -226,15 +307,15 @@ (*) = BinOp "mult" (*) fromInteger :: Integer -> Expr a- fromInteger = Lit . fromInteger + fromInteger = Lit . fromInteger negate :: Expr a -> Expr a negate = Apply "negate" negate - abs :: Num a => Expr a -> Expr a+ abs :: (Num a) => Expr a -> Expr a abs = Apply "abs" abs - signum :: Num a => Expr a -> Expr a+ signum :: (Num a) => Expr a -> Expr a signum = Apply "signum" signum instance (Fractional a, Columnable a) => Fractional (Expr a) where@@ -258,7 +339,7 @@ asin :: (Floating a, Columnable a) => Expr a -> Expr a asin = Apply "asin" asin acos :: (Floating a, Columnable a) => Expr a -> Expr a- acos = Apply "acos" acos + acos = Apply "acos" acos atan :: (Floating a, Columnable a) => Expr a -> Expr a atan = Apply "atan" atan sinh :: (Floating a, Columnable a) => Expr a -> Expr a@@ -272,10 +353,9 @@ atanh :: (Floating a, Columnable a) => Expr a -> Expr a atanh = Apply "atanh" atanh - instance (Show a) => Show (Expr a) where- show :: forall a . Show a => Expr a -> String+ show :: forall a. (Show a) => Expr a -> String show (Col name) = "col@" ++ show (typeRep @a) ++ "(" ++ T.unpack name ++ ")" show (Lit value) = show value show (Apply name f value) = T.unpack name ++ "(" ++ show value ++ ")"- show (BinOp name f a b) = T.unpack name ++ "(" ++ show a ++ ", " ++ show b ++ ")" + show (BinOp name f a b) = T.unpack name ++ "(" ++ show a ++ ", " ++ show b ++ ")"
src/DataFrame/Internal/Parsing.hs view
@@ -1,12 +1,14 @@ {-# LANGUAGE OverloadedStrings #-}+ module DataFrame.Internal.Parsing where import qualified Data.ByteString.Char8 as C import qualified Data.Set as S import qualified Data.Text as T -import Data.Text.Read+import Data.ByteString.Lex.Fractional import Data.Maybe (fromMaybe)+import Data.Text.Read import GHC.Stack (HasCallStack) import Text.Read (readMaybe) @@ -15,57 +17,64 @@ 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+ Nothing -> error $ "Could not read value: " ++ T.unpack s+ Just value -> value readInteger :: (HasCallStack) => T.Text -> Maybe Integer readInteger s = case signed decimal (T.strip s) of- Left _ -> Nothing- Right (value, "") -> Just value- Right (value, _) -> Nothing+ Left _ -> Nothing+ Right (value, "") -> Just value+ Right (value, _) -> Nothing readInt :: (HasCallStack) => T.Text -> Maybe Int readInt s = case signed decimal (T.strip s) of- Left _ -> Nothing- Right (value, "") -> Just value- Right (value, _) -> Nothing+ Left _ -> Nothing+ Right (value, "") -> Just value+ Right (value, _) -> Nothing {-# INLINE readInt #-} readByteStringInt :: (HasCallStack) => C.ByteString -> Maybe Int readByteStringInt s = case C.readInt (C.strip s) of- Nothing -> Nothing- Just (value, "") -> Just value- Just (value, _) -> Nothing+ Nothing -> Nothing+ Just (value, "") -> Just value+ Just (value, _) -> Nothing {-# INLINE readByteStringInt #-} +readByteStringDouble :: (HasCallStack) => C.ByteString -> Maybe Double+readByteStringDouble s = case readSigned readDecimal (C.strip s) of+ Nothing -> Nothing+ Just (value, "") -> Just value+ Just (value, _) -> Nothing+{-# INLINE readByteStringDouble #-}+ readDouble :: (HasCallStack) => T.Text -> Maybe Double readDouble s =- case signed double s of- Left _ -> Nothing- Right (value, "") -> Just value- Right (value, _) -> Nothing+ case signed double s of+ Left _ -> Nothing+ Right (value, "") -> Just value+ Right (value, _) -> Nothing {-# INLINE readDouble #-} readIntegerEither :: (HasCallStack) => T.Text -> Either T.Text Integer readIntegerEither s = case signed decimal (T.strip s) of- Left _ -> Left s- Right (value, "") -> Right value- Right (value, _) -> Left s+ Left _ -> Left s+ Right (value, "") -> Right value+ Right (value, _) -> Left s {-# INLINE readIntegerEither #-} readIntEither :: (HasCallStack) => T.Text -> Either T.Text Int readIntEither s = case signed decimal (T.strip s) of- Left _ -> Left s- Right (value, "") -> Right value- Right (value, _) -> Left s+ Left _ -> Left s+ Right (value, "") -> Right value+ Right (value, _) -> Left s {-# INLINE readIntEither #-} readDoubleEither :: (HasCallStack) => T.Text -> Either T.Text Double readDoubleEither s =- case signed double s of- Left _ -> Left s- Right (value, "") -> Right value- Right (value, _) -> Left s+ case signed double s of+ Left _ -> Left s+ Right (value, "") -> Right value+ Right (value, _) -> Left s {-# INLINE readDoubleEither #-} safeReadValue :: (Read a) => T.Text -> Maybe a
src/DataFrame/Internal/Row.hs view
@@ -1,10 +1,11 @@-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE ExplicitNamespaces #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE InstanceSigs #-}-{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+ module DataFrame.Internal.Row where import qualified Data.List as L@@ -12,25 +13,24 @@ import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Vector as V+import qualified Data.Vector.Algorithms.Merge as VA import qualified Data.Vector.Generic as VG import qualified Data.Vector.Unboxed as VU-import qualified Data.Vector.Algorithms.Merge as VA import Control.Exception (throw) import Control.Monad.ST (runST)-import DataFrame.Errors (DataFrameException(..))-import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame-import DataFrame.Internal.Types import Data.Function (on) import Data.Maybe (fromMaybe)+import Data.Type.Equality (TestEquality (..)) import Data.Typeable (Typeable, type (:~:) (..))-import Data.Word ( Word8, Word16, Word32, Word64 )-import Type.Reflection (TypeRep, typeOf, typeRep)-import Data.Type.Equality (TestEquality(..))-import Text.ParserCombinators.ReadPrec(ReadPrec)+import Data.Word (Word16, Word32, Word64, Word8)+import DataFrame.Errors (DataFrameException (..))+import DataFrame.Internal.Column+import DataFrame.Internal.DataFrame+import DataFrame.Internal.Types+import Text.ParserCombinators.ReadPrec (ReadPrec) import Text.Read (Lexeme (Ident), lexP, parens, readListPrec, readListPrecDefault, readPrec)-+import Type.Reflection (TypeRep, typeOf, typeRep) data Any where Value :: (Columnable' a) => a -> Any@@ -51,71 +51,76 @@ show :: Any -> String show (Value a) = T.unpack (showValue a) -showValue :: forall a . (Columnable' a) => a -> T.Text+showValue :: forall a. (Columnable' a) => a -> T.Text showValue v = case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl -> v- Nothing -> case testEquality (typeRep @a) (typeRep @String) of- Just Refl -> T.pack v- Nothing -> (T.pack . show) v+ Just Refl -> v+ Nothing -> case testEquality (typeRep @a) (typeRep @String) of+ Just Refl -> T.pack v+ Nothing -> (T.pack . show) v instance Read Any where- readListPrec :: ReadPrec [Any]- readListPrec = readListPrecDefault-- readPrec :: ReadPrec Any- readPrec = parens $ do- Ident "Value" <- lexP- readPrec+ readListPrec :: ReadPrec [Any]+ readListPrec = readListPrecDefault + readPrec :: ReadPrec Any+ readPrec = parens $ do+ Ident "Value" <- lexP+ readPrec -toAny :: forall a . (Columnable' a) => a -> Any-toAny = Value+toAny :: forall a. (Columnable' a) => a -> Any+toAny = Value type Row = V.Vector Any toRowList :: [T.Text] -> DataFrame -> [Row]-toRowList names df = let- nameSet = S.fromList names- in map (mkRowRep df nameSet) [0..(fst (dataframeDimensions df) - 1)]+toRowList names df =+ let+ nameSet = S.fromList names+ in+ map (mkRowRep df nameSet) [0 .. (fst (dataframeDimensions df) - 1)] toRowVector :: [T.Text] -> DataFrame -> V.Vector Row-toRowVector names df = let- nameSet = S.fromList names- in V.generate (fst (dataframeDimensions df)) (mkRowRep df nameSet)+toRowVector names df =+ let+ nameSet = S.fromList names+ in+ V.generate (fst (dataframeDimensions df)) (mkRowRep df nameSet) mkRowFromArgs :: [T.Text] -> DataFrame -> Int -> Row mkRowFromArgs names df i = V.map get (V.fromList names) where get name = case getColumn name df of- Nothing -> throw $ ColumnNotFoundException name "[INTERNAL] mkRowFromArgs" (map fst $ M.toList $ columnIndices df)- Just (BoxedColumn column) -> toAny (column V.! i)- Just (UnboxedColumn column) -> toAny (column VU.! i)- Just (OptionalColumn column) -> toAny (column V.! i)+ Nothing -> throw $ ColumnNotFoundException name "[INTERNAL] mkRowFromArgs" (map fst $ M.toList $ columnIndices df)+ Just (BoxedColumn column) -> toAny (column V.! i)+ Just (UnboxedColumn column) -> toAny (column VU.! i)+ Just (OptionalColumn column) -> toAny (column V.! i) mkRowRep :: DataFrame -> S.Set T.Text -> Int -> Row mkRowRep df names i = V.generate (S.size names) (\index -> get (names' V.! index)) where inOrderIndexes = map fst $ L.sortBy (compare `on` snd) $ M.toList (columnIndices df) names' = V.fromList [n | n <- inOrderIndexes, S.member n names]- throwError name = error $ "Column "+ throwError name =+ error $+ "Column " ++ T.unpack name ++ " has less items than " ++ "the other columns at index " ++ show i get name = case getColumn name df of- Just (BoxedColumn c) -> case c V.!? i of- Just e -> toAny e- Nothing -> throwError name- Just (OptionalColumn c) -> case c V.!? i of- Just e -> toAny e- Nothing -> throwError name- Just (UnboxedColumn c) -> case c VU.!? i of- Just e -> toAny e- Nothing -> throwError name+ Just (BoxedColumn c) -> case c V.!? i of+ Just e -> toAny e+ Nothing -> throwError name+ Just (OptionalColumn c) -> case c V.!? i of+ Just e -> toAny e+ Nothing -> throwError name+ Just (UnboxedColumn c) -> case c VU.!? i of+ Just e -> toAny e+ Nothing -> throwError name sortedIndexes' :: Bool -> V.Vector Row -> VU.Vector Int sortedIndexes' asc rows = runST $ do- withIndexes <- VG.thaw (V.indexed rows)- VA.sortBy ((if asc then compare else flip compare) `on` snd) withIndexes- sorted <- VG.unsafeFreeze withIndexes- return $ VU.generate (VG.length rows) (\i -> fst (sorted VG.! i))+ withIndexes <- VG.thaw (V.indexed rows)+ VA.sortBy ((if asc then compare else flip compare) `on` snd) withIndexes+ sorted <- VG.unsafeFreeze withIndexes+ return $ VU.generate (VG.length rows) (\i -> fst (sorted VG.! i))
src/DataFrame/Internal/Schema.hs view
@@ -1,23 +1,24 @@-{-# LANGUAGE GADTs #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+ module DataFrame.Internal.Schema where import qualified Data.Map as M import qualified Data.Proxy as P import qualified Data.Text as T -import DataFrame.Internal.Column import Data.Maybe-import Data.Type.Equality (type (:~:)(Refl), TestEquality (..))+import Data.Type.Equality (TestEquality (..), type (:~:) (Refl))+import DataFrame.Internal.Column import Type.Reflection (typeRep) data SchemaType where- SType :: Columnable a => P.Proxy a -> SchemaType+ SType :: (Columnable a) => P.Proxy a -> SchemaType instance Show SchemaType where show (SType (_ :: P.Proxy a)) = show (typeRep @a)@@ -25,9 +26,10 @@ instance Eq SchemaType where (==) (SType (_ :: P.Proxy a)) (SType (_ :: P.Proxy b)) = isJust (testEquality (typeRep @a) (typeRep @b)) -schemaType :: forall a . Columnable a => SchemaType+schemaType :: forall a. (Columnable a) => SchemaType schemaType = SType (P.Proxy @a) -data Schema = Schema {- elements :: M.Map T.Text SchemaType-} deriving (Show, Eq)+data Schema = Schema+ { elements :: M.Map T.Text SchemaType+ }+ deriving (Show, Eq)
src/DataFrame/Internal/Types.hs view
@@ -1,101 +1,103 @@+{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-}+{-# LANGUAGE PolyKinds #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE UndecidableInstances #-}+ module DataFrame.Internal.Types where -import Data.Int ( Int8, Int16, Int32, Int64 )-import Data.Kind (Type, Constraint)+import Data.Int (Int16, Int32, Int64, Int8)+import Data.Kind (Constraint, Type) import Data.Maybe (fromMaybe)+import Data.Type.Equality (TestEquality (..)) import Data.Typeable (Typeable, type (:~:) (..))-import Data.Word ( Word8, Word16, Word32, Word64 )-import Type.Reflection (TypeRep, typeOf, typeRep)-import Data.Type.Equality (TestEquality(..)) import qualified Data.Vector as VB import qualified Data.Vector.Unboxed as VU+import Data.Word (Word16, Word32, Word64, Word8)+import Type.Reflection (TypeRep, typeOf, typeRep) type Columnable' a = (Typeable a, Show a, Ord a, Eq a, Read a) --- | A type with column representations used to select the--- "right" representation when specializing the `toColumn` function.+{- | A type with column representations used to select the+"right" representation when specializing the `toColumn` function.+-} data Rep- = RBoxed- | RUnboxed- | ROptional+ = RBoxed+ | RUnboxed+ | ROptional -- | Type-level if statement. type family If (cond :: Bool) (yes :: k) (no :: k) :: k where- If 'True yes _ = yes- If 'False _ no = no+ If 'True yes _ = yes+ If 'False _ no = no -- | All unboxable types (according to the `vector` package). type family Unboxable (a :: Type) :: Bool where- Unboxable Int = 'True- Unboxable Int8 = 'True- Unboxable Int16 = 'True- Unboxable Int32 = 'True- Unboxable Int64 = 'True- Unboxable Word = 'True- Unboxable Word8 = 'True- Unboxable Word16 = 'True- Unboxable Word32 = 'True- Unboxable Word64 = 'True- Unboxable Char = 'True- Unboxable Bool = 'True- Unboxable Double = 'True- Unboxable Float = 'True- Unboxable _ = 'False+ Unboxable Int = 'True+ Unboxable Int8 = 'True+ Unboxable Int16 = 'True+ Unboxable Int32 = 'True+ Unboxable Int64 = 'True+ Unboxable Word = 'True+ Unboxable Word8 = 'True+ Unboxable Word16 = 'True+ Unboxable Word32 = 'True+ Unboxable Word64 = 'True+ Unboxable Char = 'True+ Unboxable Bool = 'True+ Unboxable Double = 'True+ Unboxable Float = 'True+ Unboxable _ = 'False -- | All unboxable types (according to the `vector` package). type family Numeric (a :: Type) :: Bool where- Numeric Int = 'True- Numeric Int8 = 'True- Numeric Int16 = 'True- Numeric Int32 = 'True- Numeric Int64 = 'True- Numeric Word = 'True- Numeric Word8 = 'True- Numeric Word16 = 'True- Numeric Word32 = 'True- Numeric Word64 = 'True- Numeric Double = 'True- Numeric Float = 'True- Numeric _ = 'False+ Numeric Int = 'True+ Numeric Int8 = 'True+ Numeric Int16 = 'True+ Numeric Int32 = 'True+ Numeric Int64 = 'True+ Numeric Word = 'True+ Numeric Word8 = 'True+ Numeric Word16 = 'True+ Numeric Word32 = 'True+ Numeric Word64 = 'True+ Numeric Double = 'True+ Numeric Float = 'True+ Numeric _ = 'False -- | Compute the column representation tag for any ‘a’. type family KindOf a :: Rep where- KindOf (Maybe a) = 'ROptional- KindOf a = If (Unboxable a) 'RUnboxed 'RBoxed+ KindOf (Maybe a) = 'ROptional+ KindOf a = If (Unboxable a) 'RUnboxed 'RBoxed -- | Type-level boolean for constraint/type comparison. data SBool (b :: Bool) where- STrue :: SBool 'True- SFalse :: SBool 'False+ STrue :: SBool 'True+ SFalse :: SBool 'False -- | The runtime witness for our type-level branching. class SBoolI (b :: Bool) where- sbool :: SBool b+ sbool :: SBool b -instance SBoolI 'True where sbool = STrue+instance SBoolI 'True where sbool = STrue instance SBoolI 'False where sbool = SFalse -- | Type-level function to determine whether or not a type is unboxa-sUnbox :: forall a. SBoolI (Unboxable a) => SBool (Unboxable a)+sUnbox :: forall a. (SBoolI (Unboxable a)) => SBool (Unboxable a) sUnbox = sbool @(Unboxable a) -sNumeric :: forall a. SBoolI (Numeric a) => SBool (Numeric a)+sNumeric :: forall a. (SBoolI (Numeric a)) => SBool (Numeric a) sNumeric = sbool @(Numeric a) type family When (flag :: Bool) (c :: Constraint) :: Constraint where- When 'True c = c- When 'False c = () -- empty constraint+ When 'True c = c+ When 'False c = () -- empty constraint type UnboxIf a = When (Unboxable a) (VU.Unbox a)
src/DataFrame/Lazy/IO/CSV.hs view
@@ -1,10 +1,11 @@ {-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE RankNTypes #-}+ module DataFrame.Lazy.IO.CSV where import qualified Data.ByteString.Char8 as C@@ -12,62 +13,65 @@ import qualified Data.Map as M import qualified Data.Set as S import qualified Data.Text as T+import qualified Data.Text.IO as TIO import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.IO as TLIO-import qualified Data.Text.IO as TIO import qualified Data.Vector as V-import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Mutable as VM+import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Unboxed.Mutable as VUM -import Control.Applicative ((<$>), (<|>), (<*>), (<*), (*>), many)-import Control.Monad (forM_, zipWithM_, unless, when, void, replicateM_)+import Control.Applicative (many, (*>), (<$>), (<*), (<*>), (<|>))+import Control.Monad (forM_, replicateM_, unless, void, when, zipWithM_) import Data.Attoparsec.Text import Data.Char-import DataFrame.Internal.Column (Column(..), MutableColumn(..), freezeColumn', writeColumn, columnLength)-import DataFrame.Internal.DataFrame (DataFrame(..))-import DataFrame.Internal.Parsing-import DataFrame.Operations.Typing import Data.Foldable (fold) import Data.Function (on) import Data.IORef import Data.Maybe import Data.Text.Encoding (decodeUtf8Lenient)-import Data.Type.Equality- ( TestEquality (testEquality),- type (:~:) (Refl)- )+import Data.Type.Equality (+ TestEquality (testEquality),+ type (:~:) (Refl),+ )+import DataFrame.Internal.Column (Column (..), MutableColumn (..), columnLength, freezeColumn', writeColumn)+import DataFrame.Internal.DataFrame (DataFrame (..))+import DataFrame.Internal.Parsing+import DataFrame.Operations.Typing import GHC.IO.Handle (Handle)-import Prelude hiding (concat, takeWhile) import System.IO import Type.Reflection+import Prelude hiding (concat, takeWhile) -- | Record for CSV read options.-data ReadOptions = ReadOptions {- hasHeader :: Bool,- inferTypes :: Bool,- safeRead :: Bool,- rowRange :: !(Maybe (Int, Int)), -- (start, length)- seekPos :: !(Maybe Integer),- totalRows :: !(Maybe Int),- leftOver :: !T.Text,- rowsRead :: !Int-}+data ReadOptions = ReadOptions+ { hasHeader :: Bool+ , inferTypes :: Bool+ , safeRead :: Bool+ , rowRange :: !(Maybe (Int, Int)) -- (start, length)+ , seekPos :: !(Maybe Integer)+ , totalRows :: !(Maybe Int)+ , leftOver :: !T.Text+ , rowsRead :: !Int+ } --- | By default we assume the file has a header, we infer the types on read--- and we convert any rows with nullish objects into Maybe (safeRead).+{- | By default we assume the file has a header, we infer the types on read+and we convert any rows with nullish objects into Maybe (safeRead).+-} defaultOptions :: ReadOptions-defaultOptions = ReadOptions { hasHeader = True, inferTypes = True, safeRead = True, rowRange = Nothing, seekPos = Nothing, totalRows = Nothing, leftOver = "", rowsRead = 0 }+defaultOptions = ReadOptions{hasHeader = True, inferTypes = True, safeRead = True, rowRange = Nothing, seekPos = Nothing, totalRows = Nothing, leftOver = "", rowsRead = 0} --- | Reads a CSV file from the given path.--- Note this file stores intermediate temporary files--- while converting the CSV from a row to a columnar format.+{- | Reads a CSV file from the given path.+Note this file stores intermediate temporary files+while converting the CSV from a row to a columnar format.+-} readCsv :: String -> IO DataFrame readCsv path = fst <$> readSeparated ',' defaultOptions path --- | Reads a tab separated file from the given path.--- Note this file stores intermediate temporary files--- while converting the CSV from a row to a columnar format.+{- | Reads a tab separated file from the given path.+Note this file stores intermediate temporary files+while converting the CSV from a row to a columnar format.+-} readTsv :: String -> IO DataFrame readTsv path = fst <$> readSeparated '\t' defaultOptions path @@ -78,13 +82,14 @@ Nothing -> countRows c path >>= \total -> if hasHeader opts then return (total - 1) else return total Just n -> if hasHeader opts then return (n - 1) else return n let (begin, len) = case rowRange opts of- Nothing -> (0, totalRows)+ Nothing -> (0, totalRows) Just (start, len) -> (start, min len (totalRows - rowsRead opts)) withFile path ReadMode $ \handle -> do firstRow <- map T.strip . parseSep c <$> TIO.hGetLine handle- let columnNames = if hasHeader opts- then map (T.filter (/= '\"')) firstRow- else map (T.singleton . intToDigit) [0..(length firstRow - 1)]+ let columnNames =+ if hasHeader opts+ then map (T.filter (/= '\"')) firstRow+ else map (T.singleton . intToDigit) [0 .. (length firstRow - 1)] -- If there was no header rewind the file cursor. unless (hasHeader opts) $ hSeek handle AbsoluteSeek 0 @@ -115,64 +120,80 @@ cols <- V.mapM (freezeColumn mutableCols nulls' opts) (V.generate numColumns id) pos <- hTell handle - return (DataFrame {- columns = cols,- columnIndices = M.fromList (zip columnNames [0..]),- dataframeDimensions = (maybe 0 columnLength (cols V.!? 0), V.length cols)- }, (pos, unconsumed, r + 1))+ return+ ( DataFrame+ { columns = cols+ , columnIndices = M.fromList (zip columnNames [0 ..])+ , dataframeDimensions = (maybe 0 columnLength (cols V.!? 0), V.length cols)+ }+ , (pos, unconsumed, r + 1)+ ) {-# INLINE readSeparated #-} getInitialDataVectors :: Int -> VM.IOVector MutableColumn -> [T.Text] -> IO () getInitialDataVectors n mCol xs = do- forM_ (zip [0..] xs) $ \(i, x) -> do+ forM_ (zip [0 ..] xs) $ \(i, x) -> do col <- case inferValueType x of- "Int" -> MUnboxedColumn <$> ((VUM.unsafeNew n :: IO (VUM.IOVector Int)) >>= \c -> VUM.unsafeWrite c 0 (fromMaybe 0 $ readInt x) >> return c)- "Double" -> MUnboxedColumn <$> ((VUM.unsafeNew n :: IO (VUM.IOVector Double)) >>= \c -> VUM.unsafeWrite c 0 (fromMaybe 0 $ readDouble x) >> return c)- _ -> MBoxedColumn <$> ((VM.unsafeNew n :: IO (VM.IOVector T.Text)) >>= \c -> VM.unsafeWrite c 0 x >> return c)+ "Int" -> MUnboxedColumn <$> ((VUM.unsafeNew n :: IO (VUM.IOVector Int)) >>= \c -> VUM.unsafeWrite c 0 (fromMaybe 0 $ readInt x) >> return c)+ "Double" -> MUnboxedColumn <$> ((VUM.unsafeNew n :: IO (VUM.IOVector Double)) >>= \c -> VUM.unsafeWrite c 0 (fromMaybe 0 $ readDouble x) >> return c)+ _ -> MBoxedColumn <$> ((VM.unsafeNew n :: IO (VM.IOVector T.Text)) >>= \c -> VM.unsafeWrite c 0 x >> return c) VM.unsafeWrite mCol i col {-# INLINE getInitialDataVectors #-} inferValueType :: T.Text -> T.Text-inferValueType s = let+inferValueType s =+ let example = s- in case readInt example of- Just _ -> "Int"- Nothing -> case readDouble example of- Just _ -> "Double"- Nothing -> "Other"+ in+ case readInt example of+ Just _ -> "Int"+ Nothing -> case readDouble example of+ Just _ -> "Double"+ Nothing -> "Other" {-# INLINE inferValueType #-} readSingleLine :: Char -> T.Text -> Handle -> IO ([T.Text], T.Text)-readSingleLine c unused handle = parseWith (TIO.hGetChunk handle) (parseRow c) unused >>= \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 :: T.Text) (row :: [T.Text]) -> do- return (row, unconsumed)+readSingleLine c unused handle =+ parseWith (TIO.hGetChunk handle) (parseRow c) unused >>= \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 :: T.Text) (row :: [T.Text]) -> do+ return (row, unconsumed) -- | Reads rows from the handle and stores values in mutable vectors. fillColumns :: Int -> Char -> VM.IOVector MutableColumn -> VM.IOVector [(Int, T.Text)] -> T.Text -> Handle -> IO (T.Text, Int) fillColumns n c mutableCols nullIndices unused handle = do input <- newIORef unused rowsRead <- newIORef (0 :: Int)- forM_ [1..(n - 1)] $ \i -> do+ forM_ [1 .. (n - 1)] $ \i -> do isEOF <- hIsEOF handle input' <- readIORef input unless (isEOF && input' == mempty) $ do- parseWith (TIO.hGetChunk handle) (parseRow c) input' >>= \case+ parseWith (TIO.hGetChunk handle) (parseRow c) input' >>= \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+ 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"+ fail "Partial handler is called" Done (unconsumed :: T.Text) (row :: [T.Text]) -> do- writeIORef input unconsumed- modifyIORef rowsRead (+1)- zipWithM_ (writeValue mutableCols nullIndices i) [0..] row+ writeIORef input unconsumed+ modifyIORef rowsRead (+ 1)+ zipWithM_ (writeValue mutableCols nullIndices i) [0 ..] row l <- readIORef input r <- readIORef rowsRead pure (l, r)@@ -200,17 +221,17 @@ record :: Char -> Parser [T.Text] record c =- field c `sepBy1` char c- <?> "record"+ field c `sepBy1` char c+ <?> "record" {-# INLINE record #-} parseRow :: Char -> Parser [T.Text]-parseRow c = (record c <* lineEnd) <?> "record-new-line"+parseRow c = (record c <* lineEnd) <?> "record-new-line" field :: Char -> Parser T.Text field c =- quotedField <|> unquotedField c- <?> "field"+ quotedField <|> unquotedField c+ <?> "field" {-# INLINE field #-} unquotedTerminators :: Char -> S.Set Char@@ -218,59 +239,71 @@ unquotedField :: Char -> Parser T.Text unquotedField sep =- takeWhile (not . (`S.member` terminators)) <?> "unquoted field"- where terminators = unquotedTerminators sep+ takeWhile (not . (`S.member` terminators)) <?> "unquoted field"+ where+ terminators = unquotedTerminators sep {-# INLINE unquotedField #-} quotedField :: Parser T.Text quotedField = char '"' *> contents <* char '"' <?> "quoted field"- where- contents = fold <$> many (unquote <|> unescape)- where- unquote = takeWhile1 (notInClass "\"\\")- unescape = char '\\' *> do- T.singleton <$> do- char '\\' <|> char '"'+ where+ contents = fold <$> many (unquote <|> unescape)+ where+ unquote = takeWhile1 (notInClass "\"\\")+ unescape =+ char '\\' *> do+ T.singleton <$> do+ char '\\' <|> char '"' {-# INLINE quotedField #-} lineEnd :: Parser () lineEnd =- (endOfLine <|> endOfInput)- <?> "end of line"+ (endOfLine <|> endOfInput)+ <?> "end of line" {-# INLINE lineEnd #-} -- | First pass to count rows for exact allocation countRows :: Char -> FilePath -> IO Int countRows c path = withFile path ReadMode $! go 0 ""- where- go n input h = do- isEOF <- hIsEOF h- if isEOF && input == mempty+ where+ go n input h = do+ isEOF <- hIsEOF h+ if isEOF && input == mempty then pure n else- parseWith (TIO.hGetChunk h) (parseRow c) input >>= \case- Fail unconsumed ctx er -> do- erpos <- hTell h- fail $ "Failed to parse CSV file around " <> show erpos <> " byte; due: "- <> show er <> "; context: " <> show ctx <> " " <> show unconsumed- Partial c -> do- fail $ "Partial handler is called; n = " <> show n- Done (unconsumed :: T.Text) _ ->- go (n + 1) unconsumed h+ parseWith (TIO.hGetChunk h) (parseRow c) input >>= \case+ Fail unconsumed ctx er -> do+ erpos <- hTell h+ fail $+ "Failed to parse CSV file around "+ <> show erpos+ <> " byte; due: "+ <> show er+ <> "; context: "+ <> show ctx+ <> " "+ <> show unconsumed+ Partial c -> do+ fail $ "Partial handler is called; n = " <> show n+ Done (unconsumed :: T.Text) _ ->+ go (n + 1) unconsumed h {-# INLINE countRows #-} writeCsv :: String -> DataFrame -> IO () writeCsv = writeSeparated ',' -writeSeparated :: Char -- ^ Separator- -> String -- ^ Path to write to- -> DataFrame- -> IO ()-writeSeparated c filepath df = withFile filepath WriteMode $ \handle ->do+writeSeparated ::+ -- | Separator+ Char ->+ -- | Path to write to+ String ->+ DataFrame ->+ IO ()+writeSeparated c filepath df = withFile filepath WriteMode $ \handle -> do let (rows, columns) = dataframeDimensions df let headers = map fst (L.sortBy (compare `on` snd) (M.toList (columnIndices df))) TIO.hPutStrLn handle (T.intercalate ", " headers)- forM_ [0..(rows - 1)] $ \i -> do+ forM_ [0 .. (rows - 1)] $ \i -> do let row = getRowAsText df i TIO.hPutStrLn handle (T.intercalate ", " row) @@ -280,43 +313,46 @@ indexMap = M.fromList (map (\(a, b) -> (b, a)) $ M.toList (columnIndices df)) go k (BoxedColumn (c :: V.Vector a)) acc = case c V.!? i of Just e -> textRep : acc- where textRep = case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl -> e- Nothing -> case typeRep @a of- App t1 t2 -> case eqTypeRep t1 (typeRep @Maybe) of- Just HRefl -> case testEquality t2 (typeRep @T.Text) of- Just Refl -> fromMaybe "null" e- Nothing -> (fromOptional . (T.pack . show)) e- where fromOptional s- | T.isPrefixOf "Just " s = T.drop (T.length "Just ") s- | otherwise = "null"- Nothing -> (T.pack . show) e- _ -> (T.pack . show) e+ where+ textRep = case testEquality (typeRep @a) (typeRep @T.Text) of+ Just Refl -> e+ Nothing -> case typeRep @a of+ App t1 t2 -> case eqTypeRep t1 (typeRep @Maybe) of+ Just HRefl -> case testEquality t2 (typeRep @T.Text) of+ Just Refl -> fromMaybe "null" e+ Nothing -> (fromOptional . (T.pack . show)) e+ where+ fromOptional s+ | T.isPrefixOf "Just " s = T.drop (T.length "Just ") s+ | otherwise = "null"+ Nothing -> (T.pack . show) e+ _ -> (T.pack . show) e Nothing -> error $ "Column "- ++ T.unpack (indexMap M.! k)- ++ " has less items than "- ++ "the other columns at index "- ++ show i+ ++ T.unpack (indexMap M.! k)+ ++ " has less items than "+ ++ "the other columns at index "+ ++ show i go k (UnboxedColumn c) acc = case c VU.!? i of Just e -> T.pack (show e) : acc Nothing -> error $ "Column "- ++ T.unpack (indexMap M.! k)- ++ " has less items than "- ++ "the other columns at index "- ++ show i+ ++ T.unpack (indexMap M.! k)+ ++ " has less items than "+ ++ "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+ where+ textRep = case testEquality (typeRep @a) (typeRep @T.Text) of+ Just Refl -> fromMaybe "Nothing" e+ Nothing -> (T.pack . show) e Nothing -> error $ "Column "- ++ T.unpack (indexMap M.! k)- ++ " has less items than "- ++ "the other columns at index "- ++ show i+ ++ T.unpack (indexMap M.! k)+ ++ " has less items than "+ ++ "the other columns at index "+ ++ show i
src/DataFrame/Lazy/Internal/DataFrame.hs view
@@ -1,90 +1,103 @@-{-# LANGUAGE GADTs #-}+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE InstanceSigs #-}-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE OverloadedStrings #-}+ module DataFrame.Lazy.Internal.DataFrame where -import Control.Monad (forM, foldM)-import Data.IORef-import Data.Kind+import Control.Monad (foldM, forM)+import Data.IORef+import Data.Kind import qualified Data.List as L import qualified Data.Map as M import qualified Data.Text as T import qualified Data.Vector as V-import qualified DataFrame.Internal.DataFrame as D import qualified DataFrame.Internal.Column as C+import qualified DataFrame.Internal.DataFrame as D import qualified DataFrame.Internal.Expression as E+import qualified DataFrame.Lazy.IO.CSV as D import qualified DataFrame.Operations.Core as D-import DataFrame.Operations.Merge+import DataFrame.Operations.Merge import qualified DataFrame.Operations.Subset as D import qualified DataFrame.Operations.Transformations as D-import qualified DataFrame.Lazy.IO.CSV as D-import System.FilePath+import System.FilePath data LazyOperation where- Derive :: C.Columnable a => T.Text -> E.Expr a -> LazyOperation- Select :: [T.Text] -> LazyOperation- Filter :: E.Expr Bool -> LazyOperation+ Derive :: (C.Columnable a) => T.Text -> E.Expr a -> LazyOperation+ Select :: [T.Text] -> LazyOperation+ Filter :: E.Expr Bool -> LazyOperation instance Show LazyOperation where- show :: LazyOperation -> String- show (Derive name expr) = T.unpack name ++ " := " ++ show expr- show (Select columns) = "select(" ++ show columns ++ ")"- show (Filter expr) = "filter(" ++ show expr ++ ")"+ show :: LazyOperation -> String+ show (Derive name expr) = T.unpack name ++ " := " ++ show expr+ show (Select columns) = "select(" ++ show columns ++ ")"+ show (Filter expr) = "filter(" ++ show expr ++ ")" -data InputType = ICSV deriving Show+data InputType = ICSV deriving (Show) data LazyDataFrame = LazyDataFrame- { inputPath :: FilePath- , inputType :: InputType- , operations :: [LazyOperation]- , batchSize :: Int- } deriving Show+ { inputPath :: FilePath+ , inputType :: InputType+ , operations :: [LazyOperation]+ , batchSize :: Int+ }+ deriving (Show) eval :: LazyOperation -> D.DataFrame -> D.DataFrame eval (Derive name expr) = D.derive name expr eval (Select columns) = D.select columns eval (Filter expr) = D.filterWhere expr -runDataFrame :: forall a . (C.Columnable a) => LazyDataFrame -> IO D.DataFrame+runDataFrame :: forall a. (C.Columnable a) => LazyDataFrame -> IO D.DataFrame runDataFrame df = do- let path = inputPath df- totalRows <- D.countRows ',' path- let batches = batchRanges totalRows (batchSize df)- (df', _) <- foldM (\(accDf, (pos, unused, r)) (start, end) -> do- mapM_ putStr ["Scanning: ", show start, " to ", show end, " rows out of ", show totalRows, "\n"] + let path = inputPath df+ totalRows <- D.countRows ',' path+ let batches = batchRanges totalRows (batchSize df)+ (df', _) <-+ foldM+ ( \(accDf, (pos, unused, r)) (start, end) -> do+ mapM_ putStr ["Scanning: ", show start, " to ", show end, " rows out of ", show totalRows, "\n"] - (sdf, (pos', unconsumed, rowsRead)) <- D.readSeparated ',' (- D.defaultOptions { D.rowRange = Just (start, batchSize df)- , D.totalRows = Just totalRows- , D.seekPos = pos- , D.rowsRead = r- , D.leftOver = unused}) path- let rdf = L.foldl' (flip eval) sdf (operations df)- return (accDf <> rdf, (Just pos', unconsumed, rowsRead + r)) ) (D.empty, (Nothing, "", 0)) batches- return df'+ (sdf, (pos', unconsumed, rowsRead)) <-+ D.readSeparated+ ','+ ( D.defaultOptions+ { D.rowRange = Just (start, batchSize df)+ , D.totalRows = Just totalRows+ , D.seekPos = pos+ , D.rowsRead = r+ , D.leftOver = unused+ }+ )+ path+ let rdf = L.foldl' (flip eval) sdf (operations df)+ return (accDf <> rdf, (Just pos', unconsumed, rowsRead + r))+ )+ (D.empty, (Nothing, "", 0))+ batches+ return df' batchRanges :: Int -> Int -> [(Int, Int)]-batchRanges n inc = go n [0,inc..n]+batchRanges n inc = go n [0, inc .. n] where- go _ [] = []- go n [x] = [(x, n)]- go n (f:s:rest) =(f, s) : go n (s:rest)+ go _ [] = []+ go n [x] = [(x, n)]+ go n (f : s : rest) = (f, s) : go n (s : rest) scanCsv :: T.Text -> LazyDataFrame scanCsv path = LazyDataFrame (T.unpack path) ICSV [] 512_000 addOperation :: LazyOperation -> LazyDataFrame -> LazyDataFrame-addOperation op df = df { operations = operations df ++ [op] }+addOperation op df = df{operations = operations df ++ [op]} -derive :: C.Columnable a => T.Text -> E.Expr a -> LazyDataFrame -> LazyDataFrame+derive :: (C.Columnable a) => T.Text -> E.Expr a -> LazyDataFrame -> LazyDataFrame derive name expr = addOperation (Derive name expr) -select :: C.Columnable a => [T.Text] -> LazyDataFrame -> LazyDataFrame+select :: (C.Columnable a) => [T.Text] -> LazyDataFrame -> LazyDataFrame select columns = addOperation (Select columns) -filter :: C.Columnable a => E.Expr Bool -> LazyDataFrame -> LazyDataFrame+filter :: (C.Columnable a) => E.Expr Bool -> LazyDataFrame -> LazyDataFrame filter cond = addOperation (Filter cond)
src/DataFrame/Operations/Aggregation.hs view
@@ -1,10 +1,11 @@ {-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}-{-# LANGUAGE FlexibleContexts #-}+ module DataFrame.Operations.Aggregation where import qualified Data.Set as S@@ -14,114 +15,129 @@ import qualified Data.Map as M import qualified Data.Map.Strict as MS import qualified Data.Text as T-import qualified Data.Vector.Generic as VG import qualified Data.Vector as V+import qualified Data.Vector.Algorithms.Merge as VA+import qualified Data.Vector.Generic as VG import qualified Data.Vector.Mutable as VM import qualified Data.Vector.Unboxed as VU-import qualified Data.Vector.Algorithms.Merge as VA-import qualified Statistics.Quantile as SS-import qualified Statistics.Sample as SS import Control.Exception (throw) import Control.Monad (foldM_) import Control.Monad.ST (runST)-import DataFrame.Internal.Column (Column(..), fromVector,- getIndicesUnboxed, getIndices, - atIndicesStable,- Columnable, unwrapTypedColumn,- columnVersionString)-import DataFrame.Internal.DataFrame (GroupedDataFrame(..), DataFrame(..), empty, getColumn, unsafeGetColumn)+import Data.Function ((&))+import Data.Hashable+import Data.List ((\\))+import Data.Maybe+import Data.Type.Equality (TestEquality (..), type (:~:) (Refl))+import DataFrame.Errors+import DataFrame.Internal.Column (+ Column (..),+ Columnable,+ atIndicesStable,+ columnVersionString,+ fromVector,+ getIndices,+ getIndicesUnboxed,+ unwrapTypedColumn,+ )+import DataFrame.Internal.DataFrame (DataFrame (..), GroupedDataFrame (..), empty, getColumn, unsafeGetColumn) import DataFrame.Internal.Expression import DataFrame.Internal.Parsing import DataFrame.Internal.Types-import DataFrame.Errors import DataFrame.Operations.Core import DataFrame.Operations.Merge import DataFrame.Operations.Subset-import Data.Function ((&))-import Data.Hashable-import Data.List ((\\))-import Data.Maybe-import Data.Type.Equality (type (:~:)(Refl), TestEquality(..))-import Type.Reflection (typeRep, typeOf)+import Type.Reflection (typeOf, typeRep) --- | O(k * n) groups the dataframe by the given rows aggregating the remaining rows--- into vector that should be reduced later.+{- | O(k * n) groups the dataframe by the given rows aggregating the remaining rows+into vector that should be reduced later.+-} groupBy ::- [T.Text] ->- DataFrame ->- GroupedDataFrame+ [T.Text] ->+ DataFrame ->+ GroupedDataFrame groupBy names df- | any (`notElem` columnNames df) names = throw $ ColumnNotFoundException (T.pack $ show $ names L.\\ columnNames df) "groupBy" (columnNames df)- | otherwise = Grouped df names (VG.map fst valueIndices) (VU.fromList (reverse (changingPoints valueIndices)))+ | any (`notElem` columnNames df) names = throw $ ColumnNotFoundException (T.pack $ show $ names L.\\ columnNames df) "groupBy" (columnNames df)+ | otherwise = Grouped df names (VG.map fst valueIndices) (VU.fromList (reverse (changingPoints valueIndices))) where indicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` names) (columnIndices df) rowRepresentations = VU.generate (fst (dimensions df)) (mkRowRep indicesToGroup df) valueIndices = runST $ do- withIndexes <- VG.thaw $ VG.indexed rowRepresentations- VA.sortBy (\(a, b) (a', b') -> compare b b') withIndexes- VG.unsafeFreeze withIndexes+ withIndexes <- VG.thaw $ VG.indexed rowRepresentations+ VA.sortBy (\(a, b) (a', b') -> compare b b') withIndexes+ VG.unsafeFreeze withIndexes changingPoints :: (Eq a, VU.Unbox a) => VU.Vector (Int, a) -> [Int] changingPoints vs = VG.length vs : (fst (VU.ifoldl findChangePoints initialState vs)) where initialState = ([0], snd (VG.head vs)) findChangePoints (offsets, currentVal) index (_, newVal)- | currentVal == newVal = (offsets, currentVal)- | otherwise = (index : offsets, newVal)+ | currentVal == newVal = (offsets, currentVal)+ | otherwise = (index : offsets, newVal) mkRowRep :: [Int] -> DataFrame -> Int -> Int mkRowRep groupColumnIndices df i = case h of- (x:[]) -> x- xs -> hash h+ (x : []) -> x+ xs -> hash h where h = (map mkHash groupColumnIndices) getHashedElem :: Column -> Int -> Int getHashedElem (BoxedColumn (c :: V.Vector a)) j = hash' @a (c V.! j) getHashedElem (UnboxedColumn (c :: VU.Vector a)) j = hash' @a (c VU.! j) getHashedElem (OptionalColumn (c :: V.Vector a)) j = hash' @a (c V.! j)- mkHash j = getHashedElem ((V.!) (columns df) j) i + mkHash j = getHashedElem ((V.!) (columns df) j) i --- | This hash function returns the hash when given a non numeric type but--- the value when given a numeric.-hash' :: Columnable a => a -> Int+{- | This hash function returns the hash when given a non numeric type but+the value when given a numeric.+-}+hash' :: (Columnable a) => a -> Int hash' value = case testEquality (typeOf value) (typeRep @Double) of- Just Refl -> round $ value * 1000- Nothing -> case testEquality (typeOf value) (typeRep @Int) of- Just Refl -> value- Nothing -> case testEquality (typeOf value) (typeRep @T.Text) of- Just Refl -> hash value- Nothing -> hash (show value)+ Just Refl -> round $ value * 1000+ Nothing -> case testEquality (typeOf value) (typeRep @Int) of+ Just Refl -> value+ Nothing -> case testEquality (typeOf value) (typeRep @T.Text) of+ Just Refl -> hash value+ Nothing -> hash (show value) mkGroupedColumns :: VU.Vector Int -> DataFrame -> DataFrame -> T.Text -> DataFrame mkGroupedColumns indices df acc name =- case (V.!) (columns df) (columnIndices df M.! name) of- BoxedColumn column ->- let vs = indices `getIndices` column- in insertVector name vs acc- OptionalColumn column ->- let vs = indices `getIndices` column- in insertVector name vs acc- UnboxedColumn column ->- let vs = indices `getIndicesUnboxed` column- in insertUnboxedVector name vs acc+ case (V.!) (columns df) (columnIndices df M.! name) of+ BoxedColumn column ->+ let vs = indices `getIndices` column+ in insertVector name vs acc+ OptionalColumn column ->+ let vs = indices `getIndices` column+ in insertVector name vs acc+ UnboxedColumn column ->+ let vs = indices `getIndicesUnboxed` column+ in insertUnboxedVector name vs acc +{- | Aggregate a grouped dataframe using the expressions give.+All ungrouped columns will be dropped.+-} aggregate :: [(T.Text, UExpr)] -> GroupedDataFrame -> DataFrame--- aggregate aggs df = undefined-aggregate aggs gdf@(Grouped df groupingColumns valueIndices offsets) = let- df' = selectIndices (VG.map (valueIndices VG.!) (VG.init offsets)) (select groupingColumns df)- groupedColumns = columnNames df L.\\ groupingColumns- f (name, Wrap (expr :: Expr a)) d = let- value = interpretAggregation @a gdf expr- in insertColumn name (unwrapTypedColumn value) d- in fold f aggs df'+aggregate aggs gdf@(Grouped df groupingColumns valueIndices offsets) =+ let+ df' = selectIndices (VG.map (valueIndices VG.!) (VG.init offsets)) (select groupingColumns df)+ groupedColumns = columnNames df L.\\ groupingColumns+ f (name, Wrap (expr :: Expr a)) d =+ let+ value = interpretAggregation @a gdf expr+ in+ insertColumn name (unwrapTypedColumn value) d+ in+ fold f aggs df' selectIndices :: VU.Vector Int -> DataFrame -> DataFrame-selectIndices xs df = df { columns = VG.map (atIndicesStable xs) (columns df)- , dataframeDimensions = (VG.length xs, VG.length (columns df))- }+selectIndices xs df =+ df+ { columns = VG.map (atIndicesStable xs) (columns df)+ , dataframeDimensions = (VG.length xs, VG.length (columns df))+ } +-- | Filter out all non-unique values in a dataframe. distinct :: DataFrame -> DataFrame distinct df = selectIndices (VG.map (indices VG.!) (VG.init os)) df- where (Grouped _ _ indices os) = groupBy (columnNames df) df+ where+ (Grouped _ _ indices os) = groupBy (columnNames df) df
src/DataFrame/Operations/Core.hs view
@@ -5,6 +5,7 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}+ module DataFrame.Operations.Core where import qualified Data.List as L@@ -12,225 +13,529 @@ import qualified Data.Map.Strict as MS import qualified Data.Set as S import qualified Data.Text as T-import qualified Data.Vector.Generic as VG import qualified Data.Vector as V+import qualified Data.Vector.Generic as VG import qualified Data.Vector.Unboxed as VU -import Control.Exception ( throw )-import DataFrame.Errors-import DataFrame.Internal.Column ( Column(..), fromVector, fromList, columnLength, columnTypeString, expandColumn, Columnable)-import DataFrame.Internal.DataFrame (DataFrame(..), getColumn, null, empty)-import DataFrame.Internal.Parsing (isNullish)+import Control.Exception (throw) import Data.Either import Data.Function (on, (&)) import Data.Maybe-import Data.Type.Equality (type (:~:)(Refl), TestEquality(..))+import Data.Type.Equality (TestEquality (..), type (:~:) (Refl))+import DataFrame.Errors+import DataFrame.Internal.Column (Column (..), Columnable, columnLength, columnTypeString, expandColumn, fromList, fromVector)+import DataFrame.Internal.DataFrame (DataFrame (..), empty, getColumn, null)+import DataFrame.Internal.Parsing (isNullish) import Type.Reflection import Prelude hiding (null) --- | O(1) Get DataFrame dimensions i.e. (rows, columns)+{- | O(1) Get DataFrame dimensions i.e. (rows, columns)++==== __Example__+@+ghci> D.dimensions df++(100, 3)+@+-} dimensions :: DataFrame -> (Int, Int) dimensions = dataframeDimensions {-# INLINE dimensions #-} --- | O(k) Get column names of the DataFrame in order of insertion.+{- | O(k) Get column names of the DataFrame in order of insertion.++==== __Example__+@+ghci> D.columnNames df++["col_a", "col_b", "col_c"]+@+-} columnNames :: DataFrame -> [T.Text]-columnNames = map fst . L.sortBy (compare `on` snd). M.toList . columnIndices+columnNames = map fst . L.sortBy (compare `on` snd) . M.toList . columnIndices {-# INLINE columnNames #-} --- | /O(n)/ Adds a vector to the dataframe.+{- | Adds a vector to the dataframe. If the vector has less elements than the dataframe and the dataframe is not empty+the vector is converted to type `Maybe a` filled with `Nothing` to match the size of the dataframe. Similarly,+if the vector 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`.++==== __Example__+@+ghci> import qualified Data.Vector as V++ghci> D.insertVector "numbers" (V.fromList [1..10]) D.empty++---------------+index | numbers+------|--------+ Int | Int+------|--------+0 | 1+1 | 2+2 | 3+3 | 4+4 | 5+5 | 6+6 | 7+7 | 8+8 | 9+9 | 10++@+-} insertVector ::- forall a.- Columnable a =>- -- | Column Name- T.Text ->- -- | Vector to add to column- V.Vector a ->- -- | DataFrame to add column to- DataFrame ->- DataFrame+ forall a.+ (Columnable a) =>+ -- | Column Name+ T.Text ->+ -- | Vector to add to column+ V.Vector a ->+ -- | DataFrame to add column to+ DataFrame ->+ DataFrame insertVector name xs = insertColumn name (fromVector xs) {-# INLINE insertVector #-} -cloneColumn :: T.Text -> T.Text -> DataFrame -> DataFrame-cloneColumn original new df = fromMaybe (throw $ ColumnNotFoundException original "cloneColumn" (map fst $ M.toList $ columnIndices df)) $ do- column <- getColumn original df- return $ insertColumn new column df+{- | /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.+-}+insertVectorWithDefault ::+ forall a.+ (Columnable a) =>+ -- | Default Value+ a ->+ -- | Column name+ T.Text ->+ -- | Data to add to column+ V.Vector a ->+ -- | DataFrame to add to column+ DataFrame ->+ DataFrame+insertVectorWithDefault defaultValue name xs d =+ let (rows, _) = dataframeDimensions d+ values = xs V.++ V.replicate (rows - V.length xs) defaultValue+ in insertColumn name (fromVector values) d --- | /O(n)/ Adds an unboxed vector to the dataframe.+{- | /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+into an unboxed vector so this function saves that extra work/conversion.+-} insertUnboxedVector ::- forall a.- (Columnable a, VU.Unbox a) =>- -- | Column Name- T.Text ->- -- | Unboxed vector to add to column- VU.Vector a ->- -- | DataFrame to add to column- DataFrame ->- DataFrame+ forall a.+ (Columnable a, VU.Unbox a) =>+ -- | Column Name+ T.Text ->+ -- | Unboxed vector to add to column+ VU.Vector a ->+ -- | DataFrame to add to column+ DataFrame ->+ DataFrame insertUnboxedVector name xs = insertColumn name (UnboxedColumn xs) --- -- | /O(n)/ Add a column to the dataframe. Not meant for external use.+{- | /O(n)/ Add a column to the dataframe.++==== __Example__+@+ghci> D.insertColumn "numbers" (D.fromList [1..10]) D.empty++---------------+index | numbers+------|--------+ Int | Int+------|--------+0 | 1+1 | 2+2 | 3+3 | 4+4 | 5+5 | 6+6 | 7+7 | 8+8 | 9+9 | 10++@+-} insertColumn ::- -- | Column Name- T.Text ->- -- | Column to add- Column ->- -- | DataFrame to add to column- DataFrame ->- DataFrame-insertColumn name column d = let- (r, c) = dataframeDimensions d- n = max (columnLength column) r- in case M.lookup name (columnIndices d) of- Just i -> DataFrame (V.map (expandColumn n) (columns d V.// [(i, column)])) (columnIndices d) (n, c)- Nothing -> DataFrame (V.map (expandColumn n) (columns d `V.snoc` column)) (M.insert name c (columnIndices d)) (n, c + 1)+ -- | Column Name+ T.Text ->+ -- | Column to add+ Column ->+ -- | DataFrame to add to column+ DataFrame ->+ DataFrame+insertColumn name column d =+ let+ (r, c) = dataframeDimensions d+ n = max (columnLength column) r+ in+ case M.lookup name (columnIndices d) of+ Just i -> DataFrame (V.map (expandColumn n) (columns d V.// [(i, column)])) (columnIndices d) (n, c)+ Nothing -> DataFrame (V.map (expandColumn n) (columns d `V.snoc` column)) (M.insert name c (columnIndices d)) (n, c + 1) --- | /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.-insertVectorWithDefault ::- forall a.- (Columnable a) =>- -- | Default Value- a ->- -- | Column name- T.Text ->- -- | Data to add to column- V.Vector a ->- -- | DataFrame to add to column- DataFrame ->- DataFrame-insertVectorWithDefault defaultValue name xs d =- let (rows, _) = dataframeDimensions d- values = xs V.++ V.replicate (rows - V.length xs) defaultValue- in insertColumn name (fromVector values) d+{- | /O(n)/ Clones a column and places it under a new name in the dataframe. +==== __Example__+@+ghci> import qualified Data.Vector as V++ghci> df = insertVector "numbers" (V.fromList [1..10]) D.empty++ghci> D.cloneColumn "numbers" "others" df++------------------------+index | numbers | others+------|---------|-------+ Int | Int | Int+------|---------|-------+0 | 1 | 1+1 | 2 | 2+2 | 3 | 3+3 | 4 | 4+4 | 5 | 5+5 | 6 | 6+6 | 7 | 7+7 | 8 | 8+8 | 9 | 9+9 | 10 | 10++@+-}+cloneColumn :: T.Text -> T.Text -> DataFrame -> DataFrame+cloneColumn original new df = fromMaybe (throw $ ColumnNotFoundException original "cloneColumn" (map fst $ M.toList $ columnIndices df)) $ do+ column <- getColumn original df+ return $ insertColumn new column df++{- | /O(n)/ Renames a single column.++==== __Example__+@+ghci> import qualified Data.Vector as V++ghci> df = insertVector "numbers" (V.fromList [1..10]) D.empty++ghci> D.rename "numbers" "others" df++--------------+index | others+------|-------+ Int | Int+------|-------+0 | 1+1 | 2+2 | 3+3 | 4+4 | 5+5 | 6+6 | 7+7 | 8+8 | 9+9 | 10++@+-} rename :: T.Text -> T.Text -> DataFrame -> DataFrame rename orig new df = either throw id (renameSafe orig new df) +{- | /O(n)/ Renames many columns.++==== __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++------------------------+index | numbers | others+------|---------|-------+ Int | Int | Int+------|---------|-------+0 | 1 | 11+1 | 2 | 12+2 | 3 | 13+3 | 4 | 14+4 | 5 | 15+5 | 6 | 16+6 | 7 | 17+7 | 8 | 18+8 | 9 | 19+9 | 10 | 20++ghci> D.renameMany [("numbers", "first_10"), ("others", "next_10")] df++--------------------------+index | first_10 | next_10+------|----------|--------+ Int | Int | Int+------|----------|--------+0 | 1 | 11+1 | 2 | 12+2 | 3 | 13+3 | 4 | 14+4 | 5 | 15+5 | 6 | 16+6 | 7 | 17+7 | 8 | 18+8 | 9 | 19+9 | 10 | 20++@+-} renameMany :: [(T.Text, T.Text)] -> DataFrame -> DataFrame renameMany replacements df = fold (uncurry rename) replacements df renameSafe :: T.Text -> T.Text -> DataFrame -> Either DataFrameException DataFrame renameSafe orig new df = fromMaybe (Left $ ColumnNotFoundException orig "rename" (map fst $ M.toList $ columnIndices df)) $ do- columnIndex <- M.lookup orig (columnIndices df)- let origRemoved = M.delete orig (columnIndices df)- let newAdded = M.insert new columnIndex origRemoved- return (Right df { columnIndices = newAdded })+ columnIndex <- M.lookup orig (columnIndices df)+ let origRemoved = M.delete orig (columnIndices df)+ let newAdded = M.insert new columnIndex origRemoved+ return (Right df{columnIndices = newAdded}) --- | O(1) Get the number of elements in a given column.-columnSize :: T.Text -> DataFrame -> Maybe Int-columnSize name df = columnLength <$> getColumn name df+data ColumnInfo = ColumnInfo+ { nameOfColumn :: !T.Text+ , nonNullValues :: !Int+ , nullValues :: !Int+ , partiallyParsedValues :: !Int+ , uniqueValues :: !Int+ , typeOfColumn :: !T.Text+ } -data ColumnInfo = ColumnInfo {- nameOfColumn :: !T.Text,- nonNullValues :: !Int,- nullValues :: !Int,- partiallyParsedValues :: !Int,- uniqueValues :: !Int,- typeOfColumn :: !T.Text- }+{- | O(n * k ^ 2) Returns the number of non-null columns in the dataframe and the type associated with each column. --- | O(n) Returns the number of non-null columns in the dataframe and the type associated--- with each column.+==== __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++-----------------------------------------------------------------------------------------------------+index | Column Name | # Non-null Values | # Null Values | # Partially parsed | # Unique Values | Type+------|-------------|-------------------|---------------|--------------------|-----------------|-----+ Int | Text | Int | Int | Int | Int | Text+------|-------------|-------------------|---------------|--------------------|-----------------|-----+0 | others | 10 | 0 | 0 | 10 | Int+1 | numbers | 10 | 0 | 0 | 10 | Int++@+-} describeColumns :: DataFrame -> DataFrame-describeColumns df = empty & insertColumn "Column Name" (fromList (map nameOfColumn infos))- & insertColumn "# Non-null Values" (fromList (map nonNullValues infos))- & insertColumn "# Null Values" (fromList (map nullValues infos))- & insertColumn "# Partially parsed" (fromList (map partiallyParsedValues infos))- & insertColumn "# Unique Values" (fromList (map uniqueValues infos))- & insertColumn "Type" (fromList (map typeOfColumn infos))+describeColumns df =+ empty+ & insertColumn "Column Name" (fromList (map nameOfColumn infos))+ & insertColumn "# Non-null Values" (fromList (map nonNullValues infos))+ & insertColumn "# Null Values" (fromList (map nullValues infos))+ & insertColumn "# Partially parsed" (fromList (map partiallyParsedValues infos))+ & insertColumn "# Unique Values" (fromList (map uniqueValues infos))+ & insertColumn "Type" (fromList (map typeOfColumn infos)) where infos = L.sortBy (compare `on` nonNullValues) (V.ifoldl' go [] (columns df)) :: [ColumnInfo] indexMap = M.fromList (map (\(a, b) -> (b, a)) $ M.toList (columnIndices df)) columnName i = M.lookup i indexMap- go acc i col@(OptionalColumn (c :: V.Vector a)) = let- cname = columnName i- countNulls = nulls col- countPartial = partiallyParsed col- columnType = T.pack $ show $ typeRep @a- unique = S.size $ VG.foldr S.insert S.empty c- in if isNothing cname then acc else ColumnInfo (fromMaybe "" cname) (columnLength col - countNulls) countNulls countPartial unique columnType : acc- go acc i col@(BoxedColumn (c :: V.Vector a)) = let- cname = columnName i- countPartial = partiallyParsed col- columnType = T.pack $ show $ typeRep @a- unique = S.size $ VG.foldr S.insert S.empty c- in if isNothing cname then acc else ColumnInfo (fromMaybe "" cname) (columnLength col) 0 countPartial unique columnType : acc- go acc i col@(UnboxedColumn c) = let- cname = columnName i- columnType = T.pack $ columnTypeString col- unique = S.size $ VG.foldr S.insert S.empty c- -- Unboxed columns cannot have nulls since Maybe- -- is not an instance of Unbox a- in if isNothing cname then acc else ColumnInfo (fromMaybe "" cname) (columnLength col) 0 0 unique columnType : acc+ go acc i col@(OptionalColumn (c :: V.Vector a)) =+ let+ cname = columnName i+ countNulls = nulls col+ countPartial = partiallyParsed col+ columnType = T.pack $ show $ typeRep @a+ unique = S.size $ VG.foldr S.insert S.empty c+ in+ if isNothing cname then acc else ColumnInfo (fromMaybe "" cname) (columnLength col - countNulls) countNulls countPartial unique columnType : acc+ go acc i col@(BoxedColumn (c :: V.Vector a)) =+ let+ cname = columnName i+ countPartial = partiallyParsed col+ columnType = T.pack $ show $ typeRep @a+ unique = S.size $ VG.foldr S.insert S.empty c+ in+ if isNothing cname then acc else ColumnInfo (fromMaybe "" cname) (columnLength col) 0 countPartial unique columnType : acc+ go acc i col@(UnboxedColumn c) =+ let+ cname = columnName i+ columnType = T.pack $ columnTypeString col+ unique = S.size $ VG.foldr S.insert S.empty c+ in+ -- Unboxed columns cannot have nulls since Maybe+ -- is not an instance of Unbox a+ if isNothing cname then acc else ColumnInfo (fromMaybe "" cname) (columnLength col) 0 0 unique columnType : acc nulls :: Column -> Int nulls (OptionalColumn xs) = VG.length $ VG.filter isNothing xs nulls (BoxedColumn (xs :: V.Vector a)) = case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl -> VG.length $ VG.filter isNullish xs- Nothing -> case testEquality (typeRep @a) (typeRep @String) of- Just Refl -> VG.length $ VG.filter (isNullish . T.pack) xs- Nothing -> case typeRep @a of- App t1 t2 -> case eqTypeRep t1 (typeRep @Maybe) of- Just HRefl -> VG.length $ VG.filter isNothing xs- Nothing -> 0- _ -> 0+ Just Refl -> VG.length $ VG.filter isNullish xs+ Nothing -> case testEquality (typeRep @a) (typeRep @String) of+ Just Refl -> VG.length $ VG.filter (isNullish . T.pack) xs+ Nothing -> case typeRep @a of+ App t1 t2 -> case eqTypeRep t1 (typeRep @Maybe) of+ Just HRefl -> VG.length $ VG.filter isNothing xs+ Nothing -> 0+ _ -> 0 nulls _ = 0 partiallyParsed :: Column -> Int partiallyParsed (BoxedColumn (xs :: V.Vector a)) =- case typeRep @a of- App (App tycon t1) t2 -> case eqTypeRep tycon (typeRep @Either) of- Just HRefl -> VG.length $ VG.filter isLeft xs- Nothing -> 0- _ -> 0+ case typeRep @a of+ App (App tycon t1) t2 -> case eqTypeRep tycon (typeRep @Either) of+ Just HRefl -> VG.length $ VG.filter isLeft xs+ Nothing -> 0+ _ -> 0 partiallyParsed _ = 0 +{- | Creates a dataframe from a list of tuples with name and column.++==== __Example__+@+ghci> df = D.fromNamedColumns [("numbers", D.fromList [1..10]), ("others", D.fromList [11..20])]++ghci> df++------------------------+index | numbers | others+------|---------|-------+ Int | Int | Int+------|---------|-------+0 | 1 | 11+1 | 2 | 12+2 | 3 | 13+3 | 4 | 14+4 | 5 | 15+5 | 6 | 16+6 | 7 | 17+7 | 8 | 18+8 | 9 | 19+9 | 10 | 20++@+-} fromNamedColumns :: [(T.Text, Column)] -> DataFrame fromNamedColumns = L.foldl' (\df (name, column) -> insertColumn name column df) empty +{- | Create a dataframe from a list of columns. The column names are "0", "1"... etc.+Useful for quick exploration but you should probably alwyas rename the columns after+or drop the ones you don't want.++==== __Example__+@+ghci> df = D.fromUnnamedColumns [D.fromList [1..10], D.fromList [11..20]]++ghci> df++-----------------+index | 0 | 1+------|-----|----+ Int | Int | Int+------|-----|----+0 | 1 | 11+1 | 2 | 12+2 | 3 | 13+3 | 4 | 14+4 | 5 | 15+5 | 6 | 16+6 | 7 | 17+7 | 8 | 18+8 | 9 | 19+9 | 10 | 20++@+-} fromUnnamedColumns :: [Column] -> DataFrame-fromUnnamedColumns = fromNamedColumns . zip (map (T.pack . show) [0..])+fromUnnamedColumns = fromNamedColumns . zip (map (T.pack . show) [0 ..]) --- | O (k * n) Counts the occurences of each value in a given column.+{- | O (k * n) Counts the occurences of each value in a given column.++==== __Example__+@+ghci> df = D.fromUnnamedColumns [D.fromList [1..10], D.fromList [11..20]]++ghci> 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)]++@+-} valueCounts :: forall a. (Columnable a) => T.Text -> DataFrame -> [(a, Int)] valueCounts columnName df = case getColumn columnName df of- Nothing -> throw $ ColumnNotFoundException columnName "valueCounts" (map fst $ M.toList $ columnIndices df)- Just (BoxedColumn (column' :: V.Vector c)) ->+ Nothing -> throw $ ColumnNotFoundException columnName "valueCounts" (map fst $ M.toList $ columnIndices df)+ Just (BoxedColumn (column' :: V.Vector c)) -> let- column = V.foldl' (\m v -> MS.insertWith (+) v (1 :: Int) m) M.empty column'- in case (typeRep @a) `testEquality` (typeRep @c) of- Nothing -> throw $ TypeMismatchException (MkTypeErrorContext- { userType = Right $ typeRep @a- , expectedType = Right $ typeRep @c- , errorColumnName = Just (T.unpack columnName)- , callingFunctionName = Just "valueCounts"- })- Just Refl -> M.toAscList column- Just (OptionalColumn (column' :: V.Vector c)) ->+ column = V.foldl' (\m v -> MS.insertWith (+) v (1 :: Int) m) M.empty column'+ in+ case (typeRep @a) `testEquality` (typeRep @c) of+ Nothing ->+ throw $+ TypeMismatchException+ ( MkTypeErrorContext+ { userType = Right $ typeRep @a+ , expectedType = Right $ typeRep @c+ , errorColumnName = Just (T.unpack columnName)+ , callingFunctionName = Just "valueCounts"+ }+ )+ Just Refl -> M.toAscList column+ Just (OptionalColumn (column' :: V.Vector c)) -> let- column = V.foldl' (\m v -> MS.insertWith (+) v (1 :: Int) m) M.empty column'- in case (typeRep @a) `testEquality` (typeRep @c) of- Nothing -> throw $ TypeMismatchException (MkTypeErrorContext- { userType = Right $ typeRep @a- , expectedType = Right $ typeRep @c- , errorColumnName = Just (T.unpack columnName)- , callingFunctionName = Just "valueCounts"- })- Just Refl -> M.toAscList column- Just (UnboxedColumn (column' :: VU.Vector c)) -> let- column = V.foldl' (\m v -> MS.insertWith (+) v (1 :: Int) m) M.empty (V.convert column')- in case (typeRep @a) `testEquality` (typeRep @c) of- Nothing -> throw $ TypeMismatchException (MkTypeErrorContext- { userType = Right $ typeRep @a- , expectedType = Right $ typeRep @c- , errorColumnName = Just (T.unpack columnName)- , callingFunctionName = Just "valueCounts"- })- Just Refl -> M.toAscList column+ column = V.foldl' (\m v -> MS.insertWith (+) v (1 :: Int) m) M.empty column'+ in+ case (typeRep @a) `testEquality` (typeRep @c) of+ Nothing ->+ throw $+ TypeMismatchException+ ( MkTypeErrorContext+ { userType = Right $ typeRep @a+ , expectedType = Right $ typeRep @c+ , errorColumnName = Just (T.unpack columnName)+ , callingFunctionName = Just "valueCounts"+ }+ )+ Just Refl -> M.toAscList column+ Just (UnboxedColumn (column' :: VU.Vector c)) ->+ let+ column = V.foldl' (\m v -> MS.insertWith (+) v (1 :: Int) m) M.empty (V.convert column')+ in+ case (typeRep @a) `testEquality` (typeRep @c) of+ Nothing ->+ throw $+ TypeMismatchException+ ( MkTypeErrorContext+ { userType = Right $ typeRep @a+ , expectedType = Right $ typeRep @c+ , errorColumnName = Just (T.unpack columnName)+ , callingFunctionName = Just "valueCounts"+ }+ )+ Just Refl -> M.toAscList column +{- | A left fold for dataframes that takes the dataframe as the last object.+this makes it easier to chain operations.++==== __Example__+@+ghci> D.fold (const id) [1..5] df++-----------------+index | 0 | 1+------|-----|----+ Int | Int | Int+------|-----|----+0 | 1 | 11+1 | 2 | 12+2 | 3 | 13+3 | 4 | 14+4 | 5 | 15+5 | 6 | 16+6 | 7 | 17+7 | 8 | 18+8 | 9 | 19+9 | 10 | 20++@+-} fold :: (a -> DataFrame -> DataFrame) -> [a] -> DataFrame -> DataFrame fold f xs acc = L.foldl' (flip f) acc xs
src/DataFrame/Operations/Join.hs view
@@ -1,42 +1,52 @@ {-# LANGUAGE OverloadedStrings #-}+ module DataFrame.Operations.Join where import qualified Data.Map.Strict as M+import qualified Data.Text as T import qualified Data.Vector as VB import qualified Data.Vector.Unboxed as VU-import qualified Data.Text as T-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 DataFrame.Internal.Column as D+import DataFrame.Internal.DataFrame as D+import DataFrame.Operations.Aggregation as D+import DataFrame.Operations.Core as D -data JoinType = INNER- | LEFT- | RIGHT- | FULL_OUTER+-- | Equivalent to SQL join types.+data JoinType+ = INNER+ | LEFT+ | RIGHT+ | FULL_OUTER -join :: JoinType- -> [T.Text]- -> DataFrame -- Right hand side- -> DataFrame -- Left hand side- -> DataFrame+{- | Join two dataframes using SQL join semantics.++Only inner join is implemented for now.+-}+join ::+ JoinType ->+ [T.Text] ->+ DataFrame -> -- Right hand side+ DataFrame -> -- Left hand side+ DataFrame join INNER xs right = innerJoin xs right join LEFT xs right = error "UNIMPLEMENTED" join RIGHT xs right = error "UNIMPLEMENTED" join FULL_OUTER xs right = error "UNIMPLEMENTED" --- Create row representation for each of the two dataframes--- get the product of left and right counts for each key+{- | Inner join of two dataframes. Note: for chaining, the left dataframe is actually+on the right side.+-} innerJoin :: [T.Text] -> DataFrame -> DataFrame -> DataFrame-innerJoin cs right left = let+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)+ 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)+ 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)]@@ -46,13 +56,14 @@ expandedLeftIndicies = mconcat (map fst expandedIndices) expandedRightIndicies = mconcat (map snd expandedIndices) -- df- expandedLeft = left { columns = VB.map (D.atIndicesStable expandedLeftIndicies) (D.columns left), dataframeDimensions = (VU.length expandedLeftIndicies, snd (D.dataframeDimensions left))}- -- df - expandedRight = right { columns = VB.map (D.atIndicesStable expandedRightIndicies) (D.columns right), dataframeDimensions = (VU.length expandedRightIndicies, snd (D.dataframeDimensions right))}+ expandedLeft = left{columns = VB.map (D.atIndicesStable expandedLeftIndicies) (D.columns left), dataframeDimensions = (VU.length expandedLeftIndicies, snd (D.dataframeDimensions left))}+ -- df+ expandedRight = right{columns = VB.map (D.atIndicesStable expandedRightIndicies) (D.columns right), dataframeDimensions = (VU.length expandedRightIndicies, snd (D.dataframeDimensions right))} -- [string] leftColumns = D.columnNames left- rightColumns = D.columnNames right + rightColumns = D.columnNames right initDf = expandedLeft insertIfPresent _ Nothing df = df insertIfPresent name (Just c) df = D.insertColumn name c df- in D.fold (\name df -> if name `elem` cs then df else (if name `elem` leftColumns then insertIfPresent ("Right_" <> name) (D.getColumn name expandedRight) df else insertIfPresent name (D.getColumn name expandedRight) df)) rightColumns initDf+ in+ D.fold (\name df -> if name `elem` cs then df 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/Merge.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE InstanceSigs #-}+ module DataFrame.Operations.Merge where import qualified Data.List as L@@ -11,8 +12,10 @@ import Data.Maybe instance Semigroup D.DataFrame where+ -- \| Vertically merge two dataframes using shared columns. (<>) :: D.DataFrame -> D.DataFrame -> D.DataFrame- (<>) a b = let+ (<>) a b =+ let columnsInBOnly = filter (\c -> c `notElem` D.columnNames b) (D.columnNames b) columnsInA = D.columnNames a addColumns a' b' df name@@ -23,25 +26,29 @@ | fst (D.dimensions b') == 0 = fromMaybe df $ do col <- D.getColumn name a' pure $ D.insertColumn name col df- | otherwise = let+ | otherwise =+ let numColumnsA = (fst $ D.dimensions a') numColumnsB = (fst $ D.dimensions b') numColumns = max numColumnsA numColumnsB optA = D.getColumn name a' optB = D.getColumn name b'- in case optB of- Nothing -> case optA of- Nothing -> D.insertColumn name (D.fromList ([] :: [T.Text])) df- Just a'' -> D.insertColumn name (D.expandColumn numColumnsB a'') df- Just b'' -> case optA of- Nothing -> D.insertColumn name (D.leftExpandColumn numColumnsA b'') df- Just a'' -> fromMaybe df $ do- concatedColumns <- D.concatColumns a'' b''- pure $ D.insertColumn name concatedColumns df- in L.foldl' (addColumns a b) D.empty (D.columnNames a `L.union` D.columnNames b)+ in+ case optB of+ Nothing -> case optA of+ Nothing -> D.insertColumn name (D.fromList ([] :: [T.Text])) df+ Just a'' -> D.insertColumn name (D.expandColumn numColumnsB a'') df+ Just b'' -> case optA of+ Nothing -> D.insertColumn name (D.leftExpandColumn numColumnsA b'') df+ Just a'' -> fromMaybe df $ do+ concatedColumns <- D.concatColumns a'' b''+ pure $ D.insertColumn name concatedColumns df+ in+ L.foldl' (addColumns a b) D.empty (D.columnNames a `L.union` D.columnNames b) instance Monoid D.DataFrame where- mempty = D.empty+ mempty = D.empty +-- | Add two dataframes side by side/horizontally. (|||) :: D.DataFrame -> D.DataFrame -> D.DataFrame (|||) a b = D.fold (\name acc -> D.insertColumn name (D.unsafeGetColumn name b) acc) (D.columnNames b) a
src/DataFrame/Operations/Sorting.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}+ module DataFrame.Operations.Sorting where import qualified Data.List as L@@ -6,28 +7,31 @@ import qualified Data.Vector as V import Control.Exception (throw)-import DataFrame.Errors (DataFrameException(..))+import DataFrame.Errors (DataFrameException (..)) import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (DataFrame(..), getColumn)+import DataFrame.Internal.DataFrame (DataFrame (..), getColumn) import DataFrame.Internal.Row import DataFrame.Operations.Core -- | Sort order taken as a parameter by the sortby function. data SortOrder = Ascending | Descending deriving (Eq) --- | O(k log n) Sorts the dataframe by a given row.------ > sortBy "Age" df+{- | O(k log n) Sorts the dataframe by a given row.++> sortBy "Age" df+-} sortBy ::- SortOrder ->- [T.Text] ->- DataFrame ->- DataFrame+ SortOrder ->+ [T.Text] ->+ DataFrame ->+ DataFrame sortBy order names df- | any (`notElem` columnNames df) names = throw $ ColumnNotFoundException (T.pack $ show $ names L.\\ columnNames df) "sortBy" (columnNames df)- | otherwise = let- -- TODO: Remove the SortOrder defintion from operations so we can share it between here and internal and- -- we don't have to do this Bool mapping.- indexes = sortedIndexes' (order == Ascending) (toRowVector names df)- pick idxs col = atIndicesStable idxs col- in df {columns = V.map (pick indexes) (columns df)}+ | any (`notElem` columnNames df) names = throw $ ColumnNotFoundException (T.pack $ show $ names L.\\ columnNames df) "sortBy" (columnNames df)+ | otherwise =+ let+ -- TODO: Remove the SortOrder defintion from operations so we can share it between here and internal and+ -- we don't have to do this Bool mapping.+ indexes = sortedIndexes' (order == Ascending) (toRowVector names df)+ pick idxs col = atIndicesStable idxs col+ in+ df{columns = V.map (pick indexes) (columns df)}
src/DataFrame/Operations/Statistics.hs view
@@ -1,171 +1,257 @@-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+ module DataFrame.Operations.Statistics where import Data.Bifunctor (second) import qualified Data.List as L import qualified Data.Map as M import qualified Data.Text as T-import qualified Data.Vector.Generic as VG import qualified Data.Vector as V+import qualified Data.Vector.Algorithms.Intro as VA+import qualified Data.Vector.Generic as VG import qualified Data.Vector.Unboxed as VU+import qualified Data.Vector.Unboxed.Mutable as VUM import qualified Statistics.Quantile as SS import qualified Statistics.Sample as SS import Prelude as P import Control.Exception (throw)-import DataFrame.Errors (DataFrameException(..))+import Control.Monad.ST (runST)+import qualified Data.Bifunctor as Data+import Data.Foldable (asum)+import Data.Function ((&))+import Data.Maybe (fromMaybe, isJust)+import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))+import DataFrame.Errors (DataFrameException (..)) import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (DataFrame(..), getColumn, empty, unsafeGetColumn)+import DataFrame.Internal.DataFrame (DataFrame (..), empty, getColumn, unsafeGetColumn)+import DataFrame.Internal.Row (showValue, toAny) import DataFrame.Operations.Core import DataFrame.Operations.Subset (filterJust)-import Data.Foldable (asum)-import Data.Maybe (isJust, fromMaybe)-import Data.Function ((&))-import Data.Type.Equality (type (:~:)(Refl), TestEquality (testEquality))-import Type.Reflection (typeRep)-import qualified Data.Bifunctor as Data-import DataFrame.Internal.Row (toAny, showValue) import GHC.Float (int2Double) import Text.Printf (printf)+import Type.Reflection (typeRep) +{- | Show a frequency table for a categorical feaure.++__Examples:__++@+ghci> df <- D.readCsv "./data/housing.csv"++ghci> D.frequencies "ocean_proximity" df++----------------------------------------------------------------------------+index | Statistic | <1H OCEAN | INLAND | ISLAND | NEAR BAY | NEAR OCEAN+------|----------------|-----------|--------|--------|----------|-----------+ Int | Text | Any | Any | Any | Any | Any+------|----------------|-----------|--------|--------|----------|-----------+0 | Count | 9136 | 6551 | 5 | 2290 | 2658+1 | Percentage (%) | 44.26% | 31.74% | 0.02% | 11.09% | 12.88%+@+-} frequencies :: T.Text -> DataFrame -> DataFrame-frequencies name df = let- counts :: forall a . Columnable a => [(a, Int)]- counts = valueCounts name df- calculatePercentage cs k = toAny $ toPct2dp (fromIntegral k / fromIntegral (P.sum $ map snd cs))- initDf = empty & insertVector "Statistic" (V.fromList ["Count" :: T.Text, "Percentage (%)"])- freqs :: forall v a . (VG.Vector v a, Columnable a) => v a -> DataFrame- freqs col = L.foldl' (\d (col, k) -> insertVector (showValue @a col) (V.fromList [toAny k, calculatePercentage (counts @a) k]) d) initDf counts- in case getColumn name df of- Nothing -> throw $ ColumnNotFoundException name "frequencies" (map fst $ M.toList $ columnIndices df)- Just ((BoxedColumn (column :: V.Vector a))) -> freqs column- Just ((OptionalColumn (column :: V.Vector a))) -> freqs column- Just ((UnboxedColumn (column :: VU.Vector a))) -> freqs column+frequencies name df =+ let+ counts :: forall a. (Columnable a) => [(a, Int)]+ counts = valueCounts name df+ calculatePercentage cs k = toAny $ toPct2dp (fromIntegral k / fromIntegral (P.sum $ map snd cs))+ initDf = empty & insertVector "Statistic" (V.fromList ["Count" :: T.Text, "Percentage (%)"])+ freqs :: forall v a. (VG.Vector v a, Columnable a) => v a -> DataFrame+ freqs col = L.foldl' (\d (col, k) -> insertVector (showValue @a col) (V.fromList [toAny k, calculatePercentage (counts @a) k]) d) initDf counts+ in+ case getColumn name df of+ Nothing -> throw $ ColumnNotFoundException name "frequencies" (map fst $ M.toList $ columnIndices df)+ Just ((BoxedColumn (column :: V.Vector a))) -> freqs column+ Just ((OptionalColumn (column :: V.Vector a))) -> freqs column+ Just ((UnboxedColumn (column :: VU.Vector a))) -> freqs column +-- | Calculates the mean of a given column as a standalone value. mean :: T.Text -> DataFrame -> Maybe Double mean = applyStatistic mean' +-- | Calculates the median of a given column as a standalone value. median :: T.Text -> DataFrame -> Maybe Double-median = applyStatistic (SS.median SS.medianUnbiased)+median = applyStatistic median' +-- | Calculates the standard deviation of a given column as a standalone value. standardDeviation :: T.Text -> DataFrame -> Maybe Double-standardDeviation = applyStatistic SS.fastStdDev+standardDeviation = applyStatistic (sqrt . variance') +-- | Calculates the skewness of a given column as a standalone value. skewness :: T.Text -> DataFrame -> Maybe Double skewness = applyStatistic SS.skewness +-- | Calculates the variance of a given column as a standalone value. variance :: T.Text -> DataFrame -> Maybe Double variance = applyStatistic variance' +-- | Calculates the inter-quartile range of a given column as a standalone value. interQuartileRange :: T.Text -> DataFrame -> Maybe Double interQuartileRange = applyStatistic (SS.midspread SS.medianUnbiased 4) +-- | Calculates the Pearson's correlation coefficient between two given columns as a standalone value. correlation :: T.Text -> T.Text -> DataFrame -> Maybe Double correlation first second df = do- f <- _getColumnAsDouble first df- s <- _getColumnAsDouble second df- return $ SS.correlation (VG.zip f s)+ f <- _getColumnAsDouble first df+ s <- _getColumnAsDouble second df+ correlation' f s _getColumnAsDouble :: T.Text -> DataFrame -> Maybe (VU.Vector Double) _getColumnAsDouble name df = case getColumn name df of- Just (UnboxedColumn (f :: VU.Vector a)) -> case testEquality (typeRep @a) (typeRep @Double) of- Just Refl -> Just f- Nothing -> case testEquality (typeRep @a) (typeRep @Int) of- Just Refl -> Just $ VU.map fromIntegral f- Nothing -> Nothing- _ -> Nothing+ Just (UnboxedColumn (f :: VU.Vector a)) -> case testEquality (typeRep @a) (typeRep @Double) of+ Just Refl -> Just f+ Nothing -> case testEquality (typeRep @a) (typeRep @Int) of+ Just Refl -> Just $ VU.map fromIntegral f+ Nothing -> case testEquality (typeRep @a) (typeRep @Float) of+ Just Refl -> Just $ VU.map realToFrac f+ Nothing -> Nothing+ _ -> Nothing+{-# INLINE _getColumnAsDouble #-} +-- | Calculates the sum of a given column as a standalone value. sum :: forall a. (Columnable a, Num a, VU.Unbox a) => T.Text -> DataFrame -> Maybe a sum name df = case getColumn name df of- Nothing -> throw $ ColumnNotFoundException name "sum" (map fst $ M.toList $ columnIndices df)- Just ((UnboxedColumn (column :: VU.Vector a'))) -> case testEquality (typeRep @a') (typeRep @a) of- Just Refl -> Just $ VG.sum column- Nothing -> Nothing+ Nothing -> throw $ ColumnNotFoundException name "sum" (map fst $ M.toList $ columnIndices df)+ Just ((UnboxedColumn (column :: VU.Vector a'))) -> case testEquality (typeRep @a') (typeRep @a) of+ Just Refl -> Just $ VG.sum column+ Nothing -> Nothing applyStatistic :: (VU.Vector Double -> Double) -> T.Text -> DataFrame -> Maybe Double applyStatistic f name df = case getColumn name (filterJust name df) of- Nothing -> throw $ ColumnNotFoundException name "applyStatistic" (map fst $ M.toList $ columnIndices df)- Just column@(UnboxedColumn (col :: VU.Vector a)) -> case testEquality (typeRep @a) (typeRep @Double) of- Just Refl -> reduceColumn f column+ Nothing -> throw $ ColumnNotFoundException name "applyStatistic" (map fst $ M.toList $ columnIndices df)+ Just column@(UnboxedColumn (col :: VU.Vector a)) -> case testEquality (typeRep @a) (typeRep @Double) of+ Just Refl ->+ let+ res = (f col)+ in+ if isNaN res then Nothing else pure res Nothing -> do- matching <- asum [mapColumn (fromIntegral :: Int -> Double) column,- mapColumn (fromIntegral :: Integer -> Double) column,- mapColumn (realToFrac :: Float -> Double) column,- Just column ]- reduceColumn f matching- _ -> Nothing+ col' <- _getColumnAsDouble name df+ let res = (f col')+ if isNaN res then Nothing else pure res+ _ -> Nothing+{-# INLINE applyStatistic #-} applyStatistics :: (VU.Vector Double -> VU.Vector Double) -> T.Text -> DataFrame -> Maybe (VU.Vector Double) applyStatistics f name df = case getColumn name (filterJust name df) of- Just ((UnboxedColumn (column :: VU.Vector a'))) -> case testEquality (typeRep @a') (typeRep @Int) of- Just Refl -> Just $! f (VU.map fromIntegral column)- Nothing -> case testEquality (typeRep @a') (typeRep @Double) of- Just Refl -> Just $! f column- Nothing -> case testEquality (typeRep @a') (typeRep @Float) of- Just Refl -> Just $! f (VG.map realToFrac column)- Nothing -> Nothing- _ -> Nothing+ Just ((UnboxedColumn (column :: VU.Vector a'))) -> case testEquality (typeRep @a') (typeRep @Int) of+ Just Refl -> Just $! f (VU.map fromIntegral column)+ Nothing -> case testEquality (typeRep @a') (typeRep @Double) of+ Just Refl -> Just $! f column+ Nothing -> case testEquality (typeRep @a') (typeRep @Float) of+ Just Refl -> Just $! f (VG.map realToFrac column)+ Nothing -> Nothing+ _ -> Nothing +-- | Descriprive statistics of the numeric columns. summarize :: DataFrame -> DataFrame-summarize df = fold columnStats (columnNames df) (fromNamedColumns [("Statistic", fromList ["Count" :: T.Text, "Mean", "Minimum", "25%" ,"Median", "75%", "Max", "StdDev", "IQR", "Skewness"])])- where columnStats name d = if all isJust (stats name) then insertUnboxedVector name (VU.fromList (map (roundTo 2 . fromMaybe 0) $ stats name)) d else d- stats name = let+summarize df = fold columnStats (columnNames df) (fromNamedColumns [("Statistic", fromList ["Count" :: T.Text, "Mean", "Minimum", "25%", "Median", "75%", "Max", "StdDev", "IQR", "Skewness"])])+ where+ columnStats name d = if all isJust (stats name) then insertUnboxedVector name (VU.fromList (map (roundTo 2 . fromMaybe 0) $ stats name)) d else d+ stats name =+ let count = fromIntegral . numElements <$> getColumn name df- quantiles = applyStatistics (SS.quantilesVec SS.medianUnbiased (VU.fromList [0,1,2,3,4]) 4) name df+ quantiles = applyStatistics (SS.quantilesVec SS.medianUnbiased (VU.fromList [0, 1, 2, 3, 4]) 4) name df min' = flip (VG.!) 0 <$> quantiles quartile1 = flip (VG.!) 1 <$> quantiles median' = flip (VG.!) 2 <$> quantiles quartile3 = flip (VG.!) 3 <$> quantiles max' = flip (VG.!) 4 <$> quantiles iqr = (-) <$> quartile3 <*> quartile1- in [count,- mean name df,- min',- quartile1,- median',- quartile3,- max',- standardDeviation name df,- iqr,- skewness name df]+ in+ [ count+ , mean name df+ , min'+ , quartile1+ , median'+ , quartile3+ , max'+ , standardDeviation name df+ , iqr+ , skewness name df+ ] -- | Round a @Double@ to Specified Precision roundTo :: Int -> Double -> Double-roundTo n x = fromInteger (round $ x * (10^n)) / (10.0^^n)+roundTo n x = fromInteger (round $ x * (10 ^ n)) / (10.0 ^^ n) toPct2dp :: Double -> String toPct2dp x- | x < 0.00005 = "<0.01%"- | otherwise = printf "%.2f%%" (x * 100)+ | x < 0.00005 = "<0.01%"+ | otherwise = printf "%.2f%%" (x * 100) mean' :: VU.Vector Double -> Double-mean' samp = let- (!total, !n) = VG.foldl' (\(!total, !n) v -> (total + v, n + 1)) (0 :: Double, 0 :: Int) samp- in total / fromIntegral n+mean' samp = VU.sum samp / fromIntegral (VU.length samp)+{-# INLINE mean #-} +median' :: VU.Vector Double -> Double+median' samp+ | VU.null samp = throw $ EmptyDataSetException "median"+ | otherwise = runST $ do+ mutableSamp <- VU.thaw samp+ VA.sort mutableSamp+ let len = VU.length samp+ middleIndex = len `div` 2+ middleElement <- VUM.read mutableSamp middleIndex+ if odd len+ then pure middleElement+ else do+ prev <- VUM.read mutableSamp (middleIndex - 1)+ pure ((middleElement + prev) / 2)+{-# INLINE median' #-}+ -- accumulator: count, mean, m2-data VarAcc = VarAcc !Int !Double !Double deriving Show+data VarAcc = VarAcc !Int !Double !Double deriving (Show) step :: VarAcc -> Double -> VarAcc step (VarAcc !n !mean !m2) !x =- let !n' = n + 1- !delta = x - mean- !mean' = mean + delta / fromIntegral n'- !m2' = m2 + delta * (x - mean')- in VarAcc n' mean' m2'+ let !n' = n + 1+ !delta = x - mean+ !mean' = mean + delta / fromIntegral n'+ !m2' = m2 + delta * (x - mean')+ in VarAcc n' mean' m2'+{-# INLINE step #-} computeVariance :: VarAcc -> Double-computeVariance (VarAcc n _ m2)- | n < 2 = 0 -- or error "variance of <2 samples"- | otherwise = m2 / fromIntegral (n - 1)+computeVariance (VarAcc !n _ !m2)+ | n < 2 = 0 -- or error "variance of <2 samples"+ | otherwise = m2 / fromIntegral (n - 1)+{-# INLINE computeVariance #-} variance' :: VU.Vector Double -> Double-variance' = computeVariance . VG.foldl' step (VarAcc 0 0 0)+variance' = computeVariance . VU.foldl' step (VarAcc 0 0 0)+{-# INLINE variance' #-}++correlation' :: VU.Vector Double -> VU.Vector Double -> Maybe Double+correlation' xs ys+ | VU.length xs /= VU.length ys = Nothing+ | nI < 2 = Nothing+ | otherwise =+ let !nf = fromIntegral nI+ (!sumX, !sumY, !sumSquaredX, !sumSquaredY, !sumXY) = go 0 0 0 0 0 0+ !num = nf * sumXY - sumX * sumY+ !den = sqrt ((nf * sumSquaredX - sumX * sumX) * (nf * sumSquaredY - sumY * sumY))+ in pure (num / den)+ where+ !nI = VU.length xs+ go !i !sumX !sumY !sumSquaredX !sumSquaredY !sumXY+ | i < nI =+ let !x = VU.unsafeIndex xs i+ !y = VU.unsafeIndex ys i+ !sumX' = sumX + x+ !sumY' = sumY + y+ !sumSquaredX' = sumSquaredX + x * x+ !sumSquaredY' = sumSquaredY + y * y+ !sumXY' = sumXY + x * y+ in go (i + 1) sumX' sumY' sumSquaredX' sumSquaredY' sumXY'+ | otherwise = (sumX, sumY, sumSquaredX, sumSquaredY, sumXY)+{-# INLINE correlation' #-}
src/DataFrame/Operations/Subset.hs view
@@ -1,11 +1,12 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExplicitNamespaces #-}+ module DataFrame.Operations.Subset where import qualified Data.List as L@@ -13,54 +14,57 @@ import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Vector as V+import qualified Data.Vector.Generic as VG import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Unboxed.Mutable as VUM-import qualified Data.Vector.Generic as VG import qualified Prelude import Control.Exception (throw)-import DataFrame.Errors (DataFrameException(..), TypeErrorContext(..))+import Control.Monad.ST+import Data.Function ((&))+import Data.Maybe (fromJust, fromMaybe, isJust)+import Data.Type.Equality (TestEquality (..), type (:~:) (Refl))+import DataFrame.Errors (DataFrameException (..), TypeErrorContext (..)) import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (DataFrame(..), getColumn, empty)+import DataFrame.Internal.DataFrame (DataFrame (..), empty, getColumn) import DataFrame.Internal.Expression-import DataFrame.Internal.Row (mkRowFromArgs, Any, toAny)-import Data.Type.Equality (type (:~:)(Refl), TestEquality (..))+import DataFrame.Internal.Row (Any, mkRowFromArgs, toAny) import DataFrame.Operations.Core import DataFrame.Operations.Transformations (apply)-import Data.Function ((&))-import Data.Maybe (isJust, fromJust, fromMaybe)-import Prelude hiding (filter, take) import Type.Reflection-import Control.Monad.ST+import Prelude hiding (filter, take) -- | O(k * n) Take the first n rows of a DataFrame. take :: Int -> DataFrame -> DataFrame-take n d = d {columns = V.map (takeColumn n') (columns d), dataframeDimensions = (n', c)}+take n d = d{columns = V.map (takeColumn n') (columns d), dataframeDimensions = (n', c)} where (r, c) = dataframeDimensions d n' = clip n 0 r +-- | O(k * n) Take the last n rows of a DataFrame. takeLast :: Int -> DataFrame -> DataFrame-takeLast n d = d {columns = V.map (takeLastColumn n') (columns d), dataframeDimensions = (n', c)}+takeLast n d = d{columns = V.map (takeLastColumn n') (columns d), dataframeDimensions = (n', c)} where (r, c) = dataframeDimensions d n' = clip n 0 r +-- | O(k * n) Drop the first n rows of a DataFrame. drop :: Int -> DataFrame -> DataFrame-drop n d = d {columns = V.map (sliceColumn n' (max (r - n') 0)) (columns d), dataframeDimensions = (max (r - n') 0, c)}+drop n d = d{columns = V.map (sliceColumn n' (max (r - n') 0)) (columns d), dataframeDimensions = (max (r - n') 0, c)} where (r, c) = dataframeDimensions d n' = clip n 0 r +-- | O(k * n) Drop the last n rows of a DataFrame. dropLast :: Int -> DataFrame -> DataFrame-dropLast n d = d {columns = V.map (sliceColumn 0 n') (columns d), dataframeDimensions = (n', c)}+dropLast n d = d{columns = V.map (sliceColumn 0 n') (columns d), dataframeDimensions = (n', c)} where (r, c) = dataframeDimensions d n' = clip (r - n) 0 r -- | O(k * n) Take a range of rows of a DataFrame. range :: (Int, Int) -> DataFrame -> DataFrame-range (start, end) d = d {columns = V.map (sliceColumn (clip start 0 r) n') (columns d), dataframeDimensions = (n', c)}+range (start, end) d = d{columns = V.map (sliceColumn (clip start 0 r) n') (columns d), dataframeDimensions = (n', c)} where (r, c) = dataframeDimensions d n' = clip (end - start) 0 r@@ -68,106 +72,126 @@ clip :: Int -> Int -> Int -> Int clip n left right = min right $ max n left --- | O(n * k) Filter rows by a given condition.------ filter "x" even df+{- | O(n * k) Filter rows by a given condition.++filter "x" even df+-} filter ::- forall a.- (Columnable a) =>- -- | Column to filter by- T.Text ->- -- | Filter condition- (a -> Bool) ->- -- | Dataframe to filter- DataFrame ->- DataFrame+ forall a.+ (Columnable a) =>+ -- | Column to filter by+ T.Text ->+ -- | Filter condition+ (a -> Bool) ->+ -- | Dataframe to filter+ DataFrame ->+ DataFrame filter filterColumnName condition df = case getColumn filterColumnName df of- Nothing -> throw $ ColumnNotFoundException filterColumnName "filter" (map fst $ M.toList $ columnIndices df)- Just (BoxedColumn (column :: V.Vector b)) -> filterByVector filterColumnName column condition df- Just (OptionalColumn (column :: V.Vector b)) -> filterByVector filterColumnName column condition df- Just (UnboxedColumn (column :: VU.Vector b)) -> filterByVector filterColumnName column condition df+ Nothing -> throw $ ColumnNotFoundException filterColumnName "filter" (map fst $ M.toList $ columnIndices df)+ Just (BoxedColumn (column :: V.Vector b)) -> filterByVector filterColumnName column condition df+ Just (OptionalColumn (column :: V.Vector b)) -> filterByVector filterColumnName column condition df+ Just (UnboxedColumn (column :: VU.Vector b)) -> filterByVector filterColumnName column condition df -filterByVector :: forall a b v . (VG.Vector v b, Columnable a, Columnable b) => T.Text -> v b -> (a -> Bool) -> DataFrame -> DataFrame+filterByVector :: forall a b v. (VG.Vector v b, Columnable a, Columnable b) => T.Text -> v b -> (a -> Bool) -> DataFrame -> DataFrame filterByVector filterColumnName column condition df = case testEquality (typeRep @a) (typeRep @b) of- Nothing -> throw $ TypeMismatchException (MkTypeErrorContext- { userType = Right $ typeRep @a- , expectedType = Right $ typeRep @b- , errorColumnName = Just (T.unpack filterColumnName)- , callingFunctionName = Just "filter"- })- Just Refl -> let- ixs = indexes condition column- in df {columns = V.map (atIndicesStable ixs) (columns df), dataframeDimensions = (VG.length ixs, snd (dataframeDimensions df))}+ Nothing ->+ throw $+ TypeMismatchException+ ( MkTypeErrorContext+ { userType = Right $ typeRep @a+ , expectedType = Right $ typeRep @b+ , errorColumnName = Just (T.unpack filterColumnName)+ , callingFunctionName = Just "filter"+ }+ )+ Just Refl ->+ let+ ixs = indexes condition column+ in+ df{columns = V.map (atIndicesStable ixs) (columns df), dataframeDimensions = (VG.length ixs, snd (dataframeDimensions df))} indexes :: (VG.Vector v a) => (a -> Bool) -> v a -> VU.Vector Int indexes condition cols = runST $ do- ixs <- VUM.new 8192- (!icount, _, _, !ixs') <- VG.foldM (\(!icount, !vcount, !cap, mv) v -> do- if not (condition v) then- pure (icount, vcount + 1, cap, mv)- else do- let shouldGrow = icount == cap- mv' <- if shouldGrow then VUM.grow mv cap else pure mv- VUM.write mv' icount vcount- pure (icount + 1, vcount + 1, cap + (cap * fromEnum shouldGrow), mv')) (0, 0, 8192, ixs) cols- VU.freeze (VUM.slice 0 icount ixs')+ ixs <- VUM.new 8192+ (!icount, _, _, !ixs') <-+ VG.foldM+ ( \(!icount, !vcount, !cap, mv) v -> do+ if not (condition v)+ then+ pure (icount, vcount + 1, cap, mv)+ else do+ let shouldGrow = icount == cap+ mv' <- if shouldGrow then VUM.grow mv cap else pure mv+ VUM.write mv' icount vcount+ pure (icount + 1, vcount + 1, cap + (cap * fromEnum shouldGrow), mv')+ )+ (0, 0, 8192, ixs)+ cols+ VU.freeze (VUM.slice 0 icount ixs') --- | O(k) a version of filter where the predicate comes first.------ > filterBy even "x" df+{- | O(k) a version of filter where the predicate comes first.++> filterBy even "x" df+-} filterBy :: (Columnable a) => (a -> Bool) -> T.Text -> DataFrame -> DataFrame filterBy = flip filter --- | O(k) filters the dataframe with a row predicate. The arguments in the function--- must appear in the same order as they do in the list.------ > filterWhere (["x", "y"], func (\x y -> x + y > 5)) df+{- | O(k) filters the dataframe with a row predicate. The arguments in the function+ must appear in the same order as they do in the list.++> filterWhere (["x", "y"], func (\x y -> x + y > 5)) df+-} filterWhere :: Expr Bool -> DataFrame -> DataFrame-filterWhere expr df = let- (TColumn col) = interpret @Bool df expr- (Just indexes) = findIndices (==True) col- c' = snd $ dataframeDimensions df- pick idxs col = atIndicesStable idxs col- in df {columns = V.map (pick indexes) (columns df), dataframeDimensions = (VU.length indexes, c')}+filterWhere expr df =+ let+ (TColumn col) = interpret @Bool df expr+ (Just indexes) = findIndices (== True) col+ c' = snd $ dataframeDimensions df+ pick idxs col = atIndicesStable idxs col+ in+ df{columns = V.map (pick indexes) (columns df), dataframeDimensions = (VU.length indexes, c')} +{- | O(k) removes all rows with `Nothing` in a given column from the dataframe. --- | O(k) removes all rows with `Nothing` in a given column from the dataframe.------ > filterJust df+> filterJust df+-} filterJust :: T.Text -> DataFrame -> DataFrame filterJust name df = case getColumn name df of- Nothing -> throw $ ColumnNotFoundException name "filterJust" (map fst $ M.toList $ columnIndices df)- Just column@(OptionalColumn (col :: V.Vector (Maybe a))) -> filter @(Maybe a) name isJust df & apply @(Maybe a) fromJust name- Just column -> df+ Nothing -> throw $ ColumnNotFoundException name "filterJust" (map fst $ M.toList $ columnIndices df)+ Just column@(OptionalColumn (col :: V.Vector (Maybe a))) -> filter @(Maybe a) name isJust df & apply @(Maybe a) fromJust name+ Just column -> df --- | O(n * k) removes all rows with `Nothing` from the dataframe.------ > filterJust df+{- | O(n * k) removes all rows with `Nothing` from the dataframe.++> filterJust df+-} filterAllJust :: DataFrame -> DataFrame filterAllJust df = foldr filterJust df (columnNames df) --- | O(k) cuts the dataframe in a cube of size (a, b) where--- a is the length and b is the width. ------ > cube (10, 5) df+{- | O(k) cuts the dataframe in a cube of size (a, b) where+ a is the length and b is the width.++> cube (10, 5) df+-} cube :: (Int, Int) -> DataFrame -> DataFrame cube (length, width) = take length . selectIntRange (0, width - 1) --- | O(n) Selects a number of columns in a given dataframe.------ > select ["name", "age"] df+{- | O(n) Selects a number of columns in a given dataframe.++> select ["name", "age"] df+-} select ::- [T.Text] ->- DataFrame ->- DataFrame+ [T.Text] ->+ DataFrame ->+ DataFrame select cs df- | L.null cs = empty- | any (`notElem` columnNames df) cs = throw $ ColumnNotFoundException (T.pack $ show $ cs L.\\ columnNames df) "select" (columnNames df)- | otherwise = L.foldl' addKeyValue empty cs+ | L.null cs = empty+ | any (`notElem` columnNames df) cs = throw $ ColumnNotFoundException (T.pack $ show $ cs L.\\ columnNames df) "select" (columnNames df)+ | otherwise = L.foldl' addKeyValue empty cs where addKeyValue d k = fromMaybe df $ do- col <- getColumn k df- pure $ insertColumn k col d+ col <- getColumn k df+ pure $ insertColumn k col d -- | O(n) select columns by index range of column names. selectIntRange :: (Int, Int) -> DataFrame -> DataFrame@@ -181,13 +205,14 @@ selectBy :: (T.Text -> Bool) -> DataFrame -> DataFrame selectBy f df = select (L.filter f (columnNames df)) df --- | O(n) inverse of select------ > exclude ["Name"] df+{- | O(n) inverse of select++> exclude ["Name"] df+-} exclude ::- [T.Text] ->- DataFrame ->- DataFrame+ [T.Text] ->+ DataFrame ->+ DataFrame exclude cs df =- let keysToKeep = columnNames df L.\\ cs- in select keysToKeep df+ let keysToKeep = columnNames df L.\\ cs+ in select keysToKeep df
src/DataFrame/Operations/Transformations.hs view
@@ -1,166 +1,186 @@+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}-{-# LANGUAGE FlexibleContexts #-}+ module DataFrame.Operations.Transformations where import qualified Data.List as L-import qualified Data.Text as T import qualified Data.Map as M-import qualified Data.Vector.Generic as VG+import qualified Data.Text as T import qualified Data.Vector as V+import qualified Data.Vector.Generic as VG import qualified Data.Vector.Unboxed as VU import Control.Exception (throw)-import DataFrame.Errors (DataFrameException(..), TypeErrorContext(..))-import DataFrame.Internal.Column (Column(..), columnTypeString, imapColumn, ifoldrColumn, TypedColumn (TColumn), Columnable, mapColumn, unwrapTypedColumn)-import DataFrame.Internal.DataFrame (DataFrame(..), getColumn)+import Data.Maybe+import DataFrame.Errors (DataFrameException (..), TypeErrorContext (..))+import DataFrame.Internal.Column (Column (..), Columnable, TypedColumn (TColumn), columnTypeString, ifoldrColumn, imapColumn, mapColumn, unwrapTypedColumn)+import DataFrame.Internal.DataFrame (DataFrame (..), getColumn) import DataFrame.Internal.Expression-import DataFrame.Internal.Row (mkRowFromArgs, Any, toAny)+import DataFrame.Internal.Row (Any, mkRowFromArgs, toAny) import DataFrame.Operations.Core-import Data.Maybe-import Type.Reflection (typeRep, typeOf, TypeRep)+import Type.Reflection (TypeRep, typeOf, typeRep) -- | O(k) Apply a function to a given column in a dataframe. apply ::- forall b c.- (Columnable b, Columnable c) =>- -- | function to apply- (b -> c) ->- -- | Column name- T.Text ->- -- | DataFrame to apply operation to- DataFrame ->- DataFrame+ forall b c.+ (Columnable b, Columnable c) =>+ -- | function to apply+ (b -> c) ->+ -- | Column name+ T.Text ->+ -- | DataFrame to apply operation to+ DataFrame ->+ DataFrame apply f columnName d = case safeApply f columnName d of- Left exception -> throw exception- Right df -> df+ Left exception -> throw exception+ Right df -> df -- | O(k) Safe version of the apply function. Returns (instead of throwing) the error. safeApply ::- forall b c.- (Columnable b, Columnable c) =>- -- | function to apply- (b -> c) ->- -- | Column name- T.Text ->- -- | DataFrame to apply operation to- DataFrame ->- Either DataFrameException DataFrame+ forall b c.+ (Columnable b, Columnable c) =>+ -- | function to apply+ (b -> c) ->+ -- | Column name+ T.Text ->+ -- | DataFrame to apply operation to+ DataFrame ->+ Either DataFrameException DataFrame safeApply f columnName d = case getColumn columnName d of- Nothing -> Left $ ColumnNotFoundException columnName "apply" (map fst $ M.toList $ columnIndices d)- Just column -> case mapColumn f column of- Nothing -> Left $ TypeMismatchException (MkTypeErrorContext- { userType = Right $ typeRep @b- , expectedType = Left (columnTypeString column) :: Either String (TypeRep ()) - , errorColumnName = Just (T.unpack columnName)- , callingFunctionName = Just "apply"- })- Just column' -> Right $ insertColumn columnName column' d---- | O(k) Apply a function to a combination of columns in a dataframe and--- add the result into `alias` column.-derive :: forall a . Columnable a => T.Text -> Expr a -> DataFrame -> DataFrame-derive name expr df = let- value = interpret @a df expr- in insertColumn name (unwrapTypedColumn value) df+ Nothing -> Left $ ColumnNotFoundException columnName "apply" (map fst $ M.toList $ columnIndices d)+ Just column -> case mapColumn f column of+ Nothing ->+ Left $+ TypeMismatchException+ ( MkTypeErrorContext+ { userType = Right $ typeRep @b+ , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())+ , errorColumnName = Just (T.unpack columnName)+ , callingFunctionName = Just "apply"+ }+ )+ Just column' -> Right $ insertColumn columnName column' d +{- | O(k) Apply a function to a combination of columns in a dataframe and+add the result into `alias` column.+-}+derive :: forall a. (Columnable a) => T.Text -> Expr a -> DataFrame -> DataFrame+derive name expr df =+ let+ value = interpret @a df expr+ in+ insertColumn name (unwrapTypedColumn value) df -- | O(k * n) Apply a function to given column names in a dataframe. applyMany ::- (Columnable b, Columnable c) =>- (b -> c) ->- [T.Text] ->- DataFrame ->- DataFrame+ (Columnable b, Columnable c) =>+ (b -> c) ->+ [T.Text] ->+ DataFrame ->+ DataFrame applyMany f names df = L.foldl' (flip (apply f)) df names -- | O(k) Convenience function that applies to an int column. applyInt ::- (Columnable b) =>- -- | Column name- -- | function to apply- (Int -> b) ->- T.Text ->- -- | DataFrame to apply operation to- DataFrame ->- DataFrame+ (Columnable b) =>+ {- | Column name+ | function to apply+ -}+ (Int -> b) ->+ T.Text ->+ -- | DataFrame to apply operation to+ DataFrame ->+ DataFrame applyInt = apply -- | O(k) Convenience function that applies to an double column. applyDouble ::- (Columnable b) =>- -- | Column name- -- | function to apply- (Double -> b) ->- T.Text ->- -- | DataFrame to apply operation to- DataFrame ->- DataFrame+ (Columnable b) =>+ {- | Column name+ | function to apply+ -}+ (Double -> b) ->+ T.Text ->+ -- | DataFrame to apply operation to+ DataFrame ->+ DataFrame applyDouble = apply --- | O(k * n) Apply a function to a column only if there is another column--- value that matches the given criterion.------ > applyWhere "Age" (<20) "Generation" (const "Gen-Z")+{- | O(k * n) Apply a function to a column only if there is another column+value that matches the given criterion.++> applyWhere "Age" (<20) "Generation" (const "Gen-Z")+-} applyWhere ::- forall a b .- (Columnable a, Columnable b) =>- (a -> Bool) -> -- Filter condition- T.Text -> -- Criterion Column- (b -> b) -> -- function to apply- T.Text -> -- Column name- DataFrame -> -- DataFrame to apply operation to- DataFrame+ forall a b.+ (Columnable a, Columnable b) =>+ (a -> Bool) -> -- Filter condition+ T.Text -> -- Criterion Column+ (b -> b) -> -- function to apply+ T.Text -> -- Column name+ DataFrame -> -- DataFrame to apply operation to+ DataFrame applyWhere condition filterColumnName f columnName df = case getColumn filterColumnName df of- Nothing -> throw $ ColumnNotFoundException filterColumnName "applyWhere" (map fst $ M.toList $ columnIndices df)- Just column -> case ifoldrColumn (\i val acc -> if condition val then V.cons i acc else acc) V.empty column of- Nothing -> throw $ TypeMismatchException (MkTypeErrorContext- { userType = Right $ typeRep @a- , expectedType = Left (columnTypeString column) :: Either String (TypeRep ()) - , errorColumnName = Just (T.unpack columnName)- , callingFunctionName = Just "applyWhere"- })- Just indexes -> if V.null indexes- then df- else L.foldl' (\d i -> applyAtIndex i f columnName d) df indexes+ Nothing -> throw $ ColumnNotFoundException filterColumnName "applyWhere" (map fst $ M.toList $ columnIndices df)+ Just column -> case ifoldrColumn (\i val acc -> if condition val then V.cons i acc else acc) V.empty column of+ Nothing ->+ throw $+ TypeMismatchException+ ( MkTypeErrorContext+ { userType = Right $ typeRep @a+ , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())+ , errorColumnName = Just (T.unpack columnName)+ , callingFunctionName = Just "applyWhere"+ }+ )+ Just indexes ->+ if V.null indexes+ then df+ else L.foldl' (\d i -> applyAtIndex i f columnName d) df indexes -- | O(k) Apply a function to the column at a given index. applyAtIndex ::- forall a.- (Columnable a) =>- -- | Index- Int ->- -- | function to apply- (a -> a) ->- -- | Column name- T.Text ->- -- | DataFrame to apply operation to- DataFrame ->- DataFrame+ forall a.+ (Columnable a) =>+ -- | Index+ Int ->+ -- | function to apply+ (a -> a) ->+ -- | Column name+ T.Text ->+ -- | DataFrame to apply operation to+ DataFrame ->+ DataFrame applyAtIndex i f columnName df = case getColumn columnName df of- Nothing -> throw $ ColumnNotFoundException columnName "applyAtIndex" (map fst $ M.toList $ columnIndices df)- Just column -> case imapColumn (\index value -> if index == i then f value else value) column of- Nothing -> throw $ TypeMismatchException (MkTypeErrorContext- { userType = Right $ typeRep @a- , expectedType = Left (columnTypeString column) :: Either String (TypeRep ()) - , errorColumnName = Just (T.unpack columnName)- , callingFunctionName = Just "applyAtIndex"- })- Just column' -> insertColumn columnName column' df+ Nothing -> throw $ ColumnNotFoundException columnName "applyAtIndex" (map fst $ M.toList $ columnIndices df)+ Just column -> case imapColumn (\index value -> if index == i then f value else value) column of+ Nothing ->+ throw $+ TypeMismatchException+ ( MkTypeErrorContext+ { userType = Right $ typeRep @a+ , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())+ , errorColumnName = Just (T.unpack columnName)+ , callingFunctionName = Just "applyAtIndex"+ }+ )+ Just column' -> insertColumn columnName column' df +-- | Replace all instances of `Nothing` in a column with the given value. impute ::- forall b .- (Columnable b) =>- T.Text ->- b ->- DataFrame ->- DataFrame+ forall b.+ (Columnable b) =>+ T.Text ->+ b ->+ DataFrame ->+ DataFrame impute columnName value df = case getColumn columnName df of- Nothing -> throw $ ColumnNotFoundException columnName "impute" (map fst $ M.toList $ columnIndices df)- Just (OptionalColumn _) -> case safeApply (fromMaybe value) columnName df of- Left (TypeMismatchException context) -> throw $ TypeMismatchException (context { callingFunctionName = Just "impute" })- Left exception -> throw exception- Right res -> res- _ -> error "Cannot impute to a non-Empty column"+ Nothing -> throw $ ColumnNotFoundException columnName "impute" (map fst $ M.toList $ columnIndices df)+ Just (OptionalColumn _) -> case safeApply (fromMaybe value) columnName df of+ Left (TypeMismatchException context) -> throw $ TypeMismatchException (context{callingFunctionName = Just "impute"})+ Left exception -> throw exception+ Right res -> res+ _ -> error "Cannot impute to a non-Empty column"
src/DataFrame/Operations/Typing.hs view
@@ -3,6 +3,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}+ module DataFrame.Operations.Typing where import qualified Data.Set as S@@ -10,64 +11,74 @@ import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU -import DataFrame.Internal.Column (Column(..))-import DataFrame.Internal.DataFrame (DataFrame(..))-import DataFrame.Internal.Parsing import Data.Either import Data.Maybe import Data.Time-import Data.Type.Equality (type (:~:)(Refl), TestEquality(..))+import Data.Type.Equality (TestEquality (..), type (:~:) (Refl))+import DataFrame.Internal.Column (Column (..))+import DataFrame.Internal.DataFrame (DataFrame (..))+import DataFrame.Internal.Parsing import Type.Reflection (typeRep) parseDefaults :: Bool -> DataFrame -> DataFrame-parseDefaults safeRead df = df {columns = V.map (parseDefault safeRead) (columns df)}+parseDefaults safeRead df = df{columns = V.map (parseDefault safeRead) (columns df)} parseDefault :: Bool -> Column -> Column-parseDefault safeRead (BoxedColumn (c :: V.Vector a)) = let- parseTimeOpt s = parseTimeM {- Accept leading/trailing whitespace -} True defaultTimeLocale "%Y-%m-%d" (T.unpack s) :: Maybe Day- unsafeParseTime s = parseTimeOrError {- Accept leading/trailing whitespace -} True defaultTimeLocale "%Y-%m-%d" (T.unpack s) :: Day- in case (typeRep @a) `testEquality` (typeRep @T.Text) of- Nothing -> case (typeRep @a) `testEquality` (typeRep @String) of- Just Refl -> let- emptyToNothing v = if isNullish (T.pack v) then Nothing else Just v- safeVector = V.map emptyToNothing c- hasNulls = V.foldl' (\acc v -> if isNothing v then acc || True else acc) False safeVector- in if safeRead && hasNulls then BoxedColumn safeVector else BoxedColumn c- Nothing -> BoxedColumn c- Just Refl ->- let example = T.strip (V.head c)- emptyToNothing v = if isNullish v then Nothing else Just v- in case readInt example of- Just _ ->- let safeVector = V.map ((=<<) readInt . emptyToNothing) c- hasNulls = V.elem Nothing safeVector- in if safeRead && hasNulls then BoxedColumn safeVector else UnboxedColumn (VU.generate (V.length c) (fromMaybe 0 . (safeVector V.!)))- Nothing -> case readDouble example of- Just _ ->- let safeVector = V.map ((=<<) readDouble . emptyToNothing) c- hasNulls = V.elem Nothing safeVector- in if safeRead && hasNulls then BoxedColumn safeVector else UnboxedColumn (VU.generate (V.length c) (fromMaybe 0 . (safeVector V.!)))- Nothing -> case parseTimeOpt example of- Just d -> let- -- failed parse should be Either, nullish should be Maybe- emptyToNothing' v = if isNullish v then Left v else Right v- parseTimeEither v = case parseTimeOpt v of- Just v' -> Right v'- Nothing -> Left v- safeVector = V.map ((=<<) parseTimeEither . emptyToNothing') c- toMaybe (Left _) = Nothing- toMaybe (Right value) = Just value- lefts = V.filter isLeft safeVector- onlyNulls = (not (V.null lefts) && V.all (isNullish . fromLeft "non-null") lefts)- in if safeRead- then if onlyNulls- then BoxedColumn (V.map toMaybe safeVector)- else if V.any isLeft safeVector- then BoxedColumn safeVector- else BoxedColumn (V.map unsafeParseTime c)- else BoxedColumn (V.map unsafeParseTime c)- Nothing -> let+parseDefault safeRead (BoxedColumn (c :: V.Vector a)) =+ let+ parseTimeOpt s = parseTimeM {- Accept leading/trailing whitespace -} True defaultTimeLocale "%Y-%m-%d" (T.unpack s) :: Maybe Day+ unsafeParseTime s = parseTimeOrError {- Accept leading/trailing whitespace -} True defaultTimeLocale "%Y-%m-%d" (T.unpack s) :: Day+ in+ case (typeRep @a) `testEquality` (typeRep @T.Text) of+ Nothing -> case (typeRep @a) `testEquality` (typeRep @String) of+ Just Refl ->+ let+ emptyToNothing v = if isNullish (T.pack v) then Nothing else Just v safeVector = V.map emptyToNothing c- hasNulls = V.any isNullish c- in if safeRead && hasNulls then BoxedColumn safeVector else BoxedColumn c+ hasNulls = V.foldl' (\acc v -> if isNothing v then acc || True else acc) False safeVector+ in+ if safeRead && hasNulls then BoxedColumn safeVector else BoxedColumn c+ Nothing -> BoxedColumn c+ Just Refl ->+ let example = T.strip (V.head c)+ emptyToNothing v = if isNullish v then Nothing else Just v+ in case readInt example of+ Just _ ->+ let safeVector = V.map ((=<<) readInt . emptyToNothing) c+ hasNulls = V.elem Nothing safeVector+ in if safeRead && hasNulls then BoxedColumn safeVector else UnboxedColumn (VU.generate (V.length c) (fromMaybe 0 . (safeVector V.!)))+ Nothing -> case readDouble example of+ Just _ ->+ let safeVector = V.map ((=<<) readDouble . emptyToNothing) c+ hasNulls = V.elem Nothing safeVector+ in if safeRead && hasNulls then BoxedColumn safeVector else UnboxedColumn (VU.generate (V.length c) (fromMaybe 0 . (safeVector V.!)))+ Nothing -> case parseTimeOpt example of+ Just d ->+ let+ -- failed parse should be Either, nullish should be Maybe+ emptyToNothing' v = if isNullish v then Left v else Right v+ parseTimeEither v = case parseTimeOpt v of+ Just v' -> Right v'+ Nothing -> Left v+ safeVector = V.map ((=<<) parseTimeEither . emptyToNothing') c+ toMaybe (Left _) = Nothing+ toMaybe (Right value) = Just value+ lefts = V.filter isLeft safeVector+ onlyNulls = (not (V.null lefts) && V.all (isNullish . fromLeft "non-null") lefts)+ in+ if safeRead+ then+ if onlyNulls+ then BoxedColumn (V.map toMaybe safeVector)+ else+ if V.any isLeft safeVector+ then BoxedColumn safeVector+ else BoxedColumn (V.map unsafeParseTime c)+ else BoxedColumn (V.map unsafeParseTime c)+ Nothing ->+ let+ safeVector = V.map emptyToNothing c+ hasNulls = V.any isNullish c+ in+ if safeRead && hasNulls then BoxedColumn safeVector else BoxedColumn c parseDefault safeRead column = column
tests/Assertions.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE ScopedTypeVariables #-}+ module Assertions where import qualified Data.List as L@@ -7,20 +8,28 @@ import Test.HUnit -- Adapted from: https://github.com/BartMassey/chunk/blob/1ee4bd6545e0db6b8b5f4935d97e7606708eacc9/hunit.hs#L29-assertExpectException :: String -> String ->- IO a -> Assertion+assertExpectException ::+ String ->+ String ->+ IO a ->+ Assertion assertExpectException preface expected action = do- r <- catch- (action >> (return . Just) "no exception thrown")- (\(e::SomeException) ->- return (checkForExpectedException e))- case r of- Nothing -> return ()- Just msg -> assertFailure $ preface ++ ": " ++ msg+ r <-+ catch+ (action >> (return . Just) "no exception thrown")+ ( \(e :: SomeException) ->+ return (checkForExpectedException e)+ )+ case r of+ Nothing -> return ()+ Just msg -> assertFailure $ preface ++ ": " ++ msg where checkForExpectedException :: SomeException -> Maybe String checkForExpectedException e | expected `L.isInfixOf` show e = Nothing | otherwise =- Just $ "wrong exception detail, expected " ++- expected ++ ", got: " ++ show e+ Just $+ "wrong exception detail, expected "+ ++ expected+ ++ ", got: "+ ++ show e
tests/Functions.hs view
@@ -1,42 +1,62 @@ {-# LANGUAGE OverloadedStrings #-}+ module Functions where import Control.Exception-import Test.HUnit-import DataFrame.Functions (sanitize) import qualified Data.Text as T+import DataFrame.Functions (sanitize)+import Test.HUnit -- Test cases for the sanitize function sanitizeIdentifiers :: Test-sanitizeIdentifiers = TestList- [ TestCase $ assertEqual "Reserved word 'Data' should become '_data_'"- "_data_" (sanitize "Data")- - , TestCase $ assertEqual "Spaces should become underscores"- "my_data" (sanitize "My Data")- - , TestCase $ assertEqual "Punctuation and parentheses should be handled"- "distance_km_h" (sanitize "Distance (km/h)")- - , TestCase $ assertEqual "Leading digit should be wrapped"- "_0_age_" (sanitize "0 Age")- - , TestCase $ assertEqual "Valid camelCase should be unchanged"- "camelCaseStr" (sanitize "camelCaseStr")- - , TestCase $ assertEqual "Valid camelCase with invalid characters mixed in"- "camelcase_str" (sanitize "camelCase$Str")-- , TestCase $ assertEqual "Valid snake_case should remain unchanged"- "snake_case_str" (sanitize "snake_case_str")- - , TestCase $ assertEqual "Leading digit with snake_case should be wrapped"- "_12_snake_case_" (sanitize "12_snake_case")- - , TestCase $ assertEqual "All symbols should become underscores"- "_____" (sanitize "***")- ]+sanitizeIdentifiers =+ TestList+ [ TestCase $+ assertEqual+ "Reserved word 'Data' should become '_data_'"+ "_data_"+ (sanitize "Data")+ , TestCase $+ assertEqual+ "Spaces should become underscores"+ "my_data"+ (sanitize "My Data")+ , TestCase $+ assertEqual+ "Punctuation and parentheses should be handled"+ "distance_km_h"+ (sanitize "Distance (km/h)")+ , TestCase $+ assertEqual+ "Leading digit should be wrapped"+ "_0_age_"+ (sanitize "0 Age")+ , TestCase $+ assertEqual+ "Valid camelCase should be unchanged"+ "camelCaseStr"+ (sanitize "camelCaseStr")+ , TestCase $+ assertEqual+ "Valid camelCase with invalid characters mixed in"+ "camelcase_str"+ (sanitize "camelCase$Str")+ , TestCase $+ assertEqual+ "Valid snake_case should remain unchanged"+ "snake_case_str"+ (sanitize "snake_case_str")+ , TestCase $+ assertEqual+ "Leading digit with snake_case should be wrapped"+ "_12_snake_case_"+ (sanitize "12_snake_case")+ , TestCase $+ assertEqual+ "All symbols should become underscores"+ "_____"+ (sanitize "***")+ ] tests :: [Test] tests = pure $ TestLabel "sanitizeIdentifiers" sanitizeIdentifiers-
tests/Main.hs view
@@ -1,13 +1,18 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}+ module Main where -import qualified DataFrame as D-import qualified DataFrame as DI import qualified Data.List as L import qualified Data.Text as T import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU+import qualified DataFrame as D+import qualified DataFrame.Internal.Column as D+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Internal.DataFrame as D+import qualified DataFrame.Internal.DataFrame as DI+import qualified DataFrame.Operations.Typing as D import qualified System.Exit as Exit import Control.Exception@@ -23,13 +28,16 @@ import qualified Operations.GroupBy import qualified Operations.InsertColumn import qualified Operations.Sort+import qualified Operations.Statistics import qualified Operations.Take import qualified Parquet testData :: D.DataFrame-testData = D.fromNamedColumns [ ("test1", DI.fromList ([1..26] :: [Int]))- , ("test2", DI.fromList ['a'..'z'])- ]+testData =+ D.fromNamedColumns+ [ ("test1", DI.fromList ([1 .. 26] :: [Int]))+ , ("test2", DI.fromList ['a' .. 'z'])+ ] -- Dimensions correctDimensions :: Test@@ -39,48 +47,58 @@ emptyDataframeDimensions = TestCase (assertEqual "should be (0, 0)" (0, 0) (D.dimensions D.empty)) dimensionsTest :: [Test]-dimensionsTest = [ TestLabel "dimensions_correctDimensions" correctDimensions- , TestLabel "dimensions_emptyDataframeDimensions" emptyDataframeDimensions- ]+dimensionsTest =+ [ TestLabel "dimensions_correctDimensions" correctDimensions+ , TestLabel "dimensions_emptyDataframeDimensions" emptyDataframeDimensions+ ] -- parsing. parseDate :: Test-parseDate = let- expected = DI.BoxedColumn (V.fromList [fromGregorian 2020 02 14, fromGregorian 2021 02 14, fromGregorian 2022 02 14])- actual = D.parseDefault True (DI.fromVector (V.fromList ["2020-02-14" :: T.Text, "2021-02-14", "2022-02-14"]))- in TestCase (assertEqual "Correctly parses gregorian date" expected actual)+parseDate =+ let+ expected = DI.BoxedColumn (V.fromList [fromGregorian 2020 02 14, fromGregorian 2021 02 14, fromGregorian 2022 02 14])+ actual = D.parseDefault True (DI.fromVector (V.fromList ["2020-02-14" :: T.Text, "2021-02-14", "2022-02-14"]))+ in+ TestCase (assertEqual "Correctly parses gregorian date" expected actual) incompleteDataParseEither :: Test-incompleteDataParseEither = let- expected = DI.BoxedColumn (V.fromList [Right $ fromGregorian 2020 02 14, Left ("2021-02-" :: T.Text), Right $ fromGregorian 2022 02 14])- actual = D.parseDefault True (DI.fromVector (V.fromList ["2020-02-14" :: T.Text, "2021-02-", "2022-02-14"]))- in TestCase (assertEqual "Parses Either for gregorian date" expected actual)+incompleteDataParseEither =+ let+ expected = DI.BoxedColumn (V.fromList [Right $ fromGregorian 2020 02 14, Left ("2021-02-" :: T.Text), Right $ fromGregorian 2022 02 14])+ actual = D.parseDefault True (DI.fromVector (V.fromList ["2020-02-14" :: T.Text, "2021-02-", "2022-02-14"]))+ in+ TestCase (assertEqual "Parses Either for gregorian date" expected actual) incompleteDataParseMaybe :: Test-incompleteDataParseMaybe = let- expected = DI.BoxedColumn (V.fromList [Just $ fromGregorian 2020 02 14, Nothing, Just $ fromGregorian 2022 02 14])- actual = D.parseDefault True (DI.fromVector (V.fromList ["2020-02-14" :: T.Text, "", "2022-02-14"]))- in TestCase (assertEqual "Parses Maybe for gregorian date with null/empty" expected actual)+incompleteDataParseMaybe =+ let+ expected = DI.BoxedColumn (V.fromList [Just $ fromGregorian 2020 02 14, Nothing, Just $ fromGregorian 2022 02 14])+ actual = D.parseDefault True (DI.fromVector (V.fromList ["2020-02-14" :: T.Text, "", "2022-02-14"]))+ in+ TestCase (assertEqual "Parses Maybe for gregorian date with null/empty" expected actual) parseTests :: [Test]-parseTests = [- TestLabel "parseDate" parseDate,- TestLabel "incompleteDataParseMaybe" incompleteDataParseMaybe,- TestLabel "incompleteDataParseEither" incompleteDataParseEither- ]+parseTests =+ [ TestLabel "parseDate" parseDate+ , TestLabel "incompleteDataParseMaybe" incompleteDataParseMaybe+ , TestLabel "incompleteDataParseEither" incompleteDataParseEither+ ] tests :: Test-tests = TestList $ dimensionsTest- ++ Operations.Apply.tests- ++ Operations.Derive.tests- ++ Operations.Filter.tests- ++ Operations.GroupBy.tests- ++ Operations.InsertColumn.tests- ++ Operations.Sort.tests- ++ Operations.Take.tests- ++ Functions.tests- ++ Parquet.tests- ++ parseTests+tests =+ TestList $+ dimensionsTest+ ++ Operations.Apply.tests+ ++ Operations.Derive.tests+ ++ Operations.Filter.tests+ ++ Operations.GroupBy.tests+ ++ Operations.InsertColumn.tests+ ++ Operations.Sort.tests+ ++ Operations.Statistics.tests+ ++ Operations.Take.tests+ ++ Functions.tests+ ++ Parquet.tests+ ++ parseTests main :: IO () main = do
tests/Operations/Apply.hs view
@@ -1,122 +1,203 @@-{-# LANGUAGE TypeApplications #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeApplications #-}+ module Operations.Apply where -import qualified DataFrame as D-import qualified DataFrame as DI-import qualified DataFrame as DE import qualified Data.Text as T import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU+import qualified DataFrame as D+import qualified DataFrame as DE+import qualified DataFrame.Internal.Column as D+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Internal.DataFrame as D+import qualified DataFrame.Internal.DataFrame as DI import Assertions import Test.HUnit import Type.Reflection (typeRep) values :: [(T.Text, DI.Column)]-values = [ ("test1", DI.fromList ([1..26] :: [Int]))- , ("test2", DI.fromList (map show ['a'..'z']))- , ("test3", DI.fromList ([1..26] :: [Int]))- , ("test4", DI.fromList ['a'..'z'])- , ("test5", DI.fromList ([1..26] :: [Int]))- , ("test6", DI.fromList ['a'..'z'])- , ("test7", DI.fromList ([1..26] :: [Int]))- , ("test8", DI.fromList ['a'..'z'])- ]+values =+ [ ("test1", DI.fromList ([1 .. 26] :: [Int]))+ , ("test2", DI.fromList (map show ['a' .. 'z']))+ , ("test3", DI.fromList ([1 .. 26] :: [Int]))+ , ("test4", DI.fromList ['a' .. 'z'])+ , ("test5", DI.fromList ([1 .. 26] :: [Int]))+ , ("test6", DI.fromList ['a' .. 'z'])+ , ("test7", DI.fromList ([1 .. 26] :: [Int]))+ , ("test8", DI.fromList ['a' .. 'z'])+ ] testData :: D.DataFrame testData = D.fromNamedColumns values applyBoxedToUnboxed :: Test-applyBoxedToUnboxed = TestCase (assertEqual "Boxed apply unboxed when result is unboxed"- (Just $ DI.UnboxedColumn (VU.fromList (replicate 26 (1 :: Int))))- (DI.getColumn "test2" $ D.apply @String (const (1::Int)) "test2" testData))+applyBoxedToUnboxed =+ TestCase+ ( assertEqual+ "Boxed apply unboxed when result is unboxed"+ (Just $ DI.UnboxedColumn (VU.fromList (replicate 26 (1 :: Int))))+ (DI.getColumn "test2" $ D.apply @String (const (1 :: Int)) "test2" testData)+ ) applyBoxedToBoxed :: Test-applyBoxedToBoxed = TestCase (assertEqual "Boxed apply remains in boxed vector"- (Just $ DI.BoxedColumn (V.fromList (replicate 26 (1 :: Integer))))- (DI.getColumn "test2" $ D.apply @String (const (1::Integer)) "test2" testData))+applyBoxedToBoxed =+ TestCase+ ( assertEqual+ "Boxed apply remains in boxed vector"+ (Just $ DI.BoxedColumn (V.fromList (replicate 26 (1 :: Integer))))+ (DI.getColumn "test2" $ D.apply @String (const (1 :: Integer)) "test2" testData)+ ) applyWrongType :: Test-applyWrongType = TestCase (assertExpectException "[Error Case]"- (DE.typeMismatchError (show $ typeRep @Char) (show $ typeRep @[Char]))- (print $ DI.getColumn "test2" $ D.apply @Char (const (1::Int)) "test2" testData))+applyWrongType =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (DE.typeMismatchError (show $ typeRep @Char) (show $ typeRep @[Char]))+ (print $ DI.getColumn "test2" $ D.apply @Char (const (1 :: Int)) "test2" testData)+ ) applyUnknownColumn :: Test-applyUnknownColumn = TestCase (assertExpectException "[Error Case]"- (DE.columnNotFound "test9" "apply" (D.columnNames testData))- (print $ D.apply @[Char] (const (1::Int)) "test9" testData))+applyUnknownColumn =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (DE.columnNotFound "test9" "apply" (D.columnNames testData))+ (print $ D.apply @[Char] (const (1 :: Int)) "test9" testData)+ ) applyManyOnlyGivenFields :: Test-applyManyOnlyGivenFields = TestCase (assertEqual "Applies function to many fields"- (D.fromNamedColumns (map (, D.fromList $ replicate 26 (1 :: Integer)) ["test4", "test6"] ++- -- All other fields should have their original values.- filter (\(name, col) -> name /= "test4" && name /= "test6") values))- (D.applyMany @Char (const (1::Integer))- ["test4", "test6"] testData))+applyManyOnlyGivenFields =+ TestCase+ ( assertEqual+ "Applies function to many fields"+ ( D.fromNamedColumns+ ( map (,D.fromList $ replicate 26 (1 :: Integer)) ["test4", "test6"]+ +++ -- All other fields should have their original values.+ filter (\(name, col) -> name /= "test4" && name /= "test6") values+ )+ )+ ( D.applyMany @Char+ (const (1 :: Integer))+ ["test4", "test6"]+ testData+ )+ ) applyManyBoxedToBoxed :: Test-applyManyBoxedToBoxed = TestCase (assertEqual "Applies function to many fields"- (D.fromNamedColumns (map (, D.fromList $ replicate 26 (1 :: Integer)) ["test4", "test6", "test8"]))- (D.select ["test4", "test6", "test8"] $ D.applyMany @Char (const (1::Integer))- ["test4", "test6", "test8"] testData))+applyManyBoxedToBoxed =+ TestCase+ ( assertEqual+ "Applies function to many fields"+ (D.fromNamedColumns (map (,D.fromList $ replicate 26 (1 :: Integer)) ["test4", "test6", "test8"]))+ ( D.select ["test4", "test6", "test8"] $+ D.applyMany @Char+ (const (1 :: Integer))+ ["test4", "test6", "test8"]+ testData+ )+ ) applyManyBoxedToUnboxed :: Test-applyManyBoxedToUnboxed = TestCase (assertEqual "Unboxes fields when necessary"- (D.fromNamedColumns (map (, D.fromList $ replicate 26 (1 :: Int)) ["test4", "test6", "test8"]))- (D.select ["test4", "test6", "test8"] $ D.applyMany @Char (const (1::Int))- ["test4", "test6", "test8"] testData))+applyManyBoxedToUnboxed =+ TestCase+ ( assertEqual+ "Unboxes fields when necessary"+ (D.fromNamedColumns (map (,D.fromList $ replicate 26 (1 :: Int)) ["test4", "test6", "test8"]))+ ( D.select ["test4", "test6", "test8"] $+ D.applyMany @Char+ (const (1 :: Int))+ ["test4", "test6", "test8"]+ testData+ )+ ) applyManyColumnNotFound :: Test-applyManyColumnNotFound = TestCase (assertExpectException "[Error Case]"- (DE.columnNotFound "test0" "apply" (D.columnNames testData))- (print $ D.applyMany @Char (const (1::Integer))- ["test0", "test6", "test8"] testData))+applyManyColumnNotFound =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (DE.columnNotFound "test0" "apply" (D.columnNames testData))+ ( print $+ D.applyMany @Char+ (const (1 :: Integer))+ ["test0", "test6", "test8"]+ testData+ )+ ) applyManyWrongType :: Test-applyManyWrongType = TestCase (assertExpectException "[Error Case]"- (DE.typeMismatchError (show $ typeRep @Char) (show $ typeRep @[Char]))- (print $ DI.getColumn "test2" $ D.applyMany @Char (const (1::Int)) ["test2"] testData))+applyManyWrongType =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (DE.typeMismatchError (show $ typeRep @Char) (show $ typeRep @[Char]))+ (print $ DI.getColumn "test2" $ D.applyMany @Char (const (1 :: Int)) ["test2"] testData)+ ) applyWhereWrongConditionType :: Test-applyWhereWrongConditionType = TestCase (assertExpectException "[Error Case]"- (DE.typeMismatchError (show $ typeRep @Integer) (show $ typeRep @Int))- (print $ D.applyWhere (even @Integer) "test1" ((+1) :: Int -> Int) "test5" testData))+applyWhereWrongConditionType =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (DE.typeMismatchError (show $ typeRep @Integer) (show $ typeRep @Int))+ (print $ D.applyWhere (even @Integer) "test1" ((+ 1) :: Int -> Int) "test5" testData)+ ) applyWhereWrongTargetType :: Test-applyWhereWrongTargetType = TestCase (assertExpectException "[Error Case]"- (DE.typeMismatchError (show $ typeRep @Float) (show $ typeRep @Int))- (print $ D.applyWhere (even @Int) "test1" ((+1) :: Float -> Float) "test5" testData))+applyWhereWrongTargetType =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (DE.typeMismatchError (show $ typeRep @Float) (show $ typeRep @Int))+ (print $ D.applyWhere (even @Int) "test1" ((+ 1) :: Float -> Float) "test5" testData)+ ) applyWhereConditionColumnNotFound :: Test-applyWhereConditionColumnNotFound = TestCase (assertExpectException "[Error Case]"- (DE.columnNotFound "test0" "applyWhere" (D.columnNames testData))- (print $ D.applyWhere (even @Int) "test0" ((+1) :: Int -> Int) "test5" testData))+applyWhereConditionColumnNotFound =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (DE.columnNotFound "test0" "applyWhere" (D.columnNames testData))+ (print $ D.applyWhere (even @Int) "test0" ((+ 1) :: Int -> Int) "test5" testData)+ ) applyWhereTargetColumnNotFound :: Test-applyWhereTargetColumnNotFound = TestCase (assertExpectException "[Error Case]"- (DE.columnNotFound "test0" "applyAtIndex" (D.columnNames testData))- (print $ D.applyWhere (even @Int) "test1" ((+1) :: Int -> Int) "test0" testData))+applyWhereTargetColumnNotFound =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (DE.columnNotFound "test0" "applyAtIndex" (D.columnNames testData))+ (print $ D.applyWhere (even @Int) "test1" ((+ 1) :: Int -> Int) "test0" testData)+ ) applyWhereWAI :: Test-applyWhereWAI = TestCase (assertEqual "applyWhere works as intended"- (Just $ DI.UnboxedColumn (VU.fromList (zipWith ($) (cycle [id, (+1)]) [(1 :: Int)..26])))- (D.getColumn "test5" $ D.applyWhere (even @Int) "test1" ((+1) :: Int -> Int) "test5" testData))+applyWhereWAI =+ TestCase+ ( assertEqual+ "applyWhere works as intended"+ (Just $ DI.UnboxedColumn (VU.fromList (zipWith ($) (cycle [id, (+ 1)]) [(1 :: Int) .. 26])))+ (D.getColumn "test5" $ D.applyWhere (even @Int) "test1" ((+ 1) :: Int -> Int) "test5" testData)+ ) tests :: [Test]-tests = [ TestLabel "applyBoxedToUnboxed" applyBoxedToUnboxed- , TestLabel "applyWrongType" applyWrongType- , TestLabel "applyUnknownColumn" applyUnknownColumn- , TestLabel "applyBoxedToBoxed" applyBoxedToBoxed- , TestLabel "applyManyBoxedToBoxed" applyManyBoxedToBoxed- , TestLabel "applyManyOnlyGivenFields" applyManyOnlyGivenFields- , TestLabel "applyManyBoxedToUnboxed" applyManyBoxedToUnboxed- , TestLabel "applyManyColumnNotFound" applyManyColumnNotFound- , TestLabel "applyManyWrongType" applyManyWrongType- , TestLabel "applyWhereWrongConditionType" applyWhereWrongConditionType- , TestLabel "applyWhereWrongTargetType" applyWhereWrongTargetType- , TestLabel "applyWhereConditionColumnNotFound" applyWhereConditionColumnNotFound- , TestLabel "applyWhereTargetColumnNotFound" applyWhereTargetColumnNotFound- , TestLabel "applyWhereWAI" applyWhereWAI- ]+tests =+ [ TestLabel "applyBoxedToUnboxed" applyBoxedToUnboxed+ , TestLabel "applyWrongType" applyWrongType+ , TestLabel "applyUnknownColumn" applyUnknownColumn+ , TestLabel "applyBoxedToBoxed" applyBoxedToBoxed+ , TestLabel "applyManyBoxedToBoxed" applyManyBoxedToBoxed+ , TestLabel "applyManyOnlyGivenFields" applyManyOnlyGivenFields+ , TestLabel "applyManyBoxedToUnboxed" applyManyBoxedToUnboxed+ , TestLabel "applyManyColumnNotFound" applyManyColumnNotFound+ , TestLabel "applyManyWrongType" applyManyWrongType+ , TestLabel "applyWhereWrongConditionType" applyWhereWrongConditionType+ , TestLabel "applyWhereWrongTargetType" applyWhereWrongTargetType+ , TestLabel "applyWhereConditionColumnNotFound" applyWhereConditionColumnNotFound+ , TestLabel "applyWhereTargetColumnNotFound" applyWhereTargetColumnNotFound+ , TestLabel "applyWhereWAI" applyWhereWAI+ ]
tests/Operations/Derive.hs view
@@ -1,36 +1,48 @@-{-# LANGUAGE TypeApplications #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+ module Operations.Derive where -import qualified DataFrame as D-import qualified DataFrame.Functions as F-import qualified DataFrame as DI-import qualified DataFrame as DE import qualified Data.Text as T import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU+import qualified DataFrame as D+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as D+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Internal.DataFrame as D+import qualified DataFrame.Internal.DataFrame as DI import Assertions import Test.HUnit import Type.Reflection (typeRep) values :: [(T.Text, DI.Column)]-values = [ ("test1", DI.fromList ([1..26] :: [Int]))- , ("test2", DI.fromList (map show ['a'..'z']))- , ("test3", DI.fromList ['a'..'z'])- ]+values =+ [ ("test1", DI.fromList ([1 .. 26] :: [Int]))+ , ("test2", DI.fromList (map show ['a' .. 'z']))+ , ("test3", DI.fromList ['a' .. 'z'])+ ] testData :: D.DataFrame testData = D.fromNamedColumns values deriveWAI :: Test-deriveWAI = TestCase (assertEqual "derive works with column expression"- (Just $ DI.BoxedColumn (V.fromList (zipWith (\n c -> show n ++ [c]) [1..26] ['a'..'z'])))- (DI.getColumn "test4" $ D.derive "test4" (- F.lift2 (++) (F.lift show (F.col @Int "test1")) (F.lift (: ([] :: [Char])) (F.col @Char "test3"))- ) testData))+deriveWAI =+ TestCase+ ( assertEqual+ "derive works with column expression"+ (Just $ DI.BoxedColumn (V.fromList (zipWith (\n c -> show n ++ [c]) [1 .. 26] ['a' .. 'z'])))+ ( DI.getColumn "test4" $+ D.derive+ "test4"+ (F.lift2 (++) (F.lift show (F.col @Int "test1")) (F.lift (: ([] :: [Char])) (F.col @Char "test3")))+ testData+ )+ ) tests :: [Test]-tests = [ TestLabel "deriveWAI" deriveWAI- ]+tests =+ [ TestLabel "deriveWAI" deriveWAI+ ]
tests/Operations/Filter.hs view
@@ -1,73 +1,104 @@-{-# LANGUAGE TypeApplications #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+ module Operations.Filter where -import qualified DataFrame as D-import qualified DataFrame as DI-import qualified DataFrame as DE import qualified Data.Text as T import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU+import qualified DataFrame as D+import qualified DataFrame as DE+import qualified DataFrame as DI import Assertions import Test.HUnit import Type.Reflection (typeRep) values :: [(T.Text, DI.Column)]-values = [ ("test1", DI.fromList ([1..26] :: [Int]))- , ("test2", DI.fromList (map show ['a'..'z']))- , ("test3", DI.fromList ([1..26] :: [Int]))- , ("test4", DI.fromList ['a'..'z'])- , ("test5", DI.fromList ([1..26] :: [Int]))- , ("test6", DI.fromList ['a'..'z'])- , ("test7", DI.fromList ([1..26] :: [Int]))- , ("test8", DI.fromList ['a'..'z'])- ]+values =+ [ ("test1", DI.fromList ([1 .. 26] :: [Int]))+ , ("test2", DI.fromList (map show ['a' .. 'z']))+ , ("test3", DI.fromList ([1 .. 26] :: [Int]))+ , ("test4", DI.fromList ['a' .. 'z'])+ , ("test5", DI.fromList ([1 .. 26] :: [Int]))+ , ("test6", DI.fromList ['a' .. 'z'])+ , ("test7", DI.fromList ([1 .. 26] :: [Int]))+ , ("test8", DI.fromList ['a' .. 'z'])+ ] testData :: D.DataFrame testData = D.fromNamedColumns values filterColumnDoesNotExist :: Test-filterColumnDoesNotExist = TestCase (assertExpectException "[Error Case]"- (DE.columnNotFound "test0" "filter" (D.columnNames testData))- (print $ D.filter @Int "test0" even testData))+filterColumnDoesNotExist =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (DE.columnNotFound "test0" "filter" (D.columnNames testData))+ (print $ D.filter @Int "test0" even testData)+ ) filterColumnWrongType :: Test-filterColumnWrongType = TestCase (assertExpectException "[Error Case]"- (DE.typeMismatchError (show $ typeRep @Integer) (show $ typeRep @Int))- (print $ D.filter @Integer "test1" even testData))+filterColumnWrongType =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (DE.typeMismatchError (show $ typeRep @Integer) (show $ typeRep @Int))+ (print $ D.filter @Integer "test1" even testData)+ ) filterByColumnDoesNotExist :: Test-filterByColumnDoesNotExist = TestCase (assertExpectException "[Error Case]"- (DE.columnNotFound "test0" "filter" (D.columnNames testData))- (print $ D.filterBy @Int even "test0" testData))+filterByColumnDoesNotExist =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (DE.columnNotFound "test0" "filter" (D.columnNames testData))+ (print $ D.filterBy @Int even "test0" testData)+ ) filterByColumnWrongType :: Test-filterByColumnWrongType = TestCase (assertExpectException "[Error Case]"- (DE.typeMismatchError (show $ typeRep @Integer) (show $ typeRep @Int))- (print $ D.filterBy @Integer even "test1" testData))+filterByColumnWrongType =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (DE.typeMismatchError (show $ typeRep @Integer) (show $ typeRep @Int))+ (print $ D.filterBy @Integer even "test1" testData)+ ) filterColumnInexistentValues :: Test-filterColumnInexistentValues = TestCase (assertEqual "Non existent filter value returns no rows"- (0, 8)- (D.dimensions $ D.filter @Int "test1" (<0) testData))+filterColumnInexistentValues =+ TestCase+ ( assertEqual+ "Non existent filter value returns no rows"+ (0, 8)+ (D.dimensions $ D.filter @Int "test1" (< 0) testData)+ ) filterColumnAllValues :: Test-filterColumnAllValues = TestCase (assertEqual "Filters all columns"- (26, 8)- (D.dimensions $ D.filter @Int "test1" (const True) testData))+filterColumnAllValues =+ TestCase+ ( assertEqual+ "Filters all columns"+ (26, 8)+ (D.dimensions $ D.filter @Int "test1" (const True) testData)+ ) filterJustWAI :: Test-filterJustWAI = TestCase (assertEqual "Filters out Nothing and unwraps Maybe"- (D.fromNamedColumns [("test", D.fromList $ replicate 5 (1 :: Int))])- (D.filterJust "test" (D.fromNamedColumns [("test", D.fromList$ take 10 $ cycle [Just (1 :: Int), Nothing])])))+filterJustWAI =+ TestCase+ ( assertEqual+ "Filters out Nothing and unwraps Maybe"+ (D.fromNamedColumns [("test", D.fromList $ replicate 5 (1 :: Int))])+ (D.filterJust "test" (D.fromNamedColumns [("test", D.fromList $ take 10 $ cycle [Just (1 :: Int), Nothing])]))+ ) tests :: [Test]-tests = [ TestLabel "filterColumnDoesNotExist" filterColumnDoesNotExist- , TestLabel "filterColumnWrongType" filterColumnWrongType- , TestLabel "filterByColumnDoesNotExist" filterByColumnDoesNotExist- , TestLabel "filterByColumnWrongType" filterByColumnWrongType- , TestLabel "filterColumnInexistentValues" filterColumnInexistentValues- , TestLabel "filterColumnAllValues" filterColumnAllValues- , TestLabel "filterJustWAI" filterJustWAI- ]+tests =+ [ TestLabel "filterColumnDoesNotExist" filterColumnDoesNotExist+ , TestLabel "filterColumnWrongType" filterColumnWrongType+ , TestLabel "filterByColumnDoesNotExist" filterByColumnDoesNotExist+ , TestLabel "filterByColumnWrongType" filterByColumnWrongType+ , TestLabel "filterColumnInexistentValues" filterColumnInexistentValues+ , TestLabel "filterColumnAllValues" filterColumnAllValues+ , TestLabel "filterJustWAI" filterJustWAI+ ]
tests/Operations/GroupBy.hs view
@@ -1,46 +1,62 @@ {-# LANGUAGE OverloadedStrings #-}+ module Operations.GroupBy where -import qualified DataFrame as D-import qualified DataFrame as DI-import qualified DataFrame as DE import qualified Data.Text as T import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU+import qualified DataFrame as D+import qualified DataFrame.Internal.Column as D+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Internal.DataFrame as D+import qualified DataFrame.Internal.DataFrame as DI import Assertions import Test.HUnit values :: [(T.Text, DI.Column)]-values = [ ("test1", DI.fromList (concatMap (replicate 10) [1 :: Int, 2, 3, 4]))- , ("test2", DI.fromList (take 40 $ cycle [1 :: Int,2]))- , ("test3", DI.fromList [(1 :: Int)..40])- , ("test4", DI.fromList (reverse [(1 :: Int)..40]))- ]+values =+ [ ("test1", DI.fromList (concatMap (replicate 10) [1 :: Int, 2, 3, 4]))+ , ("test2", DI.fromList (take 40 $ cycle [1 :: Int, 2]))+ , ("test3", DI.fromList [(1 :: Int) .. 40])+ , ("test4", DI.fromList (reverse [(1 :: Int) .. 40]))+ ] testData :: D.DataFrame testData = D.fromNamedColumns values groupBySingleRowWAI :: Test-groupBySingleRowWAI = TestCase (assertEqual "Groups by single column"- -- We don't yet compare offsets and indices- (D.Grouped testData ["test1"] VU.empty VU.empty)- (D.groupBy ["test1"] testData))+groupBySingleRowWAI =+ TestCase+ ( assertEqual+ "Groups by single column"+ -- We don't yet compare offsets and indices+ (D.Grouped testData ["test1"] VU.empty VU.empty)+ (D.groupBy ["test1"] testData)+ ) groupByMultipleRowsWAI :: Test-groupByMultipleRowsWAI = TestCase (assertEqual "Groups by single column"- -- We don't yet compare offsets and indices- (D.Grouped testData ["test1", "test2"] VU.empty VU.empty)- (D.groupBy ["test1", "test2"] testData))+groupByMultipleRowsWAI =+ TestCase+ ( assertEqual+ "Groups by single column"+ -- We don't yet compare offsets and indices+ (D.Grouped testData ["test1", "test2"] VU.empty VU.empty)+ (D.groupBy ["test1", "test2"] testData)+ ) groupByColumnDoesNotExist :: Test-groupByColumnDoesNotExist = TestCase (assertExpectException "[Error Case]"- (DE.columnNotFound "[\"test0\"]" "groupBy" (D.columnNames testData))- (print $ D.groupBy ["test0"] testData))+groupByColumnDoesNotExist =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (D.columnNotFound "[\"test0\"]" "groupBy" (D.columnNames testData))+ (print $ D.groupBy ["test0"] testData)+ ) tests :: [Test]-tests = [ TestLabel "groupBySingleRowWAI" groupBySingleRowWAI- , TestLabel "groupByMultipleRowsWAI" groupByMultipleRowsWAI- , TestLabel "groupByColumnDoesNotExist" groupByColumnDoesNotExist- ]-+tests =+ [ TestLabel "groupBySingleRowWAI" groupBySingleRowWAI+ , TestLabel "groupByMultipleRowsWAI" groupByMultipleRowsWAI+ , TestLabel "groupByColumnDoesNotExist" groupByColumnDoesNotExist+ ]
tests/Operations/InsertColumn.hs view
@@ -1,118 +1,171 @@-{-# LANGUAGE TypeApplications #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+ module Operations.InsertColumn where -import qualified DataFrame as D-import qualified DataFrame as DI import qualified Data.Text as T import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU+import qualified DataFrame as D+import qualified DataFrame.Internal.Column as D+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Internal.DataFrame as D+import qualified DataFrame.Internal.DataFrame as DI import Assertions import Test.HUnit testData :: D.DataFrame-testData = D.fromNamedColumns [ ("test1", DI.fromList([1..26] :: [Int]))- , ("test2", DI.fromList ['a'..'z'])- , ("test3", DI.fromList ([1..26] :: [Int]))- , ("test4", DI.fromList ['a'..'z'])- , ("test5", DI.fromList ([1..26] :: [Int]))- , ("test6", DI.fromList ['a'..'z'])- , ("test7", DI.fromList ([1..26] :: [Int]))- , ("test8", DI.fromList ['a'..'z'])- ]+testData =+ D.fromNamedColumns+ [ ("test1", DI.fromList ([1 .. 26] :: [Int]))+ , ("test2", DI.fromList ['a' .. 'z'])+ , ("test3", DI.fromList ([1 .. 26] :: [Int]))+ , ("test4", DI.fromList ['a' .. 'z'])+ , ("test5", DI.fromList ([1 .. 26] :: [Int]))+ , ("test6", DI.fromList ['a' .. 'z'])+ , ("test7", DI.fromList ([1 .. 26] :: [Int]))+ , ("test8", DI.fromList ['a' .. 'z'])+ ] -- Adding a boxed vector to an empty dataframe creates a new column boxed containing the vector elements. addBoxedColumn :: Test-addBoxedColumn = TestCase (assertEqual "Two columns should be equal"- (Just $ DI.BoxedColumn (V.fromList ["Thuba" :: T.Text, "Zodwa", "Themba"]))- (DI.getColumn "new" $ D.insertVector "new" (V.fromList ["Thuba" :: T.Text, "Zodwa", "Themba"]) D.empty))+addBoxedColumn =+ TestCase+ ( assertEqual+ "Two columns should be equal"+ (Just $ DI.BoxedColumn (V.fromList ["Thuba" :: T.Text, "Zodwa", "Themba"]))+ (DI.getColumn "new" $ D.insertVector "new" (V.fromList ["Thuba" :: T.Text, "Zodwa", "Themba"]) D.empty)+ ) addBoxedColumn' :: Test-addBoxedColumn' = TestCase (assertEqual "Two columns should be equal"- (Just $ DI.fromList["Thuba" :: T.Text, "Zodwa", "Themba"])- (DI.getColumn "new" $ D.insertColumn "new" (DI.fromList["Thuba" :: T.Text, "Zodwa", "Themba"]) D.empty))+addBoxedColumn' =+ TestCase+ ( assertEqual+ "Two columns should be equal"+ (Just $ DI.fromList ["Thuba" :: T.Text, "Zodwa", "Themba"])+ (DI.getColumn "new" $ D.insertColumn "new" (DI.fromList ["Thuba" :: T.Text, "Zodwa", "Themba"]) D.empty)+ ) -- Adding an boxed vector with an unboxable type (Int/Double) to an empty dataframe creates a new column boxed containing the vector elements. addUnboxedColumn :: Test-addUnboxedColumn = TestCase (assertEqual "Value should be boxed"- (Just $ DI.UnboxedColumn (VU.fromList [1 :: Int, 2, 3]))- (DI.getColumn "new" $ D.insertVector "new" (V.fromList [1 :: Int, 2, 3]) D.empty))+addUnboxedColumn =+ TestCase+ ( assertEqual+ "Value should be boxed"+ (Just $ DI.UnboxedColumn (VU.fromList [1 :: Int, 2, 3]))+ (DI.getColumn "new" $ D.insertVector "new" (V.fromList [1 :: Int, 2, 3]) D.empty)+ ) addUnboxedColumn' :: Test-addUnboxedColumn' = TestCase (assertEqual "Value should be boxed"- (Just $ DI.fromList[1 :: Int, 2, 3])- (DI.getColumn "new" $ D.insertColumn "new" (DI.fromList[1 :: Int, 2, 3]) D.empty))+addUnboxedColumn' =+ TestCase+ ( assertEqual+ "Value should be boxed"+ (Just $ DI.fromList [1 :: Int, 2, 3])+ (DI.getColumn "new" $ D.insertColumn "new" (DI.fromList [1 :: Int, 2, 3]) D.empty)+ ) -- Adding a column with less values than the current DF dimensions adds column with optionals. addSmallerColumnBoxed :: Test-addSmallerColumnBoxed = TestCase (- assertEqual "Missing values should be replaced with Nothing"- (Just $ DI.OptionalColumn (V.fromList [Just "a" :: Maybe T.Text, Just "b", Just "c", Nothing, Nothing]))- (DI.getColumn "newer" $ D.insertVector "newer" (V.fromList ["a" :: T.Text, "b", "c"]) $ D.insertVector "new" (V.fromList ["a" :: T.Text, "b", "c", "d", "e"]) D.empty)- )+addSmallerColumnBoxed =+ TestCase+ ( assertEqual+ "Missing values should be replaced with Nothing"+ (Just $ DI.OptionalColumn (V.fromList [Just "a" :: Maybe T.Text, Just "b", Just "c", Nothing, Nothing]))+ (DI.getColumn "newer" $ D.insertVector "newer" (V.fromList ["a" :: T.Text, "b", "c"]) $ D.insertVector "new" (V.fromList ["a" :: T.Text, "b", "c", "d", "e"]) D.empty)+ ) addSmallerColumnUnboxed :: Test-addSmallerColumnUnboxed = TestCase (- assertEqual "Missing values should be replaced with Nothing"- (Just $ DI.OptionalColumn (V.fromList [Just 1 :: Maybe Int, Just 2, Just 3, Nothing, Nothing]))- (DI.getColumn "newer" $ D.insertVector "newer" (V.fromList [1 :: Int, 2, 3]) $ D.insertVector "new" (V.fromList [1 :: Int, 2, 3, 4, 5]) D.empty)- )+addSmallerColumnUnboxed =+ TestCase+ ( assertEqual+ "Missing values should be replaced with Nothing"+ (Just $ DI.OptionalColumn (V.fromList [Just 1 :: Maybe Int, Just 2, Just 3, Nothing, Nothing]))+ (DI.getColumn "newer" $ D.insertVector "newer" (V.fromList [1 :: Int, 2, 3]) $ D.insertVector "new" (V.fromList [1 :: Int, 2, 3, 4, 5]) D.empty)+ ) insertColumnWithDefaultFillsWithDefault :: Test-insertColumnWithDefaultFillsWithDefault = TestCase (- assertEqual "Missing values should be replaced with Nothing"- (Just $ DI.UnboxedColumn (VU.fromList [1 :: Int, 2, 3, 0, 0]))- (DI.getColumn "newer" $ D.insertVectorWithDefault 0 "newer" (V.fromList [1 :: Int, 2, 3]) $ D.insertVector "new" (V.fromList [1 :: Int, 2, 3, 4, 5]) D.empty)- )+insertColumnWithDefaultFillsWithDefault =+ TestCase+ ( assertEqual+ "Missing values should be replaced with Nothing"+ (Just $ DI.UnboxedColumn (VU.fromList [1 :: Int, 2, 3, 0, 0]))+ (DI.getColumn "newer" $ D.insertVectorWithDefault 0 "newer" (V.fromList [1 :: Int, 2, 3]) $ D.insertVector "new" (V.fromList [1 :: Int, 2, 3, 4, 5]) D.empty)+ ) insertColumnWithDefaultFillsLargerNoop :: Test-insertColumnWithDefaultFillsLargerNoop = TestCase (- assertEqual "Lists should be the same size"- (Just $ DI.UnboxedColumn (VU.fromList [(6 :: Int)..10]))- (DI.getColumn "newer" $ D.insertVectorWithDefault 0 "newer" (V.fromList [(6 :: Int)..10]) $ D.insertVector "new" (V.fromList [1 :: Int, 2, 3, 4, 5]) D.empty)- )+insertColumnWithDefaultFillsLargerNoop =+ TestCase+ ( assertEqual+ "Lists should be the same size"+ (Just $ DI.UnboxedColumn (VU.fromList [(6 :: Int) .. 10]))+ (DI.getColumn "newer" $ D.insertVectorWithDefault 0 "newer" (V.fromList [(6 :: Int) .. 10]) $ D.insertVector "new" (V.fromList [1 :: Int, 2, 3, 4, 5]) D.empty)+ ) addLargerColumnBoxed :: Test addLargerColumnBoxed =- TestCase (assertEqual "Smaller lists should grow and contain optionals"- (D.fromNamedColumns [("new", D.fromList [Just "a" :: Maybe T.Text, Just "b", Just "c", Nothing, Nothing]),- ("newer", D.fromList ["a" :: T.Text, "b", "c", "d", "e"])])- (D.insertVector "newer" (V.fromList ["a" :: T.Text, "b", "c", "d", "e"])- $ D.insertVector "new" (V.fromList ["a" :: T.Text, "b", "c"]) D.empty))+ TestCase+ ( assertEqual+ "Smaller lists should grow and contain optionals"+ ( D.fromNamedColumns+ [ ("new", D.fromList [Just "a" :: Maybe T.Text, Just "b", Just "c", Nothing, Nothing])+ , ("newer", D.fromList ["a" :: T.Text, "b", "c", "d", "e"])+ ]+ )+ ( D.insertVector "newer" (V.fromList ["a" :: T.Text, "b", "c", "d", "e"]) $+ D.insertVector "new" (V.fromList ["a" :: T.Text, "b", "c"]) D.empty+ )+ ) addLargerColumnUnboxed :: Test addLargerColumnUnboxed =- TestCase (assertEqual "Smaller lists should grow and contain optionals"- (D.fromNamedColumns [("old", D.fromList [Just 1 :: Maybe Int, Just 2, Nothing, Nothing, Nothing]),- ("new", D.fromList [Just 1 :: Maybe Int, Just 2, Just 3, Nothing, Nothing]),- ("newer", D.fromList [1 :: Int, 2, 3, 4, 5])])- (D.insertVector "newer" (V.fromList [1 :: Int, 2, 3, 4, 5])- $ D.insertVector "new" (V.fromList [1 :: Int, 2, 3]) $ - D.insertVector "old" (V.fromList [1 :: Int, 2]) D.empty))+ TestCase+ ( assertEqual+ "Smaller lists should grow and contain optionals"+ ( D.fromNamedColumns+ [ ("old", D.fromList [Just 1 :: Maybe Int, Just 2, Nothing, Nothing, Nothing])+ , ("new", D.fromList [Just 1 :: Maybe Int, Just 2, Just 3, Nothing, Nothing])+ , ("newer", D.fromList [1 :: Int, 2, 3, 4, 5])+ ]+ )+ ( D.insertVector "newer" (V.fromList [1 :: Int, 2, 3, 4, 5]) $+ D.insertVector "new" (V.fromList [1 :: Int, 2, 3]) $+ D.insertVector "old" (V.fromList [1 :: Int, 2]) D.empty+ )+ ) dimensionsChangeAfterAdd :: Test-dimensionsChangeAfterAdd = TestCase (assertEqual "should be (26, 3)"- (26, 9)- (D.dimensions $ D.insertVector @Int "new" (V.fromList [1..26]) testData))+dimensionsChangeAfterAdd =+ TestCase+ ( assertEqual+ "should be (26, 3)"+ (26, 9)+ (D.dimensions $ D.insertVector @Int "new" (V.fromList [1 .. 26]) testData)+ ) dimensionsNotChangedAfterDuplicate :: Test-dimensionsNotChangedAfterDuplicate = TestCase (assertEqual "should be (26, 3)"- (26, 9)- (D.dimensions $ D.insertVector @Int "new" (V.fromList [1..26])- $ D.insertVector @Int "new" (V.fromList [1..26]) testData))-+dimensionsNotChangedAfterDuplicate =+ TestCase+ ( assertEqual+ "should be (26, 3)"+ (26, 9)+ ( D.dimensions $+ D.insertVector @Int "new" (V.fromList [1 .. 26]) $+ D.insertVector @Int "new" (V.fromList [1 .. 26]) testData+ )+ ) tests :: [Test]-tests = [- TestLabel "dimensionsChangeAfterAdd" dimensionsChangeAfterAdd- , TestLabel "dimensionsNotChangedAfterDuplicate" dimensionsNotChangedAfterDuplicate- , TestLabel "addBoxedColunmToEmpty" addBoxedColumn- , TestLabel "addBoxedColumnAutoUnboxes" addBoxedColumn- , TestLabel "addSmallerColumnBoxed" addSmallerColumnBoxed- , TestLabel "addSmallerColumnUnboxed" addSmallerColumnUnboxed- , TestLabel "addLargerColumnBoxed" addLargerColumnBoxed- , TestLabel "addLargerColumnUnboxed" addLargerColumnUnboxed- , TestLabel "insertColumnWithDefaultFillsWithDefault" insertColumnWithDefaultFillsWithDefault- , TestLabel "insertColumnWithDefaultFillsLargerNoop" insertColumnWithDefaultFillsLargerNoop- ]+tests =+ [ TestLabel "dimensionsChangeAfterAdd" dimensionsChangeAfterAdd+ , TestLabel "dimensionsNotChangedAfterDuplicate" dimensionsNotChangedAfterDuplicate+ , TestLabel "addBoxedColunmToEmpty" addBoxedColumn+ , TestLabel "addBoxedColumnAutoUnboxes" addBoxedColumn+ , TestLabel "addSmallerColumnBoxed" addSmallerColumnBoxed+ , TestLabel "addSmallerColumnUnboxed" addSmallerColumnUnboxed+ , TestLabel "addLargerColumnBoxed" addLargerColumnBoxed+ , TestLabel "addLargerColumnUnboxed" addLargerColumnUnboxed+ , TestLabel "insertColumnWithDefaultFillsWithDefault" insertColumnWithDefaultFillsWithDefault+ , TestLabel "insertColumnWithDefaultFillsLargerNoop" insertColumnWithDefaultFillsLargerNoop+ ]
tests/Operations/Sort.hs view
@@ -1,50 +1,72 @@ {-# LANGUAGE OverloadedStrings #-}-module Operations.Sort where -import qualified DataFrame as D-import qualified DataFrame as DI-import qualified DataFrame as DE-import qualified Data.Text as T-import qualified Data.Vector as V-import qualified Data.Vector.Unboxed as VU+module Operations.Sort where import Assertions import Control.Monad import Data.Char+import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import qualified DataFrame as D+import qualified DataFrame.Internal.Column as D+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Internal.DataFrame as D+import qualified DataFrame.Internal.DataFrame as DI import System.Random import System.Random.Shuffle (shuffle') import Test.HUnit values :: [(T.Text, DI.Column)]-values = let- ns = shuffle' [(1::Int)..26] 26 $ mkStdGen 252- in [ ("test1", DI.fromList ns)- , ("test2", DI.fromList (map (chr . (+96)) ns))- ]+values =+ let+ ns = shuffle' [(1 :: Int) .. 26] 26 $ mkStdGen 252+ in+ [ ("test1", DI.fromList ns)+ , ("test2", DI.fromList (map (chr . (+ 96)) ns))+ ] testData :: D.DataFrame testData = D.fromNamedColumns values sortByAscendingWAI :: Test-sortByAscendingWAI = TestCase (assertEqual "Sorting rows by ascending works as intended"- (D.fromNamedColumns [("test1", DI.fromList [(1::Int)..26]),- ("test2", DI.fromList ['a'..'z'])])- (D.sortBy D.Ascending ["test1"] testData))+sortByAscendingWAI =+ TestCase+ ( assertEqual+ "Sorting rows by ascending works as intended"+ ( D.fromNamedColumns+ [ ("test1", DI.fromList [(1 :: Int) .. 26])+ , ("test2", DI.fromList ['a' .. 'z'])+ ]+ )+ (D.sortBy D.Ascending ["test1"] testData)+ ) sortByDescendingWAI :: Test-sortByDescendingWAI = TestCase (assertEqual "Sorting rows by descending works as intended"- (D.fromNamedColumns [("test1", DI.fromList $ reverse [(1::Int)..26]),- ("test2", DI.fromList $ reverse ['a'..'z'])])- (D.sortBy D.Descending ["test1"] testData))+sortByDescendingWAI =+ TestCase+ ( assertEqual+ "Sorting rows by descending works as intended"+ ( D.fromNamedColumns+ [ ("test1", DI.fromList $ reverse [(1 :: Int) .. 26])+ , ("test2", DI.fromList $ reverse ['a' .. 'z'])+ ]+ )+ (D.sortBy D.Descending ["test1"] testData)+ ) sortByColumnDoesNotExist :: Test-sortByColumnDoesNotExist = TestCase (assertExpectException "[Error Case]"- (DE.columnNotFound "[\"test0\"]" "sortBy" (D.columnNames testData))- (print $ D.sortBy D.Ascending ["test0"] testData))+sortByColumnDoesNotExist =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (D.columnNotFound "[\"test0\"]" "sortBy" (D.columnNames testData))+ (print $ D.sortBy D.Ascending ["test0"] testData)+ ) tests :: [Test]-tests = [ TestLabel "sortByAscendingWAI" sortByAscendingWAI- , TestLabel "sortByDescendingWAI" sortByDescendingWAI- , TestLabel "sortByColumnDoesNotExist" sortByColumnDoesNotExist- ]-+tests =+ [ TestLabel "sortByAscendingWAI" sortByAscendingWAI+ , TestLabel "sortByDescendingWAI" sortByDescendingWAI+ , TestLabel "sortByColumnDoesNotExist" sortByColumnDoesNotExist+ ]
+ tests/Operations/Statistics.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Operations.Statistics where++import qualified Data.Vector.Unboxed as VU+import qualified DataFrame as D+import qualified DataFrame.Internal.Column as D+import qualified DataFrame.Internal.DataFrame as D+import qualified DataFrame.Operations.Statistics as D++import Assertions+import Test.HUnit++medianOfOddLengthDataSet :: Test+medianOfOddLengthDataSet =+ TestCase+ ( assertEqual+ "Median of an odd length data set"+ (D.median' (VU.fromList [179.94, 231.94, 839.06, 534.23, 248.94]))+ 248.94+ )++medianOfEvenLengthDataSet :: Test+medianOfEvenLengthDataSet =+ TestCase+ ( assertEqual+ "Median of an even length data set"+ (D.median' (VU.fromList [179.94, 231.94, 839.06, 534.23, 248.94, 276.37]))+ 262.655+ )++medianOfEmptyDataSet :: Test+medianOfEmptyDataSet =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (D.emptyDataSetError "median")+ (print $ D.median' (VU.fromList []))+ )++tests :: [Test]+tests =+ [ TestLabel "medianOfOddLengthDataSet" medianOfOddLengthDataSet+ , TestLabel "medianOfEvenLengthDataSet" medianOfEvenLengthDataSet+ , TestLabel "medianOfEmptyDataSet" medianOfEmptyDataSet+ ]
tests/Operations/Take.hs view
@@ -1,22 +1,27 @@ {-# LANGUAGE OverloadedStrings #-}+ module Operations.Take where import qualified DataFrame as D-import qualified DataFrame as DI+import qualified DataFrame.Internal.Column as D+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Internal.DataFrame as D+import qualified DataFrame.Internal.DataFrame as DI import Test.HUnit testData :: D.DataFrame-testData = D.fromNamedColumns [ ("test1", DI.fromList ([1..26] :: [Int]))- , ("test2", DI.fromList ['a'..'z'])- ]-+testData =+ D.fromNamedColumns+ [ ("test1", DI.fromList ([1 .. 26] :: [Int]))+ , ("test2", DI.fromList ['a' .. 'z'])+ ] takeWAI :: Test-takeWAI = TestCase (assertEqual "Gets first 10 numbers" (Just $ D.fromList [(1 :: Int)..10]) (D.getColumn "test1" $ D.take 10 testData))+takeWAI = TestCase (assertEqual "Gets first 10 numbers" (Just $ D.fromList [(1 :: Int) .. 10]) (D.getColumn "test1" $ D.take 10 testData)) takeLastWAI :: Test-takeLastWAI = TestCase (assertEqual "Gets first 10 numbers" (Just $ D.fromList [(17 :: Int)..26]) (D.getColumn "test1" $ D.takeLast 10 testData))+takeLastWAI = TestCase (assertEqual "Gets first 10 numbers" (Just $ D.fromList [(17 :: Int) .. 26]) (D.getColumn "test1" $ D.takeLast 10 testData)) lengthEqualsTakeParam :: Test lengthEqualsTakeParam = TestCase (assertEqual "should be (5, 2)" (5, 2) (D.dimensions $ D.take 5 testData))@@ -43,14 +48,15 @@ negativeIsZeroTakeLast = TestCase (assertEqual "should be (0, 2)" (0, 2) (D.dimensions $ D.takeLast (-1) testData)) tests :: [Test]-tests = [ TestLabel "takeWAI" takeWAI- , TestLabel "takeLastWAI" takeLastWAI- , TestLabel "lengthEqualsTakeParam" lengthEqualsTakeParam- , TestLabel "lengthGreaterThanTakeParam" lengthGreaterThanTakeParam- , TestLabel "emptyIsZero" emptyIsZero- , TestLabel "negativeIsZero" negativeIsZero- , TestLabel "lengthEqualsTakeLastParam" lengthEqualsTakeLastParam- , TestLabel "lengthGreaterThanTakeLastParam" lengthGreaterThanTakeLastParam- , TestLabel "emptyIsZeroTakeLast" emptyIsZeroTakeLast- , TestLabel "negativeIsZeroTakeLast" negativeIsZeroTakeLast- ]+tests =+ [ TestLabel "takeWAI" takeWAI+ , TestLabel "takeLastWAI" takeLastWAI+ , TestLabel "lengthEqualsTakeParam" lengthEqualsTakeParam+ , TestLabel "lengthGreaterThanTakeParam" lengthGreaterThanTakeParam+ , TestLabel "emptyIsZero" emptyIsZero+ , TestLabel "negativeIsZero" negativeIsZero+ , TestLabel "lengthEqualsTakeLastParam" lengthEqualsTakeLastParam+ , TestLabel "lengthGreaterThanTakeLastParam" lengthGreaterThanTakeLastParam+ , TestLabel "emptyIsZeroTakeLast" emptyIsZeroTakeLast+ , TestLabel "negativeIsZeroTakeLast" negativeIsZeroTakeLast+ ]
tests/Parquet.hs view
@@ -1,17 +1,73 @@ {-# LANGUAGE OverloadedStrings #-}+ module Parquet where import qualified DataFrame as D -import Assertions-import GHC.IO (unsafePerformIO)-import Test.HUnit+import Assertions+import Data.Int+import Data.Text (Text)+import Data.Time+import Data.Time.Calendar+import GHC.IO (unsafePerformIO)+import Test.HUnit --- TODO: This currently fails+allTypes :: D.DataFrame+allTypes =+ D.fromNamedColumns+ [ ("id", D.fromList [(4 :: Int32), 5, 6, 7, 2, 3, 0, 1])+ , ("bool_col", D.fromList [True, False, True, False, True, False, True, False])+ , ("tinyint_col", D.fromList [(0 :: Int32), 1, 0, 1, 0, 1, 0, 1])+ , ("smallint_col", D.fromList [(0 :: Int32), 1, 0, 1, 0, 1, 0, 1])+ , ("int_col", D.fromList [(0 :: Int32), 1, 0, 1, 0, 1, 0, 1])+ , ("bigint_col", D.fromList [(0 :: Int64), 10, 0, 10, 0, 10, 0, 10])+ , ("float_col", D.fromList [(0 :: Float), 1.1, 0, 1.1, 0, 1.1, 0, 1.1])+ , ("double_col", D.fromList [(0 :: Double), 10.1, 0, 10.1, 0, 10.1, 0, 10.1])+ , ("date_string_col", D.fromList [("03/01/09" :: Text), "03/01/09", "04/01/09", "04/01/09", "02/01/09", "02/01/09", "01/01/09", "01/01/09"])+ , ("string_col", D.fromList (take 8 (cycle [("0" :: Text), "1"])))+ ,+ ( "timestamp_col"+ , D.fromList+ [ UTCTime (fromGregorian 2009 3 1) (secondsToDiffTime 0)+ , UTCTime (fromGregorian 2009 3 1) (secondsToDiffTime 60)+ , UTCTime (fromGregorian 2009 4 1) (secondsToDiffTime 0)+ , UTCTime (fromGregorian 2009 4 1) (secondsToDiffTime 60)+ , UTCTime (fromGregorian 2009 2 1) (secondsToDiffTime 0)+ , UTCTime (fromGregorian 2009 2 1) (secondsToDiffTime 60)+ , UTCTime (fromGregorian 2009 1 1) (secondsToDiffTime 0)+ , UTCTime (fromGregorian 2009 1 1) (secondsToDiffTime 60)+ ]+ )+ ]+ allTypesPlain :: Test-allTypesPlain = TestCase (assertEqual "allTypesPlain"- (unsafePerformIO (D.readCsv "./data/mtcars.csv"))- (unsafePerformIO (D.readParquet "./data/mtcars.parquet")))+allTypesPlain =+ TestCase+ ( assertEqual+ "allTypesPlain"+ allTypes+ (unsafePerformIO (D.readParquet "./tests/data/alltypes_plain.parquet"))+ ) +allTypesPlainSnappy :: Test+allTypesPlainSnappy =+ TestCase+ ( assertEqual+ "allTypesPlainSnappy"+ (D.filter "id" (`elem` [(6 :: Int32), 7]) allTypes)+ (unsafePerformIO (D.readParquet "./tests/data/alltypes_plain.snappy.parquet"))+ )++allTypesDictionary :: Test+allTypesDictionary =+ TestCase+ ( assertEqual+ "allTypesPlainSnappy"+ (D.filter "id" (`elem` [(0 :: Int32), 1]) allTypes)+ (unsafePerformIO (D.readParquet "./tests/data/alltypes_dictionary.parquet"))+ )++-- Uncomment to run parquet tests.+-- Currently commented because they don't run with github CI tests :: [Test]-tests = []+tests = [] -- [allTypesPlain, allTypesPlainSnappy, allTypesDictionary]