dataframe-lazy-2.4.2.0: src/DataFrame/Lazy/Internal/DataFrame.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module DataFrame.Lazy.Internal.DataFrame where
import qualified Data.Text as T
import DataFrame.IO.CSV (
CsvBytesReader,
CsvReader,
decodeSeparatedStrict,
readSeparated,
)
import qualified DataFrame.Internal.Column as C
import qualified DataFrame.Internal.DataFrame as D
import qualified DataFrame.Internal.Expression as E
import DataFrame.Lazy.Internal.Executor (execute)
import DataFrame.Lazy.Internal.LogicalPlan (
DataSource (..),
LogicalPlan (..),
SortOrder (..),
)
import qualified DataFrame.Lazy.Internal.Optimizer as Opt
import DataFrame.Operations.Join (JoinType)
import DataFrame.Schema (Schema)
{- | A lazy query that has not been executed yet: a 'LogicalPlan' tree whose
execution is deferred until 'runDataFrame' is called.
-}
data LazyDataFrame = LazyDataFrame
{ plan :: LogicalPlan
, batchSize :: Int
}
instance Show LazyDataFrame where
show ldf =
"LazyDataFrame { batchSize = "
<> (show (batchSize ldf) <> (", plan = " <> (show (plan ldf) <> " }")))
-- ---------------------------------------------------------------------------
-- Entry point
-- ---------------------------------------------------------------------------
{- | Execute the lazy query: optimise the logical plan, then stream-execute
the resulting physical plan into a fully-materialised 'D.DataFrame'.
-}
runDataFrame :: LazyDataFrame -> IO D.DataFrame
runDataFrame ldf = execute (Opt.optimize (batchSize ldf) (plan ldf))
-- ---------------------------------------------------------------------------
-- Builders that construct the logical plan tree
-- ---------------------------------------------------------------------------
-- | Lift an already-loaded eager 'D.DataFrame' into the lazy plan.
fromDataFrame :: D.DataFrame -> LazyDataFrame
fromDataFrame df = LazyDataFrame{plan = SourceDF df, batchSize = 1_000_000}
{- | Scan a CSV file with the default comma separator and the in-tree
strict reader. For the SIMD reader use 'scanCsvWith'.
The 'Schema' both types and selects: only the columns it names are read,
matching 'scanParquet'.
==== __Example__
@
ghci> schema = D.makeSchema [("id", D.schemaType \@Int), ("name", D.schemaType \@Text)]
ghci> L.runDataFrame (L.scanCsv schema \"customers.csv\")
@
-}
scanCsv :: Schema -> T.Text -> LazyDataFrame
scanCsv = scanCsvWith readSeparated
{- | Like 'scanCsv' but with an explicit CSV reader (e.g. the SIMD reader
@fastReadCsvWithOpts@ from @dataframe-fastcsv@). The scan derives the
reader's 'DataFrame.IO.CSV.ReadOptions' from the schema and separator, so
any 'CsvReader' projects.
==== __Example__
@
ghci> import qualified DataFrame.IO.CSV.Fast as Fast
ghci> L.runDataFrame (L.scanCsvWith Fast.fastReadCsvWithOpts schema \"customers.csv\")
@
-}
scanCsvWith :: CsvReader -> Schema -> T.Text -> LazyDataFrame
scanCsvWith reader schema path =
LazyDataFrame
{ plan = Scan (CsvSource (T.unpack path) ',' reader) schema
, batchSize = 1_000_000
}
{- | Scan a CSV file in bounded-memory windows with the default in-tree
strict reader. Windows are decoded directly from memory, without temporary
files.
Use this for files too large to hold in memory even after the schema's
projection.
==== __Example__
@
ghci> L.runDataFrame (L.scanCsvStreaming schema "huge.csv")
@
-}
scanCsvStreaming :: Schema -> T.Text -> LazyDataFrame
scanCsvStreaming = scanCsvStreamingBytesWith decodeSeparatedStrict
{- | Like 'scanCsvWith', but the file is read in bounded-memory windows.
This compatibility entry point accepts an existing path-based 'CsvReader'.
Because such a reader can only consume file paths, each window is staged in a
temporary file. New readers should use 'scanCsvStreamingBytesWith' to decode
windows directly from memory.
==== __Example__
@
ghci> L.runDataFrame (L.scanCsvStreamingWith Fast.fastReadCsvWithOpts schema \"huge.csv\")
@
-}
scanCsvStreamingWith :: CsvReader -> Schema -> T.Text -> LazyDataFrame
scanCsvStreamingWith reader schema path =
LazyDataFrame
{ plan = Scan (CsvSourceStreaming (T.unpack path) ',' reader) schema
, batchSize = 1_000_000
}
{- | Stream a CSV file in bounded-memory windows decoded by an in-memory
'CsvBytesReader'. This avoids the temporary-file staging required by
'scanCsvStreamingWith'.
==== __Example__
@
ghci> L.runDataFrame (L.scanCsvStreamingBytesWith decodeSeparatedStrict schema "huge.csv")
@
-}
scanCsvStreamingBytesWith :: CsvBytesReader -> Schema -> T.Text -> LazyDataFrame
scanCsvStreamingBytesWith reader schema path =
LazyDataFrame
{ plan = Scan (CsvSourceStreamingBytes (T.unpack path) ',' reader) schema
, batchSize = 1_000_000
}
{- | Scan a character-separated file with the default strict reader.
==== __Example__
@
ghci> L.runDataFrame (L.scanSeparated ';' schema \"customers.txt\")
@
-}
scanSeparated :: Char -> Schema -> T.Text -> LazyDataFrame
scanSeparated = scanSeparatedWith readSeparated
{- | Like 'scanSeparated' but with an explicit CSV reader.
==== __Example__
@
ghci> L.runDataFrame (L.scanSeparatedWith Fast.fastReadCsvWithOpts ';' schema \"customers.txt\")
@
-}
scanSeparatedWith ::
CsvReader -> Char -> Schema -> T.Text -> LazyDataFrame
scanSeparatedWith reader sep schema path =
LazyDataFrame
{ plan = Scan (CsvSource (T.unpack path) sep reader) schema
, batchSize = 1_000_000
}
-- | Scan a Parquet file, directory of files, or glob pattern.
scanParquet :: Schema -> T.Text -> LazyDataFrame
scanParquet schema path =
LazyDataFrame
{ plan = Scan (ParquetSource (T.unpack path)) schema
, batchSize = 1_000_000
}
-- | Add a computed column (or overwrite an existing one).
derive ::
(C.Columnable a) => T.Text -> E.Expr a -> LazyDataFrame -> LazyDataFrame
derive name expr ldf =
ldf{plan = Derive name (E.UExpr expr) (plan ldf)}
-- | Retain only the listed columns.
select :: [T.Text] -> LazyDataFrame -> LazyDataFrame
select cols ldf = ldf{plan = Project cols (plan ldf)}
-- | Keep rows that satisfy the predicate.
filter :: E.Expr Bool -> LazyDataFrame -> LazyDataFrame
filter cond ldf = ldf{plan = Filter cond (plan ldf)}
-- | Join two lazy queries on the given key columns.
join ::
JoinType ->
-- | Left join key column name
T.Text ->
-- | Right join key column name
T.Text ->
-- | Left sub-query
LazyDataFrame ->
-- | Right sub-query
LazyDataFrame ->
LazyDataFrame
join jt leftKey rightKey left right =
LazyDataFrame
{ plan = Join jt leftKey rightKey (plan left) (plan right)
, batchSize = batchSize left
}
{- | Group by a set of columns and compute aggregate expressions.
Each aggregate expression should use an 'Agg' node (e.g. @sumOf@, @meanOf@).
-}
groupBy ::
-- | Group-by key columns
[T.Text] ->
-- | @[(outputName, aggregateExpr)]@
[(T.Text, E.UExpr)] ->
LazyDataFrame ->
LazyDataFrame
groupBy keys aggs ldf = ldf{plan = Aggregate keys aggs (plan ldf)}
-- | Sort the result by the given @(column, direction)@ pairs.
sortBy :: [(T.Text, SortOrder)] -> LazyDataFrame -> LazyDataFrame
sortBy cols ldf = ldf{plan = Sort cols (plan ldf)}
-- | Retain at most @n@ rows.
take :: Int -> LazyDataFrame -> LazyDataFrame
take n ldf = ldf{plan = Limit n (plan ldf)}