dataframe 0.7.0.0 → 3.0.0.0
raw patch · 152 files changed
Files
- CHANGELOG.md +141/−0
- README.md +541/−42
- app/LazyBenchmark.hs +16/−46
- app/Main.hs +1/−1
- app/Synthesis.hs +101/−68
- benchmark/Main.hs +12/−17
- cbits/arrow_abi.h +44/−0
- cbits/process_csv.c +0/−198
- cbits/process_csv.h +0/−33
- data/ml/blobs.csv +151/−0
- data/ml/golden.json +68/−0
- data/ml/iris.csv +151/−0
- data/ml/iris_binary.csv +151/−0
- data/ml/regression.csv +443/−0
- data/sharded/part-0.parquet binary
- data/sharded/part-1.parquet binary
- dataframe.cabal +283/−121
- ffi/DataFrame/IO/Arrow.hs +552/−0
- ffi/DataFrame/IR.hs +474/−0
- src/DataFrame.hs +159/−223
- src/DataFrame/DecisionTree.hs +0/−761
- src/DataFrame/Display.hs +0/−26
- src/DataFrame/Display/Terminal/Colours.hs +0/−14
- src/DataFrame/Display/Terminal/Plot.hs +0/−615
- src/DataFrame/Display/Terminal/PrettyPrint.hs +0/−78
- src/DataFrame/Display/Web/Plot.hs +0/−1051
- src/DataFrame/Errors.hs +0/−156
- src/DataFrame/Functions.hs +0/−525
- src/DataFrame/IO/CSV.hs +0/−615
- src/DataFrame/IO/JSON.hs +0/−133
- src/DataFrame/IO/Parquet.hs +0/−584
- src/DataFrame/IO/Parquet/Binary.hs +0/−177
- src/DataFrame/IO/Parquet/ColumnStatistics.hs +0/−19
- src/DataFrame/IO/Parquet/Compression.hs +0/−26
- src/DataFrame/IO/Parquet/Dictionary.hs +0/−303
- src/DataFrame/IO/Parquet/Encoding.hs +0/−90
- src/DataFrame/IO/Parquet/Levels.hs +0/−184
- src/DataFrame/IO/Parquet/Page.hs +0/−441
- src/DataFrame/IO/Parquet/Thrift.hs +0/−1184
- src/DataFrame/IO/Parquet/Time.hs +0/−62
- src/DataFrame/IO/Parquet/Types.hs +0/−309
- src/DataFrame/IO/Unstable/CSV.hs +0/−350
- src/DataFrame/Internal/Column.hs +0/−1661
- src/DataFrame/Internal/DataFrame.hs +0/−175
- src/DataFrame/Internal/Expression.hs +0/−395
- src/DataFrame/Internal/Interpreter.hs +0/−486
- src/DataFrame/Internal/Parsing.hs +0/−232
- src/DataFrame/Internal/Row.hs +0/−224
- src/DataFrame/Internal/Schema.hs +0/−98
- src/DataFrame/Internal/Statistics.hs +0/−282
- src/DataFrame/Internal/Types.hs +0/−129
- src/DataFrame/Lazy.hs +0/−3
- src/DataFrame/Lazy/IO/Binary.hs +0/−444
- src/DataFrame/Lazy/IO/CSV.hs +0/−452
- src/DataFrame/Lazy/Internal/DataFrame.hs +0/−138
- src/DataFrame/Lazy/Internal/Executor.hs +0/−543
- src/DataFrame/Lazy/Internal/LogicalPlan.hs +0/−44
- src/DataFrame/Lazy/Internal/Optimizer.hs +0/−209
- src/DataFrame/Lazy/Internal/PhysicalPlan.hs +0/−36
- src/DataFrame/Monad.hs +0/−93
- src/DataFrame/Operations/Aggregation.hs +0/−312
- src/DataFrame/Operations/Core.hs +0/−947
- src/DataFrame/Operations/Join.hs +0/−1065
- src/DataFrame/Operations/Merge.hs +0/−64
- src/DataFrame/Operations/Permutation.hs +0/−99
- src/DataFrame/Operations/Statistics.hs +0/−389
- src/DataFrame/Operations/Subset.hs +0/−469
- src/DataFrame/Operations/Transformations.hs +0/−207
- src/DataFrame/Operations/Typing.hs +0/−215
- src/DataFrame/Operators.hs +0/−161
- src/DataFrame/Synthesis.hs +0/−484
- src/DataFrame/TH.hs +35/−0
- src/DataFrame/Typed.hs +198/−16
- src/DataFrame/Typed/Access.hs +0/−55
- src/DataFrame/Typed/Aggregate.hs +0/−97
- src/DataFrame/Typed/Expr.hs +0/−276
- src/DataFrame/Typed/Freeze.hs +0/−86
- src/DataFrame/Typed/Join.hs +0/−72
- src/DataFrame/Typed/Operations.hs +0/−378
- src/DataFrame/Typed/Schema.hs +0/−351
- src/DataFrame/Typed/TH.hs +27/−88
- src/DataFrame/Typed/Types.hs +0/−117
- tests/GenDataFrame.hs +5/−4
- tests/IO/CSV.hs +97/−0
- tests/IO/CsvGolden.hs +556/−0
- tests/IR/ExprJsonRoundtrip.hs +209/−0
- tests/Internal/ColumnBuilder.hs +298/−0
- tests/Internal/DictEncode.hs +144/−0
- tests/Internal/Markdown.hs +45/−0
- tests/Internal/PackedText.hs +186/−0
- tests/LazyParity.hs +146/−0
- tests/LazyParquet.hs +3/−3
- tests/Learn/Denotation.hs +94/−0
- tests/Learn/Ensembles.hs +163/−0
- tests/Learn/Metamorphic.hs +366/−0
- tests/Learn/MetricsTests.hs +116/−0
- tests/Learn/Models.hs +250/−0
- tests/Learn/Segmented.hs +316/−0
- tests/Learn/SklearnParity.hs +186/−0
- tests/Learn/Synthesis.hs +83/−0
- tests/Learn/TypedModel.hs +113/−0
- tests/Main.hs +79/−4
- tests/Monad.hs +6/−2
- tests/Operations/Aggregations.hs +48/−2
- tests/Operations/Apply.hs +46/−8
- tests/Operations/Derive.hs +3/−2
- tests/Operations/GroupBy.hs +65/−1
- tests/Operations/Inference.hs +229/−0
- tests/Operations/InsertColumn.hs +8/−6
- tests/Operations/Join.hs +202/−2
- tests/Operations/Nullable.hs +773/−0
- tests/Operations/NullableHashing.hs +153/−0
- tests/Operations/ParallelGroupBy.hs +240/−0
- tests/Operations/ParallelJoin.hs +213/−0
- tests/Operations/Provenance.hs +235/−0
- tests/Operations/ReadCsv.hs +514/−119
- tests/Operations/Record.hs +374/−0
- tests/Operations/SetOps.hs +111/−0
- tests/Operations/Sort.hs +73/−1
- tests/Operations/Statistics.hs +1/−1
- tests/Operations/Subset.hs +116/−2
- tests/Operations/Typing.hs +1502/−5054
- tests/Operations/VectorKernel.hs +136/−0
- tests/Operations/Window.hs +247/−0
- tests/Operations/WriteCsv.hs +88/−0
- tests/PackedTextMain.hs +12/−0
- tests/PackedTextMigration.hs +115/−0
- tests/Parquet.hs +1432/−1700
- tests/ParquetTestData.hs +727/−0
- tests/Plotting.hs +233/−0
- tests/PrettyPrint.hs +70/−0
- tests/Properties/Categorical.hs +188/−0
- tests/Simplify.hs +363/−0
- tests/Typed/IOReaders.hs +65/−0
- tests/Typed/Parity.hs +205/−0
- tests/data/timestamp_nanos.parquet binary
- tests/data/typing/texts.txt +34/−0
- tests/data/typing/texts_with_empties.txt +41/−0
- tests/data/typing/texts_with_empties_and_nullish.txt +44/−0
- tests/data/unstable_csv/all_empty_row.csv +2/−0
- tests/data/unstable_csv/crlf.csv +3/−0
- tests/data/unstable_csv/crlf.tsv +2/−0
- tests/data/unstable_csv/either_read_mixed.csv +9/−0
- tests/data/unstable_csv/empty_file.csv +0/−0
- tests/data/unstable_csv/extra_fields.csv +2/−0
- tests/data/unstable_csv/header_only.csv +1/−0
- tests/data/unstable_csv/missing_fields.csv +3/−0
- tests/data/unstable_csv/no_trailing_newline.csv +2/−0
- tests/data/unstable_csv/single_col.csv +4/−0
- tests/data/unstable_csv/trailing_blank_line.csv +3/−0
- tests/data/unstable_csv/utf8_bom.csv +3/−0
- tests/data/unstable_csv/whitespace_fields.csv +3/−0
CHANGELOG.md view
@@ -1,5 +1,146 @@ # Revision history for dataframe +## 3.0.0.0++A breaking release that removes internal functions from the public API.++### Highlights+* Internalish modules now have public APIs/contracts. `DataFrame.Core`, `DataFrame.Schema`, and `DataFrame.Learn`.+* `import DataFrame` now surfaces `Columnable`, `Columnable'`, `eSize`, `NamedExpr`,+ `SchemaType(..)`, and the `Semigroup`/`Monoid DataFrame` instances directly.++### Breaking changes+* Internal modules are no longer on the public surface.+* Subpackage major bumps.++## 2.3.0.0++### Breaking changes+* HuggingFace (`hf://`) Parquet reading moved out of `dataframe-parquet` and+ `dataframe-lazy` into a new dedicated `dataframe-huggingface` package, so the+ heavy `aeson` and `http-conduit` dependencies are no longer pulled in by the+ Parquet/lazy stack (or by this meta-package). `D.readParquet "hf://..."` no+ longer fetches; depend on `dataframe-huggingface` and use+ `DataFrame.IO.HuggingFace.readParquet` (eager) or `scanParquet` (lazy,+ download-then-scan) instead.+* Coordinated major bumps: `dataframe-parquet` and `dataframe-lazy` →+ `1.1.0.0`, with inter-package lower bounds raised to `^>= 1.1`; umbrella+ `dataframe` → `2.3.0.0`.++## 2.2.0.0++A large performance and ML release.++### Highlights+* **Much faster I/O and analytics.** CSV reading, group-by, joins and sorting were rebuilt on compact unboxed / `PackedText` columns, parallel open-addressing hash tables, and vectorized aggregation; the default reader drops `cassava` for a single-pass scanner and `dataframe-fastcsv` adds multicore chunking. The end-to-end join + group-by pipeline runs several times faster with much lower memory and scales with `-threaded` / `+RTS -N`; results are byte-identical (golden-tested).+* **Lazy engine on par with eager** — bounded-source queries route through the same fast paths (streaming preserved for unbounded); lazy `sortBy` now also orders non-`Text` columns correctly.+* **New ML library (`dataframe-learn`).** scikit-learn-style estimators behind a uniform `fit` / `predict`: linear / ridge / lasso / logistic regression, SVM, trees, boosting, k-means, GMM, DBSCAN, PCA and kernel PCA, symbolic regression and feature synthesis, with metrics and cross-validation. Pure and deterministic; every model also compiles to a dataframe `Expr`.+* **Typed joins are checked at compile time** — keys must exist in both schemas with matching types (previously a runtime failure or silent empty result).++### Breaking changes+* The `Column` GADT gains a `PackedText` constructor — exhaustive matches need a new arm (`materializePacked` decodes it back to boxed `Text`). CSV reads are now strict / fully forced; schema columns parse as their declared type; ragged rows pad with null instead of silently misaligning columns; overflowing integers parse as `Double`. `dataframe-learn`'s old beam-search synthesis and per-model `fit*` helpers are replaced by `fit` / `predict`.+* Coordinated major bumps (`dataframe-core`, `dataframe-learn` → `1.1.0.0`; umbrella `dataframe` → `2.2.0.0`) with inter-package lower bounds tightened so a newer package cannot resolve against an incompatible sibling. Drops `cassava` and `unordered-containers`; requires `text >= 2.1`.++## 2.1.0.3+### Packaging+* Fix dependency resolution for the `dataframe` meta-package and its satellites+ so a clean `cabal install dataframe` resolves and builds. `dataframe-core+ 1.0.2.0` introduced the `UnaryOp`/`BinaryOp` typeclasses whose method names+ overlap the `UnUDF`/`BinUDF` record fields; only `dataframe-operations >= 1.1`+ enables the disambiguation. The bounds now keep the two in lockstep:+ `dataframe-operations 1.1.0.2` requires `dataframe-core >= 1.0.2 && < 1.1`,+ and `dataframe-parquet 1.0.1.1` / `dataframe-th 1.0.1.1` publish their+ `dataframe-operations ^>= 1.1` bound. The meta re-pins its lower bounds to the+ fixed satellite versions.++## 0.1.3.0.0+### New features+* New `DataFrame.Typed.TH.deriveSchemaFromType` Template Haskell splice generates a typed schema synonym and a `HasSchema` instance from a Haskell record ADT. Pair with `DataFrame.fromRecords` / `DataFrame.toRecords` (or `DataFrame.Typed.fromRecordsTyped` / `toRecordsTyped`) to convert between `[Order]` and `DataFrame`/`TypedDataFrame OrderSchema`. Field names are translated `camelCase → snake_case` by default; the transform is configurable via `SchemaOptions`.+* New `DataFrame.Typed.Generic` exposes `SchemaOf`/`SchemaOfRaw` plus `genericToColumns` / `genericFromColumns`, so users who prefer `GHC.Generics` over a TH splice can derive the same schema and row bridge.+* New `DataFrame.Internal.Schema.deriveSchema` Template Haskell splice generates, from a record ADT, both a runtime `Schema` value (`orderSchema :: Schema`, suitable for `readCsvWithSchema` / `readCsvWithOpts`) and one `Expr` accessor per field (`orderCustomerId :: Expr Int`, etc.), so expression-DSL code can refer to columns by typed name without writing `col @T "snake_case_name"` at each call site. Re-exported from `DataFrame`.++### Refactor+* The untyped Template Haskell splices (`declareColumns`, `declareColumnsFromCsvFile`, `declareColumnsFromCsvWithOpts`, `declareColumnsFromParquetFile`, `declareColumnsWithPrefix`, `declareColumnsWithPrefix'`) have moved from `DataFrame.Functions` to a new `DataFrame.TH` module (re-exported from `DataFrame`). Update imports accordingly; the bundled `dataframe.ghci` already points to the new module.++## 1.2.0.0+### Breaking changes+* Remove `Eq` and `Ord` instances for `Expr` — they were buggy under cross-type comparison. Use `eqExpr` / `compareExpr` from `DataFrame.Internal.Expression` directly.+* Lazy executor: `ExecutorConfig` removed; `execute` now takes only a `PhysicalPlan`. `CsvSource` carries the `CsvReader` per scan, so the executor no longer has to know about the CSV implementation.++### New features+* New `dataframe-fusion` package: typed Apache DataFusion-backed query API (`DataFrame.Fusion.Typed`).+* Can now use any CSV reader for the lazy pipeline.+* New `readCsvWithSchema :: Schema -> FilePath -> IO DataFrame` for schema-driven CSV reads, plus a `CsvReader` type alias exported from `DataFrame.IO.CSV`.+* Numeric comparison operators (`.==`, `./=`, `.<`, `.>`, `.<=`, `.>=`) now widen their operands to a common numeric type, so e.g. `Expr Double .== Expr Int` typechecks. Mirrors the existing arithmetic widening.++### Internal+* Bump `cabal-version` to 3.0; add `dataframe-fastcsv` to the lint targets; nix flake and `cabal.project` improvements.+* `dataframe-persistent` license corrected to MIT in cabal (#201).++## 1.1.2.1+* Add `over` and `median` functions to typed API+* Fix bug opening web plots in MacOS++## 1.1.2.0+* Safe read can now choose between Either and Maybe for error handling.+* Add countAll and over (window) functions.+* Add mkRandom to make random columns.+* Faster CSV parsing.++## 1.1.1.0+### New features+* Add `DataFrame.Typed.Lazy` module — a type-safe lazy query pipeline combining compile-time schema tracking with deferred execution.+* Add `fromCsv` function for parsing a CSV string directly into a DataFrame.+* Add `DataKinds` extension and `DataFrame.Typed` import to the GHCi file for easier interactive typed dataframe workflows.++### Performance+* Specialize and inline aggregation functions (`sum`, `mean`, `variance`, `median`, `stddev`, etc.) to avoid expensive numeric conversions at runtime.+* Remove `Ord` constraint from `Columnable'` and move it to call sites, reducing unnecessary constraint propagation.+* Replace exponential type-level `If` nesting in typed schema families with linear helper type families, fixing slow compilation times.++### Bug fixes+* Fix Functions module compilation under GHC 9.10 (#194).+* Document and test `safeColumns` option in `ParquetReadOptions` (#190).++## 1.1.0.0+### Breaking changes+* Remove `OptionalColumn` constructor; fold nullability into `BoxedColumn`/`UnboxedColumn` via bit-packed bitmap.+* Remove `NFData` instance from `Columnable` constraint.++### New features+* Add `toCsv` and `toSeparated` for converting a DataFrame to CSV/delimited text without writing to a file.+* `safeRead` now defaults reading columns to `Maybe a`.+* Split SIMD CSV reader into a separate `dataframe-fastcsv` package.++### Bug fixes+* Fix joins for missing key columns (#187).+* Fix single column not found error when using typed dataframe.+* Fix Synthesis to use `SafeLookup` constraint.+* Fix `writeSeparated` ignoring separator parameter (was hardcoded to comma).++### Internal+* Slice groups now use custom backpermute instead of converting unboxed vectors.+* Reuse comparison operators in Subset.+* Refactor `getRowAsText` for readability using pattern guards.++## 1.0.0.1+* toMarkdownTable is now toMarkdown (mostly used internally)+* Provide toMarkdown' that outputs string+* Add associativity to nullable operators+* Better null dataframe handling/error messages for core operations.+* Fix some function display names.+* Examples now build with CI++## 1.0.0.0+* Fix mappend to respect schema of empty columns.+* Add cast operators that force column schema+* Add null aware operators so some operations are easier.+* Add arrow shim with python example.+* Add numeric promotion for numeric operations.+* Add column provenance tracking+* Add stratified sampling+* Read files from hugging face+ ## 0.7.0.0 * This release adds A LOT of AI code to the repo (which we'll now pause in favour of refactoring, testing, and completeness for 1.0) * The lazy reader now has a custom binary format that it spills to. This almost halved the time it takes to run the 1 billion row challenge. The lazy evaluation now also supports Parquet.
README.md view
@@ -1,3 +1,15 @@+<!--+ This file is the runnable scripths source for the project README.+ Every ```haskell block below executes in order in a single shared session+ (later blocks see bindings from earlier blocks), and scripths inserts each+ block's output beneath it as a blockquote.++ Regenerate the rendered README with:+ scripths docs/base_scripts/base_readme.md -o README.md+ (run from the repo root, so the ./data/... paths resolve).++ Blocks tagged ```text are intentional compile-error demos and are NOT run.+--> <h1 align="center"> <a href="https://dataframe.readthedocs.io/en/latest/"> <img width="100" height="100" src="https://raw.githubusercontent.com/mchav/dataframe/master/docs/_static/haskell-logo.svg" alt="dataframe logo">@@ -21,58 +33,545 @@ # DataFrame -A fast, safe, and intuitive DataFrame library.+Tabular data analysis in Haskell. Read CSV, Parquet, and JSON files, transform columns with a typed expression DSL, and optionally lock down your entire schema at the type level for compile-time safety. -## Why use this DataFrame library?+The library ships three API layers — all operating on the same underlying `DataFrame` type at runtime: -* Encourages concise, declarative, and composable data pipelines.-* Lets you opt into your preferred level of type safety: keep it lightweight for rapid exploration or lock it down completely for robust production pipelines.-* Delivers high performance thanks to Haskell’s optimizing compiler and efficient memory model.-* Designed for interactivity: expressive syntax, helpful error messages, and sensible defaults.-* Works seamlessly in both command-line and notebook environments—great for exploration and scripting alike.+- **Untyped** (`import qualified DataFrame as D`) — string-based column names, great for exploration and scripting.+- **Typed** (`import qualified DataFrame.Typed as T`) — phantom-type schema tracking with compile-time column validation.+- **Monadic API** — write your transformation as a self contained pipeline. -## Features-- Type-safe column operations with compile-time guarantees-- Familiar, approachable API designed to feel easy coming from other languages.-- Interactive REPL for data exploration and plotting.+> **This README is a runnable [scripths](https://github.com/DataHaskell/scripths) notebook.** Every Haskell block runs top-to-bottom in one shared session against the datasets in [`./data`](./data). Reproduce every output below with `scripths docs/base_scripts/base_readme.md -o README.md` from the repo root. -## Quick start-Browse through some examples in [binder](https://mybinder.org/v2/gh/mchav/ihaskell-dataframe/HEAD).+## Why this library? +* Concise, declarative, composable data pipelines using the `|>` pipe operator.+* Choose your level of type safety: keep it lightweight for quick analysis, or lock it down for production pipelines.+* High performance from Haskell's optimizing compiler and an efficient columnar memory model with bitmap-backed nullability.+* Designed for interactivity: a custom REPL, IHaskell notebook support, terminal and web plotting, and helpful error messages.+ ## Install-See the [Quick Start](https://dataframe.readthedocs.io/en/latest/quick_start.html) guide for setup and installation instructions. -## Example +```bash+cabal update+cabal install dataframe+```+++To use as a dependency in a project:+++```+build-depends: base >= 4, dataframe+```+++Works with GHC 9.4 through 9.12. A custom REPL with all imports pre-loaded is available after installing:+++```bash+dataframe+```+++## Quick Start++Group sales by product and compute totals. The first block carries the+`scripths` cabal directives and the imports shared by the rest of the document;+you can also drop the same code into an `Example.hs` and run it with+`cabal run Example.hs` after adding a `#!/usr/bin/env cabal` header.++ ```haskell-dataframe> df = D.fromNamedColumns [("product_id", D.fromList [1,1,2,2,3,3]), ("sales", D.fromList [100,120,50,20,40,30])]-dataframe> df--------------------product_id | sales------------|------- Int | Int ------------|-------1 | 100 -1 | 120 -2 | 50 -2 | 20 -3 | 40 -3 | 30 +-- cabal: build-depends: dataframe, text+-- cabal: default-extensions: OverloadedStrings, TypeApplications, TemplateHaskell, DataKinds, TypeFamilies, FlexibleInstances, FlexibleContexts, ScopedTypeVariables, DeriveGeneric, UndecidableInstances+import qualified DataFrame as D+import qualified DataFrame.Functions as F+import qualified DataFrame.Typed as DT+import DataFrame.Operators+import Data.Text (Text)+import Data.Int (Int64) -dataframe> :declareColumns df-"product_id :: Expr Int"-"sales :: Expr Int"-dataframe> df |> D.groupBy [F.name product_id] |> D.aggregate [F.sum sales `as` "total_sales"]--------------------------product_id | total_sales------------|------------- Int | Int ------------|-------------1 | 220 -2 | 70 -3 | 70 +sales = D.fromNamedColumns+ [ ("product", D.fromList [1, 1, 2, 2, 3, 3 :: Int])+ , ("amount", D.fromList [100, 120, 50, 20, 40, 30 :: Int])+ ]++-- Group by product and compute totals+sales+ |> D.groupBy ["product"]+ |> D.aggregate [ F.sum (F.col @Int "amount") `as` "total"+ , F.count (F.col @Int "amount") `as` "orders"+ ]+ |> D.toMarkdown' ``` +> <!-- sabela:mime text/plain -->+> | product<br>Int | total<br>Int | orders<br>Int |+> | ---------------|--------------|-------------- |+> | 1 | 220 | 2 |+> | 3 | 70 | 2 |+> | 2 | 70 | 2 |+++Reading from files works the same way:+++```haskell+fileDf <- D.readCsv "./data/housing.csv"+fileDf <- D.readParquet "./data/mtcars.parquet"++-- Hugging Face datasets (needs network access, via the dataframe-huggingface package):+-- import qualified DataFrame.IO.HuggingFace as HF+-- fileDf <- HF.readParquet "hf://datasets/scikit-learn/iris/default/train/0000.parquet"++D.dimensions fileDf+```++> <!-- sabela:mime text/plain -->+> (32,12)+++## Interactive REPL++The `dataframe` REPL comes with all imports pre-loaded. Here's a typical exploration session (each block runs as a cell):+++```haskell+df <- D.readCsv "./data/housing.csv"+D.dimensions df+```++> <!-- sabela:mime text/plain -->+> (20640,10)++++```haskell+D.describeColumns df |> D.toMarkdown'+```++> <!-- sabela:mime text/plain -->+> | Column Name<br>Text | # Non-null Values<br>Int | # Null Values<br>Int | Type<br>Text |+> | --------------------|--------------------------|----------------------|------------- |+> | total_bedrooms | 20433 | 207 | Maybe Double |+> | ocean_proximity | 20640 | 0 | Text |+> | median_house_value | 20640 | 0 | Double |+> | median_income | 20640 | 0 | Double |+> | households | 20640 | 0 | Double |+> | population | 20640 | 0 | Double |+> | total_rooms | 20640 | 0 | Double |+> | housing_median_age | 20640 | 0 | Double |+> | latitude | 20640 | 0 | Double |+> | longitude | 20640 | 0 | Double |+++The `:declareColumns` macro (`$(D.declareColumns df)` outside the REPL) generates typed column references from a dataframe, so you can use column names directly in expressions instead of writing `F.col @Double "median_income"` every time:+++```haskell+$(D.declareColumns df)+```++> <!-- sabela:mime text/plain -->++++```haskell+df |> D.groupBy ["ocean_proximity"]+ |> D.aggregate [F.mean median_house_value `as` "avg_value"]+ |> D.toMarkdown'+```++> <!-- sabela:mime text/plain -->+> | ocean_proximity<br>Text | avg_value<br>Double |+> | ------------------------|-------------------- |+> | NEAR BAY | 259212.31179039303 |+> | NEAR OCEAN | 249433.97742663656 |+> | INLAND | 124805.39200122119 |+> | <1H OCEAN | 240084.28546409807 |+> | ISLAND | 380440.0 |+++Create new columns from existing ones:+++```haskell+df |> D.derive "rooms_per_household" (total_rooms / households) |> D.take 3 |> D.toMarkdown'+```++> <!-- sabela:mime text/plain -->+> | longitude<br>Double | latitude<br>Double | housing_median_age<br>Double | total_rooms<br>Double | total_bedrooms<br>Maybe Double | population<br>Double | households<br>Double | median_income<br>Double | median_house_value<br>Double | ocean_proximity<br>Text | rooms_per_household<br>Double |+> | --------------------|--------------------|------------------------------|-----------------------|--------------------------------|----------------------|----------------------|-------------------------|------------------------------|-------------------------|------------------------------ |+> | -122.23 | 37.88 | 41.0 | 880.0 | Just 129.0 | 322.0 | 126.0 | 8.3252 | 452600.0 | NEAR BAY | 6.984126984126984 |+> | -122.22 | 37.86 | 21.0 | 7099.0 | Just 1106.0 | 2401.0 | 1138.0 | 8.3014 | 358500.0 | NEAR BAY | 6.238137082601054 |+> | -122.24 | 37.85 | 52.0 | 1467.0 | Just 190.0 | 496.0 | 177.0 | 7.2574 | 352100.0 | NEAR BAY | 8.288135593220339 |+++Type mismatches are caught as compile errors — adding a `Double` column to a `Text` column won't silently produce garbage:+++```text+dataframe> df |> D.derive "nonsense" (latitude + ocean_proximity)++<interactive>:14:47: error: [GHC-83865]+ • Couldn't match type 'Text' with 'Double'+ Expected: Expr Double+ Actual: Expr Text+ • In the second argument of '(+)', namely 'ocean_proximity'+ In the second argument of 'derive', namely+ '(latitude + ocean_proximity)'+```+++## Template Haskell++For scripts and projects, Template Haskell can generate column bindings at compile time.++### Generate column references from a CSV++`declareColumnsFromCsvFile` (in `DataFrame.TH`, also re-exported from `DataFrame`)+reads your CSV at compile time and generates typed `Expr` bindings for every column:+++```haskell+-- Reads housing.csv at compile time and generates:+-- latitude :: Expr Double+-- total_rooms :: Expr Double+-- ocean_proximity :: Expr Text+-- ... one binding per column+$(D.declareColumnsFromCsvFile "./data/housing.csv")++df <- D.readCsv "./data/housing.csv"+df |> D.derive "rooms_per_household" (total_rooms / households)+ |> D.filterWhere (median_income .>. 5)+ |> D.groupBy ["ocean_proximity"]+ |> D.aggregate [F.mean median_house_value `as` "avg_value"]+ |> D.toMarkdown'+```++> <!-- sabela:mime text/plain -->+> | ocean_proximity<br>Text | avg_value<br>Double |+> | ------------------------|-------------------- |+> | NEAR BAY | 361441.9354304636 |+> | NEAR OCEAN | 380041.63071895426 |+> | INLAND | 234817.86695906433 |+> | <1H OCEAN | 333411.75125531096 |+++Compare this to the manual version which requires spelling out every column name and type:+++```haskell+-- Without TH — every column needs its name and type spelled out+df |> D.derive "rooms_per_household"+ (F.col @Double "total_rooms" / F.col @Double "households")+ |> D.filterWhere (F.col @Double "median_income" .>. F.lit 5)+ |> D.take 5+ |> D.toMarkdown'+```++> <!-- sabela:mime text/plain -->+> | longitude<br>Double | latitude<br>Double | housing_median_age<br>Double | total_rooms<br>Double | total_bedrooms<br>Maybe Double | population<br>Double | households<br>Double | median_income<br>Double | median_house_value<br>Double | ocean_proximity<br>Text | rooms_per_household<br>Double |+> | --------------------|--------------------|------------------------------|-----------------------|--------------------------------|----------------------|----------------------|-------------------------|------------------------------|-------------------------|------------------------------ |+> | -122.23 | 37.88 | 41.0 | 880.0 | Just 129.0 | 322.0 | 126.0 | 8.3252 | 452600.0 | NEAR BAY | 6.984126984126984 |+> | -122.22 | 37.86 | 21.0 | 7099.0 | Just 1106.0 | 2401.0 | 1138.0 | 8.3014 | 358500.0 | NEAR BAY | 6.238137082601054 |+> | -122.24 | 37.85 | 52.0 | 1467.0 | Just 190.0 | 496.0 | 177.0 | 7.2574 | 352100.0 | NEAR BAY | 8.288135593220339 |+> | -122.25 | 37.85 | 52.0 | 1274.0 | Just 235.0 | 558.0 | 219.0 | 5.6431000000000004 | 341300.0 | NEAR BAY | 5.8173515981735155 |+> | -122.29 | 37.82 | 49.0 | 135.0 | Just 29.0 | 86.0 | 23.0 | 6.1183 | 75000.0 | NEAR BAY | 5.869565217391305 |+++### Generate a schema type from a CSV++`deriveSchemaFromCsvFile` generates a type synonym for use with the typed API — instead of manually writing out every column name and type:+++```haskell+-- Generates:+-- type HousingSchema = '[ DT.Column "longitude" Double+-- , DT.Column "latitude" Double+-- , DT.Column "total_rooms" Double+-- , ...+-- ]+$(DT.deriveSchemaFromCsvFile "HousingSchema" "./data/housing.csv")+```++> <!-- sabela:mime text/plain -->+++### Generate a schema (and a row bridge) from a record ADT++When the canonical row shape lives in your code as a Haskell record,+`deriveSchemaFromType` produces both the typed schema and a `HasSchema`+instance that converts between `[Order]` and a `DataFrame` (or+`TypedDataFrame OrderSchema`) at runtime:+++```haskell+data Order = Order+ { orderId :: Int64+ , region :: Text+ , amount :: Double+ } deriving (Show, Eq)++$(DT.deriveSchemaFromType ''Order)+-- expands to:+-- type OrderSchema =+-- '[DT.Column "order_id" Int64, DT.Column "region" Text, DT.Column "amount" Double]+-- instance DT.HasSchema Order where+-- type Schema Order = OrderSchema+-- toColumns = ...+-- fromColumns = ...++xs :: [Order]+xs = [Order 1 "us" 10.0, Order 2 "eu" 20.5]++-- Untyped: [Order] -> DataFrame+ordersDf :: D.DataFrame+ordersDf = D.fromRecords xs++ordersDf |> D.toMarkdown'+```++> <!-- sabela:mime text/plain -->+> | order_id<br>Int64 | region<br>Text | amount<br>Double |+> | ------------------|----------------|----------------- |+> | 1 | us | 10.0 |+> | 2 | eu | 20.5 |+++The runtime-checked round-trip back to records:+++```haskell+D.toRecords ordersDf :: Either Text [Order]+```++> <!-- sabela:mime text/plain -->+> Right [Order {orderId = 1, region = "us", amount = 10.0},Order {orderId = 2, region = "eu", amount = 20.5}]+++And the typed bridge — `[Order]` to `TypedDataFrame OrderSchema` and back:+++```haskell+DT.thaw (DT.fromRecordsTyped xs :: DT.TypedDataFrame OrderSchema) |> D.toMarkdown'+```++> <!-- sabela:mime text/plain -->+> | order_id<br>Int64 | region<br>Text | amount<br>Double |+> | ------------------|----------------|----------------- |+> | 1 | us | 10.0 |+> | 2 | eu | 20.5 |+++Field names are translated `camelCase → snake_case` by default; override+the translation with `deriveSchemaFromTypeWith+defaultSchemaOptions{nameTransform = id}` (or any `String -> String`).++If all you need is a runtime `Schema` to drive `readCsvWithSchema` (no+typed-dataframe machinery), there's a companion splice in+`DataFrame.Internal.Schema` (re-exported from `DataFrame`):+++```haskell+$(D.deriveSchema ''Order)+-- emits:+-- orderSchema :: Schema+-- orderSchema = makeSchema [("order_id", schemaType @Int64), ...]+-- orderOrderId :: Expr Int64+-- orderOrderId = col "order_id"+-- orderRegion :: Expr Text+-- orderRegion = col "region"+-- orderAmount :: Expr Double+-- orderAmount = col "amount"++orders :: IO D.DataFrame+orders = do+ raw <- D.readCsvWithSchema orderSchema "./data/orders.csv"+ pure (D.filter orderAmount (> 100) raw)+```++> <!-- sabela:mime text/plain -->+++Each record field gets a typed accessor named `<lower-first TyConName><UpperFirst FieldName>`,+so `data Order { customerId :: Int }` yields `orderCustomerId :: Expr Int = col "customer_id"`.+That's the same shape as `$(D.declareColumns df)` produces from a runtime+`DataFrame`, but driven off the ADT instead of an existing frame.++If you'd rather not depend on Template Haskell, the same schema is+available via `GHC.Generics` (shown here on an equivalent record):+++```haskell+import GHC.Generics (Generic)+import DataFrame.Typed (Schema)++data OrderG = OrderG+ { orderGId :: Int64+ , regionG :: Text+ , amountG :: Double+ } deriving (Generic)++type OrderGSchema = DT.SchemaOf OrderG++instance DT.HasSchema OrderG where+ type Schema OrderG = OrderGSchema+ toColumns = DT.genericToColumns+ fromColumns = DT.genericFromColumns+```++> <!-- sabela:mime text/plain -->+++## Typed API++When you want compile-time guarantees that column names exist and types match, wrap your `DataFrame` in a `TypedDataFrame`:+++```haskell+type EmployeeSchema =+ '[ DT.Column "name" Text+ , DT.Column "department" Text+ , DT.Column "salary" Double+ ]++employees <- D.readCsv "./data/employees.csv"+case DT.freeze @EmployeeSchema employees of+ Nothing -> "Schema mismatch!"+ Just tdf -> tdf+ |> DT.derive @"bonus" (DT.col @"salary" * DT.lit 0.1)+ |> DT.filterWhere (DT.col @"salary" DT..>. DT.lit 50000)+ |> DT.select @'["name", "bonus"]+ |> DT.thaw+ |> D.toMarkdown'+```++> <!-- sabela:mime text/plain -->+> | name<br>Text | bonus<br>Double |+> | -------------|---------------- |+> | Alice | 8500.0 |+> | Carol | 12000.0 |+> | Dave | 5200.0 |+> | Frank | 6700.0 |+++`DT.freeze` validates the runtime `DataFrame` against your schema once at the boundary. After that, every column access is checked at compile time:+++```text+-- Typo in column name -> compile error+tdf |> DT.filterWhere (DT.col @"slary" DT..>. DT.lit 50000)+-- error: Column "slary" not found in schema++-- Wrong type -> compile error+tdf |> DT.filterWhere (DT.col @"name" DT..>. DT.lit 50000)+-- error: Couldn't match type 'Text' with 'Double'+```+++`filterAllJust` goes further — it strips `Maybe` from every column in the schema type, so downstream code can't accidentally treat cleaned columns as nullable:+++```haskell+type ScoreSchema = '[ DT.Column "name" Text, DT.Column "score" (Maybe Double) ]++scoresDf = D.fromNamedColumns+ [ ("name", D.fromList ["a", "b", "c" :: Text])+ , ("score", D.fromList [Just 1.0, Nothing, Just 3.0 :: Maybe Double])+ ]++Just stdf = DT.freeze @ScoreSchema scoresDf++-- filterAllJust drops the null row and changes the column type from+-- (Maybe Double) to Double, so `scaled` can multiply it directly.+DT.thaw (DT.filterAllJust stdf |> DT.derive @"scaled" (DT.col @"score" * DT.lit 100)) |> D.toMarkdown'+```++> <!-- sabela:mime text/plain -->+> | name<br>Text | score<br>Double | scaled<br>Double |+> | -------------|-----------------|----------------- |+> | a | 1.0 | 100.0 |+> | c | 3.0 | 300.0 |+++## Features++**I/O**: CSV, TSV, Parquet (Snappy, ZSTD, Gzip), JSON. Read Parquet from Hugging Face datasets (`hf://` URIs) via the `dataframe-huggingface` package. Column projection and predicate pushdown for Parquet reads.++**Operations**: filter, select, derive, groupBy, aggregate, joins (inner, left, right, full outer), sort, sample, stratified sample, distinct, k-fold splits.++**Expressions**: typed column references (`F.col @Double "x"`), arithmetic, comparisons, logical operators, nullable-aware three-valued logic (`.==`, `.&&`), string matching (`like`, `regex`), casting, and user-defined functions via `lift`/`lift2`.++**Statistics**: mean, median, mode, variance, standard deviation, percentiles, inter-quartile range, correlation, skewness, frequency tables, imputation.++**Plotting**: terminal plots (histogram, scatter, line, bar, box, pie, heatmap, stacked bar, correlation matrix) and interactive HTML plots.++**Lazy engine**: streaming query execution for files that don't fit in memory. Rule-based optimizer with filter fusion, predicate pushdown, and dead column elimination. Pull-based executor with configurable batch sizes.++**Interop**: Arrow C Data Interface for zero-copy round-trips with Python and Polars.++**ML**: decision trees (TAO algorithm), feature synthesis, k-fold cross-validation, stratified sampling.++**Notebooks**: IHaskell integration with [pre-built Binder examples](https://mybinder.org/v2/gh/mchav/ihaskell-dataframe/HEAD).++## Lazy Queries++For files too large to fit in memory, `DataFrame.Lazy` provides a streaming query engine. Declare a schema, build a query plan with the same familiar operations, and `runDataFrame` runs it through an optimizer before streaming results batch-by-batch:+++```haskell+import qualified DataFrame.Lazy as L+import DataFrame.Internal.Schema (schemaType, makeSchema)++housingSchema = makeSchema+ [ ("longitude", schemaType @Double)+ , ("latitude", schemaType @Double)+ , ("housing_median_age", schemaType @Double)+ , ("total_rooms", schemaType @Double)+ , ("total_bedrooms", schemaType @(Maybe Double))+ , ("population", schemaType @Double)+ , ("households", schemaType @Double)+ , ("median_income", schemaType @Double)+ , ("median_house_value", schemaType @Double)+ , ("ocean_proximity", schemaType @Text)+ ]++lazyResult <- L.runDataFrame $+ L.scanCsv housingSchema "./data/housing.csv"+ |> L.filter (F.col @Double "median_income" .>. F.lit 5)+ |> L.derive "value_per_income"+ (F.col @Double "median_house_value" / F.col @Double "median_income")+ |> L.select ["ocean_proximity", "median_house_value", "value_per_income"]+ |> L.take 1000++D.take 10 lazyResult |> D.toMarkdown'+```++> <!-- sabela:mime text/plain -->+> | ocean_proximity<br>Text | median_house_value<br>Double | value_per_income<br>Double |+> | ------------------------|------------------------------|--------------------------- |+> | NEAR BAY | 452600.0 | 54365.06029885168 |+> | NEAR BAY | 358500.0 | 43185.48678536151 |+> | NEAR BAY | 352100.0 | 48515.997464656764 |+> | NEAR BAY | 341300.0 | 60480.94132657581 |+> | NEAR BAY | 75000.0 | 12258.307046074891 |+> | NEAR BAY | 262500.0 | 51554.49064163246 |+> | NEAR BAY | 327600.0 | 55908.25312308007 |+> | NEAR BAY | 347600.0 | 65748.65703260951 |+> | NEAR BAY | 366100.0 | 61467.42780389524 |+> | NEAR BAY | 373600.0 | 58895.860264211624 |+++The optimizer pushes the filter into the scan, drops unreferenced columns before reading, and stops pulling batches once 1000 rows have been collected.+ ## Documentation-* 📚 User guide: https://dataframe.readthedocs.io/en/latest/-* 📖 API reference: https://hackage.haskell.org/package/dataframe/docs/DataFrame.html++* User guide: https://dataframe.readthedocs.io/en/latest/+* API reference: https://hackage.haskell.org/package/dataframe/docs/DataFrame.html+* [Coming from pandas, Polars, dplyr, or Frames?](docs/coming_from_other_implementations.md)+* [Cookbook (SQL-style patterns)](docs/cookbook.md)+* [Tutorials](docs/tutorial.md)+* Discord: https://discord.gg/8u8SCWfrNC
app/LazyBenchmark.hs view
@@ -2,21 +2,14 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} -{- | End-to-end smoke test and benchmark for the Lazy streaming API.+{- | End-to-end smoke test and benchmark for the Lazy streaming API:+ generate a CSV then run five Lazy queries over it. Usage:- cabal run lazy-bench [-- [OPTIONS]]-- Options:- --rows N Number of rows to generate (default: 1_000_000_000)- --file PATH Output CSV path (default: /tmp/lazy_1b.csv)- --skip-gen Skip generation if the file already exists-- The executable generates a CSV file (streaming, constant memory) then- runs five Lazy queries over it, printing timing and result summaries.-- For heap/GC stats run with:- cabal run lazy-bench -- +RTS -s -RTS+ cabal run lazy-bench [-- [OPTIONS]] (+RTS -s -RTS for heap stats)+ --rows N rows to generate (default 1_000_000_000)+ --file PATH output CSV path (default /tmp/lazy_1b.csv)+ --skip-gen reuse the file if it already exists -} module Main where @@ -26,9 +19,9 @@ import qualified Data.Text as T import Data.Time (UTCTime, diffUTCTime, getCurrentTime) import qualified DataFrame as D-import DataFrame.Internal.Schema (Schema (..), schemaType) import qualified DataFrame.Lazy as L import DataFrame.Operators+import DataFrame.Schema (Schema (..), schemaType) import System.Directory (doesFileExist, getFileSize) import System.Environment (getArgs) import System.Exit (exitFailure)@@ -185,9 +178,6 @@ n = optRows opts pathT = T.pack path - -- ------------------------------------------------------------------------ -- Phase 1: Generate- -- ----------------------------------------------------------------------- putStrLn "=== Lazy API 1B-row benchmark ===" putStrLn $ " rows : " ++ commas n putStrLn $ " file : " ++ path@@ -205,12 +195,8 @@ sz <- getFileSize path putStrLn $ " file size: " ++ showBytes sz - -- ------------------------------------------------------------------------ -- Phase 2: Lazy queries- -- ----------------------------------------------------------------------- putStrLn "\nPhase 2: Lazy queries" - -- Schema for the generated CSV: id (Int), x (Double), y (Double), category (Text) let schema = Schema $ M.fromList@@ -220,48 +206,32 @@ , ("category", schemaType @T.Text) ] - -- Q1: Preview — limit 20, no filter.- -- Demonstrates that the executor reads only the first batch. runQuery "Q1 — preview first 20 rows (no filter)" $ L.runDataFrame $- L.limit 20 $+ L.take 20 $ L.scanCsv schema pathT - -- Q2: Filter + limit.- -- x > 0.999 ≈ 0.1% of rows. With a 512K-row batch the executor finds- -- ~512 matches in the first batch and stops — reads only one batch. runQuery "Q2 — filter (x > 0.999), limit 20" $ L.runDataFrame $- L.limit 20 $- L.filter (col @Double "x" .> lit 0.999) $+ L.take 20 $+ L.filter (col @Double "x" .> lit (0.999 :: Double)) $ L.scanCsv schema pathT - -- Q3: Filter + derive + select + limit.- -- Shows projection pushdown: only id/x/y/category are read, z is derived.- -- Predicate pushdown moves the filter into the scan batch loop. runQuery "Q3 — filter (x > 0.999), derive z = x*y, select [id,z], limit 20" $ L.runDataFrame $- L.limit 20 $+ L.take 20 $ L.select ["id", "z"] $ L.derive "z" (col @Double "x" * col @Double "y") $- L.filter (col @Double "x" .> lit 0.999) $+ L.filter (col @Double "x" .> lit (0.999 :: Double)) $ L.scanCsv schema pathT - -- Q4: Filter fusion demo.- -- Two consecutive filters are fused into one AND predicate by the optimizer.- -- Result: rows where x > 0.5 AND y > 0.5 (≈ 25% of total).- -- We limit to keep result size manageable. runQuery "Q4 — filter fusion: (x > 0.5) . (y > 0.5), limit 20" $ L.runDataFrame $- L.limit 20 $- L.filter (col @Double "y" .> lit 0.5) $- L.filter (col @Double "x" .> lit 0.5) $+ L.take 20 $+ L.filter (col @Double "y" .> lit (0.5 :: Double)) $+ L.filter (col @Double "x" .> lit (0.5 :: Double)) $ L.scanCsv schema pathT - -- Q5: Full scan, heavy filter, count results.- -- x > 0.999 across the whole file ≈ 0.1% × N rows.- -- For 1B rows that is ~1M results — materialised into one DataFrame.- -- This query exercises streaming across all batches. runQuery ( "Q5 — full scan, filter (x > 0.999), count (~" ++ approx (n `div` 1000)@@ -269,7 +239,7 @@ ) $ L.runDataFrame $ L.select ["id", "x"]- $ L.filter (col @Double "x" .> lit 0.999)+ $ L.filter (col @Double "x" .> lit (0.999 :: Double)) $ L.scanCsv schema pathT putStrLn "\nDone."
app/Main.hs view
@@ -108,7 +108,7 @@ , " main-is: Main.hs" , " hs-source-dirs: ." , " default-language: Haskell2010"- , " build-depends: base, dataframe, text, time, random"+ , " build-depends: base, dataframe, text, time, random, dataframe-core, dataframe-operations, dataframe-csv, dataframe-parquet, dataframe-json, dataframe-th, dataframe-csv-th, dataframe-parquet-th, dataframe-viz, dataframe-lazy, dataframe-parsing" ] writeIfMissing
app/Synthesis.hs view
@@ -1,110 +1,143 @@+{-# LANGUAGE DataKinds #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-} +import Data.Char import qualified Data.Text as T import qualified DataFrame as D+import DataFrame.DecisionTree import qualified DataFrame.Functions as F+import DataFrame.Operators+import qualified DataFrame.Typed as DT+import System.Random -import Data.Char-import DataFrame.DecisionTree-import DataFrame.Operators hiding (name)+$( DT.deriveSchemaFromCsvFileWith+ D.defaultReadOptions{D.safeRead = D.MaybeRead}+ "TrainSchema"+ "./data/titanic/train.csv"+ )+$( DT.deriveSchemaFromCsvFileWith+ D.defaultReadOptions{D.safeRead = D.MaybeRead}+ "TestSchema"+ "./data/titanic/test.csv"+ ) -import System.Random+-- Survived is Maybe Int (safeRead = MaybeRead); prediction is Int (model output).+type RawPredSchema =+ '[DT.Column "Survived" (Maybe Int), DT.Column "prediction" Int] -$(F.declareColumnsFromCsvFile "./data/titanic/train.csv")+prediction :: D.Expr Int+prediction = F.col @Int "prediction" main :: IO () main = do- train <- D.readCsv "./data/titanic/train.csv"- test <- D.readCsv "./data/titanic/test.csv"+ rawTrain <- D.readCsv "./data/titanic/train.csv"+ rawTest <- D.readCsv "./data/titanic/test.csv" - -- Apply the same transformations to training and test.- let combined =- (train <> test)- |> D.deriveMany- [ "Ticket" .= F.lift (T.filter isAlpha) ticket- , "Name" .= F.match "\\s*([A-Za-z]+)\\." name- , "Cabin" .= F.whenPresent (T.take 1) cabin- ]- |> D.renameMany- [ (F.name name, "title")- , (F.name cabin, "cabin_prefix")- , (F.name pclass, "passenger_class")- , (F.name sibsp, "number_of_siblings_and_spouses")- , (F.name parch, "number_of_parents_and_children")- ]- print combined+ train <-+ maybe (fail "train.csv schema mismatch") pure (DT.freeze @TrainSchema rawTrain)+ test <-+ maybe (fail "test.csv schema mismatch") pure (DT.freeze @TestSchema rawTest) - let (train', validation) =- D.take- (D.nRows train)- combined- |> D.filterJust (F.name survived)- |> D.randomSplit (mkStdGen 4232) 0.7- -- Split the test out again.- test' =- D.drop- (D.nRows train)- combined+ let (trainDf, validDf) =+ D.randomSplit (mkStdGen 4232) 0.7 (DT.thaw (clean train))+ testDf = DT.thaw (clean test) - model =- fitDecisionTree+ fitted =+ fit ( defaultTreeConfig { maxTreeDepth = 5- , minSamplesSplit = 10+ , minSamplesSplit = 5 , minLeafSize = 3 , taoIterations = 100 , synthConfig = defaultSynthConfig- { complexityPenalty = 0.00- , maxExprDepth = 2+ { complexityPenalty = 0.1+ , maxExprDepth = 3 , disallowedCombinations =- [ (F.name age, F.name fare)+ [ ("Age", "Fare") , ("passenger_class", "number_of_siblings_and_spouses") , ("passenger_class", "number_of_parents_and_children") ] } } )- survived -- Label to predict- ( train'- |> D.exclude [F.name passengerid]- )+ (F.fromMaybe 0 (F.col @(Maybe Int) "Survived"))+ (trainDf |> D.exclude ["PassengerId"])+ model = predict fitted - print model+ print fitted putStrLn "Training accuracy: "- print $- computeAccuracy- (train' |> D.derive (F.name prediction) model)+ print $ computeAccuracy (trainDf |> D.derive (F.name prediction) model) putStrLn "Validation accuracy: "- print $- computeAccuracy- ( validation- |> D.derive (F.name prediction) model- )+ print $ computeAccuracy (validDf |> D.derive (F.name prediction) model) - let predictions = D.derive (F.name survived) model test' D.writeCsv "./predictions.csv"- (predictions |> D.select [F.name passengerid, F.name survived])+ ( testDf+ |> D.derive "Survived" model+ |> D.select ["PassengerId", "Survived"]+ ) -prediction :: D.Expr Int-prediction = F.col @Int "prediction"+clean ::+ ( DT.AssertPresent "Ticket" cols+ , DT.SafeLookup "Ticket" cols ~ Maybe T.Text+ , DT.AssertPresent "Name" cols+ , DT.SafeLookup "Name" cols ~ Maybe T.Text+ , DT.AssertPresent "Cabin" cols+ , DT.SafeLookup "Cabin" cols ~ Maybe T.Text+ ) =>+ DT.TypedDataFrame cols ->+ DT.TypedDataFrame+ ( DT.RenameManyInSchema+ '[ '("Name", "title")+ , '("Cabin", "cabin_prefix")+ , '("Pclass", "passenger_class")+ , '("SibSp", "number_of_siblings_and_spouses")+ , '("Parch", "number_of_parents_and_children")+ ]+ cols+ )+clean tdf =+ tdf+ |> DT.replaceColumn @"Ticket" (DT.nullLift (T.filter isAlpha) (DT.col @"Ticket"))+ |> DT.replaceColumn @"Name" (DT.nullLift extractTitle (DT.col @"Name"))+ |> DT.replaceColumn @"Cabin" (DT.nullLift (T.take 1) (DT.col @"Cabin"))+ |> DT.renameMany+ @'[ '("Name", "title")+ , '("Cabin", "cabin_prefix")+ , '("Pclass", "passenger_class")+ , '("SibSp", "number_of_siblings_and_spouses")+ , '("Parch", "number_of_parents_and_children")+ ] +-- | Extract title (e.g. "Mr", "Mrs") from a full Titanic passenger name.+extractTitle :: T.Text -> T.Text+extractTitle fullName =+ case filter (T.isSuffixOf ".") (T.words fullName) of+ (w : _) -> T.dropEnd 1 w+ [] -> ""++{- | Compute binary classification accuracy from a DataFrame containing+ "Survived" and "prediction" columns.+-} computeAccuracy :: D.DataFrame -> Double computeAccuracy df =- let- tp =- fromIntegral $ D.nRows (D.filterWhere (survived .== 1 .&& prediction .== 1) df)- tn =- fromIntegral $ D.nRows (D.filterWhere (survived .== 0 .&& prediction .== 0) df)- fp =- fromIntegral $ D.nRows (D.filterWhere (survived .== 0 .&& prediction .== 1) df)- fn =- fromIntegral $ D.nRows (D.filterWhere (survived .== 1 .&& prediction .== 0) df)- in- (tp + tn) / (tp + tn + fp + fn)+ let tdf =+ DT.impute @"Survived" 0 $+ DT.unsafeFreeze @RawPredSchema $+ df |> D.select ["Survived", "prediction"]+ survived = DT.col @"Survived"+ predCol = DT.col @"prediction"+ count expr = fromIntegral (DT.nRows (DT.filterWhere expr tdf))+ tp = count ((survived DT..==. DT.lit 1) DT..&&. (predCol DT..==. DT.lit 1))+ tn = count ((survived DT..==. DT.lit 0) DT..&&. (predCol DT..==. DT.lit 0))+ fp = count ((survived DT..==. DT.lit 0) DT..&&. (predCol DT..==. DT.lit 1))+ fn = count ((survived DT..==. DT.lit 1) DT..&&. (predCol DT..==. DT.lit 0))+ in (tp + tn) / (tp + tn + fp + fn)
benchmark/Main.hs view
@@ -1,17 +1,28 @@ {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -fno-warn-orphans #-} import qualified DataFrame as D import qualified DataFrame.Functions as F +import Control.DeepSeq (NFData (..)) import Control.Monad (void) import Criterion.Main+import DataFrame.Internal.DataFrame (forceDataFrame) import DataFrame.Operations.Join import DataFrame.Operators import System.Process hiding (env) import System.Random.Stateful +{- | Criterion's 'nf' and 'env' force benchmark inputs/results to normal form+via 'NFData'. The core library intentionally dropped its @instance NFData+DataFrame@ in favour of 'forceDataFrame', so we provide a thin orphan here+(scoped to the benchmark) that reuses it.+-}+instance NFData D.DataFrame where+ rnf df = forceDataFrame df `seq` ()+ haskell :: IO () haskell = do output <- readProcess "cabal" ["run", "dataframe-benchmark-example", "-O2"] ""@@ -43,7 +54,7 @@ groupByHaskell :: IO () groupByHaskell = do- df <- D.fastReadCsvUnstable "./data/housing.csv"+ df <- D.readCsv "./data/housing.csv" print $ df |> D.groupBy ["ocean_proximity"]@@ -84,12 +95,6 @@ parseFile :: String -> IO () parseFile = void . D.readCsv -parseFileUnstable :: String -> IO ()-parseFileUnstable = void . D.readCsvUnstable--parseFileUnstableSIMD :: String -> IO ()-parseFileUnstableSIMD = void . D.fastReadCsvUnstable- parseHousingCSV :: IO () parseHousingCSV = parseFile "./data/housing.csv" @@ -215,22 +220,12 @@ , bgroup "housing.csv (1.4 MB)" [ bench "Attoparsec" $ nfIO $ parseFile "./data/housing.csv"- , bench "Native Haskell" $ nfIO $ parseFileUnstable "./data/housing.csv"- , bench "SIMD" $ nfIO $ parseFileUnstableSIMD "./data/housing.csv" ] , bgroup "effects-of-covid-19-on-trade-at-15-december-2021-provisional.csv (9.1 MB)" [ bench "Attoparsec" $ nfIO $ parseFile- "./data/effects-of-covid-19-on-trade-at-15-december-2021-provisional.csv"- , bench "Native Haskell" $- nfIO $- parseFileUnstable- "./data/effects-of-covid-19-on-trade-at-15-december-2021-provisional.csv"- , bench "SIMD" $- nfIO $- parseFileUnstableSIMD "./data/effects-of-covid-19-on-trade-at-15-december-2021-provisional.csv" ] , bgroup
+ cbits/arrow_abi.h view
@@ -0,0 +1,44 @@+/* Arrow C Data Interface structs (verbatim from specification).+ See https://arrow.apache.org/docs/format/CDataInterface.html */++#pragma once++#include <stdint.h>++#ifdef __cplusplus+extern "C" {+#endif++struct ArrowSchema {+ /* Array type description */+ const char *format;+ const char *name;+ const char *metadata;+ int64_t flags;+ int64_t n_children;+ struct ArrowSchema **children;+ struct ArrowSchema *dictionary;++ void (*release)(struct ArrowSchema *);+ void *private_data;+};++struct ArrowArray {+ /* Array data description */+ int64_t length;+ int64_t null_count;+ int64_t offset;+ int64_t n_buffers;+ int64_t n_children;+ const void **buffers;+ struct ArrowArray **children;+ struct ArrowArray *dictionary;++ void (*release)(struct ArrowArray *);+ /* Opaque producer-specific data */+ void *private_data;+};++#ifdef __cplusplus+}+#endif
− cbits/process_csv.c
@@ -1,198 +0,0 @@-#include "process_csv.h"-#include <stdlib.h>-#include <stdio.h>--/*- * compile with clang -O3- * in cabal use - * cc-options: -O3- *- * Produce an array of field delimiter indices- * Fields can be delimited by commas and newlines- * TODO: allow the user to provide a custom delimiter- * character to replace commas.- * This should work with UTF-8, so long as the delimiter- * character is a single byte.- *- * Delimiters can be escaped inside of quotes. Quotes- * can also be placed inside quotes by double quoting.- * For the purposes of this parser we can ignore double- * quotes inside quotes, thereby treating the first quote- * as the closing of the string and the next one the- * immediate opening of a new one- * - * We can find the quoted regions by first finding- * the positions of the quotes (cmpeq and then movemask)- * and then using the carryless multiplication operation- * to know the regions that are quoted. We can then simply- * and the inverse of the quotemask to exclude commas and- * newlines inside quotes- *- */--// if the character is found at a particular-// position in the array of bytes, the-// corresponding bit in the returned uint64_t should-// be turned on.-// Example: searching for commas in-// input: one, two, three-// result: 000100001000000-#ifdef HAS_SIMD_CSV-static uint64_t find_character_in_chunk(uint8_t *in, uint8_t c) {-#ifdef USE_AVX2- // AVX2 implementation: load two 32-byte chunks- __m256i v0 = _mm256_loadu_si256((const __m256i *)(in));- __m256i v1 = _mm256_loadu_si256((const __m256i *)(in + 32));- __m256i b = _mm256_set1_epi8((char)c);- __m256i m0 = _mm256_cmpeq_epi8(v0, b);- __m256i m1 = _mm256_cmpeq_epi8(v1, b);- uint32_t lo = (uint32_t)_mm256_movemask_epi8(m0);- uint32_t hi = (uint32_t)_mm256_movemask_epi8(m1);- return ((uint64_t)hi << 32) | (uint64_t)lo;-#else // USE_NEON- // ARM NEON implementation: load 64 bytes deinterleaved- uint8x16x4_t src = vld4q_u8(in);- uint8x16_t mask = vmovq_n_u8(c);- uint8x16_t cmp0 = vceqq_u8(src.val[0], mask);- uint8x16_t cmp1 = vceqq_u8(src.val[1], mask);- uint8x16_t cmp2 = vceqq_u8(src.val[2], mask);- uint8x16_t cmp3 = vceqq_u8(src.val[3], mask);-- // For an explanation of how to do movemask in- // NEON, see: https://branchfree.org/2019/04/01/fitting-my-head-through-the-arm-holes-or-two-sequences-to-substitute-for-the-missing-pmovmskb-instruction-on-arm-neon/- // The specific implementation below is owed to the - // user 'aqrit' in a comment on the blog above- // There's also https://community.arm.com/arm-community-blogs/b/servers-and-cloud-computing-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon- // - // The input to the move mask must be de-interleaved.- // That is, for a string after vceqq_u8 we have,- // cmp0 = aaaaaaaa / eeeeeeee / ...- // cmp1 = bbbbbbbb / ffffffff / ...- // cmp2 = cccccccc / gggggggg / ...- // cmp3 = dddddddd / hhhhhhhh / ...- // Luckily vld4q_u8 does this for us. Now we want- // to interleave this into a 64bit integer with bits- // abcdefgh...- // cmp0 holds bits for positions- // 0,4,8,..., cmp1 holds bits for 1,5,9,..., and so on.- // So to bring together the bits for different positions- // we right shift and combine with vsriq_n_u8- //- // vsriq_n_u8 shifts each byte of the first operand - // right by n bits, and combines it with the bits of- // the second operand.- // example:- // uint8_t mask = 0xFF >> n; // if n = 1, 0111 1111- // uint8_t shifted = operand1 >> n // 0bbb, bbbb- // operand1 = (operand2 & (!mask)) | shifted- // (aaaa aaaa & 1000 0000) | 0bbb bbbb = abbb bbbb- // - // So we first bring together the bits the first two- // rows and the next two rows (so to speak)- // t0 = abbbbbbb/efffffff/...- uint8x16_t t0 = vsriq_n_u8(cmp1, cmp0, 1);- // t1 = cddddddd/ghhhhhhh/...- uint8x16_t t1 = vsriq_n_u8(cmp3, cmp2, 1);- // Now we must combine each of our combined rows- // so that we get back our column interleaved- // t2 = abcddddd/efghhhhh/...- uint8x16_t t2 = vsriq_n_u8(t1, t0, 2);- // Then to get rid of the repeated bits in the upper half- // t3 = abcdabcd/efghefgh/...- uint8x16_t t3 = vsriq_n_u8(t2, t2, 4);- // and now it's the relatively simple matter of getting- // rid of half the bits. We combine our 8bit words into 16- // bit words for this step, and then we shift right by 4- // and turn the result into an 8 bit word- // afterreinterpert: abcdabcdefghefgh/...- // afterrightshift: 0000abcdabcdefgh/...- // take the lower bits: abcdefgh/...- uint8x8_t t4 = vshrn_n_u16(vreinterpretq_u16_u8(t3), 4);- // Finally we recombine them into a 64 bit integer- // (vreinterpret_u64_u8 here does uint8x8 -> uint64x1- // and vget_lane_u64 does uint64x1 -> uint64) - return vget_lane_u64(vreinterpret_u64_u8(t4), 0);-#endif-}-#endif--// I owe a debt to https://github.com/geofflangdale/simdcsv-// Let's go ahead and assume `in` will only ever get 64 bytes-// initial_quoted will be either all_ones ~0ULL or all_zeros 0ULL-#ifdef HAS_SIMD_CSV-static uint64_t parse_chunk(uint8_t *in, uint8_t separator, uint64_t *initial_quoted) {- uint64_t quotebits = find_character_in_chunk(in, QUOTE_CHAR);- // See https://wunkolo.github.io/post/2020/05/pclmulqdq-tricks/- // Also, section 3.1.1 of Parsing Gigabytes of JSON per Second,- // Geoff Langdale, Daniel Lemire, https://arxiv.org/pdf/1902.08318-#ifdef USE_AVX2- // Use PCLMUL for carryless multiplication on x86- __m128i a = _mm_set_epi64x(0, (int64_t)ALL_ONES_MASK);- __m128i b = _mm_set_epi64x(0, (int64_t)quotebits);- __m128i result = _mm_clmulepi64_si128(a, b, 0);- uint64_t quotemask = (uint64_t)_mm_cvtsi128_si64(result);-#else // USE_NEON- // Use vmull_p64 (PMULL) for carryless multiplication on ARM- // Requires ARM crypto extensions (compile: __ARM_FEATURE_AES, runtime: pmull flag)- uint64_t quotemask = vmull_p64(ALL_ONES_MASK, quotebits);-#endif- quotemask ^= (*initial_quoted);- // Find out if the chunk ends in a quoted region by looking- // at the last bit- (*initial_quoted) = (uint64_t)((int64_t)quotemask >> 63);-- uint64_t commabits = find_character_in_chunk(in, separator);- uint64_t newlinebits = find_character_in_chunk(in, NEWLINE_CHAR);-- uint64_t delimiter_bits = (commabits | newlinebits) & ~quotemask;- return delimiter_bits;-}-#endif--#ifdef HAS_SIMD_CSV-static size_t find_one_indices(size_t start_index, uint64_t bits, size_t *indices, size_t *base) {- size_t position = 0;- uint64_t bitset = bits;- while (bitset != 0) {- // temp only has the least significant bit of- // bitset turned on.- // In twos complement: 0 - x = ~ x + 1- uint64_t temp = bitset & -bitset;- // count trailing zeros- size_t r = __builtin_ctzll(bitset);- indices[(*base) + position] = start_index + r;- position++;-- bitset ^= temp;- }- *base += position;- return position;-}-#endif--size_t get_delimiter_indices(uint8_t *buf, size_t len, uint8_t separator, size_t *indices) {- // Recall we padded our file with 64 empty bytes.- // So if, for example, we had a file of 187 bytes- // We pad it with zeros and so we have 251 bytes- // The chunks we have are ptr + 0, ptr + 64, and pt- // (we don't do ptr + 192 since we do len - 64- // below. This way, in ptr + 128 we have 59 bytes of- // actual data and 5 bytes of zeros. If we didn't do this- // we'd be reading past the end of file on the last row-#ifdef HAS_SIMD_CSV- size_t unpaddedLen = len < 64 ? 0 : len - 64;- uint64_t initial_quoted = 0ULL;- size_t base = 0;- for (size_t i = 0; i < unpaddedLen; i += 64) {- uint8_t *in = buf + i;- uint64_t delimiter_bits = parse_chunk(in, separator, &initial_quoted);- find_one_indices(i, delimiter_bits, indices, &base);- }- return base;-#else- // SIMD not available or carryless multiplication not supported.- // Signal fallback to Haskell implementation.- (void)buf; (void)len; (void)indices;- return (size_t)-1;-#endif-}
− cbits/process_csv.h
@@ -1,33 +0,0 @@-#ifndef PROCESS_CSV-#define PROCESS_CSV--#include <stddef.h>-#include <stdint.h>--// Define feature macros for SIMD support with carryless multiplication-// We need both SIMD instructions AND carryless multiplication for the CSV parser-#if defined(__AVX2__) && defined(__PCLMUL__)- #define HAS_SIMD_CSV 1- #define USE_AVX2 1- #include <immintrin.h>- #include <wmmintrin.h>-#elif defined(__ARM_NEON) && (defined(__ARM_FEATURE_AES) || defined(__ARM_FEATURE_CRYPTO))- // Note: __ARM_FEATURE_CRYPTO is deprecated; prefer __ARM_FEATURE_AES- // We need polynomial multiply (vmull_p64/PMULL) for carryless multiplication- // Runtime check: 'pmull' flag in /proc/cpuinfo on Linux- // We support both macros for compatibility with older compilers- #define HAS_SIMD_CSV 1- #define USE_NEON 1- #include <arm_neon.h>-#endif--// CSV parsing constants-#define COMMA_CHAR 0x2C-#define NEWLINE_CHAR 0x0A-#define QUOTE_CHAR 0x22-#define ALL_ONES_MASK ~0ULL-#define ALL_ZEROS_MASK 0ULL--size_t get_delimiter_indices(uint8_t *buf, size_t len, uint8_t separator, size_t* indices);--#endif
+ data/ml/blobs.csv view
@@ -0,0 +1,151 @@+x,y,cluster+10.338264760556216,0.847533060653685,2.0+-8.030169963007346,5.6974805690319235,0.0+10.72808765826904,0.8199785012860177,2.0+-9.06787703268451,6.456351950062689,0.0+-1.9752744232671209,4.798741021511607,1.0+-7.950069649558368,6.313857688658719,0.0+9.185263691410944,-0.02689531876281781,2.0+-1.2924664642268644,5.094345005459218,1.0+-8.386915515325072,6.748967461389298,0.0+9.47365658115339,-0.3512223275732281,2.0+-9.794694684045528,6.026719329863166,0.0+-8.474546521207202,4.194596399725956,0.0+-1.8006579280809625,4.866203894410084,1.0+-8.798016496863006,3.7677237632249145,0.0+9.608066918226854,1.550543508284739,2.0+-0.46295420029132506,4.91792529440678,1.0+9.085876965700251,0.8012523279907401,2.0+-7.2757229200427895,5.372267655941989,0.0+9.40388503768542,1.2438256022898402,2.0+-8.208254840130058,6.1863971123041805,0.0+-9.8405055750545,4.1558969180185,0.0+-1.0130643168373106,3.870050967862403,1.0+-1.750299605496018,5.8673653480977395,1.0+-7.250170505806332,6.133910399530022,0.0+-0.8427472133887682,4.013725740204026,1.0+-8.783546167149966,7.221633621411183,0.0+-7.801247618806028,5.454386723800413,0.0+10.043495267038827,1.5187267683935621,2.0+-9.428707750327403,4.758362395941994,0.0+-0.7221251049321777,4.573801938428627,1.0+-8.768888987279134,4.073279941977919,0.0+-2.3850503444929023,3.3944007047064506,1.0+9.52005242972435,1.344762816352543,2.0+-1.6485932394255796,4.637081202200104,1.0+-7.634316973046253,5.265196389986278,0.0+-2.873923777802405,6.120339997469102,1.0+-8.01938470265847,4.996106087665121,0.0+8.650080809399894,0.5164250456305327,2.0+-6.944219122655304,5.788217322112048,0.0+9.411501808610677,1.1207212024133286,2.0+8.727685388903897,0.7881397821440224,2.0+9.940157883783744,0.31129800696905785,2.0+-1.5441245562713808,3.7925654469165777,1.0+-8.668033475458893,4.43578271480966,0.0+-8.974177391694264,5.461137235845716,0.0+-8.579735970574333,5.350794772331189,0.0+-0.8507452109763282,5.35913594514609,1.0+9.799881364516688,1.4185320368386882,2.0+7.715643621051086,1.4926421993810217,2.0+9.204875175494418,0.6321707789639058,2.0+10.106053390869812,0.1776808950835841,2.0+-8.549654572872772,6.552844582677501,0.0+10.555503658306312,1.421054556659824,2.0+-0.8409150384679815,4.759055557379698,1.0+10.580079627664762,0.7854064702080787,2.0+-10.125366055017796,5.068648373068846,0.0+-0.36588996340355817,3.8233854113954955,1.0+9.426230450546782,0.5860742620560562,2.0+-1.885479568645208,4.623149715673959,1.0+9.223577182486164,1.5910144728588864,2.0+-7.2711793961873274,5.428189471188245,0.0+9.453560211741014,-0.7499716472172048,2.0+-1.984038662573283,4.364283036544825,1.0+-8.392687024351563,5.800438033246547,0.0+11.030025753690552,0.7766578881818996,2.0+-2.2915284396471147,5.001466504584993,1.0+-1.6145496750257826,4.777612786934544,1.0+10.168318216276937,0.6986657131606543,2.0+-2.2266293822669425,4.0573617784558555,1.0+-8.254266273512885,4.37715621930642,0.0+-9.437210089000642,6.767956346573112,0.0+10.590353086938768,0.2993071633588282,2.0+-2.79339785001823,4.767968183672174,1.0+-1.2140195803496816,4.126048681774031,1.0+-8.141432121082447,4.859540119431719,0.0+9.089586732772219,-0.9279621694174798,2.0+-7.1532744595716755,5.721844273167379,0.0+-8.510143036409373,4.437832885485033,0.0+-1.138567419393835,2.986258859412659,1.0+-8.034838116698193,6.139679036447002,0.0+-1.4384684377722188,5.3796868046940745,1.0+-1.2202242152011087,3.034384563178521,1.0+9.10502555609624,0.429281886212569,2.0+10.585464339946231,0.008965041738488957,2.0+-7.659707807990062,6.078774657537933,0.0+9.823590118434234,0.6308585417952228,2.0+-2.476845488661084,5.076283717062412,1.0+-8.553522718036747,6.958005682199637,0.0+-8.069594713163454,5.389290712648975,0.0+-1.4505216161787537,3.810535489405869,1.0+0.14236866854605823,4.102802122310017,1.0+-8.713989201960986,4.654390170035729,0.0+-1.462202464398755,4.709150238434744,1.0+9.139446464353718,0.9973645548743786,2.0+-2.0942032865826947,3.003362783235963,1.0+9.384194636323251,0.5881152884036329,2.0+-0.09043844586306131,4.594778743582171,1.0+-8.630656062367228,5.125759989711783,0.0+-8.258304908114546,5.178692149281611,0.0+-1.6564983727218412,3.57173364605436,1.0+-8.747527831262346,5.220834523095475,0.0+8.289191474819738,0.9762956762429598,2.0+-0.2667896158894523,5.2537491444891975,1.0+-1.4567017014328651,3.240421719648844,1.0+-2.578532846092601,5.281315504987536,1.0+-7.060905510177389,5.334844843271841,0.0+9.544505126636004,1.2726118372517006,2.0+-1.789088183709013,4.9356797992475885,1.0+-9.012750362421714,5.553681884762233,0.0+-2.0885293775780265,3.819536691473433,1.0+-8.160597657718792,4.443878519201542,0.0+9.953505807271908,0.7455103282555472,2.0+-8.628118580800681,4.175965562435486,0.0+-0.4807339560151918,4.5869562471000975,1.0+9.458262920723499,0.3546075672900924,2.0+-1.3869004247455148,5.027138844442485,1.0+-1.3046003840083444,5.232595722796476,1.0+10.903405766013115,0.2879089162540264,2.0+-8.167336731114846,7.396451887048452,0.0+-1.1612914315034135,3.4000871166542685,1.0+-1.7253529304545083,4.179477773883045,1.0+-0.9912710391638034,5.444758663793727,1.0+-2.3277031463654856,5.054227064960736,1.0+8.147437705805391,1.071564094656846,2.0+9.877041844526358,1.7070321683770884,2.0+-0.3870567318504991,4.922009547520749,1.0+-8.713603202383117,6.635883998983632,0.0+9.549394662765959,1.8843175122058762,2.0+-6.665876623219847,6.293607308205353,0.0+-9.6218049853493,5.999675143347125,0.0+-8.780572710866066,4.886490355985694,0.0+10.03429276137997,0.5300549650285779,2.0+9.157196652555177,1.2812079409319164,2.0+8.1447728798683,0.5915572830522724,2.0+9.146956239818431,1.017246090363041,2.0+-9.165426205789453,5.897872124592354,0.0+9.959392438724798,0.19080965893214255,2.0+8.486697738274069,1.4784940535418505,2.0+-0.3582643293862715,5.840807754114097,1.0+-2.2186345261641804,4.615624117910637,1.0+10.47604297470776,0.4921540622294554,2.0+-2.7604144732798868,4.9784338438022795,1.0+9.937212447233478,-0.1112822931713835,2.0+-8.483627961806011,5.53784507561264,0.0+9.270455589800079,1.4007210675022714,2.0+0.25622916659958644,3.2714846427335473,1.0+10.148892613250826,-0.16841584321546238,2.0+9.702422433216821,2.162194444522933,2.0+-9.104972635421447,5.6000283031270515,0.0+-0.46045498366900267,4.696212695686656,1.0
+ data/ml/golden.json view
@@ -0,0 +1,68 @@+{+ "gbm": {+ "accuracy": 1.0+ },+ "kmeans": {+ "inertia": 173.75480764755966+ },+ "linear_svc": {+ "accuracy": 1.0+ },+ "logistic_binary": {+ "accuracy": 1.0+ },+ "logistic_iris": {+ "accuracy": 0.9733333333333334+ },+ "ols": {+ "coef": [+ -10.009866299810287,+ -239.81564367242294,+ 519.8459200544604,+ 324.38464550232385,+ -792.1756385522323,+ 476.7390210052585,+ 101.04326793803448,+ 177.06323767134674,+ 751.2736995571044,+ 67.62669218370488+ ],+ "intercept": 152.13348416289597+ },+ "pca": {+ "components_abs": [+ [+ 0.3613865917853659,+ 0.08452251406457255,+ 0.8566706059498347,+ 0.3582891971515517+ ],+ [+ 0.6565887712868534,+ 0.7301614347850159,+ 0.17337266279585964,+ 0.0754810199174582+ ]+ ],+ "evr": [+ 0.9246187232017291,+ 0.05306648311706544+ ]+ },+ "ridge": {+ "alpha": 1.0,+ "coef": [+ 29.466111893477002,+ -83.15427636187523,+ 306.3526801506861,+ 201.62773437326962,+ 5.909614367497247,+ -29.515495079689597,+ -152.04028006186414,+ 117.31173160030173,+ 262.9442900143125,+ 111.87895643952352+ ],+ "intercept": 152.133484162896+ }+}
+ data/ml/iris.csv view
@@ -0,0 +1,151 @@+sepal_length,sepal_width,petal_length,petal_width,species+5.1,3.5,1.4,0.2,0.0+4.9,3.0,1.4,0.2,0.0+4.7,3.2,1.3,0.2,0.0+4.6,3.1,1.5,0.2,0.0+5.0,3.6,1.4,0.2,0.0+5.4,3.9,1.7,0.4,0.0+4.6,3.4,1.4,0.3,0.0+5.0,3.4,1.5,0.2,0.0+4.4,2.9,1.4,0.2,0.0+4.9,3.1,1.5,0.1,0.0+5.4,3.7,1.5,0.2,0.0+4.8,3.4,1.6,0.2,0.0+4.8,3.0,1.4,0.1,0.0+4.3,3.0,1.1,0.1,0.0+5.8,4.0,1.2,0.2,0.0+5.7,4.4,1.5,0.4,0.0+5.4,3.9,1.3,0.4,0.0+5.1,3.5,1.4,0.3,0.0+5.7,3.8,1.7,0.3,0.0+5.1,3.8,1.5,0.3,0.0+5.4,3.4,1.7,0.2,0.0+5.1,3.7,1.5,0.4,0.0+4.6,3.6,1.0,0.2,0.0+5.1,3.3,1.7,0.5,0.0+4.8,3.4,1.9,0.2,0.0+5.0,3.0,1.6,0.2,0.0+5.0,3.4,1.6,0.4,0.0+5.2,3.5,1.5,0.2,0.0+5.2,3.4,1.4,0.2,0.0+4.7,3.2,1.6,0.2,0.0+4.8,3.1,1.6,0.2,0.0+5.4,3.4,1.5,0.4,0.0+5.2,4.1,1.5,0.1,0.0+5.5,4.2,1.4,0.2,0.0+4.9,3.1,1.5,0.2,0.0+5.0,3.2,1.2,0.2,0.0+5.5,3.5,1.3,0.2,0.0+4.9,3.6,1.4,0.1,0.0+4.4,3.0,1.3,0.2,0.0+5.1,3.4,1.5,0.2,0.0+5.0,3.5,1.3,0.3,0.0+4.5,2.3,1.3,0.3,0.0+4.4,3.2,1.3,0.2,0.0+5.0,3.5,1.6,0.6,0.0+5.1,3.8,1.9,0.4,0.0+4.8,3.0,1.4,0.3,0.0+5.1,3.8,1.6,0.2,0.0+4.6,3.2,1.4,0.2,0.0+5.3,3.7,1.5,0.2,0.0+5.0,3.3,1.4,0.2,0.0+7.0,3.2,4.7,1.4,1.0+6.4,3.2,4.5,1.5,1.0+6.9,3.1,4.9,1.5,1.0+5.5,2.3,4.0,1.3,1.0+6.5,2.8,4.6,1.5,1.0+5.7,2.8,4.5,1.3,1.0+6.3,3.3,4.7,1.6,1.0+4.9,2.4,3.3,1.0,1.0+6.6,2.9,4.6,1.3,1.0+5.2,2.7,3.9,1.4,1.0+5.0,2.0,3.5,1.0,1.0+5.9,3.0,4.2,1.5,1.0+6.0,2.2,4.0,1.0,1.0+6.1,2.9,4.7,1.4,1.0+5.6,2.9,3.6,1.3,1.0+6.7,3.1,4.4,1.4,1.0+5.6,3.0,4.5,1.5,1.0+5.8,2.7,4.1,1.0,1.0+6.2,2.2,4.5,1.5,1.0+5.6,2.5,3.9,1.1,1.0+5.9,3.2,4.8,1.8,1.0+6.1,2.8,4.0,1.3,1.0+6.3,2.5,4.9,1.5,1.0+6.1,2.8,4.7,1.2,1.0+6.4,2.9,4.3,1.3,1.0+6.6,3.0,4.4,1.4,1.0+6.8,2.8,4.8,1.4,1.0+6.7,3.0,5.0,1.7,1.0+6.0,2.9,4.5,1.5,1.0+5.7,2.6,3.5,1.0,1.0+5.5,2.4,3.8,1.1,1.0+5.5,2.4,3.7,1.0,1.0+5.8,2.7,3.9,1.2,1.0+6.0,2.7,5.1,1.6,1.0+5.4,3.0,4.5,1.5,1.0+6.0,3.4,4.5,1.6,1.0+6.7,3.1,4.7,1.5,1.0+6.3,2.3,4.4,1.3,1.0+5.6,3.0,4.1,1.3,1.0+5.5,2.5,4.0,1.3,1.0+5.5,2.6,4.4,1.2,1.0+6.1,3.0,4.6,1.4,1.0+5.8,2.6,4.0,1.2,1.0+5.0,2.3,3.3,1.0,1.0+5.6,2.7,4.2,1.3,1.0+5.7,3.0,4.2,1.2,1.0+5.7,2.9,4.2,1.3,1.0+6.2,2.9,4.3,1.3,1.0+5.1,2.5,3.0,1.1,1.0+5.7,2.8,4.1,1.3,1.0+6.3,3.3,6.0,2.5,2.0+5.8,2.7,5.1,1.9,2.0+7.1,3.0,5.9,2.1,2.0+6.3,2.9,5.6,1.8,2.0+6.5,3.0,5.8,2.2,2.0+7.6,3.0,6.6,2.1,2.0+4.9,2.5,4.5,1.7,2.0+7.3,2.9,6.3,1.8,2.0+6.7,2.5,5.8,1.8,2.0+7.2,3.6,6.1,2.5,2.0+6.5,3.2,5.1,2.0,2.0+6.4,2.7,5.3,1.9,2.0+6.8,3.0,5.5,2.1,2.0+5.7,2.5,5.0,2.0,2.0+5.8,2.8,5.1,2.4,2.0+6.4,3.2,5.3,2.3,2.0+6.5,3.0,5.5,1.8,2.0+7.7,3.8,6.7,2.2,2.0+7.7,2.6,6.9,2.3,2.0+6.0,2.2,5.0,1.5,2.0+6.9,3.2,5.7,2.3,2.0+5.6,2.8,4.9,2.0,2.0+7.7,2.8,6.7,2.0,2.0+6.3,2.7,4.9,1.8,2.0+6.7,3.3,5.7,2.1,2.0+7.2,3.2,6.0,1.8,2.0+6.2,2.8,4.8,1.8,2.0+6.1,3.0,4.9,1.8,2.0+6.4,2.8,5.6,2.1,2.0+7.2,3.0,5.8,1.6,2.0+7.4,2.8,6.1,1.9,2.0+7.9,3.8,6.4,2.0,2.0+6.4,2.8,5.6,2.2,2.0+6.3,2.8,5.1,1.5,2.0+6.1,2.6,5.6,1.4,2.0+7.7,3.0,6.1,2.3,2.0+6.3,3.4,5.6,2.4,2.0+6.4,3.1,5.5,1.8,2.0+6.0,3.0,4.8,1.8,2.0+6.9,3.1,5.4,2.1,2.0+6.7,3.1,5.6,2.4,2.0+6.9,3.1,5.1,2.3,2.0+5.8,2.7,5.1,1.9,2.0+6.8,3.2,5.9,2.3,2.0+6.7,3.3,5.7,2.5,2.0+6.7,3.0,5.2,2.3,2.0+6.3,2.5,5.0,1.9,2.0+6.5,3.0,5.2,2.0,2.0+6.2,3.4,5.4,2.3,2.0+5.9,3.0,5.1,1.8,2.0
+ data/ml/iris_binary.csv view
@@ -0,0 +1,151 @@+sepal_length,sepal_width,petal_length,petal_width,label+5.1,3.5,1.4,0.2,0.0+4.9,3.0,1.4,0.2,0.0+4.7,3.2,1.3,0.2,0.0+4.6,3.1,1.5,0.2,0.0+5.0,3.6,1.4,0.2,0.0+5.4,3.9,1.7,0.4,0.0+4.6,3.4,1.4,0.3,0.0+5.0,3.4,1.5,0.2,0.0+4.4,2.9,1.4,0.2,0.0+4.9,3.1,1.5,0.1,0.0+5.4,3.7,1.5,0.2,0.0+4.8,3.4,1.6,0.2,0.0+4.8,3.0,1.4,0.1,0.0+4.3,3.0,1.1,0.1,0.0+5.8,4.0,1.2,0.2,0.0+5.7,4.4,1.5,0.4,0.0+5.4,3.9,1.3,0.4,0.0+5.1,3.5,1.4,0.3,0.0+5.7,3.8,1.7,0.3,0.0+5.1,3.8,1.5,0.3,0.0+5.4,3.4,1.7,0.2,0.0+5.1,3.7,1.5,0.4,0.0+4.6,3.6,1.0,0.2,0.0+5.1,3.3,1.7,0.5,0.0+4.8,3.4,1.9,0.2,0.0+5.0,3.0,1.6,0.2,0.0+5.0,3.4,1.6,0.4,0.0+5.2,3.5,1.5,0.2,0.0+5.2,3.4,1.4,0.2,0.0+4.7,3.2,1.6,0.2,0.0+4.8,3.1,1.6,0.2,0.0+5.4,3.4,1.5,0.4,0.0+5.2,4.1,1.5,0.1,0.0+5.5,4.2,1.4,0.2,0.0+4.9,3.1,1.5,0.2,0.0+5.0,3.2,1.2,0.2,0.0+5.5,3.5,1.3,0.2,0.0+4.9,3.6,1.4,0.1,0.0+4.4,3.0,1.3,0.2,0.0+5.1,3.4,1.5,0.2,0.0+5.0,3.5,1.3,0.3,0.0+4.5,2.3,1.3,0.3,0.0+4.4,3.2,1.3,0.2,0.0+5.0,3.5,1.6,0.6,0.0+5.1,3.8,1.9,0.4,0.0+4.8,3.0,1.4,0.3,0.0+5.1,3.8,1.6,0.2,0.0+4.6,3.2,1.4,0.2,0.0+5.3,3.7,1.5,0.2,0.0+5.0,3.3,1.4,0.2,0.0+7.0,3.2,4.7,1.4,1.0+6.4,3.2,4.5,1.5,1.0+6.9,3.1,4.9,1.5,1.0+5.5,2.3,4.0,1.3,1.0+6.5,2.8,4.6,1.5,1.0+5.7,2.8,4.5,1.3,1.0+6.3,3.3,4.7,1.6,1.0+4.9,2.4,3.3,1.0,1.0+6.6,2.9,4.6,1.3,1.0+5.2,2.7,3.9,1.4,1.0+5.0,2.0,3.5,1.0,1.0+5.9,3.0,4.2,1.5,1.0+6.0,2.2,4.0,1.0,1.0+6.1,2.9,4.7,1.4,1.0+5.6,2.9,3.6,1.3,1.0+6.7,3.1,4.4,1.4,1.0+5.6,3.0,4.5,1.5,1.0+5.8,2.7,4.1,1.0,1.0+6.2,2.2,4.5,1.5,1.0+5.6,2.5,3.9,1.1,1.0+5.9,3.2,4.8,1.8,1.0+6.1,2.8,4.0,1.3,1.0+6.3,2.5,4.9,1.5,1.0+6.1,2.8,4.7,1.2,1.0+6.4,2.9,4.3,1.3,1.0+6.6,3.0,4.4,1.4,1.0+6.8,2.8,4.8,1.4,1.0+6.7,3.0,5.0,1.7,1.0+6.0,2.9,4.5,1.5,1.0+5.7,2.6,3.5,1.0,1.0+5.5,2.4,3.8,1.1,1.0+5.5,2.4,3.7,1.0,1.0+5.8,2.7,3.9,1.2,1.0+6.0,2.7,5.1,1.6,1.0+5.4,3.0,4.5,1.5,1.0+6.0,3.4,4.5,1.6,1.0+6.7,3.1,4.7,1.5,1.0+6.3,2.3,4.4,1.3,1.0+5.6,3.0,4.1,1.3,1.0+5.5,2.5,4.0,1.3,1.0+5.5,2.6,4.4,1.2,1.0+6.1,3.0,4.6,1.4,1.0+5.8,2.6,4.0,1.2,1.0+5.0,2.3,3.3,1.0,1.0+5.6,2.7,4.2,1.3,1.0+5.7,3.0,4.2,1.2,1.0+5.7,2.9,4.2,1.3,1.0+6.2,2.9,4.3,1.3,1.0+5.1,2.5,3.0,1.1,1.0+5.7,2.8,4.1,1.3,1.0+6.3,3.3,6.0,2.5,1.0+5.8,2.7,5.1,1.9,1.0+7.1,3.0,5.9,2.1,1.0+6.3,2.9,5.6,1.8,1.0+6.5,3.0,5.8,2.2,1.0+7.6,3.0,6.6,2.1,1.0+4.9,2.5,4.5,1.7,1.0+7.3,2.9,6.3,1.8,1.0+6.7,2.5,5.8,1.8,1.0+7.2,3.6,6.1,2.5,1.0+6.5,3.2,5.1,2.0,1.0+6.4,2.7,5.3,1.9,1.0+6.8,3.0,5.5,2.1,1.0+5.7,2.5,5.0,2.0,1.0+5.8,2.8,5.1,2.4,1.0+6.4,3.2,5.3,2.3,1.0+6.5,3.0,5.5,1.8,1.0+7.7,3.8,6.7,2.2,1.0+7.7,2.6,6.9,2.3,1.0+6.0,2.2,5.0,1.5,1.0+6.9,3.2,5.7,2.3,1.0+5.6,2.8,4.9,2.0,1.0+7.7,2.8,6.7,2.0,1.0+6.3,2.7,4.9,1.8,1.0+6.7,3.3,5.7,2.1,1.0+7.2,3.2,6.0,1.8,1.0+6.2,2.8,4.8,1.8,1.0+6.1,3.0,4.9,1.8,1.0+6.4,2.8,5.6,2.1,1.0+7.2,3.0,5.8,1.6,1.0+7.4,2.8,6.1,1.9,1.0+7.9,3.8,6.4,2.0,1.0+6.4,2.8,5.6,2.2,1.0+6.3,2.8,5.1,1.5,1.0+6.1,2.6,5.6,1.4,1.0+7.7,3.0,6.1,2.3,1.0+6.3,3.4,5.6,2.4,1.0+6.4,3.1,5.5,1.8,1.0+6.0,3.0,4.8,1.8,1.0+6.9,3.1,5.4,2.1,1.0+6.7,3.1,5.6,2.4,1.0+6.9,3.1,5.1,2.3,1.0+5.8,2.7,5.1,1.9,1.0+6.8,3.2,5.9,2.3,1.0+6.7,3.3,5.7,2.5,1.0+6.7,3.0,5.2,2.3,1.0+6.3,2.5,5.0,1.9,1.0+6.5,3.0,5.2,2.0,1.0+6.2,3.4,5.4,2.3,1.0+5.9,3.0,5.1,1.8,1.0
+ data/ml/regression.csv view
@@ -0,0 +1,443 @@+f0,f1,f2,f3,f4,f5,f6,f7,f8,f9,target+0.038075906433423026,0.05068011873981862,0.061696206518683294,0.0218723855140367,-0.04422349842444599,-0.03482076283769895,-0.04340084565202491,-0.002592261998183278,0.019907486170462722,-0.01764612515980379,151.0+-0.0018820165277906047,-0.044641636506989144,-0.051474061238800654,-0.02632752814785296,-0.008448724111216851,-0.019163339748222204,0.07441156407875721,-0.03949338287409329,-0.0683315470939731,-0.092204049626824,75.0+0.08529890629667548,0.05068011873981862,0.04445121333659049,-0.00567042229275739,-0.04559945128264711,-0.03419446591411989,-0.03235593223976409,-0.002592261998183278,0.002861309289833047,-0.025930338989472702,141.0+-0.0890629393522567,-0.044641636506989144,-0.011595014505211082,-0.03665608107540074,0.01219056876179996,0.02499059336410222,-0.036037570043851025,0.03430885887772673,0.022687744966501246,-0.009361911330134878,206.0+0.005383060374248237,-0.044641636506989144,-0.03638469220446948,0.0218723855140367,0.003934851612593237,0.015596139510416171,0.008142083605192267,-0.002592261998183278,-0.03198763948805312,-0.04664087356364498,135.0+-0.09269547780327612,-0.044641636506989144,-0.040695940499992665,-0.019441826196154435,-0.06899064987206617,-0.07928784441181291,0.04127682384197474,-0.0763945037500033,-0.041176166918895155,-0.09634615654165846,97.0+-0.045472477940023646,0.05068011873981862,-0.047162812943277475,-0.015998975220305175,-0.04009563984984263,-0.02480001206043385,0.0007788079970183853,-0.03949338287409329,-0.06291687914365544,-0.03835665973397607,138.0+0.06350367559055897,0.05068011873981862,-0.0018947058402839008,0.0666294482000771,0.09061988167926385,0.10891438112369757,0.022868634821540033,0.01770335448356722,-0.0358161925842373,0.0030644094143684884,63.0+0.04170844488444244,0.05068011873981862,0.061696206518683294,-0.04009893205125,-0.013952535544021335,0.0062016856567301245,-0.028674294435677143,-0.002592261998183278,-0.014959693812643405,0.0113486232440374,110.0+-0.07090024709715959,-0.044641636506989144,0.039062152967186486,-0.03321323009955148,-0.012576582685820214,-0.034507614375909414,-0.024992656631590206,-0.002592261998183278,0.06773705306493534,-0.013504018244969336,310.0+-0.09632801625429555,-0.044641636506989144,-0.08380842345522464,0.008100981610639655,-0.10338947132709418,-0.09056118903623617,-0.01394774321932938,-0.0763945037500033,-0.06291687914365544,-0.03421455281914162,101.0+0.027178291080364757,0.05068011873981862,0.0175059114895705,-0.03321323009955148,-0.007072771253015731,0.045971540304001066,-0.06549067247654655,0.07120997975363674,-0.09643494994048712,-0.05906719430814835,69.0+0.016280675727306498,-0.044641636506989144,-0.028840007687303888,-0.009113273268606652,-0.004320865536613489,-0.009768885894536158,0.04495846164606168,-0.03949338287409329,-0.030747917533098208,-0.042498766648810526,179.0+0.005383060374248237,0.05068011873981862,-0.0018947058402839008,0.008100981610639655,-0.004320865536613489,-0.015718706668537318,-0.002902829807068556,-0.002592261998183278,0.038393928263466416,-0.013504018244969336,185.0+0.04534098333546186,-0.044641636506989144,-0.02560657146566148,-0.012556124244455912,0.017694380194604446,-6.128357906057276e-05,0.0817748396869311,-0.03949338287409329,-0.03198763948805312,-0.07563562196748617,118.0+-0.052737554842062495,0.05068011873981862,-0.018061886948495892,0.08040085210347414,0.08924392882106273,0.10766178727653941,-0.03971920784793797,0.10811110062954676,0.036060333995316066,-0.042498766648810526,171.0+-0.005514554978810025,-0.044641636506989144,0.0422955891888289,0.049415193320830796,0.024574144485610048,-0.02386056667506523,0.07441156407875721,-0.03949338287409329,0.05227699103843915,0.027917050903375224,166.0+0.0707687524925978,0.05068011873981862,0.012116851120166501,0.056300895272529315,0.0342058144930179,0.04941617338368593,-0.03971920784793797,0.03430885887772673,0.02736404910541198,-0.0010776975004659671,144.0+-0.03820740103798481,-0.044641636506989144,-0.010517202431330305,-0.03665608107540074,-0.037343734133440394,-0.019476488210011744,-0.028674294435677143,-0.002592261998183278,-0.018113692315690322,-0.01764612515980379,97.0+-0.027309785684926546,-0.044641636506989144,-0.018061886948495892,-0.04009893205125,-0.0029449126784123676,-0.011334628203483833,0.0375951860378878,-0.03949338287409329,-0.00894339609006817,-0.05492508739331389,168.0+-0.04910501639104307,-0.044641636506989144,-0.05686312160820465,-0.04354178302709927,-0.04559945128264711,-0.043275771306016404,0.0007788079970183853,-0.03949338287409329,-0.011896851335695978,0.015490730158871856,68.0+-0.08543040090123728,0.05068011873981862,-0.022373135244019075,0.0012152796589411327,-0.037343734133440394,-0.02636575436938152,0.01550535921336615,-0.03949338287409329,-0.07213275338232743,-0.01764612515980379,49.0+-0.08543040090123728,-0.044641636506989144,-0.004050329988045492,-0.009113273268606652,-0.0029449126784123676,0.0077674279656778,0.022868634821540033,-0.03949338287409329,-0.061175799045152635,-0.013504018244969336,68.0+0.04534098333546186,0.05068011873981862,0.06061839444480248,0.031064797619554236,0.028702003060213414,-0.04734670130928034,-0.05444575906428573,0.07120997975363674,0.13359728192191356,0.13561183068907107,245.0+-0.06363517019512076,-0.044641636506989144,0.03582871674554409,-0.0228846771720037,-0.030463969842434782,-0.018850191286432668,-0.006584467611155497,-0.002592261998183278,-0.025953110560258,-0.05492508739331389,184.0+-0.06726770864614018,0.05068011873981862,-0.012672826579091896,-0.04009893205125,-0.015328488402222454,0.00463594334778245,-0.05812739686837268,0.03430885887772673,0.019196469166885697,-0.03421455281914162,202.0+-0.1072256316073538,-0.044641636506989144,-0.07734155101193986,-0.02632752814785296,-0.08962994274508297,-0.09619786134844781,0.026550272625626974,-0.0763945037500033,-0.042570854118219384,-0.005219804415300423,137.0+-0.02367724723390713,-0.044641636506989144,0.05954058237092167,-0.04009893205125,-0.04284754556624487,-0.04358891976780594,0.01182372140927921,-0.03949338287409329,-0.015998872510179042,0.040343371647878594,85.0+0.0526060602375007,-0.044641636506989144,-0.0212953231701383,-0.07452744180974262,-0.04009563984984263,-0.03763909899380476,-0.006584467611155497,-0.03949338287409329,-0.0006117353045626216,-0.05492508739331389,131.0+0.06713621404157838,0.05068011873981862,-0.006205954135807083,0.06318659722422783,-0.04284754556624487,-0.09588471288665826,0.05232173725423556,-0.0763945037500033,0.059423623484649676,0.05276969239238195,283.0+-0.06000263174410134,-0.044641636506989144,0.04445121333659049,-0.019441826196154435,-0.009824676969417972,-0.007576846662009428,0.022868634821540033,-0.03949338287409329,-0.02712902329694316,-0.009361911330134878,129.0+-0.02367724723390713,-0.044641636506989144,-0.06548561819925106,-0.08141314376144114,-0.038719686991641515,-0.05360967054507104,0.059685012862409445,-0.0763945037500033,-0.0371288393600719,-0.042498766648810526,59.0+0.0344433679824036,0.05068011873981862,0.12528711887765046,0.028758087465735226,-0.05385516843185383,-0.012900370512431508,-0.10230705051741597,0.10811110062954676,0.00027247814860377354,0.027917050903375224,341.0+0.03081082953138418,-0.044641636506989144,-0.050396249164919873,-0.002227571316908129,-0.04422349842444599,-0.0899348921126571,0.1185912177278005,-0.0763945037500033,-0.018113692315690322,0.0030644094143684884,87.0+0.016280675727306498,-0.044641636506989144,-0.06332999405148947,-0.057313186930496314,-0.0579830270064572,-0.048912443618228024,0.008142083605192267,-0.03949338287409329,-0.05947118135708968,-0.06735140813781726,65.0+0.04897352178648128,0.05068011873981862,-0.03099563183506548,-0.04929134415676754,0.04934129593323023,-0.004132213582324539,0.13331776894414826,-0.05351580880693909,0.021311288972396977,0.019632837073706312,102.0+0.012648137276287077,-0.044641636506989144,0.022894971858974496,0.052858044296680055,0.0080627101871966,-0.02855779360190825,0.0375951860378878,-0.03949338287409329,0.05471997253790904,-0.025930338989472702,265.0+-0.009147093429829445,-0.044641636506989144,0.011039039046285686,-0.057313186930496314,-0.0249601584096303,-0.04296262284422686,0.030231910429713918,-0.03949338287409329,0.017036071348324546,-0.005219804415300423,276.0+-0.0018820165277906047,0.05068011873981862,0.07139651518361048,0.09761510698272045,0.08786797596286161,0.07540749571221732,-0.02131101882750326,0.07120997975363674,0.07142887212197009,0.02377494398854077,252.0+-0.0018820165277906047,0.05068011873981862,0.014272475267928093,-0.07452744180974262,0.0025588987543921156,0.0062016856567301245,-0.01394774321932938,-0.002592261998183278,0.019196469166885697,0.0030644094143684884,90.0+0.005383060374248237,0.05068011873981862,-0.008361578283568675,0.0218723855140367,0.05484510736603471,0.07321545647969056,-0.024992656631590206,0.03430885887772673,0.012551194864223063,0.09419076154072652,100.0+-0.09996055470531495,-0.044641636506989144,-0.06764124234701265,-0.10895595156823522,-0.07449446130487065,-0.07271172671423268,0.01550535921336615,-0.03949338287409329,-0.049872451808799324,-0.009361911330134878,55.0+-0.06000263174410134,0.05068011873981862,-0.010517202431330305,-0.014862834398274925,-0.04972730985725048,-0.02354741821327569,-0.05812739686837268,0.01585829843977173,-0.009918765569334137,-0.03421455281914162,61.0+0.01991321417832592,-0.044641636506989144,-0.023450947317899894,-0.07108459083389335,0.020446285911006685,-0.010082034356325698,0.1185912177278005,-0.0763945037500033,-0.042570854118219384,0.07348022696655424,92.0+0.04534098333546186,0.05068011873981862,0.0681630789619681,0.008100981610639655,-0.016704441260423575,0.00463594334778245,-0.07653558588880739,0.07120997975363674,0.03243232415655107,-0.01764612515980379,259.0+0.027178291080364757,0.05068011873981862,-0.035306880130588664,0.03220093844158448,-0.011200629827619093,0.0015044587298871017,-0.010266105415242439,-0.002592261998183278,-0.014959693812643405,-0.05078298047847944,53.0+-0.056370093293081916,-0.044641636506989144,-0.011595014505211082,-0.03321323009955148,-0.046975404140848234,-0.047659849771069886,0.0044604458011053266,-0.03949338287409329,-0.007977142213412223,-0.08806194271198954,190.0+-0.07816532399919843,-0.044641636506989144,-0.07303030271641665,-0.057313186930496314,-0.0841261313122785,-0.07427746902318036,-0.024992656631590206,-0.03949338287409329,-0.018113692315690322,-0.08391983579715509,142.0+0.06713621404157838,0.05068011873981862,-0.041773752573873474,0.011543832586488917,0.0025588987543921156,0.0058885371949405855,0.04127682384197474,-0.03949338287409329,-0.05947118135708968,-0.021788232074638245,75.0+-0.04183993948900423,0.05068011873981862,0.014272475267928093,-0.00567042229275739,-0.012576582685820214,0.0062016856567301245,-0.07285394808472044,0.07120997975363674,0.03545870422305857,-0.013504018244969336,142.0+0.0344433679824036,-0.044641636506989144,-0.007283766209687899,0.014986683562338177,-0.04422349842444599,-0.03732595053201525,-0.002902829807068556,-0.03949338287409329,-0.021395309255276825,0.007206516329202944,155.0+0.059871137139539544,0.05068011873981862,0.016428099415689682,0.028758087465735226,-0.04147159270804375,-0.02918409052548733,-0.028674294435677143,-0.002592261998183278,-0.002398393416115213,-0.021788232074638245,225.0+-0.052737554842062495,-0.044641636506989144,-0.00943939035744949,-0.00567042229275739,0.039709625925822375,0.04471894645684291,0.026550272625626974,-0.002592261998183278,-0.018113692315690322,-0.013504018244969336,59.0+-0.009147093429829445,-0.044641636506989144,-0.015906262800734303,0.07007229917592636,0.01219056876179996,0.022172257207996385,0.01550535921336615,-0.002592261998183278,-0.03324559264822791,0.0486275854775475,104.0+-0.04910501639104307,-0.044641636506989144,0.02505059600673609,0.008100981610639655,0.020446285911006685,0.017788178742942903,0.05232173725423556,-0.03949338287409329,-0.041176166918895155,0.007206516329202944,182.0+-0.04183993948900423,-0.044641636506989144,-0.049318437091039065,-0.03665608107540074,-0.007072771253015731,-0.022607972827907094,0.08545647749101803,-0.03949338287409329,-0.06649019536676071,0.007206516329202944,128.0+-0.04183993948900423,-0.044641636506989144,0.04121777711494808,-0.02632752814785296,-0.0318399227006359,-0.030436684372645465,-0.036037570043851025,0.0029429061332032365,0.033653814906286016,-0.01764612515980379,52.0+-0.027309785684926546,-0.044641636506989144,-0.06332999405148947,-0.05042748497879779,-0.08962994274508297,-0.1043397213549757,0.05232173725423556,-0.0763945037500033,-0.05615310200706333,-0.06735140813781726,37.0+0.04170844488444244,-0.044641636506989144,-0.06440780612537028,0.03564378941743375,0.01219056876179996,-0.05799374901012452,0.18117906039727852,-0.0763945037500033,-0.0006117353045626216,-0.05078298047847944,170.0+0.06350367559055897,0.05068011873981862,-0.02560657146566148,0.011543832586488917,0.06447677737344255,0.048476727998317336,0.030231910429713918,-0.002592261998183278,0.038393928263466416,0.019632837073706312,170.0+-0.07090024709715959,-0.044641636506989144,-0.004050329988045492,-0.04009893205125,-0.06623874415566393,-0.07866154748823384,0.05232173725423556,-0.0763945037500033,-0.05140387304727299,-0.03421455281914162,61.0+-0.04183993948900423,0.05068011873981862,0.004572166603000912,-0.05387033595464705,-0.04422349842444599,-0.02730519975475012,-0.08021722369289432,0.07120997975363674,0.03664373256235367,0.019632837073706312,144.0+-0.027309785684926546,0.05068011873981862,-0.007283766209687899,-0.04009893205125,-0.011200629827619093,-0.013839815897800126,0.059685012862409445,-0.03949338287409329,-0.08237869071592514,-0.025930338989472702,52.0+-0.03457486258696539,-0.044641636506989144,-0.03746250427835029,-0.060756037906345574,0.020446285911006685,0.043466352609684754,-0.01394774321932938,-0.002592261998183278,-0.030747917533098208,-0.07149351505265171,128.0+0.06713621404157838,0.05068011873981862,-0.02560657146566148,-0.04009893205125,-0.06348683843926169,-0.05987263978086174,-0.002902829807068556,-0.03949338287409329,-0.019198449026275908,0.0113486232440374,71.0+-0.045472477940023646,0.05068011873981862,-0.02452875939178067,0.059743746248378575,0.005310804470794357,0.014969842586837095,-0.05444575906428573,0.07120997975363674,0.04234098419358016,0.015490730158871856,163.0+-0.009147093429829445,0.05068011873981862,-0.018061886948495892,-0.03321323009955148,-0.02083229983502694,0.012151506430731283,-0.07285394808472044,0.07120997975363674,0.00027247814860377354,0.019632837073706312,150.0+0.04170844488444244,0.05068011873981862,-0.014828450726853487,-0.017135116042335426,-0.005696818394814609,0.008393724889256856,-0.01394774321932938,-0.0018542395806650938,-0.011896851335695978,0.0030644094143684884,97.0+0.038075906433423026,0.05068011873981862,-0.029917819761184662,-0.04009893205125,-0.033215875558837024,-0.02417371513685477,-0.010266105415242439,-0.002592261998183278,-0.012908683225401873,0.0030644094143684884,160.0+0.016280675727306498,-0.044641636506989144,-0.04608500086939666,-0.00567042229275739,-0.07587041416307178,-0.06143838208980942,-0.01394774321932938,-0.03949338287409329,-0.05140387304727299,0.019632837073706312,178.0+-0.0018820165277906047,-0.044641636506989144,-0.06979686649477428,-0.012556124244455912,-0.00019300696201012598,-0.009142588970957101,0.07072992627467027,-0.03949338287409329,-0.06291687914365544,0.040343371647878594,48.0+-0.0018820165277906047,-0.044641636506989144,0.03367309259778249,0.12515791478951455,0.024574144485610048,0.02624318721126033,-0.010266105415242439,-0.002592261998183278,0.02671684132010462,0.06105390622205087,270.0+0.06350367559055897,0.05068011873981862,-0.004050329988045492,-0.012556124244455912,0.10300345740307394,0.04878987646010685,0.05600337505832251,-0.002592261998183278,0.08449153066204618,-0.01764612515980379,202.0+0.012648137276287077,0.05068011873981862,-0.020217511096257485,-0.002227571316908129,0.03833367306762126,0.05317395492516036,-0.006584467611155497,0.03430885887772673,-0.005142189801713891,-0.009361911330134878,111.0+0.012648137276287077,0.05068011873981862,0.002416542455239321,0.056300895272529315,0.027326050202012293,0.017161881819363848,0.04127682384197474,-0.03949338287409329,0.0037090603325595967,0.07348022696655424,85.0+-0.009147093429829445,0.05068011873981862,-0.03099563183506548,-0.02632752814785296,-0.011200629827619093,-0.0010007289644291908,-0.02131101882750326,-0.002592261998183278,0.006206735447689297,0.027917050903375224,42.0+-0.030942324135945967,0.05068011873981862,0.028284032228378497,0.07007229917592636,-0.12678066991651324,-0.10684490904929198,-0.05444575906428573,-0.047980640675552584,-0.030747917533098208,0.015490730158871856,170.0+-0.09632801625429555,-0.044641636506989144,-0.03638469220446948,-0.07452744180974262,-0.038719686991641515,-0.02761834821653966,0.01550535921336615,-0.03949338287409329,-0.07409260794346935,-0.0010776975004659671,200.0+0.005383060374248237,-0.044641636506989144,-0.05794093368208547,-0.0228846771720037,-0.06761469701386505,-0.0683276482491792,-0.05444575906428573,-0.002592261998183278,0.04289703595278786,-0.08391983579715509,252.0+-0.10359309315633439,-0.044641636506989144,-0.03746250427835029,-0.02632752814785296,0.0025588987543921156,0.019980217975469634,0.01182372140927921,-0.002592261998183278,-0.0683315470939731,-0.025930338989472702,113.0+0.0707687524925978,-0.044641636506989144,0.012116851120166501,0.04252949136913227,0.07135654166444816,0.053487103386949876,0.05232173725423556,-0.002592261998183278,0.025395078941660074,-0.005219804415300423,143.0+0.012648137276287077,0.05068011873981862,-0.022373135244019075,-0.029770379123702218,0.010814615903598841,0.02843522644378708,-0.02131101882750326,0.03430885887772673,-0.006081096870540014,-0.0010776975004659671,51.0+-0.016412170331868287,-0.044641636506989144,-0.035306880130588664,-0.02632752814785296,0.03282986163481677,0.017161881819363848,0.10018302870736581,-0.03949338287409329,-0.07020936123162536,-0.07977772888232063,52.0+-0.03820740103798481,-0.044641636506989144,0.009961226972404908,-0.04698463400294853,-0.05935897986465832,-0.05298337362149199,-0.010266105415242439,-0.03949338287409329,-0.015998872510179042,-0.042498766648810526,210.0+0.001750521923228816,-0.044641636506989144,-0.039618128426111884,-0.10093410879450646,-0.029088016984233665,-0.03012353591085593,0.04495846164606168,-0.05019470792810719,-0.0683315470939731,-0.1294830118603341,65.0+0.04534098333546186,-0.044641636506989144,0.07139651518361048,0.0012152796589411327,-0.009824676969417972,-0.0010007289644291908,0.01550535921336615,-0.03949338287409329,-0.041176166918895155,-0.07149351505265171,141.0+-0.07090024709715959,0.05068011873981862,-0.07518592686417827,-0.04009893205125,-0.051103262715451604,-0.015092409744958261,-0.03971920784793797,-0.002592261998183278,-0.09643494994048712,-0.03421455281914162,55.0+0.04534098333546186,-0.044641636506989144,-0.006205954135807083,0.011543832586488917,0.06310082451524143,0.01622243643399523,0.09650139090327886,-0.03949338287409329,0.04289703595278786,-0.03835665973397607,134.0+-0.052737554842062495,0.05068011873981862,-0.040695940499992665,-0.06764173985804409,-0.0318399227006359,-0.037012802070225705,0.0375951860378878,-0.03949338287409329,-0.03452177701362266,0.06933812005171978,42.0+-0.045472477940023646,-0.044641636506989144,-0.048240625017158284,-0.019441826196154435,-0.00019300696201012598,-0.016031855130326858,0.06704828847058333,-0.03949338287409329,-0.024795429028792802,0.019632837073706312,111.0+0.012648137276287077,-0.044641636506989144,-0.02560657146566148,-0.04009893205125,-0.030463969842434782,-0.045154662076753616,0.07809320188284416,-0.0763945037500033,-0.07213275338232743,0.0113486232440374,98.0+0.04534098333546186,-0.044641636506989144,0.05199589785375607,-0.05387033595464705,0.06310082451524143,0.06476044801137316,-0.010266105415242439,0.03430885887772673,0.037236246732001224,0.019632837073706312,164.0+-0.020044708782887707,-0.044641636506989144,0.004572166603000912,0.09761510698272045,0.005310804470794357,-0.02072908205716988,0.06336665066649638,-0.03949338287409329,0.012551194864223063,0.0113486232440374,48.0+-0.04910501639104307,-0.044641636506989144,-0.06440780612537028,-0.10207024961653671,-0.0029449126784123676,-0.015405558206747801,0.06336665066649638,-0.047242618258034386,-0.03324559264822791,-0.05492508739331389,96.0+-0.07816532399919843,-0.044641636506989144,-0.01698407487461508,-0.012556124244455912,-0.00019300696201012598,-0.013526667436010586,0.07072992627467027,-0.03949338287409329,-0.041176166918895155,-0.092204049626824,90.0+-0.07090024709715959,-0.044641636506989144,-0.05794093368208547,-0.08141314376144114,-0.04559945128264711,-0.02887094206369779,-0.04340084565202491,-0.002592261998183278,0.0011475759991601464,-0.005219804415300423,162.0+0.056238598688520124,0.05068011873981862,0.009961226972404908,0.049415193320830796,-0.004320865536613489,-0.012274073588852453,-0.04340084565202491,0.03430885887772673,0.060790963876144,0.03205915781820968,150.0+-0.027309785684926546,-0.044641636506989144,0.08864150836570328,-0.02519138732582271,0.02182223876920781,0.04252690722431616,-0.03235593223976409,0.03430885887772673,0.002861309289833047,0.07762233388138869,279.0+0.001750521923228816,0.05068011873981862,-0.0051281420619263066,-0.012556124244455912,-0.015328488402222454,-0.013839815897800126,0.008142083605192267,-0.03949338287409329,-0.006081096870540014,-0.06735140813781726,92.0+-0.0018820165277906047,-0.044641636506989144,-0.06440780612537028,0.011543832586488917,0.027326050202012293,0.03751653183568362,-0.01394774321932938,0.03430885887772673,0.011785484244986226,-0.05492508739331389,83.0+0.016280675727306498,-0.044641636506989144,0.0175059114895705,-0.0228846771720037,0.06034891879883919,0.04440579799505339,0.030231910429713918,-0.002592261998183278,0.037236246732001224,-0.0010776975004659671,128.0+0.016280675727306498,0.05068011873981862,-0.04500718879551588,0.06318659722422783,0.010814615903598841,-0.00037443204085011215,0.06336665066649638,-0.03949338287409329,-0.030747917533098208,0.036201264733044136,102.0+-0.09269547780327612,-0.044641636506989144,0.028284032228378497,-0.015998975220305175,0.03695772020942014,0.02499059336410222,0.05600337505832251,-0.03949338287409329,-0.005142189801713891,-0.0010776975004659671,302.0+0.059871137139539544,0.05068011873981862,0.04121777711494808,0.011543832586488917,0.041085578784023497,0.0707102687853743,-0.036037570043851025,0.03430885887772673,-0.010903250651210127,-0.03007244590430716,198.0+-0.027309785684926546,-0.044641636506989144,0.06492964274032566,-0.002227571316908129,-0.0249601584096303,-0.017284448977484993,0.022868634821540033,-0.03949338287409329,-0.061175799045152635,-0.0632093012229828,95.0+0.02354575262934534,0.05068011873981862,-0.0320734439089463,-0.04009893205125,-0.0318399227006359,-0.0216685274425385,-0.01394774321932938,-0.002592261998183278,-0.010903250651210127,0.019632837073706312,53.0+-0.09632801625429555,-0.044641636506989144,-0.07626373893805906,-0.04354178302709927,-0.04559945128264711,-0.03482076283769895,0.008142083605192267,-0.03949338287409329,-0.05947118135708968,-0.08391983579715509,134.0+0.027178291080364757,-0.044641636506989144,0.04984027370599448,-0.0550064767766773,-0.0029449126784123676,0.04064801645357896,-0.05812739686837268,0.05275941931568174,-0.05296264109357657,-0.005219804415300423,144.0+0.01991321417832592,0.05068011873981862,0.045529025410471304,0.029894228287765473,-0.062110885581060565,-0.0558017097775978,-0.07285394808472044,0.026928634702544724,0.0456043699279467,0.040343371647878594,232.0+0.038075906433423026,0.05068011873981862,-0.00943939035744949,0.002351420480971383,0.001182945896190995,0.03751653183568362,-0.05444575906428573,0.05017634085436802,-0.025953110560258,0.1066170822852299,81.0+0.04170844488444244,0.05068011873981862,-0.0320734439089463,-0.0228846771720037,-0.04972730985725048,-0.04014428668812105,0.030231910429713918,-0.03949338287409329,-0.12609712083330468,0.015490730158871856,104.0+0.01991321417832592,-0.044641636506989144,0.004572166603000912,-0.02632752814785296,0.02319819162740893,0.01027261565999407,0.06704828847058333,-0.03949338287409329,-0.02364686309993755,-0.04664087356364498,59.0+-0.08543040090123728,-0.044641636506989144,0.020739347711212906,-0.02632752814785296,0.005310804470794357,0.019667069513680118,-0.002902829807068556,-0.002592261998183278,-0.02364686309993755,0.0030644094143684884,246.0+0.01991321417832592,0.05068011873981862,0.014272475267928093,0.06318659722422783,0.014942474478202204,0.020293366437259194,-0.04708248345611185,0.03430885887772673,0.04666177983070229,0.09004865462589207,297.0+0.02354575262934534,-0.044641636506989144,0.11019774984331929,0.06318659722422783,0.013566521620001083,-0.03294187206696173,-0.024992656631590206,0.020655444153640023,0.09924057568496533,0.02377494398854077,258.0+-0.030942324135945967,0.05068011873981862,0.0013387303813585058,-0.00567042229275739,0.06447677737344255,0.04941617338368593,-0.04708248345611185,0.10811110062954676,0.08379874486368903,0.0030644094143684884,229.0+0.04897352178648128,0.05068011873981862,0.05846277029704089,0.07007229917592636,0.013566521620001083,0.020606514899048713,-0.02131101882750326,0.03430885887772673,0.02200407477075404,0.027917050903375224,275.0+0.059871137139539544,-0.044641636506989144,-0.0212953231701383,0.08728655405517267,0.045213437358626866,0.031566711061682434,-0.04708248345611185,0.07120997975363674,0.07912244072477838,0.13561183068907107,281.0+-0.056370093293081916,0.05068011873981862,-0.010517202431330305,0.02531523648988596,0.02319819162740893,0.04002171952999989,-0.03971920784793797,0.03430885887772673,0.02060938757142981,0.05691179930721642,179.0+0.016280675727306498,-0.044641636506989144,-0.047162812943277475,-0.002227571316908129,-0.019456346976825818,-0.04296262284422686,0.033913548233800855,-0.03949338287409329,0.02736404910541198,0.027917050903375224,200.0+-0.04910501639104307,-0.044641636506989144,0.004572166603000912,0.011543832586488917,-0.037343734133440394,-0.01853704282464315,-0.01762938102341632,-0.002592261998183278,-0.03980882652740082,-0.021788232074638245,200.0+0.06350367559055897,-0.044641636506989144,0.0175059114895705,0.0218723855140367,0.0080627101871966,0.02154596028441731,-0.036037570043851025,0.03430885887772673,0.019907486170462722,0.0113486232440374,173.0+0.04897352178648128,0.05068011873981862,0.08109682384853766,0.0218723855140367,0.04383748450042574,0.06413415108779408,-0.05444575906428573,0.07120997975363674,0.03243232415655107,0.0486275854775475,180.0+0.005383060374248237,0.05068011873981862,0.03475090467166331,-0.0010914304948778783,0.15253776029831428,0.19878798965729408,-0.06180903467245962,0.18523444326019867,0.015568459328120622,0.07348022696655424,84.0+-0.005514554978810025,-0.044641636506989144,0.023972783932855315,0.008100981610639655,-0.034591828417038145,-0.038891692840962916,0.022868634821540033,-0.03949338287409329,-0.015998872510179042,-0.013504018244969336,121.0+-0.005514554978810025,0.05068011873981862,-0.008361578283568675,-0.002227571316908129,-0.033215875558837024,-0.06363042132233616,-0.036037570043851025,-0.002592261998183278,0.0805900527449823,0.007206516329202944,161.0+-0.0890629393522567,-0.044641636506989144,-0.061174369903727877,-0.02632752814785296,-0.05523112129005496,-0.054549115930439665,0.04127682384197474,-0.0763945037500033,-0.09393727482535742,-0.05492508739331389,99.0+0.0344433679824036,0.05068011873981862,-0.0018947058402839008,-0.012556124244455912,0.03833367306762126,0.013717248739678958,0.07809320188284416,-0.03949338287409329,0.004547695772676124,-0.09634615654165846,109.0+-0.052737554842062495,-0.044641636506989144,-0.06225218197760866,-0.02632752814785296,-0.005696818394814609,-0.005071658967693135,0.030231910429713918,-0.03949338287409329,-0.030747917533098208,-0.07149351505265171,115.0+0.009015598825267658,-0.044641636506989144,0.016428099415689682,0.004658130634790395,0.00943866304539772,0.01058576412178361,-0.028674294435677143,0.03430885887772673,0.03896821122789408,0.11904340302973325,268.0+-0.06363517019512076,0.05068011873981862,0.09618619288286882,0.10450080893441897,-0.0029449126784123676,-0.0047585105059035964,-0.006584467611155497,-0.002592261998183278,0.022687744966501246,0.07348022696655424,274.0+-0.09632801625429555,-0.044641636506989144,-0.06979686649477428,-0.06764173985804409,-0.019456346976825818,-0.010708331279904778,0.01550535921336615,-0.03949338287409329,-0.04688253415273158,-0.07977772888232063,158.0+0.016280675727306498,0.05068011873981862,-0.0212953231701383,-0.009113273268606652,0.0342058144930179,0.047850431074738256,0.0007788079970183853,-0.002592261998183278,-0.012908683225401873,0.02377494398854077,107.0+-0.04183993948900423,0.05068011873981862,-0.05362968538656229,-0.04009893205125,-0.0841261313122785,-0.07177228132886408,-0.002902829807068556,-0.03949338287409329,-0.07213275338232743,-0.03007244590430716,83.0+-0.074532785548179,-0.044641636506989144,0.04337340126270967,-0.03321323009955148,0.01219056876179996,0.00025186488272894433,0.06336665066649638,-0.03949338287409329,-0.02712902329694316,-0.04664087356364498,103.0+-0.005514554978810025,-0.044641636506989144,0.0563071461492793,-0.03665608107540074,-0.04835135699904936,-0.04296262284422686,-0.07285394808472044,0.03799897096531772,0.050782032210405316,0.05691179930721642,272.0+-0.09269547780327612,-0.044641636506989144,-0.08165279930746305,-0.057313186930496314,-0.060734932722859444,-0.06801449978738965,0.04864009945014862,-0.0763945037500033,-0.06649019536676071,-0.021788232074638245,85.0+0.005383060374248237,-0.044641636506989144,0.04984027370599448,0.09761510698272045,-0.015328488402222454,-0.016345003592116398,-0.006584467611155497,-0.002592261998183278,0.017036071348324546,-0.013504018244969336,280.0+0.0344433679824036,0.05068011873981862,0.11127556191720007,0.07695800112762488,-0.0318399227006359,-0.033881317452330355,-0.02131101882750326,-0.002592261998183278,0.02802037249332928,0.07348022696655424,336.0+0.02354575262934534,-0.044641636506989144,0.061696206518683294,0.052858044296680055,-0.034591828417038145,-0.048912443618228024,-0.028674294435677143,-0.002592261998183278,0.05471997253790904,-0.005219804415300423,281.0+0.04170844488444244,0.05068011873981862,0.014272475267928093,0.04252949136913227,-0.030463969842434782,-0.0013138774262187302,-0.04340084565202491,-0.002592261998183278,-0.03324559264822791,0.015490730158871856,118.0+-0.027309785684926546,-0.044641636506989144,0.04768464955823289,-0.04698463400294853,0.0342058144930179,0.057244884928424306,-0.08021722369289432,0.13025177315509276,0.04506654937395887,0.13146972377423663,317.0+0.04170844488444244,0.05068011873981862,0.012116851120166501,0.03908664039328301,0.05484510736603471,0.04440579799505339,0.0044604458011053266,-0.002592261998183278,0.0456043699279467,-0.0010776975004659671,235.0+-0.030942324135945967,-0.044641636506989144,0.005649978676881689,-0.009113273268606652,0.019070333052805567,0.006827982580309182,0.07441156407875721,-0.03949338287409329,-0.041176166918895155,-0.042498766648810526,60.0+0.03081082953138418,0.05068011873981862,0.04660683748435208,-0.015998975220305175,0.020446285911006685,0.05066876723084409,-0.05812739686837268,0.07120997975363674,0.006206735447689297,0.007206516329202944,174.0+-0.04183993948900423,-0.044641636506989144,0.12852055509929283,0.06318659722422783,-0.033215875558837024,-0.03262872360517222,0.01182372140927921,-0.03949338287409329,-0.015998872510179042,-0.05078298047847944,259.0+-0.030942324135945967,0.05068011873981862,0.05954058237092167,0.0012152796589411327,0.01219056876179996,0.031566711061682434,-0.04340084565202491,0.03430885887772673,0.014820979914103747,0.007206516329202944,178.0+-0.056370093293081916,-0.044641636506989144,0.09295275666122646,-0.019441826196154435,0.014942474478202204,0.023424851055154544,-0.028674294435677143,0.02545258986750832,0.026060517932187322,0.040343371647878594,128.0+-0.06000263174410134,0.05068011873981862,0.015350287341808908,-0.019441826196154435,0.03695772020942014,0.04816357953652778,0.019186997017453092,-0.002592261998183278,-0.030747917533098208,-0.0010776975004659671,96.0+-0.04910501639104307,0.05068011873981862,-0.0051281420619263066,-0.04698463400294853,-0.02083229983502694,-0.02041593359538034,-0.0691723102806335,0.07120997975363674,0.06123762840403217,-0.03835665973397607,126.0+0.02354575262934534,-0.044641636506989144,0.07031870310972965,0.02531523648988596,-0.034591828417038145,-0.014466112821379181,-0.03235593223976409,-0.002592261998183278,-0.019198449026275908,-0.009361911330134878,288.0+0.001750521923228816,-0.044641636506989144,-0.004050329988045492,-0.00567042229275739,-0.008448724111216851,-0.02386056667506523,0.05232173725423556,-0.03949338287409329,-0.00894339609006817,-0.013504018244969336,88.0+-0.03457486258696539,0.05068011873981862,-0.0008168937664030856,0.07007229917592636,0.039709625925822375,0.06695248724389988,-0.06549067247654655,0.10811110062954676,0.02671684132010462,0.07348022696655424,292.0+0.04170844488444244,0.05068011873981862,-0.04392937672163507,0.06318659722422783,-0.004320865536613489,0.01622243643399523,-0.01394774321932938,-0.002592261998183278,-0.03452177701362266,0.0113486232440374,71.0+0.06713621404157838,0.05068011873981862,0.020739347711212906,-0.00567042229275739,0.020446285911006685,0.02624318721126033,-0.002902829807068556,-0.002592261998183278,0.008640601344549246,0.0030644094143684884,197.0+-0.027309785684926546,0.05068011873981862,0.06061839444480248,0.049415193320830796,0.08511607024645937,0.08636769187485104,-0.002902829807068556,0.03430885887772673,0.037810529696428806,0.0486275854775475,186.0+-0.016412170331868287,-0.044641636506989144,-0.010517202431330305,0.0012152796589411327,-0.037343734133440394,-0.035760208223067566,0.01182372140927921,-0.03949338287409329,-0.021395309255276825,-0.03421455281914162,25.0+-0.0018820165277906047,0.05068011873981862,-0.033151255982827074,-0.018305685374124185,0.03145390877661565,0.042840055686105716,-0.01394774321932938,0.019917421736121838,0.010226716198682649,0.027917050903375224,84.0+-0.012779631880848867,-0.044641636506989144,-0.06548561819925106,-0.0699484500118631,0.001182945896190995,0.016848733357574308,-0.002902829807068556,-0.007020396503292483,-0.030747917533098208,-0.05078298047847944,96.0+-0.005514554978810025,-0.044641636506989144,0.04337340126270967,0.08728655405517267,0.013566521620001083,0.0071411310420987206,-0.01394774321932938,-0.002592261998183278,0.04234098419358016,-0.01764612515980379,195.0+-0.009147093429829445,-0.044641636506989144,-0.06225218197760866,-0.07452744180974262,-0.02358420555142918,-0.013213518974221048,0.0044604458011053266,-0.03949338287409329,-0.0358161925842373,-0.04664087356364498,53.0+-0.045472477940023646,0.05068011873981862,0.06385183066644486,0.07007229917592636,0.1332744202834986,0.1314610703725441,-0.03971920784793797,0.10811110062954676,0.07574055215648227,0.0859065477110576,217.0+-0.052737554842062495,-0.044641636506989144,0.030439656376140087,-0.07452744180974262,-0.02358420555142918,-0.011334628203483833,-0.002902829807068556,-0.002592261998183278,-0.030747917533098208,-0.0010776975004659671,172.0+0.016280675727306498,0.05068011873981862,0.0724743272574913,0.07695800112762488,-0.008448724111216851,0.0055753887331510465,-0.006584467611155497,-0.002592261998183278,-0.02364686309993755,0.06105390622205087,131.0+0.04534098333546186,-0.044641636506989144,-0.01913969902237667,0.0218723855140367,0.027326050202012293,-0.013526667436010586,0.10018302870736581,-0.03949338287409329,0.017765319557121535,-0.013504018244969336,214.0+-0.04183993948900423,-0.044641636506989144,-0.06656343027313188,-0.04698463400294853,-0.037343734133440394,-0.043275771306016404,0.04864009945014862,-0.03949338287409329,-0.05615310200706333,-0.013504018244969336,59.0+-0.056370093293081916,0.05068011873981862,-0.06009655782984706,-0.03665608107540074,-0.08825398988688185,-0.07083283594349546,-0.01394774321932938,-0.03949338287409329,-0.07813993550229265,-0.10463037037132736,70.0+0.0707687524925978,-0.044641636506989144,0.06924089103584885,0.03795049957125276,0.02182223876920781,0.0015044587298871017,-0.036037570043851025,0.03910600459159503,0.07763659749935449,0.1066170822852299,220.0+0.001750521923228816,0.05068011873981862,0.05954058237092167,-0.002227571316908129,0.06172487165704031,0.0631947057024255,-0.05812739686837268,0.10811110062954676,0.0689858906225002,0.12732761685940217,268.0+-0.0018820165277906047,-0.044641636506989144,-0.0266843835395423,0.049415193320830796,0.05897296594063807,-0.016031855130326858,-0.04708248345611185,0.07120997975363674,0.13359728192191356,0.019632837073706312,152.0+0.02354575262934534,0.05068011873981862,-0.020217511096257485,-0.03665608107540074,-0.013952535544021335,-0.015092409744958261,0.059685012862409445,-0.03949338287409329,-0.09643494994048712,-0.01764612515980379,47.0+-0.020044708782887707,-0.044641636506989144,-0.04608500086939666,-0.09862739864068745,-0.07587041416307178,-0.05987263978086174,-0.01762938102341632,-0.03949338287409329,-0.05140387304727299,-0.04664087356364498,74.0+0.04170844488444244,0.05068011873981862,0.07139651518361048,0.008100981610639655,0.03833367306762126,0.01590928797220569,-0.01762938102341632,0.03430885887772673,0.07340695788833193,0.0859065477110576,295.0+-0.06363517019512076,0.05068011873981862,-0.07949717515970146,-0.00567042229275739,-0.07174255558846841,-0.06644875747844198,-0.010266105415242439,-0.03949338287409329,-0.018113692315690322,-0.05492508739331389,101.0+0.016280675727306498,0.05068011873981862,0.009961226972404908,-0.04354178302709927,-0.09650970703608859,-0.09463211903950011,-0.03971920784793797,-0.03949338287409329,0.017036071348324546,0.007206516329202944,151.0+0.06713621404157838,-0.044641636506989144,-0.03854031635223107,-0.02632752814785296,-0.0318399227006359,-0.02636575436938152,0.008142083605192267,-0.03949338287409329,-0.02712902329694316,0.0030644094143684884,127.0+0.04534098333546186,0.05068011873981862,0.01966153563733209,0.03908664039328301,0.020446285911006685,0.025930038749470818,0.008142083605192267,-0.002592261998183278,-0.003300838074501491,0.019632837073706312,237.0+0.04897352178648128,-0.044641636506989144,0.027206220154497678,-0.02519138732582271,0.02319819162740893,0.018414475666521983,-0.06180903467245962,0.08006624876385515,0.07222192954903682,0.03205915781820968,225.0+0.04170844488444244,-0.044641636506989144,-0.008361578283568675,-0.02632752814785296,0.024574144485610048,0.01622243643399523,0.07072992627467027,-0.03949338287409329,-0.04835926177554553,-0.03007244590430716,81.0+-0.02367724723390713,-0.044641636506989144,-0.015906262800734303,-0.012556124244455912,0.020446285911006685,0.04127431337715804,-0.04340084565202491,0.03430885887772673,0.014073500500086794,-0.009361911330134878,151.0+-0.03820740103798481,0.05068011873981862,0.004572166603000912,0.03564378941743375,-0.011200629827619093,0.0058885371949405855,-0.04708248345611185,0.03430885887772673,0.016306823139527554,-0.0010776975004659671,107.0+0.04897352178648128,-0.044641636506989144,-0.04285156464775429,-0.05387033595464705,0.045213437358626866,0.05004247030726501,0.033913548233800855,-0.002592261998183278,-0.025953110560258,-0.0632093012229828,64.0+0.04534098333546186,0.05068011873981862,0.005649978676881689,0.056300895272529315,0.06447677737344255,0.08918602803095686,-0.03971920784793797,0.07120997975363674,0.015568459328120622,-0.009361911330134878,138.0+0.04534098333546186,0.05068011873981862,-0.035306880130588664,0.06318659722422783,-0.004320865536613489,-0.0016270258880082473,-0.010266105415242439,-0.002592261998183278,0.015568459328120622,0.05691179930721642,185.0+0.016280675727306498,-0.044641636506989144,0.023972783932855315,-0.0228846771720037,-0.0249601584096303,-0.02605260590759198,-0.03235593223976409,-0.002592261998183278,0.037236246732001224,0.03205915781820968,265.0+-0.074532785548179,0.05068011873981862,-0.018061886948495892,0.008100981610639655,-0.019456346976825818,-0.02480001206043385,-0.06549067247654655,0.03430885887772673,0.06731773534487707,-0.01764612515980379,101.0+-0.08179786245021785,0.05068011873981862,0.0422955891888289,-0.019441826196154435,0.039709625925822375,0.05755803339021383,-0.0691723102806335,0.10811110062954676,0.04719048478208009,-0.03835665973397607,137.0+-0.06726770864614018,-0.044641636506989144,-0.05470749746044306,-0.02632752814785296,-0.07587041416307178,-0.08210618056791873,0.04864009945014862,-0.0763945037500033,-0.08682710478958679,-0.10463037037132736,143.0+0.005383060374248237,-0.044641636506989144,-0.002972517914164677,0.049415193320830796,0.0741084473808504,0.0707102687853743,0.04495846164606168,-0.002592261998183278,-0.0014959487577289358,-0.009361911330134878,141.0+-0.0018820165277906047,-0.044641636506989144,-0.06656343027313188,0.0012152796589411327,-0.0029449126784123676,0.003070201038834776,0.01182372140927921,-0.002592261998183278,-0.020292321339471356,-0.025930338989472702,79.0+0.009015598825267658,-0.044641636506989144,-0.012672826579091896,0.028758087465735226,-0.018080394118624697,-0.005071658967693135,-0.04708248345611185,0.03430885887772673,0.02337141516224845,-0.005219804415300423,292.0+-0.005514554978810025,0.05068011873981862,-0.041773752573873474,-0.04354178302709927,-0.07999827273767514,-0.07615635979391756,-0.03235593223976409,-0.03949338287409329,0.010226716198682649,-0.009361911330134878,178.0+0.056238598688520124,0.05068011873981862,-0.03099563183506548,0.008100981610639655,0.019070333052805567,0.02123281182262779,0.033913548233800855,-0.03949338287409329,-0.02952642678336326,-0.05906719430814835,91.0+0.009015598825267658,0.05068011873981862,-0.0051281420619263066,-0.06419888888219483,0.06998058880624704,0.08386250418053477,-0.03971920784793797,0.07120997975363674,0.03954249419232167,0.019632837073706312,116.0+-0.06726770864614018,-0.044641636506989144,-0.05901874575596628,0.03220093844158448,-0.051103262715451604,-0.049538740541807104,-0.010266105415242439,-0.03949338287409329,0.0020044426444966374,0.02377494398854077,86.0+0.027178291080364757,0.05068011873981862,0.02505059600673609,0.014986683562338177,0.02595009734381117,0.048476727998317336,-0.03971920784793797,0.03430885887772673,0.007838428314872565,0.02377494398854077,122.0+-0.02367724723390713,-0.044641636506989144,-0.04608500086939666,-0.03321323009955148,0.03282986163481677,0.03626393798852546,0.0375951860378878,-0.002592261998183278,-0.03324559264822791,0.0113486232440374,72.0+0.04897352178648128,0.05068011873981862,0.0034943545291200974,0.07007229917592636,-0.008448724111216851,0.013404100277889418,-0.05444575906428573,0.03430885887772673,0.013316905483459898,0.036201264733044136,129.0+-0.052737554842062495,-0.044641636506989144,0.05415152200151766,-0.02632752814785296,-0.05523112129005496,-0.033881317452330355,-0.01394774321932938,-0.03949338287409329,-0.07409260794346935,-0.05906719430814835,142.0+0.04170844488444244,-0.044641636506989144,-0.04500718879551588,0.03450764859540349,0.04383748450042574,-0.015718706668537318,0.0375951860378878,-0.014400620678474476,0.0898970830097539,0.007206516329202944,90.0+0.056238598688520124,-0.044641636506989144,-0.05794093368208547,-0.0079771324465764,0.05209320164963247,0.049103024921896415,0.05600337505832251,-0.021411833644897377,-0.028323167238848198,0.044485478562713045,158.0+-0.03457486258696539,0.05068011873981862,-0.05578530953432388,-0.015998975220305175,-0.009824676969417972,-0.007889995123798945,0.0375951860378878,-0.03949338287409329,-0.05296264109357657,0.027917050903375224,39.0+0.08166636784565606,0.05068011873981862,0.0013387303813585058,0.03564378941743375,0.126394655992493,0.09106491880169407,0.019186997017453092,0.03430885887772673,0.08449153066204618,-0.03007244590430716,196.0+-0.0018820165277906047,0.05068011873981862,0.030439656376140087,0.052858044296680055,0.039709625925822375,0.056618588004845226,-0.03971920784793797,0.07120997975363674,0.025395078941660074,0.027917050903375224,222.0+0.11072667545381144,0.05068011873981862,0.006727790750762504,0.028758087465735226,-0.027712064126032544,-0.007263698200219888,-0.04708248345611185,0.03430885887772673,0.0020044426444966374,0.07762233388138869,277.0+-0.030942324135945967,-0.044641636506989144,0.04660683748435208,0.014986683562338177,-0.016704441260423575,-0.047033552847490806,0.0007788079970183853,-0.002592261998183278,0.06345271983825305,-0.025930338989472702,99.0+0.001750521923228816,0.05068011873981862,0.026128408080616904,-0.009113273268606652,0.024574144485610048,0.03845597722105221,-0.02131101882750326,0.03430885887772673,0.009433658771615987,0.0030644094143684884,196.0+0.009015598825267658,-0.044641636506989144,0.045529025410471304,0.028758087465735226,0.01219056876179996,-0.013839815897800126,0.026550272625626974,-0.03949338287409329,0.04613307487932449,0.036201264733044136,202.0+0.03081082953138418,-0.044641636506989144,0.04013996504106731,0.07695800112762488,0.017694380194604446,0.037829680297473134,-0.028674294435677143,0.03430885887772673,-0.0014959487577289358,0.11904340302973325,155.0+0.038075906433423026,0.05068011873981862,-0.018061886948495892,0.0666294482000771,-0.051103262715451604,-0.016658152053905938,-0.07653558588880739,0.03430885887772673,-0.011896851335695978,-0.013504018244969336,77.0+0.009015598825267658,-0.044641636506989144,0.014272475267928093,0.014986683562338177,0.05484510736603471,0.04722413415115918,0.07072992627467027,-0.03949338287409329,-0.03324559264822791,-0.05906719430814835,191.0+0.09256398319871433,-0.044641636506989144,0.0369065288194249,0.0218723855140367,-0.0249601584096303,-0.016658152053905938,0.0007788079970183853,-0.03949338287409329,-0.022516528376302174,-0.021788232074638245,70.0+0.06713621404157838,-0.044641636506989144,0.0034943545291200974,0.03564378941743375,0.04934129593323023,0.03125356259989291,0.07072992627467027,-0.03949338287409329,-0.0006117353045626216,0.019632837073706312,73.0+0.001750521923228816,-0.044641636506989144,-0.07087467856865506,-0.0228846771720037,-0.001568959820211247,-0.0010007289644291908,0.026550272625626974,-0.03949338287409329,-0.022516528376302174,0.007206516329202944,49.0+0.03081082953138418,-0.044641636506989144,-0.033151255982827074,-0.0228846771720037,-0.046975404140848234,-0.08116673518255012,0.10386466651145274,-0.0763945037500033,-0.03980882652740082,-0.05492508739331389,65.0+0.027178291080364757,0.05068011873981862,0.09403056873510728,0.09761510698272045,-0.034591828417038145,-0.032002426681593144,-0.04340084565202491,-0.002592261998183278,0.03664373256235367,0.1066170822852299,263.0+0.012648137276287077,0.05068011873981862,0.03582871674554409,0.049415193320830796,0.053469154507833586,0.07415490186505921,-0.0691723102806335,0.14501222150545676,0.0456043699279467,0.0486275854775475,248.0+0.07440129094361722,-0.044641636506989144,0.031517468450020895,0.10105795795856971,0.04658939021682799,0.03689023491210454,0.01550535921336615,-0.002592261998183278,0.033653814906286016,0.044485478562713045,296.0+-0.04183993948900423,-0.044641636506989144,-0.06548561819925106,-0.04009893205125,-0.005696818394814609,0.014343545663258015,-0.04340084565202491,0.03430885887772673,0.007027139682585861,-0.013504018244969336,214.0+-0.0890629393522567,-0.044641636506989144,-0.041773752573873474,-0.019441826196154435,-0.06623874415566393,-0.07427746902318036,0.008142083605192267,-0.03949338287409329,0.0011475759991601464,-0.03007244590430716,185.0+0.02354575262934534,0.05068011873981862,-0.039618128426111884,-0.00567042229275739,-0.04835135699904936,-0.033255020528751275,0.01182372140927921,-0.03949338287409329,-0.10163995903077562,-0.06735140813781726,78.0+-0.045472477940023646,-0.044641636506989144,-0.03854031635223107,-0.02632752814785296,-0.015328488402222454,0.0008781618063080231,-0.03235593223976409,-0.002592261998183278,0.0011475759991601464,-0.03835665973397607,93.0+-0.02367724723390713,0.05068011873981862,-0.02560657146566148,0.04252949136913227,-0.05385516843185383,-0.047659849771069886,-0.02131101882750326,-0.03949338287409329,0.0011475759991601464,0.019632837073706312,252.0+-0.09996055470531495,-0.044641636506989144,-0.023450947317899894,-0.06419888888219483,-0.0579830270064572,-0.06018578824265128,0.01182372140927921,-0.03949338287409329,-0.018113692315690322,-0.05078298047847944,150.0+-0.027309785684926546,-0.044641636506989144,-0.06656343027313188,-0.11239880254408448,-0.04972730985725048,-0.041396880535279186,0.0007788079970183853,-0.03949338287409329,-0.0358161925842373,-0.009361911330134878,77.0+0.03081082953138418,0.05068011873981862,0.032595280523901676,0.049415193320830796,-0.04009563984984263,-0.04358891976780594,-0.0691723102806335,0.03430885887772673,0.06301517091297482,0.0030644094143684884,208.0+-0.10359309315633439,0.05068011873981862,-0.04608500086939666,-0.02632752814785296,-0.0249601584096303,-0.02480001206043385,0.030231910429713918,-0.03949338287409329,-0.03980882652740082,-0.05492508739331389,77.0+0.06713621404157838,0.05068011873981862,-0.029917819761184662,0.05743703609455957,-0.00019300696201012598,-0.015718706668537318,0.07441156407875721,-0.05056371913686628,-0.03845971734112638,0.007206516329202944,108.0+-0.052737554842062495,-0.044641636506989144,-0.012672826579091896,-0.060756037906345574,-0.00019300696201012598,0.008080576427467316,0.01182372140927921,-0.002592261998183278,-0.02712902329694316,-0.05078298047847944,160.0+-0.027309785684926546,0.05068011873981862,-0.015906262800734303,-0.029770379123702218,0.003934851612593237,-0.0006875805026396515,0.04127682384197474,-0.03949338287409329,-0.02364686309993755,0.0113486232440374,53.0+-0.03820740103798481,0.05068011873981862,0.07139651518361048,-0.057313186930496314,0.15391371315651542,0.1558866503921278,0.0007788079970183853,0.07194800217115493,0.050280674066857343,0.06933812005171978,220.0+0.009015598825267658,-0.044641636506989144,-0.03099563183506548,0.0218723855140367,0.0080627101871966,0.008706873351046395,0.0044604458011053266,-0.002592261998183278,0.009433658771615987,0.0113486232440374,154.0+0.012648137276287077,0.05068011873981862,0.00026091830747769084,-0.011419983422425662,0.039709625925822375,0.057244884928424306,-0.03971920784793797,0.056080520194513636,0.024055085357995654,0.03205915781820968,259.0+0.06713621404157838,-0.044641636506989144,0.0369065288194249,-0.05042748497879779,-0.02358420555142918,-0.034507614375909414,0.04864009945014862,-0.03949338287409329,-0.025953110560258,-0.03835665973397607,90.0+0.04534098333546186,-0.044641636506989144,0.039062152967186486,0.04597234234498153,0.006686757328995478,-0.02417371513685477,0.008142083605192267,-0.012555564634678981,0.06432781768880942,0.05691179930721642,246.0+0.06713621404157838,0.05068011873981862,-0.014828450726853487,0.05860760542634833,-0.05935897986465832,-0.034507614375909414,-0.06180903467245962,0.012906208769698923,-0.005142189801713891,0.0486275854775475,124.0+0.027178291080364757,-0.044641636506989144,0.006727790750762504,0.03564378941743375,0.07961225881365488,0.0707102687853743,0.01550535921336615,0.03430885887772673,0.04067282891595704,0.0113486232440374,67.0+0.056238598688520124,-0.044641636506989144,-0.06871905442089347,-0.06877788068007434,-0.00019300696201012598,-0.0010007289644291908,0.04495846164606168,-0.03764832683029779,-0.04835926177554553,-0.0010776975004659671,72.0+0.0344433679824036,0.05068011873981862,-0.00943939035744949,0.059743746248378575,-0.035967781275239266,-0.007576846662009428,-0.07653558588880739,0.07120997975363674,0.011010658023139448,-0.021788232074638245,257.0+0.02354575262934534,-0.044641636506989144,0.01966153563733209,-0.012556124244455912,0.08374011738825825,0.03876912568284173,0.06336665066649638,-0.002592261998183278,0.06605066658209227,0.0486275854775475,262.0+0.04897352178648128,0.05068011873981862,0.07462995140525285,0.0666294482000771,-0.009824676969417972,-0.002253322811587326,-0.04340084565202491,0.03430885887772673,0.033653814906286016,0.019632837073706312,275.0+0.03081082953138418,0.05068011873981862,-0.008361578283568675,0.004658130634790395,0.014942474478202204,0.02749578105841849,0.008142083605192267,-0.008127430129569777,-0.02952642678336326,0.05691179930721642,177.0+-0.10359309315633439,0.05068011873981862,-0.023450947317899894,-0.0228846771720037,-0.08687803702868073,-0.06770135132560012,-0.01762938102341632,-0.03949338287409329,-0.07813993550229265,-0.07149351505265171,71.0+0.016280675727306498,0.05068011873981862,-0.04608500086939666,0.011543832586488917,-0.033215875558837024,-0.016031855130326858,-0.010266105415242439,-0.002592261998183278,-0.04398377252276359,-0.042498766648810526,47.0+-0.06000263174410134,0.05068011873981862,0.05415152200151766,-0.019441826196154435,-0.04972730985725048,-0.048912443618228024,0.022868634821540033,-0.03949338287409329,-0.04398377252276359,-0.005219804415300423,187.0+-0.027309785684926546,-0.044641636506989144,-0.035306880130588664,-0.029770379123702218,-0.05660707414825608,-0.0586200459337036,0.030231910429713918,-0.03949338287409329,-0.049872451808799324,-0.1294830118603341,125.0+0.04170844488444244,-0.044641636506989144,-0.0320734439089463,-0.06189217872837582,0.07961225881365488,0.05098191569263361,0.05600337505832251,-0.009972486173365287,0.04506654937395887,-0.05906719430814835,78.0+-0.08179786245021785,-0.044641636506989144,-0.08165279930746305,-0.04009893205125,0.0025588987543921156,-0.01853704282464315,0.07072992627467027,-0.03949338287409329,-0.010903250651210127,-0.092204049626824,51.0+-0.04183993948900423,-0.044641636506989144,0.04768464955823289,0.059743746248378575,0.1277706088506941,0.1280164372928592,-0.024992656631590206,0.10811110062954676,0.0638902687635312,0.040343371647878594,258.0+-0.012779631880848867,-0.044641636506989144,0.06061839444480248,0.052858044296680055,0.04796534307502911,0.02937467182915568,-0.01762938102341632,0.03430885887772673,0.07020738137223513,0.007206516329202944,215.0+0.06713621404157838,-0.044641636506989144,0.0563071461492793,0.07351515015177562,-0.013952535544021335,-0.03920484130275244,-0.03235593223976409,-0.002592261998183278,0.07574055215648227,0.036201264733044136,303.0+-0.052737554842062495,0.05068011873981862,0.09834181703063047,0.08728655405517267,0.06034891879883919,0.04878987646010685,-0.05812739686837268,0.10811110062954676,0.08449153066204618,0.040343371647878594,243.0+0.005383060374248237,-0.044641636506989144,0.05954058237092167,-0.05617704610846606,0.024574144485610048,0.052860806463370796,-0.04340084565202491,0.05091436327188625,-0.00422151393810765,-0.03007244590430716,91.0+0.08166636784565606,-0.044641636506989144,0.03367309259778249,0.008100981610639655,0.05209320164963247,0.056618588004845226,-0.01762938102341632,0.03430885887772673,0.03486619005341102,0.06933812005171978,150.0+0.03081082953138418,0.05068011873981862,0.0563071461492793,0.07695800112762488,0.04934129593323023,-0.012274073588852453,-0.036037570043851025,0.07120997975363674,0.12005149644350945,0.09004865462589207,310.0+0.001750521923228816,-0.044641636506989144,-0.06548561819925106,-0.00567042229275739,-0.007072771253015731,-0.019476488210011744,0.04127682384197474,-0.03949338287409329,-0.003300838074501491,0.007206516329202944,153.0+-0.04910501639104307,-0.044641636506989144,0.16085491731571683,-0.04698463400294853,-0.029088016984233665,-0.019789636671801284,-0.04708248345611185,0.03430885887772673,0.02802037249332928,0.0113486232440374,346.0+-0.027309785684926546,0.05068011873981862,-0.05578530953432388,0.02531523648988596,-0.007072771253015731,-0.02354741821327569,0.05232173725423556,-0.03949338287409329,-0.005142189801713891,-0.05078298047847944,63.0+0.07803382939463664,0.05068011873981862,-0.02452875939178067,-0.04240564220506902,0.006686757328995478,0.052860806463370796,-0.0691723102806335,0.08080427118137334,-0.0371288393600719,0.05691179930721642,89.0+0.012648137276287077,-0.044641636506989144,-0.03638469220446948,0.04252949136913227,-0.013952535544021335,0.01293437758520512,-0.026833475533633678,0.005156973385757823,-0.04398377252276359,0.007206516329202944,50.0+0.04170844488444244,-0.044641636506989144,-0.008361578283568675,-0.057313186930496314,0.0080627101871966,-0.031376129758014064,0.151725957964583,-0.0763945037500033,-0.08023652410258396,-0.01764612515980379,39.0+0.04897352178648128,-0.044641636506989144,-0.041773752573873474,0.10450080893441897,0.03558176735121902,-0.02573945744580244,0.17749742259319157,-0.0763945037500033,-0.012908683225401873,0.015490730158871856,103.0+-0.016412170331868287,0.05068011873981862,0.1274427430254121,0.09761510698272045,0.016318427336403322,0.017475030281153364,-0.02131101882750326,0.03430885887772673,0.03486619005341102,0.0030644094143684884,308.0+-0.074532785548179,0.05068011873981862,-0.07734155101193986,-0.04698463400294853,-0.046975404140848234,-0.03262872360517222,0.0044604458011053266,-0.03949338287409329,-0.07213275338232743,-0.01764612515980379,116.0+0.0344433679824036,0.05068011873981862,0.028284032228378497,-0.03321323009955148,-0.04559945128264711,-0.009768885894536158,-0.050764121260198795,-0.002592261998183278,-0.05947118135708968,-0.021788232074638245,145.0+-0.03457486258696539,0.05068011873981862,-0.02560657146566148,-0.017135116042335426,0.001182945896190995,-0.0028796197351664047,0.008142083605192267,-0.015507654304751785,0.014820979914103747,0.040343371647878594,74.0+-0.052737554842062495,0.05068011873981862,-0.06225218197760866,0.011543832586488917,-0.008448724111216851,-0.03669965360843617,0.12227285553188745,-0.0763945037500033,-0.08682710478958679,0.0030644094143684884,45.0+0.059871137139539544,-0.044641636506989144,-0.0008168937664030856,-0.0848559947372904,0.07548440023905152,0.07947842571548126,0.0044604458011053266,0.03430885887772673,0.02337141516224845,0.027917050903375224,115.0+0.06350367559055897,0.05068011873981862,0.08864150836570328,0.07007229917592636,0.020446285911006685,0.03751653183568362,-0.050764121260198795,0.07120997975363674,0.02929655685872395,0.07348022696655424,264.0+0.009015598825267658,-0.044641636506989144,-0.0320734439089463,-0.02632752814785296,0.04246153164222462,-0.010395182818115238,0.15908923357275687,-0.0763945037500033,-0.011896851335695978,-0.03835665973397607,87.0+0.005383060374248237,0.05068011873981862,0.030439656376140087,0.08384370307932341,-0.037343734133440394,-0.04734670130928034,0.01550535921336615,-0.03949338287409329,0.008640601344549246,0.015490730158871856,202.0+0.038075906433423026,0.05068011873981862,0.008883414898524095,0.04252949136913227,-0.04284754556624487,-0.02104223051895942,-0.03971920784793797,-0.002592261998183278,-0.018113692315690322,0.007206516329202944,127.0+0.012648137276287077,-0.044641636506989144,0.006727790750762504,-0.05617704610846606,-0.07587041416307178,-0.06644875747844198,-0.02131101882750326,-0.03764832683029779,-0.018113692315690322,-0.092204049626824,182.0+0.07440129094361722,0.05068011873981862,-0.020217511096257485,0.04597234234498153,0.0741084473808504,0.032819304908840594,-0.036037570043851025,0.07120997975363674,0.10635074572073594,0.036201264733044136,241.0+0.016280675727306498,-0.044641636506989144,-0.02452875939178067,0.03564378941743375,-0.007072771253015731,-0.003192768196955922,-0.01394774321932938,-0.002592261998183278,0.015568459328120622,0.015490730158871856,66.0+-0.005514554978810025,0.05068011873981862,-0.011595014505211082,0.011543832586488917,-0.02220825269322806,-0.015405558206747801,-0.02131101882750326,-0.002592261998183278,0.011010658023139448,0.06933812005171978,94.0+0.012648137276287077,-0.044641636506989144,0.026128408080616904,0.06318659722422783,0.12501870313429186,0.09169121572527314,0.06336665066649638,-0.002592261998183278,0.057573156154827256,-0.021788232074638245,283.0+-0.03457486258696539,-0.044641636506989144,-0.05901874575596628,0.0012152796589411327,-0.05385516843185383,-0.07803525056465478,0.06704828847058333,-0.0763945037500033,-0.021395309255276825,0.015490730158871856,64.0+0.06713621404157838,0.05068011873981862,-0.03638469220446948,-0.0848559947372904,-0.007072771253015731,0.019667069513680118,-0.05444575906428573,0.03430885887772673,0.0011475759991601464,0.03205915781820968,102.0+0.038075906433423026,0.05068011873981862,-0.02452875939178067,0.004658130634790395,-0.026336111267831423,-0.02636575436938152,0.01550535921336615,-0.03949338287409329,-0.015998872510179042,-0.025930338989472702,200.0+0.009015598825267658,0.05068011873981862,0.018583723563451313,0.03908664039328301,0.017694380194604446,0.01058576412178361,0.019186997017453092,-0.002592261998183278,0.016306823139527554,-0.01764612515980379,265.0+-0.09269547780327612,0.05068011873981862,-0.09027529589850945,-0.057313186930496314,-0.0249601584096303,-0.030436684372645465,-0.006584467611155497,-0.002592261998183278,0.024055085357995654,0.0030644094143684884,94.0+0.0707687524925978,-0.044641636506989144,-0.0051281420619263066,-0.00567042229275739,0.08786797596286161,0.10296456034969638,0.01182372140927921,0.03430885887772673,-0.00894339609006817,0.027917050903375224,230.0+-0.016412170331868287,-0.044641636506989144,-0.05255187331268147,-0.03321323009955148,-0.04422349842444599,-0.036386505146646625,0.019186997017453092,-0.03949338287409329,-0.0683315470939731,-0.03007244590430716,181.0+0.04170844488444244,0.05068011873981862,-0.022373135244019075,0.028758087465735226,-0.06623874415566393,-0.045154662076753616,-0.06180903467245962,-0.002592261998183278,0.002861309289833047,-0.05492508739331389,156.0+0.012648137276287077,-0.044641636506989144,-0.020217511096257485,-0.015998975220305175,0.01219056876179996,0.02123281182262779,-0.07653558588880739,0.10811110062954676,0.05987940361514779,-0.021788232074638245,233.0+-0.03820740103798481,-0.044641636506989144,-0.05470749746044306,-0.07797029278559188,-0.033215875558837024,-0.08649025903297221,0.14068104455232217,-0.0763945037500033,-0.019198449026275908,-0.005219804415300423,60.0+0.04534098333546186,-0.044641636506989144,-0.006205954135807083,-0.015998975220305175,0.12501870313429186,0.1251981011367534,0.019186997017453092,0.03430885887772673,0.03243232415655107,-0.005219804415300423,219.0+0.0707687524925978,0.05068011873981862,-0.01698407487461508,0.0218723855140367,0.04383748450042574,0.05630543954305571,0.0375951860378878,-0.002592261998183278,-0.07020936123162536,-0.01764612515980379,80.0+-0.074532785548179,0.05068011873981862,0.055229334075398484,-0.04009893205125,0.053469154507833586,0.05317395492516036,-0.04340084565202491,0.07120997975363674,0.06123762840403217,-0.03421455281914162,68.0+0.059871137139539544,0.05068011873981862,0.07678557555301448,0.02531523648988596,0.001182945896190995,0.016848733357574308,-0.05444575906428573,0.03430885887772673,0.02993464904142137,0.044485478562713045,332.0+0.07440129094361722,-0.044641636506989144,0.018583723563451313,0.06318659722422783,0.06172487165704031,0.042840055686105716,0.008142083605192267,-0.002592261998183278,0.05803805188793539,-0.05906719430814835,248.0+0.009015598825267658,-0.044641636506989144,-0.022373135244019075,-0.03207708927752123,-0.04972730985725048,-0.06864079671096873,0.07809320188284416,-0.0708593356186168,-0.06291687914365544,-0.03835665973397607,84.0+-0.07090024709715959,-0.044641636506989144,0.09295275666122646,0.012679973408519169,0.020446285911006685,0.04252690722431616,0.0007788079970183853,0.0003598276718895252,-0.05453964034510003,-0.0010776975004659671,200.0+0.02354575262934534,0.05068011873981862,-0.03099563183506548,-0.00567042229275739,-0.016704441260423575,0.017788178742942903,-0.03235593223976409,-0.002592261998183278,-0.07409260794346935,-0.03421455281914162,55.0+-0.052737554842062495,0.05068011873981862,0.039062152967186486,-0.04009893205125,-0.005696818394814609,-0.012900370512431508,0.01182372140927921,-0.03949338287409329,0.016306823139527554,0.0030644094143684884,85.0+0.06713621404157838,-0.044641636506989144,-0.061174369903727877,-0.04009893205125,-0.026336111267831423,-0.02448686359864431,0.033913548233800855,-0.03949338287409329,-0.05615310200706333,-0.05906719430814835,89.0+0.001750521923228816,-0.044641636506989144,-0.008361578283568675,-0.06419888888219483,-0.038719686991641515,-0.02448686359864431,0.0044604458011053266,-0.03949338287409329,-0.06468530604998815,-0.05492508739331389,31.0+0.02354575262934534,0.05068011873981862,-0.03746250427835029,-0.04698463400294853,-0.0910058956032841,-0.07553006287033849,-0.03235593223976409,-0.03949338287409329,-0.030747917533098208,-0.013504018244969336,129.0+0.038075906433423026,0.05068011873981862,-0.013750638652972673,-0.015998975220305175,-0.035967781275239266,-0.021981675904328014,-0.01394774321932938,-0.002592261998183278,-0.025953110560258,-0.0010776975004659671,83.0+0.016280675727306498,-0.044641636506989144,0.0735521393313721,-0.04123507287328025,-0.004320865536613489,-0.013526667436010586,-0.01394774321932938,-0.0011162171631468765,0.04289703595278786,0.044485478562713045,275.0+-0.0018820165277906047,0.05068011873981862,-0.02452875939178067,0.052858044296680055,0.027326050202012293,0.03000096875273476,0.030231910429713918,-0.002592261998183278,-0.021395309255276825,0.036201264733044136,65.0+0.012648137276287077,-0.044641636506989144,0.03367309259778249,0.03333707926361473,0.030077955918414535,0.02718263259662897,-0.002902829807068556,0.00884708547334881,0.031192602201596156,0.027917050903375224,198.0+0.07440129094361722,-0.044641636506989144,0.03475090467166331,0.0941722560068712,0.05759701308243695,0.020293366437259194,0.022868634821540033,-0.002592261998183278,0.07379892880056028,-0.021788232074638245,236.0+0.04170844488444244,0.05068011873981862,-0.03854031635223107,0.052858044296680055,0.07686035309725264,0.11642994420664642,-0.03971920784793797,0.07120997975363674,-0.022516528376302174,-0.013504018244969336,253.0+-0.009147093429829445,0.05068011873981862,-0.039618128426111884,-0.04009893205125,-0.008448724111216851,0.01622243643399523,-0.06549067247654655,0.07120997975363674,0.017765319557121535,-0.06735140813781726,124.0+0.009015598825267658,0.05068011873981862,-0.0018947058402839008,0.0218723855140367,-0.038719686991641515,-0.02480001206043385,-0.006584467611155497,-0.03949338287409329,-0.03980882652740082,-0.013504018244969336,44.0+0.06713621404157838,0.05068011873981862,-0.03099563183506548,0.004658130634790395,0.024574144485610048,0.03563764106494638,-0.028674294435677143,0.03430885887772673,0.02337141516224845,0.08176444079622315,172.0+0.001750521923228816,-0.044641636506989144,-0.04608500086939666,-0.03321323009955148,-0.07311850844666953,-0.08147988364433965,0.04495846164606168,-0.06938329078358041,-0.061175799045152635,-0.07977772888232063,114.0+-0.009147093429829445,0.05068011873981862,0.0013387303813585058,-0.002227571316908129,0.07961225881365488,0.07008397186179521,0.033913548233800855,-0.002592261998183278,0.02671684132010462,0.08176444079622315,142.0+-0.005514554978810025,-0.044641636506989144,0.06492964274032566,0.03564378941743375,-0.001568959820211247,0.014969842586837095,-0.01394774321932938,0.0007288388806486174,-0.018113692315690322,0.03205915781820968,109.0+0.09619652164973376,-0.044641636506989144,0.04013996504106731,-0.057313186930496314,0.045213437358626866,0.06068951800810917,-0.02131101882750326,0.03615391492152222,0.012551194864223063,0.02377494398854077,180.0+-0.074532785548179,-0.044641636506989144,-0.023450947317899894,-0.00567042229275739,-0.02083229983502694,-0.014152964359589643,0.01550535921336615,-0.03949338287409329,-0.03845971734112638,-0.03007244590430716,144.0+0.059871137139539544,0.05068011873981862,0.053073709927636895,0.052858044296680055,0.03282986163481677,0.019667069513680118,-0.010266105415242439,0.03430885887772673,0.055203099476237055,-0.0010776975004659671,163.0+-0.02367724723390713,-0.044641636506989144,0.04013996504106731,-0.012556124244455912,-0.009824676969417972,-0.0010007289644291908,-0.002902829807068556,-0.002592261998183278,-0.011896851335695978,-0.03835665973397607,147.0+0.009015598825267658,-0.044641636506989144,-0.020217511096257485,-0.05387033595464705,0.03145390877661565,0.020606514899048713,0.05600337505832251,-0.03949338287409329,-0.010903250651210127,-0.0010776975004659671,97.0+0.016280675727306498,0.05068011873981862,0.014272475267928093,0.0012152796589411327,0.001182945896190995,-0.02135537898074896,-0.03235593223976409,0.03430885887772673,0.0749657259346355,0.040343371647878594,220.0+0.01991321417832592,-0.044641636506989144,-0.03422906805670789,0.05516475445049906,0.0672286830898448,0.07415490186505921,-0.006584467611155497,0.032832814042690325,0.024729639951132837,0.06933812005171978,190.0+0.0889314447476949,-0.044641636506989144,0.006727790750762504,0.02531523648988596,0.030077955918414535,0.008706873351046395,0.06336665066649638,-0.03949338287409329,0.009433658771615987,0.03205915781820968,109.0+0.01991321417832592,-0.044641636506989144,0.004572166603000912,0.04597234234498153,-0.018080394118624697,-0.054549115930439665,0.06336665066649638,-0.03949338287409329,0.028658464676026615,0.06105390622205087,191.0+-0.02367724723390713,-0.044641636506989144,0.030439656376140087,-0.00567042229275739,0.08236416453005713,0.09200436418706266,-0.01762938102341632,0.07120997975363674,0.0330430695314185,0.0030644094143684884,122.0+0.09619652164973376,-0.044641636506989144,0.05199589785375607,0.0792647112814439,0.05484510736603471,0.036577086450315016,-0.07653558588880739,0.14132210941786577,0.0986480615153178,0.06105390622205087,230.0+0.02354575262934534,0.05068011873981862,0.061696206518683294,0.06205045640219759,0.024574144485610048,-0.03607335668485709,-0.09126213710515514,0.15534453535071155,0.13339673866449434,0.08176444079622315,242.0+0.0707687524925978,0.05068011873981862,-0.007283766209687899,0.049415193320830796,0.06034891879883919,-0.004445362044114079,-0.05444575906428573,0.10811110062954676,0.12902124941171242,0.05691179930721642,248.0+0.03081082953138418,-0.044641636506989144,0.005649978676881689,0.011543832586488917,0.07823630595545376,0.0779126834065336,-0.04340084565202491,0.10811110062954676,0.06605066658209227,0.019632837073706312,249.0+-0.0018820165277906047,-0.044641636506989144,0.05415152200151766,-0.06650559903601384,0.07273249452264928,0.056618588004845226,-0.04340084565202491,0.08486339447772344,0.08449153066204618,0.0486275854775475,192.0+0.04534098333546186,0.05068011873981862,-0.008361578283568675,-0.03321323009955148,-0.007072771253015731,0.0011913102680975625,-0.03971920784793797,0.03430885887772673,0.02993464904142137,0.027917050903375224,131.0+0.07440129094361722,-0.044641636506989144,0.11450899813884247,0.028758087465735226,0.024574144485610048,0.02499059336410222,0.019186997017453092,-0.002592261998183278,-0.0006117353045626216,-0.005219804415300423,237.0+-0.03820740103798481,-0.044641636506989144,0.0670852668880873,-0.060756037906345574,-0.029088016984233665,-0.023234269751486174,-0.010266105415242439,-0.002592261998183278,-0.0014959487577289358,0.019632837073706312,78.0+-0.012779631880848867,0.05068011873981862,-0.05578530953432388,-0.002227571316908129,-0.027712064126032544,-0.02918409052548733,0.019186997017453092,-0.03949338287409329,-0.017056282412934727,0.044485478562713045,135.0+0.009015598825267658,0.05068011873981862,0.030439656376140087,0.04252949136913227,-0.0029449126784123676,0.03689023491210454,-0.06549067247654655,0.07120997975363674,-0.02364686309993755,0.015490730158871856,244.0+0.08166636784565606,0.05068011873981862,-0.02560657146566148,-0.03665608107540074,-0.07036660273026729,-0.046407255923911754,-0.03971920784793797,-0.002592261998183278,-0.041176166918895155,-0.005219804415300423,199.0+0.03081082953138418,-0.044641636506989144,0.10480868947391528,0.07695800112762488,-0.011200629827619093,-0.011334628203483833,-0.05812739686837268,0.03430885887772673,0.0571082604217192,0.036201264733044136,270.0+0.027178291080364757,0.05068011873981862,-0.006205954135807083,0.028758087465735226,-0.016704441260423575,-0.0016270258880082473,-0.05812739686837268,0.03430885887772673,0.02929655685872395,0.03205915781820968,164.0+-0.06000263174410134,0.05068011873981862,-0.047162812943277475,-0.0228846771720037,-0.07174255558846841,-0.05768060054833501,-0.006584467611155497,-0.03949338287409329,-0.06291687914365544,-0.05492508739331389,72.0+0.005383060374248237,-0.044641636506989144,-0.048240625017158284,-0.012556124244455912,0.001182945896190995,-0.0066374012766408095,0.06336665066649638,-0.03949338287409329,-0.05140387304727299,-0.05906719430814835,96.0+-0.020044708782887707,-0.044641636506989144,0.08540807214406083,-0.03665608107540074,0.09199583453746497,0.08949917649274639,-0.06180903467245962,0.14501222150545676,0.08094556124677081,0.05276969239238195,306.0+0.01991321417832592,0.05068011873981862,-0.012672826579091896,0.07007229917592636,-0.011200629827619093,0.0071411310420987206,-0.03971920784793797,0.03430885887772673,0.005386331212792652,0.0030644094143684884,91.0+-0.06363517019512076,-0.044641636506989144,-0.033151255982827074,-0.03321323009955148,0.001182945896190995,0.024051147978733624,-0.024992656631590206,-0.002592261998183278,-0.022516528376302174,-0.05906719430814835,214.0+0.027178291080364757,-0.044641636506989144,-0.007283766209687899,-0.05042748497879779,0.07548440023905152,0.056618588004845226,0.033913548233800855,-0.002592261998183278,0.04344397210938562,0.015490730158871856,95.0+-0.016412170331868287,-0.044641636506989144,-0.013750638652972673,0.13204361674121307,-0.009824676969417972,-0.0038190651205350003,0.019186997017453092,-0.03949338287409329,-0.0358161925842373,-0.03007244590430716,216.0+0.03081082953138418,0.05068011873981862,0.05954058237092167,0.056300895272529315,-0.02220825269322806,0.0011913102680975625,-0.03235593223976409,-0.002592261998183278,-0.024795429028792802,-0.01764612515980379,263.0+0.056238598688520124,0.05068011873981862,0.021817159785093684,0.056300895272529315,-0.007072771253015731,0.018101327204732443,-0.03235593223976409,-0.002592261998183278,-0.02364686309993755,0.02377494398854077,178.0+-0.020044708782887707,-0.044641636506989144,0.018583723563451313,0.09072940503102193,0.003934851612593237,0.008706873351046395,0.0375951860378878,-0.03949338287409329,-0.05780302607946657,0.007206516329202944,113.0+-0.1072256316073538,-0.044641636506989144,-0.011595014505211082,-0.04009893205125,0.04934129593323023,0.0644472995495836,-0.01394774321932938,0.03430885887772673,0.007027139682585861,-0.03007244590430716,200.0+0.08166636784565606,0.05068011873981862,-0.002972517914164677,-0.03321323009955148,0.04246153164222462,0.057871181852003385,-0.010266105415242439,0.03430885887772673,-0.0006117353045626216,-0.0010776975004659671,139.0+0.005383060374248237,0.05068011873981862,0.0175059114895705,0.03220093844158448,0.1277706088506941,0.12739014036928015,-0.02131101882750326,0.07120997975363674,0.06257762198769659,0.015490730158871856,139.0+0.038075906433423026,0.05068011873981862,-0.029917819761184662,-0.07452744180974262,-0.012576582685820214,-0.012587222050641968,0.0044604458011053266,-0.002592261998183278,0.0037090603325595967,-0.03007244590430716,88.0+0.03081082953138418,-0.044641636506989144,-0.020217511096257485,-0.00567042229275739,-0.004320865536613489,-0.02949723898727687,0.07809320188284416,-0.03949338287409329,-0.010903250651210127,-0.0010776975004659671,148.0+0.001750521923228816,0.05068011873981862,-0.05794093368208547,-0.04354178302709927,-0.09650970703608859,-0.047033552847490806,-0.09862541271332903,0.03430885887772673,-0.061175799045152635,-0.07149351505265171,88.0+-0.027309785684926546,0.05068011873981862,0.06061839444480248,0.10794365991026823,0.01219056876179996,-0.017597597439274533,-0.002902829807068556,-0.002592261998183278,0.07020738137223513,0.13561183068907107,243.0+-0.08543040090123728,0.05068011873981862,-0.040695940499992665,-0.03321323009955148,-0.08137422559587626,-0.06958024209633733,-0.006584467611155497,-0.03949338287409329,-0.05780302607946657,-0.042498766648810526,71.0+0.012648137276287077,0.05068011873981862,-0.07195249064253588,-0.04698463400294853,-0.051103262715451604,-0.09713730673381639,0.1185912177278005,-0.0763945037500033,-0.020292321339471356,-0.03835665973397607,77.0+-0.052737554842062495,-0.044641636506989144,-0.05578530953432388,-0.03665608107540074,0.08924392882106273,-0.003192768196955922,0.008142083605192267,0.03430885887772673,0.1323757911721786,0.0030644094143684884,109.0+-0.02367724723390713,0.05068011873981862,0.045529025410471304,0.0218723855140367,0.10988322169407955,0.08887287956916731,0.0007788079970183853,0.03430885887772673,0.07419089971278872,0.06105390622205087,272.0+-0.074532785548179,0.05068011873981862,-0.00943939035744949,0.014986683562338177,-0.037343734133440394,-0.0216685274425385,-0.01394774321932938,-0.002592261998183278,-0.03324559264822791,0.0113486232440374,60.0+-0.005514554978810025,0.05068011873981862,-0.033151255982827074,-0.015998975220305175,0.0080627101871966,0.01622243643399523,0.01550535921336615,-0.002592261998183278,-0.028323167238848198,-0.07563562196748617,54.0+-0.06000263174410134,0.05068011873981862,0.04984027370599448,0.01842953453818744,-0.016704441260423575,-0.03012353591085593,-0.01762938102341632,-0.002592261998183278,0.04977020032069951,-0.05906719430814835,221.0+-0.020044708782887707,-0.044641636506989144,-0.08488623552910546,-0.02632752814785296,-0.035967781275239266,-0.03419446591411989,0.04127682384197474,-0.05167075276314359,-0.08237869071592514,-0.04664087356364498,90.0+0.038075906433423026,0.05068011873981862,0.005649978676881689,0.03220093844158448,0.006686757328995478,0.017475030281153364,-0.024992656631590206,0.03430885887772673,0.014820979914103747,0.06105390622205087,311.0+0.016280675727306498,-0.044641636506989144,0.020739347711212906,0.0218723855140367,-0.013952535544021335,-0.013213518974221048,-0.006584467611155497,-0.002592261998183278,0.013316905483459898,0.040343371647878594,281.0+0.04170844488444244,-0.044641636506989144,-0.007283766209687899,0.028758087465735226,-0.04284754556624487,-0.048286146694648965,0.05232173725423556,-0.0763945037500033,-0.07213275338232743,0.02377494398854077,182.0+0.01991321417832592,0.05068011873981862,0.10480868947391528,0.07007229917592636,-0.035967781275239266,-0.02667890283117104,-0.024992656631590206,-0.002592261998183278,0.0037090603325595967,0.040343371647878594,321.0+-0.04910501639104307,0.05068011873981862,-0.02452875939178067,7.913883691088233e-05,-0.046975404140848234,-0.02824464514011871,-0.06549067247654655,0.028404679537581124,0.019196469166885697,0.0113486232440374,58.0+0.001750521923228816,0.05068011873981862,-0.006205954135807083,-0.019441826196154435,-0.009824676969417972,0.004949091809571968,-0.03971920784793797,0.03430885887772673,0.014820979914103747,0.09833286845556097,262.0+0.0344433679824036,-0.044641636506989144,-0.03854031635223107,-0.012556124244455912,0.00943866304539772,0.0052622402713615075,-0.006584467611155497,-0.002592261998183278,0.031192602201596156,0.09833286845556097,206.0+-0.045472477940023646,0.05068011873981862,0.13714305169033927,-0.015998975220305175,0.041085578784023497,0.03187985952347199,-0.04340084565202491,0.07120997975363674,0.07101867000452176,0.0486275854775475,233.0+-0.009147093429829445,0.05068011873981862,0.17055522598064407,0.014986683562338177,0.030077955918414535,0.03375875029420919,-0.02131101882750326,0.03430885887772673,0.033653814906286016,0.03205915781820968,242.0+-0.016412170331868287,0.05068011873981862,0.002416542455239321,0.014986683562338177,0.02182223876920781,-0.010082034356325698,-0.024992656631590206,0.03430885887772673,0.08553070935958189,0.08176444079622315,123.0+-0.009147093429829445,-0.044641636506989144,0.03798434089330568,-0.04009893205125,-0.0249601584096303,-0.0038190651205350003,-0.04340084565202491,0.01585829843977173,-0.005142189801713891,0.027917050903375224,167.0+0.01991321417832592,-0.044641636506989144,-0.05794093368208547,-0.057313186930496314,-0.001568959820211247,-0.012587222050641968,0.07441156407875721,-0.03949338287409329,-0.061175799045152635,-0.07563562196748617,63.0+0.0526060602375007,0.05068011873981862,-0.00943939035744949,0.049415193320830796,0.05071724879143135,-0.019163339748222204,-0.01394774321932938,0.03430885887772673,0.11934047943993234,-0.01764612515980379,197.0+-0.027309785684926546,0.05068011873981862,-0.023450947317899894,-0.015998975220305175,0.013566521620001083,0.012777803354310339,0.026550272625626974,-0.002592261998183278,-0.010903250651210127,-0.021788232074638245,71.0+-0.074532785548179,-0.044641636506989144,-0.010517202431330305,-0.00567042229275739,-0.06623874415566393,-0.05705430362475593,-0.002902829807068556,-0.03949338287409329,-0.042570854118219384,-0.0010776975004659671,168.0+-0.1072256316073538,-0.044641636506989144,-0.03422906805670789,-0.06764173985804409,-0.06348683843926169,-0.07051968748170592,0.008142083605192267,-0.03949338287409329,-0.0006117353045626216,-0.07977772888232063,140.0+0.04534098333546186,0.05068011873981862,-0.002972517914164677,0.10794365991026823,0.03558176735121902,0.02248540566978595,0.026550272625626974,-0.002592261998183278,0.02802037249332928,0.019632837073706312,217.0+-0.0018820165277906047,-0.044641636506989144,0.0681630789619681,-0.00567042229275739,0.11951489170148738,0.13020847652538595,-0.024992656631590206,0.08670845052151895,0.04613307487932449,-0.0010776975004659671,121.0+0.01991321417832592,0.05068011873981862,0.009961226972404908,0.01842953453818744,0.014942474478202204,0.04471894645684291,-0.06180903467245962,0.07120997975363674,0.009433658771615987,-0.0632093012229828,235.0+0.016280675727306498,0.05068011873981862,0.002416542455239321,-0.00567042229275739,-0.005696818394814609,0.010898912583573148,-0.050764121260198795,0.03430885887772673,0.022687744966501246,-0.03835665973397607,245.0+-0.0018820165277906047,-0.044641636506989144,-0.03854031635223107,0.0218723855140367,-0.10889328275989867,-0.11561306597939897,0.022868634821540033,-0.0763945037500033,-0.04688253415273158,0.02377494398854077,40.0+0.016280675727306498,-0.044641636506989144,0.026128408080616904,0.05860760542634833,-0.060734932722859444,-0.044215216691385,-0.01394774321932938,-0.03395821474270679,-0.05140387304727299,-0.025930338989472702,52.0+-0.07090024709715959,0.05068011873981862,-0.08919748382462865,-0.07452744180974262,-0.04284754556624487,-0.02573945744580244,-0.03235593223976409,-0.002592261998183278,-0.012908683225401873,-0.05492508739331389,104.0+0.04897352178648128,-0.044641636506989144,0.06061839444480248,-0.0228846771720037,-0.02358420555142918,-0.07271172671423268,-0.04340084565202491,-0.002592261998183278,0.10413565428651514,0.036201264733044136,132.0+0.005383060374248237,0.05068011873981862,-0.028840007687303888,-0.009113273268606652,-0.0318399227006359,-0.02887094206369779,0.008142083605192267,-0.03949338287409329,-0.018113692315690322,0.007206516329202944,88.0+0.0344433679824036,0.05068011873981862,-0.029917819761184662,0.004658130634790395,0.0933717873956661,0.08699398879843012,0.033913548233800855,-0.002592261998183278,0.024055085357995654,-0.03835665973397607,69.0+0.02354575262934534,0.05068011873981862,-0.01913969902237667,0.049415193320830796,-0.06348683843926169,-0.06112523362801988,0.0044604458011053266,-0.03949338287409329,-0.025953110560258,-0.013504018244969336,219.0+0.01991321417832592,-0.044641636506989144,-0.040695940499992665,-0.015998975220305175,-0.008448724111216851,-0.017597597439274533,0.05232173725423556,-0.03949338287409329,-0.030747917533098208,0.0030644094143684884,72.0+-0.045472477940023646,-0.044641636506989144,0.015350287341808908,-0.07452744180974262,-0.04972730985725048,-0.017284448977484993,-0.028674294435677143,-0.002592261998183278,-0.10436552421115437,-0.07563562196748617,201.0+0.0526060602375007,0.05068011873981862,-0.02452875939178067,0.056300895272529315,-0.007072771253015731,-0.005071658967693135,-0.02131101882750326,-0.002592261998183278,0.02671684132010462,-0.03835665973397607,110.0+-0.005514554978810025,0.05068011873981862,0.0013387303813585058,-0.0848559947372904,-0.011200629827619093,-0.016658152053905938,0.04864009945014862,-0.03949338287409329,-0.041176166918895155,-0.08806194271198954,51.0+0.009015598825267658,0.05068011873981862,0.06924089103584885,0.059743746248378575,0.017694380194604446,-0.023234269751486174,-0.04708248345611185,0.03430885887772673,0.10329701884639861,0.07348022696655424,277.0+-0.02367724723390713,-0.044641636506989144,-0.06979686649477428,-0.06419888888219483,-0.05935897986465832,-0.05047818592717569,0.019186997017453092,-0.03949338287409329,-0.08913335224990723,-0.05078298047847944,63.0+-0.04183993948900423,0.05068011873981862,-0.029917819761184662,-0.002227571316908129,0.02182223876920781,0.036577086450315016,0.01182372140927921,-0.002592261998183278,-0.041176166918895155,0.06519601313688532,118.0+-0.074532785548179,-0.044641636506989144,-0.04608500086939666,-0.04354178302709927,-0.029088016984233665,-0.023234269751486174,0.01550535921336615,-0.03949338287409329,-0.03980882652740082,-0.021788232074638245,69.0+0.0344433679824036,-0.044641636506989144,0.018583723563451313,0.056300895272529315,0.01219056876179996,-0.054549115930439665,-0.0691723102806335,0.07120997975363674,0.13007865931446802,0.007206516329202944,273.0+-0.06000263174410134,-0.044641636506989144,0.0013387303813585058,-0.029770379123702218,-0.007072771253015731,-0.0216685274425385,0.01182372140927921,-0.002592261998183278,0.031812463179073616,-0.05492508739331389,258.0+-0.08543040090123728,0.05068011873981862,-0.03099563183506548,-0.0228846771720037,-0.06348683843926169,-0.05423596746865012,0.019186997017453092,-0.03949338287409329,-0.09643494994048712,-0.03421455281914162,43.0+0.0526060602375007,-0.044641636506989144,-0.004050329988045492,-0.03090651994573247,-0.046975404140848234,-0.05830689747191407,-0.01394774321932938,-0.02583996815000658,0.036060333995316066,0.02377494398854077,198.0+0.012648137276287077,-0.044641636506989144,0.015350287341808908,-0.03321323009955148,0.041085578784023497,0.032193007985261514,-0.002902829807068556,-0.002592261998183278,0.04506654937395887,-0.06735140813781726,242.0+0.059871137139539544,0.05068011873981862,0.022894971858974496,0.049415193320830796,0.016318427336403322,0.011838357968941744,-0.01394774321932938,-0.002592261998183278,0.03954249419232167,0.019632837073706312,232.0+-0.02367724723390713,-0.044641636506989144,0.045529025410471304,0.09072940503102193,-0.018080394118624697,-0.03544705976127803,0.07072992627467027,-0.03949338287409329,-0.03452177701362266,-0.009361911330134878,175.0+0.016280675727306498,-0.044641636506989144,-0.04500718879551588,-0.057313186930496314,-0.034591828417038145,-0.053922819006860585,0.07441156407875721,-0.0763945037500033,-0.042570854118219384,0.040343371647878594,93.0+0.11072667545381144,0.05068011873981862,-0.033151255982827074,-0.0228846771720037,-0.004320865536613489,0.020293366437259194,-0.06180903467245962,0.07120997975363674,0.015568459328120622,0.044485478562713045,168.0+-0.020044708782887707,-0.044641636506989144,0.09726400495674965,-0.00567042229275739,-0.005696818394814609,-0.02386056667506523,-0.02131101882750326,-0.002592261998183278,0.06168429293192034,0.040343371647878594,275.0+-0.016412170331868287,-0.044641636506989144,0.05415152200151766,0.07007229917592636,-0.033215875558837024,-0.0279314966783292,0.008142083605192267,-0.03949338287409329,-0.02712902329694316,-0.009361911330134878,293.0+0.04897352178648128,0.05068011873981862,0.12313149472988882,0.08384370307932341,-0.10476542418529532,-0.10089508827529081,-0.0691723102806335,-0.002592261998183278,0.03664373256235367,-0.03007244590430716,281.0+-0.056370093293081916,-0.044641636506989144,-0.08057498723358228,-0.0848559947372904,-0.037343734133440394,-0.037012802070225705,0.033913548233800855,-0.03949338287409329,-0.05615310200706333,-0.13776722569000302,72.0+0.027178291080364757,-0.044641636506989144,0.09295275666122646,-0.0527341951326168,0.0080627101871966,0.039708571068210366,-0.028674294435677143,0.021024455362399115,-0.04835926177554553,0.019632837073706312,140.0+0.06350367559055897,-0.044641636506989144,-0.050396249164919873,0.10794365991026823,0.03145390877661565,0.019353921051890578,-0.01762938102341632,0.02360753382371283,0.05803805188793539,0.040343371647878594,189.0+-0.052737554842062495,0.05068011873981862,-0.011595014505211082,0.056300895272529315,0.05622106022423583,0.07290230801790105,-0.03971920784793797,0.07120997975363674,0.030563625621508765,-0.005219804415300423,181.0+-0.009147093429829445,0.05068011873981862,-0.027762195613423073,0.008100981610639655,0.04796534307502911,0.037203383373894054,-0.028674294435677143,0.03430885887772673,0.06605066658209227,-0.042498766648810526,209.0+0.005383060374248237,-0.044641636506989144,0.05846277029704089,-0.04354178302709927,-0.07311850844666953,-0.07239857825244314,0.019186997017453092,-0.0763945037500033,-0.05140387304727299,-0.025930338989472702,136.0+0.07440129094361722,-0.044641636506989144,0.08540807214406083,0.06318659722422783,0.014942474478202204,0.013090951816099879,0.01550535921336615,-0.002592261998183278,0.006206735447689297,0.0859065477110576,261.0+-0.052737554842062495,-0.044641636506989144,-0.0008168937664030856,-0.02632752814785296,0.010814615903598841,0.0071411310420987206,0.04864009945014862,-0.03949338287409329,-0.0358161925842373,0.019632837073706312,113.0+0.08166636784565606,0.05068011873981862,0.006727790750762504,-0.00453428147072714,0.10988322169407955,0.11705624113022545,-0.03235593223976409,0.09187460744414634,0.05471997253790904,0.007206516329202944,131.0+-0.005514554978810025,-0.044641636506989144,0.008883414898524095,-0.05042748497879779,0.02595009734381117,0.04722413415115918,-0.04340084565202491,0.07120997975363674,0.014820979914103747,0.0030644094143684884,174.0+-0.027309785684926546,-0.044641636506989144,0.08001901177465684,0.0987512478047507,-0.0029449126784123676,0.018101327204732443,-0.01762938102341632,0.003311917341962329,-0.02952642678336326,0.036201264733044136,257.0+-0.052737554842062495,-0.044641636506989144,0.07139651518361048,-0.07452744180974262,-0.015328488402222454,-0.0013138774262187302,0.0044604458011053266,-0.021411833644897377,-0.04688253415273158,0.0030644094143684884,55.0+0.009015598825267658,-0.044641636506989144,-0.02452875939178067,-0.02632752814785296,0.09887559882847057,0.0941964034195894,0.07072992627467027,-0.002592261998183278,-0.021395309255276825,0.007206516329202944,84.0+-0.020044708782887707,-0.044641636506989144,-0.05470749746044306,-0.05387033595464705,-0.06623874415566393,-0.05736745208654548,0.01182372140927921,-0.03949338287409329,-0.07409260794346935,-0.005219804415300423,42.0+0.02354575262934534,-0.044641636506989144,-0.03638469220446948,7.913883691088233e-05,0.001182945896190995,0.034698195679577784,-0.04340084565202491,0.03430885887772673,-0.03324559264822791,0.06105390622205087,146.0+0.038075906433423026,0.05068011873981862,0.016428099415689682,0.0218723855140367,0.039709625925822375,0.04503209491863242,-0.04340084565202491,0.07120997975363674,0.04977020032069951,0.015490730158871856,212.0+-0.07816532399919843,0.05068011873981862,0.07786338762689529,0.052858044296680055,0.07823630595545376,0.0644472995495836,0.026550272625626974,-0.002592261998183278,0.04067282891595704,-0.009361911330134878,233.0+0.009015598825267658,0.05068011873981862,-0.039618128426111884,0.028758087465735226,0.03833367306762126,0.07352860494148013,-0.07285394808472044,0.10811110062954676,0.015568459328120622,-0.04664087356364498,91.0+0.001750521923228816,0.05068011873981862,0.011039039046285686,-0.019441826196154435,-0.016704441260423575,-0.0038190651205350003,-0.04708248345611185,0.03430885887772673,0.024055085357995654,0.02377494398854077,111.0+-0.07816532399919843,-0.044641636506989144,-0.040695940499992665,-0.08141314376144114,-0.10063756561069194,-0.11279472982329314,0.022868634821540033,-0.0763945037500033,-0.020292321339471356,-0.05078298047847944,152.0+0.03081082953138418,0.05068011873981862,-0.03422906805670789,0.04366563219116252,0.05759701308243695,0.0688313780146371,-0.03235593223976409,0.05755656502955003,0.03545870422305857,0.0859065477110576,120.0+-0.03457486258696539,0.05068011873981862,0.005649978676881689,-0.00567042229275739,-0.07311850844666953,-0.06269097593696756,-0.006584467611155497,-0.03949338287409329,-0.04542403773513769,0.03205915781820968,67.0+0.04897352178648128,0.05068011873981862,0.08864150836570328,0.08728655405517267,0.03558176735121902,0.02154596028441731,-0.024992656631590206,0.03430885887772673,0.06605066658209227,0.13146972377423663,310.0+-0.04183993948900423,-0.044641636506989144,-0.033151255982827074,-0.0228846771720037,0.04658939021682799,0.041587461838947556,0.05600337505832251,-0.024732934523729287,-0.025953110560258,-0.03835665973397607,94.0+-0.009147093429829445,-0.044641636506989144,-0.05686312160820465,-0.05042748497879779,0.02182223876920781,0.04534524338042199,-0.028674294435677143,0.03430885887772673,-0.009918765569334137,-0.01764612515980379,183.0+0.0707687524925978,0.05068011873981862,-0.03099563183506548,0.0218723855140367,-0.037343734133440394,-0.047033552847490806,0.033913548233800855,-0.03949338287409329,-0.014959693812643405,-0.0010776975004659671,66.0+0.009015598825267658,-0.044641636506989144,0.055229334075398484,-0.00567042229275739,0.05759701308243695,0.04471894645684291,-0.002902829807068556,0.023238522614953735,0.05568622641456506,0.1066170822852299,173.0+-0.027309785684926546,-0.044641636506989144,-0.06009655782984706,-0.029770379123702218,0.04658939021682799,0.019980217975469634,0.12227285553188745,-0.03949338287409329,-0.05140387304727299,-0.009361911330134878,72.0+0.016280675727306498,-0.044641636506989144,0.0013387303813585058,0.008100981610639655,0.005310804470794357,0.010898912583573148,0.030231910429713918,-0.03949338287409329,-0.04542403773513769,0.03205915781820968,49.0+-0.012779631880848867,-0.044641636506989144,-0.023450947317899894,-0.04009893205125,-0.016704441260423575,0.00463594334778245,-0.01762938102341632,-0.002592261998183278,-0.03845971734112638,-0.03835665973397607,64.0+-0.056370093293081916,-0.044641636506989144,-0.07410811479029747,-0.05042748497879779,-0.0249601584096303,-0.047033552847490806,0.09281975309919192,-0.0763945037500033,-0.061175799045152635,-0.04664087356364498,48.0+0.04170844488444244,0.05068011873981862,0.01966153563733209,0.059743746248378575,-0.005696818394814609,-0.0025664712733768653,-0.028674294435677143,-0.002592261998183278,0.031192602201596156,0.007206516329202944,178.0+-0.005514554978810025,0.05068011873981862,-0.015906262800734303,-0.06764173985804409,0.04934129593323023,0.07916527725369175,-0.028674294435677143,0.03430885887772673,-0.018113692315690322,0.044485478562713045,104.0+0.04170844488444244,0.05068011873981862,-0.015906262800734303,0.01729339371615719,-0.037343734133440394,-0.013839815897800126,-0.024992656631590206,-0.011079519799642579,-0.04688253415273158,0.015490730158871856,132.0+-0.045472477940023646,-0.044641636506989144,0.039062152967186486,0.0012152796589411327,0.016318427336403322,0.015282991048626631,-0.028674294435677143,0.02655962349378563,0.04452872881997113,-0.025930338989472702,220.0+-0.045472477940023646,-0.044641636506989144,-0.07303030271641665,-0.08141314376144114,0.08374011738825825,0.027808929520208008,0.17381578478910462,-0.03949338287409329,-0.00422151393810765,0.0030644094143684884,57.0
+ data/sharded/part-0.parquet view
binary file changed (absent → 2684 bytes)
+ data/sharded/part-1.parquet view
binary file changed (absent → 2462 bytes)
dataframe.cabal view
@@ -1,7 +1,6 @@-cabal-version: 2.4+cabal-version: 3.4 name: dataframe-version: 0.7.0.0-+version: 3.0.0.0 synopsis: A fast, safe, and intuitive DataFrame library. description: A fast, safe, and intuitive DataFrame library for exploratory data analysis.@@ -12,12 +11,18 @@ author: Michael Chavinda maintainer: mschavinda@gmail.com -copyright: (c) 2024-2025 Michael Chavinda+copyright: (c) 2024-2026 Michael Chavinda category: Data tested-with: GHC ==9.4.8 || ==9.6.7 || ==9.8.4 || ==9.10.3 || ==9.12.2 extra-doc-files: CHANGELOG.md README.md-extra-source-files: cbits/process_csv.h+extra-source-files: cbits/arrow_abi.h+ tests/data/typing/texts.txt+ tests/data/typing/texts_with_empties.txt+ tests/data/typing/texts_with_empties_and_nullish.txt data/titanic/*.csv+ data/ml/*.csv+ data/ml/golden.json+ data/sharded/*.parquet tests/data/*.csv tests/data/*.tsv tests/data/*.parquet@@ -34,126 +39,195 @@ location: https://github.com/mchav/dataframe common warnings- -- TODO add more warnings, eventually -Wall ghc-options:- -Wincomplete-patterns- -Wincomplete-uni-patterns- -Wunused-imports- -Wunused-packages- -Wunused-local-binds+ -Wall +flag no-csv+ default: False+ manual: True+ description: Exclude the CSV reader/writer (@dataframe-csv@). Enable+ with @-f +no-csv@ (or @flags: no-csv@ in cabal.project)+ to trim the dep set of the meta package when CSV is not+ needed.++flag no-parquet+ default: False+ manual: True+ description: Exclude the Parquet reader/writer (@dataframe-parquet@+ plus pinch, zstd, snappy). Enable with+ @-f +no-parquet@. (Reading Parquet from HuggingFace lives+ in the separate @dataframe-huggingface@ package, which is+ not a dependency of this meta-package.)++flag no-th+ default: False+ manual: True+ description: Exclude the Template Haskell splices (@dataframe-th@,+ @dataframe-csv-th@, @dataframe-parquet-th@). Enable+ with @-f +no-th@ to drop the @template-haskell@ dep for+ downstream packages that don't use compile-time schema+ derivation.+ library import: warnings default-extensions: Strict+ -- Lock the unused-packages gate locally; haskell-ci already runs the+ -- same Werror against this stanza. Catches the case where a satellite+ -- gets added to build-depends but never re-exported.+ ghc-options: -Werror=unused-packages exposed-modules: DataFrame,- DataFrame.Lazy,- DataFrame.Functions,- DataFrame.Synthesis,- DataFrame.Display.Web.Plot,- DataFrame.Internal.Types,- DataFrame.Internal.Expression,- DataFrame.Internal.Interpreter,- DataFrame.Internal.Parsing,- DataFrame.Internal.Column,- DataFrame.Internal.Statistics,- DataFrame.Display.Terminal.PrettyPrint,- DataFrame.Display.Terminal.Colours,- DataFrame.Internal.DataFrame,- DataFrame.Internal.Row,- DataFrame.Internal.Schema,- DataFrame.Errors,- DataFrame.Operations.Core,- DataFrame.Operations.Join,- DataFrame.Operations.Merge,- DataFrame.Operators,- DataFrame.Operations.Permutation,- DataFrame.Operations.Subset,- DataFrame.Operations.Statistics,- DataFrame.Operations.Transformations,- DataFrame.Operations.Typing,- DataFrame.Operations.Aggregation,- DataFrame.Display,- DataFrame.Display.Terminal.Plot,- DataFrame.IO.CSV,- DataFrame.IO.JSON,- DataFrame.IO.Unstable.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.IO.Binary,- DataFrame.Lazy.Internal.DataFrame,- DataFrame.Lazy.Internal.LogicalPlan,- DataFrame.Lazy.Internal.PhysicalPlan,- DataFrame.Lazy.Internal.Optimizer,- DataFrame.Lazy.Internal.Executor,- DataFrame.Monad,- DataFrame.DecisionTree,- DataFrame.Typed.Types,- DataFrame.Typed.Schema,- DataFrame.Typed.Freeze,- DataFrame.Typed.Access,- DataFrame.Typed.Operations,- DataFrame.Typed.Join,- DataFrame.Typed.Aggregate,- DataFrame.Typed.TH,- DataFrame.Typed.Expr, DataFrame.Typed+ reexported-modules: DataFrame.Functions,+ DataFrame.Synthesis,+ DataFrame.Display.Web.Plot,+ DataFrame.Display.Web.Chart,+ DataFrame.Display.Web.Chart.Typed,+ DataFrame.Core,+ DataFrame.Schema,+ DataFrame.Errors,+ DataFrame.Operations.Core,+ DataFrame.Operations.Join,+ DataFrame.Operations.Merge,+ DataFrame.Operators,+ DataFrame.Operations.Permutation,+ DataFrame.Operations.Subset,+ DataFrame.Operations.Statistics,+ DataFrame.Operations.Transformations,+ DataFrame.Operations.Typing,+ DataFrame.Operations.Aggregation,+ DataFrame.Display,+ DataFrame.Display.Terminal.Plot,+ DataFrame.Monad,+ DataFrame.DecisionTree,+ DataFrame.Model,+ DataFrame.ModelSelection,+ DataFrame.LinearModel,+ DataFrame.LinearModel.Regression,+ DataFrame.LinearModel.Logistic,+ DataFrame.Segmented,+ DataFrame.SVM,+ DataFrame.KMeans,+ DataFrame.PCA,+ DataFrame.DBSCAN,+ DataFrame.GMM,+ DataFrame.Boosting,+ DataFrame.Boosting.GBM,+ DataFrame.Boosting.AdaBoost,+ DataFrame.Metrics,+ DataFrame.IO.JSON,+ DataFrame.Expr.Serialize,+ DataFrame.IR.ExprJson,+ DataFrame.Typed.Types,+ DataFrame.Typed.Schema,+ DataFrame.Typed.Freeze,+ DataFrame.Typed.Access,+ DataFrame.Typed.Operations,+ DataFrame.Typed.Join,+ DataFrame.Typed.Aggregate,+ DataFrame.Typed.Expr,+ DataFrame.Typed.Record,+ DataFrame.Typed.Generic build-depends: base >= 4 && <5,- deepseq >= 1 && < 2,- aeson >= 0.11.0.0 && < 3,- array >= 0.5.4.0 && < 0.6,- attoparsec >= 0.12 && < 0.15,- bytestring >= 0.11 && < 0.13,- bytestring-lexing >= 0.5 && < 0.6,- cassava >= 0.1 && < 1,- containers >= 0.6.7 && < 0.9,- directory >= 1.3.0.0 && < 2,- granite ^>= 0.4,- hashable >= 1.2 && < 2,- process ^>= 1.6,- snappy-hs ^>= 0.1,- random >= 1.2 && < 2,- regex-tdfa >= 1.3.0 && < 2,- scientific >=0.3.1 && <0.4,- template-haskell >= 2.0 && < 3,- text >= 2.0 && < 3,- these >= 1.1 && < 2,- time >= 1.12 && < 2,- unordered-containers >= 0.1 && < 1,- vector ^>= 0.13,- vector-algorithms ^>= 0.9,- zlib >= 0.5 && < 1,- zstd >= 0.1.2.0 && < 0.2,- mmap >= 0.5.8 && < 0.6,- parallel >= 3.2.2.0 && < 5,- stm >= 2.5 && < 3,- filepath >= 1.4 && < 2,- Glob >= 0.10 && < 1,+ dataframe-core >= 2.0 && < 2.1,+ dataframe-json >= 1.1 && < 1.2,+ dataframe-expr-serializer >= 1.1 && < 1.2,+ dataframe-operations >= 2.0 && < 2.1,+ dataframe-parsing >= 2.0 && < 2.1,+ dataframe-viz >= 1.1 && < 1.2,+ dataframe-learn >= 2.0 && < 2.1 + if !flag(no-csv)+ reexported-modules: DataFrame.IO.CSV,+ DataFrame.Typed.IO.CSV+ build-depends: dataframe-csv >= 2.0 && < 2.1+ cpp-options: -DWITH_CSV++ if !flag(no-parquet)+ reexported-modules: DataFrame.IO.Parquet,+ DataFrame.IO.Parquet.Binary,+ DataFrame.IO.Parquet.Dictionary,+ DataFrame.IO.Parquet.Levels,+ DataFrame.IO.Parquet.Thrift,+ DataFrame.IO.Parquet.Decompress,+ DataFrame.IO.Parquet.Encoding,+ DataFrame.IO.Parquet.Page,+ DataFrame.IO.Parquet.Schema,+ DataFrame.IO.Parquet.Utils,+ DataFrame.IO.Parquet.Seeking,+ DataFrame.IO.Parquet.Time,+ DataFrame.IO.Utils.RandomAccess,+ DataFrame.Typed.IO.Parquet+ build-depends: dataframe-parquet >= 1.2 && < 1.3+ cpp-options: -DWITH_PARQUET++ -- The lazy executor calls both CSV and Parquet readers directly, so+ -- it can only be pulled in when both backends are present.+ if !flag(no-csv) && !flag(no-parquet)+ reexported-modules: DataFrame.Lazy,+ DataFrame.Lazy.IO.Binary,+ DataFrame.Lazy.IO.CSV,+ DataFrame.Typed.Lazy+ build-depends: dataframe-lazy >= 2.0 && < 2.1+ cpp-options: -DWITH_LAZY++ if !flag(no-th)+ build-depends: dataframe-th >= 2.0 && < 2.1+ cpp-options: -DWITH_TH+ exposed-modules: DataFrame.TH,+ DataFrame.Typed.TH++ if !flag(no-th) && !flag(no-csv)+ build-depends: dataframe-csv-th >= 1.1 && < 1.2+ cpp-options: -DWITH_CSV_TH++ if !flag(no-th) && !flag(no-parquet)+ build-depends: dataframe-parquet-th >= 1.1 && < 1.2+ cpp-options: -DWITH_PARQUET_TH+ hs-source-dirs: src- c-sources: cbits/process_csv.c+ default-language: Haskell2010++library arrow-bridge+ import: warnings+ visibility: public+ hs-source-dirs: ffi+ exposed-modules: DataFrame.IO.Arrow+ DataFrame.IR+ -- The expr/pipeline JSON codec now lives in its own lightweight package;+ -- re-export it so the Python FFI and Haskell consumers keep importing+ -- @DataFrame.IR.ExprJson@ unchanged.+ reexported-modules: DataFrame.IR.ExprJson+ -- The Arrow bridge IR loads CSV + Parquet readers, so it requires+ -- both backends. If either flag is off, skip building it.+ if flag(no-csv) || flag(no-parquet)+ buildable: False+ build-depends:+ base >= 4 && < 5,+ dataframe-core:internal >= 2.0 && < 2.1,+ dataframe-expr-serializer >= 1.1 && < 1.2,+ dataframe-csv >= 2.0 && < 2.1,+ dataframe-json >= 1.1 && < 1.2,+ dataframe-lazy >= 2.0 && < 2.1,+ dataframe-operations >= 2.0 && < 2.1,+ dataframe-parquet >= 1.2 && < 1.3,+ dataframe-parsing >= 2.0 && < 2.1,+ text >= 2.1 && < 3,+ aeson >= 0.11 && < 3,+ bytestring >= 0.11 && < 0.14,+ containers >= 0.6.7 && < 0.10,+ vector >= 0.13 && < 0.15 include-dirs: cbits- includes: process_csv.h- install-includes: process_csv.h default-language: Haskell2010 executable dataframe-benchmark-example import: warnings main-is: Benchmark.hs build-depends: base >= 4 && < 5,- dataframe >= 0.5 && < 1,+ dataframe >= 3.0 && < 3.1,+ dataframe-operations >= 2.0 && < 2.1, random >= 1 && < 2, time >= 1.12 && < 2,- vector ^>= 0.13,+ vector >= 0.13 && < 0.15, hs-source-dirs: app default-language: Haskell2010 ghc-options: -rtsopts -threaded -with-rtsopts=-N@@ -162,9 +236,12 @@ import: warnings main-is: Synthesis.hs build-depends: base >= 4 && < 5,- dataframe >= 0.5 && < 1,+ dataframe >= 3.0 && < 3.1,+ dataframe-core >= 2.0 && < 2.1,+ dataframe-learn >= 2.0 && < 2.1,+ dataframe-operations >= 2.0 && < 2.1, random >= 1 && < 2,- text >= 2.0 && < 3+ text >= 2.1 && < 3 hs-source-dirs: app default-language: Haskell2010 ghc-options: -rtsopts -threaded -with-rtsopts=-N@@ -186,13 +263,20 @@ executable lazy-bench import: warnings main-is: LazyBenchmark.hs+ -- lazy-bench drives the lazy executor against CSV/Parquet sources, so+ -- it can only build when both backends are present.+ if flag(no-csv) || flag(no-parquet)+ buildable: False build-depends: base >= 4 && < 5,- bytestring >= 0.11 && < 0.13,- containers >= 0.6.7 && < 0.9,- dataframe >= 0.5 && < 1,+ bytestring >= 0.11 && < 0.14,+ containers >= 0.6.7 && < 0.10,+ dataframe >= 3.0 && < 3.1,+ dataframe-core >= 2.0 && < 2.1,+ dataframe-lazy >= 2.0 && < 2.1,+ dataframe-parsing >= 2.0 && < 2.1, directory >= 1.3.0.0 && < 2, random >= 1 && < 2,- text >= 2.0 && < 3,+ text >= 2.1 && < 3, time >= 1.12 && < 2, hs-source-dirs: app default-language: Haskell2010@@ -205,8 +289,11 @@ hs-source-dirs: benchmark build-depends: base >= 4 && < 5, criterion >= 1 && < 2,+ deepseq >= 1.4 && < 2, process >= 1.6 && < 2,- dataframe >= 0.5 && < 1,+ dataframe >= 3.0 && < 3.1,+ dataframe-core:internal >= 2.0 && < 2.1,+ dataframe-operations >= 2.0 && < 2.1, random >= 1 && < 2, default-language: Haskell2010 ghc-options:@@ -218,44 +305,119 @@ import: warnings type: exitcode-stdio-1.0 main-is: Main.hs+ -- Threaded RTS with -N so the parallel-grouping parity test exercises the+ -- real multi-capability fork path rather than the sequential fallback.+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ -- The test runner imports CSV, JSON, Parquet, Lazy, and TH-derived+ -- schema modules. All features must be enabled for it to compile.+ if flag(no-csv) || flag(no-parquet) || flag(no-th)+ buildable: False other-modules: Assertions, Functions, GenDataFrame,+ Internal.ColumnBuilder,+ Internal.DictEncode,+ Internal.Markdown,+ Internal.PackedText, Internal.Parsing,+ PackedTextMigration,+ PrettyPrint,+ Learn.Denotation,+ Learn.Models,+ Learn.TypedModel,+ Learn.Segmented,+ Learn.Ensembles,+ Learn.SklearnParity,+ Learn.Synthesis,+ Learn.MetricsTests,+ Learn.Metamorphic,+ IO.CSV,+ IO.CsvGolden, IO.JSON,+ IR.ExprJsonRoundtrip, Operations.Aggregations, Operations.Apply, Operations.Core, Operations.Derive, Operations.Filter, Operations.GroupBy,+ Operations.ParallelGroupBy,+ Operations.ParallelJoin,+ Operations.Inference, Operations.InsertColumn, Operations.Join, Operations.Merge,+ Operations.Nullable,+ Operations.NullableHashing,+ Operations.Provenance, Operations.ReadCsv,+ Operations.Window,+ Operations.WriteCsv, Operations.Shuffle, Operations.Sort,+ Operations.SetOps, Operations.Subset, Operations.Statistics, Operations.Take, Operations.Typing,+ Operations.VectorKernel,+ Operations.Record, LazyParquet,+ LazyParity, Parquet,+ ParquetTestData,+ Plotting, Properties,+ Properties.Categorical,+ Simplify,+ Typed.Parity,+ Typed.IOReaders, Monad build-depends: base >= 4 && < 5,- bytestring >= 0.11 && < 0.13,- dataframe >= 0.5 && < 1,- directory >= 1.3.0.0 && < 2,- HUnit ^>= 1.6,+ aeson >= 0.11.0.0 && < 3,+ bytestring >= 0.11 && < 0.14,+ dataframe >= 3.0 && < 3.1,+ dataframe-core >= 2.0 && < 2.1,+ dataframe-core:internal >= 2.0 && < 2.1,+ dataframe-csv >= 2.0 && < 2.1,+ dataframe-expr-serializer >= 1.1 && < 1.2,+ dataframe-json >= 1.1 && < 1.2,+ dataframe-lazy >= 2.0 && < 2.1,+ dataframe-learn >= 2.0 && < 2.1,+ dataframe-operations >= 2.0 && < 2.1,+ dataframe-operations:internal >= 2.0 && < 2.1,+ dataframe-parquet >= 1.2 && < 1.3,+ dataframe-parsing >= 2.0 && < 2.1,+ dataframe-parsing:internal >= 2.0 && < 2.1,+ HUnit >= 1.6 && < 1.8, QuickCheck >= 2 && < 3, random-shuffle >= 0.0.4 && < 1, random >= 1 && < 2,- text >= 2.0 && < 3,- these >= 1.1 && < 2,+ text >= 2.1 && < 3, time >= 1.12 && < 2,- vector ^>= 0.13,- containers >= 0.6.7 && < 0.9+ vector >= 0.13 && < 0.15,+ temporary >= 1.3 && < 1.5,+ directory >= 1.3 && < 1.4,+ containers >= 0.6.7 && < 0.10 hs-source-dirs: tests default-language: Haskell2010 +-- Focused oracle for the packed-text column variant. Kept as its own suite so+-- it can run independently of the (heavier) `tests` runner.+test-suite packed-text+ import: warnings+ type: exitcode-stdio-1.0+ main-is: PackedTextMain.hs+ if flag(no-csv) || flag(no-parquet) || flag(no-th)+ buildable: False+ other-modules: Internal.PackedText+ build-depends: base >= 4 && < 5,+ bytestring >= 0.11 && < 0.14,+ dataframe >= 3.0 && < 3.1,+ dataframe-core:internal >= 2.0 && < 2.1,+ dataframe-operations >= 2.0 && < 2.1,+ HUnit >= 1.6 && < 1.8,+ text >= 2.1 && < 3,+ vector >= 0.13 && < 0.15+ hs-source-dirs: tests+ default-language: Haskell2010
+ ffi/DataFrame/IO/Arrow.hs view
@@ -0,0 +1,552 @@+{-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE ForeignFunctionInterface #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Convert a 'DataFrame' to Arrow C Data Interface structs for zero-copy+ transfer to Python (or any other Arrow consumer).+-}+module DataFrame.IO.Arrow (+ dataframeToArrow,+ columnToArrow,+ arrowToDataframe,+ releaseSchemaImpl,+ releaseArrayImpl,+) where++import qualified Data.ByteString as BS+import qualified Data.Map as M+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import qualified DataFrame.Internal.Column as DI++import Control.Monad (foldM_, forM, join, when, zipWithM_)+import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))+import Foreign+import Foreign.C.String (CString, newCString, peekCString)+import Type.Reflection (typeRep)++import DataFrame.Internal.Column (Column (..))+import DataFrame.Internal.DataFrame (DataFrame (..), fromNamedColumns)++-- ---------------------------------------------------------------------------+-- Opaque phantom types for the Arrow structs+-- ---------------------------------------------------------------------------++data ArrowSchema+data ArrowArray++arrowSchemaSize :: Int+arrowSchemaSize = 72 -- 9 × 8 bytes++arrowArraySize :: Int+arrowArraySize = 80 -- 10 × 8 bytes++-- ArrowSchema field byte offsets+_schemaFormat+ , _schemaName+ , _schemaMetadata+ , _schemaFlags+ , _schemaNChildren+ , _schemaChildren+ , _schemaDictionary+ , _schemaRelease+ , _schemaPrivateData ::+ Int+_schemaFormat = 0+_schemaName = 8+_schemaMetadata = 16+_schemaFlags = 24+_schemaNChildren = 32+_schemaChildren = 40+_schemaDictionary = 48+_schemaRelease = 56+_schemaPrivateData = 64++-- ArrowArray field byte offsets+_arrayLength+ , _arrayNullCount+ , _arrayOffset+ , _arrayNBuffers+ , _arrayNChildren+ , _arrayBuffers+ , _arrayChildren+ , _arrayDictionary+ , _arrayRelease+ , _arrayPrivateData ::+ Int+_arrayLength = 0+_arrayNullCount = 8+_arrayOffset = 16+_arrayNBuffers = 24+_arrayNChildren = 32+_arrayBuffers = 40+_arrayChildren = 48+_arrayDictionary = 56+_arrayRelease = 64+_arrayPrivateData = 72++-- ---------------------------------------------------------------------------+-- Helpers+-- ---------------------------------------------------------------------------++-- Write a Storable value at a byte offset from a base pointer.+at :: (Storable a) => Ptr b -> Int -> a -> IO ()+at p off = poke (castPtr (p `plusPtr` off))++-- Read a Storable value at a byte offset from a base pointer.+readAt :: (Storable a) => Ptr b -> Int -> IO a+readAt p off = peek (castPtr (p `plusPtr` off))++-- ---------------------------------------------------------------------------+-- Release callbacks (self-import trick for compile-time-constant FunPtr)+-- ---------------------------------------------------------------------------++foreign export ccall "df_release_schema"+ releaseSchemaImpl :: Ptr ArrowSchema -> IO ()++foreign import ccall "&df_release_schema"+ pReleaseSchema :: FunPtr (Ptr ArrowSchema -> IO ())++foreign export ccall "df_release_array"+ releaseArrayImpl :: Ptr ArrowArray -> IO ()++foreign import ccall "&df_release_array"+ pReleaseArray :: FunPtr (Ptr ArrowArray -> IO ())++-- Dynamic wrappers to call producer's release callbacks after copying.+foreign import ccall "dynamic"+ callRelSchema :: FunPtr (Ptr ArrowSchema -> IO ()) -> Ptr ArrowSchema -> IO ()++foreign import ccall "dynamic"+ callRelArray :: FunPtr (Ptr ArrowArray -> IO ()) -> Ptr ArrowArray -> IO ()++releaseSchemaImpl :: Ptr ArrowSchema -> IO ()+releaseSchemaImpl p = do+ rawPriv <- peek (castPtr (p `plusPtr` _schemaPrivateData) :: Ptr (Ptr ()))+ let sp = castPtrToStablePtr rawPriv :: StablePtr (IO ())+ join (deRefStablePtr sp)+ freeStablePtr sp+ -- Arrow spec: release callback must set release to NULL to signal completion.+ -- p here is Arrow C++'s internal copy of the struct (not our mallocBytes+ -- allocation); our original allocation is freed inside the cleanup closure.+ p `at` _schemaRelease $ (nullFunPtr :: FunPtr (Ptr ArrowSchema -> IO ()))++releaseArrayImpl :: Ptr ArrowArray -> IO ()+releaseArrayImpl p = do+ rawPriv <- peek (castPtr (p `plusPtr` _arrayPrivateData) :: Ptr (Ptr ()))+ let sp = castPtrToStablePtr rawPriv :: StablePtr (IO ())+ join (deRefStablePtr sp)+ freeStablePtr sp+ -- Same reasoning as releaseSchemaImpl.+ p `at` _arrayRelease $ (nullFunPtr :: FunPtr (Ptr ArrowArray -> IO ()))++makeLeafSchema :: String -> T.Text -> IO (Ptr ArrowSchema)+makeLeafSchema fmt colName = do+ p <- mallocBytes arrowSchemaSize+ fmtStr <- newCString fmt+ nameStr <- newCString (T.unpack colName)+ p `at` _schemaFormat $ fmtStr+ p `at` _schemaName $ nameStr+ p `at` _schemaMetadata $ (nullPtr :: Ptr ())+ p `at` _schemaFlags $ (0 :: Int64)+ p `at` _schemaNChildren $ (0 :: Int64)+ p `at` _schemaChildren $ (nullPtr :: Ptr ())+ p `at` _schemaDictionary $ (nullPtr :: Ptr ())+ p `at` _schemaRelease $ pReleaseSchema+ -- Capture p so our original mallocBytes allocation is freed when release runs.+ cleanup <- newStablePtr (free fmtStr >> free nameStr >> free p)+ p `at` _schemaPrivateData $ castStablePtrToPtr cleanup+ return p++makeLeafArray :: Int -> Int64 -> [Ptr ()] -> IO () -> IO (Ptr ArrowArray)+makeLeafArray nRows nullCnt bufPtrs extraCleanup = do+ p <- mallocBytes arrowArraySize+ let nb = length bufPtrs+ bufArr <- mallocArray nb :: IO (Ptr (Ptr ()))+ zipWithM_ (pokeElemOff bufArr) [0 ..] bufPtrs+ p `at` _arrayLength $ (fromIntegral nRows :: Int64)+ p `at` _arrayNullCount $ nullCnt+ p `at` _arrayOffset $ (0 :: Int64)+ p `at` _arrayNBuffers $ (fromIntegral nb :: Int64)+ p `at` _arrayNChildren $ (0 :: Int64)+ p `at` _arrayBuffers $ bufArr+ p `at` _arrayChildren $ (nullPtr :: Ptr ())+ p `at` _arrayDictionary $ (nullPtr :: Ptr ())+ p `at` _arrayRelease $ pReleaseArray+ -- Capture p so our original mallocBytes allocation is freed when release runs.+ cleanup <- newStablePtr (free bufArr >> extraCleanup >> free p)+ p `at` _arrayPrivateData $ castStablePtrToPtr cleanup+ return p++{- | Allocate an Arrow-format validity bitmap from a 'DI.Bitmap'.+Returns (ptr, nullCount). Caller must 'free' the pointer.+-}+bitmapToPtr :: Int -> DI.Bitmap -> IO (Ptr Word8, Int)+bitmapToPtr n bm = do+ let numBytes = max 1 ((n + 7) `div` 8)+ validCount = VU.foldl' (\acc b -> acc + popCount b) 0 bm+ nullCount = n - validCount+ bitmapPtr <- mallocBytes numBytes :: IO (Ptr Word8)+ VU.imapM_ (pokeElemOff bitmapPtr) bm+ when (VU.length bm < numBytes) $+ mapM_+ (\i -> pokeElemOff bitmapPtr i (0 :: Word8))+ [VU.length bm .. numBytes - 1]+ return (bitmapPtr, nullCount)++-- | Read an Arrow validity bitmap into a 'DI.Bitmap'.+readArrowBitmap :: Ptr Word8 -> Int -> IO DI.Bitmap+readArrowBitmap bitmapPtr n = VU.generateM ((n + 7) `div` 8) (peekElemOff bitmapPtr)++columnToArrow :: T.Text -> Column -> IO (Ptr ArrowSchema, Ptr ArrowArray)+columnToArrow colName (UnboxedColumn _ (vec :: VU.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) = do+ let n = VU.length vec+ dataPtr <- mallocArray (max 1 n) :: IO (Ptr Int64)+ VU.imapM_ (\i v -> pokeElemOff dataPtr i (fromIntegral v)) vec+ sPtr <- makeLeafSchema "l" colName+ aPtr <- makeLeafArray n 0 [nullPtr, castPtr dataPtr] (free dataPtr)+ return (sPtr, aPtr)+columnToArrow colName (UnboxedColumn _ (vec :: VU.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) = do+ let n = VU.length vec+ dataPtr <- mallocArray (max 1 n) :: IO (Ptr Double)+ VU.imapM_ (pokeElemOff dataPtr) vec+ sPtr <- makeLeafSchema "g" colName+ aPtr <- makeLeafArray n 0 [nullPtr, castPtr dataPtr] (free dataPtr)+ return (sPtr, aPtr)+columnToArrow colName (BoxedColumn Nothing (vec :: V.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @T.Text) = do+ let n = V.length vec+ bss = map TE.encodeUtf8 (V.toList vec)+ cumOff = scanl (+) 0 (map BS.length bss)+ total = last cumOff+ offPtr <- mallocArray (n + 1) :: IO (Ptr Int32)+ zipWithM_+ (\i o -> pokeElemOff offPtr i (fromIntegral o :: Int32))+ [0 ..]+ cumOff+ charsPtr <- mallocBytes (max 1 total) :: IO (Ptr Word8)+ foldM_+ ( \pos bs -> do+ BS.useAsCStringLen bs $ \(src, len) ->+ copyBytes (charsPtr `plusPtr` pos) (castPtr src) len+ return (pos + BS.length bs)+ )+ 0+ bss+ sPtr <- makeLeafSchema "u" colName+ aPtr <-+ makeLeafArray+ n+ 0+ [nullPtr, castPtr offPtr, castPtr charsPtr]+ (free offPtr >> free charsPtr)+ return (sPtr, aPtr)+columnToArrow colName (BoxedColumn Nothing (vec :: V.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) = do+ let n = V.length vec+ dataPtr <- mallocArray (max 1 n) :: IO (Ptr Double)+ V.imapM_ (pokeElemOff dataPtr) vec+ sPtr <- makeLeafSchema "g" colName+ aPtr <- makeLeafArray n 0 [nullPtr, castPtr dataPtr] (free dataPtr)+ return (sPtr, aPtr)+columnToArrow colName (BoxedColumn Nothing (vec :: V.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) = do+ let n = V.length vec+ dataPtr <- mallocArray (max 1 n) :: IO (Ptr Int64)+ V.imapM_ (\i v -> pokeElemOff dataPtr i (fromIntegral v)) vec+ sPtr <- makeLeafSchema "l" colName+ aPtr <- makeLeafArray n 0 [nullPtr, castPtr dataPtr] (free dataPtr)+ return (sPtr, aPtr)+-- Nullable Int (UnboxedColumn with bitmap)+columnToArrow colName (UnboxedColumn (Just bm) (vec :: VU.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) = do+ let n = VU.length vec+ (bitmapPtr, nullCount) <- bitmapToPtr n bm+ dataPtr <- mallocArray (max 1 n) :: IO (Ptr Int64)+ VU.imapM_ (\i v -> pokeElemOff dataPtr i (fromIntegral v :: Int64)) vec+ sPtr <- makeLeafSchema "l" colName+ aPtr <-+ makeLeafArray+ n+ (fromIntegral nullCount)+ [castPtr bitmapPtr, castPtr dataPtr]+ (free bitmapPtr >> free dataPtr)+ return (sPtr, aPtr)+-- Nullable Double (UnboxedColumn with bitmap)+columnToArrow colName (UnboxedColumn (Just bm) (vec :: VU.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) = do+ let n = VU.length vec+ (bitmapPtr, nullCount) <- bitmapToPtr n bm+ dataPtr <- mallocArray (max 1 n) :: IO (Ptr Double)+ VU.imapM_ (\i v -> pokeElemOff dataPtr i (realToFrac v :: Double)) vec+ sPtr <- makeLeafSchema "g" colName+ aPtr <-+ makeLeafArray+ n+ (fromIntegral nullCount)+ [castPtr bitmapPtr, castPtr dataPtr]+ (free bitmapPtr >> free dataPtr)+ return (sPtr, aPtr)+-- Nullable Text (BoxedColumn with bitmap)+columnToArrow colName (BoxedColumn (Just bm) (vec :: V.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @T.Text) = do+ let n = V.length vec+ -- For null positions, use empty BS (null placeholder in vec is never evaluated)+ bss =+ map+ (\i -> if DI.bitmapTestBit bm i then TE.encodeUtf8 (vec V.! i) else BS.empty)+ [0 .. n - 1]+ cumOff = scanl (+) 0 (map BS.length bss)+ total = last cumOff+ (bitmapPtr, nullCount) <- bitmapToPtr n bm+ offPtr <- mallocArray (n + 1) :: IO (Ptr Int32)+ zipWithM_+ (\i o -> pokeElemOff offPtr i (fromIntegral o :: Int32))+ [0 ..]+ cumOff+ charsPtr <- mallocBytes (max 1 total) :: IO (Ptr Word8)+ foldM_+ ( \pos bs -> do+ BS.useAsCStringLen bs $ \(src, len) ->+ copyBytes (charsPtr `plusPtr` pos) (castPtr src) len+ return (pos + BS.length bs)+ )+ 0+ bss+ sPtr <- makeLeafSchema "u" colName+ aPtr <-+ makeLeafArray+ n+ (fromIntegral nullCount)+ [castPtr bitmapPtr, castPtr offPtr, castPtr charsPtr]+ (free bitmapPtr >> free offPtr >> free charsPtr)+ return (sPtr, aPtr)+columnToArrow colName _ =+ error $+ "DataFrame.IO.Arrow.columnToArrow: unsupported column type for '"+ ++ T.unpack colName+ ++ "'"++dataframeToArrow :: DataFrame -> IO (Ptr ArrowSchema, Ptr ArrowArray)+dataframeToArrow df = do+ let idxToName = M.fromList [(v, k) | (k, v) <- M.toList (columnIndices df)]+ ncols = M.size (columnIndices df)+ colsInOrder =+ [ (idxToName M.! i, columns df V.! i)+ | i <- [0 .. ncols - 1]+ ]++ childPairs <- forM colsInOrder (uncurry columnToArrow)+ let childSPtrs = map fst childPairs+ childAPtrs = map snd childPairs++ let nRows = case colsInOrder of+ [] -> 0+ (_, col) : _ -> DI.columnLength col+ topSchema <- mallocBytes arrowSchemaSize+ fmtStr <- newCString "+s"+ nameStr <- newCString ""+ childSArr <- mallocArray ncols :: IO (Ptr (Ptr ArrowSchema))+ zipWithM_ (pokeElemOff childSArr) [0 ..] childSPtrs+ topSchema `at` _schemaFormat $ fmtStr+ topSchema `at` _schemaName $ nameStr+ topSchema `at` _schemaMetadata $ (nullPtr :: Ptr ())+ topSchema `at` _schemaFlags $ (0 :: Int64)+ topSchema `at` _schemaNChildren $ (fromIntegral ncols :: Int64)+ topSchema `at` _schemaChildren $ childSArr+ topSchema `at` _schemaDictionary $ (nullPtr :: Ptr ())+ topSchema `at` _schemaRelease $ pReleaseSchema+ -- Do NOT loop over children here: Arrow C++ zeroes children[i]->release+ -- during import, so reading it would yield a null function pointer.+ -- Children are released independently by Arrow C++; their own cleanup+ -- closures free their buffers and struct memory.+ cleanupS <- newStablePtr $ do+ free childSArr+ free fmtStr+ free nameStr+ free topSchema -- free our original mallocBytes allocation+ topSchema `at` _schemaPrivateData $ castStablePtrToPtr cleanupS++ -- ── Top-level struct array ──────────────────────────────────────────────+ topArray <- mallocBytes arrowArraySize+ childAArr <- mallocArray ncols :: IO (Ptr (Ptr ArrowArray))+ zipWithM_ (pokeElemOff childAArr) [0 ..] childAPtrs+ topBufArr <- mallocArray 1 :: IO (Ptr (Ptr ()))+ pokeElemOff topBufArr 0 nullPtr+ topArray `at` _arrayLength $ (fromIntegral nRows :: Int64)+ topArray `at` _arrayNullCount $ (0 :: Int64)+ topArray `at` _arrayOffset $ (0 :: Int64)+ topArray `at` _arrayNBuffers $ (1 :: Int64)+ topArray `at` _arrayNChildren $ (fromIntegral ncols :: Int64)+ topArray `at` _arrayBuffers $ topBufArr+ topArray `at` _arrayChildren $ childAArr+ topArray `at` _arrayDictionary $ (nullPtr :: Ptr ())+ topArray `at` _arrayRelease $ pReleaseArray+ -- Same reasoning as cleanupS: Arrow C++ manages children independently.+ cleanupA <- newStablePtr $ do+ free childAArr+ free topBufArr+ free topArray -- free our original mallocBytes allocation+ topArray `at` _arrayPrivateData $ castStablePtrToPtr cleanupA++ return (topSchema, topArray)++{- | Import an Arrow RecordBatch from raw C Data Interface pointers.+ Copies all data into GC-managed Haskell vectors, then calls the+ producer's release callbacks.+-}+arrowToDataframe :: Ptr () -> Ptr () -> IO DataFrame+arrowToDataframe rawSchema rawArray = do+ let schemaPtr = castPtr rawSchema :: Ptr ArrowSchema+ arrayPtr = castPtr rawArray :: Ptr ArrowArray+ nCols <- readAt schemaPtr _schemaNChildren :: IO Int64+ childSArr <- readAt schemaPtr _schemaChildren :: IO (Ptr (Ptr ArrowSchema))+ childAArr <- readAt arrayPtr _arrayChildren :: IO (Ptr (Ptr ArrowArray))+ cols <- forM [0 .. fromIntegral nCols - 1] $ \i -> do+ cs <- peekElemOff childSArr i+ ca <- peekElemOff childAArr i+ readArrowColumn cs ca+ -- Call producer's release callbacks after all data has been copied.+ relA <- readAt arrayPtr _arrayRelease :: IO (FunPtr (Ptr ArrowArray -> IO ()))+ when (relA /= nullFunPtr) $ callRelArray relA arrayPtr+ relS <-+ readAt schemaPtr _schemaRelease :: IO (FunPtr (Ptr ArrowSchema -> IO ()))+ when (relS /= nullFunPtr) $ callRelSchema relS schemaPtr+ return $ fromNamedColumns cols++readArrowColumn :: Ptr ArrowSchema -> Ptr ArrowArray -> IO (T.Text, Column)+readArrowColumn schemaPtr arrayPtr = do+ fmtStr <- (readAt schemaPtr _schemaFormat :: IO CString) >>= peekCString+ nameStr <- (readAt schemaPtr _schemaName :: IO CString) >>= peekCString+ let name = T.pack nameStr+ len <- readAt arrayPtr _arrayLength :: IO Int64+ nullCnt <- readAt arrayPtr _arrayNullCount :: IO Int64+ bufArr <- readAt arrayPtr _arrayBuffers :: IO (Ptr (Ptr ()))+ let n = fromIntegral len+ col <- case fmtStr of+ "l" -> readInt64Col n nullCnt bufArr+ "i" -> readInt32Col n nullCnt bufArr+ "g" -> readFloat64Col n nullCnt bufArr+ "f" -> readFloat32Col n nullCnt bufArr+ "U" -> readLargeUtf8Col n nullCnt bufArr+ "u" -> readUtf8Col n nullCnt bufArr+ _ ->+ error $+ "DataFrame.IO.Arrow.readArrowColumn: unsupported format '"+ ++ fmtStr+ ++ "' for column '"+ ++ nameStr+ ++ "'"+ return (name, col)++readInt64Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column+readInt64Col n nullCnt bufArr = do+ bitmapVoid <- peekElemOff bufArr 0+ dataVoid <- peekElemOff bufArr 1+ let dataPtr = castPtr dataVoid :: Ptr Int64+ if nullCnt > 0+ then do+ let bitmapPtr = castPtr bitmapVoid :: Ptr Word8+ bm <- readArrowBitmap bitmapPtr n+ vec <- VU.generateM n $ \i -> fmap fromIntegral (peekElemOff dataPtr i :: IO Int64)+ return $ UnboxedColumn (Just bm) (vec :: VU.Vector Int)+ else do+ vec <- VU.generateM n $ \i -> fmap fromIntegral (peekElemOff dataPtr i :: IO Int64)+ return $ UnboxedColumn Nothing (vec :: VU.Vector Int)++readInt32Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column+readInt32Col n nullCnt bufArr = do+ bitmapVoid <- peekElemOff bufArr 0+ dataVoid <- peekElemOff bufArr 1+ let dataPtr = castPtr dataVoid :: Ptr Int32+ if nullCnt > 0+ then do+ let bitmapPtr = castPtr bitmapVoid :: Ptr Word8+ bm <- readArrowBitmap bitmapPtr n+ vec <- VU.generateM n $ \i -> fmap fromIntegral (peekElemOff dataPtr i :: IO Int32)+ return $ UnboxedColumn (Just bm) (vec :: VU.Vector Int)+ else do+ vec <- VU.generateM n $ \i -> fmap fromIntegral (peekElemOff dataPtr i :: IO Int32)+ return $ UnboxedColumn Nothing (vec :: VU.Vector Int)++readFloat64Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column+readFloat64Col n nullCnt bufArr = do+ bitmapVoid <- peekElemOff bufArr 0+ dataVoid <- peekElemOff bufArr 1+ let dataPtr = castPtr dataVoid :: Ptr Double+ if nullCnt > 0+ then do+ let bitmapPtr = castPtr bitmapVoid :: Ptr Word8+ bm <- readArrowBitmap bitmapPtr n+ vec <- VU.generateM n (peekElemOff dataPtr)+ return $ UnboxedColumn (Just bm) (vec :: VU.Vector Double)+ else do+ vec <- VU.generateM n (peekElemOff dataPtr)+ return $ UnboxedColumn Nothing (vec :: VU.Vector Double)++readFloat32Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column+readFloat32Col n nullCnt bufArr = do+ bitmapVoid <- peekElemOff bufArr 0+ dataVoid <- peekElemOff bufArr 1+ let dataPtr = castPtr dataVoid :: Ptr Float+ if nullCnt > 0+ then do+ let bitmapPtr = castPtr bitmapVoid :: Ptr Word8+ bm <- readArrowBitmap bitmapPtr n+ vec <- VU.generateM n $ \i -> fmap (realToFrac :: Float -> Double) (peekElemOff dataPtr i)+ return $ UnboxedColumn (Just bm) (vec :: VU.Vector Double)+ else do+ vec <- VU.generateM n $ \i -> fmap (realToFrac :: Float -> Double) (peekElemOff dataPtr i)+ return $ UnboxedColumn Nothing (vec :: VU.Vector Double)++-- | Read a large_string (format "U") column with int64 offsets.+readLargeUtf8Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column+readLargeUtf8Col n nullCnt bufArr = do+ bitmapVoid <- peekElemOff bufArr 0+ offsetVoid <- peekElemOff bufArr 1+ charVoid <- peekElemOff bufArr 2+ let offsetPtr = castPtr offsetVoid :: Ptr Int64+ charPtr = castPtr charVoid :: Ptr Word8+ let readText i = do+ start <- fromIntegral <$> peekElemOff offsetPtr i+ end <- fromIntegral <$> peekElemOff offsetPtr (i + 1)+ TE.decodeUtf8+ <$> BS.packCStringLen (castPtr (charPtr `plusPtr` start), end - start)+ if nullCnt > 0+ then do+ let bitmapPtr = castPtr bitmapVoid :: Ptr Word8+ bm <- readArrowBitmap bitmapPtr n+ vec <- V.generateM n readText+ return $ BoxedColumn (Just bm) vec+ else do+ vec <- V.generateM n readText+ return $ BoxedColumn Nothing vec++-- | Read a utf8 (format "u") column with int32 offsets.+readUtf8Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column+readUtf8Col n nullCnt bufArr = do+ bitmapVoid <- peekElemOff bufArr 0+ offsetVoid <- peekElemOff bufArr 1+ charVoid <- peekElemOff bufArr 2+ let offsetPtr = castPtr offsetVoid :: Ptr Int32+ charPtr = castPtr charVoid :: Ptr Word8+ readText i = do+ start <- fromIntegral <$> peekElemOff offsetPtr i+ end <- fromIntegral <$> peekElemOff offsetPtr (i + 1)+ TE.decodeUtf8+ <$> BS.packCStringLen (castPtr (charPtr `plusPtr` start), end - start)+ if nullCnt > 0+ then do+ let bitmapPtr = castPtr bitmapVoid :: Ptr Word8+ bm <- readArrowBitmap bitmapPtr n+ vec <- V.generateM n readText+ return $ BoxedColumn (Just bm) vec+ else do+ vec <- V.generateM n readText+ return $ BoxedColumn Nothing vec
+ ffi/DataFrame/IR.hs view
@@ -0,0 +1,474 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Intermediate Representation for DataFrame query plans.+ JSON-decodable plan tree + interpreter.+-}+module DataFrame.IR (+ PlanNode (..),+ AggSpec (..),+ executePlan,+) where++import Data.Aeson (FromJSON (..), withObject, (.:))+import qualified Data.Aeson as Aeson+import Data.Aeson.Types (Parser)+import qualified Data.ByteString as BS+import Data.Int (Int16, Int32, Int64, Int8)+import qualified Data.Text as T+import Data.Type.Equality (+ TestEquality (testEquality),+ type (:~:) (Refl),+ type (:~~:) (HRefl),+ )+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import Data.Word (Word16, Word32, Word64, Word8)+import Foreign (wordPtrToPtr)+import Type.Reflection (SomeTypeRep (..), eqTypeRep, typeRep)++import DataFrame.Functions (count, mean, meanMaybe, sumMaybe)+import qualified DataFrame.Functions as Functions+import DataFrame.IO.Arrow (arrowToDataframe)+import DataFrame.IO.CSV (+ CsvReader,+ defaultReadOptions,+ readSeparated,+ readTsv,+ writeCsv,+ )+import DataFrame.IO.JSON (readJSON)+import qualified DataFrame.IO.Parquet as Parquet+import DataFrame.IR.ExprJson (SomeExpr (..), decodeExprAny, decodeExprAt)+import DataFrame.Internal.Column (Column (..), Columnable)+import DataFrame.Internal.DataFrame (DataFrame, unsafeGetColumn)+import DataFrame.Internal.Expression (Expr (..), NamedExpr)+import qualified DataFrame.Lazy as Lazy+import DataFrame.Operations.Aggregation (aggregate, distinct, groupBy)+import DataFrame.Operations.Core (insertVector, renameMany)+import DataFrame.Operations.Join (JoinType (..), join)+import DataFrame.Operations.Permutation (SortOrder (..), sortBy)+import qualified DataFrame.Operations.Statistics as Stats+import DataFrame.Operations.Subset (exclude, filterWhere, range, select)+import qualified DataFrame.Operations.Subset as Subset+import DataFrame.Operations.Transformations (derive)+import DataFrame.Operators ((.=))+import DataFrame.Schema (Schema, makeSchema, schemaType)++-- ---------------------------------------------------------------------------+-- IR types+-- ---------------------------------------------------------------------------++data AggSpec = AggSpec+ { aggName :: T.Text+ , aggFn :: T.Text+ , aggCol :: T.Text+ }+ deriving (Show)++data PlanNode+ = ReadCsv FilePath+ | ReadTsv FilePath+ | -- | schema_addr array_addr+ FromArrow Word64 Word64+ | Select [T.Text] PlanNode+ | GroupBy [T.Text] [AggSpec] PlanNode+ | Sort [T.Text] Bool PlanNode+ | Limit Int PlanNode+ | -- | predicate JSON, child plan+ Filter Aeson.Value PlanNode+ | -- | column name, expr JSON, child plan+ Derive T.Text Aeson.Value PlanNode+ | Exclude [T.Text] PlanNode+ | Rename [(T.Text, T.Text)] PlanNode+ | Distinct PlanNode+ | TakeLast Int PlanNode+ | Drop Int PlanNode+ | DropLast Int PlanNode+ | Range Int Int PlanNode+ | -- | joinType ("inner"|"left"|"right"|"outer"), shared key columns, left, right+ Join T.Text [T.Text] PlanNode PlanNode+ | Describe PlanNode+ | -- | first column, second column, child plan+ Correlation T.Text T.Text PlanNode+ | Frequencies T.Text PlanNode+ | ReadParquet FilePath+ | ReadJson FilePath+ | -- | path, separator (single character), child plan; runs as a terminal op+ WriteCsv FilePath PlanNode+ | {- | path, schema (column name → type-tag map). Reads via the lazy+ engine with predicate / projection pushdown; subsequent ops+ currently still run eagerly on the materialized result.+ -}+ ScanCsv FilePath [(T.Text, T.Text)]+ | ScanParquet FilePath [(T.Text, T.Text)]+ deriving (Show)++-- ---------------------------------------------------------------------------+-- JSON decoding+-- ---------------------------------------------------------------------------++instance FromJSON AggSpec where+ parseJSON = withObject "AggSpec" $ \o ->+ AggSpec+ <$> o .: "name"+ <*> o .: "agg"+ <*> o .: "col"++instance FromJSON PlanNode where+ parseJSON = withObject "PlanNode" $ \o -> do+ op <- o .: "op" :: Parser T.Text+ case op of+ "ReadCsv" -> ReadCsv <$> o .: "path"+ "ReadTsv" -> ReadTsv <$> o .: "path"+ "FromArrow" -> FromArrow <$> o .: "schema" <*> o .: "array"+ "Select" -> Select <$> o .: "cols" <*> o .: "input"+ "GroupBy" -> GroupBy <$> o .: "keys" <*> o .: "aggregations" <*> o .: "input"+ "Sort" -> Sort <$> o .: "cols" <*> o .: "ascending" <*> o .: "input"+ "Limit" -> Limit <$> o .: "n" <*> o .: "input"+ "Filter" -> Filter <$> o .: "predicate" <*> o .: "input"+ "Derive" -> Derive <$> o .: "name" <*> o .: "expr" <*> o .: "input"+ "Exclude" -> Exclude <$> o .: "cols" <*> o .: "input"+ "Rename" -> Rename <$> o .: "pairs" <*> o .: "input"+ "Distinct" -> Distinct <$> o .: "input"+ "TakeLast" -> TakeLast <$> o .: "n" <*> o .: "input"+ "Drop" -> Drop <$> o .: "n" <*> o .: "input"+ "DropLast" -> DropLast <$> o .: "n" <*> o .: "input"+ "Range" -> Range <$> o .: "start" <*> o .: "end" <*> o .: "input"+ "Join" ->+ Join+ <$> o .: "how"+ <*> o .: "on"+ <*> o .: "left"+ <*> o .: "right"+ "Describe" -> Describe <$> o .: "input"+ "Correlation" ->+ Correlation+ <$> o .: "first"+ <*> o .: "second"+ <*> o .: "input"+ "Frequencies" -> Frequencies <$> o .: "col" <*> o .: "input"+ "ReadParquet" -> ReadParquet <$> o .: "path"+ "ReadJson" -> ReadJson <$> o .: "path"+ "WriteCsv" -> WriteCsv <$> o .: "path" <*> o .: "input"+ "ScanCsv" -> ScanCsv <$> o .: "path" <*> o .: "schema"+ "ScanParquet" -> ScanParquet <$> o .: "path" <*> o .: "schema"+ _ -> fail $ "DataFrame.IR: unknown op: " ++ T.unpack op++executePlan :: CsvReader -> PlanNode -> IO DataFrame+executePlan _reader (ReadCsv path) =+ readSeparated defaultReadOptions path+executePlan _reader (ReadTsv path) =+ readTsv path+executePlan _reader (FromArrow schemaAddr arrayAddr) =+ arrowToDataframe+ (wordPtrToPtr (fromIntegral schemaAddr))+ (wordPtrToPtr (fromIntegral arrayAddr))+executePlan reader (Select cols node) =+ select cols <$> executePlan reader node+executePlan reader (GroupBy keys aggs node) = do+ df <- executePlan reader node+ nes <- mapM (buildNamedExpr df) aggs+ return $ aggregate nes (groupBy keys df)+executePlan reader (Sort cols ascending node) = do+ df <- executePlan reader node+ let orders = map (\c -> mkSortOrder ascending c (unsafeGetColumn c df)) cols+ return $ sortBy orders df+executePlan reader (Limit k node) =+ Subset.take k <$> executePlan reader node+executePlan reader (Filter predJson node) = do+ df <- executePlan reader node+ case decodeExprAt @Bool predJson of+ Right pred_ -> return $ filterWhere pred_ df+ Left err -> ioError $ userError $ "DataFrame.IR.Filter: " <> err+executePlan reader (Derive name exprJson node) = do+ df <- executePlan reader node+ case decodeExprAny exprJson of+ Right (SomeExpr _trep expr) -> return $ derive name expr df+ Left err -> ioError $ userError $ "DataFrame.IR.Derive: " <> err+executePlan reader (Exclude cols node) =+ exclude cols <$> executePlan reader node+executePlan reader (Rename pairs node) =+ renameMany pairs <$> executePlan reader node+executePlan reader (Distinct node) =+ distinct <$> executePlan reader node+executePlan reader (TakeLast n node) =+ Subset.takeLast n <$> executePlan reader node+executePlan reader (Drop n node) =+ Subset.drop n <$> executePlan reader node+executePlan reader (DropLast n node) =+ Subset.dropLast n <$> executePlan reader node+executePlan reader (Range start end node) =+ range (start, end) <$> executePlan reader node+executePlan reader (Join how on leftPlan rightPlan) = do+ left <- executePlan reader leftPlan+ right <- executePlan reader rightPlan+ jt <- case how of+ "inner" -> return INNER+ "left" -> return LEFT+ "right" -> return RIGHT+ "outer" -> return FULL_OUTER+ "full_outer" -> return FULL_OUTER+ other ->+ ioError . userError $+ "DataFrame.IR.Join: unknown join type " <> T.unpack other+ return $ join jt on left right+executePlan reader (Describe node) = Stats.summarize <$> executePlan reader node+executePlan reader (Correlation a b node) = do+ df <- executePlan reader node+ let r = Stats.correlation a b df+ valueCol = case r of+ Just d -> V.singleton d+ Nothing -> V.singleton (0 / 0 :: Double)+ return $+ insertVector "first" (V.singleton a) $+ insertVector "second" (V.singleton b) $+ insertVector "correlation" valueCol mempty+executePlan reader (Frequencies colName node) = do+ df <- executePlan reader node+ runFrequencies colName df+executePlan _reader (ReadParquet path) = Parquet.readParquet path+executePlan _reader (ReadJson path) = readJSON path+executePlan reader (WriteCsv path node) = do+ df <- executePlan reader node+ writeCsv path df+ return df+executePlan reader (ScanCsv path schemaPairs) = do+ schema <- buildSchema schemaPairs+ Lazy.runDataFrame (Lazy.scanCsvWith reader schema (T.pack path))+executePlan _reader (ScanParquet path schemaPairs) = do+ schema <- buildSchema schemaPairs+ Lazy.runDataFrame (Lazy.scanParquet schema (T.pack path))++{- | Build a SortOrder from a column's runtime type.+Uses type dispatch to recover Ord for known column types.+-}+mkSortOrder :: Bool -> T.Text -> Column -> SortOrder+mkSortOrder isAsc name col = dispatchType (columnTypeRep col)+ where+ columnTypeRep :: Column -> SomeTypeRep+ columnTypeRep (UnboxedColumn _ (_ :: VU.Vector a)) = SomeTypeRep (typeRep @a)+ columnTypeRep (BoxedColumn _ (_ :: V.Vector a)) = SomeTypeRep (typeRep @a)+ columnTypeRep (PackedText _ _) = SomeTypeRep (typeRep @T.Text)+ mk :: (Columnable a, Ord a) => Expr a -> SortOrder+ mk = if isAsc then Asc else Desc+ dispatchType (SomeTypeRep tr)+ | Just HRefl <- eqTypeRep tr (typeRep @Int) = mk (Col @Int name)+ | Just HRefl <- eqTypeRep tr (typeRep @Int8) = mk (Col @Int8 name)+ | Just HRefl <- eqTypeRep tr (typeRep @Int16) = mk (Col @Int16 name)+ | Just HRefl <- eqTypeRep tr (typeRep @Int32) = mk (Col @Int32 name)+ | Just HRefl <- eqTypeRep tr (typeRep @Int64) = mk (Col @Int64 name)+ | Just HRefl <- eqTypeRep tr (typeRep @Word) = mk (Col @Word name)+ | Just HRefl <- eqTypeRep tr (typeRep @Word8) = mk (Col @Word8 name)+ | Just HRefl <- eqTypeRep tr (typeRep @Word16) = mk (Col @Word16 name)+ | Just HRefl <- eqTypeRep tr (typeRep @Word32) = mk (Col @Word32 name)+ | Just HRefl <- eqTypeRep tr (typeRep @Word64) = mk (Col @Word64 name)+ | Just HRefl <- eqTypeRep tr (typeRep @Integer) = mk (Col @Integer name)+ | Just HRefl <- eqTypeRep tr (typeRep @Double) = mk (Col @Double name)+ | Just HRefl <- eqTypeRep tr (typeRep @Float) = mk (Col @Float name)+ | Just HRefl <- eqTypeRep tr (typeRep @Bool) = mk (Col @Bool name)+ | Just HRefl <- eqTypeRep tr (typeRep @Char) = mk (Col @Char name)+ | Just HRefl <- eqTypeRep tr (typeRep @T.Text) = mk (Col @T.Text name)+ | Just HRefl <- eqTypeRep tr (typeRep @String) = mk (Col @String name)+ | Just HRefl <- eqTypeRep tr (typeRep @BS.ByteString) =+ mk (Col @BS.ByteString name)+ | otherwise = error $ "mkSortOrder: unsupported column type: " ++ show tr++-- | Dispatch aggregation by fn name and runtime column type.+buildNamedExpr :: DataFrame -> AggSpec -> IO NamedExpr+buildNamedExpr df (AggSpec name fn colName) =+ case fn of+ "count" -> countExpr name colName (unsafeGetColumn colName df)+ "sum" -> sumExpr name colName (unsafeGetColumn colName df)+ "mean" -> meanExpr name colName (unsafeGetColumn colName df)+ "min" -> minMaxExpr Functions.minimum name colName (unsafeGetColumn colName df)+ "max" -> minMaxExpr Functions.maximum name colName (unsafeGetColumn colName df)+ "median" -> doubleStatExpr Functions.median name colName (unsafeGetColumn colName df)+ "variance" -> doubleStatExpr Functions.variance name colName (unsafeGetColumn colName df)+ "std" -> doubleStatExpr stdDevExpr name colName (unsafeGetColumn colName df)+ other ->+ ioError $+ userError $+ "DataFrame.IR: unknown aggregation '" ++ T.unpack other ++ "'"++-- | Variance → standard deviation; sqrt of the underlying variance Expr.+stdDevExpr :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double+stdDevExpr e = sqrt (Functions.variance e)++-- | Build a 'Schema' from a list of (col, type-tag) pairs sent over the wire.+buildSchema :: [(T.Text, T.Text)] -> IO Schema+buildSchema pairs = do+ sch <- mapM resolve pairs+ return (makeSchema sch)+ where+ resolve (name, tag) = case tag of+ "int" -> return (name, schemaType @Int)+ "int8" -> return (name, schemaType @Int8)+ "int16" -> return (name, schemaType @Int16)+ "int32" -> return (name, schemaType @Int32)+ "int64" -> return (name, schemaType @Int64)+ "double" -> return (name, schemaType @Double)+ "float" -> return (name, schemaType @Float)+ "bool" -> return (name, schemaType @Bool)+ "text" -> return (name, schemaType @T.Text)+ "string" -> return (name, schemaType @String)+ other ->+ ioError . userError $+ "DataFrame.IR.buildSchema: unsupported schema type tag '"+ ++ T.unpack other+ ++ "' for column '"+ ++ T.unpack name+ ++ "'"++-- | Dispatch 'frequencies' on the column's runtime element type.+runFrequencies :: T.Text -> DataFrame -> IO DataFrame+runFrequencies colName df = dispatchType (columnTypeRep (unsafeGetColumn colName df))+ where+ columnTypeRep :: Column -> SomeTypeRep+ columnTypeRep (UnboxedColumn _ (_ :: VU.Vector a)) = SomeTypeRep (typeRep @a)+ columnTypeRep (BoxedColumn _ (_ :: V.Vector a)) = SomeTypeRep (typeRep @a)+ columnTypeRep (PackedText _ _) = SomeTypeRep (typeRep @T.Text)++ fr :: forall a. (Columnable a, Ord a) => IO DataFrame+ fr = return $ Stats.frequencies (Col @a colName) df++ dispatchType :: SomeTypeRep -> IO DataFrame+ dispatchType (SomeTypeRep tr)+ | Just HRefl <- eqTypeRep tr (typeRep @Int) = fr @Int+ | Just HRefl <- eqTypeRep tr (typeRep @Int8) = fr @Int8+ | Just HRefl <- eqTypeRep tr (typeRep @Int16) = fr @Int16+ | Just HRefl <- eqTypeRep tr (typeRep @Int32) = fr @Int32+ | Just HRefl <- eqTypeRep tr (typeRep @Int64) = fr @Int64+ | Just HRefl <- eqTypeRep tr (typeRep @Word) = fr @Word+ | Just HRefl <- eqTypeRep tr (typeRep @Word8) = fr @Word8+ | Just HRefl <- eqTypeRep tr (typeRep @Word16) = fr @Word16+ | Just HRefl <- eqTypeRep tr (typeRep @Word32) = fr @Word32+ | Just HRefl <- eqTypeRep tr (typeRep @Word64) = fr @Word64+ | Just HRefl <- eqTypeRep tr (typeRep @Integer) = fr @Integer+ | Just HRefl <- eqTypeRep tr (typeRep @Double) = fr @Double+ | Just HRefl <- eqTypeRep tr (typeRep @Float) = fr @Float+ | Just HRefl <- eqTypeRep tr (typeRep @Bool) = fr @Bool+ | Just HRefl <- eqTypeRep tr (typeRep @Char) = fr @Char+ | Just HRefl <- eqTypeRep tr (typeRep @T.Text) = fr @T.Text+ | Just HRefl <- eqTypeRep tr (typeRep @String) = fr @String+ | otherwise =+ ioError . userError $+ "DataFrame.IR.Frequencies: unsupported column type for '"+ ++ T.unpack colName+ ++ "'"++countExpr :: T.Text -> T.Text -> Column -> IO NamedExpr+countExpr name colName (UnboxedColumn Nothing (_ :: VU.Vector a)) = return $ name .= count (Col @a colName)+countExpr name colName (UnboxedColumn (Just _) (_ :: VU.Vector a)) = return $ name .= count (Col @(Maybe a) colName)+countExpr name colName (BoxedColumn Nothing (_ :: V.Vector a)) = return $ name .= count (Col @a colName)+countExpr name colName (BoxedColumn (Just _) (_ :: V.Vector a)) = return $ name .= count (Col @(Maybe a) colName)+countExpr name colName (PackedText Nothing _) = return $ name .= count (Col @T.Text colName)+countExpr name colName (PackedText (Just _) _) = return $ name .= count (Col @(Maybe T.Text) colName)++sumExpr :: T.Text -> T.Text -> Column -> IO NamedExpr+sumExpr name colName (UnboxedColumn Nothing (_ :: VU.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+ return $ name .= Functions.sum (Col @Int colName)+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+ return $ name .= Functions.sum (Col @Double colName)+sumExpr name colName (UnboxedColumn (Just _) (_ :: VU.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+ return $ name .= sumMaybe (Col @(Maybe Int) colName)+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+ return $ name .= sumMaybe (Col @(Maybe Double) colName)+sumExpr name colName (BoxedColumn Nothing (_ :: V.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+ return $ name .= Functions.sum (Col @Int colName)+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+ return $ name .= Functions.sum (Col @Double colName)+sumExpr name colName (BoxedColumn (Just _) (_ :: V.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+ return $ name .= sumMaybe (Col @(Maybe Int) colName)+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+ return $ name .= sumMaybe (Col @(Maybe Double) colName)+sumExpr _ colName _ =+ ioError $+ userError $+ "DataFrame.IR: sum: unsupported column type for '" ++ T.unpack colName ++ "'"++meanExpr :: T.Text -> T.Text -> Column -> IO NamedExpr+meanExpr name colName (UnboxedColumn Nothing (_ :: VU.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+ return $ name .= mean (Col @Int colName)+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+ return $ name .= mean (Col @Double colName)+meanExpr name colName (UnboxedColumn (Just _) (_ :: VU.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+ return $ name .= meanMaybe (Col @(Maybe Double) colName)+ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+ return $ name .= meanMaybe (Col @(Maybe Int) colName)+meanExpr name colName (BoxedColumn Nothing (_ :: V.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+ return $ name .= mean (Col @Double colName)+meanExpr name colName (BoxedColumn (Just _) (_ :: V.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+ return $ name .= meanMaybe (Col @(Maybe Double) colName)+ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+ return $ name .= meanMaybe (Col @(Maybe Int) colName)+meanExpr _ colName _ =+ ioError $+ userError $+ "DataFrame.IR: mean: unsupported column type for '" ++ T.unpack colName ++ "'"++-- | min / max — preserve column type, require Ord.+minMaxExpr ::+ (forall a. (Columnable a, Ord a) => Expr a -> Expr a) ->+ T.Text ->+ T.Text ->+ Column ->+ IO NamedExpr+minMaxExpr op name colName (UnboxedColumn Nothing (_ :: VU.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+ return $ name .= op (Col @Int colName)+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+ return $ name .= op (Col @Double colName)+ | Just Refl <- testEquality (typeRep @a) (typeRep @Float) =+ return $ name .= op (Col @Float colName)+minMaxExpr op name colName (BoxedColumn Nothing (_ :: V.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @T.Text) =+ return $ name .= op (Col @T.Text colName)+ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+ return $ name .= op (Col @Int colName)+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+ return $ name .= op (Col @Double colName)+minMaxExpr _ _ colName _ =+ ioError . userError $+ "DataFrame.IR: min/max: unsupported column type for '"+ ++ T.unpack colName+ ++ "'"++-- | median / variance / std — return Double, require Real + Unbox.+doubleStatExpr ::+ (forall a. (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double) ->+ T.Text ->+ T.Text ->+ Column ->+ IO NamedExpr+doubleStatExpr op name colName (UnboxedColumn Nothing (_ :: VU.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+ return $ name .= op (Col @Int colName)+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+ return $ name .= op (Col @Double colName)+ | Just Refl <- testEquality (typeRep @a) (typeRep @Float) =+ return $ name .= op (Col @Float colName)+doubleStatExpr op name colName (BoxedColumn Nothing (_ :: V.Vector a))+ | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+ return $ name .= op (Col @Int colName)+ | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+ return $ name .= op (Col @Double colName)+doubleStatExpr _ _ colName _ =+ ioError . userError $+ "DataFrame.IR: median/variance/std: unsupported column type for '"+ ++ T.unpack colName+ ++ "'"
src/DataFrame.hs view
@@ -1,206 +1,20 @@+{-# LANGUAGE CPP #-}+ {- | Module : DataFrame-Copyright : (c) 2025+Copyright : (c) 2024 - 2026 Michael Chavinda 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:--We provide a script that imports the core functionality and defines helpful-macros for writing safe code.--@-\$ cabal update-\$ cabal install dataframe-\$ dataframe-Configuring library for fake-package-0...-Warning: No exposed modules-GHCi, version 9.6.7: https:\/\/www.haskell.org\/ghc\/ :? for help-Loaded GHCi configuration from \/tmp\/cabal-repl.-242816\/setcwd.ghci-========================================- 📦Dataframe-========================================--✨ Modules were automatically imported.--💡 Use prefix 'D' for core functionality.- ● E.g. D.readCsv \"\/path\/to\/file\"-💡 Use prefix 'F' for expression functions.- ● E.g. F.sum (F.col \@Int \"value\")--✅ Ready.-Loaded GHCi configuration from ./dataframe.ghci-ghci>-@--= 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--------------------------------------------------------------------------------------------------------------- Column Name | # Non-null Values | # Null Values | # Partially parsed | # Unique Values | Type---------------------|-------------------|---------------|--------------------|-----------------|-------------- Text | Int | Int | Int | Int | Text---------------------|-------------------|---------------|--------------------|-----------------|-------------- ocean_proximity | 20640 | 0 | 0 | 5 | Text- median_house_value | 20640 | 0 | 0 | 3842 | Double- median_income | 20640 | 0 | 0 | 12928 | Double- households | 20640 | 0 | 0 | 1815 | Double- population | 20640 | 0 | 0 | 3888 | Double- total_bedrooms | 20640 | 0 | 0 | 1924 | Maybe Double- total_rooms | 20640 | 0 | 0 | 5926 | Double- housing_median_age | 20640 | 0 | 0 | 52 | Double- latitude | 20640 | 0 | 0 | 862 | Double- longitude | 20640 | 0 | 0 | 844 | Double---- 2) Project & filter-ghci> :declareColumns df-ghci> df1 = D.filterWhere (ocean_proximity .== \"ISLAND\") df0 D.|> D.select [F.name median_house_value, F.name median_income, F.name ocean_proximity]---- 3) Add a derived column using the expression DSL--- (col types are explicit via TypeApplications)-ghci> df2 = D.derive "rooms_per_household" (total_rooms / households) df0---- 4) Group + aggregate-ghci> import DataFrame.Operators-ghci> let grouped = D.groupBy ["ocean_proximity"] df0-ghci> let summary =- D.aggregate- [ F.maximum median_house_value \`as\` "max_house_value"]- grouped-ghci> D.take 5 summary------------------------------------ ocean_proximity | max_house_value------------------|----------------- Text | Double------------------|----------------- <1H OCEAN | 500001.0- INLAND | 500001.0- ISLAND | 450000.0- NEAR BAY | 500001.0- NEAR OCEAN | 500001.0-@--== Simple operations (cheat sheet)-Most users only need a handful of verbs:--__I/O__-- * @D.readCsv :: FilePath -> IO DataFrame@- * @D.readTsv :: FilePath -> IO DataFrame@- * @D.writeCsv :: FilePath -> DataFrame -> IO ()@- * @D.readParquet :: FilePath -> IO DataFrame@- * @D.readParquetWithOpts :: ParquetReadOptions -> FilePath -> IO DataFrame@- * @D.readParquetFiles :: FilePath -> IO DataFrame@- * @D.readParquetFilesWithOpts :: ParquetReadOptions -> 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 :: Expr a -> (a -> Bool) -> DataFrame -> DataFrame@- * @D.filterWhere :: Expr 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 :: [NamedExpr] -> GroupedDataFrame -> DataFrame@--__Joins__-- * @D.innerJoin \/ D.leftJoin \/ D.rightJoin \/ D.fullOuterJoin@--== 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: ':declareColumns'--Use @:declareColumns <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> :declareColumns 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 * 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)@.+Batteries-included entry point for @dataframe@: re-exports the most commonly+used pieces for GHCi and scripts. Use the @D.@ prefix for core table operations+and @F.@ for the expression DSL. -} module DataFrame ( -- * Core data structures- module Dataframe,- module Column,- module Row,- module Expression,+ module CoreTypes, -- * Operator symbols. module Operators,@@ -213,29 +27,61 @@ -- * Types module Schema,+#ifdef WITH_TH+ module SchemaTH,+#endif -- * I/O+#ifdef WITH_CSV module CSV,- module UnstableCSV,+#endif+#ifdef WITH_PARQUET module Parquet,+#endif+ module JSON, + -- * Lazy query engine+#ifdef WITH_LAZY+ module Lazy,+#endif++ -- * Feature synthesis & decision trees+ module Synthesis,+ module DecisionTree,++ -- * Type conversion+ module Typing,+ -- * Operations module Subset, module Transformations, module Aggregation, module Permutation, module Merge,+ module SetOps, module Join, module Statistics, -- * Errors module Errors, + -- * Record bridge+ module Record,++ -- * Template Haskell column-binding splices+#ifdef WITH_TH+ module TH,+#endif+ -- * Plotting module Plot, ) where +-- DecisionTree's own `percentile`/`percentiles` are hidden here; the public+-- versions come from Operations.Statistics / DataFrame.Synthesis, avoiding+-- ambiguous-export errors.+import DataFrame.DecisionTree as DecisionTree import DataFrame.Display as Display ( DisplayOptions (..), defaultDisplayOptions,@@ -243,18 +89,31 @@ ) import DataFrame.Display.Terminal.Plot as Plot import DataFrame.Errors as Errors+#ifdef WITH_CSV import DataFrame.IO.CSV as CSV ( HeaderSpec (..), ReadOptions (..), TypeSpec (..), defaultReadOptions,+ fromCsv,+ fromCsvBytes, readCsv, readCsvWithOpts,+ readCsvWithSchema, readSeparated, readTsv, writeCsv, writeSeparated, )+#endif+import DataFrame.IO.JSON as JSON (+ readJSON,+ readJSONEither,+ )+-- Marks the dataframe-expr-serializer dependency as used (the codec modules are+-- surfaced to consumers via this library's @reexported-modules@).+import DataFrame.Expr.Serialize ()+#ifdef WITH_PARQUET import DataFrame.IO.Parquet as Parquet ( ParquetReadOptions (..), defaultParquetReadOptions,@@ -263,56 +122,91 @@ readParquetFilesWithOpts, readParquetWithOpts, )-import DataFrame.IO.Unstable.CSV as UnstableCSV (- fastReadCsvUnstable,- fastReadTsvUnstable,- readCsvUnstable,- readTsvUnstable,- )-import DataFrame.Internal.Column as Column (+#endif+import DataFrame.Core as CoreTypes (+ Any, Column,+ Columnable,+ Columnable',+ DataFrame,+ Expr,+ GroupedDataFrame,+ NamedExpr,+ Row,+ TruncateConfig (..),+ columnNames,+ defaultTruncateConfig,+ eSize,+ empty,+ fromAny, fromList,+ fromNamedColumns, fromUnboxedVector, fromVector, hasElemType, hasMissing,+ insertColumn, isNumeric,- toList,- toVector,- )-import DataFrame.Internal.DataFrame as Dataframe (- DataFrame,- GroupedDataFrame,- empty,+ mkRandom, null,- toMarkdownTable,- )-import DataFrame.Internal.Expression as Expression (Expr, prettyPrint)-import DataFrame.Internal.Row as Row (- Any,- Row,- fromAny,+ prettyPrint,+ prettyPrintWidth, rowValue, toAny,+ toCsv,+ toCsv',+ toList,+ toMarkdown,+ toMarkdown', toRowList, toRowVector,+ toSeparated,+ toVector, )-import DataFrame.Internal.Schema as Schema (+import DataFrame.Schema as Schema (+ SchemaType (..),+ makeSchema, schemaType, )+#ifdef WITH_TH+import DataFrame.Typed.TH.Records as SchemaTH (deriveSchemaFromType, deriveSchemaValues)+#endif+#ifdef WITH_LAZY+-- Re-export only the lazy engine's types and source constructors; its+-- operator surface (filter/select/derive/...) collides with the eager API,+-- so users wanting full lazy access `import qualified DataFrame.Lazy as L`.+import DataFrame.Lazy as Lazy (+ LazyDataFrame,+ fromDataFrame,+ runDataFrame,+ scanCsv,+ scanCsvWith,+ scanParquet,+ scanSeparated,+ scanSeparatedWith,+ )+#endif import DataFrame.Operations.Aggregation as Aggregation ( aggregate, distinct, groupBy, )-import DataFrame.Operations.Core as Core hiding (- ColumnInfo (..),- nulls,- partiallyParsed,- renameSafe,+import DataFrame.Operations.Core as Core+import DataFrame.Operations.Join as Join (+ JoinType (..),+ fullOuterJoin,+ innerJoin,+ join,+ leftJoin,+ rightJoin, )-import DataFrame.Operations.Join as Join import DataFrame.Operations.Merge as Merge+import DataFrame.Operations.SetOps as SetOps (+ difference,+ intersect,+ symmetricDifference,+ union,+ ) import DataFrame.Operations.Permutation as Permutation ( SortOrder (..), shuffle,@@ -335,6 +229,7 @@ summarize, variance, )+import DataFrame.Synthesis as Synthesis import DataFrame.Operations.Subset as Subset ( SelectionCriteria, byIndexRange,@@ -359,8 +254,49 @@ sample, select, selectBy,+ selectRows,+ stratifiedSample,+ stratifiedSplit, take, takeLast, )-import DataFrame.Operations.Transformations as Transformations+import DataFrame.Operations.Transformations as Transformations (+ apply,+ applyAtIndex,+ applyDouble,+ applyInt,+ applyMany,+ applyWhere,+ derive,+ deriveMany,+ deriveWithExpr,+ impute,+ safeApply,+ )+import DataFrame.Operations.Typing as Typing (+ ParseOptions (..),+ SafeReadMode (..),+ defaultParseOptions,+ effectiveSafeRead,+ parseDefaults,+ ) import DataFrame.Operators as Operators+#ifdef WITH_TH+import DataFrame.TH as TH (+ declareColumns,+#ifdef WITH_CSV_TH+ declareColumnsFromCsvFile,+ declareColumnsFromCsvWithOpts,+#endif+#ifdef WITH_PARQUET_TH+ declareColumnsFromParquetFile,+#endif+ declareColumnsWithPrefix,+ declareColumnsWithPrefix',+ )+#endif+import DataFrame.Typed.Record as Record (+ HasSchema (..),+ fromRecords,+ toRecords,+ )
− src/DataFrame/DecisionTree.hs
@@ -1,761 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.DecisionTree where--import qualified DataFrame.Functions as F-import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (DataFrame (..), unsafeGetColumn)-import DataFrame.Internal.Expression (Expr (..), eSize, getColumns)-import DataFrame.Internal.Interpreter (interpret)-import DataFrame.Internal.Statistics (percentile', percentileOrd')-import DataFrame.Internal.Types-import DataFrame.Operations.Core (columnNames, nRows)-import DataFrame.Operations.Subset (exclude, filterWhere)--import Control.Exception (throw)-import Control.Monad (guard)-import Data.Containers.ListUtils (nubOrd)-import Data.Function (on)-import Data.List (foldl', maximumBy, minimumBy, sort, sortBy)-import qualified Data.Map.Strict as M-import Data.Maybe-import qualified Data.Text as T-import Data.Type.Equality-import qualified Data.Vector as V-import qualified Data.Vector.Unboxed as VU-import Type.Reflection (typeRep)--import DataFrame.Operators--data TreeConfig = TreeConfig- { maxTreeDepth :: Int- , minSamplesSplit :: Int- , minLeafSize :: Int- , percentiles :: [Int]- , expressionPairs :: Int- , synthConfig :: SynthConfig- , taoIterations :: Int- , taoConvergenceTol :: Double- }- deriving (Eq, Show)--data SynthConfig = SynthConfig- { maxExprDepth :: Int- , boolExpansion :: Int- , disallowedCombinations :: [(T.Text, T.Text)]- , complexityPenalty :: Double- , enableStringOps :: Bool- , enableCrossCols :: Bool- , enableArithOps :: Bool- }- deriving (Eq, Show)--defaultSynthConfig :: SynthConfig-defaultSynthConfig =- SynthConfig- { maxExprDepth = 2- , boolExpansion = 2- , disallowedCombinations = []- , complexityPenalty = 0.05- , enableStringOps = True- , enableCrossCols = True- , enableArithOps = True- }--defaultTreeConfig :: TreeConfig-defaultTreeConfig =- TreeConfig- { maxTreeDepth = 4- , minSamplesSplit = 5- , minLeafSize = 1- , percentiles = [0, 10 .. 100]- , expressionPairs = 10- , synthConfig = defaultSynthConfig- , taoIterations = 10- , taoConvergenceTol = 1e-6- }--data Tree a- = Leaf !a- | Branch !(Expr Bool) !(Tree a) !(Tree a)- deriving (Eq, Show)--treeDepth :: Tree a -> Int-treeDepth (Leaf _) = 0-treeDepth (Branch _ l r) = 1 + max (treeDepth l) (treeDepth r)--treeToExpr :: (Columnable a) => Tree a -> Expr a-treeToExpr (Leaf v) = Lit v-treeToExpr (Branch cond left right) =- F.ifThenElse cond (treeToExpr left) (treeToExpr right)---- | Fit a TAO decision tree-fitDecisionTree ::- forall a.- (Columnable a) =>- TreeConfig ->- Expr a ->- DataFrame ->- Expr a-fitDecisionTree cfg (Col target) df =- let- conds =- nubOrd $- numericConditions cfg (exclude [target] df)- ++ generateConditionsOld cfg (exclude [target] df)-- initialTree = buildGreedyTree @a cfg (maxTreeDepth cfg) target conds df-- indices = V.enumFromN 0 (nRows df)-- optimizedTree = taoOptimize @a cfg target conds df indices initialTree- in- pruneExpr (treeToExpr optimizedTree)-fitDecisionTree _ expr _ = error $ "Cannot create tree for compound expression: " ++ show expr--taoOptimize ::- forall a.- (Columnable a) =>- TreeConfig ->- T.Text -> -- Target column name- [Expr Bool] -> -- Candidate conditions- DataFrame -> -- Full dataset- V.Vector Int -> -- Indices of points reaching the root- Tree a -> -- Current tree- Tree a-taoOptimize cfg target conds df rootIndices initialTree =- go 0 initialTree (computeTreeLoss @a target df rootIndices initialTree)- where- go :: Int -> Tree a -> Double -> Tree a- go iter tree prevLoss- | iter >= taoIterations cfg = pruneDead tree- | otherwise =- let- tree' = taoIteration @a cfg target conds df rootIndices tree-- newLoss = computeTreeLoss @a target df rootIndices tree'- improvement = prevLoss - newLoss- in- if improvement < taoConvergenceTol cfg- then pruneDead tree'- else go (iter + 1) tree' newLoss--taoIteration ::- forall a.- (Columnable a) =>- TreeConfig ->- T.Text ->- [Expr Bool] ->- DataFrame ->- V.Vector Int ->- Tree a ->- Tree a-taoIteration cfg target conds df rootIndices tree =- let depth = treeDepth tree- in foldl'- (optimizeDepthLevel @a cfg target conds df rootIndices)- tree- [depth, depth - 1 .. 0] -- Bottom to top--optimizeDepthLevel ::- forall a.- (Columnable a) =>- TreeConfig ->- T.Text ->- [Expr Bool] ->- DataFrame ->- V.Vector Int ->- Tree a ->- Int -> -- Target depth- Tree a-optimizeDepthLevel cfg target conds df rootIndices tree = optimizeAtDepth @a cfg target conds df rootIndices tree 0--optimizeAtDepth ::- forall a.- (Columnable a) =>- TreeConfig ->- T.Text ->- [Expr Bool] ->- DataFrame ->- V.Vector Int ->- Tree a ->- Int ->- Int ->- Tree a-optimizeAtDepth cfg target conds df indices tree currentDepth targetDepth- | currentDepth == targetDepth =- optimizeNode @a cfg target conds df indices tree- | otherwise = case tree of- Leaf v -> Leaf v- Branch cond left right ->- let- (indicesL, indicesR) = partitionIndices cond df indices- left' =- optimizeAtDepth @a- cfg- target- conds- df- indicesL- left- (currentDepth + 1)- targetDepth- right' =- optimizeAtDepth @a- cfg- target- conds- df- indicesR- right- (currentDepth + 1)- targetDepth- in- Branch cond left' right'--optimizeNode ::- forall a.- (Columnable a) =>- TreeConfig ->- T.Text ->- [Expr Bool] ->- DataFrame ->- V.Vector Int ->- Tree a ->- Tree a-optimizeNode cfg target conds df indices tree- | V.null indices = tree- | otherwise = case tree of- Leaf _ -> Leaf (majorityValueFromIndices @a target df indices)- Branch oldCond left right ->- let- newCond = findBestSplitTAO @a cfg target conds df indices left right oldCond-- (newIndicesL, newIndicesR) = partitionIndices newCond df indices- in- if V.length newIndicesL < minLeafSize cfg- || V.length newIndicesR < minLeafSize cfg- then Leaf (majorityValueFromIndices @a target df indices)- else Branch newCond left right--findBestSplitTAO ::- forall a.- (Columnable a) =>- TreeConfig ->- T.Text ->- [Expr Bool] ->- DataFrame ->- V.Vector Int ->- Tree a -> -- Left subtree (FIXED)- Tree a -> -- Right subtree (FIXED)- Expr Bool -> -- Current condition (fallback)- Expr Bool-findBestSplitTAO cfg target conds df indices leftTree rightTree currentCond- | V.null indices = currentCond- | null validConds = currentCond- | otherwise =- let- carePoints = identifyCarePoints @a target df indices leftTree rightTree- in- if null carePoints- then currentCond- else- let- evalSplit :: Expr Bool -> Int- evalSplit cond = countCarePointErrors cond df carePoints-- evalWithPenalty c =- let errors = evalSplit c- penalty =- floor- ( complexityPenalty (synthConfig cfg)- * fromIntegral (eSize c)- )- in errors + penalty-- sortedConds =- take (expressionPairs cfg) $- sortBy (compare `on` evalWithPenalty) validConds-- expandedConds =- boolExprs- df- sortedConds- sortedConds- 0- (boolExpansion (synthConfig cfg))- in- if null expandedConds- then currentCond- else minimumBy (compare `on` evalWithPenalty) expandedConds- where- validConds = filter isValidSplit conds- isValidSplit c =- let (t, f) = partitionIndices c df indices- in V.length t >= minLeafSize cfg && V.length f >= minLeafSize cfg---- | A care point with its index and which direction leads to correct classification-data CarePoint = CarePoint- { cpIndex :: !Int- , cpCorrectDir :: !Direction -- Which child classifies this point correctly- }- deriving (Eq, Show)--data Direction = GoLeft | GoRight- deriving (Eq, Show)--{- | Identify care points: points where exactly one subtree classifies correctly-- For each point reaching the node:- 1. Compute what label the left subtree would predict- 2. Compute what label the right subtree would predict- 3. If exactly one matches the true label, it's a care point- 4. Record which direction leads to correct classification--}-identifyCarePoints ::- forall a.- (Columnable a) =>- T.Text ->- DataFrame ->- V.Vector Int ->- Tree a -> -- Left subtree- Tree a -> -- Right subtree- [CarePoint]-identifyCarePoints target df indices leftTree rightTree =- case interpret @a df (Col target) of- Left _ -> []- Right (TColumn col) ->- case toVector @a col of- Left _ -> []- Right targetVals ->- V.toList $ V.mapMaybe (checkPoint targetVals) indices- where- checkPoint :: V.Vector a -> Int -> Maybe CarePoint- checkPoint targetVals idx =- let- trueLabel = targetVals V.! idx- leftPred = predictWithTree @a target df idx leftTree- rightPred = predictWithTree @a target df idx rightTree- leftCorrect = leftPred == trueLabel- rightCorrect = rightPred == trueLabel- in- case (leftCorrect, rightCorrect) of- (True, False) -> Just $ CarePoint idx GoLeft- (False, True) -> Just $ CarePoint idx GoRight- _ -> Nothing -- Don't-care point (both correct or both wrong)---- | Predict the label for a single point using a fixed tree-predictWithTree ::- forall a.- (Columnable a) =>- T.Text ->- DataFrame ->- Int -> -- Row index- Tree a ->- a-predictWithTree target df idx (Leaf v) = v-predictWithTree target df idx (Branch cond left right) =- case interpret @Bool df cond of- Left _ -> predictWithTree @a target df idx left -- Default to left on error- Right (TColumn col) ->- case toVector @Bool col of- Left _ -> predictWithTree @a target df idx left- Right boolVals ->- if boolVals V.! idx- then predictWithTree @a target df idx left- else predictWithTree @a target df idx right--countCarePointErrors :: Expr Bool -> DataFrame -> [CarePoint] -> Int-countCarePointErrors cond df carePoints =- case interpret @Bool df cond of- Left _ -> length carePoints- Right (TColumn col) ->- case toVector @Bool col of- Left _ -> length carePoints- Right boolVals ->- length $ filter (isMisclassified boolVals) carePoints- where- isMisclassified :: V.Vector Bool -> CarePoint -> Bool- isMisclassified boolVals cp =- let goesLeft = boolVals V.! cpIndex cp- shouldGoLeft = cpCorrectDir cp == GoLeft- in goesLeft /= shouldGoLeft--partitionIndices ::- Expr Bool -> DataFrame -> V.Vector Int -> (V.Vector Int, V.Vector Int)-partitionIndices cond df indices =- case interpret @Bool df cond of- Left _ -> (indices, V.empty)- Right (TColumn col) ->- case toVector @Bool col of- Left _ -> (indices, V.empty)- Right boolVals ->- V.partition (boolVals V.!) indices--majorityValueFromIndices ::- forall a.- (Columnable a) =>- T.Text ->- DataFrame ->- V.Vector Int ->- a-majorityValueFromIndices target df indices =- case interpret @a df (Col target) of- Left e -> throw e- Right (TColumn col) ->- case toVector @a col of- Left e -> throw e- Right vals ->- let counts =- V.foldl'- (\acc i -> M.insertWith (+) (vals V.! i) 1 acc)- M.empty- indices- in if M.null counts- then error "Empty indices in majorityValueFromIndices"- else fst $ maximumBy (compare `on` snd) (M.toList counts)--computeTreeLoss ::- forall a.- (Columnable a) =>- T.Text ->- DataFrame ->- V.Vector Int ->- Tree a ->- Double-computeTreeLoss target df indices tree- | V.null indices = 0- | otherwise =- case interpret @a df (Col target) of- Left _ -> 1.0- Right (TColumn col) ->- case toVector @a col of- Left _ -> 1.0- Right targetVals ->- let- n = V.length indices- errors =- V.length $- V.filter- (\i -> targetVals V.! i /= predictWithTree @a target df i tree)- indices- in- fromIntegral errors / fromIntegral n--pruneDead :: Tree a -> Tree a-pruneDead (Leaf v) = Leaf v-pruneDead (Branch cond left right) =- let- left' = pruneDead left- right' = pruneDead right- in- Branch cond left' right'--pruneExpr :: forall a. (Columnable a, Eq a) => Expr a -> Expr a-pruneExpr (If cond trueBranch falseBranch) =- let t = pruneExpr trueBranch- f = pruneExpr falseBranch- in if t == f- then t- else case (t, f) of- (If condInner tInner _, _) | cond == condInner -> If cond tInner f- (_, If condInner _ fInner) | cond == condInner -> If cond t fInner- _ -> If cond t f-pruneExpr (Unary op e) = Unary op (pruneExpr e)-pruneExpr (Binary op l r) = Binary op (pruneExpr l) (pruneExpr r)-pruneExpr e = e--buildGreedyTree ::- forall a.- (Columnable a) =>- TreeConfig ->- Int ->- T.Text ->- [Expr Bool] ->- DataFrame ->- Tree a-buildGreedyTree cfg depth target conds df- | depth <= 0 || nRows df <= minSamplesSplit cfg =- Leaf (majorityValue @a target df)- | otherwise =- case findBestGreedySplit @a cfg target conds df of- Nothing -> Leaf (majorityValue @a target df)- Just bestCond ->- let (dfTrue, dfFalse) = partitionDataFrame bestCond df- in if nRows dfTrue < minLeafSize cfg || nRows dfFalse < minLeafSize cfg- then Leaf (majorityValue @a target df)- else- Branch- bestCond- (buildGreedyTree @a cfg (depth - 1) target conds dfTrue)- (buildGreedyTree @a cfg (depth - 1) target conds dfFalse)--findBestGreedySplit ::- forall a.- (Columnable a) =>- TreeConfig -> T.Text -> [Expr Bool] -> DataFrame -> Maybe (Expr Bool)-findBestGreedySplit cfg target conds df =- let- initialImpurity = calculateGini @a target df- calculateComplexity c = complexityPenalty (synthConfig cfg) * fromIntegral (eSize c)-- evalGain :: Expr Bool -> (Double, Int)- evalGain cond =- let (t, f) = partitionDataFrame cond df- n = fromIntegral @Int @Double (nRows df)- weightT = fromIntegral @Int @Double (nRows t) / n- weightF = fromIntegral @Int @Double (nRows f) / n- newImpurity =- weightT * calculateGini @a target t- + weightF * calculateGini @a target f- in ( (initialImpurity - newImpurity) - calculateComplexity cond- , negate (eSize cond)- )-- validConds =- filter- ( \c ->- let (t, f) = partitionDataFrame c df- in nRows t >= minLeafSize cfg && nRows f >= minLeafSize cfg- )- conds-- sortedConditions =- map fst $- take- (expressionPairs cfg)- ( filter- (\(c, v) -> ((> negate (calculateComplexity c)) . fst) v)- (sortBy (flip compare `on` snd) (map (\c -> (c, evalGain c)) validConds))- )- in- if null sortedConditions- then Nothing- else- Just $- maximumBy- (compare `on` evalGain)- ( boolExprs- df- sortedConditions- sortedConditions- 0- (boolExpansion (synthConfig cfg))- )--numericConditions :: TreeConfig -> DataFrame -> [Expr Bool]-numericConditions = generateNumericConds--generateNumericConds :: TreeConfig -> DataFrame -> [Expr Bool]-generateNumericConds cfg df = do- expr <- numericExprsWithTerms (synthConfig cfg) df- let thresholds = map (\p -> percentile p expr df) (percentiles cfg)- threshold <- thresholds- [ expr .<= F.lit threshold- , expr .>= F.lit threshold- , expr .< F.lit threshold- , expr .> F.lit threshold- ]--numericExprsWithTerms :: SynthConfig -> DataFrame -> [Expr Double]-numericExprsWithTerms cfg df =- concatMap (numericExprs cfg df [] 0) [0 .. maxExprDepth cfg]--numericCols :: DataFrame -> [Expr Double]-numericCols df = concatMap extract (columnNames df)- where- extract col = case unsafeGetColumn col df of- UnboxedColumn (_ :: VU.Vector b) ->- case testEquality (typeRep @b) (typeRep @Double) of- Just Refl -> [Col col]- Nothing -> case sIntegral @b of- STrue -> [F.toDouble (Col @b col)]- SFalse -> []- _ -> []--numericExprs ::- SynthConfig -> DataFrame -> [Expr Double] -> Int -> Int -> [Expr Double]-numericExprs cfg df prevExprs depth maxDepth- | depth == 0 = baseExprs ++ numericExprs cfg df baseExprs (depth + 1) maxDepth- | depth >= maxDepth = []- | otherwise =- combinedExprs ++ numericExprs cfg df combinedExprs (depth + 1) maxDepth- where- baseExprs = numericCols df- combinedExprs- | not (enableArithOps cfg) = []- | otherwise = do- e1 <- prevExprs- e2 <- baseExprs- let cols = getColumns e1 <> getColumns e2- guard- ( e1 /= e2- && not- ( any- (\(l, r) -> l `elem` cols && r `elem` cols)- (disallowedCombinations cfg)- )- )- [e1 + e2, e1 - e2, e1 * e2, F.ifThenElse (e2 ./= 0) (e1 / e2) 0]--boolExprs ::- DataFrame -> [Expr Bool] -> [Expr Bool] -> Int -> Int -> [Expr Bool]-boolExprs df baseExprs prevExprs depth maxDepth- | depth == 0 =- baseExprs ++ boolExprs df baseExprs prevExprs (depth + 1) maxDepth- | depth >= maxDepth = []- | otherwise =- combinedExprs ++ boolExprs df baseExprs combinedExprs (depth + 1) maxDepth- where- combinedExprs = do- e1 <- prevExprs- e2 <- baseExprs- guard (e1 /= e2)- [F.and e1 e2, F.or e1 e2]--generateConditionsOld :: TreeConfig -> DataFrame -> [Expr Bool]-generateConditionsOld cfg df =- let- genConds :: T.Text -> [Expr Bool]- genConds colName = case unsafeGetColumn colName df of- (BoxedColumn (col :: V.Vector a)) ->- let ps = map (Lit . (`percentileOrd'` col)) [1, 25, 75, 99]- in map (Col @a colName .==) ps- (OptionalColumn (col :: V.Vector (Maybe a))) -> case sFloating @a of- STrue ->- let doubleCol =- VU.convert- (V.map fromJust (V.filter isJust (V.map (fmap (realToFrac @a @Double)) col)))- in zipWith- ($)- [ (Col @(Maybe a) colName .==)- , (Col @(Maybe a) colName .<=)- , (Col @(Maybe a) colName .>=)- ]- ( Lit Nothing- : map- (Lit . Just . realToFrac . (`percentile'` doubleCol))- (percentiles cfg)- )- SFalse -> case sIntegral @a of- STrue ->- let doubleCol =- VU.convert- (V.map fromJust (V.filter isJust (V.map (fmap (fromIntegral @a @Double)) col)))- in zipWith- ($)- [ (Col @(Maybe a) colName .==)- , (Col @(Maybe a) colName .<=)- , (Col @(Maybe a) colName .>=)- ]- ( Lit Nothing- : map- (Lit . Just . round . (`percentile'` doubleCol))- (percentiles cfg)- )- SFalse ->- map- ((Col @(Maybe a) colName .==) . Lit . (`percentileOrd'` col))- [1, 25, 75, 99]- (UnboxedColumn (_ :: VU.Vector a)) -> []-- columnConds =- concatMap- colConds- [ (l, r)- | l <- columnNames df- , r <- columnNames df- , not- ( any- (\(l', r') -> sort [l', r'] == sort [l, r])- (disallowedCombinations (synthConfig cfg))- )- ]- where- colConds (!l, !r) = case (unsafeGetColumn l df, unsafeGetColumn r df) of- (BoxedColumn (col1 :: V.Vector a), BoxedColumn (_ :: V.Vector b)) ->- case testEquality (typeRep @a) (typeRep @b) of- Nothing -> []- Just Refl -> [Col @a l .== Col @a r]- (UnboxedColumn (_ :: VU.Vector a), UnboxedColumn (_ :: VU.Vector b)) -> []- ( OptionalColumn (_ :: V.Vector (Maybe a))- , OptionalColumn (_ :: V.Vector (Maybe b))- ) -> case testEquality (typeRep @a) (typeRep @b) of- Nothing -> []- Just Refl -> case testEquality (typeRep @a) (typeRep @T.Text) of- Nothing -> [Col @(Maybe a) l .<= Col r, Col @(Maybe a) l .== Col r]- Just Refl -> [Col @(Maybe a) l .== Col r]- _ -> []- in- concatMap genConds (columnNames df) ++ columnConds--partitionDataFrame :: Expr Bool -> DataFrame -> (DataFrame, DataFrame)-partitionDataFrame cond df = (filterWhere cond df, filterWhere (F.not cond) df)--calculateGini :: forall a. (Columnable a) => T.Text -> DataFrame -> Double-calculateGini target df =- let n = fromIntegral $ nRows df- counts = getCounts @a target df- numClasses = fromIntegral $ M.size counts- probs = map (\c -> (fromIntegral c + 1) / (n + numClasses)) (M.elems counts)- in if n == 0 then 0 else 1 - sum (map (^ 2) probs)--majorityValue :: forall a. (Columnable a) => T.Text -> DataFrame -> a-majorityValue target df =- let counts = getCounts @a target df- in if M.null counts- then error "Empty DataFrame in leaf"- else fst $ maximumBy (compare `on` snd) (M.toList counts)--getCounts :: forall a. (Columnable a) => T.Text -> DataFrame -> M.Map a Int-getCounts target df =- case interpret @a df (Col target) of- Left e -> throw e- Right (TColumn col) ->- case toVector @a col of- Left e -> throw e- Right vals -> foldl' (\acc x -> M.insertWith (+) x 1 acc) M.empty (V.toList vals)--percentile :: Int -> Expr Double -> DataFrame -> Double-percentile p expr df =- case interpret @Double df expr of- Left _ -> 0- Right (TColumn col) ->- case toVector @Double col of- Left _ -> 0- Right vals ->- let sorted = V.fromList $ sort $ V.toList vals- n = V.length sorted- idx = min (n - 1) $ max 0 $ (p * n) `div` 100- in if n == 0 then 0 else sorted V.! idx--buildTree ::- forall a.- (Columnable a) =>- TreeConfig ->- Int ->- T.Text ->- [Expr Bool] ->- DataFrame ->- Expr a-buildTree cfg depth target conds df =- let- tree = buildGreedyTree @a cfg depth target conds df- indices = V.enumFromN 0 (nRows df)- optimized = taoOptimize @a cfg target conds df indices tree- in- pruneExpr (treeToExpr optimized)--findBestSplit ::- forall a.- (Columnable a) =>- TreeConfig -> T.Text -> [Expr Bool] -> DataFrame -> Maybe (Expr Bool)-findBestSplit = findBestGreedySplit @a--pruneTree :: forall a. (Columnable a, Eq a) => Expr a -> Expr a-pruneTree = pruneExpr
− src/DataFrame/Display.hs
@@ -1,26 +0,0 @@-{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}--{-# HLINT ignore "Use newtype instead of data" #-}-module DataFrame.Display where--import qualified Data.Text.IO as T-import qualified DataFrame.Internal.DataFrame as D-import qualified DataFrame.Operations.Subset as D--import Data.Function--data DisplayOptions = DisplayOptions- { {-- | Maximum number of rows to render.- --- -- * If this value is less than or equal to 0, no rows are printed.- -- * If it is greater than the number of rows in the frame, all rows are printed.- --}- displayRows :: Int- }--defaultDisplayOptions :: DisplayOptions-defaultDisplayOptions = DisplayOptions 10---- | Render a 'DataFrame' to stdout according to 'DisplayOptions'.-display :: DisplayOptions -> D.DataFrame -> IO ()-display opts df = df & D.take (displayRows opts) & (`D.asText` False) & T.putStrLn
− src/DataFrame/Display/Terminal/Colours.hs
@@ -1,14 +0,0 @@-module DataFrame.Display.Terminal.Colours where---- terminal color functions-red :: String -> String-red s = "\ESC[31m" ++ s ++ "\ESC[0m"--green :: String -> String-green s = "\ESC[32m" ++ s ++ "\ESC[0m"--brightGreen :: String -> String-brightGreen s = "\ESC[92m" ++ s ++ "\ESC[0m"--brightBlue :: String -> String-brightBlue s = "\ESC[94m" ++ s ++ "\ESC[0m"
− src/DataFrame/Display/Terminal/Plot.hs
@@ -1,615 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# 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 qualified Data.Text as T-import qualified Data.Text.IO as T-import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))-import qualified Data.Vector as V-import qualified Data.Vector.Generic as VG-import qualified Data.Vector.Unboxed as VU-import DataFrame.Internal.Types-import GHC.Stack (HasCallStack)-import Type.Reflection (typeRep)--import DataFrame.Internal.Column (Column (..), Columnable, isNumeric)-import qualified DataFrame.Internal.Column as D-import DataFrame.Internal.DataFrame (DataFrame (..), getColumn)-import DataFrame.Internal.Expression-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 = plotHistogramWith colName 30 (defaultPlotConfig Histogram)--plotHistogramWith ::- (HasCallStack) => T.Text -> Int -> PlotConfig -> DataFrame -> IO ()-plotHistogramWith colName numBins 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 numBins minVal maxVal) values (plotSettings config)--plotScatter :: (HasCallStack) => T.Text -> T.Text -> DataFrame -> IO ()-plotScatter xCol yCol = plotScatterWith xCol yCol (defaultPlotConfig Scatter)--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 = plotScatterByWith xCol yCol grouping (defaultPlotConfig Scatter)--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 (Col 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 = plotLinesWith xAxis colNames (defaultPlotConfig Line)--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 = plotBoxPlotsWith colNames (defaultPlotConfig BoxPlot)--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 = plotStackedBarsWith categoryCol valueColumns (defaultPlotConfig StackedBar)--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 = plotHeatmapWith (defaultPlotConfig Heatmap)--plotHeatmapWith :: (HasCallStack) => PlotConfig -> DataFrame -> IO ()-plotHeatmapWith config df = do- let numericCols = filter (isNumericColumn df) (columnNames df)- matrix = map (`extractNumericColumn` df) numericCols- T.putStrLn $ heatmap matrix (plotSettings config)--isNumericColumn :: DataFrame -> T.Text -> Bool-isNumericColumn df colName = maybe False isNumeric (getColumn colName df)--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 = plotBarsWith colName Nothing (defaultPlotConfig Bar)--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 = plotBarsTopNWith n colName (defaultPlotConfig Bar)--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 colIsNumeric = isNumericColumnCheck valCol df-- if colIsNumeric- then do- let groups = extractStringColumn groupCol df- values = extractNumericColumn valCol df- m = M.fromListWith (+) (zip groups values)- grouped = map (\v -> (v, m M.! v)) groups- T.putStrLn $ bars grouped (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 = plotValueCountsWith colName 10 (defaultPlotConfig Bar)--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- 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]- OptionalColumn vec ->- let counts = countValues vec- in Just [(T.pack (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 = isNumericColumn df colName--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. (Columnable a, Show a) => V.Vector a -> [Double]-vectorToDoubles vec =- case testEquality (typeRep @a) (typeRep @Double) of- Just Refl -> V.toList vec- Nothing -> case sIntegral @a of- STrue -> V.toList $ V.map fromIntegral vec- SFalse -> case sFloating @a of- STrue -> V.toList $ V.map realToFrac vec- SFalse -> error $ "Column is not numeric (type: " ++ show (typeRep @a) ++ ")"--unboxedVectorToDoubles ::- forall a. (Columnable 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 sIntegral @a of- STrue -> VU.toList $ VU.map fromIntegral vec- SFalse -> case sFloating @a of- STrue -> VU.toList $ VU.map realToFrac vec- SFalse -> 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 = plotPieWith valCol labelCol (defaultPlotConfig Pie)--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 = plotPieWithPercentagesConfig colName (defaultPlotConfig Pie)--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 = plotPieTopNWith n colName (defaultPlotConfig Pie)--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 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 = plotPieGroupedWith groupCol valCol (defaultPlotConfig Pie)--plotPieGroupedWith ::- (HasCallStack) => T.Text -> T.Text -> PlotConfig -> DataFrame -> IO ()-plotPieGroupedWith groupCol valCol config df = do- let colIsNumeric = isNumericColumnCheck valCol df-- if colIsNumeric- 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 = 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 = plotMarketShareWith colName (defaultPlotConfig Pie)--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
@@ -1,78 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module DataFrame.Display.Terminal.PrettyPrint where--import qualified Data.Text as T--import Data.List (transpose)---- Utility functions to show a DataFrame as a Markdown-ish table.---- Adapted from: https://stackoverflow.com/questions/5929377/format-list-output-in-haskell--- a type for fill functions-type Filler = Int -> T.Text -> T.Text---- a type for describing table columns-data ColDesc t = ColDesc- { 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-fillLeft :: Char -> Int -> T.Text -> T.Text-fillLeft c n s = s `T.append` T.replicate (n - T.length s) (T.singleton c)--fillRight :: Char -> Int -> T.Text -> T.Text-fillRight c n s = T.replicate (n - T.length s) (T.singleton c) `T.append` s--fillCenter :: Char -> Int -> T.Text -> T.Text-fillCenter c n s =- T.replicate l (T.singleton c)- `T.append` s- `T.append` T.replicate r (T.singleton c)- where- x = n - T.length s- l = x `div` 2- r = x - l---- functions that fill with spaces-left :: Int -> T.Text -> T.Text-left = fillLeft ' '--right :: Int -> T.Text -> T.Text-right = fillRight ' '--center :: Int -> T.Text -> T.Text-center = fillCenter ' '--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- T.concat ["| ", fillCols colTitleFill consolidatedHeader, " |"]- : T.concat ["| ", separator, " |"]- : map ((\t -> T.concat ["| ", t, " |"]) . fillCols colValueFill) rows- else- border- : fillCols colTitleFill consolidatedHeader- : separator- : fillCols colTitleFill types- : separator- : map (fillCols colValueFill) rows- in T.unlines lines
− src/DataFrame/Display/Web/Plot.hs
@@ -1,1051 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.Display.Web.Plot where--import Control.Monad-import Data.Char-import qualified Data.List as L-import qualified Data.Map as M-import qualified Data.Text as T-import qualified Data.Text.IO as T-import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))-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 System.Random (newStdGen, randomRs)-import Type.Reflection (typeRep)--import DataFrame.Internal.Column (Column (..), Columnable, isNumeric)-import qualified DataFrame.Internal.Column as D-import DataFrame.Internal.DataFrame (DataFrame (..), getColumn)-import DataFrame.Internal.Expression-import DataFrame.Internal.Types-import DataFrame.Operations.Core-import qualified DataFrame.Operations.Subset as D-import Numeric (showFFloat)-import System.Directory-import System.Info-import System.Process (- StdStream (NoStream),- createProcess,- proc,- std_err,- std_in,- std_out,- waitForProcess,- )--newtype HtmlPlot = HtmlPlot T.Text deriving (Show)--data PlotConfig = PlotConfig- { plotType :: PlotType- , plotTitle :: T.Text- , plotWidth :: Int- , plotHeight :: Int- , plotFile :: Maybe FilePath- }--data PlotType- = Histogram- | Scatter- | Line- | Bar- | BoxPlot- | Pie- | StackedBar- | Heatmap- deriving (Eq, Show)--defaultPlotConfig :: PlotType -> PlotConfig-defaultPlotConfig ptype =- PlotConfig- { plotType = ptype- , plotTitle = ""- , plotWidth = 600- , plotHeight = 400- , plotFile = Nothing- }--generateChartId :: IO T.Text-generateChartId = do- gen <- newStdGen- let randomWords =- filter- (\c -> c `elem` ([49 .. 57] ++ [65 .. 90] ++ [97 .. 122]))- (take 64 (randomRs (49, 126) gen :: [Int]))- return $ "chart_" <> T.pack (map chr randomWords)--wrapInHTML :: T.Text -> T.Text -> Int -> Int -> T.Text-wrapInHTML chartId content width height =- T.concat- [ "<canvas id=\""- , chartId- , "\" style=\"width:100%;max-width:"- , T.pack (show width)- , "px;height:"- , T.pack (show height)- , "px\"></canvas>\n"- , "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js\"></script>\n"- , "<script>\n"- , content- , "\n</script>\n"- ]--plotHistogram :: (HasCallStack) => T.Text -> DataFrame -> IO HtmlPlot-plotHistogram colName = plotHistogramWith colName 30 (defaultPlotConfig Histogram)--plotHistogramWith ::- (HasCallStack) => T.Text -> Int -> PlotConfig -> DataFrame -> IO HtmlPlot-plotHistogramWith colName numBins config df = do- chartId <- generateChartId- let values = extractNumericColumn colName df- (minVal, maxVal) = if null values then (0, 1) else (minimum values, maximum values)- binWidth = (maxVal - minVal) / fromIntegral numBins- bins = [minVal + fromIntegral i * binWidth | i <- [0 .. numBins - 1]]- counts = calculateHistogram values bins binWidth- precision = max 0 $ ceiling (negate $ logBase 10 binWidth)-- labels =- T.intercalate- ","- [ "\"" <> T.pack (showFFloat (Just precision) b "") <> "\""- | b <- bins- ]- dataPoints = T.intercalate "," [T.pack (show c) | c <- counts]-- chartTitle =- if T.null (plotTitle config)- then "Histogram of " <> colName- else plotTitle config-- jsCode =- T.concat- [ "setTimeout(function() { new Chart(\""- , chartId- , "\", {\n"- , " type: \"bar\",\n"- , " data: {\n"- , " labels: ["- , labels- , "],\n"- , " datasets: [{\n"- , " label: \""- , colName- , "\",\n"- , " data: ["- , dataPoints- , "],\n"- , " backgroundColor: \"rgba(75, 192, 192, 0.6)\",\n"- , " borderColor: \"rgba(75, 192, 192, 1)\",\n"- , " borderWidth: 1\n"- , " }]\n"- , " },\n"- , " options: {\n"- , " title: { display: true, text: \""- , chartTitle- , "\" },\n"- , " scales: {\n"- , " yAxes: [{ ticks: { beginAtZero: true } }]\n"- , " }\n"- , " }\n"- , "})}, 100);"- ]-- return $- HtmlPlot $- wrapInHTML chartId jsCode (plotWidth config) (plotHeight config)--calculateHistogram :: [Double] -> [Double] -> Double -> [Int]-calculateHistogram values bins binWidth =- let countBin b = length [v | v <- values, v >= b && v < b + binWidth]- in map countBin bins--plotScatter :: (HasCallStack) => T.Text -> T.Text -> DataFrame -> IO HtmlPlot-plotScatter xCol yCol = plotScatterWith xCol yCol (defaultPlotConfig Scatter)--plotScatterWith ::- (HasCallStack) => T.Text -> T.Text -> PlotConfig -> DataFrame -> IO HtmlPlot-plotScatterWith xCol yCol config df = do- chartId <- generateChartId- let xVals = extractNumericColumn xCol df- yVals = extractNumericColumn yCol df- points = zip xVals yVals-- dataPoints =- T.intercalate- ","- [ "{x:" <> T.pack (show x) <> ", y:" <> T.pack (show y) <> "}" | (x, y) <- points- ]- chartTitle =- if T.null (plotTitle config) then xCol <> " vs " <> yCol else plotTitle config-- jsCode =- T.concat- [ "setTimeout(function() { new Chart(\""- , chartId- , "\", {\n"- , " type: \"scatter\",\n"- , " data: {\n"- , " datasets: [{\n"- , " label: \""- , chartTitle- , "\",\n"- , " data: ["- , dataPoints- , "],\n"- , " pointRadius: 4,\n"- , " pointBackgroundColor: \"rgb(75, 192, 192)\"\n"- , " }]\n"- , " },\n"- , " options: {\n"- , " title: { display: true, text: \""- , chartTitle- , "\" },\n"- , " scales: {\n"- , " xAxes: [{ scaleLabel: { display: true, labelString: \""- , xCol- , "\" } }],\n"- , " yAxes: [{ scaleLabel: { display: true, labelString: \""- , yCol- , "\" } }]\n"- , " }\n"- , " }\n"- , "})}, 100);"- ]-- return $- HtmlPlot $- wrapInHTML chartId jsCode (plotWidth config) (plotHeight config)--plotScatterBy ::- (HasCallStack) => T.Text -> T.Text -> T.Text -> DataFrame -> IO HtmlPlot-plotScatterBy xCol yCol grouping = plotScatterByWith xCol yCol grouping (defaultPlotConfig Scatter)--plotScatterByWith ::- (HasCallStack) =>- T.Text -> T.Text -> T.Text -> PlotConfig -> DataFrame -> IO HtmlPlot-plotScatterByWith xCol yCol grouping config df = do- chartId <- generateChartId- let vals = extractStringColumn grouping df- df' = insertColumn grouping (D.fromList vals) df- uniqueVals = L.nub vals-- colors =- cycle- [ "rgb(255, 99, 132)"- , "rgb(54, 162, 235)"- , "rgb(255, 206, 86)"- , "rgb(75, 192, 192)"- , "rgb(153, 102, 255)"- , "rgb(255, 159, 64)"- ]-- datasets <- forM (zip uniqueVals colors) $ \(val, color) -> do- let filtered = D.filter (Col grouping) (== val) df'- xVals = extractNumericColumn xCol filtered- yVals = extractNumericColumn yCol filtered- points = zip xVals yVals- dataPoints =- T.intercalate- ","- [ "{x:" <> T.pack (show x) <> ", y:" <> T.pack (show y) <> "}" | (x, y) <- points- ]- return $- T.concat- [ " {\n"- , " label: \""- , val- , "\",\n"- , " data: ["- , dataPoints- , "],\n"- , " pointRadius: 4,\n"- , " pointBackgroundColor: \""- , color- , "\"\n"- , " }"- ]-- let datasetsStr = T.intercalate ",\n" datasets- chartTitle =- if T.null (plotTitle config)- then xCol <> " vs " <> yCol <> " by " <> grouping- else plotTitle config-- jsCode =- T.concat- [ "setTimeout(function() { new Chart(\""- , chartId- , "\", {\n"- , " type: \"scatter\",\n"- , " data: {\n"- , " datasets: [\n"- , datasetsStr- , "\n ]\n"- , " },\n"- , " options: {\n"- , " title: { display: true, text: \""- , chartTitle- , "\" },\n"- , " scales: {\n"- , " xAxes: [{ scaleLabel: { display: true, labelString: \""- , xCol- , "\" } }],\n"- , " yAxes: [{ scaleLabel: { display: true, labelString: \""- , yCol- , "\" } }]\n"- , " }\n"- , " }\n"- , "})}, 100);"- ]-- return $- HtmlPlot $- wrapInHTML chartId jsCode (plotWidth config) (plotHeight config)--plotLines :: (HasCallStack) => T.Text -> [T.Text] -> DataFrame -> IO HtmlPlot-plotLines xAxis colNames = plotLinesWith xAxis colNames (defaultPlotConfig Line)--plotLinesWith ::- (HasCallStack) => T.Text -> [T.Text] -> PlotConfig -> DataFrame -> IO HtmlPlot-plotLinesWith xAxis colNames config df = do- chartId <- generateChartId- let xValues = extractNumericColumn xAxis df- labels = T.intercalate "," [T.pack (show x) | x <- xValues]-- colors =- cycle- [ "rgb(255, 99, 132)"- , "rgb(54, 162, 235)"- , "rgb(255, 206, 86)"- , "rgb(75, 192, 192)"- , "rgb(153, 102, 255)"- , "rgb(255, 159, 64)"- ]-- datasets <- forM (zip colNames colors) $ \(col, color) -> do- let values = extractNumericColumn col df- dataPoints = T.intercalate "," [T.pack (show v) | v <- values]- return $- T.concat- [ " {\n"- , " label: \""- , col- , "\",\n"- , " data: ["- , dataPoints- , "],\n"- , " fill: false,\n"- , " borderColor: \""- , color- , "\",\n"- , " tension: 0.1\n"- , " }"- ]-- let datasetsStr = T.intercalate ",\n" datasets- chartTitle = if T.null (plotTitle config) then "Line Chart" else plotTitle config-- jsCode =- T.concat- [ "setTimeout(function() { new Chart(\""- , chartId- , "\", {\n"- , " type: \"line\",\n"- , " data: {\n"- , " labels: ["- , labels- , "],\n"- , " datasets: [\n"- , datasetsStr- , "\n ]\n"- , " },\n"- , " options: {\n"- , " title: { display: true, text: \""- , chartTitle- , "\" },\n"- , " scales: {\n"- , " xAxes: [{ scaleLabel: { display: true, labelString: \""- , xAxis- , "\" } }]\n"- , " }\n"- , " }\n"- , "})}, 100);"- ]-- return $- HtmlPlot $- wrapInHTML chartId jsCode (plotWidth config) (plotHeight config)--plotBars :: (HasCallStack) => T.Text -> DataFrame -> IO HtmlPlot-plotBars colName = plotBarsWith colName Nothing (defaultPlotConfig Bar)--plotBarsWith ::- (HasCallStack) =>- T.Text -> Maybe T.Text -> PlotConfig -> DataFrame -> IO HtmlPlot-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 HtmlPlot-plotSingleBars colName config df = do- chartId <- generateChartId- let barData = getCategoricalCounts colName df- case barData of- Just counts -> do- let grouped = groupWithOther 10 counts- labels = T.intercalate "," ["\"" <> label <> "\"" | (label, _) <- grouped]- dataPoints = T.intercalate "," [T.pack (show val) | (_, val) <- grouped]- chartTitle = if T.null (plotTitle config) then colName else plotTitle config-- jsCode =- T.concat- [ "setTimeout(function() { new Chart(\""- , chartId- , "\", {\n"- , " type: \"bar\",\n"- , " data: {\n"- , " labels: ["- , labels- , "],\n"- , " datasets: [{\n"- , " label: \"Count\",\n"- , " data: ["- , dataPoints- , "],\n"- , " backgroundColor: \"rgba(54, 162, 235, 0.6)\",\n"- , " borderColor: \"rgba(54, 162, 235, 1)\",\n"- , " borderWidth: 1\n"- , " }]\n"- , " },\n"- , " options: {\n"- , " title: { display: true, text: \""- , chartTitle- , "\" },\n"- , " scales: {\n"- , " yAxes: [{ ticks: { beginAtZero: true } }]\n"- , " }\n"- , " }\n"- , "})}, 100);"- ]- return $- HtmlPlot $- wrapInHTML chartId jsCode (plotWidth config) (plotHeight config)- Nothing -> do- let values = extractNumericColumn colName df- labels' =- if length values > 20- then take 20 ["Item " <> T.pack (show i) | i <- [1 ..]]- else ["Item " <> T.pack (show i) | i <- [1 .. length values]]- vals = if length values > 20 then take 20 values else values- labels = T.intercalate "," ["\"" <> label <> "\"" | label <- labels']- dataPoints = T.intercalate "," [T.pack (show val) | val <- vals]- chartTitle = if T.null (plotTitle config) then colName else plotTitle config-- jsCode =- T.concat- [ "setTimeout(function() { new Chart(\""- , chartId- , "\", {\n"- , " type: \"bar\",\n"- , " data: {\n"- , " labels: ["- , labels- , "],\n"- , " datasets: [{\n"- , " label: \"Value\",\n"- , " data: ["- , dataPoints- , "],\n"- , " backgroundColor: \"rgba(54, 162, 235, 0.6)\",\n"- , " borderColor: \"rgba(54, 162, 235, 1)\",\n"- , " borderWidth: 1\n"- , " }]\n"- , " },\n"- , " options: {\n"- , " title: { display: true, text: \""- , chartTitle- , "\" },\n"- , " scales: {\n"- , " yAxes: [{ ticks: { beginAtZero: true } }]\n"- , " }\n"- , " }\n"- , "})}, 100);"- ]- return $- HtmlPlot $- wrapInHTML chartId jsCode (plotWidth config) (plotHeight config)--plotPie :: (HasCallStack) => T.Text -> Maybe T.Text -> DataFrame -> IO HtmlPlot-plotPie valCol labelCol = plotPieWith valCol labelCol (defaultPlotConfig Pie)--plotPieWith ::- (HasCallStack) =>- T.Text -> Maybe T.Text -> PlotConfig -> DataFrame -> IO HtmlPlot-plotPieWith valCol labelCol config df = do- chartId <- generateChartId- let categoricalData = getCategoricalCounts valCol df- case categoricalData of- Just counts -> do- let grouped = groupWithOtherForPie 8 counts- labels = T.intercalate "," ["\"" <> label <> "\"" | (label, _) <- grouped]- dataPoints = T.intercalate "," [T.pack (show val) | (_, val) <- grouped]- colors = T.intercalate "," ["\"" <> c <> "\"" | c <- take (length grouped) pieColors]- chartTitle = if T.null (plotTitle config) then valCol else plotTitle config-- jsCode =- T.concat- [ "setTimeout(function() { new Chart(\""- , chartId- , "\", {\n"- , " type: \"pie\",\n"- , " data: {\n"- , " labels: ["- , labels- , "],\n"- , " datasets: [{\n"- , " data: ["- , dataPoints- , "],\n"- , " backgroundColor: ["- , colors- , "]\n"- , " }]\n"- , " },\n"- , " options: {\n"- , " title: { display: true, text: \""- , chartTitle- , "\" }\n"- , " }\n"- , "})}, 100);"- ]- return $- HtmlPlot $- wrapInHTML chartId jsCode (plotWidth config) (plotHeight 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- pieData = zip labels' values- grouped =- if length pieData > 10- then groupWithOtherForPie 8 pieData- else pieData- labels = T.intercalate "," ["\"" <> label <> "\"" | (label, _) <- grouped]- dataPoints = T.intercalate "," [T.pack (show val) | (_, val) <- grouped]- colors = T.intercalate "," ["\"" <> c <> "\"" | c <- take (length grouped) pieColors]- chartTitle = if T.null (plotTitle config) then valCol else plotTitle config-- jsCode =- T.concat- [ "setTimeout(function() { new Chart(\""- , chartId- , "\", {\n"- , " type: \"pie\",\n"- , " data: {\n"- , " labels: ["- , labels- , "],\n"- , " datasets: [{\n"- , " data: ["- , dataPoints- , "],\n"- , " backgroundColor: ["- , colors- , "]\n"- , " }]\n"- , " },\n"- , " options: {\n"- , " title: { display: true, text: \""- , chartTitle- , "\" }\n"- , " }\n"- , "})}, 100);"- ]- return $- HtmlPlot $- wrapInHTML chartId jsCode (plotWidth config) (plotHeight config)--pieColors :: [T.Text]-pieColors =- [ "rgb(255, 99, 132)"- , "rgb(54, 162, 235)"- , "rgb(255, 206, 86)"- , "rgb(75, 192, 192)"- , "rgb(153, 102, 255)"- , "rgb(255, 159, 64)"- , "rgb(201, 203, 207)"- , "rgb(255, 99, 71)"- , "rgb(60, 179, 113)"- , "rgb(238, 130, 238)"- ]--plotStackedBars ::- (HasCallStack) => T.Text -> [T.Text] -> DataFrame -> IO HtmlPlot-plotStackedBars categoryCol valueColumns = plotStackedBarsWith categoryCol valueColumns (defaultPlotConfig StackedBar)--plotStackedBarsWith ::- (HasCallStack) => T.Text -> [T.Text] -> PlotConfig -> DataFrame -> IO HtmlPlot-plotStackedBarsWith categoryCol valueColumns config df = do- chartId <- generateChartId- let categories = extractStringColumn categoryCol df- uniqueCategories = L.nub categories-- colors =- cycle- [ "rgb(255, 99, 132)"- , "rgb(54, 162, 235)"- , "rgb(255, 206, 86)"- , "rgb(75, 192, 192)"- , "rgb(153, 102, 255)"- , "rgb(255, 159, 64)"- ]-- datasets <- forM (zip valueColumns colors) $ \(col, color) -> do- dataVals <- forM uniqueCategories $ \cat -> do- let indices = [i | (i, c) <- zip [0 ..] categories, c == cat]- allValues = extractNumericColumn col df- values = [allValues !! i | i <- indices, i < length allValues]- return $ sum values- let dataPoints = T.intercalate "," [T.pack (show v) | v <- dataVals]- return $- T.concat- [ " {\n"- , " label: \""- , col- , "\",\n"- , " data: ["- , dataPoints- , "],\n"- , " backgroundColor: \""- , color- , "\"\n"- , " }"- ]-- let datasetsStr = T.intercalate ",\n" datasets- labels = T.intercalate "," ["\"" <> cat <> "\"" | cat <- uniqueCategories]- chartTitle = if T.null (plotTitle config) then "Stacked Bar Chart" else plotTitle config-- jsCode =- T.concat- [ "setTimeout(function() { new Chart(\""- , chartId- , "\", {\n"- , " type: \"bar\",\n"- , " data: {\n"- , " labels: ["- , labels- , "],\n"- , " datasets: [\n"- , datasetsStr- , "\n ]\n"- , " },\n"- , " options: {\n"- , " title: { display: true, text: \""- , chartTitle- , "\" },\n"- , " scales: {\n"- , " xAxes: [{ stacked: true }],\n"- , " yAxes: [{ stacked: true, ticks: { beginAtZero: true } }]\n"- , " }\n"- , " }\n"- , "})}, 100);"- ]-- return $- HtmlPlot $- wrapInHTML chartId jsCode (plotWidth config) (plotHeight config)--plotBoxPlots :: (HasCallStack) => [T.Text] -> DataFrame -> IO HtmlPlot-plotBoxPlots colNames = plotBoxPlotsWith colNames (defaultPlotConfig BoxPlot)--plotBoxPlotsWith ::- (HasCallStack) => [T.Text] -> PlotConfig -> DataFrame -> IO HtmlPlot-plotBoxPlotsWith colNames config df = do- chartId <- generateChartId- boxData <- forM colNames $ \col -> do- let values = extractNumericColumn col df- sorted = L.sort values- n = length values- q1 = sorted !! (n `div` 4)- median = sorted !! (n `div` 2)- q3 = sorted !! (3 * n `div` 4)- minVal = minimum values- maxVal = maximum values- return (col, minVal, q1, median, q3, maxVal)-- let labels = T.intercalate "," ["\"" <> col <> "\"" | (col, _, _, _, _, _) <- boxData]- medians = T.intercalate "," [T.pack (show med) | (_, _, _, med, _, _) <- boxData]- chartTitle = if T.null (plotTitle config) then "Box Plot" else plotTitle config-- jsCode =- T.concat- [ "setTimeout(function() { new Chart(\""- , chartId- , "\", {\n"- , " type: \"bar\",\n"- , " data: {\n"- , " labels: ["- , labels- , "],\n"- , " datasets: [{\n"- , " label: \"Median\",\n"- , " data: ["- , medians- , "],\n"- , " backgroundColor: \"rgba(75, 192, 192, 0.6)\",\n"- , " borderColor: \"rgba(75, 192, 192, 1)\",\n"- , " borderWidth: 1\n"- , " }]\n"- , " },\n"- , " options: {\n"- , " title: { display: true, text: \""- , chartTitle- , " (showing medians)\" },\n"- , " scales: {\n"- , " yAxes: [{ ticks: { beginAtZero: true } }]\n"- , " }\n"- , " }\n"- , "})}, 100);"- ]-- return $- HtmlPlot $- wrapInHTML chartId jsCode (plotWidth config) (plotHeight config)--plotGroupedBarsWith ::- (HasCallStack) => T.Text -> T.Text -> PlotConfig -> DataFrame -> IO HtmlPlot-plotGroupedBarsWith = plotGroupedBarsWithN 10--plotGroupedBarsWithN ::- (HasCallStack) =>- Int -> T.Text -> T.Text -> PlotConfig -> DataFrame -> IO HtmlPlot-plotGroupedBarsWithN n groupCol valCol config df = do- chartId <- generateChartId- let colIsNumeric = isNumericColumnCheck valCol df-- if colIsNumeric- then do- let groups = extractStringColumn groupCol df- values = extractNumericColumn valCol df- m = M.fromListWith (+) (zip groups values)- grouped = map (\v -> (v, m M.! v)) groups- labels = T.intercalate "," ["\"" <> label <> "\"" | (label, _) <- grouped]- dataPoints = T.intercalate "," [T.pack (show val) | (_, val) <- grouped]- chartTitle =- if T.null (plotTitle config)- then groupCol <> " by " <> valCol- else plotTitle config-- jsCode =- T.concat- [ "setTimeout(function() { new Chart(\""- , chartId- , "\", {\n"- , " type: \"bar\",\n"- , " data: {\n"- , " labels: ["- , labels- , "],\n"- , " datasets: [{\n"- , " label: \""- , valCol- , "\",\n"- , " data: ["- , dataPoints- , "],\n"- , " backgroundColor: \"rgba(54, 162, 235, 0.6)\",\n"- , " borderColor: \"rgba(54, 162, 235, 1)\",\n"- , " borderWidth: 1\n"- , " }]\n"- , " },\n"- , " options: {\n"- , " title: { display: true, text: \""- , chartTitle- , "\" },\n"- , " scales: {\n"- , " yAxes: [{ ticks: { beginAtZero: true } }]\n"- , " }\n"- , " }\n"- , "})}, 100);"- ]- return $- HtmlPlot $- wrapInHTML chartId jsCode (plotWidth config) (plotHeight 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]- labels = T.intercalate "," ["\"" <> label <> "\"" | (label, _) <- finalCounts]- dataPoints = T.intercalate "," [T.pack (show val) | (_, val) <- finalCounts]- chartTitle =- if T.null (plotTitle config)- then groupCol <> " by " <> valCol- else plotTitle config-- jsCode =- T.concat- [ "setTimeout(function() { new Chart(\""- , chartId- , "\", {\n"- , " type: \"bar\",\n"- , " data: {\n"- , " labels: ["- , labels- , "],\n"- , " datasets: [{\n"- , " label: \"Count\",\n"- , " data: ["- , dataPoints- , "],\n"- , " backgroundColor: \"rgba(54, 162, 235, 0.6)\",\n"- , " borderColor: \"rgba(54, 162, 235, 1)\",\n"- , " borderWidth: 1\n"- , " }]\n"- , " },\n"- , " options: {\n"- , " title: { display: true, text: \""- , chartTitle- , "\" },\n"- , " scales: {\n"- , " yAxes: [{ ticks: { beginAtZero: true } }]\n"- , " }\n"- , " }\n"- , "})}, 100);"- ]- return $- HtmlPlot $- wrapInHTML chartId jsCode (plotWidth config) (plotHeight config)---- TODO: Move these helpers to a common module.--isNumericColumn :: DataFrame -> T.Text -> Bool-isNumericColumn df colName = maybe False isNumeric (getColumn colName df)--isNumericColumnCheck :: T.Text -> DataFrame -> Bool-isNumericColumnCheck colName df = isNumericColumn df colName--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.Vector a) -> case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl -> V.toList vec- Nothing -> V.toList $ V.map (T.pack . show) vec- UnboxedColumn vec -> V.toList $ VG.map (T.pack . show) (VG.convert vec)- OptionalColumn (vec :: V.Vector (Maybe a)) -> case testEquality (typeRep @a) (typeRep @T.Text) of- Nothing -> V.toList $ V.map (T.pack . show) vec- Just Refl -> V.toList $ V.map (maybe "Nothing" ("Just " <>)) 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. (Columnable a, Show a) => V.Vector a -> [Double]-vectorToDoubles vec =- case testEquality (typeRep @a) (typeRep @Double) of- Just Refl -> V.toList vec- Nothing -> case sIntegral @a of- STrue -> V.toList $ V.map fromIntegral vec- SFalse -> case sFloating @a of- STrue -> V.toList $ V.map realToFrac vec- SFalse -> error $ "Column is not numeric (type: " ++ show (typeRep @a) ++ ")"--unboxedVectorToDoubles ::- forall a. (Columnable 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 sIntegral @a of- STrue -> VU.toList $ VU.map fromIntegral vec- SFalse -> case sFloating @a of- STrue -> VU.toList $ VU.map realToFrac vec- SFalse -> error $ "Column is not numeric (type: " ++ show (typeRep @a) ++ ")"--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 :: V.Vector a) ->- let counts = countValues vec- in case testEquality (typeRep @a) (typeRep @T.Text) of- Nothing -> Just [(T.pack (show k), fromIntegral v) | (k, v) <- counts]- Just Refl -> Just [(k, fromIntegral v) | (k, v) <- counts]- UnboxedColumn vec ->- let counts = countValuesUnboxed vec- in Just [(T.pack (show k), fromIntegral v) | (k, v) <- counts]- OptionalColumn (vec :: V.Vector (Maybe a)) ->- let counts = countValues vec- in case testEquality (typeRep @a) (typeRep @T.Text) of- Nothing -> Just [((T.pack . show) k, fromIntegral v) | (k, v) <- counts]- Just Refl ->- Just- [(maybe "Nothing" ("Just " <>) 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--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--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--plotBarsTopN :: (HasCallStack) => Int -> T.Text -> DataFrame -> IO HtmlPlot-plotBarsTopN n colName = plotBarsTopNWith n colName (defaultPlotConfig Bar)--plotBarsTopNWith ::- (HasCallStack) => Int -> T.Text -> PlotConfig -> DataFrame -> IO HtmlPlot-plotBarsTopNWith n colName config df = do- let config' = config{plotTitle = plotTitle config <> " (Top " <> T.pack (show n) <> ")"}- plotBarsWith colName Nothing config' df--plotValueCounts :: (HasCallStack) => T.Text -> DataFrame -> IO HtmlPlot-plotValueCounts colName = plotValueCountsWith colName 10 (defaultPlotConfig Bar)--plotValueCountsWith ::- (HasCallStack) => T.Text -> Int -> PlotConfig -> DataFrame -> IO HtmlPlot-plotValueCountsWith colName maxBars config df = do- let config' = config{plotTitle = "Value counts for " <> colName}- plotBarsTopNWith maxBars colName config' df--plotAllHistograms :: (HasCallStack) => DataFrame -> IO HtmlPlot-plotAllHistograms df = do- let numericCols = filter (isNumericColumn df) (columnNames df)- xs <- forM numericCols $ \col -> do- plotHistogram col df- let allPlots = L.foldl' (\acc (HtmlPlot contents) -> acc <> "\n" <> contents) "" xs- return (HtmlPlot allPlots)--plotCategoricalSummary :: (HasCallStack) => DataFrame -> IO HtmlPlot-plotCategoricalSummary df = do- let cols = columnNames df- xs <- forM cols $ \col -> do- let counts = getCategoricalCounts col df- case counts of- Just c -> do- if length c > 1- then- ( 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- )- else return (HtmlPlot "")- Nothing -> return (HtmlPlot "")- let allPlots = L.foldl' (\acc (HtmlPlot contents) -> acc <> "\n" <> contents) "" xs- return (HtmlPlot allPlots)--plotBarsWithPercentages :: (HasCallStack) => T.Text -> DataFrame -> IO HtmlPlot-plotBarsWithPercentages colName df = do- let config = (defaultPlotConfig Bar){plotTitle = "Distribution of " <> colName}- plotBarsWith colName Nothing config df--smartPlotBars :: (HasCallStack) => T.Text -> DataFrame -> IO HtmlPlot-smartPlotBars colName df = do- let counts = getCategoricalCounts colName df- case counts of- Just c -> do- let numUnique = length c- config =- (defaultPlotConfig Bar)- { plotTitle = colName <> " (" <> T.pack (show numUnique) <> " unique values)"- }- if numUnique <= 12- then plotBarsWith colName Nothing config df- else plotBarsTopNWith 10 colName config df- Nothing -> plotBars colName df--showInDefaultBrowser :: HtmlPlot -> IO ()-showInDefaultBrowser (HtmlPlot p) = do- plotId <- generateChartId- home <- getHomeDirectory- let operatingSystem = os- let path = "plot-" <> T.unpack plotId <> ".html"-- let fullPath =- if operatingSystem == "mingw32"- then home <> "\\" <> path- else home <> "/" <> path- putStr "Saving plot to: "- putStrLn fullPath- T.writeFile fullPath p- if operatingSystem == "mingw32"- then openFileSilently "start" fullPath- else openFileSilently "xdg-open" fullPath- pure ()--openFileSilently :: FilePath -> FilePath -> IO ()-openFileSilently program path = do- (_, _, _, ph) <-- createProcess- (proc program [path])- { std_in = NoStream- , std_out = NoStream- , std_err = NoStream- }- void (waitForProcess ph)
− src/DataFrame/Errors.hs
@@ -1,156 +0,0 @@-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE InstanceSigs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}--module DataFrame.Errors where--import qualified Data.Text as T-import qualified Data.Vector.Unboxed as VU--import Control.Exception-import Data.Array-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- }--data DataFrameException where- TypeMismatchException ::- forall a b.- (Typeable a, Typeable b) =>- TypeErrorContext a b ->- DataFrameException- AggregatedAndNonAggregatedException :: T.Text -> T.Text -> DataFrameException- ColumnNotFoundException :: T.Text -> T.Text -> [T.Text] -> DataFrameException- EmptyDataSetException :: T.Text -> DataFrameException- InternalException :: T.Text -> DataFrameException- NonColumnReferenceException :: T.Text -> DataFrameException- UnaggregatedException :: T.Text -> DataFrameException- WrongQuantileNumberException :: Int -> DataFrameException- WrongQuantileIndexException :: VU.Vector Int -> Int -> 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 (ColumnNotFoundException columnName callPoint availableColumns) = columnNotFound columnName callPoint availableColumns- show (EmptyDataSetException callPoint) = emptyDataSetError callPoint- show (WrongQuantileNumberException q) = wrongQuantileNumberError q- show (WrongQuantileIndexException qs q) = wrongQuantileIndexError qs q- show (InternalException msg) = "Internal error: " ++ T.unpack msg- show (NonColumnReferenceException msg) = "Expression must be a column reference in: " ++ T.unpack msg- show (UnaggregatedException expr) = "Expression is not fully aggregated: " ++ T.unpack expr- show (AggregatedAndNonAggregatedException expr1 expr2) =- "Cannot combine aggregated and non-aggregated expressions: \n"- ++ T.unpack expr1- ++ "\n"- ++ T.unpack expr2--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"--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)--emptyDataSetError :: T.Text -> String-emptyDataSetError callPoint =- red "\n\n[ERROR] "- ++ T.unpack callPoint- ++ " cannot be called on empty data sets"--wrongQuantileNumberError :: Int -> String-wrongQuantileNumberError q =- red "\n\n[ERROR] "- ++ "Quantile number q should satisfy "- ++ "q >= 2, but here q is "- ++ show q--wrongQuantileIndexError :: VU.Vector Int -> Int -> String-wrongQuantileIndexError qs q =- red "\n\n[ERROR] "- ++ "For quantile number q, "- ++ "each quantile index i "- ++ "should satisfy 0 <= i <= q, "- ++ "but here q is "- ++ show q- ++ " and indexes are "- ++ show qs--addCallPointInfo :: Maybe String -> Maybe String -> String -> String-addCallPointInfo (Just name) (Just cp) err =- err- ++ ( "\n\tThis happened when calling function "- ++ brightGreen cp- ++ " on "- ++ brightGreen name- )-addCallPointInfo Nothing (Just cp) err =- err- ++ ( "\n\tThis happened when calling function "- ++ brightGreen cp- )-addCallPointInfo (Just name) Nothing err =- err- ++ ( "\n\tOn "- ++ name- ++ "\n\n"- )-addCallPointInfo Nothing Nothing err = err--guessColumnName :: T.Text -> [T.Text] -> T.Text-guessColumnName userInput columns = case map (\k -> (editDistance userInput k, k)) columns of- [] -> ""- res -> (snd . minimum) res--editDistance :: T.Text -> T.Text -> Int-editDistance xs ys = table ! (m, n)- where- (m, n) = (T.length xs, T.length ys)- x = array (1, m) (zip [1 ..] (T.unpack xs))- y = array (1, n) (zip [1 ..] (T.unpack ys))-- table :: Array (Int, Int) Int- table = array bnds [(ij, dist ij) | ij <- range bnds]- bnds = ((0, 0), (m, n))-- 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)- ]
− src/DataFrame/Functions.hs
@@ -1,525 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE InstanceSigs #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE UndecidableInstances #-}--module DataFrame.Functions (module DataFrame.Functions, module DataFrame.Operators) where--import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (- DataFrame (..),- empty,- unsafeGetColumn,- )-import DataFrame.Internal.Expression hiding (normalize)-import DataFrame.Internal.Statistics-import DataFrame.Operations.Core--import Control.Applicative-import Control.Monad-import Control.Monad.IO.Class-import qualified Data.Char as Char-import Data.Function-import Data.Functor-import Data.Int-import qualified Data.List as L-import qualified Data.Map as M-import qualified Data.Maybe as Maybe-import qualified Data.Text as T-import Data.Time-import qualified Data.Vector as V-import qualified Data.Vector.Unboxed as VU-import Data.Word-import qualified DataFrame.IO.CSV as CSV-import qualified DataFrame.IO.Parquet as Parquet-import DataFrame.IO.Parquet.Thrift-import DataFrame.Operators-import Debug.Trace (trace)-import Language.Haskell.TH-import qualified Language.Haskell.TH.Syntax as TH-import System.Directory (doesDirectoryExist)-import System.FilePath ((</>))-import System.FilePath.Glob (glob)-import Text.Regex.TDFA-import Prelude hiding (maximum, minimum)-import Prelude as P--lift :: (Columnable a, Columnable b) => (a -> b) -> Expr a -> Expr b-lift f =- Unary (MkUnaryOp{unaryFn = f, unaryName = "unaryUdf", unarySymbol = Nothing})--lift2 ::- (Columnable c, Columnable b, Columnable a) =>- (c -> b -> a) -> Expr c -> Expr b -> Expr a-lift2 f =- Binary- ( MkBinaryOp- { binaryFn = f- , binaryName = "binaryUdf"- , binarySymbol = Nothing- , binaryCommutative = False- , binaryPrecedence = 0- }- )--liftDecorated ::- (Columnable a, Columnable b) =>- (a -> b) -> T.Text -> Maybe T.Text -> Expr a -> Expr b-liftDecorated f name rep = Unary (MkUnaryOp{unaryFn = f, unaryName = name, unarySymbol = rep})--lift2Decorated ::- (Columnable c, Columnable b, Columnable a) =>- (c -> b -> a) ->- T.Text ->- Maybe T.Text ->- Bool ->- Int ->- Expr c ->- Expr b ->- Expr a-lift2Decorated f name rep comm prec =- Binary- ( MkBinaryOp- { binaryFn = f- , binaryName = name- , binarySymbol = rep- , binaryCommutative = comm- , binaryPrecedence = prec- }- )--toDouble :: (Columnable a, Real a) => Expr a -> Expr Double-toDouble =- Unary- ( MkUnaryOp- { unaryFn = realToFrac- , unaryName = "toDouble"- , unarySymbol = Nothing- }- )--infix 8 `div`-div :: (Integral a, Columnable a) => Expr a -> Expr a -> Expr a-div = lift2Decorated Prelude.div "div" (Just "//") False 7--mod :: (Integral a, Columnable a) => Expr a -> Expr a -> Expr a-mod = lift2Decorated Prelude.mod "mod" Nothing False 7--eq :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool-eq = (.==)--lt :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool-lt = (.<)--gt :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool-gt = (.>)--leq :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool-leq = (.<=)--geq :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool-geq = (.>=)--and :: Expr Bool -> Expr Bool -> Expr Bool-and = (.&&)--or :: Expr Bool -> Expr Bool -> Expr Bool-or = (.||)--not :: Expr Bool -> Expr Bool-not =- Unary- (MkUnaryOp{unaryFn = Prelude.not, unaryName = "not", unarySymbol = Just "~"})--count :: (Columnable a) => Expr a -> Expr Int-count = Agg (MergeAgg "count" (0 :: Int) (\c _ -> c + 1) (+) id)--collect :: (Columnable a) => Expr a -> Expr [a]-collect = Agg (FoldAgg "collect" (Just []) (flip (:)))--mode :: (Ord a, Columnable a, Eq a) => Expr a -> Expr a-mode =- Agg- ( CollectAgg- "mode"- ( fst- . L.maximumBy (compare `on` snd)- . M.toList- . V.foldl' (\m e -> M.insertWith (+) e 1 m) M.empty- )- )--minimum :: (Columnable a, Ord a) => Expr a -> Expr a-minimum = Agg (FoldAgg "minimum" Nothing Prelude.min)--maximum :: (Columnable a, Ord a) => Expr a -> Expr a-maximum = Agg (FoldAgg "maximum" Nothing Prelude.max)--sum :: forall a. (Columnable a, Num a) => Expr a -> Expr a-sum = Agg (FoldAgg "sum" Nothing (+))-{-# SPECIALIZE DataFrame.Functions.sum :: Expr Double -> Expr Double #-}-{-# SPECIALIZE DataFrame.Functions.sum :: Expr Int -> Expr Int #-}-{-# INLINEABLE DataFrame.Functions.sum #-}--sumMaybe :: forall a. (Columnable a, Num a) => Expr (Maybe a) -> Expr a-sumMaybe = Agg (CollectAgg "sumMaybe" (P.sum . Maybe.catMaybes . V.toList))--mean :: (Columnable a, Real a) => Expr a -> Expr Double-mean =- Agg- ( MergeAgg- "mean"- (MeanAcc 0.0 0)- (\(MeanAcc s c) x -> MeanAcc (s + realToFrac x) (c + 1))- (\(MeanAcc s1 c1) (MeanAcc s2 c2) -> MeanAcc (s1 + s2) (c1 + c2))- (\(MeanAcc s c) -> if c == 0 then 0 / 0 else s / fromIntegral c)- )--meanMaybe :: forall a. (Columnable a, Real a) => Expr (Maybe a) -> Expr Double-meanMaybe = Agg (CollectAgg "meanMaybe" (mean' . optionalToDoubleVector))--variance :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double-variance = Agg (CollectAgg "variance" variance')--median :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double-median = Agg (CollectAgg "median" median')--medianMaybe :: (Columnable a, Real a) => Expr (Maybe a) -> Expr Double-medianMaybe = Agg (CollectAgg "meanMaybe" (median' . optionalToDoubleVector))--optionalToDoubleVector :: (Real a) => V.Vector (Maybe a) -> VU.Vector Double-optionalToDoubleVector =- VU.fromList- . V.foldl'- (\acc e -> if Maybe.isJust e then realToFrac (Maybe.fromMaybe 0 e) : acc else acc)- []--percentile :: Int -> Expr Double -> Expr Double-percentile n =- Agg- ( CollectAgg- (T.pack $ "percentile " ++ show n)- (percentile' n)- )--stddev :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double-stddev = Agg (CollectAgg "stddev" (sqrt . variance'))--stddevMaybe :: forall a. (Columnable a, Real a) => Expr (Maybe a) -> Expr Double-stddevMaybe = Agg (CollectAgg "stddevMaybe" (sqrt . variance' . optionalToDoubleVector))--zScore :: Expr Double -> Expr Double-zScore c = (c - mean c) / stddev c--pow :: (Columnable a, Num a) => Expr a -> Int -> Expr a-pow = (.^^)--relu :: (Columnable a, Num a, Ord a) => Expr a -> Expr a-relu = liftDecorated (Prelude.max 0) "relu" Nothing--min :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr a-min = lift2Decorated Prelude.min "max" Nothing True 1--max :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr a-max = lift2Decorated Prelude.max "max" Nothing True 1--reduce ::- forall a b.- (Columnable a, Columnable b) => Expr b -> a -> (a -> b -> a) -> Expr a-reduce expr start f = Agg (FoldAgg "foldUdf" (Just start) f) expr--toMaybe :: (Columnable a) => Expr a -> Expr (Maybe a)-toMaybe = liftDecorated Just "toMaybe" Nothing--fromMaybe :: (Columnable a) => a -> Expr (Maybe a) -> Expr a-fromMaybe d = liftDecorated (Maybe.fromMaybe d) "fromMaybe" Nothing--isJust :: (Columnable a) => Expr (Maybe a) -> Expr Bool-isJust = liftDecorated Maybe.isJust "isJust" Nothing--isNothing :: (Columnable a) => Expr (Maybe a) -> Expr Bool-isNothing = liftDecorated Maybe.isNothing "isNothing" Nothing--fromJust :: (Columnable a) => Expr (Maybe a) -> Expr a-fromJust = liftDecorated Maybe.fromJust "fromJust" Nothing--whenPresent ::- forall a b.- (Columnable a, Columnable b) => (a -> b) -> Expr (Maybe a) -> Expr (Maybe b)-whenPresent f = lift (fmap f)--whenBothPresent ::- forall a b c.- (Columnable a, Columnable b, Columnable c) =>- (a -> b -> c) -> Expr (Maybe a) -> Expr (Maybe b) -> Expr (Maybe c)-whenBothPresent f = lift2 (\l r -> f <$> l <*> r)--recode ::- forall a b.- (Columnable a, Columnable b) => [(a, b)] -> Expr a -> Expr (Maybe b)-recode mapping =- Unary- ( MkUnaryOp- { unaryFn = (`lookup` mapping)- , unaryName = "recode " <> T.pack (show mapping)- , unarySymbol = Nothing- }- )--recodeWithCondition ::- forall a b.- (Columnable a, Columnable b) =>- Expr b -> [(Expr a -> Expr Bool, b)] -> Expr a -> Expr b-recodeWithCondition fallback [] value = fallback-recodeWithCondition fallback ((cond, value) : rest) expr = ifThenElse (cond expr) (lit value) (recodeWithCondition fallback rest expr)--recodeWithDefault ::- forall a b.- (Columnable a, Columnable b) => b -> [(a, b)] -> Expr a -> Expr b-recodeWithDefault d mapping =- Unary- ( MkUnaryOp- { unaryFn = Maybe.fromMaybe d . (`lookup` mapping)- , unaryName =- "recodeWithDefault " <> T.pack (show d) <> " " <> T.pack (show mapping)- , unarySymbol = Nothing- }- )--firstOrNothing :: (Columnable a) => Expr [a] -> Expr (Maybe a)-firstOrNothing = liftDecorated Maybe.listToMaybe "firstOrNothing" Nothing--lastOrNothing :: (Columnable a) => Expr [a] -> Expr (Maybe a)-lastOrNothing = liftDecorated (Maybe.listToMaybe . reverse) "lastOrNothing" Nothing--splitOn :: T.Text -> Expr T.Text -> Expr [T.Text]-splitOn delim = liftDecorated (T.splitOn delim) "splitOn" Nothing--match :: T.Text -> Expr T.Text -> Expr (Maybe T.Text)-match regex =- liftDecorated- ((\r -> if T.null r then Nothing else Just r) . (=~ regex))- ("match " <> T.pack (show regex))- Nothing--matchAll :: T.Text -> Expr T.Text -> Expr [T.Text]-matchAll regex =- liftDecorated- (getAllTextMatches . (=~ regex))- ("matchAll " <> T.pack (show regex))- Nothing--parseDate ::- (ParseTime t, Columnable t) => T.Text -> Expr T.Text -> Expr (Maybe t)-parseDate format =- liftDecorated- (parseTimeM True defaultTimeLocale (T.unpack format) . T.unpack)- ("parseDate " <> format)- Nothing--daysBetween :: Expr Day -> Expr Day -> Expr Int-daysBetween =- lift2Decorated- (\d1 d2 -> fromIntegral (diffDays d1 d2))- "daysBetween"- Nothing- True- 2--bind ::- forall a b m.- (Columnable a, Columnable (m a), Monad m, Columnable b, Columnable (m b)) =>- (a -> m b) -> Expr (m a) -> Expr (m b)-bind f = lift (>>= f)---- 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--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--isHaskellIdentifier :: T.Text -> Bool-isHaskellIdentifier t = Prelude.not (isVarId t) || isReservedId t--sanitize :: T.Text -> T.Text-sanitize t- | isValid = t- | isHaskellIdentifier t' = "_" <> t' <> "_"- | otherwise = t'- where- isValid =- Prelude.not (isHaskellIdentifier t)- && isVarId t- && T.all Char.isAlphaNum t- t' = T.map replaceInvalidCharacters . T.filter (Prelude.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- parentheses c = case c of- '(' -> True- ')' -> True- '{' -> True- '}' -> True- '[' -> True- ']' -> True- _ -> False--typeFromString :: [String] -> Q Type-typeFromString [] = fail "No type specified"-typeFromString [t0] = do- let t = normalize t0- case stripBrackets t of- Just inner -> typeFromString [inner] <&> AppT ListT- Nothing- | t == "Text" || t == "Data.Text.Text" || t == "T.Text" ->- pure (ConT ''T.Text)- | otherwise -> do- m <- lookupTypeName t- case m of- Just name -> pure (ConT name)- Nothing -> fail $ "Unsupported type: " ++ t0-typeFromString [tycon, t1] = AppT <$> typeFromString [tycon] <*> typeFromString [t1]-typeFromString [tycon, t1, t2] =- (\outer a b -> AppT (AppT outer a) b)- <$> typeFromString [tycon]- <*> typeFromString [t1]- <*> typeFromString [t2]-typeFromString s = fail $ "Unsupported types: " ++ unwords s--normalize :: String -> String-normalize = dropWhile (== ' ') . reverse . dropWhile (== ' ') . reverse--stripBrackets :: String -> Maybe String-stripBrackets s =- case s of- ('[' : rest)- | P.not (null rest) && last rest == ']' ->- Just (init rest)- _ -> Nothing--declareColumnsFromCsvFile :: String -> DecsQ-declareColumnsFromCsvFile path = do- df <-- liftIO- (CSV.readSeparated (CSV.defaultReadOptions{CSV.numColumns = Just 100}) path)- declareColumns df--declareColumnsFromParquetFile :: String -> DecsQ-declareColumnsFromParquetFile path = do- isDir <- liftIO $ doesDirectoryExist path-- let pat = if isDir then path </> "*.parquet" else path-- matches <- liftIO $ glob pat-- files <- liftIO $ filterM (fmap Prelude.not . doesDirectoryExist) matches- df <-- liftIO $- foldM- ( \acc p -> do- (metadata, _) <- liftIO (Parquet.readMetadataFromPath path)- let d = schemaToEmptyDataFrame (schema metadata)- pure $ acc <> d- )- DataFrame.Internal.DataFrame.empty- files- declareColumns df--schemaToEmptyDataFrame :: [SchemaElement] -> DataFrame-schemaToEmptyDataFrame elems =- let leafElems = filter (\e -> numChildren e == 0) elems- in fromNamedColumns (map schemaElemToColumn leafElems)--schemaElemToColumn :: SchemaElement -> (T.Text, Column)-schemaElemToColumn elem =- let name = elementName elem- in (name, emptyColumnForType (elementType elem))--emptyColumnForType :: TType -> Column-emptyColumnForType = \case- BOOL -> fromList @Bool []- BYTE -> fromList @Word8 []- I16 -> fromList @Int16 []- I32 -> fromList @Int32 []- I64 -> fromList @Int64 []- I96 -> fromList @Int64 []- FLOAT -> fromList @Float []- DOUBLE -> fromList @Double []- STRING -> fromList @T.Text []- other -> error $ "Unsupported parquet type for column: " <> show other--declareColumnsFromCsvWithOpts :: CSV.ReadOptions -> String -> DecsQ-declareColumnsFromCsvWithOpts opts path = do- df <- liftIO (CSV.readSeparated opts path)- declareColumns df--declareColumns :: DataFrame -> DecsQ-declareColumns = declareColumnsWithPrefix' Nothing--declareColumnsWithPrefix :: T.Text -> DataFrame -> DecsQ-declareColumnsWithPrefix prefix = declareColumnsWithPrefix' (Just prefix)--declareColumnsWithPrefix' :: Maybe T.Text -> DataFrame -> DecsQ-declareColumnsWithPrefix' prefix df =- let- names = (map fst . L.sortBy (compare `on` snd) . M.toList . columnIndices) df- types = map (columnTypeString . (`unsafeGetColumn` df)) names- specs =- zipWith- ( \name type_ -> (name, maybe "" (sanitize . (<> "_")) prefix <> sanitize name, type_)- )- names- types- in- fmap concat $ forM specs $ \(raw, nm, tyStr) -> do- ty <- typeFromString (words tyStr)- trace (T.unpack (nm <> " :: Expr " <> T.pack tyStr)) pure ()- let n = mkName (T.unpack nm)- sig <- sigD n [t|Expr $(pure ty)|]- val <- valD (varP n) (normalB [|col $(TH.lift raw)|]) []- pure [sig, val]
− src/DataFrame/IO/CSV.hs
@@ -1,615 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE NumericUnderscores #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.IO.CSV where--import qualified Data.ByteString as BS-import qualified Data.ByteString.Char8 as C-import qualified Data.ByteString.Lazy as BL-import qualified Data.List as L-import qualified Data.Map.Strict as M-import qualified Data.Proxy as P-import qualified Data.Text as T-import qualified Data.Text.Encoding as TE-import qualified Data.Text.IO as TIO-import qualified Data.Vector as V-import qualified Data.Vector.Mutable as VM-import qualified Data.Vector.Unboxed as VU-import qualified Data.Vector.Unboxed.Mutable as VUM--import Data.Csv.Streaming (Records (..))-import qualified Data.Csv.Streaming as CsvStream--import Control.Monad-import Data.Char-import qualified Data.Csv as Csv-import Data.Either-import Data.Function (on)-import Data.Functor-import Data.IORef-import Data.Maybe-import Data.Type.Equality (TestEquality (testEquality))-import Data.Word (Word8)-import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (DataFrame (..))-import DataFrame.Internal.Parsing-import DataFrame.Internal.Schema-import DataFrame.Operations.Typing-import System.IO-import Type.Reflection-import Prelude hiding (concat, takeWhile)--chunkSize :: Int-chunkSize = 16_384--data PagedVector a = PagedVector- { pvChunks :: !(IORef [V.Vector a])- -- ^ Finished chunks (reverse order)- , pvActive :: !(IORef (VM.IOVector a))- -- ^ Current mutable chunk- , pvCount :: !(IORef Int)- -- ^ Items written in current chunk- }--data PagedUnboxedVector a = PagedUnboxedVector- { puvChunks :: !(IORef [VU.Vector a])- , puvActive :: !(IORef (VUM.IOVector a))- , puvCount :: !(IORef Int)- }--data BuilderColumn- = BuilderInt !(PagedUnboxedVector Int) !(PagedUnboxedVector Word8)- | BuilderDouble !(PagedUnboxedVector Double) !(PagedUnboxedVector Word8)- | BuilderText !(PagedVector T.Text) !(PagedUnboxedVector Word8)- | BuilderBS !(PagedVector BS.ByteString) !(PagedUnboxedVector Word8)--newPagedVector :: IO (PagedVector a)-newPagedVector = do- active <- VM.unsafeNew chunkSize- PagedVector <$> newIORef [] <*> newIORef active <*> newIORef 0--newPagedUnboxedVector :: (VUM.Unbox a) => IO (PagedUnboxedVector a)-newPagedUnboxedVector = do- active <- VUM.unsafeNew chunkSize- PagedUnboxedVector <$> newIORef [] <*> newIORef active <*> newIORef 0--appendPagedVector :: PagedVector a -> a -> IO ()-appendPagedVector (PagedVector chunksRef activeRef countRef) !val = do- count <- readIORef countRef- active <- readIORef activeRef-- if count < chunkSize- then do- VM.unsafeWrite active count val- writeIORef countRef $! count + 1- else do- frozen <- V.unsafeFreeze active- modifyIORef' chunksRef (frozen :)-- newActive <- VM.unsafeNew chunkSize- VM.unsafeWrite newActive 0 val-- writeIORef activeRef newActive- writeIORef countRef 1-{-# INLINE appendPagedVector #-}--appendPagedUnboxedVector :: (VUM.Unbox a) => PagedUnboxedVector a -> a -> IO ()-appendPagedUnboxedVector (PagedUnboxedVector chunksRef activeRef countRef) !val = do- count <- readIORef countRef- active <- readIORef activeRef-- if count < chunkSize- then do- VUM.unsafeWrite active count val- writeIORef countRef $! count + 1- else do- frozen <- VU.unsafeFreeze active- modifyIORef' chunksRef (frozen :)-- newActive <- VUM.unsafeNew chunkSize- VUM.unsafeWrite newActive 0 val-- writeIORef activeRef newActive- writeIORef countRef 1-{-# INLINE appendPagedUnboxedVector #-}--freezePagedVector :: PagedVector a -> IO (V.Vector a)-freezePagedVector (PagedVector chunksRef activeRef countRef) = do- count <- readIORef countRef- active <- readIORef activeRef- chunks <- readIORef chunksRef-- writeIORef chunksRef [] -- release chunk references- let frozenChunks = reverse chunks- totalLen = count + sum (map V.length frozenChunks)-- mv <- VM.unsafeNew totalLen-- let copyChunk !offset chunk = do- V.copy (VM.slice offset (V.length chunk) mv) chunk- pure (offset + V.length chunk)-- offset <- foldM copyChunk 0 frozenChunks- VM.copy (VM.slice offset count mv) (VM.slice 0 count active)-- V.unsafeFreeze mv--freezePagedUnboxedVector ::- (VUM.Unbox a) => PagedUnboxedVector a -> IO (VU.Vector a)-freezePagedUnboxedVector (PagedUnboxedVector chunksRef activeRef countRef) = do- count <- readIORef countRef- active <- readIORef activeRef- chunks <- readIORef chunksRef-- writeIORef chunksRef [] -- release chunk references- let frozenChunks = reverse chunks- totalLen = count + sum (map VU.length frozenChunks)-- mv <- VUM.unsafeNew totalLen-- let copyChunk !offset chunk = do- VU.copy (VUM.slice offset (VU.length chunk) mv) chunk- pure (offset + VU.length chunk)-- offset <- foldM copyChunk 0 frozenChunks- VUM.copy (VUM.slice offset count mv) (VUM.slice 0 count active)-- VU.unsafeFreeze mv---- | STANDARD CONFIG TYPES-data HeaderSpec = NoHeader | UseFirstRow | ProvideNames [T.Text]- deriving (Eq, Show)--data TypeSpec- = InferFromSample Int- | SpecifyTypes [(T.Text, SchemaType)]- | NoInference---- | CSV read parameters.-data ReadOptions = ReadOptions- { headerSpec :: HeaderSpec- -- ^ Where to get the headers from. (default: UseFirstRow)- , typeSpec :: TypeSpec- -- ^ Whether/how to infer types. (default: InferFromSample 100)- , safeRead :: Bool- -- ^ Whether to partially parse values into `Maybe`/`Either`. (default: True)- , dateFormat :: String- {- ^ Format of date fields as recognized by the Data.Time.Format module.-- __Examples:__-- @- > parseTimeM True defaultTimeLocale "%Y/%-m/%-d" "2010/3/04" :: Maybe Day- Just 2010-03-04- > parseTimeM True defaultTimeLocale "%d/%-m/%-Y" "04/3/2010" :: Maybe Day- Just 2010-03-04- @- -}- , columnSeparator :: Char- -- ^ Character that separates column values.- , numColumns :: Maybe Int- -- ^ Number of columns to read.- , missingIndicators :: [T.Text]- -- ^ Values that should be read as `Nothing`.- }--shouldInferFromSample :: TypeSpec -> Bool-shouldInferFromSample (InferFromSample _) = True-shouldInferFromSample _ = False--schemaTypeMap :: TypeSpec -> M.Map T.Text SchemaType-schemaTypeMap (SpecifyTypes xs) = M.fromList xs-schemaTypeMap _ = M.empty--typeInferenceSampleSize :: TypeSpec -> Int-typeInferenceSampleSize (InferFromSample n) = n-typeInferenceSampleSize _ = 0--defaultReadOptions :: ReadOptions-defaultReadOptions =- ReadOptions- { headerSpec = UseFirstRow- , typeSpec = InferFromSample 100- , safeRead = True- , dateFormat = "%Y-%m-%d"- , columnSeparator = ','- , numColumns = Nothing- , missingIndicators = []- }--{- | Read CSV file from path and load it into a dataframe.--==== __Example__-@-ghci> D.readCsv ".\/data\/taxi.csv"--@--}-readCsv :: FilePath -> IO DataFrame-readCsv = readSeparated defaultReadOptions--{- | Read CSV file from path and load it into a dataframe.--==== __Example__-@-ghci> D.readCsvWithOpts ".\/data\/taxi.csv" (D.defaultReadOptions { dateFormat = "%d/%-m/%-Y" })--@--}-readCsvWithOpts :: ReadOptions -> FilePath -> IO DataFrame-readCsvWithOpts = readSeparated--{- | Read TSV (tab separated) file from path and load it into a dataframe.--==== __Example__-@-ghci> D.readTsv ".\/data\/taxi.tsv"--@--}-readTsv :: FilePath -> IO DataFrame-readTsv = readSeparated (defaultReadOptions{columnSeparator = '\t'})--{- | Read text file with specified delimiter into a dataframe.--==== __Example__-@-ghci> D.readSeparated (D.defaultReadOptions { columnSeparator = ';' }) ".\/data\/taxi.txt"--@--}-readSeparated :: ReadOptions -> FilePath -> IO DataFrame-readSeparated opts !path = do- let stripUtf8Bom bs = fromMaybe bs (BL.stripPrefix "\xEF\xBB\xBF" bs)- csvData <- stripUtf8Bom <$> BL.readFile path- decodeSeparated opts csvData--decodeSeparated :: ReadOptions -> BL.ByteString -> IO DataFrame-decodeSeparated !opts csvData = do- let sep = columnSeparator opts- let decodeOpts = Csv.defaultDecodeOptions{Csv.decDelimiter = fromIntegral (ord sep)}- let stream = CsvStream.decodeWith decodeOpts Csv.NoHeader csvData-- let peekStream (Cons (Right row) rest) = return (row, rest)- peekStream (Cons (Left err) _) = error $ "Error parsing CSV header: " ++ err- peekStream (Nil Nothing _) = error "Empty CSV file"- peekStream (Nil (Just err) _) = error err-- (firstRowRaw, dataStream) <- peekStream stream-- let (columnNames, rowsToProcess) = case headerSpec opts of- NoHeader ->- ( map (T.pack . show) [0 .. V.length firstRowRaw - 1]- , Cons (Right firstRowRaw) dataStream- )- UseFirstRow ->- ( map (T.strip . TE.decodeUtf8Lenient . BL.toStrict) (V.toList firstRowRaw)- , dataStream- )- ProvideNames ns ->- ( ns ++ drop (length ns) (map (T.pack . show) [0 .. V.length firstRowRaw - 1])- , Cons (Right firstRowRaw) dataStream- )-- (sampleRow, _) <- peekStream rowsToProcess- builderCols <- initializeColumns columnNames (V.toList sampleRow) opts- let !builderColsV = V.fromList builderCols- processStream- (missingIndicators opts)- rowsToProcess- builderColsV- (numColumns opts)-- frozenCols <- V.mapM (finalizeBuilderColumn opts) builderColsV- let numRows = maybe 0 columnLength (frozenCols V.!? 0)-- return $- DataFrame- frozenCols- (M.fromList (zip columnNames [0 ..]))- (numRows, V.length frozenCols)- M.empty -- TODO give typed column references--initializeColumns ::- [T.Text] -> [BL.ByteString] -> ReadOptions -> IO [BuilderColumn]-initializeColumns names row opts = zipWithM initColumn names (map lookupType names)- where- typeMap = schemaTypeMap (typeSpec opts)- -- Return Nothing for columns that should be inferred from BS- shouldInfer = case typeSpec opts of- InferFromSample _ -> True- SpecifyTypes _ -> True- NoInference -> False- lookupType name = M.lookup name typeMap- initColumn :: T.Text -> Maybe SchemaType -> IO BuilderColumn- initColumn _ Nothing | shouldInfer = do- validityRef <- newPagedUnboxedVector- BuilderBS <$> newPagedVector <*> pure validityRef- initColumn _ mtype = do- validityRef <- newPagedUnboxedVector- let t = fromMaybe (schemaType @T.Text) mtype- case t of- SType (_ :: P.Proxy a) -> case testEquality (typeRep @a) (typeRep @Int) of- Just Refl -> BuilderInt <$> newPagedUnboxedVector <*> pure validityRef- Nothing -> case testEquality (typeRep @a) (typeRep @Double) of- Just Refl -> BuilderDouble <$> newPagedUnboxedVector <*> pure validityRef- Nothing -> BuilderText <$> newPagedVector <*> pure validityRef--processStream ::- [T.Text] ->- CsvStream.Records (V.Vector BL.ByteString) ->- V.Vector BuilderColumn ->- Maybe Int ->- IO ()-processStream _ _ _ (Just 0) = return ()-processStream missing (Cons (Right row) rest) cols n =- processRow missing row cols- >> processStream missing rest cols (fmap (flip (-) 1) n)-processStream missing (Cons (Left err) _) _ _ = error ("CSV Parse Error: " ++ err)-processStream missing (Nil _ _) _ _ = return ()--processRow ::- [T.Text] -> V.Vector BL.ByteString -> V.Vector BuilderColumn -> IO ()-processRow missing !vals !cols = V.zipWithM_ processValue vals cols- where- processValue !bs !col = do- let !bs' = BL.toStrict bs- case col of- BuilderInt gv valid -> case readByteStringInt bs' of- Just !i -> appendPagedUnboxedVector gv i >> appendPagedUnboxedVector valid 1- Nothing -> appendPagedUnboxedVector gv 0 >> appendPagedUnboxedVector valid 0- BuilderDouble gv valid -> case readByteStringDouble bs' of- Just !d -> appendPagedUnboxedVector gv d >> appendPagedUnboxedVector valid 1- Nothing -> appendPagedUnboxedVector gv 0.0 >> appendPagedUnboxedVector valid 0- BuilderText gv valid -> do- let !val = T.strip (TE.decodeUtf8Lenient bs')- if isNullish val || val `elem` missing- then appendPagedVector gv T.empty >> appendPagedUnboxedVector valid 0- else appendPagedVector gv val >> appendPagedUnboxedVector valid 1- BuilderBS gv valid -> do- let !bs'' = C.strip bs'- if isNullishBS bs'' || TE.decodeUtf8Lenient bs'' `elem` missing- then appendPagedVector gv BS.empty >> appendPagedUnboxedVector valid 0- else appendPagedVector gv bs'' >> appendPagedUnboxedVector valid 1--freezeBuilderColumn :: BuilderColumn -> IO Column-freezeBuilderColumn (BuilderInt gv validRef) = do- vec <- freezePagedUnboxedVector gv- valid <- freezePagedUnboxedVector validRef- if VU.all (== 1) valid- then return $! UnboxedColumn vec- else constructOptional vec valid-freezeBuilderColumn (BuilderDouble gv validRef) = do- vec <- freezePagedUnboxedVector gv- valid <- freezePagedUnboxedVector validRef- if VU.all (== 1) valid- then return $! UnboxedColumn vec- else constructOptional vec valid-freezeBuilderColumn (BuilderText gv validRef) = do- vec <- freezePagedVector gv- valid <- freezePagedUnboxedVector validRef- if VU.all (== 1) valid- then return $! BoxedColumn vec- else constructOptionalBoxed vec valid-freezeBuilderColumn (BuilderBS _ _) =- error- "freezeBuilderColumn: BuilderBS must be finalized via finalizeBuilderColumn"--finalizeBuilderColumn :: ReadOptions -> BuilderColumn -> IO Column-finalizeBuilderColumn opts (BuilderBS gv validRef) = do- vec <- freezePagedVector gv- valid <- freezePagedUnboxedVector validRef- return $! inferColumnFromBS opts vec valid-finalizeBuilderColumn _ bc = freezeBuilderColumn bc--inferColumnFromBS ::- ReadOptions -> V.Vector BS.ByteString -> VU.Vector Word8 -> Column-inferColumnFromBS opts vec valid =- let sampleN = let n = typeInferenceSampleSize (typeSpec opts) in if n == 0 then 100 else n- dfmt = dateFormat opts- asMaybeFull = V.generate (V.length vec) $ \i ->- if valid VU.! i == 1 then Just (vec V.! i) else Nothing- samples = V.take sampleN asMaybeFull- assumption = makeParsingAssumptionBS dfmt samples- in case assumption of- IntAssumption -> handleBSInt dfmt asMaybeFull- DoubleAssumption -> handleBSDouble asMaybeFull- BoolAssumption -> handleBSBool asMaybeFull- DateAssumption -> handleBSDate dfmt asMaybeFull- TextAssumption -> handleBSText asMaybeFull- NoAssumption -> handleBSNo dfmt asMaybeFull--makeParsingAssumptionBS ::- String -> V.Vector (Maybe BS.ByteString) -> ParsingAssumption-makeParsingAssumptionBS dfmt asMaybe- | V.all (== Nothing) asMaybe = NoAssumption- | vecSameConstructor asMaybe asMaybeBool = BoolAssumption- | vecSameConstructor asMaybe asMaybeInt- && vecSameConstructor asMaybe asMaybeDouble =- IntAssumption- | vecSameConstructor asMaybe asMaybeDouble = DoubleAssumption- | vecSameConstructor asMaybe asMaybeDate = DateAssumption- | otherwise = TextAssumption- where- asMaybeBool = V.map (>>= readByteStringBool) asMaybe- asMaybeInt = V.map (>>= readByteStringInt) asMaybe- asMaybeDouble = V.map (>>= readByteStringDouble) asMaybe- asMaybeDate = V.map (>>= readByteStringDate dfmt) asMaybe--handleBSBool :: V.Vector (Maybe BS.ByteString) -> Column-handleBSBool asMaybe- | parsableAsBool =- maybe (fromVector asMaybeBool) fromVector (sequenceA asMaybeBool)- | otherwise = handleBSText asMaybe- where- asMaybeBool = V.map (>>= readByteStringBool) asMaybe- parsableAsBool = vecSameConstructor asMaybe asMaybeBool--handleBSInt :: String -> V.Vector (Maybe BS.ByteString) -> Column-handleBSInt dfmt asMaybe- | parsableAsInt =- maybe (fromVector asMaybeInt) fromVector (sequenceA asMaybeInt)- | parsableAsDouble =- maybe (fromVector asMaybeDouble) fromVector (sequenceA asMaybeDouble)- | otherwise = handleBSText asMaybe- where- asMaybeInt = V.map (>>= readByteStringInt) asMaybe- asMaybeDouble = V.map (>>= readByteStringDouble) asMaybe- parsableAsInt =- vecSameConstructor asMaybe asMaybeInt- && vecSameConstructor asMaybe asMaybeDouble- parsableAsDouble = vecSameConstructor asMaybe asMaybeDouble--handleBSDouble :: V.Vector (Maybe BS.ByteString) -> Column-handleBSDouble asMaybe- | parsableAsDouble =- maybe (fromVector asMaybeDouble) fromVector (sequenceA asMaybeDouble)- | otherwise = handleBSText asMaybe- where- asMaybeDouble = V.map (>>= readByteStringDouble) asMaybe- parsableAsDouble = vecSameConstructor asMaybe asMaybeDouble--handleBSDate :: String -> V.Vector (Maybe BS.ByteString) -> Column-handleBSDate dfmt asMaybe- | parsableAsDate =- maybe (fromVector asMaybeDate) fromVector (sequenceA asMaybeDate)- | otherwise = handleBSText asMaybe- where- asMaybeDate = V.map (>>= readByteStringDate dfmt) asMaybe- parsableAsDate = vecSameConstructor asMaybe asMaybeDate--handleBSText :: V.Vector (Maybe BS.ByteString) -> Column-handleBSText asMaybe =- let asMaybeText = V.map (fmap TE.decodeUtf8Lenient) asMaybe- in maybe (fromVector asMaybeText) fromVector (sequenceA asMaybeText)--handleBSNo :: String -> V.Vector (Maybe BS.ByteString) -> Column-handleBSNo dfmt asMaybe- | V.all (== Nothing) asMaybe =- fromVector (V.map (const (Nothing :: Maybe T.Text)) asMaybe)- | parsableAsBool =- maybe (fromVector asMaybeBool) fromVector (sequenceA asMaybeBool)- | parsableAsInt =- maybe (fromVector asMaybeInt) fromVector (sequenceA asMaybeInt)- | parsableAsDouble =- maybe (fromVector asMaybeDouble) fromVector (sequenceA asMaybeDouble)- | parsableAsDate =- maybe (fromVector asMaybeDate) fromVector (sequenceA asMaybeDate)- | otherwise = handleBSText asMaybe- where- asMaybeBool = V.map (>>= readByteStringBool) asMaybe- asMaybeInt = V.map (>>= readByteStringInt) asMaybe- asMaybeDouble = V.map (>>= readByteStringDouble) asMaybe- asMaybeDate = V.map (>>= readByteStringDate dfmt) asMaybe- parsableAsBool = vecSameConstructor asMaybe asMaybeBool- parsableAsInt =- vecSameConstructor asMaybe asMaybeInt- && vecSameConstructor asMaybe asMaybeDouble- parsableAsDouble = vecSameConstructor asMaybe asMaybeDouble- parsableAsDate = vecSameConstructor asMaybe asMaybeDate--constructOptional ::- (VU.Unbox a, Columnable a) => VU.Vector a -> VU.Vector Word8 -> IO Column-constructOptional vec valid = do- let size = VU.length vec- mvec <- VM.new size- forM_ [0 .. size - 1] $ \i ->- if (valid VU.! i) == 0- then VM.write mvec i Nothing- else VM.write mvec i (Just (vec VU.! i))- OptionalColumn <$> V.freeze mvec--constructOptionalBoxed :: V.Vector T.Text -> VU.Vector Word8 -> IO Column-constructOptionalBoxed vec valid = do- let size = V.length vec- mvec <- VM.new size- forM_ [0 .. size - 1] $ \i ->- if (valid VU.! i) == 0- then VM.write mvec i Nothing- else VM.write mvec i (Just (vec V.! i))- OptionalColumn <$> V.freeze mvec--writeCsv :: FilePath -> DataFrame -> IO ()-writeCsv = writeSeparated ','--writeTsv :: FilePath -> DataFrame -> IO ()-writeTsv = writeSeparated '\t'--writeSeparated ::- -- | Separator- Char ->- -- | Path to write to- FilePath ->- DataFrame ->- IO ()-writeSeparated c filepath df = withFile filepath WriteMode $ \handle -> do- let (rows, _) = 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- let row = getRowAsText df i- TIO.hPutStrLn handle (T.intercalate "," row)--getRowAsText :: DataFrame -> Int -> [T.Text]-getRowAsText df i = V.ifoldr go [] (columns df)- where- 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- Nothing ->- error $- "Column "- ++ 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- go k (OptionalColumn (c :: V.Vector (Maybe a))) acc = case c V.!? i of- Just e -> case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl -> fromMaybe T.empty e : acc- Nothing -> maybe T.empty (T.pack . show) e : acc- Nothing ->- error $- "Column "- ++ T.unpack (indexMap M.! k)- ++ " has less items than "- ++ "the other columns at index "- ++ show i--stripQuotes :: T.Text -> T.Text-stripQuotes txt =- case T.uncons txt of- Just ('"', rest) ->- case T.unsnoc rest of- Just (middle, '"') -> middle- _ -> txt- _ -> txt
− src/DataFrame/IO/JSON.hs
@@ -1,133 +0,0 @@-{-# LANGUAGE TypeApplications #-}--module DataFrame.IO.JSON (- readJSON,- readJSONEither,-) where--import Control.Monad (forM)-import Data.Aeson-import qualified Data.Aeson.Key as K-import qualified Data.Aeson.KeyMap as KM-import qualified Data.ByteString.Lazy as LBS-import Data.Maybe (catMaybes)-import Data.Scientific (toRealFloat)-import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Vector as V--import qualified DataFrame.Internal.Column as D-import qualified DataFrame.Internal.DataFrame as D-import qualified DataFrame.Operations.Core as D--readJSONEither :: LBS.ByteString -> Either String D.DataFrame-readJSONEither bs = do- v <- note "Could not decode JSON" (decode @Value bs)- rows <- toArrayOfObjects v- let cols :: [Text]- cols =- uniq- . concatMap (map K.toText . KM.keys)- . V.toList- $ rows-- columns <- forM cols $ \c -> do- let col = buildColumn rows c- pure (c, col)-- pure $ D.fromNamedColumns columns--readJSON :: FilePath -> IO D.DataFrame-readJSON path = do- contents <- LBS.readFile path- case readJSONEither contents of- Left err -> fail $ "readJSON: " <> err- Right df -> pure df--toArrayOfObjects :: Value -> Either String (V.Vector Object)-toArrayOfObjects (Array xs)- | V.null xs = Left "Top-level JSON array is empty"- | otherwise = traverse asObject xs-toArrayOfObjects _ =- Left "Top-level JSON value must be a JSON array of objects"--asObject :: Value -> Either String Object-asObject (Object o) = Right o-asObject _ = Left "Expected each element of the array to be an object"--uniq :: (Ord a) => [a] -> [a]-uniq = go mempty- where- go _ [] = []- go seen (x : xs)- | x `elem` seen = go seen xs- | otherwise = x : go (x : seen) xs--note :: e -> Maybe a -> Either e a-note e = maybe (Left e) Right--data ColType- = CTString- | CTNumber- | CTBool- | CTArray- | CTMixed--buildColumn :: V.Vector Object -> Text -> D.Column-buildColumn rows colName =- let key = K.fromText colName- values :: V.Vector (Maybe Value)- values = V.map (KM.lookup key) rows- colType = detectColType values- in case colType of- CTString ->- D.fromVector (fmap (fmap asText) values)- CTNumber ->- D.fromVector (fmap (fmap asDouble) values)- CTBool ->- D.fromVector (fmap (fmap asBool) values)- CTArray ->- D.fromVector (fmap (fmap asArray) values)- CTMixed ->- D.fromVector values--detectColType :: V.Vector (Maybe Value) -> ColType-detectColType vals =- case nonMissing of- [] -> CTMixed- vs- | all isString vs -> CTString- | all isNumber vs -> CTNumber- | all isBool vs -> CTBool- | all isArray vs -> CTArray- | otherwise -> CTMixed- where- nonMissing = catMaybes (V.toList vals)-- isString (String _) = True- isString _ = False-- isNumber (Number _) = True- isNumber _ = False-- isBool (Bool _) = True- isBool _ = False-- isArray (Array _) = True- isArray _ = False--asText :: Value -> Text-asText (String s) = s-asText v = T.pack (show v)--asDouble :: Value -> Double-asDouble (Number s) = toRealFloat @Double s-asDouble v = error $ "asDouble: non-number value: " <> show v--asBool :: Value -> Bool-asBool (Bool b) = b-asBool v = error $ "asBool: non-bool value: " <> show v--asArray :: Value -> V.Vector Value-asArray (Array a) = a-asArray v = error $ "asArray: non-array value: " <> show v
− src/DataFrame/IO/Parquet.hs
@@ -1,584 +0,0 @@-{-# LANGUAGE NumericUnderscores #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.IO.Parquet where--import Control.Exception (throw)-import Control.Monad-import Data.Bits-import qualified Data.ByteString as BSO-import Data.Either-import Data.IORef-import Data.Int-import qualified Data.List as L-import qualified Data.Map as M-import qualified Data.Set as S-import qualified Data.Text as T-import Data.Text.Encoding-import Data.Time-import Data.Time.Clock.POSIX (posixSecondsToUTCTime)-import Data.Word-import DataFrame.Errors (DataFrameException (ColumnNotFoundException))-import qualified DataFrame.Internal.Column as DI-import DataFrame.Internal.DataFrame (DataFrame)-import DataFrame.Internal.Expression (Expr, getColumns)-import qualified DataFrame.Operations.Core as DI-import DataFrame.Operations.Merge ()-import qualified DataFrame.Operations.Subset as DS-import System.FilePath.Glob (glob)--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-import System.Directory (doesDirectoryExist)--import qualified Data.Vector.Unboxed as VU-import System.FilePath ((</>))---- Options -------------------------------------------------------------------{- | Options for reading Parquet data.--These options are applied in this order:--1. predicate filtering-2. column projection-3. row range--Column selection for @selectedColumns@ uses leaf column names only.--}-data ParquetReadOptions = ParquetReadOptions- { selectedColumns :: Maybe [T.Text]- {- ^ Columns to keep in the final dataframe. If set, only these columns are returned.- Predicate-referenced columns are read automatically when needed and projected out after filtering.- -}- , predicate :: Maybe (Expr Bool)- -- ^ Optional row filter expression applied before projection.- , rowRange :: Maybe (Int, Int)- -- ^ Optional row slice @(start, end)@ with start-inclusive/end-exclusive semantics.- }- deriving (Eq, Show)--{- | Default Parquet read options.--Equivalent to:--@-ParquetReadOptions- { selectedColumns = Nothing- , predicate = Nothing- , rowRange = Nothing- }-@--}-defaultParquetReadOptions :: ParquetReadOptions-defaultParquetReadOptions =- ParquetReadOptions- { selectedColumns = Nothing- , predicate = Nothing- , rowRange = Nothing- }---- Public API ----------------------------------------------------------------{- | Read a parquet file from path and load it into a dataframe.--==== __Example__-@-ghci> D.readParquet ".\/data\/mtcars.parquet"-@--}-readParquet :: FilePath -> IO DataFrame-readParquet = readParquetWithOpts defaultParquetReadOptions--{- | Read a Parquet file using explicit read options.--==== __Example__-@-ghci> D.readParquetWithOpts-ghci| (D.defaultParquetReadOptions{D.selectedColumns = Just ["id"], D.rowRange = Just (0, 10)})-ghci| "./tests/data/alltypes_plain.parquet"-@--When @selectedColumns@ is set and @predicate@ references other columns, those predicate columns-are auto-included for decoding, then projected back to the requested output columns.--}--{- | Strip Parquet encoding artifact names (REPEATED wrappers and their single- list-element children) from a raw column path, leaving user-visible names.--}-cleanColPath :: [SNode] -> [String] -> [String]-cleanColPath nodes path = go nodes path False- where- go _ [] _ = []- go ns (p : ps) skipThis =- case L.find (\n -> sName n == p) ns of- Nothing -> []- Just n- | sRep n == REPEATED && not (null (sChildren n)) ->- let skipChildren = length (sChildren n) == 1- in go (sChildren n) ps skipChildren- | skipThis ->- go (sChildren n) ps False- | null (sChildren n) ->- [p]- | otherwise ->- p : go (sChildren n) ps False--readParquetWithOpts :: ParquetReadOptions -> FilePath -> IO DataFrame-readParquetWithOpts opts path = do- (fileMetadata, contents) <- readMetadataFromPath path- let columnPaths = getColumnPaths (drop 1 $ schema fileMetadata)- let columnNames = map fst columnPaths- let leafNames = map (last . T.splitOn ".") columnNames- let availableSelectedColumns = L.nub leafNames- let predicateColumns = maybe [] (L.nub . getColumns) (predicate opts)- let selectedColumnsForRead = case selectedColumns opts of- Nothing -> Nothing- Just selected -> Just (L.nub (selected ++ predicateColumns))- let selectedColumnSet = S.fromList <$> selectedColumnsForRead- let shouldReadColumn colName _ =- case selectedColumnSet of- Nothing -> True- Just selected -> colName `S.member` selected-- case selectedColumnsForRead of- Nothing -> pure ()- Just requested ->- let missing = requested L.\\ availableSelectedColumns- in unless- (L.null missing)- ( throw- ( ColumnNotFoundException- (T.pack $ show missing)- "readParquetWithOpts"- availableSelectedColumns- )- )-- let totalRows = sum (map (fromIntegral . rowGroupNumRows) (rowGroups fileMetadata)) :: Int- colMutMap <- newIORef (M.empty :: M.Map T.Text DI.MutableColumn)- colOffMap <- newIORef (M.empty :: M.Map T.Text Int)- lTypeMap <- newIORef (M.empty :: M.Map T.Text LogicalType)-- let schemaElements = schema fileMetadata- let sNodes = parseAll (drop 1 schemaElements)- 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 cleanPath = cleanColPath sNodes colPath- let colLeafName =- if null cleanPath- then T.pack $ "col_" ++ show colIdx- else T.pack $ last cleanPath- let colFullName =- if null cleanPath- then colLeafName- else T.intercalate "." $ map T.pack cleanPath-- when (shouldReadColumn colLeafName colPath) $ do- let colDataPageOffset = columnDataPageOffset metadata- let colDictionaryPageOffset = columnDictionaryPageOffset metadata- let colStart =- if colDictionaryPageOffset > 0 && colDataPageOffset > colDictionaryPageOffset- then colDictionaryPageOffset- else colDataPageOffset- let colLength = columnTotalCompressedSize metadata-- let columnBytes = BSO.take (fromIntegral colLength) (BSO.drop (fromIntegral colStart) contents)-- pages <- readAllPages (columnCodec metadata) columnBytes-- let maybeTypeLength =- if columnType metadata == PFIXED_LEN_BYTE_ARRAY- then getTypeLength colPath- else Nothing-- let primaryEncoding = maybe EPLAIN fst (L.uncons (columnEncodings metadata))-- let schemaTail = drop 1 (schema fileMetadata)- let (maxDef, maxRep) = levelsForPath schemaTail colPath- let lType =- maybe- LOGICAL_TYPE_UNKNOWN- logicalType- (findLeafSchema schemaTail colPath)- column <-- processColumnPages- (maxDef, maxRep)- pages- (columnType metadata)- primaryEncoding- maybeTypeLength- lType-- mutMapSnap <- readIORef colMutMap- case M.lookup colFullName mutMapSnap of- Nothing -> do- mc <- DI.newMutableColumn totalRows column- DI.copyIntoMutableColumn mc 0 column- modifyIORef colMutMap (M.insert colFullName mc)- modifyIORef colOffMap (M.insert colFullName (DI.columnLength column))- Just mc -> do- off <- (M.! colFullName) <$> readIORef colOffMap- DI.copyIntoMutableColumn mc off column- modifyIORef colOffMap (M.adjust (+ DI.columnLength column) colFullName)- modifyIORef lTypeMap (M.insert colFullName lType)-- finalMutMap <- readIORef colMutMap- finalColMap <-- M.traverseWithKey (\_ mc -> DI.freezeMutableColumn mc) finalMutMap- finalLTypeMap <- readIORef lTypeMap- let orderedColumns =- map- ( \name ->- ( name- , applyLogicalType (finalLTypeMap M.! name) $ finalColMap M.! name- )- )- (filter (`M.member` finalColMap) columnNames)-- pure $ applyReadOptions opts (DI.fromNamedColumns orderedColumns)--{- | Read Parquet files from a directory or glob path.--This is equivalent to calling 'readParquetFilesWithOpts' with 'defaultParquetReadOptions'.--}-readParquetFiles :: FilePath -> IO DataFrame-readParquetFiles = readParquetFilesWithOpts defaultParquetReadOptions--{- | Read multiple Parquet files (directory or glob) using explicit options.--If @path@ is a directory, all non-directory entries are read.-If @path@ is a glob, matching files are read.--For multi-file reads, @rowRange@ is applied once after concatenation (global range semantics).--==== __Example__-@-ghci> D.readParquetFilesWithOpts-ghci| (D.defaultParquetReadOptions{D.selectedColumns = Just ["id"], D.rowRange = Just (0, 5)})-ghci| "./tests/data/alltypes_plain*.parquet"-@--}-readParquetFilesWithOpts :: ParquetReadOptions -> FilePath -> IO DataFrame-readParquetFilesWithOpts opts path = do- isDir <- doesDirectoryExist path-- let pat = if isDir then path </> "*.parquet" else path-- matches <- glob pat-- files <- filterM (fmap not . doesDirectoryExist) matches-- case files of- [] ->- error $- "readParquetFiles: no parquet files found for " ++ path- _ -> do- let optsWithoutRowRange = opts{rowRange = Nothing}- dfs <- mapM (readParquetWithOpts optsWithoutRowRange) files- pure (applyRowRange opts (mconcat dfs))---- Options application -------------------------------------------------------applyRowRange :: ParquetReadOptions -> DataFrame -> DataFrame-applyRowRange opts df =- maybe df (`DS.range` df) (rowRange opts)--applySelectedColumns :: ParquetReadOptions -> DataFrame -> DataFrame-applySelectedColumns opts df =- maybe df (`DS.select` df) (selectedColumns opts)--applyPredicate :: ParquetReadOptions -> DataFrame -> DataFrame-applyPredicate opts df =- maybe df (`DS.filterWhere` df) (predicate opts)--applyReadOptions :: ParquetReadOptions -> DataFrame -> DataFrame-applyReadOptions opts =- applyRowRange opts- . applySelectedColumns opts- . applyPredicate opts---- File and metadata parsing -------------------------------------------------readMetadataFromPath :: FilePath -> IO (FileMetadata, BSO.ByteString)-readMetadataFromPath path = do- contents <- BSO.readFile path- let (size, magicString) = contents `seq` readMetadataSizeFromFooter contents- when (magicString /= "PAR1") $ error "Invalid Parquet file"- meta <- readMetadata contents size- pure (meta, contents)--readMetadataSizeFromFooter :: BSO.ByteString -> (Int, BSO.ByteString)-readMetadataSizeFromFooter contents =- let- footerOffSet = BSO.length contents - 8- sizeBytes =- map- (fromIntegral @Word8 @Int32 . BSO.index contents)- [footerOffSet .. footerOffSet + 3]- size = fromIntegral $ L.foldl' (.|.) 0 $ zipWith shift sizeBytes [0, 8, 16, 24]- magicStringBytes = map (BSO.index contents) [footerOffSet + 4 .. footerOffSet + 7]- magicString = BSO.pack magicStringBytes- in- (size, magicString)---- Schema navigation ---------------------------------------------------------getColumnPaths :: [SchemaElement] -> [(T.Text, Int)]-getColumnPaths schemaElements =- let nodes = parseAll schemaElements- in go nodes 0 [] False- where- go [] _ _ _ = []- go (n : ns) idx path skipThis- | null (sChildren n) =- let newPath = if skipThis then path else path ++ [T.pack (sName n)]- fullPath = T.intercalate "." newPath- in (fullPath, idx) : go ns (idx + 1) path skipThis- | sRep n == REPEATED =- let skipChildren = length (sChildren n) == 1- childLeaves = go (sChildren n) idx path skipChildren- in childLeaves ++ go ns (idx + length childLeaves) path skipThis- | skipThis =- let childLeaves = go (sChildren n) idx path False- in childLeaves ++ go ns (idx + length childLeaves) path skipThis- | otherwise =- let subPath = path ++ [T.pack (sName n)]- childLeaves = go (sChildren n) idx subPath False- in childLeaves ++ go ns (idx + length childLeaves) path skipThis--findLeafSchema :: [SchemaElement] -> [String] -> Maybe SchemaElement-findLeafSchema elems path =- case go (parseAll elems) path of- Just node -> L.find (\e -> T.unpack (elementName e) == sName node) elems- Nothing -> Nothing- where- go [] _ = Nothing- go _ [] = Nothing- go nodes [p] = L.find (\n -> sName n == p) nodes- go nodes (p : ps) = L.find (\n -> sName n == p) nodes >>= \n -> go (sChildren n) ps---- Page decoding -------------------------------------------------------------processColumnPages ::- (Int, Int) ->- [Page] ->- ParquetType ->- ParquetEncoding ->- Maybe Int32 ->- LogicalType ->- IO DI.Column-processColumnPages (maxDef, maxRep) pages pType _ maybeTypeLength lType = 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 Just dictionaryPageHeaderNumValues- else maybeTypeLength- in Just (readDictVals pType (pageBytes dictPage) countForBools)- _ -> Nothing-- cols <- forM dataPages $ \page -> do- let bs0 = pageBytes page- case pageTypeHeader (pageHeader page) of- DataPageHeader{..} -> do- let n = fromIntegral dataPageHeaderNumValues- (defLvls, repLvls, afterLvls) = readLevelsV1 n maxDef maxRep bs0- nPresent = length (filter (== maxDef) defLvls)- decodePageData- dictValsM- (maxDef, maxRep)- pType- maybeTypeLength- dataPageHeaderEncoding- defLvls- repLvls- nPresent- afterLvls- "v1"- DataPageHeaderV2{..} -> do- let n = fromIntegral dataPageHeaderV2NumValues- (defLvls, repLvls, afterLvls) =- readLevelsV2- n- maxDef- maxRep- definitionLevelByteLength- repetitionLevelByteLength- bs0- nPresent- | dataPageHeaderV2NumNulls > 0 =- fromIntegral (dataPageHeaderV2NumValues - dataPageHeaderV2NumNulls)- | otherwise = length (filter (== maxDef) defLvls)- decodePageData- dictValsM- (maxDef, maxRep)- pType- maybeTypeLength- dataPageHeaderV2Encoding- defLvls- repLvls- nPresent- afterLvls- "v2"-- -- Cannot happen as these are filtered out by isDataPage above- DictionaryPageHeader{} -> error "processColumnPages: impossible DictionaryPageHeader"- INDEX_PAGE_HEADER -> error "processColumnPages: impossible INDEX_PAGE_HEADER"- PAGE_TYPE_HEADER_UNKNOWN -> error "processColumnPages: impossible PAGE_TYPE_HEADER_UNKNOWN"- pure $ DI.concatManyColumns cols--decodePageData ::- Maybe DictVals ->- (Int, Int) ->- ParquetType ->- Maybe Int32 ->- ParquetEncoding ->- [Int] ->- [Int] ->- Int ->- BSO.ByteString ->- String ->- IO DI.Column-decodePageData dictValsM (maxDef, maxRep) pType maybeTypeLength encoding defLvls repLvls nPresent afterLvls versionLabel =- case encoding of- EPLAIN ->- case pType of- PBOOLEAN ->- let (vals, _) = readNBool nPresent afterLvls- in pure $- if maxRep > 0- then stitchForRepBool maxRep maxDef repLvls defLvls vals- else toMaybeBool maxDef defLvls vals- PINT32- | maxDef == 0- , maxRep == 0 ->- pure $ DI.fromUnboxedVector (readNInt32Vec nPresent afterLvls)- PINT32 ->- let (vals, _) = readNInt32 nPresent afterLvls- in pure $- if maxRep > 0- then stitchForRepInt32 maxRep maxDef repLvls defLvls vals- else toMaybeInt32 maxDef defLvls vals- PINT64- | maxDef == 0- , maxRep == 0 ->- pure $ DI.fromUnboxedVector (readNInt64Vec nPresent afterLvls)- PINT64 ->- let (vals, _) = readNInt64 nPresent afterLvls- in pure $- if maxRep > 0- then stitchForRepInt64 maxRep maxDef repLvls defLvls vals- else toMaybeInt64 maxDef defLvls vals- PINT96 ->- let (vals, _) = readNInt96Times nPresent afterLvls- in pure $- if maxRep > 0- then stitchForRepUTCTime maxRep maxDef repLvls defLvls vals- else toMaybeUTCTime maxDef defLvls vals- PFLOAT- | maxDef == 0- , maxRep == 0 ->- pure $ DI.fromUnboxedVector (readNFloatVec nPresent afterLvls)- PFLOAT ->- let (vals, _) = readNFloat nPresent afterLvls- in pure $- if maxRep > 0- then stitchForRepFloat maxRep maxDef repLvls defLvls vals- else toMaybeFloat maxDef defLvls vals- PDOUBLE- | maxDef == 0- , maxRep == 0 ->- pure $ DI.fromUnboxedVector (readNDoubleVec nPresent afterLvls)- PDOUBLE ->- let (vals, _) = readNDouble nPresent afterLvls- in pure $- if maxRep > 0- then stitchForRepDouble maxRep maxDef repLvls defLvls vals- else toMaybeDouble maxDef defLvls vals- PBYTE_ARRAY ->- let (raws, _) = readNByteArrays nPresent afterLvls- texts = map decodeUtf8Lenient raws- in pure $- if maxRep > 0- then stitchForRepText maxRep maxDef repLvls defLvls texts- else toMaybeText maxDef defLvls texts- PFIXED_LEN_BYTE_ARRAY ->- case maybeTypeLength of- Just len ->- let (raws, _) = splitFixed nPresent (fromIntegral len) afterLvls- texts = map decodeUtf8Lenient raws- in pure $- if maxRep > 0- then stitchForRepText maxRep maxDef repLvls defLvls texts- else 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 maxRep repLvls defLvls nPresent afterLvls- EPLAIN_DICTIONARY -> decodeDictV1 dictValsM maxDef maxRep repLvls defLvls nPresent afterLvls- other -> error ("Unsupported " ++ versionLabel ++ " encoding: " ++ show other)---- Logical type conversion ---------------------------------------------------applyLogicalType :: LogicalType -> DI.Column -> DI.Column-applyLogicalType (TimestampType _ unit) col =- fromRight col $- DI.mapColumn- (microsecondsToUTCTime . (* (1_000_000 `div` unitDivisor unit)))- col-applyLogicalType (DecimalType precision scale) col- | precision <= 9 = case DI.toVector @Int32 @VU.Vector col of- Right xs ->- DI.fromUnboxedVector $- VU.map (\raw -> fromIntegral @Int32 @Double raw / 10 ^ scale) xs- Left _ -> col- | precision <= 18 = case DI.toVector @Int64 @VU.Vector col of- Right xs ->- DI.fromUnboxedVector $- VU.map (\raw -> fromIntegral @Int64 @Double raw / 10 ^ scale) xs- Left _ -> col- | otherwise = col-applyLogicalType _ col = col--microsecondsToUTCTime :: Int64 -> UTCTime-microsecondsToUTCTime us =- posixSecondsToUTCTime (fromIntegral us / 1_000_000)--unitDivisor :: TimeUnit -> Int64-unitDivisor MILLISECONDS = 1_000-unitDivisor MICROSECONDS = 1_000_000-unitDivisor NANOSECONDS = 1_000_000_000-unitDivisor TIME_UNIT_UNKNOWN = 1--applyScale :: Int32 -> Int32 -> Double-applyScale scale rawValue =- fromIntegral rawValue / (10 ^ scale)
− src/DataFrame/IO/Parquet/Binary.hs
@@ -1,177 +0,0 @@-{-# LANGUAGE TypeApplications #-}--module DataFrame.IO.Parquet.Binary where--import Control.Exception (bracketOnError)-import Control.Monad-import Data.Bits-import qualified Data.ByteString as BS-import qualified Data.ByteString.Unsafe as BSU-import Data.Char-import Data.IORef-import Data.Int-import Data.Word-import qualified Foreign.Marshal.Alloc as Foreign-import qualified Foreign.Ptr as Foreign-import qualified Foreign.Storable as Foreign--littleEndianWord32 :: BS.ByteString -> Word32-littleEndianWord32 bytes- | BS.length bytes >= 4 =- foldr- (.|.)- 0- ( zipWith- (\b i -> fromIntegral b `shiftL` i)- (BS.unpack $ BS.take 4 bytes)- [0, 8, 16, 24]- )- | otherwise =- littleEndianWord32 (BS.take 4 $ bytes `BS.append` BS.pack [0, 0, 0, 0])--littleEndianWord64 :: BS.ByteString -> Word64-littleEndianWord64 bytes =- foldr- (.|.)- 0- ( zipWith- (\b i -> fromIntegral b `shiftL` i)- (BS.unpack $ BS.take 8 bytes)- [0, 8 ..]- )--littleEndianInt32 :: BS.ByteString -> Int32-littleEndianInt32 = fromIntegral . littleEndianWord32--word64ToLittleEndian :: Word64 -> BS.ByteString-word64ToLittleEndian w =- BS.map- (\i -> fromIntegral (w `shiftR` fromIntegral i))- (BS.pack [0, 8, 16, 24, 32, 40, 48, 56])--word32ToLittleEndian :: Word32 -> BS.ByteString-word32ToLittleEndian w =- BS.map (\i -> fromIntegral (w `shiftR` fromIntegral i)) (BS.pack [0, 8, 16, 24])--readUVarInt :: BS.ByteString -> (Word64, BS.ByteString)-readUVarInt xs = loop xs 0 0 0- where- {-- Each input byte contributes:- - lower 7 payload bits- - The high bit (0x80) is the continuation flag: 1 = more bytes follow, 0 = last byte- Why the magic number 10: For a 64‑bit integer we need at most ceil(64 / 7) = 10 bytes- -}- loop :: BS.ByteString -> Word64 -> Int -> Int -> (Word64, BS.ByteString)- loop bs result _ 10 = (result, bs)- loop xs result shift i = case BS.uncons xs of- Nothing -> error "readUVarInt: not enough input bytes"- Just (b, bs) ->- if b < 0x80- then (result .|. (fromIntegral b `shiftL` shift), bs)- else- let payloadBits = fromIntegral (b .&. 0x7f) :: Word64- in loop bs (result .|. (payloadBits `shiftL` shift)) (shift + 7) (i + 1)--readVarIntFromBytes :: (Integral a) => BS.ByteString -> (a, BS.ByteString)-readVarIntFromBytes bs = (fromIntegral n, rem)- where- (n, rem) = loop 0 0 bs- loop shift result bs = case BS.uncons bs of- Nothing -> (result, BS.empty)- Just (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) => BS.ByteString -> (a, BS.ByteString)-readIntFromBytes bs =- let (n, rem) = readVarIntFromBytes bs- u = fromIntegral n :: Word32- in (fromIntegral $ (fromIntegral (u `shiftR` 1) :: Int32) .^. (-(n .&. 1)), rem)--readInt32FromBytes :: BS.ByteString -> (Int32, BS.ByteString)-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 -> BS.ByteString -> IO Word8-readAndAdvance bufferPos buffer = do- pos <- readIORef bufferPos- let b = BS.index buffer pos- modifyIORef bufferPos (+ 1)- return b--readVarIntFromBuffer :: (Integral a) => BS.ByteString -> 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) => BS.ByteString -> 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 :: BS.ByteString -> 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 :: BS.ByteString -> IORef Int -> IO String-readString buf pos = do- nameSize <- readVarIntFromBuffer @Int buf pos- replicateM nameSize (chr . fromIntegral <$> readAndAdvance pos buf)--readByteStringFromBytes :: BS.ByteString -> (BS.ByteString, BS.ByteString)-readByteStringFromBytes xs =- let- (size, rem) = readVarIntFromBytes @Int xs- in- BS.splitAt size rem--readByteString :: BS.ByteString -> IORef Int -> IO BS.ByteString-readByteString buf pos = do- size <- readVarIntFromBuffer @Int buf pos- fillByteStringByWord8 size (\_ -> readAndAdvance pos buf)--readByteString' :: BS.ByteString -> Int64 -> IO BS.ByteString-readByteString' buf size =- fillByteStringByWord8- (fromIntegral size)- ((`readSingleByte` buf) . fromIntegral)--{- | Allocate a fixed-size buffer, repeat the action on each index.-Fill it into the buffer to get a ByteString.--}-fillByteStringByWord8 :: Int -> (Int -> IO Word8) -> IO BS.ByteString-fillByteStringByWord8 size getByte = do- bracketOnError- (Foreign.mallocBytes size :: IO (Foreign.Ptr Word8))- Foreign.free- -- \^ ensures p is freed if (IO Word8) throws.- ( \p -> do- fill 0 p- BSU.unsafePackCStringFinalizer p size (Foreign.free p)- )- where- fill i p- | i >= size = pure ()- | otherwise = getByte i >>= Foreign.pokeByteOff p i >> fill (i + 1) p-{-# INLINE fillByteStringByWord8 #-}--readSingleByte :: Int64 -> BS.ByteString -> IO Word8-readSingleByte pos buffer = return $ BS.index buffer (fromIntegral pos)--readNoAdvance :: IORef Int -> BS.ByteString -> IO Word8-readNoAdvance bufferPos buffer = do- pos <- readIORef bufferPos- return $ BS.index buffer pos
− src/DataFrame/IO/Parquet/ColumnStatistics.hs
@@ -1,19 +0,0 @@-module DataFrame.IO.Parquet.ColumnStatistics where--import qualified Data.ByteString as BS-import Data.Int (Int64)--data ColumnStatistics = ColumnStatistics- { columnMin :: BS.ByteString- , columnMax :: BS.ByteString- , columnNullCount :: Int64- , columnDistictCount :: Int64- , columnMinValue :: BS.ByteString- , columnMaxValue :: BS.ByteString- , isColumnMaxValueExact :: Bool- , isColumnMinValueExact :: Bool- }- deriving (Show, Eq)--emptyColumnStatistics :: ColumnStatistics-emptyColumnStatistics = ColumnStatistics BS.empty BS.empty 0 0 BS.empty BS.empty False False
− src/DataFrame/IO/Parquet/Compression.hs
@@ -1,26 +0,0 @@-module DataFrame.IO.Parquet.Compression where--import Data.Int--data CompressionCodec- = UNCOMPRESSED- | SNAPPY- | GZIP- | LZO- | BROTLI- | LZ4- | ZSTD- | LZ4_RAW- | COMPRESSION_CODEC_UNKNOWN- deriving (Show, Eq)--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/IO/Parquet/Dictionary.hs
@@ -1,303 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE MonoLocalBinds #-}-{-# LANGUAGE OverloadedStrings #-}--module DataFrame.IO.Parquet.Dictionary where--import Control.Monad-import Data.Bits-import qualified Data.ByteString as BS-import Data.IORef-import Data.Int-import Data.Maybe-import qualified Data.Text as T-import Data.Text.Encoding-import Data.Time-import qualified Data.Vector as V-import qualified Data.Vector.Mutable as VM-import qualified Data.Vector.Unboxed as VU-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 GHC.Float--dictCardinality :: DictVals -> Int-dictCardinality (DBool ds) = V.length ds-dictCardinality (DInt32 ds) = V.length ds-dictCardinality (DInt64 ds) = V.length ds-dictCardinality (DInt96 ds) = V.length ds-dictCardinality (DFloat ds) = V.length ds-dictCardinality (DDouble ds) = V.length ds-dictCardinality (DText ds) = V.length ds--readDictVals :: ParquetType -> BS.ByteString -> Maybe Int32 -> DictVals-readDictVals PBOOLEAN bs (Just count) = DBool (V.fromList (take (fromIntegral count) $ readPageBool bs))-readDictVals PINT32 bs _ = DInt32 (V.fromList (readPageInt32 bs))-readDictVals PINT64 bs _ = DInt64 (V.fromList (readPageInt64 bs))-readDictVals PINT96 bs _ = DInt96 (V.fromList (readPageInt96Times bs))-readDictVals PFLOAT bs _ = DFloat (V.fromList (readPageFloat bs))-readDictVals PDOUBLE bs _ = DDouble (V.fromList (readPageWord64 bs))-readDictVals PBYTE_ARRAY bs _ = DText (V.fromList (readPageBytes bs))-readDictVals PFIXED_LEN_BYTE_ARRAY bs (Just len) = DText (V.fromList (readPageFixedBytes bs (fromIntegral len)))-readDictVals t _ _ = error $ "Unsupported dictionary type: " ++ show t--readPageInt32 :: BS.ByteString -> [Int32]-readPageInt32 xs- | BS.null xs = []- | otherwise = littleEndianInt32 (BS.take 4 xs) : readPageInt32 (BS.drop 4 xs)--readPageWord64 :: BS.ByteString -> [Double]-readPageWord64 xs- | BS.null xs = []- | otherwise =- castWord64ToDouble (littleEndianWord64 (BS.take 8 xs))- : readPageWord64 (BS.drop 8 xs)--readPageBytes :: BS.ByteString -> [T.Text]-readPageBytes xs- | BS.null xs = []- | otherwise =- let lenBytes = fromIntegral (littleEndianInt32 $ BS.take 4 xs)- totalBytesRead = lenBytes + 4- in decodeUtf8Lenient (BS.take lenBytes (BS.drop 4 xs))- : readPageBytes (BS.drop totalBytesRead xs)--readPageBool :: BS.ByteString -> [Bool]-readPageBool bs =- concatMap (\b -> map (\i -> (b `shiftR` i) .&. 1 == 1) [0 .. 7]) (BS.unpack bs)--readPageInt64 :: BS.ByteString -> [Int64]-readPageInt64 xs- | BS.null xs = []- | otherwise =- fromIntegral (littleEndianWord64 (BS.take 8 xs)) : readPageInt64 (BS.drop 8 xs)--readPageFloat :: BS.ByteString -> [Float]-readPageFloat xs- | BS.null xs = []- | otherwise =- castWord32ToFloat (littleEndianWord32 (BS.take 4 xs))- : readPageFloat (BS.drop 4 xs)--readNInt96Times :: Int -> BS.ByteString -> ([UTCTime], BS.ByteString)-readNInt96Times 0 bs = ([], bs)-readNInt96Times k bs =- let timestamp96 = BS.take 12 bs- utcTime = int96ToUTCTime timestamp96- bs' = BS.drop 12 bs- (times, rest) = readNInt96Times (k - 1) bs'- in (utcTime : times, rest)--readPageInt96Times :: BS.ByteString -> [UTCTime]-readPageInt96Times bs- | BS.null bs = []- | otherwise =- let (times, _) = readNInt96Times (BS.length bs `div` 12) bs- in times--readPageFixedBytes :: BS.ByteString -> Int -> [T.Text]-readPageFixedBytes xs len- | BS.null xs = []- | otherwise =- decodeUtf8Lenient (BS.take len xs) : readPageFixedBytes (BS.drop len xs) len--{- | Dispatch to the right multi-level list stitching function.-For maxRep=1 uses stitchList; for 2/3 uses stitchList2/3 with computed thresholds.-Threshold formula: defT_r = maxDef - 2*(maxRep - r).--}-stitchForRepBool :: Int -> Int -> [Int] -> [Int] -> [Bool] -> DI.Column-stitchForRepBool maxRep maxDef rep def vals = case maxRep of- 2 -> DI.fromList (stitchList2 (maxDef - 2) maxDef rep def vals)- 3 -> DI.fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef rep def vals)- _ -> DI.fromList (stitchList maxDef rep def vals)--stitchForRepInt32 :: Int -> Int -> [Int] -> [Int] -> [Int32] -> DI.Column-stitchForRepInt32 maxRep maxDef rep def vals = case maxRep of- 2 -> DI.fromList (stitchList2 (maxDef - 2) maxDef rep def vals)- 3 -> DI.fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef rep def vals)- _ -> DI.fromList (stitchList maxDef rep def vals)--stitchForRepInt64 :: Int -> Int -> [Int] -> [Int] -> [Int64] -> DI.Column-stitchForRepInt64 maxRep maxDef rep def vals = case maxRep of- 2 -> DI.fromList (stitchList2 (maxDef - 2) maxDef rep def vals)- 3 -> DI.fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef rep def vals)- _ -> DI.fromList (stitchList maxDef rep def vals)--stitchForRepUTCTime :: Int -> Int -> [Int] -> [Int] -> [UTCTime] -> DI.Column-stitchForRepUTCTime maxRep maxDef rep def vals = case maxRep of- 2 -> DI.fromList (stitchList2 (maxDef - 2) maxDef rep def vals)- 3 -> DI.fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef rep def vals)- _ -> DI.fromList (stitchList maxDef rep def vals)--stitchForRepFloat :: Int -> Int -> [Int] -> [Int] -> [Float] -> DI.Column-stitchForRepFloat maxRep maxDef rep def vals = case maxRep of- 2 -> DI.fromList (stitchList2 (maxDef - 2) maxDef rep def vals)- 3 -> DI.fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef rep def vals)- _ -> DI.fromList (stitchList maxDef rep def vals)--stitchForRepDouble :: Int -> Int -> [Int] -> [Int] -> [Double] -> DI.Column-stitchForRepDouble maxRep maxDef rep def vals = case maxRep of- 2 -> DI.fromList (stitchList2 (maxDef - 2) maxDef rep def vals)- 3 -> DI.fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef rep def vals)- _ -> DI.fromList (stitchList maxDef rep def vals)--stitchForRepText :: Int -> Int -> [Int] -> [Int] -> [T.Text] -> DI.Column-stitchForRepText maxRep maxDef rep def vals = case maxRep of- 2 -> DI.fromList (stitchList2 (maxDef - 2) maxDef rep def vals)- 3 -> DI.fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef rep def vals)- _ -> DI.fromList (stitchList maxDef rep def vals)--{- | Build a Column from a dictionary + index vector + def levels in a single-mutable-vector pass, avoiding the intermediate [a] and [Maybe a] lists.-For maxRep > 0 (list columns) the caller must use the rep-stitching path instead.--}-applyDictToColumn ::- (DI.Columnable a, DI.Columnable (Maybe a)) =>- V.Vector a ->- VU.Vector Int ->- Int -> -- maxDef- [Int] -> -- defLvls- IO DI.Column-applyDictToColumn dict idxs maxDef defLvls- | maxDef == 0 = do- -- All rows are required; no nullability to check.- let n = VU.length idxs- pure $ DI.fromVector (V.generate n (\i -> dict V.! (idxs VU.! i)))- | otherwise = do- let n = length defLvls- mv <- VM.new n- hasNullRef <- newIORef False- let go _ _ [] = pure ()- go !i !j (d : ds)- | d == maxDef = do- VM.write mv i (Just (dict V.! (idxs VU.! j)))- go (i + 1) (j + 1) ds- | otherwise = do- writeIORef hasNullRef True- VM.write mv i Nothing- go (i + 1) j ds- go 0 0 defLvls- vec <- V.freeze mv- hasNull <- readIORef hasNullRef- pure $- if hasNull- then DI.fromVector vec -- VB.Vector (Maybe a) → OptionalColumn- else DI.fromVector (V.map fromJust vec) -- VB.Vector a → BoxedColumn/UnboxedColumn--decodeDictV1 ::- Maybe DictVals ->- Int ->- Int ->- [Int] ->- [Int] ->- Int ->- BS.ByteString ->- IO DI.Column-decodeDictV1 dictValsM maxDef maxRep repLvls 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 (VU.length idxs /= nPresent) $- error $- "dict index count mismatch: got "- ++ show (VU.length idxs)- ++ ", expected "- ++ show nPresent- if maxRep > 0- then do- case dictVals of- DBool ds ->- pure $- stitchForRepBool maxRep maxDef repLvls defLvls (map (ds V.!) (VU.toList idxs))- DInt32 ds ->- pure $- stitchForRepInt32 maxRep maxDef repLvls defLvls (map (ds V.!) (VU.toList idxs))- DInt64 ds ->- pure $- stitchForRepInt64 maxRep maxDef repLvls defLvls (map (ds V.!) (VU.toList idxs))- DInt96 ds ->- pure $- stitchForRepUTCTime- maxRep- maxDef- repLvls- defLvls- (map (ds V.!) (VU.toList idxs))- DFloat ds ->- pure $- stitchForRepFloat maxRep maxDef repLvls defLvls (map (ds V.!) (VU.toList idxs))- DDouble ds ->- pure $- stitchForRepDouble maxRep maxDef repLvls defLvls (map (ds V.!) (VU.toList idxs))- DText ds ->- pure $- stitchForRepText maxRep maxDef repLvls defLvls (map (ds V.!) (VU.toList idxs))- else case dictVals of- -- Fast path: unboxable types, no nulls — one allocation via VU.map- DInt32 ds | maxDef == 0 -> pure $ DI.fromUnboxedVector (VU.map (ds V.!) idxs)- DInt64 ds | maxDef == 0 -> pure $ DI.fromUnboxedVector (VU.map (ds V.!) idxs)- DFloat ds | maxDef == 0 -> pure $ DI.fromUnboxedVector (VU.map (ds V.!) idxs)- DDouble ds | maxDef == 0 -> pure $ DI.fromUnboxedVector (VU.map (ds V.!) idxs)- DBool ds -> applyDictToColumn ds idxs maxDef defLvls- DInt32 ds -> applyDictToColumn ds idxs maxDef defLvls- DInt64 ds -> applyDictToColumn ds idxs maxDef defLvls- DInt96 ds -> applyDictToColumn ds idxs maxDef defLvls- DFloat ds -> applyDictToColumn ds idxs maxDef defLvls- DDouble ds -> applyDictToColumn ds idxs maxDef defLvls- DText ds -> applyDictToColumn ds idxs maxDef defLvls--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
@@ -1,90 +0,0 @@-{-# LANGUAGE BangPatterns #-}--module DataFrame.IO.Parquet.Encoding where--import Data.Bits-import qualified Data.ByteString as BS-import qualified Data.ByteString.Unsafe as BSU-import Data.List (foldl')-import qualified Data.Vector.Unboxed as VU-import Data.Word-import DataFrame.IO.Parquet.Binary--ceilLog2 :: Int -> Int-ceilLog2 x- | x <= 1 = 0- | otherwise = 1 + ceilLog2 ((x + 1) `div` 2)--bitWidthForMaxLevel :: Int -> Int-bitWidthForMaxLevel maxLevel = ceilLog2 (maxLevel + 1)--bytesForBW :: Int -> Int-bytesForBW bw = (bw + 7) `div` 8--unpackBitPacked :: Int -> Int -> BS.ByteString -> ([Word32], BS.ByteString)-unpackBitPacked bw count bs- | count <= 0 = ([], bs)- | BS.null bs = ([], bs)- | otherwise =- let totalBytes = (bw * count + 7) `div` 8- chunk = BS.take totalBytes bs- rest = BS.drop totalBytes bs- in (extractBits bw count chunk, rest)---- | LSB-first bit accumulator: reads each byte once with no intermediate ByteString allocation.-extractBits :: Int -> Int -> BS.ByteString -> [Word32]-extractBits bw count bs = go 0 (0 :: Word64) 0 count- where- !mask = if bw == 32 then maxBound else (1 `shiftL` bw) - 1 :: Word64- !len = BS.length bs- go !byteIdx !acc !accBits !remaining- | remaining <= 0 = []- | accBits >= bw =- fromIntegral (acc .&. mask)- : go byteIdx (acc `shiftR` bw) (accBits - bw) (remaining - 1)- | byteIdx >= len = []- | otherwise =- let b = fromIntegral (BSU.unsafeIndex bs byteIdx) :: Word64- in go (byteIdx + 1) (acc .|. (b `shiftL` accBits)) (accBits + 8) remaining--decodeRLEBitPackedHybrid ::- Int -> Int -> BS.ByteString -> ([Word32], BS.ByteString)-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 :: Int -> BS.ByteString -> [Word32] -> ([Word32], BS.ByteString)- go 0 rest acc = (reverse acc, rest)- go n rest acc- | BS.null rest = (reverse acc, rest)- | 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 (BS.take 4 afterHdr)- afterV = BS.drop nbytes afterHdr- val = word32 .&. mask- takeN = min n runLen- in go (n - takeN) afterV (replicate takeN val ++ acc)--decodeDictIndicesV1 ::- Int -> Int -> BS.ByteString -> (VU.Vector Int, BS.ByteString)-decodeDictIndicesV1 need dictCard bs =- case BS.uncons bs of- Nothing -> error "empty dictionary index stream"- Just (w0, rest0) ->- let bw = fromIntegral w0 :: Int- (u32s, rest1) = decodeRLEBitPackedHybrid bw need rest0- in (VU.fromList (map fromIntegral u32s), rest1)
− src/DataFrame/IO/Parquet/Levels.hs
@@ -1,184 +0,0 @@-module DataFrame.IO.Parquet.Levels where--import qualified Data.ByteString as BS-import Data.Int-import Data.List-import qualified Data.Text as T--import DataFrame.IO.Parquet.Binary-import DataFrame.IO.Parquet.Encoding-import DataFrame.IO.Parquet.Thrift-import DataFrame.IO.Parquet.Types--readLevelsV1 ::- Int -> Int -> Int -> BS.ByteString -> ([Int], [Int], BS.ByteString)-readLevelsV1 n maxDef maxRep bs =- let bwDef = bitWidthForMaxLevel maxDef- bwRep = bitWidthForMaxLevel maxRep-- (repLvls, afterRep) =- if bwRep == 0- then (replicate n 0, bs)- else- let repLength = littleEndianWord32 (BS.take 4 bs)- repData = BS.take (fromIntegral repLength) (BS.drop 4 bs)- afterRepData = BS.drop (4 + fromIntegral repLength) bs- (repVals, _) = decodeRLEBitPackedHybrid bwRep n repData- in (map fromIntegral repVals, afterRepData)-- (defLvls, afterDef) =- if bwDef == 0- then (replicate n 0, afterRep)- else- let defLength = littleEndianWord32 (BS.take 4 afterRep)- defData = BS.take (fromIntegral defLength) (BS.drop 4 afterRep)- afterDefData = BS.drop (4 + fromIntegral defLength) afterRep- (defVals, _) = decodeRLEBitPackedHybrid bwDef n defData- in (map fromIntegral defVals, afterDefData)- in (defLvls, repLvls, afterDef)--readLevelsV2 ::- Int ->- Int ->- Int ->- Int32 ->- Int32 ->- BS.ByteString ->- ([Int], [Int], BS.ByteString)-readLevelsV2 n maxDef maxRep defLen repLen bs =- let (repBytes, afterRepBytes) = BS.splitAt (fromIntegral repLen) bs- (defBytes, afterDefBytes) = BS.splitAt (fromIntegral defLen) afterRepBytes- bwDef = bitWidthForMaxLevel maxDef- bwRep = bitWidthForMaxLevel maxRep- (repLvlsRaw, _) =- if bwRep == 0- then (replicate n 0, repBytes)- else decodeRLEBitPackedHybrid bwRep n repBytes- (defLvlsRaw, _) =- if bwDef == 0- then (replicate n 0, defBytes)- else decodeRLEBitPackedHybrid bwDef n defBytes- in (map fromIntegral defLvlsRaw, map fromIntegral repLvlsRaw, afterDefBytes)--stitchNullable :: Int -> [Int] -> [a] -> [Maybe a]-stitchNullable maxDef = go- 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'---- | Tag leaf values as Just/Nothing according to maxDef.-pairWithVals :: Int -> [(Int, Int)] -> [a] -> [(Int, Int, Maybe a)]-pairWithVals _ [] _ = []-pairWithVals maxDef ((r, d) : rds) vs- | d == maxDef = case vs of- (v : vs') -> (r, d, Just v) : pairWithVals maxDef rds vs'- [] -> error "pairWithVals: value stream exhausted"- | otherwise = (r, d, Nothing) : pairWithVals maxDef rds vs---- | Split triplets into groups; a new group begins whenever rep <= bound.-splitAtRepBound :: Int -> [(Int, Int, Maybe a)] -> [[(Int, Int, Maybe a)]]-splitAtRepBound _ [] = []-splitAtRepBound bound (t : ts) =- let (rest, remaining) = span (\(r, _, _) -> r > bound) ts- in (t : rest) : splitAtRepBound bound remaining--{- | Reconstruct a list column from Dremel encoding levels.-rep=0 starts a new top-level row; def=0 means the entire list slot is null.-Returns one Maybe [Maybe a] per row.--}-stitchList :: Int -> [Int] -> [Int] -> [a] -> [Maybe [Maybe a]]-stitchList maxDef repLvls defLvls vals =- let triplets = pairWithVals maxDef (zip repLvls defLvls) vals- rows = splitAtRepBound 0 triplets- in map toRow rows- where- toRow [] = Nothing- toRow ((_, d, _) : _) | d == 0 = Nothing- toRow grp = Just [v | (_, _, v) <- grp]--{- | Reconstruct a 2-level nested list (maxRep=2) from Dremel triplets.-defT1: def threshold at which the depth-1 element is present (not null).-maxDef: def threshold at which the leaf is present.--}-stitchList2 :: Int -> Int -> [Int] -> [Int] -> [a] -> [Maybe [Maybe [Maybe a]]]-stitchList2 defT1 maxDef repLvls defLvls vals =- let triplets = pairWithVals maxDef (zip repLvls defLvls) vals- in map toRow (splitAtRepBound 0 triplets)- where- toRow [] = Nothing- toRow ((_, d, _) : _) | d == 0 = Nothing- toRow row = Just (map toOuter (splitAtRepBound 1 row))- toOuter [] = Nothing- toOuter ((_, d, _) : _) | d < defT1 = Nothing- toOuter outer = Just (map toLeaf (splitAtRepBound 2 outer))- toLeaf [] = Nothing- toLeaf ((_, _, v) : _) = v--{- | Reconstruct a 3-level nested list (maxRep=3) from Dremel triplets.-defT1, defT2: def thresholds at which depth-1 and depth-2 elements are present.-maxDef: def threshold at which the leaf is present.--}-stitchList3 ::- Int -> Int -> Int -> [Int] -> [Int] -> [a] -> [Maybe [Maybe [Maybe [Maybe a]]]]-stitchList3 defT1 defT2 maxDef repLvls defLvls vals =- let triplets = pairWithVals maxDef (zip repLvls defLvls) vals- in map toRow (splitAtRepBound 0 triplets)- where- toRow [] = Nothing- toRow ((_, d, _) : _) | d == 0 = Nothing- toRow row = Just (map toOuter (splitAtRepBound 1 row))- toOuter [] = Nothing- toOuter ((_, d, _) : _) | d < defT1 = Nothing- toOuter outer = Just (map toMiddle (splitAtRepBound 2 outer))- toMiddle [] = Nothing- toMiddle ((_, d, _) : _) | d < defT2 = Nothing- toMiddle middle = Just (map toLeaf (splitAtRepBound 3 middle))- toLeaf [] = Nothing- toLeaf ((_, _, v) : _) = v--levelsForPath :: [SchemaElement] -> [String] -> (Int, Int)-levelsForPath schemaTail = go 0 0 (parseAll schemaTail)- 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 || sRep n == REPEATED 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
@@ -1,441 +0,0 @@-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.IO.Parquet.Page where--import qualified Codec.Compression.GZip as GZip-import qualified Codec.Compression.Zstd.Streaming as Zstd-import Data.Bits-import qualified Data.ByteString as BS-import qualified Data.ByteString.Lazy as LB-import Data.Int-import Data.Maybe (fromMaybe)-import qualified Data.Vector.Unboxed as VU-import DataFrame.IO.Parquet.Binary-import DataFrame.IO.Parquet.Thrift-import DataFrame.IO.Parquet.Types-import GHC.Float-import qualified Snappy--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--readPage :: CompressionCodec -> BS.ByteString -> IO (Maybe Page, BS.ByteString)-readPage c columnBytes =- if BS.null columnBytes- then pure (Nothing, BS.empty)- else do- let (hdr, rem) = readPageHeader emptyPageHeader columnBytes 0-- let compressed = BS.take (fromIntegral $ compressedPageSize hdr) rem-- fullData <- case c of- ZSTD -> do- result <- Zstd.decompress- drainZstd result compressed []- where- drainZstd (Zstd.Consume f) input acc = do- result <- f input- drainZstd result BS.empty acc- drainZstd (Zstd.Produce chunk next) _ acc = do- result <- next- drainZstd result BS.empty (chunk : acc)- drainZstd (Zstd.Done final) _ acc =- pure $ BS.concat (reverse (final : acc))- drainZstd (Zstd.Error msg msg2) _ _ =- error ("ZSTD error: " ++ msg ++ " " ++ msg2)- SNAPPY -> case Snappy.decompress compressed of- Left e -> error (show e)- Right res -> pure res- UNCOMPRESSED -> pure compressed- GZIP -> pure (LB.toStrict (GZip.decompress (BS.fromStrict compressed)))- other -> error ("Unsupported compression type: " ++ show other)- pure- ( Just $ Page hdr fullData- , BS.drop (fromIntegral $ compressedPageSize hdr) rem- )--readPageHeader ::- PageHeader -> BS.ByteString -> Int16 -> (PageHeader, BS.ByteString)-readPageHeader hdr xs lastFieldId =- if BS.null xs- then (hdr, BS.empty)- else- let- fieldContents = readField' xs lastFieldId- in- case fieldContents of- Nothing -> (hdr, BS.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 -> BS.ByteString -> Int16 -> (PageTypeHeader, BS.ByteString)-readPageTypeHeader INDEX_PAGE_HEADER _ _ = error "readPageTypeHeader: unsupported INDEX_PAGE_HEADER"-readPageTypeHeader PAGE_TYPE_HEADER_UNKNOWN _ _ = error "readPageTypeHeader: unsupported PAGE_TYPE_HEADER_UNKNOWN"-readPageTypeHeader hdr@(DictionaryPageHeader{..}) xs lastFieldId =- if BS.null xs- then (hdr, BS.empty)- else- let- fieldContents = readField' xs lastFieldId- in- case fieldContents of- Nothing -> (hdr, BS.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 = fromMaybe (error "readPageTypeHeader: not enough bytes") (rem BS.!? 0)- in- readPageTypeHeader- (hdr{dictionaryPageIsSorted = isSorted == compactBooleanTrue})- -- TODO(mchavinda): The bool logic here is a little tricky.- -- If the field is a bool then you can get the value- -- from the byte (and you don't have to drop a field).- -- But in other cases you do.- -- This might become a problem later but in the mean- -- time I'm not dropping (this assumes this is the common case).- rem- identifier- n ->- error $ "readPageTypeHeader: unsupported identifier " ++ show n-readPageTypeHeader hdr@(DataPageHeader{..}) xs lastFieldId =- if BS.null xs- then (hdr, BS.empty)- else- let- fieldContents = readField' xs lastFieldId- in- case fieldContents of- Nothing -> (hdr, BS.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 =- if BS.null xs- then (hdr, BS.empty)- else- let- fieldContents = readField' xs lastFieldId- in- case fieldContents of- Nothing -> (hdr, BS.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, rem') = case BS.uncons rem of- Just (b, bytes) -> ((b .&. 0x0f) == compactBooleanTrue, bytes)- Nothing -> (True, BS.empty)- in- readPageTypeHeader- (hdr{dataPageHeaderV2IsCompressed = isCompressed})- rem'- identifier- 8 ->- let- (stats, rem') = readStatisticsFromBytes emptyColumnStatistics rem 0- in- readPageTypeHeader- (hdr{dataPageHeaderV2Statistics = stats})- rem'- identifier- n -> error $ show n--readField' :: BS.ByteString -> Int16 -> Maybe (BS.ByteString, TType, Int16)-readField' bs lastFieldId = case BS.uncons bs of- Nothing -> Nothing- Just (x, xs) ->- if x .&. 0x0f == 0- then Nothing- else- 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 -> BS.ByteString -> IO [Page]-readAllPages codec bytes = go bytes []- where- go bs acc =- if BS.null bs- then return (reverse acc)- else do- (maybePage, remaining) <- readPage codec bs- case maybePage of- Nothing -> return (reverse acc)- Just page -> go remaining (page : acc)---- | Read n Int32 values directly into an unboxed vector (no intermediate list).-readNInt32Vec :: Int -> BS.ByteString -> VU.Vector Int32-readNInt32Vec n bs = VU.generate n (\i -> littleEndianInt32 (BS.drop (4 * i) bs))---- | Read n Int64 values directly into an unboxed vector.-readNInt64Vec :: Int -> BS.ByteString -> VU.Vector Int64-readNInt64Vec n bs = VU.generate n (\i -> fromIntegral (littleEndianWord64 (BS.drop (8 * i) bs)))---- | Read n Float values directly into an unboxed vector.-readNFloatVec :: Int -> BS.ByteString -> VU.Vector Float-readNFloatVec n bs =- VU.generate- n- (\i -> castWord32ToFloat (littleEndianWord32 (BS.drop (4 * i) bs)))---- | Read n Double values directly into an unboxed vector.-readNDoubleVec :: Int -> BS.ByteString -> VU.Vector Double-readNDoubleVec n bs =- VU.generate- n- (\i -> castWord64ToDouble (littleEndianWord64 (BS.drop (8 * i) bs)))--readNInt32 :: Int -> BS.ByteString -> ([Int32], BS.ByteString)-readNInt32 0 bs = ([], bs)-readNInt32 k bs =- let x = littleEndianInt32 (BS.take 4 bs)- bs' = BS.drop 4 bs- (xs, rest) = readNInt32 (k - 1) bs'- in (x : xs, rest)--readNDouble :: Int -> BS.ByteString -> ([Double], BS.ByteString)-readNDouble 0 bs = ([], bs)-readNDouble k bs =- let x = castWord64ToDouble (littleEndianWord64 (BS.take 8 bs))- bs' = BS.drop 8 bs- (xs, rest) = readNDouble (k - 1) bs'- in (x : xs, rest)--readNByteArrays :: Int -> BS.ByteString -> ([BS.ByteString], BS.ByteString)-readNByteArrays 0 bs = ([], bs)-readNByteArrays k bs =- let len = fromIntegral (littleEndianInt32 (BS.take 4 bs)) :: Int- body = BS.take len (BS.drop 4 bs)- bs' = BS.drop (4 + len) bs- (xs, rest) = readNByteArrays (k - 1) bs'- in (body : xs, rest)--readNBool :: Int -> BS.ByteString -> ([Bool], BS.ByteString)-readNBool 0 bs = ([], bs)-readNBool count bs =- let totalBytes = (count + 7) `div` 8- chunk = BS.take totalBytes bs- rest = BS.drop totalBytes bs- bits =- concatMap- (\b -> map (\i -> (b `shiftR` i) .&. 1 == 1) [0 .. 7])- (BS.unpack chunk)- bools = take count bits- in (bools, rest)--readNInt64 :: Int -> BS.ByteString -> ([Int64], BS.ByteString)-readNInt64 0 bs = ([], bs)-readNInt64 k bs =- let x = fromIntegral (littleEndianWord64 (BS.take 8 bs))- bs' = BS.drop 8 bs- (xs, rest) = readNInt64 (k - 1) bs'- in (x : xs, rest)--readNFloat :: Int -> BS.ByteString -> ([Float], BS.ByteString)-readNFloat 0 bs = ([], bs)-readNFloat k bs =- let x = castWord32ToFloat (littleEndianWord32 (BS.take 4 bs))- bs' = BS.drop 4 bs- (xs, rest) = readNFloat (k - 1) bs'- in (x : xs, rest)--splitFixed :: Int -> Int -> BS.ByteString -> ([BS.ByteString], BS.ByteString)-splitFixed 0 _ bs = ([], bs)-splitFixed k len bs =- let body = BS.take len bs- bs' = BS.drop len bs- (xs, rest) = splitFixed (k - 1) len bs'- in (body : xs, rest)--readStatisticsFromBytes ::- ColumnStatistics -> BS.ByteString -> Int16 -> (ColumnStatistics, BS.ByteString)-readStatisticsFromBytes cs xs lastFieldId =- let- fieldContents = readField' xs lastFieldId- in- case fieldContents of- Nothing -> (cs, BS.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 ->- case BS.uncons rem of- Nothing ->- error "readStatisticsFromBytes: not enough bytes"- Just (isMaxValueExact, rem') ->- readStatisticsFromBytes- (cs{isColumnMaxValueExact = isMaxValueExact == compactBooleanTrue})- rem'- identifier- 8 ->- case BS.uncons rem of- Nothing ->- error "readStatisticsFromBytes: not enough bytes"- Just (isMinValueExact, rem') ->- readStatisticsFromBytes- (cs{isColumnMinValueExact = isMinValueExact == compactBooleanTrue})- rem'- identifier- n -> error $ show n
− src/DataFrame/IO/Parquet/Thrift.hs
@@ -1,1184 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE StrictData #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.IO.Parquet.Thrift where--import Control.Monad-import Data.Bits-import qualified Data.ByteString as BS-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.Typeable (Typeable)-import qualified Data.Vector as V-import qualified Data.Vector.Unboxed as VU-import Data.Word-import DataFrame.IO.Parquet.Binary-import DataFrame.IO.Parquet.Types-import qualified DataFrame.Internal.Column as DI-import DataFrame.Internal.DataFrame (DataFrame, unsafeGetColumn)-import qualified DataFrame.Operations.Core as DI-import Type.Reflection (- eqTypeRep,- typeRep,- (:~~:) (HRefl),- )--data SchemaElement = SchemaElement- { elementName :: T.Text- , elementType :: TType- , typeLength :: Int32- , numChildren :: Int32- , fieldId :: Int32- , repetitionType :: RepetitionType- , convertedType :: Int32- , scale :: Int32- , precision :: Int32- , logicalType :: LogicalType- }- deriving (Show, Eq)--createParquetSchema :: DataFrame -> [SchemaElement]-createParquetSchema df = schemaDef : map toSchemaElement (DI.columnNames df)- where- -- The schema always contains an initial element- -- indicating the group of fields.- schemaDef =- SchemaElement- { elementName = "schema"- , elementType = STOP- , typeLength = 0- , numChildren = fromIntegral (snd (DI.dimensions df))- , fieldId = -1- , repetitionType = UNKNOWN_REPETITION_TYPE- , convertedType = 0- , scale = 0- , precision = 0- , logicalType = LOGICAL_TYPE_UNKNOWN- }- toSchemaElement colName =- let- colType :: TType- colType = case unsafeGetColumn colName df of- (DI.BoxedColumn (col :: V.Vector a)) -> haskellToTType @a- (DI.UnboxedColumn (col :: VU.Vector a)) -> haskellToTType @a- (DI.OptionalColumn (col :: V.Vector (Maybe a))) -> haskellToTType @a- lType =- if DI.hasElemType @T.Text (unsafeGetColumn colName df)- || DI.hasElemType @(Maybe T.Text) (unsafeGetColumn colName df)- then STRING_TYPE- else LOGICAL_TYPE_UNKNOWN- in- SchemaElement colName colType 0 0 (-1) OPTIONAL 0 0 0 lType--data KeyValue = KeyValue- { key :: String- , value :: String- }- deriving (Show, Eq)--data FileMetadata = FileMetaData- { version :: Int32- , schema :: [SchemaElement]- , numRows :: Integer- , rowGroups :: [RowGroup]- , keyValueMetadata :: [KeyValue]- , createdBy :: Maybe String- , columnOrders :: [ColumnOrder]- , encryptionAlgorithm :: EncryptionAlgorithm- , footerSigningKeyMetadata :: BS.ByteString- }- deriving (Show, Eq)--data TType- = STOP- | BOOL- | BYTE- | I16- | I32- | I64- | I96- | FLOAT- | DOUBLE- | STRING- | LIST- | SET- | MAP- | STRUCT- | UUID- deriving (Show, Eq)--haskellToTType :: forall a. (Typeable a) => TType-haskellToTType- | is @Bool = BOOL- | is @Int8 = BYTE- | is @Word8 = BYTE- | is @Int16 = I16- | is @Word16 = I16- | is @Int32 = I32- | is @Word32 = I32- | is @Int64 = I64- | is @Word64 = I64- | is @Float = FLOAT- | is @Double = DOUBLE- | is @String = STRING- | is @T.Text = STRING- | is @BS.ByteString = STRING- | otherwise = STOP- where- is :: forall x. (Typeable x) => Bool- is = case eqTypeRep (typeRep @a) (typeRep @x) of- Just HRefl -> True- Nothing -> False--defaultMetadata :: FileMetadata-defaultMetadata =- FileMetaData- { version = 0- , schema = []- , numRows = 0- , rowGroups = []- , keyValueMetadata = []- , createdBy = Nothing- , columnOrders = []- , encryptionAlgorithm = ENCRYPTION_ALGORITHM_UNKNOWN- , footerSigningKeyMetadata = BS.empty- }--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 :: BS.ByteString- }- 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- BS.empty--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 ::- BS.ByteString -> IORef Int -> Int16 -> IO (Maybe (TType, Int16))-readField buf pos lastFieldId = 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 :: BS.ByteString -> 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 -> BS.ByteString -> IORef Int -> IO ()-skipFieldData fieldType buf pos = case fieldType of- BOOL -> return ()- I32 -> void (readIntFromBuffer @Int32 buf pos)- I64 -> void (readIntFromBuffer @Int64 buf pos)- DOUBLE -> void (readIntFromBuffer @Int64 buf pos)- STRING -> void (readByteString buf pos)- LIST -> skipList buf pos- STRUCT -> skipToStructEnd buf pos- _ -> error $ "Unknown field type" ++ show fieldType--skipList :: BS.ByteString -> 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 :: BS.ByteString -> Int -> IO FileMetadata-readMetadata contents size = do- let metadataStartPos = BS.length contents - footerSize - size- let metadataBytes =- BS.pack $- map (BS.index contents) [metadataStartPos .. (metadataStartPos + size - 1)]- let lastFieldId = 0- bufferPos <- newIORef (0 :: Int)- readFileMetaData defaultMetadata metadataBytes bufferPos lastFieldId--readFileMetaData ::- FileMetadata ->- BS.ByteString ->- IORef Int ->- Int16 ->- IO FileMetadata-readFileMetaData metadata metaDataBuf bufferPos lastFieldId = do- fieldContents <- readField metaDataBuf bufferPos lastFieldId- 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- 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- 3 -> do- numRows <- readIntFromBuffer @Int64 metaDataBuf bufferPos- readFileMetaData- (metadata{numRows = fromIntegral numRows})- metaDataBuf- bufferPos- identifier- 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)-- -- TODO actually check elemType agrees (also for all the other underscored _elemType in this module)- let _elemType = toTType sizeAndType- rowGroups <-- replicateM listSize (readRowGroup emptyRowGroup metaDataBuf bufferPos 0)- readFileMetaData- (metadata{rowGroups = rowGroups})- metaDataBuf- bufferPos- identifier- 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- 6 -> do- createdBy <- readString metaDataBuf bufferPos- readFileMetaData- (metadata{createdBy = Just createdBy})- metaDataBuf- bufferPos- identifier- 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- 8 -> do- encryptionAlgorithm <- readEncryptionAlgorithm metaDataBuf bufferPos 0- readFileMetaData- (metadata{encryptionAlgorithm = encryptionAlgorithm})- metaDataBuf- bufferPos- identifier- 9 -> do- footerSigningKeyMetadata <- readByteString metaDataBuf bufferPos- readFileMetaData- (metadata{footerSigningKeyMetadata = footerSigningKeyMetadata})- metaDataBuf- bufferPos- identifier- n -> return $ error $ "UNIMPLEMENTED " ++ show n--readSchemaElement ::- SchemaElement ->- BS.ByteString ->- IORef Int ->- Int16 ->- IO SchemaElement-readSchemaElement schemaElement buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- case fieldContents of- Nothing -> return schemaElement- Just (elemType, identifier) -> case identifier of- 1 -> do- schemaElemType <- toIntegralType <$> readInt32FromBuffer buf pos- readSchemaElement- (schemaElement{elementType = schemaElemType})- buf- pos- identifier- 2 -> do- typeLength <- readInt32FromBuffer buf pos- readSchemaElement- (schemaElement{typeLength = typeLength})- buf- pos- identifier- 3 -> do- fieldRepetitionType <- readInt32FromBuffer buf pos- readSchemaElement- (schemaElement{repetitionType = repetitionTypeFromInt fieldRepetitionType})- buf- pos- identifier- 4 -> do- nameSize <- readVarIntFromBuffer @Int buf pos- if nameSize <= 0- then readSchemaElement schemaElement buf pos identifier- else do- contents <- replicateM nameSize (readAndAdvance pos buf)- readSchemaElement- (schemaElement{elementName = T.pack (map (chr . fromIntegral) contents)})- buf- pos- identifier- 5 -> do- numChildren <- readInt32FromBuffer buf pos- readSchemaElement- (schemaElement{numChildren = numChildren})- buf- pos- identifier- 6 -> do- convertedType <- readInt32FromBuffer buf pos- readSchemaElement- (schemaElement{convertedType = convertedType})- buf- pos- identifier- 7 -> do- scale <- readInt32FromBuffer buf pos- readSchemaElement (schemaElement{scale = scale}) buf pos identifier- 8 -> do- precision <- readInt32FromBuffer buf pos- readSchemaElement- (schemaElement{precision = precision})- buf- pos- identifier- 9 -> do- fieldId <- readInt32FromBuffer buf pos- readSchemaElement- (schemaElement{fieldId = fieldId})- buf- pos- identifier- 10 -> do- logicalType <- readLogicalType LOGICAL_TYPE_UNKNOWN buf pos 0- readSchemaElement- (schemaElement{logicalType = logicalType})- buf- pos- identifier- n -> error ("Uknown schema element: " ++ show n)--readRowGroup ::- RowGroup -> BS.ByteString -> IORef Int -> Int16 -> IO RowGroup-readRowGroup r buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- case fieldContents of- Nothing -> return r- Just (elemType, identifier) -> case identifier of- 1 -> do- sizeAndType <- readAndAdvance pos buf- listSize <-- if (sizeAndType `shiftR` 4) .&. 0x0f == 15- then readVarIntFromBuffer @Int buf pos- else return $ fromIntegral ((sizeAndType `shiftR` 4) .&. 0x0f)- let _elemType = toTType sizeAndType- columnChunks <-- replicateM listSize (readColumnChunk emptyColumnChunk buf pos 0)- readRowGroup (r{rowGroupColumns = columnChunks}) buf pos identifier- 2 -> do- totalBytes <- readIntFromBuffer @Int64 buf pos- readRowGroup (r{totalByteSize = totalBytes}) buf pos identifier- 3 -> do- nRows <- readIntFromBuffer @Int64 buf pos- readRowGroup (r{rowGroupNumRows = nRows}) buf pos identifier- 4 -> return r- 5 -> do- offset <- readIntFromBuffer @Int64 buf pos- readRowGroup (r{fileOffset = offset}) buf pos identifier- 6 -> do- compressedSize <- readIntFromBuffer @Int64 buf pos- readRowGroup- (r{totalCompressedSize = compressedSize})- buf- pos- identifier- 7 -> do- ordinal <- readIntFromBuffer @Int16 buf pos- readRowGroup (r{ordinal = ordinal}) buf pos identifier- _ -> error $ "Unknown row group field: " ++ show identifier--readColumnChunk ::- ColumnChunk -> BS.ByteString -> IORef Int -> Int16 -> IO ColumnChunk-readColumnChunk c buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- 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- 2 -> do- columnChunkMetadataFileOffset <- readIntFromBuffer @Int64 buf pos- readColumnChunk- (c{columnChunkMetadataFileOffset = columnChunkMetadataFileOffset})- buf- pos- identifier- 3 -> do- columnMetadata <- readColumnMetadata emptyColumnMetadata buf pos 0- readColumnChunk- (c{columnMetaData = columnMetadata})- buf- pos- identifier- 4 -> do- columnOffsetIndexOffset <- readIntFromBuffer @Int64 buf pos- readColumnChunk- (c{columnChunkOffsetIndexOffset = columnOffsetIndexOffset})- buf- pos- identifier- 5 -> do- columnOffsetIndexLength <- readInt32FromBuffer buf pos- readColumnChunk- (c{columnChunkOffsetIndexLength = columnOffsetIndexLength})- buf- pos- identifier- 6 -> do- columnChunkColumnIndexOffset <- readIntFromBuffer @Int64 buf pos- readColumnChunk- (c{columnChunkColumnIndexOffset = columnChunkColumnIndexOffset})- buf- pos- identifier- 7 -> do- columnChunkColumnIndexLength <- readInt32FromBuffer buf pos- readColumnChunk- (c{columnChunkColumnIndexLength = columnChunkColumnIndexLength})- buf- pos- identifier- _ -> error "Unknown column chunk"--readColumnMetadata ::- ColumnMetaData ->- BS.ByteString ->- IORef Int ->- Int16 ->- IO ColumnMetaData-readColumnMetadata cm buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- 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- 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- 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- 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- 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- 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- 17 -> return $ error "UNIMPLEMENTED"- _ -> error $ "Unknown column metadata " ++ show identifier--readEncryptionAlgorithm ::- BS.ByteString -> IORef Int -> Int16 -> IO EncryptionAlgorithm-readEncryptionAlgorithm buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- case fieldContents of- Nothing -> return ENCRYPTION_ALGORITHM_UNKNOWN- Just (elemType, identifier) -> case identifier of- 1 -> do- readAesGcmV1- ( AesGcmV1- { aadPrefix = BS.empty- , aadFileUnique = BS.empty- , supplyAadPrefix = False- }- )- buf- pos- 0- 2 -> do- readAesGcmCtrV1- ( AesGcmCtrV1- { aadPrefix = BS.empty- , aadFileUnique = BS.empty- , supplyAadPrefix = False- }- )- buf- pos- 0- n -> return ENCRYPTION_ALGORITHM_UNKNOWN--readColumnOrder ::- BS.ByteString -> IORef Int -> Int16 -> IO ColumnOrder-readColumnOrder buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- case fieldContents of- Nothing -> return COLUMN_ORDER_UNKNOWN- Just (elemType, identifier) -> case identifier of- 1 -> do- -- Read begin struct and stop since this an empty struct.- replicateM_ 2 (readTypeOrder buf pos 0)- return TYPE_ORDER- _ -> return COLUMN_ORDER_UNKNOWN--readAesGcmCtrV1 ::- EncryptionAlgorithm ->- BS.ByteString ->- IORef Int ->- Int16 ->- IO EncryptionAlgorithm-readAesGcmCtrV1 v@(AesGcmCtrV1 aadPrefix aadFileUnique supplyAadPrefix) buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- case fieldContents of- Nothing -> return v- Just (elemType, identifier) -> case identifier of- 1 -> do- aadPrefix <- readByteString buf pos- readAesGcmCtrV1 (v{aadPrefix = aadPrefix}) buf pos identifier- 2 -> do- aadFileUnique <- readByteString buf pos- readAesGcmCtrV1- (v{aadFileUnique = aadFileUnique})- buf- pos- identifier- 3 -> do- supplyAadPrefix <- readAndAdvance pos buf- readAesGcmCtrV1- (v{supplyAadPrefix = supplyAadPrefix == compactBooleanTrue})- buf- pos- identifier- _ -> return ENCRYPTION_ALGORITHM_UNKNOWN-readAesGcmCtrV1 _ _ _ _ =- error "readAesGcmCtrV1 called with non AesGcmCtrV1"--readAesGcmV1 ::- EncryptionAlgorithm ->- BS.ByteString ->- IORef Int ->- Int16 ->- IO EncryptionAlgorithm-readAesGcmV1 v@(AesGcmV1 aadPrefix aadFileUnique supplyAadPrefix) buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- case fieldContents of- Nothing -> return v- Just (elemType, identifier) -> case identifier of- 1 -> do- aadPrefix <- readByteString buf pos- readAesGcmV1 (v{aadPrefix = aadPrefix}) buf pos identifier- 2 -> do- aadFileUnique <- readByteString buf pos- readAesGcmV1 (v{aadFileUnique = aadFileUnique}) buf pos identifier- 3 -> do- supplyAadPrefix <- readAndAdvance pos buf- readAesGcmV1- (v{supplyAadPrefix = supplyAadPrefix == compactBooleanTrue})- buf- pos- identifier- _ -> return ENCRYPTION_ALGORITHM_UNKNOWN-readAesGcmV1 _ _ _ _ =- error "readAesGcmV1 called with non AesGcmV1"--readTypeOrder ::- BS.ByteString -> IORef Int -> Int16 -> IO ColumnOrder-readTypeOrder buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- case fieldContents of- Nothing -> return TYPE_ORDER- Just (elemType, identifier) ->- if elemType == STOP- then return TYPE_ORDER- else readTypeOrder buf pos identifier--readKeyValue ::- KeyValue -> BS.ByteString -> IORef Int -> Int16 -> IO KeyValue-readKeyValue kv buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- 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- 2 -> do- v <- readString buf pos- readKeyValue (kv{value = v}) buf pos identifier- _ -> error "Unknown kv"--readPageEncodingStats ::- PageEncodingStats ->- BS.ByteString ->- IORef Int ->- Int16 ->- IO PageEncodingStats-readPageEncodingStats pes buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- 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- _ -> error "Unknown page encoding stats"--readParquetEncoding ::- BS.ByteString -> IORef Int -> Int16 -> IO ParquetEncoding-readParquetEncoding buf pos lastFieldId = parquetEncodingFromInt <$> readInt32FromBuffer buf pos--readStatistics ::- ColumnStatistics ->- BS.ByteString ->- IORef Int ->- Int16 ->- IO ColumnStatistics-readStatistics cs buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- 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- 2 -> do- minInBytes <- readByteString buf pos- readStatistics (cs{columnMin = minInBytes}) buf pos identifier- 3 -> do- nullCount <- readIntFromBuffer @Int64 buf pos- readStatistics (cs{columnNullCount = nullCount}) buf pos identifier- 4 -> do- distinctCount <- readIntFromBuffer @Int64 buf pos- readStatistics- (cs{columnDistictCount = distinctCount})- buf- pos- identifier- 5 -> do- maxInBytes <- readByteString buf pos- readStatistics (cs{columnMaxValue = maxInBytes}) buf pos identifier- 6 -> do- minInBytes <- readByteString buf pos- readStatistics (cs{columnMinValue = minInBytes}) buf pos identifier- 7 -> do- isMaxValueExact <- readAndAdvance pos buf- readStatistics- (cs{isColumnMaxValueExact = isMaxValueExact == compactBooleanTrue})- buf- pos- identifier- 8 -> do- isMinValueExact <- readAndAdvance pos buf- readStatistics- (cs{isColumnMinValueExact = isMinValueExact == compactBooleanTrue})- buf- pos- identifier- _ -> error "Unknown statistics"--readSizeStatistics ::- SizeStatistics ->- BS.ByteString ->- IORef Int ->- Int16 ->- IO SizeStatistics-readSizeStatistics ss buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- 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- 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- 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- _ -> error "Unknown size statistics"--footerSize :: Int-footerSize = 8--toIntegralType :: Int32 -> TType-toIntegralType n- | n == 0 = BOOL- | n == 1 = I32- | n == 2 = I64- | n == 3 = I96- | n == 4 = FLOAT- | n == 5 = DOUBLE- | n == 6 = STRING- | n == 7 = STRING- | otherwise = error ("Unknown type in schema: " ++ show n)--readLogicalType ::- LogicalType -> BS.ByteString -> IORef Int -> Int16 -> IO LogicalType-readLogicalType logicalType buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- case fieldContents of- Nothing -> pure logicalType- Just (elemType, identifier) -> case identifier of- 1 -> do- -- This is an empty enum and is read as a field.- _ <- readField buf pos 0- readLogicalType STRING_TYPE buf pos identifier- 2 -> do- _ <- readField buf pos 0- readLogicalType MAP_TYPE buf pos identifier- 3 -> do- _ <- readField buf pos 0- readLogicalType LIST_TYPE buf pos identifier- 4 -> do- _ <- readField buf pos 0- readLogicalType ENUM_TYPE buf pos identifier- 5 -> do- decimal <- readDecimalType 0 0 buf pos 0- readLogicalType decimal buf pos identifier- 6 -> do- _ <- readField buf pos 0- readLogicalType DATE_TYPE buf pos identifier- 7 -> do- time <- readTimeType False MILLISECONDS buf pos 0- readLogicalType time buf pos identifier- 8 -> do- timestamp <- readTimestampType False MILLISECONDS buf pos 0- readLogicalType timestamp buf pos identifier- -- Apparently reserved for interval types- 9 -> do- _ <- readField buf pos 0- readLogicalType LOGICAL_TYPE_UNKNOWN buf pos identifier- 10 -> do- intType <- readIntType 0 False buf pos 0- readLogicalType intType buf pos identifier- 11 -> do- _ <- readField buf pos 0- readLogicalType LOGICAL_TYPE_UNKNOWN buf pos identifier- 12 -> do- _ <- readField buf pos 0- readLogicalType JSON_TYPE buf pos identifier- 13 -> do- _ <- readField buf pos 0- readLogicalType BSON_TYPE buf pos identifier- 14 -> do- _ <- readField buf pos 0- readLogicalType UUID_TYPE buf pos identifier- 15 -> do- _ <- readField buf pos 0- readLogicalType FLOAT16_TYPE buf pos identifier- 16 -> error "Variant fields are unsupported"- 17 -> error "Geometry fields are unsupported"- 18 -> error "Geography fields are unsupported"- n -> error $ "Unknown logical type field: " ++ show n--readIntType ::- Int8 ->- Bool ->- BS.ByteString ->- IORef Int ->- Int16 ->- IO LogicalType-readIntType bitWidth intIsSigned buf pos lastFieldId = do- t <- readAndAdvance pos buf- if t .&. 0x0f == 0- then return (IntType bitWidth intIsSigned)- 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- bitWidth' <- readAndAdvance pos buf- readIntType (fromIntegral bitWidth') intIsSigned buf pos identifier- 2 -> do- let intIsSigned' = (t .&. 0x0f) == compactBooleanTrue- readIntType bitWidth intIsSigned' buf pos identifier- _ -> error $ "UNKNOWN field ID for IntType: " ++ show identifier--readDecimalType ::- Int32 ->- Int32 ->- BS.ByteString ->- IORef Int ->- Int16 ->- IO LogicalType-readDecimalType precision scale buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- case fieldContents of- Nothing -> return (DecimalType precision scale)- Just (elemType, identifier) -> case identifier of- 1 -> do- scale' <- readInt32FromBuffer buf pos- readDecimalType precision scale' buf pos identifier- 2 -> do- precision' <- readInt32FromBuffer buf pos- readDecimalType precision' scale buf pos identifier- _ -> error $ "UNKNOWN field ID for DecimalType" ++ show identifier--readTimeType ::- Bool ->- TimeUnit ->- BS.ByteString ->- IORef Int ->- Int16 ->- IO LogicalType-readTimeType isAdjustedToUTC unit buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- case fieldContents of- Nothing -> return (TimeType{isAdjustedToUTC = isAdjustedToUTC, unit = unit})- Just (elemType, identifier) -> case identifier of- 1 -> do- let isAdjustedToUTC' = elemType == toTType compactBooleanTrue- readTimeType isAdjustedToUTC' unit buf pos identifier- 2 -> do- unit' <- readUnit TIME_UNIT_UNKNOWN buf pos 0- readTimeType isAdjustedToUTC unit' buf pos identifier- _ -> error $ "UNKNOWN field ID for TimeType" ++ show identifier--readTimestampType ::- Bool ->- TimeUnit ->- BS.ByteString ->- IORef Int ->- Int16 ->- IO LogicalType-readTimestampType isAdjustedToUTC unit buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- case fieldContents of- Nothing -> return (TimestampType{isAdjustedToUTC = isAdjustedToUTC, unit = unit})- Just (elemType, identifier) -> case identifier of- 1 -> do- let isAdjustedToUTC' = elemType == toTType compactBooleanTrue- readTimestampType isAdjustedToUTC' unit buf pos identifier- 2 -> do- unit' <- readUnit TIME_UNIT_UNKNOWN buf pos 0- readTimestampType isAdjustedToUTC unit' buf pos identifier- _ -> error $ "UNKNOWN field ID for TimestampType " ++ show identifier--readUnit :: TimeUnit -> BS.ByteString -> IORef Int -> Int16 -> IO TimeUnit-readUnit unit buf pos lastFieldId = do- fieldContents <- readField buf pos lastFieldId- case fieldContents of- Nothing -> return unit- Just (elemType, identifier) -> case identifier of- 1 -> do- _ <- readField buf pos 0- readUnit MILLISECONDS buf pos identifier- 2 -> do- _ <- readField buf pos 0- readUnit MICROSECONDS buf pos identifier- 3 -> do- _ <- readField buf pos 0- readUnit NANOSECONDS buf pos identifier- n -> error $ "Unknown time unit: " ++ show n
− src/DataFrame/IO/Parquet/Time.hs
@@ -1,62 +0,0 @@-{-# LANGUAGE NumericUnderscores #-}--module DataFrame.IO.Parquet.Time where--import qualified Data.ByteString as BS-import Data.Time-import Data.Word--import DataFrame.IO.Parquet.Binary--int96ToUTCTime :: BS.ByteString -> UTCTime-int96ToUTCTime bytes- | BS.length bytes /= 12 = error "INT96 must be exactly 12 bytes"- | otherwise =- let (nanosBytes, julianBytes) = BS.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 + 32_044- b = (4 * a + 3) `div` 146_097- c = a - (146_097 * 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 -> BS.ByteString-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 `BS.append` 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- - 32_045
− src/DataFrame/IO/Parquet/Types.hs
@@ -1,309 +0,0 @@-module DataFrame.IO.Parquet.Types where--import qualified Data.ByteString as BS-import Data.Int-import qualified Data.Text as T-import Data.Time-import qualified Data.Vector as V--data ParquetType- = PBOOLEAN- | PINT32- | PINT64- | PINT96- | PFLOAT- | PDOUBLE- | PBYTE_ARRAY- | PFIXED_LEN_BYTE_ARRAY- | PARQUET_TYPE_UNKNOWN- deriving (Show, Eq)--parquetTypeFromInt :: Int32 -> ParquetType-parquetTypeFromInt 0 = PBOOLEAN-parquetTypeFromInt 1 = PINT32-parquetTypeFromInt 2 = PINT64-parquetTypeFromInt 3 = PINT96-parquetTypeFromInt 4 = PFLOAT-parquetTypeFromInt 5 = PDOUBLE-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 :: BS.ByteString- , columnMax :: BS.ByteString- , columnNullCount :: Int64- , columnDistictCount :: Int64- , columnMinValue :: BS.ByteString- , columnMaxValue :: BS.ByteString- , isColumnMaxValueExact :: Bool- , isColumnMinValueExact :: Bool- }- deriving (Show, Eq)--emptyColumnStatistics :: ColumnStatistics-emptyColumnStatistics = ColumnStatistics BS.empty BS.empty 0 0 BS.empty BS.empty False False--data ColumnCryptoMetadata- = COLUMN_CRYPTO_METADATA_UNKNOWN- | ENCRYPTION_WITH_FOOTER_KEY- | EncryptionWithColumnKey- { columnCryptPathInSchema :: [String]- , columnKeyMetadata :: BS.ByteString- }- 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 :: BS.ByteString- , aadFileUnique :: BS.ByteString- , supplyAadPrefix :: Bool- }- | AesGcmCtrV1- { aadPrefix :: BS.ByteString- , aadFileUnique :: BS.ByteString- , supplyAadPrefix :: Bool- }- deriving (Show, Eq)--data DictVals- = DBool (V.Vector Bool)- | DInt32 (V.Vector Int32)- | DInt64 (V.Vector Int64)- | DInt96 (V.Vector UTCTime)- | DFloat (V.Vector Float)- | DDouble (V.Vector Double)- | DText (V.Vector T.Text)- deriving (Show, Eq)--data Page = Page- { pageHeader :: PageHeader- , pageBytes :: BS.ByteString- }- 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/IO/Unstable/CSV.hs
@@ -1,350 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE CApiFFI #-}-{-# LANGUAGE ForeignFunctionInterface #-}--module DataFrame.IO.Unstable.CSV (- fastReadCsvUnstable,- readCsvUnstable,- fastReadTsvUnstable,- readTsvUnstable,- getDelimiterIndices,-) where--import qualified Data.Vector as Vector-import qualified Data.Vector.Storable as VS-import Data.Vector.Storable.Mutable (- grow,- unsafeFromForeignPtr,- )-import qualified Data.Vector.Storable.Mutable as VSM-import System.IO.MMap (- Mode (WriteCopy),- mmapFileForeignPtr,- )--import Foreign (- Ptr,- castForeignPtr,- castPtr,- mallocArray,- newForeignPtr_,- )-import Foreign.C.Types--import qualified Data.ByteString as BS-import Data.ByteString.Internal (ByteString (PS))-import qualified Data.Map as M-import Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Encoding as TextEncoding-import Data.Word (Word8)--import Control.Parallel.Strategies (parList, rpar, using)-import Data.Array.IArray (array, (!))-import Data.Array.Unboxed (UArray)-import Data.Ix (range)--import DataFrame.IO.CSV (- HeaderSpec (..),- ReadOptions (..),- defaultReadOptions,- shouldInferFromSample,- stripQuotes,- typeInferenceSampleSize,- )-import DataFrame.Internal.DataFrame (DataFrame (..))-import DataFrame.Operations.Typing (parseFromExamples)--readSeparatedDefaultFast :: Word8 -> FilePath -> IO DataFrame-readSeparatedDefaultFast separator =- readSeparated- separator- defaultReadOptions- getDelimiterIndices--readSeparatedDefault :: Word8 -> FilePath -> IO DataFrame-readSeparatedDefault separator =- readSeparated- separator- defaultReadOptions- ( \separator originalLen v -> do- indices <- mallocArray originalLen- getDelimiterIndices_ separator originalLen v indices- )--fastReadCsvUnstable :: FilePath -> IO DataFrame-fastReadCsvUnstable = readSeparatedDefaultFast comma--readCsvUnstable :: FilePath -> IO DataFrame-readCsvUnstable = readSeparatedDefault comma--fastReadTsvUnstable :: FilePath -> IO DataFrame-fastReadTsvUnstable = readSeparatedDefaultFast tab--readTsvUnstable :: FilePath -> IO DataFrame-readTsvUnstable = readSeparatedDefault tab--readSeparated ::- Word8 ->- ReadOptions ->- (Word8 -> Int -> VS.Vector Word8 -> IO (VS.Vector CSize)) ->- FilePath ->- IO DataFrame-readSeparated separator opts delimiterIndices filePath = do- -- We use write copy mode so that we can append- -- padding to the end of the memory space- (bufferPtr, offset, len) <-- mmapFileForeignPtr- filePath- WriteCopy- Nothing- let mutableFile = unsafeFromForeignPtr bufferPtr offset len- paddedMutableFile <- grow mutableFile 64- paddedCSVFile <- VS.unsafeFreeze paddedMutableFile- indices <- delimiterIndices separator len paddedCSVFile- let numCol = countColumnsInFirstRow paddedCSVFile indices- totalRows = VS.length indices `div` numCol- extractField' = extractField paddedCSVFile indices- (columnNames, dataStartRow) = case headerSpec opts of- NoHeader ->- ( Vector.fromList $- map (Text.pack . show) [0 .. numCol - 1]- , 0- )- UseFirstRow ->- ( Vector.fromList $- map (stripQuotes . extractField') [0 .. numCol - 1]- , 1- )- ProvideNames ns ->- (Vector.fromList ns, 0)- numRow = totalRows - dataStartRow- parseTypes col =- let n =- if shouldInferFromSample (typeSpec opts)- then typeInferenceSampleSize (typeSpec opts)- else 0- in parseFromExamples- (missingIndicators opts)- n- (safeRead opts)- (dateFormat opts)- col- generateColumn col =- parseTypes $- Vector.fromListN- numRow- ( map- ( \row ->- (stripQuotes . extractField')- (row * numCol + col)- )- [dataStartRow .. totalRows - 1]- )- columns =- Vector.fromListN- numCol- ( map generateColumn [0 .. numCol - 1]- `using` parList rpar- )- columnIndices =- M.fromList $- zip (Vector.toList columnNames) [0 ..]- dataframeDimensions = (numRow, numCol)- return $- DataFrame columns columnIndices dataframeDimensions M.empty--{-# INLINE extractField #-}-extractField ::- VS.Vector Word8 ->- VS.Vector CSize ->- Int ->- Text-extractField file indices position =- Text.strip- . TextEncoding.decodeUtf8Lenient- . unsafeToByteString- $ VS.slice- previous- (next - previous)- file- where- previous =- if position == 0- then 0- else 1 + fromIntegral (indices VS.! (position - 1))- next = fromIntegral $ indices VS.! position- unsafeToByteString :: VS.Vector Word8 -> BS.ByteString- unsafeToByteString v = PS (castForeignPtr ptr) 0 len- where- (ptr, len) = VS.unsafeToForeignPtr0 v--foreign import capi "process_csv.h get_delimiter_indices"- get_delimiter_indices ::- Ptr CUChar -> -- input- CSize -> -- input size- CUChar -> -- separator character- Ptr CSize -> -- result array- IO CSize -- occupancy of result array--{-# INLINE getDelimiterIndices #-}-getDelimiterIndices ::- Word8 ->- Int ->- VS.Vector Word8 ->- IO (VS.Vector CSize)-getDelimiterIndices separator originalLen csvFile =- VS.unsafeWith csvFile $ \buffer -> do- let paddedLen = VS.length csvFile- -- GC-managed pinned memory: freed automatically, no leak in streaming use.- resultMV <- VSM.unsafeNew paddedLen- num_fields <-- VSM.unsafeWith resultMV $ \indicesPtr ->- get_delimiter_indices- (castPtr buffer)- (fromIntegral paddedLen)- (fromIntegral separator)- (castPtr indicesPtr)- if num_fields == -1- then do- -- Haskell state-machine fallback, writing directly into resultMV.- let trans = stateTransitionTable separator- processChar (!state, !idx) i byte =- case state of- UnEscaped ->- if byte == lf || byte == separator- then do- VSM.unsafeWrite resultMV idx (fromIntegral i)- return (toEnum (trans ! (fromEnum state, byte)), idx + 1)- else return (toEnum (trans ! (fromEnum state, byte)), idx)- Escaped ->- return (toEnum (trans ! (fromEnum state, byte)), idx)- (_, finalIdx) <- VS.ifoldM' processChar (UnEscaped, 0 :: Int) csvFile- finalLen <-- if originalLen > 0 && csvFile VS.! (originalLen - 1) /= lf- then do- VSM.unsafeWrite resultMV finalIdx (fromIntegral originalLen)- return (finalIdx + 1)- else return finalIdx- VS.unsafeFreeze (VSM.slice 0 finalLen resultMV)- else do- let n = fromIntegral num_fields- finalLen <-- if originalLen > 0 && csvFile VS.! (originalLen - 1) /= lf- then do- VSM.write resultMV n (fromIntegral originalLen)- return (n + 1)- else return n- VS.unsafeFreeze (VSM.slice 0 finalLen resultMV)---- We have a Native version in case the C version--- cannot be used. For example if neither ARM_NEON--- nor AVX2 are available--lf, cr, comma, tab, quote :: Word8-lf = 0x0A-cr = 0x0D-comma = 0x2C-tab = 0x09-quote = 0x22---- We parse using a state machine-data State- = UnEscaped -- non quoted- | Escaped -- quoted- deriving (Enum)--{-# INLINE stateTransitionTable #-}-stateTransitionTable :: Word8 -> UArray (Int, Word8) Int-stateTransitionTable separator = array ((0, 0), (1, 255)) [(i, f i) | i <- range ((0, 0), (1, 255))]- where- f (0, character)- -- Unescaped newline- | character == 0x0A = fromEnum UnEscaped- -- Unescaped separator- | character == separator = fromEnum UnEscaped- -- Unescaped quote- | character == 0x22 = fromEnum Escaped- | otherwise = fromEnum UnEscaped- -- Escaped quote- -- escaped quote in fields are dealt as- -- consecutive quoted sections of a field- -- example: If we have- -- field1, "abc""def""ghi, field3- -- we end up processing abc, def, and ghi- -- as consecutive quoted strings.- f (1, 0x22) = fromEnum UnEscaped- -- Everything else- f (state, _) = state--{-# INLINE getDelimiterIndices_ #-}-getDelimiterIndices_ ::- Word8 ->- Int ->- VS.Vector Word8 ->- Ptr CSize ->- IO (VS.Vector CSize)-getDelimiterIndices_ separator originalLen csvFile resultPtr = do- resultVector <- resultVectorM- (_, resultLen) <-- VS.ifoldM'- (processCharacter resultVector)- (UnEscaped, 0)- csvFile- -- Handle the case where the file doesn't end with a newline- -- We need to add a final delimiter for the last field- finalResultLen <-- if originalLen > 0 && csvFile VS.! (originalLen - 1) /= lf- then do- VSM.write resultVector resultLen (fromIntegral originalLen)- return (resultLen + 1)- else return resultLen- VS.unsafeFreeze $ VSM.slice 0 finalResultLen resultVector- where- paddedLen = VS.length csvFile- resultVectorM = do- resultForeignPtr <- newForeignPtr_ resultPtr- return $ VSM.unsafeFromForeignPtr0 resultForeignPtr paddedLen- transitionTable = stateTransitionTable separator- processCharacter ::- VSM.IOVector CSize ->- (State, Int) ->- Int ->- Word8 ->- IO (State, Int)- processCharacter- resultVector- (!state, !resultIndex)- index- character =- case state of- UnEscaped ->- if character == lf || character == separator- then do- VSM.write- resultVector- resultIndex- (fromIntegral index)- return (newState, resultIndex + 1)- else return (newState, resultIndex)- Escaped -> return (newState, resultIndex)- where- newState =- toEnum $- transitionTable- ! (fromEnum state, character)--{-# INLINE countColumnsInFirstRow #-}-countColumnsInFirstRow ::- VS.Vector Word8 ->- VS.Vector CSize ->- Int-countColumnsInFirstRow file indices- | VS.length indices == 0 = 0- | otherwise =- 1- + VS.length- ( VS.takeWhile- (\i -> file VS.! fromIntegral i /= lf)- indices- )
− src/DataFrame/Internal/Column.hs
@@ -1,1661 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE BangPatterns #-}-{-# 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 TypeFamilies #-}-{-# LANGUAGE UndecidableInstances #-}--module DataFrame.Internal.Column where--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.Mutable as VBM-import qualified Data.Vector.Unboxed as VU-import qualified Data.Vector.Unboxed.Mutable as VUM--import Control.DeepSeq (NFData (..), rnf)-import Control.Exception (throw)-import Control.Monad.ST (runST)-import Data.Kind (Type)-import Data.Maybe-import Data.These-import Data.Type.Equality (TestEquality (..))-import DataFrame.Errors-import DataFrame.Internal.Parsing-import DataFrame.Internal.Types-import System.IO.Unsafe (unsafePerformIO)-import Type.Reflection--{- | 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--data MutableColumn where- MBoxedColumn :: (Columnable a) => VBM.IOVector a -> MutableColumn- MUnboxedColumn :: (Columnable a, VU.Unbox a) => VUM.IOVector a -> MutableColumn- MOptionalColumn :: (Columnable a) => VBM.IOVector (Maybe a) -> MutableColumn--{- | 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--instance (Eq a) => Eq (TypedColumn a) where- (==) :: (Eq a) => TypedColumn a -> TypedColumn a -> Bool- (==) (TColumn a) (TColumn b) = a == b--instance (Ord a) => Ord (TypedColumn a) where- compare :: (Ord a) => TypedColumn a -> TypedColumn a -> Ordering- compare (TColumn a) (TColumn b) = compare a b---- | Gets the underlying value from a TypedColumn.-unwrapTypedColumn :: TypedColumn a -> Column-unwrapTypedColumn (TColumn value) = value---- | Gets the underlying vector from a TypedColumn.-vectorFromTypedColumn :: TypedColumn a -> VB.Vector a-vectorFromTypedColumn (TColumn value) = either throw id (toVector value)---- | Checks if a column contains missing values.-hasMissing :: Column -> Bool-hasMissing (OptionalColumn column) = True-hasMissing _ = False---- | Checks if a column contains only missing values.-allMissing :: Column -> Bool-allMissing (OptionalColumn column) = VB.length (VB.filter isNothing column) == VB.length column-allMissing _ = False---- | Checks if a column contains numeric values.-isNumeric :: Column -> Bool-isNumeric (UnboxedColumn (vec :: VU.Vector a)) = case sNumeric @a of- STrue -> True- _ -> False-isNumeric (BoxedColumn (vec :: VB.Vector a)) = case testEquality (typeRep @a) (typeRep @Integer) of- Nothing -> False- Just Refl -> True-isNumeric _ = False---- | Checks if a column is of a given type values.-hasElemType :: forall a. (Columnable a) => Column -> Bool-hasElemType (BoxedColumn (column :: VB.Vector b)) = fromMaybe False $ do- Refl <- testEquality (typeRep @a) (typeRep @b)- pure True-hasElemType (UnboxedColumn (column :: VU.Vector b)) = fromMaybe False $ do- Refl <- testEquality (typeRep @a) (typeRep @b)- pure True-hasElemType (OptionalColumn (column :: VB.Vector b)) = fromMaybe False $ do- Refl <- testEquality (typeRep @a) (typeRep @b)- pure True---- | 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"--{- | 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)--instance (Show a) => Show (TypedColumn a) where- show :: (Show a) => TypedColumn a -> String- show (TColumn col) = show col--instance NFData Column where- rnf (BoxedColumn (v :: VB.Vector a)) = rnf v- rnf (UnboxedColumn v) = v `seq` ()- rnf (OptionalColumn (v :: VB.Vector (Maybe a))) = rnf v--instance Show Column where- 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---- Generalised LEQ that does reflection.-generalLEQ ::- forall a b. (Typeable a, Typeable b, Ord a, Ord b) => a -> b -> Bool-generalLEQ x y = case testEquality (typeRep @a) (typeRep @b) of- Nothing -> False- Just Refl -> x <= y--instance Ord Column where- (<=) :: Column -> Column -> Bool- (<=) (BoxedColumn (a :: VB.Vector t1)) (BoxedColumn (b :: VB.Vector t2)) = generalLEQ a b- (<=) (OptionalColumn (a :: VB.Vector t1)) (OptionalColumn (b :: VB.Vector t2)) = generalLEQ a b- (<=) (UnboxedColumn (a :: VU.Vector t1)) (UnboxedColumn (b :: VU.Vector t2)) = generalLEQ a b- (<=) _ _ = False--{- | A class for converting a vector to a column of the appropriate type.-Given each Rep we tell the `toColumnRep` function which Column type to pick.--}-class ColumnifyRep (r :: Rep) a where- 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- , IntegralIf a- , FloatingIf a- , SBoolI (Unboxable a)- , SBoolI (Numeric a)- , SBoolI (IntegralTypes a)- , SBoolI (FloatingTypes a)- )--instance- (Columnable a, VU.Unbox a) =>- ColumnifyRep 'RUnboxed a- where- toColumnRep :: (Columnable a, VUM.Unbox a) => VB.Vector a -> Column- toColumnRep = UnboxedColumn . VU.convert--instance- (Columnable a) =>- ColumnifyRep 'RBoxed a- where- toColumnRep :: (Columnable a) => VB.Vector a -> Column- toColumnRep = BoxedColumn--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.--__Examples:__--@-> import qualified Data.Vector as V-> fromVector (VB.fromList [(1 :: Int), 2, 3, 4])-[1,2,3,4]-@--}-fromVector ::- 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.--__Examples:__--@-> import qualified Data.Vector.Unboxed as V-> fromUnboxedVector (VB.fromList [(1 :: Int), 2, 3, 4])-[1,2,3,4]-@--}-fromUnboxedVector ::- forall a. (Columnable a, VU.Unbox a) => VU.Vector a -> Column-fromUnboxedVector = UnboxedColumn--{- | O(n) Convert a list to a column. Automatically picks the best representation of a vector to store the underlying data in.--__Examples:__--@-> fromList [(1 :: Int), 2, 3, 4]-[1,2,3,4]-@--}-fromList ::- forall a.- (Columnable a, ColumnifyRep (KindOf a) a) =>- [a] -> Column-fromList = toColumnRep @(KindOf a) . VB.fromList--throwTypeMismatch ::- forall (a :: Type) (b :: Type).- (Typeable a, Typeable b) => Either DataFrameException Column-throwTypeMismatch =- Left $- TypeMismatchException- MkTypeErrorContext- { userType = Right (typeRep @b)- , expectedType = Right (typeRep @a)- , callingFunctionName = Just "mapColumn"- , errorColumnName = Nothing- }---- | An internal function to map a function over the values of a column.-mapColumn ::- forall b c.- (Columnable b, Columnable c) =>- (b -> c) -> Column -> Either DataFrameException Column-mapColumn f = \case- BoxedColumn (col :: VB.Vector a) -> run col- OptionalColumn (col :: VB.Vector a) -> run col- UnboxedColumn (col :: VU.Vector a) -> runUnboxed col- where- run :: forall a. (Typeable a) => VB.Vector a -> Either DataFrameException Column- run col = case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> Right (fromVector @c (VB.map f col))- Nothing -> throwTypeMismatch @a @b-- runUnboxed ::- forall a.- (Typeable a, VU.Unbox a) => VU.Vector a -> Either DataFrameException Column- runUnboxed col = case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> Right $ case sUnbox @c of- STrue -> UnboxedColumn (VU.map f col)- SFalse -> fromVector @c (VB.generate (VU.length col) (f . VU.unsafeIndex col))- Nothing -> throwTypeMismatch @a @b-{-# INLINEABLE mapColumn #-}---- | Applies a function that returns an unboxed result to an unboxed vector, storing the result in a column.-imapColumn ::- forall b c.- (Columnable b, Columnable c) =>- (Int -> b -> c) -> Column -> Either DataFrameException Column-imapColumn f = \case- BoxedColumn (col :: VB.Vector a) -> run col- OptionalColumn (col :: VB.Vector a) -> run col- UnboxedColumn (col :: VU.Vector a) -> runUnboxed col- where- run :: forall a. (Typeable a) => VB.Vector a -> Either DataFrameException Column- run col = case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> Right (fromVector @c (VB.imap f col))- Nothing -> throwTypeMismatch @a @b-- runUnboxed ::- forall a.- (Typeable a, VU.Unbox a) => VU.Vector a -> Either DataFrameException Column- runUnboxed col = case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> Right $ case sUnbox @c of- STrue -> UnboxedColumn (VU.imap f col)- SFalse -> BoxedColumn (VB.imap f (VG.convert col))- Nothing -> throwTypeMismatch @a @b---- | O(1) Gets the number of elements in the column.-columnLength :: Column -> Int-columnLength (BoxedColumn xs) = VG.length xs-columnLength (UnboxedColumn xs) = VG.length xs-columnLength (OptionalColumn xs) = VG.length xs-{-# INLINE columnLength #-}---- | O(n) Gets the number of elements in the column.-numElements :: Column -> Int-numElements (BoxedColumn xs) = VG.length xs-numElements (UnboxedColumn xs) = VG.length xs-numElements (OptionalColumn xs) = VG.foldl' (\acc x -> acc + fromEnum (isJust x)) 0 xs-{-# INLINE numElements #-}---- | O(n) Takes the first n values of a column.-takeColumn :: Int -> Column -> Column-takeColumn n (BoxedColumn xs) = BoxedColumn $ VG.take n xs-takeColumn n (UnboxedColumn xs) = UnboxedColumn $ VG.take n xs-takeColumn n (OptionalColumn xs) = OptionalColumn $ VG.take n xs-{-# INLINE takeColumn #-}---- | O(n) Takes the last n values of a column.-takeLastColumn :: Int -> Column -> Column-takeLastColumn n column = sliceColumn (columnLength column - n) n column-{-# INLINE takeLastColumn #-}---- | O(n) Takes n values after a given column index.-sliceColumn :: Int -> Int -> Column -> Column-sliceColumn start n (BoxedColumn xs) = BoxedColumn $ VG.slice start n xs-sliceColumn start n (UnboxedColumn xs) = UnboxedColumn $ VG.slice start n xs-sliceColumn start n (OptionalColumn xs) = OptionalColumn $ VG.slice start n xs-{-# INLINE sliceColumn #-}---- | O(n) Selects the elements at a given set of indices. May change the order.-atIndices :: S.Set Int -> Column -> Column-atIndices indexes (BoxedColumn column) = BoxedColumn $ VG.ifilter (\i _ -> i `S.member` indexes) column-atIndices indexes (OptionalColumn column) = OptionalColumn $ VG.ifilter (\i _ -> i `S.member` indexes) column-atIndices indexes (UnboxedColumn column) = UnboxedColumn $ VU.ifilter (\i _ -> i `S.member` indexes) column-{-# INLINE atIndices #-}---- | 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 $- VB.generate- (VU.length indexes)- (\i -> column `VB.unsafeIndex` (indexes `VU.unsafeIndex` i))-atIndicesStable indexes (UnboxedColumn column) = UnboxedColumn $ VU.unsafeBackpermute column indexes-atIndicesStable indexes (OptionalColumn column) =- OptionalColumn $- VB.generate- (VU.length indexes)- (\i -> column `VB.unsafeIndex` (indexes `VU.unsafeIndex` i))-{-# INLINE atIndicesStable #-}--{- | Like 'atIndicesStable' but treats negative indices as null,-producing an 'OptionalColumn'. Keeps the index vector fully-unboxed (no @VB.Vector (Maybe Int)@).--}-gatherWithSentinel :: VU.Vector Int -> Column -> Column-gatherWithSentinel indices col =- let !n = VU.length indices- in case col of- BoxedColumn v ->- OptionalColumn $- VB.generate n $ \i ->- let !idx = indices `VU.unsafeIndex` i- in if idx < 0 then Nothing else Just (v `VB.unsafeIndex` idx)- UnboxedColumn v ->- OptionalColumn $- VB.generate n $ \i ->- let !idx = indices `VU.unsafeIndex` i- in if idx < 0 then Nothing else Just (v `VU.unsafeIndex` idx)- OptionalColumn v ->- OptionalColumn $- VB.generate n $ \i ->- let !idx = indices `VU.unsafeIndex` i- in if idx < 0 then Nothing else v `VB.unsafeIndex` idx-{-# INLINE gatherWithSentinel #-}--atIndicesWithNulls :: VB.Vector (Maybe Int) -> Column -> Column-atIndicesWithNulls indices column =- case column of- BoxedColumn col ->- OptionalColumn $ VB.map (fmap (col VB.!)) indices- UnboxedColumn col ->- OptionalColumn $ VB.map (fmap (col VU.!)) indices- OptionalColumn col ->- OptionalColumn $ VB.map (\ix -> ix >>= (col VB.!)) indices---- | Internal helper to get indices in a boxed vector.-getIndices :: VU.Vector Int -> VB.Vector a -> VB.Vector a-getIndices indices xs = VB.generate (VU.length indices) (\i -> xs VB.! (indices VU.! i))-{-# INLINE getIndices #-}---- | Internal helper to get indices in an unboxed vector.-getIndicesUnboxed :: (VU.Unbox a) => VU.Vector Int -> VU.Vector a -> VU.Vector a-getIndicesUnboxed indices xs = VU.generate (VU.length indices) (\i -> xs VU.! (indices VU.! i))-{-# INLINE getIndicesUnboxed #-}--findIndices ::- forall a.- (Columnable a) =>- (a -> Bool) ->- Column ->- Either DataFrameException (VU.Vector Int)-findIndices pred = \case- BoxedColumn (v :: VB.Vector b) -> run v VG.convert- OptionalColumn (v :: VB.Vector b) -> run v VG.convert- UnboxedColumn (v :: VU.Vector b) -> run v id- where- run ::- forall b v.- (Typeable b, VG.Vector v b, VG.Vector v Int) =>- v b ->- (v Int -> VU.Vector Int) ->- Either DataFrameException (VU.Vector Int)- run column finalize = case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> Right . finalize $ VG.findIndices pred column- Nothing ->- Left $- TypeMismatchException- MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @b)- , callingFunctionName = Just "findIndices"- , errorColumnName = Nothing- }---- | An internal function that returns a vector of how indexes change after a column is sorted.-sortedIndexes :: Bool -> Column -> VU.Vector Int-sortedIndexes asc = \case- BoxedColumn column -> sortWorker column- UnboxedColumn column -> sortWorker column- OptionalColumn column -> sortWorker column- where- sortWorker ::- (VG.Vector v a, Ord a, VG.Vector v (Int, a), VG.Vector v Int) =>- v a -> VU.Vector Int- sortWorker column = runST $ do- withIndexes <- VG.thaw $ VG.indexed column- let cmp = if asc then compare else flip compare- VA.sortBy (\(_, b) (_, b') -> cmp b b') withIndexes- sorted <- VG.unsafeFreeze withIndexes- return $ VG.convert $ VG.map fst sorted-{-# INLINE sortedIndexes #-}---- | Fold (right) column with index.-ifoldrColumn ::- forall a b.- (Columnable a, Columnable b) =>- (Int -> a -> b -> b) -> b -> Column -> Either DataFrameException b-ifoldrColumn f acc c@(BoxedColumn (column :: VB.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of- Just Refl -> pure $ VG.ifoldr f acc column- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "ifoldrColumn"- , errorColumnName = Nothing- }- )-ifoldrColumn f acc c@(OptionalColumn (column :: VB.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of- Just Refl -> pure $ VG.ifoldr f acc column- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "ifoldrColumn"- , errorColumnName = Nothing- }- )-ifoldrColumn f acc c@(UnboxedColumn (column :: VU.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of- Just Refl -> pure $ VG.ifoldr f acc column- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "ifoldrColumn"- , errorColumnName = Nothing- }- )--foldlColumn ::- forall a b.- (Columnable a, Columnable b) =>- (b -> a -> b) -> b -> Column -> Either DataFrameException b-foldlColumn f acc c@(BoxedColumn (column :: VB.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of- Just Refl -> pure $ VG.foldl' f acc column- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldlColumn"- , errorColumnName = Nothing- }- )-foldlColumn f acc c@(OptionalColumn (column :: VB.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of- Just Refl -> pure $ VG.foldl' f acc column- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldlColumn"- , errorColumnName = Nothing- }- )-foldlColumn f acc c@(UnboxedColumn (column :: VU.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of- Just Refl -> pure $ VG.foldl' f acc column- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldlColumn"- , errorColumnName = Nothing- }- )--foldlColumnWith ::- forall a b.- (Columnable a) =>- (b -> a -> b) -> b -> Column -> Either DataFrameException b-foldlColumnWith f acc (BoxedColumn (column :: VB.Vector d)) =- case testEquality (typeRep @a) (typeRep @d) of- Just Refl -> pure $ VG.foldl' f acc column- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldlColumnWith"- , errorColumnName = Nothing- }- )-foldlColumnWith f acc (OptionalColumn (column :: VB.Vector d)) =- case testEquality (typeRep @a) (typeRep @d) of- Just Refl -> pure $ VG.foldl' f acc column- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldlColumnWith"- , errorColumnName = Nothing- }- )-foldlColumnWith f acc (UnboxedColumn (column :: VU.Vector d)) =- case testEquality (typeRep @a) (typeRep @d) of- Just Refl -> pure $ VG.foldl' f acc column- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldlColumnWith"- , errorColumnName = Nothing- }- )--foldl1Column ::- forall a.- (Columnable a) =>- (a -> a -> a) -> Column -> Either DataFrameException a-foldl1Column f c@(BoxedColumn (column :: VB.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of- Just Refl -> pure $ VG.foldl1' f column- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldl1Column"- , errorColumnName = Nothing- }- )-foldl1Column f c@(OptionalColumn (column :: VB.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of- Just Refl -> pure $ VG.foldl1' f column- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldl1Column"- , errorColumnName = Nothing- }- )-foldl1Column f c@(UnboxedColumn (column :: VU.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of- Just Refl -> pure $ VG.foldl1' f column- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldl1Column"- , errorColumnName = Nothing- }- )--{- | O(n) Fold a column over groups without materialising a sorted copy.-Instead of backpermuting the column (expensive: O(n) allocation + random reads),-this iterates @valueIndices@ sequentially and accesses the column at each index.-Avoids one 10M-element allocation per column per aggregation expression.--}-foldDirectGroups ::- forall b acc.- (Columnable b) =>- (acc -> b -> acc) ->- acc ->- Column ->- VU.Vector Int -> -- valueIndices (sorted row order)- VU.Vector Int -> -- offsets (group boundaries)- Either DataFrameException (VB.Vector acc)-foldDirectGroups f seed col valueIndices offsets- | VU.length offsets <= 1 = Right VB.empty- | otherwise =- let !nGroups = VU.length offsets - 1- in case col of- UnboxedColumn (vec :: VU.Vector d) ->- case testEquality (typeRep @b) (typeRep @d) of- Just Refl ->- Right $- VB.generate nGroups foldGroup- where- foldGroup k =- let !s = VU.unsafeIndex offsets k- !e = VU.unsafeIndex offsets (k + 1)- in go s e seed- go !i !e !acc- | i >= e = acc- | otherwise =- go (i + 1) e $!- f acc (VU.unsafeIndex vec (VU.unsafeIndex valueIndices i))- Nothing ->- Left $- TypeMismatchException- MkTypeErrorContext- { userType = Right (typeRep @b)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldDirectGroups"- , errorColumnName = Nothing- }- BoxedColumn (vec :: VB.Vector d) ->- case testEquality (typeRep @b) (typeRep @d) of- Just Refl ->- Right $- VB.generate nGroups foldGroup- where- foldGroup k =- let !s = VU.unsafeIndex offsets k- !e = VU.unsafeIndex offsets (k + 1)- in go s e seed- go !i !e !acc- | i >= e = acc- | otherwise =- go (i + 1) e $!- f acc (VB.unsafeIndex vec (VU.unsafeIndex valueIndices i))- Nothing ->- Left $- TypeMismatchException- MkTypeErrorContext- { userType = Right (typeRep @b)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldDirectGroups"- , errorColumnName = Nothing- }- OptionalColumn (vec :: VB.Vector (Maybe d)) ->- case testEquality (typeRep @b) (typeRep @(Maybe d)) of- Just Refl ->- Right $- VB.generate nGroups foldGroup- where- foldGroup k =- let !s = VU.unsafeIndex offsets k- !e = VU.unsafeIndex offsets (k + 1)- in go s e seed- go !i !e !acc- | i >= e = acc- | otherwise =- go (i + 1) e $!- f acc (VB.unsafeIndex vec (VU.unsafeIndex valueIndices i))- Nothing ->- Left $- TypeMismatchException- MkTypeErrorContext- { userType = Right (typeRep @b)- , expectedType = Right (typeRep @(Maybe d))- , callingFunctionName = Just "foldDirectGroups"- , errorColumnName = Nothing- }-{-# INLINEABLE foldDirectGroups #-}--{- | O(n) Seedless fold over groups using the first element of each group as seed.-Like 'foldDirectGroups' but for the case where no initial accumulator is available.--}-foldl1DirectGroups ::- forall a.- (Columnable a) =>- (a -> a -> a) ->- Column ->- VU.Vector Int ->- VU.Vector Int ->- Either DataFrameException (VB.Vector a)-foldl1DirectGroups f col valueIndices offsets- | VU.length offsets <= 1 = Right VB.empty- | otherwise =- let !nGroups = VU.length offsets - 1- in case col of- UnboxedColumn (vec :: VU.Vector d) ->- case testEquality (typeRep @a) (typeRep @d) of- Just Refl ->- Right $- VB.generate nGroups foldGroup- where- foldGroup k =- let !s = VU.unsafeIndex offsets k- !e = VU.unsafeIndex offsets (k + 1)- !seed = VU.unsafeIndex vec (VU.unsafeIndex valueIndices s)- in go (s + 1) e seed- go !i !e !acc- | i >= e = acc- | otherwise =- go (i + 1) e $!- f acc (VU.unsafeIndex vec (VU.unsafeIndex valueIndices i))- Nothing ->- Left $- TypeMismatchException- MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldl1DirectGroups"- , errorColumnName = Nothing- }- BoxedColumn (vec :: VB.Vector d) ->- case testEquality (typeRep @a) (typeRep @d) of- Just Refl ->- Right $- VB.generate nGroups foldGroup- where- foldGroup k =- let !s = VU.unsafeIndex offsets k- !e = VU.unsafeIndex offsets (k + 1)- !seed = VB.unsafeIndex vec (VU.unsafeIndex valueIndices s)- in go (s + 1) e seed- go !i !e !acc- | i >= e = acc- | otherwise =- go (i + 1) e $!- f acc (VB.unsafeIndex vec (VU.unsafeIndex valueIndices i))- Nothing ->- Left $- TypeMismatchException- MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldl1DirectGroups"- , errorColumnName = Nothing- }- OptionalColumn (vec :: VB.Vector (Maybe d)) ->- case testEquality (typeRep @a) (typeRep @(Maybe d)) of- Just Refl ->- Right $- VB.generate nGroups foldGroup- where- foldGroup k =- let !s = VU.unsafeIndex offsets k- !e = VU.unsafeIndex offsets (k + 1)- !seed = VB.unsafeIndex vec (VU.unsafeIndex valueIndices s)- in go (s + 1) e seed- go !i !e !acc- | i >= e = acc- | otherwise =- go (i + 1) e $!- f acc (VB.unsafeIndex vec (VU.unsafeIndex valueIndices i))- Nothing ->- Left $- TypeMismatchException- MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @(Maybe d))- , callingFunctionName = Just "foldl1DirectGroups"- , errorColumnName = Nothing- }-{-# INLINEABLE foldl1DirectGroups #-}--{- | O(n) fold over groups by scanning the column LINEARLY.-rowToGroup[i] = group index for row i.-Avoids random column reads; random writes go to the accumulator array which is-small (nGroups entries) and typically cache-resident.-When @acc@ is unboxable, uses an unboxed mutable vector for the accumulator-array, eliminating pointer indirection on every read/write.--}-foldLinearGroups ::- forall b acc.- (Columnable b, Columnable acc) =>- (acc -> b -> acc) ->- acc ->- Column ->- VU.Vector Int -> -- rowToGroup (length n)- Int -> -- nGroups- Either DataFrameException Column-foldLinearGroups f seed col rowToGroup nGroups- | nGroups == 0 = Right (fromVector @acc VB.empty)- | otherwise = case col of- UnboxedColumn (vec :: VU.Vector d) ->- case testEquality (typeRep @b) (typeRep @d) of- Just Refl ->- Right $- unsafePerformIO $- runWith- ( \readAt writeAt ->- VU.iforM_ vec $ \row x -> do- let !k = VU.unsafeIndex rowToGroup row- cur <- readAt k- writeAt k $! f cur x- )- Nothing ->- Left $- TypeMismatchException- MkTypeErrorContext- { userType = Right (typeRep @b)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldLinearGroups"- , errorColumnName = Nothing- }- BoxedColumn (vec :: VB.Vector d) ->- case testEquality (typeRep @b) (typeRep @d) of- Just Refl ->- Right $- unsafePerformIO $- runWith- ( \readAt writeAt ->- VB.iforM_ vec $ \row x -> do- let !k = VU.unsafeIndex rowToGroup row- cur <- readAt k- writeAt k $! f cur x- )- Nothing ->- Left $- TypeMismatchException- MkTypeErrorContext- { userType = Right (typeRep @b)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "foldLinearGroups"- , errorColumnName = Nothing- }- OptionalColumn (vec :: VB.Vector (Maybe d)) ->- case testEquality (typeRep @b) (typeRep @(Maybe d)) of- Just Refl ->- Right $- unsafePerformIO $- runWith- ( \readAt writeAt ->- VB.iforM_ vec $ \row x -> do- let !k = VU.unsafeIndex rowToGroup row- cur <- readAt k- writeAt k $! f cur x- )- Nothing ->- Left $- TypeMismatchException- MkTypeErrorContext- { userType = Right (typeRep @b)- , expectedType = Right (typeRep @(Maybe d))- , callingFunctionName = Just "foldLinearGroups"- , errorColumnName = Nothing- }- where- -- \| Allocate accumulators, run the traversal, return a frozen Column.- -- When @acc@ is unboxable, uses an unboxed mutable vector (no pointer- -- indirection per read/write) and returns UnboxedColumn directly —- -- avoiding a round-trip through VB.Vector.- runWith :: ((Int -> IO acc) -> (Int -> acc -> IO ()) -> IO ()) -> IO Column- runWith body = case sUnbox @acc of- STrue -> do- accs <- VUM.replicate nGroups seed- body (VUM.unsafeRead accs) (VUM.unsafeWrite accs)- UnboxedColumn <$> VU.unsafeFreeze accs- SFalse -> do- accs <- VBM.replicate nGroups seed- body (VBM.unsafeRead accs) (VBM.unsafeWrite accs)- fromVector @acc <$> VB.unsafeFreeze accs- {-# INLINE runWith #-}-{-# INLINEABLE foldLinearGroups #-}--headColumn :: forall a. (Columnable a) => Column -> Either DataFrameException a-headColumn (BoxedColumn (col :: VB.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of- Just Refl ->- if VG.null col- then Left (EmptyDataSetException "headColumn")- else pure (VG.head col)- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @b)- , callingFunctionName = Just "headColumn"- , errorColumnName = Nothing- }- )-headColumn (UnboxedColumn (col :: VU.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of- Just Refl ->- if VG.null col- then Left (EmptyDataSetException "headColumn")- else pure (VG.head col)- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @b)- , callingFunctionName = Just "headColumn"- , errorColumnName = Nothing- }- )-headColumn (OptionalColumn (col :: VB.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of- Just Refl ->- if VG.null col- then Left (EmptyDataSetException "headColumn")- else pure (VG.head col)- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @b)- , callingFunctionName = Just "headColumn"- , errorColumnName = Nothing- }- )---- | An internal, column version of zip.-zipColumns :: Column -> Column -> Column-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 #-}---- | Merge two columns using `These`.-mergeColumns :: Column -> Column -> Column-mergeColumns colA colB = case (colA, colB) of- (OptionalColumn c1, OptionalColumn c2) ->- OptionalColumn $ mkVec c1 c2 $ \v1 v2 ->- case (v1, v2) of- (Nothing, Nothing) -> Nothing- (Just x, Nothing) -> Just (This x)- (Nothing, Just y) -> Just (That y)- (Just x, Just y) -> Just (These x y)- (OptionalColumn c1, BoxedColumn c2) -> optReq c1 c2- (OptionalColumn c1, UnboxedColumn c2) -> optReq c1 c2- (BoxedColumn c1, OptionalColumn c2) -> reqOpt c1 c2- (UnboxedColumn c1, OptionalColumn c2) -> reqOpt c1 c2- (BoxedColumn c1, BoxedColumn c2) -> reqReq c1 c2- (BoxedColumn c1, UnboxedColumn c2) -> reqReq c1 c2- (UnboxedColumn c1, BoxedColumn c2) -> reqReq c1 c2- (UnboxedColumn c1, UnboxedColumn c2) -> reqReq c1 c2- where- mkVec c1 c2 combineElements =- VB.generate- (min (VG.length c1) (VG.length c2))- (\i -> combineElements (c1 VG.! i) (c2 VG.! i))- {-# INLINE mkVec #-}-- reqReq c1 c2 = BoxedColumn $ mkVec c1 c2 These-- reqOpt c1 c2 = BoxedColumn $ mkVec c1 c2 $ \v1 v2 ->- case v2 of- Nothing -> This v1- Just y -> These v1 y-- optReq c1 c2 = BoxedColumn $ mkVec c1 c2 $ \v1 v2 ->- case v1 of- Nothing -> That v2- Just x -> These x v2-{-# INLINE mergeColumns #-}---- | An internal, column version of zipWith.-zipWithColumns ::- forall a b c.- (Columnable a, Columnable b, Columnable c) =>- (a -> b -> c) -> Column -> Column -> Either DataFrameException 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)- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @b)- , expectedType = Right (typeRep @e)- , callingFunctionName = Just "zipWithColumns"- , errorColumnName = Nothing- }- )- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "zipWithColumns"- , errorColumnName = Nothing- }- )-zipWithColumns f left right = case toVector @a left of- Left (TypeMismatchException context) ->- Left $- TypeMismatchException (context{callingFunctionName = Just "zipWithColumns"})- Left e -> Left e- Right left' -> case toVector @b right of- Left (TypeMismatchException context) ->- Left $- TypeMismatchException (context{callingFunctionName = Just "zipWithColumns"})- Left e -> Left e- Right right' -> 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- then VBM.unsafeWrite col i "" >> return (Left $! value)- else VBM.unsafeWrite col i value >> return (Right True)- )- Nothing -> return (Left value)-writeColumn i value (MOptionalColumn (col :: VBM.IOVector (Maybe a))) =- let- in case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl ->- ( if isNullish value- then VBM.unsafeWrite col i Nothing >> return (Left $! value)- else VBM.unsafeWrite col i (Just 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- 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 (MOptionalColumn col) = OptionalColumn <$> VB.unsafeFreeze col-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 "UNEXPECTED ERROR DURING FREEZE") (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 "UNEXPECTED ERROR DURING FREEZE") (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-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---- | 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-leftExpandColumn n column@(BoxedColumn col)- | 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--{- | Concatenates two columns.-Returns Nothing if the columns are of different types.--}-concatColumns :: Column -> Column -> Either DataFrameException Column-concatColumns (OptionalColumn left) (OptionalColumn right) = case testEquality (typeOf left) (typeOf right) of- Just Refl -> pure (OptionalColumn $ left <> right)- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeOf right)- , expectedType = Right (typeOf left)- , callingFunctionName = Just "concatColumns"- , errorColumnName = Nothing- }- )-concatColumns (BoxedColumn left) (BoxedColumn right) = case testEquality (typeOf left) (typeOf right) of- Just Refl -> pure (BoxedColumn $ left <> right)- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeOf right)- , expectedType = Right (typeOf left)- , callingFunctionName = Just "concatColumns"- , errorColumnName = Nothing- }- )-concatColumns (UnboxedColumn left) (UnboxedColumn right) = case testEquality (typeOf left) (typeOf right) of- Just Refl -> pure (UnboxedColumn $ left <> right)- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeOf right)- , expectedType = Right (typeOf left)- , callingFunctionName = Just "concatColumns"- , errorColumnName = Nothing- }- )-concatColumns left right =- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeOf right)- , expectedType = Right (typeOf left)- , callingFunctionName = Just "concatColumns"- , errorColumnName = Nothing- }- )--{- | Concatenates two columns.--Works similar to 'concatColumns', but unlike that function, it will also combine columns of different types-by wrapping the values in an Either.--E.g. combining Column containing [1,2] with Column containing ["a","b"]-will result in a Column containing [Left 1, Left 2, Right "a", Right "b"].--}--{- | O(n) Concatenate a list of same-type columns in a single allocation.-All columns must have the same constructor and element type (as they will-within a single Parquet column). Calls 'error' on mismatch.--}-concatManyColumns :: [Column] -> Column-concatManyColumns [] = fromList ([] :: [Maybe Int])-concatManyColumns [c] = c-concatManyColumns (c0 : cs) = case c0 of- OptionalColumn v0 ->- let getVec (OptionalColumn v) = case testEquality (typeOf v0) (typeOf v) of- Just Refl -> v- Nothing -> error "concatManyColumns: OptionalColumn type mismatch"- getVec _ = error "concatManyColumns: column constructor mismatch"- in OptionalColumn (VB.concat (v0 : map getVec cs))- BoxedColumn v0 ->- let getVec (BoxedColumn v) = case testEquality (typeOf v0) (typeOf v) of- Just Refl -> v- Nothing -> error "concatManyColumns: BoxedColumn type mismatch"- getVec _ = error "concatManyColumns: column constructor mismatch"- in BoxedColumn (VB.concat (v0 : map getVec cs))- UnboxedColumn v0 ->- let getVec (UnboxedColumn v) = case testEquality (typeOf v0) (typeOf v) of- Just Refl -> v- Nothing -> error "concatManyColumns: UnboxedColumn type mismatch"- getVec _ = error "concatManyColumns: column constructor mismatch"- in UnboxedColumn (VU.concat (v0 : map getVec cs))--concatColumnsEither :: Column -> Column -> Column-concatColumnsEither (OptionalColumn left) (OptionalColumn right) = case testEquality (typeOf left) (typeOf right) of- Nothing ->- OptionalColumn $ fmap (fmap Left) left <> fmap (fmap Right) right- Just Refl ->- OptionalColumn $ left <> right-concatColumnsEither (BoxedColumn left) (BoxedColumn right) = case testEquality (typeOf left) (typeOf right) of- Nothing ->- BoxedColumn $ fmap Left left <> fmap Right right- Just Refl ->- BoxedColumn $ left <> right-concatColumnsEither (UnboxedColumn left) (UnboxedColumn right) = case testEquality (typeOf left) (typeOf right) of- Nothing ->- BoxedColumn $ fmap Left (VG.convert left) <> fmap Right (VG.convert right)- Just Refl ->- UnboxedColumn $ left <> right-concatColumnsEither (BoxedColumn left) (UnboxedColumn right) =- BoxedColumn $ fmap Left left <> fmap Right (VG.convert right)-concatColumnsEither (UnboxedColumn left) (BoxedColumn right) =- BoxedColumn $ fmap Left (VG.convert left) <> fmap Right right-concatColumnsEither (OptionalColumn (left :: VB.Vector (Maybe a))) (BoxedColumn (right :: VB.Vector b)) =- case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> OptionalColumn $ left <> fmap Just right- Nothing -> OptionalColumn $ fmap (fmap Left) left <> fmap (Just . Right) right-concatColumnsEither (BoxedColumn (left :: VB.Vector a)) (OptionalColumn (right :: VB.Vector (Maybe b))) =- case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> OptionalColumn $ fmap Just left <> right- Nothing -> OptionalColumn $ fmap (Just . Left) left <> fmap (fmap Right) right-concatColumnsEither (OptionalColumn (left :: VB.Vector (Maybe a))) (UnboxedColumn (right :: VU.Vector b)) =- case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> OptionalColumn $ left <> fmap Just (VG.convert right)- Nothing ->- OptionalColumn $ fmap (fmap Left) left <> fmap (Just . Right) (VG.convert right)-concatColumnsEither (UnboxedColumn (left :: VU.Vector a)) (OptionalColumn (right :: VB.Vector (Maybe b))) =- case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> OptionalColumn $ fmap Just (VG.convert left) <> right- Nothing ->- OptionalColumn $ fmap (Just . Left) (VG.convert left) <> fmap (fmap Right) right---- | Allocate a mutable column of size @n@ matching the constructor/type of the given column.-newMutableColumn :: Int -> Column -> IO MutableColumn-newMutableColumn n (OptionalColumn (_ :: VB.Vector (Maybe a))) =- MOptionalColumn <$> (VBM.new n :: IO (VBM.IOVector (Maybe a)))-newMutableColumn n (BoxedColumn (_ :: VB.Vector a)) =- MBoxedColumn <$> (VBM.new n :: IO (VBM.IOVector a))-newMutableColumn n (UnboxedColumn (_ :: VU.Vector a)) =- MUnboxedColumn <$> (VUM.new n :: IO (VUM.IOVector a))---- | Copy a column chunk into a mutable column starting at offset @off@.-copyIntoMutableColumn :: MutableColumn -> Int -> Column -> IO ()-copyIntoMutableColumn (MOptionalColumn (mv :: VBM.IOVector (Maybe b))) off (OptionalColumn (v :: VB.Vector (Maybe a))) =- case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> VG.imapM_ (\i x -> VBM.unsafeWrite mv (off + i) x) v- Nothing -> error "copyIntoMutableColumn: Optional type mismatch"-copyIntoMutableColumn (MBoxedColumn (mv :: VBM.IOVector b)) off (BoxedColumn (v :: VB.Vector a)) =- case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> VG.imapM_ (\i x -> VBM.unsafeWrite mv (off + i) x) v- Nothing -> error "copyIntoMutableColumn: Boxed type mismatch"-copyIntoMutableColumn (MUnboxedColumn (mv :: VUM.IOVector b)) off (UnboxedColumn (v :: VU.Vector a)) =- case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> VG.imapM_ (\i x -> VUM.unsafeWrite mv (off + i) x) v- Nothing -> error "copyIntoMutableColumn: Unboxed type mismatch"-copyIntoMutableColumn _ _ _ =- error "copyIntoMutableColumn: constructor mismatch"---- | Freeze a mutable column into an immutable column.-freezeMutableColumn :: MutableColumn -> IO Column-freezeMutableColumn (MOptionalColumn mv) = OptionalColumn <$> VB.unsafeFreeze mv-freezeMutableColumn (MBoxedColumn mv) = BoxedColumn <$> VB.unsafeFreeze mv-freezeMutableColumn (MUnboxedColumn mv) = UnboxedColumn <$> VU.unsafeFreeze mv--{- | O(n) Converts a column to a list. Throws an exception if the wrong type is specified.--__Examples:__--@-> column = fromList [(1 :: Int), 2, 3, 4]-> toList @Int column-[1,2,3,4]-> toList @Double column-exception: ...-@--}-toList :: forall a. (Columnable a) => Column -> [a]-toList xs = case toVector @a xs of- Left err -> throw err- Right val -> VB.toList val--{- | Converts a column to a vector of a specific type.--This is a type-safe conversion that requires the column's element type-to exactly match the requested type. You must specify the desired type-via type applications.--==== __Type Parameters__--[@a@] The element type to convert to-[@v@] The vector type (e.g., 'VU.Vector', 'VB.Vector')--==== __Examples__-->>> toVector @Int @VU.Vector column-Right (unboxed vector of Ints)-->>> toVector @Text @VB.Vector column-Right (boxed vector of Text)--==== __Returns__--* 'Right' - The converted vector if types match-* 'Left' 'TypeMismatchException' - If the column's type doesn't match the requested type--==== __See also__--For numeric conversions with automatic type coercion, see 'toDoubleVector',-'toFloatVector', and 'toIntVector'.--}-toVector ::- forall a v.- (VG.Vector v a, Columnable a) => Column -> Either DataFrameException (v a)-toVector 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 "toVector"- , errorColumnName = Nothing- }- )-toVector (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 "toVector"- , errorColumnName = Nothing- }- )-toVector (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 "toVector"- , errorColumnName = Nothing- }- )---- Some common types we will use for numerical computing.--{- | Converts a column to an unboxed vector of 'Double' values.--This function performs intelligent type coercion for numeric types:--* If the column is already 'Double', returns it directly-* If the column contains other floating-point types, converts via 'realToFrac'-* If the column contains integral types, converts via 'fromIntegral' (beware of overflow if the type is `Integer`).--==== __Optional column handling__--For 'OptionalColumn' types, 'Nothing' values are converted to @NaN@ (Not a Number).-This allows optional numeric data to be represented in the resulting vector.--==== __Returns__--* 'Right' - The converted 'Double' vector-* 'Left' 'TypeMismatchException' - If the column is not numeric--}-toDoubleVector :: Column -> Either DataFrameException (VU.Vector Double)-toDoubleVector column =- case column of- UnboxedColumn (f :: VU.Vector a) -> case testEquality (typeRep @a) (typeRep @Double) of- Just Refl -> Right f- Nothing -> case sFloating @a of- STrue -> Right (VU.map realToFrac f)- SFalse -> case sIntegral @a of- STrue -> Right (VU.map fromIntegral f)- SFalse ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @Double)- , expectedType = Right (typeRep @a)- , callingFunctionName = Just "toDoubleVector"- , errorColumnName = Nothing- }- )- OptionalColumn (f :: VB.Vector (Maybe a)) -> case testEquality (typeRep @a) (typeRep @Double) of- Just Refl -> Right (VB.convert $ VB.map (fromMaybe (read @Double "NaN")) f)- Nothing -> case sFloating @a of- STrue ->- Right- (VB.convert $ VB.map (maybe (read @Double "NaN") realToFrac) f)- SFalse -> case sIntegral @a of- STrue ->- Right- (VB.convert $ VB.map (maybe (read @Double "NaN") fromIntegral) f)- SFalse ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @Double)- , expectedType = Right (typeRep @a)- , callingFunctionName = Just "toDoubleVector"- , errorColumnName = Nothing- }- )- BoxedColumn (f :: VB.Vector a) -> case testEquality (typeRep @a) (typeRep @Integer) of- Just Refl -> Right (VB.convert $ VB.map fromIntegral f)- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @Double)- , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())- , callingFunctionName = Just "toDoubleVector"- , errorColumnName = Nothing- }- )--{- | Converts a column to an unboxed vector of 'Float' values.--This function performs intelligent type coercion for numeric types:--* If the column is already 'Float', returns it directly-* If the column contains other floating-point types, converts via 'realToFrac'-* If the column contains integral types, converts via 'fromIntegral'-* If the column is boxed 'Integer', converts via 'fromIntegral' (beware of overflow for 64-bit integers and `Integer`)--==== __Optional column handling__--For 'OptionalColumn' types, 'Nothing' values are converted to @NaN@ (Not a Number).-This allows optional numeric data to be represented in the resulting vector.--==== __Returns__--* 'Right' - The converted 'Float' vector-* 'Left' 'TypeMismatchException' - If the column is not numeric--==== __Precision warning__--Converting from 'Double' to 'Float' may result in loss of precision.--}-toFloatVector :: Column -> Either DataFrameException (VU.Vector Float)-toFloatVector column =- case column of- UnboxedColumn (f :: VU.Vector a) -> case testEquality (typeRep @a) (typeRep @Float) of- Just Refl -> Right f- Nothing -> case sFloating @a of- STrue -> Right (VU.map realToFrac f)- SFalse -> case sIntegral @a of- STrue -> Right (VU.map fromIntegral f)- SFalse ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @Float)- , expectedType = Right (typeRep @a)- , callingFunctionName = Just "toFloatVector"- , errorColumnName = Nothing- }- )- OptionalColumn (f :: VB.Vector (Maybe a)) -> case testEquality (typeRep @a) (typeRep @Float) of- Just Refl -> Right (VB.convert $ VB.map (fromMaybe (read @Float "NaN")) f)- Nothing -> case sFloating @a of- STrue ->- Right- (VB.convert $ VB.map (maybe (read @Float "NaN") realToFrac) f)- SFalse -> case sIntegral @a of- STrue ->- Right- (VB.convert $ VB.map (maybe (read @Float "NaN") fromIntegral) f)- SFalse ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @Float)- , expectedType = Right (typeRep @a)- , callingFunctionName = Just "toFloatVector"- , errorColumnName = Nothing- }- )- BoxedColumn (f :: VB.Vector a) -> case testEquality (typeRep @a) (typeRep @Integer) of- Just Refl -> Right (VB.convert $ VB.map fromIntegral f)- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @Float)- , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())- , callingFunctionName = Just "toFloatVector"- , errorColumnName = Nothing- }- )--{- | Converts a column to an unboxed vector of 'Int' values.--This function performs intelligent type coercion for numeric types:--* If the column is already 'Int', returns it directly-* If the column contains floating-point types, rounds via 'round' and converts-* If the column contains other integral types, converts via 'fromIntegral'-* If the column is boxed 'Integer', converts via 'fromIntegral'--==== __Returns__--* 'Right' - The converted 'Int' vector-* 'Left' 'TypeMismatchException' - If the column is not numeric--==== __Note__--Unlike 'toDoubleVector' and 'toFloatVector', this function does NOT support-'OptionalColumn'. Optional columns must be handled separately.--==== __Rounding behavior__--Floating-point values are rounded to the nearest integer using 'round'.-For example: 2.5 rounds to 2, 3.5 rounds to 4 (banker's rounding).--}-toIntVector :: Column -> Either DataFrameException (VU.Vector Int)-toIntVector column =- case column of- UnboxedColumn (f :: VU.Vector a) -> case testEquality (typeRep @a) (typeRep @Int) of- Just Refl -> Right f- Nothing -> case sFloating @a of- STrue -> Right (VU.map (round . realToFrac) f)- SFalse -> case sIntegral @a of- STrue -> Right (VU.map fromIntegral f)- SFalse ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @Int)- , expectedType = Right (typeRep @a)- , callingFunctionName = Just "toIntVector"- , errorColumnName = Nothing- }- )- BoxedColumn (f :: VB.Vector a) -> case testEquality (typeRep @a) (typeRep @Integer) of- Just Refl -> Right (VB.convert $ VB.map fromIntegral f)- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @Int)- , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())- , callingFunctionName = Just "toIntVector"- , errorColumnName = Nothing- }- )- _ ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @Int)- , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())- , callingFunctionName = Just "toIntVector"- , errorColumnName = Nothing- }- )--toUnboxedVector ::- forall a.- (Columnable a, VU.Unbox a) => Column -> Either DataFrameException (VU.Vector a)-toUnboxedVector column =- case column of- UnboxedColumn (f :: VU.Vector b) -> case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> Right f- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @Int)- , expectedType = Right (typeRep @a)- , callingFunctionName = Just "toUnboxedVector"- , errorColumnName = Nothing- }- )- _ ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @a)- , expectedType = Left (columnTypeString column) :: Either String (TypeRep ())- , callingFunctionName = Just "toUnboxedVector"- , errorColumnName = Nothing- }- )-{-# SPECIALIZE toUnboxedVector ::- Column -> Either DataFrameException (VU.Vector Double)- #-}-{-# INLINE toUnboxedVector #-}
− src/DataFrame/Internal/DataFrame.hs
@@ -1,175 +0,0 @@-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE InstanceSigs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--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 Control.DeepSeq (NFData (..), rnf)-import Control.Exception (throw)-import Data.Function (on)-import Data.List (sortBy, transpose, (\\))-import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))-import DataFrame.Display.Terminal.PrettyPrint-import DataFrame.Errors-import DataFrame.Internal.Column-import DataFrame.Internal.Expression-import Text.Printf-import Type.Reflection (typeRep)--data DataFrame = DataFrame- { 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)- -- ^ (rows, columns)- , derivingExpressions :: M.Map T.Text UExpr- }--instance NFData DataFrame where- rnf (DataFrame cols idx dims _exprs) =- rnf cols `seq` rnf idx `seq` rnf dims--{- | 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- , rowToGroup :: VU.Vector Int- {- ^ rowToGroup[i] = group index for row i. Length n (one per row).- Built once in 'groupBy'; reused by every aggregation.- -}- }--instance Show GroupedDataFrame where- show (Grouped df cols _indices _os _rtg) =- printf- "{ keyColumns: %s groupedColumns: %s }"- (show cols)- (show (M.keys (columnIndices df) \\ cols))--instance Eq GroupedDataFrame where- (==) (Grouped df cols _indices _os _rtg) (Grouped df' cols' _indices' _os' _rtg') = (df == df') && (cols == cols')--instance Eq DataFrame where- (==) :: DataFrame -> DataFrame -> Bool- a == b =- M.keys (columnIndices a) == M.keys (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 =- let- rows = 20- (r, c) = dataframeDimensions d- d' =- d- { columns = V.map (takeColumn rows) (columns d)- , dataframeDimensions = (min rows r, c)- }- truncationInfo =- "\n"- ++ "Showing "- ++ show (min rows r)- ++ " rows out of "- ++ show r- in- T.unpack (asText d' False) ++ (if r > rows then truncationInfo else "")---- | For showing the dataframe as markdown in notebooks.-toMarkdownTable :: DataFrame -> T.Text-toMarkdownTable df = asText df True--asText :: DataFrame -> Bool -> T.Text-asText d properMarkdown =- let header = 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- get Nothing = V.empty- getTextColumnFromFrame df (i, name) = get $ (V.!?) (columns d) ((M.!) (columnIndices d) name)- rows =- transpose $- zipWith (curry (V.toList . getTextColumnFromFrame d)) [0 ..] header- in showTable properMarkdown header types rows---- | O(1) Creates an empty dataframe-empty :: DataFrame-empty =- DataFrame- { columns = V.empty- , columnIndices = M.empty- , dataframeDimensions = (0, 0)- , derivingExpressions = M.empty- }--{- | Safely retrieves a column by name from the dataframe.--Returns 'Nothing' if the column does not exist.--==== __Examples__-->>> getColumn "age" df-Just (UnboxedColumn ...)-->>> getColumn "nonexistent" df-Nothing--}-getColumn :: T.Text -> DataFrame -> Maybe Column-getColumn name df = do- i <- columnIndices df M.!? name- columns df V.!? i--{- | Retrieves a column by name from the dataframe, throwing an exception if not found.--This is an unsafe version of 'getColumn' that throws 'ColumnNotFoundException'-if the column does not exist. Use this when you are certain the column exists.--==== __Throws__--* 'ColumnNotFoundException' - if the column with the given name does not exist--}-unsafeGetColumn :: T.Text -> DataFrame -> Column-unsafeGetColumn name df = case getColumn name df of- Nothing -> throw $ ColumnNotFoundException name "" (M.keys $ columnIndices df)- Just col -> col--{- | Checks if the dataframe is empty (has no columns).--Returns 'True' if the dataframe has no columns, 'False' otherwise.-Note that a dataframe with columns but no rows is not considered null.--}-null :: DataFrame -> Bool-null df = V.null (columns df)
− src/DataFrame/Internal/Expression.hs
@@ -1,395 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE InstanceSigs #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE UndecidableInstances #-}--module DataFrame.Internal.Expression where--import Control.DeepSeq (NFData (..))-import Data.String-import qualified Data.Text as T-import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))-import qualified Data.Vector.Generic as VG-import DataFrame.Internal.Column-import Type.Reflection (Typeable, typeOf, typeRep)--data UnaryOp a b = MkUnaryOp- { unaryFn :: a -> b- , unaryName :: T.Text- , unarySymbol :: Maybe T.Text- }--data BinaryOp a b c = MkBinaryOp- { binaryFn :: a -> b -> c- , binaryName :: T.Text- , binarySymbol :: Maybe T.Text- , binaryCommutative :: Bool- , binaryPrecedence :: Int- }--data MeanAcc = MeanAcc {-# UNPACK #-} !Double {-# UNPACK #-} !Int- deriving (Show, Eq, Ord, Read)--instance NFData MeanAcc where- rnf (MeanAcc _ _) = ()--data AggStrategy a b where- CollectAgg ::- (VG.Vector v b, Typeable v) => T.Text -> (v b -> a) -> AggStrategy a b- FoldAgg :: T.Text -> Maybe a -> (a -> b -> a) -> AggStrategy a b- MergeAgg ::- (Columnable acc) =>- T.Text ->- acc ->- (acc -> b -> acc) ->- (acc -> acc -> acc) ->- (acc -> a) ->- AggStrategy a b--data Expr a where- Col :: (Columnable a) => T.Text -> Expr a- Lit :: (Columnable a) => a -> Expr a- Unary ::- (Columnable a, Columnable b) => UnaryOp b a -> Expr b -> Expr a- Binary ::- (Columnable c, Columnable b, Columnable a) =>- BinaryOp c b a -> Expr c -> Expr b -> Expr a- If :: (Columnable a) => Expr Bool -> Expr a -> Expr a -> Expr a- Agg :: (Columnable a, Columnable b) => AggStrategy a b -> Expr b -> Expr a--data UExpr where- UExpr :: (Columnable a) => Expr a -> UExpr--instance Show UExpr where- show :: UExpr -> String- show (UExpr expr) = show expr--type NamedExpr = (T.Text, UExpr)--instance (Num a, Columnable a) => Num (Expr a) where- (+) :: Expr a -> Expr a -> Expr a- (+) =- Binary- ( MkBinaryOp- { binaryFn = (+)- , binaryName = "add"- , binarySymbol = Just "+"- , binaryCommutative = True- , binaryPrecedence = 6- }- )-- (-) :: Expr a -> Expr a -> Expr a- (-) =- Binary- ( MkBinaryOp- { binaryFn = (-)- , binaryName = "sub"- , binarySymbol = Just "-"- , binaryCommutative = False- , binaryPrecedence = 6- }- )-- (*) :: Expr a -> Expr a -> Expr a- (*) =- Binary- ( MkBinaryOp- { binaryFn = (*)- , binaryName = "mult"- , binarySymbol = Just "*"- , binaryCommutative = True- , binaryPrecedence = 7- }- )-- fromInteger :: Integer -> Expr a- fromInteger = Lit . fromInteger-- negate :: Expr a -> Expr a- negate =- Unary- (MkUnaryOp{unaryFn = negate, unaryName = "negate", unarySymbol = Nothing})-- abs :: (Num a) => Expr a -> Expr a- abs = Unary (MkUnaryOp{unaryFn = abs, unaryName = "abs", unarySymbol = Nothing})-- signum :: (Num a) => Expr a -> Expr a- signum =- Unary- (MkUnaryOp{unaryFn = signum, unaryName = "signum", unarySymbol = Nothing})--add :: (Num a, Columnable a) => Expr a -> Expr a -> Expr a-add = (+)--sub :: (Num a, Columnable a) => Expr a -> Expr a -> Expr a-sub = (-)--mult :: (Num a, Columnable a) => Expr a -> Expr a -> Expr a-mult = (*)--instance (Fractional a, Columnable a) => Fractional (Expr a) where- fromRational :: (Fractional a, Columnable a) => Rational -> Expr a- fromRational = Lit . fromRational-- (/) :: (Fractional a, Columnable a) => Expr a -> Expr a -> Expr a- (/) =- Binary- ( MkBinaryOp- { binaryFn = (/)- , binaryName = "divide"- , binarySymbol = Just "/"- , binaryCommutative = False- , binaryPrecedence = 7- }- )--divide :: (Fractional a, Columnable a) => Expr a -> Expr a -> Expr a-divide = (/)--instance (IsString a, Columnable a) => IsString (Expr a) where- fromString :: String -> Expr a- fromString s = Lit (fromString s)--instance (Floating a, Columnable a) => Floating (Expr a) where- pi :: (Floating a, Columnable a) => Expr a- pi = Lit pi- exp :: (Floating a, Columnable a) => Expr a -> Expr a- exp = Unary (MkUnaryOp{unaryFn = exp, unaryName = "exp", unarySymbol = Nothing})- sqrt :: (Floating a, Columnable a) => Expr a -> Expr a- sqrt =- Unary (MkUnaryOp{unaryFn = sqrt, unaryName = "sqrt", unarySymbol = Nothing})- (**) :: (Floating a, Columnable a) => Expr a -> Expr a -> Expr a- (**) =- Binary- ( MkBinaryOp- { binaryFn = (**)- , binaryName = "exponentiate"- , binarySymbol = Just "**"- , binaryCommutative = False- , binaryPrecedence = 8- }- )- log :: (Floating a, Columnable a) => Expr a -> Expr a- log = Unary (MkUnaryOp{unaryFn = log, unaryName = "log", unarySymbol = Nothing})- logBase :: (Floating a, Columnable a) => Expr a -> Expr a -> Expr a- logBase =- Binary- ( MkBinaryOp- { binaryFn = logBase- , binaryName = "logBase"- , binarySymbol = Nothing- , binaryCommutative = False- , binaryPrecedence = 1- }- )- sin :: (Floating a, Columnable a) => Expr a -> Expr a- sin = Unary (MkUnaryOp{unaryFn = sin, unaryName = "sin", unarySymbol = Nothing})- cos :: (Floating a, Columnable a) => Expr a -> Expr a- cos = Unary (MkUnaryOp{unaryFn = cos, unaryName = "cos", unarySymbol = Nothing})- tan :: (Floating a, Columnable a) => Expr a -> Expr a- tan = Unary (MkUnaryOp{unaryFn = tan, unaryName = "tan", unarySymbol = Nothing})- asin :: (Floating a, Columnable a) => Expr a -> Expr a- asin =- Unary (MkUnaryOp{unaryFn = asin, unaryName = "asin", unarySymbol = Nothing})- acos :: (Floating a, Columnable a) => Expr a -> Expr a- acos =- Unary (MkUnaryOp{unaryFn = acos, unaryName = "acos", unarySymbol = Nothing})- atan :: (Floating a, Columnable a) => Expr a -> Expr a- atan =- Unary (MkUnaryOp{unaryFn = atan, unaryName = "atan", unarySymbol = Nothing})- sinh :: (Floating a, Columnable a) => Expr a -> Expr a- sinh =- Unary (MkUnaryOp{unaryFn = sinh, unaryName = "sinh", unarySymbol = Nothing})- cosh :: (Floating a, Columnable a) => Expr a -> Expr a- cosh =- Unary (MkUnaryOp{unaryFn = cosh, unaryName = "cosh", unarySymbol = Nothing})- asinh :: (Floating a, Columnable a) => Expr a -> Expr a- asinh =- Unary- (MkUnaryOp{unaryFn = asinh, unaryName = "asinh", unarySymbol = Nothing})- acosh :: (Floating a, Columnable a) => Expr a -> Expr a- acosh =- Unary- (MkUnaryOp{unaryFn = acosh, unaryName = "acosh", unarySymbol = Nothing})- atanh :: (Floating a, Columnable a) => Expr a -> Expr a- atanh =- Unary- (MkUnaryOp{unaryFn = atanh, unaryName = "atanh", unarySymbol = Nothing})--instance (Show a) => Show (Expr a) where- show :: forall a. (Show a) => Expr a -> String- show (Col name) = "(col @" ++ show (typeRep @a) ++ " " ++ show name ++ ")"- show (Lit value) = "(lit (" ++ show value ++ "))"- show (If cond l r) = "(ifThenElse " ++ show cond ++ " " ++ show l ++ " " ++ show r ++ ")"- show (Unary op value) = "(" ++ T.unpack (unaryName op) ++ " " ++ show value ++ ")"- show (Binary op a b) = "(" ++ T.unpack (binaryName op) ++ " " ++ show a ++ " " ++ show b ++ ")"- show (Agg (CollectAgg op _) expr) = "(" ++ T.unpack op ++ " " ++ show expr ++ ")"- show (Agg (FoldAgg op _ _) expr) = "(" ++ T.unpack op ++ " " ++ show expr ++ ")"- show (Agg (MergeAgg op _ _ _ _) expr) = "(" ++ T.unpack op ++ " " ++ show expr ++ ")"--normalize :: (Eq a, Ord a, Show a, Typeable a) => Expr a -> Expr a-normalize expr = case expr of- Col name -> Col name- Lit val -> Lit val- If cond th el -> If (normalize cond) (normalize th) (normalize el)- Unary op e -> Unary op (normalize e)- Binary op e1 e2- | binaryCommutative op ->- let n1 = normalize e1- n2 = normalize e2- in case testEquality (typeOf n1) (typeOf n2) of- Nothing -> expr- Just Refl ->- if compareExpr n1 n2 == GT- then Binary op n2 n1 -- Swap to canonical order- else Binary op n1 n2- | otherwise -> Binary op (normalize e1) (normalize e2)- Agg strat e -> Agg strat (normalize e)---- Compare expressions for ordering (used in normalization)-compareExpr :: Expr a -> Expr a -> Ordering-compareExpr e1 e2 = compare (exprKey e1) (exprKey e2)- where- exprKey :: Expr a -> String- exprKey (Col name) = "0:" ++ T.unpack name- exprKey (Lit val) = "1:" ++ show val- exprKey (If c t e) = "2:" ++ exprKey c ++ exprKey t ++ exprKey e- exprKey (Unary op e) = "3:" ++ T.unpack (unaryName op) ++ exprKey e- exprKey (Binary op e1 e2) = "4:" ++ T.unpack (binaryName op) ++ exprKey e1 ++ exprKey e2- exprKey (Agg (CollectAgg name _) e) = "5:" ++ T.unpack name ++ exprKey e- exprKey (Agg (FoldAgg name _ _) e) = "5:" ++ T.unpack name ++ exprKey e- exprKey (Agg (MergeAgg name _ _ _ _) e) = "5:" ++ T.unpack name ++ exprKey e--instance (Eq a, Columnable a) => Eq (Expr a) where- (==) l r = eqNormalized (normalize l) (normalize r)- where- exprEq :: (Columnable b, Columnable c) => Expr b -> Expr c -> Bool- exprEq e1 e2 = case testEquality (typeOf e1) (typeOf e2) of- Just Refl -> e1 == e2- Nothing -> False- eqNormalized :: Expr a -> Expr a -> Bool- eqNormalized (Col n1) (Col n2) = n1 == n2- eqNormalized (Lit v1) (Lit v2) = v1 == v2- eqNormalized (If c1 t1 e1) (If c2 t2 e2) =- c1 == c2 && t1 `exprEq` t2 && e1 `exprEq` e2- eqNormalized (Unary op1 e1) (Unary op2 e2) = unaryName op1 == unaryName op2 && e1 `exprEq` e2- eqNormalized (Binary op1 e1a e1b) (Binary op2 e2a e2b) = binaryName op1 == binaryName op2 && e1a `exprEq` e2a && e1b `exprEq` e2b- eqNormalized (Agg (CollectAgg n1 _) e1) (Agg (CollectAgg n2 _) e2) =- n1 == n2 && e1 `exprEq` e2- eqNormalized (Agg (FoldAgg n1 _ _) e1) (Agg (FoldAgg n2 _ _) e2) =- n1 == n2 && e1 `exprEq` e2- eqNormalized (Agg (MergeAgg n1 _ _ _ _) e1) (Agg (MergeAgg n2 _ _ _ _) e2) =- n1 == n2 && e1 `exprEq` e2- eqNormalized _ _ = False--instance (Ord a, Columnable a) => Ord (Expr a) where- compare :: Expr a -> Expr a -> Ordering- compare e1 e2 = case (e1, e2) of- (Col n1, Col n2) -> compare n1 n2- (Lit v1, Lit v2) -> compare v1 v2- (If c1 t1 e1', If c2 t2 e2') ->- compare c1 c2 <> exprComp t1 t2 <> exprComp e1' e2'- (Unary op1 e1', Unary op2 e2') -> compare (unaryName op1) (unaryName op2) <> exprComp e1' e2'- (Binary op1 a1 b1, Binary op2 a2 b2) ->- compare (binaryName op1) (binaryName op2) <> exprComp a1 a2 <> exprComp b1 b2- (Agg (CollectAgg n1 _) e1', Agg (CollectAgg n2 _) e2') -> compare n1 n2 <> exprComp e1' e2'- (Agg (FoldAgg n1 _ _) e1', Agg (FoldAgg n2 _ _) e2') -> compare n1 n2 <> exprComp e1' e2'- (Agg (MergeAgg n1 _ _ _ _) e1', Agg (MergeAgg n2 _ _ _ _) e2') -> compare n1 n2 <> exprComp e1' e2'- -- Different constructors - compare by priority- (Col _, _) -> LT- (_, Col _) -> GT- (Lit _, _) -> LT- (_, Lit _) -> GT- (Unary{}, _) -> LT- (_, Unary{}) -> GT- (Binary{}, _) -> LT- (_, Binary{}) -> GT- (If{}, _) -> LT- (_, If{}) -> GT- (Agg{}, _) -> LT--exprComp :: (Columnable b, Columnable c) => Expr b -> Expr c -> Ordering-exprComp e1 e2 = case testEquality (typeOf e1) (typeOf e2) of- Just Refl -> e1 `compare` e2- Nothing -> LT--replaceExpr ::- forall a b c.- (Columnable a, Columnable b, Columnable c) =>- Expr a -> Expr b -> Expr c -> Expr c-replaceExpr new old expr = case testEquality (typeRep @b) (typeRep @c) of- Just Refl -> case testEquality (typeRep @a) (typeRep @c) of- Just Refl -> if old == expr then new else replace'- Nothing -> expr- Nothing -> replace'- where- replace' = case expr of- (Col _) -> expr- (Lit _) -> expr- (If cond l r) ->- If (replaceExpr new old cond) (replaceExpr new old l) (replaceExpr new old r)- (Unary op value) -> Unary op (replaceExpr new old value)- (Binary op l r) -> Binary op (replaceExpr new old l) (replaceExpr new old r)- (Agg op expr) -> Agg op (replaceExpr new old expr)--eSize :: Expr a -> Int-eSize (Col _) = 1-eSize (Lit _) = 1-eSize (If c l r) = 1 + eSize c + eSize l + eSize r-eSize (Unary _ e) = 1 + eSize e-eSize (Binary _ l r) = 1 + eSize l + eSize r-eSize (Agg strategy expr) = eSize expr + 1--getColumns :: Expr a -> [T.Text]-getColumns (Col cName) = [cName]-getColumns expr@(Lit _) = []-getColumns (If cond l r) = getColumns cond <> getColumns l <> getColumns r-getColumns (Unary op value) = getColumns value-getColumns (Binary op l r) = getColumns l <> getColumns r-getColumns (Agg strategy expr) = getColumns expr--prettyPrint :: Expr a -> String-prettyPrint = go 0 0- where- indent :: Int -> String- indent n = replicate (n * 2) ' '-- go :: Int -> Int -> Expr a -> String- go depth prec expr = case expr of- Col name -> T.unpack name- Lit value -> show value- If cond t e ->- let inner =- "if "- ++ go (depth + 1) 0 cond- ++ "\n"- ++ indent (depth + 1)- ++ "then "- ++ go (depth + 1) 0 t- ++ "\n"- ++ indent (depth + 1)- ++ "else "- ++ go (depth + 1) 0 e- in if prec > 0 then "(" ++ inner ++ ")" else inner- Unary op arg -> case unarySymbol op of- Nothing -> T.unpack (unaryName op) ++ "(" ++ go depth 0 arg ++ ")"- Just sym -> T.unpack sym ++ "(" ++ go depth 0 arg ++ ")"- Binary op l r ->- let p = binaryPrecedence op- inner = case binarySymbol op of- Just name -> go depth p l ++ " " ++ T.unpack name ++ " " ++ go depth p r- Nothing ->- T.unpack (binaryName op) ++ "(" ++ go depth p l ++ ", " ++ go depth p r ++ ")"- in if prec > p then "(" ++ inner ++ ")" else inner- Agg (CollectAgg op _) arg -> T.unpack op ++ "(" ++ go depth 0 arg ++ ")"- Agg (FoldAgg op _ _) arg -> T.unpack op ++ "(" ++ go depth 0 arg ++ ")"- Agg (MergeAgg op _ _ _ _) arg -> T.unpack op ++ "(" ++ go depth 0 arg ++ ")"
− src/DataFrame/Internal/Interpreter.hs
@@ -1,486 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE UndecidableInstances #-}--module DataFrame.Internal.Interpreter (- -- * New core API- Value (..),- Ctx (..),- eval,- materialize,-- -- * Backward-compatible API- interpret,- interpretAggregation,- AggregationResult (..),-) where--import Data.Bifunctor (first)-import qualified Data.Map as M-import qualified Data.Text as T-import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))-import qualified Data.Vector as V-import qualified Data.Vector.Generic as VG-import qualified Data.Vector.Unboxed as VU-import DataFrame.Errors-import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame-import DataFrame.Internal.Expression-import DataFrame.Internal.Types-import Type.Reflection (Typeable, typeRep)------------------------------------------------------------------------------------ Value: the unified result type----------------------------------------------------------------------------------{- | The result of interpreting an expression. Keeps literals as scalars-until the point where a concrete column is needed, avoiding premature-broadcast allocations.--}-data Value a where- -- | A single value, not yet broadcast to any length.- Scalar :: (Columnable a) => a -> Value a- {- | A flat column (one element per row in the flat case, or one- element per group after aggregation).- -}- Flat :: (Columnable a) => Column -> Value a- {- | A grouped column: one 'Column' slice per group. Only produced- when interpreting inside a 'GroupCtx'.- -}- Group :: (Columnable a) => V.Vector Column -> Value a---- | The interpretation context.-data Ctx- = FlatCtx DataFrame- | GroupCtx GroupedDataFrame------------------------------------------------------------------------------------ Materialisation----------------------------------------------------------------------------------{- | Force a 'Value' into a flat 'Column' of the given length. Scalars-are broadcast; flat columns are returned as-is.--}-materialize :: forall a. (Columnable a) => Int -> Value a -> Column-materialize n (Scalar v) = broadcastScalar @a n v-materialize _ (Flat c) = c-materialize _ (Group _) =- error "materialize: cannot flatten a grouped value to a single column"--{- | Replicate a scalar to a column of length @n@, choosing the most-efficient representation.--}-broadcastScalar :: forall a. (Columnable a) => Int -> a -> Column-broadcastScalar n v = case sUnbox @a of- STrue -> fromUnboxedVector (VU.replicate n v)- SFalse -> fromVector (V.replicate n v)------------------------------------------------------------------------------------ Lifting: the core combinators------------------------------------------------------------------------------------ | Apply a pure function to a 'Value'.-liftValue ::- (Columnable b, Columnable a) =>- (b -> a) -> Value b -> Either DataFrameException (Value a)-liftValue f (Scalar v) = Right (Scalar (f v))-liftValue f (Flat col) = Flat <$> mapColumn f col-liftValue f (Group gs) = Group <$> V.mapM (mapColumn f) gs--{- | Apply a binary function to two 'Value's. When one side is a-'Scalar' the operation degenerates to a 'liftValue' — this is how the-old @Binary op (Lit l) right@ special cases are recovered without-explicit pattern matches in the evaluator.--}-liftValue2 ::- (Columnable c, Columnable b, Columnable a) =>- (c -> b -> a) ->- Value c ->- Value b ->- Either DataFrameException (Value a)-liftValue2 f (Scalar l) (Scalar r) = Right (Scalar (f l r))-liftValue2 f (Scalar l) v = liftValue (f l) v-liftValue2 f v (Scalar r) = liftValue (`f` r) v-liftValue2 f (Flat l) (Flat r) = Flat <$> zipWithColumns f l r-liftValue2 f (Group ls) (Group rs)- | V.length ls == V.length rs =- Group <$> V.zipWithM (zipWithColumns f) ls rs--- Shape mismatches: aggregated vs. non-aggregated.-liftValue2 _ (Flat _) (Group _) =- Left $ AggregatedAndNonAggregatedException "aggregated" "non-aggregated"-liftValue2 _ (Group _) (Flat _) =- Left $ AggregatedAndNonAggregatedException "non-aggregated" "aggregated"-liftValue2 _ (Group _) (Group _) =- Left $ InternalException "Group count mismatch in binary operation"---- | Branch on a boolean 'Value', selecting from two same-typed 'Value's.-branchValue ::- forall a.- (Columnable a) =>- Value Bool ->- Value a ->- Value a ->- Either DataFrameException (Value a)-branchValue (Scalar True) l _ = Right l-branchValue (Scalar False) _ r = Right r-branchValue cond (Scalar l) (Scalar r) =- liftValue (\c -> if c then l else r) cond-branchValue cond (Scalar l) r =- liftValue2 (\c rv -> if c then l else rv) cond r-branchValue cond l (Scalar r) =- liftValue2 (\c lv -> if c then lv else r) cond l-branchValue (Flat cc) (Flat lc) (Flat rc) =- Flat <$> branchColumn @a cc lc rc-branchValue (Group cgs) (Group lgs) (Group rgs)- | V.length cgs == V.length lgs- && V.length lgs == V.length rgs =- Group- <$> V.generateM- (V.length cgs)- ( \i ->- branchColumn @a (cgs V.! i) (lgs V.! i) (rgs V.! i)- )-branchValue _ _ _ =- Left $- AggregatedAndNonAggregatedException- "if-then-else branches"- "mismatched shapes"--{- | Low-level column branch: given a boolean column and two same-typed-columns, produce the element-wise selection.--}-branchColumn ::- forall a.- (Columnable a) =>- Column ->- Column ->- Column ->- Either DataFrameException Column-branchColumn cc lc rc = do- cs <- toVector @Bool @V.Vector cc- ls <- toVector @a @V.Vector lc- rs <- toVector @a @V.Vector rc- pure $- fromVector @a $- V.zipWith3 (\c l r -> if c then l else r) cs ls rs------------------------------------------------------------------------------------ Error enrichment----------------------------------------------------------------------------------{- | Wrap an interpretation step so that any 'TypeMismatchException' gets-annotated with the expression that was being evaluated.--}-addContext ::- (Show a) => Expr a -> Either DataFrameException b -> Either DataFrameException b-addContext expr = first (enrichError (show expr))--enrichError :: String -> DataFrameException -> DataFrameException-enrichError loc (TypeMismatchException ctx) =- TypeMismatchException- ctx- { callingFunctionName =- callingFunctionName ctx <|+> Just "eval"- , errorColumnName =- errorColumnName ctx <|+> Just loc- }- where- -- Prefer the existing value; fall back to the new one.- Nothing <|+> b = b- a <|+> _ = a-enrichError _ e = e------------------------------------------------------------------------------------ Group slicing----------------------------------------------------------------------------------{- | Given a flat column and grouping metadata, produce one 'Column' per-group. Each result column is an O(1) slice into a sorted copy of the-input — the sort happens once, not per-group.--}-sliceGroups :: Column -> VU.Vector Int -> VU.Vector Int -> V.Vector Column-sliceGroups col os indices = case col of- BoxedColumn vec ->- let !sorted = V.unsafeBackpermute vec (V.convert indices)- in V.generate nGroups $ \i ->- BoxedColumn (V.unsafeSlice (start i) (len i) sorted)- UnboxedColumn vec ->- let !sorted = VU.unsafeBackpermute vec indices- in V.generate nGroups $ \i ->- UnboxedColumn (VU.unsafeSlice (start i) (len i) sorted)- OptionalColumn vec ->- let !sorted = V.unsafeBackpermute vec (V.convert indices)- in V.generate nGroups $ \i ->- OptionalColumn (V.unsafeSlice (start i) (len i) sorted)- where- !nGroups = VU.length os - 1- start i = os `VU.unsafeIndex` i- len i = os `VU.unsafeIndex` (i + 1) - start i-{-# INLINE sliceGroups #-}--numGroups :: GroupedDataFrame -> Int-numGroups gdf = VU.length (offsets gdf) - 1------------------------------------------------------------------------------------ eval: the unified interpreter----------------------------------------------------------------------------------{- | Evaluate an expression in a given context, producing a 'Value'.-This single function replaces both the old @interpret@ (flat) and-@interpretAggregation@ (grouped) code paths.--}-eval ::- forall a.- (Columnable a) =>- Ctx -> Expr a -> Either DataFrameException (Value a)--- Leaves -------------------------------------------------------------------eval _ (Lit v) = Right (Scalar v)-eval (FlatCtx df) (Col name) =- case getColumn name df of- Nothing ->- Left $- ColumnNotFoundException name "" (M.keys $ columnIndices df)- Just c -> Right (Flat c)-eval (GroupCtx gdf) (Col name) =- case getColumn name (fullDataframe gdf) of- Nothing ->- Left $- ColumnNotFoundException- name- ""- (M.keys $ columnIndices $ fullDataframe gdf)- Just c ->- Right- ( Group- (sliceGroups c (offsets gdf) (valueIndices gdf))- )--- Unary --------------------------------------------------------------------eval ctx expr@(Unary (op :: UnaryOp b a) inner) = addContext expr $ do- v <- eval @b ctx inner- liftValue (unaryFn op) v---- Binary -------------------------------------------------------------------eval ctx expr@(Binary (op :: BinaryOp c b a) left right) =- addContext expr $ do- l <- eval @c ctx left- r <- eval @b ctx right- liftValue2 (binaryFn op) l r---- If -----------------------------------------------------------------------eval ctx expr@(If cond l r) = addContext expr $ do- c <- eval @Bool ctx cond- lv <- eval @a ctx l- rv <- eval @a ctx r- branchValue c lv rv---- Fast path: FoldAgg (seeded) on a bare Col in GroupCtx.--- Avoids the O(n) backpermute in sliceGroups by folding directly over--- permuted indices. Only matches when inner is exactly (Col name).--eval (GroupCtx gdf) expr@(Agg (FoldAgg _ (Just seed) (f :: a -> b -> a)) (Col name :: Expr b)) =- addContext expr $- case getColumn name (fullDataframe gdf) of- Nothing ->- Left $- ColumnNotFoundException- name- ""- (M.keys $ columnIndices $ fullDataframe gdf)- Just col ->- Flat <$> foldLinearGroups @b @a f seed col (rowToGroup gdf) (numGroups gdf)--- Fast path: FoldAgg (seedless) on a bare Col in GroupCtx.--eval (GroupCtx gdf) expr@(Agg (FoldAgg _ Nothing (f :: a -> b -> a)) (Col name :: Expr b)) =- addContext expr $- case testEquality (typeRep @a) (typeRep @b) of- Nothing ->- Left $- InternalException- "Type mismatch in seedless fold: \- \accumulator and element types must match"- Just Refl ->- case getColumn name (fullDataframe gdf) of- Nothing ->- Left $- ColumnNotFoundException- name- ""- (M.keys $ columnIndices $ fullDataframe gdf)- Just col ->- Flat . fromVector- <$> foldl1DirectGroups @b f col (valueIndices gdf) (offsets gdf)--- Fast path: MergeAgg on a bare Col in GroupCtx.--eval- (GroupCtx gdf)- expr@( Agg- (MergeAgg _ seed (step :: acc -> b -> acc) _ (finalize :: acc -> a))- (Col name :: Expr b)- ) =- addContext expr $- case getColumn name (fullDataframe gdf) of- Nothing ->- Left $- ColumnNotFoundException- name- ""- (M.keys $ columnIndices $ fullDataframe gdf)- Just col ->- Flat- <$> ( foldLinearGroups @b step seed col (rowToGroup gdf) (numGroups gdf)- >>= mapColumn finalize- )--- Aggregation: CollectAgg --------------------------------------------------eval ctx expr@(Agg (CollectAgg _ (f :: v b -> a)) inner) =- addContext expr $ do- v <- eval @b ctx inner- case v of- Scalar _ ->- Left $- InternalException- "Cannot apply a collection aggregation to a scalar"- Flat col ->- Scalar <$> applyCollect @v @b @a f col- Group gs ->- Flat . fromVector- <$> V.mapM (applyCollect @v @b @a f) gs---- Aggregation: FoldAgg with seed -------------------------------------------eval ctx expr@(Agg (FoldAgg _ (Just seed) (f :: a -> b -> a)) inner) =- addContext expr $ do- v <- eval @b ctx inner- case v of- Scalar _ ->- Left $- InternalException- "Cannot apply a fold aggregation to a scalar"- Flat col ->- Scalar <$> foldlColumn @b @a f seed col- Group gs ->- Flat . fromVector- <$> V.mapM (foldlColumn @b @a f seed) gs---- Aggregation: MergeAgg ----------------------------------------------------eval- ctx- expr@( Agg- (MergeAgg _ seed (step :: acc -> b -> acc) _ (finalize :: acc -> a))- (inner :: Expr b)- ) =- addContext expr $ do- v <- eval @b ctx inner- case v of- Scalar _ ->- Left $- InternalException- "Cannot apply a merge aggregation to a scalar"- Flat col ->- Scalar . finalize <$> foldlColumnWith @b step seed col- Group gs ->- Flat . fromVector- <$> V.mapM (fmap finalize . foldlColumnWith @b step seed) gs---- Aggregation: FoldAgg without seed (fold1) --------------------------------eval ctx expr@(Agg (FoldAgg _ Nothing (f :: a -> b -> a)) inner) =- addContext expr $- case testEquality (typeRep @a) (typeRep @b) of- Nothing ->- Left $- InternalException- "Type mismatch in seedless fold: \- \accumulator and element types must match"- Just Refl -> do- v <- eval @b ctx inner- case v of- Scalar _ ->- Left $- InternalException- "Cannot apply a fold aggregation to a scalar"- Flat col ->- Scalar <$> foldl1Column @a f col- Group gs ->- Flat . fromVector- <$> V.mapM (foldl1Column @a f) gs------------------------------------------------------------------------------------ Aggregation helpers----------------------------------------------------------------------------------{- | Apply a 'CollectAgg' function to a single column, extracting the-appropriate vector type and applying the aggregation function.--}-applyCollect ::- forall v b a.- (VG.Vector v b, Typeable v, Columnable b, Columnable a) =>- (v b -> a) -> Column -> Either DataFrameException a-applyCollect f col = f <$> toVector @b @v col------------------------------------------------------------------------------------ Backward-compatible wrappers----------------------------------------------------------------------------------{- | Result of interpreting an expression in a grouped context.-Retained for backward compatibility with 'aggregate' and friends.--}-data AggregationResult a- = UnAggregated Column- | Aggregated (TypedColumn a)--{- | Interpret an expression against a flat 'DataFrame', producing a-typed column. This is the original top-level entry point; internally-it calls 'eval' and materialises the result.--NOTE: unlike the old implementation, 'Lit' values are no longer-eagerly broadcast. The broadcast happens here, at the boundary,-via 'materialize'.--}-interpret ::- forall a.- (Columnable a) =>- DataFrame -> Expr a -> Either DataFrameException (TypedColumn a)-interpret df expr = do- v <- eval (FlatCtx df) expr- pure $ TColumn $ materialize @a (fst (dataframeDimensions df)) v--{- | Interpret an expression against a 'GroupedDataFrame',-distinguishing aggregated results from bare column references.-Internally calls 'eval'.--}-interpretAggregation ::- forall a.- (Columnable a) =>- GroupedDataFrame ->- Expr a ->- Either DataFrameException (AggregationResult a)-interpretAggregation gdf expr = do- v <- eval (GroupCtx gdf) expr- case v of- Scalar a ->- Right $- Aggregated $- TColumn $- broadcastScalar @a (numGroups gdf) a- Flat col ->- Right $ Aggregated $ TColumn col- Group _ ->- -- The Column payload is intentionally unused — the only- -- call-site ('aggregate') immediately throws- -- 'UnaggregatedException' on this constructor.- Right $ UnAggregated $ BoxedColumn @T.Text V.empty
− src/DataFrame/Internal/Parsing.hs
@@ -1,232 +0,0 @@-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}--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 qualified Data.Text.IO as TIO--import Control.Applicative (many, (<|>))-import Data.Attoparsec.Text hiding (decimal, double, signed)-import Data.ByteString.Lex.Fractional-import Data.Foldable (fold)-import Data.Maybe (fromMaybe)-import Data.Text.Read (decimal, double, signed)-import Data.Time (Day, defaultTimeLocale, parseTimeM)-import GHC.Stack (HasCallStack)-import System.IO (Handle, IOMode (..), hIsEOF, hTell, withFile)-import Text.Read (readMaybe)-import Prelude hiding (takeWhile)--isNullish :: T.Text -> Bool-isNullish =- ( `S.member`- S.fromList- ["Nothing", "NULL", "", " ", "nan", "null", "N/A", "NaN", "NAN", "NA"]- )--isNullishBS :: C.ByteString -> Bool-isNullishBS =- ( `S.member`- S.fromList- ["Nothing", "NULL", "", " ", "nan", "null", "N/A", "NaN", "NAN", "NA"]- )--isTrueish :: T.Text -> Bool-isTrueish t = t `elem` ["True", "true", "TRUE"]--isFalseish :: T.Text -> Bool-isFalseish t = t `elem` ["False", "false", "FALSE"]--readValue :: (HasCallStack, Read a) => T.Text -> a-readValue s = case readMaybe (T.unpack s) of- Nothing -> error ("Could not read value: " <> T.unpack s)- Just value -> value--readBool :: (HasCallStack) => T.Text -> Maybe Bool-readBool s- | isTrueish s = Just True- | isFalseish s = Just False- | otherwise = Nothing--readByteStringBool :: C.ByteString -> Maybe Bool-readByteStringBool s- | s `elem` ["True", "true", "TRUE"] = Just True- | s `elem` ["False", "false", "FALSE"] = Just False- | otherwise = Nothing--readByteStringDate :: String -> C.ByteString -> Maybe Day-readByteStringDate fmt = parseTimeM True defaultTimeLocale fmt . C.unpack--readInteger :: (HasCallStack) => T.Text -> Maybe Integer-readInteger s = case signed decimal (T.strip s) of- 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-{-# 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-{-# INLINE readByteStringInt #-}--readByteStringDouble :: (HasCallStack) => C.ByteString -> Maybe Double-readByteStringDouble s =- let- readFunc = if C.any (\c -> c == 'e' || c == 'E') s then readExponential else readDecimal- in- case readSigned readFunc (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-{-# 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-{-# 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-{-# 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-{-# INLINE readDoubleEither #-}--safeReadValue :: (Read a) => T.Text -> Maybe a-safeReadValue s = readMaybe (T.unpack s)--readWithDefault :: (HasCallStack, Read a) => a -> T.Text -> a-readWithDefault v s = fromMaybe v (readMaybe (T.unpack s))---- ------------------------------------------------------------------------------ Attoparsec CSV parser combinators (shared between Lazy.IO.CSV and others)--- -----------------------------------------------------------------------------parseSep :: Char -> T.Text -> [T.Text]-parseSep c s = either error id (parseOnly (record c) s)-{-# INLINE parseSep #-}--record :: Char -> Parser [T.Text]-record c =- field c `sepBy1` char c- <?> "record"-{-# INLINE record #-}--parseRow :: Char -> Parser [T.Text]-parseRow c = (record c <* lineEnd) <?> "record-new-line"--field :: Char -> Parser T.Text-field c =- quotedField <|> unquotedField c- <?> "field"-{-# INLINE field #-}--unquotedTerminators :: Char -> S.Set Char-unquotedTerminators sep = S.fromList [sep, '\n', '\r', '"']--unquotedField :: Char -> Parser T.Text-unquotedField 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 '"'-{-# INLINE quotedField #-}--lineEnd :: Parser ()-lineEnd =- (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- 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 _ -> fail $ "Partial handler is called; n = " <> show n- Done (unconsumed :: T.Text) _ ->- go (n + 1) unconsumed h-{-# INLINE countRows #-}---- | Infer the Haskell type name from a text sample.-inferValueType :: T.Text -> T.Text-inferValueType s = case readInt s of- Just _ -> "Int"- Nothing -> case readDouble s of- Just _ -> "Double"- Nothing -> "Other"-{-# INLINE inferValueType #-}---- | Read a single CSV row from a handle using the given separator.-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 _ -> fail "Partial handler is called"- Done (unconsumed :: T.Text) (row :: [T.Text]) ->- return (row, unconsumed)
− src/DataFrame/Internal/Row.hs
@@ -1,224 +0,0 @@-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE InstanceSigs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.Internal.Row where--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.Algorithms.Merge as VA-import qualified Data.Vector.Generic as VG-import qualified Data.Vector.Unboxed as VU--import Control.DeepSeq (NFData (..))-import Control.Exception (throw)-import Control.Monad.ST (runST)-import Data.Function (on)-import Data.Maybe (fromMaybe)-import Data.Type.Equality (TestEquality (..))-import Data.Typeable (type (:~:) (..))-import DataFrame.Errors (DataFrameException (..))-import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame-import DataFrame.Internal.Expression (Expr (..))-import Text.ParserCombinators.ReadPrec (ReadPrec)-import Text.Read (- Lexeme (Ident),- lexP,- parens,- readListPrec,- readListPrecDefault,- readPrec,- )-import Type.Reflection (typeOf, typeRep)--data Any where- Value :: (Columnable a) => a -> Any--instance Eq Any where- (==) :: Any -> Any -> Bool- (Value a) == (Value b) = fromMaybe False $ do- Refl <- testEquality (typeOf a) (typeOf b)- return $ a == b--instance Ord Any where- (<=) :: Any -> Any -> Bool- (Value a) <= (Value b) = fromMaybe False $ do- Refl <- testEquality (typeOf a) (typeOf b)- return $ a <= b--instance Show Any where- show :: Any -> String- show (Value a) = T.unpack (showValue a)--instance NFData Any where- rnf (Value a) = rnf a--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--instance Read Any where- readListPrec :: ReadPrec [Any]- readListPrec = readListPrecDefault-- readPrec :: ReadPrec Any- readPrec = parens $ do- Ident "Value" <- lexP- readPrec---- | Wraps a value into an \Any\ type. This helps up represent rows as heterogenous lists.-toAny :: forall a. (Columnable a) => a -> Any-toAny = Value---- | Unwraps a value from an \Any\ type.-fromAny :: forall a. (Columnable a) => Any -> Maybe a-fromAny (Value (v :: b)) = do- Refl <- testEquality (typeRep @a) (typeRep @b)- pure v--type Row = V.Vector Any--(!?) :: [a] -> Int -> Maybe a-(!?) [] _ = Nothing-(!?) (x : _) 0 = Just x-(!?) (x : xs) n = (!?) xs (n - 1)--mkColumnFromRow :: Int -> [[Any]] -> Column-mkColumnFromRow i rows = case rows of- [] -> fromList ([] :: [T.Text])- (row : _) -> case row !? i of- Nothing -> fromList ([] :: [T.Text])- Just (Value (v :: a)) -> fromList $ reverse $ L.foldl' addToList [v] (drop 1 rows)- where- addToList acc r = case r !? i of- Nothing -> acc- Just (Value (v' :: b)) -> case testEquality (typeRep @a) (typeRep @b) of- Nothing -> acc- Just Refl -> v' : acc--{- | Converts the entire dataframe to a list of rows.--Each row contains all columns in the dataframe, ordered by their column indices.-The rows are returned in their natural order (from index 0 to n-1).--==== __Examples__-->>> toRowList df-[[("name", "Alice"), ("age", 25), ...], [("name", "Bob"), ("age", 30), ...], ...]--==== __Performance note__--This function materializes all rows into a list, which may be memory-intensive-for large dataframes. Consider using 'toRowVector' if you need random access-or streaming operations.--}-toRowList :: DataFrame -> [[(T.Text, Any)]]-toRowList df =- let- names = map fst (L.sortBy (compare `on` snd) $ M.toList (columnIndices df))- in- map- (zip names . V.toList . mkRowRep df names)- [0 .. (fst (dataframeDimensions df) - 1)]--{- | Converts the dataframe to a vector of rows with only the specified columns.--Each row will contain only the columns named in the @names@ parameter.-This is useful when you only need a subset of columns or want to control-the column order in the resulting rows.--==== __Parameters__--[@names@] List of column names to include in each row. The order of names- determines the order of fields in the resulting rows.--[@df@] The dataframe to convert.--==== __Examples__-->>> toRowVector ["name", "age"] df-Vector of rows with only name and age fields-->>> toRowVector [] df -- Empty column list-Vector of empty rows (one per dataframe row)--}-toRowVector :: [T.Text] -> DataFrame -> V.Vector Row-toRowVector names df = V.generate (fst (dataframeDimensions df)) (mkRowRep df names)--{- | Given a row gets the value associated with a field.--==== __Examples__-->>> map (rowValue (F.col @Int "age")) (toRowList df)-[25,30, ...]--}-rowValue :: forall a. Expr a -> [(T.Text, Any)] -> Maybe a-rowValue (Col name) row = lookup name row >>= fromAny @a-rowValue _ _ = error "Can only get rowValue of column reference"--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"- (M.keys $ columnIndices df)- Just (BoxedColumn column) -> toAny (column V.! i)- Just (UnboxedColumn column) -> toAny (column VU.! i)- Just (OptionalColumn column) -> toAny (column V.! i)---- This function will return the items in the order that is specified--- by the user. For example, if the dataframe consists of the columns--- "Age", "Pclass", "Name", and the user asks for ["Name", "Age"],--- this will order the values in the order ["Mr Smith", 50]-mkRowRep :: DataFrame -> [T.Text] -> Int -> Row-mkRowRep df names i = V.generate (L.length names) (\index -> get (names' V.! index))- where- names' = V.fromList names- 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- Nothing ->- throw $ ColumnNotFoundException name "mkRowRep" (M.keys $ columnIndices df)--sortedIndexes' :: [Bool] -> V.Vector Row -> VU.Vector Int-sortedIndexes' flipCompare rows = runST $ do- withIndexes <- VG.thaw (V.indexed rows)- VA.sortBy (produceOrderingFromRow flipCompare `on` snd) withIndexes- sorted <- VG.unsafeFreeze withIndexes- return $ VU.generate (VG.length rows) (\i -> fst (sorted VG.! i))--produceOrderingFromRow :: [Bool] -> Row -> Row -> Ordering-produceOrderingFromRow mustFlips v1 v2 = V.foldr (<>) mempty vZipped- where- vFlip = V.fromList mustFlips- vZipped =- V.zipWith3 (\b e1 e2 -> if b then compare e1 e2 else compare e2 e1) vFlip v1 v2
− src/DataFrame/Internal/Schema.hs
@@ -1,98 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE InstanceSigs #-}-{-# 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 Data.Maybe (isJust)-import Data.Type.Equality (TestEquality (..))-import DataFrame.Internal.Column (Columnable)-import Type.Reflection (typeRep)---- | A runtime tag for a column’s element type.-data SchemaType where- -- | Constructor carrying a 'Proxy' of the element type.- SType :: (Columnable a) => P.Proxy a -> SchemaType--{- | Show the underlying element type using 'typeRep'.--==== __Examples__->>> :set -XTypeApplications->>> show (schemaType @Bool)-"Bool"--}-instance Show SchemaType where- show :: SchemaType -> String- show (SType (_ :: P.Proxy a)) = show (typeRep @a)--{- | Two 'SchemaType's are equal iff their element types are the same.--==== __Examples__->>> :set -XTypeApplications->>> schemaType @Int == schemaType @Int-True-->>> schemaType @Int == schemaType @Integer-False--}-instance Eq SchemaType where- (==) :: SchemaType -> SchemaType -> Bool- (==) (SType (_ :: P.Proxy a)) (SType (_ :: P.Proxy b)) =- isJust (testEquality (typeRep @a) (typeRep @b))--{- | Construct a 'SchemaType' for the given @a@.--==== __Examples__->>> :set -XTypeApplications->>> schemaType @T.Text == schemaType @T.Text-True-->>> show (schemaType @Double)-"Double"--}-schemaType :: forall a. (Columnable a) => SchemaType-schemaType = SType (P.Proxy @a)--{- | Logical schema of a 'DataFrame': a mapping from column names to their-element types ('SchemaType').--==== __Examples__-Constructing and querying a schema:-->>> import qualified Data.Map as M->>> import qualified Data.Text as T->>> let s = Schema (M.fromList [("country", schemaType @T.Text), ("amount", schemaType @Double)])->>> M.lookup "amount" (elements s) == Just (schemaType @Double)-True--Extending a schema:-->>> let s' = Schema (M.insert "discount" (schemaType @Double) (elements s))->>> M.member "discount" (elements s')-True--Equality is structural over the map contents:-->>> let a = Schema (M.fromList [("x", schemaType @Int), ("y", schemaType @Double)])->>> let b = Schema (M.fromList [("y", schemaType @Double), ("x", schemaType @Int)])->>> a == b-True--}-newtype Schema = Schema- { elements :: M.Map T.Text SchemaType- {- ^ Mapping from /column name/ to its 'SchemaType'.-- Invariant: keys are unique column names. A missing key means the column- is not present in the schema.- -}- }- deriving (Show, Eq)
− src/DataFrame/Internal/Statistics.hs
@@ -1,282 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}--module DataFrame.Internal.Statistics where--import qualified Data.Vector as V-import qualified Data.Vector.Algorithms.Intro as VA-import qualified Data.Vector.Mutable as VM-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.Errors (DataFrameException (..))--mean' :: (Real a, VU.Unbox a) => VU.Vector a -> Double-mean' samp- | VU.null samp = throw $ EmptyDataSetException "mean"- | otherwise = rtf (VU.sum samp) / fromIntegral (VU.length samp)-{-# INLINE [0] mean' #-}--meanDouble' :: VU.Vector Double -> Double-meanDouble' samp- | VU.null samp = throw $ EmptyDataSetException "mean"- | otherwise = VU.sum samp / fromIntegral (VU.length samp)-{-# INLINE meanDouble' #-}--meanInt' :: VU.Vector Int -> Double-meanInt' samp- | VU.null samp = throw $ EmptyDataSetException "mean"- | otherwise = fromIntegral (VU.sum samp) / fromIntegral (VU.length samp)-{-# INLINE meanInt' #-}--{-# RULES-"mean'/Double" [1] forall (xs :: VU.Vector Double).- mean' xs =- meanDouble' xs-"mean'/Int" [1] forall (xs :: VU.Vector Int).- mean' xs =- meanInt' xs- #-}--median' :: (Real a, VU.Unbox a) => VU.Vector a -> 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 (rtf middleElement)- else do- prev <- VUM.read mutableSamp (middleIndex - 1)- pure (rtf (middleElement + prev) / 2)-{-# INLINE median' #-}---- accumulator: count, mean, m2-data VarAcc- = VarAcc {-# UNPACK #-} !Int {-# UNPACK #-} !Double {-# UNPACK #-} !Double- deriving (Show)--varianceStep :: VarAcc -> Double -> VarAcc-varianceStep (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'-{-# INLINE varianceStep #-}--computeVariance :: VarAcc -> Double-computeVariance (VarAcc !n _ !m2)- | n < 2 = 0 -- or error "variance of <2 samples"- | otherwise = m2 / fromIntegral (n - 1)-{-# INLINE computeVariance #-}--variance' :: (Real a, VU.Unbox a) => VU.Vector a -> Double-variance' = computeVariance . VU.foldl' varianceStep (VarAcc 0 0 0) . VU.map rtf-{-# INLINE variance' #-}--varianceDouble' :: VU.Vector Double -> Double-varianceDouble' = computeVariance . VU.foldl' varianceStep (VarAcc 0 0 0)-{-# INLINE varianceDouble' #-}---- accumulator: count, mean, m2, m3-data SkewAcc = SkewAcc !Int !Double !Double !Double deriving (Show)--skewnessStep :: (VU.Unbox a, Num a, Real a) => SkewAcc -> a -> SkewAcc-skewnessStep (SkewAcc !n !mean !m2 !m3) !x' =- let !n' = n + 1- x = rtf x'- !k = fromIntegral n'- !delta = x - mean- !mean' = mean + delta / k- !m2' = m2 + (delta ^ 2 * (k - 1)) / k- !m3' = m3 + (delta ^ 3 * (k - 1) * (k - 2)) / k ^ 2 - (3 * delta * m2) / k- in SkewAcc n' mean' m2' m3'-{-# INLINE skewnessStep #-}--computeSkewness :: SkewAcc -> Double-computeSkewness (SkewAcc n _ m2 m3)- | n < 3 = 0 -- or error "skewness of <3 samples"- | otherwise = (sqrt (fromIntegral n - 1) * m3) / sqrt (m2 ^ 3)-{-# INLINE computeSkewness #-}--skewness' :: (VU.Unbox a, Real a, Num a) => VU.Vector a -> Double-skewness' = computeSkewness . VU.foldl' skewnessStep (SkewAcc 0 0 0 0)-{-# INLINE skewness' #-}--data CorrelationStats- = CorrelationStats- {-# UNPACK #-} !Double- {-# UNPACK #-} !Double- {-# UNPACK #-} !Double- {-# UNPACK #-} !Double- {-# UNPACK #-} !Double--correlation' :: VU.Vector Double -> VU.Vector Double -> Maybe Double-correlation' xs ys- | n < 2 = Nothing- | VU.length xs /= VU.length ys = Nothing- | otherwise =- let nf = fromIntegral n- initial = CorrelationStats 0 0 0 0 0- (CorrelationStats sumX sumY sumXX sumYY sumXY) = VU.ifoldl' step initial xs-- !num = nf * sumXY - sumX * sumY- !den = sqrt ((nf * sumXX - sumX * sumX) * (nf * sumYY - sumY * sumY))- in Just (num / den)- where- n = VU.length xs- step (CorrelationStats sx sy sxx syy sxy) i x =- let !y = VU.unsafeIndex ys i- in CorrelationStats (sx + x) (sy + y) (sxx + x * x) (syy + y * y) (sxy + x * y)-{-# INLINE correlation' #-}--quantiles' ::- (VU.Unbox a, Num a, Real a) =>- VU.Vector Int -> Int -> VU.Vector a -> VU.Vector Double-quantiles' qs q samp- | VU.null samp = throw $ EmptyDataSetException "quantiles"- | q < 2 = throw $ WrongQuantileNumberException q- | VU.any (\i -> i < 0 || i > q) qs = throw $ WrongQuantileIndexException qs q- | otherwise = runST $ do- let !n = VU.length samp- mutableSamp <- VU.thaw samp- VA.sort mutableSamp- VU.mapM- ( \i -> do- let !p = fromIntegral i / fromIntegral q- !position = p * fromIntegral (n - 1)- !index = floor position- !f = position - fromIntegral index- x <- fmap rtf (VUM.read mutableSamp index)- if f == 0- then return x- else do- y <- fmap rtf (VUM.read mutableSamp (index + 1))- return $ (1 - f) * x + f * y- )- qs-{-# INLINE quantiles' #-}--percentile' :: (VU.Unbox a, Num a, Real a) => Int -> VU.Vector a -> Double-percentile' n = VU.head . quantiles' (VU.fromList [n]) 100--quantilesOrd' ::- (Ord a, Eq a) =>- VU.Vector Int -> Int -> V.Vector a -> V.Vector a-quantilesOrd' qs q samp- | V.null samp = throw $ EmptyDataSetException "quantiles"- | q < 2 = throw $ WrongQuantileNumberException q- | VU.any (\i -> i < 0 || i > q) qs = throw $ WrongQuantileIndexException qs q- | otherwise = runST $ do- let !n = V.length samp- mutableSamp <- V.thaw samp- VA.sort mutableSamp- V.mapM- ( \i -> do- let !p = fromIntegral i / fromIntegral q- !position = p * fromIntegral (n - 1)- !index = floor position- -- This is not exact for Ord instances.- -- Figure out how to make it so.- VM.read mutableSamp index- )- (V.convert qs)--percentileOrd' :: (Ord a, Eq a) => Int -> V.Vector a -> a-percentileOrd' n = V.head . quantilesOrd' (VU.fromList [n]) 100--interQuartileRange' :: (VU.Unbox a, Num a, Real a) => VU.Vector a -> Double-interQuartileRange' samp =- let quartiles = quantiles' (VU.fromList [1, 3]) 4 samp- in quartiles VU.! 1 - quartiles VU.! 0-{-# INLINE interQuartileRange' #-}--meanSquaredError :: VU.Vector Double -> VU.Vector Double -> Maybe Double-meanSquaredError target prediction =- let- squareDiff = VU.ifoldl' (\sq i e -> (e - target VU.! i) ^ 2 + sq) 0 prediction- in- Just $ squareDiff / fromIntegral (max (VU.length target) (VU.length prediction))-{-# INLINE meanSquaredError #-}--mutualInformationBinned ::- Int -> VU.Vector Double -> VU.Vector Double -> Maybe Double-mutualInformationBinned k xs ys- | VU.length xs /= VU.length ys = Nothing- | VU.null xs = Nothing- | k < 2 = Nothing- | rx <= 0 || ry <= 0 = Just 0- | otherwise =- let bx = VU.map (binIndex xmin xmax k) xs- by = VU.map (binIndex ymin ymax k) ys- n = fromIntegral (VU.length xs) :: Double- mx = bincount k bx- my = bincount k by- mxy = jointBincount k bx by- in Just $- sum- [ let !cxy = fromIntegral c- !pxy = cxy / n- !px = fromIntegral (mx VU.! i) / n- !py = fromIntegral (my VU.! j) / n- in if c == 0 then 0 else pxy * logBase 2 (pxy / (px * py))- | i <- [0 .. k - 1]- , j <- [0 .. k - 1]- , let !c = mxy VU.! (i * k + j)- ]- where- (xmin, xmax) = (VU.minimum xs, VU.maximum xs)- (ymin, ymax) = (VU.minimum ys, VU.maximum ys)- rx = xmax - xmin- ry = ymax - ymin--binIndex :: Double -> Double -> Int -> Double -> Int-binIndex lo hi k x- | hi == lo = 0- | otherwise =- let !t = (x - lo) / (hi - lo)- !ix = floor (fromIntegral k * t) :: Int- in max 0 (min (k - 1) ix)-{-# INLINE binIndex #-}--bincount :: Int -> VU.Vector Int -> VU.Vector Int-bincount k bs = VU.create $ do- mv <- VU.thaw (VU.replicate k 0)- VU.forM_ bs $ \b -> do- let i- | b < 0 = 0- | b >= k = k - 1- | otherwise = b- x <- VUM.read mv i- VUM.write mv i (x + 1)- pure mv-{-# INLINE bincount #-}--jointBincount :: Int -> VU.Vector Int -> VU.Vector Int -> VU.Vector Int-jointBincount k bx by = VU.create $ do- mv <- VU.thaw (VU.replicate (k * k) 0)- VU.forM_ (VU.zip bx by) $ \(i, j) -> do- let ii = clamp i 0 (k - 1)- jj = clamp j 0 (k - 1)- ix = ii * k + jj- x <- VUM.read mv ix- VUM.write mv ix (x + 1)- pure mv- where- clamp z a b = max a (min b z)-{-# INLINE jointBincount #-}--rtf :: (Real a) => a -> Double-rtf = realToFrac-{-# NOINLINE [1] rtf #-}--{-# RULES-"rtf/Double" [2] forall (x :: Double). rtf x = x- #-}
− src/DataFrame/Internal/Types.hs
@@ -1,129 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE UndecidableInstances #-}--module DataFrame.Internal.Types where--import Control.DeepSeq (NFData)-import Data.Int (Int16, Int32, Int64, Int8)-import Data.Kind (Constraint, Type)-import Data.Typeable (Typeable)-import qualified Data.Vector.Unboxed as VU-import Data.Word (Word16, Word32, Word64, Word8)--type Columnable' a = (Typeable a, Show a, Ord a, Eq a, Read a, NFData a)--{- | A type with column representations used to select the-"right" representation when specializing the `toColumn` function.--}-data Rep- = 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---- | 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--type family Numeric (a :: Type) :: Bool where- Numeric Integer = 'True- 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---- | Type-level boolean for constraint/type comparison.-data SBool (b :: Bool) where- STrue :: SBool 'True- SFalse :: SBool 'False---- | The runtime witness for our type-level branching.-class SBoolI (b :: Bool) where- sbool :: SBool b--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 = sbool @(Unboxable 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--type UnboxIf a = When (Unboxable a) (VU.Unbox a)--type family IntegralTypes (a :: Type) :: Bool where- IntegralTypes Integer = 'True- IntegralTypes Int = 'True- IntegralTypes Int8 = 'True- IntegralTypes Int16 = 'True- IntegralTypes Int32 = 'True- IntegralTypes Int64 = 'True- IntegralTypes Word = 'True- IntegralTypes Word8 = 'True- IntegralTypes Word16 = 'True- IntegralTypes Word32 = 'True- IntegralTypes Word64 = 'True- IntegralTypes _ = 'False--sIntegral :: forall a. (SBoolI (IntegralTypes a)) => SBool (IntegralTypes a)-sIntegral = sbool @(IntegralTypes a)--type IntegralIf a = When (IntegralTypes a) (Integral a)--type family FloatingTypes (a :: Type) :: Bool where- FloatingTypes Float = 'True- FloatingTypes Double = 'True- FloatingTypes _ = 'False--sFloating :: forall a. (SBoolI (FloatingTypes a)) => SBool (FloatingTypes a)-sFloating = sbool @(FloatingTypes a)--type FloatingIf a = When (FloatingTypes a) (Real a, Fractional a)
− src/DataFrame/Lazy.hs
@@ -1,3 +0,0 @@-module DataFrame.Lazy (module DataFrame.Lazy.Internal.DataFrame) where--import DataFrame.Lazy.Internal.DataFrame
− src/DataFrame/Lazy/IO/Binary.hs
@@ -1,444 +0,0 @@-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE StrictData #-}-{-# LANGUAGE TypeApplications #-}--{- | Simple column-oriented binary spill format (DFBN).--Layout (all integers little-endian):--@-[magic: 4 bytes] "DFBN"-[num_columns: 4 bytes] Word32- per column:- [name_len: 2 bytes] Word16 (byte length of UTF-8 name)- [name: name_len bytes]- [type_tag: 1 byte] Word8-[num_rows: 8 bytes] Word64--per column data block (order matches schema):- type_tag 0 (Int): num_rows × Int64 LE- type_tag 1 (Double): num_rows × Double LE (IEEE 754)- type_tag 2 (Text): (num_rows+1) × Word32 offsets ++ payload bytes (UTF-8)- type_tag 3 (Maybe Int): ceil(num_rows/8)-byte null bitmap ++ num_rows × Int64 LE- type_tag 4 (Maybe Double): ceil(num_rows/8)-byte null bitmap ++ num_rows × Double LE- type_tag 5 (Maybe Text): ceil(num_rows/8)-byte null bitmap- ++ (num_rows+1) × Word32 offsets ++ payload bytes-@--Null bitmap: bit @i@ of byte @i\/8@ is 1 when row @i@ is non-null.--}-module DataFrame.Lazy.IO.Binary (- spillToDisk,- readSpilled,- withSpilled,-) where--import Control.Exception (SomeException, bracket, try)-import Control.Monad (foldM, void, when)-import qualified Data.ByteString as BS-import qualified Data.ByteString.Builder as BSB-import qualified Data.ByteString.Internal as BSI-import qualified Data.ByteString.Unsafe as BSU-import qualified Data.List as L-import qualified Data.Map.Strict as M-import qualified Data.Text as T-import qualified Data.Text.Encoding as TE-import qualified Data.Vector as V-import qualified Data.Vector.Storable as VS-import qualified Data.Vector.Unboxed as VU--import Data.Bits (setBit, shiftL, testBit, (.|.))-import Data.Maybe (fromMaybe, isJust)-import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))-import Data.Word (Word16, Word32, Word64, Word8)-import DataFrame.Internal.Column (Column (..))-import DataFrame.Internal.DataFrame (DataFrame (..))-import Foreign (ForeignPtr, castForeignPtr, plusForeignPtr, sizeOf)-import System.Directory (getTemporaryDirectory, removeFile)-import System.IO (IOMode (..), hClose, openTempFile, withFile)-import Type.Reflection (typeRep)---- ------------------------------------------------------------------------------ Type tags--- -----------------------------------------------------------------------------tagInt, tagDouble, tagText, tagMaybeInt, tagMaybeDouble, tagMaybeText :: Word8-tagInt = 0-tagDouble = 1-tagText = 2-tagMaybeInt = 3-tagMaybeDouble = 4-tagMaybeText = 5---- ------------------------------------------------------------------------------ Write--- ------------------------------------------------------------------------------- | Serialise a 'DataFrame' to a DFBN binary file.-spillToDisk :: FilePath -> DataFrame -> IO ()-spillToDisk path df =- withFile path WriteMode $ \h -> BSB.hPutBuilder h (buildDataFrame df)--buildDataFrame :: DataFrame -> BSB.Builder-buildDataFrame df =- BSB.byteString "DFBN"- <> BSB.word32LE ncols- <> foldMap (uncurry buildColumnSchema) (zip names cols)- <> BSB.word64LE nrows- <> foldMap (buildColumnData nrowsInt) cols- where- names =- fmap- fst- (L.sortBy (\a b -> compare (snd a) (snd b)) (M.toList (columnIndices df)))- ncols = fromIntegral (length names) :: Word32- cols = V.toList (columns df)- nrowsInt = fst (dataframeDimensions df)- nrows = fromIntegral nrowsInt :: Word64--buildColumnSchema :: T.Text -> Column -> BSB.Builder-buildColumnSchema name col =- BSB.word16LE nameLen- <> BSB.byteString nameBytes- <> BSB.word8 (columnTypeTag col)- where- nameBytes = TE.encodeUtf8 name- nameLen = fromIntegral (BS.length nameBytes) :: Word16--columnTypeTag :: Column -> Word8-columnTypeTag (UnboxedColumn (_ :: VU.Vector a)) =- case testEquality (typeRep @a) (typeRep @Int) of- Just Refl -> tagInt- Nothing -> case testEquality (typeRep @a) (typeRep @Double) of- Just Refl -> tagDouble- Nothing -> error "spillToDisk: unsupported UnboxedColumn element type"-columnTypeTag (BoxedColumn _) = tagText-columnTypeTag (OptionalColumn (_ :: V.Vector (Maybe a))) =- case testEquality (typeRep @a) (typeRep @Int) of- Just Refl -> tagMaybeInt- Nothing -> case testEquality (typeRep @a) (typeRep @Double) of- Just Refl -> tagMaybeDouble- Nothing -> tagMaybeText--buildColumnData :: Int -> Column -> BSB.Builder-buildColumnData _ (UnboxedColumn (v :: VU.Vector a)) =- case testEquality (typeRep @a) (typeRep @Int) of- Just Refl -> buildIntVector v- Nothing ->- case testEquality (typeRep @a) (typeRep @Double) of- Just Refl -> buildDoubleVector v- Nothing -> error "spillToDisk: unsupported UnboxedColumn element type"-buildColumnData _ (BoxedColumn (v :: V.Vector a)) =- case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl -> buildTextVector v- Nothing -> error "spillToDisk: unsupported BoxedColumn element type"-buildColumnData _ (OptionalColumn (v :: V.Vector (Maybe a))) =- case testEquality (typeRep @a) (typeRep @Int) of- Just Refl ->- buildNullBitmap (V.map isJust v)- <> buildIntVector (VU.convert (V.map (fromMaybe 0) v))- Nothing ->- case testEquality (typeRep @a) (typeRep @Double) of- Just Refl ->- buildNullBitmap (V.map isJust v)- <> buildDoubleVector (VU.convert (V.map (fromMaybe 0.0) v))- Nothing ->- let showText x = case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl -> x- Nothing -> T.pack (show x)- texts = V.map (maybe T.empty showText) v- in buildNullBitmap (V.map isJust v) <> buildTextVector texts--{- | Bulk-encode an Int vector as 8-byte LE values (native layout on LE platforms).-hPutBuilder flushes synchronously so the underlying ForeignPtr outlives the Builder.--}-buildIntVector :: VU.Vector Int -> BSB.Builder-buildIntVector v =- let sv = VU.convert v :: VS.Vector Int- (fp, n) = VS.unsafeToForeignPtr0 sv- bs = BSI.fromForeignPtr (castForeignPtr fp) 0 (n * sizeOf (0 :: Int))- in BSB.byteString bs---- | Bulk-encode a Double vector as 8-byte LE IEEE 754 values (native layout on LE platforms).-buildDoubleVector :: VU.Vector Double -> BSB.Builder-buildDoubleVector v =- let sv = VU.convert v :: VS.Vector Double- (fp, n) = VS.unsafeToForeignPtr0 sv- bs = BSI.fromForeignPtr (castForeignPtr fp) 0 (n * sizeOf (0 :: Double))- in BSB.byteString bs---- | Write a Text vector: (num_rows+1) Word32 offsets followed by UTF-8 payload.-buildTextVector :: V.Vector T.Text -> BSB.Builder-buildTextVector v =- foldMap BSB.word32LE offsets <> foldMap BSB.byteString encoded- where- encoded = V.toList (V.map TE.encodeUtf8 v)- offsets = scanl (\acc bs -> acc + fromIntegral (BS.length bs)) (0 :: Word32) encoded---- | Build a null-validity bitmap: 1 bit per row, packed LSB-first into bytes.-buildNullBitmap :: V.Vector Bool -> BSB.Builder-buildNullBitmap valids = foldMap (BSB.word8 . mkByte) [0 .. numBytes - 1]- where- n = V.length valids- numBytes = (n + 7) `div` 8- mkByte byteIdx =- foldr- ( \bit acc ->- let row = byteIdx * 8 + bit- in if row < n && (valids V.! row) then setBit acc bit else acc- )- (0 :: Word8)- [0 .. 7]---- ------------------------------------------------------------------------------ Read--- ------------------------------------------------------------------------------- | @(new_offset, value)@-type ParseResult a = Either String (Int, a)---- | Deserialise a DFBN binary file into a 'DataFrame'.-readSpilled :: FilePath -> IO DataFrame-readSpilled path = do- bs <- BS.readFile path- case parseDataFrame bs 0 of- Left err -> fail ("readSpilled: " <> err)- Right (_, df) -> return df--parseDataFrame :: BS.ByteString -> Int -> ParseResult DataFrame-parseDataFrame bs off0 = do- (off1, magic) <- readBytes bs off0 4- when (magic /= "DFBN") $ Left "bad magic bytes"- (off2, ncols) <- readWord32LE bs off1- let ncolsInt = fromIntegral ncols :: Int- (off3, schema) <- readN ncolsInt (readColumnSchema bs) off2- (off4, nrows64) <- readWord64LE bs off3- let nrows = fromIntegral nrows64 :: Int- (off5, cols) <-- foldM- ( \(o, acc) (_, tag) -> do- (o', col) <- readColumnData bs o nrows tag- return (o', acc ++ [col])- )- (off4, [])- schema- let names = fmap fst schema- return- ( off5- , DataFrame- { columns = V.fromList cols- , columnIndices = M.fromList (zip names [0 ..])- , dataframeDimensions = (nrows, ncolsInt)- , derivingExpressions = M.empty- }- )--readColumnSchema :: BS.ByteString -> Int -> ParseResult (T.Text, Word8)-readColumnSchema bs off = do- (off1, nameLen) <- readWord16LE bs off- let nameLenInt = fromIntegral nameLen :: Int- (off2, nameBytes) <- readBytes bs off1 nameLenInt- (off3, tag) <- readWord8 bs off2- return (off3, (TE.decodeUtf8 nameBytes, tag))--readColumnData :: BS.ByteString -> Int -> Int -> Word8 -> ParseResult Column-readColumnData bs off nrows tag- | tag == tagInt = do- (off', v) <- readIntColumn bs off nrows- return (off', UnboxedColumn v)- | tag == tagDouble = do- (off', v) <- readDoubleColumn bs off nrows- return (off', UnboxedColumn v)- | tag == tagText = do- (off', v) <- readTextColumn bs off nrows- return (off', BoxedColumn v)- | tag == tagMaybeInt = do- (off1, bitmap) <- readNullBitmap bs off nrows- (off2, v) <- readIntColumn bs off1 nrows- let maybes =- V.fromList- (zipWith (\valid x -> if valid then Just x else Nothing) bitmap (VU.toList v)) ::- V.Vector (Maybe Int)- return (off2, OptionalColumn maybes)- | tag == tagMaybeDouble = do- (off1, bitmap) <- readNullBitmap bs off nrows- (off2, v) <- readDoubleColumn bs off1 nrows- let maybes =- V.fromList- (zipWith (\valid x -> if valid then Just x else Nothing) bitmap (VU.toList v)) ::- V.Vector (Maybe Double)- return (off2, OptionalColumn maybes)- | tag == tagMaybeText = do- (off1, bitmap) <- readNullBitmap bs off nrows- (off2, v) <- readTextColumn bs off1 nrows- let maybes =- V.fromList- (zipWith (\valid x -> if valid then Just x else Nothing) bitmap (V.toList v)) ::- V.Vector (Maybe T.Text)- return (off2, OptionalColumn maybes)- | otherwise = Left ("unknown type tag " <> show tag)--{- | Zero-copy Int column read: reuses the ByteString buffer's ForeignPtr.-Safe as long as 'bs' stays live during the caller's use of the resulting vector.-Only correct on little-endian platforms (aarch64/x86_64).--}-readIntColumn :: BS.ByteString -> Int -> Int -> ParseResult (VU.Vector Int)-readIntColumn bs off nrows- | off + nrows * 8 > BS.length bs = Left "unexpected end of input"- | otherwise =- let (fp, bsOff, _) = BSI.toForeignPtr bs- fp' = castForeignPtr (plusForeignPtr fp (bsOff + off)) :: ForeignPtr Int- sv = VS.unsafeFromForeignPtr0 fp' nrows :: VS.Vector Int- in Right (off + nrows * 8, VU.convert sv)--{- | Zero-copy Double column read: reuses the ByteString buffer's ForeignPtr.-Safe as long as 'bs' stays live during the caller's use of the resulting vector.-Only correct on little-endian platforms (aarch64/x86_64).--}-readDoubleColumn ::- BS.ByteString -> Int -> Int -> ParseResult (VU.Vector Double)-readDoubleColumn bs off nrows- | off + nrows * 8 > BS.length bs = Left "unexpected end of input"- | otherwise =- let (fp, bsOff, _) = BSI.toForeignPtr bs- fp' = castForeignPtr (plusForeignPtr fp (bsOff + off)) :: ForeignPtr Double- sv = VS.unsafeFromForeignPtr0 fp' nrows :: VS.Vector Double- in Right (off + nrows * 8, VU.convert sv)--readTextColumn :: BS.ByteString -> Int -> Int -> ParseResult (V.Vector T.Text)-readTextColumn bs off nrows = do- offsets <- readWord32Array bs off (nrows + 1)- let payloadStart = off + (nrows + 1) * 4- totalPayload = fromIntegral (last offsets) :: Int- when (payloadStart + totalPayload > BS.length bs) $- Left "unexpected end of input"- let sizes =- zipWith- (\a b -> fromIntegral b - fromIntegral a :: Int)- offsets- (drop 1 offsets)- texts =- zipWith- ( \o sz ->- TE.decodeUtf8- (BS.take sz (BS.drop (payloadStart + fromIntegral o) bs))- )- offsets- sizes- return (payloadStart + totalPayload, V.fromList texts)---- | Read @nrows@ null-bitmap bits (ceil(nrows\/8) bytes).-readNullBitmap :: BS.ByteString -> Int -> Int -> ParseResult [Bool]-readNullBitmap bs off nrows- | off + numBytes > BS.length bs = Left "unexpected end of input"- | otherwise =- Right- ( off + numBytes- , take- nrows- [ testBit (BSU.unsafeIndex bs (off + row `div` 8)) (row `mod` 8)- | row <- [0 ..]- ]- )- where- numBytes = (nrows + 7) `div` 8--readWord8 :: BS.ByteString -> Int -> ParseResult Word8-readWord8 bs off- | off >= BS.length bs = Left "unexpected end of input"- | otherwise = Right (off + 1, BSU.unsafeIndex bs off)--readWord16LE :: BS.ByteString -> Int -> ParseResult Word16-readWord16LE bs off- | off + 2 > BS.length bs = Left "unexpected end of input"- | otherwise =- let b0 = fromIntegral (BSU.unsafeIndex bs off) :: Word16- b1 = fromIntegral (BSU.unsafeIndex bs (off + 1)) :: Word16- in Right (off + 2, b0 .|. (b1 `shiftL` 8))--readWord32LE :: BS.ByteString -> Int -> ParseResult Word32-readWord32LE bs off- | off + 4 > BS.length bs = Left "unexpected end of input"- | otherwise =- let b0 = fromIntegral (BSU.unsafeIndex bs off) :: Word32- b1 = fromIntegral (BSU.unsafeIndex bs (off + 1)) :: Word32- b2 = fromIntegral (BSU.unsafeIndex bs (off + 2)) :: Word32- b3 = fromIntegral (BSU.unsafeIndex bs (off + 3)) :: Word32- in Right- (off + 4, b0 .|. (b1 `shiftL` 8) .|. (b2 `shiftL` 16) .|. (b3 `shiftL` 24))--readWord64LE :: BS.ByteString -> Int -> ParseResult Word64-readWord64LE bs off- | off + 8 > BS.length bs = Left "unexpected end of input"- | otherwise =- let b0 = fromIntegral (BSU.unsafeIndex bs off) :: Word64- b1 = fromIntegral (BSU.unsafeIndex bs (off + 1)) :: Word64- b2 = fromIntegral (BSU.unsafeIndex bs (off + 2)) :: Word64- b3 = fromIntegral (BSU.unsafeIndex bs (off + 3)) :: Word64- b4 = fromIntegral (BSU.unsafeIndex bs (off + 4)) :: Word64- b5 = fromIntegral (BSU.unsafeIndex bs (off + 5)) :: Word64- b6 = fromIntegral (BSU.unsafeIndex bs (off + 6)) :: Word64- b7 = fromIntegral (BSU.unsafeIndex bs (off + 7)) :: Word64- in Right- ( off + 8- , b0- .|. (b1 `shiftL` 8)- .|. (b2 `shiftL` 16)- .|. (b3 `shiftL` 24)- .|. (b4 `shiftL` 32)- .|. (b5 `shiftL` 40)- .|. (b6 `shiftL` 48)- .|. (b7 `shiftL` 56)- )---- | Read @n@ consecutive Word32LE values starting at offset @off@.-readWord32Array :: BS.ByteString -> Int -> Int -> Either String [Word32]-readWord32Array bs off n- | off + n * 4 > BS.length bs = Left "unexpected end of input"- | otherwise =- Right- [ let i = off + k * 4- b0 = fromIntegral (BSU.unsafeIndex bs i) :: Word32- b1 = fromIntegral (BSU.unsafeIndex bs (i + 1)) :: Word32- b2 = fromIntegral (BSU.unsafeIndex bs (i + 2)) :: Word32- b3 = fromIntegral (BSU.unsafeIndex bs (i + 3)) :: Word32- in b0 .|. (b1 `shiftL` 8) .|. (b2 `shiftL` 16) .|. (b3 `shiftL` 24)- | k <- [0 .. n - 1]- ]---- | Read @n@ bytes from @bs@ at @off@.-readBytes :: BS.ByteString -> Int -> Int -> ParseResult BS.ByteString-readBytes bs off n- | off + n > BS.length bs = Left "unexpected end of input"- | otherwise = Right (off + n, BS.take n (BS.drop off bs))---- | Apply @f@ @n@ times sequentially, threading the offset.-readN :: Int -> (Int -> ParseResult a) -> Int -> ParseResult [a]-readN 0 _ off = Right (off, [])-readN n f off = do- (off', x) <- f off- (off'', xs) <- readN (n - 1) f off'- return (off'', x : xs)---- ------------------------------------------------------------------------------ Bracket helper--- -----------------------------------------------------------------------------{- | Spill a DataFrame to a temporary file, run an action with the path,-then delete the file even if the action throws.--}-withSpilled :: DataFrame -> (FilePath -> IO a) -> IO a-withSpilled df action = do- tmpDir <- getTemporaryDirectory- bracket- ( do- (path, h) <- openTempFile tmpDir "dataframe_spill.dfbn"- hClose h- spillToDisk path df- return path- )- (\path -> void (try (removeFile path) :: IO (Either SomeException ())))- action
− src/DataFrame/Lazy/IO/CSV.hs
@@ -1,452 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.Lazy.IO.CSV where--import qualified Data.ByteString as BS-import qualified Data.Map as M-import qualified Data.Proxy as P-import qualified Data.Text as T-import qualified Data.Text.Encoding as TextEncoding-import qualified Data.Text.IO as TIO-import qualified Data.Vector as V-import qualified Data.Vector.Mutable as VM-import qualified Data.Vector.Unboxed.Mutable as VUM--import Control.Monad (forM_, unless, when, zipWithM_)-import Data.Attoparsec.Text (IResult (..), parseWith)-import Data.Char (intToDigit)-import Data.IORef-import Data.Maybe (fromMaybe, isJust)-import Data.Type.Equality (TestEquality (testEquality))-import Data.Word (Word8)-import DataFrame.Internal.Column (- Column (..),- MutableColumn (..),- columnLength,- freezeColumn',- writeColumn,- )-import DataFrame.Internal.DataFrame (DataFrame (..))-import DataFrame.Internal.Parsing-import DataFrame.Internal.Schema (Schema, SchemaType (..), elements)-import System.IO-import Type.Reflection-import Prelude hiding (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- }--{- | 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- }--{- | 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 :: FilePath -> 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.--}-readTsv :: FilePath -> IO DataFrame-readTsv path = fst <$> readSeparated '\t' defaultOptions path---- | Reads a character separated file into a dataframe using mutable vectors.-readSeparated ::- Char -> ReadOptions -> FilePath -> IO (DataFrame, (Integer, T.Text, Int))-readSeparated c opts path = do- totalRows <- case totalRows opts of- 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 (_, len) = case rowRange opts of- Nothing -> (0, totalRows)- Just (start, len) -> (start, min len (totalRows - rowsRead opts))- withFile path ReadMode $ \handle -> do- firstRow <- fmap T.strip . parseSep c <$> TIO.hGetLine handle- let columnNames =- if hasHeader opts- then fmap (T.filter (/= '\"')) firstRow- else fmap (T.singleton . intToDigit) [0 .. (length firstRow - 1)]- -- If there was no header rewind the file cursor.- unless (hasHeader opts) $ hSeek handle AbsoluteSeek 0-- currPos <- hTell handle- when (isJust $ seekPos opts) $- hSeek handle AbsoluteSeek (fromMaybe currPos (seekPos opts))-- -- Initialize mutable vectors for each column- let numColumns = length columnNames- let numRows = len- -- Use this row to infer the types of the rest of the column.- (dataRow, remainder) <- readSingleLine c (leftOver opts) handle-- -- This array will track the indices of all null values for each column.- nullIndices <- VM.unsafeNew numColumns- VM.set nullIndices []- mutableCols <- VM.unsafeNew numColumns- getInitialDataVectors numRows mutableCols dataRow-- -- Read rows into the mutable vectors- (unconsumed, r) <-- fillColumns numRows c mutableCols nullIndices remainder handle-- -- Freeze the mutable vectors into immutable ones- nulls' <- V.unsafeFreeze nullIndices- 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)- , derivingExpressions = M.empty- }- , (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- 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 #-}---- | 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- 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 _ -> do- 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- l <- readIORef input- r <- readIORef rowsRead- pure (l, r)-{-# INLINE fillColumns #-}---- | 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 #-}---- | 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 #-}---- ------------------------------------------------------------------------------ Streaming scan API--- -----------------------------------------------------------------------------{- | Open a CSV/separated file for streaming, returning an open handle-(positioned just after the header line) and the column specification-for the schema columns that appear in the file header.--The caller is responsible for closing the handle when done.--}-openCsvStream ::- Char ->- Schema ->- FilePath ->- IO (Handle, [(Int, T.Text, SchemaType)])-openCsvStream sep schema path = do- handle <- openFile path ReadMode- hSetBuffering handle (BlockBuffering (Just (8 * 1024 * 1024)))- headerLine <- TIO.hGetLine handle- let headerCols = fmap (T.filter (/= '"') . T.strip) (parseSep sep headerLine)- let schemaMap = elements schema- let colSpec =- [ (idx, name, stype)- | (idx, name) <- zip [0 ..] headerCols- , Just stype <- [M.lookup name schemaMap]- ]- when (null colSpec) $- hClose handle- >> fail- ("openCsvStream: none of the schema columns appear in the header of " <> path)- return (handle, colSpec)--{- | Read up to @batchSz@ rows from the open handle, returning a batch-'DataFrame' and the unconsumed leftover text. Returns 'Nothing' when-the handle is at EOF and there is no leftover input.--The caller must pass the leftover returned by the previous call (use @""@-for the first call).--}-readBatch ::- Char ->- [(Int, T.Text, SchemaType)] ->- Int ->- BS.ByteString ->- Handle ->- IO (Maybe (DataFrame, BS.ByteString))-readBatch sep colSpec batchSz leftover handle = do- let sepByte = fromIntegral (fromEnum sep) :: Word8- numCols = length colSpec- -- Read in 8 MB chunks; only the partial-line tail is copied on refill.- chunkSize = 8 * 1024 * 1024- nullsArr <- VM.unsafeNew numCols- VM.set nullsArr []- mCols <- VM.unsafeNew numCols- forM_ (zip [0 ..] colSpec) $ \(ci, (_, _, st)) ->- VM.unsafeWrite mCols ci =<< makeCol batchSz st- -- buf holds unprocessed bytes; refilled on demand when no newline is found.- bufRef <- newIORef leftover- -- Row-by-row scan. When the buffer has no unquoted newline, fetch another chunk.- -- The copy on refill is only the partial-line tail (≤ one row ≈ few hundred bytes).- let loop !rowIdx = do- remaining <- readIORef bufRef- if rowIdx >= batchSz- then return (rowIdx, remaining)- else case findUnquotedNewline remaining of- Nothing -> do- chunk <- BS.hGet handle chunkSize- if BS.null chunk- then return (rowIdx, remaining) -- EOF- else writeIORef bufRef (remaining <> chunk) >> loop rowIdx- Just nlIdx -> do- let line = BS.take nlIdx remaining- rest' = BS.drop (nlIdx + 1) remaining- line' =- if not (BS.null line) && BS.last line == 0x0D- then BS.init line- else line- writeIORef bufRef rest'- forM_ (zip [0 ..] colSpec) $ \(ci, (fi, _, _)) -> do- let fieldBs = getNthFieldBs sepByte fi line'- col <- VM.unsafeRead mCols ci- res <- writeColumnBs rowIdx fieldBs col- case res of- Left nv -> VM.unsafeModify nullsArr ((rowIdx, nv) :) ci- Right _ -> return ()- loop (rowIdx + 1)- (completeRows, newLeftover) <- loop 0- if completeRows == 0- then return Nothing- else do- forM_ [0 .. numCols - 1] $ \ci -> do- col <- VM.unsafeRead mCols ci- VM.unsafeWrite mCols ci (sliceCol completeRows col)- nullsVec <- V.unsafeFreeze nullsArr- cols <- V.generateM numCols $ \ci -> do- col <- VM.unsafeRead mCols ci- freezeColumn' (nullsVec V.! ci) col- let colNames = [name | (_, name, _) <- colSpec]- return $- Just- ( DataFrame- { columns = cols- , columnIndices = M.fromList (zip colNames [0 ..])- , dataframeDimensions = (completeRows, numCols)- , derivingExpressions = M.empty- }- , newLeftover- )--{- | Write a 'ByteString' field value directly into a mutable column,-parsing numerics without an intermediate 'T.Text' allocation.--}-writeColumnBs ::- Int -> BS.ByteString -> MutableColumn -> IO (Either T.Text Bool)-writeColumnBs i bs (MBoxedColumn (col :: VM.IOVector a)) =- case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl ->- let val = TextEncoding.decodeUtf8Lenient bs- in if isNullish val- then VM.unsafeWrite col i T.empty >> return (Left val)- else VM.unsafeWrite col i val >> return (Right True)- Nothing -> return (Left (TextEncoding.decodeUtf8Lenient bs))-writeColumnBs i bs (MOptionalColumn (col :: VM.IOVector (Maybe a))) =- case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl ->- let val = TextEncoding.decodeUtf8Lenient bs- in if isNullish val- then VM.unsafeWrite col i Nothing >> return (Left val)- else VM.unsafeWrite col i (Just val) >> return (Right True)- Nothing -> return (Left (TextEncoding.decodeUtf8Lenient bs))-writeColumnBs i bs (MUnboxedColumn (col :: VUM.IOVector a)) =- case testEquality (typeRep @a) (typeRep @Double) of- Just Refl -> case readByteStringDouble bs of- Just v -> VUM.unsafeWrite col i v >> return (Right True)- Nothing -> VUM.unsafeWrite col i 0 >> return (Left (TextEncoding.decodeUtf8Lenient bs))- Nothing -> case testEquality (typeRep @a) (typeRep @Int) of- Just Refl -> case readByteStringInt bs of- Just v -> VUM.unsafeWrite col i v >> return (Right True)- Nothing -> VUM.unsafeWrite col i 0 >> return (Left (TextEncoding.decodeUtf8Lenient bs))- Nothing -> return (Left (TextEncoding.decodeUtf8Lenient bs))-{-# INLINE writeColumnBs #-}--{- | Extracts the Nth field (0-indexed), respecting double quotes and stripping them.-Fast path: uses memchr-based 'BS.break' when no quotes are present in the line.-Slow path: quote-aware character-by-character scan.--}-getNthFieldBs :: Word8 -> Int -> BS.ByteString -> BS.ByteString-getNthFieldBs sep targetIdx bs- | not (BS.any (== 0x22) bs) = skipFast targetIdx bs- | otherwise = go 0 0 False 0- where- -- Fast path: skip fields using elemIndex (memchr); avoids pair allocation.- skipFast k s =- case BS.elemIndex sep s of- Nothing -> if k == 0 then s else BS.empty- Just i ->- if k == 0- then BS.take i s- else skipFast (k - 1) (BS.drop (i + 1) s)-- -- Slow path: quote-aware scan.- quoteChar = 0x22 :: Word8- len = BS.length bs- go !idx !start !inQ !pos- | pos >= len =- if idx == targetIdx then extract start pos else BS.empty- | otherwise =- let c = BS.index bs pos- in if c == quoteChar- then go idx start (not inQ) (pos + 1)- else- if c == sep && not inQ- then- if idx == targetIdx- then extract start pos- else go (idx + 1) (pos + 1) False (pos + 1)- else go idx start inQ (pos + 1)-- extract s e =- let field = BS.take (e - s) (BS.drop s bs)- in if BS.length field >= 2- && BS.head field == quoteChar- && BS.last field == quoteChar- then BS.init (BS.tail field)- else field-{-# INLINE getNthFieldBs #-}---- | Allocate a fresh 'MutableColumn' for @n@ slots based on a 'SchemaType'.-makeCol :: Int -> SchemaType -> IO MutableColumn-makeCol n (SType (_ :: P.Proxy a)) =- case testEquality (typeRep @a) (typeRep @Int) of- Just Refl -> MUnboxedColumn <$> (VUM.unsafeNew n :: IO (VUM.IOVector Int))- Nothing -> case testEquality (typeRep @a) (typeRep @Double) of- Just Refl -> MUnboxedColumn <$> (VUM.unsafeNew n :: IO (VUM.IOVector Double))- Nothing -> MBoxedColumn <$> (VM.unsafeNew n :: IO (VM.IOVector T.Text))---- | Slice a 'MutableColumn' to @n@ elements (no-copy view).-sliceCol :: Int -> MutableColumn -> MutableColumn-sliceCol n (MBoxedColumn col) = MBoxedColumn (VM.take n col)-sliceCol n (MUnboxedColumn col) = MUnboxedColumn (VUM.take n col)-sliceCol n (MOptionalColumn col) = MOptionalColumn (VM.take n col)--{- | Finds the index of the next unquoted newline (0x0A).-Fast path: uses memchr (SIMD) and falls back to a quote-aware linear scan-only if a double-quote appears before the candidate newline.--}-findUnquotedNewline :: BS.ByteString -> Maybe Int-findUnquotedNewline bs =- case BS.elemIndex 0x0A bs of- Nothing -> Nothing- Just nlPos- -- No quote before the newline → safe to use this position.- -- Check with elemIndex to avoid allocating a ByteString slice.- | maybe True (>= nlPos) (BS.elemIndex 0x22 bs) -> Just nlPos- -- Quote present → may be a newline inside a quoted field; scan carefully.- | otherwise -> slowScan 0 False- where- len = BS.length bs- slowScan !pos !inQ- | pos >= len = Nothing- | otherwise =- let c = BS.index bs pos- in if c == 0x22- then slowScan (pos + 1) (not inQ)- else- if c == 0x0A && not inQ- then Just pos- else slowScan (pos + 1) inQ-{-# INLINE findUnquotedNewline #-}
− src/DataFrame/Lazy/Internal/DataFrame.hs
@@ -1,138 +0,0 @@-{-# 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 qualified DataFrame.Internal.Column as C-import qualified DataFrame.Internal.DataFrame as D-import qualified DataFrame.Internal.Expression as E-import DataFrame.Internal.Schema (Schema)-import DataFrame.Lazy.Internal.Executor (- ExecutorConfig (..),- defaultExecutorConfig,- execute,- )-import DataFrame.Lazy.Internal.LogicalPlan (- DataSource (..),- LogicalPlan (..),- SortOrder (..),- )-import qualified DataFrame.Lazy.Internal.Optimizer as Opt-import DataFrame.Operations.Join (JoinType)--{- | A lazy query that has not been executed yet.--The query is represented as a 'LogicalPlan' tree; 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, returning a fully-materialised 'D.DataFrame'.--}-runDataFrame :: LazyDataFrame -> IO D.DataFrame-runDataFrame ldf = do- let physPlan = Opt.optimize (batchSize ldf) (plan ldf)- execute physPlan defaultExecutorConfig{defaultBatchSize = batchSize 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.-scanCsv :: Schema -> T.Text -> LazyDataFrame-scanCsv schema path =- LazyDataFrame- { plan = Scan (CsvSource (T.unpack path) ',') schema- , batchSize = 1_000_000- }---- | Scan a character-separated file.-scanSeparated :: Char -> Schema -> T.Text -> LazyDataFrame-scanSeparated sep schema path =- LazyDataFrame- { plan = Scan (CsvSource (T.unpack path) sep) 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.-limit :: Int -> LazyDataFrame -> LazyDataFrame-limit n ldf = ldf{plan = Limit n (plan ldf)}
− src/DataFrame/Lazy/Internal/Executor.hs
@@ -1,543 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE NumericUnderscores #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE TypeApplications #-}--{- | Pull-based (iterator) execution engine.--Each operator returns a 'Stream' — an IO action that produces the next-'DataFrame' batch on each call and returns 'Nothing' when exhausted.-Blocking operators (Sort, HashJoin) materialise their input before producing-output. HashAggregate uses streaming partial aggregation when all aggregate-expressions support it.--}-module DataFrame.Lazy.Internal.Executor (- ExecutorConfig (..),- defaultExecutorConfig,- execute,- foldBatches,-) where--import Control.Concurrent (forkIO)-import Control.Concurrent.STM (atomically)-import Control.Concurrent.STM.TBQueue (newTBQueueIO, readTBQueue, writeTBQueue)-import Control.DeepSeq (force)-import Control.Exception (evaluate)-import Control.Monad (filterM, when)-import qualified Data.ByteString as BS-import Data.IORef-import qualified Data.Map as M-import qualified Data.Set as S-import qualified Data.Text as T-import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))-import qualified Data.Vector.Unboxed as VU-import qualified DataFrame.IO.Parquet as Parquet-import qualified DataFrame.Internal.Column as C-import qualified DataFrame.Internal.DataFrame as D-import qualified DataFrame.Internal.Expression as E-import DataFrame.Internal.Schema (elements)-import qualified DataFrame.Lazy.IO.Binary as Bin-import qualified DataFrame.Lazy.IO.CSV as LCSV-import DataFrame.Lazy.Internal.LogicalPlan (DataSource (..), SortOrder (..))-import DataFrame.Lazy.Internal.PhysicalPlan-import qualified DataFrame.Operations.Aggregation as Agg-import qualified DataFrame.Operations.Core as Core-import qualified DataFrame.Operations.Join as Join-import DataFrame.Operations.Merge ()-import qualified DataFrame.Operations.Permutation as Perm-import qualified DataFrame.Operations.Subset as Sub-import qualified DataFrame.Operations.Transformations as Trans-import System.Directory (doesDirectoryExist)-import System.FilePath ((</>))-import System.FilePath.Glob (glob)-import System.IO (hClose)-import Type.Reflection (typeRep)---- ------------------------------------------------------------------------------ Configuration--- -----------------------------------------------------------------------------data ExecutorConfig = ExecutorConfig- { memoryBudgetBytes :: !Int- -- ^ Per-node spill threshold (currently informational; not enforced yet).- , spillDirectory :: FilePath- , defaultBatchSize :: !Int- }--defaultExecutorConfig :: ExecutorConfig-defaultExecutorConfig =- ExecutorConfig- { memoryBudgetBytes = 512 * 1_048_576 -- 512 MiB- , spillDirectory = "/tmp"- , defaultBatchSize = 1_000_000- }---- ------------------------------------------------------------------------------ Stream abstraction--- -----------------------------------------------------------------------------{- | A pull-based stream: each call to the action yields the next batch or-'Nothing' when the stream is exhausted. State is captured by the closure.--}-newtype Stream = Stream {pullBatch :: IO (Maybe D.DataFrame)}---- | Drain all batches from a stream and concatenate them into one DataFrame.-collectStream :: Stream -> IO D.DataFrame-collectStream stream = go D.empty- where- go acc = do- mb <- pullBatch stream- case mb of- Nothing -> return acc- Just df -> go (acc <> df)---- ------------------------------------------------------------------------------ Top-level entry point--- -----------------------------------------------------------------------------{- | Execute a physical plan, returning the complete result as a single-'DataFrame'.--}-execute :: PhysicalPlan -> ExecutorConfig -> IO D.DataFrame-execute plan cfg = buildStream plan cfg >>= collectStream--{- | Fold a function over every batch produced by a physical plan.-The fold is strict in the accumulator; each batch is discarded after folding.--}-foldBatches ::- (b -> D.DataFrame -> IO b) -> b -> PhysicalPlan -> ExecutorConfig -> IO b-foldBatches f seed plan cfg = do- stream <- buildStream plan cfg- let loop !acc = do- mb <- pullBatch stream- case mb of- Nothing -> return acc- Just batch -> do- !acc' <- f acc batch- loop acc'- loop seed---- ------------------------------------------------------------------------------ Per-operator stream builders--- -----------------------------------------------------------------------------buildStream :: PhysicalPlan -> ExecutorConfig -> IO Stream--- Scan ------------------------------------------------------------------------buildStream (PhysicalScan (CsvSource path sep) cfg) _ =- executeCsvScan path sep cfg-buildStream (PhysicalScan (ParquetSource path) cfg) _ =- executeParquetScan path cfg-buildStream (PhysicalSpill child path) execCfg = do- df <- execute child execCfg- Bin.spillToDisk path df- df' <- Bin.readSpilled path- ref <- newIORef (Just df')- return . Stream $- ( do- mb <- readIORef ref- writeIORef ref Nothing- return mb- )--- Filter ----------------------------------------------------------------------buildStream (PhysicalFilter p child) execCfg = do- childStream <- buildStream child execCfg- return . Stream $- ( do- mb <- pullBatch childStream- return $ fmap (Sub.filterWhere p) mb- )--- Project ---------------------------------------------------------------------buildStream (PhysicalProject cols child) execCfg = do- childStream <- buildStream child execCfg- return . Stream $- ( do- mb <- pullBatch childStream- return $ fmap (Sub.select cols) mb- )--- Derive ----------------------------------------------------------------------buildStream (PhysicalDerive name uexpr child) execCfg = do- childStream <- buildStream child execCfg- return . Stream $- ( do- mb <- pullBatch childStream- return $ fmap (Trans.deriveMany [(name, uexpr)]) mb- )--- Limit -----------------------------------------------------------------------buildStream (PhysicalLimit n child) execCfg = do- childStream <- buildStream child execCfg- countRef <- newIORef (0 :: Int)- return . Stream $- ( do- remaining <- readIORef countRef- if remaining >= n- then return Nothing- else do- mb <- pullBatch childStream- case mb of- Nothing -> return Nothing- Just df -> do- let toTake = min (Core.nRows df) (n - remaining)- modifyIORef' countRef (+ toTake)- return $ Just (Sub.take toTake df)- )--- Sort (blocking) -------------------------------------------------------------buildStream (PhysicalSort cols child) execCfg = do- df <- execute child execCfg- let sortOrds = fmap toPermSortOrder cols- let sorted = Perm.sortBy sortOrds df- ref <- newIORef (Just sorted)- return . Stream $- ( do- mb <- readIORef ref- writeIORef ref Nothing- return mb- )--- HashAggregate ---------------------------------------------------------------buildStream (PhysicalHashAggregate keys aggs child) execCfg = do- childStream <- buildStream child execCfg- if all (isStreamableAgg . snd) aggs- then do- -- Streaming partial aggregation: O(|groups|) memory- let (partialAggs, mergeAggs, finalizer) = buildAggPlan aggs- accRef <- newIORef (Nothing :: Maybe D.DataFrame)- let loop = do- mb <- pullBatch childStream- case mb of- Nothing -> return ()- Just batch -> do- -- Force to NF so the batch DataFrame can be GC'd immediately.- -- evaluate . force breaks the thunk chain that would otherwise- -- keep every batch (~60 MB each) alive until the end = OOM.- !partial <-- evaluate . force $ Agg.aggregate partialAggs (Agg.groupBy keys batch)- mAcc <- readIORef accRef- !newAcc <- case mAcc of- Nothing -> return partial- Just acc ->- evaluate . force $- Agg.aggregate mergeAggs $- Agg.groupBy keys (acc <> partial)- writeIORef accRef (Just newAcc)- loop- loop- mFinal <- fmap (fmap finalizer) (readIORef accRef)- ref <- newIORef mFinal- return . Stream $ do- mb <- readIORef ref- writeIORef ref Nothing- return mb- else do- -- Fallback: materialise entire child (for CollectAgg etc.)- df <- collectStream childStream- let result = Agg.aggregate aggs (Agg.groupBy keys df)- ref <- newIORef (Just result)- return . Stream $ do- mb <- readIORef ref- writeIORef ref Nothing- return mb--- SourceDF (split pre-loaded DataFrame into batches) --------------------------buildStream (PhysicalSourceDF df) execCfg = do- let bs = defaultBatchSize execCfg- total = Core.nRows df- posRef <- newIORef (0 :: Int)- return . Stream $ do- i <- readIORef posRef- if i >= total- then return Nothing- else do- let n = min bs (total - i)- batch = Sub.range (i, i + n) df- writeIORef posRef (i + n)- return (Just batch)--- HashJoin — streaming probe (INNER/LEFT) or blocking fallback -----------------buildStream (PhysicalHashJoin jt leftKey rightKey leftPlan rightPlan) execCfg =- case jt of- Join.INNER -> streamingHashJoin assembleInnerBatch- Join.LEFT -> streamingHashJoin assembleLeftBatch- _ -> do- -- Blocking fallback for RIGHT / FULL_OUTER- leftDf <- execute leftPlan execCfg- rightDf <- execute rightPlan execCfg- let result = performJoin jt leftKey rightKey leftDf rightDf- ref <- newIORef (Just result)- return . Stream $ do- mb <- readIORef ref- writeIORef ref Nothing- return mb- where- streamingHashJoin assembleFn = do- -- Materialise build (right) side once and build the compact index.- rightDf <- execute rightPlan execCfg- let rightDf' =- if leftKey == rightKey- then rightDf- else Core.rename rightKey leftKey rightDf- joinKey = leftKey- csSet = S.fromList [joinKey]- rightHashes = Join.buildHashColumn [joinKey] rightDf'- ci = Join.buildCompactIndex rightHashes- -- Stream probe (left) side batch by batch.- leftStream <- buildStream leftPlan execCfg- return . Stream $ do- mBatch <- pullBatch leftStream- case mBatch of- Nothing -> return Nothing- Just probeBatch -> do- let probeHashes = Join.buildHashColumn [joinKey] probeBatch- (probeIxs, buildIxs) = Join.hashProbeKernel ci probeHashes- return . Just $ assembleFn csSet probeBatch rightDf' probeIxs buildIxs-- assembleLeftBatch csSet probeBatch rightDf' probeIxs buildIxs =- let batchN = Core.nRows probeBatch- -- Mark which probe rows were matched (may have duplicates — that's fine).- matched =- VU.accumulate- (\_ b -> b)- (VU.replicate batchN False)- (VU.map (,True) probeIxs)- unmatchedIxs = VU.findIndices not matched- allProbeIxs = probeIxs VU.++ unmatchedIxs- allBuildIxs = buildIxs VU.++ VU.replicate (VU.length unmatchedIxs) (-1)- in Join.assembleLeft csSet probeBatch rightDf' allProbeIxs allBuildIxs-- assembleInnerBatch = Join.assembleInner---- SortMergeJoin (blocking on both sides) --------------------------------------buildStream (PhysicalSortMergeJoin jt leftKey rightKey leftPlan rightPlan) execCfg = do- leftDf <- execute leftPlan execCfg- rightDf <- execute rightPlan execCfg- let result = performJoin jt leftKey rightKey leftDf rightDf- ref <- newIORef (Just result)- return . Stream $- ( do- mb <- readIORef ref- writeIORef ref Nothing- return mb- )---- ------------------------------------------------------------------------------ Streaming aggregation helpers--- -----------------------------------------------------------------------------{- | True when an aggregate expression can be computed incrementally-(i.e., partial results can be merged without materialising all rows).--}-isStreamableAgg :: E.UExpr -> Bool-isStreamableAgg (E.UExpr (E.Agg (E.CollectAgg _ _) _)) = False-isStreamableAgg (E.UExpr (E.Agg (E.FoldAgg _ Nothing (_ :: a -> b -> a)) _)) =- case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> True -- self-merging: min, max, sum- Nothing -> False-isStreamableAgg (E.UExpr (E.Agg (E.FoldAgg _ (Just _) (_ :: a -> b -> a)) _)) =- case testEquality (typeRep @a) (typeRep @Int) of- Just Refl -> True -- seeded Int fold (old-style count): merge by sum- Nothing ->- case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> True -- seeded self-merging- Nothing -> False-isStreamableAgg (E.UExpr (E.Agg (E.MergeAgg{}) _)) = True-isStreamableAgg _ = False--{- | Build the partial, merge, and finalizer plan for a list of streamable-aggregate expressions.--* @partialAggs@ — applied per batch, producing one row per group-* @mergeAggs@ — applied when combining two partial-result DataFrames-* @finalizer@ — post-process after all batches (needed for 'MergeAgg'- where the accumulator type differs from the output type)--}-buildAggPlan ::- [(T.Text, E.UExpr)] ->- ( [(T.Text, E.UExpr)]- , [(T.Text, E.UExpr)]- , D.DataFrame -> D.DataFrame- )-buildAggPlan aggs = foldl combine ([], [], id) (map processAgg aggs)- where- combine (p1, m1, f1) (p2, m2, f2) = (p1 ++ p2, m1 ++ m2, f1 . f2)-- processAgg ::- (T.Text, E.UExpr) ->- ([(T.Text, E.UExpr)], [(T.Text, E.UExpr)], D.DataFrame -> D.DataFrame)- processAgg (name, ue) = case ue of- -- Seedless FoldAgg: min, max, sum (self-merging when a = b)- E.UExpr (E.Agg (E.FoldAgg n Nothing (f :: a -> b -> a)) (_ :: E.Expr b)) ->- case testEquality (typeRep @a) (typeRep @b) of- Just Refl ->- ( [(name, ue)]- , [(name, E.UExpr (E.Agg (E.FoldAgg n Nothing f) (E.Col @a name)))]- , id- )- Nothing ->- -- a /= b but a = Int: merge by sum (backward compat)- case testEquality (typeRep @a) (typeRep @Int) of- Just Refl ->- ( [(name, ue)]- ,- [- ( name- , E.UExpr- (E.Agg (E.FoldAgg "sum" Nothing ((+) :: Int -> Int -> Int)) (E.Col @Int name))- )- ]- , id- )- Nothing -> ([(name, ue)], [(name, ue)], id)- -- Seeded FoldAgg: old-style count (a = Int)- E.UExpr (E.Agg (E.FoldAgg n (Just _) (f :: a -> b -> a)) (_ :: E.Expr b)) ->- case testEquality (typeRep @a) (typeRep @Int) of- Just Refl ->- ( [(name, ue)]- ,- [- ( name- , E.UExpr- (E.Agg (E.FoldAgg "sum" Nothing ((+) :: Int -> Int -> Int)) (E.Col @Int name))- )- ]- , id- )- Nothing ->- case testEquality (typeRep @a) (typeRep @b) of- Just Refl ->- ( [(name, ue)]- , [(name, E.UExpr (E.Agg (E.FoldAgg n Nothing f) (E.Col @a name)))]- , id- )- Nothing -> ([(name, ue)], [(name, ue)], id)- -- MergeAgg: count, mean, etc.- -- Partial step: accumulate into acc type (using id as finalizer).- -- Merge step: apply merge function to two acc-typed partial results.- -- Finalizer: apply fin to convert acc column to output type.- E.UExpr- ( E.Agg- ( E.MergeAgg- n- seed- (step :: acc -> b -> acc)- (merge :: acc -> acc -> acc)- (fin :: acc -> a)- )- (inner :: E.Expr b)- ) ->- let partialExpr =- E.UExpr- ( E.Agg- (E.MergeAgg n seed step merge (id :: acc -> acc))- inner- )- mergeExpr =- E.UExpr- ( E.Agg- (E.FoldAgg ("merge_" <> n) Nothing merge)- (E.Col @acc name)- )- finalize df =- let accCol = D.unsafeGetColumn name df- finalCol =- either- (error "buildAggPlan: MergeAgg finalize failed")- id- (C.mapColumn @acc @a fin accCol)- in Core.insertColumn name finalCol df- in ( [(name, partialExpr)]- , [(name, mergeExpr)]- , finalize- )- _ -> ([(name, ue)], [(name, ue)], id)---- ------------------------------------------------------------------------------ Parquet scan implementation--- -----------------------------------------------------------------------------{- | Scan a Parquet file, directory, or glob. Each file becomes one batch.-Column projection and predicate pushdown are forwarded to 'readParquetWithOpts'-via 'ParquetReadOptions'.--}-executeParquetScan :: FilePath -> ScanConfig -> IO Stream-executeParquetScan path cfg = do- isDir <- doesDirectoryExist path- let pat = if isDir then path </> "*" else path- matches <- glob pat- files <- filterM (fmap not . doesDirectoryExist) matches- when (null files) $- error ("executeParquetScan: no parquet files found for " ++ path)- let opts =- Parquet.defaultParquetReadOptions- { Parquet.selectedColumns = Just (M.keys (elements (scanSchema cfg)))- , Parquet.predicate = scanPushdownPredicate cfg- }- ref <- newIORef files- return . Stream $ do- fs <- readIORef ref- case fs of- [] -> return Nothing- (f : rest) -> do- writeIORef ref rest- Just <$> Parquet.readParquetWithOpts opts f---- ------------------------------------------------------------------------------ CSV scan implementation--- -----------------------------------------------------------------------------{- | CSV scan with pipeline parallelism: a dedicated reader thread fills a-bounded queue while the caller's thread applies pushdown predicates and-delivers batches to the rest of the pipeline. The queue depth of 8 keeps-at most eight raw batches in flight, bounding memory while hiding I/O latency.--}-executeCsvScan :: FilePath -> Char -> ScanConfig -> IO Stream-executeCsvScan path sep cfg = do- (handle, colSpec) <- LCSV.openCsvStream sep (scanSchema cfg) path- -- Queue carries raw batches; Nothing is the end-of-stream sentinel.- -- Depth 2: each batch holds ~60 MB (1M Text + Double columns); 8 would be ~480 MB.- queue <- newTBQueueIO 2- _ <- forkIO $ do- let loop lo = do- result <- LCSV.readBatch sep colSpec (scanBatchSize cfg) lo handle- case result of- Nothing ->- hClose handle >> atomically (writeTBQueue queue Nothing)- Just (df, lo') ->- atomically (writeTBQueue queue (Just df)) >> loop lo'- loop BS.empty- return . Stream $- ( do- mb <- atomically (readTBQueue queue)- case mb of- -- Re-insert the sentinel so repeated pulls after EOF stay Nothing.- Nothing -> atomically (writeTBQueue queue Nothing) >> return Nothing- Just df ->- let df' = case scanPushdownPredicate cfg of- Nothing -> df- Just p -> Sub.filterWhere p df- in return (Just df')- )---- ------------------------------------------------------------------------------ Join helper--- -----------------------------------------------------------------------------{- | Route join to the existing Operations.Join implementation.-When the left and right key names differ, rename the right key before joining.--}-performJoin ::- Join.JoinType -> T.Text -> T.Text -> D.DataFrame -> D.DataFrame -> D.DataFrame-performJoin jt leftKey rightKey leftDf rightDf =- if leftKey == rightKey- then Join.join jt [leftKey] rightDf leftDf- else- let rightRenamed = Core.rename rightKey leftKey rightDf- in Join.join jt [leftKey] rightRenamed leftDf---- ------------------------------------------------------------------------------ Sort order conversion--- ------------------------------------------------------------------------------- | Convert plan-level sort order to the Permutation module's SortOrder.-toPermSortOrder :: (T.Text, SortOrder) -> Perm.SortOrder-toPermSortOrder (col, Ascending) = Perm.Asc (E.Col @T.Text col)-toPermSortOrder (col, Descending) = Perm.Desc (E.Col @T.Text col)
− src/DataFrame/Lazy/Internal/LogicalPlan.hs
@@ -1,44 +0,0 @@-{-# LANGUAGE GADTs #-}--module DataFrame.Lazy.Internal.LogicalPlan where--import qualified Data.Text as T-import qualified DataFrame.Internal.DataFrame as D-import qualified DataFrame.Internal.Expression as E-import DataFrame.Internal.Schema (Schema)-import DataFrame.Operations.Join (JoinType)---- | Data source for a scan node.-data DataSource- = -- | path, separator- CsvSource FilePath Char- | ParquetSource FilePath- deriving (Show)---- | Sort direction used in Sort nodes and the public API.-data SortOrder = Ascending | Descending- deriving (Show, Eq, Ord)--{- | Relational-algebra tree that represents what the query computes.-No physical decisions (batch size, join strategy) are made here.--}-data LogicalPlan- = -- | Read columns described by the schema from a source.- Scan DataSource Schema- | -- | Retain only the listed columns.- Project [T.Text] LogicalPlan- | -- | Keep rows matching the predicate.- Filter (E.Expr Bool) LogicalPlan- | -- | Add or overwrite a column via an expression.- Derive T.Text E.UExpr LogicalPlan- | -- | Join two sub-plans on the given key columns.- Join JoinType T.Text T.Text LogicalPlan LogicalPlan- | -- | Group then aggregate.- Aggregate [T.Text] [(T.Text, E.UExpr)] LogicalPlan- | -- | Sort by a list of (column, direction) pairs.- Sort [(T.Text, SortOrder)] LogicalPlan- | -- | Retain at most N rows.- Limit Int LogicalPlan- | -- | Lift an already-loaded DataFrame into the lazy plan.- SourceDF D.DataFrame- deriving (Show)
− src/DataFrame/Lazy/Internal/Optimizer.hs
@@ -1,209 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module DataFrame.Lazy.Internal.Optimizer (optimize) where--import qualified Data.Map as M-import qualified Data.Set as S-import qualified Data.Text as T-import qualified DataFrame.Internal.Expression as E-import DataFrame.Internal.Schema (Schema (..), elements)-import DataFrame.Lazy.Internal.LogicalPlan-import DataFrame.Lazy.Internal.PhysicalPlan--{- | Optimise a logical plan and lower it to a physical plan.--Rules applied bottom-up (in order):- 1. Filter fusion — merge consecutive Filter nodes into a conjunction- 2. Predicate pushdown — move Filter past Derive/Project toward Scan- 3. Dead column elim — drop Derive nodes whose output is never referenced--After rule application @toPhysical@ selects concrete operators.--}-optimize :: Int -> LogicalPlan -> PhysicalPlan-optimize batchSz =- toPhysical batchSz- . eliminateDeadColumns- . pushPredicates- . fuseFilters---- ------------------------------------------------------------------------------ Rule 1: Filter fusion--- ------------------------------------------------------------------------------- | Merge @Filter p1 (Filter p2 child)@ into @Filter (p1 && p2) child@.-fuseFilters :: LogicalPlan -> LogicalPlan-fuseFilters (Filter p1 (Filter p2 child)) =- fuseFilters (Filter (andExpr p1 p2) (fuseFilters child))-fuseFilters (Filter p child) = Filter p (fuseFilters child)-fuseFilters (Project cols child) = Project cols (fuseFilters child)-fuseFilters (Derive name expr child) = Derive name expr (fuseFilters child)-fuseFilters (Join jt l r left right) =- Join jt l r (fuseFilters left) (fuseFilters right)-fuseFilters (Aggregate keys aggs child) =- Aggregate keys aggs (fuseFilters child)-fuseFilters (Sort cols child) = Sort cols (fuseFilters child)-fuseFilters (Limit n child) = Limit n (fuseFilters child)-fuseFilters leaf = leaf---- | Logical AND of two @Bool@ expressions.-andExpr :: E.Expr Bool -> E.Expr Bool -> E.Expr Bool-andExpr =- E.Binary- ( E.MkBinaryOp- { E.binaryFn = (&&)- , E.binaryName = "and"- , E.binarySymbol = Just "&&"- , E.binaryCommutative = True- , E.binaryPrecedence = 3- }- )---- ------------------------------------------------------------------------------ Rule 2: Predicate pushdown--- -----------------------------------------------------------------------------{- | Push Filter nodes as close to the Scan as possible.--* Past a @Derive@ when the predicate doesn't reference the derived column.-* Past a @Project@ when all predicate columns are in the projected set.-* Into @ScanConfig.scanPushdownPredicate@ when the child is a @Scan@.--}-pushPredicates :: LogicalPlan -> LogicalPlan-pushPredicates (Filter p (Derive name expr child))- | name `notElem` E.getColumns p =- Derive name expr (pushPredicates (Filter p child))- | otherwise =- Filter p (Derive name expr (pushPredicates child))-pushPredicates (Filter p (Project cols child))- | all (`elem` cols) (E.getColumns p) =- Project cols (pushPredicates (Filter p child))- | otherwise =- Filter p (Project cols (pushPredicates child))-pushPredicates (Filter p child) = Filter p (pushPredicates child)-pushPredicates (Project cols child) = Project cols (pushPredicates child)-pushPredicates (Derive name expr child) = Derive name expr (pushPredicates child)-pushPredicates (Join jt l r left right) =- Join jt l r (pushPredicates left) (pushPredicates right)-pushPredicates (Aggregate keys aggs child) =- Aggregate keys aggs (pushPredicates child)-pushPredicates (Sort cols child) = Sort cols (pushPredicates child)-pushPredicates (Limit n child) = Limit n (pushPredicates child)-pushPredicates leaf = leaf---- ------------------------------------------------------------------------------ Rule 3: Dead column elimination--- -----------------------------------------------------------------------------{- | Collect every column name that is explicitly referenced somewhere in the-plan (in filter predicates, sort keys, aggregate keys, projection lists,-join keys, and derived expressions). Returns Nothing when "all columns-are needed" (i.e. no Project restricts the output).--}-referencedCols :: LogicalPlan -> Maybe (S.Set T.Text)-referencedCols (Scan _ schema) = Just (S.fromList (M.keys (elements schema)))-referencedCols (Project cols _) = Just (S.fromList cols)-referencedCols (Filter p child) =- fmap (S.union (S.fromList (E.getColumns p))) (referencedCols child)-referencedCols (Derive _ expr child) =- fmap (S.union (S.fromList (uExprCols expr))) (referencedCols child)-referencedCols (Join _ l r left right) =- let keySet = S.fromList [l, r]- lRef = fmap (S.union keySet) (referencedCols left)- rRef = fmap (S.union keySet) (referencedCols right)- in liftMaybe2 S.union lRef rRef-referencedCols (Aggregate keys aggs child) =- let aggCols = S.fromList (keys <> concatMap (uExprCols . snd) aggs)- in fmap (S.union aggCols) (referencedCols child)-referencedCols (Sort cols child) =- fmap (S.union (S.fromList (fmap fst cols))) (referencedCols child)-referencedCols (Limit _ child) = referencedCols child-referencedCols (SourceDF _) = Nothing--liftMaybe2 :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c-liftMaybe2 f (Just a) (Just b) = Just (f a b)-liftMaybe2 _ _ _ = Nothing--uExprCols :: E.UExpr -> [T.Text]-uExprCols (E.UExpr expr) = E.getColumns expr---- | Drop @Derive@ nodes whose output column is never consumed downstream.-eliminateDeadColumns :: LogicalPlan -> LogicalPlan-eliminateDeadColumns plan = go (referencedCols plan) plan- where- go needed (Derive name expr child) =- case needed of- Nothing -> Derive name expr (go needed child)- Just cols- | name `S.notMember` cols -> go needed child- | otherwise ->- Derive- name- expr- (go (Just (S.union cols (S.fromList (uExprCols expr)))) child)- go needed (Filter p child) =- Filter p (go (fmap (S.union (S.fromList (E.getColumns p))) needed) child)- go needed (Project cols child) =- Project cols (go (Just (S.fromList cols)) child)- go needed (Join jt l r left right) =- let keySet = fmap (S.union (S.fromList [l, r])) needed- in Join jt l r (go keySet left) (go keySet right)- go needed (Aggregate keys aggs child) =- let aggCols = fmap (S.union (S.fromList (keys <> concatMap (uExprCols . snd) aggs))) needed- in Aggregate keys aggs (go aggCols child)- go needed (Sort cols child) =- Sort cols (go (fmap (S.union (S.fromList (fmap fst cols))) needed) child)- go needed (Limit n child) = Limit n (go needed child)- go needed (Scan ds schema) =- case needed of- Nothing -> Scan ds schema- Just cols ->- Scan ds (Schema (M.filterWithKey (\k _ -> k `S.member` cols) (elements schema)))- go _ (SourceDF df) = SourceDF df---- ------------------------------------------------------------------------------ Logical → Physical lowering--- -----------------------------------------------------------------------------{- | Lower the (already-optimised) logical plan to a physical plan.--Join strategy: always HashJoin (the executor can fall back to SortMerge-at runtime once statistics are available).--}-toPhysical :: Int -> LogicalPlan -> PhysicalPlan--- Special case: Filter directly on a Scan → push into ScanConfig.-toPhysical batchSz (Filter p (Scan (CsvSource path sep) schema)) =- PhysicalScan- (CsvSource path sep)- (ScanConfig batchSz sep schema (Just p))-toPhysical batchSz (Scan (CsvSource path sep) schema) =- PhysicalScan- (CsvSource path sep)- (ScanConfig batchSz sep schema Nothing)-toPhysical batchSz (Filter p (Scan (ParquetSource path) schema)) =- PhysicalScan- (ParquetSource path)- (ScanConfig batchSz ',' schema (Just p))-toPhysical batchSz (Scan (ParquetSource path) schema) =- PhysicalScan- (ParquetSource path)- (ScanConfig batchSz ',' schema Nothing)-toPhysical batchSz (Project cols child) =- PhysicalProject cols (toPhysical batchSz child)-toPhysical batchSz (Filter p child) =- PhysicalFilter p (toPhysical batchSz child)-toPhysical batchSz (Derive name expr child) =- PhysicalDerive name expr (toPhysical batchSz child)-toPhysical batchSz (Join jt l r left right) =- PhysicalHashJoin- jt- l- r- (toPhysical batchSz left)- (toPhysical batchSz right)-toPhysical batchSz (Aggregate keys aggs child) =- PhysicalHashAggregate keys aggs (toPhysical batchSz child)-toPhysical batchSz (Sort cols child) =- PhysicalSort cols (toPhysical batchSz child)-toPhysical batchSz (Limit n child) =- PhysicalLimit n (toPhysical batchSz child)-toPhysical _ (SourceDF df) = PhysicalSourceDF df
− src/DataFrame/Lazy/Internal/PhysicalPlan.hs
@@ -1,36 +0,0 @@-module DataFrame.Lazy.Internal.PhysicalPlan where--import qualified Data.Text as T-import qualified DataFrame.Internal.DataFrame as D-import qualified DataFrame.Internal.Expression as E-import DataFrame.Internal.Schema (Schema)-import DataFrame.Lazy.Internal.LogicalPlan (DataSource, SortOrder)-import DataFrame.Operations.Join (JoinType)---- | Scan-level configuration: batch size, separator, optional pushdowns.-data ScanConfig = ScanConfig- { scanBatchSize :: !Int- , scanSeparator :: !Char- , scanSchema :: !Schema- , scanPushdownPredicate :: !(Maybe (E.Expr Bool))- }- deriving (Show)--{- | Physical plan: every node carries enough information for the executor-to allocate resources and choose algorithms without further analysis.--}-data PhysicalPlan- = PhysicalScan DataSource ScanConfig- | PhysicalProject [T.Text] PhysicalPlan- | PhysicalFilter (E.Expr Bool) PhysicalPlan- | PhysicalDerive T.Text E.UExpr PhysicalPlan- | PhysicalHashJoin JoinType T.Text T.Text PhysicalPlan PhysicalPlan- | PhysicalSortMergeJoin JoinType T.Text T.Text PhysicalPlan PhysicalPlan- | PhysicalHashAggregate [T.Text] [(T.Text, E.UExpr)] PhysicalPlan- | PhysicalSort [(T.Text, SortOrder)] PhysicalPlan- | PhysicalLimit Int PhysicalPlan- | -- | Materialize child to a binary file on disk (used for build sides).- PhysicalSpill PhysicalPlan FilePath- | -- | Emit an already-loaded DataFrame as a stream of batches.- PhysicalSourceDF D.DataFrame- deriving (Show)
− src/DataFrame/Monad.hs
@@ -1,93 +0,0 @@-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE InstanceSigs #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TupleSections #-}--module DataFrame.Monad where--import DataFrame (DataFrame)-import qualified DataFrame as D-import DataFrame.Internal.Column (Columnable)-import DataFrame.Internal.Expression (Expr (..))--import qualified Data.Text as T-import System.Random---- A re-implementation of the state monad.--- `mtl` might be too heavy a dependency just to get--- a single monad instance.-newtype FrameM a = FrameM {runFrameM_ :: DataFrame -> (DataFrame, a)}--instance Functor FrameM where- fmap :: (a -> b) -> FrameM a -> FrameM b- fmap f (FrameM g) = FrameM $ \df ->- let (df', x) = g df- in (df', f x)--instance Applicative FrameM where- pure x = FrameM (,x)- (<*>) :: FrameM (a -> b) -> FrameM a -> FrameM b- FrameM ff <*> FrameM fx = FrameM $ \df ->- let (df1, f) = ff df- (df2, x) = fx df1- in (df2, f x)--instance Monad FrameM where- (>>=) :: FrameM a -> (a -> FrameM b) -> FrameM b- FrameM g >>= f = FrameM $ \df ->- let (df1, x) = g df- FrameM h = f x- in h df1--modifyM :: (DataFrame -> DataFrame) -> FrameM ()-modifyM f = FrameM $ \df -> (f df, ())--inspectM :: (DataFrame -> b) -> FrameM b-inspectM f = FrameM $ \df -> (df, f df)--deriveM :: (Columnable a) => T.Text -> Expr a -> FrameM (Expr a)-deriveM name expr = FrameM $ \df ->- let df' = D.derive name expr df- in (df', Col name)--renameM :: (Columnable a) => Expr a -> T.Text -> FrameM (Expr a)-renameM (Col oldName) newName = FrameM $ \df ->- let df' = D.rename oldName newName df- in (df', Col newName)-renameM expr newName = deriveM newName expr--filterWhereM :: Expr Bool -> FrameM ()-filterWhereM p = modifyM (D.filterWhere p)--sampleM :: (RandomGen g) => g -> Double -> FrameM ()-sampleM pureGen p = modifyM (D.sample pureGen p)--takeM :: Int -> FrameM ()-takeM n = modifyM (D.take n)--filterJustM :: (Columnable a) => Expr (Maybe a) -> FrameM (Expr a)-filterJustM (Col name) = FrameM $ \df ->- let df' = D.filterJust name df- in (df', Col name)-filterJustM expr =- error $ "Cannot filter on compound expression: " ++ show expr--imputeM :: (Columnable a) => Expr (Maybe a) -> a -> FrameM (Expr a)-imputeM expr@(Col name) value = FrameM $ \df ->- let df' = D.impute expr value df- in (df', Col name)-imputeM expr _ = error $ "Cannot impute on compound expression: " ++ show expr--runFrameM :: DataFrame -> FrameM a -> (a, DataFrame)-runFrameM df (FrameM action) =- let (df', a) = action df- in (a, df')--evalFrameM :: DataFrame -> FrameM a -> a-evalFrameM df m = fst (runFrameM df m)--execFrameM :: DataFrame -> FrameM a -> DataFrame-execFrameM df m = snd (runFrameM df m)
− src/DataFrame/Operations/Aggregation.hs
@@ -1,312 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE Strict #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.Operations.Aggregation where--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.Algorithms.Radix as VA-import qualified Data.Vector.Unboxed as VU-import qualified Data.Vector.Unboxed.Mutable as VUM--import Control.Exception (throw)-import Control.Monad-import Control.Monad.ST (runST)-import Data.Bits-import Data.Hashable-import Data.Type.Equality (TestEquality (..), type (:~:) (Refl))-import DataFrame.Errors-import DataFrame.Internal.Column (- Column (..),- TypedColumn (..),- atIndicesStable,- )-import DataFrame.Internal.DataFrame (DataFrame (..), GroupedDataFrame (..))-import DataFrame.Internal.Expression-import DataFrame.Internal.Interpreter-import DataFrame.Internal.Types-import DataFrame.Operations.Core-import DataFrame.Operations.Subset-import Type.Reflection (typeRep)--{- | 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-groupBy names df- | any (`notElem` columnNames df) names =- throw $- ColumnNotFoundException- (T.pack $ show $ names L.\\ columnNames df)- "groupBy"- (columnNames df)- | nRows df == 0 =- Grouped- df- names- VU.empty- (VU.fromList [0])- VU.empty- | otherwise =- let !vis = VU.map fst valueIndices- !os = changingPoints valueIndices- !n = fst (dimensions df)- in Grouped- df- names- vis- os- (buildRowToGroup n vis os)- where- indicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` names) (columnIndices df)- doubleToInt :: Double -> Int- doubleToInt = floor . (* 1000)- valueIndices = runST $ do- let n = fst (dimensions df)- mv <- VUM.new n-- let selectedCols = map (columns df V.!) indicesToGroup-- forM_ selectedCols $ \case- UnboxedColumn (v :: VU.Vector a) ->- case testEquality (typeRep @a) (typeRep @Int) of- Just Refl ->- VU.imapM_- ( \i x -> do- (_, !h) <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (i, hashWithSalt h x)- )- v- Nothing ->- case testEquality (typeRep @a) (typeRep @Double) of- Just Refl ->- VU.imapM_- ( \i d -> do- (_, !h) <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (i, hashWithSalt h (doubleToInt d))- )- v- Nothing ->- case sIntegral @a of- STrue ->- VU.imapM_- ( \i d -> do- let x :: Int- x = fromIntegral @a @Int d- (_, !h) <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (i, hashWithSalt h x)- )- v- SFalse ->- case sFloating @a of- STrue ->- VU.imapM_- ( \i d -> do- let x :: Int- x = doubleToInt (realToFrac d :: Double)- (_, !h) <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (i, hashWithSalt h x)- )- v- SFalse ->- VU.imapM_- ( \i d -> do- let x = hash (show d)- (_, !h) <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (i, hashWithSalt h x)- )- v- BoxedColumn (v :: V.Vector a) ->- case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl ->- V.imapM_- ( \i t -> do- (_, !h) <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (i, hashWithSalt h t)- )- v- Nothing ->- V.imapM_- ( \i d -> do- let x = hash (show d)- (_, !h) <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (i, hashWithSalt h x)- )- v- OptionalColumn v ->- V.imapM_- ( \i d -> do- let x = hash (show d)- (_, !h) <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (i, hashWithSalt h x)- )- v-- let numPasses = 4- bucketSize = 65536- radixFunc k (_, !h) =- let h' = fromIntegral h `xor` (1 `unsafeShiftL` 63) :: Word- shiftBits = k * 16- in fromIntegral ((h' `unsafeShiftR` shiftBits) .&. 65535)- VA.sortBy numPasses bucketSize radixFunc mv- VU.unsafeFreeze mv--{- | Build the rowToGroup lookup vector from valueIndices and offsets.-rowToGroup[i] = k means row i belongs to group k.--}-buildRowToGroup :: Int -> VU.Vector Int -> VU.Vector Int -> VU.Vector Int-buildRowToGroup n vis os = runST $ do- rtg <- VUM.new n- let nGroups = VU.length os - 1- forM_ [0 .. nGroups - 1] $ \k ->- let s = VU.unsafeIndex os k- e = VU.unsafeIndex os (k + 1)- in forM_ [s .. e - 1] $ \i ->- VUM.unsafeWrite rtg (VU.unsafeIndex vis i) k- VU.unsafeFreeze rtg-{-# NOINLINE buildRowToGroup #-}--changingPoints :: VU.Vector (Int, Int) -> VU.Vector Int-changingPoints vs =- VU.reverse- (VU.fromList (VU.length vs : fst (VU.ifoldl' findChangePoints initialState vs)))- where- initialState = ([0], snd (VU.head vs))- findChangePoints (!offsets, !currentVal) index (_, !newVal)- | currentVal == newVal = (offsets, currentVal)- | otherwise = (index : offsets, newVal)--computeRowHashes :: [Int] -> DataFrame -> VU.Vector Int-computeRowHashes indices df = runST $ do- let n = fst (dimensions df)- mv <- VUM.new n-- let selectedCols = map (columns df V.!) indices-- forM_ selectedCols $ \case- UnboxedColumn (v :: VU.Vector a) ->- case testEquality (typeRep @a) (typeRep @Int) of- Just Refl ->- VU.imapM_- ( \i (x :: Int) -> do- h <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (hashWithSalt h x)- )- v- Nothing ->- case testEquality (typeRep @a) (typeRep @Double) of- Just Refl ->- VU.imapM_- ( \i (d :: Double) -> do- h <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (hashWithSalt h (doubleToInt d))- )- v- Nothing ->- case sIntegral @a of- STrue ->- VU.imapM_- ( \i d -> do- let x :: Int- x = fromIntegral @a @Int d- h <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (hashWithSalt h x)- )- v- SFalse ->- case sFloating @a of- STrue ->- VU.imapM_- ( \i d -> do- let x :: Int- x = doubleToInt (realToFrac d :: Double)- h <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (hashWithSalt h x)- )- v- SFalse ->- VU.imapM_- ( \i d -> do- let x = hash (show d)- h <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (hashWithSalt h x)- )- v- BoxedColumn (v :: V.Vector a) ->- case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl ->- V.imapM_- ( \i (t :: T.Text) -> do- h <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (hashWithSalt h t)- )- v- Nothing ->- V.imapM_- ( \i d -> do- let x = hash (show d)- h <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (hashWithSalt h x)- )- v- OptionalColumn v ->- V.imapM_- ( \i d -> do- let x = hash (show d)- h <- VUM.unsafeRead mv i- VUM.unsafeWrite mv i (hashWithSalt h x)- )- v-- VU.unsafeFreeze mv- where- doubleToInt :: Double -> Int- doubleToInt = floor . (* 1000)--{- | Aggregate a grouped dataframe using the expressions given.-All ungrouped columns will be dropped.--}-aggregate :: [NamedExpr] -> GroupedDataFrame -> DataFrame-aggregate aggs gdf@(Grouped df groupingColumns valueIndices offsets _rowToGroup) =- let- df' =- selectIndices- (VU.map (valueIndices VU.!) (VU.init offsets))- (select groupingColumns df)-- f (name, UExpr (expr :: Expr a)) d =- let- value = case interpretAggregation @a gdf expr of- Left e -> throw e- Right (UnAggregated _) -> throw $ UnaggregatedException (T.pack $ show expr)- Right (Aggregated (TColumn col)) -> col- in- insertColumn name value d- in- fold f aggs df'--selectIndices :: VU.Vector Int -> DataFrame -> DataFrame-selectIndices xs df =- df- { columns = V.map (atIndicesStable xs) (columns df)- , dataframeDimensions = (VU.length xs, V.length (columns df))- }---- | Filter out all non-unique values in a dataframe.-distinct :: DataFrame -> DataFrame-distinct df = selectIndices (VU.map (indices VU.!) (VU.init os)) df- where- (Grouped _ _ indices os _rtg) = groupBy (columnNames df) df
− src/DataFrame/Operations/Core.hs
@@ -1,947 +0,0 @@-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.Operations.Core where--import qualified Data.List as L-import qualified Data.Map as M-import qualified Data.Map.Strict as MS-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 Data.Either-import qualified Data.Foldable as Fold-import Data.Function (on, (&))-import Data.Maybe-import Data.Type.Equality (TestEquality (..))-import DataFrame.Errors-import DataFrame.Internal.Column (- Column (..),- Columnable,- TypedColumn (..),- columnLength,- columnTypeString,- expandColumn,- fromList,- fromVector,- toDoubleVector,- toFloatVector,- toIntVector,- toUnboxedVector,- toVector,- )-import DataFrame.Internal.DataFrame (- DataFrame (..),- columnIndices,- empty,- getColumn,- )-import DataFrame.Internal.Expression-import DataFrame.Internal.Interpreter-import DataFrame.Internal.Parsing (isNullish)-import DataFrame.Internal.Row (Any, mkColumnFromRow)-import Type.Reflection-import Prelude hiding (null)--{- | O(1) Get DataFrame dimensions i.e. (rows, columns)--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> df = D.fromNamedColumns [("a", D.fromList [1..100]), ("b", D.fromList [1..100]), ("c", D.fromList [1..100])]->>> D.dimensions df--(100, 3)-@--}-dimensions :: DataFrame -> (Int, Int)-dimensions = dataframeDimensions-{-# INLINE dimensions #-}--{- | O(1) Get number of rows in a dataframe.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> df = D.fromNamedColumns [("a", D.fromList [1..100]), ("b", D.fromList [1..100]), ("c", D.fromList [1..100])]->>> D.nRows df-100-@--}-nRows :: DataFrame -> Int-nRows = fst . dataframeDimensions--{- | O(1) Get number of columns in a dataframe.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> df = D.fromNamedColumns [("a", D.fromList [1..100]), ("b", D.fromList [1..100]), ("c", D.fromList [1..100])]->>> D.nColumns df-3-@--}-nColumns :: DataFrame -> Int-nColumns = snd . dataframeDimensions--{- | O(k) Get column names of the DataFrame in order of insertion.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> df = D.fromNamedColumns [("a", D.fromList [1..100]), ("b", D.fromList [1..100]), ("c", D.fromList [1..100])]->>> D.columnNames df--["a", "b", "c"]-@--}-columnNames :: DataFrame -> [T.Text]-columnNames = map fst . L.sortBy (compare `on` snd) . M.toList . columnIndices-{-# INLINE columnNames #-}--{- | 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__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> import qualified Data.Vector as V->>> D.insertVector "numbers" (V.fromList [(1 :: Int)..10]) D.empty----------- numbers---------- Int---------- 1- 2- 3- 4- 5- 6- 7- 8- 9- 10--@--}-insertVector ::- 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 #-}--{- | Adds a foldable collection to the dataframe. If the collection has less elements than the-dataframe and the dataframe is not empty-the collection is converted to type `Maybe a` filled with `Nothing` to match the size of the dataframe. Similarly,-if the collection has more elements than what's currently in the dataframe, the other columns in the dataframe are-change to `Maybe <Type>` and filled with `Nothing`.--Be careful not to insert infinite collections with this function as that will crash the program.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> D.insert "numbers" [(1 :: Int)..10] D.empty----------- numbers---------- Int---------- 1- 2- 3- 4- 5- 6- 7- 8- 9- 10--@--}-insert ::- forall a t.- (Columnable a, Foldable t) =>- -- | Column Name- T.Text ->- -- | Sequence to add to dataframe- t a ->- -- | DataFrame to add column to- DataFrame ->- DataFrame-insert name xs = insertColumn name (fromList (Fold.foldr' (:) [] xs)) -- TODO: Do reflection on container type so we can sometimes avoid the list construction.-{-# INLINE insert #-}--{- | Adds a vector to the dataframe and pads it with a default value if it has less elements than the number of rows.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified Data.Vector as V->>> import qualified DataFrame as D->>> df = D.fromNamedColumns [("x", D.fromList [(1 :: Int)..10])]->>> D.insertVectorWithDefault 0 "numbers" (V.fromList [(1 :: Int),2,3]) df---------------- x | numbers-----|---------Int | Int-----|---------1 | 1-2 | 2-3 | 3-4 | 0-5 | 0-6 | 0-7 | 0-8 | 0-9 | 0-10 | 0--@--}-insertVectorWithDefault ::- forall a.- (Columnable a) =>- -- | Default Value- a ->- -- | Column name- T.Text ->- -- | Data to add to column- V.Vector a ->- -- | DataFrame to add the column to- 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--{- | Adds a list to the dataframe and pads it with a default value if it has less elements than the number of rows.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> df = D.fromNamedColumns [("x", D.fromList [(1 :: Int)..10])]->>> D.insertWithDefault 0 "numbers" [(1 :: Int),2,3] df---------------- x | numbers-----|---------Int | Int-----|---------1 | 1-2 | 2-3 | 3-4 | 0-5 | 0-6 | 0-7 | 0-8 | 0-9 | 0-10 | 0--@--}-insertWithDefault ::- forall a t.- (Columnable a, Foldable t) =>- -- | Default Value- a ->- -- | Column name- T.Text ->- -- | Data to add to column- t a ->- -- | DataFrame to add the column to- DataFrame ->- DataFrame-insertWithDefault defaultValue name xs d =- let (rows, _) = dataframeDimensions d- xs' = Fold.foldr' (:) [] xs- values = xs' ++ replicate (rows - length xs') defaultValue- in insertColumn name (fromList values) d--{- | /O(n)/ Adds an unboxed vector to the dataframe.--Same as insertVector but takes an unboxed vector. If you insert a vector of numbers through insertVector it will either way be converted-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 the column to- DataFrame ->- DataFrame-insertUnboxedVector name xs = insertColumn name (UnboxedColumn xs)--{- | /O(n)/ Add a column to the dataframe.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> D.insertColumn "numbers" (D.fromList [(1 :: Int)..10]) D.empty----------- numbers---------- Int---------- 1- 2- 3- 4- 5- 6- 7- 8- 9- 10--@--}-insertColumn ::- -- | Column Name- T.Text ->- -- | Column to add- Column ->- -- | DataFrame to add the column to- 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)- M.empty- Nothing ->- DataFrame- (V.map (expandColumn n) (columns d `V.snoc` column))- (M.insert name c (columnIndices d))- (n, c + 1)- M.empty--{- | /O(n)/ Clones a column and places it under a new name in the dataframe.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified Data.Vector as V->>> df = insertVector "numbers" (V.fromList [1..10]) D.empty->>> D.cloneColumn "numbers" "others" df-------------------- numbers | others----------|-------- Int | Int----------|-------- 1 | 1- 2 | 2- 3 | 3- 4 | 4- 5 | 5- 6 | 6- 7 | 7- 8 | 8- 9 | 9- 10 | 10--@--}-cloneColumn :: T.Text -> T.Text -> DataFrame -> DataFrame-cloneColumn original new df = fromMaybe- ( throw $- ColumnNotFoundException original "cloneColumn" (M.keys $ columnIndices df)- )- $ do- column <- getColumn original df- return $ insertColumn new column df--{- | /O(n)/ Renames a single column.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> import qualified Data.Vector as V->>> df = insertVector "numbers" (V.fromList [1..10]) D.empty->>> D.rename "numbers" "others" df---------- others--------- Int--------- 1- 2- 3- 4- 5- 6- 7- 8- 9- 10--@--}-rename :: T.Text -> T.Text -> DataFrame -> DataFrame-rename orig new df = either throw id (renameSafe orig new df)--{- | /O(n)/ Renames many columns.--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> import qualified Data.Vector as V->>> df = D.insertVector "others" (V.fromList [11..20]) (D.insertVector "numbers" (V.fromList [1..10]) D.empty)->>> df-------------------- numbers | others----------|-------- Int | Int----------|-------- 1 | 11- 2 | 12- 3 | 13- 4 | 14- 5 | 15- 6 | 16- 7 | 17- 8 | 18- 9 | 19- 10 | 20-->>> D.renameMany [("numbers", "first_10"), ("others", "next_10")] df---------------------- first_10 | next_10-----------|--------- Int | Int-----------|--------- 1 | 11- 2 | 12- 3 | 13- 4 | 14- 5 | 15- 6 | 16- 7 | 17- 8 | 18- 9 | 19- 10 | 20--@--}-renameMany :: [(T.Text, T.Text)] -> DataFrame -> DataFrame-renameMany = fold (uncurry rename)--renameSafe ::- T.Text -> T.Text -> DataFrame -> Either DataFrameException DataFrame-renameSafe orig new df = fromMaybe- (Left $ ColumnNotFoundException orig "rename" (M.keys $ 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})--data ColumnInfo = ColumnInfo- { nameOfColumn :: !T.Text- , nonNullValues :: !Int- , nullValues :: !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.--==== __Example__-@->>> import qualified Data.Vector as V->>> df = D.insertVector "others" (V.fromList [11..20]) (D.insertVector "numbers" (V.fromList [1..10]) D.empty)->>> D.describeColumns df----------------------------------------------------------- Column Name | # Non-null Values | # Null Values | Type--------------|-------------------|---------------|------ Text | Int | Int | Text--------------|-------------------|---------------|------ others | 10 | 0 | Int- numbers | 10 | 0 | Int--@--}-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 "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- columnType = T.pack $ show $ typeRep @a- in- if isNothing cname- then acc- else- ColumnInfo- (fromMaybe "" cname)- (columnLength col - countNulls)- countNulls- columnType- : acc- go acc i col@(BoxedColumn (c :: V.Vector a)) =- let- cname = columnName i- columnType = T.pack $ show $ typeRep @a- in- if isNothing cname- then acc- else- ColumnInfo- (fromMaybe "" cname)- (columnLength col)- 0- columnType- : acc- go acc i col@(UnboxedColumn c) =- let- cname = columnName i- columnType = T.pack $ columnTypeString col- 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 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-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-partiallyParsed _ = 0--{- | Creates a dataframe from a list of tuples with name and column.--==== __Example__-@->>> df = D.fromNamedColumns [("numbers", D.fromList [1..10]), ("others", D.fromList [11..20])]->>> df------------------- numbers | others----------|-------- Int | Int----------|-------- 1 | 11- 2 | 12- 3 | 13- 4 | 14- 5 | 15- 6 | 16- 7 | 17- 8 | 18- 9 | 19- 10 | 20--@--}-fromNamedColumns :: [(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 always rename the columns after-or drop the ones you don't want.--==== __Example__-@->>> df = D.fromUnnamedColumns [D.fromList [1..10], D.fromList [11..20]]->>> df------------------- 0 | 1------|----- Int | Int------|----- 1 | 11- 2 | 12- 3 | 13- 4 | 14- 5 | 15- 6 | 16- 7 | 17- 8 | 18- 9 | 19- 10 | 20--@--}-fromUnnamedColumns :: [Column] -> DataFrame-fromUnnamedColumns = fromNamedColumns . zip (map (T.pack . show) [0 ..])--{- | Create a dataframe from a list of column names and rows.--==== __Example__-@->>> df = D.fromRows ["A", "B"] [[D.toAny 1, D.toAny 11], [D.toAny 2, D.toAny 12], [D.toAny 3, D.toAny 13]]-->>> df------------- A | B------|----- Int | Int------|----- 1 | 11- 2 | 12- 3 | 13--@--}-fromRows :: [T.Text] -> [[Any]] -> DataFrame-fromRows names rows =- L.foldl'- (\df i -> insertColumn (names !! i) (mkColumnFromRow i rows) df)- empty- [0 .. length names - 1]--{- | O (k * n) Counts the occurences of each value in a given column.--==== __Example__-@->>> df = D.fromUnnamedColumns [D.fromList [1..10], D.fromList [11..20]]-->>> D.valueCounts @Int "0" df--[(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,1),(10,1)]--@--}-valueCounts ::- forall a. (Ord a, Columnable a) => Expr a -> DataFrame -> [(a, Int)]-valueCounts expr df = case columnAsVector expr df of- Left e -> throw e- Right column' ->- let- column = V.foldl' (\m v -> MS.insertWith (+) v (1 :: Int) m) M.empty column'- in- M.toAscList column--{- | O (k * n) Shows the proportions of each value in a given column.--==== __Example__-@->>> df = D.fromUnnamedColumns [D.fromList [1..10], D.fromList [11..20]]-->>> D.valueCounts @Int "0" df--[(1,0.1),(2,0.1),(3,0.1),(4,0.1),(5,0.1),(6,0.1),(7,0.1),(8,0.1),(9,0.1),(10,0.1)]--@--}-valueProportions ::- forall a. (Ord a, Columnable a) => Expr a -> DataFrame -> [(a, Double)]-valueProportions expr df = case columnAsVector expr df of- Left e -> throw e- Right column' ->- let- counts =- M.toAscList- (V.foldl' (\m v -> MS.insertWith (+) v (1 :: Int) m) M.empty column')- total = fromIntegral (sum (map snd counts))- in- map (fmap ((/ total) . fromIntegral)) counts--{- | A left fold for dataframes that takes the dataframe as the last object.-This makes it easier to chain operations.--==== __Example__-@->>> df = D.fromNamedColumns [("x", D.fromList [1..100]), ("y", D.fromList [11..110])]->>> D.fold D.dropLast [1..5] df------------ x | y-----|-----Int | Int-----|-----1 | 11-2 | 12-3 | 13-4 | 14-5 | 15-6 | 16-7 | 17-8 | 18-9 | 19-10 | 20-11 | 21-12 | 22-13 | 23-14 | 24-15 | 25-16 | 26-17 | 27-18 | 28-19 | 29-20 | 30--Showing 20 rows out of 85--@--}-fold :: (a -> DataFrame -> DataFrame) -> [a] -> DataFrame -> DataFrame-fold f xs acc = L.foldl' (flip f) acc xs--{- | Returns a dataframe as a two dimensional vector of floats.--Converts all columns in the dataframe to float vectors and transposes them-into a row-major matrix representation.--This is useful for handing data over into ML systems.--Returns 'Left' with an error if any column cannot be converted to floats.--}-toFloatMatrix ::- DataFrame -> Either DataFrameException (V.Vector (VU.Vector Float))-toFloatMatrix df = case V.foldl'- (\acc c -> V.snoc <$> acc <*> toFloatVector c)- (Right V.empty :: Either DataFrameException (V.Vector (VU.Vector Float)))- (columns df) of- Left e -> Left e- Right m ->- pure $- V.generate- (fst (dataframeDimensions df))- ( \i ->- foldl- (\acc j -> acc `VU.snoc` ((m VG.! j) VG.! i))- VU.empty- [0 .. (V.length m - 1)]- )--{- | Returns a dataframe as a two dimensional vector of doubles.--Converts all columns in the dataframe to double vectors and transposes them-into a row-major matrix representation.--This is useful for handing data over into ML systems.--Returns 'Left' with an error if any column cannot be converted to doubles.--}-toDoubleMatrix ::- DataFrame -> Either DataFrameException (V.Vector (VU.Vector Double))-toDoubleMatrix df = case V.foldl'- (\acc c -> V.snoc <$> acc <*> toDoubleVector c)- (Right V.empty :: Either DataFrameException (V.Vector (VU.Vector Double)))- (columns df) of- Left e -> Left e- Right m ->- pure $- V.generate- (fst (dataframeDimensions df))- ( \i ->- foldl- (\acc j -> acc `VU.snoc` ((m VG.! j) VG.! i))- VU.empty- [0 .. (V.length m - 1)]- )--{- | Returns a dataframe as a two dimensional vector of ints.--Converts all columns in the dataframe to int vectors and transposes them-into a row-major matrix representation.--This is useful for handing data over into ML systems.--Returns 'Left' with an error if any column cannot be converted to ints.--}-toIntMatrix :: DataFrame -> Either DataFrameException (V.Vector (VU.Vector Int))-toIntMatrix df = case V.foldl'- (\acc c -> V.snoc <$> acc <*> toIntVector c)- (Right V.empty :: Either DataFrameException (V.Vector (VU.Vector Int)))- (columns df) of- Left e -> Left e- Right m ->- pure $- V.generate- (fst (dataframeDimensions df))- ( \i ->- foldl- (\acc j -> acc `VU.snoc` ((m VG.! j) VG.! i))- VU.empty- [0 .. (V.length m - 1)]- )--{- | Get a specific column as a vector.--You must specify the type via type applications.--==== __Examples__-->>> columnAsVector (F.col @Int "age") df-Right [25, 30, 35, ...]-->>> columnAsVector (F.col @Text "name") df-Right ["Alice", "Bob", "Charlie", ...]--}-columnAsVector ::- forall a.- (Columnable a) => Expr a -> DataFrame -> Either DataFrameException (V.Vector a)-columnAsVector (Col name) df = case getColumn name df of- Just col -> toVector col- Nothing ->- Left $ ColumnNotFoundException name "columnAsVector" (M.keys $ columnIndices df)-columnAsVector expr df = case interpret df expr of- Left e -> throw e- Right (TColumn col) -> toVector col--{- | Retrieves a column as an unboxed vector of 'Int' values.--Returns 'Left' with a 'DataFrameException' if the column cannot be converted to ints.-This may occur if the column contains non-numeric data or values outside the 'Int' range.--}-columnAsIntVector ::- (Columnable a, Num a) =>- Expr a -> DataFrame -> Either DataFrameException (VU.Vector Int)-columnAsIntVector (Col name) df = case getColumn name df of- Just col -> toIntVector col- Nothing ->- Left $- ColumnNotFoundException name "columnAsIntVector" (M.keys $ columnIndices df)-columnAsIntVector expr df = case interpret df expr of- Left e -> throw e- Right (TColumn col) -> toIntVector col--{- | Retrieves a column as an unboxed vector of 'Double' values.--Returns 'Left' with a 'DataFrameException' if the column cannot be converted to doubles.-This may occur if the column contains non-numeric data.--}-columnAsDoubleVector ::- (Columnable a, Num a) =>- Expr a -> DataFrame -> Either DataFrameException (VU.Vector Double)-columnAsDoubleVector (Col name) df = case getColumn name df of- Just col -> toDoubleVector col- Nothing ->- Left $- ColumnNotFoundException name "columnAsDoubleVector" (M.keys $ columnIndices df)-columnAsDoubleVector expr df = case interpret df expr of- Left e -> throw e- Right (TColumn col) -> toDoubleVector col--{- | Retrieves a column as an unboxed vector of 'Float' values.--Returns 'Left' with a 'DataFrameException' if the column cannot be converted to floats.-This may occur if the column contains non-numeric data.--}-columnAsFloatVector ::- (Columnable a, Num a) =>- Expr a -> DataFrame -> Either DataFrameException (VU.Vector Float)-columnAsFloatVector (Col name) df = case getColumn name df of- Just col -> toFloatVector col- Nothing ->- Left $- ColumnNotFoundException name "columnAsFloatVector" (M.keys $ columnIndices df)-columnAsFloatVector expr df = case interpret df expr of- Left e -> throw e- Right (TColumn col) -> toFloatVector col--columnAsUnboxedVector ::- forall a.- (Columnable a, VU.Unbox a) =>- Expr a -> DataFrame -> Either DataFrameException (VU.Vector a)-columnAsUnboxedVector (Col name) df = case getColumn name df of- Just col -> toUnboxedVector col- Nothing ->- Left $- ColumnNotFoundException name "columnAsFloatVector" (M.keys $ columnIndices df)-columnAsUnboxedVector expr df = case interpret df expr of- Left e -> throw e- Right (TColumn col) -> toUnboxedVector col-{-# SPECIALIZE columnAsUnboxedVector ::- Expr Double -> DataFrame -> Either DataFrameException (VU.Vector Double)- #-}-{-# INLINE columnAsUnboxedVector #-}--{- | Get a specific column as a list.--You must specify the type via type applications.--==== __Examples__-->>> columnAsList @Int "age" df-[25, 30, 35, ...]-->>> columnAsList @Text "name" df-["Alice", "Bob", "Charlie", ...]--==== __Throws__--* 'error' - if the column type doesn't match the requested type--}-columnAsList :: forall a. (Columnable a) => Expr a -> DataFrame -> [a]-columnAsList expr df = either throw V.toList (columnAsVector expr df)
− src/DataFrame/Operations/Join.hs
@@ -1,1065 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE NumericUnderscores #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.Operations.Join where--import Control.Applicative ((<|>))-import Control.Monad (forM_, when)-import Control.Monad.ST (ST, runST)-import qualified Data.HashMap.Strict as HM-import Data.List (foldl')-import qualified Data.Map.Strict as M-import Data.Maybe (fromMaybe)-import Data.STRef (newSTRef, readSTRef, writeSTRef)-import qualified Data.Set as S-import qualified Data.Text as T-import Data.Type.Equality (TestEquality (..))-import qualified Data.Vector as VB-import qualified Data.Vector.Algorithms.Merge as VA-import qualified Data.Vector.Unboxed as VU-import qualified Data.Vector.Unboxed.Mutable as VUM-import DataFrame.Internal.Column as D-import DataFrame.Internal.DataFrame as D-import DataFrame.Operations.Aggregation as D-import DataFrame.Operations.Core as D-import Type.Reflection---- | Equivalent to SQL join types.-data JoinType- = INNER- | LEFT- | RIGHT- | FULL_OUTER- deriving (Show)---- | Join two dataframes using SQL join semantics.-join ::- JoinType ->- [T.Text] ->- DataFrame -> -- Right hand side- DataFrame -> -- Left hand side- DataFrame-join INNER xs right = innerJoin xs right-join LEFT xs right = leftJoin xs right-join RIGHT xs right = rightJoin xs right-join FULL_OUTER xs right = fullOuterJoin xs right--{- | Row-count threshold for the build side.-When the build side exceeds this, sort-merge join is used-instead of hash join to avoid L3 cache thrashing.--}-joinStrategyThreshold :: Int-joinStrategyThreshold = 500_000--{- | A compact index mapping hash values to contiguous slices of-original row indices. All indices live in a single unboxed vector;-the HashMap stores @(offset, length)@ into that vector.--}-data CompactIndex = CompactIndex- { ciSortedIndices :: {-# UNPACK #-} !(VU.Vector Int)- , ciOffsets :: !(HM.HashMap Int (Int, Int))- }--{- | Build a compact index from a vector of row hashes.-Sorts @(hash, originalIndex)@ pairs by hash, then scans for-contiguous runs to populate the offset map.--}-buildCompactIndex :: VU.Vector Int -> CompactIndex-buildCompactIndex hashes =- let n = VU.length hashes- (sortedHashes, sortedIndices) = sortWithIndices hashes- !offsets = buildOffsets sortedHashes n 0 HM.empty- in CompactIndex sortedIndices offsets- where- buildOffsets ::- VU.Vector Int ->- Int ->- Int ->- HM.HashMap Int (Int, Int) ->- HM.HashMap Int (Int, Int)- buildOffsets !sh !n !i !acc- | i >= n = acc- | otherwise =- let !h = sh `VU.unsafeIndex` i- !end = findGroupEnd sh h (i + 1) n- in buildOffsets sh n end (HM.insert h (i, end - i) acc)---- | Find the end of a contiguous run of equal values starting at @j@.-findGroupEnd :: VU.Vector Int -> Int -> Int -> Int -> Int-findGroupEnd !v !h !j !n- | j >= n = j- | v `VU.unsafeIndex` j == h = findGroupEnd v h (j + 1) n- | otherwise = j-{-# INLINE findGroupEnd #-}--{- | Sort a hash vector, returning sorted hashes and corresponding original indices.-Sorts an index array using hash values as the comparison key, avoiding the-intermediate pair vector used by the naive zip-then-sort approach.--}-sortWithIndices :: VU.Vector Int -> (VU.Vector Int, VU.Vector Int)-sortWithIndices hashes = runST $ do- let n = VU.length hashes- mv <- VU.thaw (VU.enumFromN 0 n)- VA.sortBy- (\i j -> compare (hashes `VU.unsafeIndex` i) (hashes `VU.unsafeIndex` j))- mv- sortedIdxs <- VU.unsafeFreeze mv- return (VU.unsafeBackpermute hashes sortedIdxs, sortedIdxs)---- | Write the cross product of two index ranges into mutable vectors.-fillCrossProduct ::- VU.Vector Int ->- VU.Vector Int ->- Int ->- Int ->- Int ->- Int ->- VUM.MVector s Int ->- VUM.MVector s Int ->- Int ->- ST s ()-fillCrossProduct !leftSI !rightSI !lStart !lEnd !rStart !rEnd !lv !rv !pos = goL lStart pos- where- !rLen = rEnd - rStart- goL !li !p- | li >= lEnd = return ()- | otherwise = do- let !lOrigIdx = leftSI `VU.unsafeIndex` li- goR lOrigIdx rStart p- goL (li + 1) (p + rLen)- goR !lOrigIdx !ri !q- | ri >= rEnd = return ()- | otherwise = do- VUM.unsafeWrite lv q lOrigIdx- VUM.unsafeWrite rv q (rightSI `VU.unsafeIndex` ri)- goR lOrigIdx (ri + 1) (q + 1)-{-# INLINE fillCrossProduct #-}---- | Compute key-column indices from the column index map.-keyColIndices :: S.Set T.Text -> DataFrame -> [Int]-keyColIndices csSet df = M.elems $ M.restrictKeys (D.columnIndices df) csSet---- ============================================================--- Inner Join--- ============================================================--{- | Performs an inner join on two dataframes using the specified key columns.-Returns only rows where the key values exist in both dataframes.--==== __Example__-@-ghci> df = D.fromNamedColumns [("key", D.fromList ["K0", "K1", "K2", "K3"]), ("A", D.fromList ["A0", "A1", "A2", "A3"])]-ghci> other = D.fromNamedColumns [("key", D.fromList ["K0", "K1", "K2"]), ("B", D.fromList ["B0", "B1", "B2"])]-ghci> D.innerJoin ["key"] df other-------------------- key | A | B-------|-----|----- Text | Text| Text-------|-----|----- K0 | A0 | B0- K1 | A1 | B1- K2 | A2 | B2--@--}-innerJoin :: [T.Text] -> DataFrame -> DataFrame -> DataFrame-innerJoin cs left right- | D.null right || D.null left = D.empty- | otherwise =- let- csSet = S.fromList cs- leftRows = fst (D.dimensions left)- rightRows = fst (D.dimensions right)-- leftKeyIdxs = keyColIndices csSet left- rightKeyIdxs = keyColIndices csSet right- leftHashes = D.computeRowHashes leftKeyIdxs left- rightHashes = D.computeRowHashes rightKeyIdxs right-- buildRows = min leftRows rightRows- (leftIxs, rightIxs)- | buildRows > joinStrategyThreshold =- sortMergeInnerKernel leftHashes rightHashes- | rightRows <= leftRows =- -- Build on right (smaller or equal), probe with left- hashInnerKernel leftHashes rightHashes- | otherwise =- -- Build on left (smaller), probe with right, swap result- let (!rIxs, !lIxs) = hashInnerKernel rightHashes leftHashes- in (lIxs, rIxs)- in- assembleInner csSet left right leftIxs rightIxs---- | Compute hashes for the given key column names in a DataFrame.-buildHashColumn :: [T.Text] -> DataFrame -> VU.Vector Int-buildHashColumn keys df =- let csSet = S.fromList keys- keyIdxs = keyColIndices csSet df- in D.computeRowHashes keyIdxs df--{- | Probe one batch of rows against a pre-built 'CompactIndex'.-Returns @(probeExpandedIxs, buildExpandedIxs)@.-Unlike 'hashInnerKernel', does not build the index (it is pre-built once)-and has no cross-product row guard — the caller controls probe batch size.--}-hashProbeKernel ::- -- | Built once from the full right\/build side.- CompactIndex ->- -- | Probe hashes (one batch).- VU.Vector Int ->- (VU.Vector Int, VU.Vector Int)-hashProbeKernel ci probeHashes =- let ciIxs = ciSortedIndices ci- ciOff = ciOffsets ci- (pFrozen, bFrozen) = runST $ do- let !probeN = VU.length probeHashes- initCap = max 1 (min probeN 1_000_000)-- initPv <- VUM.unsafeNew initCap- initBv <- VUM.unsafeNew initCap- pvRef <- newSTRef initPv- bvRef <- newSTRef initBv- capRef <- newSTRef initCap- posRef <- newSTRef (0 :: Int)-- let ensureCapacity needed = do- cap <- readSTRef capRef- when (needed > cap) $ do- let newCap = max needed (cap * 2)- delta = newCap - cap- pv <- readSTRef pvRef- bv <- readSTRef bvRef- newPv <- VUM.unsafeGrow pv delta- newBv <- VUM.unsafeGrow bv delta- writeSTRef pvRef newPv- writeSTRef bvRef newBv- writeSTRef capRef newCap-- go !i- | i >= probeN = return ()- | otherwise = do- let !h = probeHashes `VU.unsafeIndex` i- case HM.lookup h ciOff of- Nothing -> go (i + 1)- Just (!start, !len) -> do- !p <- readSTRef posRef- ensureCapacity (p + len)- pv <- readSTRef pvRef- bv <- readSTRef bvRef- fillBuild i start len p 0 pv bv- writeSTRef posRef (p + len)- go (i + 1)- fillBuild !probeIdx !start !len !p !j !pv !bv- | j >= len = return ()- | otherwise = do- VUM.unsafeWrite pv (p + j) probeIdx- VUM.unsafeWrite bv (p + j) (ciIxs `VU.unsafeIndex` (start + j))- fillBuild probeIdx start len p (j + 1) pv bv- go 0-- !total <- readSTRef posRef- pv <- readSTRef pvRef- bv <- readSTRef bvRef- (,)- <$> VU.unsafeFreeze (VUM.slice 0 total pv)- <*> VU.unsafeFreeze (VUM.slice 0 total bv)- in (VU.force pFrozen, VU.force bFrozen)--{- | Hash-based inner join kernel.-Builds compact index on @buildHashes@ (second arg), probes with-@probeHashes@ (first arg).-Returns @(probeExpandedIndices, buildExpandedIndices)@.-Uses a dynamically growing output buffer to avoid pre-allocating the full-cross-product size (which can be astronomically large for low-cardinality keys).--}--{- | Maximum number of output rows allowed from a join kernel.-Exceeding this limit indicates a cross-product explosion (e.g. low-cardinality keys).--}-maxJoinOutputRows :: Int-maxJoinOutputRows = 500_000_000--hashInnerKernel ::- VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int)-hashInnerKernel probeHashes buildHashes =- let (pFrozen, bFrozen) = runST $ do- let ci = buildCompactIndex buildHashes- ciIxs = ciSortedIndices ci- ciOff = ciOffsets ci- !probeN = VU.length probeHashes- !buildN = VU.length buildHashes- initCap = max 1 (min (probeN + buildN) 1_000_000)-- initPv <- VUM.unsafeNew initCap- initBv <- VUM.unsafeNew initCap- pvRef <- newSTRef initPv- bvRef <- newSTRef initBv- capRef <- newSTRef initCap- posRef <- newSTRef (0 :: Int)-- let ensureCapacity needed = do- cap <- readSTRef capRef- when (needed > cap) $ do- let newCap = max needed (cap * 2)- delta = newCap - cap- pv <- readSTRef pvRef- bv <- readSTRef bvRef- newPv <- VUM.unsafeGrow pv delta- newBv <- VUM.unsafeGrow bv delta- writeSTRef pvRef newPv- writeSTRef bvRef newBv- writeSTRef capRef newCap-- go !i- | i >= probeN = return ()- | otherwise = do- let !h = probeHashes `VU.unsafeIndex` i- case HM.lookup h ciOff of- Nothing -> go (i + 1)- Just (!start, !len) -> do- !p <- readSTRef posRef- when (p + len > maxJoinOutputRows) $- error $- "Join output would exceed "- ++ show maxJoinOutputRows- ++ " rows (cross-product explosion). "- ++ "Consider filtering or using higher-cardinality join keys or using the lazy API."- ensureCapacity (p + len)- pv <- readSTRef pvRef- bv <- readSTRef bvRef- fillBuild i start len p 0 pv bv- writeSTRef posRef (p + len)- go (i + 1)- fillBuild !probeIdx !start !len !p !j !pv !bv- | j >= len = return ()- | otherwise = do- VUM.unsafeWrite pv (p + j) probeIdx- VUM.unsafeWrite bv (p + j) (ciIxs `VU.unsafeIndex` (start + j))- fillBuild probeIdx start len p (j + 1) pv bv- go 0-- !total <- readSTRef posRef- pv <- readSTRef pvRef- bv <- readSTRef bvRef- (,)- <$> VU.unsafeFreeze (VUM.slice 0 total pv)- <*> VU.unsafeFreeze (VUM.slice 0 total bv)- in -- VU.force copies the slice into a compact array, releasing the oversized- -- backing buffer allocated by the doubling strategy.- (VU.force pFrozen, VU.force bFrozen)--{- | Sort-merge inner join kernel.-Sorts both sides by hash, walks in lockstep.-Returns @(leftExpandedIndices, rightExpandedIndices)@.-Uses a dynamically growing output buffer instead of a two-pass count-then-allocate-strategy, which OOMs when low-cardinality keys produce large cross products.--}-sortMergeInnerKernel ::- VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int)-sortMergeInnerKernel leftHashes rightHashes =- let (lFrozen, rFrozen) = runST $ do- let (leftSH, leftSI) = sortWithIndices leftHashes- (rightSH, rightSI) = sortWithIndices rightHashes- !leftN = VU.length leftHashes- !rightN = VU.length rightHashes- initCap = max 1 (min (leftN + rightN) 1_000_000)-- initLv <- VUM.unsafeNew initCap- initRv <- VUM.unsafeNew initCap- lvRef <- newSTRef initLv- rvRef <- newSTRef initRv- capRef <- newSTRef initCap- posRef <- newSTRef (0 :: Int)-- let ensureCapacity needed = do- cap <- readSTRef capRef- when (needed > cap) $ do- let newCap = max needed (cap * 2)- delta = newCap - cap- lv <- readSTRef lvRef- rv <- readSTRef rvRef- newLv <- VUM.unsafeGrow lv delta- newRv <- VUM.unsafeGrow rv delta- writeSTRef lvRef newLv- writeSTRef rvRef newRv- writeSTRef capRef newCap-- fillGroup !li !lEnd !ri !rEnd = do- let !lLen = lEnd - li- !rLen = rEnd - ri- !groupSize = lLen * rLen- !p <- readSTRef posRef- when (p + groupSize > maxJoinOutputRows) $- error $- "Join output would exceed "- ++ show maxJoinOutputRows- ++ " rows (cross-product explosion with group sizes "- ++ show lLen- ++ " × "- ++ show rLen- ++ "). Consider filtering or using higher-cardinality join keys."- ensureCapacity (p + groupSize)- lv <- readSTRef lvRef- rv <- readSTRef rvRef- let goL !lIdx !pos- | lIdx >= lEnd = return ()- | otherwise = do- let !lOrig = leftSI `VU.unsafeIndex` lIdx- goR lOrig ri pos- goL (lIdx + 1) (pos + rLen)- goR !lOrig !rIdx !pos- | rIdx >= rEnd = return ()- | otherwise = do- VUM.unsafeWrite lv pos lOrig- VUM.unsafeWrite rv pos (rightSI `VU.unsafeIndex` rIdx)- goR lOrig (rIdx + 1) (pos + 1)- goL li p- writeSTRef posRef (p + groupSize)-- fill !li !ri- | li >= leftN || ri >= rightN = return ()- | lh < rh = fill (li + 1) ri- | lh > rh = fill li (ri + 1)- | otherwise = do- let !lEnd = findGroupEnd leftSH lh (li + 1) leftN- !rEnd = findGroupEnd rightSH rh (ri + 1) rightN- fillGroup li lEnd ri rEnd- fill lEnd rEnd- where- !lh = leftSH `VU.unsafeIndex` li- !rh = rightSH `VU.unsafeIndex` ri-- fill 0 0-- !total <- readSTRef posRef- lv <- readSTRef lvRef- rv <- readSTRef rvRef- (,)- <$> VU.unsafeFreeze (VUM.slice 0 total lv)- <*> VU.unsafeFreeze (VUM.slice 0 total rv)- in -- VU.force copies the slice into a compact array, releasing the oversized- -- backing buffer allocated by the doubling strategy.- (VU.force lFrozen, VU.force rFrozen)---- | Assemble the result DataFrame for an inner join from expanded index vectors.-assembleInner ::- S.Set T.Text ->- DataFrame ->- DataFrame ->- VU.Vector Int ->- VU.Vector Int ->- DataFrame-assembleInner csSet left right leftIxs rightIxs =- let !resultLen = VU.length leftIxs- leftColSet = S.fromList (D.columnNames left)- rightColNames = D.columnNames right-- -- Pre-expand every column once- expandedLeftCols = VB.map (D.atIndicesStable leftIxs) (D.columns left)- expandedRightCols = VB.map (D.atIndicesStable rightIxs) (D.columns right)-- getExpandedLeft name = do- idx <- M.lookup name (D.columnIndices left)- return (expandedLeftCols `VB.unsafeIndex` idx)-- getExpandedRight name = do- idx <- M.lookup name (D.columnIndices right)- return (expandedRightCols `VB.unsafeIndex` idx)-- -- Base DataFrame: all left columns, expanded- baseDf =- left- { columns = expandedLeftCols- , dataframeDimensions = (resultLen, snd (D.dataframeDimensions left))- , derivingExpressions = M.empty- }-- insertIfPresent _ Nothing df = df- insertIfPresent name (Just c) df = D.insertColumn name c df- in D.fold- ( \name df ->- if S.member name csSet- then df -- Key column already present from left side- else- if S.member name leftColSet- then -- Overlapping non-key column: merge with These- insertIfPresent- name- (D.mergeColumns <$> getExpandedLeft name <*> getExpandedRight name)- df- else -- Right-only column- insertIfPresent name (getExpandedRight name) df- )- rightColNames- baseDf---- ============================================================--- Left Join--- ============================================================--{- | Performs a left join on two dataframes using the specified key columns.-Returns all rows from the left dataframe, with matching rows from the right dataframe.-Non-matching rows will have Nothing/null values for columns from the right dataframe.--==== __Example__-@-ghci> df = D.fromNamedColumns [("key", D.fromList ["K0", "K1", "K2", "K3"]), ("A", D.fromList ["A0", "A1", "A2", "A3"])]-ghci> other = D.fromNamedColumns [("key", D.fromList ["K0", "K1", "K2"]), ("B", D.fromList ["B0", "B1", "B2"])]-ghci> D.leftJoin ["key"] df other--------------------------- key | A | B-------|-----|----------- Text | Text| Maybe Text-------|-----|----------- K0 | A0 | Just "B0"- K1 | A1 | Just "B1"- K2 | A2 | Just "B2"- K3 | A3 | Nothing--@--}-leftJoin :: [T.Text] -> DataFrame -> DataFrame -> DataFrame-leftJoin cs left right- | D.null right || D.nRows right == 0 = left- | D.null left || D.nRows left == 0 = D.empty- | otherwise =- let- csSet = S.fromList cs- rightRows = fst (D.dimensions right)-- leftKeyIdxs = keyColIndices csSet left- rightKeyIdxs = keyColIndices csSet right- leftHashes = D.computeRowHashes leftKeyIdxs left- rightHashes = D.computeRowHashes rightKeyIdxs right-- -- Right is always the build side for left join- (leftIxs, rightIxs)- | rightRows > joinStrategyThreshold =- sortMergeLeftKernel leftHashes rightHashes- | otherwise =- hashLeftKernel leftHashes rightHashes- in- -- rightIxs uses -1 as sentinel for "no match"- assembleLeft csSet left right leftIxs rightIxs--{- | Hash-based left join kernel.-Returns @(leftExpandedIndices, rightExpandedIndices)@ where-right indices use @-1@ as sentinel for unmatched rows.-Uses a dynamically growing output buffer to avoid pre-allocating the full-cross-product size (which can be astronomically large for low-cardinality keys).--}-hashLeftKernel ::- VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int)-hashLeftKernel leftHashes rightHashes = runST $ do- let ci = buildCompactIndex rightHashes- ciIxs = ciSortedIndices ci- ciOff = ciOffsets ci- !leftN = VU.length leftHashes- !rightN = VU.length rightHashes- initCap = max 1 (min (leftN + rightN) 1_000_000)-- initLv <- VUM.unsafeNew initCap- initRv <- VUM.unsafeNew initCap- lvRef <- newSTRef initLv- rvRef <- newSTRef initRv- capRef <- newSTRef initCap- posRef <- newSTRef (0 :: Int)-- let ensureCapacity needed = do- cap <- readSTRef capRef- when (needed > cap) $ do- let newCap = max needed (cap * 2)- delta = newCap - cap- lv <- readSTRef lvRef- rv <- readSTRef rvRef- newLv <- VUM.unsafeGrow lv delta- newRv <- VUM.unsafeGrow rv delta- writeSTRef lvRef newLv- writeSTRef rvRef newRv- writeSTRef capRef newCap-- go !i- | i >= leftN = return ()- | otherwise = do- let !h = leftHashes `VU.unsafeIndex` i- !p <- readSTRef posRef- case HM.lookup h ciOff of- Nothing -> do- ensureCapacity (p + 1)- lv <- readSTRef lvRef- rv <- readSTRef rvRef- VUM.unsafeWrite lv p i- VUM.unsafeWrite rv p (-1)- writeSTRef posRef (p + 1)- Just (!start, !len) -> do- ensureCapacity (p + len)- lv <- readSTRef lvRef- rv <- readSTRef rvRef- fillBuild i start len p 0 lv rv- writeSTRef posRef (p + len)- go (i + 1)- fillBuild !leftIdx !start !len !p !j !lv !rv- | j >= len = return ()- | otherwise = do- VUM.unsafeWrite lv (p + j) leftIdx- VUM.unsafeWrite rv (p + j) (ciIxs `VU.unsafeIndex` (start + j))- fillBuild leftIdx start len p (j + 1) lv rv- go 0-- !total <- readSTRef posRef- lv <- readSTRef lvRef- rv <- readSTRef rvRef- (,)- <$> VU.unsafeFreeze (VUM.slice 0 total lv)- <*> VU.unsafeFreeze (VUM.slice 0 total rv)--{- | Sort-merge left join kernel.-Returns @(leftExpandedIndices, rightExpandedIndices)@ with @-1@ sentinel.-Uses a dynamically growing output buffer instead of a two-pass count-then-allocate-strategy, which OOMs when low-cardinality keys produce large cross products.--}-sortMergeLeftKernel ::- VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int)-sortMergeLeftKernel leftHashes rightHashes = runST $ do- let (leftSH, leftSI) = sortWithIndices leftHashes- (rightSH, rightSI) = sortWithIndices rightHashes- !leftN = VU.length leftHashes- !rightN = VU.length rightHashes- initCap = max 1 (min (leftN + rightN) 1_000_000)-- initLv <- VUM.unsafeNew initCap- initRv <- VUM.unsafeNew initCap- lvRef <- newSTRef initLv- rvRef <- newSTRef initRv- capRef <- newSTRef initCap- posRef <- newSTRef (0 :: Int)-- let ensureCapacity needed = do- cap <- readSTRef capRef- when (needed > cap) $ do- let newCap = max needed (cap * 2)- delta = newCap - cap- lv <- readSTRef lvRef- rv <- readSTRef rvRef- newLv <- VUM.unsafeGrow lv delta- newRv <- VUM.unsafeGrow rv delta- writeSTRef lvRef newLv- writeSTRef rvRef newRv- writeSTRef capRef newCap-- fillGroup !li !lEnd !ri !rEnd = do- let !lLen = lEnd - li- !rLen = rEnd - ri- !groupSize = lLen * rLen- !p <- readSTRef posRef- ensureCapacity (p + groupSize)- lv <- readSTRef lvRef- rv <- readSTRef rvRef- let goL !lIdx !pos- | lIdx >= lEnd = return ()- | otherwise = do- let !lOrig = leftSI `VU.unsafeIndex` lIdx- goR lOrig ri pos- goL (lIdx + 1) (pos + rLen)- goR !lOrig !rIdx !pos- | rIdx >= rEnd = return ()- | otherwise = do- VUM.unsafeWrite lv pos lOrig- VUM.unsafeWrite rv pos (rightSI `VU.unsafeIndex` rIdx)- goR lOrig (rIdx + 1) (pos + 1)- goL li p- writeSTRef posRef (p + groupSize)-- fill !li !ri- | li >= leftN = return ()- | ri >= rightN = fillRemainingLeft li- | lh < rh = do- !p <- readSTRef posRef- ensureCapacity (p + 1)- lv <- readSTRef lvRef- rv <- readSTRef rvRef- VUM.unsafeWrite lv p (leftSI `VU.unsafeIndex` li)- VUM.unsafeWrite rv p (-1)- writeSTRef posRef (p + 1)- fill (li + 1) ri- | lh > rh = fill li (ri + 1)- | otherwise = do- let !lEnd = findGroupEnd leftSH lh (li + 1) leftN- !rEnd = findGroupEnd rightSH rh (ri + 1) rightN- fillGroup li lEnd ri rEnd- fill lEnd rEnd- where- !lh = leftSH `VU.unsafeIndex` li- !rh = rightSH `VU.unsafeIndex` ri-- fillRemainingLeft !i = do- let !remaining = leftN - i- when (remaining > 0) $ do- !p <- readSTRef posRef- ensureCapacity (p + remaining)- lv <- readSTRef lvRef- rv <- readSTRef rvRef- let go !j- | j >= remaining = return ()- | otherwise = do- VUM.unsafeWrite lv (p + j) (leftSI `VU.unsafeIndex` (i + j))- VUM.unsafeWrite rv (p + j) (-1)- go (j + 1)- go 0- writeSTRef posRef (p + remaining)-- fill 0 0-- !total <- readSTRef posRef- lv <- readSTRef lvRef- rv <- readSTRef rvRef- (,)- <$> VU.unsafeFreeze (VUM.slice 0 total lv)- <*> VU.unsafeFreeze (VUM.slice 0 total rv)--{- | Assemble the result DataFrame for a left join.-Right index vectors use @-1@ sentinel, gathered via 'gatherWithSentinel'.--}-assembleLeft ::- S.Set T.Text ->- DataFrame ->- DataFrame ->- VU.Vector Int ->- VU.Vector Int ->- DataFrame-assembleLeft csSet left right leftIxs rightIxs =- let !resultLen = VU.length leftIxs- leftColSet = S.fromList (D.columnNames left)- rightColNames = D.columnNames right-- expandedLeftCols = VB.map (D.atIndicesStable leftIxs) (D.columns left)- expandedRightCols = VB.map (D.gatherWithSentinel rightIxs) (D.columns right)-- getExpandedLeft name = do- idx <- M.lookup name (D.columnIndices left)- return (expandedLeftCols `VB.unsafeIndex` idx)-- getExpandedRight name = do- idx <- M.lookup name (D.columnIndices right)- return (expandedRightCols `VB.unsafeIndex` idx)-- baseDf =- left- { columns = expandedLeftCols- , dataframeDimensions = (resultLen, snd (D.dataframeDimensions left))- , derivingExpressions = M.empty- }-- insertIfPresent _ Nothing df = df- insertIfPresent name (Just c) df = D.insertColumn name c df- in D.fold- ( \name df ->- if S.member name csSet- then df- else- if S.member name leftColSet- then- insertIfPresent- name- (D.mergeColumns <$> getExpandedLeft name <*> getExpandedRight name)- df- else insertIfPresent name (getExpandedRight name) df- )- rightColNames- baseDf--{- | Performs a right join on two dataframes using the specified key columns.-Returns all rows from the right dataframe, with matching rows from the left dataframe.-Non-matching rows will have Nothing/null values for columns from the left dataframe.--==== __Example__-@-ghci> df = D.fromNamedColumns [("key", D.fromList ["K0", "K1", "K2", "K3"]), ("A", D.fromList ["A0", "A1", "A2", "A3"])]-ghci> other = D.fromNamedColumns [("key", D.fromList ["K0", "K1"]), ("B", D.fromList ["B0", "B1"])]-ghci> D.rightJoin ["key"] df other-------------------- key | A | B-------|-----|----- Text | Text| Text-------|-----|----- K0 | A0 | B0- K1 | A1 | B1--@--}-rightJoin ::- [T.Text] -> DataFrame -> DataFrame -> DataFrame-rightJoin cs left right = leftJoin cs right left--fullOuterJoin ::- [T.Text] -> DataFrame -> DataFrame -> DataFrame-fullOuterJoin cs left right- | D.null right || D.nRows right == 0 = left- | D.null left || D.nRows left == 0 = right- | otherwise =- let- csSet = S.fromList cs- leftRows = fst (D.dimensions left)- rightRows = fst (D.dimensions right)-- leftKeyIdxs = keyColIndices csSet left- rightKeyIdxs = keyColIndices csSet right- leftHashes = D.computeRowHashes leftKeyIdxs left- rightHashes = D.computeRowHashes rightKeyIdxs right-- -- Both sides can have nulls in full outer- (leftIxs, rightIxs)- | max leftRows rightRows > joinStrategyThreshold =- sortMergeFullOuterKernel leftHashes rightHashes- | otherwise =- hashFullOuterKernel leftHashes rightHashes- in- -- Both index vectors use -1 as sentinel- assembleFullOuter csSet left right leftIxs rightIxs--{- | Hash-based full outer join kernel.-Builds compact indices on both sides.-Returns @(leftExpandedIndices, rightExpandedIndices)@ with @-1@ sentinels.--}-hashFullOuterKernel ::- VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int)-hashFullOuterKernel leftHashes rightHashes = runST $ do- let leftCI = buildCompactIndex leftHashes- rightCI = buildCompactIndex rightHashes- leftOff = ciOffsets leftCI- rightOff = ciOffsets rightCI- leftSI = ciSortedIndices leftCI- rightSI = ciSortedIndices rightCI-- -- Count: matched + left-only + right-only- let leftEntries = HM.toList leftOff- rightEntries = HM.toList rightOff-- !matchedCount =- foldl'- ( \acc (h, (_, ll)) ->- case HM.lookup h rightOff of- Nothing -> acc- Just (_, rl) -> acc + ll * rl- )- 0- leftEntries-- !leftOnlyCount =- foldl'- ( \acc (h, (_, ll)) ->- if HM.member h rightOff then acc else acc + ll- )- 0- leftEntries-- !rightOnlyCount =- foldl'- ( \acc (h, (_, rl)) ->- if HM.member h leftOff then acc else acc + rl- )- 0- rightEntries-- !totalCount = matchedCount + leftOnlyCount + rightOnlyCount-- lv <- VUM.unsafeNew totalCount- rv <- VUM.unsafeNew totalCount- posRef <- newSTRef (0 :: Int)-- -- Fill matched + left-only (iterate left keys)- forM_ leftEntries $ \(h, (lStart, lLen)) -> do- !p <- readSTRef posRef- case HM.lookup h rightOff of- Nothing -> do- -- Left-only rows- let goL !j !q- | j >= lLen = return ()- | otherwise = do- VUM.unsafeWrite lv q (leftSI `VU.unsafeIndex` (lStart + j))- VUM.unsafeWrite rv q (-1)- goL (j + 1) (q + 1)- goL 0 p- writeSTRef posRef (p + lLen)- Just (!rStart, !rLen) -> do- -- Cross product- fillCrossProduct- leftSI- rightSI- lStart- (lStart + lLen)- rStart- (rStart + rLen)- lv- rv- p- writeSTRef posRef (p + lLen * rLen)-- -- Fill right-only (iterate right keys not in left)- forM_ rightEntries $ \(h, (rStart, rLen)) ->- case HM.lookup h leftOff of- Just _ -> return ()- Nothing -> do- !p <- readSTRef posRef- let goR !j !q- | j >= rLen = return ()- | otherwise = do- VUM.unsafeWrite lv q (-1)- VUM.unsafeWrite rv q (rightSI `VU.unsafeIndex` (rStart + j))- goR (j + 1) (q + 1)- goR 0 p- writeSTRef posRef (p + rLen)-- (,) <$> VU.unsafeFreeze lv <*> VU.unsafeFreeze rv--{- | Sort-merge full outer join kernel.-Returns @(leftExpandedIndices, rightExpandedIndices)@ with @-1@ sentinels.--}-sortMergeFullOuterKernel ::- VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int)-sortMergeFullOuterKernel leftHashes rightHashes = runST $ do- let (leftSH, leftSI) = sortWithIndices leftHashes- (rightSH, rightSI) = sortWithIndices rightHashes- !leftN = VU.length leftHashes- !rightN = VU.length rightHashes-- -- Pass 1: count- let countLoop !li !ri !c- | li >= leftN && ri >= rightN = c- | li >= leftN = c + (rightN - ri)- | ri >= rightN = c + (leftN - li)- | lh < rh = countLoop (li + 1) ri (c + 1)- | lh > rh = countLoop li (ri + 1) (c + 1)- | otherwise =- let !lEnd = findGroupEnd leftSH lh (li + 1) leftN- !rEnd = findGroupEnd rightSH rh (ri + 1) rightN- in countLoop lEnd rEnd (c + (lEnd - li) * (rEnd - ri))- where- !lh = leftSH `VU.unsafeIndex` li- !rh = rightSH `VU.unsafeIndex` ri- !totalRows = countLoop 0 0 0-- -- Pass 2: fill- lv <- VUM.unsafeNew totalRows- rv <- VUM.unsafeNew totalRows-- let fill !li !ri !pos- | li >= leftN && ri >= rightN = return ()- | li >= leftN = fillRemainingRight ri pos- | ri >= rightN = fillRemainingLeft li pos- | lh < rh = do- VUM.unsafeWrite lv pos (leftSI `VU.unsafeIndex` li)- VUM.unsafeWrite rv pos (-1)- fill (li + 1) ri (pos + 1)- | lh > rh = do- VUM.unsafeWrite lv pos (-1)- VUM.unsafeWrite rv pos (rightSI `VU.unsafeIndex` ri)- fill li (ri + 1) (pos + 1)- | otherwise = do- let !lEnd = findGroupEnd leftSH lh (li + 1) leftN- !rEnd = findGroupEnd rightSH rh (ri + 1) rightN- !groupSize = (lEnd - li) * (rEnd - ri)- fillCrossProduct leftSI rightSI li lEnd ri rEnd lv rv pos- fill lEnd rEnd (pos + groupSize)- where- !lh = leftSH `VU.unsafeIndex` li- !rh = rightSH `VU.unsafeIndex` ri-- fillRemainingLeft !i !pos- | i >= leftN = return ()- | otherwise = do- VUM.unsafeWrite lv pos (leftSI `VU.unsafeIndex` i)- VUM.unsafeWrite rv pos (-1)- fillRemainingLeft (i + 1) (pos + 1)-- fillRemainingRight !i !pos- | i >= rightN = return ()- | otherwise = do- VUM.unsafeWrite lv pos (-1)- VUM.unsafeWrite rv pos (rightSI `VU.unsafeIndex` i)- fillRemainingRight (i + 1) (pos + 1)-- fill 0 0 0- (,) <$> VU.unsafeFreeze lv <*> VU.unsafeFreeze rv--{- | Assemble the result DataFrame for a full outer join.-Both index vectors use @-1@ sentinel; all columns gathered via-'gatherWithSentinel'. Key columns are coalesced (first non-null wins).--}-assembleFullOuter ::- S.Set T.Text ->- DataFrame ->- DataFrame ->- VU.Vector Int ->- VU.Vector Int ->- DataFrame-assembleFullOuter csSet left right leftIxs rightIxs =- let !resultLen = VU.length leftIxs- leftColSet = S.fromList (D.columnNames left)- rightColNames = D.columnNames right-- expandedLeftCols = VB.map (D.gatherWithSentinel leftIxs) (D.columns left)- expandedRightCols = VB.map (D.gatherWithSentinel rightIxs) (D.columns right)-- getExpandedLeft name = do- idx <- M.lookup name (D.columnIndices left)- return (expandedLeftCols `VB.unsafeIndex` idx)-- getExpandedRight name = do- idx <- M.lookup name (D.columnIndices right)- return (expandedRightCols `VB.unsafeIndex` idx)-- baseDf =- left- { columns = expandedLeftCols- , dataframeDimensions = (resultLen, snd (D.dataframeDimensions left))- , derivingExpressions = M.empty- }-- insertIfPresent _ Nothing df = df- insertIfPresent name (Just c) df = D.insertColumn name c df-- -- Coalesce two OptionalColumns: take first non-Nothing per row,- -- producing a non-optional column.- coalesceKeyColumn :: Column -> Column -> Column- coalesceKeyColumn- (OptionalColumn (lCol :: VB.Vector (Maybe a)))- (OptionalColumn (rCol :: VB.Vector (Maybe b))) =- case testEquality (typeRep @a) (typeRep @b) of- Just Refl ->- D.fromVector $- VB.zipWith- ( \l r ->- fromMaybe (error "fullOuterJoin: null on both sides of key column") (l <|> r)- )- lCol- rCol- Nothing -> error "Cannot join columns of different types"- coalesceKeyColumn _ _ = error "fullOuterJoin: expected OptionalColumn for key columns"- in D.fold- ( \name df ->- if S.member name csSet- then -- Key column: coalesce left and right- case (getExpandedLeft name, getExpandedRight name) of- (Just lc, Just rc) -> D.insertColumn name (coalesceKeyColumn lc rc) df- _ -> df- else- if S.member name leftColSet- then- insertIfPresent- name- (D.mergeColumns <$> getExpandedLeft name <*> getExpandedRight name)- df- else insertIfPresent name (getExpandedRight name) df- )- rightColNames- baseDf
− src/DataFrame/Operations/Merge.hs
@@ -1,64 +0,0 @@-{-# LANGUAGE InstanceSigs #-}--module DataFrame.Operations.Merge where--import qualified Data.List as L-import qualified Data.Text as T-import qualified DataFrame.Internal.Column as D-import qualified DataFrame.Internal.DataFrame as D-import qualified DataFrame.Operations.Core as D--import Data.Maybe--{- | Vertically merge two dataframes using shared columns.-Columns that exist in only one dataframe are padded with Nothing.--}-instance Semigroup D.DataFrame where- (<>) :: D.DataFrame -> D.DataFrame -> D.DataFrame- (<>) a b =- let- addColumns a' b' df name- | fst (D.dimensions a') == 0 && fst (D.dimensions b') == 0 = df- | fst (D.dimensions a') == 0 = fromMaybe df $ do- col <- D.getColumn name b'- pure $ D.insertColumn name col df- | fst (D.dimensions b') == 0 = fromMaybe df $ do- col <- D.getColumn name a'- pure $ D.insertColumn name col df- | otherwise =- let- numRowsA = fst $ D.dimensions a'- numRowsB = fst $ D.dimensions b'- sumRows = numRowsA + numRowsB-- optA = D.getColumn name a'- optB = D.getColumn name b'- in- case optB of- Nothing -> case optA of- Nothing ->- -- N.B. this case should never happen, because we're dealing with columns coming from- -- union of column names of both dataframes. Nothing + Nothing would mean column- -- wasn't in either dataframe, which shouldn't happen- D.insertColumn name (D.fromList ([] :: [T.Text])) df- Just a'' ->- D.insertColumn name (D.expandColumn sumRows a'') df- Just b'' -> case optA of- Nothing ->- D.insertColumn name (D.leftExpandColumn sumRows b'') df- Just a'' ->- let concatedColumns = D.concatColumnsEither a'' b''- in 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---- | 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/Permutation.hs
@@ -1,99 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE InstanceSigs #-}-{-# LANGUAGE OverloadedStrings #-}--module DataFrame.Operations.Permutation where--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 Data.Vector.Unboxed.Mutable as VUM--import Control.Exception (throw)-import Control.Monad.ST (runST)-import Data.Vector.Internal.Check (HasCallStack)-import DataFrame.Errors (DataFrameException (..))-import DataFrame.Internal.Column (Columnable, atIndicesStable)-import DataFrame.Internal.DataFrame (DataFrame (..))-import DataFrame.Internal.Expression (Expr (Col))-import DataFrame.Internal.Row (sortedIndexes', toRowVector)-import DataFrame.Operations.Core (columnNames, dimensions)-import System.Random (Random (randomR), RandomGen)---- | Sort order taken as a parameter by the 'sortBy' function.-data SortOrder where- Asc :: (Columnable a) => Expr a -> SortOrder- Desc :: (Columnable a) => Expr a -> SortOrder--instance Eq SortOrder where- (==) :: SortOrder -> SortOrder -> Bool- (==) (Asc _) (Asc _) = True- (==) (Desc _) (Desc _) = True- (==) _ _ = False--getSortColumnName :: SortOrder -> T.Text-getSortColumnName (Asc (Col n)) = n-getSortColumnName (Desc (Col n)) = n-getSortColumnName _ = error "Sorting on compound column"--mustFlipCompare :: SortOrder -> Bool-mustFlipCompare (Asc _) = True-mustFlipCompare (Desc _) = False--{- | O(k log n) Sorts the dataframe by a given row.--> sortBy Ascending ["Age"] df--}-sortBy ::- [SortOrder] ->- DataFrame ->- DataFrame-sortBy sortOrds df- | any (`notElem` columnNames df) names =- throw $- ColumnNotFoundException- (T.pack $ show $ names L.\\ columnNames df)- "sortBy"- (columnNames df)- | otherwise =- let- indexes = sortedIndexes' mustFlips (toRowVector names df)- in- df{columns = V.map (atIndicesStable indexes) (columns df)}- where- names = map getSortColumnName sortOrds- mustFlips = map mustFlipCompare sortOrds--shuffle ::- (RandomGen g) =>- g ->- DataFrame ->- DataFrame-shuffle pureGen df =- let- indexes = shuffledIndices pureGen (fst (dimensions df))- in- df{columns = V.map (atIndicesStable indexes) (columns df)}--shuffledIndices :: (HasCallStack, RandomGen g) => g -> Int -> VU.Vector Int-shuffledIndices pureGen k- | k < 0 = error $ "Vector index may not be a neative number: " <> show k- | k == 0 = VU.empty- | otherwise = shuffleVec pureGen- where- shuffleVec :: (RandomGen g) => g -> VU.Vector Int- shuffleVec g = runST $ do- vm <- VUM.generate k id- let (n, nGen) = randomR (1, k - 1) g- go vm n nGen- VU.unsafeFreeze vm-- go v (-1) _ = pure ()- go v 0 _ = pure ()- go v maxInd gen =- let- (n, nextGen) = randomR (1, maxInd) gen- in- VUM.swap v 0 n *> go (VUM.tail v) (maxInd - 1) nextGen
− src/DataFrame/Operations/Statistics.hs
@@ -1,389 +0,0 @@-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.Operations.Statistics where--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 Prelude as P--import Control.Exception (throw)-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 (..),- empty,- getColumn,- )-import DataFrame.Internal.Expression-import DataFrame.Internal.Interpreter-import DataFrame.Internal.Row (showValue, toAny)-import DataFrame.Internal.Statistics-import DataFrame.Internal.Types-import DataFrame.Operations.Core-import DataFrame.Operations.Subset (filterJust)-import DataFrame.Operations.Transformations (impute)-import Text.Printf (printf)-import Type.Reflection (typeRep)--{- | Show a frequency table for a categorical feaure.--__Examples:__--@-ghci> df <- D.readCsv ".\/data\/housing.csv"--ghci> D.frequencies "ocean_proximity" df------------------------------------------------------------------------ Statistic | <1H OCEAN | INLAND | ISLAND | NEAR BAY | NEAR OCEAN-----------------|-----------|--------|--------|----------|------------ Text | Any | Any | Any | Any | Any-----------------|-----------|--------|--------|----------|------------ Count | 9136 | 6551 | 5 | 2290 | 2658- Percentage (%) | 44.26% | 31.74% | 0.02% | 11.09% | 12.88%-@--}-frequencies :: forall a. (Columnable a) => Expr a -> DataFrame -> DataFrame-frequencies expr df =- let- counts = valueCounts expr 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 col =- L.foldl'- ( \d (col, k) ->- insertVector- (showValue @a col)- (V.fromList [toAny k, calculatePercentage counts k])- d- )- initDf- counts- in- case columnAsVector expr df of- Left err -> throw err- Right column -> freqs column---- | Calculates the mean of a given column as a standalone value.-mean ::- forall a. (Columnable a, Real a, VU.Unbox a) => Expr a -> DataFrame -> Double-mean (Col name) df = case _getColumnAsDouble name df of- Just xs -> meanDouble' xs- Nothing -> error "[INTERNAL ERROR] Column is non-numeric"-mean expr df = case interpret df expr of- Left e -> throw e- Right (TColumn col) -> case toUnboxedVector @a col of- Left e -> throw e- Right xs -> mean' xs--meanMaybe ::- forall a. (Columnable a, Real a) => Expr (Maybe a) -> DataFrame -> Double-meanMaybe (Col name) df =- (mean' . optionalToDoubleVector)- (either throw id (columnAsVector (Col @(Maybe a) name) df))-meanMaybe expr df = case interpret @(Maybe a) df expr of- Left e -> throw e- Right (TColumn col) -> case toVector @(Maybe a) col of- Left e -> throw e- Right xs -> (mean' . optionalToDoubleVector) xs---- | Calculates the median of a given column as a standalone value.-median ::- forall a. (Columnable a, Real a, VU.Unbox a) => Expr a -> DataFrame -> Double-median (Col name) df = case columnAsUnboxedVector (Col @a name) df of- Right xs -> median' xs- Left e -> throw e-median expr df = case interpret df expr of- Left e -> throw e- Right (TColumn col) -> case toUnboxedVector @a col of- Left e -> throw e- Right xs -> median' xs---- | Calculates the median of a given column (containing optional values) as a standalone value.-medianMaybe ::- forall a. (Columnable a, Real a) => Expr (Maybe a) -> DataFrame -> Double-medianMaybe (Col name) df =- (median' . optionalToDoubleVector)- (either throw id (columnAsVector (Col @(Maybe a) name) df))-medianMaybe expr df = case interpret @(Maybe a) df expr of- Left e -> throw e- Right (TColumn col) -> case toVector @(Maybe a) col of- Left e -> throw e- Right xs -> (median' . optionalToDoubleVector) xs---- | Calculates the nth percentile of a given column as a standalone value.-percentile ::- forall a.- (Columnable a, Real a, VU.Unbox a) => Int -> Expr a -> DataFrame -> Double-percentile n (Col name) df = case columnAsUnboxedVector (Col @a name) df of- Right xs -> percentile' n xs- Left e -> throw e-percentile n expr df = case interpret df expr of- Left e -> throw e- Right (TColumn col) -> case toUnboxedVector @a col of- Left e -> throw e- Right xs -> percentile' n xs---- | Calculates the nth percentile of a given column as a standalone value.-genericPercentile ::- forall a.- (Columnable a, Ord a) => Int -> Expr a -> DataFrame -> a-genericPercentile n (Col name) df = case columnAsVector (Col @a name) df of- Right xs -> percentileOrd' n xs- Left e -> throw e-genericPercentile n expr df = case interpret df expr of- Left e -> throw e- Right (TColumn col) -> case toVector @a col of- Left e -> throw e- Right xs -> percentileOrd' n xs---- | Calculates the standard deviation of a given column as a standalone value.-standardDeviation ::- forall a. (Columnable a, Real a, VU.Unbox a) => Expr a -> DataFrame -> Double-standardDeviation (Col name) df = case columnAsUnboxedVector (Col @a name) df of- Right xs -> (sqrt . variance') xs- Left e -> throw e-standardDeviation expr df = case interpret df expr of- Left e -> throw e- Right (TColumn col) -> case toUnboxedVector @a col of- Left e -> throw e- Right xs -> (sqrt . variance') xs---- | Calculates the skewness of a given column as a standalone value.-skewness ::- forall a. (Columnable a, Real a, VU.Unbox a) => Expr a -> DataFrame -> Double-skewness (Col name) df = case columnAsUnboxedVector (Col @a name) df of- Right xs -> skewness' xs- Left e -> throw e-skewness expr df = case interpret df expr of- Left e -> throw e- Right (TColumn col) -> case toUnboxedVector @a col of- Left e -> throw e- Right xs -> skewness' xs---- | Calculates the variance of a given column as a standalone value.-variance ::- forall a. (Columnable a, Real a, VU.Unbox a) => Expr a -> DataFrame -> Double-variance (Col name) df = case _getColumnAsDouble name df of- Just xs -> varianceDouble' xs- Nothing -> error "[INTERNAL ERROR] Column is non-numeric"-variance expr df = case interpret df expr of- Left e -> throw e- Right (TColumn col) -> case toUnboxedVector @a col of- Left e -> throw e- Right xs -> variance' xs---- | Calculates the inter-quartile range of a given column as a standalone value.-interQuartileRange ::- forall a. (Columnable a, Real a, VU.Unbox a) => Expr a -> DataFrame -> Double-interQuartileRange (Col name) df = case columnAsUnboxedVector (Col @a name) df of- Right xs -> interQuartileRange' xs- Left e -> throw e-interQuartileRange expr df = case interpret df expr of- Left e -> throw e- Right (TColumn col) -> case toUnboxedVector @a col of- Left e -> throw e- Right xs -> interQuartileRange' xs---- | 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- 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 sIntegral @a of- STrue -> Just (VU.map fromIntegral f)- SFalse -> case sFloating @a of- STrue -> Just (VU.map realToFrac f)- SFalse -> Nothing- Nothing ->- throw $- ColumnNotFoundException name "_getColumnAsDouble" (M.keys $ columnIndices df)- _ -> Nothing -- Return a type mismatch error here.-{-# INLINE _getColumnAsDouble #-}--optionalToDoubleVector :: (Real a) => V.Vector (Maybe a) -> VU.Vector Double-optionalToDoubleVector =- VU.fromList- . V.foldl'- (\acc e -> if isJust e then realToFrac (fromMaybe 0 e) : acc else acc)- []---- | Calculates the sum of a given column as a standalone value.-sum ::- forall a. (Columnable a, Num a) => Expr a -> DataFrame -> a-sum (Col name) df = case getColumn name df of- Nothing -> throw $ ColumnNotFoundException name "sum" (M.keys $ columnIndices df)- Just ((UnboxedColumn (column :: VU.Vector a'))) -> case testEquality (typeRep @a') (typeRep @a) of- Just Refl -> VG.sum column- Nothing -> 0- Just ((BoxedColumn (column :: V.Vector a'))) -> case testEquality (typeRep @a') (typeRep @a) of- Just Refl -> VG.sum column- Nothing -> 0- Just ((OptionalColumn (column :: V.Vector (Maybe a')))) -> case testEquality (typeRep @a') (typeRep @a) of- Just Refl -> VG.sum (VG.map (fromMaybe 0) column)- Nothing -> 0-sum expr df = case interpret df expr of- Left e -> throw e- Right (TColumn xs) -> case toVector @a @V.Vector xs of- Left e -> throw e- Right xs -> VG.sum xs--{- | /O(n)/ Impute missing values in a column using a derived scalar.--Given--* an expression @f :: 'Expr' b -> 'Expr' b@ that, when interpreted over a- non-nullable column, produces the same value in every row (for example a- mean, median, or other aggregate), and-* a nullable column @'Expr' ('Maybe' b)@--this function:--1. Drops all @Nothing@ values from the target column.-2. Interprets @f@ on the remaining non-null values.-3. Checks that the resulting column contains a single repeated value.-4. Uses that value to impute all @Nothing@s in the original column.--==== __Throws__--* 'DataFrameException' - if the column does not exist, is empty,--==== __Example__-@->>> :set -XOverloadedStrings->>> import qualified DataFrame as D->>> let df =-... D.fromNamedColumns-... [ ("age", D.fromList [Just 10, Nothing, Just 20 :: Maybe Int]) ]->>>->>> -- Impute missing ages with the mean of the observed ages->>> D.imputeWith F.mean "age" df--- age--- ------- 10--- 15--- 20-@--}-imputeWith ::- forall b.- (Columnable b) =>- (Expr b -> Expr b) ->- Expr (Maybe b) ->- DataFrame ->- DataFrame-imputeWith f col@(Col columnName) df = case interpret @b (filterJust columnName df) (f (Col @b columnName)) of- Left e -> throw e- Right (TColumn value) -> case headColumn @b value of- Left e -> throw e- Right h ->- if all (== h) (toList @b value)- then impute col h df- else error "Impute expression returned more than one value"-imputeWith _ _ df = df--applyStatistic ::- (VU.Vector Double -> Double) -> T.Text -> DataFrame -> Maybe Double-applyStatistic f name df = apply =<< _getColumnAsDouble name (filterJust name df)- where- apply col =- let- res = f col- in- if isNaN res then Nothing else pure res-{-# INLINE applyStatistic #-}--applyStatistics ::- (VU.Vector Double -> VU.Vector Double) ->- T.Text ->- DataFrame ->- Maybe (VU.Vector Double)-applyStatistics f name df = fmap f (_getColumnAsDouble name (filterJust name df))---- | Descriptive 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- count = fromIntegral . numElements <$> getColumn name df- quantiles = applyStatistics (quantiles' (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- doubleColumn col = _getColumnAsDouble col (filterJust col df)- in- [ count- , mean' <$> doubleColumn name- , min'- , quartile1- , median'- , quartile3- , max'- , sqrt . variance' <$> doubleColumn name- , iqr- , skewness' <$> doubleColumn name- ]---- | Round a @Double@ to Specified Precision-roundTo :: Int -> Double -> Double-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)
− src/DataFrame/Operations/Subset.hs
@@ -1,469 +0,0 @@-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.Operations.Subset where--import qualified Data.List as L-import qualified Data.Map as M-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 Prelude--import Control.Exception (throw)-import Data.Function ((&))-import Data.Maybe (- fromJust,- fromMaybe,- isJust,- isNothing,- )-import Data.Type.Equality (TestEquality (..))-import DataFrame.Errors (- DataFrameException (..),- TypeErrorContext (..),- )-import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (- DataFrame (..),- empty,- getColumn,- )-import DataFrame.Internal.Expression-import DataFrame.Internal.Interpreter-import DataFrame.Operations.Core-import DataFrame.Operations.Transformations (apply)-import System.Random-import Type.Reflection-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)}- 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)- }- 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)- }- 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)}- 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)- }- where- (r, c) = dataframeDimensions d- n' = clip (end - start) 0 r--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--}-filter ::- forall a.- (Columnable a) =>- -- | Column to filter by- Expr a ->- -- | Filter condition- (a -> Bool) ->- -- | Dataframe to filter- DataFrame ->- DataFrame-filter (Col filterColumnName) condition df = case getColumn filterColumnName df of- Nothing ->- throw $- ColumnNotFoundException filterColumnName "filter" (M.keys $ 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-filter expr condition df =- let- (TColumn col) = case interpret @a df (normalize expr) of- Left e -> throw e- Right c -> c- indexes = case findIndices condition col of- Right ixs -> ixs- Left e -> throw e- c' = snd $ dataframeDimensions df- in- df- { columns = V.map (atIndicesStable indexes) (columns df)- , dataframeDimensions = (VU.length indexes, c')- }--filterByVector ::- forall a b v.- (VG.Vector v b, VG.Vector v Int, 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 = VG.convert (VG.findIndices condition column)- in- df- { columns = V.map (atIndicesStable ixs) (columns df)- , dataframeDimensions = (VG.length ixs, snd (dataframeDimensions df))- }--{- | O(k) a version of filter where the predicate comes first.--> filterBy even "x" df--}-filterBy :: (Columnable a) => (a -> Bool) -> Expr a -> DataFrame -> DataFrame-filterBy = flip filter--{- | O(k) filters the dataframe with a boolean expression.--> filterWhere (F.col @Int x + F.col y F.> 5) df--}-filterWhere :: Expr Bool -> DataFrame -> DataFrame-filterWhere expr df =- let- (TColumn col) = case interpret @Bool df (normalize expr) of- Left e -> throw e- Right c -> c- indexes = case findIndices id col of- Right ixs -> ixs- Left e -> throw e- c' = snd $ dataframeDimensions df- in- df- { columns = V.map (atIndicesStable indexes) (columns df)- , dataframeDimensions = (VU.length indexes, c')- }--{- | O(k) removes all rows with `Nothing` in a given column from the dataframe.--> filterJust "col" df--}-filterJust :: T.Text -> DataFrame -> DataFrame-filterJust name df = case getColumn name df of- Nothing ->- throw $ ColumnNotFoundException name "filterJust" (M.keys $ columnIndices df)- Just column@(OptionalColumn (col :: V.Vector (Maybe a))) -> filter (Col @(Maybe a) name) isJust df & apply @(Maybe a) fromJust name- Just column -> df--{- | O(k) returns all rows with `Nothing` in a give column.--> filterNothing "col" df--}-filterNothing :: T.Text -> DataFrame -> DataFrame-filterNothing name df = case getColumn name df of- Nothing ->- throw $ ColumnNotFoundException name "filterNothing" (M.keys $ columnIndices df)- Just (OptionalColumn (col :: V.Vector (Maybe a))) -> filter (Col @(Maybe a) name) isNothing df- _ -> df--{- | O(n * k) removes all rows with `Nothing` from the dataframe.--> filterAllJust df--}-filterAllJust :: DataFrame -> DataFrame-filterAllJust df = foldr filterJust df (columnNames df)--{- | O(n * k) keeps any row with a null value.--> filterAllNothing df--}-filterAllNothing :: DataFrame -> DataFrame-filterAllNothing df = foldr filterNothing df (columnNames df)--{- | O(k) cuts the dataframe in a cube of size (a, b) where- a is the length and b is the width.--> cube (10, 5) df--}-cube :: (Int, Int) -> DataFrame -> DataFrame-cube (length, width) = take length . selectBy [ColumnIndexRange (0, width - 1)]--{- | O(n) Selects a number of columns in a given dataframe.--> select ["name", "age"] df--}-select ::- [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- where- addKeyValue d k = fromMaybe df $ do- col <- getColumn k df- pure $ insertColumn k col d--data SelectionCriteria- = ColumnProperty (Column -> Bool)- | ColumnNameProperty (T.Text -> Bool)- | ColumnTextRange (T.Text, T.Text)- | ColumnIndexRange (Int, Int)- | ColumnName T.Text--{- | Criteria for selecting a column by name.--> selectBy [byName "Age"] df--equivalent to:--> select ["Age"] df--}-byName :: T.Text -> SelectionCriteria-byName = ColumnName--{- | Criteria for selecting columns whose property satisfies given predicate.--> selectBy [byProperty isNumeric] df--}-byProperty :: (Column -> Bool) -> SelectionCriteria-byProperty = ColumnProperty--{- | Criteria for selecting columns whose name satisfies given predicate.--> selectBy [byNameProperty (T.isPrefixOf "weight")] df--}-byNameProperty :: (T.Text -> Bool) -> SelectionCriteria-byNameProperty = ColumnNameProperty--{- | Criteria for selecting columns whose names are in the given lexicographic range (inclusive).--> selectBy [byNameRange ("a", "c")] df--}-byNameRange :: (T.Text, T.Text) -> SelectionCriteria-byNameRange = ColumnTextRange--{- | Criteria for selecting columns whose indices are in the given (inclusive) range.--> selectBy [byIndexRange (0, 5)] df--}-byIndexRange :: (Int, Int) -> SelectionCriteria-byIndexRange = ColumnIndexRange---- | O(n) select columns by column predicate name.-selectBy :: [SelectionCriteria] -> DataFrame -> DataFrame-selectBy xs df = select finalSelection df- where- finalSelection = Prelude.filter (`S.member` columnsWithProperties) (columnNames df)- columnsWithProperties = S.fromList (L.foldl' columnWithProperty [] xs)- columnWithProperty acc (ColumnName name) = acc ++ [name]- columnWithProperty acc (ColumnNameProperty f) = acc ++ L.filter f (columnNames df)- columnWithProperty acc (ColumnTextRange (from, to)) =- acc- ++ reverse- (Prelude.dropWhile (to /=) $ reverse $ dropWhile (from /=) (columnNames df))- columnWithProperty acc (ColumnIndexRange (from, to)) = acc ++ Prelude.take (to - from + 1) (Prelude.drop from (columnNames df))- columnWithProperty acc (ColumnProperty f) =- acc- ++ map fst (L.filter (\(k, v) -> v `elem` ixs) (M.toAscList (columnIndices df)))- where- ixs = V.ifoldl' (\acc i c -> if f c then i : acc else acc) [] (columns df)--{- | O(n) inverse of select--> exclude ["Name"] df--}-exclude ::- [T.Text] ->- DataFrame ->- DataFrame-exclude cs df =- let keysToKeep = columnNames df L.\\ cs- in select keysToKeep df--{- | Sample a dataframe. The double parameter must be between 0 and 1 (inclusive).--==== __Example__-@-ghci> import System.Random-ghci> D.sample (mkStdGen 137) 0.1 df--@--}-sample :: (RandomGen g) => g -> Double -> DataFrame -> DataFrame-sample pureGen p df =- let- rand = generateRandomVector pureGen (fst (dataframeDimensions df))- in- df- & insertUnboxedVector "__rand__" rand- & filterWhere- ( Binary- ( MkBinaryOp- { binaryFn = (>=)- , binaryName = "geq"- , binarySymbol = Just ">="- , binaryCommutative = False- , binaryPrecedence = 1- }- )- (Col @Double "__rand__")- (Lit (1 - p))- )- & exclude ["__rand__"]--{- | Split a dataset into two. The first in the tuple gets a sample of p (0 <= p <= 1) and the second gets (1 - p). This is useful for creating test and train splits.--==== __Example__-@-ghci> import System.Random-ghci> D.randomSplit (mkStdGen 137) 0.9 df--@--}-randomSplit ::- (RandomGen g) => g -> Double -> DataFrame -> (DataFrame, DataFrame)-randomSplit pureGen p df =- let- rand = generateRandomVector pureGen (fst (dataframeDimensions df))- withRand = df & insertUnboxedVector "__rand__" rand- in- ( withRand- & filterWhere- ( Binary- ( MkBinaryOp- { binaryFn = (<=)- , binaryName = "leq"- , binarySymbol = Just "<="- , binaryCommutative = False- , binaryPrecedence = 1- }- )- (Col @Double "__rand__")- (Lit p)- )- & exclude ["__rand__"]- , withRand- & filterWhere- ( Binary- ( MkBinaryOp- { binaryFn = (>)- , binaryName = "gt"- , binarySymbol = Just ">"- , binaryCommutative = False- , binaryPrecedence = 1- }- )- (Col @Double "__rand__")- (Lit p)- )- & exclude ["__rand__"]- )--{- | Creates n folds of a dataframe.--==== __Example__-@-ghci> import System.Random-ghci> D.kFolds (mkStdGen 137) 5 df--@--}-kFolds :: (RandomGen g) => g -> Int -> DataFrame -> [DataFrame]-kFolds pureGen folds df =- let- rand = generateRandomVector pureGen (fst (dataframeDimensions df))- withRand = df & insertUnboxedVector "__rand__" rand- partitionSize = 1 / fromIntegral folds- singleFold n d =- d- & filterWhere- ( Binary- ( MkBinaryOp- { binaryFn = (>=)- , binaryName = "geq"- , binarySymbol = Just ">="- , binaryCommutative = False- , binaryPrecedence = 1- }- )- (Col @Double "__rand__")- (Lit (fromIntegral n * partitionSize))- )- go (-1) _ = []- go n d =- let- d' = singleFold n d- d'' =- d- & filterWhere- ( Binary- ( MkBinaryOp- { binaryFn = (<)- , binaryName = "lt"- , binarySymbol = Just "<"- , binaryCommutative = False- , binaryPrecedence = 1- }- )- (Col @Double "__rand__")- (Lit (fromIntegral n * partitionSize))- )- in- d' : go (n - 1) d''- in- map (exclude ["__rand__"]) (go (folds - 1) withRand)--generateRandomVector :: (RandomGen g) => g -> Int -> VU.Vector Double-generateRandomVector pureGen k = VU.fromList $ go pureGen k- where- go g 0 = []- go g n =- let- (v, g') = uniformR (0 :: Double, 1 :: Double) g- in- v : go g' (n - 1)
− src/DataFrame/Operations/Transformations.hs
@@ -1,207 +0,0 @@-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.Operations.Transformations where--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 Control.Exception (throw)-import Data.Maybe-import DataFrame.Errors (DataFrameException (..), TypeErrorContext (..))-import DataFrame.Internal.Column (- Column (..),- Columnable,- TypedColumn (..),- ifoldrColumn,- imapColumn,- mapColumn,- )-import DataFrame.Internal.DataFrame (DataFrame (..), getColumn)-import DataFrame.Internal.Expression-import DataFrame.Internal.Interpreter-import DataFrame.Operations.Core---- | 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-apply f columnName d = case safeApply f columnName d of- Left (TypeMismatchException context) ->- throw $ TypeMismatchException (context{callingFunctionName = Just "apply"})- 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-safeApply f columnName d = case getColumn columnName d of- Nothing -> Left $ ColumnNotFoundException columnName "apply" (M.keys $ columnIndices d)- Just column -> do- column' <- mapColumn f column- pure $ insertColumn columnName column' d--{- | O(k) Apply a function to an expression in a dataframe and-add the result into `alias` column.--}-derive :: forall a. (Columnable a) => T.Text -> Expr a -> DataFrame -> DataFrame-derive name expr df = case interpret @a df (normalize expr) of- Left e -> throw e- Right (TColumn value) ->- (insertColumn name value df)- { derivingExpressions = M.insert name (UExpr expr) (derivingExpressions df)- }--{- | O(k) Apply a function to an expression in a dataframe and-add the result into `alias` column but--==== __Examples__-->>> (z, df') = deriveWithExpr "z" (F.col @Int "x" + F.col "y") df->>> filterWhere (z .>= 50)--}-deriveWithExpr ::- forall a. (Columnable a) => T.Text -> Expr a -> DataFrame -> (Expr a, DataFrame)-deriveWithExpr name expr df = case interpret @a df (normalize expr) of- Left e -> throw e- Right (TColumn value) -> (Col name, insertColumn name value df)--deriveMany :: [NamedExpr] -> DataFrame -> DataFrame-deriveMany exprs df =- let- f (name, UExpr (expr :: Expr a)) d =- case interpret @a df expr of- Left e -> throw e- Right (TColumn value) -> insertColumn name value d- in- fold f exprs 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-applyMany f names df = L.foldl' (flip (apply f)) df names---- | O(k) Convenience function that applies to an int column.-applyInt ::- (Columnable b) =>- -- | function to apply- (Int -> b) ->- -- | Column name- T.Text ->- -- | DataFrame to apply operation to- DataFrame ->- DataFrame-applyInt = apply---- | O(k) Convenience function that applies to an double column.-applyDouble ::- (Columnable b) =>- -- | function to apply- (Double -> b) ->- -- | Column name- 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 (<20) "Age" (const "Gen-Z") "Generation" df--}-applyWhere ::- forall a b.- (Columnable a, Columnable b) =>- -- | Filter condition- (a -> Bool) ->- -- | Criterion Column- T.Text ->- -- | function to apply- (b -> b) ->- -- | Column name- T.Text ->- -- | DataFrame to apply operation to- DataFrame ->- DataFrame-applyWhere condition filterColumnName f columnName df = case getColumn filterColumnName df of- Nothing ->- throw $- ColumnNotFoundException- filterColumnName- "applyWhere"- (M.keys $ columnIndices df)- Just column -> case ifoldrColumn- (\i val acc -> if condition val then V.cons i acc else acc)- V.empty- column of- Left e -> throw e- Right 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-applyAtIndex i f columnName df = case getColumn columnName df of- Nothing ->- throw $- ColumnNotFoundException columnName "applyAtIndex" (M.keys $ columnIndices df)- Just column -> case imapColumn (\index value -> if index == i then f value else value) column of- Left e -> throw e- Right column' -> insertColumn columnName column' df---- | Replace all instances of `Nothing` in a column with the given value.-impute ::- forall b.- (Columnable b) =>- Expr (Maybe b) ->- b ->- DataFrame ->- DataFrame-impute (Col columnName) value df = case getColumn columnName df of- Nothing ->- throw $ ColumnNotFoundException columnName "impute" (M.keys $ 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: " ++ T.unpack columnName-impute _ _ df = df
− src/DataFrame/Operations/Typing.hs
@@ -1,215 +0,0 @@-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.Operations.Typing where--import qualified Data.Text as T-import qualified Data.Vector as V--import Data.Maybe (fromMaybe)-import qualified Data.Proxy as P-import Data.Time-import Data.Type.Equality (TestEquality (..), type (:~:) (Refl))-import DataFrame.Internal.Column (Column (..), fromVector)-import DataFrame.Internal.DataFrame (DataFrame (..))-import DataFrame.Internal.Parsing-import DataFrame.Internal.Schema-import Text.Read-import Type.Reflection (typeRep)--type DateFormat = String--parseDefaults :: [T.Text] -> Int -> Bool -> DateFormat -> DataFrame -> DataFrame-parseDefaults missing n safeRead dateFormat df = df{columns = V.map (parseDefault missing n safeRead dateFormat) (columns df)}--parseDefault :: [T.Text] -> Int -> Bool -> DateFormat -> Column -> Column-parseDefault missing n safeRead dateFormat (BoxedColumn (c :: V.Vector a)) =- case (typeRep @a) `testEquality` (typeRep @T.Text) of- Nothing -> case (typeRep @a) `testEquality` (typeRep @String) of- Just Refl -> parseFromExamples missing n safeRead dateFormat (V.map T.pack c)- Nothing -> BoxedColumn c- Just Refl -> parseFromExamples missing n safeRead dateFormat c-parseDefault missing n safeRead dateFormat (OptionalColumn (c :: V.Vector (Maybe a))) =- case (typeRep @a) `testEquality` (typeRep @T.Text) of- Nothing -> case (typeRep @a) `testEquality` (typeRep @String) of- Just Refl ->- parseFromExamples- missing- n- safeRead- dateFormat- (V.map (T.pack . fromMaybe "") c)- Nothing -> BoxedColumn c- Just Refl -> parseFromExamples missing n safeRead dateFormat (V.map (fromMaybe "") c)-parseDefault _ _ _ _ column = column--parseFromExamples ::- [T.Text] -> Int -> Bool -> DateFormat -> V.Vector T.Text -> Column-parseFromExamples missing n safeRead dateFormat cols =- let- converter = if safeRead then convertNullish missing else convertOnlyEmpty- examples = V.map converter (V.take n cols)- asMaybeText = V.map converter cols- in- case makeParsingAssumption dateFormat examples of- BoolAssumption -> handleBoolAssumption asMaybeText- IntAssumption -> handleIntAssumption asMaybeText- DoubleAssumption -> handleDoubleAssumption asMaybeText- TextAssumption -> handleTextAssumption asMaybeText- DateAssumption -> handleDateAssumption dateFormat asMaybeText- NoAssumption -> handleNoAssumption dateFormat asMaybeText--handleBoolAssumption :: V.Vector (Maybe T.Text) -> Column-handleBoolAssumption asMaybeText- | parsableAsBool =- maybe (fromVector asMaybeBool) fromVector (sequenceA asMaybeBool)- | otherwise = maybe (fromVector asMaybeText) fromVector (sequenceA asMaybeText)- where- asMaybeBool = V.map (>>= readBool) asMaybeText- parsableAsBool = vecSameConstructor asMaybeText asMaybeBool--handleIntAssumption :: V.Vector (Maybe T.Text) -> Column-handleIntAssumption asMaybeText- | parsableAsInt =- maybe (fromVector asMaybeInt) fromVector (sequenceA asMaybeInt)- | parsableAsDouble =- maybe (fromVector asMaybeDouble) fromVector (sequenceA asMaybeDouble)- | otherwise = maybe (fromVector asMaybeText) fromVector (sequenceA asMaybeText)- where- asMaybeInt = V.map (>>= readInt) asMaybeText- asMaybeDouble = V.map (>>= readDouble) asMaybeText- parsableAsInt =- vecSameConstructor asMaybeText asMaybeInt- && vecSameConstructor asMaybeText asMaybeDouble- parsableAsDouble = vecSameConstructor asMaybeText asMaybeDouble--handleDoubleAssumption :: V.Vector (Maybe T.Text) -> Column-handleDoubleAssumption asMaybeText- | parsableAsDouble =- maybe (fromVector asMaybeDouble) fromVector (sequenceA asMaybeDouble)- | otherwise = maybe (fromVector asMaybeText) fromVector (sequenceA asMaybeText)- where- asMaybeDouble = V.map (>>= readDouble) asMaybeText- parsableAsDouble = vecSameConstructor asMaybeText asMaybeDouble--handleDateAssumption :: DateFormat -> V.Vector (Maybe T.Text) -> Column-handleDateAssumption dateFormat asMaybeText- | parsableAsDate =- maybe (fromVector asMaybeDate) fromVector (sequenceA asMaybeDate)- | otherwise = maybe (fromVector asMaybeText) fromVector (sequenceA asMaybeText)- where- asMaybeDate = V.map (>>= parseTimeOpt dateFormat) asMaybeText- parsableAsDate = vecSameConstructor asMaybeText asMaybeDate--handleTextAssumption :: V.Vector (Maybe T.Text) -> Column-handleTextAssumption asMaybeText = maybe (fromVector asMaybeText) fromVector (sequenceA asMaybeText)--handleNoAssumption :: DateFormat -> V.Vector (Maybe T.Text) -> Column-handleNoAssumption dateFormat asMaybeText- -- No need to check for null values. If we are in this condition, that- -- means that the examples consisted only of null values, so we can- -- confidently know that this column must be an OptionalColumn- | V.all (== Nothing) asMaybeText = fromVector asMaybeText- | parsableAsBool = fromVector asMaybeBool- | parsableAsInt = fromVector asMaybeInt- | parsableAsDouble = fromVector asMaybeDouble- | parsableAsDate = fromVector asMaybeDate- | otherwise = fromVector asMaybeText- where- asMaybeBool = V.map (>>= readBool) asMaybeText- asMaybeInt = V.map (>>= readInt) asMaybeText- asMaybeDouble = V.map (>>= readDouble) asMaybeText- asMaybeDate = V.map (>>= parseTimeOpt dateFormat) asMaybeText- parsableAsBool = vecSameConstructor asMaybeText asMaybeBool- parsableAsInt =- vecSameConstructor asMaybeText asMaybeInt- && vecSameConstructor asMaybeText asMaybeDouble- parsableAsDouble = vecSameConstructor asMaybeText asMaybeDouble- parsableAsDate = vecSameConstructor asMaybeText asMaybeDate--convertNullish :: [T.Text] -> T.Text -> Maybe T.Text-convertNullish missing v = if isNullish v || v `elem` missing then Nothing else Just v--convertOnlyEmpty :: T.Text -> Maybe T.Text-convertOnlyEmpty v = if v == "" then Nothing else Just v--parseTimeOpt :: DateFormat -> T.Text -> Maybe Day-parseTimeOpt dateFormat s =- parseTimeM {- Accept leading/trailing whitespace -}- True- defaultTimeLocale- dateFormat- (T.unpack s)--unsafeParseTime :: DateFormat -> T.Text -> Day-unsafeParseTime dateFormat s =- parseTimeOrError {- Accept leading/trailing whitespace -}- True- defaultTimeLocale- dateFormat- (T.unpack s)--hasNullValues :: (Eq a) => V.Vector (Maybe a) -> Bool-hasNullValues = V.any (== Nothing)--vecSameConstructor :: V.Vector (Maybe a) -> V.Vector (Maybe b) -> Bool-vecSameConstructor xs ys = (V.length xs == V.length ys) && V.and (V.zipWith hasSameConstructor xs ys)- where- hasSameConstructor :: Maybe a -> Maybe b -> Bool- hasSameConstructor (Just _) (Just _) = True- hasSameConstructor Nothing Nothing = True- hasSameConstructor _ _ = False--makeParsingAssumption ::- DateFormat -> V.Vector (Maybe T.Text) -> ParsingAssumption-makeParsingAssumption dateFormat asMaybeText- -- All the examples are "NA", "Null", "", so we can't make any shortcut- -- assumptions and just have to go the long way.- | V.all (== Nothing) asMaybeText = NoAssumption- -- After accounting for nulls, parsing for Ints and Doubles results in the- -- same corresponding positions of Justs and Nothings, so we assume- -- that the best way to parse is Int- | vecSameConstructor asMaybeText asMaybeBool = BoolAssumption- | vecSameConstructor asMaybeText asMaybeInt- && vecSameConstructor asMaybeText asMaybeDouble =- IntAssumption- -- After accounting for nulls, the previous condition fails, so some (or none) can be parsed as Ints- -- and some can be parsed as Doubles, so we make the assumpotion of doubles.- | vecSameConstructor asMaybeText asMaybeDouble = DoubleAssumption- -- After accounting for nulls, parsing for Dates results in the same corresponding- -- positions of Justs and Nothings, so we assume that the best way to parse is Date.- | vecSameConstructor asMaybeText asMaybeDate = DateAssumption- | otherwise = TextAssumption- where- asMaybeBool = V.map (>>= readBool) asMaybeText- asMaybeInt = V.map (>>= readInt) asMaybeText- asMaybeDouble = V.map (>>= readDouble) asMaybeText- asMaybeDate = V.map (>>= parseTimeOpt dateFormat) asMaybeText--data ParsingAssumption- = BoolAssumption- | IntAssumption- | DoubleAssumption- | DateAssumption- | NoAssumption- | TextAssumption--parseWithTypes :: [SchemaType] -> DataFrame -> DataFrame-parseWithTypes ts df = df{columns = go 0 ts (columns df)}- where- go :: Int -> [SchemaType] -> V.Vector Column -> V.Vector Column- go n [] xs = xs- go n (t : rest) xs- | n >= V.length xs = xs- | otherwise =- go (n + 1) rest (V.update xs (V.fromList [(n, asType t (xs V.! n))]))- asType :: SchemaType -> Column -> Column- asType (SType (_ :: P.Proxy a)) c@(BoxedColumn (col :: V.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> c- Nothing -> case testEquality (typeRep @T.Text) (typeRep @b) of- Just Refl -> fromVector (V.map ((readMaybe @a) . T.unpack) col)- Nothing -> fromVector (V.map ((readMaybe @a) . show) col)- asType _ c = c
− src/DataFrame/Operators.hs
@@ -1,161 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE OverloadedStrings #-}--module DataFrame.Operators where--import Data.Function ((&))-import qualified Data.Text as T-import DataFrame.Internal.Column (Columnable)-import DataFrame.Internal.Expression (- BinaryOp (- MkBinaryOp,- binaryCommutative,- binaryFn,- binaryName,- binaryPrecedence,- binarySymbol- ),- Expr (Binary, Col, If, Lit),- NamedExpr,- UExpr (UExpr),- )--infix 8 .^^-infix 4 .==, .<, .<=, .>=, .>, ./=-infixr 3 .&&-infixr 2 .||-infixr 0 .=--(|>) :: a -> (a -> b) -> b-(|>) = (&)--as :: (Columnable a) => Expr a -> T.Text -> NamedExpr-as expr name = (name, UExpr expr)--name :: (Show a) => Expr a -> T.Text-name (Col n) = n-name other =- error $- "You must call `name` on a column reference. Not the expression: " ++ show other--col :: (Columnable a) => T.Text -> Expr a-col = Col--ifThenElse :: (Columnable a) => Expr Bool -> Expr a -> Expr a -> Expr a-ifThenElse = If--lit :: (Columnable a) => a -> Expr a-lit = Lit--(.=) :: (Columnable a) => T.Text -> Expr a -> NamedExpr-(.=) = flip as--(.==) :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool-(.==) =- Binary- ( MkBinaryOp- { binaryFn = (==)- , binaryName = "eq"- , binarySymbol = Just "=="- , binaryCommutative = True- , binaryPrecedence = 4- }- )--(./=) :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool-(./=) =- Binary- ( MkBinaryOp- { binaryFn = (/=)- , binaryName = "neq"- , binarySymbol = Just "/="- , binaryCommutative = True- , binaryPrecedence = 4- }- )--(.<) :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool-(.<) =- Binary- ( MkBinaryOp- { binaryFn = (<)- , binaryName = "lt"- , binarySymbol = Just "<"- , binaryCommutative = False- , binaryPrecedence = 4- }- )--(.>) :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool-(.>) =- Binary- ( MkBinaryOp- { binaryFn = (>)- , binaryName = "gt"- , binarySymbol = Just ">"- , binaryCommutative = False- , binaryPrecedence = 4- }- )--(.<=) :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool-(.<=) =- Binary- ( MkBinaryOp- { binaryFn = (<=)- , binaryName = "leq"- , binarySymbol = Just "<="- , binaryCommutative = False- , binaryPrecedence = 4- }- )--(.>=) :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool-(.>=) =- Binary- ( MkBinaryOp- { binaryFn = (>=)- , binaryName = "geq"- , binarySymbol = Just ">="- , binaryCommutative = False- , binaryPrecedence = 4- }- )--(.&&) :: Expr Bool -> Expr Bool -> Expr Bool-(.&&) =- Binary- ( MkBinaryOp- { binaryFn = (&&)- , binaryName = "and"- , binarySymbol = Just "&&"- , binaryCommutative = True- , binaryPrecedence = 3- }- )--(.||) :: Expr Bool -> Expr Bool -> Expr Bool-(.||) =- Binary- ( MkBinaryOp- { binaryFn = (||)- , binaryName = "or"- , binarySymbol = Just "||"- , binaryCommutative = True- , binaryPrecedence = 2- }- )--(.^^) :: (Columnable a, Num a) => Expr a -> Int -> Expr a-(.^^) expr i =- Binary- ( MkBinaryOp- { binaryFn = (^)- , binaryName = "pow"- , binarySymbol = Just "^"- , binaryCommutative = False- , binaryPrecedence = 8- }- )- expr- (Lit i)
− src/DataFrame/Synthesis.hs
@@ -1,484 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE UndecidableInstances #-}--module DataFrame.Synthesis where--import qualified DataFrame.Functions as F-import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (- DataFrame (..),- )-import DataFrame.Internal.Expression (- Expr (..),- eSize,- )-import DataFrame.Internal.Interpreter (interpret)-import DataFrame.Internal.Statistics-import DataFrame.Operations.Core (columnAsDoubleVector)-import qualified DataFrame.Operations.Statistics as Stats-import DataFrame.Operations.Subset (exclude)--import Control.Exception (throw)-import Data.Containers.ListUtils-import Data.Function-import qualified Data.List as L-import qualified Data.Map as M-import Data.Maybe (listToMaybe)-import qualified Data.Set as S-import qualified Data.Text as T-import Data.Type.Equality-import qualified Data.Vector.Unboxed as VU-import qualified DataFrame.Operations.Core as D-import DataFrame.Operators-import Debug.Trace (trace)-import Type.Reflection (typeRep)--generateConditions ::- TypedColumn Double -> [Expr Bool] -> [Expr Double] -> DataFrame -> [Expr Bool]-generateConditions labels conds ps df =- let- newConds =- [ p .<= q- | p <- filter (not . isLiteral) ps- , q <- ps- , p /= q- ]- ++ [ F.not p- | p <- conds- ]- expandedConds =- conds- ++ newConds- ++ [p .&& q | p <- newConds, q <- conds, p /= q]- ++ [p .|| q | p <- newConds, q <- conds, p /= q]- in- pickTopNBool df labels (deduplicate df expandedConds)--generatePrograms ::- Bool ->- [Expr Bool] ->- [Expr Double] ->- [Expr Double] ->- [Expr Double] ->- [Expr Double]-generatePrograms _ _ vars' constants [] = vars' ++ constants-generatePrograms includeConds conds vars constants ps =- let- existingPrograms = ps ++ vars ++ constants- in- existingPrograms- ++ [ transform p- | p <- ps ++ vars- , Prelude.not (isConditional p)- , transform <-- [ sqrt- , abs- , log . (+ Lit 1)- , exp- , sin- , cos- , F.relu- , signum- ]- ]- ++ [ F.pow p i- | p <- existingPrograms- , Prelude.not (isConditional p)- , i <- [2 .. 6]- ]- ++ [ p + q- | (i, p) <- zip [0 ..] existingPrograms- , (j, q) <- zip [0 ..] existingPrograms- , Prelude.not (isLiteral p && isLiteral q)- , Prelude.not (isConditional p || isConditional q)- , i >= j- ]- ++ [ p - q- | (i, p) <- zip [0 ..] existingPrograms- , (j, q) <- zip [0 ..] existingPrograms- , Prelude.not (isLiteral p && isLiteral q)- , Prelude.not (isConditional p || isConditional q)- , i /= j- ]- ++ ( if includeConds- then- [ F.min p q- | (i, p) <- zip [0 ..] existingPrograms- , (j, q) <- zip [0 ..] existingPrograms- , Prelude.not (isLiteral p && isLiteral q)- , Prelude.not (isConditional p || isConditional q)- , p /= q- , i > j- ]- ++ [ F.max p q- | (i, p) <- zip [0 ..] existingPrograms- , (j, q) <- zip [0 ..] existingPrograms- , Prelude.not (isLiteral p && isLiteral q)- , Prelude.not (isConditional p || isConditional q)- , p /= q- , i > j- ]- ++ [ F.ifThenElse cond r s- | cond <- conds- , r <- existingPrograms- , s <- existingPrograms- , Prelude.not (isConditional r || isConditional s)- , r /= s- ]- else []- )- ++ [ p * q- | (i, p) <- zip [0 ..] existingPrograms- , (j, q) <- zip [0 ..] existingPrograms- , Prelude.not (isLiteral p && isLiteral q)- , Prelude.not (isConditional p || isConditional q)- , i >= j- ]- ++ [ p / q- | p <- existingPrograms- , q <- existingPrograms- , Prelude.not (isLiteral p && isLiteral q)- , Prelude.not (isConditional p || isConditional q)- , p /= q- ]--isLiteral :: Expr a -> Bool-isLiteral (Lit _) = True-isLiteral _ = False--isConditional :: Expr a -> Bool-isConditional (If{}) = True-isConditional _ = False--deduplicate ::- forall a.- (Columnable a) =>- DataFrame ->- [Expr a] ->- [(Expr a, TypedColumn a)]-deduplicate df = go S.empty . nubOrd . L.sortBy (\e1 e2 -> compare (eSize e1) (eSize e2))- where- go _ [] = []- go seen (x : xs)- | hasInvalid = go seen xs- | S.member res seen = go seen xs- | otherwise = (x, res) : go (S.insert res seen) xs- where- res = case interpret @a df x of- Left e -> throw e- Right v -> v- hasInvalid = case res of- (TColumn (UnboxedColumn (col :: VU.Vector b))) -> case testEquality (typeRep @Double) (typeRep @b) of- Just Refl -> VU.any (\n -> isNaN n || isInfinite n) col- Nothing -> False- _ -> False---- | Checks if two programs generate the same outputs given all the same inputs.-equivalent :: DataFrame -> Expr Double -> Expr Double -> Bool-equivalent df p1 p2 = case (==) <$> interpret df p1 <*> interpret df p2 of- Left e -> throw e- Right v -> v--synthesizeFeatureExpr ::- -- | Target expression- T.Text ->- BeamConfig ->- DataFrame ->- Either String (Expr Double)-synthesizeFeatureExpr target cfg df =- let- df' = exclude [target] df- t = case interpret df (Col target) of- Left e -> throw e- Right v -> v- in- case beamSearch- df'- cfg- t- (percentiles df')- []- [] of- Nothing -> Left "No programs found"- Just p -> Right p--f1FromBinary :: VU.Vector Double -> VU.Vector Double -> Maybe Double-f1FromBinary trues preds =- let (!tp, !fp, !fn) =- VU.foldl' step (0 :: Int, 0 :: Int, 0 :: Int) $- VU.zip (VU.map (> 0) preds) (VU.map (> 0) trues)- in f1FromCounts tp fp fn- where- step (!tp, !fp, !fn) (!p, !t) =- case (p, t) of- (True, True) -> (tp + 1, fp, fn)- (True, False) -> (tp, fp + 1, fn)- (False, True) -> (tp, fp, fn + 1)- (False, False) -> (tp, fp, fn)--f1FromCounts :: Int -> Int -> Int -> Maybe Double-f1FromCounts tp fp fn =- let tp' = fromIntegral tp- fp' = fromIntegral fp- fn' = fromIntegral fn- precision = if tp' + fp' == 0 then 0 else tp' / (tp' + fp')- recall = if tp' + fn' == 0 then 0 else tp' / (tp' + fn')- in if precision + recall == 0- then Nothing- else Just (2 * precision * recall / (precision + recall))--fitClassifier ::- -- | Target expression- T.Text ->- -- | Depth of search (Roughly, how many terms in the final expression)- Int ->- -- | Beam size - the number of candidate expressions to consider at a time.- Int ->- DataFrame ->- Either String (Expr Int)-fitClassifier target d b df =- let- df' = exclude [target] df- t = case interpret df (Col target) of- Left e -> throw e- Right v -> v- in- case beamSearch- df'- (BeamConfig d b F1 True)- t- (percentiles df' ++ [Lit 1, Lit 0, Lit (-1)])- []- [] of- Nothing -> Left "No programs found"- Just p -> Right (F.ifThenElse (p .> 0) 1 0)--percentiles :: DataFrame -> [Expr Double]-percentiles df =- let- doubleColumns =- map- (either throw id . ((`columnAsDoubleVector` df) . Col @Double))- (D.columnNames df)- in- concatMap- (\c -> map (Lit . roundTo2SigDigits . (`percentile'` c)) [1, 25, 75, 99])- doubleColumns- ++ map (Lit . roundTo2SigDigits . variance') doubleColumns- ++ map (Lit . roundTo2SigDigits . sqrt . variance') doubleColumns--roundToSigDigits :: Int -> Double -> Double-roundToSigDigits n x- | x == 0 = 0- | otherwise =- let magnitude = floor (logBase 10 (abs x))- scale = 10 ** fromIntegral (n - 1 - magnitude)- in fromIntegral (round (x * scale)) / scale--roundTo2SigDigits :: Double -> Double-roundTo2SigDigits = roundToSigDigits 2--fitRegression ::- -- | Target expression- T.Text ->- -- | Depth of search (Roughly, how many terms in the final expression)- Int ->- -- | Beam size - the number of candidate expressions to consider at a time.- Int ->- DataFrame ->- Either String (Expr Double)-fitRegression target d b df =- let- df' = exclude [target] df- targetMean = Stats.mean (Col @Double target) df- t = case interpret df (Col target) of- Left e -> throw e- Right v -> v- cfg = BeamConfig d b MeanSquaredError True- constants =- percentiles df'- ++ [Lit targetMean]- ++ [ F.pow p i- | i <- [1 .. 6]- , p <- [Lit 10, Lit 1, Lit 0.1]- ]- in- case beamSearch df' cfg t constants [] [] of- Nothing -> Left "No programs found"- Just p -> Right p--data LossFunction- = PearsonCorrelation- | MutualInformation- | MeanSquaredError- | F1--getLossFunction ::- LossFunction -> (VU.Vector Double -> VU.Vector Double -> Maybe Double)-getLossFunction f = case f of- MutualInformation ->- ( \l r ->- mutualInformationBinned- (Prelude.max 10 (ceiling (sqrt (fromIntegral (VU.length l)))))- l- r- )- PearsonCorrelation -> (\l r -> (^ 2) <$> correlation' l r)- MeanSquaredError -> (\l r -> fmap negate (meanSquaredError l r))- F1 -> f1FromBinary--data BeamConfig = BeamConfig- { searchDepth :: Int- , beamLength :: Int- , lossFunction :: LossFunction- , includeConditionals :: Bool- }--defaultBeamConfig :: BeamConfig-defaultBeamConfig = BeamConfig 2 100 PearsonCorrelation False--beamSearch ::- DataFrame ->- -- | Parameters of the beam search.- BeamConfig ->- -- | Examples- TypedColumn Double ->- -- | Constants- [Expr Double] ->- -- | Conditions- [Expr Bool] ->- -- | Programs- [Expr Double] ->- Maybe (Expr Double)-beamSearch df cfg outputs constants conds programs- | searchDepth cfg == 0 = case ps of- [] -> Nothing- (x : _) -> Just x- | otherwise =- beamSearch- df- (cfg{searchDepth = searchDepth cfg - 1})- outputs- constants- conditions- (generatePrograms (includeConditionals cfg) conditions vars constants ps)- where- vars = map Col names- conditions = generateConditions outputs conds (vars ++ constants) df- ps = pickTopN df outputs cfg $ deduplicate df programs- names = (map fst . L.sortBy (compare `on` snd) . M.toList . columnIndices) df--pickTopN ::- DataFrame ->- TypedColumn Double ->- BeamConfig ->- [(Expr Double, TypedColumn a)] ->- [Expr Double]-pickTopN _ _ _ [] = []-pickTopN df (TColumn col) cfg ps =- let- l = case toVector @Double @VU.Vector col of- Left e -> throw e- Right v -> v- ordered =- Prelude.take- (beamLength cfg)- ( map fst $- L.sortBy- ( \(_, c2) (_, c1) ->- if maybe False isInfinite c1- || maybe False isInfinite c2- || maybe False isNaN c1- || maybe False isNaN c2- then LT- else compare c1 c2- )- ( map- (\(e, res) -> (e, getLossFunction (lossFunction cfg) l (asDoubleVector res)))- ps- )- )- asDoubleVector c =- let- (TColumn col') = c- in- case toVector @Double @VU.Vector col' of- Left e -> throw e- Right v -> VU.convert v- interpretDoubleVector e =- let- (TColumn col') = case interpret df e of- Left e -> throw e- Right v -> v- in- case toVector @Double @VU.Vector col' of- Left e -> throw e- Right v -> VU.convert v- in- trace- ( "Best loss: "- ++ show- ( getLossFunction (lossFunction cfg) l . interpretDoubleVector- <$> listToMaybe ordered- )- ++ " "- ++ (if null ordered then "empty" else show (listToMaybe ordered))- )- ordered--pickTopNBool ::- DataFrame ->- TypedColumn Double ->- [(Expr Bool, TypedColumn Bool)] ->- [Expr Bool]-pickTopNBool _ _ [] = []-pickTopNBool df (TColumn col) ps =- let- l = case toVector @Double @VU.Vector col of- Left e -> throw e- Right v -> v- ordered =- Prelude.take- 10- ( map fst $- L.sortBy- ( \(_, c2) (_, c1) ->- if maybe False isInfinite c1- || maybe False isInfinite c2- || maybe False isNaN c1- || maybe False isNaN c2- then LT- else compare c1 c2- )- ( map- (\(e, res) -> (e, getLossFunction MutualInformation l (asDoubleVector res)))- ps- )- )- asDoubleVector c =- let- (TColumn col') = c- in- case toVector @Bool @VU.Vector col' of- Left e -> throw e- Right v -> VU.map (fromIntegral @Int @Double . fromEnum) v- in- ordered--satisfiesExamples :: DataFrame -> TypedColumn Double -> Expr Double -> Bool-satisfiesExamples df col expr =- let- result = case interpret df expr of- Left e -> throw e- Right v -> v- in- result == col
+ src/DataFrame/TH.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE CPP #-}++{- |+Module : DataFrame.TH+License : MIT++Backwards-compatibility re-export hub for the split @DataFrame.TH.*@+modules. New code can import the specific submodule directly:++ * "DataFrame.TH.Records" — record/DataFrame-based splices (no file IO)+ * "DataFrame.TH.CSV" — CSV-file-based splices (requires @-fwith-csv@)+ * "DataFrame.TH.Parquet" — Parquet-file-based splices (requires @-fwith-parquet@)++These live in @dataframe-th@, @dataframe-csv-th@, and+@dataframe-parquet-th@ respectively. The CSV/Parquet re-exports are+guarded with the @CSV_TH@ / @PARQUET_TH@ CPP defines that the+meta-@dataframe@ package sets based on its cabal flags.+-}+module DataFrame.TH (+ module DataFrame.TH.Records,+#ifdef WITH_CSV_TH+ module DataFrame.TH.CSV,+#endif+#ifdef WITH_PARQUET_TH+ module DataFrame.TH.Parquet,+#endif+) where++import DataFrame.TH.Records+#ifdef WITH_CSV_TH+import DataFrame.TH.CSV+#endif+#ifdef WITH_PARQUET_TH+import DataFrame.TH.Parquet+#endif
src/DataFrame/Typed.hs view
@@ -1,8 +1,9 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {- | Module : DataFrame.Typed-Copyright : (c) 2025+Copyright : (c) 2024 - 2026 Michael Chavinda License : MIT Maintainer : mschavinda@gmail.com Stability : experimental@@ -46,13 +47,13 @@ T.filterAllJust df :: TypedDataFrame '[Column \"x\" Double, Column \"y\" Int] @ -== Typed aggregation (Option B)+== Typed aggregation @ result = T.aggregate- (T.agg \@\"total\" (T.tsum (T.col \@\"salary\"))- $ T.agg \@\"count\" (T.tcount (T.col \@\"salary\"))- $ T.aggNil)+ ( T.as \@\"total\" (T.sum (T.col \@\"salary\"))+ . T.as \@\"count\" (T.count (T.col \@\"salary\"))+ ) (T.groupBy \@'[\"dept\"] employees) @ -}@@ -70,8 +71,10 @@ ifThenElse, lift, lift2,+ nullLift,+ nullLift2, - -- * Comparison operators+ -- * Same-type comparison operators (.==.), (./=.), (.<.),@@ -79,6 +82,20 @@ (.>=.), (.>.), + -- * Nullable-aware arithmetic operators+ (.+),+ (.-),+ (.*),+ (./),++ -- * Nullable-aware comparison operators (three-valued logic)+ (.==),+ (./=),+ (.<),+ (.<=),+ (.>=),+ (.>),+ -- * Logical operators (.&&.), (.||.),@@ -87,19 +104,62 @@ -- * Aggregation expression combinators DataFrame.Typed.Expr.sum, mean,+ median, count,+ countAll, DataFrame.Typed.Expr.minimum, DataFrame.Typed.Expr.maximum, collect,+ over, + -- * Expression combinators (full DataFrame.Functions parity)+ DataFrame.Typed.Expr.div,+ DataFrame.Typed.Expr.mod,+ mode,+ sumMaybe,+ DataFrame.Typed.Expr.meanMaybe,+ DataFrame.Typed.Expr.variance,+ DataFrame.Typed.Expr.medianMaybe,+ DataFrame.Typed.Expr.percentile,+ stddev,+ stddevMaybe,+ zScore,+ pow,+ relu,+ DataFrame.Typed.Expr.min,+ DataFrame.Typed.Expr.max,+ reduce,+ toMaybe,+ fromMaybe,+ isJust,+ isNothing,+ fromJust,+ whenPresent,+ whenBothPresent,+ recode,+ recodeWithCondition,+ recodeWithDefault,+ firstOrNothing,+ lastOrNothing,+ splitOn,+ match,+ matchAll,+ parseDate,+ daysBetween,+ bind,++ -- * Cast / coercion expressions+ castExpr,+ castExprWithDefault,+ castExprEither,+ unsafeCastExpr,+ toDouble,+ -- * Typed sort orders TSortOrder (..), asc, desc, - -- * Named expression helper- DataFrame.Typed.Expr.as,- -- * Freeze / thaw boundary freeze, freezeWithError,@@ -109,6 +169,13 @@ -- * Typed column access columnAsVector, columnAsList,+ columnAsIntVector,+ columnAsDoubleVector,+ columnAsFloatVector,+ columnAsUnboxedVector,+ toDoubleMatrix,+ toFloatMatrix,+ toIntMatrix, -- * Schema-preserving operations filterWhere,@@ -117,6 +184,7 @@ filterAllJust, filterJust, filterNothing,+ filterAllNothing, sortBy, take, takeLast,@@ -151,31 +219,86 @@ -- * Vertical merge append, + -- * Set algebra (topos operations)+ union,+ intersect,+ difference,+ symmetricDifference,+ -- * Joins innerJoin, leftJoin, rightJoin, fullOuterJoin, - -- * GroupBy and Aggregation (Option B)+ -- * GroupBy and Aggregation groupBy,- agg,- aggNil,+ as, aggregate, aggregateUntyped, + -- * Column transformations+ applyColumn,+ applyMany,+ applyWhere,+ applyAtIndex,+ safeApply,+ deriveWithExpr,+ insertWithDefault,+ insertVectorWithDefault,+ insertUnboxedVector,+ (|||),++ -- * Sampling and splitting+ randomSplit,+ kFolds,+ selectRows,+ stratifiedSample,+ stratifiedSplit,++ -- * Frequencies+ valueCounts,+ valueProportions,++#ifdef WITH_TH -- * Template Haskell deriveSchema,+#ifdef WITH_CSV_TH deriveSchemaFromCsvFile,+ deriveSchemaFromCsvFileWith,+#endif+#ifdef WITH_PARQUET_TH+ deriveSchemaFromParquetFile,+#endif+ deriveSchemaFromType,+ deriveSchemaFromTypeWith,+ SchemaOptions (..),+ defaultSchemaOptions,+#endif + -- * Record bridge (ADT <-> TypedDataFrame)+ HasSchema (..),+ fromRecordsTyped,+ toRecordsTyped,++ -- * Generics opt-in for schema derivation+ SchemaOf,+ SchemaOfRaw,+ NameCase (..),+ genericToColumns,+ genericFromColumns,+ -- * Schema type families (for advanced use) Lookup,+ SafeLookup, HasName, SubsetSchema, ExcludeSchema, RenameInSchema,+ RenameManyInSchema, RemoveColumn, Impute,+ SetColumnType, Append, Reverse, StripAllMaybe,@@ -188,6 +311,10 @@ AssertAbsent, AssertAllPresent, AssertPresent,+ AssertDisjoint,+ AssertRealColumn,+ AllColumnsReal,+ IsRealType, -- * Constraints KnownSchema (..),@@ -196,20 +323,75 @@ import Prelude hiding (drop, filter, take) -import DataFrame.Typed.Access (columnAsList, columnAsVector)+import DataFrame.Typed.Access (+ columnAsDoubleVector,+ columnAsFloatVector,+ columnAsIntVector,+ columnAsList,+ columnAsUnboxedVector,+ columnAsVector,+ toDoubleMatrix,+ toFloatMatrix,+ toIntMatrix,+ ) import DataFrame.Typed.Aggregate (- agg,- aggNil, aggregate, aggregateUntyped,+ as, groupBy, )+import DataFrame.Typed.Apply (+ applyAtIndex,+ applyColumn,+ applyMany,+ applyWhere,+ deriveWithExpr,+ insertUnboxedVector,+ insertVectorWithDefault,+ insertWithDefault,+ safeApply,+ (|||),+ )+import DataFrame.Typed.Sampling (+ kFolds,+ randomSplit,+ selectRows,+ stratifiedSample,+ stratifiedSplit,+ ) import DataFrame.Typed.Expr import DataFrame.Typed.Freeze (freeze, freezeWithError, thaw, unsafeFreeze)+import DataFrame.Typed.Generic (+ NameCase (..),+ SchemaOf,+ SchemaOfRaw,+ genericFromColumns,+ genericToColumns,+ ) import DataFrame.Typed.Join (fullOuterJoin, innerJoin, leftJoin, rightJoin) import DataFrame.Typed.Operations+import DataFrame.Typed.Record (+ HasSchema (..),+ fromRecordsTyped,+ toRecordsTyped,+ ) import DataFrame.Typed.Schema-import DataFrame.Typed.TH (deriveSchema, deriveSchemaFromCsvFile)+#ifdef WITH_TH+import DataFrame.Typed.TH (+ SchemaOptions (..),+ defaultSchemaOptions,+ deriveSchema,+#ifdef WITH_CSV_TH+ deriveSchemaFromCsvFile,+ deriveSchemaFromCsvFileWith,+#endif+#ifdef WITH_PARQUET_TH+ deriveSchemaFromParquetFile,+#endif+ deriveSchemaFromType,+ deriveSchemaFromTypeWith,+ )+#endif import DataFrame.Typed.Types ( Column, TSortOrder (..),
− src/DataFrame/Typed/Access.hs
@@ -1,55 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}--module DataFrame.Typed.Access (- -- * Typed column access- columnAsVector,- columnAsList,-) where--import Control.Exception (throw)-import Data.Proxy (Proxy (..))-import qualified Data.Text as T-import qualified Data.Vector as V-import GHC.TypeLits (KnownSymbol, symbolVal)--import DataFrame.Internal.Column (Columnable)-import DataFrame.Internal.Expression (Expr (Col))-import qualified DataFrame.Operations.Core as D-import DataFrame.Typed.Schema (AssertPresent, Lookup)-import DataFrame.Typed.Types (TypedDataFrame (..))--{- | Retrieve a column as a boxed 'Vector', with the type determined by-the schema. The column must exist (enforced at compile time).--}-columnAsVector ::- forall name cols a.- ( KnownSymbol name- , a ~ Lookup name cols- , Columnable a- , AssertPresent name cols- ) =>- TypedDataFrame cols -> V.Vector a-columnAsVector (TDF df) =- either throw id $ D.columnAsVector (Col @a colName) df- where- colName = T.pack (symbolVal (Proxy @name))---- | Retrieve a column as a list, with the type determined by the schema.-columnAsList ::- forall name cols a.- ( KnownSymbol name- , a ~ Lookup name cols- , Columnable a- , AssertPresent name cols- ) =>- TypedDataFrame cols -> [a]-columnAsList (TDF df) =- D.columnAsList (Col @a colName) df- where- colName = T.pack (symbolVal (Proxy @name))
− src/DataFrame/Typed/Aggregate.hs
@@ -1,97 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}--module DataFrame.Typed.Aggregate (- -- * Typed groupBy- groupBy,-- -- * Typed aggregation builder (Option B)- agg,- aggNil,-- -- * Running aggregations- aggregate,-- -- * Escape hatch- aggregateUntyped,-) where--import Data.Proxy (Proxy (..))-import qualified Data.Text as T-import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)--import DataFrame.Internal.Column (Columnable)-import qualified DataFrame.Internal.DataFrame as D-import DataFrame.Internal.Expression (NamedExpr)-import qualified DataFrame.Operations.Aggregation as DA--import DataFrame.Typed.Freeze (unsafeFreeze)-import DataFrame.Typed.Schema-import DataFrame.Typed.Types--{- | Group a typed DataFrame by one or more key columns.--@-grouped = groupBy \@'[\"department\"] employees-@--}-groupBy ::- forall (keys :: [Symbol]) cols.- (AllKnownSymbol keys, AssertAllPresent keys cols) =>- TypedDataFrame cols -> TypedGrouped keys cols-groupBy (TDF df) = TGD (DA.groupBy (symbolVals @keys) df)---- | The empty aggregation — no output columns beyond the group keys.-aggNil :: TAgg keys cols '[]-aggNil = TAggNil--{- | Add one aggregation to the builder.--Each call prepends a @Column name a@ to the result schema and records-the runtime 'NamedExpr'. The expression is validated against the-source schema @cols@ at compile time.--@-agg \@\"total_sales\" (tsum (col \@\"salary\"))- $ agg \@\"avg_price\" (tmean (col \@\"price\"))- $ aggNil-@--}-agg ::- forall name a keys cols aggs.- ( KnownSymbol name- , Columnable a- ) =>- TExpr cols a -> TAgg keys cols aggs -> TAgg keys cols (Column name a ': aggs)-agg = TAggCons colName- where- colName = T.pack (symbolVal (Proxy @name))--{- | Run a typed aggregation.--Result schema = grouping key columns ++ aggregated columns (in declaration order).--@-result = aggregate- (agg \@\"total\" (tsum (col @"salary")) $ agg \@\"count\" (tcount (col @"salary") $ aggNil)- (groupBy \@'[\"dept\"] employees)--- result :: TDF '[Column \"dept\" Text, Column \"total\" Double, Column \"count\" Int]-@--}-aggregate ::- forall keys cols aggs.- TAgg keys cols aggs ->- TypedGrouped keys cols ->- TypedDataFrame (Append (GroupKeyColumns keys cols) (Reverse aggs))-aggregate tagg (TGD gdf) =- unsafeFreeze (DA.aggregate (taggToNamedExprs tagg) gdf)---- | Escape hatch: run an untyped aggregation and return a raw 'DataFrame'.-aggregateUntyped :: [NamedExpr] -> TypedGrouped keys cols -> D.DataFrame-aggregateUntyped exprs (TGD gdf) = DA.aggregate exprs gdf
− src/DataFrame/Typed/Expr.hs
@@ -1,276 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}-{-# LANGUAGE UndecidableInstances #-}--{- | Type-safe expression construction for typed DataFrames.--Unlike the untyped @Expr a@ where column references are unchecked strings,-'TExpr' ensures at compile time that:--* Referenced columns exist in the schema-* Column types match the expression type--== Example--@-type Schema = '[Column \"age\" Int, Column \"salary\" Double]---- This compiles:-goodExpr :: TExpr Schema Double-goodExpr = col \@\"salary\"---- This gives a compile-time error (column not found):-badExpr :: TExpr Schema Double-badExpr = col \@\"nonexistent\"---- This gives a compile-time error (type mismatch):-wrongType :: TExpr Schema Int-wrongType = col \@\"salary\" -- salary is Double, not Int-@--}-module DataFrame.Typed.Expr (- -- * Core typed expression type (re-exported from Types)- TExpr (..),-- -- * Column reference (schema-checked)- col,-- -- * Literals- lit,-- -- * Conditional- ifThenElse,-- -- * Unary / binary lifting- lift,- lift2,-- -- * Comparison operators- (.==.),- (./=.),- (.<.),- (.<=.),- (.>=.),- (.>.),-- -- * Logical operators- (.&&.),- (.||.),- DataFrame.Typed.Expr.not,-- -- * Aggregation combinators- sum,- mean,- count,- minimum,- maximum,- collect,-- -- * Named expression helper- as,-- -- * Sort helpers- asc,- desc,-) where--import Data.Proxy (Proxy (..))-import Data.String (IsString (..))-import qualified Data.Text as T-import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)--import DataFrame.Internal.Column (Columnable)-import DataFrame.Internal.Expression (- AggStrategy (..),- BinaryOp (..),- Expr (..),- MeanAcc (..),- NamedExpr,- UExpr (..),- UnaryOp (..),- )--import DataFrame.Typed.Schema (AssertPresent, Lookup)-import DataFrame.Typed.Types (TExpr (..), TSortOrder (..))-import Prelude hiding (maximum, minimum, sum)--{- | Create a typed column reference. This is the key type-safety entry point.--The column name must exist in @cols@ and its type must match @a@.-Both checks happen at compile time via type families.--@-salary :: TExpr '[Column \"salary\" Double] Double-salary = col \@\"salary\"-@--}-col ::- forall (name :: Symbol) cols a.- ( KnownSymbol name- , a ~ Lookup name cols- , Columnable a- , AssertPresent name cols- ) =>- TExpr cols a-col = TExpr (Col (T.pack (symbolVal (Proxy @name))))--{- | Create a literal expression. Valid for any schema since it-references no columns.--}-lit :: (Columnable a) => a -> TExpr cols a-lit = TExpr . Lit---- | Conditional expression.-ifThenElse ::- (Columnable a) =>- TExpr cols Bool -> TExpr cols a -> TExpr cols a -> TExpr cols a-ifThenElse (TExpr c) (TExpr t) (TExpr e) = TExpr (If c t e)------------------------------------------------------------------------------------ Numeric instances (mirror Expr's instances)----------------------------------------------------------------------------------instance (Num a, Columnable a) => Num (TExpr cols a) where- (TExpr a) + (TExpr b) = TExpr (a + b)- (TExpr a) - (TExpr b) = TExpr (a - b)- (TExpr a) * (TExpr b) = TExpr (a * b)- negate (TExpr a) = TExpr (negate a)- abs (TExpr a) = TExpr (abs a)- signum (TExpr a) = TExpr (signum a)- fromInteger = TExpr . fromInteger--instance (Fractional a, Columnable a) => Fractional (TExpr cols a) where- fromRational = TExpr . fromRational- (TExpr a) / (TExpr b) = TExpr (a / b)--instance (Floating a, Columnable a) => Floating (TExpr cols a) where- pi = TExpr pi- exp (TExpr a) = TExpr (exp a)- sqrt (TExpr a) = TExpr (sqrt a)- log (TExpr a) = TExpr (log a)- (TExpr a) ** (TExpr b) = TExpr (a ** b)- logBase (TExpr a) (TExpr b) = TExpr (logBase a b)- sin (TExpr a) = TExpr (sin a)- cos (TExpr a) = TExpr (cos a)- tan (TExpr a) = TExpr (tan a)- asin (TExpr a) = TExpr (asin a)- acos (TExpr a) = TExpr (acos a)- atan (TExpr a) = TExpr (atan a)- sinh (TExpr a) = TExpr (sinh a)- cosh (TExpr a) = TExpr (cosh a)- asinh (TExpr a) = TExpr (asinh a)- acosh (TExpr a) = TExpr (acosh a)- atanh (TExpr a) = TExpr (atanh a)--instance (IsString a, Columnable a) => IsString (TExpr cols a) where- fromString = TExpr . fromString------------------------------------------------------------------------------------ Lifting arbitrary functions------------------------------------------------------------------------------------ | Lift a unary function into a typed expression.-lift ::- (Columnable a, Columnable b) => (a -> b) -> TExpr cols a -> TExpr cols b-lift f (TExpr e) = TExpr (Unary (MkUnaryOp f "unaryUdf" Nothing) e)---- | Lift a binary function into typed expressions.-lift2 ::- (Columnable a, Columnable b, Columnable c) =>- (a -> b -> c) -> TExpr cols a -> TExpr cols b -> TExpr cols c-lift2 f (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp f "binaryUdf" Nothing False 0) a b)--infixl 4 .==., ./=., .<., .<=., .>=., .>.-infixr 3 .&&.-infixr 2 .||.--(.==.) ::- (Columnable a, Eq a) => TExpr cols a -> TExpr cols a -> TExpr cols Bool-(.==.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (==) "eq" (Just "==") True 4) a b)--(./=.) ::- (Columnable a, Eq a) => TExpr cols a -> TExpr cols a -> TExpr cols Bool-(./=.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (/=) "neq" (Just "/=") True 4) a b)--(.<.) ::- (Columnable a, Ord a) => TExpr cols a -> TExpr cols a -> TExpr cols Bool-(.<.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (<) "lt" (Just "<") False 4) a b)--(.<=.) ::- (Columnable a, Ord a) => TExpr cols a -> TExpr cols a -> TExpr cols Bool-(.<=.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (<=) "leq" (Just "<=") False 4) a b)--(.>=.) ::- (Columnable a, Ord a) => TExpr cols a -> TExpr cols a -> TExpr cols Bool-(.>=.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (>=) "geq" (Just ">=") False 4) a b)--(.>.) ::- (Columnable a, Ord a) => TExpr cols a -> TExpr cols a -> TExpr cols Bool-(.>.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (>) "gt" (Just ">") False 4) a b)--(.&&.) :: TExpr cols Bool -> TExpr cols Bool -> TExpr cols Bool-(.&&.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (&&) "and" (Just "&&") True 3) a b)--(.||.) :: TExpr cols Bool -> TExpr cols Bool -> TExpr cols Bool-(.||.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (||) "or" (Just "||") True 2) a b)--not :: TExpr cols Bool -> TExpr cols Bool-not (TExpr e) = TExpr (Unary (MkUnaryOp Prelude.not "not" (Just "!")) e)------------------------------------------------------------------------------------ Aggregation combinators----------------------------------------------------------------------------------sum :: (Columnable a, Num a) => TExpr cols a -> TExpr cols a-sum (TExpr e) = TExpr (Agg (FoldAgg "sum" Nothing (+)) e)--mean :: (Columnable a, Real a) => TExpr cols a -> TExpr cols Double-mean (TExpr e) =- TExpr- ( Agg- ( MergeAgg- "mean"- (MeanAcc 0.0 0)- (\(MeanAcc s c) x -> MeanAcc (s + realToFrac x) (c + 1))- (\(MeanAcc s1 c1) (MeanAcc s2 c2) -> MeanAcc (s1 + s2) (c1 + c2))- (\(MeanAcc s c) -> if c == 0 then 0 / 0 else s / fromIntegral c)- )- e- )--count :: (Columnable a) => TExpr cols a -> TExpr cols Int-count (TExpr e) = TExpr (Agg (MergeAgg "count" (0 :: Int) (\c _ -> c + 1) (+) id) e)--minimum :: (Columnable a, Ord a) => TExpr cols a -> TExpr cols a-minimum (TExpr e) = TExpr (Agg (FoldAgg "minimum" Nothing min) e)--maximum :: (Columnable a, Ord a) => TExpr cols a -> TExpr cols a-maximum (TExpr e) = TExpr (Agg (FoldAgg "maximum" Nothing max) e)--collect :: (Columnable a) => TExpr cols a -> TExpr cols [a]-collect (TExpr e) = TExpr (Agg (FoldAgg "collect" (Just []) (flip (:))) e)------------------------------------------------------------------------------------ Named expression helper------------------------------------------------------------------------------------ | Create a 'NamedExpr' for use with 'aggregateUntyped'.-as :: (Columnable a) => TExpr cols a -> T.Text -> NamedExpr-as (TExpr e) name = (name, UExpr e)---- | Create an ascending sort order from a typed expression.-asc :: (Columnable a) => TExpr cols a -> TSortOrder cols-asc = Asc---- | Create a descending sort order from a typed expression.-desc :: (Columnable a) => TExpr cols a -> TSortOrder cols-desc = Desc
− src/DataFrame/Typed/Freeze.hs
@@ -1,86 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.Typed.Freeze (- -- * Safe boundary- freeze,- freezeWithError,-- -- * Escape hatches- thaw,- unsafeFreeze,-) where--import qualified Data.Text as T-import Type.Reflection (SomeTypeRep)--import qualified DataFrame.Internal.Column as C-import qualified DataFrame.Internal.DataFrame as D-import DataFrame.Operations.Core (columnNames)-import DataFrame.Typed.Schema (KnownSchema (..))-import DataFrame.Typed.Types (TypedDataFrame (..))--{- | Validate that an untyped 'DataFrame' matches the expected schema @cols@,-then wrap it. Returns 'Nothing' on mismatch.--}-freeze ::- forall cols. (KnownSchema cols) => D.DataFrame -> Maybe (TypedDataFrame cols)-freeze df = case validateSchema @cols df of- Left _ -> Nothing- Right _ -> Just (TDF df)---- | Like 'freeze' but returns a descriptive error message on failure.-freezeWithError ::- forall cols.- (KnownSchema cols) =>- D.DataFrame -> Either T.Text (TypedDataFrame cols)-freezeWithError df = case validateSchema @cols df of- Left err -> Left err- Right _ -> Right (TDF df)--{- | Unwrap a typed DataFrame back to the untyped representation.-Always safe; discards type information.--}-thaw :: TypedDataFrame cols -> D.DataFrame-thaw (TDF df) = df--{- | Wrap an untyped DataFrame without any validation.-Used internally after delegation where the library guarantees schema correctness.--}-unsafeFreeze :: D.DataFrame -> TypedDataFrame cols-unsafeFreeze = TDF--validateSchema ::- forall cols.- (KnownSchema cols) =>- D.DataFrame -> Either T.Text ()-validateSchema df = mapM_ checkCol (schemaEvidence @cols)- where- checkCol :: (T.Text, SomeTypeRep) -> Either T.Text ()- checkCol (name, expectedRep) = case D.getColumn name df of- Nothing ->- Left $- "Column '"- <> name- <> "' not found in DataFrame. "- <> "Available columns: "- <> T.pack (show (columnNames df))- Just col ->- if matchesType expectedRep col- then Right ()- else- Left $- "Type mismatch on column '"- <> name- <> "': expected "- <> T.pack (show expectedRep)- <> ", got "- <> T.pack (C.columnTypeString col)---- | Check if a Column's element type matches the expected SomeTypeRep.-matchesType :: SomeTypeRep -> C.Column -> Bool-matchesType expected col = T.pack (show expected) == T.pack (C.columnTypeString col)
− src/DataFrame/Typed/Join.hs
@@ -1,72 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}--module DataFrame.Typed.Join (- -- * Typed joins- innerJoin,- leftJoin,- rightJoin,- fullOuterJoin,-) where--import GHC.TypeLits (Symbol)--import qualified DataFrame.Operations.Join as DJ--import DataFrame.Typed.Freeze (unsafeFreeze)-import DataFrame.Typed.Schema-import DataFrame.Typed.Types (TypedDataFrame (..))---- | Typed inner join on one or more key columns.-innerJoin ::- forall (keys :: [Symbol]) left right.- (AllKnownSymbol keys) =>- TypedDataFrame left ->- TypedDataFrame right ->- TypedDataFrame (InnerJoinSchema keys left right)-innerJoin (TDF l) (TDF r) =- unsafeFreeze (DJ.innerJoin keyNames r l)- where- keyNames = symbolVals @keys---- | Typed left join.-leftJoin ::- forall (keys :: [Symbol]) left right.- (AllKnownSymbol keys) =>- TypedDataFrame left ->- TypedDataFrame right ->- TypedDataFrame (LeftJoinSchema keys left right)-leftJoin (TDF l) (TDF r) =- unsafeFreeze (DJ.leftJoin keyNames l r)- where- keyNames = symbolVals @keys---- | Typed right join.-rightJoin ::- forall (keys :: [Symbol]) left right.- (AllKnownSymbol keys) =>- TypedDataFrame left ->- TypedDataFrame right ->- TypedDataFrame (RightJoinSchema keys left right)-rightJoin (TDF l) (TDF r) =- unsafeFreeze (DJ.rightJoin keyNames l r)- where- keyNames = symbolVals @keys---- | Typed full outer join.-fullOuterJoin ::- forall (keys :: [Symbol]) left right.- (AllKnownSymbol keys) =>- TypedDataFrame left ->- TypedDataFrame right ->- TypedDataFrame (FullOuterJoinSchema keys left right)-fullOuterJoin (TDF l) (TDF r) =- unsafeFreeze (DJ.fullOuterJoin keyNames r l)- where- keyNames = symbolVals @keys
− src/DataFrame/Typed/Operations.hs
@@ -1,378 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}--module DataFrame.Typed.Operations (- -- * Schema-preserving operations- filterWhere,- filter,- filterBy,- filterAllJust,- filterJust,- filterNothing,- sortBy,- take,- takeLast,- drop,- dropLast,- range,- cube,- distinct,- sample,- shuffle,-- -- * Schema-modifying operations- derive,- impute,- select,- exclude,- rename,- renameMany,- insert,- insertColumn,- insertVector,- cloneColumn,- dropColumn,- replaceColumn,-- -- * Metadata- dimensions,- nRows,- nColumns,- columnNames,-- -- * Vertical merge- append,-) where--import Data.Proxy (Proxy (..))-import qualified Data.Text as T-import qualified Data.Vector as V-import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)-import System.Random (RandomGen)-import Prelude hiding (drop, filter, take)--import qualified DataFrame.Functions as DF-import DataFrame.Internal.Column (Columnable)-import qualified DataFrame.Internal.Column as C-import qualified DataFrame.Operations.Aggregation as DA-import qualified DataFrame.Operations.Core as D-import DataFrame.Operations.Merge ()-import qualified DataFrame.Operations.Permutation as D-import qualified DataFrame.Operations.Subset as D-import qualified DataFrame.Operations.Transformations as D--import DataFrame.Typed.Freeze (unsafeFreeze)-import DataFrame.Typed.Schema-import DataFrame.Typed.Types (TExpr (..), TSortOrder (..), TypedDataFrame (..))-import qualified DataFrame.Typed.Types as T------------------------------------------------------------------------------------ Schema-preserving operations----------------------------------------------------------------------------------{- | Filter rows where a boolean expression evaluates to True.-The expression is validated against the schema at compile time.--}-filterWhere :: TExpr cols Bool -> TypedDataFrame cols -> TypedDataFrame cols-filterWhere (TExpr expr) (TDF df) = TDF (D.filterWhere expr df)---- | Filter rows by applying a predicate to a typed expression.-filter ::- (Columnable a) =>- TExpr cols a -> (a -> Bool) -> TypedDataFrame cols -> TypedDataFrame cols-filter (TExpr expr) pred' (TDF df) = TDF (D.filter expr pred' df)---- | Filter rows by a predicate on a column expression (flipped argument order).-filterBy ::- (Columnable a) =>- (a -> Bool) -> TExpr cols a -> TypedDataFrame cols -> TypedDataFrame cols-filterBy pred' (TExpr expr) (TDF df) = TDF (D.filterBy pred' expr df)--{- | Keep only rows where ALL Optional columns have Just values.-Strips 'Maybe' from all column types in the result schema.--@-df :: TDF '[Column \"x\" (Maybe Double), Column \"y\" Int]-filterAllJust df :: TDF '[Column \"x\" Double, Column \"y\" Int]-@--}-filterAllJust :: TypedDataFrame cols -> TypedDataFrame (StripAllMaybe cols)-filterAllJust (TDF df) = unsafeFreeze (D.filterAllJust df)--{- | Keep only rows where the named column has Just values.-Strips 'Maybe' from that column's type in the result schema.--@-filterJust \@\"x\" df-@--}-filterJust ::- forall name cols.- ( KnownSymbol name- , AssertPresent name cols- ) =>- TypedDataFrame cols -> TypedDataFrame (StripMaybeAt name cols)-filterJust (TDF df) = unsafeFreeze (D.filterJust colName df)- where- colName = T.pack (symbolVal (Proxy @name))--{- | Keep only rows where the named column has Nothing.-Schema is preserved (column types unchanged, just fewer rows).--}-filterNothing ::- forall name cols.- ( KnownSymbol name- , AssertPresent name cols- ) =>- TypedDataFrame cols -> TypedDataFrame cols-filterNothing (TDF df) = TDF (D.filterNothing colName df)- where- colName = T.pack (symbolVal (Proxy @name))--{- | Sort by the given typed sort orders.-Sort orders reference columns that are validated against the schema.--}-sortBy :: [TSortOrder cols] -> TypedDataFrame cols -> TypedDataFrame cols-sortBy ords (TDF df) = TDF (D.sortBy (map toUntypedSort ords) df)- where- toUntypedSort :: TSortOrder cols -> D.SortOrder- toUntypedSort (Asc (TExpr e)) = D.Asc e- toUntypedSort (Desc (TExpr e)) = D.Desc e---- | Take the first @n@ rows.-take :: Int -> TypedDataFrame cols -> TypedDataFrame cols-take n (TDF df) = TDF (D.take n df)---- | Take the last @n@ rows.-takeLast :: Int -> TypedDataFrame cols -> TypedDataFrame cols-takeLast n (TDF df) = TDF (D.takeLast n df)---- | Drop the first @n@ rows.-drop :: Int -> TypedDataFrame cols -> TypedDataFrame cols-drop n (TDF df) = TDF (D.drop n df)---- | Drop the last @n@ rows.-dropLast :: Int -> TypedDataFrame cols -> TypedDataFrame cols-dropLast n (TDF df) = TDF (D.dropLast n df)---- | Take rows in the given range (start, end).-range :: (Int, Int) -> TypedDataFrame cols -> TypedDataFrame cols-range r (TDF df) = TDF (D.range r df)---- | Take a sub-cube of the DataFrame.-cube :: (Int, Int) -> TypedDataFrame cols -> TypedDataFrame cols-cube c (TDF df) = TDF (D.cube c df)---- | Remove duplicate rows.-distinct :: TypedDataFrame cols -> TypedDataFrame cols-distinct (TDF df) = TDF (DA.distinct df)---- | Randomly sample a fraction of rows.-sample ::- (RandomGen g) => g -> Double -> TypedDataFrame cols -> TypedDataFrame cols-sample g frac (TDF df) = TDF (D.sample g frac df)---- | Shuffle all rows randomly.-shuffle :: (RandomGen g) => g -> TypedDataFrame cols -> TypedDataFrame cols-shuffle g (TDF df) = TDF (D.shuffle g df)------------------------------------------------------------------------------------ Schema-modifying operations----------------------------------------------------------------------------------{- | Derive a new column from a typed expression. The column name must NOT-already exist in the schema (enforced at compile time via 'AssertAbsent').-The expression is validated against the current schema.--@-df' = derive \@\"total\" (col \@\"price\" * col \@\"qty\") df--- df' :: TDF (Column \"total\" Double ': originalCols)-@--}-derive ::- forall name a cols.- ( KnownSymbol name- , Columnable a- , AssertAbsent name cols- ) =>- TExpr cols a ->- TypedDataFrame cols ->- TypedDataFrame (Snoc cols (T.Column name a))-derive (TExpr expr) (TDF df) = unsafeFreeze (D.derive colName expr df)- where- colName = T.pack (symbolVal (Proxy @name))--impute ::- forall name a cols.- ( KnownSymbol name- , Columnable a- , Maybe a ~ Lookup name cols- ) =>- a ->- TypedDataFrame cols ->- TypedDataFrame (Impute name cols)-impute value (TDF df) =- unsafeFreeze- (D.derive colName (DF.fromMaybe value (DF.col @(Maybe a) colName)) df)- where- colName = T.pack (symbolVal (Proxy @name))---- | Select a subset of columns by name.-select ::- forall (names :: [Symbol]) cols.- (AllKnownSymbol names, AssertAllPresent names cols) =>- TypedDataFrame cols -> TypedDataFrame (SubsetSchema names cols)-select (TDF df) = unsafeFreeze (D.select (symbolVals @names) df)---- | Exclude columns by name.-exclude ::- forall (names :: [Symbol]) cols.- (AllKnownSymbol names) =>- TypedDataFrame cols -> TypedDataFrame (ExcludeSchema names cols)-exclude (TDF df) = unsafeFreeze (D.exclude (symbolVals @names) df)---- | Rename a column.-rename ::- forall old new cols.- (KnownSymbol old, KnownSymbol new) =>- TypedDataFrame cols -> TypedDataFrame (RenameInSchema old new cols)-rename (TDF df) = unsafeFreeze (D.rename oldName newName df)- where- oldName = T.pack (symbolVal (Proxy @old))- newName = T.pack (symbolVal (Proxy @new))---- | Rename multiple columns from a type-level list of pairs.-renameMany ::- forall (pairs :: [(Symbol, Symbol)]) cols.- (AllKnownPairs pairs) =>- TypedDataFrame cols -> TypedDataFrame (RenameManyInSchema pairs cols)-renameMany (TDF df) = unsafeFreeze (foldRenames (pairVals @pairs) df)- where- foldRenames [] df' = df'- foldRenames ((old, new) : rest) df' = foldRenames rest (D.rename old new df')---- | Insert a new column from a Foldable container.-insert ::- forall name a cols t.- ( KnownSymbol name- , Columnable a- , Foldable t- , AssertAbsent name cols- ) =>- t a -> TypedDataFrame cols -> TypedDataFrame (T.Column name a ': cols)-insert xs (TDF df) = unsafeFreeze (D.insert colName xs df)- where- colName = T.pack (symbolVal (Proxy @name))---- | Insert a raw 'Column' value.-insertColumn ::- forall name a cols.- ( KnownSymbol name- , Columnable a- , AssertAbsent name cols- ) =>- C.Column -> TypedDataFrame cols -> TypedDataFrame (T.Column name a ': cols)-insertColumn col (TDF df) = unsafeFreeze (D.insertColumn colName col df)- where- colName = T.pack (symbolVal (Proxy @name))---- | Insert a boxed 'Vector'.-insertVector ::- forall name a cols.- ( KnownSymbol name- , Columnable a- , AssertAbsent name cols- ) =>- V.Vector a -> TypedDataFrame cols -> TypedDataFrame (T.Column name a ': cols)-insertVector vec (TDF df) = unsafeFreeze (D.insertVector colName vec df)- where- colName = T.pack (symbolVal (Proxy @name))---- | Clone an existing column under a new name.-cloneColumn ::- forall old new cols.- ( KnownSymbol old- , KnownSymbol new- , AssertPresent old cols- , AssertAbsent new cols- ) =>- TypedDataFrame cols -> TypedDataFrame (T.Column new (Lookup old cols) ': cols)-cloneColumn (TDF df) = unsafeFreeze (D.cloneColumn oldName newName df)- where- oldName = T.pack (symbolVal (Proxy @old))- newName = T.pack (symbolVal (Proxy @new))---- | Drop a column by name.-dropColumn ::- forall name cols.- ( KnownSymbol name- , AssertPresent name cols- ) =>- TypedDataFrame cols -> TypedDataFrame (RemoveColumn name cols)-dropColumn (TDF df) = unsafeFreeze (D.exclude [colName] df)- where- colName = T.pack (symbolVal (Proxy @name))--{- | Replace an existing column with new values derived from a typed expression.-The column must already exist and the new type must match.--}-replaceColumn ::- forall name a cols.- ( KnownSymbol name- , Columnable a- , a ~ Lookup name cols- , AssertPresent name cols- ) =>- TExpr cols a -> TypedDataFrame cols -> TypedDataFrame cols-replaceColumn (TExpr expr) (TDF df) = unsafeFreeze (D.derive colName expr df)- where- colName = T.pack (symbolVal (Proxy @name))---- | Vertically merge two DataFrames with the same schema.-append :: TypedDataFrame cols -> TypedDataFrame cols -> TypedDataFrame cols-append (TDF a) (TDF b) = TDF (a <> b)------------------------------------------------------------------------------------ Metadata (pass-through)----------------------------------------------------------------------------------dimensions :: TypedDataFrame cols -> (Int, Int)-dimensions (TDF df) = D.dimensions df--nRows :: TypedDataFrame cols -> Int-nRows (TDF df) = D.nRows df--nColumns :: TypedDataFrame cols -> Int-nColumns (TDF df) = D.nColumns df--columnNames :: TypedDataFrame cols -> [T.Text]-columnNames (TDF df) = D.columnNames df------------------------------------------------------------------------------------ Internal helpers------------------------------------------------------------------------------------ | Helper class for extracting [(Text, Text)] from type-level pairs.-class AllKnownPairs (pairs :: [(Symbol, Symbol)]) where- pairVals :: [(T.Text, T.Text)]--instance AllKnownPairs '[] where- pairVals = []--instance- (KnownSymbol a, KnownSymbol b, AllKnownPairs rest) =>- AllKnownPairs ('(a, b) ': rest)- where- pairVals =- ( T.pack (symbolVal (Proxy @a))- , T.pack (symbolVal (Proxy @b))- )- : pairVals @rest
− src/DataFrame/Typed/Schema.hs
@@ -1,351 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}-{-# LANGUAGE UndecidableInstances #-}--module DataFrame.Typed.Schema (- -- * Type families for schema manipulation- Lookup,- HasName,- RemoveColumn,- Impute,- SubsetSchema,- ExcludeSchema,- RenameInSchema,- RenameManyInSchema,- Append,- Snoc,- Reverse,- ColumnNames,- AssertAbsent,- AssertPresent,- AssertAllPresent,- IsElem,-- -- * Maybe-stripping families- StripAllMaybe,- StripMaybeAt,-- -- * Join schema families- SharedNames,- UniqueLeft,- InnerJoinSchema,- LeftJoinSchema,- RightJoinSchema,- FullOuterJoinSchema,- WrapMaybe,- WrapMaybeColumns,- CollidingColumns,-- -- * GroupBy helpers- GroupKeyColumns,-- -- * KnownSchema class- KnownSchema (..),-- -- * Helpers- AllKnownSymbol (..),-) where--import Data.Kind (Constraint, Type)-import Data.Proxy (Proxy (..))-import qualified Data.Text as T-import Data.These (These)-import GHC.TypeLits-import Type.Reflection (SomeTypeRep, Typeable, someTypeRep)--import DataFrame.Internal.Column (Columnable)-import DataFrame.Internal.Types (If)-import DataFrame.Typed.Types (Column)---- | Look up the element type of a column by name.-type family Lookup (name :: Symbol) (cols :: [Type]) :: Type where- Lookup name (Column name a ': _) = a- Lookup name (Column _ _ ': rest) = Lookup name rest- Lookup name '[] =- TypeError- ('Text "Column '" ':<>: 'Text name ':<>: 'Text "' not found in schema")---- | Unwrap a Maybe from a type after we impute values.-type family Impute (name :: Symbol) (cols :: [Type]) :: [Type] where- Impute name (Column name (Maybe a) ': rest) = Column name a ': rest- Impute name (Column name _ ': rest) =- TypeError- ('Text "Column '" ':<>: 'Text name ':<>: 'Text "' is not of kind Maybe *")- Impute name (col ': rest) = col ': Impute name rest- Impute name '[] = '[]---- | Add type to the end of a list.-type family Snoc (xs :: [k]) (x :: k) :: [k] where- Snoc '[] x = '[x]- Snoc (y ': ys) x = y ': Snoc ys x---- | Check whether a column name exists in a schema (type-level Bool).-type family HasName (name :: Symbol) (cols :: [Type]) :: Bool where- HasName name (Column name _ ': _) = 'True- HasName name (Column _ _ ': rest) = HasName name rest- HasName name '[] = 'False---- | Remove a column by name from a schema.-type family RemoveColumn (name :: Symbol) (cols :: [Type]) :: [Type] where- RemoveColumn name (Column name _ ': rest) = rest- RemoveColumn name (col ': rest) = col ': RemoveColumn name rest- RemoveColumn name '[] = '[]---- | Select a subset of columns by a list of names.-type family SubsetSchema (names :: [Symbol]) (cols :: [Type]) :: [Type] where- SubsetSchema '[] cols = '[]- SubsetSchema (n ': ns) cols = Column n (Lookup n cols) ': SubsetSchema ns cols---- | Exclude columns by a list of names.-type family ExcludeSchema (names :: [Symbol]) (cols :: [Type]) :: [Type] where- ExcludeSchema names '[] = '[]- ExcludeSchema names (Column n a ': rest) =- If- (IsElem n names)- (ExcludeSchema names rest)- (Column n a ': ExcludeSchema names rest)---- | Type-level elem for Symbols-type family IsElem (x :: Symbol) (xs :: [Symbol]) :: Bool where- IsElem x '[] = 'False- IsElem x (x ': _) = 'True- IsElem x (_ ': xs) = IsElem x xs---- | Rename a column in the schema.-type family RenameInSchema (old :: Symbol) (new :: Symbol) (cols :: [Type]) :: [Type] where- RenameInSchema old new (Column old a ': rest) = Column new a ': rest- RenameInSchema old new (col ': rest) = col ': RenameInSchema old new rest- RenameInSchema old new '[] =- TypeError- ('Text "Cannot rename: column '" ':<>: 'Text old ':<>: 'Text "' not found")---- | Rename multiple columns.-type family RenameManyInSchema (pairs :: [(Symbol, Symbol)]) (cols :: [Type]) :: [Type] where- RenameManyInSchema '[] cols = cols- RenameManyInSchema ('(old, new) ': rest) cols =- RenameManyInSchema rest (RenameInSchema old new cols)---- | Append two type-level lists.-type family Append (xs :: [k]) (ys :: [k]) :: [k] where- Append '[] ys = ys- Append (x ': xs) ys = x ': Append xs ys---- | Reverse a type-level list.-type family Reverse (xs :: [Type]) :: [Type] where- Reverse xs = ReverseAcc xs '[]--type family ReverseAcc (xs :: [Type]) (acc :: [Type]) :: [Type] where- ReverseAcc '[] acc = acc- ReverseAcc (x ': xs) acc = ReverseAcc xs (x ': acc)---- | Extract column names as a type-level list of Symbols.-type family ColumnNames (cols :: [Type]) :: [Symbol] where- ColumnNames '[] = '[]- ColumnNames (Column n _ ': rest) = n ': ColumnNames rest---- | Assert that a column name is absent from the schema (for derive/insert).-type family AssertAbsent (name :: Symbol) (cols :: [Type]) :: Constraint where- AssertAbsent name cols = AssertAbsentHelper name (HasName name cols) cols--type family- AssertAbsentHelper (name :: Symbol) (found :: Bool) (cols :: [Type]) ::- Constraint- where- AssertAbsentHelper name 'False cols = ()- AssertAbsentHelper name 'True cols =- TypeError- ( 'Text "Column '"- ':<>: 'Text name- ':<>: 'Text "' already exists in schema. "- ':<>: 'Text "Use replaceColumn to overwrite."- )---- | Assert that a column name is present in the schema.-type family AssertPresent (name :: Symbol) (cols :: [Type]) :: Constraint where- AssertPresent name cols = AssertPresentHelper name (HasName name cols) cols--type family- AssertPresentHelper (name :: Symbol) (found :: Bool) (cols :: [Type]) ::- Constraint- where- AssertPresentHelper name 'True cols = ()- AssertPresentHelper name 'False cols =- TypeError- ('Text "Column '" ':<>: 'Text name ':<>: 'Text "' not found in schema")---- | Assert that a column name is present in the schema.-type family AssertAllPresent (name :: [Symbol]) (cols :: [Type]) :: Constraint where- AssertAllPresent (name ': rest) cols =- If- (HasName name cols)- (AssertAllPresent rest cols)- ( TypeError- ('Text "Column '" ':<>: 'Text name ':<>: 'Text "' not found in schema")- )- AssertAllPresent '[] cols = ()--{- | Strip 'Maybe' from all columns. Used by 'filterAllJust'.--@Column "x" (Maybe Double)@ becomes @Column "x" Double@.-@Column "y" Int@ stays @Column "y" Int@.--}-type family StripAllMaybe (cols :: [Type]) :: [Type] where- StripAllMaybe '[] = '[]- StripAllMaybe (Column n (Maybe a) ': rest) = Column n a ': StripAllMaybe rest- StripAllMaybe (Column n a ': rest) = Column n a ': StripAllMaybe rest--{- | Strip 'Maybe' from a single named column. Used by 'filterJust'.--@StripMaybeAt "x" '[Column "x" (Maybe Double), Column "y" Int]@- = @'[Column "x" Double, Column "y" Int]@--}-type family StripMaybeAt (name :: Symbol) (cols :: [Type]) :: [Type] where- StripMaybeAt name (Column name (Maybe a) ': rest) = Column name a ': rest- StripMaybeAt name (Column name a ': rest) = Column name a ': rest- StripMaybeAt name (col ': rest) = col ': StripMaybeAt name rest- StripMaybeAt name '[] =- TypeError- ('Text "Column '" ':<>: 'Text name ':<>: 'Text "' not found in schema")---- | Extract column names that appear in both schemas.-type family SharedNames (left :: [Type]) (right :: [Type]) :: [Symbol] where- SharedNames '[] right = '[]- SharedNames (Column n _ ': rest) right =- If (HasName n right) (n ': SharedNames rest right) (SharedNames rest right)---- | Columns from @left@ whose names do NOT appear in @right@.-type family UniqueLeft (left :: [Type]) (rightNames :: [Symbol]) :: [Type] where- UniqueLeft '[] _ = '[]- UniqueLeft (Column n a ': rest) rn =- If (IsElem n rn) (UniqueLeft rest rn) (Column n a ': UniqueLeft rest rn)---- | Wrap column types in Maybe.-type family WrapMaybe (cols :: [Type]) :: [Type] where- WrapMaybe '[] = '[]- WrapMaybe (Column n a ': rest) = Column n (Maybe a) ': WrapMaybe rest---- | Wrap selected columns in Maybe by name list.-type family WrapMaybeColumns (names :: [Symbol]) (cols :: [Type]) :: [Type] where- WrapMaybeColumns names '[] = '[]- WrapMaybeColumns names (Column n a ': rest) =- If- (IsElem n names)- (Column n (Maybe a) ': WrapMaybeColumns names rest)- (Column n a ': WrapMaybeColumns names rest)---- | Columns in left whose names collide with right (excluding keys).-type family CollidingColumns (left :: [Type]) (right :: [Type]) (keys :: [Symbol]) :: [Type] where- CollidingColumns '[] _ _ = '[]- CollidingColumns (Column n a ': rest) right keys =- If- (IsElem n keys)- (CollidingColumns rest right keys)- ( If- (HasName n right)- (Column n (These a (Lookup n right)) ': CollidingColumns rest right keys)- (CollidingColumns rest right keys)- )---- | Inner join result schema.-type family InnerJoinSchema (keys :: [Symbol]) (left :: [Type]) (right :: [Type]) :: [Type] where- InnerJoinSchema keys left right =- Append- (SubsetSchema keys left)- ( Append- (UniqueLeft left (Append keys (ColumnNames right)))- ( Append- (UniqueLeft right (Append keys (ColumnNames left)))- (CollidingColumns left right keys)- )- )---- | Left join result schema.-type family LeftJoinSchema (keys :: [Symbol]) (left :: [Type]) (right :: [Type]) :: [Type] where- LeftJoinSchema keys left right =- Append- (SubsetSchema keys left)- ( Append- (UniqueLeft left (Append keys (ColumnNames right)))- ( Append- (WrapMaybe (UniqueLeft right (Append keys (ColumnNames left))))- (CollidingColumns left right keys)- )- )---- | Right join result schema.-type family RightJoinSchema (keys :: [Symbol]) (left :: [Type]) (right :: [Type]) :: [Type] where- RightJoinSchema keys left right =- Append- (SubsetSchema keys right)- ( Append- (WrapMaybe (UniqueLeft left (Append keys (ColumnNames right))))- ( Append- (UniqueLeft right (Append keys (ColumnNames left)))- (CollidingColumns left right keys)- )- )---- | Full outer join result schema.-type family- FullOuterJoinSchema (keys :: [Symbol]) (left :: [Type]) (right :: [Type]) ::- [Type]- where- FullOuterJoinSchema keys left right =- Append- (WrapMaybe (SubsetSchema keys left))- ( Append- (WrapMaybe (UniqueLeft left (Append keys (ColumnNames right))))- ( Append- (WrapMaybe (UniqueLeft right (Append keys (ColumnNames left))))- (CollidingColumns left right keys)- )- )------------------------------------------------------------------------------------ GroupBy helpers------------------------------------------------------------------------------------ | Extract Column entries from a schema whose names appear in @keys@.-type family GroupKeyColumns (keys :: [Symbol]) (cols :: [Type]) :: [Type] where- GroupKeyColumns keys '[] = '[]- GroupKeyColumns keys (Column n a ': rest) =- If- (IsElem n keys)- (Column n a ': GroupKeyColumns keys rest)- (GroupKeyColumns keys rest)---- | Provides runtime evidence of a schema: a list of (name, TypeRep) pairs.-class KnownSchema (cols :: [Type]) where- schemaEvidence :: [(T.Text, SomeTypeRep)]--instance KnownSchema '[] where- schemaEvidence = []--instance- (KnownSymbol name, Typeable a, Columnable a, KnownSchema rest) =>- KnownSchema (Column name a ': rest)- where- schemaEvidence =- (T.pack (symbolVal (Proxy @name)), someTypeRep (Proxy @a))- : schemaEvidence @rest---- | A class that provides a list of 'Text' values for a type-level list of Symbols.-class AllKnownSymbol (names :: [Symbol]) where- symbolVals :: [T.Text]--instance AllKnownSymbol '[] where- symbolVals = []--instance (KnownSymbol n, AllKnownSymbol ns) => AllKnownSymbol (n ': ns) where- symbolVals = T.pack (symbolVal (Proxy @n)) : symbolVals @ns
src/DataFrame/Typed/TH.hs view
@@ -1,94 +1,33 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TemplateHaskellQuotes #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.Typed.TH (- -- * Schema inference- deriveSchema,- deriveSchemaFromCsvFile,-- -- * Re-export for TH splices- TypedDataFrame,- Column,-) where--import Control.Monad.IO.Class-import qualified Data.List as L-import qualified Data.Map as M-import qualified Data.Text as T--import Language.Haskell.TH+{-# LANGUAGE CPP #-} -import qualified DataFrame.IO.CSV as D-import qualified DataFrame.Internal.Column as C-import qualified DataFrame.Internal.DataFrame as D-import DataFrame.Typed.Types (Column, TypedDataFrame)+{- |+Module : DataFrame.Typed.TH+License : MIT -{- | Generate a type synonym for a schema based on an existing 'DataFrame'.+Backwards-compatibility re-export hub for the split @DataFrame.Typed.TH.*@+modules: -@--}+ * "DataFrame.Typed.TH.Records" — record-based schema derivation (no file IO)+ * "DataFrame.Typed.TH.CSV" — CSV-file-based schema derivation (requires @-fwith-csv@) -{- $(deriveSchema \"IrisSchema\" irisDF)--- Generates: type IrisSchema = '[Column \"sepal_length\" Double, ...]-@+These live in @dataframe-th@ and @dataframe-csv-th@ respectively. The CSV+re-export is guarded with the @CSV_TH@ CPP define that the+meta-@dataframe@ package sets based on its cabal flags. -}--deriveSchema :: String -> D.DataFrame -> DecsQ-deriveSchema typeName df = do- let cols = getSchemaInfo df- let names = map fst cols- case findDuplicate names of- Just dup -> fail $ "Duplicate column name in DataFrame: " ++ T.unpack dup- Nothing -> pure ()- colTypes <- mapM mkColumnType cols- let schemaType = foldr (\t acc -> PromotedConsT `AppT` t `AppT` acc) PromotedNilT colTypes- let synName = mkName typeName- pure [TySynD synName [] schemaType]--deriveSchemaFromCsvFile :: String -> String -> DecsQ-deriveSchemaFromCsvFile typeName path = do- df <- liftIO (D.readCsv path)- deriveSchema typeName df--getSchemaInfo :: D.DataFrame -> [(T.Text, String)]-getSchemaInfo df =- let orderedNames =- map fst $- L.sortBy (\(_, a) (_, b) -> compare a b) $- M.toList (D.columnIndices df)- in map (\name -> (name, getColumnTypeStr name df)) orderedNames--getColumnTypeStr :: T.Text -> D.DataFrame -> String-getColumnTypeStr name df = case D.getColumn name df of- Just col -> C.columnTypeString col- Nothing -> error $ "Column not found: " ++ T.unpack name--mkColumnType :: (T.Text, String) -> Q Type-mkColumnType (name, tyStr) = do- ty <- parseTypeString tyStr- let nameLit = LitT (StrTyLit (T.unpack name))- pure $ ConT ''Column `AppT` nameLit `AppT` ty--parseTypeString :: String -> Q Type-parseTypeString "Int" = pure $ ConT ''Int-parseTypeString "Double" = pure $ ConT ''Double-parseTypeString "Float" = pure $ ConT ''Float-parseTypeString "Bool" = pure $ ConT ''Bool-parseTypeString "Char" = pure $ ConT ''Char-parseTypeString "String" = pure $ ConT ''String-parseTypeString "Text" = pure $ ConT ''T.Text-parseTypeString "Integer" = pure $ ConT ''Integer-parseTypeString s- | "Maybe " `L.isPrefixOf` s = do- inner <- parseTypeString (L.drop 6 s)- pure $ ConT ''Maybe `AppT` inner-parseTypeString s = fail $ "Unsupported column type in schema inference: " ++ s+module DataFrame.Typed.TH (+ module DataFrame.Typed.TH.Records,+#ifdef WITH_CSV_TH+ module DataFrame.Typed.TH.CSV,+#endif+#ifdef WITH_PARQUET_TH+ module DataFrame.Typed.TH.Parquet,+#endif+) where -findDuplicate :: (Eq a) => [a] -> Maybe a-findDuplicate [] = Nothing-findDuplicate (x : xs)- | x `elem` xs = Just x- | otherwise = findDuplicate xs+import DataFrame.Typed.TH.Records+#ifdef WITH_CSV_TH+import DataFrame.Typed.TH.CSV+#endif+#ifdef WITH_PARQUET_TH+import DataFrame.Typed.TH.Parquet+#endif
− src/DataFrame/Typed/Types.hs
@@ -1,117 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeOperators #-}--module DataFrame.Typed.Types (- -- * Core phantom-typed wrapper- TypedDataFrame (..),-- -- * Column phantom type (no constructors)- Column,-- -- * Typed expressions (schema-validated)- TExpr (..),-- -- * Typed sort orders- TSortOrder (..),-- -- * Grouped typed dataframe- TypedGrouped (..),-- -- * Typed aggregation builder (Option B)- TAgg (..),- taggToNamedExprs,-- -- * Re-export These- These (..),-) where--import Data.Kind (Type)-import Data.These (These (..))-import GHC.TypeLits (Symbol)--import qualified Data.Text as T-import DataFrame.Internal.Column (Columnable)-import qualified DataFrame.Internal.DataFrame as D-import DataFrame.Internal.Expression (Expr, NamedExpr, UExpr (..))--{- | A phantom-typed wrapper over the untyped 'DataFrame'.--The type parameter @cols@ is a type-level list of @Column name ty@ entries-that tracks the schema at compile time. All operations delegate to the-untyped core at runtime and update the phantom type at compile time.--}-newtype TypedDataFrame (cols :: [Type]) = TDF {unTDF :: D.DataFrame}--instance Show (TypedDataFrame cols) where- show (TDF df) = show df--instance Eq (TypedDataFrame cols) where- (TDF a) == (TDF b) = a == b--{- | A phantom type that pairs a type-level column name ('Symbol')-with its element type. Has no value-level constructors — used-purely at the type level to describe schemas.--}-data Column (name :: Symbol) (a :: Type)--{- | A typed expression validated against schema @cols@, producing values of type @a@.--Unlike the untyped 'Expr a', a 'TExpr' can only be constructed through-type-safe combinators ('col', 'lit', arithmetic operations) that verify-column references exist in the schema with the correct type.--Use 'unTExpr' to extract the underlying 'Expr' for delegation to the untyped API.--}-newtype TExpr (cols :: [Type]) a = TExpr {unTExpr :: Expr a}---- | A typed sort order validated against schema @cols@.-data TSortOrder (cols :: [Type]) where- Asc :: (Columnable a) => TExpr cols a -> TSortOrder cols- Desc :: (Columnable a) => TExpr cols a -> TSortOrder cols---- | A phantom-typed wrapper over 'GroupedDataFrame'.-newtype TypedGrouped (keys :: [Symbol]) (cols :: [Type])- = TGD {unTGD :: D.GroupedDataFrame}--{- | A typed aggregation builder (Option B).--Accumulates 'NamedExpr' values at the term level while building-the result schema at the type level. Each @agg@ call prepends a-'Column' to the @aggs@ phantom list.--Usage:--@-agg \@\"total\" (F.sum salary)- $ agg \@\"avg_age\" (F.mean age)- $ aggNil-@--}-data TAgg (keys :: [Symbol]) (cols :: [Type]) (aggs :: [Type]) where- TAggNil :: TAgg keys cols '[]- TAggCons ::- (Columnable a) =>- -- | column name- T.Text ->- -- | typed aggregation expression- TExpr cols a ->- -- | rest- TAgg keys cols aggs ->- TAgg keys cols (Column name a ': aggs)--{- | Extract the runtime 'NamedExpr' list from a 'TAgg', in-declaration order (reversed from the cons-built order).--}-taggToNamedExprs :: TAgg keys cols aggs -> [NamedExpr]-taggToNamedExprs = reverse . go- where- go :: TAgg keys cols aggs -> [NamedExpr]- go TAggNil = []- go (TAggCons name (TExpr expr) rest) = (name, UExpr expr) : go rest
tests/GenDataFrame.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -Wno-orphans #-} module GenDataFrame where @@ -14,9 +15,9 @@ genColumn :: Int -> Gen Column genColumn len = oneof- [ BoxedColumn . V.fromList <$> vectorOf len (arbitrary @Int)- , UnboxedColumn . VU.fromList <$> vectorOf len (arbitrary @Double)- , OptionalColumn . V.fromList <$> vectorOf len (arbitrary @(Maybe Int))+ [ BoxedColumn Nothing . V.fromList <$> vectorOf len (arbitrary @Int)+ , UnboxedColumn Nothing . VU.fromList <$> vectorOf len (arbitrary @Double)+ , fromVector . V.fromList <$> vectorOf len (arbitrary @(Maybe Int)) ] genDataFrame :: Gen DataFrame@@ -39,4 +40,4 @@ instance Arbitrary DataFrame where arbitrary = genDataFrame- shrink df = []+ shrink _df = []
+ tests/IO/CSV.hs view
@@ -0,0 +1,97 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module IO.CSV where++import qualified Data.ByteString.Lazy as BL+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import qualified DataFrame as D+import DataFrame.IO.CSV (fromCsv, fromCsvBytes)+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.DataFrame (DataFrame (..), toCsv)+import Test.HUnit (+ Test (TestCase, TestLabel),+ assertEqual,+ assertFailure,+ )++-- | Happy path: parse a simple CSV string with Int and Text columns.+fromCsvHappyPath :: Test+fromCsvHappyPath = TestLabel "fromCsv_happy_path" $ TestCase $ do+ result <- fromCsv "name,age\nAlice,30\nBob,25\nCharlie,35\n"+ case result of+ Left err -> assertFailure $ "Unexpected Left: " ++ err+ Right df -> do+ assertEqual "rows" 3 (D.nRows df)+ assertEqual "columns" 2 (D.nColumns df)++-- | Empty input should return a Left.+fromCsvEmpty :: Test+fromCsvEmpty = TestLabel "fromCsv_empty" $ TestCase $ do+ result <- fromCsv ""+ case result of+ Left _ -> return ()+ Right _ -> assertFailure "Expected Left for empty input"++-- | fromCsvBytes happy path.+fromCsvBytesHappyPath :: Test+fromCsvBytesHappyPath = TestLabel "fromCsvBytes_happy_path" $ TestCase $ do+ let bs = BL.fromStrict (TE.encodeUtf8 "x,y\n1,2\n3,4\n")+ df <- fromCsvBytes bs+ assertEqual "rows" 2 (D.nRows df)+ assertEqual "columns" 2 (D.nColumns df)++-- | Round trip: toCsv then fromCsv preserves data.+fromCsvRoundTrip :: Test+fromCsvRoundTrip = TestLabel "fromCsv_roundTrip" $ TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("a", DI.fromList @Int [1, 2, 3])+ , ("b", DI.fromList @T.Text ["hello", "world", "test"])+ ]+ let csvString = T.unpack (toCsv df)+ result <- fromCsv csvString+ case result of+ Left err -> assertFailure $ "Unexpected Left: " ++ err+ Right df' -> do+ assertEqual+ "round trip dimensions"+ (dataframeDimensions df)+ (dataframeDimensions df')+ assertEqual "round trip data" df df'++-- | Round trip via fromCsvBytes.+fromCsvBytesRoundTrip :: Test+fromCsvBytesRoundTrip = TestLabel "fromCsvBytes_roundTrip" $ TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("x", DI.fromList @Int [10, 20])+ , ("y", DI.fromList @Double [1.5, 2.5])+ ]+ let bs = BL.fromStrict (TE.encodeUtf8 (toCsv df))+ df' <- fromCsvBytes bs+ assertEqual+ "round trip dimensions"+ (dataframeDimensions df)+ (dataframeDimensions df')++-- | Single column CSV.+fromCsvSingleColumn :: Test+fromCsvSingleColumn = TestLabel "fromCsv_single_column" $ TestCase $ do+ result <- fromCsv "id\n10\n20\n30\n"+ case result of+ Left err -> assertFailure $ "Unexpected Left: " ++ err+ Right df -> do+ assertEqual "rows" 3 (D.nRows df)+ assertEqual "columns" 1 (D.nColumns df)++tests :: [Test]+tests =+ [ fromCsvHappyPath+ , fromCsvEmpty+ , fromCsvBytesHappyPath+ , fromCsvRoundTrip+ , fromCsvBytesRoundTrip+ , fromCsvSingleColumn+ ]
+ tests/IO/CsvGolden.hs view
@@ -0,0 +1,556 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Golden semantics for the default CSV reader, pinned against the+cassava-based oracle (2026-06-12) before the strict-scanner rewrite.+Ragged-row cases encode the one intentional change: pad-with-null (audit D6).+-}+module IO.CsvGolden (tests) where++import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BL+import qualified Data.Map.Strict as M+import qualified Data.Text as T++import Control.Exception (SomeException, evaluate, try)+import Data.List (isInfixOf)+import Data.Time.Calendar (Day, fromGregorian)+import DataFrame.IO.CSV+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.DataFrame (+ columnIndices,+ dataframeDimensions,+ forceDataFrame,+ getColumn,+ )+import DataFrame.Operations.Typing (SafeReadMode (..))+import DataFrame.Schema (schemaType)+import Test.HUnit++data Expect+ = Cols (Int, Int) [(T.Text, DI.Column)]+ | Err String++ints :: [Int] -> DI.Column+ints = DI.fromList+mints :: [Maybe Int] -> DI.Column+mints = DI.fromList+dbls :: [Double] -> DI.Column+dbls = DI.fromList+texts :: [T.Text] -> DI.Column+texts = DI.fromList+mtexts :: [Maybe T.Text] -> DI.Column+mtexts = DI.fromList+eti :: [Either T.Text Int] -> DI.Column+eti = DI.fromList+ett :: [Either T.Text T.Text] -> DI.Column+ett = DI.fromList+days :: [Day] -> DI.Column+days = DI.fromList+boolsC :: [Bool] -> DI.Column+boolsC = DI.fromList++sample :: Int -> ReadOptions+sample n = defaultReadOptions{typeSpec = InferFromSample n}++goldenCase :: (String, ReadOptions, BS.ByteString, Expect) -> Test+goldenCase (label, opts, input, expect) = TestLabel label $ TestCase $ do+ r <- try $ do+ df <- decodeSeparated opts (BL.fromStrict input)+ evaluate (forceDataFrame df)+ case (expect, r) of+ (Err sub, Left (e :: SomeException)) ->+ assertBool+ (label <> ": error should mention " <> show sub <> ", got " <> show e)+ (sub `isInfixOf` show e)+ (Err _, Right _) -> assertFailure (label <> ": expected an error")+ (Cols _ _, Left (e :: SomeException)) ->+ assertFailure (label <> ": unexpected error " <> show e)+ (Cols dims cols, Right df) -> do+ assertEqual (label <> ": dims") dims (dataframeDimensions df)+ mapM_+ ( \(name, expected) -> case getColumn name df of+ Nothing -> assertFailure (label <> ": missing column " <> show name)+ Just actual ->+ assertEqual (label <> ": column " <> show name) expected actual+ )+ cols++goldenCases :: [(String, ReadOptions, BS.ByteString, Expect)]+goldenCases =+ [+ ( "basic"+ , defaultReadOptions+ , "a,b,c\n1,2.5,x\n2,3.5,y\n"+ , Cols+ (2, 3)+ [("a", ints [1, 2]), ("b", dbls [2.5, 3.5]), ("c", texts ["x", "y"])]+ )+ ,+ ( "quoted_doubled"+ , defaultReadOptions+ , "a,b\n\"x,1\",\"he said \"\"hi\"\"\"\n\"multi\nline\",plain\n"+ , Cols+ (2, 2)+ [("a", texts ["x,1", "multi\nline"]), ("b", texts ["he said \"hi\"", "plain"])]+ )+ ,+ ( "crlf"+ , defaultReadOptions+ , "a,b\r\n1,x\r\n2,y\r\n"+ , Cols (2, 2) [("a", ints [1, 2]), ("b", texts ["x", "y"])]+ )+ ,+ ( "lone_cr"+ , defaultReadOptions+ , "a,b\r1,x\r2,y"+ , Cols (2, 2) [("a", ints [1, 2]), ("b", texts ["x", "y"])]+ )+ ,+ ( "blank_lines"+ , defaultReadOptions+ , "a,b\n\n1,x\n\n\n2,y\n\n"+ , Cols (2, 2) [("a", ints [1, 2]), ("b", texts ["x", "y"])]+ )+ ,+ ( "lone_cr_line"+ , defaultReadOptions+ , "a,b\r\n\r1,x\r\n"+ , Cols (1, 2) [("a", ints [1]), ("b", texts ["x"])]+ )+ ,+ ( "no_eof_newline"+ , defaultReadOptions+ , "a,b\n1,x"+ , Cols (1, 2) [("a", ints [1]), ("b", texts ["x"])]+ )+ ,+ ( "missing_numeric"+ , defaultReadOptions+ , "a,b\n1,2\nNA,3\n,4\nnan,5\n"+ , Cols+ (4, 2)+ [("a", mints [Just 1, Nothing, Nothing, Nothing]), ("b", ints [2, 3, 4, 5])]+ )+ ,+ ( "missing_text"+ , defaultReadOptions+ , "t,u\nfoo,1\nNA,2\n ,3\nbar,4\n"+ , Cols+ (4, 2)+ [ ("t", mtexts [Just "foo", Nothing, Nothing, Just "bar"])+ , ("u", ints [1, 2, 3, 4])+ ]+ )+ ,+ ( "ws_padding"+ , defaultReadOptions+ , "a,b\n 1 , x \n2,y\n"+ , Cols (2, 2) [("a", ints [1, 2]), ("b", texts ["x", "y"])]+ )+ ,+ ( "tabs_strip"+ , defaultReadOptions+ , "a,b\n\t1\t,\tx\t\n2,y\n"+ , Cols (2, 2) [("a", ints [1, 2]), ("b", texts ["x", "y"])]+ )+ ,+ ( "ragged_short_D6"+ , defaultReadOptions+ , "a,b,c\n1,2,3\n4,5\n6,7,8\n"+ , Cols+ (3, 3)+ [ ("a", ints [1, 4, 6])+ , ("b", ints [2, 5, 7])+ , ("c", mints [Just 3, Nothing, Just 8])+ ]+ )+ ,+ ( "ragged_long"+ , defaultReadOptions+ , "a,b\n1,2\n3,4,5\n6,7\n"+ , Cols (3, 2) [("a", ints [1, 3, 6]), ("b", ints [2, 4, 7])]+ )+ ,+ ( "all_rows_ragged_short_D6"+ , defaultReadOptions+ , "a,b,c\n1,2\n3,4\n"+ , Cols+ (2, 3)+ [("a", ints [1, 3]), ("b", ints [2, 4]), ("c", mtexts [Nothing, Nothing])]+ )+ ,+ ( "maybe_read_ragged_D6"+ , defaultReadOptions{safeRead = MaybeRead}+ , "a,b\n1,2\n3\n"+ , Cols (2, 2) [("a", mints [Just 1, Just 3]), ("b", mints [Just 2, Nothing])]+ )+ ,+ ( "either_read_ragged_D6"+ , defaultReadOptions{safeRead = EitherRead}+ , "a,b\n1,2\n3\n"+ , Cols (2, 2) [("a", eti [Right 1, Right 3]), ("b", eti [Right 2, Left ""])]+ )+ , ("stray_quote_mid", defaultReadOptions, "a,b\nx\"y,2\n", Err "")+ , ("quote_garbage", defaultReadOptions, "a,b\n\"x\"y,2\n", Err "")+ , ("space_then_quote", defaultReadOptions, "a,b\n \"x\",2\n", Err "")+ , ("quote_space_garbage", defaultReadOptions, "a,b\n\"x\" ,2\n", Err "")+ , ("quote_at_field_end", defaultReadOptions, "a,b\n1,x\"\n", Err "")+ , ("doubled_then_garbage", defaultReadOptions, "a,b\n\"\"x,2\n", Err "")+ ,+ ( "quoted_empty"+ , defaultReadOptions+ , "a,b\n\"\",2\n"+ , Cols (1, 2) [("a", mtexts [Nothing]), ("b", ints [2])]+ )+ ,+ ( "quoted_empty_row_skipped"+ , defaultReadOptions+ , "a\nx\n\"\"\ny\n"+ , Cols (2, 1) [("a", texts ["x", "y"])]+ )+ ,+ ( "noheader"+ , defaultReadOptions{headerSpec = NoHeader}+ , "1,2\n3,4\n"+ , Cols (2, 2) [("0", ints [1, 3]), ("1", ints [2, 4])]+ )+ ,+ ( "providenames_fewer"+ , defaultReadOptions{headerSpec = ProvideNames ["x"]}+ , "1,2\n3,4\n"+ , Cols (2, 2) [("x", ints [1, 3]), ("1", ints [2, 4])]+ )+ ,+ ( "providenames_more_D6"+ , defaultReadOptions{headerSpec = ProvideNames ["x", "y", "z"]}+ , "1,2\n3,4\n"+ , Cols+ (2, 3)+ [("x", ints [1, 3]), ("y", ints [2, 4]), ("z", mtexts [Nothing, Nothing])]+ )+ ,+ ( "either_read"+ , (sample 2){safeRead = EitherRead}+ , "a,b\n1,x\n2,y\nz,\n"+ , Cols+ (3, 2)+ [ ("a", eti [Right 1, Right 2, Left "z"])+ , ("b", ett [Right "x", Right "y", Left ""])+ ]+ )+ ,+ ( "maybe_read"+ , defaultReadOptions{safeRead = MaybeRead}+ , "a,b\n1,x\nNA,y\n"+ , Cols (2, 2) [("a", mints [Just 1, Nothing]), ("b", mtexts [Just "x", Just "y"])]+ )+ ,+ ( "schema_int_bad"+ , defaultReadOptions{typeSpec = SpecifyTypes [("a", schemaTypeInt)] NoInference}+ , "a,b\n1,p\nx,q\n3,r\n"+ , Cols+ (3, 2)+ [("a", mints [Just 1, Nothing, Just 3]), ("b", texts ["p", "q", "r"])]+ )+ ,+ ( "schema_int_missing"+ , defaultReadOptions{typeSpec = SpecifyTypes [("a", schemaTypeInt)] NoInference}+ , "a\n1\nNA\n\n3\n"+ , Cols (3, 1) [("a", mints [Just 1, Nothing, Just 3])]+ )+ ,+ ( "schema_text_missing"+ , defaultReadOptions{typeSpec = SpecifyTypes [("a", schemaTypeText)] NoInference}+ , "a\nx\nNA\n\nz\n"+ , Cols (3, 1) [("a", mtexts [Just "x", Nothing, Just "z"])]+ )+ ,+ ( "dates"+ , defaultReadOptions+ , "d\n2024-01-02\n2024-02-03\n"+ , Cols (2, 1) [("d", days [fromGregorian 2024 1 2, fromGregorian 2024 2 3])]+ )+ ,+ ( "dates_custom"+ , defaultReadOptions{dateFormat = "%d/%m/%Y"}+ , "d\n02/01/2024\n03/02/2024\n"+ , Cols (2, 1) [("d", days [fromGregorian 2024 1 2, fromGregorian 2024 2 3])]+ )+ ,+ ( "custom_sep"+ , defaultReadOptions{columnSeparator = ';'}+ , "a;b\n1;x\n2;y\n"+ , Cols (2, 2) [("a", ints [1, 2]), ("b", texts ["x", "y"])]+ )+ ,+ ( "custom_missing"+ , defaultReadOptions{missingIndicators = ["foo"]}+ , "a,b\n1,foo\n2,bar\n"+ , Cols (2, 2) [("a", ints [1, 2]), ("b", mtexts [Nothing, Just "bar"])]+ )+ ,+ ( "bools"+ , defaultReadOptions+ , "f\nTrue\nFalse\n"+ , Cols (2, 1) [("f", boolsC [True, False])]+ )+ ,+ ( "bool_ws"+ , defaultReadOptions+ , "f\n True\nFalse\n"+ , Cols (2, 1) [("f", boolsC [True, False])]+ )+ ,+ ( "single_space_field"+ , defaultReadOptions+ , "a,b\nx, \ny,z\n"+ , Cols (2, 2) [("a", texts ["x", "y"]), ("b", mtexts [Nothing, Just "z"])]+ )+ ,+ ( "row_cap"+ , defaultReadOptions{numColumns = Just 2}+ , "a\n1\n2\n3\n4\n"+ , Cols (2, 1) [("a", ints [1, 2])]+ )+ ,+ ( "row_cap_zero"+ , defaultReadOptions{numColumns = Just 0}+ , "a,b\n1,2\n"+ , Cols (0, 2) [("a", mtexts []), ("b", mtexts [])]+ )+ ,+ ( "trailing_sep"+ , defaultReadOptions+ , "a,b\n1,\n2,3\n"+ , Cols (2, 2) [("a", ints [1, 2]), ("b", mints [Nothing, Just 3])]+ )+ , ("header_only", defaultReadOptions, "a,b\n", Err "Empty CSV file")+ , ("empty", defaultReadOptions, "", Err "Empty CSV file")+ , ("only_blank_lines", defaultReadOptions, "\n\n\n", Err "Empty CSV file")+ ,+ ( "all_null_col"+ , defaultReadOptions+ , "a,b\n,1\n,2\n"+ , Cols (2, 2) [("a", mtexts [Nothing, Nothing]), ("b", ints [1, 2])]+ )+ ,+ ( "int_overflow"+ , defaultReadOptions+ , "a\n9223372036854775808\n1\n"+ , Cols (2, 1) [("a", dbls [9.223372036854776e18, 1.0])]+ )+ ,+ ( "mixed_int_double"+ , defaultReadOptions+ , "a\n1\n2.5\n"+ , Cols (2, 1) [("a", dbls [1.0, 2.5])]+ )+ ,+ ( "int_then_text"+ , sample 2+ , "a\n1\n2\nx\n"+ , Cols (3, 1) [("a", texts ["1", "2", "x"])]+ )+ ,+ ( "int_then_double"+ , sample 2+ , "a\n1\n2\n3.5\n"+ , Cols (3, 1) [("a", dbls [1.0, 2.0, 3.5])]+ )+ ,+ ( "sample_all_null_then_data"+ , sample 2+ , "a\n\n\n5\n7\n"+ , Cols (2, 1) [("a", ints [5, 7])]+ )+ ,+ ( "quoted_number"+ , defaultReadOptions+ , "a\n\"1\"\n\"2\"\n"+ , Cols (2, 1) [("a", ints [1, 2])]+ )+ ,+ ( "quoted_padded_number"+ , defaultReadOptions+ , "a\n\" 1 \"\n\"2\"\n"+ , Cols (2, 1) [("a", ints [1, 2])]+ )+ ,+ ( "quoted_header"+ , defaultReadOptions+ , "\"a b\",c\n1,2\n"+ , Cols (1, 2) [("a b", ints [1]), ("c", ints [2])]+ )+ ,+ ( "header_quoted_doubled"+ , defaultReadOptions+ , "\"a\"\"b\",c\n1,2\n"+ , Cols (1, 2) [("a\"b", ints [1]), ("c", ints [2])]+ )+ ,+ ( "quoted_crlf_field"+ , defaultReadOptions+ , "a,b\r\n\"x\r\ny\",2\r\n"+ , Cols (1, 2) [("a", texts ["x\r\ny"]), ("b", ints [2])]+ )+ ,+ ( "quoted_field_with_cr_alone"+ , defaultReadOptions+ , "a,b\n\"x\ry\",2\n"+ , Cols (1, 2) [("a", texts ["x\ry"]), ("b", ints [2])]+ )+ ,+ ( "tab_sep_quoted"+ , defaultReadOptions{columnSeparator = '\t'}+ , "a\tb\n\"x\t1\"\ty\n"+ , Cols (1, 2) [("a", texts ["x\t1"]), ("b", texts ["y"])]+ )+ ,+ ( "either_disables_missing"+ , defaultReadOptions{safeReadOverrides = [("b", EitherRead)]}+ , "a,b\nNA,1\n2,2\n"+ , Cols (2, 2) [("a", texts ["NA", "2"]), ("b", eti [Right 1, Right 2])]+ )+ ,+ ( "unclosed_quote_D6"+ , defaultReadOptions+ , "a,b\n\"x,2\n"+ , Cols (1, 2) [("a", texts ["x,2"]), ("b", mtexts [Nothing])]+ )+ ,+ ( "unclosed_trailing_doubled_D6"+ , defaultReadOptions+ , "a,b\n\"x\"\"\n"+ , Cols (1, 2) [("a", texts ["x\""]), ("b", mtexts [Nothing])]+ )+ ,+ ( "closed_after_doubled_D6"+ , defaultReadOptions+ , "a,b\n\"x\"\"\"\n"+ , Cols (1, 2) [("a", texts ["x\""]), ("b", mtexts [Nothing])]+ )+ ,+ ( "multi_doubled"+ , defaultReadOptions+ , "a\n\"x\"\"y\"\"z\"\nw\n"+ , Cols (2, 1) [("a", texts ["x\"y\"z", "w"])]+ )+ ,+ ( "only_doubled"+ , defaultReadOptions+ , "a\n\"\"\"\"\nw\n"+ , Cols (2, 1) [("a", texts ["\"", "w"])]+ )+ ,+ ( "doubled_at_open"+ , defaultReadOptions+ , "a,b\n\"\"\"x\",2\n"+ , Cols (1, 2) [("a", texts ["\"x"]), ("b", ints [2])]+ )+ ,+ ( "interior_doubled_inferred"+ , defaultReadOptions+ , "a\n\"x\"\"y\"\nz\n"+ , Cols (2, 1) [("a", texts ["x\"y", "z"])]+ )+ ,+ ( "trailing_cr_eof"+ , defaultReadOptions+ , "a,b\n1,x\r"+ , Cols (1, 2) [("a", ints [1]), ("b", texts ["x"])]+ )+ ,+ ( "single_col_blank"+ , defaultReadOptions+ , "a\n1\n\n2\n"+ , Cols (2, 1) [("a", ints [1, 2])]+ )+ ,+ ( "single_col_quoted_empty"+ , defaultReadOptions+ , "a\n1\n\"\"\n2\n"+ , Cols (2, 1) [("a", ints [1, 2])]+ )+ ,+ ( "row_only_sep"+ , defaultReadOptions+ , "a,b\n,\n1,2\n"+ , Cols (2, 2) [("a", mints [Nothing, Just 1]), ("b", mints [Nothing, Just 2])]+ )+ ,+ ( "sep_at_eof"+ , defaultReadOptions+ , "a,b\n1,2\n3,"+ , Cols (2, 2) [("a", ints [1, 3]), ("b", mints [Just 2, Nothing])]+ )+ ,+ ( "noheader_ragged_grow"+ , defaultReadOptions{headerSpec = NoHeader}+ , "1,2\n3,4,5\n"+ , Cols (2, 2) [("0", ints [1, 3]), ("1", ints [2, 4])]+ )+ ,+ ( "quoted_last_no_nl"+ , defaultReadOptions+ , "a,b\n1,2\n\"x\",3"+ , Cols (2, 2) [("a", texts ["1", "x"]), ("b", ints [2, 3])]+ )+ ,+ ( "empty_quoted_middle_col"+ , defaultReadOptions+ , "a,b,c\n1,\"\",3\n4,5,6\n"+ , Cols+ (2, 3)+ [("a", ints [1, 4]), ("b", mints [Nothing, Just 5]), ("c", ints [3, 6])]+ )+ ,+ ( "exp_double"+ , defaultReadOptions+ , "a\n1e3\n1E-3\n1e999\n"+ , Cols (3, 1) [("a", dbls [1000.0, 1.0e-3, 1 / 0])]+ )+ ,+ ( "five_field_double"+ , defaultReadOptions+ , "a\n+.5\n5.\n"+ , Cols (2, 1) [("a", texts ["+.5", "5."])]+ )+ ,+ ( "raw_invalid_utf8"+ , defaultReadOptions+ , BS.pack [97, 10, 255, 10, 98, 10]+ , Cols (2, 1) [("a", texts ["\65533", "b"])]+ )+ ,+ ( "raw_nbsp_edges_stripped"+ , defaultReadOptions+ , BS.pack [97, 10, 160, 120, 160, 10, 98, 10]+ , Cols (2, 1) [("a", texts ["x", "b"])]+ )+ ]+ where+ schemaTypeInt = schemaType @Int+ schemaTypeText = schemaType @T.Text++-- Duplicate header names: last index wins in the map; both columns kept.+dupHeaders :: Test+dupHeaders = TestLabel "dup_headers" $ TestCase $ do+ df <- decodeSeparated defaultReadOptions (BL.fromStrict "a,a\n1,2\n3,4\n")+ assertEqual "dims" (2, 2) (dataframeDimensions df)+ assertEqual "index map" [("a", 1)] (M.toList (columnIndices df))++-- Ingest pin: a string column freezes to a shared-buffer 'PackedText'+-- (no per-row boxed Text header), while values stay byte-identical.+textFreezesPacked :: Test+textFreezesPacked = TestLabel "text_freezes_packed" $ TestCase $ do+ df <- decodeSeparated defaultReadOptions (BL.fromStrict "a,b\n1,x\n2,y\n")+ case getColumn "b" df of+ Nothing -> assertFailure "missing text column"+ Just col -> do+ assertBool "text column is PackedText" (DI.isPackedText col)+ assertEqual "values byte-identical" (texts ["x", "y"]) col++tests :: [Test]+tests = dupHeaders : textFreezesPacked : map goldenCase goldenCases
+ tests/IR/ExprJsonRoundtrip.hs view
@@ -0,0 +1,209 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Round-trip, pipeline, end-to-end, and wire-shape contract tests for the+expression/pipeline serializer.+-}+module IR.ExprJsonRoundtrip (tests) where++import qualified Data.Aeson as Aeson+import qualified Data.ByteString.Char8 as BS8+import Data.Either (fromRight, isLeft)+import Test.HUnit++import qualified Data.Vector.Unboxed as VU+import qualified DataFrame as D+import qualified DataFrame.Functions as F+import DataFrame.Internal.Column (Columnable, TypedColumn (..), toVector)+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.Expression (Expr, UExpr (..))+import DataFrame.Internal.Interpreter (interpret)+import DataFrame.Operators (ifThenElse, (.>.))++import DataFrame.LinearModel (defaultLinearConfig)+import DataFrame.Model (fit, predict)+import DataFrame.Transform (Transform (..), applyTransform)+import DataFrame.Transform.Serialize (+ loadTransformFromFile,+ saveTransformToFile,+ )++import DataFrame.Expr.Serialize (+ SomeExpr (..),+ decodeExprAny,+ decodeExprAt,+ decodeExprFromBytes,+ encodeExpr,+ loadExprAtFromFile,+ loadPipelineFromFile,+ saveExprToFile,+ savePipelineToFile,+ )+import System.IO.Temp (withSystemTempDirectory)++interpD :: D.DataFrame -> Expr Double -> [Double]+interpD df e = case interpret @Double df e of+ Right (TColumn c) -> either (const []) VU.toList (toVector @Double @VU.Vector c)+ Left err -> error (show err)++close :: [Double] -> [Double] -> Bool+close a b = length a == length b && and (zipWith (\x y -> abs (x - y) <= 1e-9) a b)++df3 :: D.DataFrame+df3 = D.fromNamedColumns [("x", DI.fromList [1, 2, 3 :: Double])]++regDF :: D.DataFrame+regDF =+ D.fromNamedColumns+ [ ("x1", DI.fromList xs1)+ , ("x2", DI.fromList xs2)+ , ("y", DI.fromList [2 * a - 3 * b + 1 | (a, b) <- zip xs1 xs2])+ ]+ where+ xs1 = [1, 2, 3, 4, 5, 6, 7, 8] :: [Double]+ xs2 = [2, 1, 4, 3, 6, 5, 8, 7] :: [Double]++{- | An expression round-trips when encode → decode → re-encode yields the same+JSON. Avoids needing an 'Eq' instance on the GADT.+-}+roundtrips :: (Columnable a) => Expr a -> Bool+roundtrips e = fromRight False $ do+ v1 <- encodeExpr e+ SomeExpr _ e' <- decodeExprAny v1+ v2 <- encodeExpr e'+ Right (v1 == v2)++testCase :: String -> Bool -> Test+testCase name ok = TestCase (assertBool name ok)++-- Row-wise expression node kinds.+exprRoundtripTests :: [Test]+exprRoundtripTests =+ [ testCase "col" (roundtrips (F.col @Double "x"))+ , testCase "lit" (roundtrips (F.lit @Int 42))+ , testCase "arithmetic" (roundtrips (F.col @Double "x" * F.lit 2 + F.lit 1))+ , testCase "division" (roundtrips (F.col @Double "x" / F.lit 2))+ , testCase "comparison" (roundtrips (F.col @Double "x" .>. F.lit 0))+ , testCase "unary sqrt" (roundtrips (sqrt (F.col @Double "x")))+ , testCase+ "if/then/else"+ ( roundtrips+ ( ifThenElse (F.col @Double "x" .>. F.lit 0) (F.lit 1.0) (F.lit (-1.0)) ::+ Expr Double+ )+ )+ ]++-- Aggregation and window node kinds (the decoder cases added for full parity).+aggRoundtripTests :: [Test]+aggRoundtripTests =+ [ testCase "sum" (roundtrips (F.sum (F.col @Double "x")))+ , testCase "count" (roundtrips (F.count (F.col @Double "x")))+ , testCase "minimum" (roundtrips (F.minimum (F.col @Double "x")))+ , testCase "maximum" (roundtrips (F.maximum (F.col @Int "x")))+ , testCase "mean" (roundtrips (F.mean (F.col @Double "x")))+ , testCase "variance" (roundtrips (F.variance (F.col @Double "x")))+ , testCase "median" (roundtrips (F.median (F.col @Double "x")))+ , testCase "over" (roundtrips (F.over ["g"] (F.sum (F.col @Double "x"))))+ ]++-- Documented limitation: collect's list-typed output has no type tag, so it+-- cannot be encoded.+limitationTests :: [Test]+limitationTests =+ [ testCase+ "collect cannot encode"+ (isLeft (encodeExpr (F.collect (F.col @Double "x"))))+ ]++-- Pin the wire shape Python decodes against: (x * 2) + 1 over doubles.+contractTests :: [Test]+contractTests =+ [ TestCase $ case Aeson.eitherDecodeStrict (BS8.pack contractJson) >>= decodeExprAt @Double of+ Left err -> assertFailure ("contract decode failed: " <> err)+ Right e ->+ assertBool "contract interprets to [3,5,7]" (close (interpD df3 e) [3, 5, 7])+ , testCase+ "collect agg node fails to decode"+ (isLeft (decodeExprFromBytes (BS8.pack collectJson)))+ ]++contractJson :: String+contractJson =+ "{\"node\":\"binary\",\"out_type\":\"double\",\"op\":\"add\",\"arg_type\":\"double\","+ <> "\"lhs\":{\"node\":\"binary\",\"out_type\":\"double\",\"op\":\"mult\",\"arg_type\":\"double\","+ <> "\"lhs\":{\"node\":\"col\",\"out_type\":\"double\",\"name\":\"x\"},"+ <> "\"rhs\":{\"node\":\"lit\",\"out_type\":\"double\",\"value\":2.0}},"+ <> "\"rhs\":{\"node\":\"lit\",\"out_type\":\"double\",\"value\":1.0}}"++collectJson :: String+collectJson =+ "{\"node\":\"agg\",\"out_type\":\"double\",\"agg\":\"collect\",\"arg_type\":\"double\","+ <> "\"arg\":{\"node\":\"col\",\"out_type\":\"double\",\"name\":\"x\"}}"++-- File round-trips for a single expression, a pipeline, and a fitted model.+ioTests :: [Test]+ioTests =+ [ TestCase $ withSystemTempDirectory "expr-ser" $ \dir -> do+ let fp = dir ++ "/expr.json"+ e = F.col @Double "x" * F.lit 2 + F.lit 1+ sr <- saveExprToFile fp e+ assertEqual "save expr ok" (Right ()) sr+ loaded <- loadExprAtFromFile @Double fp+ case loaded of+ Left err -> assertFailure ("load expr failed: " <> err)+ Right e' ->+ assertBool+ "loaded expr interprets equal"+ (close (interpD df3 e') (interpD df3 e))+ , TestCase $ withSystemTempDirectory "expr-ser" $ \dir -> do+ let fp = dir ++ "/pipeline.json"+ nes =+ [ ("z", UExpr (F.col @Double "x" * F.lit 2))+ , ("w", UExpr (F.col @Double "x" + F.lit 10))+ ]+ sr <- savePipelineToFile fp nes+ assertEqual "save pipeline ok" (Right ()) sr+ loaded <- loadPipelineFromFile fp+ case loaded of+ Left err -> assertFailure ("load pipeline failed: " <> err)+ Right nes' -> do+ let df' = applyTransform (Transform nes') df3+ assertBool "z = 2x" (close (interpD df' (F.col @Double "z")) [2, 4, 6])+ assertBool "w = x+10" (close (interpD df' (F.col @Double "w")) [11, 12, 13])+ , TestCase $ withSystemTempDirectory "expr-ser" $ \dir -> do+ let fp = dir ++ "/transform.json"+ t = Transform [("z", UExpr (F.col @Double "x" * F.lit 2))]+ sr <- saveTransformToFile fp t+ assertEqual "save transform ok" (Right ()) sr+ loaded <- loadTransformFromFile fp+ case loaded of+ Left err -> assertFailure ("load transform failed: " <> err)+ Right t' ->+ assertBool+ "transform z = 2x"+ (close (interpD (applyTransform t' df3) (F.col @Double "z")) [2, 4, 6])+ , TestCase $ withSystemTempDirectory "expr-ser" $ \dir -> do+ -- End-to-end: fit OLS, persist its prediction, reload, compare outputs.+ let fp = dir ++ "/model.json"+ m = fit defaultLinearConfig (F.col @Double "y") regDF+ sr <- saveExprToFile fp (predict m)+ assertEqual "save model ok" (Right ()) sr+ loaded <- loadExprAtFromFile @Double fp+ case loaded of+ Left err -> assertFailure ("load model failed: " <> err)+ Right e' ->+ assertBool+ "reloaded prediction matches original"+ (close (interpD regDF e') (interpD regDF (predict m)))+ ]++tests :: [Test]+tests =+ exprRoundtripTests+ ++ aggRoundtripTests+ ++ limitationTests+ ++ contractTests+ ++ ioTests
+ tests/Internal/ColumnBuilder.hs view
@@ -0,0 +1,298 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++module Internal.ColumnBuilder (tests, props) where++import qualified Data.ByteString as B+import qualified Data.ByteString.Unsafe as BU+import qualified Data.Text as T+import qualified Data.Text.Array as A+import qualified Data.Vector as VB+import qualified Data.Vector.Unboxed as VU++import Control.Monad (forM_, zipWithM_)+import Control.Monad.ST (runST, stToIO)+import Data.Maybe (catMaybes, isNothing)+import Data.Text.Encoding (decodeUtf8Lenient, encodeUtf8)+import Data.Type.Equality (testEquality, (:~:) (Refl))+import Data.Word (Word8)+import DataFrame.Internal.Column hiding (mergeColumns)+import DataFrame.Internal.ColumnBuilder+import Foreign.Ptr (castPtr)+import Test.HUnit+import Test.QuickCheck+import Type.Reflection (typeRep)++-- Build a column by appending each list element (Nothing => appendNull).+buildIntColumn :: [Maybe Int] -> Column+buildIntColumn xs = runST $ do+ b <- newIntBuilder (length xs)+ mapM_ (maybe (appendNull b) (appendInt b)) xs+ freezeBuilder b++buildDoubleColumn :: [Maybe Double] -> Column+buildDoubleColumn xs = runST $ do+ b <- newDoubleBuilder (length xs)+ mapM_ (maybe (appendNull b) (appendDouble b)) xs+ freezeBuilder b++buildTextColumn :: [Maybe T.Text] -> Column+buildTextColumn xs = runST $ do+ b <- newTextBuilder (length xs) 16+ mapM_ (maybe (appendNull b) (appendText b)) xs+ freezeBuilder b++-- Build a text column from raw byte fields via appendTextSlice.+buildTextColumnFromBytes :: [[Word8]] -> Column+buildTextColumnFromBytes fields = runST $ do+ b <- newTextBuilder (length fields) 16+ forM_ fields $ \ws ->+ appendTextSlice b (arrayFromBytes ws) 0 (length ws)+ freezeBuilder b++arrayFromBytes :: [Word8] -> A.Array+arrayFromBytes ws = A.run $ do+ m <- A.new (length ws)+ zipWithM_ (A.unsafeWrite m) [0 ..] ws+ pure m++-- Reference column: fromVector of the plain list (no nulls) or Maybe list.+expectedColumn ::+ forall a.+ (Columnable a, Columnable (Maybe a)) =>+ [Maybe a] -> Column+expectedColumn xs+ | any isNothing xs = fromVector (VB.fromList xs)+ | otherwise = fromVector (VB.fromList (catMaybes xs))++-- Read a text column back as a Maybe list, respecting the bitmap.+-- Ingest now freezes text to 'PackedText'; materialize to the boxed form.+columnTexts :: Column -> [Maybe T.Text]+columnTexts c@(PackedText _ _) = columnTexts (materializePacked c)+columnTexts (BoxedColumn mb (v :: VB.Vector b)) =+ case testEquality (typeRep @b) (typeRep @T.Text) of+ Just Refl ->+ [ if maybe True (`bitmapTestBit` i) mb+ then Just (v VB.! i)+ else Nothing+ | i <- [0 .. VB.length v - 1]+ ]+ Nothing -> error "columnTexts: not a Text column"+columnTexts _ = error "columnTexts: not a boxed column"++-- Split a list into arbitrary contiguous chunks (at least one chunk).+splitsOf :: [a] -> Gen [[a]]+splitsOf [] = pure [[]]+splitsOf xs = go xs+ where+ go [] = pure []+ go ys = do+ k <- chooseInt (1, length ys)+ let (a, b) = splitAt k ys+ (a :) <$> go b++nullableVersion :: [Maybe a] -> String -> String -> String+nullableVersion xs nullable plain =+ if any isNothing xs then nullable else plain++-- HUnit tests++intAllValidHasNoBitmap :: Test+intAllValidHasNoBitmap =+ TestCase $+ assertEqual+ "all-valid int column is Unboxed with no bitmap"+ "Unboxed"+ (columnVersionString (buildIntColumn (map Just [1, 2, 3])))++intWithNullHasBitmap :: Test+intWithNullHasBitmap =+ TestCase $+ assertEqual+ "int column with null is NullableUnboxed"+ "NullableUnboxed"+ (columnVersionString (buildIntColumn [Just 1, Nothing]))++intNullSentinelIsZero :: Test+intNullSentinelIsZero = TestCase $+ case buildIntColumn [Just 5, Nothing, Just 7] of+ UnboxedColumn (Just _) (v :: VU.Vector b) ->+ case testEquality (typeRep @b) (typeRep @Int) of+ Just Refl -> assertEqual "null slot holds 0" 0 (v VU.! 1)+ Nothing -> assertFailure "expected an Int column"+ _ -> assertFailure "expected a nullable unboxed column"++textAllValidHasNoBitmap :: Test+textAllValidHasNoBitmap =+ TestCase $+ assertEqual+ "all-valid text column is Boxed with no bitmap"+ "Boxed"+ (columnVersionString (buildTextColumn [Just "a", Just "bc"]))++builderLengthCounts :: Test+builderLengthCounts = TestCase $ do+ let n = runST $ do+ b <- newIntBuilder 4+ appendInt b 1+ appendNull b+ appendInt b 3+ builderLength b+ assertEqual "builderLength counts nulls and values" 3 n++growthPastCapacityHint :: Test+growthPastCapacityHint = TestCase $ do+ let xs = map Just [0 .. 9999 :: Int]+ col = runST $ do+ b <- newIntBuilder 1+ mapM_ (maybe (appendNull b) (appendInt b)) xs+ freezeBuilder b+ assertEqual "doubling growth preserves data" (expectedColumn xs) col++multibyteTextRoundTrip :: Test+multibyteTextRoundTrip = TestCase $ do+ let ts = ["héllo", "wörld", "日本語", "😀😃", ""]+ assertEqual+ "multibyte text round trips"+ (map Just ts)+ (columnTexts (buildTextColumn (map Just ts)))++-- A multibyte sequence split across two fields: the whole buffer is valid+-- UTF-8 but each field alone is not. Both must lenient-decode to U+FFFD.+splitMultibyteAcrossFields :: Test+splitMultibyteAcrossFields = TestCase $ do+ let fields = [[0xC2], [0x80]]+ assertEqual+ "fields splitting a sequence decode leniently"+ (map (Just . decodeUtf8Lenient . B.pack) fields)+ (columnTexts (buildTextColumnFromBytes fields))++splitEuroAcrossFields :: Test+splitEuroAcrossFields = TestCase $ do+ let fields = [[0xE2, 0x82], [0xAC, 0x61]]+ assertEqual+ "euro sign split across fields decodes leniently"+ (map (Just . decodeUtf8Lenient . B.pack) fields)+ (columnTexts (buildTextColumnFromBytes fields))++invalidUtf8EdgeCases :: Test+invalidUtf8EdgeCases = TestCase $ do+ let fields =+ [ [0xE2, 0x28, 0xA1]+ , [0xF0, 0x9F, 0x98]+ , [0xF0, 0x28]+ , [0xC2]+ , [0x80]+ , [0xED, 0xA0, 0x80]+ , [0xF4, 0x90, 0x80, 0x80]+ , [0xC0, 0xAF]+ , [0x61, 0xF1, 0x80, 0x80, 0xE1, 0x80, 0xC2, 0x62]+ , [0xF0, 0x9F, 0x98, 0x80]+ ]+ assertEqual+ "invalid sequences match decodeUtf8Lenient"+ (map (Just . decodeUtf8Lenient . B.pack) fields)+ (columnTexts (buildTextColumnFromBytes fields))++appendFromPtrMatchesText :: Test+appendFromPtrMatchesText = TestCase $ do+ let t = "héllo wörld 😀" :: T.Text+ col <- BU.unsafeUseAsCStringLen (encodeUtf8 t) $ \(p, len) ->+ stToIO $ do+ b <- newTextBuilder 1 0+ appendTextSliceFromPtr b (castPtr p) len+ freezeBuilder b+ assertEqual "ptr slice decodes to original text" [Just t] (columnTexts col)++mergeBitmapSpliceUnaligned :: Test+mergeBitmapSpliceUnaligned = TestCase $ do+ let nullIdx = [1, 4, 9, 10, 14] :: [Int]+ xs = [if i `elem` nullIdx then Nothing else Just i | i <- [0 .. 14]]+ chunks = [take 3 xs, take 5 (drop 3 xs), drop 8 xs]+ merged = mergeColumns (map buildIntColumn chunks)+ assertEqual "merged equals unsplit" (buildIntColumn xs) merged+ forM_ [0 .. 14] $ \i ->+ assertEqual+ ("null at index " ++ show i)+ (i `elem` nullIdx)+ (columnElemIsNull merged i)++tests :: [Test]+tests =+ [ TestLabel "intAllValidHasNoBitmap" intAllValidHasNoBitmap+ , TestLabel "intWithNullHasBitmap" intWithNullHasBitmap+ , TestLabel "intNullSentinelIsZero" intNullSentinelIsZero+ , TestLabel "textAllValidHasNoBitmap" textAllValidHasNoBitmap+ , TestLabel "builderLengthCounts" builderLengthCounts+ , TestLabel "growthPastCapacityHint" growthPastCapacityHint+ , TestLabel "multibyteTextRoundTrip" multibyteTextRoundTrip+ , TestLabel "splitMultibyteAcrossFields" splitMultibyteAcrossFields+ , TestLabel "splitEuroAcrossFields" splitEuroAcrossFields+ , TestLabel "invalidUtf8EdgeCases" invalidUtf8EdgeCases+ , TestLabel "appendFromPtrMatchesText" appendFromPtrMatchesText+ , TestLabel "mergeBitmapSpliceUnaligned" mergeBitmapSpliceUnaligned+ ]++-- QuickCheck properties++prop_intMatchesFromVector :: [Maybe Int] -> Property+prop_intMatchesFromVector xs =+ buildIntColumn xs === expectedColumn xs+ .&&. columnVersionString (buildIntColumn xs)+ === nullableVersion xs "NullableUnboxed" "Unboxed"++prop_doubleMatchesFromVector :: [Maybe Double] -> Property+prop_doubleMatchesFromVector xs =+ buildDoubleColumn xs === expectedColumn xs++prop_textMatchesFromVector :: [Maybe String] -> Property+prop_textMatchesFromVector ss =+ let xs = map (fmap T.pack) ss+ in buildTextColumn xs === expectedColumn xs+ .&&. columnVersionString (buildTextColumn xs)+ === nullableVersion xs "NullableBoxed" "Boxed"++prop_textSliceMatchesLenientDecode :: [[Word8]] -> Property+prop_textSliceMatchesLenientDecode fields =+ columnTexts (buildTextColumnFromBytes fields)+ === map (Just . decodeUtf8Lenient . B.pack) fields++prop_mergeIntMatchesUnsplit :: [Maybe Int] -> Property+prop_mergeIntMatchesUnsplit xs = forAll (splitsOf xs) $ \chunks ->+ let merged = mergeColumns (map buildIntColumn chunks)+ unsplit = buildIntColumn xs+ in merged === unsplit+ .&&. columnVersionString merged === columnVersionString unsplit++prop_mergeDoubleMatchesUnsplit :: [Maybe Double] -> Property+prop_mergeDoubleMatchesUnsplit xs = forAll (splitsOf xs) $ \chunks ->+ mergeColumns (map buildDoubleColumn chunks) === buildDoubleColumn xs++prop_mergeTextMatchesUnsplit :: [Maybe String] -> Property+prop_mergeTextMatchesUnsplit ss =+ let xs = map (fmap T.pack) ss+ in forAll (splitsOf xs) $ \chunks ->+ let merged = mergeColumns (map buildTextColumn chunks)+ unsplit = buildTextColumn xs+ in merged === unsplit+ .&&. columnTexts merged === columnTexts unsplit++prop_mergeNullsLandAtRightRows :: [Maybe Int] -> Property+prop_mergeNullsLandAtRightRows xs = forAll (splitsOf xs) $ \chunks ->+ let merged = mergeColumns (map buildIntColumn chunks)+ in map isNothing xs+ === [columnElemIsNull merged i | i <- [0 .. length xs - 1]]++props :: [Property]+props =+ [ property prop_intMatchesFromVector+ , property prop_doubleMatchesFromVector+ , property prop_textMatchesFromVector+ , property prop_textSliceMatchesLenientDecode+ , property prop_mergeIntMatchesUnsplit+ , property prop_mergeDoubleMatchesUnsplit+ , property prop_mergeTextMatchesUnsplit+ , property prop_mergeNullsLandAtRightRows+ ]
+ tests/Internal/DictEncode.hs view
@@ -0,0 +1,144 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++{- | Correctness oracle for the dictionary-encode building block+('DataFrame.Internal.DictEncode'). A text column encodes to dense+first-appearance @Int@ codes: two rows share a code iff their text is equal, the+codes are a contiguous @0 .. card-1@ range in first-appearance order, packed and+boxed Text encode identically, and the cap parameter bails past a cardinality.+Pins the step that the group-by planner can route a text key through.+-}+module Internal.DictEncode (tests) where++import qualified Data.ByteString as B+import qualified Data.Map.Strict as M+import qualified Data.Text as T+import qualified Data.Text.Array as A+import qualified Data.Vector.Unboxed as VU++import Control.Monad (zipWithM_)+import Data.Maybe (isJust)+import Data.Text.Encoding (encodeUtf8)+import Data.Word (Word8)+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.DictEncode (dictEncodeColumn, dictEncodeColumnUpTo)+import DataFrame.Internal.PackedText (mkPackedContiguous)+import Test.HUnit++sampleRows :: [T.Text]+sampleRows =+ [ "apple"+ , "banana"+ , ""+ , "apple"+ , "Zebra"+ , "café"+ , "naïve"+ , "日本語"+ , "banana"+ , "apple"+ ]++arrayFromBytes :: [Word8] -> A.Array+arrayFromBytes ws = A.run $ do+ m <- A.new (length ws)+ zipWithM_ (A.unsafeWrite m) [0 ..] ws+ pure m++packedFromTexts :: [T.Text] -> DI.Column+packedFromTexts ts =+ let bytess = map (B.unpack . encodeUtf8) ts+ flat = concat bytess+ offs = scanl (+) 0 (map length bytess)+ arr = arrayFromBytes flat+ in DI.PackedText Nothing (mkPackedContiguous arr (VU.fromList offs))++boxedFromTexts :: [T.Text] -> DI.Column+boxedFromTexts = DI.fromList++{- | The reference dense first-appearance encoding computed with a plain+'Data.Map': scan in row order, assign the next id to each new value.+-}+oracleCodes :: [T.Text] -> ([Int], Int)+oracleCodes ts = go ts M.empty 0 []+ where+ go [] _ next acc = (reverse acc, next)+ go (x : xs) m next acc = case M.lookup x m of+ Just c -> go xs m next (c : acc)+ Nothing -> go xs (M.insert x next m) (next + 1) (next : acc)++codesMatchOracle :: String -> DI.Column -> Test+codesMatchOracle lbl col =+ TestCase $ case dictEncodeColumn col of+ Nothing -> assertFailure (lbl ++ ": expected Just codes")+ Just (codes, card) -> do+ let (refCodes, refCard) = oracleCodes sampleRows+ assertEqual (lbl ++ " codes") refCodes (VU.toList codes)+ assertEqual (lbl ++ " cardinality") refCard card++packedBoxedAgree :: Test+packedBoxedAgree =+ TestCase $+ assertEqual+ "packed and boxed encode identically"+ (dictEncodeColumn (boxedFromTexts sampleRows))+ (dictEncodeColumn (packedFromTexts sampleRows))++denseRange :: Test+denseRange =+ TestCase $ case dictEncodeColumn (packedFromTexts sampleRows) of+ Nothing -> assertFailure "expected Just"+ Just (codes, card) -> do+ assertBool "codes in [0,card)" (VU.all (\c -> c >= 0 && c < card) codes)+ assertEqual+ "all codes used"+ [0 .. card - 1]+ (M.keys (M.fromList [(c, ()) | c <- VU.toList codes]))++equalIffSameText :: Test+equalIffSameText =+ TestCase $ case dictEncodeColumn (packedFromTexts sampleRows) of+ Nothing -> assertFailure "expected Just"+ Just (codes, _) ->+ let pairs = [(i, j) | i <- idxs, j <- idxs, i < j]+ idxs = [0 .. length sampleRows - 1]+ ok (i, j) =+ (VU.unsafeIndex codes i == VU.unsafeIndex codes j)+ == (sampleRows !! i == sampleRows !! j)+ in assertBool "code equality matches text equality" (all ok pairs)++capBails :: Test+capBails =+ TestCase $ do+ -- sampleRows has 7 distinct values; a cap below that must bail.+ assertEqual+ "cap 3 bails"+ Nothing+ (dictEncodeColumnUpTo 3 (packedFromTexts sampleRows))+ -- a generous cap still encodes.+ assertBool+ "cap 100 encodes"+ (isJust (dictEncodeColumnUpTo 100 (packedFromTexts sampleRows)))++nonTextIsNothing :: Test+nonTextIsNothing =+ TestCase $+ assertEqual+ "int column is not dict-encoded"+ Nothing+ (dictEncodeColumn (DI.fromList [1 :: Int, 2, 3]))++tests :: [Test]+tests =+ [ TestLabel+ "dictCodesPacked"+ (codesMatchOracle "packed" (packedFromTexts sampleRows))+ , TestLabel+ "dictCodesBoxed"+ (codesMatchOracle "boxed" (boxedFromTexts sampleRows))+ , TestLabel "dictPackedBoxedAgree" packedBoxedAgree+ , TestLabel "dictDenseRange" denseRange+ , TestLabel "dictEqualIffSameText" equalIffSameText+ , TestLabel "dictCapBails" capBails+ , TestLabel "dictNonTextNothing" nonTextIsNothing+ ]
+ tests/Internal/Markdown.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE OverloadedStrings #-}++{- | Markdown rendering must escape pipe-table metacharacters in cell values,+or an operator name like @<|>@ splits the row into the wrong columns.+-}+module Internal.Markdown (tests) where++import qualified Data.Text as T++import DataFrame.Display.Terminal.PrettyPrint (escapeMarkdownCell)+import DataFrame.Internal.Column (fromList)+import qualified DataFrame.Internal.DataFrame as D+import Test.HUnit++-- Count the column-delimiter pipes in a rendered row: a bare '|' delimits a+-- cell; an escaped "\|" does not.+unescapedPipes :: T.Text -> Int+unescapedPipes = go . T.unpack+ where+ go ('\\' : '|' : rest) = go rest+ go ('|' : rest) = 1 + go rest+ go (_ : rest) = go rest+ go [] = 0++tests :: [Test]+tests =+ [ TestLabel "escapeMarkdownCell escapes a bare pipe" $+ TestCase (escapeMarkdownCell "f<|>g" @?= "f<\\|>g")+ , TestLabel "escapeMarkdownCell turns a newline into <br>" $+ TestCase (escapeMarkdownCell "a\nb" @?= "a<br>b")+ , TestLabel "toMarkdown keeps an operator name in one cell" $+ TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("name", fromList ["plain" :: T.Text, "f<|>g"])+ , ("mb", fromList [1.0 :: Double, 2.0])+ ]+ md = D.toMarkdown df+ tableRows = filter (T.isPrefixOf "|") (T.lines md)+ delims = map unescapedPipes tableRows+ assertBool "pipe in a value left unescaped" ("f<\\|>g" `T.isInfixOf` md)+ assertBool+ ("rows misaligned, delimiter counts: " ++ show delims)+ (case delims of [] -> False; (d : ds) -> all (== d) ds)+ ]
+ tests/Internal/PackedText.hs view
@@ -0,0 +1,186 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Correctness oracle for the packed-text column variant. Every assertion+builds a 'PackedText' column from raw bytes + offsets and the same data as a+'BoxedColumn Text', then checks display, extraction, equality, ordering,+hashing, groupBy and join all agree. This pins behavioral parity before any+reader emits packed columns.+-}+module Internal.PackedText (tests) where++import qualified Data.Text as T+import qualified Data.Text.Array as A+import qualified Data.Vector.Unboxed as VU++import Control.Monad (zipWithM_)+import qualified Data.ByteString as B+import Data.Text.Encoding (encodeUtf8)+import Data.Word (Word8)+import qualified DataFrame as D+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.DataFrame (unsafeGetColumn)+import DataFrame.Internal.PackedText (mkPackedContiguous)+import qualified DataFrame.Operations.Aggregation as Agg+import Test.HUnit++-- Sample string corpus, including empty, ASCII, and multibyte UTF-8 fields.+sampleRows :: [T.Text]+sampleRows =+ [ "apple"+ , "banana"+ , ""+ , "apple"+ , "Zebra"+ , "café"+ , "naïve"+ , "日本語"+ , "banana"+ , "apple"+ ]++-- Build an A.Array from a flat byte list.+arrayFromBytes :: [Word8] -> A.Array+arrayFromBytes ws = A.run $ do+ m <- A.new (length ws)+ zipWithM_ (A.unsafeWrite m) [0 ..] ws+ pure m++-- Build a PackedText column directly from raw UTF-8 bytes + offsets.+packedFromTexts :: [T.Text] -> DI.Column+packedFromTexts ts =+ let bytess = map (B.unpack . encodeUtf8) ts+ flat = concat bytess+ offs = scanl (+) 0 (map length bytess)+ arr = arrayFromBytes flat+ in DI.PackedText Nothing (mkPackedContiguous arr (VU.fromList offs))++boxedFromTexts :: [T.Text] -> DI.Column+boxedFromTexts = DI.fromList++packedCol, boxedCol :: DI.Column+packedCol = packedFromTexts sampleRows+boxedCol = boxedFromTexts sampleRows++-- Single-column dataframes for groupBy / hashing.+packedDf, boxedDf :: D.DataFrame+packedDf = D.fromNamedColumns [("k", packedCol)]+boxedDf = D.fromNamedColumns [("k", boxedCol)]++displayParity :: Test+displayParity =+ TestCase $+ assertEqual "show packed == show boxed" (show boxedCol) (show packedCol)++columnEqParity :: Test+columnEqParity = TestCase $ do+ assertBool "packed == boxed" (packedCol == boxedCol)+ assertBool "boxed == packed" (boxedCol == packedCol)+ assertBool "packed == packed" (packedCol == packedFromTexts sampleRows)++extractionParity :: Test+extractionParity =+ TestCase $+ assertEqual+ "toList @Text packed == boxed"+ (DI.toList @T.Text boxedCol)+ (DI.toList @T.Text packedCol)++orderingParity :: Test+orderingParity = TestCase $ do+ let sortedPacked = D.sortBy [D.Asc (F.col @T.Text "k")] packedDf+ sortedBoxed = D.sortBy [D.Asc (F.col @T.Text "k")] boxedDf+ assertBool "asc sort parity" (sortedPacked == sortedBoxed)+ let descPacked = D.sortBy [D.Desc (F.col @T.Text "k")] packedDf+ descBoxed = D.sortBy [D.Desc (F.col @T.Text "k")] boxedDf+ assertBool "desc sort parity" (descPacked == descBoxed)++hashingParity :: Test+hashingParity =+ TestCase $+ assertEqual+ "row hashes equal"+ (Agg.computeRowHashes [0] boxedDf)+ (Agg.computeRowHashes [0] packedDf)++groupByParity :: Test+groupByParity =+ TestCase $+ assertEqual+ "groupBy + distinct parity"+ (D.distinct boxedDf)+ (D.distinct packedDf)++joinParity :: Test+joinParity = TestCase $ do+ let lhsP = D.fromNamedColumns [("k", packedCol)]+ lhsB = D.fromNamedColumns [("k", boxedCol)]+ rhs = D.fromNamedColumns [("k", boxedFromTexts ["apple", "banana", "日本語"])]+ joinedP = D.innerJoin ["k"] lhsP rhs+ joinedB = D.innerJoin ["k"] lhsB rhs+ assertBool "inner join parity" (joinedP == joinedB)++-- A gather (atIndicesStable) on a packed column stays packed and equals the+-- boxed gather. Indices repeat, reorder, and drop rows.+gatherPreservesPacked :: Test+gatherPreservesPacked = TestCase $ do+ let ixs = VU.fromList [9, 0, 0, 5, 7, 2, 3 :: Int]+ gp = DI.atIndicesStable ixs packedCol+ gb = DI.atIndicesStable ixs boxedCol+ assertBool "gathered packed stays PackedText" (DI.isPackedText gp)+ assertBool "gathered packed == gathered boxed" (gp == gb)+ assertEqual+ "gathered packed toList == boxed"+ (DI.toList @T.Text gb)+ (DI.toList @T.Text gp)++-- A sort on a packed column stays packed and equals the boxed sort.+sortPreservesPacked :: Test+sortPreservesPacked = TestCase $ do+ let sortedP = D.sortBy [D.Asc (F.col @T.Text "k")] packedDf+ sortedB = D.sortBy [D.Asc (F.col @T.Text "k")] boxedDf+ assertBool+ "sorted packed column stays PackedText"+ (DI.isPackedText (unsafeGetColumn "k" sortedP))+ assertBool "sorted packed == sorted boxed" (sortedP == sortedB)++-- A left join (sentinel gather, -1 for missing right rows) on a packed right+-- column stays packed, builds the correct null bitmap, and equals boxed.+leftJoinSentinelPreservesPacked :: Test+leftJoinSentinelPreservesPacked = TestCase $ do+ let lhsP = D.fromNamedColumns [("k", packedCol)]+ lhsB = D.fromNamedColumns [("k", boxedCol)]+ rhsP =+ D.fromNamedColumns+ [ ("k", packedFromTexts ["apple", "banana", "日本語"])+ , ("v", packedFromTexts ["RA", "RB", "RC"])+ ]+ rhsB =+ D.fromNamedColumns+ [ ("k", boxedFromTexts ["apple", "banana", "日本語"])+ , ("v", boxedFromTexts ["RA", "RB", "RC"])+ ]+ joinedP = D.leftJoin ["k"] lhsP rhsP+ joinedB = D.leftJoin ["k"] lhsB rhsB+ assertBool+ "left-joined packed right column stays PackedText"+ (DI.isPackedText (unsafeGetColumn "v" joinedP))+ assertBool "left join packed == boxed" (joinedP == joinedB)++tests :: [Test]+tests =+ [ TestLabel "PackedText display parity" displayParity+ , TestLabel "PackedText equality parity" columnEqParity+ , TestLabel "PackedText extraction parity" extractionParity+ , TestLabel "PackedText ordering parity" orderingParity+ , TestLabel "PackedText hashing parity" hashingParity+ , TestLabel "PackedText groupBy parity" groupByParity+ , TestLabel "PackedText join parity" joinParity+ , TestLabel "PackedText gather preserves packed" gatherPreservesPacked+ , TestLabel "PackedText sort preserves packed" sortPreservesPacked+ , TestLabel+ "PackedText left-join sentinel preserves packed"+ leftJoinSentinelPreservesPacked+ ]
+ tests/LazyParity.hs view
@@ -0,0 +1,146 @@+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | The HARD GATE for the lazy engine: for a bounded source the lazy result+must be BYTE-IDENTICAL to the eager result of the same ops (bounded joins and+aggregates route through the eager fast paths). Covers a LEFT-join pipeline+(join -> groupBy -> aggregate -> sort) and a pure groupBy pipeline.+-}+module LazyParity (tests) where++import qualified Data.Map.Strict as M+import Data.Text (Text)+import qualified Data.Text as T+import qualified DataFrame as D+import qualified DataFrame.Functions as F+import qualified DataFrame.IO.CSV as Csv+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Internal.Expression as E+import DataFrame.Lazy (SortOrder (Descending))+import qualified DataFrame.Lazy as L+import qualified DataFrame.Operations.Aggregation as Agg+import DataFrame.Operations.Join (JoinType (LEFT))+import qualified DataFrame.Operations.Join as Join+import qualified DataFrame.Operations.Permutation as Perm+import DataFrame.Operators (as, (|>))+import DataFrame.Schema (Schema (..), schemaType)+import System.Directory (removeFile)+import System.IO.Temp (emptySystemTempFile)+import Test.HUnit++ordersSchema :: Schema+ordersSchema =+ Schema $+ M.fromList+ [ ("order_id", schemaType @Int)+ , ("customer_id", schemaType @Int)+ , ("amount", schemaType @Double)+ , ("discount", schemaType @Double)+ ]++customersSchema :: Schema+customersSchema =+ Schema $+ M.fromList+ [ ("customer_id", schemaType @Int)+ , ("region", schemaType @Text)+ , ("plan", schemaType @Text)+ ]++{- | @n@ orders; ~10% reference a customer id with no matching customer row so+the LEFT join produces a Nothing group (exercises unmatched-row semantics).+-}+ordersFrame :: Int -> D.DataFrame+ordersFrame n =+ D.fromNamedColumns+ [ ("order_id", DI.fromList [0 .. n - 1])+ ,+ ( "customer_id"+ , DI.fromList+ [if i `mod` 10 == 0 then 9_000_000 + i else i `mod` 257 | i <- [0 .. n - 1]]+ )+ , ("amount", DI.fromList [fromIntegral i * 1.5 :: Double | i <- [0 .. n - 1]])+ ,+ ( "discount"+ , DI.fromList [fromIntegral (i `mod` 7) * 0.25 :: Double | i <- [0 .. n - 1]]+ )+ ]++customersFrame :: Int -> D.DataFrame+customersFrame m =+ D.fromNamedColumns+ [ ("customer_id", DI.fromList [0 .. m - 1])+ , ("region", DI.fromList [T.pack ("r" ++ show (i `mod` 4)) | i <- [0 .. m - 1]])+ , ("plan", DI.fromList [T.pack ("p" ++ show (i `mod` 3)) | i <- [0 .. m - 1]])+ ]++-- | Write a frame to a temp CSV and run an action with the path.+withCsv :: D.DataFrame -> (FilePath -> IO a) -> IO a+withCsv df k = do+ csvPath <- emptySystemTempFile "lazy_parity_.csv"+ Csv.writeCsv csvPath df+ r <- k csvPath+ removeFile csvPath+ return r++{- | LEFT join 20k orders to 257 customers -> groupBy region,plan+-> sum(amount-discount), count -> sort revenue desc.+-}+joinPipelineParity :: Test+joinPipelineParity =+ TestCase $+ withCsv (ordersFrame 20_000) $ \ordersPath ->+ withCsv (customersFrame 257) $ \customersPath -> do+ let amount = F.col @Double "amount"+ discount = F.col @Double "discount"+ aggs =+ [ F.sum (amount - discount) `as` "revenue"+ , F.count amount `as` "orders"+ ]+ ordersDf <- Csv.readCsvWithSchema ordersSchema ordersPath+ customersDf <- Csv.readCsvWithSchema customersSchema customersPath+ let eager =+ Perm.sortBy [Perm.Desc (E.Col @Double "revenue")] $+ Agg.aggregate aggs $+ Agg.groupBy ["region", "plan"] $+ Join.join LEFT ["customer_id"] ordersDf customersDf+ let customersQ = L.scanCsv customersSchema (T.pack customersPath)+ lazy <-+ L.scanCsv ordersSchema (T.pack ordersPath)+ |> (\o -> L.join LEFT "customer_id" "customer_id" o customersQ)+ |> L.groupBy ["region", "plan"] aggs+ |> L.sortBy [("revenue", Descending)]+ |> L.runDataFrame+ assertEqual+ "join pipeline lazy == eager (byte-identical)"+ (show eager)+ (show lazy)++-- | Pure groupBy customer_id -> sum(amount), count, sort desc.+groupByPipelineParity :: Test+groupByPipelineParity =+ TestCase $+ withCsv (ordersFrame 20_000) $ \ordersPath -> do+ let amount = F.col @Double "amount"+ aggs =+ [ F.sum amount `as` "total"+ , F.count amount `as` "n"+ ]+ ordersDf <- Csv.readCsvWithSchema ordersSchema ordersPath+ let eager =+ Perm.sortBy [Perm.Desc (E.Col @Double "total")] $+ Agg.aggregate aggs $+ Agg.groupBy ["customer_id"] ordersDf+ lazy <-+ L.scanCsv ordersSchema (T.pack ordersPath)+ |> L.groupBy ["customer_id"] aggs+ |> L.sortBy [("total", Descending)]+ |> L.runDataFrame+ assertEqual+ "groupBy pipeline lazy == eager (byte-identical)"+ (show eager)+ (show lazy)++tests :: [Test]+tests = [joinPipelineParity, groupByPipelineParity]
tests/LazyParquet.hs view
@@ -9,9 +9,9 @@ import Data.Time (UTCTime) import qualified DataFrame as D import qualified DataFrame.Functions as F-import DataFrame.Internal.Schema (Schema (..), schemaType)+import DataFrame.Lazy (SortOrder (..)) import qualified DataFrame.Lazy as L-import DataFrame.Lazy.Internal.LogicalPlan (SortOrder (..))+import DataFrame.Schema (Schema (..), schemaType) import Test.HUnit -- | Schema matching all columns in alltypes_plain.parquet.@@ -110,7 +110,7 @@ ( do actual <- L.runDataFrame- ( L.limit+ ( L.take 3 ( L.sortBy [("id", Ascending)]
+ tests/Learn/Denotation.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | The "predict is the model's denotation" claim, discharged as a test rather+than left as a slogan: the @Expr@ that 'predict' compiles must evaluate to the+same numbers as the fitted record's own parameters, computed independently here+in plain Haskell. If @affineExpr@ / @argMinExpr@ ever drift from the record they+are built from, these fail.+-}+module Learn.Denotation (tests) where++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 DataFrame.Internal.Column (TypedColumn (..), toVector)+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.Expression (Expr)+import DataFrame.Internal.Interpreter (interpret)++import DataFrame.KMeans+import DataFrame.LinearModel+import DataFrame.Model (fit, predict)++import Test.HUnit++interpD :: D.DataFrame -> Expr Double -> [Double]+interpD df e = case interpret @Double df e of+ Right (TColumn c) -> either (const []) VU.toList (toVector @Double @VU.Vector c)+ Left err -> error (show err)++interpI :: D.DataFrame -> Expr Int -> [Int]+interpI df e = case interpret @Int df e of+ Right (TColumn c) -> either (const []) VU.toList (toVector @Int @VU.Vector c)+ Left err -> error (show err)++df :: D.DataFrame+df =+ D.fromNamedColumns+ [ ("x1", DI.fromList xs1)+ , ("x2", DI.fromList xs2)+ , ("y", DI.fromList [3 + 2 * a - 0.5 * b | (a, b) <- zip xs1 xs2])+ ]+ where+ xs1 = [1, 2, 3, 4, 5, 6, 7, 8] :: [Double]+ xs2 = [2, 1, 4, 3, 6, 5, 8, 7] :: [Double]++col :: D.DataFrame -> T.Text -> [Double]+col d n = interpD d (F.col @Double n)++-- | @interpret (predict m)@ must equal @intercept + Σ coefⱼ·featureⱼ@ from the record.+linearDenotation :: Test+linearDenotation = TestCase $ do+ let m = fit defaultLinearConfig (F.col @Double "y") df+ feats = V.toList (regFeatureNames m)+ coefs = VU.toList (regCoef m)+ cols = map (col df) feats+ native =+ [ regIntercept m + sum (zipWith (*) coefs row)+ | row <- transposeL cols+ ]+ symbolic = interpD df (predict m)+ assertBool+ "linear: predict Expr matches the record's affine prediction"+ (and (zipWith (\a b -> abs (a - b) < 1e-9) native symbolic))++-- | @interpret (predict km)@ must equal the nearest-centroid label from @kmCenters@.+kmeansDenotation :: Test+kmeansDenotation = TestCase $ do+ let feats = ["x1", "x2"]+ km = fit defaultKMeansConfig{kmK = 3, kmSeed = 1} (map (F.col @Double) feats) df+ centers = map VU.toList (V.toList (kmCenters km))+ rows = transposeL (map (col df) feats)+ native = [nearest centers row | row <- rows]+ symbolic = interpI df (predict km)+ assertEqual+ "kmeans: predict Expr matches nearest-centroid label"+ native+ symbolic+ where+ nearest centers row =+ snd (minimum [(sqDist c row, i) | (i, c) <- zip [0 ..] centers])+ sqDist c row = sum [(a - b) ^ (2 :: Int) | (a, b) <- zip c row]++transposeL :: [[a]] -> [[a]]+transposeL [] = []+transposeL xss+ | any null xss = []+ | otherwise = map head xss : transposeL (map tail xss)++tests :: [Test]+tests = [linearDenotation, kmeansDenotation]
+ tests/Learn/Ensembles.hs view
@@ -0,0 +1,163 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Learn.Ensembles (tests) where++import qualified DataFrame as D+import qualified DataFrame.Functions as F+import DataFrame.Internal.Column (TypedColumn (..), toVector)+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.Expression (Expr)+import DataFrame.Internal.Interpreter (interpret)++import DataFrame.Boosting+import DataFrame.DBSCAN+import DataFrame.GMM+import DataFrame.LinearModel+import DataFrame.Metrics (r2)+import DataFrame.ModelSelection++import Data.Maybe (isJust, isNothing, listToMaybe)+import qualified Data.Vector.Unboxed as VU+import DataFrame.Model (fit, predict)+import Test.HUnit++interpD :: D.DataFrame -> Expr Double -> [Double]+interpD df e = case interpret @Double df e of+ Right (TColumn c) -> either (const []) VU.toList (toVector @Double @VU.Vector c)+ Left err -> error (show err)++interpI :: D.DataFrame -> Expr Int -> [Int]+interpI df e = case interpret @Int df e of+ Right (TColumn c) -> either (const []) VU.toList (toVector @Int @VU.Vector c)+ Left err -> error (show err)++clsDF :: D.DataFrame+clsDF =+ D.fromNamedColumns+ [ ("x", DI.fromList ([-3, -2, -1, -0.5, 0.5, 1, 2, 3] :: [Double]))+ , ("label", DI.fromList ([0, 0, 0, 0, 1, 1, 1, 1] :: [Int]))+ ]++blobs :: D.DataFrame+blobs =+ D.fromNamedColumns+ [ ("a", DI.fromList ([0, 0.2, -0.1, 0.1, 8, 8.1, 7.9, 8.2] :: [Double]))+ , ("b", DI.fromList ([0, -0.1, 0.2, 0.0, 5, 5.2, 4.9, 5.1] :: [Double]))+ ]++testGBMRegression :: Test+testGBMRegression = TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("x", DI.fromList ([1 .. 12] :: [Double]))+ ,+ ( "y"+ , DI.fromList+ ( [sin (fromIntegral i) + fromIntegral i * 0.3 | i <- [1 .. 12 :: Int]] ::+ [Double]+ )+ )+ ]+ gb =+ fit+ defaultGBConfig{gbNEstimators = 60, gbMaxDepth = 2}+ (F.col @Double "y")+ df+ preds = interpD df (predict gb)+ truth = interpD df (F.col @Double "y")+ err = sum (zipWith (\p t -> (p - t) ^ (2 :: Int)) preds truth) / 12+ assertBool "GBM fits training data well" (err < 0.05)+ assertBool "trainScore recorded" (VU.length (gbTrainScore gb) == 60)++testGBMStaged :: Test+testGBMStaged = TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("x", DI.fromList ([1 .. 8] :: [Double]))+ , ("y", DI.fromList ([1 .. 8] :: [Double]))+ ]+ gb = fit defaultGBConfig{gbNEstimators = 10} (F.col @Double "y") df+ assertBool "stage 0 valid" (isJust (gbExprAtStage 0 gb))+ assertBool "out of range stage rejected" (isNothing (gbExprAtStage 11 gb))++testAdaBoost :: Test+testAdaBoost = TestCase $ do+ let m = fit defaultAdaBoostConfig (F.col @Int "label") clsDF+ preds = interpI clsDF (predict m)+ assertEqual "AdaBoost separates" [0, 0, 0, 0, 1, 1, 1, 1] preds++testGMM :: Test+testGMM = TestCase $ do+ let m =+ fit+ defaultGMMConfig{gmmK = 2, gmmSeed = 1}+ [F.col @Double "a", F.col @Double "b"]+ blobs+ assigns = interpI blobs (predict m)+ assertBool "GMM converged" (gmmConverged m)+ assertBool+ "GMM splits the blobs"+ (listToMaybe assigns /= listToMaybe (reverse assigns))+ let m2 =+ fit+ defaultGMMConfig{gmmK = 2, gmmSeed = 1}+ [F.col @Double "a", F.col @Double "b"]+ blobs+ assertEqual "GMM deterministic" (gmmMeans m) (gmmMeans m2)+ assertBool "BIC finite" (not (isNaN (gmmBIC m)) && not (isInfinite (gmmBIC m)))++testDBSCAN :: Test+testDBSCAN = TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("a", DI.fromList ([0, 0.1, 0.2, 5, 5.1, 5.2, 50] :: [Double]))+ , ("b", DI.fromList ([0, 0.1, 0.0, 5, 5.0, 5.1, 50] :: [Double]))+ ]+ m = fit (DBSCANConfig 1.0 2) [F.col @Double "a", F.col @Double "b"] df+ assertEqual "two clusters" 2 (dbNClusters m)+ assertEqual "last point is noise" (-1) (VU.last (dbLabels m))+ let surrogate =+ dbscanSurrogateExpr+ D.defaultTreeConfig+ [F.col @Double "a", F.col @Double "b"]+ m+ df+ preds = interpI df surrogate+ assertEqual+ "surrogate matches non-noise labels on cores"+ (take 6 (VU.toList (dbLabels m)))+ (take 6 preds)++testGridSearch :: Test+testGridSearch = TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("x", DI.fromList ([1 .. 40] :: [Double]))+ ,+ ( "y"+ , DI.fromList ([2 * fromIntegral i + 5 | i <- [1 .. 40 :: Int]] :: [Double])+ )+ ]+ score alpha train test' =+ let mdl =+ fit+ (LinearConfig (Ridge alpha) defaultSolverConfig)+ (F.col @Double "y")+ (train :: D.DataFrame)+ in r2+ (VU.fromList (interpD test' (predict mdl)))+ (VU.fromList (interpD test' (F.col @Double "y")))+ res = gridSearch 4 7 [0.0, 1.0, 100.0] score df+ assertBool "best score high" (gsBestScore res > 0.99)+ assertEqual "all configs scored" 3 (length (gsAll res))++tests :: [Test]+tests =+ [ testGBMRegression+ , testGBMStaged+ , testAdaBoost+ , testGMM+ , testDBSCAN+ , testGridSearch+ ]
+ tests/Learn/Metamorphic.hs view
@@ -0,0 +1,366 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Metamorphic, invariance, and abstraction-law tests for the ML library.++These check how the fitted model and the metrics behave under transformations of+the *input* — duplicate / permute / scale / rename / add-constant-feature — where+the math dictates the answer. Each test computes the model on two genuinely+different (transformed) frames and asserts they agree, or pins a metric/scaler to+its defining law. None compares a value to itself; each can fail if the library+is wrong.+-}+module Learn.Metamorphic (tests) where++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 DataFrame.Internal.Column (TypedColumn (..), toVector)+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.Expression (Expr)+import DataFrame.Internal.Interpreter (interpret)++import DataFrame.LinearModel+import DataFrame.Metrics+import DataFrame.Model (fit, predict)+import DataFrame.Operations.Merge ()++-- Semigroup DataFrame (row concatenation)+import DataFrame.Transform++import Test.HUnit++-- Helpers -------------------------------------------------------------------++interpD :: D.DataFrame -> Expr Double -> [Double]+interpD df e = case interpret @Double df e of+ Right (TColumn c) -> either (const []) VU.toList (toVector @Double @VU.Vector c)+ Left err -> error (show err)++close :: Double -> Double -> Double -> Bool+close tol a b = abs (a - b) <= tol++closeList :: Double -> [Double] -> [Double] -> Bool+closeList tol as bs = length as == length bs && and (zipWith (close tol) as bs)++-- Build a regression frame from explicit feature/target lists.+mkRegDF :: [(T.Text, [Double])] -> [Double] -> D.DataFrame+mkRegDF feats ys =+ D.fromNamedColumns+ ([(n, DI.fromList vs) | (n, vs) <- feats] ++ [("y", DI.fromList ys)])++-- Base design: a noiseless linear target so OLS recovers a unique solution.+baseX1, baseX2, baseY :: [Double]+baseX1 = [1, 2, 3, 4, 5, 6, 7, 8]+baseX2 = [2, 1, 4, 3, 6, 5, 8, 7]+baseY = [3 + 2 * a - 0.5 * b | (a, b) <- zip baseX1 baseX2]++baseDF :: D.DataFrame+baseDF = mkRegDF [("x1", baseX1), ("x2", baseX2)] baseY++fitBase :: D.DataFrame -> LinearRegressor+fitBase = fit defaultLinearConfig (F.col @Double "y")++-- Metamorphic: duplicate rows ----------------------------------------------++{- | OLS minimises Σ(y-ŷ)²; duplicating every row scales the loss by 2 but leaves+the argmin (the coefficients/intercept) unchanged. Catches a solver that+weights rows by frame size or mishandles the normal equations.+-}+testDuplicateRows :: Test+testDuplicateRows = TestCase $ do+ let dupDF =+ mkRegDF+ [("x1", baseX1 ++ baseX1), ("x2", baseX2 ++ baseX2)]+ (baseY ++ baseY)+ m0 = fitBase baseDF+ m1 = fitBase dupDF+ assertBool+ "duplicate rows: coefficients unchanged"+ (closeList 1e-9 (VU.toList (regCoef m0)) (VU.toList (regCoef m1)))+ assertBool+ "duplicate rows: intercept unchanged"+ (close 1e-9 (regIntercept m0) (regIntercept m1))++-- Metamorphic: permute rows -------------------------------------------------++{- | OLS is invariant to row order. We reverse the rows (a genuine non-identity+permutation) and require identical coefficients. Catches any order-dependence+in matrix assembly or solving.+-}+testPermuteRows :: Test+testPermuteRows = TestCase $ do+ let permDF =+ mkRegDF+ [("x1", reverse baseX1), ("x2", reverse baseX2)]+ (reverse baseY)+ m0 = fitBase baseDF+ m1 = fitBase permDF+ assertBool+ "permute rows: coefficients unchanged"+ (closeList 1e-9 (VU.toList (regCoef m0)) (VU.toList (regCoef m1)))+ assertBool+ "permute rows: intercept unchanged"+ (close 1e-9 (regIntercept m0) (regIntercept m1))++-- Metamorphic: scale a feature ----------------------------------------------++{- | Scaling feature x1 by k must scale that coefficient by 1/k while leaving+predictions (and every other coefficient + the intercept) invariant. Both+sides are genuine fits on different frames. Catches a regressor that drops or+mis-applies a feature's scale.+-}+testScaleFeature :: Test+testScaleFeature = TestCase $ do+ let k = 10.0+ scaledDF = mkRegDF [("x1", map (* k) baseX1), ("x2", baseX2)] baseY+ m0 = fitBase baseDF+ m1 = fitBase scaledDF+ coef0 = VU.toList (regCoef m0)+ coef1 = VU.toList (regCoef m1)+ assertBool+ "scale feature: x1 coefficient scaled by 1/k"+ (close 1e-7 (head coef0 / k) (head coef1))+ assertBool+ "scale feature: x2 coefficient unchanged"+ (close 1e-7 (coef0 !! 1) (coef1 !! 1))+ assertBool+ "scale feature: intercept unchanged"+ (close 1e-7 (regIntercept m0) (regIntercept m1))+ -- Predictions on each model's own (consistently transformed) frame agree.+ let p0 = interpD baseDF (predict m0)+ p1 = interpD scaledDF (predict m1)+ assertBool "scale feature: predictions invariant" (closeList 1e-6 p0 p1)++-- Metamorphic: rename columns -----------------------------------------------++{- | Renaming feature columns is a pure relabelling: coefficients, intercept, and+predictions are identical and the fitted feature names track the rename.+Catches any hidden dependence on specific column-name strings.+-}+testRenameColumns :: Test+testRenameColumns = TestCase $ do+ let renamedDF = mkRegDF [("feat_a", baseX1), ("feat_b", baseX2)] baseY+ m0 = fitBase baseDF+ m1 = fitBase renamedDF+ assertBool+ "rename: coefficients unchanged"+ (closeList 1e-9 (VU.toList (regCoef m0)) (VU.toList (regCoef m1)))+ assertBool+ "rename: intercept unchanged"+ (close 1e-9 (regIntercept m0) (regIntercept m1))+ assertEqual+ "rename: fitted feature names follow the rename"+ ["feat_a", "feat_b"]+ (V.toList (regFeatureNames m1))+ -- predict on the renamed frame yields the same numbers as on the original.+ let p0 = interpD baseDF (predict m0)+ p1 = interpD renamedDF (predict m1)+ assertBool "rename: predictions unchanged" (closeList 1e-9 p0 p1)++-- Metamorphic: irrelevant constant feature ----------------------------------++{- | A constant feature carries no variance, so OLS predictions for the real+features must be unchanged when one is added. (Its own weight is absorbed into+the intercept and is not separately identifiable, so we pin predictions, not+the weight.) Catches a fit whose answer drifts when a degenerate column is+present.+-}+testConstantFeatureIgnored :: Test+testConstantFeatureIgnored = TestCase $ do+ let withConst =+ mkRegDF+ [ ("x1", baseX1)+ , ("x2", baseX2)+ , ("c", replicate (length baseX1) 7.0)+ ]+ baseY+ m0 = fitBase baseDF+ m1 = fitBase withConst+ p0 = interpD baseDF (predict m0)+ p1 = interpD withConst (predict m1)+ assertBool+ "constant feature: predictions match the no-constant model"+ (closeList 1e-6 p0 p1)+ -- And the real-feature coefficients are unchanged.+ let coef1 = VU.toList (regCoef m1)+ assertBool+ "constant feature: x1 coefficient unchanged"+ (close 1e-6 (head (VU.toList (regCoef m0))) (head coef1))+ assertBool+ "constant feature: x2 coefficient unchanged"+ (close 1e-6 (VU.toList (regCoef m0) !! 1) (coef1 !! 1))++-- Metamorphic: split then concatenate --------------------------------------++{- | Genuinely split the frame into two row-range sub-frames and concatenate them+back with the DataFrame @<>@ (row concatenation). The rebuilt frame has the+same rows, so the fit must be identical — but this actually exercises the+concatenation code path, so it catches a @<>@ that drops, duplicates, or+mis-aligns rows/columns (a plain @take h ++ drop h@ would be the identity and+test nothing).+-}+testSplitConcat :: Test+testSplitConcat = TestCase $ do+ let n = length baseX1+ h = n `div` 2+ half lo hi =+ mkRegDF+ [ ("x1", slice lo hi baseX1)+ , ("x2", slice lo hi baseX2)+ ]+ (slice lo hi baseY)+ slice lo hi = take (hi - lo) . drop lo+ rebuiltDF = half 0 h <> half h n+ m0 = fitBase baseDF+ m1 = fitBase rebuiltDF+ -- the concatenated frame must have exactly the original rows back+ assertEqual "split/concat: row count preserved" n (fst (D.dimensions rebuiltDF))+ assertBool+ "split/concat: coefficients unchanged"+ (closeList 1e-9 (VU.toList (regCoef m0)) (VU.toList (regCoef m1)))+ assertBool+ "split/concat: intercept unchanged"+ (close 1e-9 (regIntercept m0) (regIntercept m1))++-- Training/inference parity: column order ----------------------------------++{- | The order of feature columns in the frame must not change predictions.+We fit on a frame whose columns are declared in the opposite order and require+the prediction expression to produce the same numbers on the original frame.+(predict compiles to a name-keyed affine Expr, so it should be order-free.)+Catches a positional, rather than name-keyed, feature mapping.+-}+testColumnOrderParity :: Test+testColumnOrderParity = TestCase $ do+ let swappedDF = mkRegDF [("x2", baseX2), ("x1", baseX1)] baseY+ m0 = fitBase baseDF+ m1 = fitBase swappedDF+ -- evaluate both fitted models on the SAME original frame.+ p0 = interpD baseDF (predict m0)+ p1 = interpD baseDF (predict m1)+ assertBool+ "column order: predictions on the same frame agree"+ (closeList 1e-7 p0 p1)++-- Law: standardScaler output has mean ~0, std ~1 ---------------------------++{- | The defining law of a standard scaler: after transforming, each scaled+column has sample mean ≈ 0 and (population) std ≈ 1. We recompute the moments+here in plain Haskell from the transformed frame — not via the scaler — so a+wrong denominator or a centring bug is caught.+-}+testStandardScalerLaw :: Test+testStandardScalerLaw = TestCase $ do+ let cols = ["x1", "x2"]+ scaler = standardScaler cols baseDF+ scaledDF = applyTransform (scalerTransform scaler) baseDF+ moments xs =+ let n = fromIntegral (length xs)+ mu = sum xs / n+ var = sum [(x - mu) ^ (2 :: Int) | x <- xs] / n+ in (mu, sqrt var)+ mapM_+ ( \c -> do+ let (mu, sd) = moments (interpD scaledDF (F.col @Double c))+ assertBool ("scaler: " ++ T.unpack c ++ " mean ~ 0") (close 1e-9 mu 0)+ assertBool ("scaler: " ++ T.unpack c ++ " std ~ 1") (close 1e-9 sd 1)+ )+ cols++{- | The scaler model's stored stats must match the data's own moments: a guard+against the scaler storing the wrong mean/std even if transform happens to+look plausible. Computed independently from the raw columns.+-}+testScalerStatsMatchData :: Test+testScalerStatsMatchData = TestCase $ do+ let scaler = standardScaler ["x1", "x2"] baseDF+ moments xs =+ let n = fromIntegral (length xs)+ mu = sum xs / n+ in (mu, sqrt (sum [(x - mu) ^ (2 :: Int) | x <- xs] / n))+ (mu1, sd1) = moments baseX1+ (mu2, sd2) = moments baseX2+ assertBool+ "scaler means match data"+ (closeList 1e-9 (VU.toList (smMeans scaler)) [mu1, mu2])+ assertBool+ "scaler stds match data"+ (closeList 1e-9 (VU.toList (smStds scaler)) [sd1, sd2])++-- Metric laws --------------------------------------------------------------++{- | Perfect prediction → accuracy exactly 1.0; a single deliberate miss drops it+below 1. Pins both ends so a metric that ignores its inputs can't pass.+-}+testAccuracyLaw :: Test+testAccuracyLaw = TestCase $ do+ let truth = VU.fromList [0, 1, 2, 1, 0, 2]+ perfect = truth+ oneWrong = VU.fromList [0, 1, 2, 1, 0, 0]+ assertBool "accuracy: perfect = 1" (close 1e-12 (accuracy perfect truth) 1.0)+ assertBool+ "accuracy: one miss out of 6 = 5/6"+ (close 1e-12 (accuracy oneWrong truth) (5 / 6))+ assertBool+ "accuracy in [0,1]"+ (let a = accuracy oneWrong truth in a >= 0 && a <= 1)++{- | Accuracy is permutation-invariant: applying the same permutation to preds+and truth leaves it unchanged. Both vectors are genuinely reordered.+-}+testAccuracyPermInvariant :: Test+testAccuracyPermInvariant = TestCase $ do+ let preds = VU.fromList [0, 0, 1, 1, 2, 2, 1, 0]+ truth = VU.fromList [0, 0, 1, 2, 2, 2, 1, 0]+ a0 = accuracy preds truth+ a1 = accuracy (VU.reverse preds) (VU.reverse truth)+ assertBool "accuracy: permutation invariant" (close 1e-12 a0 a1)+ -- Sanity: the metric is non-trivial here (not 0 or 1), so invariance is meaningful.+ assertBool "accuracy: non-degenerate baseline" (a0 > 0 && a0 < 1)++{- | r² of a perfect fit is exactly 1; r² of predicting the constant mean is+exactly 0. These are the two anchor points of the R² definition. Catches a+swapped SS_res/SS_tot or a wrong sign.+-}+testR2Anchors :: Test+testR2Anchors = TestCase $ do+ let truth = VU.fromList [1, 3, 2, 8, 5, 4]+ meanT = VU.sum truth / fromIntegral (VU.length truth)+ meanPred = VU.replicate (VU.length truth) meanT+ assertBool "r2: perfect fit = 1" (close 1e-12 (r2 truth truth) 1.0)+ assertBool "r2: predicting the mean = 0" (close 1e-12 (r2 meanPred truth) 0.0)+ -- A strictly-worse-than-mean prediction gives r2 < 0 (not clamped).+ let worse = VU.map (+ 100) truth+ assertBool "r2: far-off prediction is negative" (r2 worse truth < 0)++{- | r² on a fitted noiseless linear model equals 1, and rmse equals 0. Ties the+metric law to the model: if the fit is exact, the metric must say so.+-}+testR2OfFittedModel :: Test+testR2OfFittedModel = TestCase $ do+ let m = fitBase baseDF+ score = evaluate r2 (predict m) (F.col @Double "y") baseDF+ err = evaluate rmse (predict m) (F.col @Double "y") baseDF+ assertBool "r2 of exact linear fit ~ 1" (close 1e-9 score 1.0)+ assertBool "rmse of exact linear fit ~ 0" (err < 1e-6)++tests :: [Test]+tests =+ [ testDuplicateRows+ , testPermuteRows+ , testScaleFeature+ , testRenameColumns+ , testConstantFeatureIgnored+ , testSplitConcat+ , testColumnOrderParity+ , testStandardScalerLaw+ , testScalerStatsMatchData+ , testAccuracyLaw+ , testAccuracyPermInvariant+ , testR2Anchors+ , testR2OfFittedModel+ ]
+ tests/Learn/MetricsTests.hs view
@@ -0,0 +1,116 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Learn.MetricsTests (tests) where++import qualified DataFrame as D+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI++import DataFrame.LinearModel+import DataFrame.Metrics+import DataFrame.Metrics.Report+import DataFrame.ModelSelection+import DataFrame.PCA+import DataFrame.Transform++import qualified Data.Vector.Unboxed as VU+import DataFrame.Model (fit, predict)+import Test.HUnit++close :: Double -> Double -> Double -> Bool+close tol a b = abs (a - b) <= tol++preds3, truth3 :: VU.Vector Double+preds3 = VU.fromList [0, 0, 1, 1, 2, 2, 1, 0]+truth3 = VU.fromList [0, 0, 1, 2, 2, 2, 1, 0]++reg :: D.DataFrame+reg =+ D.fromNamedColumns+ [ ("x", DI.fromList ([1 .. 20] :: [Double]))+ ,+ ( "y"+ , DI.fromList ([2 * fromIntegral i + 1 | i <- [1 .. 20 :: Int]] :: [Double])+ )+ ]++testRegressionMetrics :: Test+testRegressionMetrics = TestCase $ do+ let p = VU.fromList [1, 2, 3, 4]+ t = VU.fromList [1, 2, 3, 5]+ assertBool "mse" (close 1e-9 (mse p t) 0.25)+ assertBool "rmse" (close 1e-9 (rmse p t) 0.5)+ assertBool "mae" (close 1e-9 (mae p t) 0.25)+ assertBool "r2 in range" (r2 p t <= 1)++testMulticlassMetrics :: Test+testMulticlassMetrics = TestCase $ do+ assertBool "accuracy" (close 1e-9 (accuracy preds3 truth3) 0.875)+ assertBool+ "binary precision class 1"+ (close 1e-9 (precision (Binary 1) preds3 truth3) (2 / 3))+ assertBool+ "macro f1 sane"+ (f1 Macro preds3 truth3 > 0.8 && f1 Macro preds3 truth3 <= 1)+ assertBool+ "micro f1 == accuracy"+ (close 1e-9 (f1 Micro preds3 truth3) (accuracy preds3 truth3))++testRocAuc :: Test+testRocAuc = TestCase $ do+ let scores = VU.fromList [0.1, 0.4, 0.35, 0.8]+ truth = VU.fromList [0, 0, 1, 1]+ assertBool "perfect-ish auc high" (rocAuc scores truth >= 0.75)+ assertBool "auc in [0,1]" (let a = rocAuc scores truth in a >= 0 && a <= 1)++testReports :: Test+testReports = TestCase $ do+ let cr = classificationReport preds3 truth3+ assertEqual "report covers 3 classes" 3 (length (crPerClass cr))+ assertBool "report accuracy" (close 1e-9 (crAccuracy cr) 0.875)+ let rr = regressionReport (VU.fromList [1, 2, 3]) (VU.fromList [1, 2, 4])+ assertBool "regression report rmse" (rrRMSE rr > 0)+ assertBool "classification report shows" (not (null (show cr)))+ assertBool "confusion shows" (not (null (show (confusionMatrix preds3 truth3))))++testEvaluateOneLiner :: Test+testEvaluateOneLiner = TestCase $ do+ let m = fit defaultLinearConfig (F.col @Double "y") reg+ score = evaluate rmse (predict m) (F.col @Double "y") reg+ assertBool "evaluate rmse ~ 0 on exact linear fit" (score < 1e-6)+ let r = regressionReportExpr (predict m) (F.col @Double "y") reg+ assertBool "report r2 ~ 1" (close 1e-6 (rrR2 r) 1)++testCrossValidate :: Test+testCrossValidate = TestCase $ do+ let cv =+ crossValidate+ 4+ 0+ r2+ (F.col @Double "y")+ (predict . fit defaultLinearConfig (F.col @Double "y"))+ reg+ assertBool "all folds high R2" (all (> 0.99) cv)+ assertEqual "four folds" 4 (length cv)++testTransformCompose :: Test+testTransformCompose = TestCase $ do+ let scaler = standardScaler ["x"] reg+ pca = fit (PCAConfig (NComp 1) False) [F.col @Double "x"] reg+ pipeline = scalerTransform scaler <> pcaTransform pca+ out = applyTransform pipeline reg+ assertBool "pipeline produced a frame" (D.columnNames out /= [])+ assertBool "pipeline has pc1 column" ("pc1" `elem` D.columnNames out)++tests :: [Test]+tests =+ [ testRegressionMetrics+ , testMulticlassMetrics+ , testRocAuc+ , testReports+ , testEvaluateOneLiner+ , testCrossValidate+ , testTransformCompose+ ]
+ tests/Learn/Models.hs view
@@ -0,0 +1,250 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Learn.Models (tests) where++import qualified DataFrame as D+import qualified DataFrame.Functions as F+import DataFrame.Internal.Column (TypedColumn (..), toVector)+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.Expression (Expr)+import DataFrame.Internal.Interpreter (interpret)++import DataFrame.DecisionTree.Model+import DataFrame.DecisionTree.Regression+import DataFrame.KMeans+import DataFrame.LinearModel+import DataFrame.PCA+import DataFrame.SVM+import DataFrame.Transform++import Control.Exception (evaluate, try)+import Data.List (isInfixOf)+import qualified Data.Map.Strict as M+import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import DataFrame.Errors (DataFrameException)+import DataFrame.Model (fit, predict)+import Test.HUnit++interpD :: D.DataFrame -> Expr Double -> [Double]+interpD df e = case interpret @Double df e of+ Right (TColumn c) -> either (const []) VU.toList (toVector @Double @VU.Vector c)+ Left err -> error (show err)++interpI :: D.DataFrame -> Expr Int -> [Int]+interpI df e = case interpret @Int df e of+ Right (TColumn c) -> either (const []) VU.toList (toVector @Int @VU.Vector c)+ Left err -> error (show err)++close :: Double -> Double -> Double -> Bool+close tol a b = abs (a - b) <= tol++regDF :: D.DataFrame+regDF =+ D.fromNamedColumns+ [ ("x1", DI.fromList xs1)+ , ("x2", DI.fromList xs2)+ , ("y", DI.fromList [2 * a - 3 * b + 1 | (a, b) <- zip xs1 xs2])+ ]+ where+ xs1 = [1, 2, 3, 4, 5, 6, 7, 8] :: [Double]+ xs2 = [2, 1, 4, 3, 6, 5, 8, 7] :: [Double]++-- | A custom enum label type — logistic regression must classify into it.+data Flower = Rose | Tulip | Daisy deriving (Show, Eq, Ord)++flowerDF :: D.DataFrame+flowerDF =+ D.fromNamedColumns+ [ ("x", DI.fromList ([-5, -4, -3, 0, 0.3, 0.6, 5, 6, 7] :: [Double]))+ ,+ ( "flower"+ , DI.fromList [Rose, Rose, Rose, Tulip, Tulip, Tulip, Daisy, Daisy, Daisy]+ )+ ]++clsDF :: D.DataFrame+clsDF =+ D.fromNamedColumns+ [ ("x", DI.fromList ([-3, -2, -1, -0.5, 0.5, 1, 2, 3] :: [Double]))+ , ("label", DI.fromList ([0, 0, 0, 0, 1, 1, 1, 1] :: [Int]))+ ]++testOLS :: Test+testOLS = TestCase $ do+ let m = fit defaultLinearConfig (F.col @Double "y") regDF+ assertBool+ "OLS coef ~ [2,-3]"+ (and (zipWith (close 1e-6) (VU.toList (regCoef m)) [2, -3]))+ assertBool "OLS intercept ~ 1" (close 1e-6 (regIntercept m) 1)+ let preds = interpD regDF (predict m)+ truth = interpD regDF (F.col @Double "y")+ assertBool "OLS expr matches y" (and (zipWith (close 1e-6) preds truth))++{- | A regression frame whose feature column @x@ is stored as @Int@; the target+@y@ is @Double@. Fitting must reject the Int feature at the boundary.+-}+intFeatureDF :: D.DataFrame+intFeatureDF =+ D.fromNamedColumns+ [ ("x", DI.fromList @Int [1 .. 8])+ , ("y", DI.fromList @Double [2, 4, 6, 8, 10, 12, 14, 16])+ ]++{- | The fit boundary mandates Double: a non-Double feature column is a fit-time+error (naming the column and pointing at the fix), not a silent coercion.+-}+testFitRequiresDouble :: Test+testFitRequiresDouble = TestCase $ do+ let m = fit defaultLinearConfig (F.col @Double "y") intFeatureDF+ result <-+ try (evaluate (VU.sum (regCoef m))) :: IO (Either DataFrameException Double)+ case result of+ Left e -> do+ let msg = show e+ assertBool "error mentions the Double requirement" ("Double" `isInfixOf` msg)+ assertBool "error names the offending column x" ("x" `isInfixOf` msg)+ Right _ -> assertFailure "expected a Double-input error for the Int feature column"++{- | A regression frame whose feature @x@ is nullable (@Maybe Double@). Fitting+must reject it rather than letting nulls poison the solve into NaN.+-}+nullableFeatureDF :: D.DataFrame+nullableFeatureDF =+ D.fromNamedColumns+ [ ("x", DI.fromList @(Maybe Double) (Nothing : map Just [2, 3, 4, 5, 6, 7, 8]))+ , ("y", DI.fromList @Double [2, 4, 6, 8, 10, 12, 14, 16])+ ]++{- | The fit boundary rejects a nullable (Maybe Double) input column with an+actionable error, instead of silently producing NaN coefficients.+-}+testFitRejectsNullable :: Test+testFitRejectsNullable = TestCase $ do+ let m = fit defaultLinearConfig (F.col @Double "y") nullableFeatureDF+ result <-+ try (evaluate (VU.sum (regCoef m))) :: IO (Either DataFrameException Double)+ case result of+ Left e -> do+ let msg = show e+ assertBool+ "error flags the non-null requirement"+ ("non-null Double" `isInfixOf` msg)+ assertBool "error names the nullable column x" ("x" `isInfixOf` msg)+ Right _ -> assertFailure "expected a nullable-input error for the Maybe Double feature"++testRidgeShrinks :: Test+testRidgeShrinks = TestCase $ do+ let r0 =+ fit (LinearConfig (Ridge 0.01) defaultSolverConfig) (F.col @Double "y") regDF+ r1 = fit (LinearConfig (Ridge 100) defaultSolverConfig) (F.col @Double "y") regDF+ norm = VU.sum . VU.map abs . regCoef+ assertBool "stronger ridge shrinks coefficients" (norm r1 < norm r0)++testLogistic :: Test+testLogistic = TestCase $ do+ let m = fit defaultLogisticConfig (F.col @Int "label") clsDF+ dec = predict m+ preds = interpI clsDF dec+ truth = [0, 0, 0, 0, 1, 1, 1, 1]+ assertEqual "logistic separates" truth preds+ let probs = logisticProbExprs m+ assertBool "prob exprs present for both classes" (M.size probs == 2)++{- | Logistic regression must work with any enum label type, not just Int —+multiclass classification into a user-defined sum type.+-}+testLogisticEnumLabel :: Test+testLogisticEnumLabel = TestCase $ do+ let m = fit defaultLogisticConfig (F.col @Flower "flower") flowerDF+ preds = case interpret @Flower flowerDF (predict m) of+ Right (TColumn c) -> either (const []) V.toList (toVector @Flower @V.Vector c)+ Left e -> error (show e)+ truth = [Rose, Rose, Rose, Tulip, Tulip, Tulip, Daisy, Daisy, Daisy]+ assertEqual "logistic recovers enum-typed labels" truth preds++testSVC :: Test+testSVC = TestCase $ do+ let m = fit defaultSVCConfig (F.col @Int "label") clsDF+ preds = interpI clsDF (predict m)+ assertEqual "linear SVC separates" [0, 0, 0, 0, 1, 1, 1, 1] preds++testRegressionTree :: Test+testRegressionTree = TestCase $ do+ let m = fit defaultRegTreeConfig (F.col @Double "y") regDF+ preds = interpD regDF (predict m)+ truth = interpD regDF (F.col @Double "y")+ sse = sum (zipWith (\p t -> (p - t) ^ (2 :: Int)) preds truth)+ assertBool "regression tree reduces error" (sse < 200)+ assertBool "tree has leaves" (dtrNLeaves m >= 2)++testClassifierStats :: Test+testClassifierStats = TestCase $ do+ let m = fit D.defaultTreeConfig (F.col @Int "label") clsDF+ assertBool "classifier depth >= 1" (dtcDepth m >= 1)++testPCA :: Test+testPCA = TestCase $ do+ let m =+ fit+ (PCAConfig (NComp 2) False)+ [F.col @Double "x1", F.col @Double "x2"]+ regDF+ ratio = VU.toList (pcaExplainedVarianceRatio m)+ assertBool "explained ratio sums ~1" (close 1e-9 (sum ratio) 1)+ assertEqual "two components" 2 (V.length (pcaComponents m))+ let comp0 = pcaComponents m V.! 0+ nrm = sqrt (VU.sum (VU.map (^ (2 :: Int)) comp0))+ assertBool "component is unit length" (close 1e-9 nrm 1)+ let es = map snd (pcaExprs m)+ assertBool+ "pca exprs evaluate finite"+ (not (any (any isNaN . interpD regDF) es))++testKMeans :: Test+testKMeans = TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("a", DI.fromList ([0, 0.1, 0.2, 10, 10.1, 10.2] :: [Double]))+ , ("b", DI.fromList ([0, 0.1, 0, 10, 10, 10.1] :: [Double]))+ ]+ cfg = defaultKMeansConfig{kmK = 2, kmNInit = 5, kmSeed = 1}+ m = fit cfg [F.col @Double "a", F.col @Double "b"] df+ labels = VU.toList (kmLabels m)+ assertBool "two blobs split" (head labels /= last labels)+ assertEqual "two centers" 2 (V.length (kmCenters m))+ let m2 = fit cfg [F.col @Double "a", F.col @Double "b"] df+ assertEqual "kmeans deterministic" (kmCenters m) (kmCenters m2)+ let assigns = interpI df (predict m)+ assertEqual "assign expr matches labels" labels assigns++testTransformCompose :: Test+testTransformCompose = TestCase $ do+ let scaler = standardScaler ["x1", "x2"] regDF+ t = scalerTransform scaler+ scaledDf = applyTransform t regDF+ m = fit defaultLinearConfig (F.col @Double "y") scaledDf+ composed = compileThrough t (predict m)+ viaCompose = interpD regDF composed+ viaStepwise = interpD scaledDf (predict m)+ assertBool+ "compileThrough == stepwise apply"+ (and (zipWith (close 1e-6) viaCompose viaStepwise))++tests :: [Test]+tests =+ [ testOLS+ , testFitRequiresDouble+ , testFitRejectsNullable+ , testRidgeShrinks+ , testLogistic+ , testLogisticEnumLabel+ , testSVC+ , testRegressionTree+ , testClassifierStats+ , testPCA+ , testKMeans+ , testTransformCompose+ ]
+ tests/Learn/Segmented.hs view
@@ -0,0 +1,316 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Learn.Segmented (tests) where++import Control.Exception (SomeException, evaluate, try)+import Data.List (isInfixOf)+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 DataFrame.Internal.Column (TypedColumn (..), toVector)+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.Expression (Expr)+import DataFrame.Internal.Interpreter (interpret)++import DataFrame.LinearModel.Logistic (defaultLogisticConfig)+import DataFrame.LinearModel.Regression (+ LinearRegressor (..),+ defaultLinearConfig,+ )+import DataFrame.Metrics (rmse)+import DataFrame.ModelSelection (crossValidate)+import DataFrame.Segmented+import DataFrame.SymbolicRegression (SRConfig (..), SRModel, defaultSRConfig)++import Test.HUnit++interpD :: D.DataFrame -> Expr Double -> [Double]+interpD df e = case interpret @Double df e of+ Right (TColumn c) -> either (const []) VU.toList (toVector @Double @VU.Vector c)+ Left err -> error (show err)++interpT :: D.DataFrame -> Expr T.Text -> [T.Text]+interpT df e = case interpret @T.Text df e of+ Right (TColumn c) -> either (const []) V.toList (toVector @T.Text @V.Vector c)+ Left err -> error (show err)++close :: Double -> Double -> Double -> Bool+close tol a b = abs (a - b) <= tol++-- | A two-segment frame: y = 2x+1 on group "a", y = -3x+5 on group "b".+twoSegDF :: D.DataFrame+twoSegDF =+ D.fromNamedColumns+ [ ("g", DI.fromList (replicate 4 ("a" :: T.Text) ++ replicate 4 "b"))+ , ("x", DI.fromList xs)+ ,+ ( "y"+ , DI.fromList ([2 * x + 1 | x <- take 4 xs] ++ [(-3) * x + 5 | x <- drop 4 xs])+ )+ ]+ where+ xs = [1, 2, 3, 4, 1, 2, 3, 4] :: [Double]++segByKey ::+ [T.Text] -> SegmentedModel a LinearRegressor -> Segment LinearRegressor+segByKey k m = head [s | s <- smSegments m, segKey s == k]++lowFloor :: Segmented cfg -> Segmented cfg+lowFloor s = s{segMinRows = 3}++recoveryTests :: [Test]+recoveryTests =+ [ "per-segment OLS recovered" ~: do+ let m = fit (lowFloor (segmented defaultLinearConfig)) (F.col @Double "y") twoSegDF+ a = segModel (segByKey ["a"] m)+ b = segModel (segByKey ["b"] m)+ assertBool "two segments" (length (smSegments m) == 2)+ assertBool "a slope ~ 2" (close 1e-9 (regCoef a VU.! 0) 2)+ assertBool "a intercept ~ 1" (close 1e-9 (regIntercept a) 1)+ assertBool "b slope ~ -3" (close 1e-9 (regCoef b VU.! 0) (-3))+ assertBool "b intercept ~ 5" (close 1e-9 (regIntercept b) 5)+ , "prediction reproduces y" ~: do+ let m = fit (lowFloor (segmented defaultLinearConfig)) (F.col @Double "y") twoSegDF+ preds = interpD twoSegDF (predict m)+ truth = interpD twoSegDF (F.col @Double "y")+ assertBool "routed predictions match y" (and (zipWith (close 1e-6) preds truth))+ ]++routingTests :: [Test]+routingTests =+ [ "unseen category routes to fallback" ~: do+ let m = fit (lowFloor (segmented defaultLinearConfig)) (F.col @Double "y") twoSegDF+ fb = smFallback m+ unseen =+ D.fromNamedColumns+ [ ("g", DI.fromList (["c"] :: [T.Text]))+ , ("x", DI.fromList ([2.0] :: [Double]))+ , ("y", DI.fromList ([0.0] :: [Double]))+ ]+ p = case interpD unseen (predict m) of+ [p'] -> p'+ _ -> error "Expecting only a single segment"+ expected = regIntercept fb + regCoef fb VU.! 0 * 2.0+ assertBool "unseen -> fallback affine" (close 1e-9 p expected)+ ]++genericityTests :: [Test]+genericityTests =+ [ "logistic base composes" ~: do+ let labels = replicate 4 ("pos" :: T.Text) ++ replicate 4 "neg"+ df =+ D.fromNamedColumns+ [ ("g", DI.fromList (replicate 4 ("a" :: T.Text) ++ replicate 4 "b"))+ , ("x", DI.fromList ([1, 2, 3, 4, 1, 2, 3, 4] :: [Double]))+ , ("label", DI.fromList labels)+ ]+ m = fit (lowFloor (segmented defaultLogisticConfig)) (F.col @T.Text "label") df+ preds = interpT df (predict m)+ assertBool "logistic predicts one label per row" (length preds == 8)+ , "symbolic base composes" ~: do+ let cfg = defaultSRConfig{srGenerations = 2, srPopSize = 12}+ m = fit (lowFloor (segmented cfg)) (F.col @Double "y") twoSegDF+ preds = interpD twoSegDF (predict m)+ assertBool "symbolic predicts one value per row" (length preds == 8)+ ]++selectionTests :: [Test]+selectionTests =+ [ "segmentOn picks only the named column" ~: do+ let df =+ D.fromNamedColumns+ [ ("g", DI.fromList (replicate 4 ("a" :: T.Text) ++ replicate 4 "b"))+ , ("h", DI.fromList (cycle3 8))+ , ("x", DI.fromList ([1, 2, 3, 4, 1, 2, 3, 4] :: [Double]))+ , ("y", DI.fromList ([1, 2, 3, 4, 5, 6, 7, 8] :: [Double]))+ ]+ m =+ fit+ (segmentOn (lowFloor (segmented defaultLinearConfig)) ["g"])+ (F.col @Double "y")+ df+ assertEqual "only g" ["g"] (smCatCols m)+ , "cardinality cap skips high-cardinality text" ~: do+ let df =+ D.fromNamedColumns+ [ ("g", DI.fromList (replicate 4 ("a" :: T.Text) ++ replicate 4 "b"))+ , ("k", DI.fromList (map (T.pack . show) [1 .. 8 :: Int]))+ , ("x", DI.fromList ([1, 2, 3, 4, 1, 2, 3, 4] :: [Double]))+ , ("y", DI.fromList ([1, 2, 3, 4, 5, 6, 7, 8] :: [Double]))+ ]+ cfg = (lowFloor (segmented defaultLinearConfig)){segMaxCard = 2}+ m = fit cfg (F.col @Double "y") df+ assertEqual "k (8 distinct) skipped, g kept" ["g"] (smCatCols m)+ ]+ where+ cycle3 n = take n (cycle (["p", "q", "r"] :: [T.Text]))++nullTests :: [Test]+nullTests =+ [ "null categorical routes to fallback" ~: do+ let gcol =+ [Just "a", Just "a", Just "a", Nothing, Just "b", Just "b", Just "b", Nothing] ::+ [Maybe T.Text]+ df =+ D.fromNamedColumns+ [ ("g", DI.fromList gcol)+ , ("x", DI.fromList ([1, 2, 3, 9, 1, 2, 3, 9] :: [Double]))+ , ("y", DI.fromList ([3, 5, 7, 0, 2, -1, -4, 0] :: [Double]))+ ]+ m = fit (lowFloor (segmented defaultLinearConfig)) (F.col @Double "y") df+ -- Only "a" and "b" appear as segments; "null" is never a key.+ assertBool+ "no null-keyed segment"+ (["null"] `notElem` map segKey (smSegments m))+ assertBool "two real segments" (length (smSegments m) == 2)+ ]++-- | Fit a segmented linear model, capturing any fit-time error message.+fitErr ::+ D.DataFrame -> IO (Either SomeException (SegmentedModel Double LinearRegressor))+fitErr df =+ try+ (evaluate (fit (lowFloor (segmented defaultLinearConfig)) (F.col @Double "y") df))++guardTests :: [Test]+guardTests =+ [ "int feature error points at toDouble" ~: do+ let df =+ D.fromNamedColumns+ [ ("g", DI.fromList (replicate 4 ("a" :: T.Text) ++ replicate 4 "b"))+ , ("n", DI.fromList ([1, 2, 3, 4, 5, 6, 7, 8] :: [Int]))+ , ("y", DI.fromList ([1, 2, 3, 4, 5, 6, 7, 8] :: [Double]))+ ]+ r <- fitErr df+ case r of+ Left e ->+ assertBool+ "names column n and toDouble"+ ("n" `isInfixOf` show e && "toDouble" `isInfixOf` show e)+ Right _ -> assertFailure "expected an actionable error for the Int feature"+ , "nullable feature error points at dropping" ~: do+ let df =+ D.fromNamedColumns+ [ ("g", DI.fromList (replicate 4 ("a" :: T.Text) ++ replicate 4 "b"))+ ,+ ( "z"+ , DI.fromList+ ( [Just 1, Nothing, Just 3, Just 4, Just 5, Just 6, Just 7, Just 8] ::+ [Maybe Double]+ )+ )+ , ("y", DI.fromList ([1, 2, 3, 4, 5, 6, 7, 8] :: [Double]))+ ]+ r <- fitErr df+ case r of+ Left e ->+ assertBool+ "names column z and filterJust"+ ("z" `isInfixOf` show e && "filterJust" `isInfixOf` show e)+ Right _ -> assertFailure "expected an actionable error for the nullable feature"+ , "well-typed frame fits after casting/dropping" ~: do+ let df =+ D.fromNamedColumns+ [ ("g", DI.fromList (replicate 4 ("a" :: T.Text) ++ replicate 4 "b"))+ , ("n", DI.fromList ([1, 2, 3, 4, 5, 6, 7, 8] :: [Double]))+ , ("y", DI.fromList ([1, 2, 3, 4, 5, 6, 7, 8] :: [Double]))+ ]+ m = fit (lowFloor (segmented defaultLinearConfig)) (F.col @Double "y") df+ assertEqual "two segments" 2 (length (smSegments m))+ ]++diagnosticTests :: [Test]+diagnosticTests =+ [ "undersized segment falls back, diagnostics correct" ~: do+ let df =+ D.fromNamedColumns+ [ ("g", DI.fromList (replicate 4 ("a" :: T.Text) ++ ["b"]))+ , ("x", DI.fromList ([1, 2, 3, 4, 9] :: [Double]))+ , ("y", DI.fromList ([3, 5, 7, 9, 0] :: [Double]))+ ]+ m = fit (lowFloor (segmented defaultLinearConfig)) (F.col @Double "y") df+ assertEqual "one local segment" 1 (length (smSegments m))+ assertEqual "segment a has 4 rows" 4 (segN (segByKey ["a"] m))+ assertEqual "b fell back with 1 row" [(["b"], 1)] (smFellBack m)+ ]++cvTests :: [Test]+cvTests =+ [ "composes with crossValidate" ~: do+ let scores =+ crossValidate+ 2+ 0+ rmse+ (F.col @Double "y")+ ( predict+ . fit ((segmented defaultLinearConfig){segMinRows = 2}) (F.col @Double "y")+ )+ twoSegDF+ assertBool "two fold scores" (length scores == 2)+ assertBool+ "scores are finite"+ (all (\s -> not (isNaN s || isInfinite s)) scores)+ ]++poolingTests :: [Test]+poolingTests =+ [ "lambda limits and interior monotonicity" ~: do+ let fitL lam =+ fit+ (pooled (lowFloor (segmented defaultLinearConfig)) lam)+ (F.col @Double "y")+ twoSegDF+ m0 = fitL 0+ mInf = fitL 1e9+ mMid = fitL 5+ slope k mm = regCoef (segModel (segByKey k mm)) VU.! 0+ inter k mm = regIntercept (segModel (segByKey k mm))+ -- lambda = 0: independent OLS (matches recovery test).+ assertBool "lambda0 a slope 2" (close 1e-9 (slope ["a"] m0) 2)+ assertBool "lambda0 b slope -3" (close 1e-9 (slope ["b"] m0) (-3))+ -- lambda -> inf: both segments converge to the n_g-weighted reference+ -- (raw slope -0.5, raw intercept 3.0 for this balanced fixture).+ assertBool "lambdaInf a slope ~ -0.5" (close 1e-3 (slope ["a"] mInf) (-0.5))+ assertBool "lambdaInf b slope ~ -0.5" (close 1e-3 (slope ["b"] mInf) (-0.5))+ assertBool "lambdaInf a intercept ~ 3" (close 1e-3 (inter ["a"] mInf) 3.0)+ assertBool+ "lambdaInf segments coincide"+ (close 1e-3 (slope ["a"] mInf) (slope ["b"] mInf))+ -- interior: a's slope sits strictly between its OLS (2) and the ref (-0.5).+ assertBool+ "interior between OLS and ref"+ (slope ["a"] mMid < 2 && slope ["a"] mMid > (-0.5))+ , "pooling unsupported for symbolic base" ~: do+ r <-+ try+ ( evaluate+ ( fit+ (pooled (lowFloor (segmented defaultSRConfig)) 1.0)+ (F.col @Double "y")+ twoSegDF+ )+ ) ::+ IO (Either SomeException (SegmentedModel Double SRModel))+ case r of+ Left _ -> return ()+ Right _ -> assertFailure "expected a fit-time error for pooling on a symbolic base"+ ]++tests :: [Test]+tests =+ concat+ [ recoveryTests+ , routingTests+ , genericityTests+ , selectionTests+ , nullTests+ , guardTests+ , diagnosticTests+ , cvTests+ , poolingTests+ ]
+ tests/Learn/SklearnParity.hs view
@@ -0,0 +1,186 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Parity tests against scikit-learn on clean Kaggle-style datasets; reference+values in @data/ml/golden.json@ come from @scripts/gen_sklearn_golden.py@.+Closed-form models get tight coefficient parity; iterative ones an accuracy floor.+-}+module Learn.SklearnParity (tests) where++import Data.Aeson (Value (..), decodeFileStrict')+import qualified Data.Aeson.Key as K+import qualified Data.Aeson.KeyMap as KM+import Data.Maybe (fromMaybe)+import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import DataFrame.Model (fit, predict)+import Test.HUnit++import qualified DataFrame as D+import qualified DataFrame.Functions as F+import DataFrame.Internal.Column (TypedColumn (..), toVector)+import DataFrame.Internal.Expression (Expr)+import DataFrame.Internal.Interpreter (interpret)++import DataFrame.Boosting (+ GBConfig (..),+ GBLoss (..),+ defaultGBConfig,+ gbProbaExpr,+ )+import DataFrame.KMeans+import DataFrame.LinearModel+import DataFrame.PCA+import DataFrame.SVM++goldenPath :: FilePath+goldenPath = "data/ml/golden.json"++loadGolden :: IO Value+loadGolden =+ fromMaybe (error "missing data/ml/golden.json") <$> decodeFileStrict' goldenPath++(.!) :: Value -> T.Text -> Value+(.!) (Object o) k = fromMaybe Null (KM.lookup (K.fromText k) o)+(.!) _ _ = Null++asNum :: Value -> Double+asNum (Number s) = realToFrac s+asNum _ = error "asNum: not a number"++asNums :: Value -> [Double]+asNums (Array a) = map asNum (V.toList a)+asNums _ = error "asNums: not an array"++asMatrix :: Value -> [[Double]]+asMatrix (Array a) = map asNums (V.toList a)+asMatrix _ = error "asMatrix: not a matrix"++interpD :: D.DataFrame -> Expr Double -> [Double]+interpD df e = case interpret @Double df e of+ Right (TColumn c) -> either (const []) VU.toList (toVector @Double @VU.Vector c)+ Left err -> error (show err)++colDoubles :: D.DataFrame -> T.Text -> [Double]+colDoubles df name = interpD df (F.col @Double name)++closeAll :: Double -> [Double] -> [Double] -> Bool+closeAll tol a b =+ length a == length b && and (zipWith (\x y -> abs (x - y) <= tol) a b)++accuracyOf :: [Double] -> [Double] -> Double+accuracyOf preds truth =+ fromIntegral (length (filter id (zipWith (==) preds truth)))+ / fromIntegral (max 1 (length truth))++testOLSParity :: Test+testOLSParity = TestCase $ do+ g <- loadGolden+ df <- D.readCsv "data/ml/regression.csv"+ let m = fit defaultLinearConfig (F.col @Double "target") df+ goldCoef = asNums (g .! "ols" .! "coef")+ goldB = asNum (g .! "ols" .! "intercept")+ assertBool+ "OLS coefficients match sklearn"+ (closeAll 1e-4 (VU.toList (regCoef m)) goldCoef)+ assertBool "OLS intercept matches sklearn" (abs (regIntercept m - goldB) < 1e-4)++testRidgeParity :: Test+testRidgeParity = TestCase $ do+ g <- loadGolden+ df <- D.readCsv "data/ml/regression.csv"+ let m =+ fit (LinearConfig (Ridge 1.0) defaultSolverConfig) (F.col @Double "target") df+ goldCoef = asNums (g .! "ridge" .! "coef")+ assertBool+ "Ridge coefficients match sklearn"+ (closeAll 1e-2 (VU.toList (regCoef m)) goldCoef)++testPCAParity :: Test+testPCAParity = TestCase $ do+ g <- loadGolden+ df <- D.readCsv "data/ml/iris.csv"+ let feats =+ map+ (F.col @Double)+ ["sepal_length", "sepal_width", "petal_length", "petal_width"]+ m = fit (PCAConfig (NComp 2) False) feats df+ goldEvr = asNums (g .! "pca" .! "evr")+ goldComp = asMatrix (g .! "pca" .! "components_abs")+ ourComp = map (map abs . VU.toList) (V.toList (pcaComponents m))+ assertBool+ "PCA explained variance ratio matches sklearn"+ (closeAll 1e-4 (VU.toList (pcaExplainedVarianceRatio m)) goldEvr)+ assertBool+ "PCA |components| match sklearn"+ (and (zipWith (closeAll 1e-3) ourComp goldComp))++testLogisticIrisParity :: Test+testLogisticIrisParity = TestCase $ do+ g <- loadGolden+ df <- D.readCsv "data/ml/iris.csv"+ let m = fit defaultLogisticConfig (F.col @Double "species") df+ preds = interpD df (predict m)+ truth = colDoubles df "species"+ acc = accuracyOf preds truth+ gold = asNum (g .! "logistic_iris" .! "accuracy")+ assertBool+ ("logistic iris accuracy within 0.06 of sklearn " ++ show (acc, gold))+ (acc >= gold - 0.06)++testSVCParity :: Test+testSVCParity = TestCase $ do+ g <- loadGolden+ df <- D.readCsv "data/ml/iris_binary.csv"+ let m = fit defaultSVCConfig (F.col @Double "label") df+ preds = interpD df (predict m)+ truth = colDoubles df "label"+ acc = accuracyOf preds truth+ gold = asNum (g .! "linear_svc" .! "accuracy")+ assertBool+ ("linear SVC accuracy within 0.06 of sklearn " ++ show (acc, gold))+ (acc >= gold - 0.06)++testGBMParity :: Test+testGBMParity = TestCase $ do+ g <- loadGolden+ df <- D.readCsv "data/ml/iris_binary.csv"+ let m =+ fit+ defaultGBConfig{gbLoss = LogisticDeviance, gbNEstimators = 100}+ (F.col @Double "label")+ df+ probs = interpD df (gbProbaExpr m)+ preds = map (\p -> if p > 0.5 then 1 else 0) probs+ truth = colDoubles df "label"+ acc = accuracyOf preds truth+ gold = asNum (g .! "gbm" .! "accuracy")+ assertBool+ ("GBM accuracy within 0.1 of sklearn " ++ show (acc, gold))+ (acc >= gold - 0.1)++testKMeansParity :: Test+testKMeansParity = TestCase $ do+ g <- loadGolden+ df <- D.readCsv "data/ml/blobs.csv"+ let m =+ fit+ defaultKMeansConfig{kmK = 3, kmNInit = 10, kmSeed = 0}+ [F.col @Double "x", F.col @Double "y"]+ df+ gold = asNum (g .! "kmeans" .! "inertia")+ assertBool+ ("k-means inertia within 10% of sklearn " ++ show (kmInertia m, gold))+ (abs (kmInertia m - gold) / gold < 0.1)++tests :: [Test]+tests =+ [ testOLSParity+ , testRidgeParity+ , testPCAParity+ , testLogisticIrisParity+ , testSVCParity+ , testGBMParity+ , testKMeansParity+ ]
+ tests/Learn/Synthesis.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Feature-synthesis enumerator: it should recover small exact features+(@x²@, @a/b@) from the example rows, score them near-perfectly, and be+deterministic.+-}+module Learn.Synthesis (tests) where++import qualified DataFrame as D+import DataFrame.Model (fit)+import DataFrame.Synthesis++import Test.HUnit++quad :: D.DataFrame+quad =+ D.fromNamedColumns+ [ ("x", D.fromList xs)+ , ("y", D.fromList (map (\x -> x * x) xs))+ ]+ where+ xs = map fromIntegral [1 .. 12 :: Int] :: [Double]++ratio :: D.DataFrame+ratio =+ D.fromNamedColumns+ [ ("a", D.fromList ([2, 6, 12, 20, 30, 42] :: [Double]))+ , ("b", D.fromList ([1, 2, 3, 4, 5, 6] :: [Double]))+ , ("y", D.fromList ([2, 3, 4, 5, 6, 7] :: [Double]))+ ]++-- | Pearson r²: enumeration should find x·x and score it ~1.+recoversQuadratic :: Test+recoversQuadratic = TestCase $ do+ let sf = fit defaultSynthesisConfig (D.col @Double "y") quad+ assertBool+ ("quadratic r2 = " ++ show (sfScore sf))+ (sfScore sf > 0.999 && sfScore sf <= 1.0001)++-- | MSE: the best feature reproduces the target exactly, so -MSE ~ 0.+exactRecoveryMSE :: Test+exactRecoveryMSE = TestCase $ do+ let sf =+ fit defaultSynthesisConfig{synLoss = MeanSquaredError} (D.col @Double "y") quad+ assertBool ("exact -mse = " ++ show (sfScore sf)) (sfScore sf > -1.0e-6)++-- | Division is enumerated (with the denominator guard): a/b is recovered.+recoversRatio :: Test+recoversRatio = TestCase $ do+ let sf = fit defaultSynthesisConfig (D.col @Double "y") ratio+ assertBool+ ("ratio r2 = " ++ show (sfScore sf))+ (sfScore sf > 0.999 && sfScore sf <= 1.0001)++-- | The bank is non-trivial and its ranked features are distinct expressions.+distinctFeatures :: Test+distinctFeatures = TestCase $ do+ let sf = fit defaultSynthesisConfig (D.col @Double "y") quad+ names = [D.prettyPrint e | (e, _) <- sfFeatures sf]+ assertBool "synthesizes more than one feature" (length names > 1)+ assertBool "ranked features are distinct" (length names == length (dedup names))+ where+ dedup = foldr (\x acc -> if x `elem` acc then acc else x : acc) []++-- | Same config and data give the same best expression.+deterministic :: Test+deterministic = TestCase $ do+ let a = fit defaultSynthesisConfig (D.col @Double "y") quad+ b = fit defaultSynthesisConfig (D.col @Double "y") quad+ assertEqual+ "same best expression"+ (D.prettyPrint (sfExpr a))+ (D.prettyPrint (sfExpr b))++tests :: [Test]+tests =+ [ recoversQuadratic+ , exactRecoveryMSE+ , recoversRatio+ , distinctFeatures+ , deterministic+ ]
+ tests/Learn/TypedModel.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | 'fit'/'predict' are frame-aware. Training on a 'TypedDataFrame' with a typed+target ('DT.col') returns a 'Fitted' carrying the schema, and 'predict' on it+yields a typed 'TExpr'; training on an untyped 'DataFrame' with an untyped target+('F.col') returns the bare model and an 'Expr'. Both paths must agree numerically.+-}+module Learn.TypedModel (tests) where++import Data.Maybe (fromJust)+import qualified Data.Vector.Unboxed as VU++import qualified DataFrame as D+import qualified DataFrame.Functions as F+import DataFrame.Internal.Column (TypedColumn (..), toVector)+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.Expression (Expr)+import DataFrame.Internal.Interpreter (interpret)++import DataFrame.LinearModel.Regression (+ LinearConfig (..),+ LinearRegressor (..),+ Penalty (..),+ defaultLinearConfig,+ )+import DataFrame.Model (Fitted (..), fit, predict)+import qualified DataFrame.Typed as DT++import Test.HUnit++-- | All-Double schema: two features and a Double target.+type Houses =+ '[ DT.Column "x1" Double+ , DT.Column "x2" Double+ , DT.Column "y" Double+ ]++regDF :: D.DataFrame+regDF =+ D.fromNamedColumns+ [ ("x1", DI.fromList xs1)+ , ("x2", DI.fromList xs2)+ , ("y", DI.fromList [2 * a - 3 * b + 1 | (a, b) <- zip xs1 xs2])+ ]+ where+ xs1 = [1, 2, 3, 4, 5, 6, 7, 8] :: [Double]+ xs2 = [2, 1, 4, 3, 6, 5, 8, 7] :: [Double]++housesTDF :: DT.TypedDataFrame Houses+housesTDF = fromJust (DT.freeze @Houses regDF)++interpD :: D.DataFrame -> Expr Double -> [Double]+interpD df e = case interpret @Double df e of+ Right (TColumn c) -> either (const []) VU.toList (toVector @Double @VU.Vector c)+ Left err -> error (show err)++close :: Double -> Double -> Double -> Bool+close tol a b = abs (a - b) <= tol++sameModel :: LinearRegressor -> LinearRegressor -> Bool+sameModel a b =+ and (zipWith (close 1e-12) (VU.toList (regCoef a)) (VU.toList (regCoef b)))+ && close 1e-12 (regIntercept a) (regIntercept b)++{- | 'fit' on a 'TypedDataFrame' returns a 'Fitted' whose model matches the+bare model from 'fit' on the equivalent 'DataFrame'.+-}+testTypedFitParity :: Test+testTypedFitParity = TestCase $ do+ let onTyped = fit defaultLinearConfig (DT.col @"y") housesTDF+ onUntyped = fit defaultLinearConfig (F.col @Double "y") regDF+ assertBool+ "fittedModel of typed fit == untyped fit"+ (sameModel (fittedModel onTyped) onUntyped)++{- | 'predict' on the typed fit yields a 'TExpr Houses Double'; unwrapped and+interpreted it matches 'predict' on the untyped fit.+-}+testTypedPredict :: Test+testTypedPredict = TestCase $ do+ let onTyped = fit defaultLinearConfig (DT.col @"y") housesTDF+ onUntyped = fit defaultLinearConfig (F.col @Double "y") regDF+ typedExpr = DT.unTExpr (predict onTyped) -- :: Expr Double via TExpr Houses Double+ untypedExpr = predict onUntyped -- :: Expr Double+ truth = interpD regDF (F.col @Double "y")+ assertBool+ "typed predict matches untyped predict"+ ( and+ (zipWith (close 1e-12) (interpD regDF typedExpr) (interpD regDF untypedExpr))+ )+ assertBool+ "typed predict recovers the target"+ (and (zipWith (close 1e-6) (interpD regDF typedExpr) truth))++-- | The frame-awareness holds for a regularized fit too.+testTypedRidge :: Test+testTypedRidge = TestCase $ do+ let cfg = defaultLinearConfig{lcPenalty = Ridge 0.01}+ onTyped = fit cfg (DT.col @"y") housesTDF+ onUntyped = fit cfg (F.col @Double "y") regDF+ assertBool+ "ridge: fittedModel of typed fit == untyped fit"+ (sameModel (fittedModel onTyped) onUntyped)++tests :: [Test]+tests =+ [ TestLabel "typed fit returns Fitted matching untyped" testTypedFitParity+ , TestLabel "typed predict yields TExpr matching untyped" testTypedPredict+ , TestLabel "typed ridge fit parity" testTypedRidge+ ]
tests/Main.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} module Main where@@ -10,9 +9,26 @@ import Test.QuickCheck import qualified Functions+import qualified IO.CSV+import qualified IO.CsvGolden import qualified IO.JSON+import qualified IR.ExprJsonRoundtrip+import qualified Internal.ColumnBuilder+import qualified Internal.DictEncode+import qualified Internal.Markdown+import qualified Internal.PackedText import qualified Internal.Parsing+import qualified LazyParity import qualified LazyParquet+import qualified Learn.Denotation+import qualified Learn.Ensembles+import qualified Learn.Metamorphic+import qualified Learn.MetricsTests+import qualified Learn.Models+import qualified Learn.Segmented+import qualified Learn.SklearnParity+import qualified Learn.Synthesis+import qualified Learn.TypedModel import qualified Monad import qualified Operations.Aggregations import qualified Operations.Apply@@ -20,45 +36,98 @@ import qualified Operations.Derive import qualified Operations.Filter import qualified Operations.GroupBy+import qualified Operations.Inference import qualified Operations.InsertColumn import qualified Operations.Join import qualified Operations.Merge+import qualified Operations.Nullable+import qualified Operations.NullableHashing+import qualified Operations.ParallelGroupBy+import qualified Operations.ParallelJoin+import qualified Operations.Provenance import qualified Operations.ReadCsv+import qualified Operations.Record+import qualified Operations.SetOps import qualified Operations.Shuffle import qualified Operations.Sort import qualified Operations.Statistics import qualified Operations.Subset import qualified Operations.Take import qualified Operations.Typing+import qualified Operations.VectorKernel+import qualified Operations.Window+import qualified Operations.WriteCsv+import qualified PackedTextMigration import qualified Parquet+import qualified Plotting+import qualified PrettyPrint import qualified Properties+import qualified Properties.Categorical+import qualified Simplify+import qualified Typed.IOReaders+import qualified Typed.Parity tests :: Test tests = TestList $- Internal.Parsing.tests+ Internal.ColumnBuilder.tests+ ++ Internal.DictEncode.tests+ ++ Internal.Markdown.tests+ ++ Internal.PackedText.tests+ ++ Internal.Parsing.tests+ ++ Learn.Denotation.tests+ ++ Learn.Models.tests+ ++ Learn.TypedModel.tests+ ++ Learn.Ensembles.tests+ ++ Learn.SklearnParity.tests+ ++ Learn.Synthesis.tests+ ++ Learn.MetricsTests.tests+ ++ Learn.Metamorphic.tests+ ++ Learn.Segmented.tests ++ Operations.Aggregations.tests ++ Operations.Apply.tests ++ Operations.Core.tests ++ Operations.Derive.tests ++ Operations.Filter.tests ++ Operations.GroupBy.tests+ ++ Operations.ParallelGroupBy.tests+ ++ Operations.ParallelJoin.tests+ ++ Operations.Inference.tests ++ Operations.InsertColumn.tests ++ Operations.Join.tests ++ Operations.Merge.tests+ ++ Operations.Nullable.tests+ ++ Operations.NullableHashing.tests+ ++ Operations.Provenance.tests ++ Operations.ReadCsv.tests+ ++ Operations.Record.tests+ ++ Operations.WriteCsv.tests+ ++ Operations.SetOps.tests ++ Operations.Shuffle.tests ++ Operations.Sort.tests ++ Operations.Statistics.tests+ ++ Operations.Subset.hunitTests ++ Operations.Take.tests ++ Operations.Typing.tests+ ++ Operations.VectorKernel.tests+ ++ Operations.Window.tests ++ Functions.tests+ ++ IO.CSV.tests+ ++ IO.CsvGolden.tests ++ IO.JSON.tests+ ++ IR.ExprJsonRoundtrip.tests ++ Parquet.tests ++ LazyParquet.tests+ ++ LazyParity.tests+ ++ Plotting.tests+ ++ Simplify.tests+ ++ PackedTextMigration.tests+ ++ PrettyPrint.tests+ ++ Typed.Parity.tests+ ++ Typed.IOReaders.tests isSuccessful :: Result -> Bool-isSuccessful (Success{..}) = True+isSuccessful (Success{}) = True isSuccessful _ = False main :: IO ()@@ -67,15 +136,21 @@ if failures result > 0 || errors result > 0 then Exit.exitFailure else do- -- Property tests propRes <- mapM (quickCheckWithResult stdArgs) Operations.Subset.tests monadRes <- mapM (quickCheckWithResult stdArgs) Monad.tests+ cbRes <-+ mapM+ (quickCheckWithResult stdArgs)+ Internal.ColumnBuilder.props propsRes <- mapM (quickCheckWithResult stdArgs) Properties.tests+ catRes <- mapM (quickCheckWithResult stdArgs) Properties.Categorical.tests if not (all isSuccessful propRes)+ || not (all isSuccessful cbRes) || not (all isSuccessful monadRes) || not (all isSuccessful propsRes)+ || not (all isSuccessful catRes) then Exit.exitFailure else Exit.exitSuccess
tests/Monad.hs view
@@ -8,7 +8,8 @@ import Test.QuickCheck import Test.QuickCheck.Monadic -roundToTwoPlaces x = fromIntegral (round (x * 100)) / 100.0+roundToTwoPlaces :: Double -> Double+roundToTwoPlaces x = fromIntegral (round (x * 100) :: Int) / 100.0 prop_sampleM :: DataFrame -> Gen (Gen Property) prop_sampleM df = monadic' $ do@@ -22,6 +23,9 @@ let finalRowCount = D.nRows finalDf let realRate = roundToTwoPlaces $ fromIntegral finalRowCount / fromIntegral rowCount let diff = abs $ expectedRate - realRate- assert (diff <= 0.11)+ -- calculates the 99.99% confidence interval (quickcheck runs 100 tests, aim for 1/10000)+ let tolerance' = 3.89 * sqrt (expectedRate * (1 - expectedRate) / fromIntegral rowCount)+ assert (diff <= tolerance') +tests :: [DataFrame -> Gen (Gen Property)] tests = [prop_sampleM]
tests/Operations/Aggregations.hs view
@@ -44,6 +44,50 @@ ) ) +countAllAggregation :: Test+countAllAggregation =+ TestCase+ ( assertEqual+ "countAll gives per-group row counts without a column argument"+ ( D.fromNamedColumns+ [ ("test1", DI.fromList [1 :: Int, 2, 3])+ , ("n", DI.fromList [6 :: Int, 3, 3])+ ]+ )+ ( testData+ & D.groupBy ["test1"]+ & D.aggregate [F.countAll `as` "n"]+ & D.sortBy [D.Asc (F.col @Int "test1")]+ )+ )++countAllAggregationTyped :: Test+countAllAggregationTyped =+ TestCase+ ( assertEqual+ "Typed countAll gives per-group row counts"+ ( D.fromNamedColumns+ [ ("test1", DI.fromList [1 :: Int, 2, 3])+ , ("n", DI.fromList [6 :: Int, 3, 3])+ ]+ )+ ( testData+ & either (error . show) id+ . DT.freezeWithError+ @[ DT.Column "test1" Int+ , DT.Column "test2" Int+ , DT.Column "test3" Int+ , DT.Column "test4" Char+ , DT.Column "test5" String+ , DT.Column "test6" Integer+ ]+ & DT.groupBy @'["test1"]+ & DT.aggregate (DT.as @"n" DT.countAll)+ & DT.sortBy [DT.asc (DT.col @"test1")]+ & DT.thaw+ )+ )+ foldAggregationTyped :: Test foldAggregationTyped = TestCase@@ -65,7 +109,7 @@ , DT.Column "test6" Integer ] & DT.groupBy @'["test1"]- & DT.aggregate (DT.agg @"test2_count" (DT.count (DT.col @"test2")) DT.aggNil)+ & DT.aggregate (DT.as @"test2_count" (DT.count (DT.col @"test2"))) & DT.sortBy [DT.asc (DT.col @"test1")] & DT.thaw )@@ -109,7 +153,7 @@ , DT.Column "test6" Integer ] & DT.groupBy @'["test1"]- & DT.aggregate (DT.agg @"test2_mean" (DT.mean (DT.col @"test2")) DT.aggNil)+ & DT.aggregate (DT.as @"test2_mean" (DT.mean (DT.col @"test2"))) & DT.sortBy [DT.asc (DT.col @"test1")] & DT.thaw )@@ -283,6 +327,8 @@ tests :: [Test] tests = [ TestLabel "foldAggregation" foldAggregation+ , TestLabel "countAllAggregation" countAllAggregation+ , TestLabel "countAllAggregationTyped" countAllAggregationTyped , TestLabel "foldAggregationTyped" foldAggregationTyped , TestLabel "numericAggregation" numericAggregation , TestLabel "numericAggregationTyped" numericAggregationTyped
tests/Operations/Apply.hs view
@@ -13,9 +13,11 @@ import qualified DataFrame.Internal.Column as DI import qualified DataFrame.Internal.DataFrame as D import qualified DataFrame.Internal.DataFrame as DI+import DataFrame.Operations.Statistics (imputeWith) import DataFrame.Operations.Transformations (impute) import Assertions+import Data.Maybe (listToMaybe) import Test.HUnit import Type.Reflection (typeRep) @@ -39,7 +41,7 @@ TestCase ( assertEqual "Boxed apply unboxed when result is unboxed"- (Just $ DI.UnboxedColumn (VU.fromList (replicate 26 (1 :: Int))))+ (Just $ DI.UnboxedColumn Nothing (VU.fromList (replicate 26 (1 :: Int)))) (DI.getColumn "test2" $ D.apply @String (const (1 :: Int)) "test2" testData) ) @@ -48,7 +50,7 @@ TestCase ( assertEqual "Boxed apply remains in boxed vector"- (Just $ DI.BoxedColumn (V.fromList (replicate 26 (1 :: Integer))))+ (Just $ DI.BoxedColumn Nothing (V.fromList (replicate 26 (1 :: Integer)))) (DI.getColumn "test2" $ D.apply @String (const (1 :: Integer)) "test2" testData) ) @@ -79,6 +81,20 @@ (print $ D.apply @[Char] (const (1 :: Int)) "test9" testData) ) +applyIntroducesNulls :: Test+applyIntroducesNulls =+ TestCase+ ( assertEqual+ "apply with a -> Maybe b yields a nullable column, not a boxed Maybe vector"+ (Just "NullableUnboxed")+ ( DI.columnVersionString+ <$> DI.getColumn "test" (D.apply @String listToMaybe "test" nullableApplyData)+ )+ )+ where+ nullableApplyData =+ D.fromNamedColumns [("test", DI.fromList ["", "b" :: String])]+ applyManyOnlyGivenFields :: Test applyManyOnlyGivenFields = TestCase@@ -88,7 +104,7 @@ ( 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+ filter (\(name, _col) -> name /= "test4" && name /= "test6") values ) ) ( D.applyMany @Char@@ -203,6 +219,7 @@ "applyWhere works as intended" ( Just $ DI.UnboxedColumn+ Nothing (VU.fromList (zipWith ($) (cycle [id, (+ 1)]) [(1 :: Int) .. 26])) ) ( D.getColumn "test5" $@@ -222,7 +239,7 @@ TestCase ( assertEqual "impute fills Nothing with the given value"- (Just $ DI.UnboxedColumn (VU.fromList [1 :: Int, 0, 3]))+ (Just $ DI.UnboxedColumn Nothing (VU.fromList [1 :: Int, 0, 3])) (DI.getColumn "opt" $ impute (F.col @(Maybe Int) "opt") 0 imputeData) ) @@ -238,18 +255,37 @@ imputeOnNonOptional :: Test imputeOnNonOptional = TestCase- ( assertExpectException- "[Error Case]"- "Cannot impute to a non-Empty column: plain"- (print $ impute (F.col @(Maybe Int) "plain") 0 imputeData)+ ( assertEqual+ "impute is a no-op on a non-nullable column"+ imputeData+ (impute (F.col @(Maybe Int) "plain") 0 imputeData) ) +imputePlainNoOp :: Test+imputePlainNoOp =+ TestCase+ ( assertEqual+ "impute with non-Maybe expr is always a no-op"+ imputeData+ (impute (F.col @Int "plain") 0 imputeData)+ )++imputeWithPlainNoOp :: Test+imputeWithPlainNoOp =+ TestCase+ ( assertEqual+ "imputeWith with non-Maybe expr is always a no-op"+ imputeData+ (imputeWith id (F.col @Int "plain") imputeData)+ )+ tests :: [Test] tests = [ TestLabel "applyBoxedToUnboxed" applyBoxedToUnboxed , TestLabel "applyWrongType" applyWrongType , TestLabel "applyUnknownColumn" applyUnknownColumn , TestLabel "applyBoxedToBoxed" applyBoxedToBoxed+ , TestLabel "applyIntroducesNulls" applyIntroducesNulls , TestLabel "applyManyBoxedToBoxed" applyManyBoxedToBoxed , TestLabel "applyManyOnlyGivenFields" applyManyOnlyGivenFields , TestLabel "applyManyBoxedToUnboxed" applyManyBoxedToUnboxed@@ -263,4 +299,6 @@ , TestLabel "imputeHappyPath" imputeHappyPath , TestLabel "imputeColumnNotFound" imputeColumnNotFound , TestLabel "imputeOnNonOptional" imputeOnNonOptional+ , TestLabel "imputePlainNoOp" imputePlainNoOp+ , TestLabel "imputeWithPlainNoOp" imputeWithPlainNoOp ]
tests/Operations/Derive.hs view
@@ -32,7 +32,8 @@ "derive works with column expression" ( Just $ DI.BoxedColumn- (V.fromList (zipWith (\n c -> show n ++ [c]) [1 .. 26] ['a' .. 'z']))+ Nothing+ (V.fromList (zipWith (\n c -> show n ++ [c]) ([1 .. 26] :: [Int]) ['a' .. 'z'])) ) ( DI.getColumn "test4" $ D.derive@@ -51,7 +52,7 @@ TestCase ( assertEqual "typed derive works with column expression"- (zipWith (\n c -> show n ++ [c]) [1 .. 26] ['a' .. 'z'])+ (zipWith (\n c -> show n ++ [c]) ([1 .. 26] :: [Int]) ['a' .. 'z']) ( DT.columnAsList @"test4" $ DT.derive @"test4"
tests/Operations/GroupBy.hs view
@@ -1,7 +1,10 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-} module Operations.GroupBy where +import qualified Data.List as L+import qualified Data.Map.Strict as M import qualified Data.Text as T import qualified Data.Vector.Unboxed as VU import qualified DataFrame as D@@ -47,13 +50,74 @@ TestCase ( assertExpectException "[Error Case]"- (D.columnNotFound "[\"test0\"]" "groupBy" (D.columnNames testData))+ (D.columnsNotFound ["test0"] "groupBy" (D.columnNames testData)) (print $ D.groupBy ["test0"] testData) ) +{- | The grouping invariant every backend (sequential now, parallel in Phase 2)+must satisfy: 'valueIndices' / 'offsets' / 'rowToGroup' agree, groups partition+all rows exactly once, each group is exactly the rows sharing a key tuple, and+no two groups share a key. Checked on a frame with several keys colliding under+the hash (forces the open-addressing key re-verification path).+-}+groupingInvariantHolds :: Test+groupingInvariantHolds =+ TestCase+ (assertBool "grouping partitions rows by exact key tuple" (checkGrouping df))+ where+ -- Repeated, interleaved keys across two columns; the dense int grid is the+ -- classic hash-collision stressor.+ df =+ D.fromNamedColumns+ [ ("k1", DI.fromList (map (`mod` 5) [0 .. 199 :: Int]))+ , ("k2", DI.fromList (map (\i -> (i * 7) `mod` 3) [0 .. 199 :: Int]))+ , ("v", DI.fromList [0 .. 199 :: Int])+ ]++{- | Recompute the reference grouping by the key tuple and assert the library's+'valueIndices'/'offsets'/'rowToGroup' describe the same partition.+-}+checkGrouping :: D.DataFrame -> Bool+checkGrouping df =+ let g = D.groupBy ["k1", "k2"] df+ vis = VU.toList (D.valueIndices g)+ os = VU.toList (D.offsets g)+ rtg = VU.toList (D.rowToGroup g)+ n = fst (D.dimensions df)+ k1 = colInts "k1"+ k2 = colInts "k2"+ colInts name = case D.getColumn name df of+ Just c -> DI.toList @Int c+ Nothing -> error "missing column"+ key i = (k1 !! i, k2 !! i)+ -- The group slices read out of vis/os.+ slices = [take (e - s) (drop s vis) | (s, e) <- zip os (tail os)]+ -- valueIndices must be a permutation of all rows.+ permutationOk = L.sort vis == [0 .. n - 1]+ -- Every slice is non-empty and internally key-constant.+ constKeys = all (\sl -> not (L.null sl) && allSame (map key sl)) slices+ -- Distinct groups have distinct keys.+ groupKeys = map (key . head) slices+ distinctKeys = L.length (L.nub groupKeys) == L.length groupKeys+ -- The number of groups equals the number of distinct key tuples.+ nGroupsOk = L.length slices == M.size (M.fromList [(key i, ()) | i <- [0 .. n - 1]])+ -- rowToGroup agrees with the slice each row lands in.+ rtgOk =+ and+ [ (rtg !! r) == gIdx+ | (gIdx, sl) <- zip [0 ..] slices+ , r <- sl+ ]+ in permutationOk && constKeys && distinctKeys && nGroupsOk && rtgOk++allSame :: (Eq a) => [a] -> Bool+allSame [] = True+allSame (x : xs) = all (== x) xs+ tests :: [Test] tests = [ TestLabel "groupBySingleRowWAI" groupBySingleRowWAI , TestLabel "groupByMultipleRowsWAI" groupByMultipleRowsWAI , TestLabel "groupByColumnDoesNotExist" groupByColumnDoesNotExist+ , TestLabel "groupingInvariantHolds" groupingInvariantHolds ]
+ tests/Operations/Inference.hs view
@@ -0,0 +1,229 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Round-2 (S2) pins for the single-pass inference lattice and the+Int -> Double promotion path: sample classification via the WS-B byte+parsers, prefix promotion instead of full-column re-parse, and the+byte-level missing-token test in the default reader's inferred path.+-}+module Operations.Inference where++import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BL+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import qualified DataFrame as D+import qualified DataFrame.IO.CSV as CSV+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Operations.Typing as D++import Data.Time (Day)+import DataFrame.Internal.DataFrame (getColumn)+import Test.HUnit (Test (TestCase, TestLabel), assertEqual, assertFailure)++assumeBytes :: [Maybe BS.ByteString] -> D.ParsingAssumption+assumeBytes = D.makeParsingAssumptionBytes "%Y-%m-%d" . V.fromList++assumeText :: [Maybe T.Text] -> D.ParsingAssumption+assumeText = D.makeParsingAssumption "%Y-%m-%d" . V.fromList++-- The candidate-mask priorities reproduce the documented fallback order:+-- Bool, then Int (only when the Double mask agrees), Double, Date, Text;+-- an all-null sample stays NoAssumption.+latticePriorities :: Test+latticePriorities = TestCase $ do+ let cases =+ [ ([Just "True", Just "FALSE"], D.BoolAssumption, "bools")+ , ([Just "1", Just "-42"], D.IntAssumption, "ints")+ , ([Just "1", Just "2.5"], D.DoubleAssumption, "int+double")+ , ([Just "1e3", Just "0.5"], D.DoubleAssumption, "doubles")+ , ([Just "2024-01-02", Just "1999-12-31"], D.DateAssumption, "dates")+ , ([Just "abc", Just "1"], D.TextAssumption, "text")+ , ([Nothing, Nothing], D.NoAssumption, "all null")+ , ([], D.NoAssumption, "empty sample")+ , ([Nothing, Just "7"], D.IntAssumption, "null then int")+ ]+ mapM_+ ( \(cells, expected, what) -> do+ assertEqual ("bytes: " <> what) expected (assumeBytes cells)+ assertEqual+ ("text: " <> what)+ expected+ (assumeText (map (fmap TE.decodeUtf8) cells))+ )+ cases++-- Pinned new behavior (WS-B parser semantics in the sample): an+-- overflowing integer clears the Int candidate instead of wrapping, so+-- the column classifies (and parses) as Double.+latticeOverflowIntIsDouble :: Test+latticeOverflowIntIsDouble = TestCase $ do+ assertEqual+ "bytes overflow -> Double"+ D.DoubleAssumption+ (assumeBytes [Just "9223372036854775808"])+ assertEqual+ "text overflow -> Double"+ D.DoubleAssumption+ (assumeText [Just "9223372036854775808"])++-- Pinned new behavior: byte-level strip-tolerant classification accepts+-- ASCII-padded numerics (the strip-equivalent unification, audit+-- divergence #2), where the old Text-path sample rejected " 42" as+-- Double and demoted to Text.+latticePaddedIntIsInt :: Test+latticePaddedIntIsInt =+ TestCase+ (assertEqual "padded int" D.IntAssumption (assumeText [Just " 42 "]))++-- End-to-end: an overflowing cell inside the sample now yields a Double+-- column with the correctly converted value (old: silently wrapped Int).+parseOverflowIntsAsDoubles :: Test+parseOverflowIntsAsDoubles =+ let beforeParse = ["9223372036854775808", "1", "2"] :: [T.Text]+ afterParse = [9.223372036854776e18, 1, 2] :: [Double]+ expected = DI.fromVector (V.fromList (map Just afterParse))+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 3}) $+ DI.fromVector (V.fromList beforeParse)+ in TestCase (assertEqual "overflow -> Double column" expected actual)++-- Promotion: a Double cell far past the sample converts the built Int+-- prefix in place; values must equal a from-scratch Double parse.+promoteIntPrefixToDouble :: Test+promoteIntPrefixToDouble =+ let ints = map (T.pack . show) ([1 .. 150] :: [Int])+ beforeParse = ints ++ ["0.5"]+ afterParse = map fromIntegral ([1 .. 150] :: [Int]) ++ [0.5] :: [Double]+ expected = DI.UnboxedColumn Nothing (VU.fromList afterParse)+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector (V.fromList beforeParse)+ in TestCase (assertEqual "Int prefix promoted to Double" expected actual)++-- An overflowing integer past the sample triggers promotion with its+-- true Double value (the old Text-path Int parse silently wrapped it).+promoteOverflowPastSample :: Test+promoteOverflowPastSample =+ let ints = map (T.pack . show) ([1 .. 30] :: [Int])+ beforeParse = ints ++ ["18446744073709551616", "1.5"]+ afterParse =+ map fromIntegral ([1 .. 30] :: [Int])+ ++ [1.8446744073709552e19, 1.5] ::+ [Double]+ expected = DI.UnboxedColumn Nothing (VU.fromList afterParse)+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector (V.fromList beforeParse)+ in TestCase (assertEqual "overflow past sample -> true Double" expected actual)++-- Promotion preserves null positions accumulated during the Int phase.+promotionPreservesNulls :: Test+promotionPreservesNulls =+ let beforeParse =+ map (T.pack . show) ([1 .. 20] :: [Int])+ ++ [""]+ ++ map (T.pack . show) ([21 .. 40] :: [Int])+ ++ ["2.5"]+ afterParse =+ map (Just . fromIntegral) ([1 .. 20] :: [Int])+ ++ [Nothing]+ ++ map (Just . fromIntegral) ([21 .. 40] :: [Int])+ ++ [Just 2.5] ::+ [Maybe Double]+ expected = DI.fromVector (V.fromList afterParse)+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector (V.fromList beforeParse)+ in TestCase (assertEqual "nulls survive promotion" expected actual)++-- A cell that parses as neither Int nor Double demotes the whole column+-- to Text with every raw cell preserved (re-extracted, not re-parsed).+promotionDemotesToText :: Test+promotionDemotesToText =+ let beforeParse = map (T.pack . show) ([1 .. 30] :: [Int]) ++ ["abc"]+ expected = DI.BoxedColumn Nothing (V.fromList beforeParse)+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector (V.fromList beforeParse)+ in TestCase (assertEqual "demotes to Text, raw values kept" expected actual)++-- Default (cassava) reader, inferred path: the same promotion runs over+-- the retained raw bytes.+readerPromotesIntColumn :: Test+readerPromotesIntColumn = TestCase $ do+ let csv =+ "x\n"+ <> T.intercalate "\n" (map (T.pack . show) ([1 .. 150] :: [Int]))+ <> "\n0.5\n"+ expected =+ DI.UnboxedColumn+ Nothing+ (VU.fromList (map fromIntegral ([1 .. 150] :: [Int]) ++ [0.5] :: [Double]))+ df <- CSV.fromCsvBytes (BL.fromStrict (TE.encodeUtf8 csv))+ case getColumn "x" df of+ Just col -> assertEqual "reader Int->Double promotion" expected col+ Nothing -> assertFailure "column x missing"++-- Default reader: canonical missing tokens null the cell through the+-- byte-level fast path, exactly as the Text-decode check did.+readerCanonicalMissingTokens :: Test+readerCanonicalMissingTokens = TestCase $ do+ let csv = "x,y\nNA,1\nnan,2\nNothing,3\n ,4\nNULL,5\n"+ expectedX = DI.fromVector (V.fromList (replicate 5 (Nothing :: Maybe T.Text)))+ expectedY = DI.UnboxedColumn Nothing (VU.fromList ([1 .. 5] :: [Int]))+ df <- CSV.fromCsvBytes (BL.fromStrict (TE.encodeUtf8 csv))+ case getColumn "x" df of+ Just col -> assertEqual "all-missing column" expectedX col+ Nothing -> assertFailure "column x missing"+ case getColumn "y" df of+ Just col -> assertEqual "int column" expectedY col+ Nothing -> assertFailure "column y missing"++-- A custom missing list replaces (not extends) the canonical one: "NA"+-- must then survive as text.+readerCustomMissingTokens :: Test+readerCustomMissingTokens = TestCase $ do+ let csv = "x\nfoo\nNA\n7\n"+ opts = CSV.defaultReadOptions{CSV.missingIndicators = ["foo"]}+ expected =+ DI.fromVector+ (V.fromList [Nothing, Just ("NA" :: T.Text), Just "7"])+ df <- CSV.decodeSeparated opts (BL.fromStrict (TE.encodeUtf8 csv))+ case getColumn "x" df of+ Just col -> assertEqual "custom list only" expected col+ Nothing -> assertFailure "column x missing"++-- Default reader date columns still parse (the WS-B fast date path is+-- bit-identical to readByteStringDate for %Y-%m-%d).+readerDatesStillParse :: Test+readerDatesStillParse = TestCase $ do+ let csv = "d\n2024-01-01\n2024-01-02\n2024-01-03\n"+ expected =+ DI.fromVector+ (V.fromList (map (read @Day) ["2024-01-01", "2024-01-02", "2024-01-03"]))+ df <- CSV.fromCsvBytes (BL.fromStrict (TE.encodeUtf8 csv))+ case getColumn "d" df of+ Just col -> assertEqual "date column" expected col+ Nothing -> assertFailure "column d missing"++tests :: [Test]+tests =+ [ TestLabel "lattice_priorities" latticePriorities+ , TestLabel "lattice_overflow_int_is_double" latticeOverflowIntIsDouble+ , TestLabel "lattice_padded_int_is_int" latticePaddedIntIsInt+ , TestLabel "parse_overflow_ints_as_doubles" parseOverflowIntsAsDoubles+ , TestLabel "promote_int_prefix_to_double" promoteIntPrefixToDouble+ , TestLabel "promote_overflow_past_sample" promoteOverflowPastSample+ , TestLabel "promotion_preserves_nulls" promotionPreservesNulls+ , TestLabel "promotion_demotes_to_text" promotionDemotesToText+ , TestLabel "reader_promotes_int_column" readerPromotesIntColumn+ , TestLabel "reader_canonical_missing_tokens" readerCanonicalMissingTokens+ , TestLabel "reader_custom_missing_tokens" readerCustomMissingTokens+ , TestLabel "reader_dates_still_parse" readerDatesStillParse+ ]
tests/Operations/InsertColumn.hs view
@@ -31,7 +31,9 @@ TestCase ( assertEqual "Two columns should be equal"- (Just $ DI.BoxedColumn (V.fromList ["Thuba" :: T.Text, "Zodwa", "Themba"]))+ ( Just $+ DI.BoxedColumn Nothing (V.fromList ["Thuba" :: T.Text, "Zodwa", "Themba"])+ ) ( DI.getColumn "new" $ D.insertVector "new" (V.fromList ["Thuba" :: T.Text, "Zodwa", "Themba"]) D.empty )@@ -57,7 +59,7 @@ TestCase ( assertEqual "Value should be boxed"- (Just $ DI.UnboxedColumn (VU.fromList [1 :: Int, 2, 3]))+ (Just $ DI.UnboxedColumn Nothing (VU.fromList [1 :: Int, 2, 3])) (DI.getColumn "new" $ D.insertVector "new" (V.fromList [1 :: Int, 2, 3]) D.empty) ) @@ -77,7 +79,7 @@ ( assertEqual "Missing values should be replaced with Nothing" ( Just $- DI.OptionalColumn+ DI.fromVector (V.fromList [Just "a" :: Maybe T.Text, Just "b", Just "c", Nothing, Nothing]) ) ( DI.getColumn "newer" $@@ -92,7 +94,7 @@ ( assertEqual "Missing values should be replaced with Nothing" ( Just $- DI.OptionalColumn+ DI.fromVector (V.fromList [Just 1 :: Maybe Int, Just 2, Just 3, Nothing, Nothing]) ) ( DI.getColumn "newer" $@@ -106,7 +108,7 @@ TestCase ( assertEqual "Missing values should be replaced with Nothing"- (Just $ DI.UnboxedColumn (VU.fromList [1 :: Int, 2, 3, 0, 0]))+ (Just $ DI.UnboxedColumn Nothing (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@@ -118,7 +120,7 @@ TestCase ( assertEqual "Lists should be the same size"- (Just $ DI.UnboxedColumn (VU.fromList [(6 :: Int) .. 10]))+ (Just $ DI.UnboxedColumn Nothing (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
tests/Operations/Join.hs view
@@ -4,10 +4,12 @@ module Operations.Join where -import Data.Text (Text)-import Data.These+import Assertions (assertExpectException)+import Control.Exception (evaluate)+import Data.Text (Text, unpack) import qualified DataFrame as D import qualified DataFrame.Functions as F+import DataFrame.Internal.Types (These (..)) import DataFrame.Operations.Join import qualified DataFrame.Typed as DT import Test.HUnit@@ -26,6 +28,21 @@ , ("B", D.fromList ["B0" :: Text, "B1", "B2"]) ] +assertMissingJoinColumns :: String -> [Text] -> D.DataFrame -> Assertion+assertMissingJoinColumns preface missingKeys result = do+ assertExpectException+ preface+ (if length missingKeys == 1 then "Column not found" else "Columns not found")+ (evaluate (D.nRows result))+ mapM_+ ( \missingKey ->+ assertExpectException preface (unpack missingKey) (evaluate (D.nRows result))+ )+ missingKeys++assertMissingJoinColumn :: String -> Text -> D.DataFrame -> Assertion+assertMissingJoinColumn preface missingKey = assertMissingJoinColumns preface [missingKey]+ testInnerJoin :: Test testInnerJoin = TestCase@@ -102,6 +119,45 @@ (DT.thaw $ DT.sortBy [DT.asc (DT.col @"key")] (DT.leftJoin @'["key"] tdf1 tdf2)) ) +-- A right-hand frame whose payload column is already optional.+dfOptional :: D.DataFrame+dfOptional =+ D.fromNamedColumns+ [ ("key", D.fromList ["K0" :: Text, "K1"])+ , ("C", D.fromList [Just 10 :: Maybe Int, Just 11])+ ]++tdfOptional ::+ DT.TypedDataFrame [DT.Column "key" Text, DT.Column "C" (Maybe Int)]+tdfOptional = either (error . show) id (DT.freezeWithError dfOptional)++{- | A left join over an already-optional column must not nest the Maybe: the+explicit @Maybe Int@ result schema below only type-checks because 'WrapMaybe'+flattens @Maybe (Maybe Int)@ to @Maybe Int@, matching the runtime column.+-}+testLeftJoinTypedOptional :: Test+testLeftJoinTypedOptional =+ TestCase+ ( assertEqual+ "Typed left join keeps an already-optional column single-Maybe"+ ( D.fromNamedColumns+ [ ("key", D.fromList ["K0" :: Text, "K1", "K2", "K3", "K4", "K5"])+ , ("A", D.fromList ["A0" :: Text, "A1", "A2", "A3", "A4", "A5"])+ ,+ ( "C"+ , D.fromList+ ([Just 10, Just 11, Nothing, Nothing, Nothing, Nothing] :: [Maybe Int])+ )+ ]+ )+ (DT.thaw $ DT.sortBy [DT.asc (DT.col @"key")] joined)+ )+ where+ joined ::+ DT.TypedDataFrame+ [DT.Column "key" Text, DT.Column "A" Text, DT.Column "C" (Maybe Int)]+ joined = DT.leftJoin @'["key"] tdf1 tdfOptional+ testRightJoinTyped :: Test testRightJoinTyped = TestCase@@ -163,6 +219,43 @@ (D.sortBy [D.Asc (F.col @Text "Name")] (fullOuterJoin ["Name"] studentDf staffDf)) ) +testFullOuterJoinUnboxedKey :: Test+testFullOuterJoinUnboxedKey =+ TestCase+ ( assertEqual+ "Full outer join on an unboxed (Int) key column coalesces correctly"+ ( D.fromNamedColumns+ [ ("customer_id", D.fromList [1 :: Int, 2, 3, 4])+ ,+ ( "amount"+ , D.fromList [Just 250.0 :: Maybe Double, Just 80.0, Nothing, Just 125.0]+ )+ ,+ ( "name"+ , D.fromList [Just "Ada" :: Maybe Text, Just "Lin", Just "Grace", Nothing]+ )+ ]+ )+ ( D.sortBy+ [D.Asc (F.col @Int "customer_id")]+ (fullOuterJoin ["customer_id"] ordersDf customersDf)+ )+ )++ordersDf :: D.DataFrame+ordersDf =+ D.fromNamedColumns+ [ ("customer_id", D.fromList [1 :: Int, 2, 4])+ , ("amount", D.fromList [250.0 :: Double, 80.0, 125.0])+ ]++customersDf :: D.DataFrame+customersDf =+ D.fromNamedColumns+ [ ("customer_id", D.fromList [1 :: Int, 2, 3])+ , ("name", D.fromList ["Ada" :: Text, "Lin", "Grace"])+ ]+ dfL :: D.DataFrame dfL = D.fromNamedColumns@@ -253,6 +346,59 @@ (D.sortBy [D.Asc (F.col @Text "key")] (fullOuterJoin ["key"] dfL dfR)) ) +testInnerJoinMissingKey :: Test+testInnerJoinMissingKey =+ TestCase $+ assertMissingJoinColumn+ "Inner join should fail when the join key is missing"+ "Cats"+ (innerJoin ["Cats"] df1 df2)++testLeftJoinMissingKey :: Test+testLeftJoinMissingKey =+ TestCase $+ assertMissingJoinColumn+ "Left join should fail when the join key is missing"+ "Cats"+ (leftJoin ["Cats"] df1 df2)++testRightJoinMissingKey :: Test+testRightJoinMissingKey =+ TestCase $+ assertMissingJoinColumn+ "Right join should fail when the join key is missing"+ "Animals"+ (rightJoin ["Animals"] df1 df2)++testFullOuterJoinMissingKey :: Test+testFullOuterJoinMissingKey =+ TestCase $+ assertMissingJoinColumn+ "Full outer join should fail when the join key is missing"+ "Cats"+ (fullOuterJoin ["Cats"] df1 df2)++testInnerJoinMissingKeys :: Test+testInnerJoinMissingKeys =+ TestCase $+ assertMissingJoinColumns+ "Inner join should report every missing join key"+ ["Animals", "Cats"]+ (innerJoin ["Animals", "Cats"] df1 df2)++testInnerJoinMissingKeysSuggestion :: Test+testInnerJoinMissingKeysSuggestion =+ TestCase $+ let typoDf =+ D.fromNamedColumns+ [ ("hello", D.fromList ["H" :: Text])+ , ("world", D.fromList ["W" :: Text])+ ]+ in assertExpectException+ "Inner join should report consolidated suggestions for missing join keys"+ "Did you mean [\"hello\", \"world\"]?"+ (evaluate (D.nRows (innerJoin ["helo", "wrld"] typoDf typoDf)))+ -- Empty DataFrame fixtures: same schema as df1/df2 but zero rows. emptyDf1 :: D.DataFrame emptyDf1 =@@ -340,23 +486,77 @@ (D.nRows (leftJoin ["key"] manyLeft manyRight)) ) +-- Stress the open-addressing hash index with many distinct integer keys.+-- Left keys 0..9999, right keys 5000..14999 (overlap 5000..9999).+bigLeft :: D.DataFrame+bigLeft =+ D.fromNamedColumns+ [ ("key", D.fromList [0 .. 9999 :: Int])+ , ("la", D.fromList [i * 2 | i <- [0 .. 9999 :: Int]])+ ]++bigRight :: D.DataFrame+bigRight =+ D.fromNamedColumns+ [ ("key", D.fromList [5000 .. 14999 :: Int])+ , ("rb", D.fromList [i * 3 | i <- [5000 .. 14999 :: Int]])+ ]++testBigInnerJoinRowCount :: Test+testBigInnerJoinRowCount =+ TestCase+ ( assertEqual+ "Inner join over 10k distinct keys matches overlap size"+ 5000+ (D.nRows (innerJoin ["key"] bigLeft bigRight))+ )++testBigLeftJoinRowCount :: Test+testBigLeftJoinRowCount =+ TestCase+ ( assertEqual+ "Left join over 10k distinct keys keeps all left rows"+ 10000+ (D.nRows (leftJoin ["key"] bigLeft bigRight))+ )++testBigFullOuterRowCount :: Test+testBigFullOuterRowCount =+ TestCase+ ( assertEqual+ "Full outer join over 10k distinct keys = union size"+ 15000+ (D.nRows (fullOuterJoin ["key"] bigLeft bigRight))+ )+ tests :: [Test] tests = [ TestLabel "innerJoin" testInnerJoin , TestLabel "testInnerJoinTyped" testInnerJoinTyped , TestLabel "leftJoin" testLeftJoin , TestLabel "testLeftJoinTyped" testLeftJoinTyped+ , TestLabel "testLeftJoinTypedOptional" testLeftJoinTypedOptional , TestLabel "rightJoin" testRightJoin , TestLabel "testRightJoinTyped" testRightJoinTyped , TestLabel "fullOuterJoin" testFullOuterJoin+ , TestLabel "fullOuterJoinUnboxedKey" testFullOuterJoinUnboxedKey , TestLabel "innerJoinWithCollisions" testInnerJoinWithCollisions , TestLabel "leftJoinWithCollisions" testLeftJoinWithCollisions , TestLabel "rightJoinWithCollisions" testRightJoinWithCollisions , TestLabel "outerJoinWithCollisions" testOuterJoinWithCollisions+ , TestLabel "innerJoinMissingKey" testInnerJoinMissingKey+ , TestLabel "leftJoinMissingKey" testLeftJoinMissingKey+ , TestLabel "rightJoinMissingKey" testRightJoinMissingKey+ , TestLabel "fullOuterJoinMissingKey" testFullOuterJoinMissingKey+ , TestLabel "innerJoinMissingKeys" testInnerJoinMissingKeys+ , TestLabel "innerJoinMissingKeysSuggestion" testInnerJoinMissingKeysSuggestion , TestLabel "innerJoinBothEmpty" testInnerJoinBothEmpty , TestLabel "innerJoinLeftEmpty" testInnerJoinLeftEmpty , TestLabel "innerJoinRightEmpty" testInnerJoinRightEmpty , TestLabel "leftJoinRightEmpty" testLeftJoinRightEmpty , TestLabel "manyToManyInnerJoin" testManyToManyInnerJoin , TestLabel "manyToManyLeftJoin" testManyToManyLeftJoin+ , TestLabel "bigInnerJoinRowCount" testBigInnerJoinRowCount+ , TestLabel "bigLeftJoinRowCount" testBigLeftJoinRowCount+ , TestLabel "bigFullOuterRowCount" testBigFullOuterRowCount ]
+ tests/Operations/Nullable.hs view
@@ -0,0 +1,773 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++module Operations.Nullable where++import qualified Data.Vector as V+import qualified DataFrame as D+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Internal.DataFrame as DI+import DataFrame.Internal.Expression (Expr)+import DataFrame.Operators ((.*), (.+), (.-), (./), (.==))+import qualified DataFrame.Typed as DT+import qualified DataFrame.Typed.Expr as TE+import DataFrame.Typed.Types (TExpr (..))++import Test.HUnit++-- ---------------------------------------------------------------------------+-- Untyped Expr layer tests+-- ---------------------------------------------------------------------------++-- DataFrame with one plain Int column and one Maybe Int column+testData :: D.DataFrame+testData =+ D.fromNamedColumns+ [ ("x", DI.fromList ([1, 2, 3] :: [Int]))+ , ("y", DI.fromVector (V.fromList [Just 10, Nothing, Just 30 :: Maybe Int]))+ ]++-- | col @Int .+ col @(Maybe Int) should give Maybe Int column+addIntMaybeInt :: Test+addIntMaybeInt =+ TestCase+ ( assertEqual+ "Int .+ Maybe Int = Maybe Int"+ (Just $ DI.fromVector (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.col @Int "x" .+ F.col @(Maybe Int) "y")+ testData+ )+ )++-- | col @(Maybe Int) .+ col @Int should give Maybe Int column+addMaybeIntInt :: Test+addMaybeIntInt =+ TestCase+ ( assertEqual+ "Maybe Int .+ Int = Maybe Int"+ (Just $ DI.fromVector (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.col @(Maybe Int) "y" .+ F.col @Int "x")+ testData+ )+ )++-- | col @Int .+ col @Int (same-type non-nullable) should give Int column+addIntInt :: Test+addIntInt =+ TestCase+ ( assertEqual+ "Int .+ Int = Int (non-nullable preserved)"+ (Just $ DI.fromList [2, 4, 6 :: Int])+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.col @Int "x" .+ F.col @Int "x")+ testData+ )+ )++-- | col @(Maybe Int) .+ col @(Maybe Int) should give Maybe Int column+addMaybeMaybe :: Test+addMaybeMaybe =+ TestCase+ ( assertEqual+ "Maybe Int .+ Maybe Int = Maybe Int"+ (Just $ DI.fromVector (V.fromList [Just 20, Nothing, Just 60 :: Maybe Int]))+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.col @(Maybe Int) "y" .+ F.col @(Maybe Int) "y")+ testData+ )+ )++-- | Subtraction: Int .- Maybe Int+subIntMaybeInt :: Test+subIntMaybeInt =+ TestCase+ ( assertEqual+ "Int .- Maybe Int = Maybe Int"+ ( Just $+ DI.fromVector (V.fromList [Just (-9), Nothing, Just (-27) :: Maybe Int])+ )+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.col @Int "x" .- F.col @(Maybe Int) "y")+ testData+ )+ )++-- | Comparison: Int .== Maybe Int gives Maybe Bool+eqIntMaybeInt :: Test+eqIntMaybeInt =+ TestCase+ ( assertEqual+ "Int .== Maybe Int = Maybe Bool"+ ( Just $+ DI.fromVector (V.fromList [Just False, Nothing, Just False :: Maybe Bool])+ )+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.col @Int "x" .== F.col @(Maybe Int) "y" :: Expr (Maybe Bool))+ testData+ )+ )++-- | Comparison: Int .== Int gives plain Bool (backwards compatible)+eqIntInt :: Test+eqIntInt =+ TestCase+ ( assertEqual+ "Int .== Int = Bool (non-nullable, backwards compatible)"+ (Just $ DI.fromList [True, False, False :: Bool])+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.col @Int "x" .== F.lit (1 :: Int) :: Expr Bool)+ testData+ )+ )++-- ---------------------------------------------------------------------------+-- nullLift / nullLift2 — untyped layer+-- ---------------------------------------------------------------------------++-- | nullLift negate on Maybe Int propagates Nothing+nullLiftMaybeInt :: Test+nullLiftMaybeInt =+ TestCase+ ( assertEqual+ "nullLift negate (Maybe Int) propagates Nothing"+ ( Just $+ DI.fromVector (V.fromList [Just (-10), Nothing, Just (-30) :: Maybe Int])+ )+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.nullLift negate (F.col @(Maybe Int) "y") :: Expr (Maybe Int))+ testData+ )+ )++-- | nullLift negate on plain Int — no Nothing, plain result+nullLiftInt :: Test+nullLiftInt =+ TestCase+ ( assertEqual+ "nullLift negate (Int) gives Int column"+ (Just $ DI.fromList [-1, -2, -3 :: Int])+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.nullLift negate (F.col @Int "x") :: Expr Int)+ testData+ )+ )++-- | nullLift2 (+) Int (Maybe Int) → Maybe Int+nullLift2IntMaybeInt :: Test+nullLift2IntMaybeInt =+ TestCase+ ( assertEqual+ "nullLift2 (+) Int (Maybe Int) = Maybe Int"+ (Just $ DI.fromVector (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.nullLift2 (+) (F.col @Int "x") (F.col @(Maybe Int) "y") :: Expr (Maybe Int))+ testData+ )+ )++-- | nullLift2 (+) (Maybe Int) Int → Maybe Int+nullLift2MaybeIntInt :: Test+nullLift2MaybeIntInt =+ TestCase+ ( assertEqual+ "nullLift2 (+) (Maybe Int) Int = Maybe Int"+ (Just $ DI.fromVector (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.nullLift2 (+) (F.col @(Maybe Int) "y") (F.col @Int "x") :: Expr (Maybe Int))+ testData+ )+ )++-- | nullLift2 (+) Int Int → plain Int+nullLift2IntInt :: Test+nullLift2IntInt =+ TestCase+ ( assertEqual+ "nullLift2 (+) Int Int = Int (non-nullable)"+ (Just $ DI.fromList [2, 4, 6 :: Int])+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.nullLift2 (+) (F.col @Int "x") (F.col @Int "x") :: Expr Int)+ testData+ )+ )++-- ---------------------------------------------------------------------------+-- Cross-type arithmetic (Int × Double promotion)+-- ---------------------------------------------------------------------------++-- DataFrame with Int, Maybe Int, Double, Maybe Double columns+crossData :: D.DataFrame+crossData =+ D.fromNamedColumns+ [ ("x", DI.fromList ([1, 2, 3] :: [Int]))+ , ("y", DI.fromVector (V.fromList [Just 10, Nothing, Just 30 :: Maybe Int]))+ , ("d", DI.fromList ([1.5, 2.5, 3.5] :: [Double]))+ ,+ ( "md"+ , DI.fromVector (V.fromList [Just 10.5, Nothing, Just 30.5 :: Maybe Double])+ )+ ]++-- | col @Int .+ col @Double → Double+addIntDouble :: Test+addIntDouble =+ TestCase+ ( assertEqual+ "Int .+ Double = Double"+ (Just $ DI.fromList [2.5, 4.5, 6.5 :: Double])+ ( DI.getColumn "result" $+ D.derive "result" (F.col @Int "x" .+ F.col @Double "d") crossData+ )+ )++-- | col @Double .+ col @Int → Double+addDoubleInt :: Test+addDoubleInt =+ TestCase+ ( assertEqual+ "Double .+ Int = Double"+ (Just $ DI.fromList [2.5, 4.5, 6.5 :: Double])+ ( DI.getColumn "result" $+ D.derive "result" (F.col @Double "d" .+ F.col @Int "x") crossData+ )+ )++-- | col @(Maybe Int) .+ col @Double → Maybe Double+addMaybeIntDouble :: Test+addMaybeIntDouble =+ TestCase+ ( assertEqual+ "Maybe Int .+ Double = Maybe Double"+ ( Just $+ DI.fromVector (V.fromList [Just 11.5, Nothing, Just 33.5 :: Maybe Double])+ )+ ( DI.getColumn "result" $+ D.derive "result" (F.col @(Maybe Int) "y" .+ F.col @Double "d") crossData+ )+ )++-- | col @Int .+ col @(Maybe Double) → Maybe Double+addIntMaybeDouble :: Test+addIntMaybeDouble =+ TestCase+ ( assertEqual+ "Int .+ Maybe Double = Maybe Double"+ ( Just $+ DI.fromVector (V.fromList [Just 11.5, Nothing, Just 33.5 :: Maybe Double])+ )+ ( DI.getColumn "result" $+ D.derive "result" (F.col @Int "x" .+ F.col @(Maybe Double) "md") crossData+ )+ )++-- | col @(Maybe Int) .+ col @(Maybe Double) → Maybe Double+addMaybeIntMaybeDouble :: Test+addMaybeIntMaybeDouble =+ TestCase+ ( assertEqual+ "Maybe Int .+ Maybe Double = Maybe Double"+ ( Just $+ DI.fromVector (V.fromList [Just 20.5, Nothing, Just 60.5 :: Maybe Double])+ )+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.col @(Maybe Int) "y" .+ F.col @(Maybe Double) "md")+ crossData+ )+ )++-- | col @Int .- col @Double → Double+subIntDouble :: Test+subIntDouble =+ TestCase+ ( assertEqual+ "Int .- Double = Double"+ (Just $ DI.fromList [-0.5, -0.5, -0.5 :: Double])+ ( DI.getColumn "result" $+ D.derive "result" (F.col @Int "x" .- F.col @Double "d") crossData+ )+ )++-- | col @Int .* col @Double → Double+mulIntDouble :: Test+mulIntDouble =+ TestCase+ ( assertEqual+ "Int .* Double = Double"+ (Just $ DI.fromList [1.5, 5.0, 10.5 :: Double])+ ( DI.getColumn "result" $+ D.derive "result" (F.col @Int "x" .* F.col @Double "d") crossData+ )+ )++-- | col @Double .- col @Int → Double+subDoubleInt :: Test+subDoubleInt =+ TestCase+ ( assertEqual+ "Double .- Int = Double"+ (Just $ DI.fromList [0.5, 0.5, 0.5 :: Double])+ ( DI.getColumn "result" $+ D.derive "result" (F.col @Double "d" .- F.col @Int "x") crossData+ )+ )++-- | col @(Maybe Int) .- col @Double → Maybe Double+subMaybeIntDouble :: Test+subMaybeIntDouble =+ TestCase+ ( assertEqual+ "Maybe Int .- Double = Maybe Double"+ ( Just $+ DI.fromVector (V.fromList [Just 8.5, Nothing, Just 26.5 :: Maybe Double])+ )+ ( DI.getColumn "result" $+ D.derive "result" (F.col @(Maybe Int) "y" .- F.col @Double "d") crossData+ )+ )++-- | col @Int .- col @(Maybe Double) → Maybe Double+subIntMaybeDouble :: Test+subIntMaybeDouble =+ TestCase+ ( assertEqual+ "Int .- Maybe Double = Maybe Double"+ ( Just $+ DI.fromVector+ (V.fromList [Just (-9.5), Nothing, Just (-27.5) :: Maybe Double])+ )+ ( DI.getColumn "result" $+ D.derive "result" (F.col @Int "x" .- F.col @(Maybe Double) "md") crossData+ )+ )++-- | col @(Maybe Int) .- col @(Maybe Double) → Maybe Double+subMaybeIntMaybeDouble :: Test+subMaybeIntMaybeDouble =+ TestCase+ ( assertEqual+ "Maybe Int .- Maybe Double = Maybe Double"+ ( Just $+ DI.fromVector+ (V.fromList [Just (-0.5), Nothing, Just (-0.5) :: Maybe Double])+ )+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.col @(Maybe Int) "y" .- F.col @(Maybe Double) "md")+ crossData+ )+ )++-- | col @Double .* col @Int → Double+mulDoubleInt :: Test+mulDoubleInt =+ TestCase+ ( assertEqual+ "Double .* Int = Double"+ (Just $ DI.fromList [1.5, 5.0, 10.5 :: Double])+ ( DI.getColumn "result" $+ D.derive "result" (F.col @Double "d" .* F.col @Int "x") crossData+ )+ )++-- | col @(Maybe Int) .* col @Double → Maybe Double+mulMaybeIntDouble :: Test+mulMaybeIntDouble =+ TestCase+ ( assertEqual+ "Maybe Int .* Double = Maybe Double"+ ( Just $+ DI.fromVector (V.fromList [Just 15.0, Nothing, Just 105.0 :: Maybe Double])+ )+ ( DI.getColumn "result" $+ D.derive "result" (F.col @(Maybe Int) "y" .* F.col @Double "d") crossData+ )+ )++-- | col @Int .* col @(Maybe Double) → Maybe Double+mulIntMaybeDouble :: Test+mulIntMaybeDouble =+ TestCase+ ( assertEqual+ "Int .* Maybe Double = Maybe Double"+ ( Just $+ DI.fromVector (V.fromList [Just 10.5, Nothing, Just 91.5 :: Maybe Double])+ )+ ( DI.getColumn "result" $+ D.derive "result" (F.col @Int "x" .* F.col @(Maybe Double) "md") crossData+ )+ )++-- | col @(Maybe Int) .* col @(Maybe Double) → Maybe Double+mulMaybeIntMaybeDouble :: Test+mulMaybeIntMaybeDouble =+ TestCase+ ( assertEqual+ "Maybe Int .* Maybe Double = Maybe Double"+ ( Just $+ DI.fromVector (V.fromList [Just 105.0, Nothing, Just 915.0 :: Maybe Double])+ )+ ( DI.getColumn "result" $+ D.derive+ "result"+ (F.col @(Maybe Int) "y" .* F.col @(Maybe Double) "md")+ crossData+ )+ )++-- Division tests use clean-dividing data: x=[2,4,6], d=[1,2,3]+divData :: D.DataFrame+divData =+ D.fromNamedColumns+ [ ("x", DI.fromList ([2, 4, 6] :: [Int]))+ , ("y", DI.fromVector (V.fromList [Just 4, Nothing, Just 6 :: Maybe Int]))+ , ("d", DI.fromList ([1.0, 2.0, 3.0] :: [Double]))+ ,+ ( "md"+ , DI.fromVector (V.fromList [Just 1.0, Nothing, Just 3.0 :: Maybe Double])+ )+ ]++-- | col @Int ./ col @Double → Double+divIntDouble :: Test+divIntDouble =+ TestCase+ ( assertEqual+ "Int ./ Double = Double"+ (Just $ DI.fromList [2.0, 2.0, 2.0 :: Double])+ ( DI.getColumn "result" $+ D.derive "result" (F.col @Int "x" ./ F.col @Double "d") divData+ )+ )++-- | col @Double ./ col @Int → Double+divDoubleInt :: Test+divDoubleInt =+ TestCase+ ( assertEqual+ "Double ./ Int = Double"+ (Just $ DI.fromList [0.5, 0.5, 0.5 :: Double])+ ( DI.getColumn "result" $+ D.derive "result" (F.col @Double "d" ./ F.col @Int "x") divData+ )+ )++-- | col @(Maybe Int) ./ col @Double → Maybe Double+divMaybeIntDouble :: Test+divMaybeIntDouble =+ TestCase+ ( assertEqual+ "Maybe Int ./ Double = Maybe Double"+ ( Just $+ DI.fromVector (V.fromList [Just 4.0, Nothing, Just 2.0 :: Maybe Double])+ )+ ( DI.getColumn "result" $+ D.derive "result" (F.col @(Maybe Int) "y" ./ F.col @Double "d") divData+ )+ )++-- | col @Int ./ col @(Maybe Double) → Maybe Double+divIntMaybeDouble :: Test+divIntMaybeDouble =+ TestCase+ ( assertEqual+ "Int ./ Maybe Double = Maybe Double"+ ( Just $+ DI.fromVector (V.fromList [Just 2.0, Nothing, Just 2.0 :: Maybe Double])+ )+ ( DI.getColumn "result" $+ D.derive "result" (F.col @Int "x" ./ F.col @(Maybe Double) "md") divData+ )+ )++-- | col @(Maybe Int) ./ col @(Maybe Double) → Maybe Double+divMaybeIntMaybeDouble :: Test+divMaybeIntMaybeDouble =+ TestCase+ ( assertEqual+ "Maybe Int ./ Maybe Double = Maybe Double"+ ( Just $+ DI.fromVector (V.fromList [Just 4.0, Nothing, Just 2.0 :: Maybe Double])+ )+ ( DI.getColumn "result" $+ D.derive "result" (F.col @(Maybe Int) "y" ./ F.col @(Maybe Double) "md") divData+ )+ )++-- ---------------------------------------------------------------------------+-- Typed TExpr layer — cross-type arithmetic+-- ---------------------------------------------------------------------------++type CrossSchema =+ '[ DT.Column "x" Int+ , DT.Column "y" (Maybe Int)+ , DT.Column "d" Double+ , DT.Column "md" (Maybe Double)+ ]++typedCrossData :: DT.TypedDataFrame CrossSchema+typedCrossData =+ either (error . show) id $+ DT.freezeWithError @CrossSchema crossData++-- | Typed: col @"x" .+ col @"d" → Double+typedAddIntDouble :: Test+typedAddIntDouble =+ TestCase+ ( assertEqual+ "Typed: Int .+ Double = Double"+ [2.5, 4.5, 6.5 :: Double]+ ( DT.columnAsList @"result" $+ DT.derive @"result" (TE.col @"x" TE..+ TE.col @"d") typedCrossData+ )+ )++-- | Typed: col @"y" .+ col @"d" → Maybe Double+typedAddMaybeIntDouble :: Test+typedAddMaybeIntDouble =+ TestCase+ ( assertEqual+ "Typed: Maybe Int .+ Double = Maybe Double"+ [Just 11.5, Nothing, Just 33.5]+ ( DT.columnAsList @"result" $+ DT.derive @"result" (TE.col @"y" TE..+ TE.col @"d") typedCrossData+ )+ )++-- | Typed: col @"x" .+ col @"md" → Maybe Double+typedAddIntMaybeDouble :: Test+typedAddIntMaybeDouble =+ TestCase+ ( assertEqual+ "Typed: Int .+ Maybe Double = Maybe Double"+ [Just 11.5, Nothing, Just 33.5]+ ( DT.columnAsList @"result" $+ DT.derive @"result" (TE.col @"x" TE..+ TE.col @"md") typedCrossData+ )+ )++-- | Typed: col @"y" .+ col @"md" → Maybe Double+typedAddMaybeIntMaybeDouble :: Test+typedAddMaybeIntMaybeDouble =+ TestCase+ ( assertEqual+ "Typed: Maybe Int .+ Maybe Double = Maybe Double"+ [Just 20.5, Nothing, Just 60.5]+ ( DT.columnAsList @"result" $+ DT.derive @"result" (TE.col @"y" TE..+ TE.col @"md") typedCrossData+ )+ )++-- | Typed: col @"x" .- col @"d" → Double+typedSubIntDouble :: Test+typedSubIntDouble =+ TestCase+ ( assertEqual+ "Typed: Int .- Double = Double"+ [-0.5, -0.5, -0.5 :: Double]+ ( DT.columnAsList @"result" $+ DT.derive @"result" (TE.col @"x" TE..- TE.col @"d") typedCrossData+ )+ )++-- | Typed: col @"x" .* col @"d" → Double+typedMulIntDouble :: Test+typedMulIntDouble =+ TestCase+ ( assertEqual+ "Typed: Int .* Double = Double"+ [1.5, 5.0, 10.5 :: Double]+ ( DT.columnAsList @"result" $+ DT.derive @"result" (TE.col @"x" TE..* TE.col @"d") typedCrossData+ )+ )++-- ---------------------------------------------------------------------------+-- Typed TExpr layer tests+-- ---------------------------------------------------------------------------++type TestSchema = '[DT.Column "x" Int, DT.Column "y" (Maybe Int)]++typedTestData :: DT.TypedDataFrame TestSchema+typedTestData =+ either (error . show) id $+ DT.freezeWithError @TestSchema testData++-- | Typed: col @"x" .+ col @"y" should give Maybe Int column+typedAddIntMaybeInt :: Test+typedAddIntMaybeInt =+ TestCase+ ( assertEqual+ "Typed: Int .+ Maybe Int = Maybe Int"+ [Just 11, Nothing, Just 33]+ ( DT.columnAsList @"result" $+ DT.derive+ @"result"+ (TE.col @"x" TE..+ TE.col @"y")+ typedTestData+ )+ )++-- | Typed: nullLift negate on Maybe Int propagates Nothing+typedNullLiftMaybeInt :: Test+typedNullLiftMaybeInt =+ TestCase+ ( assertEqual+ "Typed nullLift negate (Maybe Int) propagates Nothing"+ [Just (-10), Nothing, Just (-30 :: Int)]+ ( DT.columnAsList @"result" $+ DT.derive+ @"result"+ (TE.nullLift negate (TE.col @"y") :: TExpr TestSchema (Maybe Int))+ typedTestData+ )+ )++-- | Typed: nullLift negate on plain Int+typedNullLiftInt :: Test+typedNullLiftInt =+ TestCase+ ( assertEqual+ "Typed nullLift negate (Int) gives Int column"+ [-1, -2, -3 :: Int]+ ( DT.columnAsList @"result" $+ DT.derive+ @"result"+ (TE.nullLift negate (TE.col @"x") :: TExpr TestSchema Int)+ typedTestData+ )+ )++-- | Typed: nullLift2 (+) col @"x" col @"y" → Maybe Int+typedNullLift2IntMaybeInt :: Test+typedNullLift2IntMaybeInt =+ TestCase+ ( assertEqual+ "Typed nullLift2 (+) Int (Maybe Int) = Maybe Int"+ [Just 11, Nothing, Just 33 :: Maybe Int]+ ( DT.columnAsList @"result" $+ DT.derive+ @"result"+ (TE.nullLift2 (+) (TE.col @"x") (TE.col @"y") :: TExpr TestSchema (Maybe Int))+ typedTestData+ )+ )++-- | Typed: col @"y" .== col @"y" should give Maybe Bool column+typedEqMaybeMaybe :: Test+typedEqMaybeMaybe =+ TestCase+ ( assertEqual+ "Typed: Maybe Int .== Maybe Int = Maybe Bool"+ [Just True, Nothing, Just True]+ ( DT.columnAsList @"result" $+ DT.derive+ @"result"+ (TE.col @"y" TE..== TE.col @"y")+ typedTestData+ )+ )++-- | apply negate to a Maybe Int column — should fmap over inner values+applyFmapMaybeInt :: Test+applyFmapMaybeInt =+ TestCase+ ( assertEqual+ "apply negate to Maybe Int column fmaps"+ (Just $ DI.fromVector (V.fromList [Just (-10), Nothing, Just (-30 :: Int)]))+ ( DI.getColumn "y" $+ D.apply @Int negate "y" testData+ )+ )++-- | apply to a plain Int column still works+applyPlainInt :: Test+applyPlainInt =+ TestCase+ ( assertEqual+ "apply negate to plain Int column still works"+ (Just $ DI.fromList [-1, -2, -3 :: Int])+ ( DI.getColumn "x" $+ D.apply @Int negate "x" testData+ )+ )++tests :: [Test]+tests =+ [ TestLabel "addIntMaybeInt" addIntMaybeInt+ , TestLabel "addMaybeIntInt" addMaybeIntInt+ , TestLabel "addIntInt" addIntInt+ , TestLabel "addMaybeMaybe" addMaybeMaybe+ , TestLabel "subIntMaybeInt" subIntMaybeInt+ , TestLabel "eqIntMaybeInt" eqIntMaybeInt+ , TestLabel "eqIntInt" eqIntInt+ , TestLabel "nullLiftMaybeInt" nullLiftMaybeInt+ , TestLabel "nullLiftInt" nullLiftInt+ , TestLabel "nullLift2IntMaybeInt" nullLift2IntMaybeInt+ , TestLabel "nullLift2MaybeIntInt" nullLift2MaybeIntInt+ , TestLabel "nullLift2IntInt" nullLift2IntInt+ , TestLabel "typedAddIntMaybeInt" typedAddIntMaybeInt+ , TestLabel "typedEqMaybeMaybe" typedEqMaybeMaybe+ , TestLabel "typedNullLiftMaybeInt" typedNullLiftMaybeInt+ , TestLabel "typedNullLiftInt" typedNullLiftInt+ , TestLabel "typedNullLift2IntMaybeInt" typedNullLift2IntMaybeInt+ , TestLabel "applyFmapMaybeInt" applyFmapMaybeInt+ , TestLabel "applyPlainInt" applyPlainInt+ , TestLabel "addIntDouble" addIntDouble+ , TestLabel "addDoubleInt" addDoubleInt+ , TestLabel "addMaybeIntDouble" addMaybeIntDouble+ , TestLabel "addIntMaybeDouble" addIntMaybeDouble+ , TestLabel "addMaybeIntMaybeDouble" addMaybeIntMaybeDouble+ , TestLabel "subIntDouble" subIntDouble+ , TestLabel "subDoubleInt" subDoubleInt+ , TestLabel "subMaybeIntDouble" subMaybeIntDouble+ , TestLabel "subIntMaybeDouble" subIntMaybeDouble+ , TestLabel "subMaybeIntMaybeDouble" subMaybeIntMaybeDouble+ , TestLabel "mulIntDouble" mulIntDouble+ , TestLabel "mulDoubleInt" mulDoubleInt+ , TestLabel "mulMaybeIntDouble" mulMaybeIntDouble+ , TestLabel "mulIntMaybeDouble" mulIntMaybeDouble+ , TestLabel "mulMaybeIntMaybeDouble" mulMaybeIntMaybeDouble+ , TestLabel "divIntDouble" divIntDouble+ , TestLabel "divDoubleInt" divDoubleInt+ , TestLabel "divMaybeIntDouble" divMaybeIntDouble+ , TestLabel "divIntMaybeDouble" divIntMaybeDouble+ , TestLabel "divMaybeIntMaybeDouble" divMaybeIntMaybeDouble+ , TestLabel "typedAddIntDouble" typedAddIntDouble+ , TestLabel "typedAddMaybeIntDouble" typedAddMaybeIntDouble+ , TestLabel "typedAddIntMaybeDouble" typedAddIntMaybeDouble+ , TestLabel "typedAddMaybeIntMaybeDouble" typedAddMaybeIntMaybeDouble+ , TestLabel "typedSubIntDouble" typedSubIntDouble+ , TestLabel "typedMulIntDouble" typedMulIntDouble+ ]
+ tests/Operations/NullableHashing.hs view
@@ -0,0 +1,153 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Regression tests for null-aware row hashing and row extraction.++`Nothing` in a nullable unboxed column must be a distinct value from any+`Just x` (so @distinct@/@groupBy@/joins do not merge them), and `toRowList`+must round-trip nulls faithfully. These pin the bug surfaced by the+category-theory set-algebra laws, where @Nothing@ was stored as an+uninitialised int and collided with @Just 0@.+-}+module Operations.NullableHashing where++import qualified DataFrame as D+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.Row (toRowList)++import Test.HUnit++maybeIntCol :: [Maybe Int] -> D.DataFrame+maybeIntCol xs = D.fromNamedColumns [("k", DI.fromList xs)]++-- | distinct must keep @Nothing@ and @Just 0@ as two distinct rows.+distinctSeparatesNullFromZero :: Test+distinctSeparatesNullFromZero =+ TestCase+ ( assertEqual+ "distinct keeps Nothing and Just 0 apart"+ 2+ (fst (D.dimensions (D.distinct (maybeIntCol [Just 0, Nothing, Just 0, Nothing]))))+ )++-- | A whole row that differs only by null-vs-Just-0 must survive distinct.+distinctSeparatesNullRow :: Test+distinctSeparatesNullRow =+ TestCase+ ( assertEqual+ "distinct keeps rows differing only by null vs Just 0"+ 2+ ( fst+ ( D.dimensions+ ( D.distinct+ ( D.fromNamedColumns+ [ ("k", DI.fromList [Just (0 :: Int), Nothing])+ , ("v", DI.fromList ['a', 'a'])+ ]+ )+ )+ )+ )+ )++-- | An inner join on a nullable key: @Just 0@ must not match @Nothing@.+joinNullDoesNotMatchZero :: Test+joinNullDoesNotMatchZero =+ TestCase+ ( assertEqual+ "Just 0 key does not join with Nothing key"+ 0+ ( fst+ (D.dimensions (D.innerJoin ["k"] (maybeIntCol [Just 0]) (maybeIntCol [Nothing])))+ )+ )++-- | An inner join on a nullable key: @Nothing@ matches @Nothing@.+joinNullMatchesNull :: Test+joinNullMatchesNull =+ TestCase+ ( assertEqual+ "Nothing key joins with Nothing key"+ 1+ ( fst+ (D.dimensions (D.innerJoin ["k"] (maybeIntCol [Nothing]) (maybeIntCol [Nothing])))+ )+ )++-- | toRowList round-trips a nullable column, preserving @Nothing@.+toRowListRoundTripPreservesNull :: Test+toRowListRoundTripPreservesNull =+ let df = maybeIntCol [Just 1, Nothing, Just 3]+ rebuilt = D.fromRows ["k"] (map (map snd) (toRowList df))+ in TestCase (assertEqual "toRowList round-trip preserves nulls" df rebuilt)++{- | Hash robustness: @distinct@ must preserve every row of a dense grid of+small integers across several columns. A weak per-step hash collides badly on+adjacent integers and merges distinct rows; grouping trusts hash equality, so+this guards that the hash spreads such keys.+-}+denseIntGridNoCollisions :: Test+denseIntGridNoCollisions =+ let d = [-4 .. 4 :: Int]+ rows = [(a, b, c) | a <- d, b <- d, c <- d]+ df =+ D.fromNamedColumns+ [ ("a", DI.fromList (map (\(a, _, _) -> a) rows))+ , ("b", DI.fromList (map (\(_, b, _) -> b) rows))+ , ("c", DI.fromList (map (\(_, _, c) -> c) rows))+ ]+ in TestCase+ ( assertEqual+ "distinct preserves a dense 9x9x9 int grid (no hash collisions)"+ (length rows)+ (fst (D.dimensions (D.distinct df)))+ )++{- | Join-hash robustness: a self inner-join on a dense grid of unique integer+keys must return exactly one match per row. Joins match purely by hash, so a+weak hash that collides distinct keys would emit spurious cross-matches.+-}+joinDenseGridNoCollisions :: Test+joinDenseGridNoCollisions =+ let d = [-4 .. 4 :: Int]+ rows = [(a, b) | a <- d, b <- d]+ df =+ D.fromNamedColumns+ [ ("a", DI.fromList (map fst rows))+ , ("b", DI.fromList (map snd rows))+ ]+ in TestCase+ ( assertEqual+ "self inner-join on unique keys has no spurious hash matches"+ (length rows)+ (fst (D.dimensions (D.innerJoin ["a", "b"] df df)))+ )++-- | The same robustness check including @Nothing@ (a nullable grid).+denseNullableGridNoCollisions :: Test+denseNullableGridNoCollisions =+ let d = Nothing : map Just [-3 .. 3 :: Int]+ rows = [(a, b) | a <- d, b <- d]+ df =+ D.fromNamedColumns+ [ ("a", DI.fromList (map fst rows))+ , ("b", DI.fromList (map snd rows))+ ]+ in TestCase+ ( assertEqual+ "distinct preserves a dense nullable grid (no hash collisions)"+ (length rows)+ (fst (D.dimensions (D.distinct df)))+ )++tests :: [Test]+tests =+ [ TestLabel "distinctSeparatesNullFromZero" distinctSeparatesNullFromZero+ , TestLabel "denseIntGridNoCollisions" denseIntGridNoCollisions+ , TestLabel "joinDenseGridNoCollisions" joinDenseGridNoCollisions+ , TestLabel "denseNullableGridNoCollisions" denseNullableGridNoCollisions+ , TestLabel "distinctSeparatesNullRow" distinctSeparatesNullRow+ , TestLabel "joinNullDoesNotMatchZero" joinNullDoesNotMatchZero+ , TestLabel "joinNullMatchesNull" joinNullMatchesNull+ , TestLabel "toRowListRoundTripPreservesNull" toRowListRoundTripPreservesNull+ ]
+ tests/Operations/ParallelGroupBy.hs view
@@ -0,0 +1,240 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | The parallel-grouping correctness gate. Every case asserts that the+parallel partitioned grouping ('groupByPar') produces a 'Grouped' value+bit-for-bit identical to the sequential reference ('groupBySeq') —+'valueIndices', 'offsets' and 'rowToGroup' all equal — and that aggregating+through both yields the same result. Covered: single/multi key, Int/Text/Double+keys, heavy hash collisions, null patterns, and row counts straddling the+parallel threshold.+-}+module Operations.ParallelGroupBy where++import qualified Data.Map.Strict as M+import qualified Data.Text as T+import qualified Data.Vector.Unboxed as VU++import qualified DataFrame as D+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Internal.DataFrame as DD+import DataFrame.Internal.Grouping (groupByPar, groupBySeq)++import Assertions ()+import Test.HUnit++-- | A grouping is canonical-equal when all three index structures agree.+sameGrouping :: DD.GroupedDataFrame -> DD.GroupedDataFrame -> Bool+sameGrouping a b =+ DD.valueIndices a == DD.valueIndices b+ && DD.offsets a == DD.offsets b+ && DD.rowToGroup a == DD.rowToGroup b++mkInt :: T.Text -> [Int] -> (T.Text, DI.Column)+mkInt name xs = (name, DI.fromList xs)++mkText :: T.Text -> [T.Text] -> (T.Text, DI.Column)+mkText name xs = (name, DI.fromList xs)++mkDouble :: T.Text -> [Double] -> (T.Text, DI.Column)+mkDouble name xs = (name, DI.fromList xs)++mkMaybeInt :: T.Text -> [Maybe Int] -> (T.Text, DI.Column)+mkMaybeInt name xs = (name, DI.fromList xs)++-- | Build a frame of @n@ rows with several key columns and a value column.+caseFrame :: Int -> DD.DataFrame+caseFrame n =+ D.fromNamedColumns+ [ mkInt "ki" [i `mod` 997 | i <- [0 .. n - 1]]+ , mkText "kt" [T.pack ("g" ++ show (i `mod` 503)) | i <- [0 .. n - 1]]+ , mkDouble "kd" [fromIntegral (i `mod` 311) / 7 | i <- [0 .. n - 1]]+ , mkMaybeInt+ "kn"+ [if i `mod` 13 == 0 then Nothing else Just (i `mod` 251) | i <- [0 .. n - 1]]+ , mkInt "v" [0 .. n - 1]+ , mkDouble "vd" [fromIntegral i * 1.5 | i <- [0 .. n - 1]]+ ]++-- | Collision-heavy small grid (forces hash-collision key re-verification).+collisionFrame :: DD.DataFrame+collisionFrame =+ D.fromNamedColumns+ [ mkInt "k1" (map (`mod` 5) [0 .. 999])+ , mkInt "k2" (map (\i -> (i * 7) `mod` 3) [0 .. 999])+ , mkInt "v" [0 .. 999]+ ]++keySets :: [[T.Text]]+keySets =+ [ ["ki"]+ , ["kt"]+ , ["kd"]+ , ["kn"]+ , ["ki", "kt"]+ , ["kt", "kn"]+ , ["ki", "kt", "kd", "kn"]+ ]++{- | Sizes straddling the 200k parallel threshold (the parallel path only kicks+in for the larger ones at runtime, but 'groupByPar' is exercised on all).+-}+sizes :: [Int]+sizes = [1, 50, 4999, 200001, 350000]++parityFor :: Int -> [T.Text] -> Test+parityFor n keys =+ TestCase $+ let df = caseFrame n+ s = groupBySeq keys df+ p = groupByPar keys df+ label = "n=" ++ show n ++ " keys=" ++ show keys+ in assertBool ("parallel==sequential grouping for " ++ label) (sameGrouping s p)++-- | Aggregating through both paths must give identical numbers.+aggParityFor :: Int -> Test+aggParityFor n =+ TestCase $+ let df = caseFrame n+ v = F.col @Int "v"+ vd = F.col @Double "vd"+ aggs =+ [ "vsum" F..= F.sum v+ , "vmean" F..= F.mean vd+ , "vmin" F..= F.minimum v+ , "vmax" F..= F.maximum v+ , "vcount" F..= F.count v+ , "vsd" F..= F.stddev vd+ ]+ seqDf = D.aggregate aggs (groupBySeq ["ki", "kt"] df)+ parDf = D.aggregate aggs (groupByPar ["ki", "kt"] df)+ in assertEqual ("aggregate parity n=" ++ show n) seqDf parDf++collisionParity :: Test+collisionParity =+ TestCase $+ assertBool+ "collision-heavy parallel==sequential"+ ( sameGrouping+ (groupBySeq ["k1", "k2"] collisionFrame)+ (groupByPar ["k1", "k2"] collisionFrame)+ )++{- | The Q9 regression family: six derived/sum aggregations that together form+the moment sufficient statistics (count, Sx, Sy, Sxx, Syy, Sxy). The fused+'momentScatter' path must produce the same numbers as a deterministic oracle+computed from the raw data, at both -N1 and -N8 (parallel==sequential). The+input has columns the derive step turns into v1*v1, v1*v2, v2*v2.+-}+momentFrame :: Int -> DD.DataFrame+momentFrame n =+ D.fromNamedColumns+ [ mkInt "id2" [i `mod` 17 | i <- [0 .. n - 1]]+ , mkInt "id4" [(i * 3) `mod` 11 | i <- [0 .. n - 1]]+ , mkInt "v1" [(i * 7) `mod` 100 | i <- [0 .. n - 1]]+ , mkInt "v2" [(i * 13 + 1) `mod` 100 | i <- [0 .. n - 1]]+ ]++-- | Derive the product columns exactly as the benchmark does, then aggregate.+momentResult ::+ ([T.Text] -> DD.DataFrame -> DD.GroupedDataFrame) -> Int -> DD.DataFrame+momentResult grp n =+ let dv1 = F.toDouble (F.col @Int "v1")+ dv2 = F.toDouble (F.col @Int "v2")+ df =+ D.derive "v2v2" (dv2 * dv2) $+ D.derive "v1v1" (dv1 * dv1) $+ D.derive "v1v2" (dv1 * dv2) (momentFrame n)+ aggs =+ [ "n" F..= F.count (F.col @Int "v1")+ , "sx" F..= F.sum dv1+ , "sy" F..= F.sum dv2+ , "sxy" F..= F.sum (F.col @Double "v1v2")+ , "sxx" F..= F.sum (F.col @Double "v1v1")+ , "syy" F..= F.sum (F.col @Double "v2v2")+ ]+ in D.aggregate aggs (grp ["id2", "id4"] df)++-- | The fused path's numbers must equal the parallel-vs-sequential reference.+momentParity :: Int -> Test+momentParity n =+ TestCase $+ assertEqual+ ("moment fused parity n=" ++ show n)+ (momentResult groupBySeq n)+ (momentResult groupByPar n)++{- | Independent oracle: recompute the six moment sums per group with plain+@Data.Map@ folds in original-row order and check the aggregated frame matches.+This pins the fused kernel against a non-scatter reference (counts exact, sums+fold in the same row order so they are byte-identical).+-}+momentOracle :: Int -> Test+momentOracle n =+ TestCase $ do+ let res = momentResult groupBySeq n+ ks = [(i `mod` 17, (i * 3) `mod` 11) | i <- [0 .. n - 1]]+ accMap = foldr step M.empty (reverse ks)+ step k = M.insertWith (\_ g -> g + 1) k (1 :: Int)+ refN = sum (M.elems accMap)+ outN = case DD.getColumn "n" res of+ Just c -> VU.sum (VU.fromList (DI.toList @Int c))+ Nothing -> -1+ assertEqual ("oracle group-row count n=" ++ show n) refN outN++{- | The low-cardinality DIRECT-INDEXED grouping fast path+('DataFrame.Internal.GroupingDirect') fires from 'D.groupBy' on a single clean+small-range Int key. It emits groups in ascending key-value order rather than the+hash path's order, so we cannot compare index structures directly; instead we+assert it produces the SAME per-key aggregate values as the hash 'groupBySeq'+reference. The per-group v-sum, count, min and max are exact integers, so they+must match the hash path's results key-for-key (independent of group order).+-}+directGroupAggMatches :: Int -> Test+directGroupAggMatches n =+ TestCase $+ let df = caseFrame n+ v = F.col @Int "v"+ ki = F.col @Int "ki"+ aggs =+ [ "vsum" F..= F.sum v+ , "vcount" F..= F.count v+ , "vmin" F..= F.minimum v+ , "vmax" F..= F.maximum v+ ]+ -- 'D.groupBy' takes the direct path; 'groupBySeq' is the hash path.+ directRes = D.aggregate aggs (D.groupBy ["ki"] df)+ hashRes = D.aggregate aggs (groupBySeq ["ki"] df)+ in assertEqual+ ("direct-group agg matches hash by key, n=" ++ show n)+ (keyedAgg hashRes)+ (keyedAgg directRes)++{- | Read an aggregated frame keyed on @ki@ into a 'M.Map' from key to the four+aggregate values, so two frames can be compared independent of group order.+-}+keyedAgg :: DD.DataFrame -> M.Map Int (Int, Int, Int, Int)+keyedAgg res =+ M.fromList+ (zip (col "ki") (zip4 (col "vsum") (col "vcount") (col "vmin") (col "vmax")))+ where+ col name = case DD.getColumn name res of+ Just c -> DI.toList @Int c+ Nothing -> error ("missing column " ++ T.unpack name)+ zip4 (a : as) (b : bs) (c : cs) (d : ds) = (a, b, c, d) : zip4 as bs cs ds+ zip4 _ _ _ _ = []++tests :: [Test]+tests =+ [ TestLabel ("parity " ++ show n ++ " " ++ show keys) (parityFor n keys)+ | n <- sizes+ , keys <- keySets+ ]+ ++ [TestLabel ("aggParity " ++ show n) (aggParityFor n) | n <- [50, 200001]]+ ++ [TestLabel "collisionParity" collisionParity]+ ++ [TestLabel ("momentParity " ++ show n) (momentParity n) | n <- [3, 50, 200001]]+ ++ [ TestLabel ("directGroupAgg " ++ show n) (directGroupAggMatches n)+ | n <- [1, 50, 4999, 200001]+ ]+ ++ [TestLabel ("momentOracle " ++ show n) (momentOracle n) | n <- [50, 200001]]
+ tests/Operations/ParallelJoin.hs view
@@ -0,0 +1,213 @@+{-# LANGUAGE OverloadedStrings #-}++{- | The parallel-join correctness gate. Each case asserts that the parallel+chunked-probe kernels ('parInnerKernel' \/ 'parLeftKernel') produce index+vectors /bit-for-bit identical/ to the sequential reference kernels, and that+the assembled join through the public 'innerJoin' \/ 'leftJoin' matches a+sequentially-forced reference. Covered: int\/text keys, many-to-many duplicate+keys, unmatched left rows, null patterns, and row counts straddling the+parallel threshold.+-}+module Operations.ParallelJoin where++import qualified Data.Text as T+import qualified Data.Vector.Unboxed as VU++import qualified DataFrame as D+import DataFrame.Operations.Join (+ hashInnerKernel,+ hashLeftKernel,+ innerJoin,+ leftJoin,+ parInnerKernel,+ parLeftKernel,+ )++import Test.HUnit++-- | A probe/build hash pair straddling realistic shapes.+type HashPair = (VU.Vector Int, VU.Vector Int)++mkHashes :: [Int] -> VU.Vector Int+mkHashes = VU.fromList++{- | Synthetic hash vectors. Many collisions (mod), duplicates on both sides,+and sizes that exceed the parallel threshold so the parallel path engages.+-}+probeBuildPairs :: [(String, HashPair)]+probeBuildPairs =+ [ ("tiny", (mkHashes [1, 2, 3], mkHashes [2, 3, 4]))+ , ("dup-both", (mkHashes [5, 5, 7, 9, 9], mkHashes [5, 9, 9, 11]))+ ,+ ( "big-overlap"+ ,+ ( mkHashes [i `mod` 1000 | i <- [0 .. 300000 :: Int]]+ , mkHashes [i `mod` 1000 | i <- [0 .. 50000 :: Int]]+ )+ )+ ,+ ( "big-sparse"+ ,+ ( mkHashes [0 .. 300000 :: Int]+ , mkHashes [i * 3 | i <- [0 .. 120000 :: Int]]+ )+ )+ , -- Small-build (cache-resident) build side probed by a very large probe:+ -- the parallel small-build-probe lever (medium-factor regime). The probe+ -- exceeds parProbeThreshold so the parallel path engages with a tiny+ -- read-only shared index. Parity here is the new correctness gate.++ ( "small-build-big-probe"+ ,+ ( mkHashes [i `mod` 10000 | i <- [0 .. 1200000 :: Int]]+ , mkHashes [0 .. 10000 :: Int]+ )+ )+ ]++-- | parInnerKernel == hashInnerKernel, bit-for-bit, on each shape.+innerKernelParity :: (String, HashPair) -> Test+innerKernelParity (label, (probe, build)) =+ TestCase $+ assertEqual+ ("inner kernel parity: " ++ label)+ (hashInnerKernel probe build)+ (parInnerKernel probe build)++-- | parLeftKernel == hashLeftKernel, bit-for-bit, on each shape.+leftKernelParity :: (String, HashPair) -> Test+leftKernelParity (label, (probe, build)) =+ TestCase $+ assertEqual+ ("left kernel parity: " ++ label)+ (hashLeftKernel probe build)+ (parLeftKernel probe build)++-- | Build a frame large enough to cross the parallel threshold.+ordersFrame :: Int -> D.DataFrame+ordersFrame n =+ D.fromNamedColumns+ [ ("cid", D.fromList [i `mod` 7000 | i <- [0 .. n - 1]])+ , ("amount", D.fromList [fromIntegral i * 1.5 :: Double | i <- [0 .. n - 1]])+ ]++custFrame :: Int -> D.DataFrame+custFrame n =+ D.fromNamedColumns+ [ ("cid", D.fromList [0 .. n - 1])+ , ("region", D.fromList [T.pack ('r' : show (i `mod` 5)) | i <- [0 .. n - 1]])+ ]++{- | End-to-end: the assembled inner/left join over a frame that exceeds the+parallel threshold equals the same join sorted (order-independent value check),+and the result row count is stable.+-}+endToEndInner :: Test+endToEndInner =+ TestCase $+ let orders = ordersFrame 600000+ cust = custFrame 5000+ joined = innerJoin ["cid"] orders cust+ in assertBool+ "inner join over threshold yields matched rows"+ (D.nRows joined > 0)++endToEndLeft :: Test+endToEndLeft =+ TestCase $+ let orders = ordersFrame 600000+ cust = custFrame 5000+ joined = leftJoin ["cid"] orders cust+ in assertEqual+ "left join keeps every probe row"+ 600000+ (D.nRows joined)++{- | A probe frame whose join key is TEXT and whose row count crosses the+parallel row-hash threshold, so the inner join hashes the key in parallel.+Exercises the parallel text/factor-key hash path (the medium-factor lever) end+to end: a factor key with @k@ distinct values mapped over @n@ rows.+-}+factorOrdersFrame :: Int -> Int -> D.DataFrame+factorOrdersFrame n k =+ D.fromNamedColumns+ [ ("fk", D.fromList [T.pack ('f' : show (i `mod` k)) | i <- [0 .. n - 1]])+ , ("amount", D.fromList [fromIntegral i * 2.0 :: Double | i <- [0 .. n - 1]])+ ]++factorDimFrame :: Int -> D.DataFrame+factorDimFrame k =+ D.fromNamedColumns+ [ ("fk", D.fromList [T.pack ('f' : show i) | i <- [0 .. k - 1]])+ , ("label", D.fromList [T.pack ('L' : show i) | i <- [0 .. k - 1]])+ ]++{- | Inner join on a TEXT key over a frame past the parallel-hash threshold: the+every-key-matches case must keep all @n@ probe rows, confirming the parallel+text hashing buckets identically to a sequential pass (a miscomputed parallel+hash would drop matches and shrink the row count).+-}+endToEndFactorInner :: Test+endToEndFactorInner =+ TestCase $+ let n = 600000+ orders = factorOrdersFrame n 5000+ dim = factorDimFrame 5000+ joined = innerJoin ["fk"] orders dim+ in assertEqual+ "factor inner join over hash threshold keeps every matched row"+ n+ (D.nRows joined)++{- | Small-build (cache-resident dim), very large probe (> parProbeThreshold) on+a TEXT/factor key, the medium-factor lever. The public 'innerJoin' takes the+parallel small-build-probe path here; every probe key matches a dim key, so a+mis-partitioned parallel probe that dropped or duplicated matches would change+the kept row count. The bit-identity of the parallel and sequential kernels is+pinned separately by 'innerKernelParity' on the @small-build-big-probe@ shape.+-}+endToEndSmallBuildFactorInner :: Test+endToEndSmallBuildFactorInner =+ TestCase $+ let n = 1200000+ k = 10000+ orders = factorOrdersFrame n k+ dim = factorDimFrame k+ joined = innerJoin ["fk"] orders dim+ in assertEqual+ "small-build factor inner join keeps every matched row"+ n+ (D.nRows joined)++{- | Small-build, large-probe LEFT join on the factor key: every probe row is+kept (matched or sentinel). Exercises the parallel small-build 'parLeftKernel'+through the public 'leftJoin'. With @k@ distinct dim keys and probe keys in+@[0, 2k)@, roughly half the probe rows miss and carry a Nothing.+-}+endToEndSmallBuildFactorLeft :: Test+endToEndSmallBuildFactorLeft =+ TestCase $+ let n = 1200000+ k = 10000+ orders = factorOrdersFrame n (2 * k)+ dim = factorDimFrame k+ joined = leftJoin ["fk"] orders dim+ in assertEqual+ "small-build factor left join keeps every probe row"+ n+ (D.nRows joined)++tests :: [Test]+tests =+ [ TestLabel ("innerKernel " ++ l) (innerKernelParity p)+ | p@(l, _) <- probeBuildPairs+ ]+ ++ [ TestLabel ("leftKernel " ++ l) (leftKernelParity p)+ | p@(l, _) <- probeBuildPairs+ ]+ ++ [ TestLabel "endToEndInner" endToEndInner+ , TestLabel "endToEndLeft" endToEndLeft+ , TestLabel "endToEndFactorInner" endToEndFactorInner+ , TestLabel "endToEndSmallBuildFactorInner" endToEndSmallBuildFactorInner+ , TestLabel "endToEndSmallBuildFactorLeft" endToEndSmallBuildFactorLeft+ ]
+ tests/Operations/Provenance.hs view
@@ -0,0 +1,235 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Operations.Provenance where++import qualified Data.Map as M+import qualified DataFrame as D+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Internal.DataFrame as DI+import DataFrame.Operations.Merge ((|||))++import Test.HUnit++-- Base frame with no derived columns.+base :: D.DataFrame+base =+ D.fromNamedColumns+ [ ("x", DI.fromList [1 .. 5 :: Int])+ , ("y", DI.fromList [2 .. 6 :: Int])+ ]++-- A frame with one derived column "z".+withZ :: D.DataFrame+withZ = D.derive "z" (F.col @Int "x" + F.col "y") base++-- ── insertColumn ──────────────────────────────────────────────────────────────++-- Inserting a new column must not wipe provenance of existing derived columns.+insertPreservesProvenance :: Test+insertPreservesProvenance =+ TestCase+ ( assertBool+ "insertColumn should preserve existing derivingExpressions"+ ( M.member+ "z"+ (DI.derivingExpressions (D.insertColumn "w" (DI.fromList [0 :: Int]) withZ))+ )+ )++-- Overwriting a derived column removes only *that* expression, not others.+insertOverwriteDropsOwnExpr :: Test+insertOverwriteDropsOwnExpr =+ let+ df2 = D.derive "w" (F.col @Int "x") withZ+ df3 = D.insertColumn "z" (DI.fromList [99 :: Int]) df2+ in+ TestCase $ do+ assertBool+ "overwritten column z should be removed from derivingExpressions"+ (not $ M.member "z" (DI.derivingExpressions df3))+ assertBool+ "sibling column w expression should be preserved"+ (M.member "w" (DI.derivingExpressions df3))++-- ── derive ────────────────────────────────────────────────────────────────────++-- derive adds the expression to derivingExpressions.+deriveTracksExpression :: Test+deriveTracksExpression =+ TestCase+ ( assertBool+ "derive should record z in derivingExpressions"+ (M.member "z" (DI.derivingExpressions withZ))+ )++-- Multiple derives accumulate.+deriveManyTracksAll :: Test+deriveManyTracksAll =+ let df = D.derive "w" (F.col @Int "x") withZ+ in TestCase+ ( assertEqual+ "two derive calls should leave two expressions"+ 2+ (M.size (DI.derivingExpressions df))+ )++-- Re-deriving a column replaces its expression and keeps the count stable.+deriveOverwriteReplacesExpression :: Test+deriveOverwriteReplacesExpression =+ let df = D.derive "z" (F.col @Int "y") withZ -- overwrite z+ in TestCase+ ( assertEqual+ "re-deriving z should not duplicate the entry"+ 1+ (M.size (DI.derivingExpressions df))+ )++-- ── deriveWithExpr ────────────────────────────────────────────────────────────++-- deriveWithExpr should also track the expression.+deriveWithExprTracksExpression :: Test+deriveWithExprTracksExpression =+ let (_, df) = D.deriveWithExpr @Int "z" (F.col @Int "x" + F.col "y") base+ in TestCase+ ( assertBool+ "deriveWithExpr should record z in derivingExpressions"+ (M.member "z" (DI.derivingExpressions df))+ )++-- ── showDerivedExpressions ────────────────────────────────────────────────────++-- A frame with no derived columns returns identity provenance for each column.+showDerivedEmpty :: Test+showDerivedEmpty =+ TestCase+ ( assertEqual+ "raw-column frame should have identity provenance for each column"+ ["x", "y"]+ (map fst (D.showDerivedExpressions base))+ )++-- Frame with one derived column has an entry for the derived column.+showDerivedContainsName :: Test+showDerivedContainsName =+ TestCase+ ( assertBool+ "showDerivedExpressions should include the derived column name"+ ("z" `elem` map fst (D.showDerivedExpressions withZ))+ )++-- ── Semigroup (<>) provenance propagation ─────────────────────────────────────++-- Vertical merge preserves expressions from both sides.+semiGroupPreservesLeft :: Test+semiGroupPreservesLeft =+ let merged = withZ <> base+ in TestCase+ ( assertBool+ "<> should preserve derivingExpressions from the left frame"+ (M.member "z" (DI.derivingExpressions merged))+ )++semiGroupPreservesBoth :: Test+semiGroupPreservesBoth =+ let dfW = D.derive "w" (F.col @Int "y") base+ merged = withZ <> dfW+ in TestCase+ ( assertEqual+ "<> should union derivingExpressions from both frames"+ 2+ (M.size (DI.derivingExpressions merged))+ )++-- Left frame wins when both sides have an expression for the same column.+semiGroupLeftBias :: Test+semiGroupLeftBias =+ let dfLeft = D.derive "z" (F.col @Int "x") base+ dfRight = D.derive "z" (F.col @Int "y") base+ merged = dfLeft <> dfRight+ in TestCase+ ( assertEqual+ "<> should retain exactly one entry for a shared column name"+ 1+ (M.size (DI.derivingExpressions merged))+ )++-- Merging with an empty frame must not lose provenance.+semiGroupWithEmpty :: Test+semiGroupWithEmpty =+ TestCase+ ( assertBool+ "withZ <> empty should keep z's expression"+ (M.member "z" (DI.derivingExpressions (withZ <> D.empty)))+ )++emptyWithSemiGroup :: Test+emptyWithSemiGroup =+ TestCase+ ( assertBool+ "empty <> withZ should keep z's expression"+ (M.member "z" (DI.derivingExpressions (D.empty <> withZ)))+ )++-- ── Horizontal merge (|||) provenance propagation ────────────────────────────++horizontalMergePreservesLeft :: Test+horizontalMergePreservesLeft =+ let dfW = D.derive "w" (F.col @Int "y") base+ extra = D.fromNamedColumns [("q", DI.fromList [0 :: Int, 0, 0, 0, 0])]+ merged = dfW ||| extra+ in TestCase+ ( assertBool+ "||| should preserve derivingExpressions from the left frame"+ (M.member "w" (DI.derivingExpressions merged))+ )++horizontalMergePreservesRight :: Test+horizontalMergePreservesRight =+ let extra = D.fromNamedColumns [("q", DI.fromList [0 :: Int, 0, 0, 0, 0])]+ dfW =+ D.derive+ "w"+ (F.col @Int "y")+ (D.fromNamedColumns [("y", DI.fromList [2 .. 6 :: Int])])+ merged = extra ||| dfW+ in TestCase+ ( assertBool+ "||| should bring in derivingExpressions from the right frame"+ (M.member "w" (DI.derivingExpressions merged))+ )++horizontalMergePreservesBoth :: Test+horizontalMergePreservesBoth =+ let dfZ = withZ+ dfW = D.fromNamedColumns [("q", DI.fromList [0 :: Int, 0, 0, 0, 0])]+ -- give dfW a derived column on a separate base+ dfWD = D.derive "w" (F.col @Int "q") dfW+ merged = dfZ ||| dfWD+ in TestCase+ ( assertEqual+ "||| should union derivingExpressions"+ 2+ (M.size (DI.derivingExpressions merged))+ )++tests :: [Test]+tests =+ [ TestLabel "insertPreservesProvenance" insertPreservesProvenance+ , TestLabel "insertOverwriteDropsOwnExpr" insertOverwriteDropsOwnExpr+ , TestLabel "deriveTracksExpression" deriveTracksExpression+ , TestLabel "deriveManyTracksAll" deriveManyTracksAll+ , TestLabel "deriveOverwriteReplacesExpression" deriveOverwriteReplacesExpression+ , TestLabel "deriveWithExprTracksExpression" deriveWithExprTracksExpression+ , TestLabel "showDerivedEmpty" showDerivedEmpty+ , TestLabel "showDerivedContainsName" showDerivedContainsName+ , TestLabel "semiGroupPreservesLeft" semiGroupPreservesLeft+ , TestLabel "semiGroupPreservesBoth" semiGroupPreservesBoth+ , TestLabel "semiGroupLeftBias" semiGroupLeftBias+ , TestLabel "semiGroupWithEmpty" semiGroupWithEmpty+ , TestLabel "emptyWithSemiGroup" emptyWithSemiGroup+ , TestLabel "horizontalMergePreservesLeft" horizontalMergePreservesLeft+ , TestLabel "horizontalMergePreservesRight" horizontalMergePreservesRight+ , TestLabel "horizontalMergePreservesBoth" horizontalMergePreservesBoth+ ]
tests/Operations/ReadCsv.hs view
@@ -3,155 +3,550 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} --- Test fixtures inspired by csv-spectrum (https://github.com/max-mapper/csv-spectrum)- module Operations.ReadCsv where -import qualified Data.List as L-import qualified Data.Map as M import qualified Data.Text as T-import qualified Data.Text.IO as TIO-import qualified Data.Vector as V-import qualified Data.Vector.Unboxed as VU import qualified DataFrame as D+import qualified DataFrame.Lazy.IO.CSV as Lazy -import Data.Function (on)-import Data.Maybe (fromMaybe)-import Data.Type.Equality (testEquality, (:~:) (Refl))-import DataFrame.Internal.Column (Column (..))+import DataFrame.Internal.Column (Column (..), columnTypeString) import DataFrame.Internal.DataFrame ( DataFrame (..),- columnIndices,- columns, dataframeDimensions,+ getColumn, )-import System.Directory (removeFile)-import System.IO (IOMode (..), withFile) import Test.HUnit-import Type.Reflection (typeRep) -fixtureDir :: FilePath-fixtureDir = "./tests/data/unstable_csv/"+arbuthnotPath :: FilePath+arbuthnotPath = "./tests/data/arbuthnot.csv" -tempDir :: FilePath-tempDir = "./tests/data/unstable_csv/"+readCsvNoInfer :: FilePath -> IO DataFrame+readCsvNoInfer =+ D.readCsvWithOpts+ D.defaultReadOptions{D.typeSpec = D.NoInference} ------------------------------------------------------------------------------------ Pretty-printer---------------------------------------------------------------------------------+-- SpecifyTypes with NoInference fallback: named column is typed, rest stay Text+specifyTypesNoInferenceFallback :: Test+specifyTypesNoInferenceFallback =+ TestLabel "specifyTypes_noInference_fallback" $ TestCase $ do+ df <-+ D.readCsvWithOpts+ D.defaultReadOptions+ { D.typeSpec =+ D.SpecifyTypes+ [("year", D.schemaType @Int)]+ D.NoInference+ }+ arbuthnotPath+ -- "year" must be Int+ case getColumn "year" df of+ Just col@(UnboxedColumn _ _) -> assertEqual "year should be Int" "Int" (columnTypeString col)+ _ -> assertFailure "expected UnboxedColumn for 'year'"+ -- "boys" unspecified + NoInference → stays Text (Boxed or PackedText)+ case getColumn "boys" df of+ Just col@(BoxedColumn _ _) -> assertEqual "boys should be Text" "Text" (columnTypeString col)+ Just col@(PackedText _ _) -> assertEqual "boys should be Text" "Text" (columnTypeString col)+ _ -> assertFailure "expected Text column for 'boys' with NoInference fallback" -prettyPrintCsv :: FilePath -> DataFrame -> IO ()-prettyPrintCsv = prettyPrintSeparated ','+-- SpecifyTypes with InferFromSample fallback: named column typed, rest inferred+specifyTypesInferFallback :: Test+specifyTypesInferFallback =+ TestLabel "specifyTypes_inferFromSample_fallback" $ TestCase $ do+ df <-+ D.readCsvWithOpts+ D.defaultReadOptions+ { D.typeSpec =+ D.SpecifyTypes+ [("year", D.schemaType @Int)]+ (D.InferFromSample 100)+ }+ arbuthnotPath+ -- "year" must be Int (explicitly specified)+ case getColumn "year" df of+ Just col@(UnboxedColumn _ _) -> assertEqual "year should be Int" "Int" (columnTypeString col)+ _ -> assertFailure "expected UnboxedColumn for 'year'"+ -- "boys" unspecified + InferFromSample → inferred as Int+ case getColumn "boys" df of+ Just col@(UnboxedColumn _ _) -> assertEqual "boys should be Int" "Int" (columnTypeString col)+ _ ->+ assertFailure "expected UnboxedColumn for 'boys' with InferFromSample fallback" -prettyPrintTsv :: FilePath -> DataFrame -> IO ()-prettyPrintTsv = prettyPrintSeparated '\t'+-- SpecifyTypes: typeInferenceSampleSize delegates to fallback+specifyTypesSampleSize :: Test+specifyTypesSampleSize =+ TestLabel "specifyTypes_sampleSize_from_fallback" $ TestCase $ do+ -- Use a small sample size; all numeric columns should still be inferred+ df <-+ D.readCsvWithOpts+ D.defaultReadOptions+ { D.typeSpec =+ D.SpecifyTypes+ []+ (D.InferFromSample 10)+ }+ arbuthnotPath+ case getColumn "girls" df of+ Just col@(UnboxedColumn _ _) -> assertEqual "girls should be Int" "Int" (columnTypeString col)+ _ ->+ assertFailure+ "expected UnboxedColumn for 'girls' via fallback InferFromSample 10" -prettyPrintSeparated :: Char -> FilePath -> DataFrame -> IO ()-prettyPrintSeparated sep filepath df = withFile filepath WriteMode $ \handle -> do- let (rows, _) = dataframeDimensions df- let headers = map fst (L.sortBy (compare `on` snd) (M.toList (columnIndices df)))- TIO.hPutStrLn- handle- (T.intercalate (T.singleton sep) (map (escapeField sep) headers))- -- Write data rows- mapM_- (TIO.hPutStrLn handle . T.intercalate (T.singleton sep) . getRowEscaped sep df)- [0 .. rows - 1]+-- File: a,b,c header; row "1,2,3,EXTRA"+-- Total delimiters: 3 + 4 = 7; 7 div 3 = 2, numRow=1+-- Row-1 strides 3,4,5 → "1","2","3" — EXTRA (stride 6) is never accessed+testExtraFields :: Test+testExtraFields = TestLabel "malformed_extra_fields" $ TestCase $ do+ df <- readCsvNoInfer ("./tests/data/unstable_csv/" <> "extra_fields.csv")+ assertEqual "extra_fields.csv: 1 data row" 1 (fst (dataframeDimensions df))+ assertEqual "extra_fields.csv: 3 columns" 3 (snd (dataframeDimensions df))+ case getColumn "c" df of+ Nothing -> assertFailure "extra_fields.csv: column 'c' missing"+ Just col ->+ assertEqual+ "extra_fields.csv: 'c' = '3' (EXTRA ignored)"+ (D.fromList @T.Text ["3"])+ col --- Note: The unstable parser does not unescape doubled quotes ("" -> "),--- so we must not double-escape them here. We only wrap in quotes when needed.-escapeField :: Char -> T.Text -> T.Text-escapeField sep field- | needsQuoting = T.concat ["\"", field, "\""]- | otherwise = field- where- needsQuoting =- T.any (\c -> c == sep || c == '\n' || c == '\r' || c == '"') field+-- Inference + EitherRead: first rows of "score" are clean ints, later rows are+-- "abc" / empty / "N/A". The sample is limited to the clean prefix so the+-- assumption is Int; non-parsing later rows become Left carrying raw text.+eitherReadCsvInference :: Test+eitherReadCsvInference = TestLabel "csv_eitherRead_inference" $ TestCase $ do+ df <-+ D.readCsvWithOpts+ D.defaultReadOptions+ { D.safeRead = D.EitherRead+ , D.typeSpec = D.InferFromSample 5+ }+ "./tests/data/unstable_csv/either_read_mixed.csv"+ let expectedScore :: Column+ expectedScore =+ D.fromList @(Either T.Text Int)+ [ Right 10+ , Right 20+ , Right 30+ , Right 40+ , Right 50+ , Left "abc"+ , Left ""+ , Right 80+ ]+ case getColumn "score" df of+ Just col ->+ assertEqual+ "EitherRead: 'score' = [Right 10..Right 50, Left \"abc\", Left \"\", Right 80]"+ expectedScore+ col+ Nothing -> assertFailure "score column missing"+ -- 'id' is all ints in every row, so EitherRead wraps every value as Right.+ let expectedId :: Column+ expectedId =+ D.fromList @(Either T.Text Int) (map Right [1 .. 8])+ case getColumn "id" df of+ Just col ->+ assertEqual+ "EitherRead: 'id' is all Right (no failures)"+ expectedId+ col+ Nothing -> assertFailure "id column missing"+ -- 'name' has a row containing "N/A"; under EitherRead the text is just+ -- a value so it lands in Right. Empty cells would become Left "".+ let expectedName :: Column+ expectedName =+ D.fromList @(Either T.Text T.Text)+ (map Right ["alice", "bob", "carol", "dave", "eve", "frank", "grace", "N/A"])+ case getColumn "name" df of+ Just col ->+ assertEqual+ "EitherRead: 'name' wraps every value as Right (no parse failures for Text)"+ expectedName+ col+ Nothing -> assertFailure "name column missing" --- | Get a row from the DataFrame with all fields escaped-getRowEscaped :: Char -> DataFrame -> Int -> [T.Text]-getRowEscaped sep df i = V.ifoldr go [] (columns df)- where- go :: Int -> Column -> [T.Text] -> [T.Text]- go _ (BoxedColumn (c :: V.Vector a)) acc = case c V.!? i of- Just e -> escapeField sep textRep : acc- where- textRep = case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl -> e- Nothing -> T.pack (show e)- Nothing -> acc- go _ (UnboxedColumn c) acc = case c VU.!? i of- Just e -> escapeField sep (T.pack (show e)) : acc- Nothing -> acc- go _ (OptionalColumn (c :: V.Vector (Maybe a))) acc = case c V.!? i of- Just e -> escapeField sep textRep : acc- where- textRep = case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl -> fromMaybe "" e- Nothing -> case e of- Just val -> T.pack (show val)- Nothing -> ""- Nothing -> acc+-- MaybeRead on the same CSV yields Maybe Int (bitmap-backed Int).+maybeReadCsv :: Test+maybeReadCsv = TestLabel "csv_maybeRead" $ TestCase $ do+ df <-+ D.readCsvWithOpts+ D.defaultReadOptions{D.safeRead = D.MaybeRead}+ "./tests/data/unstable_csv/either_read_mixed.csv"+ -- "score" is not cleanly parseable as Int because "abc" fails; however the+ -- BS inference walks all rows and falls back to Text when needed. The+ -- relevant invariant for MaybeRead is that the column is nullable.+ case getColumn "score" df of+ Just (UnboxedColumn (Just _) _) -> pure () -- Int with bitmap+ Just (BoxedColumn (Just _) _) -> pure () -- Text-backed with bitmap+ Just (PackedText (Just _) _) -> pure () -- packed Text with bitmap+ Just col ->+ assertFailure $+ "MaybeRead should yield a nullable column, got "+ <> columnTypeString col+ <> " with no bitmap"+ Nothing -> assertFailure "score column missing" -testFastCsv :: String -> FilePath -> Test-testFastCsv name csvPath = TestLabel ("fast_roundtrip_" <> name) $ TestCase $ do- dfOriginal <- D.fastReadCsvUnstable csvPath- let tempPath = tempDir <> "temp_fast_" <> name <> ".csv"- prettyPrintCsv tempPath dfOriginal- dfRoundtrip <- D.fastReadCsvUnstable tempPath- assertEqual- ("Fast round-trip should produce equivalent DataFrame for " <> name)- dfOriginal- dfRoundtrip- removeFile tempPath+-- NoSafeRead leaves columns un-bitmapped when every row parses (id column is+-- all ints) but falls back to a Text column for mixed rows like "score".+noSafeReadCsv :: Test+noSafeReadCsv = TestLabel "csv_noSafeRead" $ TestCase $ do+ df <-+ D.readCsvWithOpts+ D.defaultReadOptions{D.safeRead = D.NoSafeRead}+ "./tests/data/unstable_csv/either_read_mixed.csv"+ case getColumn "id" df of+ Just (UnboxedColumn Nothing _) -> pure () -- strict Int, no bitmap+ Just col ->+ assertFailure $+ "NoSafeRead 'id' should be bare UnboxedColumn, got "+ <> columnTypeString col+ Nothing -> assertFailure "id column missing" -testTsv :: String -> FilePath -> Test-testTsv name tsvPath = TestLabel ("roundtrip_tsv_" <> name) $ TestCase $ do- dfOriginal <- D.readTsvUnstable tsvPath- let tempPath = tempDir <> "temp_" <> name <> ".tsv"- prettyPrintTsv tempPath dfOriginal- dfRoundtrip <- D.readTsvUnstable tempPath- assertEqual- ("TSV round-trip should produce equivalent DataFrame for " <> name)- dfOriginal- dfRoundtrip- removeFile tempPath+-- Lazy CSV reader + EitherRead: goes through a different pipeline+-- (MutableColumn + null indices). Every row of 'id' parses cleanly, so the+-- column is @Either Text Int@ with every value in 'Right'.+lazyEitherReadCsv :: Test+lazyEitherReadCsv = TestLabel "lazy_csv_eitherRead" $ TestCase $ do+ (df, _) <-+ Lazy.readSeparated+ ','+ Lazy.defaultOptions{Lazy.safeRead = D.EitherRead}+ "./tests/data/unstable_csv/either_read_mixed.csv"+ let expectedId :: Column+ expectedId =+ D.fromList @(Either T.Text Int) (map Right [1 .. 8])+ case getColumn "id" df of+ Just col ->+ assertEqual+ "Lazy + EitherRead: 'id' is all Right Ints"+ expectedId+ col+ Nothing -> assertFailure "id column missing (lazy reader)" --- Individual round-trip test cases for each fixture+-- Per-column overrides (eager): 'id' is strict Int (NoSafeRead), 'score' is+-- Either Text Int (EitherRead) recording the raw bytes for failures, 'name'+-- falls back to the default (MaybeRead) and becomes a nullable Text column.+perColumnEagerOverrides :: Test+perColumnEagerOverrides = TestLabel "csv_perColumnOverrides_eager" $ TestCase $ do+ df <-+ D.readCsvWithOpts+ D.defaultReadOptions+ { D.safeRead = D.MaybeRead+ , D.safeReadOverrides =+ [ ("id", D.NoSafeRead)+ , ("score", D.EitherRead)+ ]+ , D.typeSpec = D.InferFromSample 5+ }+ "./tests/data/unstable_csv/either_read_mixed.csv"+ -- 'id': NoSafeRead → plain UnboxedColumn Int, no bitmap.+ case getColumn "id" df of+ Just (UnboxedColumn Nothing _) -> pure ()+ Just col ->+ assertFailure $+ "override NoSafeRead for 'id' should give bare UnboxedColumn, got "+ <> columnTypeString col+ Nothing -> assertFailure "id column missing"+ -- 'score': EitherRead → BoxedColumn of Either Text Int.+ let expectedScore :: Column+ expectedScore =+ D.fromList @(Either T.Text Int)+ [ Right 10+ , Right 20+ , Right 30+ , Right 40+ , Right 50+ , Left "abc"+ , Left ""+ , Right 80+ ]+ case getColumn "score" df of+ Just col ->+ assertEqual+ "override EitherRead for 'score'"+ expectedScore+ col+ Nothing -> assertFailure "score column missing"+ -- 'name': default MaybeRead kicks in → nullable column (bitmap attached).+ -- Every cell is non-empty so the bitmap is all-valid, but the optional+ -- wrap is still applied.+ case getColumn "name" df of+ Just (BoxedColumn (Just _) _) -> pure ()+ Just (PackedText (Just _) _) -> pure ()+ Just col ->+ assertFailure $+ "default MaybeRead for 'name' should yield a nullable Text column, got "+ <> columnTypeString col+ Nothing -> assertFailure "name column missing" -testSimpleFast :: Test-testSimpleFast = testFastCsv "simple" (fixtureDir <> "simple.csv")+-- Per-column overrides (lazy): same configuration as the eager test, but+-- exercises the lazy freezer's resolution of per-column modes.+perColumnLazyOverrides :: Test+perColumnLazyOverrides = TestLabel "csv_perColumnOverrides_lazy" $ TestCase $ do+ (df, _) <-+ Lazy.readSeparated+ ','+ Lazy.defaultOptions+ { Lazy.safeRead = D.MaybeRead+ , Lazy.safeReadOverrides =+ [ ("id", D.NoSafeRead)+ , ("score", D.EitherRead)+ ]+ }+ "./tests/data/unstable_csv/either_read_mixed.csv"+ -- Lazy reader infers 'id' as Int from the first row; NoSafeRead → bare.+ case getColumn "id" df of+ Just (UnboxedColumn Nothing _) -> pure ()+ Just col ->+ assertFailure $+ "lazy NoSafeRead override for 'id' → bare UnboxedColumn, got "+ <> columnTypeString col+ Nothing -> assertFailure "id column missing (lazy)"+ -- EitherRead on 'score': every row is Right (first row was '10' → Int+ -- builder), though rows that failed parsing are captured as Left via the+ -- nulls list. We only assert the column is Boxed-of-Either.+ case getColumn "score" df of+ Just (BoxedColumn Nothing _) -> pure ()+ Just col ->+ assertFailure $+ "lazy EitherRead override for 'score' → BoxedColumn Nothing, got "+ <> columnTypeString col+ Nothing -> assertFailure "score column missing (lazy)" -testCommaInQuotesFast :: Test-testCommaInQuotesFast = testFastCsv "comma_in_quotes" (fixtureDir <> "comma_in_quotes.csv")+-- Default=NoSafeRead, override single column 'score' → MaybeRead.+-- 'id' and 'name' keep the NoSafeRead default; 'score' becomes nullable.+overrideNoSafeReadDefaultWithMaybeRead :: Test+overrideNoSafeReadDefaultWithMaybeRead =+ TestLabel "csv_override_noSafeRead_default_MaybeRead" $ TestCase $ do+ df <-+ D.readCsvWithOpts+ D.defaultReadOptions+ { D.safeRead = D.NoSafeRead+ , D.safeReadOverrides = [("score", D.MaybeRead)]+ }+ "./tests/data/unstable_csv/either_read_mixed.csv"+ -- 'id': NoSafeRead default → bare Int.+ case getColumn "id" df of+ Just (UnboxedColumn Nothing _) -> pure ()+ Just col ->+ assertFailure $+ "'id' should be bare UnboxedColumn under NoSafeRead, got "+ <> columnTypeString col+ Nothing -> assertFailure "id column missing"+ -- 'score': MaybeRead override → nullable (bitmap present).+ case getColumn "score" df of+ Just (UnboxedColumn (Just _) _) -> pure () -- Int with bitmap+ Just (BoxedColumn (Just _) _) -> pure () -- fallback Text with bitmap+ Just (PackedText (Just _) _) -> pure () -- packed Text with bitmap+ Just col ->+ assertFailure $+ "'score' MaybeRead override should yield nullable column, got "+ <> columnTypeString col+ Nothing -> assertFailure "score column missing"+ -- 'name': NoSafeRead default → Text; may have bitmap when data has+ -- null cells (row 8 has "N/A" which is in missingIndicators).+ -- ensureOptional is NOT forced, but a bitmap is still created when+ -- the builder detects actual nulls.+ case getColumn "name" df of+ Just (BoxedColumn _ _) -> pure ()+ Just (PackedText _ _) -> pure ()+ Just col ->+ assertFailure $+ "'name' under NoSafeRead should be a Text column, got "+ <> columnTypeString col+ Nothing -> assertFailure "name column missing" -testEscapedQuotesFast :: Test-testEscapedQuotesFast = testFastCsv "escaped_quotes" (fixtureDir <> "escaped_quotes.csv")+-- Default=NoSafeRead, override single column 'score' → EitherRead.+overrideNoSafeReadDefaultWithEitherRead :: Test+overrideNoSafeReadDefaultWithEitherRead =+ TestLabel "csv_override_noSafeRead_default_EitherRead" $ TestCase $ do+ df <-+ D.readCsvWithOpts+ D.defaultReadOptions+ { D.safeRead = D.NoSafeRead+ , D.safeReadOverrides = [("score", D.EitherRead)]+ , D.typeSpec = D.InferFromSample 5+ }+ "./tests/data/unstable_csv/either_read_mixed.csv"+ -- 'score': EitherRead override → exact cell contents+ let expectedScore :: Column+ expectedScore =+ D.fromList @(Either T.Text Int)+ [ Right 10+ , Right 20+ , Right 30+ , Right 40+ , Right 50+ , Left "abc"+ , Left ""+ , Right 80+ ]+ case getColumn "score" df of+ Just col ->+ assertEqual+ "'score' EitherRead override from NoSafeRead default"+ expectedScore+ col+ Nothing -> assertFailure "score column missing" -testNewlinesFast :: Test-testNewlinesFast = testFastCsv "newlines" (fixtureDir <> "newlines.csv")+-- Default=EitherRead, override 'id' back to NoSafeRead.+overrideEitherReadDefaultWithNoSafeRead :: Test+overrideEitherReadDefaultWithNoSafeRead =+ TestLabel "csv_override_eitherRead_default_NoSafeRead" $ TestCase $ do+ df <-+ D.readCsvWithOpts+ D.defaultReadOptions+ { D.safeRead = D.EitherRead+ , D.safeReadOverrides = [("id", D.NoSafeRead)]+ , D.typeSpec = D.InferFromSample 5+ }+ "./tests/data/unstable_csv/either_read_mixed.csv"+ -- 'id': NoSafeRead override → bare Int (every row parses).+ case getColumn "id" df of+ Just (UnboxedColumn Nothing _) -> pure ()+ Just col ->+ assertFailure $+ "'id' NoSafeRead override should give bare UnboxedColumn, got "+ <> columnTypeString col+ Nothing -> assertFailure "id column missing"+ -- 'score': default EitherRead → Either Text Int+ let expectedScore :: Column+ expectedScore =+ D.fromList @(Either T.Text Int)+ [ Right 10+ , Right 20+ , Right 30+ , Right 40+ , Right 50+ , Left "abc"+ , Left ""+ , Right 80+ ]+ case getColumn "score" df of+ Just col ->+ assertEqual+ "'score' inherits EitherRead default"+ expectedScore+ col+ Nothing -> assertFailure "score column missing"+ -- 'name': default EitherRead → Either Text Text (all values Right).+ let expectedName :: Column+ expectedName =+ D.fromList @(Either T.Text T.Text)+ (map Right ["alice", "bob", "carol", "dave", "eve", "frank", "grace", "N/A"])+ case getColumn "name" df of+ Just col ->+ assertEqual+ "'name' inherits EitherRead default"+ expectedName+ col+ Nothing -> assertFailure "name column missing" -testUtf8Fast :: Test-testUtf8Fast = testFastCsv "utf8" (fixtureDir <> "utf8.csv")+-- Default=EitherRead, override 'name' → MaybeRead.+overrideEitherReadDefaultWithMaybeRead :: Test+overrideEitherReadDefaultWithMaybeRead =+ TestLabel "csv_override_eitherRead_default_MaybeRead" $ TestCase $ do+ df <-+ D.readCsvWithOpts+ D.defaultReadOptions+ { D.safeRead = D.EitherRead+ , D.safeReadOverrides = [("name", D.MaybeRead)]+ , D.typeSpec = D.InferFromSample 5+ }+ "./tests/data/unstable_csv/either_read_mixed.csv"+ -- 'name': MaybeRead override → nullable Text column with bitmap.+ case getColumn "name" df of+ Just (BoxedColumn (Just _) _) -> pure ()+ Just (PackedText (Just _) _) -> pure ()+ Just col ->+ assertFailure $+ "'name' MaybeRead override should yield a nullable Text column, got "+ <> columnTypeString col+ Nothing -> assertFailure "name column missing"+ -- 'id': default EitherRead → Either Text Int (all values Right).+ let expectedId :: Column+ expectedId = D.fromList @(Either T.Text Int) (map Right [1 .. 8])+ case getColumn "id" df of+ Just col ->+ assertEqual "'id' inherits EitherRead default" expectedId col+ Nothing -> assertFailure "id column missing" -testQuotesAndNewlinesFast :: Test-testQuotesAndNewlinesFast = testFastCsv "quotes_and_newlines" (fixtureDir <> "quotes_and_newlines.csv")+-- Override for a column that doesn't exist in the CSV → no crash, ignored.+overrideNonExistentColumn :: Test+overrideNonExistentColumn =+ TestLabel "csv_override_nonExistent" $ TestCase $ do+ df <-+ D.readCsvWithOpts+ D.defaultReadOptions+ { D.safeRead = D.NoSafeRead+ , D.safeReadOverrides = [("doesNotExist", D.EitherRead)]+ }+ "./tests/data/unstable_csv/either_read_mixed.csv"+ assertEqual "dimensions unchanged" (8, 3) (dataframeDimensions df) -testEmptyValuesFast :: Test-testEmptyValuesFast = testFastCsv "empty_values" (fixtureDir <> "empty_values.csv")+-- Empty overrides map is the same as no overrides.+emptyOverridesEqualsGlobal :: Test+emptyOverridesEqualsGlobal =+ TestLabel "csv_emptyOverrides" $ TestCase $ do+ df1 <-+ D.readCsvWithOpts+ D.defaultReadOptions{D.safeRead = D.MaybeRead}+ "./tests/data/unstable_csv/either_read_mixed.csv"+ df2 <-+ D.readCsvWithOpts+ D.defaultReadOptions+ { D.safeRead = D.MaybeRead+ , D.safeReadOverrides = []+ }+ "./tests/data/unstable_csv/either_read_mixed.csv"+ assertEqual "empty overrides produces identical DataFrame" df1 df2 -testJsonDataFast :: Test-testJsonDataFast = testFastCsv "json_data" (fixtureDir <> "json_data.csv")+-- Lazy path: cell-level assertions for EitherRead override on 'score'.+perColumnLazyOverridesCellLevel :: Test+perColumnLazyOverridesCellLevel =+ TestLabel "csv_perColumnOverrides_lazy_cellLevel" $ TestCase $ do+ (df, _) <-+ Lazy.readSeparated+ ','+ Lazy.defaultOptions+ { Lazy.safeRead = D.NoSafeRead+ , Lazy.safeReadOverrides = [("score", D.EitherRead)]+ }+ "./tests/data/unstable_csv/either_read_mixed.csv"+ -- 'id': NoSafeRead → bare Int.+ case getColumn "id" df of+ Just (UnboxedColumn Nothing _) -> pure ()+ Just col ->+ assertFailure $+ "lazy 'id' NoSafeRead → bare UnboxedColumn, got "+ <> columnTypeString col+ Nothing -> assertFailure "id missing (lazy)"+ -- 'score': EitherRead override → BoxedColumn of Either values.+ -- Lazy reader infers Int from first row; rows that fail become Left.+ case getColumn "score" df of+ Just (BoxedColumn Nothing _) -> pure ()+ Just col ->+ assertFailure $+ "lazy 'score' EitherRead → BoxedColumn Nothing, got "+ <> columnTypeString col+ Nothing -> assertFailure "score missing (lazy)" tests :: [Test] tests =- [ testSimpleFast- , testCommaInQuotesFast- , testQuotesAndNewlinesFast- , testEscapedQuotesFast- , testNewlinesFast- , testUtf8Fast- , testQuotesAndNewlinesFast- , testEmptyValuesFast- , testJsonDataFast+ [ specifyTypesNoInferenceFallback+ , specifyTypesInferFallback+ , specifyTypesSampleSize+ , testExtraFields+ , eitherReadCsvInference+ , maybeReadCsv+ , noSafeReadCsv+ , lazyEitherReadCsv+ , perColumnEagerOverrides+ , perColumnLazyOverrides+ , -- Per-column override coverage+ overrideNoSafeReadDefaultWithMaybeRead+ , overrideNoSafeReadDefaultWithEitherRead+ , overrideEitherReadDefaultWithNoSafeRead+ , overrideEitherReadDefaultWithMaybeRead+ , overrideNonExistentColumn+ , emptyOverridesEqualsGlobal+ , perColumnLazyOverridesCellLevel ]
+ tests/Operations/Record.hs view
@@ -0,0 +1,374 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedLabels #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++module Operations.Record where++import Data.Int (Int64)+import qualified Data.Map.Strict as M+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import GHC.Generics (Generic)++import qualified DataFrame as D+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI+import DataFrame.Operators+import qualified DataFrame.Schema as IS+import DataFrame.Typed (Schema)+import qualified DataFrame.Typed as DT++import Test.HUnit++-- Basic record with three different-type fields (Int64 / Text / Double).+data Order = Order+ { orderId :: Int64+ , region :: T.Text+ , amount :: Double+ }+ deriving (Show, Eq)++$(DT.deriveSchemaFromType ''Order)+$(D.deriveSchemaValues ''Order)++-- Nullable fields (Maybe Text -> RNullableBoxed; Maybe Int -> RNullableUnboxed).+data User = User+ { userId :: Int64+ , userName :: Maybe T.Text+ , userAge :: Maybe Int+ }+ deriving (Show, Eq)++$(DT.deriveSchemaFromType ''User)+$(D.deriveSchemaValues ''User)++-- Identity-cased: keep the record selector names verbatim.+data Account = Account+ { accountId :: Int64+ , accountName :: T.Text+ }+ deriving (Show, Eq)++$( DT.deriveSchemaFromTypeWith+ DT.defaultSchemaOptions{DT.nameTransform = id}+ ''Account+ )++-- Schema-name override.+data Item = Item+ { itemId :: Int64+ , itemPrice :: Double+ }+ deriving (Show, Eq)++$( DT.deriveSchemaFromTypeWith+ DT.defaultSchemaOptions{DT.schemaTypeName = Just "ItemsSchema"}+ ''Item+ )++-- Wide record (more than zipWith3 can handle).+data Wide = Wide+ { f1 :: Int+ , f2 :: Int+ , f3 :: Int+ , f4 :: Int+ , f5 :: Int+ , f6 :: Int+ , f7 :: Int+ , f8 :: Int+ }+ deriving (Show, Eq)++$(DT.deriveSchemaFromType ''Wide)+$(D.deriveSchemaValues ''Wide)++-- Generics opt-in: derive the schema via Generic, not TH.+data Foo = Foo+ { fooId :: Int64+ , fooName :: T.Text+ , fooValue :: Double+ }+ deriving (Show, Eq, Generic)++type FooSchema = DT.SchemaOf Foo++instance DT.HasSchema Foo where+ type Schema Foo = FooSchema+ toColumns = DT.genericToColumns+ fromColumns = DT.genericFromColumns++orderSample :: [Order]+orderSample =+ [ Order 1 "us" 10.0+ , Order 2 "eu" 20.5+ , Order 3 "ap" 30.0+ ]++basicTypedRoundTrip :: Test+basicTypedRoundTrip = TestCase $ do+ let df :: DT.TypedDataFrame OrderSchema+ df = DT.fromRecordsTyped orderSample+ case DT.toRecordsTyped df of+ Left e -> assertFailure (T.unpack e)+ Right xs -> assertEqual "typed round-trip" orderSample xs++basicUntypedRoundTrip :: Test+basicUntypedRoundTrip = TestCase $ do+ let df = D.fromRecords orderSample+ case D.toRecords df :: Either T.Text [Order] of+ Left e -> assertFailure (T.unpack e)+ Right xs -> assertEqual "untyped round-trip" orderSample xs++emptyRoundTrip :: Test+emptyRoundTrip = TestCase $ do+ let df = D.fromRecords ([] :: [Order])+ case D.toRecords df :: Either T.Text [Order] of+ Left e -> assertFailure (T.unpack e)+ Right xs -> assertEqual "empty round-trip" [] xs++nullableRoundTrip :: Test+nullableRoundTrip = TestCase $ do+ let xs =+ [ User 1 (Just "alice") (Just 30)+ , User 2 Nothing (Just 40)+ , User 3 (Just "carol") Nothing+ , User 4 Nothing Nothing+ ]+ df = D.fromRecords xs+ case D.toRecords df :: Either T.Text [User] of+ Left e -> assertFailure (T.unpack e)+ Right ys -> assertEqual "nullable round-trip" xs ys++identityRoundTrip :: Test+identityRoundTrip = TestCase $ do+ let xs = [Account 1 "alice", Account 2 "bob"]+ df = D.fromRecords xs+ assertEqual+ "identity column names"+ ["accountId", "accountName"]+ (D.columnNames df)+ case D.toRecords df :: Either T.Text [Account] of+ Left e -> assertFailure (T.unpack e)+ Right ys -> assertEqual "identity round-trip" xs ys++schemaNameOverride :: Test+schemaNameOverride = TestCase $ do+ let xs = [Item 1 9.99, Item 2 19.99]+ df :: DT.TypedDataFrame ItemsSchema+ df = DT.fromRecordsTyped xs+ case DT.toRecordsTyped df of+ Left e -> assertFailure (T.unpack e)+ Right ys -> assertEqual "schema-name override round-trip" xs ys++-- order_id is deliberately Int (not the schema's Int64) to force a mismatch.+typeMismatchError :: Test+typeMismatchError = TestCase $ do+ let badDf =+ D.fromNamedColumns+ [ ("order_id", DI.fromList ([1, 2, 3] :: [Int]))+ , ("region", DI.fromList (["us", "eu", "ap"] :: [T.Text]))+ , ("amount", DI.fromList ([10.0, 20.5, 30.0] :: [Double]))+ ]+ case D.toRecords badDf :: Either T.Text [Order] of+ Right _ -> assertFailure "expected error on type mismatch"+ Left e ->+ assertBool+ ("error should mention order_id, got: " ++ T.unpack e)+ ("order_id" `T.isInfixOf` e)++missingColumnError :: Test+missingColumnError = TestCase $ do+ let badDf =+ D.fromNamedColumns+ [ ("region", DI.fromList (["us"] :: [T.Text]))+ , ("amount", DI.fromList ([10.0] :: [Double]))+ ]+ case D.toRecords badDf :: Either T.Text [Order] of+ Right _ -> assertFailure "expected error on missing column"+ Left e ->+ assertBool+ ("error should mention order_id, got: " ++ T.unpack e)+ ("order_id" `T.isInfixOf` e)++wideRoundTrip :: Test+wideRoundTrip = TestCase $ do+ let xs =+ [ Wide 1 2 3 4 5 6 7 8+ , Wide 9 10 11 12 13 14 15 16+ ]+ df = D.fromRecords xs+ case D.toRecords df :: Either T.Text [Wide] of+ Left e -> assertFailure (T.unpack e)+ Right ys -> assertEqual "wide round-trip" xs ys++genericRoundTrip :: Test+genericRoundTrip = TestCase $ do+ let xs = [Foo 1 "a" 1.0, Foo 2 "b" 2.5]+ df = D.fromRecords xs+ case D.toRecords df :: Either T.Text [Foo] of+ Left e -> assertFailure (T.unpack e)+ Right ys -> assertEqual "generic round-trip" xs ys++genericColumnNames :: Test+genericColumnNames = TestCase $ do+ let df = D.fromRecords [Foo 1 "a" 1.0]+ assertEqual+ "generic snake-cased column names"+ ["foo_id", "foo_name", "foo_value"]+ (D.columnNames df)++deriveSchemaSplice :: Test+deriveSchemaSplice = TestCase $ do+ assertEqual+ "orderSchema column names"+ ["amount", "order_id", "region"]+ (M.keys (IS.elements orderSchema))+ assertEqual+ "order_id is Int64"+ (Just (IS.schemaType @Int64))+ (M.lookup "order_id" (IS.elements orderSchema))+ assertEqual+ "region is Text"+ (Just (IS.schemaType @T.Text))+ (M.lookup "region" (IS.elements orderSchema))+ assertEqual+ "amount is Double"+ (Just (IS.schemaType @Double))+ (M.lookup "amount" (IS.elements orderSchema))++deriveSchemaNullable :: Test+deriveSchemaNullable = TestCase $ do+ assertEqual+ "userSchema column names"+ ["user_age", "user_id", "user_name"]+ (M.keys (IS.elements userSchema))+ assertEqual+ "user_id is Int64"+ (Just (IS.schemaType @Int64))+ (M.lookup "user_id" (IS.elements userSchema))+ assertEqual+ "user_name is Maybe Text"+ (Just (IS.schemaType @(Maybe T.Text)))+ (M.lookup "user_name" (IS.elements userSchema))+ assertEqual+ "user_age is Maybe Int"+ (Just (IS.schemaType @(Maybe Int)))+ (M.lookup "user_age" (IS.elements userSchema))++deriveSchemaWide :: Test+deriveSchemaWide = TestCase $ do+ assertEqual+ "wideSchema has 8 keys"+ 8+ (M.size (IS.elements wideSchema))+ assertEqual+ "f1 is Int"+ (Just (IS.schemaType @Int))+ (M.lookup "f1" (IS.elements wideSchema))+ assertEqual+ "f8 is Int"+ (Just (IS.schemaType @Int))+ (M.lookup "f8" (IS.elements wideSchema))++deriveSchemaReadsCsv :: Test+deriveSchemaReadsCsv = TestCase $ do+ let csv =+ T.unlines+ [ "order_id,region,amount"+ , "1,us,10.0"+ , "2,eu,20.5"+ , "3,ap,30.0"+ ]+ tmp = "/tmp/dataframe_test_deriveSchema.csv"+ TIO.writeFile tmp csv+ df <- D.readCsvWithSchema orderSchema tmp+ assertEqual+ "deriveSchema-driven readCsvWithSchema column names"+ ["order_id", "region", "amount"]+ (D.columnNames df)+ case D.toRecords df :: Either T.Text [Order] of+ Left e -> assertFailure (T.unpack e)+ Right xs ->+ assertEqual+ "deriveSchema-driven CSV parses back to records"+ [Order 1 "us" 10.0, Order 2 "eu" 20.5, Order 3 "ap" 30.0]+ xs++deriveSchemaAccessorFilter :: Test+deriveSchemaAccessorFilter = TestCase $ do+ let df =+ D.fromRecords+ [ Order 1 "us" 20.0+ , Order 2 "eu" 20.5+ , Order 3 "ap" 30.0+ , Order 4 "us" 25.0+ ]+ big =+ D.filterWhere+ (orderAmount .>. F.lit @Double 15.0 .&&. orderRegion .==. F.lit @T.Text "us")+ df+ case D.toRecords big :: Either T.Text [Order] of+ Left e -> assertFailure (T.unpack e)+ Right xs ->+ assertEqual+ "accessor drives D.filter (amount > 15.0 && region == \"us\")"+ [Order 1 "us" 20.0, Order 4 "us" 25.0]+ xs++deriveSchemaAccessorDerive :: Test+deriveSchemaAccessorDerive = TestCase $ do+ let df =+ D.fromRecords+ [ Order 1 "us" 10.0+ , Order 2 "eu" 20.0+ ]+ df' = D.derive "double_amount" (orderAmount + orderAmount) df+ assertEqual+ "accessor composes in derive expression"+ [20.0, 40.0]+ (D.columnAsList (D.col @Double "double_amount") df')++labelColumnFilter :: Test+labelColumnFilter = TestCase $ do+ let df :: DT.TypedDataFrame OrderSchema+ df = DT.fromRecordsTyped orderSample+ usOnly = DT.filterWhere (#region DT..==. "us") df+ case DT.toRecordsTyped usOnly of+ Left e -> assertFailure (T.unpack e)+ Right xs ->+ assertEqual+ "#region OverloadedLabel resolves to col @\"region\""+ [Order 1 "us" 10.0]+ xs++tests :: [Test]+tests =+ [ TestLabel "basicTypedRoundTrip" basicTypedRoundTrip+ , TestLabel "labelColumnFilter" labelColumnFilter+ , TestLabel "basicUntypedRoundTrip" basicUntypedRoundTrip+ , TestLabel "emptyRoundTrip" emptyRoundTrip+ , TestLabel "nullableRoundTrip" nullableRoundTrip+ , TestLabel "identityRoundTrip" identityRoundTrip+ , TestLabel "schemaNameOverride" schemaNameOverride+ , TestLabel "typeMismatchError" typeMismatchError+ , TestLabel "missingColumnError" missingColumnError+ , TestLabel "wideRoundTrip" wideRoundTrip+ , TestLabel "genericRoundTrip" genericRoundTrip+ , TestLabel "genericColumnNames" genericColumnNames+ , TestLabel "deriveSchemaSplice" deriveSchemaSplice+ , TestLabel "deriveSchemaNullable" deriveSchemaNullable+ , TestLabel "deriveSchemaWide" deriveSchemaWide+ , TestLabel "deriveSchemaReadsCsv" deriveSchemaReadsCsv+ , TestLabel "deriveSchemaAccessorFilter" deriveSchemaAccessorFilter+ , TestLabel "deriveSchemaAccessorDerive" deriveSchemaAccessorDerive+ ]
+ tests/Operations/SetOps.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Operations.SetOps where++import qualified DataFrame as D+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI++import Test.HUnit++{- | Sort by the integer key column so set results (which come out in+hash-bucket order) can be compared deterministically.+-}+sortByA :: D.DataFrame -> D.DataFrame+sortByA = D.sortBy [D.Asc (F.col @Int "A")]++dfA :: D.DataFrame+dfA =+ D.fromNamedColumns+ [ ("A", DI.fromList [1 :: Int, 2, 3, 3])+ , ("B", DI.fromList ['a', 'b', 'c', 'c'])+ ]++dfB :: D.DataFrame+dfB =+ D.fromNamedColumns+ [ ("A", DI.fromList [3 :: Int, 4])+ , ("B", DI.fromList ['c', 'd'])+ ]++expect :: [Int] -> [Char] -> D.DataFrame+expect as bs =+ D.fromNamedColumns+ [ ("A", DI.fromList as)+ , ("B", DI.fromList bs)+ ]++unionWAI :: Test+unionWAI =+ TestCase+ ( assertEqual+ "union is the deduplicated set union"+ (expect [1, 2, 3, 4] "abcd")+ (sortByA (D.union dfA dfB))+ )++intersectWAI :: Test+intersectWAI =+ TestCase+ ( assertEqual+ "intersect keeps rows present in both"+ (expect [3] "c")+ (sortByA (D.intersect dfA dfB))+ )++differenceWAI :: Test+differenceWAI =+ TestCase+ ( assertEqual+ "difference keeps left rows absent from right"+ (expect [1, 2] "ab")+ (sortByA (D.difference dfA dfB))+ )++differenceIsDirectional :: Test+differenceIsDirectional =+ TestCase+ ( assertEqual+ "difference b a is the other complement"+ (expect [4] "d")+ (sortByA (D.difference dfB dfA))+ )++symmetricDifferenceWAI :: Test+symmetricDifferenceWAI =+ TestCase+ ( assertEqual+ "symmetricDifference keeps rows in exactly one input"+ (expect [1, 2, 4] "abd")+ (sortByA (D.symmetricDifference dfA dfB))+ )++intersectWithEmptyIsEmpty :: Test+intersectWithEmptyIsEmpty =+ TestCase+ ( assertEqual+ "intersect with an empty frame is empty (schema preserved)"+ (expect [] "")+ (sortByA (D.intersect dfA (expect [] "")))+ )++differenceWithEmptyIsDistinctSelf :: Test+differenceWithEmptyIsDistinctSelf =+ TestCase+ ( assertEqual+ "difference against an empty frame is the deduplicated self"+ (expect [1, 2, 3] "abc")+ (sortByA (D.difference dfA (expect [] "")))+ )++tests :: [Test]+tests =+ [ TestLabel "unionWAI" unionWAI+ , TestLabel "intersectWAI" intersectWAI+ , TestLabel "differenceWAI" differenceWAI+ , TestLabel "differenceIsDirectional" differenceIsDirectional+ , TestLabel "symmetricDifferenceWAI" symmetricDifferenceWAI+ , TestLabel "intersectWithEmptyIsEmpty" intersectWithEmptyIsEmpty+ , TestLabel "differenceWithEmptyIsDistinctSelf" differenceWithEmptyIsDistinctSelf+ ]
tests/Operations/Sort.hs view
@@ -85,10 +85,76 @@ TestCase ( assertExpectException "[Error Case]"- (D.columnNotFound "[\"test0\"]" "sortBy" (D.columnNames testData))+ (D.columnsNotFound ["test0"] "sortBy" (D.columnNames testData)) (print $ D.sortBy [D.Asc (F.col @Int "test0")] testData) ) +compoundTestData :: D.DataFrame+compoundTestData =+ D.fromNamedColumns+ [ ("a", DI.fromList ([3, 1, 4, 1, 5] :: [Int]))+ , ("b", DI.fromList ([10, 40, 20, 30, 50] :: [Int]))+ ]++sortByCompoundExpression :: Test+sortByCompoundExpression =+ TestCase+ ( assertEqual+ "Sorting by Asc (a + b) orders rows by the sum without leaking a synthetic column"+ ( D.fromNamedColumns+ [ ("a", DI.fromList ([3, 4, 1, 1, 5] :: [Int]))+ , ("b", DI.fromList ([10, 20, 30, 40, 50] :: [Int]))+ ]+ )+ (D.sortBy [D.Asc (F.col @Int "a" + F.col @Int "b")] compoundTestData)+ )++sortByCompoundExpressionDescending :: Test+sortByCompoundExpressionDescending =+ TestCase+ ( assertEqual+ "Sorting by Desc (b - a) orders rows by descending difference"+ ( D.fromNamedColumns+ [ ("a", DI.fromList ([5, 1, 1, 4, 3] :: [Int]))+ , ("b", DI.fromList ([50, 40, 30, 20, 10] :: [Int]))+ ]+ )+ (D.sortBy [D.Desc (F.col @Int "b" - F.col @Int "a")] compoundTestData)+ )++sortByCompoundMixedWithBareColumn :: Test+sortByCompoundMixedWithBareColumn =+ TestCase+ ( assertEqual+ "Mixing a compound Asc key with a bare Desc tie-breaker works"+ ( D.fromNamedColumns+ [ ("a", DI.fromList ([1, 1, 3, 4, 5] :: [Int]))+ , ("b", DI.fromList ([40, 30, 10, 20, 50] :: [Int]))+ ]+ )+ ( D.sortBy+ [D.Asc (F.col @Int "a" * 2), D.Desc (F.col @Int "b")]+ compoundTestData+ )+ )++sortByCompoundMissingColumn :: Test+sortByCompoundMissingColumn =+ TestCase+ ( assertExpectException+ "[Error Case]"+ ( D.columnsNotFound+ ["nope"]+ "sortBy"+ (D.columnNames compoundTestData)+ )+ ( print $+ D.sortBy+ [D.Asc (F.col @Int "nope" + F.col @Int "a")]+ compoundTestData+ )+ )+ tests :: [Test] tests = [ TestLabel "sortByAscendingWAI" sortByAscendingWAI@@ -96,4 +162,10 @@ , TestLabel "sortByColumnDoesNotExist" sortByColumnDoesNotExist , TestLabel "sortByTwoColumns" sortByTwoColumns , TestLabel "sortByOneColumnAscOneColumnDesc" sortByOneColumnAscOneColumnDesc+ , TestLabel "sortByCompoundExpression" sortByCompoundExpression+ , TestLabel+ "sortByCompoundExpressionDescending"+ sortByCompoundExpressionDescending+ , TestLabel "sortByCompoundMixedWithBareColumn" sortByCompoundMixedWithBareColumn+ , TestLabel "sortByCompoundMissingColumn" sortByCompoundMissingColumn ]
tests/Operations/Statistics.hs view
@@ -244,7 +244,7 @@ (abs (r - 1.0) < 1e-10) ) --- Requesting a missing column should throw ColumnNotFoundException+-- Requesting a missing column should throw ColumnsNotFoundException correlationMissingColumn :: Test correlationMissingColumn = TestCase
tests/Operations/Subset.hs view
@@ -1,11 +1,16 @@ {-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-} module Operations.Subset where +import qualified Data.Text as T+import qualified DataFrame as D+import qualified DataFrame.Internal.Column as Col import DataFrame.Internal.DataFrame-import qualified DataFrame.Operations.Core as D-import qualified DataFrame.Operations.Subset as D+import DataFrame.Operations.Merge () import System.Random+import Test.HUnit prop_dropZero :: DataFrame -> Bool prop_dropZero df = D.drop 0 df == df@@ -75,6 +80,114 @@ sampled = D.sample gen 0.0 df in fst (dataframeDimensions sampled) == 0 +prop_stratifiedSplit_deterministic :: DataFrame -> Bool+prop_stratifiedSplit_deterministic _ =+ let df =+ D.fromNamedColumns+ [ ("label", Col.fromList (replicate 50 ("A" :: T.Text) ++ replicate 50 "B"))+ , ("val", Col.fromList ([1 .. 100] :: [Int]))+ ]+ (tr, va) = D.stratifiedSplit (mkStdGen 314) 0.7 (D.col @T.Text "label") df+ in fst (dataframeDimensions tr) + fst (dataframeDimensions va) == 100++strataDf :: DataFrame+strataDf =+ D.fromNamedColumns+ [ ("label", Col.fromList (replicate 5 ("A" :: T.Text) ++ replicate 5 "B"))+ , ("val", Col.fromList ([1 .. 10] :: [Int]))+ ]++unit_stratifiedSample_full :: Test+unit_stratifiedSample_full =+ TestCase $+ let sampled = D.stratifiedSample (mkStdGen 42) 1.0 (D.col @T.Text "label") strataDf+ in assertEqual+ "p=1.0 preserves row count"+ (fst $ dataframeDimensions strataDf)+ (fst $ dataframeDimensions sampled)++unit_stratifiedSplit_rowCount :: Test+unit_stratifiedSplit_rowCount =+ TestCase $+ let (tr, va) = D.stratifiedSplit (mkStdGen 99) 0.8 (D.col @T.Text "label") strataDf+ in assertEqual+ "train+validation == total"+ (fst $ dataframeDimensions strataDf)+ (fst (dataframeDimensions tr) + fst (dataframeDimensions va))++unit_stratifiedSplit_singleRowStratum :: Test+unit_stratifiedSplit_singleRowStratum =+ TestCase $+ let tinyDf =+ D.fromNamedColumns+ [ ("label", Col.fromList (["A", "A", "A", "A", "A", "B"] :: [T.Text]))+ , ("val", Col.fromList ([1 .. 6] :: [Int]))+ ]+ (tr, va) = D.stratifiedSplit (mkStdGen 7) 0.8 (D.col @T.Text "label") tinyDf+ in assertEqual+ "single-row stratum: no rows lost"+ (fst $ dataframeDimensions tinyDf)+ (fst (dataframeDimensions tr) + fst (dataframeDimensions va))++-- | Count occurrences of a label in a column, expressed as a fraction of total rows.+labelProportion :: T.Text -> T.Text -> DataFrame -> Double+labelProportion col label df =+ let total = fst (dataframeDimensions df)+ vals = case getColumn col df of+ Just c -> Col.toList @T.Text c+ Nothing -> []+ n = length (filter (== label) vals)+ in fromIntegral n / fromIntegral total++unit_stratifiedSplit_proportions :: Test+unit_stratifiedSplit_proportions =+ TestCase $+ let aCount = 100+ bCount = 50+ df =+ D.fromNamedColumns+ [+ ( "label"+ , Col.fromList (replicate aCount ("A" :: T.Text) ++ replicate bCount "B")+ )+ , ("val", Col.fromList ([1 .. aCount + bCount] :: [Int]))+ ]+ (tr, va) = D.stratifiedSplit (mkStdGen 42) 0.8 (D.col @T.Text "label") df+ origProp = labelProportion "label" "A" df+ trProp = labelProportion "label" "A" tr+ vaProp = labelProportion "label" "A" va+ tol = 0.05 :: Double+ in do+ assertBool+ ( "train A-proportion "+ ++ show trProp+ ++ " differs from original "+ ++ show origProp+ ++ " by more than "+ ++ show tol+ )+ (abs (trProp - origProp) < tol)+ assertBool+ ( "validation A-proportion "+ ++ show vaProp+ ++ " differs from original "+ ++ show origProp+ ++ " by more than "+ ++ show tol+ )+ (abs (vaProp - origProp) < tol)++hunitTests :: [Test]+hunitTests =+ [ TestLabel "unit_stratifiedSample_full" unit_stratifiedSample_full+ , TestLabel "unit_stratifiedSplit_rowCount" unit_stratifiedSplit_rowCount+ , TestLabel+ "unit_stratifiedSplit_singleRowStratum"+ unit_stratifiedSplit_singleRowStratum+ , TestLabel "unit_stratifiedSplit_proportions" unit_stratifiedSplit_proportions+ ]++tests :: [DataFrame -> Bool] tests = [ prop_dropZero , prop_takeZero@@ -92,4 +205,5 @@ , prop_excludeAll , prop_cubePreservesSmall , prop_sampleEmptyApprox+ , prop_stratifiedSplit_deterministic ]
tests/Operations/Typing.hs view
@@ -1,5059 +1,1507 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-}--module Operations.Typing where--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 DI-import qualified DataFrame.Operations.Typing as D--import Data.Time (Day, fromGregorian)-import Test.HUnit (Test (TestCase, TestLabel), assertEqual)--testData :: D.DataFrame-testData =- D.fromNamedColumns- [ ("test1", DI.fromList ([1 .. 26] :: [Int]))- , ("test2", DI.fromList ['a' .. 'z'])- ]---- Dimensions-correctDimensions :: Test-correctDimensions = TestCase (assertEqual "should be (26, 2)" (26, 2) (D.dimensions testData))--emptyDataframeDimensions :: Test-emptyDataframeDimensions = TestCase (assertEqual "should be (0, 0)" (0, 0) (D.dimensions D.empty))--dimensionsTest :: [Test]-dimensionsTest =- [ TestLabel "dimensions_correctDimensions" correctDimensions- , TestLabel "dimensions_emptyDataframeDimensions" emptyDataframeDimensions- ]---- PARSING TESTS-------- 1. SIMPLE CASES-parseBools :: Test-parseBools =- let afterParse :: [Bool]- afterParse = [True, True, True] ++ [False, False, False]- beforeParse :: [T.Text]- beforeParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Bools without missing values as UnboxedColumn of Bools"- expected- actual- )--parseInts :: Test-parseInts =- let afterParse :: [Int]- afterParse = [1 .. 50]- beforeParse :: [T.Text]- beforeParse = T.pack . show <$> [1 .. 50]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints without missing values as UnboxedColumn of Ints"- expected- actual- )--parseDoubles :: Test-parseDoubles =- let afterParse :: [Double]- afterParse = [1.0 .. 50.0] ++ [3.14, 2.22, 8.55, 23.3, 12.22222235049450945049504950]- beforeParse :: [T.Text]- beforeParse =- T.pack . show- <$> [1.0 .. 50.0] ++ [3.14, 2.22, 8.55, 23.3, 12.22222235049450945049504950]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Doubles without missing values as UnboxedColumn of Doubles"- expected- actual- )--parseDates :: Test-parseDates =- let afterParse :: [Day]- afterParse =- [ fromGregorian 2020 02 12- , fromGregorian 2020 02 13- , fromGregorian 2020 02 14- , fromGregorian 2020 02 15- , fromGregorian 2020 02 16- , fromGregorian 2020 02 17- , fromGregorian 2020 02 18- , fromGregorian 2020 02 19- , fromGregorian 2020 02 20- , fromGregorian 2020 02 21- , fromGregorian 2020 02 22- , fromGregorian 2020 02 23- , fromGregorian 2020 02 24- , fromGregorian 2020 02 25- , fromGregorian 2020 02 26- ]- beforeParse :: [T.Text]- beforeParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Dates without missing values as BoxedColumn of Days"- expected- actual- )--parseTexts :: Test-parseTexts =- let afterParse :: [T.Text]- afterParse =- [ "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , "Surrender now or prepare to fight!"- , "Meowth, that's right!"- ]- beforeParse :: [T.Text]- beforeParse =- [ "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , "Surrender now or prepare to fight!"- , "Meowth, that's right!"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Text without missing values as BoxedColumn of Text"- expected- actual- )----- 2. COMBINATION CASES-parseBoolsAndIntsAsTexts :: Test-parseBoolsAndIntsAsTexts =- let afterParse :: [T.Text]- afterParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"] ++ ["1", "0", "1"]- beforeParse :: [T.Text]- beforeParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"] ++ ["1", "0", "1"]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses mixture of Bools and Ints as Text"- expected- actual- )--parseIntsAndDoublesAsDoubles :: Test-parseIntsAndDoublesAsDoubles =- let afterParse :: [Double]- afterParse =- [ 1- , 2- , 3- , 4- , 5- , 6- , 7- , 8- , 9- , 10- , 11- , 12- , 13- , 14- , 15- , 16- , 17- , 18- , 19- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- , 34- , 35- , 36- , 37- , 38- , 39- , 40- , 41- , 42- , 43- , 44- , 45- , 46- , 47- , 48- , 49- , 50- , 1.0- , 2.0- , 3.0- , 4.0- , 5.0- , 6.0- , 7.0- , 8.0- , 9.0- , 10.0- , 11.0- , 12.0- , 13.0- , 14.0- , 15.0- , 16.0- , 17.0- , 18.0- , 19.0- , 20.0- , 21.0- , 22.0- , 23.0- , 24.0- , 25.0- , 26.0- , 27.0- , 28.0- , 29.0- , 30.0- , 31.0- , 32.0- , 33.0- , 34.0- , 35.0- , 36.0- , 37.0- , 38.0- , 39.0- , 40.0- , 41.0- , 42.0- , 43.0- , 44.0- , 45.0- , 46.0- , 47.0- , 48.0- , 49.0- , 50.0- , 3.14- , 2.22- , 8.55- , 23.3- , 12.22222235049450945049504950- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles"- expected- actual- )--parseIntsAndDatesAsTexts :: Test-parseIntsAndDatesAsTexts =- let afterParse :: [T.Text]- afterParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Ints and Dates as BoxedColumn of Texts"- expected- actual- )--parseTextsAndDoublesAsTexts :: Test-parseTextsAndDoublesAsTexts =- let afterParse :: [T.Text]- afterParse =- [ "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- beforeParse :: [T.Text]- beforeParse =- [ "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Texts and Doubles as BoxedColumn of Texts"- expected- actual- )--parseDatesAndTextsAsTexts :: Test-parseDatesAndTextsAsTexts =- let afterParse :: [T.Text]- afterParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- , "Jessie"- , "James"- , "Meowth"- ]- beforeParse :: [T.Text]- beforeParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- , "Jessie"- , "James"- , "Meowth"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Dates and Texts as BoxedColumn of Texts"- expected- actual- )---- 3A. PARSING WITH SAFEREAD OFF--parseBoolsWithoutSafeRead :: Test-parseBoolsWithoutSafeRead =- let afterParse :: [Bool]- afterParse = replicate 10 True ++ replicate 10 False- beforeParse :: [T.Text]- beforeParse = replicate 10 "true" ++ replicate 10 "false"- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Bools without missing values as UnboxedColumn of Bools, when safeRead is off"- expected- actual- )--parseIntsWithoutSafeRead :: Test-parseIntsWithoutSafeRead =- let afterParse :: [Int]- afterParse =- [ 1- , 2- , 3- , 4- , 5- , 6- , 7- , 8- , 9- , 10- , 11- , 12- , 13- , 14- , 15- , 16- , 17- , 18- , 19- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- , 34- , 35- , 36- , 37- , 38- , 39- , 40- , 41- , 42- , 43- , 44- , 45- , 46- , 47- , 48- , 49- , 50- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints without missing values as UnboxedColumn of Ints, when safeRead is off"- expected- actual- )--parseDoublesWithoutSafeRead :: Test-parseDoublesWithoutSafeRead =- let afterParse :: [Double]- afterParse =- [ 1.0- , 2.0- , 3.0- , 4.0- , 5.0- , 6.0- , 7.0- , 8.0- , 9.0- , 10.0- , 11.0- , 12.0- , 13.0- , 14.0- , 15.0- , 16.0- , 17.0- , 18.0- , 19.0- , 20.0- , 21.0- , 22.0- , 23.0- , 24.0- , 25.0- , 26.0- , 27.0- , 28.0- , 29.0- , 30.0- , 31.0- , 32.0- , 33.0- , 34.0- , 35.0- , 36.0- , 37.0- , 38.0- , 39.0- , 40.0- , 41.0- , 42.0- , 43.0- , 44.0- , 45.0- , 46.0- , 47.0- , 48.0- , 49.0- , 50.0- , 3.14- , 2.22- , 8.55- , 23.3- , 12.22222235049450945049504950- ]- beforeParse :: [T.Text]- beforeParse =- [ "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Doubles without missing values as UnboxedColumn of Doubles, when safeRead is off"- expected- actual- )--parseDatesWithoutSafeRead :: Test-parseDatesWithoutSafeRead =- let afterParse :: [Day]- afterParse =- [ fromGregorian 2020 02 12- , fromGregorian 2020 02 13- , fromGregorian 2020 02 14- , fromGregorian 2020 02 15- , fromGregorian 2020 02 16- , fromGregorian 2020 02 17- , fromGregorian 2020 02 18- , fromGregorian 2020 02 19- , fromGregorian 2020 02 20- , fromGregorian 2020 02 21- , fromGregorian 2020 02 22- , fromGregorian 2020 02 23- , fromGregorian 2020 02 24- , fromGregorian 2020 02 25- , fromGregorian 2020 02 26- ]- beforeParse :: [T.Text]- beforeParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Dates without missing values as BoxedColumn of Days"- expected- actual- )--parseTextsWithoutSafeRead :: Test-parseTextsWithoutSafeRead =- let afterParse :: [T.Text]- afterParse =- [ "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , "Surrender now or prepare to fight!"- , "Meowth, that's right!"- ]- beforeParse :: [T.Text]- beforeParse =- [ "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , "Surrender now or prepare to fight!"- , "Meowth, that's right!"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Text without missing values as BoxedColumn of Text"- expected- actual- )--parseBoolsAndEmptyStringsWithoutSafeRead :: Test-parseBoolsAndEmptyStringsWithoutSafeRead =- let afterParse :: [Maybe Bool]- afterParse = replicate 10 Nothing ++ replicate 10 (Just True) ++ replicate 10 (Just False)- beforeParse :: [T.Text]- beforeParse = replicate 10 "" ++ replicate 10 "true" ++ replicate 10 "false"- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Bools and empty Strings as OptionalColumn of Bools, when safeRead is off"- expected- actual- )--parseIntsAndEmptyStringsWithoutSafeRead :: Test-parseIntsAndEmptyStringsWithoutSafeRead =- let afterParse :: [Maybe Int]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just 1- , Just 2- , Just 3- , Just 4- , Just 5- , Just 6- , Just 7- , Just 8- , Just 9- , Just 10- , Just 11- , Just 12- , Just 13- , Just 14- , Just 15- , Just 16- , Just 17- , Just 18- , Just 19- , Just 20- , Just 21- , Just 22- , Just 23- , Just 24- , Just 25- , Just 26- , Just 27- , Just 28- , Just 29- , Just 30- , Just 31- , Just 32- , Just 33- , Just 34- , Just 35- , Just 36- , Just 37- , Just 38- , Just 39- , Just 40- , Just 41- , Just 42- , Just 43- , Just 44- , Just 45- , Just 46- , Just 47- , Just 48- , Just 49- , Just 50- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , ""- , ""- , ""- , ""- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is off"- expected- actual- )--parseIntsAndDoublesAndEmptyStringsWithoutSafeRead :: Test-parseIntsAndDoublesAndEmptyStringsWithoutSafeRead =- let afterParse :: [Maybe Double]- afterParse =- [ Just 1.0- , Just 2.0- , Just 3.0- , Just 4.0- , Just 5.0- , Just 6.0- , Just 7.0- , Just 8.0- , Just 9.0- , Just 10.0- , Nothing- , Just 11.0- , Just 12.0- , Just 13.0- , Just 14.0- , Just 15.0- , Just 16.0- , Just 17.0- , Just 18.0- , Just 19.0- , Just 20.0- , Nothing- , Just 21.0- , Just 22.0- , Just 23.0- , Just 24.0- , Just 25.0- , Just 26.0- , Just 27.0- , Just 28.0- , Just 29.0- , Just 30.0- , Nothing- , Just 31.0- , Just 32.0- , Just 33.0- , Just 34.0- , Just 35.0- , Just 36.0- , Just 37.0- , Just 38.0- , Just 39.0- , Just 40.0- , Nothing- , Just 41.0- , Just 42.0- , Just 43.0- , Just 44.0- , Just 45.0- , Just 46.0- , Just 47.0- , Just 48.0- , Just 49.0- , Just 50.0- , Nothing- , Just 3.14- , Just 2.22- , Just 8.55- , Just 23.3- , Just 12.22222235049451- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , ""- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , ""- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , ""- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , ""- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , ""- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"- expected- actual- )--parseDatesAndEmptyStringsWithoutSafeRead :: Test-parseDatesAndEmptyStringsWithoutSafeRead =- let afterParse :: [Maybe Day]- afterParse =- [ Just $ fromGregorian 2020 02 12- , Just $ fromGregorian 2020 02 13- , Just $ fromGregorian 2020 02 14- , Nothing- , Just $ fromGregorian 2020 02 15- , Just $ fromGregorian 2020 02 16- , Just $ fromGregorian 2020 02 17- , Nothing- , Just $ fromGregorian 2020 02 18- , Just $ fromGregorian 2020 02 19- , Just $ fromGregorian 2020 02 20- , Nothing- , Just $ fromGregorian 2020 02 21- , Just $ fromGregorian 2020 02 22- , Just $ fromGregorian 2020 02 23- , Nothing- , Just $ fromGregorian 2020 02 24- , Just $ fromGregorian 2020 02 25- , Just $ fromGregorian 2020 02 26- , Nothing- ]- beforeParse :: [T.Text]- beforeParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , ""- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , ""- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , ""- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , ""- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- , ""- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead off"- expected- actual- )--parseTextsAndEmptyStringsWithoutSafeRead :: Test-parseTextsAndEmptyStringsWithoutSafeRead =- let afterParse :: [Maybe T.Text]- afterParse =- [ Nothing- , Just "To"- , Just "protect"- , Just "the"- , Just "world"- , Just "from"- , Just "devastation"- , Nothing- , Just "To"- , Just "unite"- , Just "all"- , Just "people"- , Just "within"- , Just "our"- , Just "nation"- , Nothing- , Just "To"- , Just "denounce"- , Just "the"- , Just "evils"- , Just "of"- , Just "truth"- , Just "and"- , Just "love"- , Nothing- , Just "To"- , Just "extend"- , Just "our"- , Just "reach"- , Just "to"- , Just "the"- , Just "stars"- , Just "above"- , Nothing- , Just "JESSIE!"- , Just "JAMES!"- , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , Nothing- , Just "Surrender now or prepare to fight!"- , Nothing- , Just "Meowth, that's right!"- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , ""- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , ""- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , ""- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , ""- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , ""- , "Surrender now or prepare to fight!"- , ""- , "Meowth, that's right!"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"- expected- actual- )--parseBoolsAndNullishStringsWithoutSafeRead :: Test-parseBoolsAndNullishStringsWithoutSafeRead =- let afterParse :: [T.Text]- afterParse = replicate 10 "N/A" ++ replicate 10 "True" ++ replicate 10 "False"- beforeParse :: [T.Text]- beforeParse = replicate 10 "N/A" ++ replicate 10 "True" ++ replicate 10 "False"- expected = DI.BoxedColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Bools with nullish values as BoxedColumn of Texts, when safeRead is off"- expected- actual- )--parseIntsAndNullishStringsWithoutSafeRead :: Test-parseIntsAndNullishStringsWithoutSafeRead =- let afterParse :: [T.Text]- afterParse =- [ "N/A"- , "N/A"- , "N/A"- , "N/A"- , "N/A"- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- beforeParse :: [T.Text]- beforeParse =- [ "N/A"- , "N/A"- , "N/A"- , "N/A"- , "N/A"- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints with nullish values as BoxedColumn of Texts, when safeRead is off"- expected- actual- )--parseIntsAndDoublesAndNullishStringsWithoutSafeRead :: Test-parseIntsAndDoublesAndNullishStringsWithoutSafeRead =- let afterParse :: [T.Text]- afterParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "Nothing"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "N/A"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "NULL"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "null"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "NAN"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "Nothing"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "N/A"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "NULL"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "null"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "NAN"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"- expected- actual- )--parseIntsAndNullishAndEmptyStringsWithoutSafeRead :: Test-parseIntsAndNullishAndEmptyStringsWithoutSafeRead =- let afterParse :: [Maybe T.Text]- afterParse =- [ Just "N/A"- , Just "N/A"- , Just "N/A"- , Just "N/A"- , Just "N/A"- , Nothing- , Just "1"- , Just "2"- , Just "3"- , Just "4"- , Just "5"- , Just "6"- , Just "7"- , Just "8"- , Just "9"- , Just "10"- , Nothing- , Just "11"- , Just "12"- , Just "13"- , Just "14"- , Just "15"- , Just "16"- , Just "17"- , Just "18"- , Just "19"- , Just "20"- , Nothing- , Just "21"- , Just "22"- , Just "23"- , Just "24"- , Just "25"- , Just "26"- , Just "27"- , Just "28"- , Just "29"- , Just "30"- , Nothing- , Just "31"- , Just "32"- , Just "33"- , Just "34"- , Just "35"- , Just "36"- , Just "37"- , Just "38"- , Just "39"- , Just "40"- , Nothing- , Just "41"- , Just "42"- , Just "43"- , Just "44"- , Just "45"- , Just "46"- , Just "47"- , Just "48"- , Just "49"- , Just "50"- , Nothing- ]- beforeParse :: [T.Text]- beforeParse =- [ "N/A"- , "N/A"- , "N/A"- , "N/A"- , "N/A"- , ""- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , ""- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , ""- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , ""- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , ""- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- , ""- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints with nullish values AND empty strings as OptionalColumn of Texts, when safeRead is off"- expected- actual- )--parseTextsAndEmptyAndNullishStringsWithoutSafeRead :: Test-parseTextsAndEmptyAndNullishStringsWithoutSafeRead =- let afterParse :: [Maybe T.Text]- afterParse =- [ Nothing- , Just "To"- , Just "protect"- , Just "the"- , Just "world"- , Just "from"- , Just "devastation"- , Nothing- , Just "To"- , Just "unite"- , Just "all"- , Just "people"- , Just "within"- , Just "our"- , Just "nation"- , Nothing- , Just "To"- , Just "denounce"- , Just "the"- , Just "evils"- , Just "of"- , Just "truth"- , Just "and"- , Just "love"- , Nothing- , Just "To"- , Just "extend"- , Just "our"- , Just "reach"- , Just "to"- , Just "the"- , Just "stars"- , Just "above"- , Nothing- , Just "JESSIE!"- , Just "JAMES!"- , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , Nothing- , Just "Surrender now or prepare to fight!"- , Nothing- , Just "Meowth, that's right!"- , Just "NaN"- , Just "Nothing"- , Just "N/A"- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , ""- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , ""- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , ""- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , ""- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , ""- , "Surrender now or prepare to fight!"- , ""- , "Meowth, that's right!"- , "NaN"- , "Nothing"- , "N/A"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"- expected- actual- )---- 3B. PARSING WITH SAFEREAD ON-parseBoolsAndEmptyStringsWithSafeRead :: Test-parseBoolsAndEmptyStringsWithSafeRead =- let afterParse :: [Maybe Bool]- afterParse = replicate 10 Nothing ++ replicate 10 (Just True)- beforeParse :: [T.Text]- beforeParse = replicate 10 "" ++ replicate 10 "true"- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Bools and empty strings as OptionalColumn of Bools, when safeRead is on"- expected- actual- )--parseIntsAndEmptyStringsWithSafeRead :: Test-parseIntsAndEmptyStringsWithSafeRead =- let afterParse :: [Maybe Int]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just 1- , Just 2- , Just 3- , Just 4- , Just 5- , Just 6- , Just 7- , Just 8- , Just 9- , Just 10- , Just 11- , Just 12- , Just 13- , Just 14- , Just 15- , Just 16- , Just 17- , Just 18- , Just 19- , Just 20- , Just 21- , Just 22- , Just 23- , Just 24- , Just 25- , Just 26- , Just 27- , Just 28- , Just 29- , Just 30- , Just 31- , Just 32- , Just 33- , Just 34- , Just 35- , Just 36- , Just 37- , Just 38- , Just 39- , Just 40- , Just 41- , Just 42- , Just 43- , Just 44- , Just 45- , Just 46- , Just 47- , Just 48- , Just 49- , Just 50- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , ""- , ""- , ""- , ""- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is on"- expected- actual- )--parseIntsAndDoublesAndEmptyStringsWithSafeRead :: Test-parseIntsAndDoublesAndEmptyStringsWithSafeRead =- let afterParse :: [Maybe Double]- afterParse =- [ Just 1.0- , Just 2.0- , Just 3.0- , Just 4.0- , Just 5.0- , Just 6.0- , Just 7.0- , Just 8.0- , Just 9.0- , Just 10.0- , Nothing- , Just 11.0- , Just 12.0- , Just 13.0- , Just 14.0- , Just 15.0- , Just 16.0- , Just 17.0- , Just 18.0- , Just 19.0- , Just 20.0- , Nothing- , Just 21.0- , Just 22.0- , Just 23.0- , Just 24.0- , Just 25.0- , Just 26.0- , Just 27.0- , Just 28.0- , Just 29.0- , Just 30.0- , Nothing- , Just 31.0- , Just 32.0- , Just 33.0- , Just 34.0- , Just 35.0- , Just 36.0- , Just 37.0- , Just 38.0- , Just 39.0- , Just 40.0- , Nothing- , Just 41.0- , Just 42.0- , Just 43.0- , Just 44.0- , Just 45.0- , Just 46.0- , Just 47.0- , Just 48.0- , Just 49.0- , Just 50.0- , Nothing- , Just 3.14- , Just 2.22- , Just 8.55- , Just 23.3- , Just 12.22222235049451- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , ""- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , ""- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , ""- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , ""- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , ""- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is on"- expected- actual- )--parseDatesAndEmptyStringsWithSafeRead :: Test-parseDatesAndEmptyStringsWithSafeRead =- let afterParse :: [Maybe Day]- afterParse =- [ Just $ fromGregorian 2020 02 12- , Just $ fromGregorian 2020 02 13- , Just $ fromGregorian 2020 02 14- , Nothing- , Just $ fromGregorian 2020 02 15- , Just $ fromGregorian 2020 02 16- , Just $ fromGregorian 2020 02 17- , Nothing- , Just $ fromGregorian 2020 02 18- , Just $ fromGregorian 2020 02 19- , Just $ fromGregorian 2020 02 20- , Nothing- , Just $ fromGregorian 2020 02 21- , Just $ fromGregorian 2020 02 22- , Just $ fromGregorian 2020 02 23- , Nothing- , Just $ fromGregorian 2020 02 24- , Just $ fromGregorian 2020 02 25- , Just $ fromGregorian 2020 02 26- , Nothing- ]- beforeParse :: [T.Text]- beforeParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , ""- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , ""- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , ""- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , ""- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- , ""- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead on"- expected- actual- )--parseTextsAndEmptyStringsWithSafeRead :: Test-parseTextsAndEmptyStringsWithSafeRead =- let afterParse :: [Maybe T.Text]- afterParse =- [ Nothing- , Just "To"- , Just "protect"- , Just "the"- , Just "world"- , Just "from"- , Just "devastation"- , Nothing- , Just "To"- , Just "unite"- , Just "all"- , Just "people"- , Just "within"- , Just "our"- , Just "nation"- , Nothing- , Just "To"- , Just "denounce"- , Just "the"- , Just "evils"- , Just "of"- , Just "truth"- , Just "and"- , Just "love"- , Nothing- , Just "To"- , Just "extend"- , Just "our"- , Just "reach"- , Just "to"- , Just "the"- , Just "stars"- , Just "above"- , Nothing- , Just "JESSIE!"- , Just "JAMES!"- , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , Nothing- , Just "Surrender now or prepare to fight!"- , Nothing- , Just "Meowth, that's right!"- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , ""- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , ""- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , ""- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , ""- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , ""- , "Surrender now or prepare to fight!"- , ""- , "Meowth, that's right!"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead on"- expected- actual- )--parseIntsAndNullishStringsWithSafeRead :: Test-parseIntsAndNullishStringsWithSafeRead =- let afterParse :: [Maybe Int]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just 1- , Just 2- , Just 3- , Just 4- , Just 5- , Just 6- , Just 7- , Just 8- , Just 9- , Just 10- , Just 11- , Just 12- , Just 13- , Just 14- , Just 15- , Just 16- , Just 17- , Just 18- , Just 19- , Just 20- , Just 21- , Just 22- , Just 23- , Just 24- , Just 25- , Just 26- , Just 27- , Just 28- , Just 29- , Just 30- , Just 31- , Just 32- , Just 33- , Just 34- , Just 35- , Just 36- , Just 37- , Just 38- , Just 39- , Just 40- , Just 41- , Just 42- , Just 43- , Just 44- , Just 45- , Just 46- , Just 47- , Just 48- , Just 49- , Just 50- ]- beforeParse :: [T.Text]- beforeParse =- [ "N/A"- , "N/A"- , "N/A"- , "N/A"- , "N/A"- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints with nullish values as OptionalColumn of Ints, when safeRead is on"- expected- actual- )--parseIntsAndDoublesAndNullishStringsWithSafeRead :: Test-parseIntsAndDoublesAndNullishStringsWithSafeRead =- let afterParse :: [Maybe Double]- afterParse =- [ Just 1.0- , Just 2.0- , Just 3.0- , Just 4.0- , Just 5.0- , Just 6.0- , Just 7.0- , Just 8.0- , Just 9.0- , Just 10.0- , Nothing- , Just 11.0- , Just 12.0- , Just 13.0- , Just 14.0- , Just 15.0- , Just 16.0- , Just 17.0- , Just 18.0- , Just 19.0- , Just 20.0- , Nothing- , Just 21.0- , Just 22.0- , Just 23.0- , Just 24.0- , Just 25.0- , Just 26.0- , Just 27.0- , Just 28.0- , Just 29.0- , Just 30.0- , Nothing- , Just 31.0- , Just 32.0- , Just 33.0- , Just 34.0- , Just 35.0- , Just 36.0- , Just 37.0- , Just 38.0- , Just 39.0- , Just 40.0- , Nothing- , Just 41.0- , Just 42.0- , Just 43.0- , Just 44.0- , Just 45.0- , Just 46.0- , Just 47.0- , Just 48.0- , Just 49.0- , Just 50.0- , Nothing- , Just 3.14- , Just 2.22- , Just 8.55- , Just 23.3- , Just 12.03- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "Nothing"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "N/A"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "NULL"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "null"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "NAN"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.03"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"- expected- actual- )--parseIntsAndNullishAndEmptyStringsWithSafeRead :: Test-parseIntsAndNullishAndEmptyStringsWithSafeRead =- let afterParse :: [Maybe Int]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just 1- , Just 2- , Just 3- , Just 4- , Just 5- , Just 6- , Just 7- , Just 8- , Just 9- , Just 10- , Nothing- , Just 11- , Just 12- , Just 13- , Just 14- , Just 15- , Just 16- , Just 17- , Just 18- , Just 19- , Just 20- , Nothing- , Just 21- , Just 22- , Just 23- , Just 24- , Just 25- , Just 26- , Just 27- , Just 28- , Just 29- , Just 30- , Nothing- , Just 31- , Just 32- , Just 33- , Just 34- , Just 35- , Just 36- , Just 37- , Just 38- , Just 39- , Just 40- , Nothing- , Just 41- , Just 42- , Just 43- , Just 44- , Just 45- , Just 46- , Just 47- , Just 48- , Just 49- , Just 50- , Nothing- ]- beforeParse :: [T.Text]- beforeParse =- [ "N/A"- , "N/A"- , "N/A"- , "N/A"- , "Nothing"- , ""- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , ""- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , ""- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , ""- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , ""- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- , ""- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints with nullish values AND empty strings as OptionalColumn of Ints, when safeRead is on"- expected- actual- )--parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead :: Test-parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead =- let afterParse :: [Maybe Double]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just 1- , Just 2- , Just 3- , Just 4- , Just 5- , Just 6- , Just 7- , Just 8- , Just 9- , Just 10- , Nothing- , Just 11- , Just 12- , Just 13- , Just 14- , Just 15- , Just 16- , Just 17- , Just 18- , Just 19- , Just 20- , Nothing- , Just 21- , Just 22- , Just 23- , Just 24- , Just 25- , Just 26- , Just 27- , Just 28- , Just 29- , Just 30- , Nothing- , Just 3.14- ]- beforeParse :: [T.Text]- beforeParse =- [ "N/A"- , "N/A"- , "N/A"- , "N/A"- , "Nothing"- , ""- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , ""- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , ""- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , ""- , "3.14"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints and Doubles with nullish values AND empty strings as OptionalColumn of Doubles, when safeRead is on"- expected- actual- )--parseTextsAndEmptyAndNullishStringsWithSafeRead :: Test-parseTextsAndEmptyAndNullishStringsWithSafeRead =- let afterParse :: [Maybe T.Text]- afterParse =- [ Nothing- , Just "To"- , Just "protect"- , Just "the"- , Just "world"- , Just "from"- , Just "devastation"- , Nothing- , Just "To"- , Just "unite"- , Just "all"- , Just "people"- , Just "within"- , Just "our"- , Just "nation"- , Nothing- , Just "To"- , Just "denounce"- , Just "the"- , Just "evils"- , Just "of"- , Just "truth"- , Just "and"- , Just "love"- , Nothing- , Just "To"- , Just "extend"- , Just "our"- , Just "reach"- , Just "to"- , Just "the"- , Just "stars"- , Just "above"- , Nothing- , Just "JESSIE!"- , Just "JAMES!"- , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , Nothing- , Just "Surrender now or prepare to fight!"- , Nothing- , Just "Meowth, that's right!"- , Nothing- , Nothing- , Nothing- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , ""- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , ""- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , ""- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , ""- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , ""- , "Surrender now or prepare to fight!"- , ""- , "Meowth, that's right!"- , "NaN"- , "Nothing"- , "N/A"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"- expected- actual- )---- 4. PARSING SHOULD NOT DEPEND ON THE NUMBER OF EXAMPLES.-parseBoolsWithOneExample :: Test-parseBoolsWithOneExample =- let afterParse :: [Bool]- afterParse = False : replicate 50 True- beforeParse :: [T.Text]- beforeParse = "false" : replicate 50 "true"- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Bools without missing values as UnboxedColumn of Ints with only one example"- expected- actual- )--parseBoolsWithManyExamples :: Test-parseBoolsWithManyExamples =- let afterParse :: [Bool]- afterParse = False : replicate 50 True- beforeParse :: [T.Text]- beforeParse = "false" : replicate 50 "true"- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 49 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Bools without missing values as UnboxedColumn of Ints with only one example"- expected- actual- )--parseIntsWithOneExample :: Test-parseIntsWithOneExample =- let afterParse :: [Int]- afterParse =- [ 1- , 2- , 3- , 4- , 5- , 6- , 7- , 8- , 9- , 10- , 11- , 12- , 13- , 14- , 15- , 16- , 17- , 18- , 19- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- , 34- , 35- , 36- , 37- , 38- , 39- , 40- , 41- , 42- , 43- , 44- , 45- , 46- , 47- , 48- , 49- , 50- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints without missing values as UnboxedColumn of Ints with only one example"- expected- actual- )--parseIntsWithTwentyFiveExamples :: Test-parseIntsWithTwentyFiveExamples =- let afterParse :: [Int]- afterParse =- [ 1- , 2- , 3- , 4- , 5- , 6- , 7- , 8- , 9- , 10- , 11- , 12- , 13- , 14- , 15- , 16- , 17- , 18- , 19- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- , 34- , 35- , 36- , 37- , 38- , 39- , 40- , 41- , 42- , 43- , 44- , 45- , 46- , 47- , 48- , 49- , 50- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 25 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints without missing values as UnboxedColumn of Ints with some examples"- expected- actual- )--parseIntsWithFortyNineExamples :: Test-parseIntsWithFortyNineExamples =- let afterParse :: [Int]- afterParse =- [ 1- , 2- , 3- , 4- , 5- , 6- , 7- , 8- , 9- , 10- , 11- , 12- , 13- , 14- , 15- , 16- , 17- , 18- , 19- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- , 34- , 35- , 36- , 37- , 38- , 39- , 40- , 41- , 42- , 43- , 44- , 45- , 46- , 47- , 48- , 49- , 50- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 49 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints without missing values as UnboxedColumn of Ints with many examples"- expected- actual- )--parseDatesWithOneExample :: Test-parseDatesWithOneExample =- let afterParse :: [Day]- afterParse =- [ fromGregorian 2020 02 12- , fromGregorian 2020 02 13- , fromGregorian 2020 02 14- , fromGregorian 2020 02 15- , fromGregorian 2020 02 16- , fromGregorian 2020 02 17- , fromGregorian 2020 02 18- , fromGregorian 2020 02 19- , fromGregorian 2020 02 20- , fromGregorian 2020 02 21- , fromGregorian 2020 02 22- , fromGregorian 2020 02 23- , fromGregorian 2020 02 24- , fromGregorian 2020 02 25- , fromGregorian 2020 02 26- ]- beforeParse :: [T.Text]- beforeParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Dates without missing values as BoxedColumn of Days with only one example"- expected- actual- )--parseDatesWithFifteenExamples :: Test-parseDatesWithFifteenExamples =- let afterParse :: [Day]- afterParse =- [ fromGregorian 2020 02 12- , fromGregorian 2020 02 13- , fromGregorian 2020 02 14- , fromGregorian 2020 02 15- , fromGregorian 2020 02 16- , fromGregorian 2020 02 17- , fromGregorian 2020 02 18- , fromGregorian 2020 02 19- , fromGregorian 2020 02 20- , fromGregorian 2020 02 21- , fromGregorian 2020 02 22- , fromGregorian 2020 02 23- , fromGregorian 2020 02 24- , fromGregorian 2020 02 25- , fromGregorian 2020 02 26- ]- beforeParse :: [T.Text]- beforeParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 15 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Dates without missing values as BoxedColumn of Days with many examples"- expected- actual- )--parseIntsAndDoublesAsDoublesWithOneExample :: Test-parseIntsAndDoublesAsDoublesWithOneExample =- let afterParse :: [Double]- afterParse =- [ 1- , 2- , 3- , 4- , 5- , 6- , 7- , 8- , 9- , 10- , 11- , 12- , 13- , 14- , 15- , 16- , 17- , 18- , 19- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- , 34- , 35- , 36- , 37- , 38- , 39- , 40- , 41- , 42- , 43- , 44- , 45- , 46- , 47- , 48- , 49- , 50- , 1.0- , 2.0- , 3.0- , 4.0- , 5.0- , 6.0- , 7.0- , 8.0- , 9.0- , 10.0- , 11.0- , 12.0- , 13.0- , 14.0- , 15.0- , 16.0- , 17.0- , 18.0- , 19.0- , 20.0- , 21.0- , 22.0- , 23.0- , 24.0- , 25.0- , 26.0- , 27.0- , 28.0- , 29.0- , 30.0- , 31.0- , 32.0- , 33.0- , 34.0- , 35.0- , 36.0- , 37.0- , 38.0- , 39.0- , 40.0- , 41.0- , 42.0- , 43.0- , 44.0- , 45.0- , 46.0- , 47.0- , 48.0- , 49.0- , 50.0- , 3.14- , 2.22- , 8.55- , 23.3- , 12.22222235049450945049504950- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"- expected- actual- )--parseIntsAndDoublesAsDoublesWithManyExamples :: Test-parseIntsAndDoublesAsDoublesWithManyExamples =- let afterParse :: [Double]- afterParse =- [ 1- , 2- , 3- , 4- , 5- , 6- , 7- , 8- , 9- , 10- , 11- , 12- , 13- , 14- , 15- , 16- , 17- , 18- , 19- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- , 34- , 35- , 36- , 37- , 38- , 39- , 40- , 41- , 42- , 43- , 44- , 45- , 46- , 47- , 48- , 49- , 50- , 1.0- , 2.0- , 3.0- , 4.0- , 5.0- , 6.0- , 7.0- , 8.0- , 9.0- , 10.0- , 11.0- , 12.0- , 13.0- , 14.0- , 15.0- , 16.0- , 17.0- , 18.0- , 19.0- , 20.0- , 21.0- , 22.0- , 23.0- , 24.0- , 25.0- , 26.0- , 27.0- , 28.0- , 29.0- , 30.0- , 31.0- , 32.0- , 33.0- , 34.0- , 35.0- , 36.0- , 37.0- , 38.0- , 39.0- , 40.0- , 41.0- , 42.0- , 43.0- , 44.0- , 45.0- , 46.0- , 47.0- , 48.0- , 49.0- , 50.0- , 3.14- , 2.22- , 8.55- , 23.3- , 12.22222235049450945049504950- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 50 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"- expected- actual- )--parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff :: Test-parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff =- let afterParse :: [Maybe Double]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just 1.0- , Just 2.0- , Just 3.0- , Just 4.0- , Just 5.0- , Just 6.0- , Just 7.0- , Just 8.0- , Just 9.0- , Just 10.0- , Just 11.0- , Just 12.0- , Just 13.0- , Just 14.0- , Just 15.0- , Just 16.0- , Just 17.0- , Just 18.0- , Just 19.0- , Just 20.0- , Just 21.0- , Just 22.0- , Just 23.0- , Just 24.0- , Just 25.0- , Just 26.0- , Just 27.0- , Just 28.0- , Just 29.0- , Just 30.0- , Just 31.0- , Just 32.0- , Just 33.0- , Just 34.0- , Just 35.0- , Just 36.0- , Just 37.0- , Just 38.0- , Just 39.0- , Just 40.0- , Just 41.0- , Just 42.0- , Just 43.0- , Just 44.0- , Just 45.0- , Just 46.0- , Just 47.0- , Just 48.0- , Just 49.0- , Just 50.0- , Just 1.0- , Just 2.0- , Just 3.0- , Just 4.0- , Just 5.0- , Just 6.0- , Just 7.0- , Just 8.0- , Just 9.0- , Just 10.0- , Just 11.0- , Just 12.0- , Just 13.0- , Just 14.0- , Just 15.0- , Just 16.0- , Just 17.0- , Just 18.0- , Just 19.0- , Just 20.0- , Just 21.0- , Just 22.0- , Just 23.0- , Just 24.0- , Just 25.0- , Just 26.0- , Just 27.0- , Just 28.0- , Just 29.0- , Just 30.0- , Just 31.0- , Just 32.0- , Just 33.0- , Just 34.0- , Just 35.0- , Just 36.0- , Just 37.0- , Just 38.0- , Just 39.0- , Just 40.0- , Just 41.0- , Just 42.0- , Just 43.0- , Just 44.0- , Just 45.0- , Just 46.0- , Just 47.0- , Just 48.0- , Just 49.0- , Just 50.0- , Just 3.14- , Just 2.22- , Just 8.55- , Just 23.3- , Just 12.22222235049451- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 1 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"- expected- actual- )--parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff ::- Test-parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff =- let afterParse :: [Maybe Double]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just 1.0- , Just 2.0- , Just 3.0- , Just 4.0- , Just 5.0- , Just 6.0- , Just 7.0- , Just 8.0- , Just 9.0- , Just 10.0- , Just 11.0- , Just 12.0- , Just 13.0- , Just 14.0- , Just 15.0- , Just 16.0- , Just 17.0- , Just 18.0- , Just 19.0- , Just 20.0- , Just 21.0- , Just 22.0- , Just 23.0- , Just 24.0- , Just 25.0- , Just 26.0- , Just 27.0- , Just 28.0- , Just 29.0- , Just 30.0- , Just 31.0- , Just 32.0- , Just 33.0- , Just 34.0- , Just 35.0- , Just 36.0- , Just 37.0- , Just 38.0- , Just 39.0- , Just 40.0- , Just 41.0- , Just 42.0- , Just 43.0- , Just 44.0- , Just 45.0- , Just 46.0- , Just 47.0- , Just 48.0- , Just 49.0- , Just 50.0- , Just 1.0- , Just 2.0- , Just 3.0- , Just 4.0- , Just 5.0- , Just 6.0- , Just 7.0- , Just 8.0- , Just 9.0- , Just 10.0- , Just 11.0- , Just 12.0- , Just 13.0- , Just 14.0- , Just 15.0- , Just 16.0- , Just 17.0- , Just 18.0- , Just 19.0- , Just 20.0- , Just 21.0- , Just 22.0- , Just 23.0- , Just 24.0- , Just 25.0- , Just 26.0- , Just 27.0- , Just 28.0- , Just 29.0- , Just 30.0- , Just 31.0- , Just 32.0- , Just 33.0- , Just 34.0- , Just 35.0- , Just 36.0- , Just 37.0- , Just 38.0- , Just 39.0- , Just 40.0- , Just 41.0- , Just 42.0- , Just 43.0- , Just 44.0- , Just 45.0- , Just 46.0- , Just 47.0- , Just 48.0- , Just 49.0- , Just 50.0- , Just 3.14- , Just 2.22- , Just 8.55- , Just 23.3- , Just 12.22222235049451- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 30 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"- expected- actual- )--parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff ::- Test-parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff =- let afterParse :: [Maybe T.Text]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "1"- , Just "2"- , Just "3"- , Just "4"- , Just "5"- , Just "6"- , Just "7"- , Just "8"- , Just "9"- , Just "10"- , Just "11"- , Just "12"- , Just "13"- , Just "14"- , Just "15"- , Just "16"- , Just "17"- , Just "18"- , Just "19"- , Just "20"- , Just "1.0"- , Just "2.0"- , Just "3.0"- , Just "4.0"- , Just "5.0"- , Just "6.0"- , Just "7.0"- , Just "8.0"- , Just "9.0"- , Just "10.0"- , Just "11.0"- , Just "12.0"- , Just "13.0"- , Just "14.0"- , Just "15.0"- , Just "16.0"- , Just "17.0"- , Just "18.0"- , Just "19.0"- , Just "20.0"- , Just "21.0"- , Just "22.0"- , Just "23.0"- , Just "24.0"- , Just "25.0"- , Just "26.0"- , Just "27.0"- , Just "28.0"- , Just "29.0"- , Just "30.0"- , Just "3.14"- , Just "2.22"- , Just "8.55"- , Just "23.3"- , Just "12.22222235049451"- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 1 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Ints, Doubles, empty strings, nullish as OptionalColumn of Text with just one example, when safeRead is off"- expected- actual- )--parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff ::- Test-parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff =- let afterParse :: [Maybe T.Text]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "1"- , Just "2"- , Just "3"- , Just "4"- , Just "5"- , Just "6"- , Just "7"- , Just "8"- , Just "9"- , Just "10"- , Just "11"- , Just "12"- , Just "13"- , Just "14"- , Just "15"- , Just "16"- , Just "17"- , Just "18"- , Just "19"- , Just "20"- , Just "1.0"- , Just "2.0"- , Just "3.0"- , Just "4.0"- , Just "5.0"- , Just "6.0"- , Just "7.0"- , Just "8.0"- , Just "9.0"- , Just "10.0"- , Just "11.0"- , Just "12.0"- , Just "13.0"- , Just "14.0"- , Just "15.0"- , Just "16.0"- , Just "17.0"- , Just "18.0"- , Just "19.0"- , Just "20.0"- , Just "21.0"- , Just "22.0"- , Just "23.0"- , Just "24.0"- , Just "25.0"- , Just "26.0"- , Just "27.0"- , Just "28.0"- , Just "29.0"- , Just "30.0"- , Just "3.14"- , Just "2.22"- , Just "8.55"- , Just "23.3"- , Just "12.22222235049451"- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 30 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Ints, Doubles, empty strings, nullish as OptionalColumn of Text with many examples, when safeRead is off"- expected- actual- )---- 5. EDGE CASES THAT HAVE TO BE INTERPRETED CORRECTLY--parseManyNullishAndOneInt :: Test-parseManyNullishAndOneInt =- let afterParse :: [Maybe Int]- afterParse = replicate 100 Nothing ++ [Just 100000]- beforeParse :: [T.Text]- beforeParse = replicate 100 "NaN" ++ ["100000"]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)--parseManyNullishAndOneDouble :: Test-parseManyNullishAndOneDouble =- let afterParse :: [Maybe Double]- afterParse = replicate 100 Nothing ++ [Just 3.14]- beforeParse :: [T.Text]- beforeParse = replicate 100 "NaN" ++ ["3.14"]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)--parseManyNullishAndOneDate :: Test-parseManyNullishAndOneDate =- let afterParse :: [Maybe Day]- afterParse = replicate 100 Nothing ++ [Just $ fromGregorian 2024 12 25]- beforeParse :: [T.Text]- beforeParse = replicate 100 "NaN" ++ ["2024-12-25"]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)--parseManyNullishAndIncorrectDates :: Test-parseManyNullishAndIncorrectDates =- let afterParse :: [Maybe T.Text]- afterParse = replicate 100 Nothing ++ [Just "2024-12-25", Just "2024-12-w6"]- beforeParse :: [T.Text]- beforeParse = replicate 100 "NaN" ++ ["2024-12-25", "2024-12-w6"]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)--parseRepeatedNullish :: Test-parseRepeatedNullish =- let afterParse :: [Maybe T.Text]- afterParse = replicate 100 Nothing- beforeParse :: [T.Text]- beforeParse = replicate 100 "NaN"- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)--tests :: [Test]-tests =- dimensionsTest- ++ [- -- 1. SIMPLE CASES- TestLabel "parseBools" parseBools- , TestLabel "parseInts" parseInts- , TestLabel "parseDoubles" parseDoubles- , TestLabel "parseDates" parseDates- , TestLabel "parseTexts" parseTexts- , -- 2. COMBINATION CASES- TestLabel "parseBoolsAndIntsAsTexts" parseBoolsAndIntsAsTexts- , TestLabel "parseIntsAndDoublesAsDoubles" parseIntsAndDoublesAsDoubles- , TestLabel "parseIntsAndDatesAsTexts" parseIntsAndDatesAsTexts- , TestLabel "parseTextsAndDoublesAsTexts" parseTextsAndDoublesAsTexts- , TestLabel "parseDatesAndTextsAsTexts" parseDatesAndTextsAsTexts- , -- 3A. PARSING WITH SAFEREAD OFF- TestLabel "parseBoolsWithoutSafeRead" parseBoolsWithoutSafeRead- , TestLabel "parseIntsWithoutSafeRead" parseIntsWithoutSafeRead- , TestLabel "parseDoublesWithoutSafeRead" parseDoublesWithoutSafeRead- , TestLabel "parseDatesWithoutSafeRead" parseDatesWithoutSafeRead- , TestLabel "parseTextsWithoutSafeRead" parseTextsWithoutSafeRead- , TestLabel- "parseBoolsAndEmptyStringsWithoutSafeRead"- parseBoolsAndEmptyStringsWithoutSafeRead- , TestLabel- "parseIntsAndEmptyStringsWithoutSafeRead"- parseIntsAndEmptyStringsWithoutSafeRead- , TestLabel- "parseIntsAndDoublesAndEmptyStringsWithoutSafeRead"- parseIntsAndDoublesAndEmptyStringsWithoutSafeRead- , TestLabel- "parseDatesAndEmptyStringsWithoutSafeRead"- parseDatesAndEmptyStringsWithoutSafeRead- , TestLabel- "parseTextsAndEmptyStringsWithoutSafeRead"- parseTextsAndEmptyStringsWithoutSafeRead- , TestLabel- "parseBoolsAndNullishStringsWithoutSafeRead"- parseBoolsAndNullishStringsWithoutSafeRead- , TestLabel- "parseIntsAndNullishStringsWithoutSafeRead"- parseIntsAndNullishStringsWithoutSafeRead- , TestLabel- "parseIntsAndDoublesAndNullishStringsWithoutSafeRead"- parseIntsAndDoublesAndNullishStringsWithoutSafeRead- , TestLabel- "parseIntsAndNullishAndEmptyStringsWithoutSafeRead"- parseIntsAndNullishAndEmptyStringsWithoutSafeRead- , TestLabel- "parseTextsAndEmptyAndNullishStringsWithoutSafeRead"- parseTextsAndEmptyAndNullishStringsWithoutSafeRead- , -- 3B. PARSING WITH SAFEREAD ON- TestLabel- "parseBoolsAndEmptyStringsWithSafeRead"- parseBoolsAndEmptyStringsWithSafeRead- , TestLabel- "parseIntsAndEmptyStringsWithSafeRead"- parseIntsAndEmptyStringsWithSafeRead- , TestLabel- "parseIntsAndDoublesAndEmptyStringsWithSafeRead"- parseIntsAndDoublesAndEmptyStringsWithSafeRead- , TestLabel- "parseDatesAndEmptyStringsWithSafeRead"- parseDatesAndEmptyStringsWithSafeRead- , TestLabel- "parseTextsAndEmptyStringsWithSafeRead"- parseTextsAndEmptyStringsWithSafeRead- , TestLabel- "parseIntsAndNullishStringsWithSafeRead"- parseIntsAndNullishStringsWithSafeRead- , TestLabel- "parseIntsAndDoublesAndNullishStringsWithSafeRead"- parseIntsAndDoublesAndNullishStringsWithSafeRead- , TestLabel- "parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead"- parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead- , TestLabel- "parseIntsAndNullishAndEmptyStringsWithSafeRead"- parseIntsAndNullishAndEmptyStringsWithSafeRead- , TestLabel- "parseTextsAndEmptyAndNullishStringsWithSafeRead"- parseTextsAndEmptyAndNullishStringsWithSafeRead+{-# LANGUAGE TypeApplications #-}++module Operations.Typing where++import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import qualified DataFrame as D+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.DataFrame (getColumn)+import qualified DataFrame.Operations.Typing as D++import Data.Time (Day, fromGregorian)+import Test.HUnit (Test (TestCase, TestLabel), assertEqual, assertFailure)++testData :: D.DataFrame+testData =+ D.fromNamedColumns+ [ ("test1", DI.fromList ([1 .. 26] :: [Int]))+ , ("test2", DI.fromList ['a' .. 'z'])+ ]++-- Dimensions+correctDimensions :: Test+correctDimensions = TestCase (assertEqual "should be (26, 2)" (26, 2) (D.dimensions testData))++emptyDataframeDimensions :: Test+emptyDataframeDimensions = TestCase (assertEqual "should be (0, 0)" (0, 0) (D.dimensions D.empty))++dimensionsTest :: [Test]+dimensionsTest =+ [ TestLabel "dimensions_correctDimensions" correctDimensions+ , TestLabel "dimensions_emptyDataframeDimensions" emptyDataframeDimensions+ ]++-- Shared data directory+typingDataDir :: FilePath+typingDataDir = "./tests/data/typing/"++-- Shared input/expected data+intsInput :: [T.Text]+intsInput = T.pack . show <$> ([1 .. 50] :: [Int])++intsExpected :: [Int]+intsExpected = [1 .. 50]++doublesSpecial :: [Double]+doublesSpecial = [3.14, 2.22, 8.55, 23.3, 12.22222235049450945049504950]++doublesSpecialInput :: [T.Text]+doublesSpecialInput = ["3.14", "2.22", "8.55", "23.3", "12.22222235049451"]++doublesExpected :: [Double]+doublesExpected = [1.0 .. 50.0] ++ doublesSpecial++doublesInput :: [T.Text]+doublesInput = (T.pack . show <$> ([1.0 .. 50.0] :: [Double])) ++ doublesSpecialInput++intsAndDoublesExpected :: [Double]+intsAndDoublesExpected = ([1 .. 50] :: [Double]) ++ doublesExpected++intsAndDoublesInput :: [T.Text]+intsAndDoublesInput = intsInput ++ doublesInput++datesExpected :: [Day]+datesExpected = [fromGregorian 2020 02 12 .. fromGregorian 2020 02 26]++datesInput :: [T.Text]+datesInput = T.pack . show <$> datesExpected++-- PARSING TESTS+------- 1. SIMPLE CASES+parseBools :: Test+parseBools =+ let afterParse :: [Bool]+ afterParse = [True, True, True] ++ [False, False, False]+ beforeParse :: [T.Text]+ beforeParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Bools without missing values as OptionalColumn of Maybe Bools"+ expected+ actual+ )++parseInts :: Test+parseInts =+ let afterParse :: [Int]+ afterParse = [1 .. 50]+ beforeParse :: [T.Text]+ beforeParse = T.pack . show <$> ([1 .. 50] :: [Int])+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints without missing values as OptionalColumn of Maybe Ints"+ expected+ actual+ )++parseDoubles :: Test+parseDoubles =+ let afterParse :: [Double]+ afterParse = [1.0 .. 50.0] ++ [3.14, 2.22, 8.55, 23.3, 12.22222235049450945049504950]+ beforeParse :: [T.Text]+ beforeParse =+ T.pack . show+ <$> ( [1.0 .. 50.0] ++ [3.14, 2.22, 8.55, 23.3, 12.22222235049450945049504950] ::+ [Double]+ )+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Doubles without missing values as OptionalColumn of Maybe Doubles"+ expected+ actual+ )++parseDates :: Test+parseDates =+ let afterParse = datesExpected+ beforeParse = datesInput+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Dates without missing values as OptionalColumn of Maybe Days"+ expected+ actual+ )++parseTexts :: Test+parseTexts = TestCase $ do+ texts <- T.lines <$> TIO.readFile (typingDataDir <> "texts.txt")+ let expected = DI.fromVector $ V.fromList texts+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList texts+ assertEqual+ "Correctly parses Text without missing values as OptionalColumn of Maybe Text"+ expected+ actual++--- 2. COMBINATION CASES+parseBoolsAndIntsAsTexts :: Test+parseBoolsAndIntsAsTexts =+ let afterParse :: [T.Text]+ afterParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"] ++ ["1", "0", "1"]+ beforeParse :: [T.Text]+ beforeParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"] ++ ["1", "0", "1"]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses mixture of Bools and Ints as Maybe Text"+ expected+ actual+ )++parseIntsAndDoublesAsDoubles :: Test+parseIntsAndDoublesAsDoubles =+ let afterParse = intsAndDoublesExpected+ beforeParse = intsAndDoublesInput+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Ints and Doubles as OptionalColumn of Maybe Doubles"+ expected+ actual+ )++parseIntsAndDatesAsTexts :: Test+parseIntsAndDatesAsTexts =+ let data30 = T.pack . show <$> ([1 .. 30] :: [Int])+ data9dates = take 9 datesInput+ afterParse :: [T.Text]+ afterParse = data30 ++ data9dates+ beforeParse :: [T.Text]+ beforeParse = data30 ++ data9dates+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Ints and Dates as OptionalColumn of Maybe Texts"+ expected+ actual+ )++parseTextsAndDoublesAsTexts :: Test+parseTextsAndDoublesAsTexts =+ let shortTexts = ["To", "protect", "the", "world", "from", "devastation"]+ afterParse :: [T.Text]+ afterParse = shortTexts ++ doublesInput+ beforeParse :: [T.Text]+ beforeParse = shortTexts ++ doublesInput+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Texts and Doubles as OptionalColumn of Maybe Texts"+ expected+ actual+ )++parseDatesAndTextsAsTexts :: Test+parseDatesAndTextsAsTexts =+ let extra = ["Jessie", "James", "Meowth"]+ afterParse :: [T.Text]+ afterParse = datesInput ++ extra+ beforeParse :: [T.Text]+ beforeParse = datesInput ++ extra+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Dates and Texts as OptionalColumn of Maybe Texts"+ expected+ actual+ )++-- 3A. PARSING WITH SAFEREAD OFF++parseBoolsWithoutSafeRead :: Test+parseBoolsWithoutSafeRead =+ let afterParse :: [Bool]+ afterParse = replicate 10 True ++ replicate 10 False+ beforeParse :: [T.Text]+ beforeParse = replicate 10 "true" ++ replicate 10 "false"+ expected = DI.UnboxedColumn Nothing $ VU.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Bools without missing values as UnboxedColumn of Bools, when safeRead is off"+ expected+ actual+ )++parseIntsWithoutSafeRead :: Test+parseIntsWithoutSafeRead =+ let afterParse = intsExpected+ beforeParse = intsInput+ expected = DI.UnboxedColumn Nothing $ VU.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints without missing values as UnboxedColumn of Ints, when safeRead is off"+ expected+ actual+ )++parseDoublesWithoutSafeRead :: Test+parseDoublesWithoutSafeRead =+ let afterParse = doublesExpected+ beforeParse = doublesInput+ expected = DI.UnboxedColumn Nothing $ VU.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Doubles without missing values as UnboxedColumn of Doubles, when safeRead is off"+ expected+ actual+ )++parseDatesWithoutSafeRead :: Test+parseDatesWithoutSafeRead =+ let afterParse = datesExpected+ beforeParse = datesInput+ expected = DI.BoxedColumn Nothing $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Dates without missing values as BoxedColumn of Days"+ expected+ actual+ )++parseTextsWithoutSafeRead :: Test+parseTextsWithoutSafeRead = TestCase $ do+ texts <- T.lines <$> TIO.readFile (typingDataDir <> "texts.txt")+ let expected = DI.BoxedColumn Nothing $ V.fromList texts+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList texts+ assertEqual+ "Correctly parses Text without missing values as BoxedColumn of Text"+ expected+ actual++parseBoolsAndEmptyStringsWithoutSafeRead :: Test+parseBoolsAndEmptyStringsWithoutSafeRead =+ let afterParse :: [Maybe Bool]+ afterParse = replicate 10 Nothing ++ replicate 10 (Just True) ++ replicate 10 (Just False)+ beforeParse :: [T.Text]+ beforeParse = replicate 10 "" ++ replicate 10 "true" ++ replicate 10 "false"+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Bools and empty Strings as OptionalColumn of Bools, when safeRead is off"+ expected+ actual+ )++parseIntsAndEmptyStringsWithoutSafeRead :: Test+parseIntsAndEmptyStringsWithoutSafeRead =+ let beforeParse = replicate 5 "" ++ intsInput+ afterParse :: [Maybe Int]+ afterParse = replicate 5 Nothing ++ map Just intsExpected+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is off"+ expected+ actual+ )++parseIntsAndDoublesAndEmptyStringsWithoutSafeRead :: Test+parseIntsAndDoublesAndEmptyStringsWithoutSafeRead =+ let intGrp = T.pack . show <$> ([1 .. 10] :: [Int])+ dblGrps =+ [ T.pack . show <$> ([11.0 .. 20.0] :: [Double])+ , T.pack . show <$> ([21.0 .. 30.0] :: [Double])+ , T.pack . show <$> ([31.0 .. 40.0] :: [Double])+ , T.pack . show <$> ([41.0 .. 50.0] :: [Double])+ ]+ beforeParse = intGrp ++ [""] ++ concatMap (\g -> g ++ [""]) dblGrps ++ doublesSpecialInput+ afterParse :: [Maybe Double]+ afterParse =+ map Just ([1.0 .. 10.0] :: [Double])+ ++ [Nothing]+ ++ concatMap+ (\g -> map Just g ++ [Nothing])+ [[11.0 .. 20.0], [21.0 .. 30.0], [31.0 .. 40.0], [41.0 .. 50.0]]+ ++ map Just doublesSpecial+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"+ expected+ actual+ )++parseDatesAndEmptyStringsWithoutSafeRead :: Test+parseDatesAndEmptyStringsWithoutSafeRead =+ -- Pattern: 3 dates, empty, 3 dates, empty, 3 dates, empty, 3 dates, empty, 3 dates, empty+ let groups = chunksOf3 datesExpected+ beforeParse = concatMap (\g -> map (T.pack . show) g ++ [""]) groups+ afterParse :: [Maybe Day]+ afterParse = concatMap (\g -> map Just g ++ [Nothing]) groups+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead off"+ expected+ actual+ )+ where+ chunksOf3 [] = []+ chunksOf3 xs = take 3 xs : chunksOf3 (drop 3 xs)++parseTextsAndEmptyStringsWithoutSafeRead :: Test+parseTextsAndEmptyStringsWithoutSafeRead = TestCase $ do+ raw <- T.lines <$> TIO.readFile (typingDataDir <> "texts_with_empties.txt")+ let afterParse = map (\t -> if t == "" then Nothing else Just t) raw+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList raw+ assertEqual+ "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"+ expected+ actual++parseBoolsAndNullishStringsWithoutSafeRead :: Test+parseBoolsAndNullishStringsWithoutSafeRead =+ let afterParse :: [T.Text]+ afterParse = replicate 10 "N/A" ++ replicate 10 "True" ++ replicate 10 "False"+ beforeParse :: [T.Text]+ beforeParse = replicate 10 "N/A" ++ replicate 10 "True" ++ replicate 10 "False"+ expected = DI.BoxedColumn Nothing $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Bools with nullish values as BoxedColumn of Texts, when safeRead is off"+ expected+ actual+ )++parseIntsAndNullishStringsWithoutSafeRead :: Test+parseIntsAndNullishStringsWithoutSafeRead =+ let beforeParse = replicate 5 "N/A" ++ intsInput+ afterParse :: [T.Text]+ afterParse = beforeParse+ expected = DI.BoxedColumn Nothing $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints with nullish values as BoxedColumn of Texts, when safeRead is off"+ expected+ actual+ )++parseIntsAndDoublesAndNullishStringsWithoutSafeRead :: Test+parseIntsAndDoublesAndNullishStringsWithoutSafeRead =+ -- Nullish separators are literal text strings (not interpreted as null when safeRead=False)+ let nullishSeps = ["Nothing", "N/A", "NULL", "null", "NAN"]+ intGrp = T.pack . show <$> ([1 .. 10] :: [Int])+ dblGrps =+ [ T.pack . show <$> ([11.0 .. 20.0] :: [Double])+ , T.pack . show <$> ([21.0 .. 30.0] :: [Double])+ , T.pack . show <$> ([31.0 .. 40.0] :: [Double])+ , T.pack . show <$> ([41.0 .. 50.0] :: [Double])+ ]+ beforeParse =+ intGrp+ ++ concat (zipWith (:) nullishSeps dblGrps)+ ++ doublesSpecialInput+ afterParse :: [T.Text]+ afterParse = beforeParse+ expected = DI.BoxedColumn Nothing $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"+ expected+ actual+ )++parseIntsAndNullishAndEmptyStringsWithoutSafeRead :: Test+parseIntsAndNullishAndEmptyStringsWithoutSafeRead =+ -- Pattern: 5x"N/A", then groups of ("" : 10 ints), with trailing ""+ let groups = [[1 .. 10], [11 .. 20], [21 .. 30], [31 .. 40], [41 .. 50]] :: [[Int]]+ beforeParse =+ replicate 5 "N/A"+ ++ concatMap (\g -> "" : map (T.pack . show) g) groups+ ++ [""]+ afterParse :: [Maybe T.Text]+ afterParse =+ replicate 5 (Just "N/A")+ ++ concatMap (\g -> Nothing : map (Just . T.pack . show) g) groups+ ++ [Nothing]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints with nullish values AND empty strings as OptionalColumn of Texts, when safeRead is off"+ expected+ actual+ )++parseTextsAndEmptyAndNullishStringsWithoutSafeRead :: Test+parseTextsAndEmptyAndNullishStringsWithoutSafeRead = TestCase $ do+ raw <-+ T.lines <$> TIO.readFile (typingDataDir <> "texts_with_empties_and_nullish.txt")+ -- safeRead=False: empty strings -> Nothing, nullish text stays as Just+ let afterParse = map (\t -> if t == "" then Nothing else Just t) raw+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList raw+ assertEqual+ "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"+ expected+ actual++-- 3B. PARSING WITH SAFEREAD ON+parseBoolsAndEmptyStringsWithSafeRead :: Test+parseBoolsAndEmptyStringsWithSafeRead =+ let afterParse :: [Maybe Bool]+ afterParse = replicate 10 Nothing ++ replicate 10 (Just True)+ beforeParse :: [T.Text]+ beforeParse = replicate 10 "" ++ replicate 10 "true"+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Bools and empty strings as OptionalColumn of Bools, when safeRead is on"+ expected+ actual+ )++parseIntsAndEmptyStringsWithSafeRead :: Test+parseIntsAndEmptyStringsWithSafeRead =+ let beforeParse = replicate 5 "" ++ intsInput+ afterParse :: [Maybe Int]+ afterParse = replicate 5 Nothing ++ map Just intsExpected+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is on"+ expected+ actual+ )++parseIntsAndDoublesAndEmptyStringsWithSafeRead :: Test+parseIntsAndDoublesAndEmptyStringsWithSafeRead =+ let intGrp = T.pack . show <$> ([1 .. 10] :: [Int])+ dblGrps =+ [ T.pack . show <$> ([11.0 .. 20.0] :: [Double])+ , T.pack . show <$> ([21.0 .. 30.0] :: [Double])+ , T.pack . show <$> ([31.0 .. 40.0] :: [Double])+ , T.pack . show <$> ([41.0 .. 50.0] :: [Double])+ ]+ beforeParse = intGrp ++ [""] ++ concatMap (\g -> g ++ [""]) dblGrps ++ doublesSpecialInput+ afterParse :: [Maybe Double]+ afterParse =+ map Just ([1.0 .. 10.0] :: [Double])+ ++ [Nothing]+ ++ concatMap+ (\g -> map Just g ++ [Nothing])+ [[11.0 .. 20.0], [21.0 .. 30.0], [31.0 .. 40.0], [41.0 .. 50.0]]+ ++ map Just doublesSpecial+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is on"+ expected+ actual+ )++parseDatesAndEmptyStringsWithSafeRead :: Test+parseDatesAndEmptyStringsWithSafeRead =+ let groups = chunksOf3 datesExpected+ beforeParse = concatMap (\g -> map (T.pack . show) g ++ [""]) groups+ afterParse :: [Maybe Day]+ afterParse = concatMap (\g -> map Just g ++ [Nothing]) groups+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead on"+ expected+ actual+ )+ where+ chunksOf3 [] = []+ chunksOf3 xs = take 3 xs : chunksOf3 (drop 3 xs)++parseTextsAndEmptyStringsWithSafeRead :: Test+parseTextsAndEmptyStringsWithSafeRead = TestCase $ do+ raw <- T.lines <$> TIO.readFile (typingDataDir <> "texts_with_empties.txt")+ let afterParse = map (\t -> if t == "" then Nothing else Just t) raw+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList raw+ assertEqual+ "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead on"+ expected+ actual++parseIntsAndNullishStringsWithSafeRead :: Test+parseIntsAndNullishStringsWithSafeRead =+ let beforeParse = replicate 5 "N/A" ++ intsInput+ afterParse :: [Maybe Int]+ afterParse = replicate 5 Nothing ++ map Just intsExpected+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints with nullish values as OptionalColumn of Ints, when safeRead is on"+ expected+ actual+ )++parseIntsAndDoublesAndNullishStringsWithSafeRead :: Test+parseIntsAndDoublesAndNullishStringsWithSafeRead =+ -- Note: this test uses different special doubles and nullish separators than the safeRead=False version+ let nullishSeps = ["Nothing", "N/A", "NULL", "null", "NAN"]+ intGrp = T.pack . show <$> ([1 .. 10] :: [Int])+ dblGrps =+ [ T.pack . show <$> ([11.0 .. 20.0] :: [Double])+ , T.pack . show <$> ([21.0 .. 30.0] :: [Double])+ , T.pack . show <$> ([31.0 .. 40.0] :: [Double])+ , T.pack . show <$> ([41.0 .. 50.0] :: [Double])+ ]+ beforeParse =+ intGrp+ ++ concat (zipWith (:) nullishSeps dblGrps)+ ++ [nullishSeps !! 4]+ ++ ["3.14", "2.22", "8.55", "23.3", "12.03"]+ afterParse :: [Maybe Double]+ afterParse =+ map Just ([1.0 .. 10.0] :: [Double])+ ++ [Nothing]+ ++ concatMap+ (\g -> map Just g ++ [Nothing])+ [[11.0 .. 20.0], [21.0 .. 30.0], [31.0 .. 40.0], [41.0 .. 50.0]]+ ++ map Just [3.14, 2.22, 8.55, 23.3, 12.03]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"+ expected+ actual+ )++parseIntsAndNullishAndEmptyStringsWithSafeRead :: Test+parseIntsAndNullishAndEmptyStringsWithSafeRead =+ -- safeRead=True: N/A, Nothing -> Nothing; "" -> Nothing+ let groups = [[1 .. 10], [11 .. 20], [21 .. 30], [31 .. 40], [41 .. 50]] :: [[Int]]+ beforeParse =+ ["N/A", "N/A", "N/A", "N/A", "Nothing"]+ ++ concatMap (\g -> "" : map (T.pack . show) g) groups+ ++ [""]+ afterParse :: [Maybe Int]+ afterParse =+ replicate 5 Nothing+ ++ concatMap (\g -> Nothing : map Just g) groups+ ++ [Nothing]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints with nullish values AND empty strings as OptionalColumn of Ints, when safeRead is on"+ expected+ actual+ )++parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead :: Test+parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead =+ -- Data: 4xN/A, "Nothing", "", then 10 ints (1-10), "", 10 ints (11-20), "", 10 ints (21-30), "", 3.14+ let beforeParse =+ ["N/A", "N/A", "N/A", "N/A", "Nothing", ""]+ ++ map (T.pack . show) ([1 .. 10] :: [Int])+ ++ [""]+ ++ map (T.pack . show) ([11 .. 20] :: [Int])+ ++ [""]+ ++ map (T.pack . show) ([21 .. 30] :: [Int])+ ++ ["", "3.14"]+ afterParse :: [Maybe Double]+ afterParse =+ replicate 6 Nothing+ ++ map Just ([1 .. 10] :: [Double])+ ++ [Nothing]+ ++ map Just ([11 .. 20] :: [Double])+ ++ [Nothing]+ ++ map Just ([21 .. 30] :: [Double])+ ++ [Nothing, Just 3.14]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints and Doubles with nullish values AND empty strings as OptionalColumn of Doubles, when safeRead is on"+ expected+ actual+ )++parseTextsAndEmptyAndNullishStringsWithSafeRead :: Test+parseTextsAndEmptyAndNullishStringsWithSafeRead = TestCase $ do+ raw <-+ T.lines <$> TIO.readFile (typingDataDir <> "texts_with_empties_and_nullish.txt")+ -- safeRead=True: empty strings and nullish tokens (NaN, Nothing, N/A) -> Nothing+ let isNullish t = t `elem` ["NaN", "Nothing", "N/A", "nan", "null", "NULL", "NA", "na", "NAN"]+ afterParse = map (\t -> if t == "" || isNullish t then Nothing else Just t) raw+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList raw+ assertEqual+ "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"+ expected+ actual++-- 3C. PARSING WITH SAFEREAD = EitherRead+--+-- EitherRead wraps every column as @Either Text a@: successful parses become+-- @Right v@; failures (including nullish/empty cells) become @Left <raw>@+-- preserving the original input verbatim.++parseBoolsWithEitherRead :: Test+parseBoolsWithEitherRead =+ let beforeParse :: [T.Text]+ beforeParse = ["true", "", "false", "N/A", "True"]+ afterParse :: [Either T.Text Bool]+ afterParse =+ [Right True, Left "", Right False, Left "N/A", Right True]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 5, D.parseSafe = D.EitherRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "EitherRead wraps Bools as Either Text Bool; failures carry raw text"+ expected+ actual+ )++parseIntsWithEitherRead :: Test+parseIntsWithEitherRead =+ -- Sample (first 5 rows) is cleanly Int; later rows exercise the Left path.+ let beforeParse :: [T.Text]+ beforeParse = ["1", "2", "3", "4", "42", "abc", "", "N/A"]+ afterParse :: [Either T.Text Int]+ afterParse =+ [ Right 1+ , Right 2+ , Right 3+ , Right 4+ , Right 42+ , Left "abc"+ , Left ""+ , Left "N/A"+ ]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 5, D.parseSafe = D.EitherRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "EitherRead wraps Ints as Either Text Int; failures carry raw text"+ expected+ actual+ )++parseDoublesWithEitherRead :: Test+parseDoublesWithEitherRead =+ -- Sample is cleanly Double; "oops" / "" come after and must surface as Left.+ let beforeParse :: [T.Text]+ beforeParse = ["1.5", "2.0", "3.5", "4.25", "3.14", "oops", ""]+ afterParse :: [Either T.Text Double]+ afterParse =+ [ Right 1.5+ , Right 2.0+ , Right 3.5+ , Right 4.25+ , Right 3.14+ , Left "oops"+ , Left ""+ ]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 5, D.parseSafe = D.EitherRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "EitherRead wraps Doubles as Either Text Double; failures carry raw text"+ expected+ actual+ )++parseTextsWithEitherRead :: Test+parseTextsWithEitherRead =+ let beforeParse :: [T.Text]+ beforeParse = ["hello", "", "world"]+ afterParse :: [Either T.Text T.Text]+ afterParse = [Right "hello", Left "", Right "world"]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 3, D.parseSafe = D.EitherRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "EitherRead wraps Texts as Either Text Text; empty cells become Left \"\""+ expected+ actual+ )++parseDatesWithEitherRead :: Test+parseDatesWithEitherRead =+ -- Sample is clean dates; "not-a-date" and "" come after and become Left.+ let beforeParse :: [T.Text]+ beforeParse =+ (T.pack . show <$> take 5 datesExpected) ++ ["not-a-date", ""]+ expectedGood :: [Either T.Text Day]+ expectedGood = Right <$> take 5 datesExpected+ expectedBad :: [Either T.Text Day]+ expectedBad = [Left "not-a-date", Left ""]+ expected = DI.fromVector $ V.fromList (expectedGood ++ expectedBad)+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 5, D.parseSafe = D.EitherRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "EitherRead wraps Dates as Either Text Day; failures carry raw text"+ expected+ actual+ )++-- parseDefaults honours per-column overrides: 'ages' stays strict Int,+-- 'notes' is wrapped as Either Text Text.+parseDefaultsPerColumnOverrides :: Test+parseDefaultsPerColumnOverrides = TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("ages", DI.fromList @T.Text ["1", "2", "3"])+ , ("notes", DI.fromList @T.Text ["hello", "", "world"])+ ]+ opts =+ D.defaultParseOptions+ { D.sampleSize = 3+ , D.parseSafe = D.MaybeRead+ , D.parseSafeOverrides =+ [ ("ages", D.NoSafeRead)+ , ("notes", D.EitherRead)+ ]+ }+ result = D.parseDefaults opts df+ case getColumn "ages" result of+ Just (DI.UnboxedColumn Nothing _) -> pure () -- strict Int+ Just col ->+ assertFailure $+ "'ages' should be bare UnboxedColumn, got "+ <> show col+ Nothing -> assertFailure "ages column missing"+ let expectedNotes =+ DI.fromList @(Either T.Text T.Text)+ [Right "hello", Left "", Right "world"]+ case getColumn "notes" result of+ Just col ->+ assertEqual+ "'notes' override EitherRead → Either Text Text"+ expectedNotes+ col+ Nothing -> assertFailure "notes column missing"++-- Default=NoSafeRead, single override to MaybeRead.+parseDefaultsNoSafeReadWithMaybeOverride :: Test+parseDefaultsNoSafeReadWithMaybeOverride = TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("x", DI.fromList @T.Text ["1", "2", "3"])+ , ("y", DI.fromList @T.Text ["a", "", "c"])+ ]+ opts =+ D.defaultParseOptions+ { D.sampleSize = 3+ , D.parseSafe = D.NoSafeRead+ , D.parseSafeOverrides = [("y", D.MaybeRead)]+ }+ result = D.parseDefaults opts df+ -- 'x': NoSafeRead default → bare Int, no bitmap.+ case getColumn "x" result of+ Just (DI.UnboxedColumn Nothing _) -> pure ()+ Just col ->+ assertFailure $+ "'x' should be bare UnboxedColumn, got " <> show col+ Nothing -> assertFailure "x column missing"+ -- 'y': MaybeRead override → nullable Text with bitmap.+ case getColumn "y" result of+ Just (DI.BoxedColumn (Just _) _) -> pure ()+ Just col ->+ assertFailure $+ "'y' MaybeRead override should yield nullable column, got " <> show col+ Nothing -> assertFailure "y column missing"++-- Default=EitherRead, single override to NoSafeRead.+parseDefaultsEitherReadWithNoSafeOverride :: Test+parseDefaultsEitherReadWithNoSafeOverride = TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("nums", DI.fromList @T.Text ["10", "20", "30"])+ , ("tags", DI.fromList @T.Text ["foo", "", "baz"])+ ]+ opts =+ D.defaultParseOptions+ { D.sampleSize = 3+ , D.parseSafe = D.EitherRead+ , D.parseSafeOverrides = [("nums", D.NoSafeRead)]+ }+ result = D.parseDefaults opts df+ -- 'nums': NoSafeRead override → bare Int.+ case getColumn "nums" result of+ Just (DI.UnboxedColumn Nothing _) -> pure ()+ Just col ->+ assertFailure $+ "'nums' NoSafeRead override should give bare UnboxedColumn, got "+ <> show col+ Nothing -> assertFailure "nums column missing"+ -- 'tags': default EitherRead → Either Text Text.+ let expectedTags =+ DI.fromList @(Either T.Text T.Text) [Right "foo", Left "", Right "baz"]+ case getColumn "tags" result of+ Just col ->+ assertEqual "'tags' inherits EitherRead default" expectedTags col+ Nothing -> assertFailure "tags column missing"++-- Empty overrides: behaves identically to no overrides.+parseDefaultsEmptyOverrides :: Test+parseDefaultsEmptyOverrides = TestCase $ do+ let df =+ D.fromNamedColumns+ [("v", DI.fromList @T.Text ["1", "2", "3"])]+ opts1 = D.defaultParseOptions{D.sampleSize = 3}+ opts2 = opts1{D.parseSafeOverrides = []}+ assertEqual+ "empty parseSafeOverrides ≡ no overrides"+ (D.parseDefaults opts1 df)+ (D.parseDefaults opts2 df)++-- Override for non-existent column: silently ignored, no crash.+parseDefaultsNonExistentOverride :: Test+parseDefaultsNonExistentOverride = TestCase $ do+ let df =+ D.fromNamedColumns+ [("a", DI.fromList @T.Text ["1", "2", "3"])]+ opts =+ D.defaultParseOptions+ { D.sampleSize = 3+ , D.parseSafeOverrides = [("z", D.EitherRead)]+ }+ result = D.parseDefaults opts df+ -- 'a' should parse normally under the default (MaybeRead).+ case getColumn "a" result of+ Just (DI.UnboxedColumn (Just _) _) -> pure ()+ Just col ->+ assertFailure $+ "'a' should be nullable UnboxedColumn under MaybeRead default, got "+ <> show col+ Nothing -> assertFailure "a column missing"++parseAllNullWithEitherRead :: Test+parseAllNullWithEitherRead =+ -- NoAssumption path: sample is all empty/nullish so inference can't+ -- pick a numeric type. EitherRead still wraps as Either Text Text.+ let beforeParse :: [T.Text]+ beforeParse = ["", "", "", "N/A", ""]+ afterParse :: [Either T.Text T.Text]+ afterParse = [Left "", Left "", Left "", Right "N/A", Left ""]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 5, D.parseSafe = D.EitherRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "EitherRead with all-null sample produces Either Text Text"+ expected+ actual+ )++-- 4. PARSING SHOULD NOT DEPEND ON THE NUMBER OF EXAMPLES.+parseBoolsWithOneExample :: Test+parseBoolsWithOneExample =+ let afterParse :: [Bool]+ afterParse = False : replicate 50 True+ beforeParse :: [T.Text]+ beforeParse = "false" : replicate 50 "true"+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Bools without missing values as OptionalColumn of Maybe Bools with only one example"+ expected+ actual+ )++parseBoolsWithManyExamples :: Test+parseBoolsWithManyExamples =+ let afterParse :: [Bool]+ afterParse = False : replicate 50 True+ beforeParse :: [T.Text]+ beforeParse = "false" : replicate 50 "true"+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 49}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Bools without missing values as OptionalColumn of Maybe Bools with only one example"+ expected+ actual+ )++parseIntsWithOneExample :: Test+parseIntsWithOneExample =+ let afterParse = intsExpected+ beforeParse = intsInput+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints without missing values as OptionalColumn of Maybe Ints with only one example"+ expected+ actual+ )++parseIntsWithTwentyFiveExamples :: Test+parseIntsWithTwentyFiveExamples =+ let afterParse = intsExpected+ beforeParse = intsInput+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 25}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints without missing values as OptionalColumn of Maybe Ints with some examples"+ expected+ actual+ )++parseIntsWithFortyNineExamples :: Test+parseIntsWithFortyNineExamples =+ let afterParse = intsExpected+ beforeParse = intsInput+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 49}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints without missing values as OptionalColumn of Maybe Ints with many examples"+ expected+ actual+ )++parseDatesWithOneExample :: Test+parseDatesWithOneExample =+ let afterParse = datesExpected+ beforeParse = datesInput+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Dates without missing values as OptionalColumn of Maybe Days with only one example"+ expected+ actual+ )++parseDatesWithFifteenExamples :: Test+parseDatesWithFifteenExamples =+ let afterParse = datesExpected+ beforeParse = datesInput+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 15}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Dates without missing values as OptionalColumn of Maybe Days with many examples"+ expected+ actual+ )++parseIntsAndDoublesAsDoublesWithOneExample :: Test+parseIntsAndDoublesAsDoublesWithOneExample =+ let afterParse = intsAndDoublesExpected+ beforeParse = intsAndDoublesInput+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Ints and Doubles as OptionalColumn of Maybe Doubles with just one example"+ expected+ actual+ )++parseIntsAndDoublesAsDoublesWithManyExamples :: Test+parseIntsAndDoublesAsDoublesWithManyExamples =+ let afterParse = intsAndDoublesExpected+ beforeParse = intsAndDoublesInput+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 50}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Ints and Doubles as OptionalColumn of Maybe Doubles with many examples"+ expected+ actual+ )++parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff :: Test+parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff =+ -- Pattern: 10 empties, 50 ints (as int text), 50 doubles (as double text), 5 specials+ let beforeParse =+ replicate 10 ""+ ++ intsInput+ ++ (T.pack . show <$> ([1.0 .. 50.0] :: [Double]))+ ++ doublesSpecialInput+ afterParse :: [Maybe Double]+ afterParse =+ replicate 10 Nothing+ ++ map Just ([1.0 .. 50.0] :: [Double])+ ++ map Just ([1.0 .. 50.0] :: [Double])+ ++ map Just doublesSpecial+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 1, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"+ expected+ actual+ )++parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff ::+ Test+parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff =+ let beforeParse =+ replicate 10 ""+ ++ intsInput+ ++ (T.pack . show <$> ([1.0 .. 50.0] :: [Double]))+ ++ doublesSpecialInput+ afterParse :: [Maybe Double]+ afterParse =+ replicate 10 Nothing+ ++ map Just ([1.0 .. 50.0] :: [Double])+ ++ map Just ([1.0 .. 50.0] :: [Double])+ ++ map Just doublesSpecial+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 30, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"+ expected+ actual+ )++parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff ::+ Test+parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff =+ -- Pattern: 10 empties, 5x(NaN,N/A), 20 ints (1-20), 30 doubles (1.0-30.0), 5 specials+ let nullishPairs = concat (replicate 5 ["NaN", "N/A"])+ beforeParse =+ replicate 10 ""+ ++ nullishPairs+ ++ (T.pack . show <$> ([1 .. 20] :: [Int]))+ ++ (T.pack . show <$> ([1.0 .. 30.0] :: [Double]))+ ++ doublesSpecialInput+ afterParse :: [Maybe T.Text]+ afterParse =+ replicate 10 Nothing+ ++ map Just nullishPairs+ ++ map (Just . T.pack . show) ([1 .. 20] :: [Int])+ ++ map (Just . T.pack . show) ([1.0 .. 30.0] :: [Double])+ ++ map Just doublesSpecialInput+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 1, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Ints, Doubles, empty strings, nullish as OptionalColumn of Text with just one example, when safeRead is off"+ expected+ actual+ )++parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff ::+ Test+parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff =+ let nullishPairs = concat (replicate 5 ["NaN", "N/A"])+ beforeParse =+ replicate 10 ""+ ++ nullishPairs+ ++ (T.pack . show <$> ([1 .. 20] :: [Int]))+ ++ (T.pack . show <$> ([1.0 .. 30.0] :: [Double]))+ ++ doublesSpecialInput+ afterParse :: [Maybe T.Text]+ afterParse =+ replicate 10 Nothing+ ++ map Just nullishPairs+ ++ map (Just . T.pack . show) ([1 .. 20] :: [Int])+ ++ map (Just . T.pack . show) ([1.0 .. 30.0] :: [Double])+ ++ map Just doublesSpecialInput+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault+ (D.defaultParseOptions{D.sampleSize = 30, D.parseSafe = D.NoSafeRead})+ $ DI.fromVector+ $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Ints, Doubles, empty strings, nullish as OptionalColumn of Text with many examples, when safeRead is off"+ expected+ actual+ )++-- 5. EDGE CASES THAT HAVE TO BE INTERPRETED CORRECTLY++parseManyNullishAndOneInt :: Test+parseManyNullishAndOneInt =+ let afterParse :: [Maybe Int]+ afterParse = replicate 100 Nothing ++ [Just 100000]+ beforeParse :: [T.Text]+ beforeParse = replicate 100 "NaN" ++ ["100000"]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)++parseManyNullishAndOneDouble :: Test+parseManyNullishAndOneDouble =+ let afterParse :: [Maybe Double]+ afterParse = replicate 100 Nothing ++ [Just 3.14]+ beforeParse :: [T.Text]+ beforeParse = replicate 100 "NaN" ++ ["3.14"]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)++parseManyNullishAndOneDate :: Test+parseManyNullishAndOneDate =+ let afterParse :: [Maybe Day]+ afterParse = replicate 100 Nothing ++ [Just $ fromGregorian 2024 12 25]+ beforeParse :: [T.Text]+ beforeParse = replicate 100 "NaN" ++ ["2024-12-25"]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)++parseManyNullishAndIncorrectDates :: Test+parseManyNullishAndIncorrectDates =+ let afterParse :: [Maybe T.Text]+ afterParse = replicate 100 Nothing ++ [Just "2024-12-25", Just "2024-12-w6"]+ beforeParse :: [T.Text]+ beforeParse = replicate 100 "NaN" ++ ["2024-12-25", "2024-12-w6"]+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)++parseRepeatedNullish :: Test+parseRepeatedNullish =+ let afterParse :: [Maybe T.Text]+ afterParse = replicate 100 Nothing+ beforeParse :: [T.Text]+ beforeParse = replicate 100 "NaN"+ expected = DI.fromVector $ V.fromList afterParse+ actual =+ D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+ DI.fromVector $+ V.fromList beforeParse+ in TestCase+ (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)++tests :: [Test]+tests =+ dimensionsTest+ ++ [+ -- 1. SIMPLE CASES+ TestLabel "parseBools" parseBools+ , TestLabel "parseInts" parseInts+ , TestLabel "parseDoubles" parseDoubles+ , TestLabel "parseDates" parseDates+ , TestLabel "parseTexts" parseTexts+ , -- 2. COMBINATION CASES+ TestLabel "parseBoolsAndIntsAsTexts" parseBoolsAndIntsAsTexts+ , TestLabel "parseIntsAndDoublesAsDoubles" parseIntsAndDoublesAsDoubles+ , TestLabel "parseIntsAndDatesAsTexts" parseIntsAndDatesAsTexts+ , TestLabel "parseTextsAndDoublesAsTexts" parseTextsAndDoublesAsTexts+ , TestLabel "parseDatesAndTextsAsTexts" parseDatesAndTextsAsTexts+ , -- 3A. PARSING WITH SAFEREAD OFF+ TestLabel "parseBoolsWithoutSafeRead" parseBoolsWithoutSafeRead+ , TestLabel "parseIntsWithoutSafeRead" parseIntsWithoutSafeRead+ , TestLabel "parseDoublesWithoutSafeRead" parseDoublesWithoutSafeRead+ , TestLabel "parseDatesWithoutSafeRead" parseDatesWithoutSafeRead+ , TestLabel "parseTextsWithoutSafeRead" parseTextsWithoutSafeRead+ , TestLabel+ "parseBoolsAndEmptyStringsWithoutSafeRead"+ parseBoolsAndEmptyStringsWithoutSafeRead+ , TestLabel+ "parseIntsAndEmptyStringsWithoutSafeRead"+ parseIntsAndEmptyStringsWithoutSafeRead+ , TestLabel+ "parseIntsAndDoublesAndEmptyStringsWithoutSafeRead"+ parseIntsAndDoublesAndEmptyStringsWithoutSafeRead+ , TestLabel+ "parseDatesAndEmptyStringsWithoutSafeRead"+ parseDatesAndEmptyStringsWithoutSafeRead+ , TestLabel+ "parseTextsAndEmptyStringsWithoutSafeRead"+ parseTextsAndEmptyStringsWithoutSafeRead+ , TestLabel+ "parseBoolsAndNullishStringsWithoutSafeRead"+ parseBoolsAndNullishStringsWithoutSafeRead+ , TestLabel+ "parseIntsAndNullishStringsWithoutSafeRead"+ parseIntsAndNullishStringsWithoutSafeRead+ , TestLabel+ "parseIntsAndDoublesAndNullishStringsWithoutSafeRead"+ parseIntsAndDoublesAndNullishStringsWithoutSafeRead+ , TestLabel+ "parseIntsAndNullishAndEmptyStringsWithoutSafeRead"+ parseIntsAndNullishAndEmptyStringsWithoutSafeRead+ , TestLabel+ "parseTextsAndEmptyAndNullishStringsWithoutSafeRead"+ parseTextsAndEmptyAndNullishStringsWithoutSafeRead+ , -- 3B. PARSING WITH SAFEREAD ON+ TestLabel+ "parseBoolsAndEmptyStringsWithSafeRead"+ parseBoolsAndEmptyStringsWithSafeRead+ , TestLabel+ "parseIntsAndEmptyStringsWithSafeRead"+ parseIntsAndEmptyStringsWithSafeRead+ , TestLabel+ "parseIntsAndDoublesAndEmptyStringsWithSafeRead"+ parseIntsAndDoublesAndEmptyStringsWithSafeRead+ , TestLabel+ "parseDatesAndEmptyStringsWithSafeRead"+ parseDatesAndEmptyStringsWithSafeRead+ , TestLabel+ "parseTextsAndEmptyStringsWithSafeRead"+ parseTextsAndEmptyStringsWithSafeRead+ , TestLabel+ "parseIntsAndNullishStringsWithSafeRead"+ parseIntsAndNullishStringsWithSafeRead+ , TestLabel+ "parseIntsAndDoublesAndNullishStringsWithSafeRead"+ parseIntsAndDoublesAndNullishStringsWithSafeRead+ , TestLabel+ "parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead"+ parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead+ , TestLabel+ "parseIntsAndNullishAndEmptyStringsWithSafeRead"+ parseIntsAndNullishAndEmptyStringsWithSafeRead+ , TestLabel+ "parseTextsAndEmptyAndNullishStringsWithSafeRead"+ parseTextsAndEmptyAndNullishStringsWithSafeRead+ , -- 3C. PARSING WITH EitherRead+ TestLabel "parseBoolsWithEitherRead" parseBoolsWithEitherRead+ , TestLabel "parseIntsWithEitherRead" parseIntsWithEitherRead+ , TestLabel "parseDoublesWithEitherRead" parseDoublesWithEitherRead+ , TestLabel "parseTextsWithEitherRead" parseTextsWithEitherRead+ , TestLabel "parseDatesWithEitherRead" parseDatesWithEitherRead+ , TestLabel "parseAllNullWithEitherRead" parseAllNullWithEitherRead+ , TestLabel+ "parseDefaultsPerColumnOverrides"+ parseDefaultsPerColumnOverrides+ , TestLabel+ "parseDefaultsNoSafeReadWithMaybeOverride"+ parseDefaultsNoSafeReadWithMaybeOverride+ , TestLabel+ "parseDefaultsEitherReadWithNoSafeOverride"+ parseDefaultsEitherReadWithNoSafeOverride+ , TestLabel+ "parseDefaultsEmptyOverrides"+ parseDefaultsEmptyOverrides+ , TestLabel+ "parseDefaultsNonExistentOverride"+ parseDefaultsNonExistentOverride , -- 4. PARSING MUST NOT DEPEND ON THE NUMBER OF EXAMPLES TestLabel "parseBoolsWithOneExample" parseBoolsWithOneExample , TestLabel "parseBoolsWithManyExamples" parseBoolsWithManyExamples
+ tests/Operations/VectorKernel.hs view
@@ -0,0 +1,136 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | The vectorized scatter-accumulate kernel parity gate. Every case asserts+that 'D.aggregate' (which routes recognised reductions through the fast scatter+kernel) produces a result byte-identical to the same aggregation forced through+the expression interpreter ('interpretOnly'). Covered: sum/min/max/count over+Int and Double, mean, stddev/variance, top2Sum, median, the @max - min@+compound (Q7), and a mixed multi-aggregation over single and multi-key+groupings, on Int/Double/nullable/Text-key grids and sizes straddling the+parallel threshold.++Floating-point sums computed by the scatter follow the same left-to-right+group-order fold as the interpreter, so the two paths agree bit-for-bit here.+The PARALLEL kernel ('DataFrame.Internal.AggKernelPar') splits the work by+disjoint group-id range, and because each group's rows stay in their original+@valueIndices@ order within one worker's range, the per-group fold order is+unchanged from the sequential scatter — so the parallel path is also+byte-identical to the interpreter, asserted here at sizes above the 200k+parallel threshold (the test suite runs @-with-rtsopts=-N@, so those cases+exercise the multi-capability scatter).+-}+module Operations.VectorKernel where++import Control.Exception (throw)+import qualified Data.List as L+import qualified Data.Text as T+import qualified Data.Vector as V++import qualified DataFrame as D+import DataFrame.Errors (DataFrameException (..))+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.DataFrame (GroupedDataFrame, insertColumn)+import DataFrame.Internal.Expression (+ AggStrategy (..),+ Expr (..),+ UExpr (..),+ )+import DataFrame.Internal.Interpreter (+ AggregationResult (..),+ interpretAggregation,+ )+import qualified DataFrame.Operations.Aggregation as DA++import Assertions ()+import Test.HUnit++-- | Aggregate forcing every expression through the interpreter (no kernel).+interpretOnly :: [(T.Text, UExpr)] -> GroupedDataFrame -> D.DataFrame+interpretOnly aggs gdf =+ let base = DA.aggregate [] gdf+ f :: (T.Text, UExpr) -> D.DataFrame -> D.DataFrame+ f (name, UExpr (expr :: Expr a)) d =+ let value :: DI.Column+ value = case interpretAggregation @a gdf expr of+ Left e -> throw e+ Right (UnAggregated _) -> throw (UnaggregatedException (T.pack (show expr)))+ Right (Aggregated (DI.TColumn col)) -> col+ in insertColumn name value d+ in foldl (flip f) base aggs++-- | Per-group sum of the two largest values (the benchmark's Q8 reduction).+top2Sum :: Expr Double -> Expr Double+top2Sum = Agg (CollectAgg "top2Sum" f)+ where+ f :: V.Vector Double -> Double+ f v = sum (take 2 (L.sortBy (flip compare) (V.toList v)))++grid :: Int -> D.DataFrame+grid n =+ D.fromNamedColumns+ [ ("ki", DI.fromList [i `mod` 17 | i <- [0 .. n - 1]])+ , ("kt", DI.fromList [T.pack ('g' : show (i `mod` 11)) | i <- [0 .. n - 1]])+ , -- A high-cardinality key so the parallel group-range split sees many+ -- small groups (alongside the low-cardinality 'ki'/'kt' few-big-groups).+ ("kh", DI.fromList [(i * 2654435761) `mod` 50021 :: Int | i <- [0 .. n - 1]])+ , ("vi", DI.fromList [(i * 31 + 7) `mod` 1000 - 500 :: Int | i <- [0 .. n - 1]])+ , ("vj", DI.fromList [(i * 13) `mod` 97 :: Int | i <- [0 .. n - 1]])+ ,+ ( "vd"+ , DI.fromList [fromIntegral ((i * 7) `mod` 211) / 3 :: Double | i <- [0 .. n - 1]]+ )+ ]++vi, vj :: Expr Int+vi = F.col @Int "vi"+vj = F.col @Int "vj"++vd :: Expr Double+vd = F.col @Double "vd"++aggSets :: [[(T.Text, UExpr)]]+aggSets =+ [ ["s" F..= F.sum vi]+ , ["s" F..= F.sum vd, "c" F..= F.count vi]+ , ["mn" F..= F.minimum vi, "mx" F..= F.maximum vi]+ , ["mnd" F..= F.minimum vd, "mxd" F..= F.maximum vd]+ , ["m" F..= F.mean vi, "md" F..= F.mean vd]+ , ["sd" F..= F.stddev vd, "var" F..= F.variance vd]+ , ["t2" F..= top2Sum vd]+ , ["med" F..= F.median vd]+ , ["diff" F..= (F.maximum vi - F.minimum vj)]+ ,+ [ "s" F..= F.sum vi+ , "m" F..= F.mean vd+ , "sd" F..= F.stddev vd+ , "med" F..= F.median vd+ , "c" F..= F.count vj+ , "diff" F..= (F.maximum vi - F.minimum vj)+ ]+ ]++keySets :: [[T.Text]]+keySets = [["ki"], ["kt"], ["ki", "kt"], ["kh"]]++parityCase :: Int -> [T.Text] -> [(T.Text, UExpr)] -> Test+parityCase n keys aggs =+ TestCase $+ let df = grid n+ gdf = D.groupBy keys df+ fast = D.aggregate aggs gdf+ ref = interpretOnly aggs gdf+ label = "n=" ++ show n ++ " keys=" ++ show keys ++ " #aggs=" ++ show (length aggs)+ in assertEqual ("kernel==interpreter " ++ label) ref fast++tests :: [Test]+tests =+ [ TestLabel "vectorKernelParity" (parityCase n keys aggs)+ | -- 200001 and 500000 straddle and clear the parallel threshold, so the+ -- parallel scatter kernel is the one validated against the interpreter.+ n <- [1, 7, 5000, 200001, 500000]+ , keys <- keySets+ , aggs <- aggSets+ ]
+ tests/Operations/Window.hs view
@@ -0,0 +1,247 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Operations.Window where++import qualified Data.Text as T+import qualified DataFrame as D+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Internal.DataFrame as DI++import Data.Function ((&))+import DataFrame.Operators+import Test.HUnit++-- Similar to example discussed in https://www.sumsar.net/blog/pandas-feels-clunky-when-coming-from-r/+purchases :: D.DataFrame+purchases =+ D.fromNamedColumns+ [+ ( "country"+ , DI.fromList+ (["US", "US", "US", "UK", "UK", "UK", "France", "France", "France"] :: [T.Text])+ )+ , ("amount", DI.fromList ([100, 200, 5000, 50, 60, 800, 30, 40, 35] :: [Double]))+ , ("discount", DI.fromList ([5, 10, 0, 2, 3, 0, 1, 2, 1] :: [Double]))+ ]++globalFilterTest :: Test+globalFilterTest =+ TestCase+ ( assertEqual+ "Global median filter removes global outliers"+ 7+ ( purchases+ & D.filterWhere+ (F.col @Double "amount" .<=. F.median (F.col @Double "amount") * 10)+ & D.nRows+ )+ )++overMedianFilterTest :: Test+overMedianFilterTest =+ TestCase+ ( assertEqual+ "Per-group median filter via over removes per-country outliers"+ expected+ actual+ )+ where+ actual =+ purchases+ & D.filterWhere+ ( F.col @Double "amount"+ .<=. F.over ["country"] (F.median (F.col @Double "amount"))+ * 10+ )+ & D.sortBy [D.Asc (F.col @T.Text "country"), D.Asc (F.col @Double "amount")]+ expected =+ D.fromNamedColumns+ [+ ( "country"+ , DI.fromList (["France", "France", "France", "UK", "UK", "US", "US"] :: [T.Text])+ )+ , ("amount", DI.fromList ([30, 35, 40, 50, 60, 100, 200] :: [Double]))+ , ("discount", DI.fromList ([1, 1, 2, 2, 3, 5, 10] :: [Double]))+ ]++globalVsOverDifferentResults :: Test+globalVsOverDifferentResults =+ TestCase+ ( assertBool+ "Global filter and per-group over filter should produce different DataFrames"+ (D.nRows globalResult /= D.nRows overResult)+ )+ where+ globalResult =+ purchases+ & D.filterWhere+ (F.col @Double "amount" .<=. F.median (F.col @Double "amount") * 3)+ & D.sortBy [D.Asc (F.col @T.Text "country"), D.Asc (F.col @Double "amount")]++ overResult =+ purchases+ & D.filterWhere+ ( F.col @Double "amount"+ .<=. F.over ["country"] (F.median (F.col @Double "amount"))+ * 3+ )+ & D.sortBy [D.Asc (F.col @T.Text "country"), D.Asc (F.col @Double "amount")]++overMeanDeriveTest :: Test+overMeanDeriveTest =+ TestCase+ ( assertEqual+ "over with mean broadcasts per-group averages to all rows"+ expectedMeans+ actualMeans+ )+ where+ simpleData =+ D.fromNamedColumns+ [ ("group", DI.fromList (["A", "A", "B", "B", "B"] :: [T.Text]))+ , ("value", DI.fromList ([10, 20, 30, 60, 90] :: [Double]))+ ]+ result =+ simpleData+ & D.derive "group_mean" (F.over ["group"] (F.mean (F.col @Double "value")))+ & D.sortBy [D.Asc (F.col @T.Text "group"), D.Asc (F.col @Double "value")]+ -- A mean = 15.0, B mean = 60.0+ expectedMeans = [15.0, 15.0, 60.0, 60.0, 60.0] :: [Double]+ actualMeans = case DI.getColumn "group_mean" result of+ Nothing -> error "group_mean column not found"+ Just c -> DI.toList @Double c++overSumTest :: Test+overSumTest =+ TestCase+ ( assertEqual+ "over with sum broadcasts per-group sums"+ expectedSums+ actualSums+ )+ where+ simpleData =+ D.fromNamedColumns+ [ ("group", DI.fromList (["X", "X", "Y", "Y"] :: [T.Text]))+ , ("value", DI.fromList ([10, 20, 100, 200] :: [Int]))+ ]+ result =+ simpleData+ & D.derive "group_sum" (F.over ["group"] (F.sum (F.col @Int "value")))+ & D.sortBy [D.Asc (F.col @T.Text "group"), D.Asc (F.col @Int "value")]+ expectedSums = [30, 30, 300, 300] :: [Int]+ actualSums = case DI.getColumn "group_sum" result of+ Nothing -> error "group_sum column not found"+ Just c -> DI.toList @Int c++overCountTest :: Test+overCountTest =+ TestCase+ ( assertEqual+ "over with count broadcasts per-group counts"+ expectedCounts+ actualCounts+ )+ where+ simpleData =+ D.fromNamedColumns+ [ ("group", DI.fromList (["A", "A", "A", "B", "B"] :: [T.Text]))+ , ("value", DI.fromList ([1, 2, 3, 4, 5] :: [Int]))+ ]+ result =+ simpleData+ & D.derive "group_count" (F.over ["group"] (F.count (F.col @Int "value")))+ & D.sortBy [D.Asc (F.col @T.Text "group"), D.Asc (F.col @Int "value")]+ expectedCounts = [3, 3, 3, 2, 2] :: [Int]+ actualCounts = case DI.getColumn "group_count" result of+ Nothing -> error "group_count column not found"+ Just c -> DI.toList @Int c++mixedGlobalAndOverTest :: Test+mixedGlobalAndOverTest =+ TestCase+ ( assertEqual+ "Can mix over (per-group) and global expressions"+ expectedDeviations+ actualDeviations+ )+ where+ simpleData =+ D.fromNamedColumns+ [ ("group", DI.fromList (["A", "A", "B", "B"] :: [T.Text]))+ , ("value", DI.fromList ([10.0, 20.0, 100.0, 200.0] :: [Double]))+ ]+ result =+ simpleData+ & D.derive+ "deviation"+ (F.col @Double "value" - F.over ["group"] (F.mean (F.col @Double "value")))+ & D.sortBy [D.Asc (F.col @T.Text "group"), D.Asc (F.col @Double "value")]+ expectedDeviations = [-5.0, 5.0, -50.0, 50.0] :: [Double]+ actualDeviations = case DI.getColumn "deviation" result of+ Nothing -> error "deviation column not found"+ Just c -> DI.toList @Double c++-- | Example from https://www.sumsar.net/blog/pandas-feels-clunky-when-coming-from-r/+blogPostExampleGlobal :: Test+blogPostExampleGlobal =+ TestCase+ ( assertEqual+ "Blog post example: global filter then group+aggregate"+ expected+ actual+ )+ where+ amount = F.col @Double "amount"+ discount = F.col @Double "discount"+ actual =+ purchases+ & D.filterWhere (amount .<=. F.median amount * 10)+ & D.groupBy ["country"]+ & D.aggregate [F.sum (amount - discount) `as` "total"]+ & D.sortBy [D.Asc (F.col @T.Text "country")]+ -- Global median = 60, threshold = 600 → removes 5000 and 800+ -- France: (30-1)+(40-2)+(35-1) = 101, UK: (50-2)+(60-3) = 105, US: (100-5)+(200-10) = 285+ expected =+ D.fromNamedColumns+ [ ("country", DI.fromList (["France", "UK", "US"] :: [T.Text]))+ , ("total", DI.fromList ([101, 105, 285] :: [Double]))+ ]++blogPostExampleOver :: Test+blogPostExampleOver =+ TestCase+ ( assertEqual+ "Blog post example: per-group filter (via over) then group+aggregate"+ expected+ actual+ )+ where+ amount = F.col @Double "amount"+ discount = F.col @Double "discount"+ actual =+ purchases+ & D.filterWhere (amount .<=. F.over ["country"] (F.median amount) * 10)+ & D.groupBy ["country"]+ & D.aggregate [F.sum (amount - discount) `as` "total"]+ & D.sortBy [D.Asc (F.col @T.Text "country")]+ expected =+ D.fromNamedColumns+ [ ("country", DI.fromList (["France", "UK", "US"] :: [T.Text]))+ , ("total", DI.fromList ([101, 105, 285] :: [Double]))+ ]++tests :: [Test]+tests =+ [ TestLabel "globalFilterTest" globalFilterTest+ , TestLabel "overMedianFilterTest" overMedianFilterTest+ , TestLabel "globalVsOverDifferentResults" globalVsOverDifferentResults+ , TestLabel "overMeanDeriveTest" overMeanDeriveTest+ , TestLabel "overSumTest" overSumTest+ , TestLabel "overCountTest" overCountTest+ , TestLabel "mixedGlobalAndOverTest" mixedGlobalAndOverTest+ , TestLabel "blogPostExampleGlobal" blogPostExampleGlobal+ , TestLabel "blogPostExampleOver" blogPostExampleOver+ ]
+ tests/Operations/WriteCsv.hs view
@@ -0,0 +1,88 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Operations.WriteCsv where++import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import qualified DataFrame as D+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.DataFrame (DataFrame (..), toCsv, toSeparated)+import Test.HUnit++-- Basic test: Int and Text columns produce correct CSV+toCsvBasic :: Test+toCsvBasic = TestLabel "toCsv_basic" $ TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("name", DI.fromList @T.Text ["Alice", "Bob", "Charlie"])+ , ("age", DI.fromList @Int [30, 25, 35])+ ]+ expected = "name,age\nAlice,30\nBob,25\nCharlie,35\n"+ assertEqual "basic toCsv" expected (toCsv df)++-- Empty DataFrame produces empty text+toCsvEmpty :: Test+toCsvEmpty =+ TestLabel "toCsv_empty" $+ TestCase $+ assertEqual "empty toCsv" T.empty (toCsv D.empty)++-- toSeparated with tab produces tab-delimited output+toSeparatedTab :: Test+toSeparatedTab = TestLabel "toSeparated_tab" $ TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("x", DI.fromList @Int [1, 2])+ , ("y", DI.fromList @Int [3, 4])+ ]+ expected = "x\ty\n1\t3\n2\t4\n"+ assertEqual "tab separated" expected (toSeparated '\t' df)++-- Double values render correctly+toCsvDouble :: Test+toCsvDouble = TestLabel "toCsv_double" $ TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("value", DI.fromList @Double [1.5, 2.0, 2.5])+ ]+ expected = "value\n1.5\n2.0\n2.5\n"+ assertEqual "double toCsv" expected (toCsv df)++-- Single column DataFrame+toCsvSingleColumn :: Test+toCsvSingleColumn = TestLabel "toCsv_single_column" $ TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("id", DI.fromList @Int [10, 20, 30])+ ]+ expected = "id\n10\n20\n30\n"+ assertEqual "single column toCsv" expected (toCsv df)++-- Round trip: toCsv then readCsv preserves data+toCsvRoundTrip :: Test+toCsvRoundTrip = TestLabel "toCsv_roundTrip" $ TestCase $ do+ let df =+ D.fromNamedColumns+ [ ("a", DI.fromList @Int [1, 2, 3])+ , ("b", DI.fromList @T.Text ["hello", "world", "test"])+ ]+ let csvText = toCsv df+ let tmpPath = "/tmp/dataframe_test_toCsv_roundtrip.csv"+ TIO.writeFile tmpPath csvText+ df' <- D.readCsv tmpPath+ assertEqual+ "round trip dimensions"+ (dataframeDimensions df)+ (dataframeDimensions df')+ assertEqual "round trip data" df df'++tests :: [Test]+tests =+ [ toCsvBasic+ , toCsvEmpty+ , toSeparatedTab+ , toCsvDouble+ , toCsvSingleColumn+ , toCsvRoundTrip+ ]
+ tests/PackedTextMain.hs view
@@ -0,0 +1,12 @@+module Main (main) where++import qualified Internal.PackedText as PackedText+import qualified System.Exit as Exit+import Test.HUnit++main :: IO ()+main = do+ result <- runTestTT (TestList PackedText.tests)+ if failures result > 0 || errors result > 0+ then Exit.exitFailure+ else Exit.exitSuccess
+ tests/PackedTextMigration.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Regression tests for the PackedText migration: a string column stored as a+'DI.PackedText' (the form the CSV readers emit) must flow through the+decision-tree feature/target extraction and the full-outer-join key coalescing+without crashing, and must behave IDENTICALLY to the same data held as a boxed+@Text@ column. If a non-exhaustive @Column@ match regresses, the packed cases+either crash (test error) or diverge from the boxed baseline (assertion fails).+-}+module PackedTextMigration (tests) where++import Control.Monad (zipWithM_)+import qualified Data.ByteString as B+import qualified Data.Text as T+import qualified Data.Text.Array as A+import Data.Text.Encoding (encodeUtf8)+import qualified Data.Vector.Unboxed as VU+import Data.Word (Word8)++import qualified DataFrame as D+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.PackedText (mkPackedContiguous)++import DataFrame.DecisionTree (defaultTreeConfig)+import DataFrame.DecisionTree.Model ()+import DataFrame.Model (fit, predict)+import DataFrame.Operations.Join (fullOuterJoin)++import Test.HUnit++{- | Build a 'DI.PackedText' column directly from raw UTF-8 bytes + offsets,+exactly as the CSV readers do (mirrors @Internal.PackedText.packedFromTexts@).+-}+packedFromTexts :: [T.Text] -> DI.Column+packedFromTexts ts =+ let bytess = map (B.unpack . encodeUtf8) ts+ flat = concat bytess+ offs = scanl (+) 0 (map length bytess)+ in DI.PackedText+ Nothing+ (mkPackedContiguous (arrayFromBytes flat) (VU.fromList offs))++arrayFromBytes :: [Word8] -> A.Array+arrayFromBytes ws = A.run $ do+ m <- A.new (length ws)+ zipWithM_ (A.unsafeWrite m) [0 ..] ws+ pure m++-- | The two column encodings of the same text data.+packed, boxed :: [T.Text] -> DI.Column+packed = packedFromTexts+boxed = DI.fromList++{- | A decision tree using a TEXT FEATURE (exercises Cart.featuresOfColumn and the+categorical condition builders). The packed-feature tree must equal the+boxed-feature tree, proving the feature is used identically — not dropped or+crashed on.+-}+treeOnTextFeature :: Test+treeOnTextFeature = TestCase $ do+ let df mk =+ D.fromNamedColumns+ [ ("color", mk ["red", "red", "blue", "blue", "green", "green"])+ , ("y", DI.fromList ([0, 0, 1, 1, 2, 2] :: [Double]))+ ]+ packedTree = fit defaultTreeConfig (D.col @Double "y") (df packed)+ boxedTree = fit defaultTreeConfig (D.col @Double "y") (df boxed)+ assertEqual+ "tree on packed-text feature == tree on boxed-text feature"+ (D.prettyPrint (predict boxedTree))+ (D.prettyPrint (predict packedTree))++{- | A classifier whose TARGET is a packed-text column (exercises+Cart.cartTargetLabels). Packed target must give the same tree as a boxed one.+-}+classifierOnTextTarget :: Test+classifierOnTextTarget = TestCase $ do+ let df mk =+ D.fromNamedColumns+ [ ("x", DI.fromList ([1, 2, 3, 4, 5, 6] :: [Double]))+ , ("label", mk ["a", "a", "a", "b", "b", "b"])+ ]+ packedClf = fit defaultTreeConfig (D.col @T.Text "label") (df packed)+ boxedClf = fit defaultTreeConfig (D.col @T.Text "label") (df boxed)+ assertEqual+ "classifier with packed-text target == boxed-text target"+ (D.prettyPrint (predict boxedClf))+ (D.prettyPrint (predict packedClf))++{- | A full outer join on a packed-text KEY column (exercises the+coalesceKeyColumn path that previously errored on PackedText). Result must+match the boxed-key join and recover all four distinct keys.+-}+fullOuterJoinOnTextKey :: Test+fullOuterJoinOnTextKey = TestCase $ do+ let l mk =+ D.fromNamedColumns+ [("k", mk ["a", "b", "c"]), ("lv", DI.fromList ([1, 2, 3] :: [Double]))]+ r mk =+ D.fromNamedColumns+ [("k", mk ["b", "c", "d"]), ("rv", DI.fromList ([20, 30, 40] :: [Double]))]+ packedJoin = fullOuterJoin ["k"] (l packed) (r packed)+ boxedJoin = fullOuterJoin ["k"] (l boxed) (r boxed)+ assertEqual+ "full outer join on packed key recovers all 4 keys"+ 4+ (fst (D.dimensions packedJoin))+ assertEqual+ "packed-key join row count == boxed-key join row count"+ (fst (D.dimensions boxedJoin))+ (fst (D.dimensions packedJoin))++tests :: [Test]+tests = [treeOnTextFeature, classifierOnTextTarget, fullOuterJoinOnTextKey]
tests/Parquet.hs view
@@ -1,1701 +1,1433 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TypeApplications #-}--module Parquet where--import Assertions (assertExpectException)-import qualified DataFrame as D-import qualified DataFrame.Functions as F--import Data.Int-import Data.Text (Text)-import Data.Time-import GHC.IO (unsafePerformIO)-import Test.HUnit--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{utctDay = fromGregorian 2009 3 1, utctDayTime = secondsToDiffTime 0}- , UTCTime{utctDay = fromGregorian 2009 3 1, utctDayTime = secondsToDiffTime 60}- , UTCTime{utctDay = fromGregorian 2009 4 1, utctDayTime = secondsToDiffTime 0}- , UTCTime{utctDay = fromGregorian 2009 4 1, utctDayTime = secondsToDiffTime 60}- , UTCTime{utctDay = fromGregorian 2009 2 1, utctDayTime = secondsToDiffTime 0}- , UTCTime{utctDay = fromGregorian 2009 2 1, utctDayTime = secondsToDiffTime 60}- , UTCTime{utctDay = fromGregorian 2009 1 1, utctDayTime = secondsToDiffTime 0}- , UTCTime{utctDay = fromGregorian 2009 1 1, utctDayTime = secondsToDiffTime 60}- ]- )- ]--allTypesPlain :: Test-allTypesPlain =- TestCase- ( assertEqual- "allTypesPlain"- allTypes- (unsafePerformIO (D.readParquet "./tests/data/alltypes_plain.parquet"))- )--allTypesTinyPagesDimensions :: Test-allTypesTinyPagesDimensions =- TestCase- ( assertEqual- "allTypesTinyPages last few"- (7300, 13)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/alltypes_tiny_pages.parquet"))- )- )--tinyPagesLast10 :: D.DataFrame-tinyPagesLast10 =- D.fromNamedColumns- [ ("id", D.fromList @Int32 (reverse [6174 .. 6183]))- , ("bool_col", D.fromList @Bool (Prelude.take 10 (cycle [False, True])))- , ("tinyint_col", D.fromList @Int32 [3, 2, 1, 0, 9, 8, 7, 6, 5, 4])- , ("smallint_col", D.fromList @Int32 [3, 2, 1, 0, 9, 8, 7, 6, 5, 4])- , ("int_col", D.fromList @Int32 [3, 2, 1, 0, 9, 8, 7, 6, 5, 4])- , ("bigint_col", D.fromList @Int64 [30, 20, 10, 0, 90, 80, 70, 60, 50, 40])- ,- ( "float_col"- , D.fromList @Float [3.3, 2.2, 1.1, 0, 9.9, 8.8, 7.7, 6.6, 5.5, 4.4]- )- ,- ( "date_string_col"- , D.fromList @Text- [ "09/11/10"- , "09/11/10"- , "09/11/10"- , "09/11/10"- , "09/10/10"- , "09/10/10"- , "09/10/10"- , "09/10/10"- , "09/10/10"- , "09/10/10"- ]- )- ,- ( "string_col"- , D.fromList @Text ["3", "2", "1", "0", "9", "8", "7", "6", "5", "4"]- )- ,- ( "timestamp_col"- , D.fromList @UTCTime- [ UTCTime- { utctDay = fromGregorian 2010 9 10- , utctDayTime = secondsToDiffTime 85384- }- , UTCTime- { utctDay = fromGregorian 2010 9 10- , utctDayTime = secondsToDiffTime 85324- }- , UTCTime- { utctDay = fromGregorian 2010 9 10- , utctDayTime = secondsToDiffTime 85264- }- , UTCTime- { utctDay = fromGregorian 2010 9 10- , utctDayTime = secondsToDiffTime 85204- }- , UTCTime- { utctDay = fromGregorian 2010 9 9- , utctDayTime = secondsToDiffTime 85144- }- , UTCTime- { utctDay = fromGregorian 2010 9 9- , utctDayTime = secondsToDiffTime 85084- }- , UTCTime- { utctDay = fromGregorian 2010 9 9- , utctDayTime = secondsToDiffTime 85024- }- , UTCTime- { utctDay = fromGregorian 2010 9 9- , utctDayTime = secondsToDiffTime 84964- }- , UTCTime- { utctDay = fromGregorian 2010 9 9- , utctDayTime = secondsToDiffTime 84904- }- , UTCTime- { utctDay = fromGregorian 2010 9 9- , utctDayTime = secondsToDiffTime 84844- }- ]- )- , ("year", D.fromList @Int32 (replicate 10 2010))- , ("month", D.fromList @Int32 (replicate 10 9))- ]--allTypesTinyPagesLastFew :: Test-allTypesTinyPagesLastFew =- TestCase- ( assertEqual- "allTypesTinyPages dimensions"- tinyPagesLast10- ( unsafePerformIO- -- Excluding doubles because they are weird to compare.- ( fmap- (D.takeLast 10 . D.exclude ["double_col"])- (D.readParquet "./tests/data/alltypes_tiny_pages.parquet")- )- )- )--allTypesPlainSnappy :: Test-allTypesPlainSnappy =- TestCase- ( assertEqual- "allTypesPlainSnappy"- (D.filter (F.col @Int32 "id") (`elem` [6, 7]) allTypes)- (unsafePerformIO (D.readParquet "./tests/data/alltypes_plain.snappy.parquet"))- )--allTypesDictionary :: Test-allTypesDictionary =- TestCase- ( assertEqual- "allTypesPlainSnappy"- (D.filter (F.col @Int32 "id") (`elem` [0, 1]) allTypes)- (unsafePerformIO (D.readParquet "./tests/data/alltypes_dictionary.parquet"))- )--selectedColumnsWithOpts :: Test-selectedColumnsWithOpts =- TestCase- ( assertEqual- "selectedColumnsWithOpts"- (D.select ["id", "bool_col"] allTypes)- ( unsafePerformIO- ( D.readParquetWithOpts- (D.defaultParquetReadOptions{D.selectedColumns = Just ["id", "bool_col"]})- "./tests/data/alltypes_plain.parquet"- )- )- )--rowRangeWithOpts :: Test-rowRangeWithOpts =- TestCase- ( assertEqual- "rowRangeWithOpts"- (3, 11)- ( unsafePerformIO- ( D.dimensions- <$> D.readParquetWithOpts- (D.defaultParquetReadOptions{D.rowRange = Just (2, 5)})- "./tests/data/alltypes_plain.parquet"- )- )- )--predicateWithOpts :: Test-predicateWithOpts =- TestCase- ( assertEqual- "predicateWithOpts"- (D.fromNamedColumns [("id", D.fromList [6 :: Int32, 7])])- ( unsafePerformIO- ( D.readParquetWithOpts- ( D.defaultParquetReadOptions- { D.selectedColumns = Just ["id"]- , D.predicate =- Just- ( F.geq- (F.col @Int32 "id")- (F.lit (6 :: Int32))- )- }- )- "./tests/data/alltypes_plain.parquet"- )- )- )--predicateUsesNonSelectedColumnWithOpts :: Test-predicateUsesNonSelectedColumnWithOpts =- TestCase- ( assertEqual- "predicateUsesNonSelectedColumnWithOpts"- (D.fromNamedColumns [("bool_col", D.fromList [True, False])])- ( unsafePerformIO- ( D.readParquetWithOpts- ( D.defaultParquetReadOptions- { D.selectedColumns = Just ["bool_col"]- , D.predicate =- Just- ( F.geq- (F.col @Int32 "id")- (F.lit (6 :: Int32))- )- }- )- "./tests/data/alltypes_plain.parquet"- )- )- )--predicateWithOptsAcrossFiles :: Test-predicateWithOptsAcrossFiles =- TestCase- ( assertEqual- "predicateWithOptsAcrossFiles"- (4, 1)- ( unsafePerformIO- ( D.dimensions- <$> D.readParquetFilesWithOpts- ( D.defaultParquetReadOptions- { D.selectedColumns = Just ["id"]- , D.predicate =- Just- ( F.geq- (F.col @Int32 "id")- (F.lit (6 :: Int32))- )- }- )- "./tests/data/alltypes_plain*.parquet"- )- )- )--missingSelectedColumnWithOpts :: Test-missingSelectedColumnWithOpts =- TestCase- ( assertExpectException- "missingSelectedColumnWithOpts"- "Column not found"- ( D.readParquetWithOpts- (D.defaultParquetReadOptions{D.selectedColumns = Just ["does_not_exist"]})- "./tests/data/alltypes_plain.parquet"- )- )--transactions :: D.DataFrame-transactions =- D.fromNamedColumns- [ ("transaction_id", D.fromList [1 :: Int32, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])- ,- ( "event_time"- , D.fromList- [ UTCTime- { utctDay = fromGregorian 2024 1 3- , utctDayTime = secondsToDiffTime 29564 + picosecondsToDiffTime 2311000000- }- , UTCTime- { utctDay = fromGregorian 2024 1 3- , utctDayTime = secondsToDiffTime 35101 + picosecondsToDiffTime 118900000000- }- , UTCTime- { utctDay = fromGregorian 2024 1 4- , utctDayTime = secondsToDiffTime 39802 + picosecondsToDiffTime 774512000000- }- , UTCTime- { utctDay = fromGregorian 2024 1 5- , utctDayTime = secondsToDiffTime 53739 + picosecondsToDiffTime 1000000- }- , UTCTime- { utctDay = fromGregorian 2024 1 6- , utctDayTime = secondsToDiffTime 8278 + picosecondsToDiffTime 543210000000- }- , UTCTime- { utctDay = fromGregorian 2024 1 6- , utctDayTime = secondsToDiffTime 8284 + picosecondsToDiffTime 211000000000- }- , UTCTime- { utctDay = fromGregorian 2024 1 7- , utctDayTime = secondsToDiffTime 63000- }- , UTCTime- { utctDay = fromGregorian 2024 1 8- , utctDayTime = secondsToDiffTime 24259 + picosecondsToDiffTime 390000000000- }- , UTCTime- { utctDay = fromGregorian 2024 1 9- , utctDayTime = secondsToDiffTime 48067 + picosecondsToDiffTime 812345000000- }- , UTCTime- { utctDay = fromGregorian 2024 1 10- , utctDayTime = secondsToDiffTime 82799 + picosecondsToDiffTime 999999000000- }- , UTCTime- { utctDay = fromGregorian 2024 1 11- , utctDayTime = secondsToDiffTime 36000 + picosecondsToDiffTime 100000000000- }- , UTCTime- { utctDay = fromGregorian 2024 1 12- , utctDayTime = secondsToDiffTime 56028 + picosecondsToDiffTime 667891000000- }- ]- )- ,- ( "user_email"- , D.fromList- [ "alice@example.com" :: Text- , "bob@example.com"- , "carol@example.com"- , "alice@example.com"- , "dave@example.com"- , "dave@example.com"- , "eve@example.com"- , "frank@example.com"- , "grace@example.com"- , "dave@example.com"- , "alice@example.com"- , "heidi@example.com"- ]- )- ,- ( "transaction_type"- , D.fromList- [ "purchase" :: Text- , "purchase"- , "refund"- , "purchase"- , "purchase"- , "purchase"- , "purchase"- , "withdrawal"- , "purchase"- , "purchase"- , "purchase"- , "refund"- ]- )- ,- ( "amount"- , D.fromList- [ 142.50 :: Double- , 29.99- , 89.00- , 2399.00- , 15.00- , 15.00- , 450.75- , 200.00- , 55.20- , 3200.00- , 74.99- , 120.00- ]- )- ,- ( "currency"- , D.fromList- [ "USD" :: Text- , "USD"- , "EUR"- , "USD"- , "GBP"- , "GBP"- , "USD"- , "EUR"- , "CAD"- , "USD"- , "USD"- , "GBP"- ]- )- ,- ( "status"- , D.fromList- [ "approved" :: Text- , "approved"- , "approved"- , "declined"- , "approved"- , "declined"- , "approved"- , "approved"- , "approved"- , "flagged"- , "approved"- , "approved"- ]- )- ,- ( "location"- , D.fromList- [ "New York, US" :: Text- , "London, GB"- , "Berlin, DE"- , "New York, US"- , "Manchester, GB"- , "Lagos, NG"- , "San Francisco, US"- , "Paris, FR"- , "Toronto, CA"- , "New York, US"- , "New York, US"- , "Edinburgh, GB"- ]- )- ]--transactionsTest :: Test-transactionsTest =- TestCase- ( assertEqual- "transactions"- transactions- (unsafePerformIO (D.readParquet "./tests/data/transactions.parquet"))- )--mtCarsDataset :: D.DataFrame-mtCarsDataset =- D.fromNamedColumns- [- ( "model"- , D.fromList- [ "Mazda RX4" :: Text- , "Mazda RX4 Wag"- , "Datsun 710"- , "Hornet 4 Drive"- , "Hornet Sportabout"- , "Valiant"- , "Duster 360"- , "Merc 240D"- , "Merc 230"- , "Merc 280"- , "Merc 280C"- , "Merc 450SE"- , "Merc 450SL"- , "Merc 450SLC"- , "Cadillac Fleetwood"- , "Lincoln Continental"- , "Chrysler Imperial"- , "Fiat 128"- , "Honda Civic"- , "Toyota Corolla"- , "Toyota Corona"- , "Dodge Challenger"- , "AMC Javelin"- , "Camaro Z28"- , "Pontiac Firebird"- , "Fiat X1-9"- , "Porsche 914-2"- , "Lotus Europa"- , "Ford Pantera L"- , "Ferrari Dino"- , "Maserati Bora"- , "Volvo 142E"- ]- )- ,- ( "mpg"- , D.fromList- [ 21.0 :: Double- , 21.0- , 22.8- , 21.4- , 18.7- , 18.1- , 14.3- , 24.4- , 22.8- , 19.2- , 17.8- , 16.4- , 17.3- , 15.2- , 10.4- , 10.4- , 14.7- , 32.4- , 30.4- , 33.9- , 21.5- , 15.5- , 15.2- , 13.3- , 19.2- , 27.3- , 26.0- , 30.4- , 15.8- , 19.7- , 15.0- , 21.4- ]- )- ,- ( "cyl"- , D.fromList- [ 6 :: Int32- , 6- , 4- , 6- , 8- , 6- , 8- , 4- , 4- , 6- , 6- , 8- , 8- , 8- , 8- , 8- , 8- , 4- , 4- , 4- , 4- , 8- , 8- , 8- , 8- , 4- , 4- , 4- , 8- , 6- , 8- , 4- ]- )- ,- ( "disp"- , D.fromList- [ 160.0 :: Double- , 160.0- , 108.0- , 258.0- , 360.0- , 225.0- , 360.0- , 146.7- , 140.8- , 167.6- , 167.6- , 275.8- , 275.8- , 275.8- , 472.0- , 460.0- , 440.0- , 78.7- , 75.7- , 71.1- , 120.1- , 318.0- , 304.0- , 350.0- , 400.0- , 79.0- , 120.3- , 95.1- , 351.0- , 145.0- , 301.0- , 121.0- ]- )- ,- ( "hp"- , D.fromList- [ 110 :: Int32- , 110- , 93- , 110- , 175- , 105- , 245- , 62- , 95- , 123- , 123- , 180- , 180- , 180- , 205- , 215- , 230- , 66- , 52- , 65- , 97- , 150- , 150- , 245- , 175- , 66- , 91- , 113- , 264- , 175- , 335- , 109- ]- )- ,- ( "drat"- , D.fromList- [ 3.9 :: Double- , 3.9- , 3.85- , 3.08- , 3.15- , 2.76- , 3.21- , 3.69- , 3.92- , 3.92- , 3.92- , 3.07- , 3.07- , 3.07- , 2.93- , 3.0- , 3.23- , 4.08- , 4.93- , 4.22- , 3.7- , 2.76- , 3.15- , 3.73- , 3.08- , 4.08- , 4.43- , 3.77- , 4.22- , 3.62- , 3.54- , 4.11- ]- )- ,- ( "wt"- , D.fromList- [ 2.62 :: Double- , 2.875- , 2.32- , 3.215- , 3.44- , 3.46- , 3.57- , 3.19- , 3.15- , 3.44- , 3.44- , 4.07- , 3.73- , 3.78- , 5.25- , 5.424- , 5.345- , 2.2- , 1.615- , 1.835- , 2.465- , 3.52- , 3.435- , 3.84- , 3.845- , 1.935- , 2.14- , 1.513- , 3.17- , 2.77- , 3.57- , 2.78- ]- )- ,- ( "qsec"- , D.fromList- [ 16.46 :: Double- , 17.02- , 18.61- , 19.44- , 17.02- , 20.22- , 15.84- , 20.0- , 22.9- , 18.3- , 18.9- , 17.4- , 17.6- , 18.0- , 17.98- , 17.82- , 17.42- , 19.47- , 18.52- , 19.9- , 20.01- , 16.87- , 17.3- , 15.41- , 17.05- , 18.9- , 16.7- , 16.9- , 14.5- , 15.5- , 14.6- , 18.6- ]- )- ,- ( "vs"- , D.fromList- [ 0 :: Int32- , 0- , 1- , 1- , 0- , 1- , 0- , 1- , 1- , 1- , 1- , 0- , 0- , 0- , 0- , 0- , 0- , 1- , 1- , 1- , 1- , 0- , 0- , 0- , 0- , 1- , 0- , 1- , 0- , 0- , 0- , 1- ]- )- ,- ( "am"- , D.fromList- [ 1 :: Int32- , 1- , 1- , 0- , 0- , 0- , 0- , 0- , 0- , 0- , 0- , 0- , 0- , 0- , 0- , 0- , 0- , 1- , 1- , 1- , 0- , 0- , 0- , 0- , 0- , 1- , 1- , 1- , 1- , 1- , 1- , 1- ]- )- ,- ( "gear"- , D.fromList- [ 4 :: Int32- , 4- , 4- , 3- , 3- , 3- , 3- , 4- , 4- , 4- , 4- , 3- , 3- , 3- , 3- , 3- , 3- , 4- , 4- , 4- , 3- , 3- , 3- , 3- , 3- , 4- , 5- , 5- , 5- , 5- , 5- , 4- ]- )- ,- ( "carb"- , D.fromList- [ 4 :: Int32- , 4- , 1- , 1- , 2- , 1- , 4- , 2- , 2- , 4- , 4- , 3- , 3- , 3- , 4- , 4- , 4- , 1- , 2- , 1- , 1- , 2- , 2- , 4- , 2- , 1- , 2- , 2- , 4- , 6- , 8- , 2- ]- )- ]--mtCars :: Test-mtCars =- TestCase- ( assertEqual- "mt_cars"- mtCarsDataset- (unsafePerformIO (D.readParquet "./tests/data/mtcars.parquet"))- )---- ------------------------------------------------------------------------------ Group 1: Plain variant--- -----------------------------------------------------------------------------allTypesTinyPagesPlain :: Test-allTypesTinyPagesPlain =- TestCase- ( assertEqual- "alltypes_tiny_pages_plain dimensions"- (7300, 13)- ( unsafePerformIO- ( fmap- D.dimensions- (D.readParquet "./tests/data/alltypes_tiny_pages_plain.parquet")- )- )- )---- ------------------------------------------------------------------------------ Group 2: Compression codecs (unsupported → error tests)--- -----------------------------------------------------------------------------hadoopLz4Compressed :: Test-hadoopLz4Compressed =- TestCase- ( assertExpectException- "hadoopLz4Compressed"- "LZ4"- (D.readParquet "./tests/data/hadoop_lz4_compressed.parquet")- )--hadoopLz4CompressedLarger :: Test-hadoopLz4CompressedLarger =- TestCase- ( assertExpectException- "hadoopLz4CompressedLarger"- "LZ4"- (D.readParquet "./tests/data/hadoop_lz4_compressed_larger.parquet")- )--nonHadoopLz4Compressed :: Test-nonHadoopLz4Compressed =- TestCase- ( assertExpectException- "nonHadoopLz4Compressed"- "LZ4"- (D.readParquet "./tests/data/non_hadoop_lz4_compressed.parquet")- )--lz4RawCompressed :: Test-lz4RawCompressed =- TestCase- ( assertExpectException- "lz4RawCompressed"- "LZ4_RAW"- (D.readParquet "./tests/data/lz4_raw_compressed.parquet")- )--lz4RawCompressedLarger :: Test-lz4RawCompressedLarger =- TestCase- ( assertExpectException- "lz4RawCompressedLarger"- "LZ4_RAW"- (D.readParquet "./tests/data/lz4_raw_compressed_larger.parquet")- )--concatenatedGzipMembers :: Test-concatenatedGzipMembers =- TestCase- ( assertExpectException- "concatenatedGzipMembers"- "12"- (D.readParquet "./tests/data/concatenated_gzip_members.parquet")- )--largeBrotliMap :: Test-largeBrotliMap =- TestCase- ( assertExpectException- "largeBrotliMap"- "BROTLI"- (D.readParquet "./tests/data/large_string_map.brotli.parquet")- )---- ------------------------------------------------------------------------------ Group 3: Delta / RLE encodings (unsupported → error tests)--- -----------------------------------------------------------------------------deltaBinaryPacked :: Test-deltaBinaryPacked =- TestCase- ( assertExpectException- "deltaBinaryPacked"- "EDELTA_BINARY_PACKED"- (D.readParquet "./tests/data/delta_binary_packed.parquet")- )--deltaByteArray :: Test-deltaByteArray =- TestCase- ( assertExpectException- "deltaByteArray"- "EDELTA_BYTE_ARRAY"- (D.readParquet "./tests/data/delta_byte_array.parquet")- )--deltaEncodingOptionalColumn :: Test-deltaEncodingOptionalColumn =- TestCase- ( assertExpectException- "deltaEncodingOptionalColumn"- "EDELTA_BINARY_PACKED"- (D.readParquet "./tests/data/delta_encoding_optional_column.parquet")- )--deltaEncodingRequiredColumn :: Test-deltaEncodingRequiredColumn =- TestCase- ( assertExpectException- "deltaEncodingRequiredColumn"- "EDELTA_BINARY_PACKED"- (D.readParquet "./tests/data/delta_encoding_required_column.parquet")- )--deltaLengthByteArray :: Test-deltaLengthByteArray =- TestCase- ( assertExpectException- "deltaLengthByteArray"- "ZSTD"- (D.readParquet "./tests/data/delta_length_byte_array.parquet")- )--rleBooleanEncoding :: Test-rleBooleanEncoding =- TestCase- ( assertExpectException- "rleBooleanEncoding"- "Zlib"- (D.readParquet "./tests/data/rle_boolean_encoding.parquet")- )--dictPageOffsetZero :: Test-dictPageOffsetZero =- TestCase- ( assertExpectException- "dictPageOffsetZero"- "Unknown kv"- (D.readParquet "./tests/data/dict-page-offset-zero.parquet")- )---- ------------------------------------------------------------------------------ Group 4: Data Page V2 (unsupported → error tests)--- -----------------------------------------------------------------------------datapageV2Snappy :: Test-datapageV2Snappy =- TestCase- ( assertExpectException- "datapageV2Snappy"- "InvalidOffset"- (D.readParquet "./tests/data/datapage_v2.snappy.parquet")- )--datapageV2EmptyDatapage :: Test-datapageV2EmptyDatapage =- TestCase- ( assertExpectException- "datapageV2EmptyDatapage"- "UnexpectedEOF"- (D.readParquet "./tests/data/datapage_v2_empty_datapage.snappy.parquet")- )--pageV2EmptyCompressed :: Test-pageV2EmptyCompressed =- TestCase- ( assertExpectException- "pageV2EmptyCompressed"- "10"- (D.readParquet "./tests/data/page_v2_empty_compressed.parquet")- )---- ------------------------------------------------------------------------------ Group 5: Checksum files (all read successfully)--- -----------------------------------------------------------------------------datapageV1UncompressedChecksum :: Test-datapageV1UncompressedChecksum =- TestCase- ( assertEqual- "datapageV1UncompressedChecksum"- (5120, 2)- ( unsafePerformIO- ( fmap- D.dimensions- (D.readParquet "./tests/data/datapage_v1-uncompressed-checksum.parquet")- )- )- )--datapageV1SnappyChecksum :: Test-datapageV1SnappyChecksum =- TestCase- ( assertEqual- "datapageV1SnappyChecksum"- (5120, 2)- ( unsafePerformIO- ( fmap- D.dimensions- (D.readParquet "./tests/data/datapage_v1-snappy-compressed-checksum.parquet")- )- )- )--plainDictUncompressedChecksum :: Test-plainDictUncompressedChecksum =- TestCase- ( assertEqual- "plainDictUncompressedChecksum"- (1000, 2)- ( unsafePerformIO- ( fmap- D.dimensions- (D.readParquet "./tests/data/plain-dict-uncompressed-checksum.parquet")- )- )- )--rleDictSnappyChecksum :: Test-rleDictSnappyChecksum =- TestCase- ( assertEqual- "rleDictSnappyChecksum"- (1000, 2)- ( unsafePerformIO- ( fmap- D.dimensions- (D.readParquet "./tests/data/rle-dict-snappy-checksum.parquet")- )- )- )--datapageV1CorruptChecksum :: Test-datapageV1CorruptChecksum =- TestCase- ( assertEqual- "datapageV1CorruptChecksum"- (5120, 2)- ( unsafePerformIO- ( fmap- D.dimensions- (D.readParquet "./tests/data/datapage_v1-corrupt-checksum.parquet")- )- )- )--rleDictUncompressedCorruptChecksum :: Test-rleDictUncompressedCorruptChecksum =- TestCase- ( assertEqual- "rleDictUncompressedCorruptChecksum"- (1000, 2)- ( unsafePerformIO- ( fmap- D.dimensions- (D.readParquet "./tests/data/rle-dict-uncompressed-corrupt-checksum.parquet")- )- )- )---- ------------------------------------------------------------------------------ Group 6: NULL handling--- -----------------------------------------------------------------------------nullsSnappy :: Test-nullsSnappy =- TestCase- ( assertEqual- "nullsSnappy"- (8, 1)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/nulls.snappy.parquet"))- )- )--int32WithNullPages :: Test-int32WithNullPages =- TestCase- ( assertEqual- "int32WithNullPages"- (1000, 1)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/int32_with_null_pages.parquet"))- )- )--nullableImpala :: Test-nullableImpala =- TestCase- ( assertEqual- "nullableImpala"- (7, 13)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/nullable.impala.parquet"))- )- )--nonnullableImpala :: Test-nonnullableImpala =- TestCase- ( assertEqual- "nonnullableImpala"- (1, 13)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/nonnullable.impala.parquet"))- )- )--singleNan :: Test-singleNan =- TestCase- ( assertEqual- "singleNan"- (1, 1)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/single_nan.parquet"))- )- )--nanInStats :: Test-nanInStats =- TestCase- ( assertEqual- "nanInStats"- (2, 1)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/nan_in_stats.parquet"))- )- )---- ------------------------------------------------------------------------------ Group 7: Decimal types--- -----------------------------------------------------------------------------int32Decimal :: Test-int32Decimal =- TestCase- ( assertEqual- "int32Decimal"- (24, 1)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/int32_decimal.parquet"))- )- )--int64Decimal :: Test-int64Decimal =- TestCase- ( assertEqual- "int64Decimal"- (24, 1)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/int64_decimal.parquet"))- )- )--byteArrayDecimal :: Test-byteArrayDecimal =- TestCase- ( assertEqual- "byteArrayDecimal"- (24, 1)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/byte_array_decimal.parquet"))- )- )--fixedLengthDecimal :: Test-fixedLengthDecimal =- TestCase- ( assertExpectException- "fixedLengthDecimal"- "FIXED_LEN_BYTE_ARRAY"- (D.readParquet "./tests/data/fixed_length_decimal.parquet")- )--fixedLengthDecimalLegacy :: Test-fixedLengthDecimalLegacy =- TestCase- ( assertExpectException- "fixedLengthDecimalLegacy"- "FIXED_LEN_BYTE_ARRAY"- (D.readParquet "./tests/data/fixed_length_decimal_legacy.parquet")- )---- ------------------------------------------------------------------------------ Group 8: Binary / fixed-length bytes--- -----------------------------------------------------------------------------binaryFile :: Test-binaryFile =- TestCase- ( assertEqual- "binaryFile"- (12, 1)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/binary.parquet"))- )- )--binaryTruncatedMinMax :: Test-binaryTruncatedMinMax =- TestCase- ( assertEqual- "binaryTruncatedMinMax"- (12, 6)- ( unsafePerformIO- ( fmap- D.dimensions- (D.readParquet "./tests/data/binary_truncated_min_max.parquet")- )- )- )--fixedLengthByteArray :: Test-fixedLengthByteArray =- TestCase- ( assertExpectException- "fixedLengthByteArray"- "FIXED_LEN_BYTE_ARRAY"- (D.readParquet "./tests/data/fixed_length_byte_array.parquet")- )---- ------------------------------------------------------------------------------ Group 9: INT96 timestamps--- -----------------------------------------------------------------------------int96FromSpark :: Test-int96FromSpark =- TestCase- ( assertEqual- "int96FromSpark"- (6, 1)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/int96_from_spark.parquet"))- )- )---- ------------------------------------------------------------------------------ Group 10: Metadata / index / bloom filters--- -----------------------------------------------------------------------------columnChunkKeyValueMetadata :: Test-columnChunkKeyValueMetadata =- TestCase- ( assertExpectException- "columnChunkKeyValueMetadata"- "Unknown page header field"- (D.readParquet "./tests/data/column_chunk_key_value_metadata.parquet")- )--dataIndexBloomEncodingStats :: Test-dataIndexBloomEncodingStats =- TestCase- ( assertEqual- "dataIndexBloomEncodingStats"- (14, 1)- ( unsafePerformIO- ( fmap- D.dimensions- (D.readParquet "./tests/data/data_index_bloom_encoding_stats.parquet")- )- )- )--dataIndexBloomEncodingWithLength :: Test-dataIndexBloomEncodingWithLength =- TestCase- ( assertEqual- "dataIndexBloomEncodingWithLength"- (14, 1)- ( unsafePerformIO- ( fmap- D.dimensions- (D.readParquet "./tests/data/data_index_bloom_encoding_with_length.parquet")- )- )- )--sortColumns :: Test-sortColumns =- TestCase- ( assertEqual- "sortColumns"- (3, 2)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/sort_columns.parquet"))- )- )--overflowI16PageCnt :: Test-overflowI16PageCnt =- TestCase- ( assertExpectException- "overflowI16PageCnt"- "UNIMPLEMENTED"- (D.readParquet "./tests/data/overflow_i16_page_cnt.parquet")- )---- ------------------------------------------------------------------------------ Group 11: Nested / complex types and byte-stream-split--- -----------------------------------------------------------------------------byteStreamSplitZstd :: Test-byteStreamSplitZstd =- TestCase- ( assertExpectException- "byteStreamSplitZstd"- "EBYTE_STREAM_SPLIT"- (D.readParquet "./tests/data/byte_stream_split.zstd.parquet")- )--byteStreamSplitExtendedGzip :: Test-byteStreamSplitExtendedGzip =- TestCase- ( assertExpectException- "byteStreamSplitExtendedGzip"- "FIXED_LEN_BYTE_ARRAY"- (D.readParquet "./tests/data/byte_stream_split_extended.gzip.parquet")- )--float16NonzerosAndNans :: Test-float16NonzerosAndNans =- TestCase- ( assertExpectException- "float16NonzerosAndNans"- "PFIXED_LEN_BYTE_ARRAY"- (D.readParquet "./tests/data/float16_nonzeros_and_nans.parquet")- )--float16ZerosAndNans :: Test-float16ZerosAndNans =- TestCase- ( assertExpectException- "float16ZerosAndNans"- "PFIXED_LEN_BYTE_ARRAY"- (D.readParquet "./tests/data/float16_zeros_and_nans.parquet")- )--nestedListsSnappy :: Test-nestedListsSnappy =- TestCase- ( assertEqual- "nestedListsSnappy"- (3, 2)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/nested_lists.snappy.parquet"))- )- )--nestedMapsSnappy :: Test-nestedMapsSnappy =- TestCase- ( assertEqual- "nestedMapsSnappy"- (6, 5)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/nested_maps.snappy.parquet"))- )- )--nestedStructsRust :: Test-nestedStructsRust =- TestCase- ( assertEqual- "nestedStructsRust"- (1, 216)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/nested_structs.rust.parquet"))- )- )--listColumns :: Test-listColumns =- TestCase- ( assertEqual- "listColumns"- (3, 2)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/list_columns.parquet"))- )- )--oldListStructure :: Test-oldListStructure =- TestCase- ( assertEqual- "oldListStructure"- (1, 1)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/old_list_structure.parquet"))- )- )--nullList :: Test-nullList =- TestCase- ( assertEqual- "nullList"- (1, 1)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/null_list.parquet"))- )- )--mapNoValue :: Test-mapNoValue =- TestCase- ( assertEqual- "mapNoValue"- (3, 4)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/map_no_value.parquet"))- )- )--incorrectMapSchema :: Test-incorrectMapSchema =- TestCase- ( assertEqual- "incorrectMapSchema"- (1, 2)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/incorrect_map_schema.parquet"))- )- )--repeatedNoAnnotation :: Test-repeatedNoAnnotation =- TestCase- ( assertEqual- "repeatedNoAnnotation"- (6, 3)- ( unsafePerformIO- (fmap D.dimensions (D.readParquet "./tests/data/repeated_no_annotation.parquet"))- )- )--repeatedPrimitiveNoList :: Test-repeatedPrimitiveNoList =- TestCase- ( assertEqual- "repeatedPrimitiveNoList"- (4, 4)- ( unsafePerformIO- ( fmap- D.dimensions- (D.readParquet "./tests/data/repeated_primitive_no_list.parquet")- )- )- )--unknownLogicalType :: Test-unknownLogicalType =- TestCase- ( assertExpectException- "unknownLogicalType"- "Unknown logical type"- (D.readParquet "./tests/data/unknown-logical-type.parquet")- )---- ------------------------------------------------------------------------------ Group 12: Malformed files--- -----------------------------------------------------------------------------nationDictMalformed :: Test-nationDictMalformed =- TestCase- ( assertExpectException- "nationDictMalformed"- "dict index count mismatch"- (D.readParquet "./tests/data/nation.dict-malformed.parquet")- )--tests :: [Test]-tests =- [ allTypesPlain- , allTypesPlainSnappy- , allTypesDictionary- , selectedColumnsWithOpts- , rowRangeWithOpts- , predicateWithOpts- , predicateUsesNonSelectedColumnWithOpts- , predicateWithOptsAcrossFiles- , missingSelectedColumnWithOpts- , mtCars- , allTypesTinyPagesLastFew- , allTypesTinyPagesDimensions- , transactionsTest- , -- Group 1- allTypesTinyPagesPlain- , -- Group 2: compression codecs- hadoopLz4Compressed- , hadoopLz4CompressedLarger- , nonHadoopLz4Compressed- , lz4RawCompressed- , lz4RawCompressedLarger- , concatenatedGzipMembers- , largeBrotliMap- , -- Group 3: delta / rle encodings- deltaBinaryPacked- , deltaByteArray- , deltaEncodingOptionalColumn- , deltaEncodingRequiredColumn- , deltaLengthByteArray- , rleBooleanEncoding- , dictPageOffsetZero- , -- Group 4: Data Page V2- datapageV2Snappy- , datapageV2EmptyDatapage- , pageV2EmptyCompressed- , -- Group 5: checksum files- datapageV1UncompressedChecksum- , datapageV1SnappyChecksum- , plainDictUncompressedChecksum- , rleDictSnappyChecksum- , datapageV1CorruptChecksum- , rleDictUncompressedCorruptChecksum- , -- Group 6: NULL handling- nullsSnappy- , int32WithNullPages- , nullableImpala- , nonnullableImpala- , singleNan- , nanInStats- , -- Group 7: decimal types- int32Decimal- , int64Decimal- , byteArrayDecimal- , fixedLengthDecimal- , fixedLengthDecimalLegacy- , -- Group 8: binary / fixed-length bytes- binaryFile- , binaryTruncatedMinMax- , fixedLengthByteArray- , -- Group 9: INT96 timestamps- int96FromSpark- , -- Group 10: metadata / bloom filters- columnChunkKeyValueMetadata- , dataIndexBloomEncodingStats- , dataIndexBloomEncodingWithLength- , sortColumns- , overflowI16PageCnt- , -- Group 11: nested / complex types- byteStreamSplitZstd- , byteStreamSplitExtendedGzip- , float16NonzerosAndNans- , float16ZerosAndNans- , nestedListsSnappy- , nestedMapsSnappy- , nestedStructsRust- , listColumns- , oldListStructure- , nullList- , mapNoValue- , incorrectMapSchema- , repeatedNoAnnotation- , repeatedPrimitiveNoList- , unknownLogicalType- , -- Group 12: malformed files- nationDictMalformed+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Parquet where++import Assertions (assertExpectException)+import Control.Monad (forM_)+import qualified DataFrame as D+import qualified DataFrame.Functions as F+import qualified DataFrame.IO.Parquet as DP+import qualified DataFrame.IO.Parquet.Schema as PS+import ParquetTestData (allTypes, mtCarsDataset, tinyPagesLast10, transactions)++import qualified Data.ByteString as BS+import Data.Int (Int32, Int64)+import Data.Maybe (fromMaybe)+import qualified Data.Set as S+import qualified Data.Text as T+import Data.Time (UTCTime (UTCTime), fromGregorian, picosecondsToDiffTime)+import Data.Word+import DataFrame.IO.Parquet.Thrift (+ cc_meta_data,+ cmd_path_in_schema,+ cmd_statistics,+ rg_columns,+ row_groups,+ schema,+ stats_null_count,+ unField,+ )+import DataFrame.Internal.Binary (+ littleEndianWord32,+ littleEndianWord64,+ word32ToLittleEndian,+ word64ToLittleEndian,+ )+import DataFrame.Internal.Column (hasElemType, hasMissing)+import DataFrame.Internal.DataFrame (unsafeGetColumn)+import GHC.IO (unsafePerformIO)+import Test.HUnit++testBothReadParquetPaths :: ((FilePath -> IO D.DataFrame) -> Test) -> Test+testBothReadParquetPaths mkTest =+ TestList+ [ mkTest D.readParquet+ , mkTest (DP._readParquetWithOpts (Just True) D.defaultParquetReadOptions)+ ]++assertColumnNullability ::+ String -> [(T.Text, Bool)] -> D.DataFrame -> Assertion+assertColumnNullability label expected df =+ forM_ expected $ \(columnName, shouldBeNullable) ->+ assertBool+ ( label+ <> ": expected "+ <> T.unpack columnName+ <> if shouldBeNullable then " to be nullable" else " to be non-nullable"+ )+ (hasMissing (unsafeGetColumn columnName df) == shouldBeNullable)++allTypesPlain :: Test+allTypesPlain = testBothReadParquetPaths $ \readParquet ->+ TestCase+ ( assertEqual+ "allTypesPlain"+ allTypes+ (unsafePerformIO (readParquet "./tests/data/alltypes_plain.parquet"))+ )++allTypesTinyPagesDimensions :: Test+allTypesTinyPagesDimensions =+ TestCase+ ( assertEqual+ "allTypesTinyPages last few"+ (7300, 13)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/alltypes_tiny_pages.parquet"))+ )+ )++allTypesTinyPagesLastFew :: Test+allTypesTinyPagesLastFew = testBothReadParquetPaths $ \readParquet ->+ TestCase+ ( assertEqual+ "allTypesTinyPages dimensions"+ tinyPagesLast10+ ( unsafePerformIO+ -- Excluding doubles because they are weird to compare.+ ( fmap+ (D.takeLast 10 . D.exclude ["double_col"])+ (readParquet "./tests/data/alltypes_tiny_pages.parquet")+ )+ )+ )++allTypesPlainSnappy :: Test+allTypesPlainSnappy = testBothReadParquetPaths $ \readParquet ->+ TestCase+ ( assertEqual+ "allTypesPlainSnappy"+ (D.filter (F.col @Int32 "id") (`elem` [6, 7]) allTypes)+ (unsafePerformIO (readParquet "./tests/data/alltypes_plain.snappy.parquet"))+ )++allTypesDictionary :: Test+allTypesDictionary = testBothReadParquetPaths $ \readParquet ->+ TestCase+ ( assertEqual+ "allTypesPlainSnappy"+ (D.filter (F.col @Int32 "id") (`elem` [0, 1]) allTypes)+ (unsafePerformIO (readParquet "./tests/data/alltypes_dictionary.parquet"))+ )++selectedColumnsWithOpts :: Test+selectedColumnsWithOpts =+ TestCase+ ( assertEqual+ "selectedColumnsWithOpts"+ (D.select ["id", "bool_col"] allTypes)+ ( unsafePerformIO+ ( D.readParquetWithOpts+ (D.defaultParquetReadOptions{D.selectedColumns = Just ["id", "bool_col"]})+ "./tests/data/alltypes_plain.parquet"+ )+ )+ )++rowRangeWithOpts :: Test+rowRangeWithOpts =+ TestCase+ ( assertEqual+ "rowRangeWithOpts"+ (3, 11)+ ( unsafePerformIO+ ( D.dimensions+ <$> D.readParquetWithOpts+ (D.defaultParquetReadOptions{D.rowRange = Just (2, 5)})+ "./tests/data/alltypes_plain.parquet"+ )+ )+ )++predicateWithOpts :: Test+predicateWithOpts =+ TestCase+ ( assertEqual+ "predicateWithOpts"+ (D.fromNamedColumns [("id", D.fromList [6 :: Int32, 7])])+ ( unsafePerformIO+ ( D.readParquetWithOpts+ ( D.defaultParquetReadOptions+ { D.selectedColumns = Just ["id"]+ , D.predicate =+ Just+ ( F.geq+ (F.col @Int32 "id")+ (F.lit (6 :: Int32))+ )+ }+ )+ "./tests/data/alltypes_plain.parquet"+ )+ )+ )++predicateUsesNonSelectedColumnWithOpts :: Test+predicateUsesNonSelectedColumnWithOpts =+ TestCase+ ( assertEqual+ "predicateUsesNonSelectedColumnWithOpts"+ (D.fromNamedColumns [("bool_col", D.fromList [True, False])])+ ( unsafePerformIO+ ( D.readParquetWithOpts+ ( D.defaultParquetReadOptions+ { D.selectedColumns = Just ["bool_col"]+ , D.predicate =+ Just+ ( F.geq+ (F.col @Int32 "id")+ (F.lit (6 :: Int32))+ )+ }+ )+ "./tests/data/alltypes_plain.parquet"+ )+ )+ )++safeColumnsWithOpts :: Test+safeColumnsWithOpts =+ TestCase $ do+ defaultDf <- D.readParquet "./tests/data/alltypes_plain.parquet"+ safeDf <-+ D.readParquetWithOpts+ (D.defaultParquetReadOptions{D.safeColumns = True})+ "./tests/data/alltypes_plain.parquet"++ assertEqual+ "safeColumnsWithOpts dimensions"+ (D.dimensions defaultDf)+ (D.dimensions safeDf)+ assertColumnNullability+ "default read"+ [("id", False), ("bool_col", False)]+ defaultDf+ assertColumnNullability+ "safeColumns read"+ [("id", True), ("bool_col", True)]+ safeDf+ assertBool+ "safeColumns id type"+ (hasElemType @(Maybe Int32) (unsafeGetColumn "id" safeDf))+ assertBool+ "safeColumns bool_col type"+ (hasElemType @(Maybe Bool) (unsafeGetColumn "bool_col" safeDf))++safeColumnsWithSelectedColumns :: Test+safeColumnsWithSelectedColumns =+ TestCase $ do+ df <-+ D.readParquetWithOpts+ ( D.defaultParquetReadOptions+ { D.selectedColumns = Just ["id", "bool_col"]+ , D.safeColumns = True+ }+ )+ "./tests/data/alltypes_plain.parquet"++ assertEqual "safeColumnsWithSelectedColumns dimensions" (8, 2) (D.dimensions df)+ assertColumnNullability+ "safeColumns projected read"+ [("id", True), ("bool_col", True)]+ df+ assertBool+ "safeColumns projected id type"+ (hasElemType @(Maybe Int32) (unsafeGetColumn "id" df))+ assertBool+ "safeColumns projected bool_col type"+ (hasElemType @(Maybe Bool) (unsafeGetColumn "bool_col" df))++predicateWithOptsAcrossFiles :: Test+predicateWithOptsAcrossFiles =+ TestCase+ ( assertEqual+ "predicateWithOptsAcrossFiles"+ (4, 1)+ ( unsafePerformIO+ ( D.dimensions+ <$> D.readParquetFilesWithOpts+ ( D.defaultParquetReadOptions+ { D.selectedColumns = Just ["id"]+ , D.predicate =+ Just+ ( F.geq+ (F.col @Int32 "id")+ (F.lit (6 :: Int32))+ )+ }+ )+ "./tests/data/alltypes_plain*.parquet"+ )+ )+ )++missingSelectedColumnWithOpts :: Test+missingSelectedColumnWithOpts =+ TestCase+ ( assertExpectException+ "missingSelectedColumnWithOpts"+ "Column not found"+ ( D.readParquetWithOpts+ (D.defaultParquetReadOptions{D.selectedColumns = Just ["does_not_exist"]})+ "./tests/data/alltypes_plain.parquet"+ )+ )++transactionsTest :: Test+transactionsTest = testBothReadParquetPaths $ \readParquet ->+ TestCase+ ( assertEqual+ "transactions"+ transactions+ (unsafePerformIO (readParquet "./tests/data/transactions.parquet"))+ )++mtCars :: Test+mtCars =+ TestCase+ ( assertEqual+ "mt_cars"+ mtCarsDataset+ (unsafePerformIO (D.readParquet "./tests/data/mtcars.parquet"))+ )++littleEndianWord64KnownPattern :: Test+littleEndianWord64KnownPattern =+ TestCase+ ( assertEqual+ "littleEndianWord64KnownPattern"+ (0x1122334455667788 :: Word64)+ ( littleEndianWord64+ (BS.pack [0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11])+ )+ )++littleEndianWord32KnownPattern :: Test+littleEndianWord32KnownPattern =+ TestCase+ ( assertEqual+ "littleEndianWord32KnownPattern"+ (0x11223344 :: Word32)+ (littleEndianWord32 (BS.pack [0x44, 0x33, 0x22, 0x11]))+ )++littleEndianWord64ShortInputPadsZeroes :: Test+littleEndianWord64ShortInputPadsZeroes =+ TestCase+ ( assertEqual+ "littleEndianWord64ShortInputPadsZeroes"+ (0x00CCBBAA :: Word64)+ (littleEndianWord64 (BS.pack [0xAA, 0xBB, 0xCC]))+ )++littleEndianWord32ShortInputPadsZeroes :: Test+littleEndianWord32ShortInputPadsZeroes =+ TestCase+ ( assertEqual+ "littleEndianWord32ShortInputPadsZeroes"+ (0x0000BEEF :: Word32)+ (littleEndianWord32 (BS.pack [0xEF, 0xBE]))+ )++littleEndianWord64RoundTrip :: Test+littleEndianWord64RoundTrip =+ TestCase+ ( assertEqual+ "littleEndianWord64RoundTrip"+ value+ (littleEndianWord64 (word64ToLittleEndian value))+ )+ where+ value = 0x1122334455667788 :: Word64++littleEndianWord32RoundTrip :: Test+littleEndianWord32RoundTrip =+ TestCase+ ( assertEqual+ "littleEndianWord32RoundTrip"+ value+ (littleEndianWord32 (word32ToLittleEndian value))+ )+ where+ value = 0xA1B2C3D4 :: Word32++-- ---------------------------------------------------------------------------+-- Group 1: Plain variant+-- ---------------------------------------------------------------------------++allTypesTinyPagesPlain :: Test+allTypesTinyPagesPlain =+ TestCase+ ( assertEqual+ "alltypes_tiny_pages_plain dimensions"+ (7300, 13)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/alltypes_tiny_pages_plain.parquet")+ )+ )+ )++-- ---------------------------------------------------------------------------+-- Group 2: Compression codecs (unsupported → error tests)+-- ---------------------------------------------------------------------------++-- TODO: LZ4 and LZ4_RAW compression are not yet implemented. When support+-- is added via a Haskell lz4 binding, hadoopLz4Compressed,+-- hadoopLz4CompressedLarger, nonHadoopLz4Compressed, lz4RawCompressed, and+-- lz4RawCompressedLarger should all change from assertExpectException to+-- assertEqual checking their respective row/column dimensions.+hadoopLz4Compressed :: Test+hadoopLz4Compressed =+ TestCase+ ( assertExpectException+ "hadoopLz4Compressed"+ "LZ4"+ (D.readParquet "./tests/data/hadoop_lz4_compressed.parquet")+ )++hadoopLz4CompressedLarger :: Test+hadoopLz4CompressedLarger =+ TestCase+ ( assertExpectException+ "hadoopLz4CompressedLarger"+ "LZ4"+ (D.readParquet "./tests/data/hadoop_lz4_compressed_larger.parquet")+ )++nonHadoopLz4Compressed :: Test+nonHadoopLz4Compressed =+ TestCase+ ( assertExpectException+ "nonHadoopLz4Compressed"+ "LZ4"+ (D.readParquet "./tests/data/non_hadoop_lz4_compressed.parquet")+ )++lz4RawCompressed :: Test+lz4RawCompressed =+ TestCase+ ( assertExpectException+ "lz4RawCompressed"+ "LZ4_RAW"+ (D.readParquet "./tests/data/lz4_raw_compressed.parquet")+ )++lz4RawCompressedLarger :: Test+lz4RawCompressedLarger =+ TestCase+ ( assertExpectException+ "lz4RawCompressedLarger"+ "LZ4_RAW"+ (D.readParquet "./tests/data/lz4_raw_compressed_larger.parquet")+ )++-- Was: assertExpectException "concatenatedGzipMembers" "12" ...+-- The old parser failed with a ZLIB size error. The new decompressor+-- handles concatenated gzip members correctly.+concatenatedGzipMembers :: Test+concatenatedGzipMembers =+ TestCase+ ( assertEqual+ "concatenatedGzipMembers"+ (513, 1)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/concatenated_gzip_members.parquet")+ )+ )+ )++-- TODO: BROTLI compression is not yet implemented. When a Haskell brotli+-- binding is added, change this to assertEqual checking the actual+-- dimensions of large_string_map.brotli.parquet.+largeBrotliMap :: Test+largeBrotliMap =+ TestCase+ ( assertExpectException+ "largeBrotliMap"+ "BROTLI"+ (D.readParquet "./tests/data/large_string_map.brotli.parquet")+ )++-- ---------------------------------------------------------------------------+-- Group 3: Delta / RLE encodings (unsupported → error tests)+-- ---------------------------------------------------------------------------++-- Was: assertExpectException "deltaBinaryPacked" "EDELTA_BINARY_PACKED" ...+-- The new parser's error includes the encoding name "DELTA_BINARY_PACKED"+-- without the old "E" prefix used in the previous error format.+-- TODO: When DELTA_BINARY_PACKED (encoding id=5) is implemented, change+-- this to assertEqual checking actual dimensions. The encoding stores+-- integer data as bit-packed deltas and is common for monotonically+-- increasing columns (row IDs, timestamps):+-- https://parquet.apache.org/docs/file-format/data-pages/encodings/#delta-encoding-delta_binary_packed--5+deltaBinaryPacked :: Test+deltaBinaryPacked =+ TestCase+ ( assertExpectException+ "deltaBinaryPacked"+ "DELTA_BINARY_PACKED"+ (D.readParquet "./tests/data/delta_binary_packed.parquet")+ )++-- Was: assertExpectException "deltaByteArray" "EDELTA_BYTE_ARRAY" ...+-- Same reason as deltaBinaryPacked: new error format drops the "E" prefix.+-- TODO: When DELTA_BYTE_ARRAY (encoding id=7) is implemented, change this+-- to assertEqual checking actual dimensions. The encoding prefix-differences+-- consecutive string values, reducing storage for sorted byte arrays:+-- https://parquet.apache.org/docs/file-format/data-pages/encodings/#delta-strings-delta_byte_array--7+deltaByteArray :: Test+deltaByteArray =+ TestCase+ ( assertExpectException+ "deltaByteArray"+ "DELTA_BYTE_ARRAY"+ (D.readParquet "./tests/data/delta_byte_array.parquet")+ )++-- Was: assertExpectException "deltaEncodingOptionalColumn" "EDELTA_BINARY_PACKED" ...+-- The first column that errors in this file uses DELTA_BYTE_ARRAY encoding,+-- so we match the broader "unsupported encoding" substring instead.+-- TODO: Once DELTA_BINARY_PACKED and DELTA_BYTE_ARRAY are both implemented,+-- change this to assertEqual checking the actual row count of+-- delta_encoding_optional_column.parquet.+deltaEncodingOptionalColumn :: Test+deltaEncodingOptionalColumn =+ TestCase+ ( assertExpectException+ "deltaEncodingOptionalColumn"+ "unsupported encoding"+ (D.readParquet "./tests/data/delta_encoding_optional_column.parquet")+ )++-- Was: assertExpectException "deltaEncodingRequiredColumn" "EDELTA_BINARY_PACKED" ...+-- Same as deltaEncodingOptionalColumn: first failing column uses DELTA_BYTE_ARRAY.+-- TODO: Same as deltaEncodingOptionalColumn — change to assertEqual once+-- DELTA_BINARY_PACKED and DELTA_BYTE_ARRAY encodings are both supported.+deltaEncodingRequiredColumn :: Test+deltaEncodingRequiredColumn =+ TestCase+ ( assertExpectException+ "deltaEncodingRequiredColumn"+ "unsupported encoding"+ (D.readParquet "./tests/data/delta_encoding_required_column.parquet")+ )++-- Was: assertExpectException "deltaLengthByteArray" "ZSTD" ...+-- The old parser failed during ZSTD decompression. The new parser+-- detects the unsupported DELTA_LENGTH_BYTE_ARRAY encoding before decompression.+-- TODO: When DELTA_LENGTH_BYTE_ARRAY (encoding id=6) is implemented, change+-- this to assertEqual checking actual dimensions. The encoding stores a+-- delta-encoded list of byte-array lengths followed by the raw concatenated+-- values:+-- https://parquet.apache.org/docs/file-format/data-pages/encodings/#delta-length-byte-array-delta_length_byte_array--6+deltaLengthByteArray :: Test+deltaLengthByteArray =+ TestCase+ ( assertExpectException+ "deltaLengthByteArray"+ "DELTA_LENGTH_BYTE_ARRAY"+ (D.readParquet "./tests/data/delta_length_byte_array.parquet")+ )++-- Was: assertExpectException "rleBooleanEncoding" "Zlib" ...+-- The old parser failed during Zlib decompression. The new parser+-- detects the unsupported RLE boolean encoding before reaching decompression.+-- TODO: When RLE/Bit-Packing Hybrid (encoding id=3, bit-width=1) is+-- implemented for BOOLEAN columns, change this to assertEqual checking the+-- actual decoded boolean values. The encoding is spec-valid for BOOLEAN:+-- https://parquet.apache.org/docs/file-format/data-pages/encodings/#run-length-encoding--bit-packing-hybrid-rle--3+rleBooleanEncoding :: Test+rleBooleanEncoding =+ TestCase+ ( assertExpectException+ "rleBooleanEncoding"+ "unsupported encoding RLE"+ (D.readParquet "./tests/data/rle_boolean_encoding.parquet")+ )++-- Was: assertExpectException "dictPageOffsetZero" "Unknown kv" ...+-- The old parser reported "Unknown kv" for a bad key-value field. The new+-- Pinch-based page-header parser reports "Field 1 is absent" for the+-- malformed page header in this file.+-- TODO: Investigate whether dict-page-offset-zero.parquet can be read+-- successfully with a more lenient page-header parser. If the missing+-- mandatory field can be treated as a per-page soft error rather than+-- aborting the whole read, this test would change to assertEqual+-- checking actual dimensions.+dictPageOffsetZero :: Test+dictPageOffsetZero =+ TestCase+ ( assertExpectException+ "dictPageOffsetZero"+ "Field 1 is absent"+ (D.readParquet "./tests/data/dict-page-offset-zero.parquet")+ )++-- ---------------------------------------------------------------------------+-- Group 4: Data Page V2 (unsupported → error tests)+-- ---------------------------------------------------------------------------++-- Was: assertExpectException "datapageV2Snappy" "InvalidOffset" ...+-- The old parser failed with an offset validation error. The new parser+-- first encounters the unsupported RLE encoding used by data-page-v2.+-- TODO: Full Data Page V2 support requires two changes:+-- 1. RLE/Bit-Packing Hybrid (id=3, bit-width=1) for BOOLEAN values+-- (shared with rleBooleanEncoding above).+-- 2. Parsing DataPageHeaderV2's in-line level streams: in v2, definition+-- and repetition levels are stored uncompressed before the (optionally+-- compressed) value bytes, with lengths given by+-- definition_levels_byte_length and repetition_levels_byte_length.+-- Once both are done, change to assertEqual checking actual dimensions:+-- https://parquet.apache.org/docs/file-format/data-pages/+datapageV2Snappy :: Test+datapageV2Snappy =+ TestCase+ ( assertExpectException+ "datapageV2Snappy"+ "unsupported encoding RLE"+ (D.readParquet "./tests/data/datapage_v2.snappy.parquet")+ )++-- Was: assertExpectException "datapageV2EmptyDatapage" "UnexpectedEOF" ...+-- The old Snappy decompressor raised "UnexpectedEOF". The new Snappy+-- library raises "EmptyInput" when given zero-length compressed data.+-- The v2 page structure is parsed correctly: readLevelsV2V strips the+-- in-line level streams before decompression, leaving an empty value+-- payload (BS.empty) for a page with 0 values. The Snappy decompressor+-- then raises "EmptyInput" because it is handed zero bytes.+-- TODO: An empty data page (0 values) is valid and should contribute+-- 0 rows without raising an error. The fix is a single guard in the+-- DATA_PAGE_V2 branch of readPages (Page.hs): short-circuit+-- decompressData when compValBytes is empty, returning BS.empty+-- directly. Once fixed, change this to assertEqual checking the+-- total expected row count of the file.+datapageV2EmptyDatapage :: Test+datapageV2EmptyDatapage =+ TestCase+ ( assertExpectException+ "datapageV2EmptyDatapage"+ "EmptyInput"+ (D.readParquet "./tests/data/datapage_v2_empty_datapage.snappy.parquet")+ )++-- Was: assertExpectException "pageV2EmptyCompressed" "10" ...+-- The old parser failed on empty compressed page-v2 blocks. The new parser+-- treats empty compressed data as zero-value pages and reads all 10 rows.+pageV2EmptyCompressed :: Test+pageV2EmptyCompressed =+ TestCase+ ( assertEqual+ "pageV2EmptyCompressed"+ (10, 1)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/page_v2_empty_compressed.parquet")+ )+ )+ )++-- ---------------------------------------------------------------------------+-- Group 5: Checksum files (all read successfully)+-- ---------------------------------------------------------------------------++datapageV1UncompressedChecksum :: Test+datapageV1UncompressedChecksum =+ TestCase+ ( assertEqual+ "datapageV1UncompressedChecksum"+ (5120, 2)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/datapage_v1-uncompressed-checksum.parquet")+ )+ )+ )++datapageV1SnappyChecksum :: Test+datapageV1SnappyChecksum =+ TestCase+ ( assertEqual+ "datapageV1SnappyChecksum"+ (5120, 2)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/datapage_v1-snappy-compressed-checksum.parquet")+ )+ )+ )++plainDictUncompressedChecksum :: Test+plainDictUncompressedChecksum =+ TestCase+ ( assertEqual+ "plainDictUncompressedChecksum"+ (1000, 2)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/plain-dict-uncompressed-checksum.parquet")+ )+ )+ )++rleDictSnappyChecksum :: Test+rleDictSnappyChecksum =+ TestCase+ ( assertEqual+ "rleDictSnappyChecksum"+ (1000, 2)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/rle-dict-snappy-checksum.parquet")+ )+ )+ )++-- TODO: CRC checksum validation is not yet implemented; corrupt page+-- checksums are silently ignored. When validation is added, consider a+-- validateChecksums :: Bool field in ParquetReadOptions (default False)+-- so callers can opt in. Once implemented, datapageV1CorruptChecksum and+-- rleDictUncompressedCorruptChecksum should change to assertExpectException+-- checking for a checksum mismatch error.+datapageV1CorruptChecksum :: Test+datapageV1CorruptChecksum =+ TestCase+ ( assertEqual+ "datapageV1CorruptChecksum"+ (5120, 2)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/datapage_v1-corrupt-checksum.parquet")+ )+ )+ )++rleDictUncompressedCorruptChecksum :: Test+rleDictUncompressedCorruptChecksum =+ TestCase+ ( assertEqual+ "rleDictUncompressedCorruptChecksum"+ (1000, 2)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/rle-dict-uncompressed-corrupt-checksum.parquet")+ )+ )+ )++-- ---------------------------------------------------------------------------+-- Group 6: NULL handling+-- ---------------------------------------------------------------------------++nullsSnappy :: Test+nullsSnappy =+ TestCase+ ( assertEqual+ "nullsSnappy"+ (8, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nulls.snappy.parquet"))+ )+ )++int32WithNullPages :: Test+int32WithNullPages =+ TestCase+ ( assertEqual+ "int32WithNullPages"+ (1000, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/int32_with_null_pages.parquet"))+ )+ )++nullableImpala :: Test+nullableImpala =+ TestCase+ ( assertEqual+ "nullableImpala"+ (7, 13)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nullable.impala.parquet"))+ )+ )++nonnullableImpala :: Test+nonnullableImpala =+ TestCase+ ( assertEqual+ "nonnullableImpala"+ (1, 13)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nonnullable.impala.parquet"))+ )+ )++singleNan :: Test+singleNan =+ TestCase+ ( assertEqual+ "singleNan"+ (1, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/single_nan.parquet"))+ )+ )++nanInStats :: Test+nanInStats =+ TestCase+ ( assertEqual+ "nanInStats"+ (2, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nan_in_stats.parquet"))+ )+ )++-- ---------------------------------------------------------------------------+-- Group 7: Decimal types+-- ---------------------------------------------------------------------------++int32Decimal :: Test+int32Decimal =+ TestCase+ ( assertEqual+ "int32Decimal"+ (24, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/int32_decimal.parquet"))+ )+ )++int64Decimal :: Test+int64Decimal =+ TestCase+ ( assertEqual+ "int64Decimal"+ (24, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/int64_decimal.parquet"))+ )+ )++byteArrayDecimal :: Test+byteArrayDecimal =+ TestCase+ ( assertEqual+ "byteArrayDecimal"+ (24, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/byte_array_decimal.parquet"))+ )+ )++-- Was: assertExpectException "fixedLengthDecimal" "FIXED_LEN_BYTE_ARRAY" ...+-- The old parser recognised FIXED_LEN_BYTE_ARRAY as a physical type but+-- had no page decoder for it; reading data from such a column threw an+-- error at the decoding stage. The new parser's fixedLenByteArrayDecoder+-- reads the raw bytes and surfaces them as a text column.+-- TODO: When the DECIMAL logical type is properly decoded for+-- FIXED_LEN_BYTE_ARRAY columns, replace this dimension-only check with a+-- value-level assertion verifying the actual decimal values (e.g. as+-- Scientific or Double). The raw-byte Text column should become a typed+-- numeric column.+fixedLengthDecimal :: Test+fixedLengthDecimal =+ TestCase+ ( assertEqual+ "fixedLengthDecimal"+ (24, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/fixed_length_decimal.parquet"))+ )+ )++-- Was: assertExpectException "fixedLengthDecimalLegacy" "FIXED_LEN_BYTE_ARRAY" ...+-- Same as fixedLengthDecimal: the old parser had no page decoder for+-- FIXED_LEN_BYTE_ARRAY; the new parser's fixedLenByteArrayDecoder handles it.+-- TODO: Same as fixedLengthDecimal — add a value-level assertion once+-- DECIMAL decoding over FIXED_LEN_BYTE_ARRAY is implemented.+fixedLengthDecimalLegacy :: Test+fixedLengthDecimalLegacy =+ TestCase+ ( assertEqual+ "fixedLengthDecimalLegacy"+ (24, 1)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/fixed_length_decimal_legacy.parquet")+ )+ )+ )++-- ---------------------------------------------------------------------------+-- Group 8: Binary / fixed-length bytes+-- ---------------------------------------------------------------------------++binaryFile :: Test+binaryFile =+ TestCase+ ( assertEqual+ "binaryFile"+ (12, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/binary.parquet"))+ )+ )++binaryTruncatedMinMax :: Test+binaryTruncatedMinMax =+ TestCase+ ( assertEqual+ "binaryTruncatedMinMax"+ (12, 6)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/binary_truncated_min_max.parquet")+ )+ )+ )++-- Was: assertExpectException "fixedLengthByteArray" "FIXED_LEN_BYTE_ARRAY" ...+-- Same as fixedLengthDecimal: the old parser had no page decoder for+-- FIXED_LEN_BYTE_ARRAY; the new parser's fixedLenByteArrayDecoder handles it.+fixedLengthByteArray :: Test+fixedLengthByteArray =+ TestCase+ ( assertEqual+ "fixedLengthByteArray"+ (1000, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/fixed_length_byte_array.parquet"))+ )+ )++-- ---------------------------------------------------------------------------+-- Group 9: INT96 timestamps+-- ---------------------------------------------------------------------------++int96FromSpark :: Test+int96FromSpark =+ TestCase+ ( assertEqual+ "int96FromSpark"+ (6, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/int96_from_spark.parquet"))+ )+ )++-- ---------------------------------------------------------------------------+-- Group 10: Metadata / index / bloom filters+-- ---------------------------------------------------------------------------++-- Was: assertExpectException "columnChunkKeyValueMetadata" "Unknown page header field" ...+-- The old parser rejected extra fields in page headers. Pinch ignores+-- unknown fields gracefully. This file contains 0 data rows.+columnChunkKeyValueMetadata :: Test+columnChunkKeyValueMetadata =+ TestCase+ ( assertEqual+ "columnChunkKeyValueMetadata"+ (0, 2)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/column_chunk_key_value_metadata.parquet")+ )+ )+ )++dataIndexBloomEncodingStats :: Test+dataIndexBloomEncodingStats =+ TestCase+ ( assertEqual+ "dataIndexBloomEncodingStats"+ (14, 1)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/data_index_bloom_encoding_stats.parquet")+ )+ )+ )++dataIndexBloomEncodingWithLength :: Test+dataIndexBloomEncodingWithLength =+ TestCase+ ( assertEqual+ "dataIndexBloomEncodingWithLength"+ (14, 1)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/data_index_bloom_encoding_with_length.parquet")+ )+ )+ )++-- Was: assertEqual "sortColumns" (3, 2) ...+-- The file contains two row groups, each storing 3 rows (6 rows total).+-- DuckDB's parquet-metadata output shows row_group_num_rows=3, which is+-- the count *per row group*, not the file total.row group*, not the file total.row group*, not the file total.row group*, not the file total.+-- https://github.com/apache/parquet-testing/blob/master/data/README.md#:~:text=sort_columns.parquet+-- The above link is to the repository the test parquet files comes from.+-- The table describes sort_columns.parquet as having two row groups.+-- The old parser only read the first row group (a bug). The new parser+-- reads all row groups and returns (6, 2) correctly.+sortColumns :: Test+sortColumns =+ TestCase+ ( assertEqual+ "sortColumns"+ (6, 2)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/sort_columns.parquet"))+ )+ )++-- Was: assertExpectException "overflowI16PageCnt" "UNIMPLEMENTED" ...+-- The old parser used Int16 for page counts and overflowed on this file.+-- The new parser uses Int32 and reads all 40,000 rows correctly.+overflowI16PageCnt :: Test+overflowI16PageCnt =+ TestCase+ ( assertEqual+ "overflowI16PageCnt"+ (40000, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/overflow_i16_page_cnt.parquet"))+ )+ )++-- ---------------------------------------------------------------------------+-- Group 11: Nested / complex types and byte-stream-split+-- ---------------------------------------------------------------------------++-- Was: assertExpectException "byteStreamSplitZstd" "EBYTE_STREAM_SPLIT" ...+-- The new parser's error includes the encoding name "BYTE_STREAM_SPLIT"+-- without the old "E" prefix used in the previous error format.+-- TODO: When BYTE_STREAM_SPLIT (encoding id=9) is implemented, change this+-- to assertEqual checking actual dimensions. The encoding interleaves the+-- individual byte streams of multi-byte scalars to improve compression for+-- floating-point and other structured data:+-- https://parquet.apache.org/docs/file-format/data-pages/encodings/#byte-stream-split-byte_stream_split--9+byteStreamSplitZstd :: Test+byteStreamSplitZstd =+ TestCase+ ( assertExpectException+ "byteStreamSplitZstd"+ "BYTE_STREAM_SPLIT"+ (D.readParquet "./tests/data/byte_stream_split.zstd.parquet")+ )++-- Was: assertExpectException "byteStreamSplitExtendedGzip" "FIXED_LEN_BYTE_ARRAY" ...+-- The old parser had no page decoder for FIXED_LEN_BYTE_ARRAY and threw+-- before ever inspecting the encoding. The new parser handles the physical+-- type but the BYTE_STREAM_SPLIT encoding used for values is not yet+-- implemented, so the error message shifts from the type to the encoding.+-- TODO: Same as byteStreamSplitZstd — change to assertEqual once+-- BYTE_STREAM_SPLIT encoding is supported.+byteStreamSplitExtendedGzip :: Test+byteStreamSplitExtendedGzip =+ TestCase+ ( assertExpectException+ "byteStreamSplitExtendedGzip"+ "BYTE_STREAM_SPLIT"+ (D.readParquet "./tests/data/byte_stream_split_extended.gzip.parquet")+ )++-- Was: assertExpectException "float16NonzerosAndNans" "PFIXED_LEN_BYTE_ARRAY" ...+-- The "PFIXED_LEN_BYTE_ARRAY" in the old error was the Show of the old+-- parser's ParquetType enum hitting a catch-all dispatch branch — it+-- recognised the physical type but had no decoder for it. The new parser's+-- fixedLenByteArrayDecoder reads 2-byte FIXED_LEN_BYTE_ARRAY (float16)+-- columns as raw-byte text; proper float16 value decoding is not yet+-- implemented.+-- TODO: When IEEE 754 half-precision (float16) decoding is implemented,+-- add a value-level assertion using hasElemType @Float (or a dedicated+-- Float16 type if one is introduced). Verify that the decoded values match+-- the known reference values for float16_nonzeros_and_nans.parquet.+-- The column should no longer be exposed as raw-byte Text.+float16NonzerosAndNans :: Test+float16NonzerosAndNans =+ TestCase+ ( assertEqual+ "float16NonzerosAndNans"+ (8, 1)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/float16_nonzeros_and_nans.parquet")+ )+ )+ )++-- Was: assertExpectException "float16ZerosAndNans" "PFIXED_LEN_BYTE_ARRAY" ...+-- Same as float16NonzerosAndNans: old parser had no decoder for the+-- FIXED_LEN_BYTE_ARRAY physical type; new parser reads raw bytes as text.+-- TODO: Same as float16NonzerosAndNans — add a value-level assertion once+-- float16 decoding is implemented.+float16ZerosAndNans :: Test+float16ZerosAndNans =+ TestCase+ ( assertEqual+ "float16ZerosAndNans"+ (3, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/float16_zeros_and_nans.parquet"))+ )+ )++nestedListsSnappy :: Test+nestedListsSnappy =+ TestCase+ ( assertEqual+ "nestedListsSnappy"+ (3, 2)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nested_lists.snappy.parquet"))+ )+ )++nestedMapsSnappy :: Test+nestedMapsSnappy =+ TestCase+ ( assertEqual+ "nestedMapsSnappy"+ (6, 5)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nested_maps.snappy.parquet"))+ )+ )++nestedStructsRust :: Test+nestedStructsRust =+ TestCase+ ( assertEqual+ "nestedStructsRust"+ (1, 216)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nested_structs.rust.parquet"))+ )+ )++listColumns :: Test+listColumns =+ TestCase+ ( assertEqual+ "listColumns"+ (3, 2)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/list_columns.parquet"))+ )+ )++oldListStructure :: Test+oldListStructure =+ TestCase+ ( assertEqual+ "oldListStructure"+ (1, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/old_list_structure.parquet"))+ )+ )++nullList :: Test+nullList =+ TestCase+ ( assertEqual+ "nullList"+ (1, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/null_list.parquet"))+ )+ )++mapNoValue :: Test+mapNoValue =+ TestCase+ ( assertEqual+ "mapNoValue"+ (3, 4)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/map_no_value.parquet"))+ )+ )++incorrectMapSchema :: Test+incorrectMapSchema =+ TestCase+ ( assertEqual+ "incorrectMapSchema"+ (1, 2)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/incorrect_map_schema.parquet"))+ )+ )++repeatedNoAnnotation :: Test+repeatedNoAnnotation =+ TestCase+ ( assertEqual+ "repeatedNoAnnotation"+ (6, 3)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/repeated_no_annotation.parquet"))+ )+ )++repeatedPrimitiveNoList :: Test+repeatedPrimitiveNoList =+ TestCase+ ( assertEqual+ "repeatedPrimitiveNoList"+ (4, 4)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/repeated_primitive_no_list.parquet")+ )+ )+ )++-- Was: assertExpectException "unknownLogicalType" "Unknown logical type" ...+-- The old parser raised a custom "Unknown logical type" message. The new+-- Pinch-based metadata parser raises "Field 16 is absent" for the+-- unrecognised LogicalType variant in this file.+-- TODO: If Pinch is extended to support forward-compatible decoding of+-- unknown union variants (treating unrecognised logical-type IDs as absent+-- rather than raising an error), change this to assertEqual where the file+-- parses successfully and the column falls back to its physical type.+unknownLogicalType :: Test+unknownLogicalType =+ TestCase+ ( assertExpectException+ "unknownLogicalType"+ "Field 16 is absent"+ (D.readParquet "./tests/data/unknown-logical-type.parquet")+ )++-- ---------------------------------------------------------------------------+-- Group 12: Malformed files+-- ---------------------------------------------------------------------------++-- Was: assertExpectException "nationDictMalformed" "dict index count mismatch" ...+-- The old parser validated the dictionary entry count against data-page+-- indices and raised "dict index count mismatch". The new parser does not+-- replicate that check; the dictionary bytes happen to decode correctly+-- despite the metadata discrepancy, returning the complete 25-row dataset.+-- TODO: If a stricter dictionary-validation pass is added (checking that+-- the number of decoded entries matches num_values in the dictionary page+-- header), revert this to assertExpectException with a count-mismatch+-- substring.+nationDictMalformed :: Test+nationDictMalformed =+ TestCase+ ( assertEqual+ "nationDictMalformed"+ (25, 4)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nation.dict-malformed.parquet"))+ )+ )++shardedNullableSchema :: Test+shardedNullableSchema =+ TestCase $ do+ metas <-+ mapM+ DP.readMetadataFromPath+ ["data/sharded/part-0.parquet", "data/sharded/part-1.parquet"]+ let nullableCols =+ S.fromList+ [ last (map T.pack colPath)+ | meta <- metas+ , rg <- unField meta.row_groups+ , cc <- unField rg.rg_columns+ , Just cm <- [unField cc.cc_meta_data]+ , let colPath = map T.unpack (unField cm.cmd_path_in_schema)+ , not (null colPath)+ , let nc :: Int64+ nc = case unField cm.cmd_statistics of+ Nothing -> 0+ Just stats -> fromMaybe 0 (unField stats.stats_null_count)+ , nc > 0+ ]+ df =+ foldl+ (\acc meta -> acc <> PS.schemaToEmptyDataFrame nullableCols (unField meta.schema))+ D.empty+ metas+ assertBool "id should be nullable" (hasMissing (unsafeGetColumn "id" df))+ assertBool "name should be nullable" (hasMissing (unsafeGetColumn "name" df))+ assertBool "score should be nullable" (hasMissing (unsafeGetColumn "score" df))++singleShardNoNulls :: Test+singleShardNoNulls =+ TestCase $ do+ meta <- DP.readMetadataFromPath "data/sharded/part-0.parquet"+ let nullableCols =+ S.fromList+ [ last (map T.pack colPath)+ | rg <- unField meta.row_groups+ , cc <- unField rg.rg_columns+ , Just cm <- [unField cc.cc_meta_data]+ , let colPath = map T.unpack (unField cm.cmd_path_in_schema)+ , not (null colPath)+ , let nc :: Int64+ nc = case unField cm.cmd_statistics of+ Nothing -> 0+ Just stats -> fromMaybe 0 (unField stats.stats_null_count)+ , nc > 0+ ]+ df = PS.schemaToEmptyDataFrame nullableCols (unField meta.schema)+ assertBool+ "id should NOT be nullable"+ (not (hasMissing (unsafeGetColumn "id" df)))+ assertBool+ "name should NOT be nullable"+ (not (hasMissing (unsafeGetColumn "name" df)))+ assertBool+ "score should NOT be nullable"+ (not (hasMissing (unsafeGetColumn "score" df)))++{- | Nanosecond-precision timestamps (@TIMESTAMP(NANOS)@) must decode to the+correct 'UTCTime'. Regression test for the unit-scaling bug where the NANOS+multiplier @1_000_000 \`div\` 1_000_000_000@ truncated to 0, collapsing every+value to the epoch.+-}+timestampNanos :: Test+timestampNanos = testBothReadParquetPaths $ \readParquet ->+ TestCase $+ assertEqual+ "TIMESTAMP(NANOS) decodes to correct UTCTime"+ ( D.fromNamedColumns+ [+ ( "ts"+ , D.fromList+ [ UTCTime (fromGregorian 2020 1 1) 0+ , UTCTime (fromGregorian 2021 6 15) (picosecondsToDiffTime 45045123456789000)+ , UTCTime (fromGregorian 1999 12 31) (picosecondsToDiffTime 86399999999999000)+ ]+ )+ ]+ )+ (unsafePerformIO (readParquet "./tests/data/timestamp_nanos.parquet"))++tests :: [Test]+tests =+ [ timestampNanos+ , allTypesPlain+ , allTypesPlainSnappy+ , allTypesDictionary+ , selectedColumnsWithOpts+ , rowRangeWithOpts+ , predicateWithOpts+ , predicateUsesNonSelectedColumnWithOpts+ , safeColumnsWithOpts+ , safeColumnsWithSelectedColumns+ , predicateWithOptsAcrossFiles+ , missingSelectedColumnWithOpts+ , mtCars+ , allTypesTinyPagesLastFew+ , allTypesTinyPagesDimensions+ , transactionsTest+ , littleEndianWord64KnownPattern+ , littleEndianWord32KnownPattern+ , littleEndianWord64ShortInputPadsZeroes+ , littleEndianWord32ShortInputPadsZeroes+ , littleEndianWord64RoundTrip+ , littleEndianWord32RoundTrip+ , -- Group 1+ allTypesTinyPagesPlain+ , -- Group 2: compression codecs+ hadoopLz4Compressed+ , hadoopLz4CompressedLarger+ , nonHadoopLz4Compressed+ , lz4RawCompressed+ , lz4RawCompressedLarger+ , concatenatedGzipMembers+ , largeBrotliMap+ , -- Group 3: delta / rle encodings+ deltaBinaryPacked+ , deltaByteArray+ , deltaEncodingOptionalColumn+ , deltaEncodingRequiredColumn+ , deltaLengthByteArray+ , rleBooleanEncoding+ , dictPageOffsetZero+ , -- Group 4: Data Page V2+ datapageV2Snappy+ , datapageV2EmptyDatapage+ , pageV2EmptyCompressed+ , -- Group 5: checksum files+ datapageV1UncompressedChecksum+ , datapageV1SnappyChecksum+ , plainDictUncompressedChecksum+ , rleDictSnappyChecksum+ , datapageV1CorruptChecksum+ , rleDictUncompressedCorruptChecksum+ , -- Group 6: NULL handling+ nullsSnappy+ , int32WithNullPages+ , nullableImpala+ , nonnullableImpala+ , singleNan+ , nanInStats+ , -- Group 7: decimal types+ int32Decimal+ , int64Decimal+ , byteArrayDecimal+ , fixedLengthDecimal+ , fixedLengthDecimalLegacy+ , -- Group 8: binary / fixed-length bytes+ binaryFile+ , binaryTruncatedMinMax+ , fixedLengthByteArray+ , -- Group 9: INT96 timestamps+ int96FromSpark+ , -- Group 10: metadata / bloom filters+ columnChunkKeyValueMetadata+ , dataIndexBloomEncodingStats+ , dataIndexBloomEncodingWithLength+ , sortColumns+ , overflowI16PageCnt+ , -- Group 11: nested / complex types+ byteStreamSplitZstd+ , byteStreamSplitExtendedGzip+ , float16NonzerosAndNans+ , float16ZerosAndNans+ , nestedListsSnappy+ , nestedMapsSnappy+ , nestedStructsRust+ , listColumns+ , oldListStructure+ , nullList+ , mapNoValue+ , incorrectMapSchema+ , repeatedNoAnnotation+ , repeatedPrimitiveNoList+ , unknownLogicalType+ , -- Group 12: malformed files+ nationDictMalformed+ , -- Group 13: metadata-based null detection+ shardedNullableSchema+ , singleShardNoNulls ]
+ tests/ParquetTestData.hs view
@@ -0,0 +1,727 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Large DataFrame fixtures used by the Parquet test suite.+Kept in a separate module to avoid cluttering Parquet.hs.+-}+module ParquetTestData (+ allTypes,+ tinyPagesLast10,+ transactions,+ mtCarsDataset,+) where++import Data.Int+import Data.Text (Text)+import Data.Time+import qualified DataFrame as D++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{utctDay = fromGregorian 2009 3 1, utctDayTime = secondsToDiffTime 0}+ , UTCTime{utctDay = fromGregorian 2009 3 1, utctDayTime = secondsToDiffTime 60}+ , UTCTime{utctDay = fromGregorian 2009 4 1, utctDayTime = secondsToDiffTime 0}+ , UTCTime{utctDay = fromGregorian 2009 4 1, utctDayTime = secondsToDiffTime 60}+ , UTCTime{utctDay = fromGregorian 2009 2 1, utctDayTime = secondsToDiffTime 0}+ , UTCTime{utctDay = fromGregorian 2009 2 1, utctDayTime = secondsToDiffTime 60}+ , UTCTime{utctDay = fromGregorian 2009 1 1, utctDayTime = secondsToDiffTime 0}+ , UTCTime{utctDay = fromGregorian 2009 1 1, utctDayTime = secondsToDiffTime 60}+ ]+ )+ ]++tinyPagesLast10 :: D.DataFrame+tinyPagesLast10 =+ D.fromNamedColumns+ [ ("id", D.fromList @Int32 (reverse [6174 .. 6183]))+ , ("bool_col", D.fromList @Bool (take 10 (cycle [False, True])))+ , ("tinyint_col", D.fromList @Int32 [3, 2, 1, 0, 9, 8, 7, 6, 5, 4])+ , ("smallint_col", D.fromList @Int32 [3, 2, 1, 0, 9, 8, 7, 6, 5, 4])+ , ("int_col", D.fromList @Int32 [3, 2, 1, 0, 9, 8, 7, 6, 5, 4])+ , ("bigint_col", D.fromList @Int64 [30, 20, 10, 0, 90, 80, 70, 60, 50, 40])+ ,+ ( "float_col"+ , D.fromList @Float [3.3, 2.2, 1.1, 0, 9.9, 8.8, 7.7, 6.6, 5.5, 4.4]+ )+ ,+ ( "date_string_col"+ , D.fromList @Text+ [ "09/11/10"+ , "09/11/10"+ , "09/11/10"+ , "09/11/10"+ , "09/10/10"+ , "09/10/10"+ , "09/10/10"+ , "09/10/10"+ , "09/10/10"+ , "09/10/10"+ ]+ )+ ,+ ( "string_col"+ , D.fromList @Text ["3", "2", "1", "0", "9", "8", "7", "6", "5", "4"]+ )+ ,+ ( "timestamp_col"+ , D.fromList @UTCTime+ [ UTCTime+ { utctDay = fromGregorian 2010 9 10+ , utctDayTime = secondsToDiffTime 85384+ }+ , UTCTime+ { utctDay = fromGregorian 2010 9 10+ , utctDayTime = secondsToDiffTime 85324+ }+ , UTCTime+ { utctDay = fromGregorian 2010 9 10+ , utctDayTime = secondsToDiffTime 85264+ }+ , UTCTime+ { utctDay = fromGregorian 2010 9 10+ , utctDayTime = secondsToDiffTime 85204+ }+ , UTCTime{utctDay = fromGregorian 2010 9 9, utctDayTime = secondsToDiffTime 85144}+ , UTCTime{utctDay = fromGregorian 2010 9 9, utctDayTime = secondsToDiffTime 85084}+ , UTCTime{utctDay = fromGregorian 2010 9 9, utctDayTime = secondsToDiffTime 85024}+ , UTCTime{utctDay = fromGregorian 2010 9 9, utctDayTime = secondsToDiffTime 84964}+ , UTCTime{utctDay = fromGregorian 2010 9 9, utctDayTime = secondsToDiffTime 84904}+ , UTCTime{utctDay = fromGregorian 2010 9 9, utctDayTime = secondsToDiffTime 84844}+ ]+ )+ , ("year", D.fromList @Int32 (replicate 10 2010))+ , ("month", D.fromList @Int32 (replicate 10 9))+ ]++transactions :: D.DataFrame+transactions =+ D.fromNamedColumns+ [ ("transaction_id", D.fromList [1 :: Int32, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])+ ,+ ( "event_time"+ , D.fromList+ [ UTCTime+ { utctDay = fromGregorian 2024 1 3+ , utctDayTime = secondsToDiffTime 29564 + picosecondsToDiffTime 2311000000+ }+ , UTCTime+ { utctDay = fromGregorian 2024 1 3+ , utctDayTime = secondsToDiffTime 35101 + picosecondsToDiffTime 118900000000+ }+ , UTCTime+ { utctDay = fromGregorian 2024 1 4+ , utctDayTime = secondsToDiffTime 39802 + picosecondsToDiffTime 774512000000+ }+ , UTCTime+ { utctDay = fromGregorian 2024 1 5+ , utctDayTime = secondsToDiffTime 53739 + picosecondsToDiffTime 1000000+ }+ , UTCTime+ { utctDay = fromGregorian 2024 1 6+ , utctDayTime = secondsToDiffTime 8278 + picosecondsToDiffTime 543210000000+ }+ , UTCTime+ { utctDay = fromGregorian 2024 1 6+ , utctDayTime = secondsToDiffTime 8284 + picosecondsToDiffTime 211000000000+ }+ , UTCTime{utctDay = fromGregorian 2024 1 7, utctDayTime = secondsToDiffTime 63000}+ , UTCTime+ { utctDay = fromGregorian 2024 1 8+ , utctDayTime = secondsToDiffTime 24259 + picosecondsToDiffTime 390000000000+ }+ , UTCTime+ { utctDay = fromGregorian 2024 1 9+ , utctDayTime = secondsToDiffTime 48067 + picosecondsToDiffTime 812345000000+ }+ , UTCTime+ { utctDay = fromGregorian 2024 1 10+ , utctDayTime = secondsToDiffTime 82799 + picosecondsToDiffTime 999999000000+ }+ , UTCTime+ { utctDay = fromGregorian 2024 1 11+ , utctDayTime = secondsToDiffTime 36000 + picosecondsToDiffTime 100000000000+ }+ , UTCTime+ { utctDay = fromGregorian 2024 1 12+ , utctDayTime = secondsToDiffTime 56028 + picosecondsToDiffTime 667891000000+ }+ ]+ )+ ,+ ( "user_email"+ , D.fromList+ [ "alice@example.com" :: Text+ , "bob@example.com"+ , "carol@example.com"+ , "alice@example.com"+ , "dave@example.com"+ , "dave@example.com"+ , "eve@example.com"+ , "frank@example.com"+ , "grace@example.com"+ , "dave@example.com"+ , "alice@example.com"+ , "heidi@example.com"+ ]+ )+ ,+ ( "transaction_type"+ , D.fromList+ [ "purchase" :: Text+ , "purchase"+ , "refund"+ , "purchase"+ , "purchase"+ , "purchase"+ , "purchase"+ , "withdrawal"+ , "purchase"+ , "purchase"+ , "purchase"+ , "refund"+ ]+ )+ ,+ ( "amount"+ , D.fromList+ [ 142.50 :: Double+ , 29.99+ , 89.00+ , 2399.00+ , 15.00+ , 15.00+ , 450.75+ , 200.00+ , 55.20+ , 3200.00+ , 74.99+ , 120.00+ ]+ )+ ,+ ( "currency"+ , D.fromList+ [ "USD" :: Text+ , "USD"+ , "EUR"+ , "USD"+ , "GBP"+ , "GBP"+ , "USD"+ , "EUR"+ , "CAD"+ , "USD"+ , "USD"+ , "GBP"+ ]+ )+ ,+ ( "status"+ , D.fromList+ [ "approved" :: Text+ , "approved"+ , "approved"+ , "declined"+ , "approved"+ , "declined"+ , "approved"+ , "approved"+ , "approved"+ , "flagged"+ , "approved"+ , "approved"+ ]+ )+ ,+ ( "location"+ , D.fromList+ [ "New York, US" :: Text+ , "London, GB"+ , "Berlin, DE"+ , "New York, US"+ , "Manchester, GB"+ , "Lagos, NG"+ , "San Francisco, US"+ , "Paris, FR"+ , "Toronto, CA"+ , "New York, US"+ , "New York, US"+ , "Edinburgh, GB"+ ]+ )+ ]++mtCarsDataset :: D.DataFrame+mtCarsDataset =+ D.fromNamedColumns+ [+ ( "model"+ , D.fromList+ [ "Mazda RX4" :: Text+ , "Mazda RX4 Wag"+ , "Datsun 710"+ , "Hornet 4 Drive"+ , "Hornet Sportabout"+ , "Valiant"+ , "Duster 360"+ , "Merc 240D"+ , "Merc 230"+ , "Merc 280"+ , "Merc 280C"+ , "Merc 450SE"+ , "Merc 450SL"+ , "Merc 450SLC"+ , "Cadillac Fleetwood"+ , "Lincoln Continental"+ , "Chrysler Imperial"+ , "Fiat 128"+ , "Honda Civic"+ , "Toyota Corolla"+ , "Toyota Corona"+ , "Dodge Challenger"+ , "AMC Javelin"+ , "Camaro Z28"+ , "Pontiac Firebird"+ , "Fiat X1-9"+ , "Porsche 914-2"+ , "Lotus Europa"+ , "Ford Pantera L"+ , "Ferrari Dino"+ , "Maserati Bora"+ , "Volvo 142E"+ ]+ )+ ,+ ( "mpg"+ , D.fromList+ [ 21.0 :: Double+ , 21.0+ , 22.8+ , 21.4+ , 18.7+ , 18.1+ , 14.3+ , 24.4+ , 22.8+ , 19.2+ , 17.8+ , 16.4+ , 17.3+ , 15.2+ , 10.4+ , 10.4+ , 14.7+ , 32.4+ , 30.4+ , 33.9+ , 21.5+ , 15.5+ , 15.2+ , 13.3+ , 19.2+ , 27.3+ , 26.0+ , 30.4+ , 15.8+ , 19.7+ , 15.0+ , 21.4+ ]+ )+ ,+ ( "cyl"+ , D.fromList+ [ 6 :: Int32+ , 6+ , 4+ , 6+ , 8+ , 6+ , 8+ , 4+ , 4+ , 6+ , 6+ , 8+ , 8+ , 8+ , 8+ , 8+ , 8+ , 4+ , 4+ , 4+ , 4+ , 8+ , 8+ , 8+ , 8+ , 4+ , 4+ , 4+ , 8+ , 6+ , 8+ , 4+ ]+ )+ ,+ ( "disp"+ , D.fromList+ [ 160.0 :: Double+ , 160.0+ , 108.0+ , 258.0+ , 360.0+ , 225.0+ , 360.0+ , 146.7+ , 140.8+ , 167.6+ , 167.6+ , 275.8+ , 275.8+ , 275.8+ , 472.0+ , 460.0+ , 440.0+ , 78.7+ , 75.7+ , 71.1+ , 120.1+ , 318.0+ , 304.0+ , 350.0+ , 400.0+ , 79.0+ , 120.3+ , 95.1+ , 351.0+ , 145.0+ , 301.0+ , 121.0+ ]+ )+ ,+ ( "hp"+ , D.fromList+ [ 110 :: Int32+ , 110+ , 93+ , 110+ , 175+ , 105+ , 245+ , 62+ , 95+ , 123+ , 123+ , 180+ , 180+ , 180+ , 205+ , 215+ , 230+ , 66+ , 52+ , 65+ , 97+ , 150+ , 150+ , 245+ , 175+ , 66+ , 91+ , 113+ , 264+ , 175+ , 335+ , 109+ ]+ )+ ,+ ( "drat"+ , D.fromList+ [ 3.9 :: Double+ , 3.9+ , 3.85+ , 3.08+ , 3.15+ , 2.76+ , 3.21+ , 3.69+ , 3.92+ , 3.92+ , 3.92+ , 3.07+ , 3.07+ , 3.07+ , 2.93+ , 3.0+ , 3.23+ , 4.08+ , 4.93+ , 4.22+ , 3.7+ , 2.76+ , 3.15+ , 3.73+ , 3.08+ , 4.08+ , 4.43+ , 3.77+ , 4.22+ , 3.62+ , 3.54+ , 4.11+ ]+ )+ ,+ ( "wt"+ , D.fromList+ [ 2.62 :: Double+ , 2.875+ , 2.32+ , 3.215+ , 3.44+ , 3.46+ , 3.57+ , 3.19+ , 3.15+ , 3.44+ , 3.44+ , 4.07+ , 3.73+ , 3.78+ , 5.25+ , 5.424+ , 5.345+ , 2.2+ , 1.615+ , 1.835+ , 2.465+ , 3.52+ , 3.435+ , 3.84+ , 3.845+ , 1.935+ , 2.14+ , 1.513+ , 3.17+ , 2.77+ , 3.57+ , 2.78+ ]+ )+ ,+ ( "qsec"+ , D.fromList+ [ 16.46 :: Double+ , 17.02+ , 18.61+ , 19.44+ , 17.02+ , 20.22+ , 15.84+ , 20.0+ , 22.9+ , 18.3+ , 18.9+ , 17.4+ , 17.6+ , 18.0+ , 17.98+ , 17.82+ , 17.42+ , 19.47+ , 18.52+ , 19.9+ , 20.01+ , 16.87+ , 17.3+ , 15.41+ , 17.05+ , 18.9+ , 16.7+ , 16.9+ , 14.5+ , 15.5+ , 14.6+ , 18.6+ ]+ )+ ,+ ( "vs"+ , D.fromList+ [ 0 :: Int32+ , 0+ , 1+ , 1+ , 0+ , 1+ , 0+ , 1+ , 1+ , 1+ , 1+ , 0+ , 0+ , 0+ , 0+ , 0+ , 0+ , 1+ , 1+ , 1+ , 1+ , 0+ , 0+ , 0+ , 0+ , 1+ , 0+ , 1+ , 0+ , 0+ , 0+ , 1+ ]+ )+ ,+ ( "am"+ , D.fromList+ [ 1 :: Int32+ , 1+ , 1+ , 0+ , 0+ , 0+ , 0+ , 0+ , 0+ , 0+ , 0+ , 0+ , 0+ , 0+ , 0+ , 0+ , 0+ , 1+ , 1+ , 1+ , 0+ , 0+ , 0+ , 0+ , 0+ , 1+ , 1+ , 1+ , 1+ , 1+ , 1+ , 1+ ]+ )+ ,+ ( "gear"+ , D.fromList+ [ 4 :: Int32+ , 4+ , 4+ , 3+ , 3+ , 3+ , 3+ , 4+ , 4+ , 4+ , 4+ , 3+ , 3+ , 3+ , 3+ , 3+ , 3+ , 4+ , 4+ , 4+ , 3+ , 3+ , 3+ , 3+ , 3+ , 4+ , 5+ , 5+ , 5+ , 5+ , 5+ , 4+ ]+ )+ ,+ ( "carb"+ , D.fromList+ [ 4 :: Int32+ , 4+ , 1+ , 1+ , 2+ , 1+ , 4+ , 2+ , 2+ , 4+ , 4+ , 3+ , 3+ , 3+ , 4+ , 4+ , 4+ , 1+ , 2+ , 1+ , 1+ , 2+ , 2+ , 4+ , 2+ , 1+ , 2+ , 2+ , 4+ , 6+ , 8+ , 2+ ]+ )+ ]
+ tests/Plotting.hs view
@@ -0,0 +1,233 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DisambiguateRecordFields #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- |+Tests for the Vega-Lite web plotting backend: field-type inference, the+box-plot mark fix, NaN handling, escaping, computed-expression encodings, and+typed/untyped spec parity.+-}+module Plotting (tests) where++import Data.Aeson (Value (Array, Null, Object, String), toJSON)+import qualified Data.Aeson.Key as K+import qualified Data.Aeson.KeyMap as KM+import Data.Function ((&))+import qualified Data.List as L+import Data.Maybe (fromMaybe, isJust)+import qualified Data.Text as T+import qualified Data.Vector as V+import Test.HUnit++import qualified DataFrame as D+import DataFrame.Functions (col)+import qualified DataFrame.Typed as DT++import qualified DataFrame.Display.Web.Chart as C+import qualified DataFrame.Display.Web.Chart.Typed as CT+import qualified DataFrame.Display.Web.Plot as P++-- Unqualified so the record update below has a single 'includeZero' field+-- label in scope; GHC < 9.8 can't disambiguate the qualified duplicate field.+import DataFrame.Display.Web.Plot (Scatter (includeZero))++-- ---------------------------------------------------------------------------+-- Fixtures + JSON helpers+-- ---------------------------------------------------------------------------++numFrame :: D.DataFrame+numFrame =+ D.fromNamedColumns+ [ ("a", D.fromList ([1.0, 2.0, 3.0, 4.0] :: [Double]))+ , ("b", D.fromList ([10.0, 20.0, 30.0, 40.0] :: [Double]))+ ]++mixedFrame :: D.DataFrame+mixedFrame =+ D.fromNamedColumns+ [ ("a", D.fromList ([1.0, 2.0, 3.0] :: [Double]))+ , ("g", D.fromList (["x", "y", "x"] :: [T.Text]))+ ]++lookupKey :: T.Text -> Value -> Maybe Value+lookupKey k (Object o) = KM.lookup (K.fromText k) o+lookupKey _ _ = Nothing++jpath :: [T.Text] -> Value -> Maybe Value+jpath ks v = foldl (\mv k -> mv >>= lookupKey k) (Just v) ks++dataValues :: Value -> V.Vector Value+dataValues spec = case jpath ["data", "values"] spec of+ Just (Array xs) -> xs+ _ -> V.empty++-- ---------------------------------------------------------------------------+-- Test cases+-- ---------------------------------------------------------------------------++fieldTypeInference :: Test+fieldTypeInference = TestCase $ do+ let spec =+ C.toVegaSpec+ ( C.chart mixedFrame+ & C.mark C.Point+ & C.enc C.X (col @Double "a")+ & C.enc C.Y (col @T.Text "g")+ )+ assertEqual+ "numeric column -> quantitative"+ (Just (String "quantitative"))+ (jpath ["encoding", "x", "type"] spec)+ assertEqual+ "text column -> nominal"+ (Just (String "nominal"))+ (jpath ["encoding", "y", "type"] spec)++boxIsBoxplot :: Test+boxIsBoxplot = TestCase $ do+ let spec =+ C.toVegaSpec (C.chart numFrame & C.mark C.Boxplot & C.enc C.Y (col @Double "a"))+ assertEqual+ "Chart box uses boxplot mark"+ (Just (String "boxplot"))+ (jpath ["mark", "type"] spec)++legacyBoxIsBoxplot :: Test+legacyBoxIsBoxplot = TestCase $ do+ html <- P.box (P.mkBox ["a", "b"]) numFrame+ assertBool+ "legacy box HTML mentions the boxplot mark"+ ("boxplot" `L.isInfixOf` html)+ assertBool+ "legacy box HTML no longer claims 'showing medians'"+ (not ("showing medians" `L.isInfixOf` html))++nanBecomesNull :: Test+nanBecomesNull = TestCase $ do+ let df = D.fromNamedColumns [("a", D.fromList ([0 / 0, 1.0] :: [Double]))]+ spec = C.toVegaSpec (C.chart df & C.enc C.Y (col @Double "a"))+ firstA = lookupKey "a" (fromMaybe Null (dataValues spec V.!? 0))+ assertEqual "NaN inlines as null" (Just Null) firstA++escapingSafe :: Test+escapingSafe = TestCase $ do+ let weird = "we\"ir\\d"+ df = D.fromNamedColumns [(weird, D.fromList ([1.0, 2.0] :: [Double]))]+ spec = C.toVegaSpec (C.chart df & C.enc C.X (col @Double weird))+ row0 = fromMaybe Null (dataValues spec V.!? 0)+ assertBool+ "weird column name present as a data key"+ (Data.Maybe.isJust (lookupKey weird row0))+ assertEqual+ "encoding references the weird field name"+ (Just (String weird))+ (jpath ["encoding", "x", "field"] spec)++computedExpr :: Test+computedExpr = TestCase $ do+ let spec =+ C.toVegaSpec+ (C.chart numFrame & C.enc C.Y (col @Double "a" + col @Double "a"))+ row0 = fromMaybe Null (dataValues spec V.!? 0)+ assertEqual+ "computed field named after channel"+ (Just (String "y"))+ (jpath ["encoding", "y", "field"] spec)+ assertEqual+ "computed value is a + a = 2"+ (Just (toJSON (2.0 :: Double)))+ (lookupKey "y" row0)++includeZeroChart :: Test+includeZeroChart = TestCase $ do+ let spec =+ C.toVegaSpec+ ( C.chart numFrame+ & C.mark C.Point+ & C.enc C.X (col @Double "a")+ & C.enc C.Y (col @Double "b")+ & C.includeZero C.X False+ )+ assertEqual+ "x scale drops the zero anchor"+ (Just (toJSON False))+ (jpath ["encoding", "x", "scale", "zero"] spec)+ assertEqual+ "y scale untouched"+ Nothing+ (jpath ["encoding", "y", "scale"] spec)++includeZeroMergesWithLog :: Test+includeZeroMergesWithLog = TestCase $ do+ let spec =+ C.toVegaSpec+ ( C.chart numFrame+ & C.enc C.Y (col @Double "a")+ & C.logScale C.Y+ & C.includeZero C.Y False+ )+ assertEqual+ "log scale survives alongside the zero flag"+ (Just (String "log"))+ (jpath ["encoding", "y", "scale", "type"] spec)+ assertEqual+ "zero flag lands on the same scale object"+ (Just (toJSON False))+ (jpath ["encoding", "y", "scale", "zero"] spec)++scatterFitsAxesByDefault :: Test+scatterFitsAxesByDefault = TestCase $ do+ html <- P.scatter (P.mkScatter "a" "b") numFrame+ assertBool+ "scatter axes fit the data by default"+ ("\"zero\":false" `L.isInfixOf` html)+ anchored <- P.scatter ((P.mkScatter "a" "b"){includeZero = True}) numFrame+ assertBool+ "includeZero = True anchors the axes at zero explicitly"+ ("\"zero\":true" `L.isInfixOf` anchored)++lineFitsAxesByDefault :: Test+lineFitsAxesByDefault = TestCase $ do+ html <- P.line (P.mkLine "a" ["b"]) numFrame+ assertBool+ "line axes fit the data by default"+ ("\"zero\":false" `L.isInfixOf` html)++typedParity :: Test+typedParity = TestCase $ do+ let tdf =+ DT.unsafeFreeze numFrame ::+ DT.TypedDataFrame '[DT.Column "a" Double, DT.Column "b" Double]+ specU =+ C.toVegaSpec+ ( C.chart numFrame+ & C.mark C.Point+ & C.enc C.X (col @Double "a")+ & C.enc C.Y (col @Double "b")+ )+ specT =+ CT.toVegaSpec+ ( CT.chart tdf+ & CT.mark CT.Point+ & CT.enc CT.X (DT.col @"a")+ & CT.enc CT.Y (DT.col @"b")+ )+ assertEqual "typed spec equals untyped spec" specU specT++tests :: [Test]+tests =+ [ TestLabel "Plotting.fieldTypeInference" fieldTypeInference+ , TestLabel "Plotting.boxIsBoxplot" boxIsBoxplot+ , TestLabel "Plotting.legacyBoxIsBoxplot" legacyBoxIsBoxplot+ , TestLabel "Plotting.nanBecomesNull" nanBecomesNull+ , TestLabel "Plotting.escapingSafe" escapingSafe+ , TestLabel "Plotting.computedExpr" computedExpr+ , TestLabel "Plotting.includeZeroChart" includeZeroChart+ , TestLabel "Plotting.includeZeroMergesWithLog" includeZeroMergesWithLog+ , TestLabel "Plotting.scatterFitsAxesByDefault" scatterFitsAxesByDefault+ , TestLabel "Plotting.lineFitsAxesByDefault" lineFitsAxesByDefault+ , TestLabel "Plotting.typedParity" typedParity+ ]
+ tests/PrettyPrint.hs view
@@ -0,0 +1,70 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Golden tests pinning the format of 'prettyPrint'/'prettyPrintWidth': flat+@else if@ ladders, operator-precedence parenthesization, width-aware wrapping of+commutative chains, and short expressions staying on one line.+-}+module PrettyPrint (tests) where++import qualified Data.Text as T+import DataFrame.Internal.Expression (Expr, prettyPrint, prettyPrintWidth)+import DataFrame.Operators+import Test.HUnit++a, b, c :: Expr Double+a = col "a"+b = col "b"+c = col "c"++golden :: String -> String -> String -> Test+golden label want got =+ TestLabel label . TestCase $ assertEqual label want got++tests :: [Test]+tests =+ [ -- A short conditional stays compact; then/else still break onto own lines.+ golden+ "fits on one line"+ "if x .>=. 0.0\nthen \"pos\"\nelse \"neg\""+ ( prettyPrint+ (ifThenElse (col @Double "x" .>=. lit 0.0) (lit @T.Text "pos") (lit "neg"))+ )+ , -- Nested else-if forms a flat ladder (no staircase indentation).+ golden+ "flat else-if ladder"+ "if a .>. 1.0\nthen \"x\"\nelse if b .>. 2.0\nthen \"y\"\nelse \"z\""+ ( prettyPrint+ ( ifThenElse+ (col @Double "a" .>. lit 1.0)+ (lit @T.Text "x")+ (ifThenElse (col @Double "b" .>. lit 2.0) (lit "y") (lit "z"))+ )+ )+ , -- A lower-precedence operand is parenthesized under a higher one.+ golden "precedence: (a + b) * c" "(a + b) * c" (prettyPrint ((a + b) * c))+ , -- No spurious parens when precedence already disambiguates.+ golden "precedence: a + b * c" "a + b * c" (prettyPrint (a + b * c))+ , -- Non-commutative operators are not flattened/reordered.+ golden "left-assoc subtraction" "a - b - c" (prettyPrint (a - b - c))+ , -- A commutative chain stays on one line when it fits the width.+ golden+ "commutative chain fits"+ "alpha + beta + gamma"+ (prettyPrintWidth 80 (col @Double "alpha" + col "beta" + col "gamma"))+ , -- The same chain wraps at the operator with a hanging indent when narrow.+ golden+ "commutative chain wraps"+ "alpha\n + beta\n + gamma"+ (prettyPrintWidth 12 (col @Double "alpha" + col "beta" + col "gamma"))+ , -- A comparison/logical chain that fits stays free of grouping parens.+ golden+ "boolean chain fits: no paren noise"+ "a + b .>=. c .&&. a .>. b"+ (prettyPrintWidth 80 ((a + b .>=. c) .&&. (a .>. b)))+ , -- When a nested operand wraps, parens delimit it; flat operands stay bare.+ golden+ "wrapped operand gets parens"+ "(a + b + c\n .>=. 0.0)\n .&&. a .>. b"+ (prettyPrintWidth 16 ((a + b + c .>=. lit 0.0) .&&. (a .>. b)))+ ]
+ tests/Properties/Categorical.hs view
@@ -0,0 +1,188 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Property tests pinning the categorical laws the library is meant to obey,+following https://mchav.github.io/what-category-theory-teaches-us-about-dataframes/++ * /Topos (set algebra)/ — 'D.union', 'D.intersect', 'D.difference' and+ 'D.symmetricDifference' form the subobject lattice, with 'D.distinct'+ (image factorization) as the canonical set-valued map.+ * /Migration functor Δ/ — 'D.select'/'D.rename'/'D.exclude' are functorial:+ identities and round-trips hold.++Operands that must share a schema are produced by row-subsetting one generated+base DataFrame, so the two/three frames are always schema-compatible.+-}+module Properties.Categorical (tests) where++import Data.Text ()++import qualified DataFrame as D+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.DataFrame (+ DataFrame,+ columnNames,+ dataframeDimensions,+ )++import Test.QuickCheck++nRows :: DataFrame -> Int+nRows = fst . dataframeDimensions++{- | Equality of the underlying row sets, via the library's null-aware+@Eq DataFrame@. Set operations emit rows in a deterministic hash-bucket order+that depends only on content, and 'D.Eq' compares null slots correctly, so this+is a sound oracle for the topos laws (including over @Maybe Int@ columns).+-}+sameRows :: DataFrame -> DataFrame -> Property+sameRows x y = x === y++-- | A schema-compatible pair: two row-subsets of a common base frame.+data Pair = Pair DataFrame DataFrame deriving (Show)++-- | A schema-compatible triple, likewise.+data Triple = Triple DataFrame DataFrame DataFrame deriving (Show)++{- | A base frame of @Int@ and @Maybe Int@ columns.++@Maybe Int@ is included so the laws exercise nulls — @Nothing@ must behave as a+value distinct from any @Just n@. @Double@ is deliberately excluded: @NaN@/@-0.0@+break set semantics by IEEE rules (@NaN /= NaN@), which is not a library bug. A+small value range makes duplicate rows common, exercising deduplication.+-}+genBase :: Gen DataFrame+genBase = do+ nCols <- choose (1, 4)+ nRowsG <- choose (0, 60)+ let names = take nCols ["c0", "c1", "c2", "c3"]+ cols <- mapM (const (genCol nRowsG)) names+ pure (D.fromNamedColumns (zip names cols))+ where+ genCol n =+ oneof+ [ DI.fromList <$> vectorOf n (choose (-3, 3) :: Gen Int)+ , DI.fromList <$> vectorOf n genMaybeInt+ ]+ genMaybeInt =+ frequency+ [ (3, Just <$> (choose (-3, 3) :: Gen Int))+ , (1, pure Nothing)+ ]++subset :: DataFrame -> Gen DataFrame+subset df = do+ let n = nRows df+ lo <- choose (0, n)+ hi <- choose (0, n)+ pure (D.range (min lo hi, max lo hi) df)++instance Arbitrary Pair where+ arbitrary = do+ base <- genBase+ Pair <$> subset base <*> subset base++instance Arbitrary Triple where+ arbitrary = do+ base <- genBase+ Triple <$> subset base <*> subset base <*> subset base++{- | A single clean base frame (Int / Maybe Int columns only).++Used by the Δ-functor laws, which compare with the representation-sensitive+@Eq DataFrame@ — and that @Eq@ is not even reflexive on @NaN@, so the shared+@Double@-bearing generator cannot be used here.+-}+newtype Frame = Frame DataFrame deriving (Show)++instance Arbitrary Frame where+ arbitrary = Frame <$> genBase++-- | An empty frame with the same schema as @df@ (zero rows, same columns).+emptyLike :: DataFrame -> DataFrame+emptyLike = D.range (0, 0)++-------------------------------------------------------------------------------+-- Topos / set-algebra laws+--+-- These are statements about row /sets/, so they are asserted with 'sameRows'+-- (row-content equality) rather than the representation-sensitive @Eq DataFrame@.+-------------------------------------------------------------------------------++prop_unionCommutative :: Pair -> Property+prop_unionCommutative (Pair a b) = sameRows (D.union a b) (D.union b a)++prop_unionAssociative :: Triple -> Property+prop_unionAssociative (Triple a b c) =+ sameRows (D.union (D.union a b) c) (D.union a (D.union b c))++prop_unionIdempotent :: Pair -> Property+prop_unionIdempotent (Pair a _) = sameRows (D.union a a) (D.distinct a)++prop_intersectCommutative :: Pair -> Property+prop_intersectCommutative (Pair a b) = sameRows (D.intersect a b) (D.intersect b a)++prop_intersectIdempotent :: Pair -> Property+prop_intersectIdempotent (Pair a _) = sameRows (D.intersect a a) (D.distinct a)++prop_differenceSelfEmpty :: Pair -> Property+prop_differenceSelfEmpty (Pair a _) = nRows (D.difference a a) === 0++prop_differenceEmptyRight :: Pair -> Property+prop_differenceEmptyRight (Pair a _) =+ sameRows (D.difference a (emptyLike a)) (D.distinct a)++prop_unionAlreadyDistinct :: Pair -> Property+prop_unionAlreadyDistinct (Pair a b) =+ sameRows (D.distinct (D.union a b)) (D.union a b)++prop_symmetricDifferenceDef :: Pair -> Property+prop_symmetricDifferenceDef (Pair a b) =+ sameRows+ (D.symmetricDifference a b)+ (D.union (D.difference a b) (D.difference b a))++-- | Topos law: the complement and the intersection partition the left set.+prop_complementPartition :: Pair -> Property+prop_complementPartition (Pair a b) =+ sameRows (D.union (D.difference a b) (D.intersect a b)) (D.distinct a)++-- | The complement is disjoint from the subtrahend.+prop_differenceDisjoint :: Pair -> Property+prop_differenceDisjoint (Pair a b) =+ nRows (D.intersect (D.difference a b) b) === 0++-------------------------------------------------------------------------------+-- Δ migration functor laws+-------------------------------------------------------------------------------++-- | Excluding nothing is the identity.+prop_excludeNothingIdentity :: Frame -> Property+prop_excludeNothingIdentity (Frame df) = D.exclude [] df === df++-- | Renaming a column and back is the identity (functor preserves identities).+prop_renameRoundTrip :: Frame -> Property+prop_renameRoundTrip (Frame df) =+ case columnNames df of+ [] -> property True+ (name : _) ->+ let tmp = name <> "__rt_tmp"+ in notElem tmp (columnNames df) ==>+ D.rename tmp name (D.rename name tmp df) === df++tests :: [Property]+tests =+ [ property prop_unionCommutative+ , property prop_unionAssociative+ , property prop_unionIdempotent+ , property prop_intersectCommutative+ , property prop_intersectIdempotent+ , property prop_differenceSelfEmpty+ , property prop_differenceEmptyRight+ , property prop_unionAlreadyDistinct+ , property prop_symmetricDifferenceDef+ , property prop_complementPartition+ , property prop_differenceDisjoint+ , property prop_excludeNothingIdentity+ , property prop_renameRoundTrip+ ]
+ tests/Simplify.hs view
@@ -0,0 +1,363 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Specification for 'DataFrame.Internal.Simplify.simplify': each case is the+full predicate expression, compared with 'eqExpr'.+-}+module Simplify (tests) where++import qualified DataFrame.Functions as F+import DataFrame.Internal.Column (Columnable)+import DataFrame.Internal.Expression (Expr, eqExpr)+import DataFrame.Internal.Simplify (simplify)+import DataFrame.Operators++import Test.HUnit++simplifiesTo :: (Columnable a) => String -> Expr a -> Expr a -> Test+simplifiesTo label input want =+ TestLabel label . TestCase $+ assertBool+ (label ++ ": got " ++ show (simplify input) ++ " want " ++ show want)+ (eqExpr (simplify input) want)++unchanged :: (Columnable a) => String -> Expr a -> Test+unchanged label e = simplifiesTo label e e++sameDirection :: [Test]+sameDirection =+ [ simplifiesTo+ "and lower bounds keeps max"+ ( F.and+ (F.col @Double "age" .> F.lit (20 :: Double))+ (F.col @Double "age" .> F.lit (25 :: Double))+ )+ (F.col @Double "age" .> F.lit (25 :: Double))+ , simplifiesTo+ "and upper bounds keeps min"+ ( F.and+ (F.col @Double "age" .< F.lit (50 :: Double))+ (F.col @Double "age" .< F.lit (40 :: Double))+ )+ (F.col @Double "age" .< F.lit (40 :: Double))+ , simplifiesTo+ "or lower bounds keeps min"+ ( F.or+ (F.col @Double "age" .> F.lit (20 :: Double))+ (F.col @Double "age" .> F.lit (25 :: Double))+ )+ (F.col @Double "age" .> F.lit (20 :: Double))+ , simplifiesTo+ "or upper bounds keeps max"+ ( F.or+ (F.col @Double "age" .< F.lit (50 :: Double))+ (F.col @Double "age" .< F.lit (40 :: Double))+ )+ (F.col @Double "age" .< F.lit (50 :: Double))+ ]++mixedDirection :: [Test]+mixedDirection =+ [ simplifiesTo+ "closed interval at a point becomes equality"+ ( F.and+ (F.col @Double "age" .>= F.lit (30 :: Double))+ (F.col @Double "age" .<= F.lit (30 :: Double))+ )+ (F.col @Double "age" .== F.lit (30 :: Double))+ , simplifiesTo+ "open contradiction becomes False"+ ( F.and+ (F.col @Double "age" .> F.lit (30 :: Double))+ (F.col @Double "age" .< F.lit (30 :: Double))+ )+ (F.lit False)+ , simplifiesTo+ "disjoint bounds become False"+ ( F.and+ (F.col @Double "age" .> F.lit (30 :: Double))+ (F.col @Double "age" .< F.lit (20 :: Double))+ )+ (F.lit False)+ , simplifiesTo+ "distinct points conjoined become False"+ ( F.and+ (F.col @Double "age" .== F.lit (30 :: Double))+ (F.col @Double "age" .== F.lit (40 :: Double))+ )+ (F.lit False)+ , simplifiesTo+ "point inside half-space becomes the point"+ ( F.and+ (F.col @Double "age" .== F.lit (30 :: Double))+ (F.col @Double "age" .> F.lit (25 :: Double))+ )+ (F.col @Double "age" .== F.lit (30 :: Double))+ , simplifiesTo+ "point outside half-space becomes False"+ ( F.and+ (F.col @Double "age" .== F.lit (30 :: Double))+ (F.col @Double "age" .> F.lit (40 :: Double))+ )+ (F.lit False)+ , simplifiesTo+ "negation redundant under bound drops"+ ( F.and+ (F.col @Double "age" ./= F.lit (30 :: Double))+ (F.col @Double "age" .> F.lit (40 :: Double))+ )+ (F.col @Double "age" .> F.lit (40 :: Double))+ ]++tautologies :: [Test]+tautologies =+ [ simplifiesTo+ "integral exhaustive cover becomes True"+ ( F.or+ (F.toDouble (F.col @Int "ai") .<= F.lit (30 :: Double))+ (F.toDouble (F.col @Int "ai") .> F.lit (30 :: Double))+ )+ (F.lit True)+ , simplifiesTo+ "distinct inequalities cover everything"+ ( F.or+ (F.col @Double "age" ./= F.lit (30 :: Double))+ (F.col @Double "age" ./= F.lit (40 :: Double))+ )+ (F.lit True)+ , simplifiesTo+ "inequality or equality at same point"+ ( F.or+ (F.col @Double "age" ./= F.lit (30 :: Double))+ (F.col @Double "age" .== F.lit (30 :: Double))+ )+ (F.lit True)+ ]++booleanAlgebra :: [Test]+booleanAlgebra =+ [ simplifiesTo+ "idempotent and"+ ( F.and+ (F.col @Double "age" .> F.lit (20 :: Double))+ (F.col @Double "age" .> F.lit (20 :: Double))+ )+ (F.col @Double "age" .> F.lit (20 :: Double))+ , simplifiesTo+ "absorption and over or"+ ( F.and+ (F.col @Double "age" .> F.lit (20 :: Double))+ ( F.or+ (F.col @Double "age" .> F.lit (20 :: Double))+ (F.col @Double "hours" .> F.lit (40 :: Double))+ )+ )+ (F.col @Double "age" .> F.lit (20 :: Double))+ , simplifiesTo+ "true and unit"+ (F.and (F.lit True) (F.col @Double "hours" .> F.lit (40 :: Double)))+ (F.col @Double "hours" .> F.lit (40 :: Double))+ , simplifiesTo+ "false and annihilates"+ (F.and (F.lit False) (F.col @Double "hours" .> F.lit (40 :: Double)))+ (F.lit False)+ , simplifiesTo+ "double negation"+ (F.not (F.not (F.col @Double "age" .> F.lit (20 :: Double))))+ (F.col @Double "age" .> F.lit (20 :: Double))+ ]++ifCollapse :: [Test]+ifCollapse =+ [ simplifiesTo+ "boolean if becomes its condition"+ ( F.ifThenElse+ (F.col @Double "age" .> F.lit (20 :: Double))+ (F.lit True)+ (F.lit False)+ )+ (F.col @Double "age" .> F.lit (20 :: Double))+ , simplifiesTo+ "if with equal branches collapses"+ ( F.ifThenElse+ (F.col @Double "hours" .> F.lit (40 :: Double))+ (F.col @Double "age" .> F.lit (20 :: Double))+ (F.col @Double "age" .> F.lit (20 :: Double))+ )+ (F.col @Double "age" .> F.lit (20 :: Double))+ ]++multiPass :: [Test]+multiPass =+ [ simplifiesTo+ "long and chain keeps tightest"+ ( F.and+ ( F.and+ ( F.and+ (F.col @Double "age" .> F.lit (10 :: Double))+ (F.col @Double "age" .> F.lit (20 :: Double))+ )+ (F.col @Double "age" .> F.lit (30 :: Double))+ )+ (F.col @Double "age" .> F.lit (40 :: Double))+ )+ (F.col @Double "age" .> F.lit (40 :: Double))+ , simplifiesTo+ "consolidate then contradiction"+ ( F.and+ ( F.and+ (F.col @Double "age" .>= F.lit (30 :: Double))+ (F.col @Double "age" .>= F.lit (40 :: Double))+ )+ (F.col @Double "age" .<= F.lit (35 :: Double))+ )+ (F.lit False)+ , simplifiesTo+ "cascade of contradictions"+ ( F.or+ ( F.and+ (F.col @Double "age" .> F.lit (30 :: Double))+ (F.col @Double "age" .< F.lit (20 :: Double))+ )+ ( F.and+ (F.col @Double "hours" .> F.lit (200 :: Double))+ (F.col @Double "hours" .< F.lit (10 :: Double))+ )+ )+ (F.lit False)+ , simplifiesTo+ "consolidate enabling idempotence"+ ( F.and+ ( F.or+ (F.col @Double "age" .> F.lit (20 :: Double))+ (F.col @Double "age" .> F.lit (25 :: Double))+ )+ ( F.or+ (F.col @Double "age" .> F.lit (20 :: Double))+ (F.col @Double "age" .> F.lit (30 :: Double))+ )+ )+ (F.col @Double "age" .> F.lit (20 :: Double))+ , simplifiesTo+ "de morgan over contradiction"+ ( F.not+ ( F.and+ (F.col @Double "age" .> F.lit (30 :: Double))+ (F.col @Double "age" .< F.lit (20 :: Double))+ )+ )+ (F.lit True)+ , simplifiesTo+ "interior contradiction collapses the conjunction"+ ( F.and+ ( F.and+ (F.col @Double "age" .> F.lit (10 :: Double))+ (F.col @Double "hours" .> F.lit (40 :: Double))+ )+ ( F.and+ (F.col @Double "age" .> F.lit (30 :: Double))+ (F.col @Double "age" .< F.lit (25 :: Double))+ )+ )+ (F.lit False)+ ]++nullAware :: [Test]+nullAware =+ [ simplifiesTo+ "just-literal lower bounds keep max"+ ( (F.col @Int "age" .> F.lit (Just (30 :: Int)))+ .&& (F.col @Int "age" .> F.lit (Just (35 :: Int)))+ )+ (F.col @Int "age" .> F.lit (Just (35 :: Int)))+ , simplifiesTo+ "just-literal contradiction over non-null column becomes Just False"+ ( (F.col @Int "age" .> F.lit (Just (30 :: Int)))+ .&& (F.col @Int "age" .< F.lit (Just (20 :: Int)))+ )+ (F.lit (Just False))+ , unchanged+ "nullable column contradiction stays unknown"+ ( (F.col @(Maybe Int) "w" .> F.lit (Just (30 :: Int)))+ .&& (F.col @(Maybe Int) "w" .< F.lit (Just (20 :: Int)))+ )+ , unchanged+ "nullable column tautology stays unknown"+ ( (F.col @(Maybe Int) "w" .<= F.lit (Just (30 :: Int)))+ .|| (F.col @(Maybe Int) "w" .> F.lit (Just (30 :: Int)))+ )+ , simplifiesTo+ "fromMaybe consolidation keeps tighter"+ ( F.and+ (F.fromMaybe False (F.col @(Maybe Double) "w" .<= F.lit (5 :: Double)))+ (F.fromMaybe False (F.col @(Maybe Double) "w" .<= F.lit (3 :: Double)))+ )+ (F.fromMaybe False (F.col @(Maybe Double) "w" .<= F.lit (3 :: Double)))+ , simplifiesTo+ "fromMaybe contradiction becomes False"+ ( F.and+ (F.fromMaybe False (F.col @(Maybe Double) "w" .> F.lit (30 :: Double)))+ (F.fromMaybe False (F.col @(Maybe Double) "w" .< F.lit (20 :: Double)))+ )+ (F.lit False)+ , unchanged+ "fromMaybe tautology stays unsimplified"+ ( F.or+ (F.fromMaybe False (F.col @(Maybe Double) "w" .<= F.lit (30 :: Double)))+ (F.fromMaybe False (F.col @(Maybe Double) "w" .> F.lit (30 :: Double)))+ )+ ]++bailing :: [Test]+bailing =+ [ unchanged+ "proper interval is not collapsed"+ ( F.and+ (F.col @Double "age" .>= F.lit (20 :: Double))+ (F.col @Double "age" .<= F.lit (65 :: Double))+ )+ , unchanged+ "or with a gap is not a tautology"+ ( F.or+ (F.col @Double "age" .<= F.lit (30 :: Double))+ (F.col @Double "age" .> F.lit (40 :: Double))+ )+ , unchanged+ "two inequalities are not an interval"+ ( F.and+ (F.col @Double "age" ./= F.lit (30 :: Double))+ (F.col @Double "age" ./= F.lit (40 :: Double))+ )+ , unchanged+ "cross-column conjunction is left alone"+ ( F.and+ (F.col @Double "age" .> F.lit (50 :: Double))+ (F.col @Double "hours" .> F.lit (40 :: Double))+ )+ , unchanged+ "double exhaustive cover bails (NaN)"+ ( F.or+ (F.col @Double "age" .<= F.lit (30 :: Double))+ (F.col @Double "age" .> F.lit (30 :: Double))+ )+ , unchanged+ "punctured interval is not a single atom"+ ( F.and+ (F.col @Double "age" ./= F.lit (30 :: Double))+ (F.col @Double "age" .> F.lit (20 :: Double))+ )+ ]++tests :: [Test]+tests =+ concat+ [ sameDirection+ , mixedDirection+ , tautologies+ , booleanAlgebra+ , ifCollapse+ , multiPass+ , nullAware+ , bailing+ ]
+ tests/Typed/IOReaders.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Tests for the typed CSV reader: a write\/read round-trip matches the untyped+reader, a wrong schema surfaces as 'Left' via 'readCsvWithError', and the+throwing 'readCsv' raises a 'DataFrameException'. The Parquet reader uses the+identical freeze-on-read path.+-}+module Typed.IOReaders (tests) where++import Control.Exception (evaluate, try)+import Data.Either (isLeft)+import qualified Data.Text as T+import System.IO (hClose)+import System.IO.Temp (withSystemTempFile)++import qualified DataFrame as D+import DataFrame.Errors (DataFrameException)+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Typed as DT+import qualified DataFrame.Typed.IO.CSV as TCSV++import Test.HUnit++type S =+ '[ DT.Column "x" Int+ , DT.Column "y" Double+ , DT.Column "g" T.Text+ ]++sampleDF :: D.DataFrame+sampleDF =+ D.fromNamedColumns+ [ ("x", DI.fromList [1, 2, 3 :: Int])+ , ("y", DI.fromList [1.5, 2.5, 3.5 :: Double])+ , ("g", DI.fromList ["a", "b", "c" :: T.Text])+ ]++roundTrip :: Test+roundTrip = TestCase $ withSystemTempFile "typed_parity.csv" $ \fp h -> do+ hClose h+ D.writeCsv fp sampleDF+ untyped <- D.readCsv fp+ typed <- DT.thaw <$> TCSV.readCsv @S fp+ assertEqual "typed readCsv round-trips like untyped readCsv" untyped typed++wrongSchemaEither :: Test+wrongSchemaEither = TestCase $ withSystemTempFile "typed_err.csv" $ \fp h -> do+ hClose h+ D.writeCsv fp sampleDF+ res <- TCSV.readCsvWithError @'[DT.Column "nope" Int] fp+ assertBool "wrong schema => Left" (isLeft res)++wrongSchemaThrows :: Test+wrongSchemaThrows = TestCase $ withSystemTempFile "typed_throw.csv" $ \fp h -> do+ hClose h+ D.writeCsv fp sampleDF+ r <-+ try (TCSV.readCsv @'[DT.Column "nope" Int] fp >>= evaluate . DT.nRows) ::+ IO (Either DataFrameException Int)+ assertBool "wrong schema => throws DataFrameException" (isLeft r)++tests :: [Test]+tests = [roundTrip, wrongSchemaEither, wrongSchemaThrows]
+ tests/Typed/Parity.hs view
@@ -0,0 +1,205 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Typed.Parity (tests) where++import Data.Either (fromRight)+import qualified Data.Text as T+import System.Random (mkStdGen)++import qualified DataFrame as D+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Typed as DT+import qualified DataFrame.Typed.Statistics as TS++import Test.HUnit++type S =+ '[ DT.Column "x" Int+ , DT.Column "y" Double+ , DT.Column "g" T.Text+ ]++baseDF :: D.DataFrame+baseDF =+ D.fromNamedColumns+ [ ("x", DI.fromList [1, 2, 3, 4, 5, 6 :: Int])+ , ("y", DI.fromList [10.0, 25.0, 30.0, 5.0, 50.0, 12.0 :: Double])+ , ("g", DI.fromList ["a", "b", "a", "b", "a", "b" :: T.Text])+ ]++tdf :: DT.TypedDataFrame S+tdf = either (error . show) id (DT.freezeWithError baseDF)++-- Statistics reducers should match the untyped ones exactly (same code path).+statParity :: Test+statParity =+ TestList+ [ eq "mean" (TS.mean (DT.col @"y") tdf) (D.mean (F.col @Double "y") baseDF)+ , eq "median" (TS.median (DT.col @"y") tdf) (D.median (F.col @Double "y") baseDF)+ , eq+ "variance"+ (TS.variance (DT.col @"y") tdf)+ (D.variance (F.col @Double "y") baseDF)+ , eq+ "stddev"+ (TS.standardDeviation (DT.col @"y") tdf)+ (D.standardDeviation (F.col @Double "y") baseDF)+ , eq "sum" (TS.sum (DT.col @"x") tdf) (D.sum (F.col @Int "x") baseDF)+ , eq+ "percentile"+ (TS.percentile 50 (DT.col @"y") tdf)+ (D.percentile 50 (F.col @Double "y") baseDF)+ , eq+ "iqr"+ (TS.interQuartileRange (DT.col @"y") tdf)+ (D.interQuartileRange (F.col @Double "y") baseDF)+ , eq+ "skewness"+ (TS.skewness (DT.col @"y") tdf)+ (D.skewness (F.col @Double "y") baseDF)+ , eq+ "correlation"+ (TS.correlation @"x" @"y" tdf)+ (D.correlation "x" "y" baseDF)+ ]+ where+ eq :: (Eq a, Show a) => String -> a -> a -> Test+ eq lbl typed untyped = TestCase (assertEqual ("typed stat " ++ lbl) untyped typed)++-- Expression combinators ported in Expr.Extra: derive a column both ways.+exprParity :: Test+exprParity =+ TestList+ [ deriveEq "pow" (DT.pow (DT.col @"x") 2) (F.pow (F.col @Int "x") 2)+ , deriveEq "relu" (DT.relu (DT.col @"x")) (F.relu (F.col @Int "x"))+ , deriveEq "toMaybe" (DT.toMaybe (DT.col @"x")) (F.toMaybe (F.col @Int "x"))+ , deriveEq+ "min"+ (DT.min (DT.col @"x") (DT.lit 3))+ (F.min (F.col @Int "x") (F.lit @Int 3))+ , deriveEq+ "splitOn"+ (DT.splitOn "a" (DT.col @"g"))+ (F.splitOn "a" (F.col @T.Text "g"))+ , deriveEq+ "recodeWithDefault"+ (DT.recodeWithDefault 0 [(1 :: Int, 100 :: Int)] (DT.col @"x"))+ (F.recodeWithDefault 0 [(1 :: Int, 100 :: Int)] (F.col @Int "x"))+ ]+ where+ deriveEq ::+ (DI.Columnable a) =>+ String -> DT.TExpr S a -> D.Expr a -> Test+ deriveEq lbl te ue =+ TestCase+ ( assertEqual+ ("typed derive " ++ lbl)+ (D.derive "r" ue baseDF)+ (DT.thaw (DT.derive @"r" te tdf))+ )++-- apply / valueCounts / horizontal merge.+applyParity :: Test+applyParity =+ TestList+ [ TestCase+ ( assertEqual+ "applyColumn type-change"+ (D.apply (show :: Int -> String) "x" baseDF)+ (DT.thaw (DT.applyColumn @"x" (show :: Int -> String) tdf))+ )+ , TestCase+ ( assertEqual+ "applyMany type-preserving"+ (D.applyMany (+ (1 :: Int)) ["x"] baseDF)+ (DT.thaw (DT.applyMany @'["x"] (+ (1 :: Int)) tdf))+ )+ , TestCase+ ( assertEqual+ "valueCounts"+ (D.valueCounts (F.col @T.Text "g") baseDF)+ (DT.valueCounts (DT.col @"g") tdf)+ )+ , TestCase+ ( assertEqual+ "horizontal merge (|||)"+ (leftDF D.||| rightDF)+ (DT.thaw ((DT.|||) leftT rightT))+ )+ ]++-- Sampling/splitting determinism: same seed => same rows as the untyped form.+samplingParity :: Test+samplingParity =+ TestList+ [ TestCase+ ( assertEqual+ "randomSplit"+ (D.randomSplit (mkStdGen 7) 0.5 baseDF)+ (let (a, b) = DT.randomSplit (mkStdGen 7) 0.5 tdf in (DT.thaw a, DT.thaw b))+ )+ , TestCase+ ( assertEqual+ "kFolds"+ (D.kFolds (mkStdGen 7) 3 baseDF)+ (map DT.thaw (DT.kFolds (mkStdGen 7) 3 tdf))+ )+ ]++-- Matrix / numeric-vector extraction.+accessParity :: Test+accessParity =+ TestList+ [ TestCase+ ( assertEqual+ "columnAsDoubleVector"+ (extract (D.columnAsDoubleVector (F.col @Double "y") baseDF))+ (DT.columnAsDoubleVector @"y" tdf)+ )+ , TestCase+ ( assertEqual+ "toDoubleMatrix"+ (extract (D.toDoubleMatrix numericDF))+ (DT.toDoubleMatrix numericT)+ )+ ]+ where+ extract :: Either e a -> a+ extract = fromRight (error "extraction failed")++-- Disjoint frames for the (|||) test.+leftDF :: D.DataFrame+leftDF = D.fromNamedColumns [("x", DI.fromList [1, 2 :: Int])]++rightDF :: D.DataFrame+rightDF = D.fromNamedColumns [("y", DI.fromList [1.0, 2.0 :: Double])]++leftT :: DT.TypedDataFrame '[DT.Column "x" Int]+leftT = either (error . show) id (DT.freezeWithError leftDF)++rightT :: DT.TypedDataFrame '[DT.Column "y" Double]+rightT = either (error . show) id (DT.freezeWithError rightDF)++-- Numeric-only frame for the matrix test.+numericDF :: D.DataFrame+numericDF =+ D.fromNamedColumns+ [ ("x", DI.fromList [1, 2, 3 :: Int])+ , ("y", DI.fromList [1.5, 2.5, 3.5 :: Double])+ ]++numericT :: DT.TypedDataFrame '[DT.Column "x" Int, DT.Column "y" Double]+numericT = either (error . show) id (DT.freezeWithError numericDF)++tests :: [Test]+tests =+ [ statParity+ , exprParity+ , applyParity+ , samplingParity+ , accessParity+ ]
+ tests/data/timestamp_nanos.parquet view
binary file changed (absent → 237 bytes)
+ tests/data/typing/texts.txt view
@@ -0,0 +1,34 @@+To+protect+the+world+from+devastation+To+unite+all+people+within+our+nation+To+denounce+the+evils+of+truth+and+love+To+extend+our+reach+to+the+stars+above+JESSIE!+JAMES!+TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!+Surrender now or prepare to fight!+Meowth, that's right!
+ tests/data/typing/texts_with_empties.txt view
@@ -0,0 +1,41 @@++To+protect+the+world+from+devastation++To+unite+all+people+within+our+nation++To+denounce+the+evils+of+truth+and+love++To+extend+our+reach+to+the+stars+above++JESSIE!+JAMES!+TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!++Surrender now or prepare to fight!++Meowth, that's right!
+ tests/data/typing/texts_with_empties_and_nullish.txt view
@@ -0,0 +1,44 @@++To+protect+the+world+from+devastation++To+unite+all+people+within+our+nation++To+denounce+the+evils+of+truth+and+love++To+extend+our+reach+to+the+stars+above++JESSIE!+JAMES!+TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!++Surrender now or prepare to fight!++Meowth, that's right!+NaN+Nothing+N/A
+ tests/data/unstable_csv/all_empty_row.csv view
@@ -0,0 +1,2 @@+a,b,c+,,
+ tests/data/unstable_csv/crlf.csv view
@@ -0,0 +1,3 @@+name,city +Alice,New York +Bob,Los Angeles
+ tests/data/unstable_csv/crlf.tsv view
@@ -0,0 +1,2 @@+name city +Alice New York
+ tests/data/unstable_csv/either_read_mixed.csv view
@@ -0,0 +1,9 @@+id,score,name+1,10,alice+2,20,bob+3,30,carol+4,40,dave+5,50,eve+6,abc,frank+7,,grace+8,80,N/A
+ tests/data/unstable_csv/empty_file.csv view
+ tests/data/unstable_csv/extra_fields.csv view
@@ -0,0 +1,2 @@+a,b,c+1,2,3,EXTRA
+ tests/data/unstable_csv/header_only.csv view
@@ -0,0 +1,1 @@+first,second,third
+ tests/data/unstable_csv/missing_fields.csv view
@@ -0,0 +1,3 @@+a,b,c+1,2+X,Y,Z
+ tests/data/unstable_csv/no_trailing_newline.csv view
@@ -0,0 +1,2 @@+name,city+Alice,London
+ tests/data/unstable_csv/single_col.csv view
@@ -0,0 +1,4 @@+name+Alice+Bob+Carol
+ tests/data/unstable_csv/trailing_blank_line.csv view
@@ -0,0 +1,3 @@+a,b,c+1,2,3+
+ tests/data/unstable_csv/utf8_bom.csv view
@@ -0,0 +1,3 @@+name,value+Alice,1+Bob,2
+ tests/data/unstable_csv/whitespace_fields.csv view
@@ -0,0 +1,3 @@+name,city+ Alice , New York+ Bob , Los Angeles