packages feed

dataframe 0.4.1.0 → 2.3.0.0

raw patch · 147 files changed

Files

CHANGELOG.md view
@@ -1,5 +1,161 @@ # Revision history for dataframe +## 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.+* You can now explicitly set the schema of a subset of columns (named) as opposed to setting a list.+* Join order is changed from pipelining style to function call style.+* Bug in division being marked as commutative.+* Columnable now requires nfdata.+* Parquet reads are now faster across the board.+* Better test coverage.++## 0.6.0.0+* New typed API see https://dataframe.readthedocs.io/en/latest/using_dataframe_in_a_standalone_script.html+* Faster joins+* Fine grained parquet reads using `readParquetFilesWithOpts`.++## 0.5.0.0+* SortOrder now takes an expression rather than a string.+* selectBy now respects the original column order.+* Some changes to the internal representation of the expression GADT.+* Added fixity to binary operations for column comparisons.+* Fixed ZSTD decompression for Parquet files.+* Added logical type handling for timestamps and decimals.+* aggregation now handles null dataframes gracefully.+* Drop random dep to 1.2 and use random-shuffle for shuffling instead.+* Join now merges columns to a `These`.+* Added writeTsv function.+* Move expression operators to `Operators` namespace.+ ## 0.4.1.0 * Improve signal handling of dataframe repl. * `writeCsv` not correctly writes `Maybe` values (thanks to @mcoady).
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.-* Static typing makes code easier to reason about and catches many bugs at compile time—before your code ever runs.-* 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) or in our [playground](https://ulwazi-exh9dbh2exbzgbc9.westus-01.azurewebsites.net/lab).+## 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
@@ -0,0 +1,311 @@+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | End-to-end smoke test and benchmark for the Lazy streaming API.++     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+-}+module Main where++import Control.Monad (foldM, forM_, when)+import qualified Data.ByteString.Builder as Builder+import qualified Data.Map as M+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 System.Directory (doesFileExist, getFileSize)+import System.Environment (getArgs)+import System.Exit (exitFailure)+import System.IO (+    BufferMode (..),+    IOMode (..),+    hFlush,+    hSetBuffering,+    stdout,+    withFile,+ )+import System.Random.Stateful++-- ---------------------------------------------------------------------------+-- Defaults+-- ---------------------------------------------------------------------------++defaultRows :: Int+defaultRows = 1_000_000_000++defaultFile :: FilePath+defaultFile = "/tmp/lazy_1b.csv"++-- Rows written per Builder flush to disk.+chunkSize :: Int+chunkSize = 500_000++-- ---------------------------------------------------------------------------+-- Argument parsing+-- ---------------------------------------------------------------------------++data Opts = Opts+    { optRows :: Int+    , optFile :: FilePath+    , optSkipGen :: Bool+    }++parseArgs :: [String] -> Either String Opts+parseArgs = go (Opts defaultRows defaultFile False)+  where+    go opts [] = Right opts+    go opts ("--rows" : n : rest) = case reads n of+        [(v, "")] -> go opts{optRows = v} rest+        _ -> Left $ "bad --rows value: " ++ n+    go opts ("--file" : p : rest) = go opts{optFile = p} rest+    go opts ("--skip-gen" : rest) = go opts{optSkipGen = True} rest+    go _ (flag : _) = Left $ "unknown flag: " ++ flag++-- ---------------------------------------------------------------------------+-- CSV generation+-- ---------------------------------------------------------------------------++{- | Write @n@ rows of schema++       id (Int), x (Double), y (Double), category (Text: A/B/C/D)++     to @path@ using a streaming Builder, keeping heap usage constant.+-}+generateCsv :: FilePath -> Int -> IO ()+generateCsv path n = do+    g <- newIOGenM =<< newStdGen+    t0 <- getCurrentTime+    withFile path WriteMode $ \h -> do+        hSetBuffering h (BlockBuffering (Just (4 * 1024 * 1024)))+        Builder.hPutBuilder h (Builder.byteString "id,x,y,category\n")+        let numChunks = n `div` chunkSize+            remainder = n `mod` chunkSize+        forM_ [0 .. numChunks - 1] $ \c -> do+            bldr <- buildChunk g (c * chunkSize) chunkSize+            Builder.hPutBuilder h bldr+            when (c `mod` 200 == 0) $ do+                let done = c * chunkSize+                    pct = (done * 100) `div` n+                putStr $ "\r  " ++ show pct ++ "% — " ++ commas done ++ " rows"+                hFlush stdout+        when (remainder > 0) $ do+            bldr <- buildChunk g (numChunks * chunkSize) remainder+            Builder.hPutBuilder h bldr+    t1 <- getCurrentTime+    putStrLn $ "\r  100% — " ++ commas n ++ " rows written in " ++ showDiff t0 t1++buildChunk :: IOGenM StdGen -> Int -> Int -> IO Builder.Builder+buildChunk g baseId count =+    foldM (\acc i -> (acc <>) <$> buildRow g baseId i) mempty [0 .. count - 1]++buildRow :: IOGenM StdGen -> Int -> Int -> IO Builder.Builder+buildRow g baseId i = do+    x <- uniformRM (0.0 :: Double, 1.0) g+    y <- uniformRM (0.0 :: Double, 1.0) g+    c <- uniformRM (0 :: Int, 3) g+    return $+        Builder.intDec (baseId + i)+            <> Builder.char7 ','+            <> buildDouble x+            <> Builder.char7 ','+            <> buildDouble y+            <> Builder.char7 ','+            <> catChar c+            <> Builder.char7 '\n'++buildDouble :: Double -> Builder.Builder+buildDouble x =+    let scaled = round (x * 1_000_000) :: Int+        whole = scaled `div` 1_000_000+        frac = scaled `mod` 1_000_000+     in Builder.intDec whole+            <> Builder.char7 '.'+            <> pad6 frac++pad6 :: Int -> Builder.Builder+pad6 n+    | n < 10 = Builder.byteString "00000" <> Builder.intDec n+    | n < 100 = Builder.byteString "0000" <> Builder.intDec n+    | n < 1000 = Builder.byteString "000" <> Builder.intDec n+    | n < 10_000 = Builder.byteString "00" <> Builder.intDec n+    | n < 100_000 = Builder.byteString "0" <> Builder.intDec n+    | otherwise = Builder.intDec n++catChar :: Int -> Builder.Builder+catChar 0 = Builder.char7 'A'+catChar 1 = Builder.char7 'B'+catChar 2 = Builder.char7 'C'+catChar _ = Builder.char7 'D'++-- ---------------------------------------------------------------------------+-- Lazy queries+-- ---------------------------------------------------------------------------++runQuery :: String -> IO D.DataFrame -> IO ()+runQuery label action = do+    putStrLn $ "\n── " ++ label+    t0 <- getCurrentTime+    df <- action+    t1 <- getCurrentTime+    let (rows, cols) = D.dimensions df+    putStrLn $ "   rows returned : " ++ commas rows+    putStrLn $ "   columns       : " ++ show cols+    putStrLn $ "   time          : " ++ showDiff t0 t1+    when (rows > 0 && rows <= 30) $ print df++-- ---------------------------------------------------------------------------+-- Main+-- ---------------------------------------------------------------------------++main :: IO ()+main = do+    hSetBuffering stdout LineBuffering+    args <- getArgs+    opts <- case parseArgs args of+        Left err -> putStrLn ("Error: " ++ err) >> exitFailure+        Right o -> return o++    let path = optFile opts+        n = optRows opts+        pathT = T.pack path++    -- -----------------------------------------------------------------------+    -- Phase 1: Generate+    -- -----------------------------------------------------------------------+    putStrLn "=== Lazy API 1B-row benchmark ==="+    putStrLn $ "    rows   : " ++ commas n+    putStrLn $ "    file   : " ++ path+    putStrLn "    tip    : run with '+RTS -s -RTS' for heap stats"+    putStrLn ""++    exists <- doesFileExist path+    if optSkipGen opts && exists+        then do+            sz <- getFileSize path+            putStrLn $ "Skipping generation — file exists (" ++ showBytes sz ++ ")"+        else do+            putStrLn "Phase 1: Generating CSV …"+            generateCsv path n+            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+                    [ ("id", schemaType @Int)+                    , ("x", schemaType @Double)+                    , ("y", schemaType @Double)+                    , ("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.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.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.take 20 $+                L.select ["id", "z"] $+                    L.derive "z" (col @Double "x" * col @Double "y") $+                        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.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)+            ++ " rows expected)"+        )+        $ L.runDataFrame+        $ L.select ["id", "x"]+        $ L.filter (col @Double "x" .> lit (0.999 :: Double))+        $ L.scanCsv schema pathT++    putStrLn "\nDone."++-- ---------------------------------------------------------------------------+-- Formatting helpers+-- ---------------------------------------------------------------------------++showDiff :: UTCTime -> UTCTime -> String+showDiff t0 t1 = show (diffUTCTime t1 t0)++commas :: Int -> String+commas n+    | n < 1000 = show n+    | otherwise = commas (n `div` 1000) ++ "," ++ pad3 (n `mod` 1000)+  where+    pad3 x+        | x < 10 = "00" ++ show x+        | x < 100 = "0" ++ show x+        | otherwise = show x++approx :: Int -> String+approx n+    | n >= 1_000_000 = show (n `div` 1_000_000) ++ "M"+    | n >= 1_000 = show (n `div` 1_000) ++ "K"+    | otherwise = show n++showBytes :: Integer -> String+showBytes b+    | b >= 1_073_741_824 = fmt (fromIntegral b / 1_073_741_824) ++ " GiB"+    | b >= 1_048_576 = fmt (fromIntegral b / 1_048_576) ++ " MiB"+    | b >= 1_024 = fmt (fromIntegral b / 1_024) ++ " KiB"+    | otherwise = show b ++ " B"+  where+    fmt :: Double -> String+    fmt x =+        show (fromIntegral (round (x * 10) :: Int) `div` 10 :: Int)+            ++ "."+            ++ show (fromIntegral (round (x * 10) :: Int) `mod` 10 :: Int)
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,111 +1,142 @@+{-# 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 ((|>))-import DataFrame.DecisionTree-import DataFrame.Functions ((.&&), (.=), (.==))+$( 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                 ( 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"])      print model      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,14 +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 ((|>))-import System.Process+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"] ""@@ -40,15 +54,15 @@  groupByHaskell :: IO () groupByHaskell = do-    df <- D.fastReadCsvUnstable "./data/housing.csv"+    df <- D.readCsv "./data/housing.csv"     print $         df             |> D.groupBy ["ocean_proximity"]             |> D.aggregate                 [ F.minimum (F.col @Double "median_house_value")-                    `F.as` "minimum_median_house_value"+                    `as` "minimum_median_house_value"                 , F.maximum (F.col @Double "median_house_value")-                    `F.as` "maximum_median_house_value"+                    `as` "maximum_median_house_value"                 ]  groupByPolars :: IO ()@@ -81,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" @@ -99,6 +107,101 @@ parseMeasurementsTXT :: IO () parseMeasurementsTXT = parseFile "./data/measurements.txt" +{- | Generate a pair of dataframes for a 1:1 join scenario.+  Left has keys [0..n-1], right has keys [0..n-1] shuffled.+  overlap controls what fraction of keys appear in both sides.+-}+mkOneToOne :: Int -> Double -> IO (D.DataFrame, D.DataFrame)+mkOneToOne n overlap = do+    g <- newIOGenM =<< newStdGen+    let rightSize = max 1 (round (fromIntegral n * overlap))+    -- Left: keys 0..n-1 with a payload column+    let leftKeys = [0 :: Int .. n - 1]+        leftVals = [0 :: Int .. n - 1]+        leftDf =+            D.fromNamedColumns+                [ ("key", D.fromList leftKeys)+                , ("A", D.fromList leftVals)+                ]+    -- Right: take first `rightSize` keys, add non-overlapping keys for the rest+    rightPayload <- mapM (\_ -> uniformRM (0 :: Int, 1_000_000) g) [1 .. rightSize]+    let rightKeys = [0 :: Int .. rightSize - 1]+        rightDf =+            D.fromNamedColumns+                [ ("key", D.fromList rightKeys)+                , ("B", D.fromList rightPayload)+                ]+    return (leftDf, rightDf)++{- | Generate a pair of dataframes for a many-to-many join scenario.+  Keys are drawn from [0..cardinality-1], so rows share keys.+-}+mkManyToMany :: Int -> Int -> Int -> IO (D.DataFrame, D.DataFrame)+mkManyToMany leftRows rightRows cardinality = do+    g <- newIOGenM =<< newStdGen+    leftKeys <- mapM (\_ -> uniformRM (0 :: Int, cardinality - 1) g) [1 .. leftRows]+    rightKeys <-+        mapM (\_ -> uniformRM (0 :: Int, cardinality - 1) g) [1 .. rightRows]+    leftVals <- mapM (\_ -> uniformRM (0 :: Int, 1_000_000) g) [1 .. leftRows]+    rightVals <- mapM (\_ -> uniformRM (0 :: Int, 1_000_000) g) [1 .. rightRows]+    let leftDf =+            D.fromNamedColumns+                [ ("key", D.fromList leftKeys)+                , ("A", D.fromList leftVals)+                ]+        rightDf =+            D.fromNamedColumns+                [ ("key", D.fromList rightKeys)+                , ("B", D.fromList rightVals)+                ]+    return (leftDf, rightDf)++{- | Generate a pair of dataframes for a many-to-one join+  (fact table joining a dimension table).+  Left has n rows with keys drawn from [0..dimSize-1].+  Right has exactly dimSize rows with unique keys.+-}+mkManyToOne :: Int -> Int -> IO (D.DataFrame, D.DataFrame)+mkManyToOne factRows dimSize = do+    g <- newIOGenM =<< newStdGen+    factKeys <- mapM (\_ -> uniformRM (0 :: Int, dimSize - 1) g) [1 .. factRows]+    factVals <- mapM (\_ -> uniformRM (0 :: Int, 1_000_000) g) [1 .. factRows]+    dimVals <- mapM (\_ -> uniformRM (0 :: Int, 1_000_000) g) [1 .. dimSize]+    let factDf =+            D.fromNamedColumns+                [ ("key", D.fromList factKeys)+                , ("A", D.fromList factVals)+                ]+        dimDf =+            D.fromNamedColumns+                [ ("key", D.fromList [0 :: Int .. dimSize - 1])+                , ("B", D.fromList dimVals)+                ]+    return (factDf, dimDf)++mkMultiKey :: Int -> Int -> IO (D.DataFrame, D.DataFrame)+mkMultiKey leftRows rightRows = do+    g <- newIOGenM =<< newStdGen+    lk1 <- mapM (\_ -> uniformRM (0 :: Int, 99) g) [1 .. leftRows]+    lk2 <- mapM (\_ -> uniformRM (0 :: Int, 99) g) [1 .. leftRows]+    rk1 <- mapM (\_ -> uniformRM (0 :: Int, 99) g) [1 .. rightRows]+    rk2 <- mapM (\_ -> uniformRM (0 :: Int, 99) g) [1 .. rightRows]+    lv <- mapM (\_ -> uniformRM (0 :: Int, 1_000_000) g) [1 .. leftRows]+    rv <- mapM (\_ -> uniformRM (0 :: Int, 1_000_000) g) [1 .. rightRows]+    let leftDf =+            D.fromNamedColumns+                [ ("key1", D.fromList lk1)+                , ("key2", D.fromList lk2)+                , ("A", D.fromList lv)+                ]+        rightDf =+            D.fromNamedColumns+                [ ("key1", D.fromList rk1)+                , ("key2", D.fromList rk2)+                , ("B", D.fromList rv)+                ]+    return (leftDf, rightDf)+ main :: IO () main = do     output <- readProcess "cabal" ["build", "-O2"] ""@@ -117,8 +220,6 @@         , 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)"@@ -126,21 +227,70 @@                 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"             ]-            -- this file was removed as it's too large you may substitute-            -- with your own if you wish to run it locally on a large file-            -- , bgroup-            --     "customers.csv (334 MB)"-            --     [ bench "Attoparsec" $ nfIO $ parseFile "./data/customers.csv"-            --     , bench "Native Haskell" $ nfIO $ parseFileUnstable "./data/customers.csv"-            --     , bench "SIMD" $ nfIO $ parseFileUnstableSIMD "./data/customers.csv"-            --     ]+        , bgroup+            "join/inner/1:1"+            [ env (mkOneToOne 1_000 1.0) $ \ ~(l, r) ->+                bench "1K rows" $ nf (innerJoin ["key"] r) l+            , env (mkOneToOne 10_000 1.0) $ \ ~(l, r) ->+                bench "10K rows" $ nf (innerJoin ["key"] r) l+            , env (mkOneToOne 100_000 1.0) $ \ ~(l, r) ->+                bench "100K rows" $ nf (innerJoin ["key"] r) l+            ]+        , bgroup+            "join/inner/1:1-partial-overlap"+            [ env (mkOneToOne 100_000 0.5) $ \ ~(l, r) ->+                bench "100K rows, 50% overlap" $ nf (innerJoin ["key"] r) l+            , env (mkOneToOne 100_000 0.1) $ \ ~(l, r) ->+                bench "100K rows, 10% overlap" $ nf (innerJoin ["key"] r) l+            ]+        , bgroup+            "join/inner/many:1"+            [ env (mkManyToOne 10_000 100) $ \ ~(fact, dim) ->+                bench "10K fact x 100 dim" $ nf (innerJoin ["key"] dim) fact+            , env (mkManyToOne 100_000 1_000) $ \ ~(fact, dim) ->+                bench "100K fact x 1K dim" $ nf (innerJoin ["key"] dim) fact+            , env (mkManyToOne 100_000 100) $ \ ~(fact, dim) ->+                bench "100K fact x 100 dim" $ nf (innerJoin ["key"] dim) fact+            ]+        , bgroup+            "join/inner/many:many"+            [ env (mkManyToMany 1_000 1_000 100) $ \ ~(l, r) ->+                bench "1Kx1K, 100 keys" $ nf (innerJoin ["key"] r) l+            , env (mkManyToMany 10_000 10_000 1_000) $ \ ~(l, r) ->+                bench "10Kx10K, 1K keys" $ nf (innerJoin ["key"] r) l+            , env (mkManyToMany 10_000 10_000 100) $ \ ~(l, r) ->+                bench "10Kx10K, 100 keys" $ nf (innerJoin ["key"] r) l+            ]+        , bgroup+            "join/left"+            [ env (mkOneToOne 10_000 1.0) $ \ ~(l, r) ->+                bench "1:1, 10K rows" $ nf (leftJoin ["key"] r) l+            , env (mkOneToOne 100_000 1.0) $ \ ~(l, r) ->+                bench "1:1, 100K rows" $ nf (leftJoin ["key"] r) l+            , env (mkOneToOne 100_000 0.5) $ \ ~(l, r) ->+                bench "1:1, 100K rows, 50%" $ nf (leftJoin ["key"] r) l+            , env (mkManyToOne 100_000 1_000) $ \ ~(fact, dim) ->+                bench "many:1, 100K x 1K" $ nf (leftJoin ["key"] dim) fact+            , env (mkManyToMany 10_000 10_000 1_000) $ \ ~(l, r) ->+                bench "many:many, 10Kx10K" $ nf (leftJoin ["key"] r) l+            ]+        , bgroup+            "join/fullOuter"+            [ env (mkOneToOne 10_000 1.0) $ \ ~(l, r) ->+                bench "1:1, 10K rows" $ nf (fullOuterJoin ["key"] r) l+            , env (mkOneToOne 100_000 1.0) $ \ ~(l, r) ->+                bench "1:1, 100K rows" $ nf (fullOuterJoin ["key"] r) l+            , env (mkOneToOne 100_000 0.5) $ \ ~(l, r) ->+                bench "1:1, 100K rows, 50%" $ nf (fullOuterJoin ["key"] r) l+            , env (mkManyToOne 100_000 1_000) $ \ ~(fact, dim) ->+                bench "many:1, 100K x 1K" $ nf (fullOuterJoin ["key"] dim) fact+            ]+        , bgroup+            "join/multiKey"+            [ env (mkMultiKey 10_000 10_000) $ \ ~(l, r) ->+                bench "inner 10Kx10K, 2 keys" $ nf (innerJoin ["key1", "key2"] r) l+            , env (mkMultiKey 10_000 10_000) $ \ ~(l, r) ->+                bench "left 10Kx10K, 2 keys" $ nf (leftJoin ["key1", "key2"] r) l+            ]         ]
+ 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.0 name:               dataframe-version:            0.4.1.0-+version:            2.3.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,104 +39,185 @@   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, streamly). 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.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.Internal.DataFrame,-                    DataFrame.Monad,-                    DataFrame.DecisionTree+                    DataFrame.Typed+    reexported-modules: DataFrame.Functions,+                        DataFrame.Synthesis,+                        DataFrame.Display.Web.Plot,+                        DataFrame.Display.Web.Chart,+                        DataFrame.Display.Web.Chart.Typed,+                        DataFrame.Internal.Types,+                        DataFrame.Internal.Expression,+                        DataFrame.Internal.Grouping,+                        DataFrame.Internal.Interpreter,+                        DataFrame.Internal.Nullable,+                        DataFrame.Internal.Parsing,+                        DataFrame.Internal.Column,+                        DataFrame.Internal.Binary,+                        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.Monad,+                        DataFrame.DecisionTree,+                        DataFrame.IO.JSON,+                        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,-                      aeson >= 0.11.0.0 && < 3,-                      array >= 0.5.4.0 && < 0.6,-                      attoparsec >= 0.12 && < 0.15,-                      bytestring >= 0.11 && < 0.13,-                      bytestring-lexing >= 0.5 && < 0.6,-                      cassava >= 0.1 && < 1,-                      containers >= 0.6.7 && < 0.9,-                      directory >= 1.3.0.0 && < 2,-                      granite == 0.3.0.5,-                      hashable >= 1.2 && < 2,-                      process ^>= 1.6,-                      snappy-hs ^>= 0.1,-                      random >= 1.3 && < 2,-                      regex-tdfa >= 1.3.0 && < 2,-                      scientific >=0.3.1 && <0.4,-                      template-haskell >= 2.0 && < 3,-                      text >= 2.0 && < 3,-                      time >= 1.12 && < 2,-                      unordered-containers >= 0.1 && < 1,-                      vector ^>= 0.13,-                      vector-algorithms ^>= 0.9,-                      zlib >= 0.5 && < 1,-                      zstd >= 0.1.2.0 && < 0.2,-                      mmap >= 0.5.8 && < 0.6,-                      parallel >= 3.2.2.0 && < 5,-                      filepath >= 1.4 && < 2,-                      Glob >= 0.10 && < 1,+                      dataframe-core ^>= 1.1,+                      dataframe-json ^>= 1.0,+                      dataframe-operations >= 1.1.1 && < 1.2,+                      dataframe-parsing ^>= 1.0.2,+                      dataframe-viz ^>= 1.0.3,+                      dataframe-learn ^>= 1.1 +    if !flag(no-csv)+        reexported-modules: DataFrame.IO.CSV+        build-depends:   dataframe-csv ^>= 1.0.2+        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+        build-depends:   dataframe-parquet ^>= 1.1+        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.Lazy.Internal.DataFrame,+                            DataFrame.Lazy.Internal.LogicalPlan,+                            DataFrame.Lazy.Internal.PhysicalPlan,+                            DataFrame.Lazy.Internal.Optimizer,+                            DataFrame.Lazy.Internal.Executor,+                            DataFrame.Typed.Lazy+        build-depends:   dataframe-lazy ^>= 1.1+        cpp-options:     -DWITH_LAZY++    if !flag(no-th)+        build-depends:   dataframe-th ^>= 1.0.1.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.0+        cpp-options:     -DWITH_CSV_TH++    if !flag(no-th) && !flag(no-parquet)+        build-depends:   dataframe-parquet-th ^>= 1.0+        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+                      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 ^>= 1.1,+        dataframe-csv ^>= 1.0.2,+        dataframe-json ^>= 1.0,+        dataframe-lazy ^>= 1.1,+        dataframe-operations >= 1.1.1 && < 1.2,+        dataframe-parquet ^>= 1.1,+        dataframe-parsing ^>= 1.0.2,+        text        >= 2.1 && < 3,+        aeson       >= 0.11 && < 3,+        bytestring  >= 0.11 && < 0.13,+        containers  >= 0.6.7 && < 0.9,+        vector      ^>= 0.13     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.4,+                      dataframe >= 1 && < 3,+                      dataframe-operations >= 1.1.1 && < 1.2,                       random >= 1 && < 2,                       time >= 1.12 && < 2,                       vector ^>= 0.13,@@ -143,9 +229,12 @@     import: warnings     main-is: Synthesis.hs     build-depends:    base >= 4 && < 5,-                      dataframe ^>= 0.4,+                      dataframe >= 1 && < 3,+                      dataframe-core ^>= 1.1,+                      dataframe-learn ^>= 1.1,+                      dataframe-operations >= 1.1.1 && < 1.2,                       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@@ -157,8 +246,31 @@                       directory >= 1.3.0.0 && < 2,                       filepath >= 1.4 && < 2,                       process >= 1.6 && < 2,+                      time >= 1.12 && < 2+    if !os(windows)+        build-depends: unix >= 2 && < 3+    hs-source-dirs:   app+    default-language: Haskell2010+    ghc-options: -rtsopts -threaded -with-rtsopts=-N++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 >= 1 && < 3,+                      dataframe-core ^>= 1.1,+                      dataframe-lazy ^>= 1.1,+                      dataframe-parsing ^>= 1.0.2,+                      directory >= 1.3.0.0 && < 2,+                      random >= 1 && < 2,+                      text >= 2.1 && < 3,                       time >= 1.12 && < 2,-                      unix >= 2 && < 3     hs-source-dirs:   app     default-language: Haskell2010     ghc-options: -rtsopts -threaded -with-rtsopts=-N@@ -170,8 +282,10 @@     hs-source-dirs: benchmark     build-depends: base >= 4 && < 5,                    criterion >= 1 && < 2,+                   deepseq >= 1.4 && < 2,                    process >= 1.6 && < 2,-                   dataframe ^>= 0.4+                   dataframe >= 1 && < 3,+                   random >= 1 && < 2,     default-language: Haskell2010     ghc-options:       -threaded@@ -182,35 +296,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,+                   Cart,+                   DecisionTree,                    Functions,                    GenDataFrame,+                   Internal.ColumnBuilder,+                   Internal.DictEncode,+                   Internal.Markdown,+                   Internal.PackedText,+                   Internal.Parsing,+                   PackedTextMigration,+                   Learn.Numerics,+                   Learn.Denotation,+                   Learn.Models,+                   Learn.Ensembles,+                   Learn.Symbolic,+                   Learn.SklearnParity,+                   Learn.Synthesis,+                   Learn.MetricsTests,+                   Learn.Metamorphic,+                   Learn.EdgeCases,+                   Learn.NumericalRigor,+                   IO.CSV,+                   IO.CsvGolden,+                   IO.JSON,+                   LinearSolver,                    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,-                   Parquet+                   Operations.Typing,+                   Operations.VectorKernel,+                   Operations.Record,+                   LazyParquet,+                   LazyParity,+                   Parquet,+                   ParquetTestData,+                   Plotting,+                   Properties,+                   Properties.Categorical,+                   Properties.Simplify,+                   Simplify,+                   TreePruning,+                   Worklist,+                   Monad     build-depends:  base >= 4 && < 5,-                    dataframe ^>= 0.4,-                    directory >= 1.3.0.0 && < 2,+                    aeson >= 0.11.0.0 && < 3,+                    bytestring >= 0.11 && < 0.13,+                    dataframe >= 1 && < 3,+                    dataframe-core ^>= 1.1,+                    dataframe-csv ^>= 1.0.2,+                    dataframe-json ^>= 1.0,+                    dataframe-lazy ^>= 1.1,+                    dataframe-learn ^>= 1.1,+                    dataframe-operations >= 1.1.1 && < 1.2,+                    dataframe-parquet ^>= 1.1,+                    dataframe-parsing ^>= 1.0.2,                     HUnit ^>= 1.6,                     QuickCheck >= 2 && < 3,-                    random >= 1 && < 2,                     random-shuffle >= 0.0.4 && < 1,                     random >= 1 && < 2,-                    text >= 2.0 && < 3,+                    text >= 2.1 && < 3,                     time >= 1.12 && < 2,                     vector ^>= 0.13,+                    temporary >= 1.3 && < 1.4,+                    directory >= 1.3 && < 1.4,                     containers >= 0.6.7 && < 0.9+    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.13,+                    dataframe >= 1 && < 3,+                    dataframe-core ^>= 1.1,+                    dataframe-operations >= 1.1.1 && < 1.2,+                    HUnit ^>= 1.6,+                    text >= 2.1 && < 3,+                    vector ^>= 0.13     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,477 @@+{-# 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 DataFrame.Internal.Schema (Schema, makeSchema, schemaType)+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 ((.=))++-- ---------------------------------------------------------------------------+-- 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) -- NaN when correlation is undefined+            -- Return a single-row frame: { first, second, correlation }+    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 the same frame so callers that pipe through .collect() get the+    -- written data back too. Callers that don't care can discard.+    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+            ++ "'"
+ ffi/DataFrame/IR/ExprJson.hs view
@@ -0,0 +1,701 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | JSON wire format for 'Expr' values.++  The Python bindings build expression trees as JSON; this module decodes them+  back into 'Expr' GADTs (typed) and encodes them in the reverse direction+  (used for shipping fitted decision trees back to Python).++  Encoded shape:++  > { "node": "col"   , "out_type": "double", "name": "x" }+  > { "node": "lit"   , "out_type": "int"   , "value": 42 }+  > { "node": "unary" , "out_type": "double", "op": "toDouble"+  >                   , "arg_type": "int"   , "arg": <expr> }+  > { "node": "binary", "out_type": "bool"  , "op": "leq"+  >                   , "arg_type": "double", "lhs": <expr>, "rhs": <expr> }+  > { "node": "if"    , "out_type": "text"  , "cond": <expr>+  >                   , "then": <expr>, "else": <expr> }++  Operator names follow 'binaryName' / 'unaryName' from+  "DataFrame.Internal.Expression". Nullable variants ("nulladd", "nullor", …)+  are accepted on decode and treated as their non-null equivalents — Python+  data crosses the FFI boundary as non-null Arrow buffers, so this is lossless+  for the supported workflow.+-}+module DataFrame.IR.ExprJson (+    SomeExpr (..),+    encodeExpr,+    encodeExprToBytes,+    decodeExprAny,+    decodeExprAt,+    parseSomeExpr,+    typeTagOf,+    withTypeTag,+    withOrdTypeTag,+    withNumTypeTag,+    withFracTypeTag,+    withRealTypeTag,+) where++import Data.Aeson (object, (.:), (.=))+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Types as Aeson+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BL+import Data.Int (Int16, Int32, Int64, Int8)+import Data.Proxy (Proxy (..))+import qualified Data.Text as T+import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))+import Data.Word (Word16, Word32, Word64, Word8)+import Type.Reflection (TypeRep, Typeable, typeRep)++import qualified DataFrame.Functions as F+import DataFrame.Internal.Column (Columnable)+import DataFrame.Internal.Expression (+    AggStrategy (..),+    BinaryOp (binaryName),+    Expr (..),+    UnaryOp (unaryName),+ )+import qualified DataFrame.Internal.Expression as F+import DataFrame.Operators (+    ifThenElse,+    (.&&.),+    (./=.),+    (.<.),+    (.<=.),+    (.==.),+    (.>.),+    (.>=.),+    (.||.),+ )++{- | Existential wrapper around a typed expression decoded from JSON.+TODO: mchavinda - Maybe consolidate with UExpr from the main package.+-}+data SomeExpr where+    SomeExpr :: (Columnable a) => TypeRep a -> Expr a -> SomeExpr++-- | Map a Haskell type to its wire-format tag string.+typeTagOf :: forall a. (Typeable a) => Maybe T.Text+typeTagOf+    | Just _ <- testEquality (typeRep @a) (typeRep @Int) = Just "int"+    | Just _ <- testEquality (typeRep @a) (typeRep @Int8) = Just "int8"+    | Just _ <- testEquality (typeRep @a) (typeRep @Int16) = Just "int16"+    | Just _ <- testEquality (typeRep @a) (typeRep @Int32) = Just "int32"+    | Just _ <- testEquality (typeRep @a) (typeRep @Int64) = Just "int64"+    | Just _ <- testEquality (typeRep @a) (typeRep @Word) = Just "word"+    | Just _ <- testEquality (typeRep @a) (typeRep @Word8) = Just "word8"+    | Just _ <- testEquality (typeRep @a) (typeRep @Word16) = Just "word16"+    | Just _ <- testEquality (typeRep @a) (typeRep @Word32) = Just "word32"+    | Just _ <- testEquality (typeRep @a) (typeRep @Word64) = Just "word64"+    | Just _ <- testEquality (typeRep @a) (typeRep @Integer) = Just "integer"+    | Just _ <- testEquality (typeRep @a) (typeRep @Double) = Just "double"+    | Just _ <- testEquality (typeRep @a) (typeRep @Float) = Just "float"+    | Just _ <- testEquality (typeRep @a) (typeRep @Bool) = Just "bool"+    | Just _ <- testEquality (typeRep @a) (typeRep @Char) = Just "char"+    | Just _ <- testEquality (typeRep @a) (typeRep @T.Text) = Just "text"+    | Just _ <- testEquality (typeRep @a) (typeRep @String) = Just "string"+    | otherwise = Nothing++-- | Dispatch on a type tag with a 'Columnable' continuation.+withTypeTag ::+    T.Text ->+    (forall a. (Columnable a) => Proxy a -> Aeson.Parser r) ->+    Aeson.Parser r+withTypeTag t k = case t of+    "int" -> k (Proxy @Int)+    "int8" -> k (Proxy @Int8)+    "int16" -> k (Proxy @Int16)+    "int32" -> k (Proxy @Int32)+    "int64" -> k (Proxy @Int64)+    "word" -> k (Proxy @Word)+    "word8" -> k (Proxy @Word8)+    "word16" -> k (Proxy @Word16)+    "word32" -> k (Proxy @Word32)+    "word64" -> k (Proxy @Word64)+    "integer" -> k (Proxy @Integer)+    "double" -> k (Proxy @Double)+    "float" -> k (Proxy @Float)+    "bool" -> k (Proxy @Bool)+    "char" -> k (Proxy @Char)+    "text" -> k (Proxy @T.Text)+    "string" -> k (Proxy @String)+    _ -> fail $ "DataFrame.IR.ExprJson: unknown type tag: " <> T.unpack t++-- | Subset that supports 'Ord' (for comparison ops).+withOrdTypeTag ::+    T.Text ->+    (forall a. (Columnable a, Ord a) => Proxy a -> Aeson.Parser r) ->+    Aeson.Parser r+withOrdTypeTag t k = case t of+    "int" -> k (Proxy @Int)+    "int8" -> k (Proxy @Int8)+    "int16" -> k (Proxy @Int16)+    "int32" -> k (Proxy @Int32)+    "int64" -> k (Proxy @Int64)+    "word" -> k (Proxy @Word)+    "word8" -> k (Proxy @Word8)+    "word16" -> k (Proxy @Word16)+    "word32" -> k (Proxy @Word32)+    "word64" -> k (Proxy @Word64)+    "integer" -> k (Proxy @Integer)+    "double" -> k (Proxy @Double)+    "float" -> k (Proxy @Float)+    "bool" -> k (Proxy @Bool)+    "char" -> k (Proxy @Char)+    "text" -> k (Proxy @T.Text)+    "string" -> k (Proxy @String)+    _ ->+        fail $ "DataFrame.IR.ExprJson: type does not support ordering: " <> T.unpack t++-- | Subset that supports 'Num' (arithmetic).+withNumTypeTag ::+    T.Text ->+    (forall a. (Columnable a, Num a) => Proxy a -> Aeson.Parser r) ->+    Aeson.Parser r+withNumTypeTag t k = case t of+    "int" -> k (Proxy @Int)+    "int8" -> k (Proxy @Int8)+    "int16" -> k (Proxy @Int16)+    "int32" -> k (Proxy @Int32)+    "int64" -> k (Proxy @Int64)+    "word" -> k (Proxy @Word)+    "word8" -> k (Proxy @Word8)+    "word16" -> k (Proxy @Word16)+    "word32" -> k (Proxy @Word32)+    "word64" -> k (Proxy @Word64)+    "integer" -> k (Proxy @Integer)+    "double" -> k (Proxy @Double)+    "float" -> k (Proxy @Float)+    _ ->+        fail $ "DataFrame.IR.ExprJson: type does not support arithmetic: " <> T.unpack t++-- | Subset that supports 'Fractional' (division).+withFracTypeTag ::+    T.Text ->+    (forall a. (Columnable a, Fractional a) => Proxy a -> Aeson.Parser r) ->+    Aeson.Parser r+withFracTypeTag t k = case t of+    "double" -> k (Proxy @Double)+    "float" -> k (Proxy @Float)+    _ ->+        fail $+            "DataFrame.IR.ExprJson: type does not support fractional division: "+                <> T.unpack t++-- | Subset that supports 'Real' (used by toDouble source types).+withRealTypeTag ::+    T.Text ->+    (forall a. (Columnable a, Real a) => Proxy a -> Aeson.Parser r) ->+    Aeson.Parser r+withRealTypeTag t k = case t of+    "int" -> k (Proxy @Int)+    "int8" -> k (Proxy @Int8)+    "int16" -> k (Proxy @Int16)+    "int32" -> k (Proxy @Int32)+    "int64" -> k (Proxy @Int64)+    "word" -> k (Proxy @Word)+    "word8" -> k (Proxy @Word8)+    "word16" -> k (Proxy @Word16)+    "word32" -> k (Proxy @Word32)+    "word64" -> k (Proxy @Word64)+    "integer" -> k (Proxy @Integer)+    "double" -> k (Proxy @Double)+    "float" -> k (Proxy @Float)+    _ ->+        fail $+            "DataFrame.IR.ExprJson: type does not support Real (toDouble source): "+                <> T.unpack t++-- | Subset that supports 'Floating'.+withFloatingTypeTag ::+    T.Text ->+    (forall a. (Columnable a, Floating a) => Proxy a -> Aeson.Parser r) ->+    Aeson.Parser r+withFloatingTypeTag t k = case t of+    "double" -> k (Proxy @Double)+    "float" -> k (Proxy @Float)+    _ ->+        fail $ "DataFrame.IR.ExprJson: type does not support Floating: " <> T.unpack t++{- | Encode an 'Expr' to a JSON value. Returns 'Left' on unsupported+constructors (Agg, Over, CastWith, CastExprWith) or unsupported operator+names (binaryUdf, unaryUdf, …).+-}+encodeExpr :: forall a. (Columnable a) => Expr a -> Either String Aeson.Value+encodeExpr expr = case expr of+    Col name -> do+        outTag <- requireTypeTag @a+        Right $+            object+                [ "node" .= ("col" :: T.Text)+                , "out_type" .= outTag+                , "name" .= name+                ]+    Lit v -> do+        outTag <- requireTypeTag @a+        litVal <- encodeLit @a v+        Right $+            object+                [ "node" .= ("lit" :: T.Text)+                , "out_type" .= outTag+                , "value" .= litVal+                ]+    Unary op (arg :: Expr b) -> do+        outTag <- requireTypeTag @a+        argTag <- requireTypeTag @b+        opTag <- recognizeUnary (unaryName op)+        argEnc <- encodeExpr arg+        Right $+            object+                [ "node" .= ("unary" :: T.Text)+                , "out_type" .= outTag+                , "op" .= opTag+                , "arg_type" .= argTag+                , "arg" .= argEnc+                ]+    Binary op (lhs :: Expr c) (rhs :: Expr b) -> do+        outTag <- requireTypeTag @a+        argTag <- requireTypeTag @c+        opTag <- recognizeBinary (binaryName op)+        lEnc <- encodeExpr lhs+        rEnc <- encodeExpr rhs+        Right $+            object+                [ "node" .= ("binary" :: T.Text)+                , "out_type" .= outTag+                , "op" .= opTag+                , "arg_type" .= argTag+                , "lhs" .= lEnc+                , "rhs" .= rEnc+                ]+    If cond th el -> do+        outTag <- requireTypeTag @a+        cEnc <- encodeExpr cond+        tEnc <- encodeExpr th+        eEnc <- encodeExpr el+        Right $+            object+                [ "node" .= ("if" :: T.Text)+                , "out_type" .= outTag+                , "cond" .= cEnc+                , "then" .= tEnc+                , "else" .= eEnc+                ]+    CastWith{} ->+        Left+            "DataFrame.IR.ExprJson.encodeExpr: CastWith is not supported in the wire format"+    CastExprWith{} ->+        Left+            "DataFrame.IR.ExprJson.encodeExpr: CastExprWith is not supported in the wire format"+    Agg (strat :: F.AggStrategy a b) (inner :: F.Expr b) -> do+        outTag <- requireTypeTag @a+        argTag <- requireTypeTag @b+        innerEnc <- encodeExpr inner+        Right $+            object+                [ "node" .= ("agg" :: T.Text)+                , "out_type" .= outTag+                , "agg" .= aggStrategyName strat+                , "arg_type" .= argTag+                , "arg" .= innerEnc+                ]+    Over names inner -> do+        outTag <- requireTypeTag @a+        innerEnc <- encodeExpr inner+        Right $+            object+                [ "node" .= ("over" :: T.Text)+                , "out_type" .= outTag+                , "partition_by" .= names+                , "arg" .= innerEnc+                ]+  where+    requireTypeTag :: forall x. (Typeable x) => Either String T.Text+    requireTypeTag = case typeTagOf @x of+        Just t -> Right t+        Nothing ->+            Left $+                "DataFrame.IR.ExprJson.encodeExpr: unsupported type: " <> show (typeRep @x)++-- | Wire-format name for an aggregation strategy.+aggStrategyName :: AggStrategy a b -> T.Text+aggStrategyName (CollectAgg n _) = n+aggStrategyName (FoldAgg n _ _) = n+aggStrategyName (MergeAgg n _ _ _ _) = n++-- | Encode an 'Expr' to a strict 'BS.ByteString' of JSON.+encodeExprToBytes ::+    forall a. (Columnable a) => Expr a -> Either String BS.ByteString+encodeExprToBytes e = BL.toStrict . Aeson.encode <$> encodeExpr e++encodeLit :: forall a. (Columnable a) => a -> Either String Aeson.Value+encodeLit v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+        Right $ Aeson.toJSON (v :: Int)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int8) =+        Right $ Aeson.toJSON (v :: Int8)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int16) =+        Right $ Aeson.toJSON (v :: Int16)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int32) =+        Right $ Aeson.toJSON (v :: Int32)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int64) =+        Right $ Aeson.toJSON (v :: Int64)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Word) =+        Right $ Aeson.toJSON (v :: Word)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Word8) =+        Right $ Aeson.toJSON (v :: Word8)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Word16) =+        Right $ Aeson.toJSON (v :: Word16)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Word32) =+        Right $ Aeson.toJSON (v :: Word32)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Word64) =+        Right $ Aeson.toJSON (v :: Word64)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Integer) =+        Right $ Aeson.toJSON (v :: Integer)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+        Right $ Aeson.toJSON (v :: Double)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Float) =+        Right $ Aeson.toJSON (v :: Float)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Bool) =+        Right $ Aeson.toJSON (v :: Bool)+    | Just Refl <- testEquality (typeRep @a) (typeRep @T.Text) =+        Right $ Aeson.toJSON (v :: T.Text)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Char) =+        Right $ Aeson.toJSON [v :: Char]+    | Just Refl <- testEquality (typeRep @a) (typeRep @String) =+        Right $ Aeson.toJSON (v :: String)+    | otherwise =+        Left $+            "DataFrame.IR.ExprJson.encodeLit: unsupported type: " <> show (typeRep @a)++-- | Map a 'binaryName' string to a wire-format op name. Errors on opaque UDF names.+recognizeBinary :: T.Text -> Either String T.Text+recognizeBinary n = case n of+    "add" -> Right "add"+    "sub" -> Right "sub"+    "mult" -> Right "mult"+    "divide" -> Right "divide"+    "eq" -> Right "eq"+    "neq" -> Right "neq"+    "lt" -> Right "lt"+    "leq" -> Right "leq"+    "gt" -> Right "gt"+    "geq" -> Right "geq"+    "and" -> Right "and"+    "or" -> Right "or"+    -- Nullable-aware aliases (lossless on non-null inputs)+    "nulladd" -> Right "add"+    "nullsub" -> Right "sub"+    "nullmul" -> Right "mult"+    "nulldiv" -> Right "divide"+    "nulland" -> Right "and"+    "nullor" -> Right "or"+    "exponentiate" -> Right "exponentiate"+    "logBase" -> Right "logBase"+    "div" -> Right "div"+    "mod" -> Right "mod"+    "pow" -> Right "pow"+    other ->+        Left $+            "DataFrame.IR.ExprJson: unsupported binary op (cannot serialize): "+                <> T.unpack other++-- | Map a 'unaryName' string to a wire-format op name. Errors on opaque UDF names.+recognizeUnary :: T.Text -> Either String T.Text+recognizeUnary n = case n of+    "not" -> Right "not"+    "negate" -> Right "negate"+    "abs" -> Right "abs"+    "signum" -> Right "signum"+    "exp" -> Right "exp"+    "sqrt" -> Right "sqrt"+    "log" -> Right "log"+    "sin" -> Right "sin"+    "cos" -> Right "cos"+    "tan" -> Right "tan"+    "asin" -> Right "asin"+    "acos" -> Right "acos"+    "atan" -> Right "atan"+    "sinh" -> Right "sinh"+    "cosh" -> Right "cosh"+    "asinh" -> Right "asinh"+    "acosh" -> Right "acosh"+    "atanh" -> Right "atanh"+    "toDouble" -> Right "toDouble"+    other ->+        Left $+            "DataFrame.IR.ExprJson: unsupported unary op (cannot serialize): "+                <> T.unpack other++{- | Decode a JSON value into a 'SomeExpr'. The output type comes from the+@out_type@ field on the root node.+-}+decodeExprAny :: Aeson.Value -> Either String SomeExpr+decodeExprAny = Aeson.parseEither parseSomeExpr++-- | Decode a JSON value, asserting the result type matches @a@.+decodeExprAt ::+    forall a. (Columnable a) => Aeson.Value -> Either String (Expr a)+decodeExprAt v = do+    SomeExpr trep expr <- decodeExprAny v+    case testEquality trep (typeRep @a) of+        Just Refl -> Right expr+        Nothing ->+            Left $+                "DataFrame.IR.ExprJson.decodeExprAt: expected "+                    <> show (typeRep @a)+                    <> " but got "+                    <> show trep++-- | The Aeson.Parser entry point — useful when composing with bigger parsers.+parseSomeExpr :: Aeson.Value -> Aeson.Parser SomeExpr+parseSomeExpr = Aeson.withObject "Expr" $ \o -> do+    node <- o .: "node" :: Aeson.Parser T.Text+    outType <- o .: "out_type" :: Aeson.Parser T.Text+    case node of+        "col" -> do+            name <- o .: "name" :: Aeson.Parser T.Text+            withTypeTag outType $ \(_ :: Proxy a) ->+                return $ SomeExpr (typeRep @a) (Col @a name)+        "lit" -> do+            rawVal <- o .: "value"+            withTypeTag outType $ \(_ :: Proxy a) -> do+                litVal <- decodeLit @a rawVal+                return $ SomeExpr (typeRep @a) (Lit @a litVal)+        "if" -> do+            rawCond <- o .: "cond"+            rawThen <- o .: "then"+            rawElse <- o .: "else"+            cond <- parseExprAt @Bool rawCond+            withTypeTag outType $ \(_ :: Proxy a) -> do+                thenE <- parseExprAt @a rawThen+                elseE <- parseExprAt @a rawElse+                return $ SomeExpr (typeRep @a) (ifThenElse cond thenE elseE)+        "unary" -> do+            op <- o .: "op" :: Aeson.Parser T.Text+            argType <- o .: "arg_type" :: Aeson.Parser T.Text+            rawArg <- o .: "arg"+            parseUnary op outType argType rawArg+        "binary" -> do+            op <- o .: "op" :: Aeson.Parser T.Text+            argType <- o .: "arg_type" :: Aeson.Parser T.Text+            rawLhs <- o .: "lhs"+            rawRhs <- o .: "rhs"+            parseBinary op outType argType rawLhs rawRhs+        other -> fail $ "DataFrame.IR.ExprJson: unknown node kind: " <> T.unpack other++parseExprAt :: forall a. (Columnable a) => Aeson.Value -> Aeson.Parser (Expr a)+parseExprAt v = do+    SomeExpr trep expr <- parseSomeExpr v+    case testEquality trep (typeRep @a) of+        Just Refl -> return expr+        Nothing ->+            fail $+                "DataFrame.IR.ExprJson: expected "+                    <> show (typeRep @a)+                    <> " but got "+                    <> show trep++decodeLit :: forall a. (Columnable a) => Aeson.Value -> Aeson.Parser a+decodeLit v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int) = parseAs @Int v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int8) = parseAs @Int8 v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int16) = parseAs @Int16 v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int32) = parseAs @Int32 v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int64) = parseAs @Int64 v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Word) = parseAs @Word v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Word8) = parseAs @Word8 v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Word16) = parseAs @Word16 v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Word32) = parseAs @Word32 v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Word64) = parseAs @Word64 v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Integer) = parseAs @Integer v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Double) = parseAs @Double v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Float) = parseAs @Float v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Bool) = parseAs @Bool v+    | Just Refl <- testEquality (typeRep @a) (typeRep @T.Text) = parseAs @T.Text v+    | Just Refl <- testEquality (typeRep @a) (typeRep @Char) = parseChar v+    | Just Refl <- testEquality (typeRep @a) (typeRep @String) = parseAs @String v+    | otherwise =+        fail $+            "DataFrame.IR.ExprJson.decodeLit: unsupported type: " <> show (typeRep @a)++parseAs :: forall b. (Aeson.FromJSON b) => Aeson.Value -> Aeson.Parser b+parseAs = Aeson.parseJSON++parseChar :: Aeson.Value -> Aeson.Parser Char+parseChar v = do+    s <- Aeson.parseJSON v :: Aeson.Parser T.Text+    case T.unpack s of+        [c] -> return c+        _ ->+            fail $+                "DataFrame.IR.ExprJson: expected single-character string, got: " <> show s++requireTag :: T.Text -> T.Text -> Aeson.Parser ()+requireTag actual expected+    | actual == expected = return ()+    | otherwise =+        fail $+            "DataFrame.IR.ExprJson: type mismatch — expected "+                <> T.unpack expected+                <> " but got "+                <> T.unpack actual++requireSame :: T.Text -> T.Text -> Aeson.Parser ()+requireSame a b+    | a == b = return ()+    | otherwise =+        fail $+            "DataFrame.IR.ExprJson: type mismatch — "+                <> T.unpack a+                <> " vs "+                <> T.unpack b++parseUnary ::+    T.Text -> T.Text -> T.Text -> Aeson.Value -> Aeson.Parser SomeExpr+parseUnary op outType argType rawArg = case op of+    "not" -> do+        requireTag outType "bool"+        requireTag argType "bool"+        arg <- parseExprAt @Bool rawArg+        return $ SomeExpr (typeRep @Bool) (F.not arg)+    "negate" -> withNumTypeTag outType $ \(_ :: Proxy a) -> do+        requireSame outType argType+        arg <- parseExprAt @a rawArg+        return $ SomeExpr (typeRep @a) (negate arg)+    "abs" -> withNumTypeTag outType $ \(_ :: Proxy a) -> do+        requireSame outType argType+        arg <- parseExprAt @a rawArg+        return $ SomeExpr (typeRep @a) (abs arg)+    "signum" -> withNumTypeTag outType $ \(_ :: Proxy a) -> do+        requireSame outType argType+        arg <- parseExprAt @a rawArg+        return $ SomeExpr (typeRep @a) (signum arg)+    "toDouble" -> do+        requireTag outType "double"+        withRealTypeTag argType $ \(_ :: Proxy a) -> do+            arg <- parseExprAt @a rawArg+            return $ SomeExpr (typeRep @Double) (F.toDouble arg)+    "exp" -> floatingUnary outType argType rawArg exp+    "sqrt" -> floatingUnary outType argType rawArg sqrt+    "log" -> floatingUnary outType argType rawArg log+    "sin" -> floatingUnary outType argType rawArg sin+    "cos" -> floatingUnary outType argType rawArg cos+    "tan" -> floatingUnary outType argType rawArg tan+    "asin" -> floatingUnary outType argType rawArg asin+    "acos" -> floatingUnary outType argType rawArg acos+    "atan" -> floatingUnary outType argType rawArg atan+    "sinh" -> floatingUnary outType argType rawArg sinh+    "cosh" -> floatingUnary outType argType rawArg cosh+    "asinh" -> floatingUnary outType argType rawArg asinh+    "acosh" -> floatingUnary outType argType rawArg acosh+    "atanh" -> floatingUnary outType argType rawArg atanh+    other -> fail $ "DataFrame.IR.ExprJson: unsupported unary op: " <> T.unpack other++floatingUnary ::+    T.Text ->+    T.Text ->+    Aeson.Value ->+    (forall x. (Columnable x, Floating x) => Expr x -> Expr x) ->+    Aeson.Parser SomeExpr+floatingUnary outType argType rawArg f = withFloatingTypeTag outType $ \(_ :: Proxy a) -> do+    requireSame outType argType+    arg <- parseExprAt @a rawArg+    return $ SomeExpr (typeRep @a) (f arg)++parseBinary ::+    T.Text ->+    T.Text ->+    T.Text ->+    Aeson.Value ->+    Aeson.Value ->+    Aeson.Parser SomeExpr+parseBinary op outType argType rawLhs rawRhs = case op of+    "eq" -> do+        requireTag outType "bool"+        withTypeTag argType $ \(_ :: Proxy a) -> do+            l <- parseExprAt @a rawLhs+            r <- parseExprAt @a rawRhs+            return $ SomeExpr (typeRep @Bool) (l .==. r)+    "neq" -> do+        requireTag outType "bool"+        withTypeTag argType $ \(_ :: Proxy a) -> do+            l <- parseExprAt @a rawLhs+            r <- parseExprAt @a rawRhs+            return $ SomeExpr (typeRep @Bool) (l ./=. r)+    "lt" -> do+        requireTag outType "bool"+        withOrdTypeTag argType $ \(_ :: Proxy a) -> do+            l <- parseExprAt @a rawLhs+            r <- parseExprAt @a rawRhs+            return $ SomeExpr (typeRep @Bool) (l .<. r)+    "leq" -> do+        requireTag outType "bool"+        withOrdTypeTag argType $ \(_ :: Proxy a) -> do+            l <- parseExprAt @a rawLhs+            r <- parseExprAt @a rawRhs+            return $ SomeExpr (typeRep @Bool) (l .<=. r)+    "gt" -> do+        requireTag outType "bool"+        withOrdTypeTag argType $ \(_ :: Proxy a) -> do+            l <- parseExprAt @a rawLhs+            r <- parseExprAt @a rawRhs+            return $ SomeExpr (typeRep @Bool) (l .>. r)+    "geq" -> do+        requireTag outType "bool"+        withOrdTypeTag argType $ \(_ :: Proxy a) -> do+            l <- parseExprAt @a rawLhs+            r <- parseExprAt @a rawRhs+            return $ SomeExpr (typeRep @Bool) (l .>=. r)+    "and" -> do+        requireTag outType "bool"+        l <- parseExprAt @Bool rawLhs+        r <- parseExprAt @Bool rawRhs+        return $ SomeExpr (typeRep @Bool) (l .&&. r)+    "or" -> do+        requireTag outType "bool"+        l <- parseExprAt @Bool rawLhs+        r <- parseExprAt @Bool rawRhs+        return $ SomeExpr (typeRep @Bool) (l .||. r)+    "add" -> withNumTypeTag outType $ \(_ :: Proxy a) -> do+        requireSame outType argType+        l <- parseExprAt @a rawLhs+        r <- parseExprAt @a rawRhs+        return $ SomeExpr (typeRep @a) (l + r)+    "sub" -> withNumTypeTag outType $ \(_ :: Proxy a) -> do+        requireSame outType argType+        l <- parseExprAt @a rawLhs+        r <- parseExprAt @a rawRhs+        return $ SomeExpr (typeRep @a) (l - r)+    "mult" -> withNumTypeTag outType $ \(_ :: Proxy a) -> do+        requireSame outType argType+        l <- parseExprAt @a rawLhs+        r <- parseExprAt @a rawRhs+        return $ SomeExpr (typeRep @a) (l * r)+    "divide" -> withFracTypeTag outType $ \(_ :: Proxy a) -> do+        requireSame outType argType+        l <- parseExprAt @a rawLhs+        r <- parseExprAt @a rawRhs+        return $ SomeExpr (typeRep @a) (l / r)+    "exponentiate" -> withFloatingTypeTag outType $ \(_ :: Proxy a) -> do+        requireSame outType argType+        l <- parseExprAt @a rawLhs+        r <- parseExprAt @a rawRhs+        return $ SomeExpr (typeRep @a) (l ** r)+    "nulladd" -> parseBinary "add" outType argType rawLhs rawRhs+    "nullsub" -> parseBinary "sub" outType argType rawLhs rawRhs+    "nullmul" -> parseBinary "mult" outType argType rawLhs rawRhs+    "nulldiv" -> parseBinary "divide" outType argType rawLhs rawRhs+    "nulland" -> parseBinary "and" outType argType rawLhs rawRhs+    "nullor" -> parseBinary "or" outType argType rawLhs rawRhs+    other -> fail $ "DataFrame.IR.ExprJson: unsupported binary op: " <> T.unpack other
src/DataFrame.hs view
@@ -1,6 +1,8 @@+{-# LANGUAGE CPP #-}+ {- | Module      : DataFrame-Copyright   : (c) 2025+Copyright   : (c) 2024 - 2026 Michael Chavinda License     : GPL-3.0 Maintainer  : mschavinda@gmail.com Stability   : experimental@@ -69,7 +71,7 @@  longitude          | 20640             | 0             | 0                  | 844             | Double  -- 2) Project & filter-ghci> :exposeColumn df+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@@ -77,10 +79,11 @@ 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 \`F.as\` "max_house_value"]+             [ F.maximum median_house_value \`as\` "max_house_value"]              grouped ghci> D.take 5 summary ----------------------------------@@ -104,6 +107,9 @@   * @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__ @@ -198,6 +204,9 @@     module Row,     module Expression, +    -- * Operator symbols.+    module Operators,+     -- * Display operations     module Display, @@ -206,32 +215,62 @@      -- * 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,--    -- * Convenience functions-    (|>), ) where +-- DecisionTree defines its own `percentile`/`percentiles` helpers; the+-- public versions surfaced through the meta module are the ones from+-- Operations.Statistics / DataFrame.Synthesis. Hide the duplicates to+-- avoid ambiguous-export errors.+import DataFrame.DecisionTree as DecisionTree hiding (percentile, percentiles) import DataFrame.Display as Display (     DisplayOptions (..),     defaultDisplayOptions,@@ -239,25 +278,37 @@  ) 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,  )-import DataFrame.IO.Parquet as Parquet (readParquet, readParquetFiles)-import DataFrame.IO.Unstable.CSV as UnstableCSV (-    fastReadCsvUnstable,-    fastReadTsvUnstable,-    readCsvUnstable,-    readTsvUnstable,+#endif+import DataFrame.IO.JSON as JSON (+    readJSON,+    readJSONEither,  )+#ifdef WITH_PARQUET+import DataFrame.IO.Parquet as Parquet (+    ParquetReadOptions (..),+    defaultParquetReadOptions,+    readParquet,+    readParquetFiles,+    readParquetFilesWithOpts,+    readParquetWithOpts,+ )+#endif import DataFrame.Internal.Column as Column (     Column,     fromList,@@ -266,15 +317,25 @@     hasElemType,     hasMissing,     isNumeric,+    mkRandom,     toList,     toVector,  ) import DataFrame.Internal.DataFrame as Dataframe (     DataFrame,     GroupedDataFrame,+    TruncateConfig (..),+    columnNames,+    defaultTruncateConfig,     empty,+    fromNamedColumns,+    insertColumn,     null,-    toMarkdownTable,+    toCsv,+    toCsv',+    toMarkdown,+    toMarkdown',+    toSeparated,  ) import DataFrame.Internal.Expression as Expression (Expr, prettyPrint) import DataFrame.Internal.Row as Row (@@ -287,8 +348,28 @@     toRowVector,  ) import DataFrame.Internal.Schema as Schema (+    makeSchema,     schemaType,  )+#ifdef WITH_TH+import DataFrame.Internal.Schema.TH as SchemaTH (deriveSchema)+#endif+#ifdef WITH_LAZY+-- Re-export the lazy query engine's entry points. Only types and source+-- constructors are surfaced; the operator surface (filter/select/derive/...)+-- collides with the eager API already re-exported above, so users wanting+-- full lazy access should `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,@@ -297,11 +378,23 @@ import DataFrame.Operations.Core as Core hiding (     ColumnInfo (..),     nulls,-    partiallyParsed,     renameSafe,  )-import DataFrame.Operations.Join as Join+import DataFrame.Operations.Join as Join (+    JoinType (..),+    fullOuterJoin,+    innerJoin,+    join,+    leftJoin,+    rightJoin,+ ) import DataFrame.Operations.Merge as Merge+import DataFrame.Operations.SetOps as SetOps (+    difference,+    intersect,+    symmetricDifference,+    union,+ ) import DataFrame.Operations.Permutation as Permutation (     SortOrder (..),     shuffle,@@ -324,6 +417,7 @@     summarize,     variance,  )+import DataFrame.Synthesis as Synthesis import DataFrame.Operations.Subset as Subset (     SelectionCriteria,     byIndexRange,@@ -348,11 +442,48 @@     sample,     select,     selectBy,+    stratifiedSample,+    stratifiedSplit,     take,     takeLast,  )-import DataFrame.Operations.Transformations as Transformations--import Data.Function ((&))--(|>) = (&)+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.Functions ((./=), (.<), (.<=), (.==), (.>), (.>=))--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 (UnaryOp name op e) = UnaryOp name op (pruneExpr e)-pruneExpr (BinaryOp name op l r) = BinaryOp name op (pruneExpr l) (pruneExpr r)-pruneExpr e = e--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,79 +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 ["  ", border, "  "]-                        : 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,440 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE InstanceSigs #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE UndecidableInstances #-}--module DataFrame.Functions where--import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (-    DataFrame (..),-    unsafeGetColumn,- )-import DataFrame.Internal.Expression (-    Expr (..),-    NamedExpr,-    UExpr (..),- )-import DataFrame.Internal.Statistics--import Control.Applicative-import Control.Monad-import Control.Monad.IO.Class-import qualified Data.Char as Char-import Data.Function-import Data.Functor-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 qualified DataFrame.IO.CSV as CSV-import qualified DataFrame.IO.Parquet as Parquet-import Debug.Trace (trace)-import Language.Haskell.TH-import qualified Language.Haskell.TH.Syntax as TH-import Text.Regex.TDFA-import Prelude hiding (maximum, minimum)-import Prelude as P--infix 4 .==, .<, .<=, .>=, .>, ./=-infixr 3 .&&-infixr 2 .||--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--as :: (Columnable a) => Expr a -> T.Text -> NamedExpr-as expr name = (name, Wrap expr)--infixr 0 .=-(.=) :: (Columnable a) => T.Text -> Expr a -> NamedExpr-(.=) = flip as--ifThenElse :: (Columnable a) => Expr Bool -> Expr a -> Expr a -> Expr a-ifThenElse = If--lit :: (Columnable a) => a -> Expr a-lit = Lit--lift :: (Columnable a, Columnable b) => (a -> b) -> Expr a -> Expr b-lift = UnaryOp "udf"--lift2 ::-    (Columnable c, Columnable b, Columnable a) =>-    (c -> b -> a) -> Expr c -> Expr b -> Expr a-lift2 = BinaryOp "udf"--toDouble :: (Columnable a, Real a) => Expr a -> Expr Double-toDouble = UnaryOp "toDouble" realToFrac--div :: (Integral a, Columnable a) => Expr a -> Expr a -> Expr a-div = BinaryOp "div" Prelude.div--mod :: (Integral a, Columnable a) => Expr a -> Expr a -> Expr a-mod = BinaryOp "mod" Prelude.mod--(.==) :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool-(.==) = BinaryOp "eq" (==)--(./=) :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool-(./=) = BinaryOp "neq" (/=)--eq :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool-eq = BinaryOp "eq" (==)--(.<) :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool-(.<) = BinaryOp "lt" (<)--lt :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool-lt = BinaryOp "lt" (<)---- TODO: Generalize this pattern for other equality functions.-(.>) :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool-(.>) = BinaryOp "gt" (>)--gt :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool-gt = (.>)--(.<=) :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool-(.<=) = BinaryOp "leq" (<=)--leq :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool-leq = (.<=)--(.>=) :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool-(.>=) = BinaryOp "geq" (>=)--geq :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool-geq = BinaryOp "geq" (>=)--and :: Expr Bool -> Expr Bool -> Expr Bool-and = BinaryOp "and" (&&)--(.&&) :: Expr Bool -> Expr Bool -> Expr Bool-(.&&) = BinaryOp "and" (&&)--or :: Expr Bool -> Expr Bool -> Expr Bool-or = BinaryOp "or" (||)--(.||) :: Expr Bool -> Expr Bool -> Expr Bool-(.||) = BinaryOp "or" (||)--not :: Expr Bool -> Expr Bool-not = UnaryOp "not" Prelude.not--count :: (Columnable a) => Expr a -> Expr Int-count expr = AggFold expr "count" 0 (\acc _ -> acc + 1)--collect :: (Columnable a) => Expr a -> Expr [a]-collect expr = AggFold expr "collect" [] (flip (:))--mode :: (Columnable a, Eq a) => Expr a -> Expr a-mode expr =-    AggVector-        expr-        "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 expr = AggReduce expr "minimum" Prelude.min--maximum :: (Columnable a, Ord a) => Expr a -> Expr a-maximum expr = AggReduce expr "maximum" Prelude.max--sum :: forall a. (Columnable a, Num a) => Expr a -> Expr a-sum expr = AggReduce expr "sum" (+)-{-# 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 expr = AggVector expr "sumMaybe" (P.sum . Maybe.catMaybes . V.toList)--mean :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double-mean expr = AggNumericVector expr "mean" mean'-{-# SPECIALIZE DataFrame.Functions.mean :: Expr Double -> Expr Double #-}-{-# SPECIALIZE DataFrame.Functions.mean :: Expr Int -> Expr Double #-}-{-# INLINEABLE DataFrame.Functions.mean #-}--meanMaybe :: forall a. (Columnable a, Real a) => Expr (Maybe a) -> Expr Double-meanMaybe expr = AggVector expr "meanMaybe" (mean' . optionalToDoubleVector)--variance :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double-variance expr = AggNumericVector expr "variance" variance'--median :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double-median expr = AggNumericVector expr "median" median'--medianMaybe :: (Columnable a, Real a) => Expr (Maybe a) -> Expr Double-medianMaybe expr = AggVector expr "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 expr =-    AggNumericVector-        expr-        (T.pack $ "percentile " ++ show n)-        (percentile' n)--stddev :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double-stddev expr = AggNumericVector expr "stddev" (sqrt . variance')--stddevMaybe :: forall a. (Columnable a, Real a) => Expr (Maybe a) -> Expr Double-stddevMaybe expr = AggVector expr "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 _ 0 = Lit 1-pow (Lit n) i = Lit (n ^ i)-pow expr 1 = expr-pow expr i = BinaryOp "pow" (^) expr (lit i)--relu :: (Columnable a, Num a) => Expr a -> Expr a-relu = UnaryOp "relu" (Prelude.max 0)--min :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr a-min = BinaryOp "min" Prelude.min--max :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr a-max = BinaryOp "max" Prelude.max--reduce ::-    forall a b.-    (Columnable a, Columnable b) => Expr b -> a -> (a -> b -> a) -> Expr a-reduce expr = AggFold expr "foldUdf"--toMaybe :: (Columnable a) => Expr a -> Expr (Maybe a)-toMaybe = UnaryOp "toMaybe" Just--fromMaybe :: (Columnable a) => a -> Expr (Maybe a) -> Expr a-fromMaybe d = UnaryOp ("fromMaybe " <> T.pack (show d)) (Maybe.fromMaybe d)--isJust :: (Columnable a) => Expr (Maybe a) -> Expr Bool-isJust = UnaryOp "isJust" Maybe.isJust--isNothing :: (Columnable a) => Expr (Maybe a) -> Expr Bool-isNothing = UnaryOp "isNothing" Maybe.isNothing--fromJust :: (Columnable a) => Expr (Maybe a) -> Expr a-fromJust = UnaryOp "fromJust" Maybe.fromJust--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 = UnaryOp (T.pack ("recode " ++ show mapping)) (`lookup` mapping)--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 =-    UnaryOp-        (T.pack ("recodeWithDefault " ++ show d ++ " " ++ show mapping))-        (Maybe.fromMaybe d . (`lookup` mapping))--firstOrNothing :: (Columnable a) => Expr [a] -> Expr (Maybe a)-firstOrNothing = lift Maybe.listToMaybe--lastOrNothing :: (Columnable a) => Expr [a] -> Expr (Maybe a)-lastOrNothing = lift (Maybe.listToMaybe . reverse)--splitOn :: T.Text -> Expr T.Text -> Expr [T.Text]-splitOn delim = lift (T.splitOn delim)--match :: T.Text -> Expr T.Text -> Expr (Maybe T.Text)-match regex = lift ((\r -> if T.null r then Nothing else Just r) . (=~ regex))--matchAll :: T.Text -> Expr T.Text -> Expr [T.Text]-matchAll regex = lift (getAllTextMatches . (=~ regex))--parseDate :: T.Text -> Expr T.Text -> Expr (Maybe Day)-parseDate format = lift (parseTimeM True defaultTimeLocale (T.unpack format) . T.unpack)--daysBetween :: Expr Day -> Expr Day -> Expr Int-daysBetween d1 d2 = lift fromIntegral (lift2 diffDays d1 d2)--bind ::-    forall a b m.-    (Columnable a, Columnable (m a), Monad m, Columnable b, Columnable (m b)) =>-    (a -> m b) -> Expr (m a) -> Expr (m b)-bind f = lift (>>= f)---- 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---- TODO: We don't have to read the whole file, we can just read the schema.-declareColumnsFromParquetFile :: String -> DecsQ-declareColumnsFromParquetFile path = do-    df <- liftIO (Parquet.readParquet path)-    declareColumns df--declareColumnsFromCsvWithOpts :: CSV.ReadOptions -> String -> DecsQ-declareColumnsFromCsvWithOpts opts path = do-    df <- liftIO (CSV.readSeparated opts path)-    declareColumns df--declareColumns :: DataFrame -> DecsQ-declareColumns df =-    let-        names = (map fst . L.sortBy (compare `on` snd) . M.toList . columnIndices) df-        types = map (columnTypeString . (`unsafeGetColumn` df)) names-        specs = zipWith (\name type_ -> (name, 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,461 +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.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)--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--    lastChunk <- V.unsafeFreeze (VM.slice 0 count active)--    return $! V.concat (reverse (lastChunk : chunks))--freezePagedUnboxedVector ::-    (VUM.Unbox a) => PagedUnboxedVector a -> IO (VU.Vector a)-freezePagedUnboxedVector (PagedUnboxedVector chunksRef activeRef countRef) = do-    count <- readIORef countRef-    active <- readIORef activeRef-    chunks <- readIORef chunksRef--    lastChunk <- VU.unsafeFreeze (VUM.slice 0 count active)-    return $! VU.concat (reverse (lastChunk : chunks))---- | STANDARD CONFIG TYPES-data HeaderSpec = NoHeader | UseFirstRow | ProvideNames [T.Text]-    deriving (Eq, Show)--data TypeSpec = InferFromSample Int | SpecifyTypes [SchemaType] | NoInference---- | CSV read parameters.-data ReadOptions = ReadOptions-    { headerSpec :: HeaderSpec-    -- ^ 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--schemaTypes :: TypeSpec -> [SchemaType]-schemaTypes (SpecifyTypes xs) = xs-schemaTypes _ = []--typeInferenceSampleSize :: TypeSpec -> Int-typeInferenceSampleSize (InferFromSample n) = n-typeInferenceSampleSize _ = 0--defaultReadOptions :: ReadOptions-defaultReadOptions =-    ReadOptions-        { headerSpec = UseFirstRow-        , typeSpec = InferFromSample 100-        , safeRead = True-        , 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 sep = columnSeparator opts-    let stripUtf8Bom bs = fromMaybe bs (BL.stripPrefix "\xEF\xBB\xBF" bs)-    csvData <- stripUtf8Bom <$> BL.readFile path-    let decodeOpts = Csv.defaultDecodeOptions{Csv.decDelimiter = fromIntegral (ord sep)}-    let stream = CsvStream.decodeWith decodeOpts Csv.NoHeader csvData--    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 (V.toList sampleRow) opts-    processStream-        (missingIndicators opts)-        rowsToProcess-        builderCols-        (numColumns opts)--    frozenCols <- V.fromList <$> mapM freezeBuilderColumn builderCols-    let numRows = maybe 0 columnLength (frozenCols V.!? 0)--    let df =-            DataFrame-                frozenCols-                (M.fromList (zip columnNames [0 ..]))-                (numRows, V.length frozenCols)-                M.empty -- TODO give typed column references-    return $-        if shouldInferFromSample (typeSpec opts)-            then-                parseDefaults-                    (missingIndicators opts)-                    (typeInferenceSampleSize (typeSpec opts))-                    (safeRead opts)-                    (dateFormat opts)-                    df-            else-                if not (null (schemaTypes (typeSpec opts)))-                    then parseWithTypes (schemaTypes (typeSpec opts)) df-                    else df--initializeColumns :: [BL.ByteString] -> ReadOptions -> IO [BuilderColumn]-initializeColumns row opts = case typeSpec opts of-    NoInference -> zipWithM initColumn row (expandTypes [])-    InferFromSample _ -> zipWithM initColumn row (expandTypes [])-    SpecifyTypes ts -> zipWithM initColumn row (expandTypes ts)-  where-    expandTypes xs = xs ++ replicate (length row - length xs) (schemaType @T.Text)-    initColumn :: BL.ByteString -> SchemaType -> IO BuilderColumn-    initColumn _ t = do-        validityRef <- newPagedUnboxedVector-        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) ->-    [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 -> [BuilderColumn] -> IO ()-processRow missing !vals !cols = V.zipWithM_ processValue vals (V.fromList 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--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--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 ','--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,302 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.IO.Parquet (-    readParquet,-    readParquetFiles,-) where--import Control.Monad-import Data.Bits-import qualified Data.ByteString as BSO-import Data.Char-import Data.Either-import Data.IORef-import Data.Int-import qualified Data.List as L-import qualified Data.Map as M-import qualified Data.Text as T-import Data.Word-import qualified DataFrame.Internal.Column as DI-import DataFrame.Internal.DataFrame (DataFrame)-import qualified DataFrame.Operations.Core as DI-import DataFrame.Operations.Merge ()-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 System.FilePath ((</>))--{- | Read a parquet file from path and load it into a dataframe.--==== __Example__-@-ghci> D.readParquet ".\/data\/mtcars.parquet"-@--}-readParquet :: FilePath -> IO DataFrame-readParquet path = do-    fileMetadata <- readMetadataFromPath path-    let columnPaths = getColumnPaths (drop 1 $ schema fileMetadata)-    let columnNames = map fst columnPaths--    colMap <- newIORef (M.empty :: M.Map T.Text DI.Column)--    contents <- BSO.readFile path--    let schemaElements = schema fileMetadata-    let getTypeLength :: [String] -> Maybe Int32-        getTypeLength path = findTypeLength schemaElements path 0-          where-            findTypeLength [] _ _ = Nothing-            findTypeLength (s : ss) targetPath depth-                | map T.unpack (pathToElement s ss depth) == targetPath-                    && elementType s == STRING-                    && typeLength s > 0 =-                    Just (typeLength s)-                | otherwise =-                    findTypeLength ss targetPath (if numChildren s > 0 then depth + 1 else depth)--            pathToElement _ _ _ = []--    forM_ (rowGroups fileMetadata) $ \rowGroup -> do-        forM_ (zip (rowGroupColumns rowGroup) [0 ..]) $ \(colChunk, colIdx) -> do-            let metadata = columnMetaData colChunk-            let colPath = columnPathInSchema metadata-            let colName =-                    if null colPath-                        then T.pack $ "col_" ++ show colIdx-                        else T.pack $ last colPath--            let colDataPageOffset = columnDataPageOffset metadata-            let colDictionaryPageOffset = columnDictionaryPageOffset metadata-            let colStart =-                    if colDictionaryPageOffset > 0 && colDataPageOffset > colDictionaryPageOffset-                        then colDictionaryPageOffset-                        else colDataPageOffset-            let colLength = columnTotalCompressedSize metadata--            let columnBytes =-                    map (BSO.index contents . fromIntegral) [colStart .. (colStart + colLength - 1)]--            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 colPath = columnPathInSchema (columnMetaData colChunk)-            let (maxDef, maxRep) = levelsForPath schemaTail colPath-            column <--                processColumnPages-                    (maxDef, maxRep)-                    pages-                    (columnType metadata)-                    primaryEncoding-                    maybeTypeLength--            modifyIORef colMap (M.insertWith DI.concatColumnsEither colName column)--    finalColMap <- readIORef colMap-    let orderedColumns =-            map-                (\name -> (name, finalColMap M.! name))-                (filter (`M.member` finalColMap) columnNames)--    pure $ DI.fromNamedColumns orderedColumns--readParquetFiles :: FilePath -> IO DataFrame-readParquetFiles path = do-    isDir <- doesDirectoryExist path--    let pat = if isDir then path </> "*" else path--    matches <- glob pat--    files <- filterM (fmap not . doesDirectoryExist) matches--    case files of-        [] ->-            error $-                "readParquetFiles: no parquet files found for " ++ path-        _ -> do-            dfs <- mapM readParquet files-            pure (mconcat dfs)--readMetadataFromPath :: FilePath -> IO FileMetadata-readMetadataFromPath path = do-    contents <- BSO.readFile path-    let (size, magicString) = contents `seq` readMetadataSizeFromFooter contents-    when (magicString /= "PAR1") $ error "Invalid Parquet file"-    readMetadata contents size--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)--getColumnPaths :: [SchemaElement] -> [(T.Text, Int)]-getColumnPaths schema = extractLeafPaths schema 0 []-  where-    extractLeafPaths :: [SchemaElement] -> Int -> [T.Text] -> [(T.Text, Int)]-    extractLeafPaths [] _ _ = []-    extractLeafPaths (s : ss) idx path-        | numChildren s == 0 =-            let fullPath = T.intercalate "." (path ++ [elementName s])-             in (fullPath, idx) : extractLeafPaths ss (idx + 1) path-        | otherwise =-            let newPath = if T.null (elementName s) then path else path ++ [elementName s]-                childrenCount = fromIntegral (numChildren s)-                (children, remaining) = splitAt childrenCount ss-                childResults = extractLeafPaths children idx newPath-             in childResults ++ extractLeafPaths remaining (idx + length childResults) path--processColumnPages ::-    (Int, Int) ->-    [Page] ->-    ParquetType ->-    ParquetEncoding ->-    Maybe Int32 ->-    IO DI.Column-processColumnPages (maxDef, maxRep) pages pType _ maybeTypeLength = do-    let dictPages = filter isDictionaryPage pages-    let dataPages = filter isDataPage pages--    let dictValsM =-            case dictPages of-                [] -> Nothing-                (dictPage : _) ->-                    case pageTypeHeader (pageHeader dictPage) of-                        DictionaryPageHeader{..} ->-                            let countForBools =-                                    if pType == PBOOLEAN-                                        then error "is bool" Just dictionaryPageHeaderNumValues-                                        else maybeTypeLength-                             in Just (readDictVals pType (pageBytes dictPage) countForBools)-                        _ -> Nothing--    cols <- forM dataPages $ \page -> do-        case pageTypeHeader (pageHeader page) of-            DataPageHeader{..} -> do-                let n = fromIntegral dataPageHeaderNumValues-                let bs0 = pageBytes page-                let (defLvls, _repLvls, afterLvls) = readLevelsV1 n maxDef maxRep bs0-                let nPresent = length (filter (== maxDef) defLvls)--                case dataPageHeaderEncoding of-                    EPLAIN ->-                        case pType of-                            PBOOLEAN ->-                                let (vals, _) = readNBool nPresent afterLvls-                                 in pure (toMaybeBool maxDef defLvls vals)-                            PINT32 ->-                                let (vals, _) = readNInt32 nPresent afterLvls-                                 in pure (toMaybeInt32 maxDef defLvls vals)-                            PINT64 ->-                                let (vals, _) = readNInt64 nPresent afterLvls-                                 in pure (toMaybeInt64 maxDef defLvls vals)-                            PINT96 ->-                                let (vals, _) = readNInt96Times nPresent afterLvls-                                 in pure (toMaybeUTCTime maxDef defLvls vals)-                            PFLOAT ->-                                let (vals, _) = readNFloat nPresent afterLvls-                                 in pure (toMaybeFloat maxDef defLvls vals)-                            PDOUBLE ->-                                let (vals, _) = readNDouble nPresent afterLvls-                                 in pure (toMaybeDouble maxDef defLvls vals)-                            PBYTE_ARRAY ->-                                let (raws, _) = readNByteArrays nPresent afterLvls-                                    texts = map (T.pack . map (chr . fromIntegral)) raws-                                 in pure (toMaybeText maxDef defLvls texts)-                            PFIXED_LEN_BYTE_ARRAY ->-                                case maybeTypeLength of-                                    Just len ->-                                        let (raws, _) = splitFixed nPresent (fromIntegral len) afterLvls-                                            texts = map (T.pack . map (chr . fromIntegral)) raws-                                         in pure (toMaybeText maxDef defLvls texts)-                                    Nothing -> error "FIXED_LEN_BYTE_ARRAY requires type length"-                            PARQUET_TYPE_UNKNOWN -> error "Cannot read unknown Parquet type"-                    ERLE_DICTIONARY -> decodeDictV1 dictValsM maxDef defLvls nPresent afterLvls-                    EPLAIN_DICTIONARY -> decodeDictV1 dictValsM maxDef defLvls nPresent afterLvls-                    other -> error ("Unsupported v1 encoding: " ++ show other)-            DataPageHeaderV2{..} -> do-                let n = fromIntegral dataPageHeaderV2NumValues-                let bs0 = pageBytes page-                let (defLvls, _repLvls, afterLvls) =-                        readLevelsV2-                            n-                            maxDef-                            maxRep-                            definitionLevelByteLength-                            repetitionLevelByteLength-                            bs0-                let nPresent =-                        if dataPageHeaderV2NumNulls > 0-                            then fromIntegral (dataPageHeaderV2NumValues - dataPageHeaderV2NumNulls)-                            else length (filter (== maxDef) defLvls)--                case dataPageHeaderV2Encoding of-                    EPLAIN ->-                        case pType of-                            PBOOLEAN ->-                                let (vals, _) = readNBool nPresent afterLvls-                                 in pure (toMaybeBool maxDef defLvls vals)-                            PINT32 ->-                                let (vals, _) = readNInt32 nPresent afterLvls-                                 in pure (toMaybeInt32 maxDef defLvls vals)-                            PINT64 ->-                                let (vals, _) = readNInt64 nPresent afterLvls-                                 in pure (toMaybeInt64 maxDef defLvls vals)-                            PINT96 ->-                                let (vals, _) = readNInt96Times nPresent afterLvls-                                 in pure (toMaybeUTCTime maxDef defLvls vals)-                            PFLOAT ->-                                let (vals, _) = readNFloat nPresent afterLvls-                                 in pure (toMaybeFloat maxDef defLvls vals)-                            PDOUBLE ->-                                let (vals, _) = readNDouble nPresent afterLvls-                                 in pure (toMaybeDouble maxDef defLvls vals)-                            PBYTE_ARRAY ->-                                let (raws, _) = readNByteArrays nPresent afterLvls-                                    texts = map (T.pack . map (chr . fromIntegral)) raws-                                 in pure (toMaybeText maxDef defLvls texts)-                            PFIXED_LEN_BYTE_ARRAY ->-                                case maybeTypeLength of-                                    Just len ->-                                        let (raws, _) = splitFixed nPresent (fromIntegral len) afterLvls-                                            texts = map (T.pack . map (chr . fromIntegral)) raws-                                         in pure (toMaybeText maxDef defLvls texts)-                                    Nothing -> error "FIXED_LEN_BYTE_ARRAY requires type length"-                            PARQUET_TYPE_UNKNOWN -> error "Cannot read unknown Parquet type"-                    ERLE_DICTIONARY -> decodeDictV1 dictValsM maxDef defLvls nPresent afterLvls-                    EPLAIN_DICTIONARY -> decodeDictV1 dictValsM maxDef defLvls nPresent afterLvls-                    other -> error ("Unsupported v2 encoding: " ++ show other)-            -- 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"-    -- This is N^2. We should probably use mutable columns here.-    case cols of-        [] -> pure $ DI.fromList ([] :: [Maybe Int])-        (c : cs) ->-            pure $-                L.foldl' (\l r -> fromRight (error "concat failed") (DI.concatColumns l r)) c cs
− src/DataFrame/IO/Parquet/Binary.hs
@@ -1,134 +0,0 @@-{-# LANGUAGE TypeApplications #-}--module DataFrame.IO.Parquet.Binary where--import Control.Monad-import Data.Bits-import qualified Data.ByteString as BS-import Data.Char-import Data.IORef-import Data.Int-import Data.Word--littleEndianWord32 :: [Word8] -> Word32-littleEndianWord32 bytes-    | length bytes >= 4 =-        foldr-            (.|.)-            0-            (zipWith (\b i -> fromIntegral b `shiftL` i) (take 4 bytes) [0, 8, 16, 24])-    | otherwise = littleEndianWord32 (take 4 $ bytes ++ repeat 0)--littleEndianWord64 :: [Word8] -> Word64-littleEndianWord64 bytes =-    foldr-        (.|.)-        0-        (zipWith (\b i -> fromIntegral b `shiftL` i) (take 8 bytes) [0, 8 ..])--littleEndianInt32 :: [Word8] -> Int32-littleEndianInt32 = fromIntegral . littleEndianWord32--word64ToLittleEndian :: Word64 -> [Word8]-word64ToLittleEndian w = map (\i -> fromIntegral (w `shiftR` i)) [0, 8, 16, 24, 32, 40, 48, 56]--word32ToLittleEndian :: Word32 -> [Word8]-word32ToLittleEndian w = map (\i -> fromIntegral (w `shiftR` i)) [0, 8, 16, 24]--readUVarInt :: [Word8] -> (Word64, [Word8])-readUVarInt xs = loop xs 0 0 0-  where-    {--    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 :: [Word8] -> Word64 -> Int -> Int -> (Word64, [Word8])-    loop bs result _ 10 = (result, bs)-    loop (b : bs) result shift i-        | b < 0x80 = (result .|. (fromIntegral b `shiftL` shift), bs)-        | otherwise =-            let payloadBits = fromIntegral (b .&. 0x7f) :: Word64-             in loop bs (result .|. (payloadBits `shiftL` shift)) (shift + 7) (i + 1)-    loop [] _ _ _ = error "readUVarInt: not enough input bytes"--readVarIntFromBytes :: (Integral a) => [Word8] -> (a, [Word8])-readVarIntFromBytes bs = (fromIntegral n, rem)-  where-    (n, rem) = loop 0 0 bs-    loop _ result [] = (result, [])-    loop shift result (x : xs) =-        let res = result .|. (fromIntegral (x .&. 0x7f) :: Integer) `shiftL` shift-         in if x .&. 0x80 /= 0x80 then (res, xs) else loop (shift + 7) res xs--readIntFromBytes :: (Integral a) => [Word8] -> (a, [Word8])-readIntFromBytes bs =-    let (n, rem) = readVarIntFromBytes bs-        u = fromIntegral n :: Word32-     in (fromIntegral $ (fromIntegral (u `shiftR` 1) :: Int32) .^. (-(n .&. 1)), rem)--readInt32FromBytes :: [Word8] -> (Int32, [Word8])-readInt32FromBytes bs =-    let (n', rem) = readVarIntFromBytes @Int64 bs-        n = fromIntegral n' :: Int32-        u = fromIntegral n :: Word32-     in ((fromIntegral (u `shiftR` 1) :: Int32) .^. (-(n .&. 1)), rem)--readAndAdvance :: IORef Int -> 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-    map (chr . fromIntegral) <$> replicateM nameSize (readAndAdvance pos buf)--readByteStringFromBytes :: [Word8] -> ([Word8], [Word8])-readByteStringFromBytes xs =-    let-        (size, rem) = readVarIntFromBytes @Int xs-     in-        splitAt size rem--readByteString :: BS.ByteString -> IORef Int -> IO [Word8]-readByteString buf pos = do-    size <- readVarIntFromBuffer @Int buf pos-    replicateM size (readAndAdvance pos buf)--readByteString' :: BS.ByteString -> Int64 -> IO [Word8]-readByteString' buf size = mapM (`readSingleByte` buf) [0 .. (size - 1)]--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 Data.Int-import Data.Word--data ColumnStatistics = ColumnStatistics-    { columnMin :: [Word8]-    , columnMax :: [Word8]-    , columnNullCount :: Int64-    , columnDistictCount :: Int64-    , columnMinValue :: [Word8]-    , columnMaxValue :: [Word8]-    , isColumnMaxValueExact :: Bool-    , isColumnMinValueExact :: Bool-    }-    deriving (Show, Eq)--emptyColumnStatistics :: ColumnStatistics-emptyColumnStatistics = ColumnStatistics [] [] 0 0 [] [] 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,178 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module DataFrame.IO.Parquet.Dictionary where--import Control.Monad-import Data.Bits-import Data.Char-import Data.Int-import Data.Maybe-import qualified Data.Text as T-import Data.Time-import Data.Word-import DataFrame.IO.Parquet.Binary-import DataFrame.IO.Parquet.Encoding-import DataFrame.IO.Parquet.Levels-import DataFrame.IO.Parquet.Time-import DataFrame.IO.Parquet.Types-import qualified DataFrame.Internal.Column as DI-import GHC.Float--dictCardinality :: DictVals -> Int-dictCardinality (DBool ds) = length ds-dictCardinality (DInt32 ds) = length ds-dictCardinality (DInt64 ds) = length ds-dictCardinality (DInt96 ds) = length ds-dictCardinality (DFloat ds) = length ds-dictCardinality (DDouble ds) = length ds-dictCardinality (DText ds) = length ds--readDictVals :: ParquetType -> [Word8] -> Maybe Int32 -> DictVals-readDictVals PBOOLEAN bs (Just count) = DBool (take (fromIntegral count) $ readPageBool bs)-readDictVals PINT32 bs _ = DInt32 (readPageInt32 bs)-readDictVals PINT64 bs _ = DInt64 (readPageInt64 bs)-readDictVals PINT96 bs _ = DInt96 (readPageInt96Times bs)-readDictVals PFLOAT bs _ = DFloat (readPageFloat bs)-readDictVals PDOUBLE bs _ = DDouble (readPageWord64 bs)-readDictVals PBYTE_ARRAY bs _ = DText (readPageBytes bs)-readDictVals PFIXED_LEN_BYTE_ARRAY bs (Just len) = DText (readPageFixedBytes bs (fromIntegral len))-readDictVals t _ _ = error $ "Unsupported dictionary type: " ++ show t--readPageInt32 :: [Word8] -> [Int32]-readPageInt32 [] = []-readPageInt32 xs = littleEndianInt32 (take 4 xs) : readPageInt32 (drop 4 xs)--readPageWord64 :: [Word8] -> [Double]-readPageWord64 [] = []-readPageWord64 xs =-    castWord64ToDouble (littleEndianWord64 (take 8 xs)) : readPageWord64 (drop 8 xs)--readPageBytes :: [Word8] -> [T.Text]-readPageBytes [] = []-readPageBytes xs =-    let lenBytes = fromIntegral (littleEndianInt32 $ take 4 xs)-        totalBytesRead = lenBytes + 4-     in T.pack (map (chr . fromIntegral) $ take lenBytes (drop 4 xs))-            : readPageBytes (drop totalBytesRead xs)--readPageBool :: [Word8] -> [Bool]-readPageBool [] = []-readPageBool bs =-    concatMap (\b -> map (\i -> (b `shiftR` i) .&. 1 == 1) [0 .. 7]) bs--readPageInt64 :: [Word8] -> [Int64]-readPageInt64 [] = []-readPageInt64 xs = fromIntegral (littleEndianWord64 (take 8 xs)) : readPageInt64 (drop 8 xs)--readPageFloat :: [Word8] -> [Float]-readPageFloat [] = []-readPageFloat xs =-    castWord32ToFloat (littleEndianWord32 (take 4 xs)) : readPageFloat (drop 4 xs)--readNInt96Times :: Int -> [Word8] -> ([UTCTime], [Word8])-readNInt96Times 0 bs = ([], bs)-readNInt96Times k bs =-    let timestamp96 = take 12 bs-        utcTime = int96ToUTCTime timestamp96-        bs' = drop 12 bs-        (times, rest) = readNInt96Times (k - 1) bs'-     in (utcTime : times, rest)--readPageInt96Times :: [Word8] -> [UTCTime]-readPageInt96Times [] = []-readPageInt96Times bs =-    let (times, _) = readNInt96Times (length bs `div` 12) bs-     in times--readPageFixedBytes :: [Word8] -> Int -> [T.Text]-readPageFixedBytes [] _ = []-readPageFixedBytes xs len =-    let chunk = take len xs-        text = T.pack (map (chr . fromIntegral) chunk)-     in text : readPageFixedBytes (drop len xs) len--decodeDictV1 :: Maybe DictVals -> Int -> [Int] -> Int -> [Word8] -> IO DI.Column-decodeDictV1 dictValsM maxDef defLvls nPresent bytes =-    case dictValsM of-        Nothing -> error "Dictionary-encoded page but dictionary is missing"-        Just dictVals ->-            let (idxs, _rest) = decodeDictIndicesV1 nPresent (dictCardinality dictVals) bytes-             in do-                    when (length idxs /= nPresent) $-                        error $-                            "dict index count mismatch: got "-                                ++ show (length idxs)-                                ++ ", expected "-                                ++ show nPresent-                    case dictVals of-                        DBool ds -> do-                            let values = [ds !! i | i <- idxs]-                            pure (toMaybeBool maxDef defLvls values)-                        DInt32 ds -> do-                            let values = [ds !! i | i <- idxs]-                            pure (toMaybeInt32 maxDef defLvls values)-                        DInt64 ds -> do-                            let values = [ds !! i | i <- idxs]-                            pure (toMaybeInt64 maxDef defLvls values)-                        DInt96 ds -> do-                            let values = [ds !! i | i <- idxs]-                            pure (toMaybeUTCTime maxDef defLvls values)-                        DFloat ds -> do-                            let values = [ds !! i | i <- idxs]-                            pure (toMaybeFloat maxDef defLvls values)-                        DDouble ds -> do-                            let values = [ds !! i | i <- idxs]-                            pure (toMaybeDouble maxDef defLvls values)-                        DText ds -> do-                            let values = [ds !! i | i <- idxs]-                            pure (toMaybeText maxDef defLvls values)--toMaybeInt32 :: Int -> [Int] -> [Int32] -> DI.Column-toMaybeInt32 maxDef def xs =-    let filled = stitchNullable maxDef def xs-     in if all isJust filled-            then DI.fromList (map (fromMaybe 0) filled)-            else DI.fromList filled--toMaybeDouble :: Int -> [Int] -> [Double] -> DI.Column-toMaybeDouble maxDef def xs =-    let filled = stitchNullable maxDef def xs-     in if all isJust filled-            then DI.fromList (map (fromMaybe 0) filled)-            else DI.fromList filled--toMaybeText :: Int -> [Int] -> [T.Text] -> DI.Column-toMaybeText maxDef def xs =-    let filled = stitchNullable maxDef def xs-     in if all isJust filled-            then DI.fromList (map (fromMaybe "") filled)-            else DI.fromList filled--toMaybeBool :: Int -> [Int] -> [Bool] -> DI.Column-toMaybeBool maxDef def xs =-    let filled = stitchNullable maxDef def xs-     in if all isJust filled-            then DI.fromList (map (fromMaybe False) filled)-            else DI.fromList filled--toMaybeInt64 :: Int -> [Int] -> [Int64] -> DI.Column-toMaybeInt64 maxDef def xs =-    let filled = stitchNullable maxDef def xs-     in if all isJust filled-            then DI.fromList (map (fromMaybe 0) filled)-            else DI.fromList filled--toMaybeFloat :: Int -> [Int] -> [Float] -> DI.Column-toMaybeFloat maxDef def xs =-    let filled = stitchNullable maxDef def xs-     in if all isJust filled-            then DI.fromList (map (fromMaybe 0.0) filled)-            else DI.fromList filled--toMaybeUTCTime :: Int -> [Int] -> [UTCTime] -> DI.Column-toMaybeUTCTime maxDef def times =-    let filled = stitchNullable maxDef def times-        defaultTime = UTCTime (fromGregorian 1970 1 1) (secondsToDiffTime 0)-     in if all isJust filled-            then DI.fromList (map (fromMaybe defaultTime) filled)-            else DI.fromList filled
− src/DataFrame/IO/Parquet/Encoding.hs
@@ -1,79 +0,0 @@-module DataFrame.IO.Parquet.Encoding where--import Data.Bits-import Data.List (foldl')-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 -> [Word8] -> ([Word32], [Word8])-unpackBitPacked bw count bs-    | count <= 0 = ([], bs)-    | null bs = ([], bs)-    | otherwise =-        let totalBits = bw * count-            totalBytes = (totalBits + 7) `div` 8-            chunk = take totalBytes bs-            rest = drop totalBytes bs-            bits = concatMap (\b -> map (\i -> (b `shiftR` i) .&. 1) [0 .. 7]) chunk-            toN xs = foldl' (\a (i, b) -> a .|. (b `shiftL` i)) 0 (zip [0 ..] xs)--            extractValues _ [] = []-            extractValues n bitsLeft-                | n <= 0 = []-                | length bitsLeft < bw = []-                | otherwise =-                    let (this, bitsLeft') = splitAt bw bitsLeft-                     in toN this : extractValues (n - 1) bitsLeft'--            vals = extractValues count bits-         in (map fromIntegral vals, rest)--decodeRLEBitPackedHybrid :: Int -> Int -> [Word8] -> ([Word32], [Word8])-decodeRLEBitPackedHybrid bw need bs-    | bw == 0 = (replicate need 0, bs)-    | otherwise = go need bs []-  where-    mask :: Word32-    mask = if bw == 32 then maxBound else (1 `shiftL` bw) - 1-    go 0 rest acc = (reverse acc, rest)-    go n rest acc-        | null rest = (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 (take 4 afterHdr)-                            afterV = drop nbytes afterHdr-                            val = word32 .&. mask-                            takeN = min n runLen-                         in go (n - takeN) afterV (replicate takeN val ++ acc)--decodeDictIndicesV1 :: Int -> Int -> [Word8] -> ([Int], [Word8])-decodeDictIndicesV1 need dictCard bs =-    case bs of-        [] -> error "empty dictionary index stream"-        (w0 : rest0) ->-            let bw = fromIntegral w0 :: Int-                (u32s, rest1) = decodeRLEBitPackedHybrid bw need rest0-             in (map fromIntegral u32s, rest1)
− src/DataFrame/IO/Parquet/Levels.hs
@@ -1,107 +0,0 @@-module DataFrame.IO.Parquet.Levels where--import Data.Int-import Data.List-import qualified Data.Text as T-import Data.Word--import DataFrame.IO.Parquet.Binary-import DataFrame.IO.Parquet.Encoding-import DataFrame.IO.Parquet.Thrift-import DataFrame.IO.Parquet.Types--readLevelsV1 :: Int -> Int -> Int -> [Word8] -> ([Int], [Int], [Word8])-readLevelsV1 n maxDef maxRep bs =-    let bwDef = bitWidthForMaxLevel maxDef-        bwRep = bitWidthForMaxLevel maxRep--        (repLvlsU32, afterRep) =-            if bwRep == 0-                then (replicate n 0, bs)-                else-                    let repLength = littleEndianWord32 (take 4 bs)-                        repData = take (fromIntegral repLength) (drop 4 bs)-                        afterRepData = drop (4 + fromIntegral repLength) bs-                        (repVals, _) = decodeRLEBitPackedHybrid bwRep n repData-                     in (repVals, afterRepData)--        (defLvlsU32, afterDef) =-            if bwDef == 0-                then (replicate n 0, afterRep)-                else-                    let defLength = littleEndianWord32 (take 4 afterRep)-                        defData = take (fromIntegral defLength) (drop 4 afterRep)-                        afterDefData = drop (4 + fromIntegral defLength) afterRep-                        (defVals, _) = decodeRLEBitPackedHybrid bwDef n defData-                     in (defVals, afterDefData)-     in (map fromIntegral defLvlsU32, map fromIntegral repLvlsU32, afterDef)--readLevelsV2 ::-    Int -> Int -> Int -> Int32 -> Int32 -> [Word8] -> ([Int], [Int], [Word8])-readLevelsV2 n maxDef maxRep defLen repLen bs =-    let (repBytes, afterRepBytes) = splitAt (fromIntegral repLen) bs-        (defBytes, afterDefBytes) = splitAt (fromIntegral defLen) afterRepBytes-        bwDef = bitWidthForMaxLevel maxDef-        bwRep = bitWidthForMaxLevel maxRep-        (repLvlsU32, _) =-            if bwRep == 0-                then (replicate n 0, repBytes)-                else decodeRLEBitPackedHybrid bwRep n repBytes-        (defLvlsU32, _) =-            if bwDef == 0-                then (replicate n 0, defBytes)-                else decodeRLEBitPackedHybrid bwDef n defBytes-     in (map fromIntegral defLvlsU32, map fromIntegral repLvlsU32, afterDefBytes)--stitchNullable :: Int -> [Int] -> [a] -> [Maybe a]-stitchNullable maxDef = 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'--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 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,389 +0,0 @@-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.IO.Parquet.Page where--import qualified Codec.Compression.GZip as GZip-import Codec.Compression.Zstd.Streaming-import Data.Bits-import qualified Data.ByteString as BSO-import qualified Data.ByteString.Lazy as LB-import Data.Int-import Data.Maybe (fromMaybe, listToMaybe)-import Data.Word-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 -> [Word8] -> IO (Maybe Page, [Word8])-readPage c [] = pure (Nothing, [])-readPage c columnBytes = do-    let (hdr, rem) = readPageHeader emptyPageHeader columnBytes 0-    let compressed = take (fromIntegral $ compressedPageSize hdr) rem--    fullData <- case c of-        ZSTD -> do-            Consume dFunc <- decompress-            Consume dFunc' <- dFunc (BSO.pack compressed)-            Done res <- dFunc' BSO.empty-            pure res-        SNAPPY -> case Snappy.decompress (BSO.pack compressed) of-            Left e -> error (show e)-            Right res -> pure res-        UNCOMPRESSED -> pure (BSO.pack compressed)-        GZIP -> pure (LB.toStrict (GZip.decompress (LB.pack compressed)))-        other -> error ("Unsupported compression type: " ++ show other)-    pure-        ( Just $ Page hdr (BSO.unpack fullData)-        , drop (fromIntegral $ compressedPageSize hdr) rem-        )--readPageHeader :: PageHeader -> [Word8] -> Int16 -> (PageHeader, [Word8])-readPageHeader hdr [] _ = (hdr, [])-readPageHeader hdr xs lastFieldId =-    let-        fieldContents = readField' xs lastFieldId-     in-        case fieldContents of-            Nothing -> (hdr, drop 1 xs)-            Just (rem, elemType, identifier) -> case identifier of-                1 ->-                    let-                        (pType, rem') = readInt32FromBytes rem-                     in-                        readPageHeader (hdr{pageHeaderPageType = pageTypeFromInt pType}) rem' identifier-                2 ->-                    let-                        (uncompressedPageSize, rem') = readInt32FromBytes rem-                     in-                        readPageHeader-                            (hdr{uncompressedPageSize = uncompressedPageSize})-                            rem'-                            identifier-                3 ->-                    let-                        (compressedPageSize, rem') = readInt32FromBytes rem-                     in-                        readPageHeader (hdr{compressedPageSize = compressedPageSize}) rem' identifier-                4 ->-                    let-                        (crc, rem') = readInt32FromBytes rem-                     in-                        readPageHeader (hdr{pageHeaderCrcChecksum = crc}) rem' identifier-                5 ->-                    let-                        (dataPageHeader, rem') = readPageTypeHeader emptyDataPageHeader rem 0-                     in-                        readPageHeader (hdr{pageTypeHeader = dataPageHeader}) rem' identifier-                6 -> error "Index page header not supported"-                7 ->-                    let-                        (dictionaryPageHeader, rem') = readPageTypeHeader emptyDictionaryPageHeader rem 0-                     in-                        readPageHeader (hdr{pageTypeHeader = dictionaryPageHeader}) rem' identifier-                8 ->-                    let-                        (dataPageHeaderV2, rem') = readPageTypeHeader emptyDataPageHeaderV2 rem 0-                     in-                        readPageHeader (hdr{pageTypeHeader = dataPageHeaderV2}) rem' identifier-                n -> error $ "Unknown page header field " ++ show n--readPageTypeHeader ::-    PageTypeHeader -> [Word8] -> Int16 -> (PageTypeHeader, [Word8])-readPageTypeHeader hdr [] _ = (hdr, [])-readPageTypeHeader 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 =-    let-        fieldContents = readField' xs lastFieldId-     in-        case fieldContents of-            Nothing -> (hdr, drop 1 xs)-            Just (rem, elemType, identifier) -> case identifier of-                1 ->-                    let-                        (numValues, rem') = readInt32FromBytes rem-                     in-                        readPageTypeHeader-                            (hdr{dictionaryPageHeaderNumValues = numValues})-                            rem'-                            identifier-                2 ->-                    let-                        (enc, rem') = readInt32FromBytes rem-                     in-                        readPageTypeHeader-                            (hdr{dictionaryPageHeaderEncoding = parquetEncodingFromInt enc})-                            rem'-                            identifier-                3 ->-                    let-                        isSorted = fromMaybe (error "readPageTypeHeader: not enough bytes") (listToMaybe rem)-                     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 =-    let-        fieldContents = readField' xs lastFieldId-     in-        case fieldContents of-            Nothing -> (hdr, drop 1 xs)-            Just (rem, elemType, identifier) -> case identifier of-                1 ->-                    let-                        (numValues, rem') = readInt32FromBytes rem-                     in-                        readPageTypeHeader (hdr{dataPageHeaderNumValues = numValues}) rem' identifier-                2 ->-                    let-                        (enc, rem') = readInt32FromBytes rem-                     in-                        readPageTypeHeader-                            (hdr{dataPageHeaderEncoding = parquetEncodingFromInt enc})-                            rem'-                            identifier-                3 ->-                    let-                        (enc, rem') = readInt32FromBytes rem-                     in-                        readPageTypeHeader-                            (hdr{definitionLevelEncoding = parquetEncodingFromInt enc})-                            rem'-                            identifier-                4 ->-                    let-                        (enc, rem') = readInt32FromBytes rem-                     in-                        readPageTypeHeader-                            (hdr{repetitionLevelEncoding = parquetEncodingFromInt enc})-                            rem'-                            identifier-                5 ->-                    let-                        (stats, rem') = readStatisticsFromBytes emptyColumnStatistics rem 0-                     in-                        readPageTypeHeader (hdr{dataPageHeaderStatistics = stats}) rem' identifier-                n -> error $ show n-readPageTypeHeader hdr@(DataPageHeaderV2{..}) xs lastFieldId =-    let-        fieldContents = readField' xs lastFieldId-     in-        case fieldContents of-            Nothing -> (hdr, drop 1 xs)-            Just (rem, elemType, identifier) -> case identifier of-                1 ->-                    let-                        (numValues, rem') = readInt32FromBytes rem-                     in-                        readPageTypeHeader (hdr{dataPageHeaderV2NumValues = numValues}) rem' identifier-                2 ->-                    let-                        (numNulls, rem') = readInt32FromBytes rem-                     in-                        readPageTypeHeader (hdr{dataPageHeaderV2NumNulls = numNulls}) rem' identifier-                3 ->-                    let-                        (numRows, rem') = readInt32FromBytes rem-                     in-                        readPageTypeHeader (hdr{dataPageHeaderV2NumRows = numRows}) rem' identifier-                4 ->-                    let-                        (enc, rem') = readInt32FromBytes rem-                     in-                        readPageTypeHeader-                            (hdr{dataPageHeaderV2Encoding = parquetEncodingFromInt enc})-                            rem'-                            identifier-                5 ->-                    let-                        (n, rem') = readInt32FromBytes rem-                     in-                        readPageTypeHeader (hdr{definitionLevelByteLength = n}) rem' identifier-                6 ->-                    let-                        (n, rem') = readInt32FromBytes rem-                     in-                        readPageTypeHeader (hdr{repetitionLevelByteLength = n}) rem' identifier-                7 ->-                    let-                        (isCompressed, rem') = case rem of-                            b : bytes -> ((b .&. 0x0f) == compactBooleanTrue, bytes)-                            [] -> (True, [])-                     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' :: [Word8] -> Int16 -> Maybe ([Word8], TType, Int16)-readField' [] _ = Nothing-readField' (x : xs) lastFieldId-    | x .&. 0x0f == 0 = Nothing-    | otherwise =-        let modifier = fromIntegral ((x .&. 0xf0) `shiftR` 4) :: Int16-            (identifier, rem) =-                if modifier == 0-                    then readIntFromBytes @Int16 xs-                    else (lastFieldId + modifier, xs)-            elemType = toTType (x .&. 0x0f)-         in Just (rem, elemType, identifier)--readAllPages :: CompressionCodec -> [Word8] -> IO [Page]-readAllPages codec bytes = go bytes []-  where-    go [] acc = return (reverse acc)-    go bs acc = do-        (maybePage, remaining) <- readPage codec bs-        case maybePage of-            Nothing -> return (reverse acc)-            Just page -> go remaining (page : acc)--readNInt32 :: Int -> [Word8] -> ([Int32], [Word8])-readNInt32 0 bs = ([], bs)-readNInt32 k bs =-    let x = littleEndianInt32 (take 4 bs)-        bs' = drop 4 bs-        (xs, rest) = readNInt32 (k - 1) bs'-     in (x : xs, rest)--readNDouble :: Int -> [Word8] -> ([Double], [Word8])-readNDouble 0 bs = ([], bs)-readNDouble k bs =-    let x = castWord64ToDouble (littleEndianWord64 (take 8 bs))-        bs' = drop 8 bs-        (xs, rest) = readNDouble (k - 1) bs'-     in (x : xs, rest)--readNByteArrays :: Int -> [Word8] -> ([[Word8]], [Word8])-readNByteArrays 0 bs = ([], bs)-readNByteArrays k bs =-    let len = fromIntegral (littleEndianInt32 (take 4 bs)) :: Int-        body = take len (drop 4 bs)-        bs' = drop (4 + len) bs-        (xs, rest) = readNByteArrays (k - 1) bs'-     in (body : xs, rest)--readNBool :: Int -> [Word8] -> ([Bool], [Word8])-readNBool 0 bs = ([], bs)-readNBool count bs =-    let totalBytes = (count + 7) `div` 8-        chunk = take totalBytes bs-        rest = drop totalBytes bs-        bits = concatMap (\b -> map (\i -> (b `shiftR` i) .&. 1 == 1) [0 .. 7]) chunk-        bools = take count bits-     in (bools, rest)--readNInt64 :: Int -> [Word8] -> ([Int64], [Word8])-readNInt64 0 bs = ([], bs)-readNInt64 k bs =-    let x = fromIntegral (littleEndianWord64 (take 8 bs))-        bs' = drop 8 bs-        (xs, rest) = readNInt64 (k - 1) bs'-     in (x : xs, rest)--readNFloat :: Int -> [Word8] -> ([Float], [Word8])-readNFloat 0 bs = ([], bs)-readNFloat k bs =-    let x = castWord32ToFloat (littleEndianWord32 (take 4 bs))-        bs' = drop 4 bs-        (xs, rest) = readNFloat (k - 1) bs'-     in (x : xs, rest)--splitFixed :: Int -> Int -> [Word8] -> ([[Word8]], [Word8])-splitFixed 0 _ bs = ([], bs)-splitFixed k len bs =-    let body = take len bs-        bs' = drop len bs-        (xs, rest) = splitFixed (k - 1) len bs'-     in (body : xs, rest)--readStatisticsFromBytes ::-    ColumnStatistics -> [Word8] -> Int16 -> (ColumnStatistics, [Word8])-readStatisticsFromBytes cs xs lastFieldId =-    let-        fieldContents = readField' xs lastFieldId-     in-        case fieldContents of-            Nothing -> (cs, drop 1 xs)-            Just (rem, elemType, identifier) -> case identifier of-                1 ->-                    let-                        (maxInBytes, rem') = readByteStringFromBytes rem-                     in-                        readStatisticsFromBytes (cs{columnMax = maxInBytes}) rem' identifier-                2 ->-                    let-                        (minInBytes, rem') = readByteStringFromBytes rem-                     in-                        readStatisticsFromBytes (cs{columnMin = minInBytes}) rem' identifier-                3 ->-                    let-                        (nullCount, rem') = readIntFromBytes @Int64 rem-                     in-                        readStatisticsFromBytes (cs{columnNullCount = nullCount}) rem' identifier-                4 ->-                    let-                        (distinctCount, rem') = readIntFromBytes @Int64 rem-                     in-                        readStatisticsFromBytes (cs{columnDistictCount = distinctCount}) rem' identifier-                5 ->-                    let-                        (maxInBytes, rem') = readByteStringFromBytes rem-                     in-                        readStatisticsFromBytes (cs{columnMaxValue = maxInBytes}) rem' identifier-                6 ->-                    let-                        (minInBytes, rem') = readByteStringFromBytes rem-                     in-                        readStatisticsFromBytes (cs{columnMinValue = minInBytes}) rem' identifier-                7 ->-                    case rem of-                        [] ->-                            error "readStatisticsFromBytes: not enough bytes"-                        (isMaxValueExact : rem') ->-                            readStatisticsFromBytes-                                (cs{isColumnMaxValueExact = isMaxValueExact == compactBooleanTrue})-                                rem'-                                identifier-                8 ->-                    case rem of-                        [] ->-                            error "readStatisticsFromBytes: not enough bytes"-                        (isMinValueExact : rem') ->-                            readStatisticsFromBytes-                                (cs{isColumnMinValueExact = isMinValueExact == compactBooleanTrue})-                                rem'-                                identifier-                n -> error $ show n
− src/DataFrame/IO/Parquet/Thrift.hs
@@ -1,1165 +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 :: [Word8]-    }-    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 = []-        }--data ColumnMetaData = ColumnMetaData-    { columnType :: ParquetType-    , columnEncodings :: [ParquetEncoding]-    , columnPathInSchema :: [String]-    , columnCodec :: CompressionCodec-    , columnNumValues :: Int64-    , columnTotalUncompressedSize :: Int64-    , columnTotalCompressedSize :: Int64-    , columnKeyValueMetadata :: [KeyValue]-    , columnDataPageOffset :: Int64-    , columnIndexPageOffset :: Int64-    , columnDictionaryPageOffset :: Int64-    , columnStatistics :: ColumnStatistics-    , columnEncodingStats :: [PageEncodingStats]-    , bloomFilterOffset :: Int64-    , bloomFilterLength :: Int32-    , columnSizeStatistics :: SizeStatistics-    , columnGeospatialStatistics :: GeospatialStatistics-    }-    deriving (Show, Eq)--data ColumnChunk = ColumnChunk-    { columnChunkFilePath :: String-    , columnChunkMetadataFileOffset :: Int64-    , columnMetaData :: ColumnMetaData-    , columnChunkOffsetIndexOffset :: Int64-    , columnChunkOffsetIndexLength :: Int32-    , columnChunkColumnIndexOffset :: Int64-    , columnChunkColumnIndexLength :: Int32-    , cryptoMetadata :: ColumnCryptoMetadata-    , encryptedColumnMetadata :: [Word8]-    }-    deriving (Show, Eq)--data RowGroup = RowGroup-    { rowGroupColumns :: [ColumnChunk]-    , totalByteSize :: Int64-    , rowGroupNumRows :: Int64-    , rowGroupSortingColumns :: [SortingColumn]-    , fileOffset :: Int64-    , totalCompressedSize :: Int64-    , ordinal :: Int16-    }-    deriving (Show, Eq)--defaultSchemaElement :: SchemaElement-defaultSchemaElement =-    SchemaElement-        ""-        STOP-        0-        0-        (-1)-        UNKNOWN_REPETITION_TYPE-        0-        0-        0-        LOGICAL_TYPE_UNKNOWN--emptyColumnMetadata :: ColumnMetaData-emptyColumnMetadata =-    ColumnMetaData-        PARQUET_TYPE_UNKNOWN-        []-        []-        COMPRESSION_CODEC_UNKNOWN-        0-        0-        0-        []-        0-        0-        0-        emptyColumnStatistics-        []-        0-        0-        emptySizeStatistics-        emptyGeospatialStatistics--emptyColumnChunk :: ColumnChunk-emptyColumnChunk =-    ColumnChunk "" 0 emptyColumnMetadata 0 0 0 0 COLUMN_CRYPTO_METADATA_UNKNOWN []--emptyKeyValue :: KeyValue-emptyKeyValue = KeyValue{key = "", value = ""}--emptyRowGroup :: RowGroup-emptyRowGroup = RowGroup [] 0 0 [] 0 0 0--compactBooleanTrue-    , compactI32-    , compactI64-    , compactDouble-    , compactBinary-    , compactList-    , compactStruct ::-        Word8-compactBooleanTrue = 0x01-compactI32 = 0x05-compactI64 = 0x06-compactDouble = 0x07-compactBinary = 0x08-compactList = 0x09-compactStruct = 0x0C--toTType :: Word8 -> TType-toTType t =-    fromMaybe STOP $-        M.lookup (t .&. 0x0f) $-            M.fromList-                [ (compactBooleanTrue, BOOL)-                , (compactI32, I32)-                , (compactI64, I64)-                , (compactDouble, DOUBLE)-                , (compactBinary, STRING)-                , (compactList, LIST)-                , (compactStruct, STRUCT)-                ]--readField ::-    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 = [], aadFileUnique = [], supplyAadPrefix = False})-                    buf-                    pos-                    0-            2 -> do-                readAesGcmCtrV1-                    (AesGcmCtrV1{aadPrefix = [], aadFileUnique = [], 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 lastFieldId-            2 -> do-                aadFileUnique <- readByteString buf pos-                readAesGcmCtrV1-                    (v{aadFileUnique = aadFileUnique})-                    buf-                    pos-                    lastFieldId-            3 -> do-                supplyAadPrefix <- readAndAdvance pos buf-                readAesGcmCtrV1-                    (v{supplyAadPrefix = supplyAadPrefix == compactBooleanTrue})-                    buf-                    pos-                    lastFieldId-            _ -> 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 lastFieldId-            2 -> do-                aadFileUnique <- readByteString buf pos-                readAesGcmV1 (v{aadFileUnique = aadFileUnique}) buf pos lastFieldId-            3 -> do-                supplyAadPrefix <- readAndAdvance pos buf-                readAesGcmV1-                    (v{supplyAadPrefix = supplyAadPrefix == compactBooleanTrue})-                    buf-                    pos-                    lastFieldId-            _ -> 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 lastFieldId-            2 -> do-                precision' <- readInt32FromBuffer buf pos-                readDecimalType precision' scale buf pos lastFieldId-            _ -> 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 unit)-        Just (elemType, identifier) -> case identifier of-            1 -> do-                -- TODO: Check for empty-                isAdjustedToUTC' <- (== compactBooleanTrue) <$> readAndAdvance pos buf-                readTimeType isAdjustedToUTC' unit buf pos lastFieldId-            2 -> do-                unit' <- readUnit TIME_UNIT_UNKNOWN buf pos 0-                readTimeType isAdjustedToUTC unit' buf pos lastFieldId-            _ -> 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 unit)-        Just (elemType, identifier) -> case identifier of-            1 -> do-                -- TODO: Check for empty-                isAdjustedToUTC' <- (== compactBooleanTrue) <$> readNoAdvance pos buf-                readTimestampType False unit buf pos lastFieldId-            2 -> do-                _ <- readField buf pos identifier-                unit' <- readUnit TIME_UNIT_UNKNOWN buf pos 0-                readTimestampType isAdjustedToUTC unit' buf pos lastFieldId-            _ -> 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-                readUnit MILLISECONDS buf pos identifier-            2 -> do-                readUnit MICROSECONDS buf pos identifier-            3 -> do-                readUnit NANOSECONDS buf pos identifier-            n -> error $ "Unknown time unit: " ++ show n
− src/DataFrame/IO/Parquet/Time.hs
@@ -1,61 +0,0 @@-{-# LANGUAGE NumericUnderscores #-}--module DataFrame.IO.Parquet.Time where--import Data.Time-import Data.Word--import DataFrame.IO.Parquet.Binary--int96ToUTCTime :: [Word8] -> UTCTime-int96ToUTCTime bytes-    | length bytes /= 12 = error "INT96 must be exactly 12 bytes"-    | otherwise =-        let (nanosBytes, julianBytes) = splitAt 8 bytes-            nanosSinceMidnight = littleEndianWord64 nanosBytes-            julianDay = littleEndianWord32 julianBytes-         in julianDayAndNanosToUTCTime (fromIntegral julianDay) nanosSinceMidnight--julianDayAndNanosToUTCTime :: Integer -> Word64 -> UTCTime-julianDayAndNanosToUTCTime julianDay nanosSinceMidnight =-    let day = julianDayToDay julianDay-        secondsSinceMidnight = fromIntegral nanosSinceMidnight / 1_000_000_000-        diffTime = secondsToDiffTime (floor secondsSinceMidnight)-     in UTCTime day diffTime--julianDayToDay :: Integer -> Day-julianDayToDay julianDay =-    let a = julianDay + 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 -> [Word8]-utcTimeToInt96 (UTCTime day diffTime) =-    let julianDay = dayToJulianDay day-        nanosSinceMidnight = floor (realToFrac diffTime * 1_000_000_000)-        nanosBytes = word64ToLittleEndian nanosSinceMidnight-        julianBytes = word32ToLittleEndian (fromIntegral julianDay)-     in nanosBytes ++ julianBytes--dayToJulianDay :: Day -> Integer-dayToJulianDay day =-    let (year, month, dayOfMonth) = toGregorian day-        a = fromIntegral $ (14 - fromIntegral month) `div` 12-        y = fromIntegral $ year + 4800 - a-        m = fromIntegral $ month + 12 * fromIntegral a - 3-     in fromIntegral dayOfMonth-            + (153 * m + 2) `div` 5-            + 365 * y-            + y `div` 4-            - y `div` 100-            + y `div` 400-            - 32_045
− src/DataFrame/IO/Parquet/Types.hs
@@ -1,308 +0,0 @@-module DataFrame.IO.Parquet.Types where--import Data.Int-import qualified Data.Text as T-import Data.Time-import Data.Word--data ParquetType-    = PBOOLEAN-    | PINT32-    | PINT64-    | PINT96-    | PFLOAT-    | PDOUBLE-    | PBYTE_ARRAY-    | PFIXED_LEN_BYTE_ARRAY-    | PARQUET_TYPE_UNKNOWN-    deriving (Show, Eq)--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 :: [Word8]-    , columnMax :: [Word8]-    , columnNullCount :: Int64-    , columnDistictCount :: Int64-    , columnMinValue :: [Word8]-    , columnMaxValue :: [Word8]-    , isColumnMaxValueExact :: Bool-    , isColumnMinValueExact :: Bool-    }-    deriving (Show, Eq)--emptyColumnStatistics :: ColumnStatistics-emptyColumnStatistics = ColumnStatistics [] [] 0 0 [] [] False False--data ColumnCryptoMetadata-    = COLUMN_CRYPTO_METADATA_UNKNOWN-    | ENCRYPTION_WITH_FOOTER_KEY-    | EncryptionWithColumnKey-        { columnCryptPathInSchema :: [String]-        , columnKeyMetadata :: [Word8]-        }-    deriving (Show, Eq)--data SortingColumn = SortingColumn-    { columnIndex :: Int32-    , columnOrderDescending :: Bool-    , nullFirst :: Bool-    }-    deriving (Show, Eq)--emptySortingColumn :: SortingColumn-emptySortingColumn = SortingColumn 0 False False--data ColumnOrder-    = TYPE_ORDER-    | COLUMN_ORDER_UNKNOWN-    deriving (Show, Eq)--data EncryptionAlgorithm-    = ENCRYPTION_ALGORITHM_UNKNOWN-    | AesGcmV1-        { aadPrefix :: [Word8]-        , aadFileUnique :: [Word8]-        , supplyAadPrefix :: Bool-        }-    | AesGcmCtrV1-        { aadPrefix :: [Word8]-        , aadFileUnique :: [Word8]-        , supplyAadPrefix :: Bool-        }-    deriving (Show, Eq)--data DictVals-    = DBool [Bool]-    | DInt32 [Int32]-    | DInt64 [Int64]-    | DInt96 [UTCTime]-    | DFloat [Float]-    | DDouble [Double]-    | DText [T.Text]-    deriving (Show, Eq)--data Page = Page-    { pageHeader :: PageHeader-    , pageBytes :: [Word8]-    }-    deriving (Show, Eq)--data PageHeader = PageHeader-    { pageHeaderPageType :: PageType-    , uncompressedPageSize :: Int32-    , compressedPageSize :: Int32-    , pageHeaderCrcChecksum :: Int32-    , pageTypeHeader :: PageTypeHeader-    }-    deriving (Show, Eq)--emptyPageHeader = PageHeader PAGE_TYPE_UNKNOWN 0 0 0 PAGE_TYPE_HEADER_UNKNOWN--data PageTypeHeader-    = DataPageHeader-        { dataPageHeaderNumValues :: Int32-        , dataPageHeaderEncoding :: ParquetEncoding-        , definitionLevelEncoding :: ParquetEncoding-        , repetitionLevelEncoding :: ParquetEncoding-        , dataPageHeaderStatistics :: ColumnStatistics-        }-    | DataPageHeaderV2-        { dataPageHeaderV2NumValues :: Int32-        , dataPageHeaderV2NumNulls :: Int32-        , dataPageHeaderV2NumRows :: Int32-        , dataPageHeaderV2Encoding :: ParquetEncoding-        , definitionLevelByteLength :: Int32-        , repetitionLevelByteLength :: Int32-        , dataPageHeaderV2IsCompressed :: Bool-        , dataPageHeaderV2Statistics :: ColumnStatistics-        }-    | DictionaryPageHeader-        { dictionaryPageHeaderNumValues :: Int32-        , dictionaryPageHeaderEncoding :: ParquetEncoding-        , dictionaryPageIsSorted :: Bool-        }-    | INDEX_PAGE_HEADER-    | PAGE_TYPE_HEADER_UNKNOWN-    deriving (Show, Eq)--emptyDictionaryPageHeader = DictionaryPageHeader 0 PARQUET_ENCODING_UNKNOWN False-emptyDataPageHeader =-    DataPageHeader-        0-        PARQUET_ENCODING_UNKNOWN-        PARQUET_ENCODING_UNKNOWN-        PARQUET_ENCODING_UNKNOWN-        emptyColumnStatistics-emptyDataPageHeaderV2 =-    DataPageHeaderV2-        0-        0-        0-        PARQUET_ENCODING_UNKNOWN-        0-        0 {- default for v2 is compressed -}-        True-        emptyColumnStatistics--data RepetitionType = REQUIRED | OPTIONAL | REPEATED | UNKNOWN_REPETITION_TYPE-    deriving (Eq, Show)--data LogicalType-    = STRING_TYPE-    | MAP_TYPE-    | LIST_TYPE-    | ENUM_TYPE-    | DECIMAL_TYPE-    | DATE_TYPE-    | DecimalType {decimalTypePrecision :: Int32, decimalTypeScale :: Int32}-    | TimeType {isAdjustedToUTC :: Bool, unit :: TimeUnit}-    | -- This should probably have a different, more constrained TimeUnit type.-      TimestampType {isAdjustedToUTC :: Bool, unit :: TimeUnit}-    | IntType {bitWidth :: Int8, intIsSigned :: Bool}-    | LOGICAL_TYPE_UNKNOWN-    | JSON_TYPE-    | BSON_TYPE-    | UUID_TYPE-    | FLOAT16_TYPE-    | VariantType {specificationVersion :: Int8}-    | GeometryType {crs :: T.Text}-    | GeographyType {crs :: T.Text, algorithm :: EdgeInterpolationAlgorithm}-    deriving (Eq, Show)--data TimeUnit-    = MILLISECONDS-    | MICROSECONDS-    | NANOSECONDS-    | TIME_UNIT_UNKNOWN-    deriving (Eq, Show)--data EdgeInterpolationAlgorithm-    = SPHERICAL-    | VINCENTY-    | THOMAS-    | ANDOYER-    | KARNEY-    deriving (Eq, Show)--repetitionTypeFromInt :: Int32 -> RepetitionType-repetitionTypeFromInt 0 = REQUIRED-repetitionTypeFromInt 1 = OPTIONAL-repetitionTypeFromInt 2 = REPEATED-repetitionTypeFromInt _ = UNKNOWN_REPETITION_TYPE--compressionCodecFromInt :: Int32 -> CompressionCodec-compressionCodecFromInt 0 = UNCOMPRESSED-compressionCodecFromInt 1 = SNAPPY-compressionCodecFromInt 2 = GZIP-compressionCodecFromInt 3 = LZO-compressionCodecFromInt 4 = BROTLI-compressionCodecFromInt 5 = LZ4-compressionCodecFromInt 6 = ZSTD-compressionCodecFromInt 7 = LZ4_RAW-compressionCodecFromInt _ = COMPRESSION_CODEC_UNKNOWN
− src/DataFrame/IO/Unstable/CSV.hs
@@ -1,333 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE CApiFFI #-}-{-# LANGUAGE ForeignFunctionInterface #-}--module DataFrame.IO.Unstable.CSV (-    fastReadCsvUnstable,-    readCsvUnstable,-    fastReadTsvUnstable,-    readTsvUnstable,-) 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-        -- then number of delimiters cannot exceed the size-        -- of the input array (which would be a series of-        -- empty fields)-        indices <- mallocArray paddedLen-        num_fields <--            get_delimiter_indices-                (castPtr buffer)-                (fromIntegral paddedLen)-                (fromIntegral separator)-                (castPtr indices)-        if num_fields == -1-            then getDelimiterIndices_ separator originalLen csvFile indices-            else do-                indices' <- newForeignPtr_ indices-                let resultVector = VSM.unsafeFromForeignPtr0 indices' paddedLen-                -- 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 (fromIntegral num_fields) (fromIntegral originalLen)-                            return (fromIntegral num_fields + 1)-                        else return (fromIntegral num_fields)-                VS.unsafeFreeze $ VSM.slice 0 finalResultLen resultVector---- 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,1181 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE InstanceSigs #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE 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.Exception (throw)-import Control.Monad.ST (runST)-import Data.Kind (Type)-import Data.Maybe-import Data.Type.Equality (TestEquality (..))-import DataFrame.Errors-import DataFrame.Internal.Parsing-import DataFrame.Internal.Types-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--{- | 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-    (==) (TColumn a) (TColumn b) = a == b--instance (Ord a) => Ord (TypedColumn a) where-    compare (TColumn a) (TColumn b) = compare a b---- | Gets the underlying value from a TypedColumn.-unwrapTypedColumn :: TypedColumn a -> Column-unwrapTypedColumn (TColumn value) = 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 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-{-# SPECIALIZE mapColumn ::-    (Double -> Double) -> Column -> Either DataFrameException Column-    #-}-{-# INLINEABLE mapColumn #-}---- | Applies a function that returns an unboxed result to an unboxed vector, storing the result in a column.-imapColumn ::-    forall b c.-    (Columnable b, Columnable c) =>-    (Int -> b -> c) -> Column -> Either DataFrameException Column-imapColumn f = \case-    BoxedColumn (col :: VB.Vector a) -> run col-    OptionalColumn (col :: VB.Vector a) -> run col-    UnboxedColumn (col :: VU.Vector a) -> runUnboxed col-  where-    run :: forall a. (Typeable a) => VB.Vector a -> Either DataFrameException Column-    run col = case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> Right (fromVector @c (VB.imap f col))-        Nothing -> throwTypeMismatch @a @b--    runUnboxed ::-        forall a.-        (Typeable a, VU.Unbox a) => VU.Vector a -> Either DataFrameException Column-    runUnboxed col = case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> Right $ case sUnbox @c of-            STrue -> UnboxedColumn (VU.imap f col)-            SFalse -> BoxedColumn (VB.imap f (VG.convert col))-        Nothing -> throwTypeMismatch @a @b---- | O(1) Gets the number of elements in the column.-columnLength :: Column -> Int-columnLength (BoxedColumn xs) = VG.length xs-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 $ VG.unsafeBackpermute column (VG.convert indexes)-atIndicesStable indexes (UnboxedColumn column) = UnboxedColumn $ VU.unsafeBackpermute column indexes-atIndicesStable indexes (OptionalColumn column) = OptionalColumn $ VG.unsafeBackpermute column (VG.convert indexes)-{-# INLINE atIndicesStable #-}--atIndicesWithNulls :: VB.Vector (Maybe Int) -> Column -> Column-atIndicesWithNulls indices column-    | VB.all isJust indices =-        atIndicesStable-            (VU.fromList (catMaybes (VB.toList indices)))-            column-    | otherwise = 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-                    }-                )--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-                    }-                )--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 #-}---- | 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 (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 (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"].--}-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--{- | 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,166 +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.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-    }--{- | A record that contains information about how and what-rows are grouped in the dataframe. This can only be used with-`aggregate`.--}-data GroupedDataFrame = Grouped-    { fullDataframe :: DataFrame-    , groupedColumns :: [T.Text]-    , valueIndices :: VU.Vector Int-    , offsets :: VU.Vector Int-    }--instance Show GroupedDataFrame where-    show (Grouped df cols indices os) =-        printf-            "{ keyColumns: %s groupedColumns: %s }"-            (show cols)-            (show (M.keys (columnIndices df) \\ cols))--instance Eq GroupedDataFrame where-    (==) (Grouped df cols indices os) (Grouped df' cols' indices' os') = (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,352 +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 Data.String-import qualified Data.Text as T-import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))-import qualified Data.Vector.Generic as VG-import qualified Data.Vector.Unboxed as VU-import DataFrame.Internal.Column-import Type.Reflection (Typeable, typeOf, typeRep)--data Expr a where-    Col :: (Columnable a) => T.Text -> Expr a-    Lit :: (Columnable a) => a -> Expr a-    UnaryOp ::-        (Columnable a, Columnable b) => T.Text -> (b -> a) -> Expr b -> Expr a-    BinaryOp ::-        (Columnable c, Columnable b, Columnable a) =>-        T.Text -> (c -> b -> a) -> Expr c -> Expr b -> Expr a-    If :: (Columnable a) => Expr Bool -> Expr a -> Expr a -> Expr a-    AggVector ::-        (VG.Vector v b, Typeable v, Columnable a, Columnable b) =>-        Expr b -> T.Text -> (v b -> a) -> Expr a-    AggReduce :: (Columnable a) => Expr a -> T.Text -> (a -> a -> a) -> Expr a-    -- TODO(mchav): Numeric reduce might be superfluous since expressions are already type checked.-    AggNumericVector ::-        (Columnable a, Columnable b, VU.Unbox a, VU.Unbox b, Num a, Num b) =>-        Expr b -> T.Text -> (VU.Vector b -> a) -> Expr a-    AggFold ::-        forall a b.-        (Columnable a, Columnable b) => Expr b -> T.Text -> a -> (a -> b -> a) -> Expr a--data UExpr where-    Wrap :: (Columnable a) => Expr a -> UExpr--instance Show UExpr where-    show :: UExpr -> String-    show (Wrap expr) = show expr--type NamedExpr = (T.Text, UExpr)--instance (Num a, Columnable a) => Num (Expr a) where-    (+) :: Expr a -> Expr a -> Expr a-    (+) (Lit x) (Lit y) = Lit (x + y)-    (+) e1 e2-        | e1 == e2 = BinaryOp "mult" (*) e1 (Lit 2)-        | otherwise = BinaryOp "add" (+) e1 e2--    (-) :: Expr a -> Expr a -> Expr a-    (-) (Lit x) (Lit y) = Lit (x - y)-    (-) e1 e2 = BinaryOp "sub" (-) e1 e2--    (*) :: Expr a -> Expr a -> Expr a-    (*) (Lit 0) _ = Lit 0-    (*) _ (Lit 0) = Lit 0-    (*) (Lit 1) e = e-    (*) e (Lit 1) = e-    (*) (Lit x) (Lit y) = Lit (x * y)-    (*) e1 e2-        | e1 == e2 = BinaryOp "pow" (^) e1 (Lit @Int 2)-        | otherwise = BinaryOp "mult" (*) e1 e2--    fromInteger :: Integer -> Expr a-    fromInteger = Lit . fromInteger--    negate :: Expr a -> Expr a-    negate (Lit n) = Lit (negate n)-    negate expr = UnaryOp "negate" negate expr--    abs :: (Num a) => Expr a -> Expr a-    abs (Lit n) = Lit (abs n)-    abs expr = UnaryOp "abs" abs expr--    signum :: (Num a) => Expr a -> Expr a-    signum (Lit n) = Lit (signum n)-    signum expr = UnaryOp "signum" signum expr--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-    (/) (Lit l1) (Lit l2) = Lit (l1 / l2)-    (/) e1 e2 = BinaryOp "divide" (/) e1 e2--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 = UnaryOp "exp" exp-    log :: (Floating a, Columnable a) => Expr a -> Expr a-    log = UnaryOp "log" log-    sin :: (Floating a, Columnable a) => Expr a -> Expr a-    sin = UnaryOp "sin" sin-    cos :: (Floating a, Columnable a) => Expr a -> Expr a-    cos = UnaryOp "cos" cos-    asin :: (Floating a, Columnable a) => Expr a -> Expr a-    asin = UnaryOp "asin" asin-    acos :: (Floating a, Columnable a) => Expr a -> Expr a-    acos = UnaryOp "acos" acos-    atan :: (Floating a, Columnable a) => Expr a -> Expr a-    atan = UnaryOp "atan" atan-    sinh :: (Floating a, Columnable a) => Expr a -> Expr a-    sinh = UnaryOp "sinh" sinh-    cosh :: (Floating a, Columnable a) => Expr a -> Expr a-    cosh = UnaryOp "cosh" cosh-    asinh :: (Floating a, Columnable a) => Expr a -> Expr a-    asinh = UnaryOp "asinh" sinh-    acosh :: (Floating a, Columnable a) => Expr a -> Expr a-    acosh = UnaryOp "acosh" acosh-    atanh :: (Floating a, Columnable a) => Expr a -> Expr a-    atanh = UnaryOp "atanh" atanh--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 (UnaryOp name f value) = "(" ++ T.unpack name ++ " " ++ show value ++ ")"-    show (BinaryOp name f a b) = "(" ++ T.unpack name ++ " " ++ show a ++ " " ++ show b ++ ")"-    show (AggNumericVector expr op _) = "(" ++ T.unpack op ++ " " ++ show expr ++ ")"-    show (AggVector expr op _) = "(" ++ T.unpack op ++ " " ++ show expr ++ ")"-    show (AggReduce expr op _) = "(" ++ T.unpack op ++ " " ++ show expr ++ ")"-    show (AggFold expr op _ _) = "(" ++ 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)-    UnaryOp name f e -> UnaryOp name f (normalize e)-    BinaryOp name f e1 e2-        | isCommutative name ->-            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 BinaryOp name f n2 n1 -- Swap to canonical order-                            else BinaryOp name f n1 n2-        | otherwise -> BinaryOp name f (normalize e1) (normalize e2)-    AggVector e name f -> AggVector (normalize e) name f-    AggReduce e name f -> AggReduce (normalize e) name f-    AggNumericVector e name f -> AggNumericVector (normalize e) name f-    AggFold e name init f -> AggFold (normalize e) name init f--isCommutative :: T.Text -> Bool-isCommutative name =-    name-        `elem` [ "add"-               , "mult"-               , "min"-               , "max"-               , "eq"-               , "and"-               , "or"-               ]---- 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 (UnaryOp name _ e) = "3:" ++ T.unpack name ++ exprKey e-    exprKey (BinaryOp name _ e1 e2) = "4:" ++ T.unpack name ++ exprKey e1 ++ exprKey e2-    exprKey (AggVector e name _) = "5:" ++ T.unpack name ++ exprKey e-    exprKey (AggReduce e name _) = "6:" ++ T.unpack name ++ exprKey e-    exprKey (AggNumericVector e name _) = "7:" ++ T.unpack name ++ exprKey e-    exprKey (AggFold e name _ _) = "8:" ++ 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 (UnaryOp n1 _ e1) (UnaryOp n2 _ e2) =-            n1 == n2 && e1 `exprEq` e2-        eqNormalized (BinaryOp n1 _ e1a e1b) (BinaryOp n2 _ e2a e2b) =-            n1 == n2 && e1a `exprEq` e2a && e1b `exprEq` e2b-        eqNormalized (AggVector e1 n1 _) (AggVector e2 n2 _) =-            n1 == n2 && e1 `exprEq` e2-        eqNormalized (AggReduce e1 n1 _) (AggReduce e2 n2 _) =-            n1 == n2 && e1 `exprEq` e2-        eqNormalized (AggNumericVector e1 n1 _) (AggNumericVector e2 n2 _) =-            n1 == n2 && e1 `exprEq` e2-        eqNormalized (AggFold e1 n1 i1 _) (AggFold e2 n2 i2 _) =-            n1 == n2 && e1 `exprEq` e2 && i1 == i2-        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'-        (UnaryOp n1 _ e1', UnaryOp n2 _ e2') ->-            compare n1 n2 <> exprComp e1' e2'-        (BinaryOp n1 _ a1 b1, BinaryOp n2 _ a2 b2) ->-            compare n1 n2 <> exprComp a1 a2 <> exprComp b1 b2-        (AggVector e1' n1 _, AggVector e2' n2 _) ->-            compare n1 n2 <> exprComp e1' e2'-        (AggReduce e1' n1 _, AggReduce e2' n2 _) ->-            compare n1 n2 <> exprComp e1' e2'-        (AggNumericVector e1' n1 _, AggNumericVector e2' n2 _) ->-            compare n1 n2 <> exprComp e1' e2'-        (AggFold e1' n1 i1 _, AggFold e2' n2 i2 _) ->-            compare n1 n2 <> exprComp e1' e2' <> compare i1 i2-        -- Different constructors - compare by priority-        (Col _, _) -> LT-        (_, Col _) -> GT-        (Lit _, _) -> LT-        (_, Lit _) -> GT-        (UnaryOp{}, _) -> LT-        (_, UnaryOp{}) -> GT-        (BinaryOp{}, _) -> LT-        (_, BinaryOp{}) -> GT-        (If{}, _) -> LT-        (_, If{}) -> GT-        (AggVector{}, _) -> LT-        (_, AggVector{}) -> GT-        (AggReduce{}, _) -> LT-        (_, AggReduce{}) -> GT-        (AggNumericVector{}, _) -> LT-        (_, AggNumericVector{}) -> GT--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)-        (UnaryOp name f value) -> UnaryOp name f (replaceExpr new old value)-        (BinaryOp name f l r) -> BinaryOp name f (replaceExpr new old l) (replaceExpr new old r)-        (AggNumericVector expr op f) -> AggNumericVector (replaceExpr new old expr) op f-        (AggVector expr op f) -> AggVector (replaceExpr new old expr) op f-        (AggReduce expr op f) -> AggReduce (replaceExpr new old expr) op f-        (AggFold expr op acc f) -> AggFold (replaceExpr new old expr) op acc f--eSize :: Expr a -> Int-eSize (Col _) = 1-eSize (Lit _) = 1-eSize (If c l r) = 1 + eSize c + eSize l + eSize r-eSize (UnaryOp _ _ e) = 1 + eSize e-eSize (BinaryOp _ _ l r) = 1 + eSize l + eSize r-eSize (AggNumericVector expr op _) = eSize expr + 1-eSize (AggVector expr op _) = eSize expr + 1-eSize (AggReduce expr op _) = eSize expr + 1-eSize (AggFold expr op _ _) = 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 (UnaryOp name f value) = getColumns value-getColumns (BinaryOp name f l r) = getColumns l <> getColumns r-getColumns (AggNumericVector expr op f) = getColumns expr-getColumns (AggVector expr op f) = getColumns expr-getColumns (AggReduce expr op f) = getColumns expr-getColumns (AggFold expr op acc f) = getColumns expr--prettyPrint :: Expr a -> String-prettyPrint = go 0-  where-    go :: Int -> Expr a -> String-    go prec expr = case expr of-        Col name -> T.unpack name-        Lit value -> show value-        If cond t e ->-            "if (" ++ go 0 cond ++ ") then (" ++ go 0 t ++ ") else (" ++ go 0 e ++ ")"-        UnaryOp name _ arg -> T.unpack name ++ "(" ++ go 0 arg ++ ")"-        BinaryOp name _ l r ->-            let p = opPrec name-                inner = go p l ++ " " ++ opSym name ++ " " ++ go p r-             in if prec > p then "(" ++ inner ++ ")" else inner-        AggVector arg op _ -> T.unpack op ++ "(" ++ go 0 arg ++ ")"-        AggReduce arg op _ -> T.unpack op ++ "(" ++ go 0 arg ++ ")"-        AggNumericVector arg op _ -> T.unpack op ++ "(" ++ go 0 arg ++ ")"-        AggFold arg op _ _ -> T.unpack op ++ "(" ++ go 0 arg ++ ")"--    opPrec :: T.Text -> Int-    opPrec "add" = 1-    opPrec "sub" = 1-    opPrec "mult" = 2-    opPrec "divide" = 2-    opPrec "pow" = 3-    opPrec _ = 0--    opSym :: T.Text -> String-    opSym "add" = "+"-    opSym "sub" = "-"-    opSym "mult" = "*"-    opSym "divide" = "/"-    opSym "pow" = "^"-    opSym "lt" = "<"-    opSym "leq" = "<="-    opSym "gt" = ">"-    opSym "geq" = ">="-    opSym other = T.unpack other
− src/DataFrame/Internal/Interpreter.hs
@@ -1,953 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PatternSynonyms #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE UndecidableInstances #-}--module DataFrame.Internal.Interpreter where--import Control.Monad.ST (runST)-import Data.Bifunctor-import qualified Data.Map as M-import Data.Maybe (fromMaybe, isJust)-import qualified Data.Text as T-import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))-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 DataFrame.Errors-import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame-import DataFrame.Internal.Expression-import DataFrame.Internal.Types-import Type.Reflection (TypeRep, Typeable, typeOf, typeRep, pattern App)--interpret ::-    forall a.-    (Columnable a) =>-    DataFrame -> Expr a -> Either DataFrameException (TypedColumn a)-interpret df (Lit value) = case sUnbox @a of-    -- Specialize the creation of unboxed columns to avoid an extra allocation.-    STrue -> pure $ TColumn $ fromUnboxedVector $ VU.replicate (numRows df) value-    SFalse -> pure $ TColumn $ fromVector $ V.replicate (numRows df) value-interpret df (Col name) = maybe columnNotFound (pure . TColumn) (getColumn name df)-  where-    columnNotFound = Left $ ColumnNotFoundException name "" (M.keys $ columnIndices df)--- Unary operations.-interpret df expr@(UnaryOp _ (f :: c -> d) value) = first (handleInterpretException (show expr)) $ do-    (TColumn value') <- interpret @c df value-    fmap TColumn (mapColumn f value')--- Variations of binary operations.-interpret df expr@(BinaryOp _ (f :: c -> d -> e) left right) = first (handleInterpretException (show expr)) $ case (left, right) of-    (Lit left, Lit right) -> interpret df (Lit (f left right))-    (Lit left, right) -> do-        -- If we have a literal then we don't have to materialise-        -- the column.-        (TColumn value') <- interpret @d df right-        fmap TColumn (mapColumn (f left) value')-    (left, Lit right) -> do-        -- Same as the above except the right side is the-        -- literl.-        (TColumn value') <- interpret @c df left-        fmap TColumn (mapColumn (`f` right) value')-    (_, _) -> do-        -- In the general case we interpret and zip.-        (TColumn left') <- interpret @c df left-        (TColumn right') <- interpret @d df right-        fmap TColumn (zipWithColumns f left' right')--- Conditionals-interpret df expr@(If cond l r) = first (handleInterpretException (show expr)) $ do-    (TColumn conditions) <- interpret @Bool df cond-    (TColumn left) <- interpret @a df l-    (TColumn right) <- interpret @a df r-    let branch (c :: Bool) (l' :: a, r' :: a) = if c then l' else r'-    fmap TColumn (zipWithColumns branch conditions (zipColumns left right))-interpret df expression@(AggVector expr op (f :: v b -> c)) = do-    (TColumn column) <- interpret @b df expr-    -- Helper for errors. Should probably find a way of throwing this-    -- without leaking the fact that we use `Vector` to users.-    let aggTypeError expected =-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @(v b))-                    , expectedType = Left expected :: Either String (TypeRep ())-                    , callingFunctionName = Just "interpret"-                    , errorColumnName = Nothing-                    }-                )-    let processColumn ::-            (Columnable d) => d -> Either DataFrameException (TypedColumn a)-        processColumn col = case testEquality (typeRep @(v b)) (typeOf col) of-            Just Refl -> interpret @c df (Lit (f col))-            Nothing -> Left $ aggTypeError (show (typeOf col))-    case column of-        (BoxedColumn col) -> processColumn col-        (OptionalColumn col) -> processColumn col-        (UnboxedColumn col) -> processColumn col-interpret df expression@(AggReduce expr op (f :: a -> a -> a)) = first (handleInterpretException (show expr)) $ do-    (TColumn column) <- interpret @a df expr-    value <- foldl1Column f column-    pure $ TColumn $ fromVector $ V.replicate (fst $ dataframeDimensions df) value-interpret df expression@(AggNumericVector expr op (f :: VU.Vector b -> c)) = first (handleInterpretException (show expression)) $ do-    (TColumn column) <- interpret @b df expr-    case column of-        (UnboxedColumn (v :: VU.Vector d)) -> case testEquality (typeRep @d) (typeRep @b) of-            Just Refl ->-                pure $ TColumn $ fromVector $ V.replicate (fst $ dataframeDimensions df) (f v)-            Nothing ->-                Left $-                    TypeMismatchException-                        ( MkTypeErrorContext-                            (Right (typeRep @b))-                            (Right (typeRep @d))-                            (Just "interpret")-                            (Just (show expression))-                        )-        _ -> error "Trying to apply numeric computation to non-numeric column"-interpret df expression@(AggFold expr op start (f :: (a -> b -> a))) = first (handleInterpretException (show expression)) $ do-    (TColumn column) <- interpret @b df expr-    value <- foldlColumn f start column-    pure $ TColumn $ fromVector $ V.replicate (fst $ dataframeDimensions df) value--data AggregationResult a-    = UnAggregated Column-    | Aggregated (TypedColumn a)--interpretAggregation ::-    forall a.-    (Columnable a) =>-    GroupedDataFrame -> Expr a -> Either DataFrameException (AggregationResult a)-interpretAggregation gdf (Lit value) =-    Right $-        Aggregated $-            TColumn $-                fromVector $-                    V.replicate (VU.length (offsets gdf) - 1) value-interpretAggregation gdf@(Grouped df names indices os) (Col name) = case getColumn name df of-    Nothing -> Left $ ColumnNotFoundException name "" (M.keys $ columnIndices df)-    Just (BoxedColumn col) -> Right $ UnAggregated $ fromVector $ mkUnaggregatedColumnBoxed col os indices-    Just (OptionalColumn col) -> Right $ UnAggregated $ fromVector $ mkUnaggregatedColumnBoxed col os indices-    Just (UnboxedColumn col) ->-        Right $ UnAggregated $ fromVector $ mkUnaggregatedColumnUnboxed col os indices-interpretAggregation gdf expression@(UnaryOp _ (f :: c -> d) expr) =-    case interpretAggregation @c gdf expr of-        Left (TypeMismatchException context) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show expr)-                        }-                    )-        Left e -> Left e-        Right (UnAggregated unaggregated) -> case unaggregated of-            BoxedColumn (col :: V.Vector b) -> case testEquality (typeRep @b) (typeRep @(V.Vector c)) of-                Just Refl -> case sUnbox @d of-                    SFalse -> Right $ UnAggregated $ fromVector $ V.map (V.map f) col-                    STrue ->-                        Right $-                            UnAggregated $-                                fromVector $-                                    V.map (V.convert @V.Vector @d @VU.Vector . V.map f) col-                Nothing -> case testEquality (typeRep @b) (typeRep @(VU.Vector c)) of-                    Nothing -> Left $ nestedTypeException @b @c (show expression)-                    Just Refl -> case (sUnbox @c, sUnbox @a) of-                        (SFalse, _) -> Left $ InternalException "Boxed type inside an unboxed column"-                        (STrue, STrue) -> Right $ UnAggregated $ fromVector $ V.map (VU.map f) col-                        (STrue, _) -> Right $ UnAggregated $ fromVector $ V.map (V.map f . VU.convert) col-            _ -> Left $ InternalException "Aggregated into a non-boxed column"-        Right (Aggregated (TColumn aggregated)) -> case mapColumn f aggregated of-            Left e -> Left e-            Right col -> Right $ Aggregated $ TColumn col-interpretAggregation gdf expression@(BinaryOp name (f :: c -> d -> e) left (Lit (right :: g))) = case testEquality (typeRep @g) (typeRep @d) of-    Just Refl -> interpretAggregation gdf (UnaryOp name (`f` right) left)-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @g)-                    , expectedType = Right (typeRep @d)-                    , callingFunctionName = Just "interpretAggregation"-                    , errorColumnName = Just (show expression)-                    }-                )-interpretAggregation gdf expression@(BinaryOp name (f :: c -> d -> e) (Lit (left :: g)) right) = case testEquality (typeRep @g) (typeRep @c) of-    Just Refl -> interpretAggregation gdf (UnaryOp name (f left) right)-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @g)-                    , expectedType = Right (typeRep @c)-                    , callingFunctionName = Just "interpretAggregation"-                    , errorColumnName = Just (show expression)-                    }-                )-interpretAggregation gdf expression@(BinaryOp _ (f :: c -> d -> e) left right) =-    case (interpretAggregation @c gdf left, interpretAggregation @d gdf right) of-        (Right (Aggregated (TColumn left')), Right (Aggregated (TColumn right'))) -> case zipWithColumns f left' right' of-            Left e -> Left e-            Right col -> Right $ Aggregated $ TColumn col-        (Right (UnAggregated left'), Right (UnAggregated right')) -> case (left', right') of-            (BoxedColumn (l :: V.Vector m), BoxedColumn (r :: V.Vector n)) -> case testEquality (typeRep @m) (typeRep @(VU.Vector c)) of-                Just Refl -> case testEquality (typeRep @n) (typeRep @(VU.Vector d)) of-                    Just Refl -> case (sUnbox @c, sUnbox @d, sUnbox @e) of-                        (STrue, STrue, STrue) ->-                            Right $ UnAggregated $ fromVector $ V.zipWith (VU.zipWith f) l r-                        (STrue, STrue, SFalse) ->-                            Right $-                                UnAggregated $-                                    fromVector $-                                        V.zipWith (\l' r' -> V.zipWith f (V.convert l') (V.convert r')) l r-                        (_, _, _) -> Left $ InternalException "Boxed vectors contain unboxed types"-                    Nothing -> case testEquality (typeRep @n) (typeRep @(V.Vector d)) of-                        Just Refl -> case sUnbox @c of-                            STrue ->-                                Right $-                                    UnAggregated $-                                        fromVector $-                                            V.zipWith (V.zipWith f . V.convert) l r-                            SFalse -> Left $ InternalException "Unboxed vectors contain boxed types"-                        Nothing -> Left $ nestedTypeException @n @d (show right)-                Nothing -> case testEquality (typeRep @m) (typeRep @(V.Vector c)) of-                    Nothing -> Left $ nestedTypeException @m @c (show left)-                    Just Refl -> case testEquality (typeRep @n) (typeRep @(VU.Vector d)) of-                        Just Refl -> case (sUnbox @d, sUnbox @e) of-                            (STrue, STrue) ->-                                Right $-                                    UnAggregated $-                                        fromVector $-                                            V.zipWith-                                                (\l' r' -> V.convert @V.Vector @e @VU.Vector $ V.zipWith f l' (V.convert r'))-                                                l-                                                r-                            (STrue, SFalse) ->-                                Right $-                                    UnAggregated $-                                        fromVector $-                                            V.zipWith (\l' r' -> V.zipWith f l' (V.convert r')) l r-                            (_, _) -> Left $ InternalException "Unboxed vectors contain boxed types"-                        Nothing -> case testEquality (typeRep @n) (typeRep @(V.Vector d)) of-                            Just Refl -> case sUnbox @e of-                                SFalse ->-                                    Right $-                                        UnAggregated $-                                            fromVector $-                                                V.zipWith (V.zipWith f . V.convert) l r-                                STrue ->-                                    Right $-                                        UnAggregated $-                                            fromVector $-                                                V.zipWith (\l' r' -> V.convert @V.Vector @e @VU.Vector $ V.zipWith f l' r') l r-                            Nothing -> Left $ nestedTypeException @n @d (show right)-            _ -> Left $ InternalException "Aggregated into a non-boxed column"-        (Right _, Right _) ->-            Left $-                AggregatedAndNonAggregatedException (T.pack $ show left) (T.pack $ show right)-        (Left (TypeMismatchException context), _) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show left)-                        }-                    )-        (Left e, _) -> Left e-        (_, Left (TypeMismatchException context)) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show right)-                        }-                    )-        (_, Left e) -> Left e-interpretAggregation gdf expression@(If cond (Lit l) (Lit r)) =-    case interpretAggregation @Bool gdf cond of-        Right (Aggregated (TColumn conditions)) -> case mapColumn-            (\(c :: Bool) -> if c then l else r)-            conditions of-            Left e -> Left e-            Right v -> Right $ Aggregated (TColumn v)-        Right (UnAggregated conditions) -> case sUnbox @a of-            STrue -> case mapColumn-                (\(c :: VU.Vector Bool) -> VU.map (\c' -> if c' then l else r) c)-                conditions of-                Left (TypeMismatchException context) ->-                    Left $-                        TypeMismatchException-                            ( context-                                { callingFunctionName = Just "interpretAggregation"-                                , errorColumnName = Just (show expression)-                                }-                            )-                Left e -> Left e-                Right v -> Right $ UnAggregated v-            SFalse -> case mapColumn-                (\(c :: VU.Vector Bool) -> V.map (\c' -> if c' then l else r) (VU.convert c))-                conditions of-                Left (TypeMismatchException context) ->-                    Left $-                        TypeMismatchException-                            ( context-                                { callingFunctionName = Just "interpretAggregation"-                                , errorColumnName = Just (show expression)-                                }-                            )-                Left e -> Left e-                Right v -> Right $ UnAggregated v-        Left (TypeMismatchException context) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show cond)-                        }-                    )-        Left e -> Left e-interpretAggregation gdf expression@(If cond (Lit l) r) =-    case ( interpretAggregation @Bool gdf cond-         , interpretAggregation @a gdf r-         ) of-        ( Right (Aggregated (TColumn conditions))-            , Right (Aggregated (TColumn right))-            ) -> case zipWithColumns-                (\(c :: Bool) (r' :: a) -> if c then l else r')-                conditions-                right of-                Left e -> Left e-                Right v -> Right $ Aggregated (TColumn v)-        ( Right (UnAggregated conditions)-            , Right (UnAggregated right@(BoxedColumn (right' :: V.Vector c)))-            ) -> case testEquality (typeRep @(V.Vector a)) (typeRep @c) of-                Just Refl -> case zipWithColumns-                    ( \(c :: VU.Vector Bool) (r' :: V.Vector a) ->-                        V.zipWith-                            (\c' r'' -> if c' then l else r'')-                            (V.convert c)-                            r'-                    )-                    conditions-                    right of-                    Left (TypeMismatchException context) ->-                        Left $-                            TypeMismatchException-                                ( context-                                    { callingFunctionName = Just "interpretAggregation"-                                    , errorColumnName = Just (show expression)-                                    }-                                )-                    Left e -> Left e-                    Right v -> Right $ UnAggregated v-                Nothing -> case testEquality (typeRep @(VU.Vector a)) (typeRep @c) of-                    Nothing -> Left $ nestedTypeException @c @a (show expression)-                    Just Refl -> case sUnbox @a of-                        SFalse -> Left $ InternalException "Boxed type in unboxed column"-                        STrue -> case zipWithColumns-                            ( \(c :: VU.Vector Bool) (r' :: VU.Vector a) ->-                                VU.zipWith-                                    (\c' r'' -> if c' then l else r'')-                                    c-                                    r'-                            )-                            conditions-                            right of-                            Left (TypeMismatchException context) ->-                                Left $-                                    TypeMismatchException-                                        ( context-                                            { callingFunctionName = Just "interpretAggregation"-                                            , errorColumnName = Just (show expression)-                                            }-                                        )-                            Left e -> Left e-                            Right v -> Right $ UnAggregated v-        (Right _, Right _) ->-            Left $-                AggregatedAndNonAggregatedException (T.pack $ show l) (T.pack $ show r)-        (Left (TypeMismatchException context), _) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show cond)-                        }-                    )-        (Left e, _) -> Left e-        (_, Left (TypeMismatchException context)) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show r)-                        }-                    )-        (_, Left e) -> Left e-interpretAggregation gdf expression@(If cond l (Lit r)) =-    case ( interpretAggregation @Bool gdf cond-         , interpretAggregation @a gdf l-         ) of-        ( Right (Aggregated (TColumn conditions))-            , Right (Aggregated (TColumn left))-            ) -> case zipWithColumns-                (\(c :: Bool) (l' :: a) -> if c then l' else r)-                conditions-                left of-                Left e -> Left e-                Right v -> Right $ Aggregated (TColumn v)-        ( Right (UnAggregated conditions)-            , Right (UnAggregated left@(BoxedColumn (left' :: V.Vector c)))-            ) -> case testEquality (typeRep @(V.Vector a)) (typeRep @c) of-                Just Refl -> case zipWithColumns-                    ( \(c :: VU.Vector Bool) (l' :: V.Vector a) ->-                        V.zipWith-                            (\c' l'' -> if c' then l'' else r)-                            (V.convert c)-                            l'-                    )-                    conditions-                    left of-                    Left (TypeMismatchException context) ->-                        Left $-                            TypeMismatchException-                                ( context-                                    { callingFunctionName = Just "interpretAggregation"-                                    , errorColumnName = Just (show expression)-                                    }-                                )-                    Left e -> Left e-                    Right v -> Right $ UnAggregated v-                Nothing -> case testEquality (typeRep @(VU.Vector a)) (typeRep @c) of-                    Nothing -> Left $ nestedTypeException @c @a (show expression)-                    Just Refl -> case sUnbox @a of-                        SFalse -> Left $ InternalException "Boxed type in unboxed column"-                        STrue -> case zipWithColumns-                            ( \(c :: VU.Vector Bool) (l' :: VU.Vector a) ->-                                VU.zipWith-                                    (\c' l'' -> if c' then l'' else r)-                                    c-                                    l'-                            )-                            conditions-                            left of-                            Left (TypeMismatchException context) ->-                                Left $-                                    TypeMismatchException-                                        ( context-                                            { callingFunctionName = Just "interpretAggregation"-                                            , errorColumnName = Just (show expression)-                                            }-                                        )-                            Left e -> Left e-                            Right v -> Right $ UnAggregated v-        (Right _, Right _) ->-            Left $-                AggregatedAndNonAggregatedException (T.pack $ show l) (T.pack $ show r)-        (Left (TypeMismatchException context), _) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show cond)-                        }-                    )-        (Left e, _) -> Left e-        (_, Left (TypeMismatchException context)) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show r)-                        }-                    )-        (_, Left e) -> Left e-interpretAggregation gdf expression@(If cond l r) =-    case ( interpretAggregation @Bool gdf cond-         , interpretAggregation @a gdf l-         , interpretAggregation @a gdf r-         ) of-        ( Right (Aggregated (TColumn conditions))-            , Right (Aggregated (TColumn left))-            , Right (Aggregated (TColumn right))-            ) -> case zipWithColumns-                (\(c :: Bool) (l' :: a, r' :: a) -> if c then l' else r')-                conditions-                (zipColumns left right) of-                Left e -> Left e-                Right v -> Right $ Aggregated (TColumn v)-        ( Right (UnAggregated conditions)-            , Right (UnAggregated left@(BoxedColumn (left' :: V.Vector b)))-            , Right (UnAggregated right@(BoxedColumn (right' :: V.Vector c)))-            ) -> case testEquality (typeRep @b) (typeRep @c) of-                Nothing ->-                    Left $-                        TypeMismatchException-                            ( MkTypeErrorContext-                                { userType = Right (typeRep @b)-                                , expectedType = Right (typeRep @c)-                                , callingFunctionName = Just "interpretAggregation"-                                , errorColumnName = Just (show expression)-                                }-                            )-                Just Refl -> case testEquality (typeRep @(V.Vector a)) (typeRep @b) of-                    Just Refl -> case zipWithColumns-                        ( \(c :: VU.Vector Bool) (l' :: V.Vector a, r' :: V.Vector a) ->-                            V.zipWith-                                (\c' (l'', r'') -> if c' then l'' else r'')-                                (V.convert c)-                                (V.zip l' r')-                        )-                        conditions-                        (zipColumns left right) of-                        Left (TypeMismatchException context) ->-                            Left $-                                TypeMismatchException-                                    ( context-                                        { callingFunctionName = Just "interpretAggregation"-                                        , errorColumnName = Just (show expression)-                                        }-                                    )-                        Left e -> Left e-                        Right v -> Right $ UnAggregated v-                    Nothing -> case testEquality (typeRep @(VU.Vector a)) (typeRep @b) of-                        Nothing -> Left $ nestedTypeException @b @a (show expression)-                        Just Refl -> case sUnbox @a of-                            SFalse -> Left $ InternalException "Boxed type in unboxed column"-                            STrue -> case zipWithColumns-                                ( \(c :: VU.Vector Bool) (l' :: VU.Vector a, r' :: VU.Vector a) ->-                                    VU.zipWith-                                        (\c' (l'', r'') -> if c' then l'' else r'')-                                        c-                                        (VU.zip l' r')-                                )-                                conditions-                                (zipColumns left right) of-                                Left (TypeMismatchException context) ->-                                    Left $-                                        TypeMismatchException-                                            ( context-                                                { callingFunctionName = Just "interpretAggregation"-                                                , errorColumnName = Just (show expression)-                                                }-                                            )-                                Left e -> Left e-                                Right v -> Right $ UnAggregated v-        (Right _, Right _, Right _) ->-            Left $-                AggregatedAndNonAggregatedException (T.pack $ show l) (T.pack $ show r)-        (Left (TypeMismatchException context), _, _) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show cond)-                        }-                    )-        (Left e, _, _) -> Left e-        (_, Left (TypeMismatchException context), _) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show l)-                        }-                    )-        (_, Left e, _) -> Left e-        (_, _, Left (TypeMismatchException context)) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show r)-                        }-                    )-        (_, _, Left e) -> Left e-interpretAggregation gdf@(Grouped df names indices os) expression@(AggVector expr op (f :: v b -> c)) =-    case interpretAggregation @b gdf expr of-        Right (UnAggregated (BoxedColumn (col :: V.Vector d))) -> case testEquality (typeRep @(v b)) (typeRep @d) of-            Nothing -> Left $ nestedTypeException @d @b (show expr)-            Just Refl -> case testEquality (typeRep @v) (typeRep @V.Vector) of-                Nothing -> Right $ Aggregated $ TColumn $ fromVector $ V.map (f . V.convert) col-                Just Refl -> Right $ Aggregated $ TColumn $ fromVector $ V.map f col-        Right (UnAggregated _) -> Left $ InternalException "Aggregated into non-boxed column"-        Right (Aggregated (TColumn (BoxedColumn (col :: V.Vector d)))) -> case testEquality (typeRep @b) (typeRep @d) of-            Just Refl -> case testEquality (typeRep @v) (typeRep @V.Vector) of-                Just Refl -> interpretAggregation @c gdf (Lit (f col))-                Nothing -> interpretAggregation @c gdf (Lit ((f . V.convert) col))-            Nothing ->-                Left $-                    TypeMismatchException-                        ( MkTypeErrorContext-                            { userType = Right (typeRep @b)-                            , expectedType = Right (typeRep @d)-                            , callingFunctionName = Just "interpretAggregation"-                            , errorColumnName = Just (show expr)-                            }-                        )-        Right (Aggregated (TColumn (UnboxedColumn (col :: VU.Vector d)))) -> case testEquality (typeRep @b) (typeRep @d) of-            Just Refl -> case testEquality (typeRep @v) (typeRep @VU.Vector) of-                Just Refl -> interpretAggregation @c gdf (Lit (f col))-                Nothing -> interpretAggregation @c gdf (Lit ((f . VU.convert) col))-            Nothing ->-                Left $-                    TypeMismatchException-                        ( MkTypeErrorContext-                            { userType = Right (typeRep @b)-                            , expectedType = Right (typeRep @d)-                            , callingFunctionName = Just "interpretAggregation"-                            , errorColumnName = Just (show expr)-                            }-                        )-        Right (Aggregated (TColumn (OptionalColumn (col :: V.Vector d)))) -> case testEquality (typeRep @b) (typeRep @d) of-            Just Refl -> case testEquality (typeRep @v) (typeRep @V.Vector) of-                Just Refl -> interpretAggregation @c gdf (Lit (f col))-                Nothing -> interpretAggregation @c gdf (Lit ((f . V.convert) col))-            Nothing ->-                Left $-                    TypeMismatchException-                        ( MkTypeErrorContext-                            { userType = Right (typeRep @b)-                            , expectedType = Right (typeRep @d)-                            , callingFunctionName = Just "interpretAggregation"-                            , errorColumnName = Just (show expr)-                            }-                        )-        (Left (TypeMismatchException context)) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show expression)-                        }-                    )-        (Left e) -> Left e-interpretAggregation gdf@(Grouped df names indices os) expression@(AggNumericVector (Col name) op (f :: VU.Vector b -> c)) =-    case getColumn name df of-        -- TODO(mchavinda): Fix the compedium of type errors here-        -- This is mostly done help with the benchmarking.-        Nothing -> Left $ ColumnNotFoundException name "" (M.keys $ columnIndices df)-        Just (BoxedColumn col) -> error "Type mismatch."-        Just (OptionalColumn col) -> error "Type mismatch."-        Just (UnboxedColumn (col :: VU.Vector d)) -> case testEquality (typeRep @b) (typeRep @d) of-            Just Refl -> case testEquality (typeRep @c) (typeRep @a) of-                Just Refl ->-                    Right $-                        Aggregated $-                            TColumn $-                                fromUnboxedVector $-                                    mkAggregatedColumnUnboxed col os indices f-                Nothing -> error "Type mismatch"-            Nothing -> error "Type mismatch"-interpretAggregation gdf@(Grouped df names indices os) expression@(AggNumericVector expr op (f :: VU.Vector b -> c)) =-    case interpretAggregation @b gdf expr of-        (Left (TypeMismatchException context)) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show expression)-                        }-                    )-        (Left e) -> Left e-        Right (UnAggregated (BoxedColumn (col :: V.Vector d))) -> case testEquality (typeRep @(VU.Vector b)) (typeRep @d) of-            Nothing -> case testEquality (typeRep @(VU.Vector Int)) (typeRep @d) of-                Nothing -> case testEquality (typeRep @(V.Vector Integer)) (typeRep @d) of-                    Nothing -> Left $ nestedTypeException @d @b (show expr)-                    Just Refl ->-                        Right $-                            Aggregated $-                                TColumn $-                                    fromVector $-                                        V.map (f . VU.convert . V.map fromIntegral) col-                Just Refl ->-                    Right $-                        Aggregated $-                            TColumn $-                                fromVector $-                                    V.map f (V.map (VU.map fromIntegral) col)-            Just Refl -> Right $ Aggregated $ TColumn $ fromVector $ V.map f col-        Right (UnAggregated _) -> Left $ InternalException "Aggregated into non-boxed column"-        Right (Aggregated (TColumn (BoxedColumn (col :: V.Vector d)))) -> case testEquality (typeRep @Integer) (typeRep @d) of-            Just Refl -> interpretAggregation @c gdf (Lit ((f . V.convert . V.map fromIntegral) col))-            Nothing ->-                Left $-                    TypeMismatchException-                        ( MkTypeErrorContext-                            { userType = Right (typeRep @b)-                            , expectedType = Right (typeRep @d)-                            , callingFunctionName = Just "interpretAggregation"-                            , errorColumnName = Just (show expr)-                            }-                        )-        Right (Aggregated (TColumn (UnboxedColumn (col :: VU.Vector d)))) -> case testEquality (typeRep @b) (typeRep @d) of-            Just Refl -> interpretAggregation @c gdf (Lit (f col))-            Nothing ->-                Left $-                    TypeMismatchException-                        ( MkTypeErrorContext-                            { userType = Right (typeRep @b)-                            , expectedType = Right (typeRep @d)-                            , callingFunctionName = Just "interpretAggregation"-                            , errorColumnName = Just (show expr)-                            }-                        )-        Right (Aggregated (TColumn (OptionalColumn (col :: V.Vector (Maybe d))))) -> case testEquality (typeRep @b) (typeRep @d) of-            Just Refl ->-                interpretAggregation @c-                    gdf-                    (Lit ((f . V.convert . V.map (fromMaybe 0) . V.filter isJust) col))-            Nothing ->-                Left $-                    TypeMismatchException-                        ( MkTypeErrorContext-                            { userType = Right (typeRep @b)-                            , expectedType = Right (typeRep @d)-                            , callingFunctionName = Just "interpretAggregation"-                            , errorColumnName = Just (show expr)-                            }-                        )-interpretAggregation gdf@(Grouped df names indices os) expression@(AggReduce (Col name) op (f :: a -> a -> a)) =-    case getColumn name df of-        Nothing -> Left $ ColumnNotFoundException name "" (M.keys $ columnIndices df)-        Just (BoxedColumn (col :: V.Vector d)) -> case testEquality (typeRep @a) (typeRep @d) of-            Nothing -> error "Type mismatch"-            Just Refl ->-                Right $-                    Aggregated $-                        TColumn $-                            fromVector $-                                mkReducedColumnBoxed col os indices f-        Just (OptionalColumn (col :: V.Vector d)) -> case testEquality (typeRep @a) (typeRep @d) of-            Nothing -> error "Type mismatch"-            Just Refl ->-                Right $-                    Aggregated $-                        TColumn $-                            fromVector $-                                mkReducedColumnBoxed col os indices f-        Just (UnboxedColumn (col :: VU.Vector d)) -> case testEquality (typeRep @a) (typeRep @d) of-            Just Refl ->-                Right $-                    Aggregated $-                        TColumn $-                            fromUnboxedVector $-                                mkReducedColumnUnboxed col os indices f-            Nothing -> error "Type mismatch"-interpretAggregation gdf@(Grouped df names indices os) expression@(AggReduce expr op (f :: a -> a -> a)) =-    case interpretAggregation @a gdf expr of-        (Left (TypeMismatchException context)) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show expression)-                        }-                    )-        (Left e) -> Left e-        Right (UnAggregated (BoxedColumn (col :: V.Vector d))) -> case testEquality (typeRep @(V.Vector a)) (typeRep @d) of-            Nothing -> case testEquality (typeRep @(VU.Vector a)) (typeRep @d) of-                Nothing -> Left $ nestedTypeException @d @a (show expr)-                Just Refl -> case sUnbox @a of-                    STrue ->-                        Right $-                            Aggregated $-                                TColumn $-                                    fromVector $-                                        V.map (VU.foldl1' f) col-                    SFalse -> Left $ InternalException "Boxed type inside an unboxed column"-            Just Refl ->-                Right $-                    Aggregated $-                        TColumn $-                            fromVector $-                                V.map (V.foldl1' f) col-        Right (UnAggregated _) -> Left $ InternalException "Aggregated into non-boxed column"-        Right (Aggregated (TColumn column)) -> case foldl1Column f column of-            Left e -> Left e-            Right value -> interpretAggregation @a gdf (Lit value)-interpretAggregation gdf@(Grouped df names indices os) expression@(AggFold expr op s (f :: (a -> b -> a))) =-    case interpretAggregation @b gdf expr of-        (Left (TypeMismatchException context)) ->-            Left $-                TypeMismatchException-                    ( context-                        { callingFunctionName = Just "interpretAggregation"-                        , errorColumnName = Just (show expression)-                        }-                    )-        (Left e) -> Left e-        Right (UnAggregated (BoxedColumn (col :: V.Vector d))) -> case testEquality (typeRep @(V.Vector b)) (typeRep @d) of-            Just Refl -> Right $ Aggregated $ TColumn $ fromVector $ V.map (V.foldl' f s) col-            Nothing -> case testEquality (typeRep @(VU.Vector b)) (typeRep @d) of-                Just Refl -> case sUnbox @b of-                    STrue ->-                        Right $ Aggregated $ TColumn $ fromVector $ V.map (VU.foldl' f s) col-                    SFalse -> Left $ InternalException "Boxed type inside an unboxed column"-                Nothing -> Left $ nestedTypeException @d @b (show expr)-        Right (UnAggregated _) -> Left $ InternalException "Aggregated into non-boxed column"-        Right (Aggregated (TColumn column)) -> case foldlColumn f s column of-            Left e -> Left e-            Right value -> interpretAggregation @a gdf (Lit value)--handleInterpretException :: String -> DataFrameException -> DataFrameException-handleInterpretException errorLocation (TypeMismatchException context) = mkTypeMismatchException (Just "interpret") (Just errorLocation) context-handleInterpretException _ e = e--numRows :: DataFrame -> Int-numRows df = fst (dataframeDimensions df)--mkUnaggregatedColumnBoxed ::-    forall a.-    (Columnable a) =>-    V.Vector a -> VU.Vector Int -> VU.Vector Int -> V.Vector (V.Vector a)-mkUnaggregatedColumnBoxed col os indices =-    let-        sorted = V.unsafeBackpermute col (V.convert indices)-        n i = os `VU.unsafeIndex` (i + 1) - (os `VU.unsafeIndex` i)-        start i = os `VU.unsafeIndex` i-     in-        V.generate-            (VU.length os - 1)-            ( \i ->-                V.unsafeSlice (start i) (n i) sorted-            )--mkUnaggregatedColumnUnboxed ::-    forall a.-    (Columnable a, VU.Unbox a) =>-    VU.Vector a -> VU.Vector Int -> VU.Vector Int -> V.Vector (VU.Vector a)-mkUnaggregatedColumnUnboxed col os indices =-    let-        sorted = VU.unsafeBackpermute col indices-        n i = os `VU.unsafeIndex` (i + 1) - (os `VU.unsafeIndex` i)-        start i = os `VU.unsafeIndex` i-     in-        V.generate-            (VU.length os - 1)-            ( \i ->-                VU.unsafeSlice (start i) (n i) sorted-            )--mkAggregatedColumnUnboxed ::-    forall a b.-    (Columnable a, VU.Unbox a, Columnable b, VU.Unbox b) =>-    VU.Vector a ->-    VU.Vector Int ->-    VU.Vector Int ->-    (VU.Vector a -> b) ->-    VU.Vector b-mkAggregatedColumnUnboxed col os indices f =-    let-        sorted = VU.unsafeBackpermute col indices-        n i = os `VU.unsafeIndex` (i + 1) - (os `VU.unsafeIndex` i)-        start i = os `VU.unsafeIndex` i-     in-        VU.generate-            (VU.length os - 1)-            ( \i ->-                f (VU.unsafeSlice (start i) (n i) sorted)-            )--mkReducedColumnUnboxed ::-    forall a.-    (VU.Unbox a) =>-    VU.Vector a ->-    VU.Vector Int ->-    VU.Vector Int ->-    (a -> a -> a) ->-    VU.Vector a-mkReducedColumnUnboxed col os indices f = runST $ do-    let len = VU.length os - 1-    mvec <- VUM.unsafeNew len--    let loopOut i-            | i == len = return ()-            | otherwise = do-                let !start = os `VU.unsafeIndex` i-                let !end = os `VU.unsafeIndex` (i + 1)-                let !initVal = col `VU.unsafeIndex` (indices `VU.unsafeIndex` start)--                let loopIn !acc !idx-                        | idx == end = acc-                        | otherwise =-                            let val = col `VU.unsafeIndex` (indices `VU.unsafeIndex` idx)-                             in loopIn (f acc val) (idx + 1)-                let !finalVal = loopIn initVal (start + 1)-                VUM.unsafeWrite mvec i finalVal-                loopOut (i + 1)--    loopOut 0-    VU.unsafeFreeze mvec-{-# INLINE mkReducedColumnUnboxed #-}--mkReducedColumnBoxed ::-    V.Vector a ->-    VU.Vector Int ->-    VU.Vector Int ->-    (a -> a -> a) ->-    V.Vector a-mkReducedColumnBoxed col os indices f = runST $ do-    let len = VU.length os - 1-    mvec <- VM.unsafeNew len--    let loopOut i-            | i == len = return ()-            | otherwise = do-                let start = os `VU.unsafeIndex` i-                let end = os `VU.unsafeIndex` (i + 1)-                let initVal = col `V.unsafeIndex` (indices `VU.unsafeIndex` start)--                let loopIn !acc idx-                        | idx == end = acc-                        | otherwise =-                            let val = col `V.unsafeIndex` (indices `VU.unsafeIndex` idx)-                             in loopIn (f acc val) (idx + 1)-                let !finalVal = loopIn initVal (start + 1)-                VM.unsafeWrite mvec i finalVal-                loopOut (i + 1)--    loopOut 0-    V.unsafeFreeze mvec-{-# INLINE mkReducedColumnBoxed #-}--nestedTypeException ::-    forall a b. (Typeable a, Typeable b) => String -> DataFrameException-nestedTypeException expression = case typeRep @a of-    App t1 t2 ->-        TypeMismatchException-            ( MkTypeErrorContext-                { userType = Left (show (typeRep @b)) :: Either String (TypeRep ())-                , expectedType = Left (show (typeRep @a)) :: Either String (TypeRep ())-                , callingFunctionName = Just "interpretAggregation"-                , errorColumnName = Just expression-                }-            )-    t ->-        TypeMismatchException-            ( MkTypeErrorContext-                { userType = Right (typeRep @(VU.Vector b))-                , expectedType = Right (typeRep @b)-                , callingFunctionName = Just "interpretAggregation"-                , errorColumnName = Just expression-                }-            )--mkTypeMismatchException ::-    (Typeable a, Typeable b) =>-    Maybe String -> Maybe String -> TypeErrorContext a b -> DataFrameException-mkTypeMismatchException callPoint errorLocation context =-    TypeMismatchException-        ( context-            { callingFunctionName = callPoint-            , errorColumnName = errorLocation-            }-        )
− src/DataFrame/Internal/Parsing.hs
@@ -1,104 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module DataFrame.Internal.Parsing where--import qualified Data.ByteString.Char8 as C-import qualified Data.Set as S-import qualified Data.Text as T--import Data.ByteString.Lex.Fractional-import Data.Maybe (fromMaybe)-import Data.Text.Read-import GHC.Stack (HasCallStack)-import Text.Read (readMaybe)--isNullish :: T.Text -> Bool-isNullish =-    ( `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--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))
− src/DataFrame/Internal/Row.hs
@@ -1,220 +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.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)--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,128 +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 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)--{- | 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/CSV.hs
@@ -1,395 +0,0 @@-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.Lazy.IO.CSV 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.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 Control.Applicative (many, (<|>))-import Control.Monad (forM_, unless, when, zipWithM_)-import Data.Attoparsec.Text-import Data.Char-import Data.Foldable (fold)-import Data.Function (on)-import Data.IORef-import Data.Maybe-import Data.Type.Equality (TestEquality (testEquality))-import DataFrame.Internal.Column (-    Column (..),-    MutableColumn (..),-    columnLength,-    freezeColumn',-    writeColumn,- )-import DataFrame.Internal.DataFrame (DataFrame (..))-import DataFrame.Internal.Parsing-import System.IO-import Type.Reflection-import Prelude hiding (concat, takeWhile)---- | Record for CSV read options.-data ReadOptions = ReadOptions-    { hasHeader :: Bool-    , inferTypes :: Bool-    , safeRead :: Bool-    , rowRange :: !(Maybe (Int, Int)) -- (start, length)-    , seekPos :: !(Maybe Integer)-    , totalRows :: !(Maybe Int)-    , leftOver :: !T.Text-    , rowsRead :: !Int-    }--{- | 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 <- map T.strip . parseSep c <$> TIO.hGetLine handle-        let columnNames =-                if hasHeader opts-                    then map (T.filter (/= '\"')) firstRow-                    else map (T.singleton . intToDigit) [0 .. (length firstRow - 1)]-        -- If there was no header rewind the file cursor.-        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.-        -- TODO: this isn't robust but in so far as this is a guess anyway-        -- it's probably fine. But we should probably sample n rows and pick-        -- the most likely type from the sample.-        (dataRow, remainder) <- readSingleLine c (leftOver opts) handle--        -- This array will track the indices of all null values for each column.-        -- If any exist then the column will be an optional type.-        nullIndices <- VM.unsafeNew numColumns-        VM.set nullIndices []-        mutableCols <- VM.unsafeNew numColumns-        getInitialDataVectors numRows mutableCols dataRow--        -- 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 -- TODO create base expressions.-                }-            , (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 #-}--inferValueType :: T.Text -> T.Text-inferValueType s =-    let-        example = s-     in-        case readInt example of-            Just _ -> "Int"-            Nothing -> case readDouble example of-                Just _ -> "Double"-                Nothing -> "Other"-{-# INLINE inferValueType #-}--readSingleLine :: Char -> T.Text -> Handle -> IO ([T.Text], T.Text)-readSingleLine c unused handle =-    parseWith (TIO.hGetChunk handle) (parseRow c) unused >>= \case-        Fail unconsumed ctx er -> do-            erpos <- hTell handle-            fail $-                "Failed to parse CSV file around "-                    <> show erpos-                    <> " byte; due: "-                    <> show er-                    <> "; context: "-                    <> show ctx-        Partial c -> do-            fail "Partial handler is called"-        Done (unconsumed :: T.Text) (row :: [T.Text]) -> do-            return (row, unconsumed)---- | 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 c -> 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 #-}--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 c -> do-                        fail $ "Partial handler is called; n = " <> show n-                    Done (unconsumed :: T.Text) _ ->-                        go (n + 1) unconsumed h-{-# INLINE countRows #-}--writeCsv :: FilePath -> DataFrame -> IO ()-writeCsv = writeSeparated ','--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
− src/DataFrame/Lazy/Internal/DataFrame.hs
@@ -1,107 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE InstanceSigs #-}-{-# LANGUAGE NumericUnderscores #-}-{-# LANGUAGE OverloadedStrings #-}--module DataFrame.Lazy.Internal.DataFrame where--import Control.Monad (foldM)-import qualified Data.List as L-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 qualified DataFrame.Lazy.IO.CSV as D-import DataFrame.Operations.Merge ()-import qualified DataFrame.Operations.Subset as D-import qualified DataFrame.Operations.Transformations as D--data LazyOperation where-    Derive :: (C.Columnable a) => T.Text -> E.Expr a -> LazyOperation-    Select :: [T.Text] -> LazyOperation-    Filter :: E.Expr Bool -> LazyOperation--instance Show LazyOperation where-    show :: LazyOperation -> String-    show (Derive name expr) = T.unpack name ++ " := " ++ show expr-    show (Select columns) = "select(" ++ show columns ++ ")"-    show (Filter expr) = "filter(" ++ show expr ++ ")"--data InputType = ICSV deriving (Show)--data LazyDataFrame = LazyDataFrame-    { inputPath :: FilePath-    , inputType :: InputType-    , operations :: [LazyOperation]-    , batchSize :: Int-    }-    deriving (Show)--eval :: LazyOperation -> D.DataFrame -> D.DataFrame-eval (Derive name expr) = D.derive name expr-eval (Select columns) = D.select columns-eval (Filter expr) = D.filterWhere expr--runDataFrame :: forall a. (C.Columnable a) => LazyDataFrame -> IO D.DataFrame-runDataFrame df = do-    let path = inputPath df-    totalRows <- D.countRows ',' path-    let batches = batchRanges totalRows (batchSize df)-    (df', _) <--        foldM-            ( \(accDf, (pos, unused, r)) (start, end) -> do-                mapM_-                    putStr-                    [ "Scanning: "-                    , show start-                    , " to "-                    , show end-                    , " rows out of "-                    , show totalRows-                    , "\n"-                    ]--                (sdf, (pos', unconsumed, rowsRead)) <--                    D.readSeparated-                        ','-                        ( D.defaultOptions-                            { D.rowRange = Just (start, batchSize df)-                            , D.totalRows = Just totalRows-                            , D.seekPos = pos-                            , D.rowsRead = r-                            , D.leftOver = unused-                            }-                        )-                        path-                let rdf = L.foldl' (flip eval) sdf (operations df)-                return (accDf <> rdf, (Just pos', unconsumed, rowsRead + r))-            )-            (D.empty, (Nothing, "", 0))-            batches-    return df'--batchRanges :: Int -> Int -> [(Int, Int)]-batchRanges n inc = go n [0, inc .. n]-  where-    go _ [] = []-    go n [x] = [(x, n)]-    go n (f : s : rest) = (f, s) : go n (s : rest)--scanCsv :: T.Text -> LazyDataFrame-scanCsv path = LazyDataFrame (T.unpack path) ICSV [] 512_000--addOperation :: LazyOperation -> LazyDataFrame -> LazyDataFrame-addOperation op df = df{operations = operations df ++ [op]}--derive ::-    (C.Columnable a) => T.Text -> E.Expr a -> LazyDataFrame -> LazyDataFrame-derive name expr = addOperation (Derive name expr)--select :: (C.Columnable a) => [T.Text] -> LazyDataFrame -> LazyDataFrame-select columns = addOperation (Select columns)--filter :: (C.Columnable a) => E.Expr Bool -> LazyDataFrame -> LazyDataFrame-filter cond = addOperation (Filter cond)
− 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,286 +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)-    | otherwise =-        Grouped-            df-            names-            (VU.map fst valueIndices)-            (changingPoints valueIndices)-  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--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) =-    let-        df' =-            selectIndices-                (VU.map (valueIndices VU.!) (VU.init offsets))-                (select groupingColumns df)--        f (name, Wrap (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) = groupBy (columnNames df) df
− src/DataFrame/Operations/Core.hs
@@ -1,946 +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. (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. (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,330 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}--module DataFrame.Operations.Join where--import Control.Applicative (asum)-import qualified Data.HashMap.Strict as HM-import qualified Data.Map.Strict as M-import Data.Maybe (fromMaybe)-import qualified Data.Text as T-import Data.Type.Equality (TestEquality (..))-import qualified Data.Vector as VB-import qualified Data.Vector.Unboxed as VU-import DataFrame.Internal.Column as D-import DataFrame.Internal.DataFrame as D-import DataFrame.Operations.Aggregation as D-import DataFrame.Operations.Core as D-import Type.Reflection---- | Equivalent to SQL join types.-data JoinType-    = INNER-    | LEFT-    | RIGHT-    | FULL_OUTER--{- | Join two dataframes using SQL join semantics.--Only inner join is implemented for now.--}-join ::-    JoinType ->-    [T.Text] ->-    DataFrame -> -- Right hand side-    DataFrame -> -- Left hand side-    DataFrame-join INNER xs right = innerJoin xs right-join LEFT xs right = leftJoin xs right-join RIGHT xs right = rightJoin xs right-join FULL_OUTER xs right = fullOuterJoin xs right--{- | 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 right left =-    let-        -- Prepare Keys for the Right DataFrame-        rightIndicesToGroup =-            [c | (k, c) <- M.toList (D.columnIndices right), k `elem` cs]--        rightRowRepresentations :: VU.Vector Int-        rightRowRepresentations = D.computeRowHashes rightIndicesToGroup right--        -- Build the Hash Map: Int -> Vector of Indices-        -- We use ifoldr to efficiently insert (index, key) without intermediate allocations.-        rightKeyMap :: HM.HashMap Int (VU.Vector Int)-        rightKeyMap =-            let accumulator =-                    VU.ifoldr-                        (\i key acc -> HM.insertWith (++) key [i] acc)-                        HM.empty-                        rightRowRepresentations-             in HM.map (VU.fromList . reverse) accumulator--        -- Prepare Keys for Left DataFrame-        leftIndicesToGroup =-            [c | (k, c) <- M.toList (D.columnIndices left), k `elem` cs]--        leftRowRepresentations :: VU.Vector Int-        leftRowRepresentations = D.computeRowHashes leftIndicesToGroup left--        -- Perform the Join-        (leftIndexChunks, rightIndexChunks) =-            VU.ifoldr-                ( \lIdx key (lAcc, rAcc) ->-                    case HM.lookup key rightKeyMap of-                        Nothing -> (lAcc, rAcc)-                        Just rIndices ->-                            let len = VU.length rIndices-                                -- Replicate the Left Index to match the number of Right matches-                                lChunk = VU.replicate len lIdx-                             in (lChunk : lAcc, rIndices : rAcc)-                )-                ([], [])-                leftRowRepresentations--        -- Flatten chunks-        expandedLeftIndicies = VU.concat leftIndexChunks-        expandedRightIndicies = VU.concat rightIndexChunks--        resultLen = VU.length expandedLeftIndicies--        -- Construct Result DataFrames-        expandedLeft =-            left-                { columns = VB.map (D.atIndicesStable expandedLeftIndicies) (D.columns left)-                , dataframeDimensions = (resultLen, snd (D.dataframeDimensions left))-                }--        expandedRight =-            right-                { columns = VB.map (D.atIndicesStable expandedRightIndicies) (D.columns right)-                , dataframeDimensions = (resultLen, snd (D.dataframeDimensions right))-                }--        leftColumns = D.columnNames left-        rightColumns = D.columnNames right--        insertIfPresent _ Nothing df = df-        insertIfPresent name (Just c) df = D.insertColumn name c df-     in-        D.fold-            ( \name df ->-                if name `elem` cs-                    then df-                    else-                        ( if name `elem` leftColumns-                            then insertIfPresent ("Right_" <> name) (D.getColumn name expandedRight) df-                            else insertIfPresent name (D.getColumn name expandedRight) df-                        )-            )-            rightColumns-            expandedLeft--{- | Performs a left join on two dataframes using the specified key columns.-Returns all rows from the left dataframe, with matching rows from the right dataframe.-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 right left =-    let-        leftIndicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` cs) (D.columnIndices left)-        leftRowRepresentations = D.computeRowHashes leftIndicesToGroup left-        rightIndicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` cs) (D.columnIndices right)-        rightRowRepresentations = D.computeRowHashes rightIndicesToGroup right-        rightKeyCountsAndIndices =-            VU.foldr-                (\(i, v) acc -> M.insertWith (++) v [i] acc)-                M.empty-                (VU.indexed rightRowRepresentations)-        rightKeyCountsAndIndicesVec = M.map VU.fromList rightKeyCountsAndIndices-        leftRowCount = fst (D.dimensions left)-        pairs =-            [ (i, maybeRight)-            | i <- [0 .. leftRowCount - 1]-            , maybeRight <--                case M.lookup (leftRowRepresentations VU.! i) rightKeyCountsAndIndicesVec of-                    Nothing -> [Nothing]-                    Just rVec -> map Just (VU.toList rVec)-            ]-        expandedLeftIndicies = VU.fromList (map fst pairs)-        expandedRightIndicies = VB.fromList (map snd pairs)-        expandedLeft =-            left-                { columns = VB.map (D.atIndicesStable expandedLeftIndicies) (D.columns left)-                , dataframeDimensions =-                    (VU.length expandedLeftIndicies, snd (D.dataframeDimensions left))-                }-        expandedRight =-            right-                { columns = VB.map (D.atIndicesWithNulls expandedRightIndicies) (D.columns right)-                , dataframeDimensions =-                    (VB.length expandedRightIndicies, snd (D.dataframeDimensions right))-                }-        leftColumns = D.columnNames left-        rightColumns = D.columnNames right-        initDf = expandedLeft-        insertIfPresent _ Nothing df = df-        insertIfPresent name (Just c) df = D.insertColumn name c df-     in-        D.fold-            ( \name df ->-                if name `elem` cs-                    then df-                    else-                        ( if name `elem` leftColumns-                            then insertIfPresent ("Right_" <> name) (D.getColumn name expandedRight) df-                            else insertIfPresent name (D.getColumn name expandedRight) df-                        )-            )-            rightColumns-            initDf--{- | 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 right left =-    let-        leftIndicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` cs) (D.columnIndices left)-        leftRowRepresentations = D.computeRowHashes leftIndicesToGroup left-        leftKeyCountsAndIndices =-            VU.foldr-                (\(i, v) acc -> M.insertWith (++) v [i] acc)-                M.empty-                (VU.indexed leftRowRepresentations)-        leftKeyCountsAndIndicesVec = M.map VU.fromList leftKeyCountsAndIndices-        rightIndicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` cs) (D.columnIndices right)-        rightRowRepresentations = D.computeRowHashes rightIndicesToGroup right-        rightKeyCountsAndIndices =-            VU.foldr-                (\(i, v) acc -> M.insertWith (++) v [i] acc)-                M.empty-                (VU.indexed rightRowRepresentations)-        rightKeyCountsAndIndicesVec = M.map VU.fromList rightKeyCountsAndIndices-        matchedPairs =-            concatMap-                ( \(lVec, rVec) ->-                    [ (Just lIdx, Just rIdx)-                    | lIdx <- VU.toList lVec-                    , rIdx <- VU.toList rVec-                    ]-                )-                ( M.elems-                    (M.intersectionWith (,) leftKeyCountsAndIndicesVec rightKeyCountsAndIndicesVec)-                )-        leftOnlyPairs =-            concatMap-                (map (\lIdx -> (Just lIdx, Nothing)) . VU.toList)-                (M.elems (leftKeyCountsAndIndicesVec `M.difference` rightKeyCountsAndIndicesVec))-        rightOnlyPairs =-            concatMap-                (map (\rIdx -> (Nothing, Just rIdx)) . VU.toList)-                (M.elems (rightKeyCountsAndIndicesVec `M.difference` leftKeyCountsAndIndicesVec))-        pairs = matchedPairs ++ leftOnlyPairs ++ rightOnlyPairs-        expandedLeftIndicies = VB.fromList (map fst pairs)-        expandedRightIndicies = VB.fromList (map snd pairs)-        expandedLeft =-            left-                { columns = VB.map (D.atIndicesWithNulls expandedLeftIndicies) (D.columns left)-                , dataframeDimensions =-                    (VB.length expandedLeftIndicies, snd (D.dataframeDimensions left))-                }-        expandedRight =-            right-                { columns = VB.map (D.atIndicesWithNulls expandedRightIndicies) (D.columns right)-                , dataframeDimensions =-                    (VB.length expandedRightIndicies, snd (D.dataframeDimensions right))-                }-        leftColumns = D.columnNames left-        rightColumns = D.columnNames right-        initDf = expandedLeft-        insertIfPresent _ Nothing df = df-        insertIfPresent name (Just c) df = D.insertColumn name c df-     in-        D.fold-            ( \name df ->-                if name `elem` cs-                    then case (D.unsafeGetColumn name expandedRight, D.unsafeGetColumn name expandedLeft) of-                        ( OptionalColumn (left :: VB.Vector (Maybe a))-                            , OptionalColumn (right :: VB.Vector (Maybe b))-                            ) -> case testEquality (typeRep @a) (typeRep @b) of-                                Nothing -> error "Cannot join columns of different types"-                                Just Refl ->-                                    D.insert-                                        name-                                        (VB.map (fromMaybe undefined) (VB.zipWith (\l r -> asum [l, r]) left right))-                                        df-                        _ -> error "Join should have optional keys."-                    else-                        ( if name `elem` leftColumns-                            then insertIfPresent ("Right_" <> name) (D.getColumn name expandedRight) df-                            else insertIfPresent name (D.getColumn name expandedRight) df-                        )-            ) -- ???-            rightColumns-            initDf
− src/DataFrame/Operations/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,68 +0,0 @@-{-# 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 Control.Exception (throw)-import DataFrame.Errors (DataFrameException (..))-import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (DataFrame (..))-import DataFrame.Internal.Row-import DataFrame.Operations.Core-import System.Random---- | Sort order taken as a parameter by the 'sortBy' function.-data SortOrder-    = Asc T.Text-    | Desc T.Text-    deriving (Eq)--getSortColumnName :: SortOrder -> T.Text-getSortColumnName (Asc n) = n-getSortColumnName (Desc n) = n--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 :: (RandomGen g) => g -> Int -> VU.Vector Int-shuffledIndices pureGen k = VU.fromList (fst (uniformShuffleList [0 .. (k - 1)] pureGen))
− src/DataFrame/Operations/Statistics.hs
@@ -1,394 +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 :: T.Text -> DataFrame -> DataFrame-frequencies name df =-    let-        counts :: forall a. (Columnable a) => [(a, Int)]-        counts = valueCounts (Col @a name) df-        calculatePercentage cs k = toAny $ toPct2dp (fromIntegral k / fromIntegral (P.sum $ map snd cs))-        initDf =-            empty-                & insertVector "Statistic" (V.fromList ["Count" :: T.Text, "Percentage (%)"])-        freqs :: forall v a. (VG.Vector v a, Columnable a) => v a -> DataFrame-        freqs col =-            L.foldl'-                ( \d (col, k) ->-                    insertVector-                        (showValue @a col)-                        (V.fromList [toAny k, calculatePercentage (counts @a) k])-                        d-                )-                initDf-                counts-     in-        case getColumn name df of-            Nothing ->-                throw $ ColumnNotFoundException name "frequencies" (M.keys $ columnIndices df)-            Just ((BoxedColumn (column :: V.Vector a))) -> freqs column-            Just ((OptionalColumn (column :: V.Vector a))) -> freqs column-            Just ((UnboxedColumn (column :: VU.Vector a))) -> freqs column---- | Calculates the mean of a given column as a standalone value.-mean ::-    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,419 +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.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 columnsWithProperties df-  where-    columnsWithProperties = 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 (BinaryOp "geq" (>=) (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 (BinaryOp "leq" (<=) (Col @Double "__rand__") (Lit p))-            & exclude ["__rand__"]-        , withRand-            & filterWhere (BinaryOp "gt" (>) (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-                    ( BinaryOp-                        "geq"-                        (>=)-                        (Col @Double "__rand__")-                        (Lit (fromIntegral n * partitionSize))-                    )-        go (-1) _ = []-        go n d =-            let-                d' = singleFold n d-                d'' =-                    d-                        & filterWhere-                            ( BinaryOp-                                "lt"-                                (<)-                                (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 (Wrap 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, Wrap (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/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 DataFrame.Functions ((.&&), (.<=), (.>), (.||))-import qualified DataFrame.Operations.Core as D-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
@@ -0,0 +1,293 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}++{- |+Module      : DataFrame.Typed+Copyright   : (c) 2024 - 2026 Michael Chavinda+License     : MIT+Maintainer  : mschavinda@gmail.com+Stability   : experimental++A type-safe layer over the @dataframe@ library.++This module provides 'TypedDataFrame', a phantom-typed wrapper around+the untyped 'DataFrame' that tracks column names and types at compile time.+All operations delegate to the untyped core at runtime; the phantom type+is updated at compile time to reflect schema changes.++== Key difference from untyped API: TExpr++All expression-taking operations use 'TExpr' (typed expressions) instead+of raw @Expr@. Column references are validated at compile time:++@+{\-\# LANGUAGE DataKinds, TypeApplications, TypeOperators \#-\}+import qualified DataFrame.Typed as T++type People = '[T.Column \"name\" Text, T.Column \"age\" Int]++main = do+    raw <- D.readCsv \"people.csv\"+    case T.freeze \@People raw of+        Nothing -> putStrLn \"Schema mismatch!\"+        Just df -> do+            let adults = T.filterWhere (T.col \@\"age\" T..>=. T.lit 18) df+            let names  = T.columnAsList \@\"name\" adults  -- :: [Text]+            print names+@++Column references like @T.col \@\"age\"@ are checked at compile time — if the+column doesn't exist or has the wrong type, you get a type error, not a+runtime exception.++== filterAllJust tracks Maybe-stripping++@+df :: TypedDataFrame '[Column \"x\" (Maybe Double), Column \"y\" Int]+T.filterAllJust df :: TypedDataFrame '[Column \"x\" Double, Column \"y\" Int]+@++== Typed aggregation++@+result = T.aggregate+    ( T.as \@\"total\" (T.sum   (T.col \@\"salary\"))+    . T.as \@\"count\" (T.count (T.col \@\"salary\"))+    )+    (T.groupBy \@'[\"dept\"] employees)+@+-}+module DataFrame.Typed (+    -- * Core types+    TypedDataFrame,+    Column,+    TypedGrouped,+    These (..),++    -- * Typed expressions+    TExpr (..),+    col,+    lit,+    ifThenElse,+    lift,+    lift2,+    nullLift,+    nullLift2,++    -- * Same-type comparison operators+    (.==.),+    (./=.),+    (.<.),+    (.<=.),+    (.>=.),+    (.>.),++    -- * Nullable-aware arithmetic operators+    (.+),+    (.-),+    (.*),+    (./),++    -- * Nullable-aware comparison operators (three-valued logic)+    (.==),+    (./=),+    (.<),+    (.<=),+    (.>=),+    (.>),++    -- * Logical operators+    (.&&.),+    (.||.),+    DataFrame.Typed.Expr.not,++    -- * Aggregation expression combinators+    DataFrame.Typed.Expr.sum,+    mean,+    count,+    countAll,+    DataFrame.Typed.Expr.minimum,+    DataFrame.Typed.Expr.maximum,+    collect,+    over,++    -- * Cast / coercion expressions+    castExpr,+    castExprWithDefault,+    castExprEither,+    unsafeCastExpr,+    toDouble,++    -- * Typed sort orders+    TSortOrder (..),+    asc,+    desc,++    -- * Freeze / thaw boundary+    freeze,+    freezeWithError,+    thaw,+    unsafeFreeze,++    -- * Typed column access+    columnAsVector,+    columnAsList,++    -- * 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,++    -- * Set algebra (topos operations)+    union,+    intersect,+    difference,+    symmetricDifference,++    -- * Joins+    innerJoin,+    leftJoin,+    rightJoin,+    fullOuterJoin,++    -- * GroupBy and Aggregation+    groupBy,+    as,+    aggregate,+    aggregateUntyped,++#ifdef WITH_TH+    -- * Template Haskell+    deriveSchema,+#ifdef WITH_CSV_TH+    deriveSchemaFromCsvFile,+    deriveSchemaFromCsvFileWith,+#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,+    Append,+    Reverse,+    StripAllMaybe,+    StripMaybeAt,+    GroupKeyColumns,+    InnerJoinSchema,+    LeftJoinSchema,+    RightJoinSchema,+    FullOuterJoinSchema,+    AssertAbsent,+    AssertAllPresent,+    AssertPresent,++    -- * Constraints+    KnownSchema (..),+    AllKnownSymbol (..),+) where++import Prelude hiding (drop, filter, take)++import DataFrame.Typed.Access (columnAsList, columnAsVector)+import DataFrame.Typed.Aggregate (+    aggregate,+    aggregateUntyped,+    as,+    groupBy,+ )+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+#ifdef WITH_TH+import DataFrame.Typed.TH (+    SchemaOptions (..),+    defaultSchemaOptions,+    deriveSchema,+#ifdef WITH_CSV_TH+    deriveSchemaFromCsvFile,+    deriveSchemaFromCsvFileWith,+#endif+    deriveSchemaFromType,+    deriveSchemaFromTypeWith,+ )+#endif+import DataFrame.Typed.Types (+    Column,+    TSortOrder (..),+    These (..),+    TypedDataFrame,+    TypedGrouped,+ )
+ src/DataFrame/Typed/TH.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE CPP #-}++{- |+Module      : DataFrame.Typed.TH+License     : MIT++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@)++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.+-}+module DataFrame.Typed.TH (+    module DataFrame.Typed.TH.Records,+#ifdef WITH_CSV_TH+    module DataFrame.Typed.TH.CSV,+#endif+) where++import DataFrame.Typed.TH.Records+#ifdef WITH_CSV_TH+import DataFrame.Typed.TH.CSV+#endif
+ tests/Cart.hs view
@@ -0,0 +1,112 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Agreement tests: the Haskell 'buildCartTree' must predict identically to+sklearn @DecisionTreeClassifier(random_state=0, max_depth=4)@ on the shared+folds. The oracle is golden fixtures (per-row test predictions) generated by+@bench/export_cart_fixtures.py@ in a sklearn env. wine/bcw (continuous) assert+exact equality; adult (one-hot, RNG-tie-prone) only reports a match fraction.++Tests SKIP (pass with a notice) when a fixture is absent, so the suite stays+green until the fixtures are generated.+-}+module Cart (tests) where++import Control.Exception (SomeException, try)+import Data.Aeson (FromJSON (..), eitherDecode, withObject, (.:))+import qualified Data.ByteString.Lazy as BL+import qualified Data.Text as T+import qualified Data.Vector as V+import Test.HUnit++import qualified DataFrame as D+import DataFrame.DecisionTree (+    TreeConfig (..),+    buildCartTree,+    defaultTreeConfig,+    predictManyWithTree,+ )+import qualified DataFrame.Operations.Subset as DSub++data Fold = Fold ![Int] ![Int]+instance FromJSON Fold where+    parseJSON = withObject "fold" $ \o -> Fold <$> o .: "train" <*> o .: "test"++newtype Folds = Folds [Fold]+instance FromJSON Folds where+    parseJSON = withObject "folds" $ \o -> Folds <$> o .: "folds"++data Fixture = Fixture ![Int] ![T.Text]+instance FromJSON Fixture where+    parseJSON = withObject "fixture" $ \o -> Fixture <$> o .: "test_index" <*> o .: "test_pred"++-- sklearn cart_d4 params: max_depth 4, min_samples_leaf 1 (min_samples_split is+-- fixed at 2 inside buildCartTree).+cartCfg :: TreeConfig+cartCfg = defaultTreeConfig{maxTreeDepth = 4, minLeafSize = 1}++cartCases :: [(String, Int)]+cartCases =+    [("wine", i) | i <- [0 .. 4]] ++ [("bcw", i) | i <- [0 .. 4]] ++ [("adult", 0)]++tests :: [Test]+tests =+    [ TestLabel ("cart: " ++ n ++ " fold " ++ show i) (TestCase (runCase n i))+    | (n, i) <- cartCases+    ]++readJson :: (FromJSON a) => FilePath -> IO (Either String a)+readJson fp = do+    e <- try (BL.readFile fp) :: IO (Either SomeException BL.ByteString)+    pure $ case e of+        Left _ -> Left "missing"+        Right raw -> eitherDecode raw++runCase :: String -> Int -> IO ()+runCase name i = do+    efx <- readJson ("tests/fixtures/cart/" ++ name ++ "_fold" ++ show i ++ ".json")+    case efx of+        Left "missing" ->+            putStrLn+                ( "  [skip] cart "+                    ++ name+                    ++ " fold "+                    ++ show i+                    ++ ": fixture missing (run bench/export_cart_fixtures.py)"+                )+        Left e -> assertFailure ("fixture parse (" ++ name ++ "): " ++ e)+        Right (Fixture _ predExpected) -> do+            efolds <- readJson ("data/folds/" ++ name ++ ".json")+            case efolds of+                Left e -> assertFailure ("folds parse (" ++ name ++ "): " ++ e)+                Right (Folds fs) -> do+                    df <- D.readCsv ("data/uci/" ++ name ++ "_clean.csv")+                    let Fold trainIdx testIdx = fs !! i+                        trainDf = DSub.selectRows trainIdx df+                        tree = buildCartTree @Int cartCfg "target" trainDf+                        preds =+                            map+                                (T.pack . show)+                                (V.toList (predictManyWithTree tree df (V.fromList testIdx)))+                    -- wine is tie-free ⇒ sklearn is deterministic ⇒ exact match is the bar.+                    -- bcw/adult have equal-gain ties that sklearn breaks with a seeded per-node+                    -- feature permutation (verified: 4/5 bcw folds change with random_state); our+                    -- builder breaks ties deterministically by feature order and is gain-optimal+                    -- (verified bit-identical to an independent deterministic-CART reference), so+                    -- we only report the match fraction there rather than chase sklearn's RNG.+                    if name == "wine"+                        then assertEqual ("cart " ++ name ++ " fold " ++ show i) predExpected preds+                        else do+                            let n = length predExpected+                                m = length (filter id (zipWith (==) predExpected preds))+                            putStrLn+                                ( "  [diagnostic] cart "+                                    ++ name+                                    ++ " fold "+                                    ++ show i+                                    ++ ": "+                                    ++ show m+                                    ++ "/"+                                    ++ show n+                                    ++ " predictions match sklearn(random_state=0) (remainder = sklearn's seeded equal-gain tie-break)"+                                )
+ tests/DecisionTree.hs view
@@ -0,0 +1,1384 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module DecisionTree where++import qualified DataFrame as D+import DataFrame.DecisionTree+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.Expression (Expr (..), eqExpr, getColumns)+import DataFrame.Internal.Interpreter (interpret)+import qualified DataFrame.LinearSolver+import DataFrame.Operators++import Data.Function (on)+import Data.List (maximumBy, sort)+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 Test.HUnit++------------------------------------------------------------------------+-- Shared fixtures+------------------------------------------------------------------------++{- | Build a 'TargetInfo' or fail loudly; the test fixtures always satisfy+'mkTargetInfo', so a 'Nothing' here is a broken test, not a runtime case.+-}+requireTargetInfo :: T.Text -> D.DataFrame -> TargetInfo T.Text+requireTargetInfo target df = case mkTargetInfo @T.Text target df of+    Just ti -> ti+    Nothing -> error ("requireTargetInfo: no target info for " <> T.unpack target)++-- 4 rows: label = ["A","B","A","C"], x = [1.0,2.0,3.0,4.0]+fixtureDF :: D.DataFrame+fixtureDF =+    D.fromNamedColumns+        [ ("label", DI.fromList (["A", "B", "A", "C"] :: [T.Text]))+        , ("x", DI.fromList ([1.0, 2.0, 3.0, 4.0] :: [Double]))+        ]++allIndices :: V.Vector Int+allIndices = V.fromList [0, 1, 2, 3]++leftTree :: Tree T.Text+leftTree = Leaf "A"++rightTree :: Tree T.Text+rightTree = Leaf "B"++-- x <= 2.5: True for idx 0,1 (→ left); False for idx 2,3 (→ right)+splitCond :: Expr Bool+splitCond = F.col @Double "x" .<= F.lit (2.5 :: Double)++-- Pre-computed care points for the full fixture+carePoints3 :: [CarePoint]+carePoints3 =+    identifyCarePoints @T.Text "label" fixtureDF allIndices leftTree rightTree++------------------------------------------------------------------------+-- Unit tests: identifyCarePoints+------------------------------------------------------------------------++carePointsBothWrong :: Test+carePointsBothWrong =+    TestCase $+        assertBool+            "idx 3 (label=C, neither A nor B) should not be a care point"+            (3 `notElem` map cpIndex carePoints3)++carePointsLeftCorrect :: Test+carePointsLeftCorrect = TestCase $ do+    let cp0 = filter ((== 0) . cpIndex) carePoints3+    assertBool "idx 0 should be a care point" (not (null cp0))+    assertEqual+        "idx 0 (label=A matches left Leaf A) should route GoLeft"+        GoLeft+        (cpCorrectDir (head cp0))++carePointsRightCorrect :: Test+carePointsRightCorrect = TestCase $ do+    let cp1 = filter ((== 1) . cpIndex) carePoints3+    assertBool "idx 1 should be a care point" (not (null cp1))+    assertEqual+        "idx 1 (label=B matches right Leaf B) should route GoRight"+        GoRight+        (cpCorrectDir (head cp1))++carePointsMixed :: Test+carePointsMixed = TestCase $ do+    assertEqual "exactly 3 care points" 3 (length carePoints3)+    let idxs = map cpIndex carePoints3+    assertBool "idx 0 present" (0 `elem` idxs)+    assertBool "idx 1 present" (1 `elem` idxs)+    assertBool "idx 2 present" (2 `elem` idxs)+    assertBool "idx 3 absent" (3 `notElem` idxs)++carePointsBothCorrect :: Test+carePointsBothCorrect = TestCase $ do+    let df2 =+            D.fromNamedColumns+                [ ("label", DI.fromList (["A", "A"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0] :: [Double]))+                ]+        cps =+            identifyCarePoints @T.Text+                "label"+                df2+                (V.fromList [0, 1])+                (Leaf "A")+                (Leaf "A")+    assertEqual "no care points when both subtrees agree" 0 (length cps)++------------------------------------------------------------------------+-- Unit tests: majorityValueFromIndices+------------------------------------------------------------------------++majorityVoteTest :: Test+majorityVoteTest = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["cat", "dog", "cat", "cat"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0, 3.0, 4.0] :: [Double]))+                ]+    assertEqual+        "majority is cat (3 votes)"+        "cat"+        (majorityValueFromIndices @T.Text "label" df (V.fromList [0, 1, 2, 3]))++majorityVoteSubset :: Test+majorityVoteSubset = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["cat", "dog", "cat", "cat"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0, 3.0, 4.0] :: [Double]))+                ]+        -- indices [0,1,3]: cat×2, dog×1 → "cat" wins clearly+        result = majorityValueFromIndices @T.Text "label" df (V.fromList [0, 1, 3])+    assertEqual "majority from subset [0,1,3] is cat" "cat" result++------------------------------------------------------------------------+-- Unit tests: computeTreeLoss+------------------------------------------------------------------------++computeLossZero :: Test+computeLossZero = TestCase $ do+    -- target = ["A","A","B","B"]: perfect split on x <= 2.5+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["A", "A", "B", "B"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0, 3.0, 4.0] :: [Double]))+                ]+        stump = Branch splitCond (Leaf "A") (Leaf "B") :: Tree T.Text+        loss = computeTreeLoss @T.Text "label" df (V.fromList [0, 1, 2, 3]) stump+    assertEqual "perfect stump has zero loss" 0.0 loss++computeLossHalf :: Test+computeLossHalf = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["A", "A", "B", "B"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0, 3.0, 4.0] :: [Double]))+                ]+        constTree = Leaf "A" :: Tree T.Text+        loss = computeTreeLoss @T.Text "label" df (V.fromList [0, 1, 2, 3]) constTree+    assertEqual "constant leaf misclassifies half of balanced data" 0.5 loss++------------------------------------------------------------------------+-- Unit tests: partitionIndices+------------------------------------------------------------------------++partitionDisjoint :: Test+partitionDisjoint = TestCase $ do+    let (lft, rgt) = partitionIndices splitCond fixtureDF allIndices+        leftSet = V.toList lft+        rightSet = V.toList rgt+        intersection = filter (`elem` rightSet) leftSet+    assertEqual "left and right partitions are disjoint" [] intersection++partitionUnion :: Test+partitionUnion = TestCase $ do+    let (lft, rgt) = partitionIndices splitCond fixtureDF allIndices+        combined = sort (V.toList lft ++ V.toList rgt)+    assertEqual+        "union of partitions equals the original index set"+        [0, 1, 2, 3]+        combined++------------------------------------------------------------------------+-- Unit tests: countCarePointErrors+------------------------------------------------------------------------++countErrorsAllCorrect :: Test+countErrorsAllCorrect = TestCase $ do+    -- x <= 1.5: idx 0 goes left (True), idx 1 goes right (False)+    -- CarePoint 0 GoLeft  → goesLeft=True,  shouldGoLeft=True  → correct+    -- CarePoint 1 GoRight → goesLeft=False, shouldGoLeft=False → correct+    let cps = [CarePoint 0 GoLeft, CarePoint 1 GoRight]+        cond = F.col @Double "x" .<= F.lit (1.5 :: Double)+        errs = countCarePointErrors cond fixtureDF cps+    assertEqual "condition routes all care points correctly" 0 errs++countErrorsAllWrong :: Test+countErrorsAllWrong = TestCase $ do+    -- x > 1.5: idx 0 goes right (False), idx 1 goes left (True) — both wrong+    -- CarePoint 0 GoLeft  → goesLeft=False, shouldGoLeft=True  → wrong+    -- CarePoint 1 GoRight → goesLeft=True,  shouldGoLeft=False → wrong+    let cps = [CarePoint 0 GoLeft, CarePoint 1 GoRight]+        cond = F.col @Double "x" .> F.lit (1.5 :: Double)+        errs = countCarePointErrors cond fixtureDF cps+    assertEqual "reversed condition misroutes all care points" 2 errs++------------------------------------------------------------------------+-- Unit tests: predictWithTree+------------------------------------------------------------------------++predictLeaf :: Test+predictLeaf =+    TestCase $+        assertEqual+            "leaf prediction ignores row index"+            "Z"+            (predictWithTree @T.Text "label" fixtureDF 0 (Leaf "Z"))++predictBranch :: Test+predictBranch = TestCase $ do+    let stump = Branch splitCond (Leaf "A") (Leaf "B") :: Tree T.Text+    assertEqual+        "idx 0 (x=1.0 <= 2.5) routes left -> A"+        "A"+        (predictWithTree @T.Text "label" fixtureDF 0 stump)+    assertEqual+        "idx 3 (x=4.0 > 2.5) routes right -> B"+        "B"+        (predictWithTree @T.Text "label" fixtureDF 3 stump)++------------------------------------------------------------------------+-- Integration tests+------------------------------------------------------------------------++-- 20-row, linearly separable: x in [1..10] -> "pos", x in [11..20] -> "neg"+sepDF :: D.DataFrame+sepDF =+    let xs = map fromIntegral [1 .. 20 :: Int] :: [Double]+        labels = map (\x -> if x <= 10.0 then "pos" else "neg") xs :: [T.Text]+     in D.fromNamedColumns+            [ ("label", DI.fromList labels)+            , ("x", DI.fromList xs)+            ]++-- Candidate conditions that bracket the decision boundary+sepConds :: [Expr Bool]+sepConds =+    [ F.col @Double "x" .<= F.lit (10.5 :: Double)+    , F.col @Double "x" .> F.lit (10.5 :: Double)+    ]++testCfg :: TreeConfig+testCfg =+    defaultTreeConfig+        { taoIterations = 5+        , expressionPairs = 4+        , minLeafSize = 1+        }++-- Initial tree deliberately wrong: routes "pos" rows to the "neg" leaf+wrongStump :: Tree T.Text+wrongStump =+    Branch+        (F.col @Double "x" .> F.lit (10.5 :: Double))+        (Leaf "pos")+        (Leaf "neg")++taoNoDegradation :: Test+taoNoDegradation = TestCase $ do+    let indices = V.enumFromN 0 20+        initialLoss = computeTreeLoss @T.Text "label" sepDF indices wrongStump+        optimized =+            taoOptimize @T.Text testCfg "label" sepConds sepDF indices wrongStump+        finalLoss = computeTreeLoss @T.Text "label" sepDF indices optimized+    assertBool+        "taoOptimize must not increase loss"+        (finalLoss <= initialLoss + 1e-9)++taoMonotone :: Test+taoMonotone = TestCase $ do+    let indices = V.enumFromN 0 20+        initLoss = computeTreeLoss @T.Text "label" sepDF indices wrongStump+        stepTree = taoIteration @T.Text testCfg "label" sepConds sepDF indices+        -- Track (tree, loss) pairs: take 6 snapshots (initial + 5 steps)+        step (tree, _) =+            let tree' = stepTree tree+             in (tree', computeTreeLoss @T.Text "label" sepDF indices tree')+        snapshots = take 6 $ iterate step (wrongStump, initLoss)+        losses = map snd snapshots+        pairs = zip losses (tail losses)+    assertBool+        "loss must be non-increasing across taoIteration steps"+        (all (\(a, b) -> b <= a + 1e-9) pairs)++taoConvergesPureLabels :: Test+taoConvergesPureLabels = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (replicate 10 ("A" :: T.Text)))+                , ("x", DI.fromList ([1.0 .. 10.0] :: [Double]))+                ]+        indices = V.enumFromN 0 10+        initTree = Leaf "A" :: Tree T.Text+        initLoss = computeTreeLoss @T.Text "label" df indices initTree+        result =+            taoOptimize @T.Text testCfg "label" sepConds df indices initTree+        finalLoss = computeTreeLoss @T.Text "label" df indices result+    assertEqual "pure-label initial loss must be zero" 0.0 initLoss+    assertEqual "pure-label final loss must still be zero" 0.0 finalLoss++taoDeadBranchNoCrash :: Test+taoDeadBranchNoCrash = TestCase $ do+    -- Threshold below all x values: x <= 0.5 is False for every row+    -- → all indices route to the right child; left partition is always empty+    let badCond = F.col @Double "x" .<= F.lit (0.5 :: Double)+        indices = V.enumFromN 0 20+        initTree = Branch badCond (Leaf "pos") (Leaf "neg") :: Tree T.Text+        result =+            taoOptimize @T.Text testCfg "label" [badCond] sepDF indices initTree+        finalLoss = computeTreeLoss @T.Text "label" sepDF indices result+    assertBool+        "dead-branch tree must produce a valid loss in [0,1]"+        (finalLoss >= 0.0 && finalLoss <= 1.0)++------------------------------------------------------------------------+-- Shared fixtures: 4x4 grid+------------------------------------------------------------------------++gridPairs :: [(Double, Double)]+gridPairs = [(x, y) | y <- [1 .. 4], x <- [1 .. 4]]++gridBaseDF :: D.DataFrame+gridBaseDF =+    D.fromNamedColumns+        [ ("x", DI.fromList (map fst gridPairs))+        , ("y", DI.fromList (map snd gridPairs))+        ]++------------------------------------------------------------------------+-- Oblique recovery tests+------------------------------------------------------------------------++taoRecoversSingleObliqueDerived :: Test+taoRecoversSingleObliqueDerived = TestCase $ do+    let labelExpr =+            F.ifThenElse+                ((F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double))+                (F.lit ("pos" :: T.Text))+                (F.lit ("neg" :: T.Text))+        df = D.derive @T.Text "label" labelExpr gridBaseDF+        indices = V.enumFromN 0 16+        initTree =+            Branch+                (F.col @Double "x" .<= F.lit (2.5 :: Double))+                (Leaf "pos")+                (Leaf "neg") ::+                Tree T.Text+        conds =+            [ (F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double)+            , (F.col @Double "x" + F.col @Double "y") .> F.lit (4.5 :: Double)+            ]+        cfg = defaultTreeConfig{taoIterations = 5, expressionPairs = 4, minLeafSize = 1}+        result = taoOptimize @T.Text cfg "label" conds df indices initTree+        finalLoss = computeTreeLoss @T.Text "label" df indices result+    assertEqual+        "TAO recovers single oblique (x+y) split with zero loss"+        0.0+        finalLoss++taoRecoversNestedObliqueDerived :: Test+taoRecoversNestedObliqueDerived = TestCase $ do+    let labelExpr =+            F.ifThenElse+                ((F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double))+                (F.lit ("low" :: T.Text))+                ( F.ifThenElse+                    ((F.col @Double "x" - F.col @Double "y") .<= F.lit (0.5 :: Double))+                    (F.lit "mid")+                    (F.lit "high")+                )+        df = D.derive @T.Text "label" labelExpr gridBaseDF+        indices = V.enumFromN 0 16+        initTree =+            Branch+                (F.col @Double "x" .<= F.lit (1.5 :: Double))+                (Leaf "low")+                ( Branch+                    (F.col @Double "y" .<= F.lit (3.5 :: Double))+                    (Leaf "mid")+                    (Leaf "high")+                ) ::+                Tree T.Text+        conds =+            [ (F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double)+            , (F.col @Double "x" + F.col @Double "y") .> F.lit (4.5 :: Double)+            , (F.col @Double "x" - F.col @Double "y") .<= F.lit (0.5 :: Double)+            , (F.col @Double "x" - F.col @Double "y") .> F.lit (0.5 :: Double)+            ]+        cfg = defaultTreeConfig{taoIterations = 5, expressionPairs = 4, minLeafSize = 1}+        result = taoOptimize @T.Text cfg "label" conds df indices initTree+        finalLoss = computeTreeLoss @T.Text "label" df indices result+    assertEqual+        "TAO recovers nested oblique (x+y)/(x-y) tree with zero loss"+        0.0+        finalLoss++-- Shared setup for C2 (a) and (b): axis-aligned pool only, oblique label.+obliqueAxisAlignedFixture ::+    (D.DataFrame, V.Vector Int, [Expr Bool], Tree T.Text)+obliqueAxisAlignedFixture =+    let labelExpr =+            F.ifThenElse+                ((F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double))+                (F.lit ("pos" :: T.Text))+                (F.lit ("neg" :: T.Text))+        df = D.derive @T.Text "label" labelExpr gridBaseDF+        indices = V.enumFromN 0 16+        axisConds =+            [F.col @Double "x" .<= F.lit (t :: Double) | t <- [1.5, 2.5, 3.5]]+                ++ [F.col @Double "y" .<= F.lit (t :: Double) | t <- [1.5, 2.5, 3.5]]+        initTree =+            Branch+                (F.col @Double "x" .<= F.lit (2.5 :: Double))+                (Leaf "pos")+                (Leaf "neg") ::+                Tree T.Text+     in (df, indices, axisConds, initTree)++-- C2 (a): with the linear solver OFF, axis-aligned pool cannot recover the+-- oblique decision boundary. Preserves the original guarantee of the test.+taoAxisAlignedInsufficientForObliqueDiscreteOnly :: Test+taoAxisAlignedInsufficientForObliqueDiscreteOnly = TestCase $ do+    let (df, indices, axisConds, initTree) = obliqueAxisAlignedFixture+        cfg =+            defaultTreeConfig+                { taoIterations = 10+                , expressionPairs = 6+                , minLeafSize = 1+                , useLinearSolver = False+                }+        result = taoOptimize @T.Text cfg "label" axisConds df indices initTree+        finalLoss = computeTreeLoss @T.Text "label" df indices result+    assertBool+        "axis-aligned stump cannot recover oblique label without linear solver (loss > 0.1)"+        (finalLoss > 0.1)++-- C2 (b): with the linear solver ON, the L1-LR fit discovers the oblique+-- (x + y) hyperplane even though only axis-aligned conditions are in the+-- candidate pool. This is the test that licenses calling the implementation+-- canonical TAO.+taoLinearRecoversObliqueFromAxisAlignedPool :: Test+taoLinearRecoversObliqueFromAxisAlignedPool = TestCase $ do+    let (df, indices, axisConds, initTree) = obliqueAxisAlignedFixture+        cfg =+            defaultTreeConfig+                { taoIterations = 10+                , expressionPairs = 6+                , minLeafSize = 1+                , useLinearSolver = True+                , minCarePointsForLinear = 2+                }+        result = taoOptimize @T.Text cfg "label" axisConds df indices initTree+        finalLoss = computeTreeLoss @T.Text "label" df indices result+    assertEqual+        "linear solver recovers oblique split from axis-aligned-only pool"+        0.0+        finalLoss++------------------------------------------------------------------------+-- Nullable numeric feature tests+------------------------------------------------------------------------++-- Cleanly separable: Just 1..6 -> "pos", Just 7..12 -> "neg", no nulls.+-- Uses OptionalColumn directly to exercise the new nullable numeric path.+nullableSepDF :: D.DataFrame+nullableSepDF =+    D.fromNamedColumns+        [ ("label", DI.fromList (replicate 6 "pos" ++ replicate 6 "neg" :: [T.Text]))+        ,+            ( "x"+            , DI.fromVector+                ( V.fromList $+                    map (Just . fromIntegral) ([1 .. 6] :: [Int])+                        ++ map (Just . fromIntegral) ([7 .. 12] :: [Int]) ::+                    V.Vector (Maybe Double)+                )+            )+        ]++-- DF with genuine nulls interspersed.+nullsMixedDF :: D.DataFrame+nullsMixedDF =+    D.fromNamedColumns+        [ ("label", DI.fromList (["pos", "pos", "pos", "neg", "neg", "neg"] :: [T.Text]))+        ,+            ( "x"+            , DI.fromVector+                ( V.fromList+                    [Just 1.0, Nothing, Just 3.0, Just 7.0, Nothing, Just 9.0] ::+                    V.Vector (Maybe Double)+                )+            )+        ]++-- numericCols picks up DI.fromVector (Maybe Double) as NMaybeDouble.+numericColsNullableDoubleTest :: Test+numericColsNullableDoubleTest = TestCase $ do+    let exprs = numericCols nullableSepDF+        hasMD = any (\case NMaybeDouble _ -> True; _ -> False) exprs+    assertBool+        "numericCols finds NMaybeDouble for DI.fromVector (Maybe Double)"+        hasMD++-- numericCols picks up DI.fromVector (Maybe Int) as NMaybeDouble (via whenPresent).+numericColsNullableIntTest :: Test+numericColsNullableIntTest = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["pos", "neg"] :: [T.Text]))+                ,+                    ( "n"+                    , DI.fromVector (V.fromList [Just (1 :: Int), Just 2] :: V.Vector (Maybe Int))+                    )+                ]+        hasMD = any (\case NMaybeDouble _ -> True; _ -> False) (numericCols df)+    assertBool "numericCols finds NMaybeDouble for DI.fromVector (Maybe Int)" hasMD++-- generateNumericConds is non-empty for a DF with an DI.fromVector (Maybe Double).+numericCondsNullableNonEmptyTest :: Test+numericCondsNullableNonEmptyTest =+    TestCase $+        assertBool+            "generateNumericConds non-empty for DI.fromVector (Maybe Double)"+            (not (null (generateNumericConds defaultTreeConfig nullableSepDF)))++-- Null values evaluate to False for threshold conditions (null rows route right).+nullValueRoutesFalseTest :: Test+nullValueRoutesFalseTest = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["A", "B"] :: [T.Text]))+                ,+                    ( "x"+                    , DI.fromVector+                        (V.fromList [Nothing, Just (5.0 :: Double)] :: V.Vector (Maybe Double))+                    )+                ]+        -- Nothing <= 6.0 = Nothing  -> fromMaybe False = False -> right+        -- Just 5.0 <= 6.0 = Just True -> fromMaybe False = True  -> left+        cond = F.fromMaybe False (F.col @(Maybe Double) "x" .<= F.lit (6.0 :: Double))+        (lft, rgt) = partitionIndices cond df (V.fromList [0, 1])+    assertBool "null row (idx 0) routes to right (false) partition" (0 `V.elem` rgt)+    assertBool "Just 5.0 <= 6.0 routes to left (true) partition" (1 `V.elem` lft)++-- fitDecisionTree with an OptionalColumn nullable feature achieves zero loss+-- on cleanly separable data (no actual nulls in the column).+nullableFitZeroLossTest :: Test+nullableFitZeroLossTest = TestCase $ do+    let cfg = defaultTreeConfig{taoIterations = 5, expressionPairs = 4, minLeafSize = 1}+        featureDf = D.exclude ["label"] nullableSepDF+        conds = generateNumericConds cfg featureDf+        initTree = buildCartTree @T.Text cfg "label" nullableSepDF+        indices = V.enumFromN 0 12+        result = taoOptimize @T.Text cfg "label" conds nullableSepDF indices initTree+        loss = computeTreeLoss @T.Text "label" nullableSepDF indices result+    assertEqual "zero loss on cleanly separable OptionalColumn data" 0.0 loss++-- fitDecisionTree with genuine nulls: loss is a valid probability and no crash.+nullableFitWithNullsNoCrashTest :: Test+nullableFitWithNullsNoCrashTest = TestCase $ do+    let cfg = defaultTreeConfig{taoIterations = 3, expressionPairs = 4, minLeafSize = 1}+        featureDf = D.exclude ["label"] nullsMixedDF+        conds = generateNumericConds cfg featureDf+        initTree = buildCartTree @T.Text cfg "label" nullsMixedDF+        indices = V.enumFromN 0 6+        result = taoOptimize @T.Text cfg "label" conds nullsMixedDF indices initTree+        loss = computeTreeLoss @T.Text "label" nullsMixedDF indices result+    assertBool+        "loss is in [0,1] with null values present"+        (loss >= 0.0 && loss <= 1.0)++-- numericExprsWithTerms produces cross-column combinations when one col is+-- DI.fromVector (Maybe Double) and another is a plain UnboxedColumn Double.+numericExprsWithTermsMixedTest :: Test+numericExprsWithTermsMixedTest = TestCase $ do+    let df =+            D.fromNamedColumns+                [+                    ( "x"+                    , DI.fromVector+                        (V.fromList [Just 1.0, Just 2.0, Just 3.0] :: V.Vector (Maybe Double))+                    )+                , ("y", DI.fromList ([4.0, 5.0, 6.0] :: [Double]))+                ]+        cfg = defaultSynthConfig{maxExprDepth = 2, enableArithOps = True}+        exprs = numericExprsWithTerms cfg df+    assertBool+        "more than 2 expressions: base cols + combinations"+        (length exprs > 2)+    assertBool+        "combined exprs include NMaybeDouble (nullable arithmetic)"+        (any (\case NMaybeDouble _ -> True; _ -> False) exprs)++------------------------------------------------------------------------+-- Probability tree tests+------------------------------------------------------------------------++-- probsFromIndices: counts correct on a 3-row slice+probsFromIndicesBasic :: Test+probsFromIndicesBasic = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["A", "A", "B"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0, 3.0] :: [Double]))+                ]+        probs = probsFromIndices @T.Text "label" df (V.fromList [0, 1, 2])+    assertBool "A prob ≈ 2/3" (abs (probs M.! "A" - 2 / 3) < 1e-9)+    assertBool "B prob ≈ 1/3" (abs (probs M.! "B" - 1 / 3) < 1e-9)++-- probsFromIndices: only a subset of rows counted+probsFromIndicesSubset :: Test+probsFromIndicesSubset = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["A", "A", "B", "B"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0, 3.0, 4.0] :: [Double]))+                ]+        probs = probsFromIndices @T.Text "label" df (V.fromList [0, 1])+    assertEqual "only rows 0,1 → A:1.0" (M.fromList [("A", 1.0)]) probs++-- probsFromIndices: single class → probability 1.0+probsFromIndicesSingleClass :: Test+probsFromIndicesSingleClass = TestCase $ do+    let probs = probsFromIndices @T.Text "label" fixtureDF (V.fromList [0, 2])+    assertEqual "rows 0,2 both A → A:1.0" (M.fromList [("A", 1.0)]) probs++-- buildProbTree: Leaf preserves distribution+buildProbTreeLeaf :: Test+buildProbTreeLeaf = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["A", "A", "A"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0, 3.0] :: [Double]))+                ]+        pt = buildProbTree @T.Text (Leaf "A") "label" df (V.fromList [0, 1, 2])+    case pt of+        Leaf m -> assertEqual "pure-A leaf → {A:1.0}" (M.fromList [("A", 1.0)]) m+        _ -> assertFailure "expected Leaf"++-- buildProbTree: Branch distributes rows to left/right leaves correctly+buildProbTreeBranch :: Test+buildProbTreeBranch = TestCase $ do+    -- splitCond: x <= 2.5 → idx 0,1 go left; idx 2,3 go right+    -- left  (idx 0,1): labels ["A","B"] → {A:0.5, B:0.5}+    -- right (idx 2,3): labels ["A","C"] → {A:0.5, C:0.5}+    let stump = Branch splitCond (Leaf "A") (Leaf "B") :: Tree T.Text+        pt = buildProbTree @T.Text stump "label" fixtureDF allIndices+    case pt of+        Branch _ (Leaf lm) (Leaf rm) -> do+            assertBool "left leaf has A:0.5" (abs (M.findWithDefault 0 "A" lm - 0.5) < 1e-9)+            assertBool "left leaf has B:0.5" (abs (M.findWithDefault 0 "B" lm - 0.5) < 1e-9)+            assertBool+                "right leaf has A:0.5"+                (abs (M.findWithDefault 0 "A" rm - 0.5) < 1e-9)+            assertBool+                "right leaf has C:0.5"+                (abs (M.findWithDefault 0 "C" rm - 0.5) < 1e-9)+        _ -> assertFailure "expected Branch with two Leaves"++-- probExprs: leaf tree produces Lit values+probExprsLeaf :: Test+probExprsLeaf = TestCase $ do+    let pt = Leaf (M.fromList [("A", 0.75), ("B", 0.25)]) :: ProbTree T.Text+        pe = probExprs pt+    assertBool "A expr is Lit 0.75" (eqExpr (Lit 0.75) (pe M.! "A"))+    assertBool "B expr is Lit 0.25" (eqExpr (Lit 0.25) (pe M.! "B"))++-- probExprs: class absent from one leaf gets Lit 0.0 on that side+probExprsMissingClass :: Test+probExprsMissingClass = TestCase $ do+    let pt =+            Branch+                splitCond+                (Leaf (M.fromList [("A", 1.0)]))+                (Leaf (M.fromList [("B", 1.0)])) ::+                ProbTree T.Text+        pe = probExprs pt+    assertBool+        "A expr: If cond (Lit 1.0) (Lit 0.0)"+        (eqExpr (F.ifThenElse splitCond (Lit 1.0) (Lit 0.0)) (pe M.! "A"))+    assertBool+        "B expr: If cond (Lit 0.0) (Lit 1.0)"+        (eqExpr (F.ifThenElse splitCond (Lit 0.0) (Lit 1.0)) (pe M.! "B"))++-- probExprs: keys equal all classes that appear across any leaf+probExprsAllClasses :: Test+probExprsAllClasses = TestCase $ do+    let pt =+            Branch+                splitCond+                (Leaf (M.fromList [("A", 1.0)]))+                (Leaf (M.fromList [("B", 0.6), ("C", 0.4)])) ::+                ProbTree T.Text+        pe = probExprs pt+    assertEqual "three classes in result" (sort ["A", "B", "C"]) (sort (M.keys pe))++-- Probabilities sum to 1.0 at every row after applying probExprs+probsSumToOne :: Test+probsSumToOne = TestCase $ do+    let stump = Branch splitCond (Leaf "A") (Leaf "B") :: Tree T.Text+        pt = buildProbTree @T.Text stump "label" fixtureDF allIndices+        pe = probExprs pt+        sumExpr = foldl1 (.+) (M.elems pe)+    case interpret @Double fixtureDF sumExpr of+        Left e -> assertFailure (show e)+        Right (DI.TColumn sumCol) ->+            case DI.toVector @Double sumCol of+                Left e2 -> assertFailure (show e2)+                Right vals ->+                    mapM_+                        (\v -> assertBool ("sum ≈ 1.0, got " ++ show v) (abs (v - 1.0) < 1e-9))+                        (V.toList vals)++-- argmax of probExprs agrees with fitDecisionTree on sepDF+probArgmaxMatchesClassifier :: Test+probArgmaxMatchesClassifier = TestCase $ do+    let cfg = defaultTreeConfig{taoIterations = 5, expressionPairs = 4, minLeafSize = 1}+        hardExpr = fitDecisionTree @T.Text cfg (Col "label") sepDF+        pe = fitProbTree @T.Text cfg (Col "label") sepDF+        indices = [0 .. D.nRows sepDF - 1]+    case interpret @T.Text sepDF hardExpr of+        Left e -> assertFailure (show e)+        Right (DI.TColumn hardCol) ->+            case DI.toVector @T.Text hardCol of+                Left e2 -> assertFailure (show e2)+                Right hardVals -> do+                    probCols <-+                        mapM+                            ( \(cls, expr) -> case interpret @Double sepDF expr of+                                Left e3 -> assertFailure (show e3) >> return (cls, V.empty)+                                Right (DI.TColumn col2) -> case DI.toVector @Double col2 of+                                    Left e4 -> assertFailure (show e4) >> return (cls, V.empty)+                                    Right v -> return (cls, v)+                            )+                            (M.toList pe)+                    mapM_+                        ( \i ->+                            let argmax = fst $ maximumBy (compare `on` (V.! i) . snd) probCols+                                hard = hardVals V.! i+                             in assertEqual ("row " ++ show i) hard argmax+                        )+                        indices++------------------------------------------------------------------------+-- C4-C9 / D-series: linear solver integration tests+------------------------------------------------------------------------++-- C4: Nested oblique recovery without supplying any oblique hints.+-- The label is determined by two oblique boundaries: (x+y <= 4.5) and+-- (x-y <= 0.5). Only axis-aligned thresholds are in the candidate pool.+-- With the linear solver, both oblique splits should be learned and the+-- tree should reach zero loss.+taoRecoversNestedObliqueWithoutHint :: Test+taoRecoversNestedObliqueWithoutHint = TestCase $ do+    let labelExpr =+            F.ifThenElse+                ((F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double))+                (F.lit ("low" :: T.Text))+                ( F.ifThenElse+                    ((F.col @Double "x" - F.col @Double "y") .<= F.lit (0.5 :: Double))+                    (F.lit "mid")+                    (F.lit "high")+                )+        df = D.derive @T.Text "label" labelExpr gridBaseDF+        indices = V.enumFromN 0 16+        initTree =+            Branch+                (F.col @Double "x" .<= F.lit (1.5 :: Double))+                (Leaf "low")+                ( Branch+                    (F.col @Double "y" .<= F.lit (3.5 :: Double))+                    (Leaf "mid")+                    (Leaf "high")+                ) ::+                Tree T.Text+        axisOnlyConds =+            [F.col @Double "x" .<= F.lit (t :: Double) | t <- [1.5, 2.5, 3.5]]+                ++ [F.col @Double "y" .<= F.lit (t :: Double) | t <- [1.5, 2.5, 3.5]]+        cfg =+            defaultTreeConfig+                { taoIterations = 20+                , expressionPairs = 6+                , minLeafSize = 1+                , useLinearSolver = True+                , minCarePointsForLinear = 2+                }+        result = taoOptimize @T.Text cfg "label" axisOnlyConds df indices initTree+        finalLoss = computeTreeLoss @T.Text "label" df indices result+    assertEqual+        "linear solver recovers nested oblique tree from axis-aligned-only pool"+        0.0+        finalLoss++-- C5: Monotone loss across iterations with the linear solver enabled.+-- Resolves Issue 1 from the prior plan (currentCond included in the+-- competition pool).+taoMonotoneWithLinear :: Test+taoMonotoneWithLinear = TestCase $ do+    let indices = V.enumFromN 0 20+        cfg = defaultTreeConfig{taoIterations = 5, expressionPairs = 4, minLeafSize = 1}+        initLoss = computeTreeLoss @T.Text "label" sepDF indices wrongStump+        stepTree = taoIteration @T.Text cfg "label" sepConds sepDF indices+        step (tree, _) =+            let tree' = stepTree tree+             in (tree', computeTreeLoss @T.Text "label" sepDF indices tree')+        snapshots = take 6 $ iterate step (wrongStump, initLoss)+        losses = map snd snapshots+        pairs = zip losses (tail losses)+    assertBool+        ("loss must be non-increasing across iterations (got " ++ show losses ++ ")")+        (all (\(a, b) -> b <= a + 1e-9) pairs)++-- C6: When the discrete pool contains an exact-zero-error split (axis-aligned+-- works perfectly), the competition picks the simpler discrete candidate+-- rather than a similarly-good but more complex linear one.+taoLinearVsDiscreteCompetition :: Test+taoLinearVsDiscreteCompetition = TestCase $ do+    -- sepDF is axis-aligned-separable by x <= 10.5. The discrete pool+    -- sepConds contains this exact condition. Linear solver may also+    -- produce a hyperplane that works, but the discrete one has smaller+    -- eSize, so the tie-breaker should pick it.+    let indices = V.enumFromN 0 20+        cfg =+            defaultTreeConfig+                { taoIterations = 5+                , expressionPairs = 4+                , minLeafSize = 1+                , useLinearSolver = True+                , minCarePointsForLinear = 2+                }+        result = taoOptimize @T.Text cfg "label" sepConds sepDF indices wrongStump+        finalLoss = computeTreeLoss @T.Text "label" sepDF indices result+    assertEqual+        "axis-aligned separable data should fit to zero loss"+        0.0+        finalLoss++-- C8: Linear solver respects the L1 penalty and produces sparse hyperplanes+-- on data where only some features are informative.+taoLinearProducesSparsity :: Test+taoLinearProducesSparsity = TestCase $ do+    -- 50 rows, 4 features. label depends only on (a + b). c and d are noise.+    -- With sufficient L1 strength, the chosen split should mention only a and b.+    let n = 50 :: Int+        xs = [fromIntegral i / 10 - 2.5 :: Double | i <- [0 .. n - 1]]+        avals = xs+        bs = map (* 0.7) xs+        -- noise: take xs and shift them so they don't correlate with a+b+        cs = [fromIntegral ((i * 7) `mod` 11) / 5 - 1 :: Double | i <- [0 .. n - 1]]+        ds = [fromIntegral ((i * 13) `mod` 7) / 3 - 1 :: Double | i <- [0 .. n - 1]]+        labels =+            [ if (avals !! i) + (bs !! i) > 0 then "pos" else "neg" :: T.Text+            | i <- [0 .. n - 1]+            ]+        df =+            D.fromNamedColumns+                [ ("label", DI.fromList labels)+                , ("a", DI.fromList avals)+                , ("b", DI.fromList bs)+                , ("c", DI.fromList cs)+                , ("d", DI.fromList ds)+                ]+        cfg =+            defaultTreeConfig+                { maxTreeDepth = 1+                , taoIterations = 10+                , minLeafSize = 1+                , useLinearSolver = True+                , minCarePointsForLinear = 2+                , linearSolverConfig =+                    (linearSolverConfig defaultTreeConfig)+                        { DataFrame.LinearSolver.scL1Lambda = 0.05+                        }+                }+        result = fitDecisionTree @T.Text cfg (Col "label") df+        rootCols = getColumns result+    -- Hard fail only if NONE of a/b show up — that would mean the model+    -- is ignoring the signal. We expect at most 4 columns; the H3 target+    -- is that fewer than 4 (some noise columns dropped) -- but the test+    -- only asserts the signal columns appear.+    assertBool+        ( "informative columns 'a' or 'b' must appear in the fitted Expr (got "+            ++ show rootCols+            ++ ")"+        )+        ("a" `elem` rootCols || "b" `elem` rootCols)++-- C9: Determinism — same training data produces an equal (eqExpr) tree.+taoLinearDeterministic :: Test+taoLinearDeterministic = TestCase $ do+    let cfg =+            defaultTreeConfig+                { taoIterations = 5+                , expressionPairs = 4+                , minLeafSize = 1+                , useLinearSolver = True+                , minCarePointsForLinear = 2+                }+        r1 = fitDecisionTree @T.Text cfg (Col "label") sepDF+        r2 = fitDecisionTree @T.Text cfg (Col "label") sepDF+    assertBool "fitDecisionTree is deterministic on the same input" (eqExpr r1 r2)++-- D1: One care point — solver must not crash; integration should fall back+-- gracefully (via minCarePointsForLinear) and rely on the discrete path.+taoLinearTinyCareSet :: Test+taoLinearTinyCareSet = TestCase $ do+    -- Use the toy sepDF, but force minCarePointsForLinear = 100 so the+    -- linear path is always skipped. The result should match the+    -- linear-off baseline.+    let cfg =+            defaultTreeConfig+                { taoIterations = 5+                , expressionPairs = 4+                , minLeafSize = 1+                , useLinearSolver = True+                , minCarePointsForLinear = 100+                }+        result = fitDecisionTree @T.Text cfg (Col "label") sepDF+        -- Sanity: the tree should still classify correctly.+        cfgOff = cfg{useLinearSolver = False}+        resultOff = fitDecisionTree @T.Text cfgOff (Col "label") sepDF+    assertBool+        "skipping linear solver yields same expression as linear-off baseline"+        (eqExpr result resultOff)++------------------------------------------------------------------------+-- Categorical-condition generator tests (Phase 1-2 of the plan)+------------------------------------------------------------------------++-- A binary-target DataFrame with a 5-level Text column whose levels have+-- monotonically-increasing positive rates. Breiman's algorithm should+-- enumerate the 4 contiguous-prefix splits in that exact rate order.+breimanBinaryDF :: D.DataFrame+breimanBinaryDF =+    let n = 100 :: Int+        -- Levels chosen so positive rates after Laplace are:+        --   a: 0/n+1 / 2+n+2  → very low+        --   b: 0.25+        --   c: 0.5+        --   d: 0.75+        --   e: ~1.0+        mkLabel "a" = "neg"+        mkLabel "b" = "neg"+        mkLabel "c" = "pos"+        mkLabel "d" = "pos"+        mkLabel "e" = "pos"+        mkLabel _ = "neg"+        levels = cycle ["a", "b", "c", "d", "e"]+        feats = take n levels+        labs = map mkLabel feats+     in D.fromUnnamedColumns+            [ DI.fromList (map T.pack feats :: [T.Text])+            , DI.fromList (map T.pack labs :: [T.Text])+            ]+            |> D.rename "0" "feat"+            |> D.rename "1" "label"++testCategoricalBreimanBinary :: Test+testCategoricalBreimanBinary = TestCase $ do+    let ti = requireTargetInfo "label" breimanBinaryDF+        conds =+            discreteConditions @T.Text+                ti+                defaultTreeConfig+                (D.exclude ["label"] breimanBinaryDF)+        feat = "feat"+        -- Filter only conditions over "feat" (cross-column equality could+        -- mix in if there were other categoricals; here there aren't).+        feats = filter (\c -> feat `elem` getColumns c) conds+    -- 5 levels → 4 prefixes+    assertEqual "Breiman emits k-1 prefixes" 4 (length feats)++testCategoricalSubsetsMulticlassLowCard :: Test+testCategoricalSubsetsMulticlassLowCard = TestCase $ do+    -- 3-class target, 3-level Text column. Subset enumeration: 2^3 - 2 = 6.+    let n = 30 :: Int+        feats = take n (cycle ["x", "y", "z"])+        labs = take n (cycle ["A", "B", "C"])+        df =+            D.fromUnnamedColumns+                [ DI.fromList (map T.pack feats :: [T.Text])+                , DI.fromList (map T.pack labs :: [T.Text])+                ]+                |> D.rename "0" "feat"+                |> D.rename "1" "label"+        ti = requireTargetInfo "label" df+        conds = discreteConditions @T.Text ti defaultTreeConfig (D.exclude ["label"] df)+        feat = "feat"+        feats' = filter (\c -> feat `elem` getColumns c) conds+    -- 3 classes → multi-class path → subsets at cap=4 → 2^3 - 2 = 6+    assertEqual "subsets at low cardinality" 6 (length feats')++testCategoricalSingletonsMulticlassHighCard :: Test+testCategoricalSingletonsMulticlassHighCard = TestCase $ do+    -- 3-class target, 6-level Text column. Above cap=4 → singletons (6).+    let n = 60 :: Int+        feats = take n (cycle ["a", "b", "c", "d", "e", "f"])+        labs = take n (cycle ["A", "B", "C"])+        df =+            D.fromUnnamedColumns+                [ DI.fromList (map T.pack feats :: [T.Text])+                , DI.fromList (map T.pack labs :: [T.Text])+                ]+                |> D.rename "0" "feat"+                |> D.rename "1" "label"+        ti = requireTargetInfo "label" df+        conds = discreteConditions @T.Text ti defaultTreeConfig (D.exclude ["label"] df)+        feat = "feat"+        feats' = filter (\c -> feat `elem` getColumns c) conds+    -- 6 > cap=4 → singletons → 6 conditions+    assertEqual "singletons at high cardinality" 6 (length feats')++testCategoricalCardZero :: Test+testCategoricalCardZero = TestCase $ do+    -- Empty column → no conditions.+    let df =+            D.fromUnnamedColumns+                [ DI.fromList ([] :: [T.Text])+                , DI.fromList ([] :: [T.Text])+                ]+                |> D.rename "0" "feat"+                |> D.rename "1" "label"+        ti = requireTargetInfo "label" df+        conds = discreteConditions @T.Text ti defaultTreeConfig (D.exclude ["label"] df)+        feat = "feat"+        feats' = filter (\c -> feat `elem` getColumns c) conds+    assertEqual "no candidates on empty column" 0 (length feats')++testCategoricalNullableBinary :: Test+testCategoricalNullableBinary = TestCase $ do+    -- Maybe Text feature with nulls, binary target. Breiman should fire on+    -- the non-null distinct values; nulls drop out via validBoxedValues.+    let feats =+            [ Just "a"+            , Just "b"+            , Just "c"+            , Nothing+            , Just "a"+            , Just "b"+            , Just "c"+            , Nothing+            , Just "a"+            , Just "b"+            , Just "c"+            , Just "a"+            , Just "b"+            , Just "c"+            , Just "a"+            , Just "b"+            ]+        labs =+            [ "neg"+            , "neg"+            , "pos"+            , "neg"+            , "neg"+            , "neg"+            , "pos"+            , "neg"+            , "neg"+            , "neg"+            , "pos"+            , "neg"+            , "neg"+            , "pos"+            , "neg"+            , "pos"+            ]+        df =+            D.fromUnnamedColumns+                [ DI.fromList (feats :: [Maybe T.Text])+                , DI.fromList (map T.pack labs :: [T.Text])+                ]+                |> D.rename "0" "feat"+                |> D.rename "1" "label"+        ti = requireTargetInfo "label" df+        conds = discreteConditions @T.Text ti defaultTreeConfig (D.exclude ["label"] df)+        feat = "feat" :: T.Text+        feats' = filter (\c -> feat `elem` getColumns c) conds+    -- 3 non-null distinct levels → k-1 = 2 Breiman prefixes+    assertEqual "Breiman prefixes on nullable column ignore nulls" 2 (length feats')++------------------------------------------------------------------------+-- PR 2 extended: threshold-consolidation rewrite in combineAndVec /+-- combineOrVec. Eight positive cases (one per <, ≤, >, ≥ × AND / OR),+-- six negative cases (rule must NOT fire), one semantic-preservation+-- QuickCheck-style spot check.+------------------------------------------------------------------------++-- A small synthetic DataFrame to materialize CondVecs against.+threshFixtureDF :: D.DataFrame+threshFixtureDF =+    D.fromNamedColumns+        [ ("x", DI.fromList ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0] :: [Double]))+        , ("y", DI.fromList ([5.0, 4.0, 3.0, 2.0, 1.0, 0.0] :: [Double]))+        ]++materializeOrFail :: Expr Bool -> CondVec+materializeOrFail e = case materializeCondVec threshFixtureDF e of+    Just cv -> cv+    Nothing -> error "materializeOrFail: condition could not be materialized"++-- | Helper: assert that two `Expr Bool`s agree by 'eqExpr'.+assertEqExpr :: String -> Expr Bool -> Expr Bool -> Assertion+assertEqExpr msg expected actual =+    assertBool+        (msg ++ "\n  expected: " ++ show expected ++ "\n  actual:   " ++ show actual)+        (eqExpr expected actual)++-- Eight positive cases.++threshAndLeq :: Test+threshAndLeq = TestCase $ do+    let a = materializeOrFail (F.col @Double "x" .<=. F.lit (3.0 :: Double))+        b = materializeOrFail (F.col @Double "x" .<=. F.lit (1.0 :: Double))+        r = combineAndVec a b+    assertEqExpr+        "AND of x≤3 and x≤1 collapses to x≤1"+        (F.col @Double "x" .<=. F.lit (1.0 :: Double))+        (cvExpr r)++threshOrLeq :: Test+threshOrLeq = TestCase $ do+    let a = materializeOrFail (F.col @Double "x" .<=. F.lit (3.0 :: Double))+        b = materializeOrFail (F.col @Double "x" .<=. F.lit (1.0 :: Double))+        r = combineOrVec a b+    assertEqExpr+        "OR of x≤3 and x≤1 collapses to x≤3"+        (F.col @Double "x" .<=. F.lit (3.0 :: Double))+        (cvExpr r)++threshAndLt :: Test+threshAndLt = TestCase $ do+    let a = materializeOrFail (F.col @Double "x" .<. F.lit (3.0 :: Double))+        b = materializeOrFail (F.col @Double "x" .<. F.lit (1.0 :: Double))+        r = combineAndVec a b+    assertEqExpr+        "AND of x<3 and x<1 collapses to x<1"+        (F.col @Double "x" .<. F.lit (1.0 :: Double))+        (cvExpr r)++threshOrLt :: Test+threshOrLt = TestCase $ do+    let a = materializeOrFail (F.col @Double "x" .<. F.lit (3.0 :: Double))+        b = materializeOrFail (F.col @Double "x" .<. F.lit (1.0 :: Double))+        r = combineOrVec a b+    assertEqExpr+        "OR of x<3 and x<1 collapses to x<3"+        (F.col @Double "x" .<. F.lit (3.0 :: Double))+        (cvExpr r)++threshAndGeq :: Test+threshAndGeq = TestCase $ do+    let a = materializeOrFail (F.col @Double "x" .>=. F.lit (1.0 :: Double))+        b = materializeOrFail (F.col @Double "x" .>=. F.lit (3.0 :: Double))+        r = combineAndVec a b+    assertEqExpr+        "AND of x≥1 and x≥3 collapses to x≥3"+        (F.col @Double "x" .>=. F.lit (3.0 :: Double))+        (cvExpr r)++threshOrGeq :: Test+threshOrGeq = TestCase $ do+    let a = materializeOrFail (F.col @Double "x" .>=. F.lit (1.0 :: Double))+        b = materializeOrFail (F.col @Double "x" .>=. F.lit (3.0 :: Double))+        r = combineOrVec a b+    assertEqExpr+        "OR of x≥1 and x≥3 collapses to x≥1"+        (F.col @Double "x" .>=. F.lit (1.0 :: Double))+        (cvExpr r)++threshAndGt :: Test+threshAndGt = TestCase $ do+    let a = materializeOrFail (F.col @Double "x" .>. F.lit (1.0 :: Double))+        b = materializeOrFail (F.col @Double "x" .>. F.lit (3.0 :: Double))+        r = combineAndVec a b+    assertEqExpr+        "AND of x>1 and x>3 collapses to x>3"+        (F.col @Double "x" .>. F.lit (3.0 :: Double))+        (cvExpr r)++threshOrGt :: Test+threshOrGt = TestCase $ do+    let a = materializeOrFail (F.col @Double "x" .>. F.lit (1.0 :: Double))+        b = materializeOrFail (F.col @Double "x" .>. F.lit (3.0 :: Double))+        r = combineOrVec a b+    assertEqExpr+        "OR of x>1 and x>3 collapses to x>1"+        (F.col @Double "x" .>. F.lit (1.0 :: Double))+        (cvExpr r)++-- Six negative cases: rewrite must NOT fire.++threshNegMixedDirection :: Test+threshNegMixedDirection = TestCase $ do+    let a = materializeOrFail (F.col @Double "x" .<. F.lit (3.0 :: Double))+        b = materializeOrFail (F.col @Double "x" .>=. F.lit (1.0 :: Double))+        r = combineAndVec a b+    -- Mixed directions (< vs ≥): consolidation deliberately out-of-scope.+    -- Expect the generic F.and form.+    assertEqExpr+        "mixed-direction AND keeps generic F.and form"+        (F.and (cvExpr a) (cvExpr b))+        (cvExpr r)++threshNegCrossColumn :: Test+threshNegCrossColumn = TestCase $ do+    let a = materializeOrFail (F.col @Double "x" .>. F.lit (1.0 :: Double))+        b = materializeOrFail (F.col @Double "y" .>. F.lit (3.0 :: Double))+        r = combineAndVec a b+    -- Same op, different columns: no rewrite.+    assertEqExpr+        "cross-column AND keeps generic F.and form"+        (F.and (cvExpr a) (cvExpr b))+        (cvExpr r)++threshNegMixedOpFamily :: Test+threshNegMixedOpFamily = TestCase $ do+    let a = materializeOrFail (F.col @Double "x" .>. F.lit (1.0 :: Double))+        b = materializeOrFail (F.col @Double "x" .<. F.lit (4.0 :: Double))+        r = combineAndVec a b+    -- > and < are different op families: no rewrite.+    assertEqExpr+        "different-op-family AND keeps generic F.and form"+        (F.and (cvExpr a) (cvExpr b))+        (cvExpr r)++threshNegEqualityOp :: Test+threshNegEqualityOp = TestCase $ do+    let a = materializeOrFail (F.col @Double "x" .==. F.lit (3.0 :: Double))+        b = materializeOrFail (F.col @Double "x" .==. F.lit (1.0 :: Double))+        r = combineOrVec a b+    -- Equality is not in the threshold family; consolidate doesn't fire.+    assertEqExpr+        "equality OR keeps generic F.or form"+        (F.or (cvExpr a) (cvExpr b))+        (cvExpr r)++threshNegLitOnLeft :: Test+threshNegLitOnLeft = TestCase $ do+    -- Lit on LEFT of the comparison: pattern requires (Col, Lit) ordering.+    let a = materializeOrFail (F.lit (1.0 :: Double) .<. F.col @Double "x")+        b = materializeOrFail (F.lit (3.0 :: Double) .<. F.col @Double "x")+        r = combineAndVec a b+    assertEqExpr+        "Lit-on-left AND keeps generic F.and form"+        (F.and (cvExpr a) (cvExpr b))+        (cvExpr r)++threshNegNonLiteralRhs :: Test+threshNegNonLiteralRhs = TestCase $ do+    -- RHS is a Col, not a Lit: pattern doesn't match.+    let a = materializeOrFail (F.col @Double "x" .>. F.col @Double "y")+        b = materializeOrFail (F.col @Double "x" .>. F.lit (3.0 :: Double))+        r = combineAndVec a b+    assertEqExpr+        "non-literal RHS AND keeps generic F.and form"+        (F.and (cvExpr a) (cvExpr b))+        (cvExpr r)++-- Semantic-preservation spot check (in lieu of a full QuickCheck property+-- which would require generators for strict-op Expr Bool — followup work).+-- Verifies that the consolidated cvVec matches the elementwise AND/OR of+-- the inputs at every row of a synthetic DataFrame.+threshSemanticPreservation :: Test+threshSemanticPreservation = TestCase $ do+    let a = materializeOrFail (F.col @Double "x" .>. F.lit (1.0 :: Double))+        b = materializeOrFail (F.col @Double "x" .>. F.lit (3.0 :: Double))+        rAnd = combineAndVec a b+        rOr = combineOrVec a b+        expectedAnd = VU.zipWith (&&) (cvVec a) (cvVec b)+        expectedOr = VU.zipWith (||) (cvVec a) (cvVec b)+    assertEqual+        "consolidated AND vec matches elementwise &&"+        expectedAnd+        (cvVec rAnd)+    assertEqual+        "consolidated OR vec matches elementwise ||"+        expectedOr+        (cvVec rOr)++------------------------------------------------------------------------+-- Test list+------------------------------------------------------------------------++tests :: [Test]+tests =+    [ TestLabel "carePointsBothWrong" carePointsBothWrong+    , TestLabel "carePointsLeftCorrect" carePointsLeftCorrect+    , TestLabel "carePointsRightCorrect" carePointsRightCorrect+    , TestLabel "carePointsMixed" carePointsMixed+    , TestLabel "carePointsBothCorrect" carePointsBothCorrect+    , TestLabel "majorityVoteTest" majorityVoteTest+    , TestLabel "majorityVoteSubset" majorityVoteSubset+    , TestLabel "computeLossZero" computeLossZero+    , TestLabel "computeLossHalf" computeLossHalf+    , TestLabel "partitionDisjoint" partitionDisjoint+    , TestLabel "partitionUnion" partitionUnion+    , TestLabel "countErrorsAllCorrect" countErrorsAllCorrect+    , TestLabel "countErrorsAllWrong" countErrorsAllWrong+    , TestLabel "predictLeaf" predictLeaf+    , TestLabel "predictBranch" predictBranch+    , TestLabel "taoNoDegradation" taoNoDegradation+    , TestLabel "taoMonotone" taoMonotone+    , TestLabel "taoConvergesPureLabels" taoConvergesPureLabels+    , TestLabel "taoDeadBranchNoCrash" taoDeadBranchNoCrash+    , TestLabel "taoRecoversSingleObliqueDerived" taoRecoversSingleObliqueDerived+    , TestLabel "taoRecoversNestedObliqueDerived" taoRecoversNestedObliqueDerived+    , TestLabel+        "C2a taoAxisAlignedInsufficientForObliqueDiscreteOnly"+        taoAxisAlignedInsufficientForObliqueDiscreteOnly+    , TestLabel+        "C2b taoLinearRecoversObliqueFromAxisAlignedPool"+        taoLinearRecoversObliqueFromAxisAlignedPool+    , TestLabel "numericColsNullableDouble" numericColsNullableDoubleTest+    , TestLabel "numericColsNullableInt" numericColsNullableIntTest+    , TestLabel "numericCondsNullableNonEmpty" numericCondsNullableNonEmptyTest+    , TestLabel "nullValueRoutesFalse" nullValueRoutesFalseTest+    , TestLabel "nullableFitZeroLoss" nullableFitZeroLossTest+    , TestLabel "nullableFitWithNullsNoCrash" nullableFitWithNullsNoCrashTest+    , TestLabel "numericExprsWithTermsMixed" numericExprsWithTermsMixedTest+    , TestLabel "probsFromIndicesBasic" probsFromIndicesBasic+    , TestLabel "probsFromIndicesSubset" probsFromIndicesSubset+    , TestLabel "probsFromIndicesSingleClass" probsFromIndicesSingleClass+    , TestLabel "buildProbTreeLeaf" buildProbTreeLeaf+    , TestLabel "buildProbTreeBranch" buildProbTreeBranch+    , TestLabel "probExprsLeaf" probExprsLeaf+    , TestLabel "probExprsMissingClass" probExprsMissingClass+    , TestLabel "probExprsAllClasses" probExprsAllClasses+    , TestLabel "probsSumToOne" probsSumToOne+    , TestLabel "probArgmaxMatchesClassifier" probArgmaxMatchesClassifier+    , TestLabel+        "C4 taoRecoversNestedObliqueWithoutHint"+        taoRecoversNestedObliqueWithoutHint+    , TestLabel "C5 taoMonotoneWithLinear" taoMonotoneWithLinear+    , TestLabel "C6 taoLinearVsDiscreteCompetition" taoLinearVsDiscreteCompetition+    , TestLabel "C8 taoLinearProducesSparsity" taoLinearProducesSparsity+    , TestLabel "C9 taoLinearDeterministic" taoLinearDeterministic+    , TestLabel "D1 taoLinearTinyCareSet" taoLinearTinyCareSet+    , TestLabel "E1 categoricalBreimanBinary" testCategoricalBreimanBinary+    , TestLabel+        "E2 categoricalSubsetsMulticlassLowCard"+        testCategoricalSubsetsMulticlassLowCard+    , TestLabel+        "E3 categoricalSingletonsMulticlassHighCard"+        testCategoricalSingletonsMulticlassHighCard+    , TestLabel "E4 categoricalCardZero" testCategoricalCardZero+    , TestLabel "E5 categoricalNullableBinary" testCategoricalNullableBinary+    , -- PR 2 extended: threshold-consolidation rewrite (positive cases).+      TestLabel "F1 threshAndLeq" threshAndLeq+    , TestLabel "F2 threshOrLeq" threshOrLeq+    , TestLabel "F3 threshAndLt" threshAndLt+    , TestLabel "F4 threshOrLt" threshOrLt+    , TestLabel "F5 threshAndGeq" threshAndGeq+    , TestLabel "F6 threshOrGeq" threshOrGeq+    , TestLabel "F7 threshAndGt" threshAndGt+    , TestLabel "F8 threshOrGt" threshOrGt+    , -- PR 2 extended: negative cases (rewrite must NOT fire).+      TestLabel "F9 threshNegMixedDirection" threshNegMixedDirection+    , TestLabel "F10 threshNegCrossColumn" threshNegCrossColumn+    , TestLabel "F11 threshNegMixedOpFamily" threshNegMixedOpFamily+    , TestLabel "F12 threshNegEqualityOp" threshNegEqualityOp+    , TestLabel "F13 threshNegLitOnLeft" threshNegLitOnLeft+    , TestLabel "F14 threshNegNonLiteralRhs" threshNegNonLiteralRhs+    , TestLabel "F15 threshSemanticPreservation" threshSemanticPreservation+    ]
tests/GenDataFrame.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -Wno-orphans #-}  module GenDataFrame where @@ -14,15 +15,15 @@ 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 genDataFrame = do-    numRows <- choose (0, 100)-    numCols <- choose (0, 10)+    numRows <- choose (100, 1000)+    numCols <- choose (1, 10)     colNames <- V.fromList <$> vectorOf numCols genUniqueColName     cols <- V.fromList <$> vectorOf numCols (genColumn numRows)     let indices = M.fromList $ zip (V.toList colNames) [0 ..]@@ -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,560 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Golden semantics for the default CSV reader, pinned against the+cassava-based implementation (oracle runs of 2026-06-12) before the+strict-scanner rewrite. Ragged-row cases encode the new pad-with-null+behavior (audit D6) — the one intentional change.+-}+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.Internal.Schema (schemaType)+import DataFrame.Operations.Typing (SafeReadMode (..))+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"])]+        )+    , -- D6: short rows now pad trailing columns with null (was: misaligned).++        ( "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])]+        )+    , -- D6: the always-short padded column is now row-aligned (all null).++        ( "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])]+        )+    , -- D6: the column after the EOF-truncated quote field is now padded.++        ( "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/IO/JSON.hs view
@@ -0,0 +1,112 @@+{-# LANGUAGE OverloadedStrings #-}++module IO.JSON where++import qualified Data.ByteString.Lazy.Char8 as LBSC+import DataFrame.IO.JSON (readJSONEither)+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Internal.DataFrame as DI+import qualified DataFrame.Operations.Core as D+import Test.HUnit (+    Test (TestCase, TestLabel),+    assertBool,+    assertEqual,+    assertFailure,+ )++-- | Happy path: array of objects with string and number columns.+jsonHappyPath :: Test+jsonHappyPath =+    TestCase+        ( case readJSONEither+            (LBSC.pack "[{\"name\":\"Alice\",\"age\":30},{\"name\":\"Bob\",\"age\":25}]") of+            Left err -> assertFailure $ "Unexpected Left: " ++ err+            Right df -> do+                assertEqual "Happy path: 2 rows" 2 (D.nRows df)+                assertEqual "Happy path: 2 columns" 2 (D.nColumns df)+        )++-- | Boolean column is preserved correctly.+jsonBoolColumn :: Test+jsonBoolColumn =+    TestCase+        ( case readJSONEither (LBSC.pack "[{\"flag\":true},{\"flag\":false}]") of+            Left err -> assertFailure $ "Unexpected Left: " ++ err+            Right df -> do+                assertEqual "Bool column: 2 rows" 2 (D.nRows df)+                assertEqual "Bool column: 1 column" 1 (D.nColumns df)+        )++-- | A key absent from some objects produces an Optional column.+jsonMissingKeyBecomesOptional :: Test+jsonMissingKeyBecomesOptional =+    TestCase+        ( case readJSONEither (LBSC.pack "[{\"a\":1,\"b\":2},{\"a\":3}]") of+            Left err -> assertFailure $ "Unexpected Left: " ++ err+            Right df -> do+                assertEqual "Missing key: 2 rows" 2 (D.nRows df)+                assertEqual "Missing key: 2 columns" 2 (D.nColumns df)+                -- 'b' is absent from the second row, so must have a missing value+                assertBool+                    "b column should have missing values"+                    (maybe False DI.hasMissing (DI.getColumn "b" df))+        )++-- | When column values are different types across rows, the column becomes generic.+jsonMixedTypeColumn :: Test+jsonMixedTypeColumn =+    TestCase+        ( case readJSONEither (LBSC.pack "[{\"x\":1},{\"x\":\"hello\"}]") of+            Left err -> assertFailure $ "Unexpected Left: " ++ err+            Right df -> do+                assertEqual "Mixed type: 2 rows" 2 (D.nRows df)+                assertEqual "Mixed type: 1 column" 1 (D.nColumns df)+        )++-- | An empty top-level JSON array is rejected.+jsonEmptyArray :: Test+jsonEmptyArray =+    TestCase+        ( case readJSONEither (LBSC.pack "[]") of+            Left _ -> return ()+            Right _ -> assertFailure "Expected Left for empty array"+        )++-- | A non-array top-level JSON value is rejected.+jsonNonArray :: Test+jsonNonArray =+    TestCase+        ( case readJSONEither (LBSC.pack "{\"name\":\"Alice\"}") of+            Left _ -> return ()+            Right _ -> assertFailure "Expected Left for non-array top-level value"+        )++-- | Array elements that are not objects are rejected.+jsonNonObjectElement :: Test+jsonNonObjectElement =+    TestCase+        ( case readJSONEither (LBSC.pack "[1, 2, 3]") of+            Left _ -> return ()+            Right _ -> assertFailure "Expected Left for non-object array elements"+        )++-- | Completely invalid JSON is rejected.+jsonInvalidJSON :: Test+jsonInvalidJSON =+    TestCase+        ( case readJSONEither (LBSC.pack "not valid json at all") of+            Left _ -> return ()+            Right _ -> assertFailure "Expected Left for invalid JSON"+        )++tests :: [Test]+tests =+    [ TestLabel "jsonHappyPath" jsonHappyPath+    , TestLabel "jsonBoolColumn" jsonBoolColumn+    , TestLabel "jsonMissingKeyBecomesOptional" jsonMissingKeyBecomesOptional+    , TestLabel "jsonMixedTypeColumn" jsonMixedTypeColumn+    , TestLabel "jsonEmptyArray" jsonEmptyArray+    , TestLabel "jsonNonArray" jsonNonArray+    , TestLabel "jsonNonObjectElement" jsonNonObjectElement+    , TestLabel "jsonInvalidJSON" jsonInvalidJSON+    ]
+ 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,44 @@+{-# 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.Internal.Column (fromList)+import qualified DataFrame.Internal.DataFrame as D+import DataFrame.Display.Terminal.PrettyPrint (escapeMarkdownCell)+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/Internal/Parsing.hs view
@@ -0,0 +1,237 @@+{-# LANGUAGE OverloadedStrings #-}++module Internal.Parsing where++import DataFrame.Internal.Parsing+import Test.HUnit++-- isNullish: recognized null strings++isNullishEmptyString :: Test+isNullishEmptyString =+    TestCase (assertBool "empty string is nullish" (isNullish ""))++isNullishNA :: Test+isNullishNA = TestCase (assertBool "NA is nullish" (isNullish "NA"))++isNullishNULL :: Test+isNullishNULL = TestCase (assertBool "NULL is nullish" (isNullish "NULL"))++isNullishNull :: Test+isNullishNull = TestCase (assertBool "null is nullish" (isNullish "null"))++isNullishNaN :: Test+isNullishNaN = TestCase (assertBool "nan is nullish" (isNullish "nan"))++isNullishNaNMixed :: Test+isNullishNaNMixed = TestCase (assertBool "NaN is nullish" (isNullish "NaN"))++isNullishNANUpper :: Test+isNullishNANUpper = TestCase (assertBool "NAN is nullish" (isNullish "NAN"))++isNullishNothing :: Test+isNullishNothing =+    TestCase (assertBool "Nothing is nullish" (isNullish "Nothing"))++isNullishSpace :: Test+isNullishSpace =+    TestCase (assertBool "single space is nullish" (isNullish " "))++isNullishNSlashA :: Test+isNullishNSlashA = TestCase (assertBool "N/A is nullish" (isNullish "N/A"))++-- isNullish: values that are NOT null++notNullishNumber :: Test+notNullishNumber =+    TestCase (assertBool "\"42\" is not nullish" (not (isNullish "42")))++notNullishText :: Test+notNullishText =+    TestCase (assertBool "\"hello\" is not nullish" (not (isNullish "hello")))++notNullishTrue :: Test+notNullishTrue =+    TestCase (assertBool "\"True\" is not nullish" (not (isNullish "True")))++notNullishDouble :: Test+notNullishDouble =+    TestCase (assertBool "\"3.14\" is not nullish" (not (isNullish "3.14")))++notNullishZero :: Test+notNullishZero =+    TestCase (assertBool "\"0\" is not nullish" (not (isNullish "0")))++-- readBool: positive cases++readBoolTrue :: Test+readBoolTrue =+    TestCase (assertEqual "readBool \"True\"" (Just True) (readBool "True"))++readBoolTrueLower :: Test+readBoolTrueLower =+    TestCase (assertEqual "readBool \"true\"" (Just True) (readBool "true"))++readBoolTrueUpper :: Test+readBoolTrueUpper =+    TestCase (assertEqual "readBool \"TRUE\"" (Just True) (readBool "TRUE"))++readBoolFalse :: Test+readBoolFalse =+    TestCase (assertEqual "readBool \"False\"" (Just False) (readBool "False"))++readBoolFalseLower :: Test+readBoolFalseLower =+    TestCase (assertEqual "readBool \"false\"" (Just False) (readBool "false"))++readBoolFalseUpper :: Test+readBoolFalseUpper =+    TestCase (assertEqual "readBool \"FALSE\"" (Just False) (readBool "FALSE"))++-- readBool: values that are not booleans++readBoolDigit :: Test+readBoolDigit =+    TestCase (assertEqual "readBool \"1\" is Nothing" Nothing (readBool "1"))++readBoolYes :: Test+readBoolYes =+    TestCase (assertEqual "readBool \"yes\" is Nothing" Nothing (readBool "yes"))++readBoolEmpty :: Test+readBoolEmpty =+    TestCase (assertEqual "readBool \"\" is Nothing" Nothing (readBool ""))++readBoolPartialTrue :: Test+readBoolPartialTrue =+    TestCase (assertEqual "readBool \"Tru\" is Nothing" Nothing (readBool "Tru"))++-- readInt++readIntPositive :: Test+readIntPositive =+    TestCase (assertEqual "readInt \"42\"" (Just 42) (readInt "42"))++readIntNegative :: Test+readIntNegative =+    TestCase (assertEqual "readInt \"-17\"" (Just (-17)) (readInt "-17"))++readIntZero :: Test+readIntZero =+    TestCase (assertEqual "readInt \"0\"" (Just 0) (readInt "0"))++-- readInt strips whitespace before parsing+readIntLeadingSpace :: Test+readIntLeadingSpace =+    TestCase+        ( assertEqual+            "readInt \" 5 \" (strips whitespace)"+            (Just 5)+            (readInt " 5 ")+        )++readIntFloat :: Test+readIntFloat =+    TestCase+        (assertEqual "readInt \"3.14\" is Nothing" Nothing (readInt "3.14"))++readIntText :: Test+readIntText =+    TestCase (assertEqual "readInt \"abc\" is Nothing" Nothing (readInt "abc"))++readIntEmpty :: Test+readIntEmpty =+    TestCase (assertEqual "readInt \"\" is Nothing" Nothing (readInt ""))++-- trailing non-digits must make the parse fail+readIntPartialSuffix :: Test+readIntPartialSuffix =+    TestCase+        ( assertEqual+            "readInt \"42abc\" is Nothing"+            Nothing+            (readInt "42abc")+        )++-- readDouble++readDoublePositive :: Test+readDoublePositive =+    TestCase+        (assertEqual "readDouble \"3.14\"" (Just 3.14) (readDouble "3.14"))++readDoubleNegative :: Test+readDoubleNegative =+    TestCase+        (assertEqual "readDouble \"-1.5\"" (Just (-1.5)) (readDouble "-1.5"))++readDoubleWholeNumber :: Test+readDoubleWholeNumber =+    TestCase+        ( assertEqual+            "readDouble \"42\" parses as 42.0"+            (Just 42.0)+            (readDouble "42")+        )++readDoubleText :: Test+readDoubleText =+    TestCase+        (assertEqual "readDouble \"abc\" is Nothing" Nothing (readDouble "abc"))++readDoubleEmpty :: Test+readDoubleEmpty =+    TestCase+        (assertEqual "readDouble \"\" is Nothing" Nothing (readDouble ""))++readDoublePartialSuffix :: Test+readDoublePartialSuffix =+    TestCase+        ( assertEqual+            "readDouble \"3.14abc\" is Nothing"+            Nothing+            (readDouble "3.14abc")+        )++tests :: [Test]+tests =+    [ TestLabel "isNullishEmptyString" isNullishEmptyString+    , TestLabel "isNullishNA" isNullishNA+    , TestLabel "isNullishNULL" isNullishNULL+    , TestLabel "isNullishNull" isNullishNull+    , TestLabel "isNullishNaN" isNullishNaN+    , TestLabel "isNullishNaNMixed" isNullishNaNMixed+    , TestLabel "isNullishNANUpper" isNullishNANUpper+    , TestLabel "isNullishNothing" isNullishNothing+    , TestLabel "isNullishSpace" isNullishSpace+    , TestLabel "isNullishNSlashA" isNullishNSlashA+    , TestLabel "notNullishNumber" notNullishNumber+    , TestLabel "notNullishText" notNullishText+    , TestLabel "notNullishTrue" notNullishTrue+    , TestLabel "notNullishDouble" notNullishDouble+    , TestLabel "notNullishZero" notNullishZero+    , TestLabel "readBoolTrue" readBoolTrue+    , TestLabel "readBoolTrueLower" readBoolTrueLower+    , TestLabel "readBoolTrueUpper" readBoolTrueUpper+    , TestLabel "readBoolFalse" readBoolFalse+    , TestLabel "readBoolFalseLower" readBoolFalseLower+    , TestLabel "readBoolFalseUpper" readBoolFalseUpper+    , TestLabel "readBoolDigit" readBoolDigit+    , TestLabel "readBoolYes" readBoolYes+    , TestLabel "readBoolEmpty" readBoolEmpty+    , TestLabel "readBoolPartialTrue" readBoolPartialTrue+    , TestLabel "readIntPositive" readIntPositive+    , TestLabel "readIntNegative" readIntNegative+    , TestLabel "readIntZero" readIntZero+    , TestLabel "readIntLeadingSpace" readIntLeadingSpace+    , TestLabel "readIntFloat" readIntFloat+    , TestLabel "readIntText" readIntText+    , TestLabel "readIntEmpty" readIntEmpty+    , TestLabel "readIntPartialSuffix" readIntPartialSuffix+    , TestLabel "readDoublePositive" readDoublePositive+    , TestLabel "readDoubleNegative" readDoubleNegative+    , TestLabel "readDoubleWholeNumber" readDoubleWholeNumber+    , TestLabel "readDoubleText" readDoubleText+    , TestLabel "readDoubleEmpty" readDoubleEmpty+    , TestLabel "readDoublePartialSuffix" readDoublePartialSuffix+    ]
+ tests/LazyParity.hs view
@@ -0,0 +1,152 @@+{-# 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 computed with the same eager ops+('Join.join' / 'Agg.aggregate' . 'Agg.groupBy' / 'Perm.sortBy').++The lazy executor routes bounded HashJoin/HashAggregate through the whole-frame+eager fast paths, so 'show' of the two results must agree exactly. Both a LEFT+join pipeline (join -> groupBy -> aggregate -> sort) and a pure groupBy pipeline+are covered.+-}+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.Internal.Schema (Schema (..), schemaType)+import qualified DataFrame.Lazy as L+import DataFrame.Lazy.Internal.LogicalPlan (SortOrder (Descending))+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 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"+                        ]+                -- Eager: the exact ops the lazy executor delegates to.+                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"] customersDf ordersDf+                -- Lazy: same query through the bounded-source fast path.+                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
@@ -0,0 +1,131 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module LazyParquet where++import Data.Int (Int32, Int64)+import qualified Data.Map.Strict as M+import qualified Data.Text as T+import Data.Time (UTCTime)+import qualified DataFrame as D+import qualified DataFrame.Functions as F+import DataFrame.Internal.Schema (Schema (..), schemaType)+import qualified DataFrame.Lazy as L+import DataFrame.Lazy.Internal.LogicalPlan (SortOrder (..))+import Test.HUnit++-- | Schema matching all columns in alltypes_plain.parquet.+allTypesSchema :: Schema+allTypesSchema =+    Schema $+        M.fromList+            [ ("id", schemaType @Int32)+            , ("bool_col", schemaType @Bool)+            , ("tinyint_col", schemaType @Int32)+            , ("smallint_col", schemaType @Int32)+            , ("int_col", schemaType @Int32)+            , ("bigint_col", schemaType @Int64)+            , ("float_col", schemaType @Float)+            , ("double_col", schemaType @Double)+            , ("date_string_col", schemaType @T.Text)+            , ("string_col", schemaType @T.Text)+            , ("timestamp_col", schemaType @UTCTime)+            ]++plainPath :: FilePath+plainPath = "./tests/data/alltypes_plain.parquet"++-- Test 1: basic scan — dimensions match eager reader.+basicScan :: Test+basicScan =+    TestCase+        ( do+            eager <- D.readParquet plainPath+            actual <- L.runDataFrame (L.scanParquet allTypesSchema (T.pack plainPath))+            assertEqual "basicScan dimensions" (D.dimensions eager) (D.dimensions actual)+        )++-- Test 2: column projection — select 2 columns.+columnProjection :: Test+columnProjection =+    TestCase+        ( do+            actual <-+                L.runDataFrame+                    (L.select ["id", "bool_col"] (L.scanParquet allTypesSchema (T.pack plainPath)))+            assertEqual "columnProjection" (8, 2) (D.dimensions actual)+        )++-- Test 3: filter pushdown — id >= 6 gives 2 rows.+filterPushdown :: Test+filterPushdown =+    TestCase+        ( do+            actual <-+                L.runDataFrame+                    ( L.filter+                        (F.geq (F.col @Int32 "id") (F.lit 6))+                        (L.scanParquet allTypesSchema (T.pack plainPath))+                    )+            assertEqual "filterPushdown" (2, 11) (D.dimensions actual)+        )++-- Test 4: filter + project combined.+filterAndProject :: Test+filterAndProject =+    TestCase+        ( do+            actual <-+                L.runDataFrame+                    ( L.select+                        ["id", "bool_col"]+                        ( L.filter+                            (F.geq (F.col @Int32 "id") (F.lit 6))+                            (L.scanParquet allTypesSchema (T.pack plainPath))+                        )+                    )+            assertEqual "filterAndProject" (2, 2) (D.dimensions actual)+        )++-- Test 5: multi-file glob — alltypes_plain*.parquet matches plain + snappy = 10 rows.+multiFileGlob :: Test+multiFileGlob =+    TestCase+        ( do+            actual <-+                L.runDataFrame+                    ( L.scanParquet+                        allTypesSchema+                        "./tests/data/alltypes_plain*.parquet"+                    )+            let (rows, cols) = D.dimensions actual+            assertEqual "multiFileGlob cols" 11 cols+            assertBool "multiFileGlob rows >= 8" (rows >= 8)+        )++-- Test 6: sort + limit — get 3 smallest ids.+sortAndLimit :: Test+sortAndLimit =+    TestCase+        ( do+            actual <-+                L.runDataFrame+                    ( L.take+                        3+                        ( L.sortBy+                            [("id", Ascending)]+                            (L.scanParquet allTypesSchema (T.pack plainPath))+                        )+                    )+            assertEqual "sortAndLimit rows" 3 (fst (D.dimensions actual))+        )++tests :: [Test]+tests =+    [ basicScan+    , columnProjection+    , filterPushdown+    , filterAndProject+    , multiFileGlob+    , sortAndLimit+    ]
+ 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/EdgeCases.hs view
@@ -0,0 +1,481 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Edge-case / degeneracy tests (tests.md category 7) and numerical-stability+tests (category 8) for @dataframe-learn@.++Each test asserts the *mathematically correct* result (computed by hand, with+the derivation in a comment) or a *specific* documented degenerate behaviour --+never "it returned something". Where the library's contract is to clamp/guard a+degeneracy (e.g. @k > n@, constant columns) we assert the concrete clamped+result; where the underlying routine is stable we assert it matches the closed+form a naive implementation would get wrong.+-}+module Learn.EdgeCases (tests) where++import qualified Data.Map.Strict as M+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.GMM+import DataFrame.KMeans+import DataFrame.LinearAlgebra (logSumExp)+import DataFrame.LinearAlgebra.Eigen (jacobiEigenSym)+import DataFrame.LinearAlgebra.Solve (choleskySolve)+import DataFrame.LinearModel+import DataFrame.LinearSolver (sigmoid)+import DataFrame.Model (fit, predict)+import DataFrame.PCA++import DataFrame.Internal.Statistics (correlation', variance')++import Test.HUnit++-- Helpers (mirrors Learn.Models) --------------------------------------------++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)++mat :: [[Double]] -> V.Vector (VU.Vector Double)+mat = V.fromList . map VU.fromList++close :: Double -> Double -> Double -> Bool+close tol a b = abs (a - b) <= tol++finite :: Double -> Bool+finite x = not (isNaN x) && not (isInfinite x)++-- ===========================================================================+-- Category 8: numerical stability+-- ===========================================================================++{- logSumExp on a large-positive cluster.+   log(e^1000 + e^1001 + e^1002)+     = 1002 + log(e^-2 + e^-1 + 1)+   e^-2 = 0.1353352832366127, e^-1 = 0.36787944117144233+   sum = 1.5032147243...  ; log(sum) = 0.40760596444...+   => 1002.4076059644...+   A naive `log . sum . map exp` overflows to +Inf here. -}+testLogSumExpLargePositive :: Test+testLogSumExpLargePositive = TestCase $ do+    let lse = logSumExp (VU.fromList [1000, 1001, 1002])+        expected = 1002 + log (exp (-2) + exp (-1) + 1)+    assertBool "lse large-positive is finite" (finite lse)+    assertBool+        "lse [1000,1001,1002] == 1002 + log(e^-2+e^-1+1)"+        (close 1e-9 lse expected)+    -- sanity on the literal so the test pins the actual number, not just the formula+    assertBool "lse literal ~ 1002.40760596" (close 1e-7 lse 1002.4076059644443)++{- logSumExp on a large-negative cluster.+   log(e^-1000 + e^-1001 + e^-1002)+     = -1000 + log(1 + e^-1 + e^-2)  [max is -1000]+     = -1000 + 0.40760596444...+     = -999.59239403556...+   A naive implementation underflows every term to 0 -> log 0 = -Inf. -}+testLogSumExpLargeNegative :: Test+testLogSumExpLargeNegative = TestCase $ do+    let lse = logSumExp (VU.fromList [-1000, -1001, -1002])+        expected = -1000 + log (1 + exp (-1) + exp (-2))+    assertBool "lse large-negative is finite" (finite lse)+    assertBool+        "lse [-1000,-1001,-1002] == -1000 + log(1+e^-1+e^-2)"+        (close 1e-9 lse expected)+    assertBool "lse literal ~ -999.59239403" (close 1e-7 lse (-999.5923940355557))++{- logSumExp of a single element is that element exactly (m + log 1). -}+testLogSumExpSingleton :: Test+testLogSumExpSingleton = TestCase $ do+    assertBool "lse [42] == 42" (close 0 (logSumExp (VU.fromList [42])) 42)++{- sigmoid at extreme arguments must stay in [0,1] and not NaN/Inf.+   sigmoid(1000) is 1 up to rounding (1 - ~0); sigmoid(-1000) is 0.+   The naive 1/(1+exp(-z)) overflows exp(1000)=Inf at z=-1000 -> 1/Inf = 0 ok,+   but exp(-(-1000)) path; the stable branch in DataFrame.LinearSolver.sigmoid+   picks ez/(1+ez) for z<0 so e^-1000 underflows to 0 cleanly -> 0. -}+testSigmoidExtreme :: Test+testSigmoidExtreme = TestCase $ do+    let sp = sigmoid 1000+        sn = sigmoid (-1000)+    assertBool "sigmoid(1000) finite" (finite sp)+    assertBool "sigmoid(-1000) finite" (finite sn)+    assertBool "sigmoid(1000) in [0,1]" (sp >= 0 && sp <= 1)+    assertBool "sigmoid(-1000) in [0,1]" (sn >= 0 && sn <= 1)+    -- saturated values: 1 and 0 to within machine epsilon+    assertBool "sigmoid(1000) == 1" (close 1e-12 sp 1)+    assertBool "sigmoid(-1000) == 0" (close 1e-12 sn 0)+    -- sigmoid(0) is exactly 0.5+    assertBool "sigmoid(0) == 0.5" (close 1e-15 (sigmoid 0) 0.5)+    -- antisymmetry: sigmoid(-z) == 1 - sigmoid(z) at a moderate z+    assertBool+        "sigmoid antisymmetric at 3"+        (close 1e-12 (sigmoid (-3)) (1 - sigmoid 3))++{- Variance of large-but-low-variance data: [1e8+1, 1e8+2, 1e8+3].+   True sample variance (n-1) of {1,2,3} shifted by 1e8 is 1.0 exactly.+   A naive sum-of-squares-minus-square-of-sum formula loses all precision here+   (catastrophic cancellation) -> 0 or negative. Welford keeps it ~1. -}+testVarianceCatastrophicCancellation :: Test+testVarianceCatastrophicCancellation = TestCase $ do+    let xs = VU.fromList [1e8 + 1, 1e8 + 2, 1e8 + 3] :: VU.Vector Double+        v = variance' xs+    assertBool "variance finite" (finite v)+    assertBool "variance is positive" (v > 0)+    -- sample variance of {1,2,3} = ((1-2)^2 + 0 + (3-2)^2)/(3-1) = 2/2 = 1+    assertBool "variance of shifted {1,2,3} == 1.0" (close 1e-6 v 1.0)++{- Variance of an identical column is exactly 0 (not a tiny negative from+   cancellation). -}+testVarianceConstant :: Test+testVarianceConstant = TestCase $ do+    let v = variance' (VU.replicate 100 (7.0 :: Double))+    assertEqual "variance of constant column is 0" 0 v++{- Variance of fewer than two samples is defined to be 0 (computeVariance guard),+   not NaN from a /0. -}+testVarianceSingleton :: Test+testVarianceSingleton = TestCase $ do+    assertEqual+        "variance of one sample is 0"+        0+        (variance' (VU.fromList [3.5 :: Double]))++{- Correlation of a perfectly linear pair is exactly +1 (and -1 reversed),+   computed stably. y = 2x+1 over a spread of x. -}+testCorrelationPerfect :: Test+testCorrelationPerfect = TestCase $ do+    let xs = VU.fromList [1, 2, 3, 4, 5] :: VU.Vector Double+        ys = VU.map (\x -> 2 * x + 1) xs+        yneg = VU.map (\x -> -(2 * x) + 1) xs+    case correlation' xs ys of+        Just r -> assertBool "corr(x, 2x+1) == 1" (close 1e-9 r 1)+        Nothing -> assertFailure "expected a correlation"+    case correlation' xs yneg of+        Just r -> assertBool "corr(x, -2x+1) == -1" (close 1e-9 r (-1))+        Nothing -> assertFailure "expected a correlation"++{- Degeneracy: correlation against a constant column. The denominator+   sqrt(... * (n*Syy - Sy^2)) is 0, so the library returns Just NaN. This pins+   the ACTUAL behaviour: it does not throw, but it does produce a NaN that the+   caller must guard. If a future fix makes it return Nothing or 0 this test+   flags the contract change. -}+testCorrelationConstantColumnIsNaN :: Test+testCorrelationConstantColumnIsNaN = TestCase $ do+    let xs = VU.fromList [1, 2, 3, 4, 5] :: VU.Vector Double+        ys = VU.replicate 5 (9.0 :: Double)+    case correlation' xs ys of+        Just r ->+            assertBool+                "corr with a zero-variance column is NaN (degenerate denom)"+                (isNaN r)+        Nothing -> assertFailure "correlation' returned Nothing (contract changed)"++{- correlation' on n<2 returns Nothing (guarded), not a NaN. -}+testCorrelationTooFew :: Test+testCorrelationTooFew = TestCase $ do+    assertEqual+        "correlation of one point is Nothing"+        Nothing+        (correlation' (VU.fromList [1]) (VU.fromList [2]))++-- ===========================================================================+-- Category 8: stability inside the model expr layer+-- ===========================================================================++{- Logistic probability expressions at extreme feature values must remain in+   [0,1] and finite. The prob expr is 1/(1+exp(-margin)); with a separating+   model and |x| pushed to 1e6 the margin is huge, so a non-stable evaluation+   could overflow. We assert every probability is finite and in [0,1] and that+   the two one-vs-rest class probabilities are present. -}+testLogisticProbsExtremeFeatures :: Test+testLogisticProbsExtremeFeatures = TestCase $ do+    let trainDf =+            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]))+                ]+        m = fit defaultLogisticConfig (F.col @Int "label") trainDf+        probs = logisticProbExprs m+        -- evaluate the class-1 probability at extreme inputs+        extremeDf =+            D.fromNamedColumns+                [("x", DI.fromList ([-1e6, -1, 0, 1, 1e6] :: [Double]))]+    assertEqual "two one-vs-rest probability exprs" 2 (M.size probs)+    let allProbVals =+            concat [interpD extremeDf e | e <- M.elems probs]+    assertBool "all logistic probabilities finite" (all finite allProbVals)+    assertBool+        "all logistic probabilities in [0,1]"+        (all (\p -> p >= 0 && p <= 1) allProbVals)++-- ===========================================================================+-- Category 7: edge cases / degeneracy on models+-- ===========================================================================++{- One-row OLS: a single point (x=2, y=7). With one observation OLS is+   under-determined; the QR design [1, 2] is rank deficient (n=1 < d=2 cols of+   the intercept-augmented matrix) so olsSolve falls back to ridge(1e-8). The+   fitted line must at minimum interpolate the single training point: the+   prediction at x=2 must equal 7 (within tolerance). It must NOT be NaN/Inf. -}+testOLSOneRow :: Test+testOLSOneRow = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("x", DI.fromList ([2] :: [Double]))+                , ("y", DI.fromList ([7] :: [Double]))+                ]+        m = fit defaultLinearConfig (F.col @Double "y") df+        preds = interpD df (predict m)+    assertBool "single OLS prediction finite" (all finite preds)+    assertBool+        "OLS fits the single training point"+        (case preds of [p] -> close 1e-3 p 7; _ -> False)++{- All-identical labels in logistic regression: every row is class 1. There is+   exactly one class, so predict must return that class for every row (no+   division-by-zero, no crash, no spurious second class). -}+testLogisticSingleClass :: Test+testLogisticSingleClass = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("x", DI.fromList ([-2, -1, 0, 1, 2] :: [Double]))+                , ("label", DI.fromList ([1, 1, 1, 1, 1] :: [Int]))+                ]+        m = fit defaultLogisticConfig (F.col @Int "label") df+        preds = interpI df (predict m)+    assertEqual+        "single-class logistic predicts that class everywhere"+        [1, 1, 1, 1, 1]+        preds++{- Perfectly separable logistic data with a constant (zero-variance) feature+   added. The constant column has variance 0 and is dropped by keptIndices+   (>= 1e-12 threshold); the informative column still separates the classes.+   Prediction must recover the labels exactly and not be corrupted by the+   constant column. -}+testLogisticConstantFeatureIgnored :: Test+testLogisticConstantFeatureIgnored = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("x", DI.fromList ([-3, -2, -1, -0.5, 0.5, 1, 2, 3] :: [Double]))+                , ("const", DI.fromList (replicate 8 (5.0 :: Double)))+                , ("label", DI.fromList ([0, 0, 0, 0, 1, 1, 1, 1] :: [Int]))+                ]+        m = fit defaultLogisticConfig (F.col @Int "label") df+        preds = interpI df (predict m)+    assertEqual+        "logistic ignores constant column and separates"+        [0, 0, 0, 0, 1, 1, 1, 1]+        preds++{- Linear regression with an irrelevant constant feature: the constant column is+   near-constant (variance 0) and must get a zero weight, while the informative+   coefficient is recovered. y = 3x + 4 exactly. -}+testLinearConstantFeatureZeroWeight :: Test+testLinearConstantFeatureZeroWeight = TestCase $ do+    let xs = [1, 2, 3, 4, 5, 6] :: [Double]+        df =+            D.fromNamedColumns+                [ ("x", DI.fromList xs)+                , ("const", DI.fromList (replicate 6 (5.0 :: Double)))+                , ("y", DI.fromList [3 * x + 4 | x <- xs])+                ]+        m = fit defaultLinearConfig (F.col @Double "y") df+        coefs = VU.toList (regCoef m)+        names = V.toList (regFeatureNames m)+        byName = zip names coefs+    -- find the const column weight; it must be ~0 (no information, possibly+    -- collinear with the intercept)+    case lookup "const" byName of+        Just w -> assertBool "constant feature gets ~zero weight" (close 1e-6 w 0)+        Nothing -> assertFailure "const column missing from feature names"+    let preds = interpD df (predict m)+        truth = [3 * x + 4 | x <- xs]+    assertBool+        "regression with constant feature still fits y=3x+4"+        (and (zipWith (close 1e-4) preds truth))++{- k-means with k > n: requesting more clusters than data points. The library+   clamps k = min k (max 1 n), so with 3 rows and kmK=10 we must get exactly 3+   centres (one per point), labels in range, and finite centres -- not a crash+   or empty/duplicate-degenerate centres. -}+testKMeansKGreaterThanN :: Test+testKMeansKGreaterThanN = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("a", DI.fromList ([0, 5, 10] :: [Double]))+                , ("b", DI.fromList ([0, 5, 10] :: [Double]))+                ]+        cfg = defaultKMeansConfig{kmK = 10, kmNInit = 3, kmSeed = 1}+        m = fit cfg [F.col @Double "a", F.col @Double "b"] df+    assertEqual "k clamped to n=3 centres" 3 (V.length (kmCenters m))+    let labels = VU.toList (kmLabels m)+    assertBool "all labels in [0,2]" (all (\l -> l >= 0 && l < 3) labels)+    assertBool+        "all centre coordinates finite"+        (all (VU.all finite) (V.toList (kmCenters m)))+    -- with 3 distinct points and 3 clusters, inertia must be ~0 (each point is+    -- its own centre)+    assertBool "k=n clustering has ~zero inertia" (close 1e-9 (kmInertia m) 0)++{- k-means on an all-identical (zero-variance) feature set: every row is the same+   point. Any clustering has inertia 0; centres must all equal that point and be+   finite (no NaN from dividing by an empty cluster or by a zero distance sum in+   k-means++ sampling). -}+testKMeansAllIdentical :: Test+testKMeansAllIdentical = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("a", DI.fromList (replicate 6 (4.0 :: Double)))+                , ("b", DI.fromList (replicate 6 (4.0 :: Double)))+                ]+        cfg = defaultKMeansConfig{kmK = 2, kmNInit = 3, kmSeed = 1}+        m = fit cfg [F.col @Double "a", F.col @Double "b"] df+    assertBool+        "centres finite on degenerate identical data"+        (all (VU.all finite) (V.toList (kmCenters m)))+    assertBool "inertia is 0 for identical points" (close 1e-12 (kmInertia m) 0)+    -- predict must still assign a valid (finite, in-range) label to every row+    let assigns = interpI df (predict m)+    assertBool "assignments in [0,1]" (all (\l -> l >= 0 && l < 2) assigns)++{- GMM with k > n: clamped like k-means (k = min k (max 1 n)). Two rows, gmmK=5+   must yield 2 components, finite weights summing to ~1, and a finite log+   likelihood -- not a crash or NaN. -}+testGMMKGreaterThanN :: Test+testGMMKGreaterThanN = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("a", DI.fromList ([0, 10] :: [Double]))+                , ("b", DI.fromList ([0, 10] :: [Double]))+                ]+        cfg = defaultGMMConfig{gmmK = 5, gmmSeed = 1}+        m = fit cfg [F.col @Double "a", F.col @Double "b"] df+    assertEqual "GMM k clamped to n=2" 2 (VU.length (gmmWeights m))+    assertBool "GMM weights finite" (VU.all finite (gmmWeights m))+    assertBool "GMM weights sum to ~1" (close 1e-6 (VU.sum (gmmWeights m)) 1)+    assertBool "GMM log-likelihood finite" (finite (gmmLogLikelihood m))++{- PCA on a single informative axis plus a constant column: the constant column+   carries zero variance, so its explained-variance ratio must be ~0 and the+   ratios must still sum to ~1 and stay finite. The first component should align+   with the varying axis. Catches a divide-by-zero in the ratio normalisation+   when one eigenvalue is 0. -}+testPCAConstantColumn :: Test+testPCAConstantColumn = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("x", DI.fromList ([-2, -1, 0, 1, 2] :: [Double]))+                , ("const", DI.fromList (replicate 5 (3.0 :: Double)))+                ]+        m =+            fit+                (PCAConfig (NComp 2) False)+                [F.col @Double "x", F.col @Double "const"]+                df+        ratio = VU.toList (pcaExplainedVarianceRatio m)+    assertBool "explained ratios finite" (all finite ratio)+    assertBool "explained ratios sum to ~1" (close 1e-9 (sum ratio) 1)+    -- the variance is entirely along x, so the top ratio must be ~1+    assertBool "first PC explains ~all variance" (close 1e-9 (head ratio) 1)+    -- projection exprs must evaluate to finite numbers+    let es = map snd (pcaExprs m)+    assertBool+        "pca exprs finite on constant column"+        (all (all finite . interpD df) es)++{- PCA on extreme-scale features (1e8-shifted) without standardisation: the+   covariance entries are ~1 (variance of {1,2,3}) despite the 1e8 offset.+   Components must stay unit-length and finite -- catches cancellation in the+   covariance / eigendecomposition at large magnitudes. -}+testPCAExtremeScale :: Test+testPCAExtremeScale = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("x", DI.fromList ([1e8 + 1, 1e8 + 2, 1e8 + 3, 1e8 + 4] :: [Double]))+                , ("y", DI.fromList ([1e8 + 1, 1e8 + 2, 1e8 + 3, 1e8 + 4] :: [Double]))+                ]+        m =+            fit+                (PCAConfig (NComp 1) False)+                [F.col @Double "x", F.col @Double "y"]+                df+        comp0 = pcaComponents m V.! 0+        nrm = sqrt (VU.sum (VU.map (^ (2 :: Int)) comp0))+    assertBool "component finite at 1e8 scale" (VU.all finite comp0)+    assertBool "component unit length at 1e8 scale" (close 1e-6 nrm 1)+    assertBool+        "explained ratio finite at 1e8 scale"+        (VU.all finite (pcaExplainedVarianceRatio m))++-- ===========================================================================+-- Category 7/8: linear-algebra degeneracy+-- ===========================================================================++{- choleskySolve on a non-positive-definite (zero) matrix returns Nothing rather+   than crashing or producing NaNs. A 2x2 all-zero matrix has a zero pivot. -}+testCholeskyNonPD :: Test+testCholeskyNonPD = TestCase $ do+    assertEqual+        "cholesky of singular zero matrix is Nothing"+        Nothing+        (choleskySolve (mat [[0, 0], [0, 0]]) (VU.fromList [1, 1]))++{- Jacobi eigendecomposition of a 1x1 matrix (one-column degenerate case):+   eigenvalue is the single entry, eigenvector is [1] (sign-canonicalised). -}+testJacobi1x1 :: Test+testJacobi1x1 = TestCase $ do+    let (ev, vecs) = jacobiEigenSym (mat [[5]])+    assertBool "1x1 eigenvalue is the entry" (close 1e-12 (ev VU.! 0) 5)+    assertBool "1x1 eigenvector is [1]" (close 1e-12 ((vecs V.! 0) VU.! 0) 1)++{- Jacobi on an identity matrix: both eigenvalues are exactly 1 and the result is+   finite (the rotation angle code must not divide by zero when off-diagonals are+   already 0). -}+testJacobiIdentity :: Test+testJacobiIdentity = TestCase $ do+    let (ev, vecs) = jacobiEigenSym (mat [[1, 0], [0, 1]])+    assertBool "identity eigenvalues both 1" (VU.all (close 1e-12 1) ev)+    assertBool "identity eigenvectors finite" (all (VU.all finite) (V.toList vecs))++tests :: [Test]+tests =+    [ testLogSumExpLargePositive+    , testLogSumExpLargeNegative+    , testLogSumExpSingleton+    , testSigmoidExtreme+    , testVarianceCatastrophicCancellation+    , testVarianceConstant+    , testVarianceSingleton+    , testCorrelationPerfect+    , testCorrelationConstantColumnIsNaN+    , testCorrelationTooFew+    , testLogisticProbsExtremeFeatures+    , testOLSOneRow+    , testLogisticSingleClass+    , testLogisticConstantFeatureIgnored+    , testLinearConstantFeatureZeroWeight+    , testKMeansKGreaterThanN+    , testKMeansAllIdentical+    , testGMMKGreaterThanN+    , testPCAConstantColumn+    , testPCAExtremeScale+    , testCholeskyNonPD+    , testJacobi1x1+    , testJacobiIdentity+    ]
+ tests/Learn/Ensembles.hs view
@@ -0,0 +1,159 @@+{-# 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.LinearSolver (defaultSolverConfig)+import DataFrame.Metrics (r2)+import DataFrame.ModelSelection++import Data.Maybe (isJust, isNothing)+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" (head assigns /= last 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+             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,121 @@+{-# 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.LinearSolver (defaultSolverConfig)+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)+    -- class 1: tp=2 (idx2,6), fp=1 (idx3) -> precision 2/3+    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)+    -- micro f1 == accuracy for single-label+    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)+    -- Show instances don't crash+    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+        -- the whole point: scaler <> pcaTransform compose as a monoid+        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,169 @@+{-# 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.LinearSolver (SolverConfig (..), defaultSolverConfig)+import DataFrame.PCA+import DataFrame.SVM+import DataFrame.Transform++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.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]++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))++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)++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+        -- fit model on scaled features, then compile scaling into the expr+        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+    , testRidgeShrinks+    , testLogistic+    , testSVC+    , testRegressionTree+    , testClassifierStats+    , testPCA+    , testKMeans+    , testTransformCompose+    ]
+ tests/Learn/NumericalRigor.hs view
@@ -0,0 +1,438 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Numerical-rigor suite: gradient checks (cat 4), reproducibility across the+stochastic models (cat 16), and statistical / distributional properties of the+RNG and splitters (cat 5). Every test is constructed to FAIL on a real bug — no+@assertEqual v v@, no rubber tolerances.+-}+module Learn.NumericalRigor (tests) where++import qualified DataFrame as D+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI++import DataFrame.GMM+import DataFrame.KMeans+import DataFrame.LinearSolver.Loss (+    SmoothLoss (..),+    logisticLoss,+    sigmoid,+    sqHingeLoss,+    squaredLoss,+ )+import DataFrame.Model (fit)+import DataFrame.ModelSelection (trainTestSplit)+import DataFrame.Random+import DataFrame.SVM.RFF++import qualified Data.Vector.Unboxed as VU+import Test.HUnit++-- ---------------------------------------------------------------------------+-- Independent reference loss VALUE functions.+--+-- The library only exposes the gradient 'slGradZ'; the per-loss scalar value is+-- reconstructed here straight from the module's own documented definitions, so+-- the finite difference below is computed INDEPENDENTLY of the analytic+-- gradient (anti-tautology: never finite-difference a gradient against itself).+-- ---------------------------------------------------------------------------++-- | @½ (z - y)²@  (squaredLoss).+squaredValue :: Double -> Double -> Double+squaredValue y z = 0.5 * (z - y) * (z - y)++-- | @log (1 + exp (-y z))@  (logisticLoss). softplus form, numerically stable.+logisticValue :: Double -> Double -> Double+logisticValue y z =+    let m = negate (y * z)+     in if m >= 0 then m + log1pExp (negate m) else log1pExp m+  where+    log1pExp x = log (1 + exp x)++-- | @(max 0 (1 - y z))²@  (sqHingeLoss).+sqHingeValue :: Double -> Double -> Double+sqHingeValue y z = let m = 1 - y * z in if m > 0 then m * m else 0++-- | Central finite difference of @f@ in @z@ at @(y, z)@.+centralDiff ::+    (Double -> Double -> Double) -> Double -> Double -> Double -> Double+centralDiff f eps y z = (f y (z + eps) - f y (z - eps)) / (2 * eps)++{- | Gradient-check one 'SmoothLoss' against its reference value fn over a grid+of @(y, z)@ points (both labels in @{ -1, +1 }@, many margins including the hinge+kink neighbourhood). Fails if the analytic gradient differs from the finite+difference anywhere beyond @tol@.+-}+gradCheck ::+    String ->+    SmoothLoss ->+    (Double -> Double -> Double) ->+    Double ->+    Test+gradCheck nm loss valueFn tol =+    TestCase $+        mapM_ check [(y, z) | y <- [-1, 1], z <- zs]+  where+    -- margins, deliberately avoiding the exact hinge kink z = y (m = 0) where+    -- the squared-hinge second derivative is discontinuous; central diff is+    -- still valid away from the kink.+    zs = [-3.7, -2.1, -1.25, -0.6, -0.15, 0.22, 0.55, 1.4, 1.9, 2.8, 4.3]+    check (y, z) = do+        let analytic = slGradZ loss y z+            fd = centralDiff valueFn 1e-6 y z+            ok = abs (analytic - fd) <= tol * (1 + abs fd)+        assertBool+            ( nm+                ++ " grad mismatch at (y="+                ++ show y+                ++ ", z="+                ++ show z+                ++ "): analytic="+                ++ show analytic+                ++ " finite-diff="+                ++ show fd+            )+            ok++-- | The squared-loss gradient @z - y@ checked against ½(z-y)².+testSquaredGradient :: Test+testSquaredGradient = gradCheck "squared" squaredLoss squaredValue 1e-5++-- | The logistic gradient @-y·σ(-y z)@ checked against log(1+exp(-yz)).+testLogisticGradient :: Test+testLogisticGradient = gradCheck "logistic" logisticLoss logisticValue 1e-5++-- | The squared-hinge gradient @-2y·max(0,1-yz)@ checked against (max 0 (1-yz))².+testSqHingeGradient :: Test+testSqHingeGradient = gradCheck "squared_hinge" sqHingeLoss sqHingeValue 1e-5++{- | Sign sanity that cannot be passed by a self-referential check: at a point+where the loss is strictly increasing in @z@ the analytic gradient must be+positive (and vice versa). Catches a flipped-sign gradient even if its+magnitude were somehow right. logistic with y=+1 decreases in z, so grad < 0;+squared with z>y increases, so grad > 0.+-}+testGradientSigns :: Test+testGradientSigns = TestCase $ do+    assertBool+        "squared: grad>0 when z>y (loss rising)"+        (slGradZ squaredLoss 1.0 3.0 > 0)+    assertBool+        "squared: grad<0 when z<y (loss falling)"+        (slGradZ squaredLoss 1.0 (-2.0) < 0)+    assertBool+        "logistic y=+1: grad<0 (more positive margin lowers loss)"+        (slGradZ logisticLoss 1.0 0.5 < 0)+    assertBool+        "logistic y=-1: grad>0"+        (slGradZ logisticLoss (-1.0) 0.5 > 0)+    assertBool+        "sqHinge active margin y=+1: grad<0"+        (slGradZ sqHingeLoss 1.0 0.0 < 0)+    assertBool+        "sqHinge satisfied margin: grad==0"+        (slGradZ sqHingeLoss 1.0 5.0 == 0)++-- ---------------------------------------------------------------------------+-- Statistical / distributional tests (cat 5).+--+-- Tolerances are CI-derived from the sample size; with N = 100k the standard+-- error of a uniform-mean estimate is ~ (1/sqrt12)/sqrt(N) ~ 8.3e-4, so a 6σ+-- band is ~ 5e-3. A broken sampler (e.g. one that returns the same value, or+-- biased) falls well outside.+-- ---------------------------------------------------------------------------++-- | Draw @n@ uniforms threading the generator; returns the sample list.+drawUniforms :: Int -> Gen -> [Double]+drawUniforms n g0 = go n g0 []+  where+    go 0 _ acc = acc+    go k g acc = let (x, g') = nextDouble g in go (k - 1) g' (x : acc)++mean :: [Double] -> Double+mean xs = sum xs / fromIntegral (length xs)++variance :: [Double] -> Double+variance xs =+    let m = mean xs+     in sum [(x - m) * (x - m) | x <- xs] / fromIntegral (length xs)++{- | @nextDouble@ is ~uniform on [0,1): mean ≈ 0.5 and variance ≈ 1/12, each+within a 6σ CI for 100k samples, AND every draw is in [0,1). A constant or+out-of-range sampler fails; a biased one (mean drifts) fails.+-}+testUniformDistribution :: Test+testUniformDistribution = TestCase $ do+    let n = 100000 :: Int+        xs = drawUniforms n (mkGen 20240613)+        m = mean xs+        v = variance xs+        seMean = (1 / sqrt 12) / sqrt (fromIntegral n)+    assertBool "all uniforms in [0,1)" (all (\x -> x >= 0 && x < 1) xs)+    assertBool+        ("uniform mean ~0.5, got " ++ show m)+        (abs (m - 0.5) <= 6 * seMean)+    assertBool+        ("uniform variance ~1/12, got " ++ show v)+        (abs (v - 1 / 12) <= 0.01)+    -- not a constant: spread across the unit interval+    assertBool "uniform actually varies" (maximum xs - minimum xs > 0.9)++{- | Box-Muller @gaussianVector@ produces standard normals: sample mean ≈ 0 and+variance ≈ 1 over 100k draws. SE of the mean is 1/sqrt(N) ~ 3.2e-3, so a 6σ+band ~ 2e-2. Catches a Box-Muller bug (wrong radius/angle, missing log) that+shifts the mean or scales the variance.+-}+testGaussianMoments :: Test+testGaussianMoments = TestCase $ do+    let n = 100000 :: Int+        (vec, _) = gaussianVector n (mkGen 777)+        xs = VU.toList vec+        m = mean xs+        v = variance xs+        seMean = 1 / sqrt (fromIntegral n)+    assertBool "gaussianVector length" (VU.length vec == n)+    assertBool+        "gaussian samples finite"+        (all (\x -> not (isNaN x) && not (isInfinite x)) xs)+    assertBool+        ("gaussian mean ~0, got " ++ show m)+        (abs m <= 6 * seMean)+    assertBool+        ("gaussian variance ~1, got " ++ show v)+        (abs (v - 1) <= 0.03)+    -- tail mass: ~4.55% should be |x|>2 for a true N(0,1); allow a wide band.+    let tail2 = fromIntegral (length (filter (\x -> abs x > 2) xs)) / fromIntegral n+    assertBool+        ("gaussian |x|>2 frequency ~0.0455, got " ++ show tail2)+        (abs (tail2 - 0.0455) <= 0.01)++{- | @trainTestSplit frac@ over many seeds: the realized train fraction matches+@frac@ within a binomial CI, and train+test always sums to the input row count+(cat 2 invariant). A split that loses/dupes rows, or ignores @frac@, fails.+-}+testSplitProportions :: Test+testSplitProportions = TestCase $ do+    let nRows = 4000+        frac = 0.7 :: Double+        df = D.fromNamedColumns [("x", DI.fromList [1 .. nRows :: Int])]+        seeds = [1 .. 25] :: [Int]+        -- binomial SE of the fraction for n=4000 ~ sqrt(0.7*0.3/4000) ~ 7.2e-3+        seFrac = sqrt (frac * (1 - frac) / fromIntegral nRows)+    mapM_+        ( \s -> do+            let (tr, te) = trainTestSplit frac s df+                nTr = fst (D.dimensions tr)+                nTe = fst (D.dimensions te)+                realized = fromIntegral nTr / fromIntegral nRows+            assertEqual+                ("split preserves rows (seed " ++ show s ++ ")")+                nRows+                (nTr + nTe)+            assertBool+                ( "split fraction ~"+                    ++ show frac+                    ++ " (seed "+                    ++ show s+                    ++ "), got "+                    ++ show realized+                )+                (abs (realized - frac) <= 5 * seFrac)+        )+        seeds++{- | k-means inertia on a clean, well-separated blob is stable across many+seeds and never beats the true within-cluster sum of squares of the generating+partition (the global optimum here is the obvious 2-cluster split). A broken+inertia (wrong distance, double counting) would report values below the optimum+or wildly seed-dependent.+-}+testKMeansInertiaStable :: Test+testKMeansInertiaStable = TestCase $ do+    let+        -- two tight gaussian-ish blobs around (0,0) and (10,10)+        as = [0, 0.1, -0.1, 0.05, -0.05, 10, 10.1, 9.9, 10.05, 9.95] :: [Double]+        bs = [0, -0.1, 0.1, 0.05, -0.05, 10, 9.9, 10.1, 9.95, 10.05] :: [Double]+        df = D.fromNamedColumns [("a", DI.fromList as), ("b", DI.fromList bs)]+        fitSeed s =+            kmInertia $+                fit+                    defaultKMeansConfig{kmK = 2, kmNInit = 1, kmSeed = s}+                    [F.col @Double "a", F.col @Double "b"]+                    df+        inertias = map fitSeed [0 .. 19]+        -- optimum: each blob's SS about its own mean. Blob means are ~ the+        -- centroid; compute the true within-cluster SS for the 2-cluster split.+        blob1 = take 5 (zip as bs)+        blob2 = zip (drop 5 as) (drop 5 bs)+        ssOf pts =+            let mx = sum (map fst pts) / 5+                my = sum (map snd pts) / 5+             in sum [(x - mx) ^ (2 :: Int) + (y - my) ^ (2 :: Int) | (x, y) <- pts]+        optimum = ssOf blob1 + ssOf blob2+        best = minimum inertias+        worst = maximum inertias+    assertBool+        "k-means inertia finite"+        (all (\i -> not (isNaN i) && not (isInfinite i)) inertias)+    assertBool+        ( "k-means inertia never below optimum ("+            ++ show optimum+            ++ "), best="+            ++ show best+        )+        (best >= optimum - 1e-9)+    assertBool+        ( "k-means inertia stable across seeds, best="+            ++ show best+            ++ " worst="+            ++ show worst+        )+        (worst - best <= 1e-6 + 1e-3 * optimum)++-- ---------------------------------------------------------------------------+-- Reproducibility tests (cat 16).+--+-- Each compares two SEPARATE fits with the same seed (determinism) AND, where+-- the seed is a real input, asserts that a DIFFERENT seed CAN change the model+-- (so "always returns a constant" wouldn't pass). Models that derive Eq are+-- compared with ==; others by a representative field or predicted expr.+-- ---------------------------------------------------------------------------++blobsDF :: D.DataFrame+blobsDF =+    D.fromNamedColumns+        [+            ( "a"+            , DI.fromList ([0, 0.2, -0.1, 0.1, 8, 8.1, 7.9, 8.2, 0.05, 8.05] :: [Double])+            )+        ,+            ( "b"+            , DI.fromList ([0, -0.1, 0.2, 0.0, 5, 5.2, 4.9, 5.1, 0.1, 5.05] :: [Double])+            )+        ]++-- | Many-cluster frame so k-means++ seeding genuinely depends on the seed.+spreadDF :: D.DataFrame+spreadDF =+    D.fromNamedColumns+        [ ("a", DI.fromList ([0, 1, 2, 10, 11, 12, 20, 21, 22, 30, 31, 32] :: [Double]))+        , ("b", DI.fromList ([0, 1, 0, 10, 11, 10, 0, 1, 0, 10, 11, 10] :: [Double]))+        ]++{- | k-means: same seed → identical KMeansModel (Eq derived); a different seed+on a multi-blob frame CAN yield different centers (the seed actually drives+k-means++ init). Single-init so the seed is not washed out by nInit restarts.+-}+testKMeansReproducible :: Test+testKMeansReproducible = TestCase $ do+    let cfg s = defaultKMeansConfig{kmK = 4, kmNInit = 1, kmMaxIter = 1, kmSeed = s}+        run s = fit (cfg s) [F.col @Double "a", F.col @Double "b"]+        a = run 1 spreadDF+        b = run 1 spreadDF+    assertEqual "k-means same seed identical model" a b+    -- different seeds CAN differ: scan several and require at least one diff in+    -- the initial centroids (1 iteration keeps the init's footprint).+    let centersFor s = kmCenters (run s spreadDF)+        base = centersFor 1+        anyDiffer = any (\s -> centersFor s /= base) [2, 3, 5, 7, 11, 13]+    assertBool "k-means: a different seed changes the model" anyDiffer++{- | GMM: same seed → identical GMMModel (Eq derived); a different seed CAN move+the fitted means (seed drives the responsibility init via sampleIndices).+-}+testGMMReproducible :: Test+testGMMReproducible = TestCase $ do+    let cfg s = defaultGMMConfig{gmmK = 2, gmmMaxIter = 1, gmmSeed = s}+        run s = fit (cfg s) [F.col @Double "a", F.col @Double "b"] blobsDF+        a = run 1+        b = run 1+    assertEqual "GMM same seed identical model" a b+    let meansFor s = gmmMeans (run s)+        base = meansFor 1+        anyDiffer = any (\s -> meansFor s /= base) [2, 3, 4, 5, 6, 7, 8]+    assertBool "GMM: a different seed changes the means" anyDiffer++{- | RFF-SVM: same seed → identical random projection AND identical fitted SVC+coefficients; a different seed changes the random Fourier features. The model+type only derives Show, so compare representative numeric fields directly.+-}+testRFFReproducible :: Test+testRFFReproducible = TestCase $ do+    let 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]))+                ]+        cfg s = defaultRFFConfig{rffD = 40, rffGamma = 0.2, rffSeed = s}+        run s = fit (cfg s) (F.col @Int "label") clsDF+        a = run 5+        b = run 5+    -- projection matrix and the fitted SVC coefficients must match bit-for-bit.+    assertEqual "RFF same seed: same projection B" (rffB a) (rffB b)+    assertEqual "RFF same seed: same coefficients" (rffCoef a) (rffCoef b)+    assertEqual "RFF same seed: same intercept" (rffIntercept a) (rffIntercept b)+    -- different seed: the random projection should differ.+    let projFor s = rffB (run s)+        base = projFor 5+        anyDiffer = any (\s -> projFor s /= base) [1, 2, 3, 6, 7, 8]+    assertBool "RFF: a different seed changes the projection" anyDiffer++-- ---------------------------------------------------------------------------+-- randomSplit / trainTestSplit determinism + row-count invariant (cat 16 + 2).+-- ---------------------------------------------------------------------------++{- | @trainTestSplit@ with the same seed is bit-identical (compared via the+prettyPrinted frames), preserves total rows, and a different seed CAN change the+partition.+-}+testSplitReproducible :: Test+testSplitReproducible = TestCase $ do+    let df = D.fromNamedColumns [("x", DI.fromList [1 .. 200 :: Int])]+        (tr1, te1) = trainTestSplit 0.6 42 df+        (tr2, te2) = trainTestSplit 0.6 42 df+    assertBool "trainTestSplit same seed: same train" (tr1 == tr2)+    assertBool "trainTestSplit same seed: same test" (te1 == te2)+    assertEqual+        "trainTestSplit preserves row count"+        200+        (fst (D.dimensions tr1) + fst (D.dimensions te1))+    let trainFor s = fst (trainTestSplit 0.6 s df)+        base = trainFor 42+        anyDiffer = any (\s -> trainFor s /= base) [1, 2, 3, 7, 99]+    assertBool "trainTestSplit: a different seed changes the partition" anyDiffer++-- ---------------------------------------------------------------------------+-- A self-consistency sanity for the helpers above so a broken reference value+-- fn cannot make the gradient checks vacuous: the reference squared value must+-- actually be ½(z-y)² at a known point.+-- ---------------------------------------------------------------------------+testReferenceValueSanity :: Test+testReferenceValueSanity = TestCase $ do+    assertBool+        "squaredValue at (y=2,z=5) == 4.5"+        (abs (squaredValue 2 5 - 4.5) < 1e-12)+    assertBool+        "logisticValue at (y=1,z=0) == log 2"+        (abs (logisticValue 1 0 - log 2) < 1e-12)+    assertBool "sqHingeValue at (y=1,z=0) == 1" (abs (sqHingeValue 1 0 - 1) < 1e-12)+    assertBool "sigmoid 0 == 0.5" (abs (sigmoid 0 - 0.5) < 1e-12)++tests :: [Test]+tests =+    [ testReferenceValueSanity+    , testSquaredGradient+    , testLogisticGradient+    , testSqHingeGradient+    , testGradientSigns+    , testUniformDistribution+    , testGaussianMoments+    , testSplitProportions+    , testKMeansInertiaStable+    , testKMeansReproducible+    , testGMMReproducible+    , testRFFReproducible+    , testSplitReproducible+    ]
+ tests/Learn/Numerics.hs view
@@ -0,0 +1,99 @@+{-# LANGUAGE ScopedTypeVariables #-}++module Learn.Numerics (tests) where++import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import DataFrame.Model (fit, predict)+import Test.HUnit++import DataFrame.LinearAlgebra+import DataFrame.LinearAlgebra.Eigen+import DataFrame.LinearAlgebra.Solve+import DataFrame.Random++mat :: [[Double]] -> Matrix+mat = V.fromList . map VU.fromList++approx :: Double -> Double -> Double -> Bool+approx tol a b = abs (a - b) <= tol++vApprox :: Double -> VU.Vector Double -> VU.Vector Double -> Bool+vApprox tol a b =+    VU.length a == VU.length b && VU.and (VU.zipWith (approx tol) a b)++testQR :: Test+testQR = TestCase $ do+    let a = mat [[1, 1], [1, 2], [1, 3], [1, 4]]+        b = VU.fromList [3, 5, 7, 9]+    case qrLeastSquares a b of+        Right x ->+            assertBool "QR recovers [1,2]" (vApprox 1e-9 x (VU.fromList [1, 2]))+        Left cols -> assertFailure ("unexpected rank deficiency: " ++ show cols)++testQRRankDeficient :: Test+testQRRankDeficient = TestCase $ do+    let a = mat [[1, 2], [2, 4], [3, 6]]+    case qrLeastSquares a (VU.fromList [1, 2, 3]) of+        Left _ -> pure ()+        Right _ -> assertFailure "expected rank deficiency on collinear columns"++testCholesky :: Test+testCholesky = TestCase $ do+    let a = mat [[4, 2], [2, 3]]+    case choleskySolve a (VU.fromList [2, 1]) of+        Just x ->+            assertBool "cholesky solves" (vApprox 1e-9 x (VU.fromList [0.5, 0]))+        Nothing -> assertFailure "expected PD"+    assertEqual "non-PD rejected" Nothing (cholesky (mat [[1, 2], [2, 1]]))++testJacobi :: Test+testJacobi = TestCase $ do+    let (ev, vecs) = jacobiEigenSym (mat [[2, 1], [1, 2]])+    assertBool "eigenvalues [3,1]" (vApprox 1e-9 ev (VU.fromList [3, 1]))+    let v0 = vecs V.! 0+        recon = matVec (mat [[2, 1], [1, 2]]) v0+    assertBool+        "A v = lambda v"+        (vApprox 1e-9 recon (scaleV (ev VU.! 0) v0))++testPowerIter :: Test+testPowerIter = TestCase $ do+    let (lam, _) = powerIterTop 200 (mat [[2, 1], [1, 2]])+    assertBool "dominant eigenvalue ~3" (approx 1e-6 lam 3)++testLogSumExp :: Test+testLogSumExp = TestCase $ do+    let xs = VU.fromList [1000, 1001, 1002]+        lse = logSumExp xs+    assertBool "logSumExp stable, finite" (not (isNaN lse) && not (isInfinite lse))+    assertBool "logSumExp >= max" (lse >= 1002)++testRngDeterminism :: Test+testRngDeterminism = TestCase $ do+    let (a, _) = shuffleInts 50 (mkGen 11)+        (b, _) = shuffleInts 50 (mkGen 11)+    assertEqual "same seed same shuffle" a b+    assertBool+        "is a permutation"+        (VU.toList (VU.modify (const (pure ())) a) /= [] && all (`VU.elem` a) [0 .. 49])++testRngRange :: Test+testRngRange = TestCase $ do+    let go 0 _ acc = acc+        go k g acc =+            let (x, g') = nextIntR (3, 7) g in go (k - 1 :: Int) g' (x : acc)+        xs = go 1000 (mkGen 5) []+    assertBool "ints within range" (all (\x -> x >= 3 && x <= 7) xs)++tests :: [Test]+tests =+    [ testQR+    , testQRRankDeficient+    , testCholesky+    , testJacobi+    , testPowerIter+    , testLogSumExp+    , testRngDeterminism+    , testRngRange+    ]
+ tests/Learn/SklearnParity.hs view
@@ -0,0 +1,188 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Parity tests against scikit-learn on clean Kaggle-style datasets. The+reference values in @data/ml/golden.json@ are produced by+@scripts/gen_sklearn_golden.py@. Closed-form models (OLS, ridge, PCA) are held to+tight coefficient parity; iterative models to an accuracy/inertia 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.LinearSolver (defaultSolverConfig)+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/Symbolic.hs view
@@ -0,0 +1,134 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Learn.Symbolic (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.PCA.Kernel+import DataFrame.SVM.RFF+import DataFrame.SymbolicRegression+import DataFrame.SymbolicRegression.Expr+import DataFrame.SymbolicRegression.Optimize (meanSquaredError)+import DataFrame.SymbolicRegression.Simplify (simplify)++import qualified Data.Vector as V+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)++testSRRecovers :: Test+testSRRecovers = TestCase $ do+    let xs = [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6] :: [Double]+        df =+            D.fromNamedColumns+                [ ("x", DI.fromList xs)+                , ("y", DI.fromList [x * x + x | x <- xs])+                ]+        m =+            fit+                defaultSRConfig+                    { srSeed = 3+                    , srGenerations = 60+                    , srPopSize = 300+                    , srUnaryOps = []+                    }+                (F.col @Double "y")+                df+        preds = interpD df (srBest m)+        truth = interpD df (F.col @Double "y")+        err = sum (zipWith (\p t -> (p - t) ^ (2 :: Int)) preds truth) / 10+    assertBool "SR recovers x*x+x to low error" (err < 1e-6)+    assertBool "Pareto front non-empty" (not (null (srPareto m)))++testSRDeterminism :: Test+testSRDeterminism = TestCase $ do+    let xs = [1 .. 8] :: [Double]+        df =+            D.fromNamedColumns+                [ ("x", DI.fromList xs)+                , ("y", DI.fromList (map (\x -> 2 * x + 1) xs))+                ]+        run =+            fit+                defaultSRConfig{srSeed = 9, srGenerations = 20}+                (F.col @Double "y")+                df+    assertEqual "SR deterministic best MSE" (srBestMSE run) (srBestMSE (rerun df))+  where+    rerun =+        fit+            defaultSRConfig{srSeed = 9, srGenerations = 20}+            (F.col @Double "y")++testSimplifyPreservesEval :: Test+testSimplifyPreservesEval = TestCase $ do+    let feats = V.singleton (VU.fromList [1, 2, 3, 4 :: Double])+        n = 4+        e = SBin SAdd (SBin SMul (SVar 0) (SConst 1)) (SConst 0)+        target = VU.fromList [1, 2, 3, 4]+    assertBool+        "simplify preserves evaluation"+        ( abs+            (meanSquaredError feats n target e - meanSquaredError feats n target (simplify e))+            < 1e-12+        )+    assertBool "simplify is size non-increasing" (srSize (simplify e) <= srSize e)+    assertEqual "simplify idempotent" (simplify e) (simplify (simplify e))++testKernelPCA :: Test+testKernelPCA = TestCase $ do+    let df =+            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]))+                ]+        m =+            fit+                defaultKernelPCAConfig{kpcaNComponents = 2, kpcaNLandmarks = 8, kpcaSeed = 1}+                [F.col @Double "a", F.col @Double "b"]+                df+        pc1 = interpD df (snd (head (kernelPCAExprs m)))+    assertBool "kPCA finite" (not (any isNaN pc1))+    assertBool+        "kPCA first component separates blobs"+        (signum (head pc1) /= signum (last pc1))++testRFFSVM :: Test+testRFFSVM = TestCase $ do+    let df =+            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]))+                ]+        m =+            fit+                defaultRFFConfig{rffD = 80, rffGamma = 0.2, rffSeed = 2}+                (F.col @Int "label")+                df+        preds = interpI df (predict m)+    assertEqual "RFF SVM separates" [0, 0, 0, 0, 1, 1, 1, 1] preds++tests :: [Test]+tests =+    [ testSRRecovers+    , testSRDeterminism+    , testSimplifyPreservesEval+    , testKernelPCA+    , testRFFSVM+    ]
+ 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/LinearSolver.hs view
@@ -0,0 +1,828 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module LinearSolver where++import qualified DataFrame as D+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.Expression (Expr (..), getColumns)+import DataFrame.Internal.Interpreter (interpret)+import DataFrame.LinearSolver++import Data.List (sort)+import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import System.Random (StdGen, mkStdGen, randomR)+import Test.HUnit++------------------------------------------------------------------------+-- Test fixtures and helpers+------------------------------------------------------------------------++-- Generate n points with d features, each value uniform in [-1, 1], from a seed.+syntheticPoints :: Int -> Int -> Int -> V.Vector (VU.Vector Double)+syntheticPoints seed n d =+    let (rows, _) = foldr step ([], mkStdGen seed) [1 .. n]+     in V.fromList (take n rows)+  where+    step _ (acc, g) =+        let (row, g') = genRow d g+         in (row : acc, g')+    genRow k g0 = go k g0 []+      where+        go 0 g xs = (VU.fromList (reverse xs), g)+        go i g xs =+            let (v, g') = randomR (-1.0 :: Double, 1.0) g+             in go (i - 1) g' (v : xs)++-- Label each row by sign(w . x + b); +1 if score > 0, else -1.+labelsForHyperplane ::+    V.Vector (VU.Vector Double) ->+    VU.Vector Double ->+    Double ->+    VU.Vector Double+labelsForHyperplane rows w b =+    VU.generate+        (V.length rows)+        ( \i ->+            let score = dotProduct w (rows V.! i) + b+             in if score > 0 then 1 else -1+        )++-- Cosine similarity between two non-zero vectors.+cosineSim :: VU.Vector Double -> VU.Vector Double -> Double+cosineSim u v =+    let nu = sqrt (dotProduct u u)+        nv = sqrt (dotProduct v v)+     in if nu == 0 || nv == 0 then 0 else dotProduct u v / (nu * nv)++-- Predict +1 or -1 from a fitted LinearModel.+predict :: LinearModel -> VU.Vector Double -> Double+predict m x =+    let score = dotProduct (lmWeights m) x + lmIntercept m+     in if score > 0 then 1 else -1++-- Predict directly on standardized features (skipping de-standardization).+predictStandardized :: VU.Vector Double -> Double -> VU.Vector Double -> Double+predictStandardized w b x =+    if dotProduct w x + b > 0 then 1 else -1++-- Average binary logistic loss at (w, b).+logisticLoss ::+    V.Vector (VU.Vector Double) ->+    VU.Vector Double ->+    VU.Vector Double ->+    Double ->+    Double+logisticLoss features labels w b =+    let n = V.length features+        loss i =+            let yi = labels VU.! i+                row = features V.! i+                margin = yi * (dotProduct w row + b)+             in -- log(1 + exp(-margin)), numerically stable+                if margin >= 0+                    then log (1 + exp (-margin))+                    else (-margin) + log (1 + exp margin)+     in sum [loss i | i <- [0 .. n - 1]] / fromIntegral n++------------------------------------------------------------------------+-- A1: Recover known hyperplane with no L1+------------------------------------------------------------------------++testA1RecoverHyperplane :: Test+testA1RecoverHyperplane = TestCase $ do+    let groundTruth = VU.fromList [0.7, -0.5]+        groundBias = 0.3+        rows = syntheticPoints 1 200 2+        labels = labelsForHyperplane rows groundTruth groundBias+        cfg =+            defaultSolverConfig+                { scL1Lambda = 0+                , scL2Lambda = 0+                , scMaxIter = 500+                , scTol = 1e-6+                }+        model = fitL1Logistic cfg rows labels (V.fromList ["x", "y"])+        cosSim = cosineSim (lmWeights model) groundTruth+        sameSignAll =+            all+                (\i -> predict model (rows V.! i) == labels VU.! i)+                [0 .. V.length rows - 1]+    assertBool+        ("recovered weights should align with ground truth (cos = " ++ show cosSim ++ ")")+        (cosSim > 0.99)+    assertBool "all training points predicted correctly" sameSignAll++------------------------------------------------------------------------+-- A2: L1 produces sparse weights+------------------------------------------------------------------------++testA2L1Sparsity :: Test+testA2L1Sparsity = TestCase $ do+    -- 10 features, only feature 1 and feature 4 carry signal.+    let groundTruth = VU.fromList [0, 1.2, 0, 0, -1.5, 0, 0, 0, 0, 0]+        groundBias = 0+        rows = syntheticPoints 7 500 10+        labels = labelsForHyperplane rows groundTruth groundBias+        cfg =+            defaultSolverConfig+                { scL1Lambda = 0.1+                , scL2Lambda = 0+                , scMaxIter = 500+                , scTol = 1e-6+                }+        names = V.fromList [T.pack ("f" ++ show i) | i <- [0 .. 9 :: Int]]+        model = fitL1Logistic cfg rows labels names+        ws = VU.toList (lmWeights model)+        nonZeroIdxs = [i | (i, w) <- zip [0 :: Int ..] ws, w /= 0]+        zeroIdxs = [i | (i, w) <- zip [0 :: Int ..] ws, w == 0]+    assertBool+        ( "informative feature 1 should have non-zero weight (got "+            ++ show (ws !! 1)+            ++ ")"+        )+        (ws !! 1 /= 0)+    assertBool+        ( "informative feature 4 should have non-zero weight (got "+            ++ show (ws !! 4)+            ++ ")"+        )+        (ws !! 4 /= 0)+    -- Of the 8 noise features (indices 0,2,3,5,6,7,8,9), expect at least 6 to be 0.+    let noiseFeatures = [0, 2, 3, 5, 6, 7, 8, 9] :: [Int]+        noiseZero = length [i | i <- noiseFeatures, i `elem` zeroIdxs]+    assertBool+        ( "at least 6 noise features zeroed (got "+            ++ show noiseZero+            ++ "; non-zero idxs = "+            ++ show nonZeroIdxs+            ++ ")"+        )+        (noiseZero >= 6)++------------------------------------------------------------------------+-- A3: Convergence on well-conditioned input+------------------------------------------------------------------------++testA3Convergence :: Test+testA3Convergence = TestCase $ do+    let groundTruth = VU.fromList [1.0, -0.5, 0.7]+        rows = syntheticPoints 2 300 3+        labels = labelsForHyperplane rows groundTruth 0+        cfg =+            defaultSolverConfig+                { scL1Lambda = 0.01+                , scL2Lambda = 0+                , scMaxIter = 1000+                , scTol = 1e-5+                }+        model = fitL1Logistic cfg rows labels (V.fromList ["a", "b", "c"])+        -- Loss at the fitted model+        (rowsStd, _, _, _) = standardize rows+        ws = lmWeights model+        b = lmIntercept model+        -- Re-standardize the weights for loss comparison on standardized data+        loss0 = logisticLoss rowsStd labels (VU.replicate 3 0) 0+        -- Fit gives raw weights; compute loss on raw rows+        lossFit = logisticLoss rows labels ws b+    assertBool+        ( "loss decreased from initial (initial="+            ++ show loss0+            ++ ", final="+            ++ show lossFit+            ++ ")"+        )+        (lossFit < loss0)++------------------------------------------------------------------------+-- A4: Final loss <= initial loss (monotone or near-monotone in FISTA)+------------------------------------------------------------------------++testA4LossNotIncreasing :: Test+testA4LossNotIncreasing = TestCase $ do+    let groundTruth = VU.fromList [0.8, 0.4]+        rows = syntheticPoints 3 100 2+        labels = labelsForHyperplane rows groundTruth 0+        cfg = defaultSolverConfig{scL1Lambda = 0.05, scL2Lambda = 0, scMaxIter = 100}+        model = fitL1Logistic cfg rows labels (V.fromList ["x", "y"])+        loss0 = logisticLoss rows labels (VU.replicate 2 0) 0+        lossFit = logisticLoss rows labels (lmWeights model) (lmIntercept model)+    assertBool+        ( "final loss must be <= initial loss (l0="+            ++ show loss0+            ++ ", lf="+            ++ show lossFit+            ++ ")"+        )+        (lossFit <= loss0 + 1e-9)++------------------------------------------------------------------------+-- A5: Degenerate input — all labels +1+------------------------------------------------------------------------++testA5AllSameDirection :: Test+testA5AllSameDirection = TestCase $ do+    let rows = syntheticPoints 4 50 3+        labels = VU.replicate 50 1.0+        cfg = defaultSolverConfig{scL1Lambda = 0.01, scL2Lambda = 0, scMaxIter = 100}+        model = fitL1Logistic cfg rows labels (V.fromList ["a", "b", "c"])+        ws = VU.toList (lmWeights model)+        b = lmIntercept model+        anyNaN = any isNaN ws || isNaN b+        anyInf = any isInfinite ws || isInfinite b+        allPositive = all (\i -> predict model (rows V.! i) == 1) [0 .. V.length rows - 1]+    assertBool "no NaN in weights/intercept" (not anyNaN)+    assertBool "no Inf in weights/intercept" (not anyInf)+    assertBool+        "all-same labels should produce a positive-predicting model"+        allPositive++------------------------------------------------------------------------+-- A6: Degenerate — empty input+------------------------------------------------------------------------++testA6Empty :: Test+testA6Empty = TestCase $ do+    let cfg = defaultSolverConfig+        emptyRows = V.empty :: V.Vector (VU.Vector Double)+        emptyLabels = VU.empty :: VU.Vector Double+        names = V.fromList ["a", "b"]+        model = fitL1Logistic cfg emptyRows emptyLabels names+    assertEqual+        "empty input -> 2 zero weights"+        (VU.fromList [0, 0])+        (lmWeights model)+    assertEqual "empty input -> zero intercept" 0 (lmIntercept model)++------------------------------------------------------------------------+-- A7: Degenerate — constant feature+------------------------------------------------------------------------++testA7ConstantFeature :: Test+testA7ConstantFeature = TestCase $ do+    -- Feature 1 is informative (uniform in [-1,1]); feature 0 is constant at 0.5.+    let baseRows = syntheticPoints 5 100 1+        rows =+            V.map+                (\row -> VU.fromList (0.5 : VU.toList row))+                baseRows+        groundTruth = VU.fromList [0.0, 1.0] -- only feature 1 matters+        labels = labelsForHyperplane rows groundTruth 0+        cfg =+            defaultSolverConfig+                { scL1Lambda = 0.01+                , scL2Lambda = 0+                , scMaxIter = 300+                , scTol = 1e-6+                }+        model = fitL1Logistic cfg rows labels (V.fromList ["constant", "signal"])+        ws = VU.toList (lmWeights model)+        anyBad = any (\x -> isNaN x || isInfinite x) ws+    assertBool+        ("constant feature weight ~ 0 (got " ++ show (head ws) ++ ")")+        (abs (head ws) < 1e-6)+    assertBool+        ("signal feature non-zero (got " ++ show (ws !! 1) ++ ")")+        (ws !! 1 /= 0)+    assertBool "no NaN/Inf" (not anyBad)++------------------------------------------------------------------------+-- A8: Numerical stability with large feature values+------------------------------------------------------------------------++testA8LargeValues :: Test+testA8LargeValues = TestCase $ do+    let scale = 1000.0 :: Double+        baseRows = syntheticPoints 6 100 2+        rows = V.map (VU.map (* scale)) baseRows+        groundTruth = VU.fromList [0.5, -0.7]+        labels = labelsForHyperplane rows groundTruth 0+        cfg = defaultSolverConfig{scL1Lambda = 0.01, scL2Lambda = 0, scMaxIter = 300}+        model = fitL1Logistic cfg rows labels (V.fromList ["x", "y"])+        ws = VU.toList (lmWeights model)+        b = lmIntercept model+        anyBad = any (\x -> isNaN x || isInfinite x) (b : ws)+        sameSigns =+            length+                [ () | i <- [0 .. V.length rows - 1], predict model (rows V.! i) == labels VU.! i+                ]+    assertBool "no NaN/Inf with scaled features" (not anyBad)+    assertBool+        ( "should correctly classify the vast majority of rows ("+            ++ show sameSigns+            ++ "/100)"+        )+        (sameSigns >= 90)++------------------------------------------------------------------------+-- A9: Standardization round-trip — recovered weights point in the true+-- direction even when raw-feature scales differ by orders of magnitude.+-- A broken de-standardization formula would scramble the per-feature scale+-- of @wRaw@ and the cosine to ground truth would drop sharply.+------------------------------------------------------------------------++testA9StandardizationRoundTrip :: Test+testA9StandardizationRoundTrip = TestCase $ do+    let nRows = 80 :: Int+        -- Column 0 ranges 0..400 (mean ~200, std ~115).+        -- Column 1 ranges 0..0.2 (mean ~0.1, std ~0.058).+        -- True hyperplane:  (col0 - 200) + 1000 * (col1 - 0.1)  > 0+        -- True raw weights (modulo positive scaling):  [1.0, 1000.0]+        col0 = [fromIntegral i * 5 :: Double | i <- [0 .. nRows - 1]]+        col1 = [fromIntegral i * 0.0025 :: Double | i <- [0 .. nRows - 1]]+        rows = V.fromList [VU.fromList [c0, c1] | (c0, c1) <- zip col0 col1]+        labels =+            VU.fromList+                [ if (c0 - 200) + 1000 * (c1 - 0.1) > 0 then 1.0 else -1.0+                | (c0, c1) <- zip col0 col1+                ]+        cfg =+            defaultSolverConfig+                { scL1Lambda = 1.0e-4+                , scL2Lambda = 0+                , scMaxIter = 2000+                , scTol = 1.0e-7+                }+        model = fitL1Logistic cfg rows labels (V.fromList ["x", "y"])+        truthDir = VU.fromList [1.0, 1000.0]+        cs = cosineSim (lmWeights model) truthDir+        -- All training points correctly classified+        trainPreds =+            [predict model (rows V.! i) | i <- [0 .. nRows - 1]]+        trainLabs =+            [labels VU.! i | i <- [0 .. nRows - 1]]+        correct =+            length+                [() | (p, l) <- zip trainPreds trainLabs, p == l]+    assertEqual "all training points correctly classified" nRows correct+    assertBool+        ( "recovered raw weights align with ground-truth direction across "+            ++ "vastly different feature scales (cos = "+            ++ show cs+            ++ ")"+        )+        (cs > 0.95)++------------------------------------------------------------------------+-- A10: Determinism — same input -> same output+------------------------------------------------------------------------++testA10Determinism :: Test+testA10Determinism = TestCase $ do+    let groundTruth = VU.fromList [0.6, 0.4]+        rows = syntheticPoints 9 60 2+        labels = labelsForHyperplane rows groundTruth 0+        cfg = defaultSolverConfig{scL1Lambda = 0.05, scL2Lambda = 0, scMaxIter = 200}+        m1 = fitL1Logistic cfg rows labels (V.fromList ["x", "y"])+        m2 = fitL1Logistic cfg rows labels (V.fromList ["x", "y"])+    assertEqual "same input -> same weights" (lmWeights m1) (lmWeights m2)+    assertEqual "same input -> same intercept" (lmIntercept m1) (lmIntercept m2)++------------------------------------------------------------------------+-- A11: Two-feature ground truth recovery (w_2/w_1 ratio)+------------------------------------------------------------------------++testA11GroundTruthRatio :: Test+testA11GroundTruthRatio = TestCase $ do+    -- y = sign(x1 + 2*x2 - 3); pull from a larger range so a non-zero intercept matters.+    let groundTruth = VU.fromList [1.0, 2.0]+        groundBias = -3.0+        n = 500+        baseRows = syntheticPoints 10 n 2+        -- Scale up so x_i can range over [-3, 3] -- gives wider coverage of the boundary+        rows = V.map (VU.map (* 3)) baseRows+        labels = labelsForHyperplane rows groundTruth groundBias+        cfg =+            defaultSolverConfig+                { scL1Lambda = 0.001+                , scL2Lambda = 0+                , scMaxIter = 1000+                , scTol = 1e-7+                }+        model = fitL1Logistic cfg rows labels (V.fromList ["x", "y"])+        ws = lmWeights model+        b = lmIntercept model+        ratio = (ws VU.! 1) / (ws VU.! 0)+        biasRatio = b / (ws VU.! 0)+    assertBool+        ("w2/w1 should approximate 2.0 (got " ++ show ratio ++ ")")+        (ratio > 1.7 && ratio < 2.3)+    assertBool+        ("b/w1 should approximate -3.0 (got " ++ show biasRatio ++ ")")+        (biasRatio > -3.4 && biasRatio < -2.6)++------------------------------------------------------------------------+-- B1: modelToExpr produces a well-typed Expr Bool+------------------------------------------------------------------------++testB1ExprWellTyped :: Test+testB1ExprWellTyped = TestCase $ do+    let model =+            LinearModel+                { lmWeights = VU.fromList [1.0, -2.0]+                , lmIntercept = 0.5+                , lmFeatureNames = V.fromList ["x", "y"]+                }+        expr = modelToExpr model+        -- Evaluate on a 3-row DataFrame+        df =+            D.fromNamedColumns+                [ ("x", DI.fromList ([0.0, 1.0, 2.0] :: [Double]))+                , ("y", DI.fromList ([0.0, 0.0, 5.0] :: [Double]))+                ]+        -- Manual predictions: 1*x - 2*y + 0.5 > 0 ?+        manual =+            [ (1.0 * 0.0 - 2.0 * 0.0 + 0.5) > 0+            , (1.0 * 1.0 - 2.0 * 0.0 + 0.5) > 0+            , (1.0 * 2.0 - 2.0 * 5.0 + 0.5) > 0+            ]+    case interpret @Bool df expr of+        Left e -> assertFailure ("interpret failed: " ++ show e)+        Right (DI.TColumn col) -> case DI.toVector @Bool col of+            Left e -> assertFailure ("toVector failed: " ++ show e)+            Right vals ->+                assertEqual "Expr matches manual evaluation" manual (V.toList vals)++------------------------------------------------------------------------+-- B2: Zero weights are dropped from the resulting Expr+------------------------------------------------------------------------++testB2ZeroWeightsPruned :: Test+testB2ZeroWeightsPruned = TestCase $ do+    let model =+            LinearModel+                { lmWeights = VU.fromList [0.0, 1.5, 0.0]+                , lmIntercept = 0.0+                , lmFeatureNames = V.fromList ["a", "b", "c"]+                }+        expr = modelToExpr model+        cols = sort (getColumns expr)+    assertEqual "only column b appears in the Expr" ["b"] cols++------------------------------------------------------------------------+-- A14: Constant feature at large raw value — weight must be exactly 0+-- and no NaN/Inf leaks into the rest of the fit.+------------------------------------------------------------------------++testA14ConstantHugeValue :: Test+testA14ConstantHugeValue = TestCase $ do+    let baseRows = syntheticPoints 14 100 1 -- one informative feature+    -- Prepend a constant column at 1e8 to each row.+        rows =+            V.map+                (\row -> VU.fromList (1.0e8 : VU.toList row))+                baseRows+        -- Label depends only on the informative (second) feature.+        labels = labelsForHyperplane rows (VU.fromList [0.0, 1.0]) 0+        cfg = defaultSolverConfig{scL1Lambda = 0.01, scL2Lambda = 0, scMaxIter = 300}+        model = fitL1Logistic cfg rows labels (V.fromList ["constant", "signal"])+        ws = VU.toList (lmWeights model)+        b = lmIntercept model+        anyBad = any (\v -> isNaN v || isInfinite v) (b : ws)+    assertBool "no NaN/Inf with constant-at-1e8 feature" (not anyBad)+    assertEqual+        "constant feature is dropped — weight is exactly zero"+        0+        (head ws)+    assertBool+        ("signal feature has non-zero weight (got " ++ show (ws !! 1) ++ ")")+        (ws !! 1 /= 0)++------------------------------------------------------------------------+-- A15: Variance exactly zero (all rows identical for that column).+------------------------------------------------------------------------++testA15AllZeroFeature :: Test+testA15AllZeroFeature = TestCase $ do+    -- A column that is exactly 0 for every row.+    let baseRows = syntheticPoints 15 80 1+        rows =+            V.map+                (\row -> VU.fromList (0.0 : VU.toList row))+                baseRows+        labels = labelsForHyperplane rows (VU.fromList [0.0, 1.0]) 0+        cfg = defaultSolverConfig{scL1Lambda = 0.01, scL2Lambda = 0, scMaxIter = 300}+        model = fitL1Logistic cfg rows labels (V.fromList ["zero", "signal"])+        ws = VU.toList (lmWeights model)+    assertEqual "zero-variance column has weight zero" 0 (head ws)+    assertBool ("signal weight non-zero (" ++ show (ws !! 1) ++ ")") (ws !! 1 /= 0)++------------------------------------------------------------------------+-- A16: Severely imbalanced labels (99:1) — should not collapse to a+-- constant predictor on the majority class without some learning.+------------------------------------------------------------------------++testA16ImbalancedLabels :: Test+testA16ImbalancedLabels = TestCase $ do+    let nPos = 99+        nNeg = 1+        n = nPos + nNeg+        rows = syntheticPoints 16 n 2+        labels =+            VU.fromList+                (replicate nPos 1.0 ++ replicate nNeg (-1.0))+        cfg = defaultSolverConfig{scL1Lambda = 0.01, scL2Lambda = 0, scMaxIter = 500}+        model = fitL1Logistic cfg rows labels (V.fromList ["x", "y"])+        ws = VU.toList (lmWeights model)+        b = lmIntercept model+        anyBad = any (\v -> isNaN v || isInfinite v) (b : ws)+    assertBool "no NaN/Inf with 99:1 imbalance" (not anyBad)+    -- The intercept should be positive (the easy thing for the model is to+    -- predict the majority); weights may or may not be zero depending on lambda.+    assertBool ("intercept favors majority class (got b=" ++ show b ++ ")") (b > 0)++------------------------------------------------------------------------+-- A17: Mixed per-feature raw scales — should not diverge.+------------------------------------------------------------------------++testA17ImbalancedRawScales :: Test+testA17ImbalancedRawScales = TestCase $ do+    let baseRows = syntheticPoints 17 100 3+        -- Per-row: [1e-6 * v, v, 1e6 * v] — three columns with vastly+        -- different scales but the same underlying signal.+        rows =+            V.map+                ( \row ->+                    let v0 = row VU.! 0+                        v1 = row VU.! 1+                        v2 = row VU.! 2+                     in VU.fromList [1.0e-6 * v0, v1, 1.0e6 * v2]+                )+                baseRows+        labels = labelsForHyperplane baseRows (VU.fromList [1.0, -0.5, 0.7]) 0+        cfg = defaultSolverConfig{scL1Lambda = 1.0e-4, scL2Lambda = 0, scMaxIter = 500}+        model = fitL1Logistic cfg rows labels (V.fromList ["tiny", "unit", "huge"])+        ws = VU.toList (lmWeights model)+        b = lmIntercept model+        anyBad = any (\v -> isNaN v || isInfinite v) (b : ws)+    assertBool ("no NaN/Inf with mixed scales (ws=" ++ show ws ++ ")") (not anyBad)+    -- The fit should classify the training points correctly on aggregate.+    let preds = [predict model (rows V.! i) | i <- [0 .. V.length rows - 1]]+        lbls = [labels VU.! i | i <- [0 .. VU.length labels - 1]]+        correct = length [() | (p, l) <- zip preds lbls, p == l]+    -- The wild per-feature scales make the problem poorly conditioned for+    -- L1-regularized FISTA with a fixed Lipschitz upper bound. We don't+    -- expect optimal accuracy — the assertion is "not random" (>=65%),+    -- catching divergence-to-garbage rather than guaranteeing fit quality.+    assertBool+        ("non-divergent under wild scales (got " ++ show correct ++ "/100)")+        (correct >= 65)++------------------------------------------------------------------------+-- A12: maxIter = 0 returns the initial point unchanged+------------------------------------------------------------------------++testA12MaxIterZero :: Test+testA12MaxIterZero = TestCase $ do+    let rows = syntheticPoints 20 50 2+        labels = labelsForHyperplane rows (VU.fromList [1.0, -0.5]) 0+        cfg = defaultSolverConfig{scMaxIter = 0}+        model = fitL1Logistic cfg rows labels (V.fromList ["x", "y"])+    assertEqual+        "maxIter=0 returns zero weights"+        (VU.fromList [0, 0])+        (lmWeights model)+    assertEqual "maxIter=0 returns zero intercept" 0 (lmIntercept model)++------------------------------------------------------------------------+-- A13: maxIter = 1 takes exactly one prox step (results differ from+-- the initial zero point but may not be near the optimum).+------------------------------------------------------------------------++testA13MaxIterOne :: Test+testA13MaxIterOne = TestCase $ do+    let rows = syntheticPoints 21 80 2+        labels = labelsForHyperplane rows (VU.fromList [1.0, -0.5]) 0+        cfg = defaultSolverConfig{scMaxIter = 1, scL1Lambda = 0.001, scL2Lambda = 0}+        cfg0 = cfg{scMaxIter = 0}+        m1 = fitL1Logistic cfg rows labels (V.fromList ["x", "y"])+        m0 = fitL1Logistic cfg0 rows labels (V.fromList ["x", "y"])+        anyNonZero v = not (VU.all (== 0) v)+    -- maxIter=0 returns zeros+    assertEqual "baseline m0 weights are zero" (VU.fromList [0, 0]) (lmWeights m0)+    -- maxIter=1 differs from maxIter=0 (one step actually happened)+    assertBool+        ("maxIter=1 must change at least one weight (got " ++ show (lmWeights m1) ++ ")")+        (anyNonZero (lmWeights m1) || lmIntercept m1 /= 0)+    -- Final value is finite+    let badW = VU.any (\x -> isNaN x || isInfinite x) (lmWeights m1)+        badB = isNaN (lmIntercept m1) || isInfinite (lmIntercept m1)+    assertBool "no NaN/Inf after one iteration" (not (badW || badB))++------------------------------------------------------------------------+-- PR 3: Elastic Net recovery on correlated-feature pairs.+-- Pure L1 picks ONE of two correlated informative features at random;+-- Elastic Net keeps BOTH non-zero (Zou & Hastie 2005 "grouping effect",+-- §2.3 Theorem 1).+--+-- Two cases per the ML reviewer: ρ ≈ 0.97 (strong) and ρ ≈ 0.7 (moderate).+------------------------------------------------------------------------++-- Generate two correlated features f0, f1 with correlation ρ, plus+-- noise features f2..f7. Truth is sign(f0 + f1).+correlatedPairData ::+    Int -> Double -> (V.Vector (VU.Vector Double), VU.Vector Double)+correlatedPairData seed rho =+    let n = 400 :: Int+        d = 8 :: Int+        g0 = mkStdGen seed+        drawUnit = randomR (-1.0 :: Double, 1.0)+        drawRow !gIn =+            let (z0, g1) = drawUnit gIn+                (epsRaw, g2) = drawUnit g1+                eps = epsRaw * sqrt (max 0 (1 - rho * rho))+                f0 = z0+                f1 = rho * z0 + eps -- corr(f0, f1) ≈ rho by construction+                drawNoise k g+                    | k >= d - 2 = ([], g)+                    | otherwise =+                        let (x, g') = drawUnit g+                            (xs, g'') = drawNoise (k + 1) g'+                         in (x : xs, g'')+                (noise, g3) = drawNoise 0 g2+                row = f0 : f1 : noise+             in (VU.fromList row, g3)+        go 0 _ acc = reverse acc+        go k g acc =+            let (r, g') = drawRow g+             in go (k - 1) g' (r : acc)+        rows = V.fromList (go n g0 [])+        labels =+            VU.generate n $ \i ->+                let r = rows V.! i+                    s = VU.unsafeIndex r 0 + VU.unsafeIndex r 1+                 in if s > 0 then 1.0 else -1.0+     in (rows, labels)++testA19ElasticNetRecoveryHigh :: Test+testA19ElasticNetRecoveryHigh = TestCase $ do+    -- ρ ≈ 0.97: positive test for Elastic Net's "grouping effect" —+    -- both correlated informative features kept non-zero and on the+    -- same order of magnitude. (We don't assert pure L1 picks just one;+    -- with strong-signal features L1 sometimes keeps both anyway.)+    let (rows, labels) = correlatedPairData 31 0.97+        names = V.fromList ["f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7"]+        cfgEN = defaultSolverConfig{scL1Lambda = 0.05, scL2Lambda = 0.05, scMaxIter = 1000}+        men = fitL1Logistic cfgEN rows labels names+        wEN = VU.toList (lmWeights men)+        nzCount xs = length (filter (/= 0) xs)+        (aEN, bEN) = case wEN of+            (a : b : _) -> (a, b)+            _ -> error "elastic-net test: expected at least two weights"+    assertBool+        ("ρ=0.97 EN keeps f0 non-zero; wEN[:2] = " ++ show (take 2 wEN))+        (aEN /= 0)+    assertBool+        ("ρ=0.97 EN keeps f1 non-zero; wEN[:2] = " ++ show (take 2 wEN))+        (bEN /= 0)+    let ratio = abs aEN / max (abs bEN) 1e-9+    assertBool+        ("ρ=0.97 EN grouping: |w0/w1| ∈ [0.33, 3.0]; got ratio=" ++ show ratio)+        (ratio >= 0.33 && ratio <= 3.0)+    -- Sanity: shouldn't have spuriously activated all noise features.+    assertBool+        ("ρ=0.97 EN sparsity: total non-zero ≤ 5; got " ++ show (nzCount wEN))+        (nzCount wEN <= 5)++testA19ElasticNetRecoveryMid :: Test+testA19ElasticNetRecoveryMid = TestCase $ do+    -- ρ ≈ 0.7: theoretically required regime for grouping (Zou-Hastie 2005 §5.1).+    let (rows, labels) = correlatedPairData 37 0.7+        names = V.fromList ["f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7"]+        cfgEN = defaultSolverConfig{scL1Lambda = 0.05, scL2Lambda = 0.05, scMaxIter = 1000}+        men = fitL1Logistic cfgEN rows labels names+        wEN = VU.toList (lmWeights men)+        (aEN, bEN) = case wEN of+            (a : b : _) -> (a, b)+            _ -> error "elastic-net test: expected at least two weights"+    assertBool+        ("ρ=0.7 EN keeps f0 non-zero; wEN[:2] = " ++ show (take 2 wEN))+        (aEN /= 0)+    assertBool+        ("ρ=0.7 EN keeps f1 non-zero; wEN[:2] = " ++ show (take 2 wEN))+        (bEN /= 0)+    let ratio = abs aEN / max (abs bEN) 1e-9+    assertBool+        ("ρ=0.7 EN grouping: |w0/w1| ∈ [0.33, 3.0]; got ratio=" ++ show ratio)+        (ratio >= 0.33 && ratio <= 3.0)++------------------------------------------------------------------------+-- PR 3: A20 — class-balanced fit on 95/5 imbalance.+-- Without weights the intercept polarises toward logit(0.95) ≈ 2.94.+-- With sample weights mean-1 sklearn-form, the intercept sits near 0 and+-- predictions become roughly balanced on a symmetric test set.+------------------------------------------------------------------------++testA20ClassBalancedFit :: Test+testA20ClassBalancedFit = TestCase $ do+    -- Generate 200 rows: 190 positive, 10 negative. Class-conditional+    -- means are at ±0.15 with σ ≈ 0.6 — only weakly informative on a+    -- single feature, so the unweighted MLE intercept absorbs the+    -- class prior @logit(0.95) ≈ 2.94@; class-balanced weighting must+    -- pull it back toward zero. Highly-separable features (e.g. mu=±1)+    -- would let the slope dominate and mask the intercept effect.+    let n = 200 :: Int+        nPos = 190 :: Int+        g0 = mkStdGen 41+        drawN = randomR (-1.0 :: Double, 1.0)+        drawRowAt mu g =+            let (z, g') = drawN g+                x = mu + 0.6 * z+             in (VU.singleton x, g')+        rowsAndLabels =+            let go _ 0 _ acc = reverse acc+                go !pCnt k g acc =+                    let !mu = if pCnt > 0 then 0.15 else -0.15+                        (row, g') = drawRowAt mu g+                        !y = if pCnt > 0 then 1.0 else -1.0+                     in go (pCnt - 1) (k - 1) g' ((row, y) : acc)+             in go nPos n g0 []+        rows = V.fromList (map fst rowsAndLabels)+        labels = VU.fromList (map snd rowsAndLabels)+        names = V.fromList ["x"]+        cfgUnbal =+            defaultSolverConfig+                { scL1Lambda = 0.001+                , scL2Lambda = 0+                , scMaxIter = 2000+                , scTol = 1e-7+                , scSampleWeights = Nothing+                }+        nNeg = n - nPos+        balanced =+            VU.generate n $ \i ->+                let !y = VU.unsafeIndex labels i+                 in if y > 0+                        then fromIntegral n / (2 * fromIntegral nPos)+                        else fromIntegral n / (2 * fromIntegral nNeg)+        cfgBal = cfgUnbal{scSampleWeights = Just balanced}+        mUnbal = fitL1Logistic cfgUnbal rows labels names+        mBal = fitL1Logistic cfgBal rows labels names+        bUnbal = lmIntercept mUnbal+        bBal = lmIntercept mBal+        -- Test set: 100 rows at each class-conditional mean. We measure+        -- predictions on this BALANCED test set; the unweighted model+        -- will predict mostly positive (intercept dominates), the+        -- balanced model close to 50/50.+        testRows =+            V.fromList+                ( replicate 100 (VU.singleton 0.15)+                    ++ replicate 100 (VU.singleton (-0.15))+                )+        predFracPos m =+            let preds = V.map (predict m) testRows+                ps = V.length (V.filter (> 0) preds)+             in fromIntegral ps / fromIntegral (V.length testRows) :: Double+        fracUnbal = predFracPos mUnbal+        fracBal = predFracPos mBal+    -- Reviewer-tightened intercept bounds (logit(0.95) ≈ 2.94 is the+    -- intercept-only solution; the weak slope shrinks this slightly).+    assertBool+        ("unbalanced |b| > 2.0; got " ++ show bUnbal)+        (abs bUnbal > 2.0)+    assertBool+        ("balanced |b| < 0.3; got " ++ show bBal)+        (abs bBal < 0.3)+    -- Prediction-class-balance assertion:+    assertBool+        ("unbalanced fraction-positive on balanced test ≥ 0.90; got " ++ show fracUnbal)+        (fracUnbal >= 0.90)+    assertBool+        ( "balanced fraction-positive on balanced test ∈ [0.40, 0.60]; got "+            ++ show fracBal+        )+        (fracBal >= 0.40 && fracBal <= 0.60)++------------------------------------------------------------------------+-- Test list+------------------------------------------------------------------------++tests :: [Test]+tests =+    [ TestLabel "A1 recover known hyperplane" testA1RecoverHyperplane+    , TestLabel "A2 L1 sparsity" testA2L1Sparsity+    , TestLabel "A3 convergence" testA3Convergence+    , TestLabel "A4 loss not increasing" testA4LossNotIncreasing+    , TestLabel "A5 all same direction" testA5AllSameDirection+    , TestLabel "A6 empty input" testA6Empty+    , TestLabel "A7 constant feature" testA7ConstantFeature+    , TestLabel "A8 large feature values" testA8LargeValues+    , TestLabel "A9 standardization round-trip" testA9StandardizationRoundTrip+    , TestLabel "A10 determinism" testA10Determinism+    , TestLabel "A11 ground truth ratio" testA11GroundTruthRatio+    , TestLabel "A12 maxIter zero" testA12MaxIterZero+    , TestLabel "A13 maxIter one" testA13MaxIterOne+    , TestLabel "A14 constant huge value" testA14ConstantHugeValue+    , TestLabel "A15 all-zero feature" testA15AllZeroFeature+    , TestLabel "A16 imbalanced 99:1 labels" testA16ImbalancedLabels+    , TestLabel "A17 imbalanced raw scales" testA17ImbalancedRawScales+    , TestLabel "B1 Expr well-typed" testB1ExprWellTyped+    , TestLabel "B2 zero weights pruned" testB2ZeroWeightsPruned+    , -- PR 3: Elastic Net + class-balanced weights.+      TestLabel "A19 Elastic Net grouping ρ=0.97" testA19ElasticNetRecoveryHigh+    , TestLabel "A19 Elastic Net grouping ρ=0.7" testA19ElasticNetRecoveryMid+    , TestLabel "A20 class-balanced fit on 95/5" testA20ClassBalancedFit+    ]
tests/Main.hs view
@@ -1,5143 +1,168 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ScopedTypeVariables #-}--module Main 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 qualified System.Exit as Exit--import Data.Time-import GenDataFrame ()-import Test.HUnit-import Test.QuickCheck--import qualified Functions-import qualified Operations.Aggregations-import qualified Operations.Apply-import qualified Operations.Core-import qualified Operations.Derive-import qualified Operations.Filter-import qualified Operations.GroupBy-import qualified Operations.InsertColumn-import qualified Operations.Join-import qualified Operations.Merge-import qualified Operations.ReadCsv-import qualified Operations.Sort-import qualified Operations.Statistics-import qualified Operations.Subset-import qualified Operations.Take-import qualified Parquet--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)--parseTests :: [Test]-parseTests =-    [ -- 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-    , -- 4. PARSING MUST NOT DEPEND ON THE NUMBER OF EXAMPLES-      TestLabel "parseBoolsWithOneExample" parseBoolsWithOneExample-    , TestLabel "parseBoolsWithManyExamples" parseBoolsWithManyExamples-    , TestLabel "parseIntsWithOneExample" parseIntsWithOneExample-    , TestLabel "parseIntsWithTwentyFiveExamples" parseIntsWithTwentyFiveExamples-    , TestLabel "parseIntsWithFortyNineExamples" parseIntsWithFortyNineExamples-    , TestLabel "parseDatesWithOneExample" parseDatesWithOneExample-    , TestLabel "parseDatesWithFifteenExamples" parseDatesWithFifteenExamples-    , TestLabel-        "parseIntsAndDoublesAsDoublesWithOneExample"-        parseIntsAndDoublesAsDoublesWithOneExample-    , TestLabel-        "parseIntsAndDoublesAsDoublesWithManyExamples"-        parseIntsAndDoublesAsDoublesWithManyExamples-    , TestLabel-        "parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff"-        parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff-    , TestLabel-        "parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff"-        parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff-    , TestLabel-        "parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff"-        parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff-    , TestLabel-        "parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff"-        parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff-    , -- 5. EDGE CASES THAT HAVE TO BE PARSED CORRECTLY-      TestLabel "parseManyNullishAndOneInt" parseManyNullishAndOneInt-    , TestLabel "parseManyNullishAndOneDouble" parseManyNullishAndOneDouble-    , TestLabel "parseRepeatedNullish" parseRepeatedNullish-    ]--tests :: Test-tests =-    TestList $-        dimensionsTest-            ++ Operations.Aggregations.tests-            ++ Operations.Apply.tests-            ++ Operations.Core.tests-            ++ Operations.Derive.tests-            ++ Operations.Filter.tests-            ++ Operations.GroupBy.tests-            ++ Operations.InsertColumn.tests-            ++ Operations.Join.tests-            ++ Operations.Merge.tests-            ++ Operations.ReadCsv.tests-            ++ Operations.Sort.tests-            ++ Operations.Statistics.tests-            ++ Operations.Take.tests-            ++ Functions.tests-            ++ Parquet.tests-            ++ parseTests--isSuccessful :: Result -> Bool-isSuccessful (Success{..}) = True-isSuccessful _ = False--main :: IO ()-main = do-    result <- runTestTT tests-    -- Property tests-    propRes <--        mapM-            (quickCheckWithResult stdArgs)-            Operations.Subset.tests-    if failures result > 0 || errors result > 0 || not (all isSuccessful propRes)-        then Exit.exitFailure-        else Exit.exitSuccess+{-# LANGUAGE ScopedTypeVariables #-}++module Main where++import qualified System.Exit as Exit++import GenDataFrame ()+import Test.HUnit+import Test.QuickCheck++import qualified Cart+import qualified DecisionTree+import qualified Functions+import qualified IO.CSV+import qualified IO.CsvGolden+import qualified IO.JSON+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.EdgeCases+import qualified Learn.Ensembles+import qualified Learn.Metamorphic+import qualified Learn.MetricsTests+import qualified Learn.Models+import qualified Learn.NumericalRigor+import qualified Learn.Numerics+import qualified Learn.SklearnParity+import qualified Learn.Symbolic+import qualified Learn.Synthesis+import qualified LinearSolver+import qualified Monad+import qualified Operations.Aggregations+import qualified Operations.Apply+import qualified Operations.Core+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 Properties+import qualified Properties.Categorical+import qualified Properties.Simplify+import qualified Simplify+import qualified TreePruning+import qualified Worklist++tests :: Test+tests =+    TestList $+        DecisionTree.tests+            ++ Internal.ColumnBuilder.tests+            ++ Internal.DictEncode.tests+            ++ Internal.Markdown.tests+            ++ Internal.PackedText.tests+            ++ Internal.Parsing.tests+            ++ Learn.Numerics.tests+            ++ Learn.Denotation.tests+            ++ Learn.Models.tests+            ++ Learn.Ensembles.tests+            ++ Learn.Symbolic.tests+            ++ Learn.SklearnParity.tests+            ++ Learn.Synthesis.tests+            ++ Learn.MetricsTests.tests+            ++ Learn.Metamorphic.tests+            ++ Learn.EdgeCases.tests+            ++ Learn.NumericalRigor.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+            ++ Parquet.tests+            ++ LazyParquet.tests+            ++ LazyParity.tests+            ++ Plotting.tests+            ++ LinearSolver.tests+            ++ Simplify.tests+            ++ TreePruning.tests+            ++ Worklist.tests+            ++ Cart.tests+            ++ PackedTextMigration.tests++isSuccessful :: Result -> Bool+isSuccessful (Success{}) = True+isSuccessful _ = False++main :: IO ()+main = do+    result <- runTestTT tests+    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+            simpRes <- mapM (quickCheckWithResult stdArgs) Properties.Simplify.tests+            wlRes <- mapM (quickCheckWithResult stdArgs) Worklist.props+            if not (all isSuccessful propRes)+                || not (all isSuccessful cbRes)+                || not (all isSuccessful monadRes)+                || not (all isSuccessful propsRes)+                || not (all isSuccessful catRes)+                || not (all isSuccessful simpRes)+                || not (all isSuccessful wlRes)+                then Exit.exitFailure+                else Exit.exitSuccess
+ tests/Monad.hs view
@@ -0,0 +1,31 @@+module Monad where++import qualified DataFrame as D+import DataFrame.Internal.DataFrame+import DataFrame.Monad+import GenDataFrame ()+import System.Random+import Test.QuickCheck+import Test.QuickCheck.Monadic++roundToTwoPlaces :: Double -> Double+roundToTwoPlaces x = fromIntegral (round (x * 100) :: Int) / 100.0++prop_sampleM :: DataFrame -> Gen (Gen Property)+prop_sampleM df = monadic' $ do+    p <- run $ choose (0.0 :: Double, 1.0 :: Double)+    let expectedRate = roundToTwoPlaces p+    seed <- run $ choose (0, 1000)+    let rowCount = D.nRows df+    let colCount = D.nColumns df+    pre (colCount > 1 && rowCount > 100)+    let finalDf = execFrameM df (sampleM (mkStdGen seed) expectedRate)+    let finalRowCount = D.nRows finalDf+    let realRate = roundToTwoPlaces $ fromIntegral finalRowCount / fromIntegral rowCount+    let diff = abs $ expectedRate - realRate+    -- 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
@@ -1,3 +1,4 @@+{-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} @@ -7,8 +8,10 @@ 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 Data.Function+import DataFrame.Operators import Test.HUnit  values :: [(T.Text, DI.Column)]@@ -17,7 +20,7 @@     , ("test2", DI.fromList ([12, 11 .. 1] :: [Int]))     , ("test3", DI.fromList ([1 .. 12] :: [Int]))     , ("test4", DI.fromList ['a' .. 'l'])-    , ("test4", DI.fromList (map show ['a' .. 'l']))+    , ("test5", DI.fromList (map show ['a' .. 'l']))     , ("test6", DI.fromList ([1 .. 12] :: [Integer]))     ] @@ -36,11 +39,82 @@             )             ( testData                 & D.groupBy ["test1"]-                & D.aggregate [F.count (F.col @Int "test2") `F.as` "test2"]-                & D.sortBy [D.Asc "test1"]+                & D.aggregate [F.count (F.col @Int "test2") `as` "test2"]+                & D.sortBy [D.Asc (F.col @Int "test1")]             )         ) +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+        ( assertEqual+            "Typed counting elements after grouping gives correct numbers"+            ( D.fromNamedColumns+                [ ("test1", DI.fromList [1 :: Int, 2, 3])+                , ("test2_count", 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 @"test2_count" (DT.count (DT.col @"test2")))+                & DT.sortBy [DT.asc (DT.col @"test1")]+                & DT.thaw+            )+        )+ numericAggregation :: Test numericAggregation =     TestCase@@ -53,11 +127,38 @@             )             ( testData                 & D.groupBy ["test1"]-                & D.aggregate [F.mean (F.col @Int "test2") `F.as` "test2"]-                & D.sortBy [D.Asc "test1"]+                & D.aggregate [F.mean (F.col @Int "test2") `as` "test2"]+                & D.sortBy [D.Asc (F.col @Int "test1")]             )         ) +numericAggregationTyped :: Test+numericAggregationTyped =+    TestCase+        ( assertEqual+            "Typed ean works for ints"+            ( D.fromNamedColumns+                [ ("test1", DI.fromList [1 :: Int, 2, 3])+                , ("test2_mean", DI.fromList [6.5 :: Double, 8.0, 5.0])+                ]+            )+            ( 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 @"test2_mean" (DT.mean (DT.col @"test2")))+                & DT.sortBy [DT.asc (DT.col @"test1")]+                & DT.thaw+            )+        )+ numericAggregationOfUnaggregatedUnaryOp :: Test numericAggregationOfUnaggregatedUnaryOp =     TestCase@@ -71,9 +172,9 @@             ( testData                 & D.groupBy ["test1"]                 & D.aggregate-                    [ F.mean (F.lift (fromIntegral @Int @Double) (F.col @Int "test2")) `F.as` "test2"+                    [ F.mean (F.lift (fromIntegral @Int @Double) (F.col @Int "test2")) `as` "test2"                     ]-                & D.sortBy [D.Asc "test1"]+                & D.sortBy [D.Asc (F.col @Int "test1")]             )         ) @@ -89,8 +190,8 @@             )             ( testData                 & D.groupBy ["test1"]-                & D.aggregate [F.mean (F.col @Int "test2" + F.col @Int "test2") `F.as` "test2"]-                & D.sortBy [D.Asc "test1"]+                & D.aggregate [F.mean (F.col @Int "test2" + F.col @Int "test2") `as` "test2"]+                & D.sortBy [D.Asc (F.col @Int "test1")]             )         ) @@ -108,9 +209,9 @@                 & D.groupBy ["test1"]                 & D.aggregate                     [ F.maximum (F.lift (fromIntegral @Int @Double) (F.col @Int "test2"))-                        `F.as` "test2"+                        `as` "test2"                     ]-                & D.sortBy [D.Asc "test1"]+                & D.sortBy [D.Asc (F.col @Int "test1")]             )         ) @@ -127,15 +228,110 @@             ( testData                 & D.groupBy ["test1"]                 & D.aggregate-                    [F.maximum (F.col @Int "test2" + F.col @Int "test2") `F.as` "test2"]-                & D.sortBy [D.Asc "test1"]+                    [F.maximum (F.col @Int "test2" + F.col @Int "test2") `as` "test2"]+                & D.sortBy [D.Asc (F.col @Int "test1")]             )         ) +aggregationOnNoRows :: Test+aggregationOnNoRows =+    TestCase+        ( assertEqual+            "Aggregation on DataFrame with no rows"+            ( D.fromNamedColumns+                [ ("test1", DI.fromList ([] :: [Int]))+                , ("sum(test2)", DI.fromList ([] :: [Int]))+                ]+            )+            ( testData+                & D.drop 12+                & D.groupBy ["test1"]+                & D.aggregate+                    [F.sum (F.col @Int "test2") `as` "sum(test2)"]+            )+        )++-- distinct++distinctRemovesDuplicates :: Test+distinctRemovesDuplicates =+    TestCase+        ( assertEqual+            "distinct reduces duplicate rows to one representative each"+            3+            ( D.nRows+                ( D.distinct+                    ( D.fromNamedColumns+                        [ ("x", DI.fromList [1 :: Int, 1, 2, 2, 3])+                        , ("y", DI.fromList [10 :: Int, 10, 20, 20, 30])+                        ]+                    )+                )+            )+        )++distinctNoDuplicates :: Test+distinctNoDuplicates =+    TestCase+        ( assertEqual+            "distinct on a DataFrame with no duplicates preserves all rows"+            3+            ( D.nRows+                ( D.distinct+                    ( D.fromNamedColumns+                        [ ("x", DI.fromList [1 :: Int, 2, 3])+                        , ("y", DI.fromList [10 :: Int, 20, 30])+                        ]+                    )+                )+            )+        )++distinctAllSameRows :: Test+distinctAllSameRows =+    TestCase+        ( assertEqual+            "distinct on all-identical rows leaves exactly one row"+            1+            ( D.nRows+                ( D.distinct+                    ( D.fromNamedColumns+                        [("x", DI.fromList [42 :: Int, 42, 42, 42])]+                    )+                )+            )+        )++-- groupBy on an Optional (nullable) column: Nothing values form their own group.+optGroupByDf :: D.DataFrame+optGroupByDf =+    D.fromNamedColumns+        [ ("key", DI.fromList [Just 1 :: Maybe Int, Just 1, Just 2, Nothing, Nothing])+        , ("val", DI.fromList [10 :: Int, 20, 30, 40, 50])+        ]++groupByOptionalColumn :: Test+groupByOptionalColumn =+    TestCase+        ( assertEqual+            "groupBy on an Optional column groups Nothing values together"+            3 -- groups: Just 1, Just 2, Nothing+            ( D.nRows+                ( optGroupByDf+                    & D.groupBy ["key"]+                    & D.aggregate [F.count (F.col @Int "val") `as` "val"]+                )+            )+        )+ tests :: [Test] tests =     [ TestLabel "foldAggregation" foldAggregation+    , TestLabel "countAllAggregation" countAllAggregation+    , TestLabel "countAllAggregationTyped" countAllAggregationTyped+    , TestLabel "foldAggregationTyped" foldAggregationTyped     , TestLabel "numericAggregation" numericAggregation+    , TestLabel "numericAggregationTyped" numericAggregationTyped     , TestLabel         "numericAggregationOfUnaggregatedUnaryOp"         numericAggregationOfUnaggregatedUnaryOp@@ -148,4 +344,11 @@     , TestLabel         "reduceAggregationOfUnaggregatedBinaryOp"         reduceAggregationOfUnaggregatedBinaryOp+    , TestLabel+        "aggregationOnNoRows"+        aggregationOnNoRows+    , TestLabel "distinctRemovesDuplicates" distinctRemovesDuplicates+    , TestLabel "distinctNoDuplicates" distinctNoDuplicates+    , TestLabel "distinctAllSameRows" distinctAllSameRows+    , TestLabel "groupByOptionalColumn" groupByOptionalColumn     ]
tests/Operations/Apply.hs view
@@ -9,9 +9,12 @@ import qualified Data.Vector.Unboxed as VU import qualified DataFrame as D import qualified DataFrame as DE+import qualified DataFrame.Functions as F 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 Test.HUnit@@ -37,7 +40,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)         ) @@ -46,7 +49,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)         ) @@ -86,7 +89,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@@ -201,6 +204,7 @@             "applyWhere works as intended"             ( Just $                 DI.UnboxedColumn+                    Nothing                     (VU.fromList (zipWith ($) (cycle [id, (+ 1)]) [(1 :: Int) .. 26]))             )             ( D.getColumn "test5" $@@ -208,6 +212,58 @@             )         ) +imputeData :: D.DataFrame+imputeData =+    D.fromNamedColumns+        [ ("opt", DI.fromList [Just (1 :: Int), Nothing, Just 3])+        , ("plain", DI.fromList [10 :: Int, 20, 30])+        ]++imputeHappyPath :: Test+imputeHappyPath =+    TestCase+        ( assertEqual+            "impute fills Nothing with the given value"+            (Just $ DI.UnboxedColumn Nothing (VU.fromList [1 :: Int, 0, 3]))+            (DI.getColumn "opt" $ impute (F.col @(Maybe Int) "opt") 0 imputeData)+        )++imputeColumnNotFound :: Test+imputeColumnNotFound =+    TestCase+        ( assertExpectException+            "[Error Case]"+            (DE.columnNotFound "missing" "impute" (D.columnNames imputeData))+            (print $ impute (F.col @(Maybe Int) "missing") 0 imputeData)+        )++imputeOnNonOptional :: Test+imputeOnNonOptional =+    TestCase+        ( 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@@ -224,4 +280,9 @@     , TestLabel "applyWhereConditionColumnNotFound" applyWhereConditionColumnNotFound     , TestLabel "applyWhereTargetColumnNotFound" applyWhereTargetColumnNotFound     , TestLabel "applyWhereWAI" applyWhereWAI+    , TestLabel "imputeHappyPath" imputeHappyPath+    , TestLabel "imputeColumnNotFound" imputeColumnNotFound+    , TestLabel "imputeOnNonOptional" imputeOnNonOptional+    , TestLabel "imputePlainNoOp" imputePlainNoOp+    , TestLabel "imputeWithPlainNoOp" imputeWithPlainNoOp     ]
tests/Operations/Derive.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}@@ -10,6 +11,7 @@ import qualified DataFrame.Functions as F import qualified DataFrame.Internal.Column as DI import qualified DataFrame.Internal.DataFrame as DI+import qualified DataFrame.Typed as DT  import Test.HUnit @@ -30,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@@ -44,7 +47,33 @@             )         ) +deriveWAITyped :: Test+deriveWAITyped =+    TestCase+        ( assertEqual+            "typed derive works with column expression"+            (zipWith (\n c -> show n ++ [c]) ([1 .. 26] :: [Int]) ['a' .. 'z'])+            ( DT.columnAsList @"test4" $+                DT.derive+                    @"test4"+                    ( DT.lift2+                        (++)+                        (DT.lift show (DT.col @"test1"))+                        (DT.lift (: ([] :: [Char])) (DT.col @"test3"))+                    )+                    ( either+                        (error . show)+                        id+                        ( DT.freezeWithError+                            @[DT.Column "test1" Int, DT.Column "test2" String, DT.Column "test3" Char]+                            testData+                        )+                    )+            )+        )+ tests :: [Test] tests =     [ TestLabel "deriveWAI" deriveWAI+    , TestLabel "deriveWAITyped" deriveWAITyped     ]
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@@ -28,7 +31,7 @@         ( assertEqual             "Groups by single column"             -- We don't yet compare offsets and indices-            (D.Grouped testData ["test1"] VU.empty VU.empty)+            (D.Grouped testData ["test1"] VU.empty VU.empty VU.empty)             (D.groupBy ["test1"] testData)         ) @@ -38,7 +41,7 @@         ( assertEqual             "Groups by single column"             -- We don't yet compare offsets and indices-            (D.Grouped testData ["test1", "test2"] VU.empty VU.empty)+            (D.Grouped testData ["test1", "test2"] VU.empty VU.empty VU.empty)             (D.groupBy ["test1", "test2"] testData)         ) @@ -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
@@ -1,11 +1,17 @@+{-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-}  module Operations.Join where -import Data.Text (Text)+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  df1 :: D.DataFrame@@ -22,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@@ -33,7 +54,7 @@                 , ("B", D.fromList ["B0" :: Text, "B1", "B2"])                 ]             )-            (D.sortBy [D.Asc "key"] (innerJoin ["key"] df1 df2))+            (D.sortBy [D.Asc (F.col @Text "key")] (innerJoin ["key"] df1 df2))         )  testLeftJoin :: Test@@ -47,7 +68,7 @@                 , ("B", D.fromList [Just "B0", Just "B1" :: Maybe Text, Just "B2"])                 ]             )-            (D.sortBy [D.Asc "key"] (leftJoin ["key"] df2 df1))+            (D.sortBy [D.Asc (F.col @Text "key")] (leftJoin ["key"] df1 df2))         )  testRightJoin :: Test@@ -57,13 +78,100 @@             "Test right join with single key"             ( D.fromNamedColumns                 [ ("key", D.fromList ["K0" :: Text, "K1", "K2"])+                , ("A", D.fromList [Just "A0" :: Maybe Text, Just "A1", Just "A2"])+                , ("B", D.fromList ["B0" :: Text, "B1", "B2"])+                ]+            )+            (D.sortBy [D.Asc (F.col @Text "key")] (rightJoin ["key"] df1 df2))+        )++tdf1 :: DT.TypedDataFrame [DT.Column "key" Text, DT.Column "A" Text]+tdf1 = either (error . show) id (DT.freezeWithError df1)++tdf2 :: DT.TypedDataFrame [DT.Column "key" Text, DT.Column "B" Text]+tdf2 = either (error . show) id (DT.freezeWithError df2)++testInnerJoinTyped :: Test+testInnerJoinTyped =+    TestCase+        ( assertEqual+            "Test typed inner join with single key"+            ( D.fromNamedColumns+                [ ("key", D.fromList ["K0" :: Text, "K1", "K2"])                 , ("A", D.fromList ["A0" :: Text, "A1", "A2"])                 , ("B", D.fromList ["B0" :: Text, "B1", "B2"])                 ]             )-            (D.sortBy [D.Asc "key"] (rightJoin ["key"] df2 df1))+            (DT.thaw $ DT.sortBy [DT.asc (DT.col @"key")] (DT.innerJoin @'["key"] tdf1 tdf2))         ) +testLeftJoinTyped :: Test+testLeftJoinTyped =+    TestCase+        ( assertEqual+            "Test typed left join with single key"+            ( D.fromNamedColumns+                [ ("key", D.fromList ["K0" :: Text, "K1", "K2", "K3", "K4", "K5"])+                , ("A", D.fromList ["A0" :: Text, "A1", "A2", "A3", "A4", "A5"])+                , ("B", D.fromList [Just "B0", Just "B1" :: Maybe Text, Just "B2"])+                ]+            )+            (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+        ( assertEqual+            "Test typed right join with single key"+            ( D.fromNamedColumns+                [ ("key", D.fromList ["K0" :: Text, "K1", "K2"])+                , ("A", D.fromList [Just "A0" :: Maybe Text, Just "A1", Just "A2"])+                , ("B", D.fromList ["B0" :: Text, "B1", "B2"])+                ]+            )+            (DT.thaw $ DT.sortBy [DT.asc (DT.col @"key")] (DT.rightJoin @'["key"] tdf1 tdf2))+        )+ staffDf :: D.DataFrame staffDf =     D.fromRows@@ -108,13 +216,347 @@                     )                 ]             )-            (D.sortBy [D.Asc "Name"] (fullOuterJoin ["Name"] studentDf staffDf))+            (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+        [ ("key", D.fromList ["K0" :: Text, "K1", "K2"])+        , ("X", D.fromList ["LX0" :: Text, "LX1", "LX2"])+        , ("Lonly", D.fromList ["L0" :: Text, "L1", "L2"])+        ]++dfR :: D.DataFrame+dfR =+    D.fromNamedColumns+        [ ("key", D.fromList ["K0" :: Text, "K1", "K3"])+        , ("X", D.fromList ["RX0" :: Text, "RX1", "RX3"])+        , ("Ronly", D.fromList [10 :: Int, 11, 13])+        ]++testInnerJoinWithCollisions :: Test+testInnerJoinWithCollisions =+    TestCase+        ( assertEqual+            "Test inner join with single key"+            ( D.fromNamedColumns+                [ ("key", D.fromList ["K0" :: Text, "K1"])+                , ("X", D.fromList [These "LX0" "RX0" :: These Text Text, These "LX1" "RX1"])+                , ("Lonly", D.fromList ["L0" :: Text, "L1"])+                , ("Ronly", D.fromList [10 :: Int, 11])+                ]+            )+            (D.sortBy [D.Asc (F.col @Text "key")] (innerJoin ["key"] dfL dfR))+        )++testLeftJoinWithCollisions :: Test+testLeftJoinWithCollisions =+    TestCase+        ( assertEqual+            "Test left join with single key"+            ( D.fromNamedColumns+                [ ("key", D.fromList ["K0" :: Text, "K1", "K2"])+                ,+                    ( "X"+                    , D.fromList [These "LX0" "RX0" :: These Text Text, These "LX1" "RX1", This "LX2"]+                    )+                , ("Lonly", D.fromList ["L0" :: Text, "L1", "L2"])+                , ("Ronly", D.fromList [Just 10 :: Maybe Int, Just 11, Nothing])+                ]+            )+            (D.sortBy [D.Asc (F.col @Text "key")] (leftJoin ["key"] dfL dfR))+        )++testRightJoinWithCollisions :: Test+testRightJoinWithCollisions =+    TestCase+        ( assertEqual+            "Test right join with single key"+            ( D.fromNamedColumns+                [ ("key", D.fromList ["K0" :: Text, "K1", "K3"])+                ,+                    ( "X"+                    , D.fromList [These "RX0" "LX0" :: These Text Text, These "RX1" "LX1", This "RX3"]+                    )+                , ("Ronly", D.fromList [10 :: Int, 11, 13])+                , ("Lonly", D.fromList [Just "L0" :: Maybe Text, Just "L1", Nothing])+                ]+            )+            (D.sortBy [D.Asc (F.col @Text "key")] (rightJoin ["key"] dfL dfR))+        )++testOuterJoinWithCollisions :: Test+testOuterJoinWithCollisions =+    TestCase+        ( assertEqual+            "Test right join with single key"+            ( D.fromNamedColumns+                [ ("key", D.fromList ["K0" :: Text, "K1", "K2", "K3"])+                ,+                    ( "X"+                    , D.fromList+                        [ Just (These "LX0" "RX0") :: Maybe (These Text Text)+                        , Just (These "LX1" "RX1")+                        , Just (This "LX2")+                        , Just (That "RX3")+                        ]+                    )+                , ("Lonly", D.fromList [Just "L0" :: Maybe Text, Just "L1", Just "L2", Nothing])+                , ("Ronly", D.fromList [Just 10 :: Maybe Int, Just 11, Nothing, Just 13])+                ]+            )+            (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 =+    D.fromNamedColumns+        [ ("key", D.fromList ([] :: [Text]))+        , ("A", D.fromList ([] :: [Text]))+        ]++emptyDf2 :: D.DataFrame+emptyDf2 =+    D.fromNamedColumns+        [ ("key", D.fromList ([] :: [Text]))+        , ("B", D.fromList ([] :: [Text]))+        ]++testInnerJoinBothEmpty :: Test+testInnerJoinBothEmpty =+    TestCase+        ( assertEqual+            "Inner join of two empty DataFrames produces 0 rows"+            0+            (D.nRows (innerJoin ["key"] emptyDf1 emptyDf2))+        )++testInnerJoinLeftEmpty :: Test+testInnerJoinLeftEmpty =+    TestCase+        ( assertEqual+            "Inner join with empty left produces 0 rows"+            0+            (D.nRows (innerJoin ["key"] emptyDf1 df2))+        )++testInnerJoinRightEmpty :: Test+testInnerJoinRightEmpty =+    TestCase+        ( assertEqual+            "Inner join with empty right produces 0 rows"+            0+            (D.nRows (innerJoin ["key"] df1 emptyDf2))+        )++testLeftJoinRightEmpty :: Test+testLeftJoinRightEmpty =+    TestCase+        ( assertEqual+            "Left join with empty right returns left"+            6+            (D.nRows (leftJoin ["key"] df1 emptyDf2))+        )++-- Many-to-many: duplicate keys on both sides produce the cross-product.+-- manyLeft:  K0->A0, K1->A1, K1->A2+-- manyRight: K0->B0, K1->B1, K1->B2+-- Expected inner join: 1 K0 pair + 4 K1 pairs = 5 rows+manyLeft :: D.DataFrame+manyLeft =+    D.fromNamedColumns+        [ ("key", D.fromList ["K0" :: Text, "K1", "K1"])+        , ("A", D.fromList ["A0" :: Text, "A1", "A2"])+        ]++manyRight :: D.DataFrame+manyRight =+    D.fromNamedColumns+        [ ("key", D.fromList ["K0" :: Text, "K1", "K1"])+        , ("B", D.fromList ["B0" :: Text, "B1", "B2"])+        ]++testManyToManyInnerJoin :: Test+testManyToManyInnerJoin =+    TestCase+        ( assertEqual+            "Many-to-many inner join produces cross-product row count"+            5+            (D.nRows (innerJoin ["key"] manyLeft manyRight))+        )++testManyToManyLeftJoin :: Test+testManyToManyLeftJoin =+    TestCase+        ( assertEqual+            "Many-to-many left join includes all cross-product rows"+            5+            (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,373 @@+{-# 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 qualified DataFrame.Internal.Schema as IS+import DataFrame.Operators+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.deriveSchema ''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.deriveSchema ''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.deriveSchema ''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++typeMismatchError :: Test+typeMismatchError = TestCase $ do+    let badDf =+            D.fromNamedColumns+                [ ("order_id", DI.fromList ([1, 2, 3] :: [Int])) -- wrong: Int, not Int64+                , ("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/Shuffle.hs view
@@ -0,0 +1,102 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Operations.Shuffle where++import qualified DataFrame as D++import qualified Data.Set as Set+import qualified Data.Vector.Unboxed as VU+import DataFrame.Operations.Permutation (shuffle, shuffledIndices)+import System.Random (mkStdGen)+import Test.HUnit (Test (..), assertEqual)++testDataFrame :: D.DataFrame+testDataFrame =+    D.fromNamedColumns+        [ ("numbers", D.fromList @Int [1 .. 26])+        ]++-- Test that shuffling does anything at all+shuffleShuffles :: Test+shuffleShuffles =+    let gen = mkStdGen 1234+        shuffled = shuffle gen testDataFrame+        initialNumbers = D.extractNumericColumn "numbers" testDataFrame+        shuffledNumbers = D.extractNumericColumn "numbers" shuffled+     in TestCase+            ( assertEqual+                "Shuffled column unequal to initial column"+                False+                (initialNumbers == shuffledNumbers)+            )++shufflePreservesColumnNames :: Test+shufflePreservesColumnNames =+    let gen = mkStdGen 837+        shuffled = shuffle gen testDataFrame+     in TestCase+            ( assertEqual+                "Column names are unchanged"+                (D.columnNames shuffled)+                (D.columnNames testDataFrame)+            )++-- Test that un-shuffling restores the original dataframe+-- which is known to be sorted in this case+shufflePreservesData :: Test+shufflePreservesData =+    let gen = mkStdGen 1234+        shuffled = shuffle gen testDataFrame+        sortedShuffled = D.sortBy [D.Asc (D.col @Int "numbers")] shuffled+     in TestCase+            (assertEqual "sort recovers initial numbers" testDataFrame sortedShuffled)++-- Test that shuffling isn't doing anything sneaky with summoning+-- random numbers somehow+shuffleSameSeedIsSameShuffle :: Test+shuffleSameSeedIsSameShuffle =+    let gen = mkStdGen 1234+        shuffled1 = shuffle gen testDataFrame+        shuffled2 = shuffle gen testDataFrame+     in TestCase+            (assertEqual "shuffle with same seed gives same result" shuffled1 shuffled2)++-- Test that different seeds give different results+shuffleDifferentSeedIsDifferent :: Test+shuffleDifferentSeedIsDifferent =+    let gen1 = mkStdGen 1234+        gen2 = mkStdGen 4321+        shuffled1 = shuffle gen1 testDataFrame+        shuffled2 = shuffle gen2 testDataFrame+     in TestCase+            ( assertEqual+                "shuffle with different seeds gives different results"+                False+                (shuffled1 == shuffled2)+            )++-- Test that ShuffleIndeces does not dorp, add, or repeat any index+shuffleDoesNotAddOrDropIndices :: Test+shuffleDoesNotAddOrDropIndices =+    let+        gen = mkStdGen 42+        actual = Set.fromList [0 .. 10]+        computedVector = shuffledIndices gen 11+        computed = (Set.fromList $ VU.toList $ shuffledIndices gen 11)+     in+        TestList+            [ TestCase+                (assertEqual "Indecis are not dropped or added" (VU.length computedVector) 11)+            , TestCase (assertEqual "There are no repeated indecis" computed actual)+            ]++tests :: [Test]+tests =+    [ TestLabel "shuffleShuffles" shuffleShuffles+    , TestLabel "shufflePreservesData" shufflePreservesData+    , TestLabel "shufflePreservesColumnNames" shufflePreservesColumnNames+    , TestLabel "shuffleSameSeedIsSameShuffle" shuffleSameSeedIsSameShuffle+    , TestLabel "shuffleDifferentSeedIsDifferent" shuffleDifferentSeedIsDifferent+    , TestLabel "shuffleDoesNotAddOrDropIndices" shuffleDoesNotAddOrDropIndices+    ]
tests/Operations/Sort.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}  module Operations.Sort where @@ -6,6 +7,7 @@ import Data.Char 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 System.Random import System.Random.Shuffle (shuffle')@@ -40,7 +42,7 @@                 , ("test2", DI.fromList ['a' .. 'z'])                 ]             )-            (D.sortBy [D.Asc "test1"] testData)+            (D.sortBy [D.Asc (F.col @Int "test1")] testData)         )  sortByDescendingWAI :: Test@@ -53,7 +55,7 @@                 , ("test2", DI.fromList $ reverse ['a' .. 'z'])                 ]             )-            (D.sortBy [D.Desc "test1"] testData)+            (D.sortBy [D.Desc (F.col @Int "test1")] testData)         )  sortByTwoColumns :: Test@@ -62,7 +64,7 @@         ( assertEqual             "Sorting moreTestData (which is already sorted) is idempotent."             moreTestData-            (D.sortBy [D.Asc "test1", D.Asc "test2"] moreTestData)+            (D.sortBy [D.Asc (F.col @Int "test1"), D.Asc (F.col @Int "test2")] moreTestData)         )  sortByOneColumnAscOneColumnDesc :: Test@@ -75,7 +77,7 @@                 , ("test2", DI.fromList $ [10 :: Int, 9 .. 1] ++ [10, 9 .. 1])                 ]             )-            (D.sortBy [D.Asc "test1", D.Desc "test2"] moreTestData)+            (D.sortBy [D.Asc (F.col @Int "test1"), D.Desc (F.col @Int "test2")] moreTestData)         )  sortByColumnDoesNotExist :: Test@@ -83,10 +85,76 @@     TestCase         ( assertExpectException             "[Error Case]"-            (D.columnNotFound "[\"test0\"]" "sortBy" (D.columnNames testData))-            (print $ D.sortBy [D.Asc "test0"] 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@@ -94,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
@@ -6,6 +6,7 @@  import qualified Data.Vector.Unboxed as VU import qualified DataFrame as D+import qualified DataFrame.Internal.Column as DI import qualified DataFrame.Internal.Statistics as D  import Assertions@@ -197,6 +198,62 @@             )         ) +-- correlation++correlationDf :: D.DataFrame+correlationDf =+    D.fromNamedColumns+        [ ("x", DI.fromList [1 :: Int, 2, 3, 4, 5])+        , ("y_pos", DI.fromList [1 :: Int, 2, 3, 4, 5])+        , ("y_neg", DI.fromList [5 :: Int, 4, 3, 2, 1])+        ]++-- x perfectly predicts y_pos → r = 1.0+correlationPerfectPositive :: Test+correlationPerfectPositive =+    TestCase+        ( case D.correlation "x" "y_pos" correlationDf of+            Nothing -> assertFailure "Expected Just 1.0, got Nothing"+            Just r ->+                assertBool+                    "Perfect positive correlation should be 1.0"+                    (abs (r - 1.0) < 1e-10)+        )++-- x perfectly anti-predicts y_neg → r = -1.0+correlationPerfectNegative :: Test+correlationPerfectNegative =+    TestCase+        ( case D.correlation "x" "y_neg" correlationDf of+            Nothing -> assertFailure "Expected Just (-1.0), got Nothing"+            Just r ->+                assertBool+                    "Perfect negative correlation should be -1.0"+                    (abs (r + 1.0) < 1e-10)+        )++-- Correlation of a column with itself is 1.0+correlationSelfIdentity :: Test+correlationSelfIdentity =+    TestCase+        ( case D.correlation "x" "x" correlationDf of+            Nothing -> assertFailure "Expected Just 1.0, got Nothing"+            Just r ->+                assertBool+                    "Correlation of a column with itself should be 1.0"+                    (abs (r - 1.0) < 1e-10)+        )++-- Requesting a missing column should throw ColumnsNotFoundException+correlationMissingColumn :: Test+correlationMissingColumn =+    TestCase+        ( assertExpectException+            "[Error Case]"+            "missingcol"+            (print $ D.correlation "x" "missingcol" correlationDf)+        )+ tests :: [Test] tests =     [ TestLabel "medianOfOddLengthDataSet" medianOfOddLengthDataSet@@ -220,4 +277,8 @@     , TestLabel "wrongQuantileNumber" wrongQuantileNumber     , TestLabel "wrongQuantileIndex" wrongQuantileIndex     , TestLabel "summarizeOptional" summarizeOptional+    , TestLabel "correlationPerfectPositive" correlationPerfectPositive+    , TestLabel "correlationPerfectNegative" correlationPerfectNegative+    , TestLabel "correlationSelfIdentity" correlationSelfIdentity+    , TestLabel "correlationMissingColumn" correlationMissingColumn     ]
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
@@ -0,0 +1,1535 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# 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+           , TestLabel "parseIntsWithOneExample" parseIntsWithOneExample+           , TestLabel "parseIntsWithTwentyFiveExamples" parseIntsWithTwentyFiveExamples+           , TestLabel "parseIntsWithFortyNineExamples" parseIntsWithFortyNineExamples+           , TestLabel "parseDatesWithOneExample" parseDatesWithOneExample+           , TestLabel "parseDatesWithFifteenExamples" parseDatesWithFifteenExamples+           , TestLabel+                "parseIntsAndDoublesAsDoublesWithOneExample"+                parseIntsAndDoublesAsDoublesWithOneExample+           , TestLabel+                "parseIntsAndDoublesAsDoublesWithManyExamples"+                parseIntsAndDoublesAsDoublesWithManyExamples+           , TestLabel+                "parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff"+                parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff+           , TestLabel+                "parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff"+                parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff+           , TestLabel+                "parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff"+                parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff+           , TestLabel+                "parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff"+                parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff+           , -- 5. EDGE CASES THAT HAVE TO BE PARSED CORRECTLY+             TestLabel "parseManyNullishAndOneInt" parseManyNullishAndOneInt+           , TestLabel "parseManyNullishAndOneDouble" parseManyNullishAndOneDouble+           , TestLabel "parseRepeatedNullish" parseRepeatedNullish+           ]
+ 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,543 +1,1408 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TypeApplications #-}--module Parquet where--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 (fromGregorian 2009 3 1) (secondsToDiffTime 0)-                , UTCTime (fromGregorian 2009 3 1) (secondsToDiffTime 60)-                , UTCTime (fromGregorian 2009 4 1) (secondsToDiffTime 0)-                , UTCTime (fromGregorian 2009 4 1) (secondsToDiffTime 60)-                , UTCTime (fromGregorian 2009 2 1) (secondsToDiffTime 0)-                , UTCTime (fromGregorian 2009 2 1) (secondsToDiffTime 60)-                , UTCTime (fromGregorian 2009 1 1) (secondsToDiffTime 0)-                , UTCTime (fromGregorian 2009 1 1) (secondsToDiffTime 60)-                ]-            )-        ]--allTypesPlain :: Test-allTypesPlain =-    TestCase-        ( assertEqual-            "allTypesPlain"-            allTypes-            (unsafePerformIO (D.readParquet "./tests/data/alltypes_plain.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"))-        )--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"))-        )---- Uncomment to run parquet tests.--- Currently commented because they don't run with github CI-tests :: [Test]-tests = [allTypesPlain, allTypesPlainSnappy, allTypesDictionary, mtCars]+{-# 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.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)))++tests :: [Test]+tests =+    [ 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/Properties.hs view
@@ -0,0 +1,37 @@+module Properties where++import qualified DataFrame as D+import DataFrame.Internal.DataFrame (DataFrame, dataframeDimensions)++-- | distinct never increases row count.+prop_distinctMonotone :: DataFrame -> Bool+prop_distinctMonotone df =+    fst (dataframeDimensions (D.distinct df)) <= fst (dataframeDimensions df)++-- | distinct is idempotent.+prop_distinctIdempotent :: DataFrame -> Bool+prop_distinctIdempotent df = D.distinct (D.distinct df) == D.distinct df++-- | Merging empty on the left is identity for row count.+prop_mergeEmptyLeft :: DataFrame -> Bool+prop_mergeEmptyLeft df =+    fst (dataframeDimensions (D.empty <> df)) == fst (dataframeDimensions df)++-- | Merging empty on the right is identity for row count.+prop_mergeEmptyRight :: DataFrame -> Bool+prop_mergeEmptyRight df =+    fst (dataframeDimensions (df <> D.empty)) == fst (dataframeDimensions df)++-- | Merging a DataFrame with itself doubles the row count.+prop_mergeSelfDoubles :: DataFrame -> Bool+prop_mergeSelfDoubles df =+    fst (dataframeDimensions (df <> df)) == 2 * fst (dataframeDimensions df)++tests :: [DataFrame -> Bool]+tests =+    [ prop_distinctMonotone+    , prop_distinctIdempotent+    , prop_mergeEmptyLeft+    , prop_mergeEmptyRight+    , prop_mergeSelfDoubles+    ]
+ 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/Properties/Simplify.hs view
@@ -0,0 +1,172 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Property tests for the simplifier and tree-pruning pass: the durable+guarantees the hand-written examples only sample.++  * @simplify@ preserves denotation (@interpret e ≡ interpret (simplify e)@),+    over Bool and Maybe Bool, on a DataFrame that includes NaN, null, and+    exact-boundary rows.+  * @simplify@ is idempotent (reaches a normal form within the fixpoint cap).+  * @pruneDead@ preserves the function the tree computes, on every row.+-}+module Properties.Simplify (tests) where++import qualified DataFrame as D+import DataFrame.DecisionTree (Tree (..), predictWithTree, pruneDead)+import qualified DataFrame.Functions as F+import DataFrame.Internal.Column (TypedColumn (TColumn), toVector)+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.Expression (Expr, eqExpr)+import DataFrame.Internal.Interpreter (interpret)+import DataFrame.Internal.Simplify (simplify)+import DataFrame.Operators++import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import Test.QuickCheck++-- A fixture spanning the interesting rows: exact thresholds, gaps, NaN, null.+fixtureDF :: D.DataFrame+fixtureDF =+    D.fromNamedColumns+        [+            ( "x"+            , DI.fromList ([10, 20, 25, 30, 35, 40, 50, 0 / 0, -(1 / 0), 1 / 0] :: [Double])+            )+        , ("n", DI.fromList ([10, 20, 25, 30, 35, 40, 50, 0, 100, -5] :: [Int]))+        ,+            ( "m"+            , DI.fromList+                ( [ Just 10+                  , Nothing+                  , Just 30+                  , Just 35+                  , Nothing+                  , Just 50+                  , Just 0+                  , Just 30+                  , Nothing+                  , Just 40+                  ] ::+                    [Maybe Double]+                )+            )+        ]++thresholds :: [Double]+thresholds = [20, 25, 30, 35, 40]++-- ---- generators ----++-- strict-Bool comparison atoms over the Double column "x" and Int column "n"+genAtomBool :: Gen (Expr Bool)+genAtomBool = do+    t <- elements thresholds+    oneof+        [ elements+            [ F.col @Double "x" .< F.lit t+            , F.col @Double "x" .<= F.lit t+            , F.col @Double "x" .> F.lit t+            , F.col @Double "x" .>= F.lit t+            , F.col @Double "x" .== F.lit t+            , F.col @Double "x" ./= F.lit t+            ]+        , elements+            [ F.toDouble (F.col @Int "n") .< F.lit t+            , F.toDouble (F.col @Int "n") .<= F.lit t+            , F.toDouble (F.col @Int "n") .> F.lit t+            , F.toDouble (F.col @Int "n") .>= F.lit t+            ]+        ]++genBoolExpr :: Int -> Gen (Expr Bool)+genBoolExpr d+    | d <= 0 = genAtomBool+    | otherwise =+        oneof+            [ genAtomBool+            , F.and <$> genBoolExpr (d - 1) <*> genBoolExpr (d - 1)+            , F.or <$> genBoolExpr (d - 1) <*> genBoolExpr (d - 1)+            , F.not <$> genBoolExpr (d - 1)+            ]++-- nullable comparison atoms over the Maybe Double column "m"+genAtomMaybe :: Gen (Expr (Maybe Bool))+genAtomMaybe = do+    t <- elements thresholds+    elements+        [ F.col @(Maybe Double) "m" .< F.lit t+        , F.col @(Maybe Double) "m" .<= F.lit t+        , F.col @(Maybe Double) "m" .> F.lit t+        , F.col @(Maybe Double) "m" .>= F.lit t+        , F.col @(Maybe Double) "m" .== F.lit t+        , F.col @(Maybe Double) "m" ./= F.lit t+        ]++genMaybeExpr :: Int -> Gen (Expr (Maybe Bool))+genMaybeExpr d+    | d <= 0 = genAtomMaybe+    | otherwise =+        oneof+            [ genAtomMaybe+            , (.&&) <$> genMaybeExpr (d - 1) <*> genMaybeExpr (d - 1)+            , (.||) <$> genMaybeExpr (d - 1) <*> genMaybeExpr (d - 1)+            ]++genTree :: Int -> Gen (Tree T.Text)+genTree d+    | d <= 0 = Leaf <$> elements ["A", "B", "C"]+    | otherwise =+        oneof+            [ Leaf <$> elements ["A", "B", "C"]+            , do+                cond <- genAtomBool+                Branch cond <$> genTree (d - 1) <*> genTree (d - 1)+            ]++-- ---- evaluation helpers ----++evalBool :: D.DataFrame -> Expr Bool -> Maybe (VU.Vector Bool)+evalBool df e = case interpret @Bool df e of+    Right (TColumn tcol) -> either (const Nothing) Just (toVector @Bool @VU.Vector tcol)+    Left _ -> Nothing++evalMaybe :: D.DataFrame -> Expr (Maybe Bool) -> Maybe (V.Vector (Maybe Bool))+evalMaybe df e = case interpret @(Maybe Bool) df e of+    Right (TColumn tcol) -> either (const Nothing) Just (toVector @(Maybe Bool) @V.Vector tcol)+    Left _ -> Nothing++-- ---- properties ----++prop_simplifyPreservesBool :: Property+prop_simplifyPreservesBool =+    forAll (genBoolExpr 4) $ \e ->+        evalBool fixtureDF e === evalBool fixtureDF (simplify e)++prop_simplifyPreservesMaybe :: Property+prop_simplifyPreservesMaybe =+    forAll (genMaybeExpr 3) $ \e ->+        evalMaybe fixtureDF e === evalMaybe fixtureDF (simplify e)++prop_simplifyIdempotent :: Property+prop_simplifyIdempotent =+    forAll (genBoolExpr 4) $ \e ->+        let s = simplify e in property (eqExpr (simplify s) s)++prop_pruneDeadPreserves :: Property+prop_pruneDeadPreserves =+    forAll (genTree 4) $ \t ->+        let n = D.nRows fixtureDF+            predAll tr = [predictWithTree @T.Text "x" fixtureDF i tr | i <- [0 .. n - 1]]+         in predAll (pruneDead t) === predAll t++tests :: [Property]+tests =+    [ prop_simplifyPreservesBool+    , prop_simplifyPreservesMaybe+    , prop_simplifyIdempotent+    , prop_pruneDeadPreserves+    ]
+ 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/TreePruning.hs view
@@ -0,0 +1,114 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Specification for the fitted-tree pruning pass ('pruneDead'): path-condition+entailment, the false-edge NaN gate, and same-branch collapse.+-}+module TreePruning (tests) where++import DataFrame.DecisionTree (Tree (..), pruneDead)+import qualified DataFrame.Functions as F+import DataFrame.Internal.Expression (eqExpr)+import DataFrame.Operators++import qualified Data.Text as T+import Test.HUnit++treeEq :: (Eq a) => Tree a -> Tree a -> Bool+treeEq (Leaf x) (Leaf y) = x == y+treeEq (Branch c1 l1 r1) (Branch c2 l2 r2) = eqExpr c1 c2 && treeEq l1 l2 && treeEq r1 r2+treeEq _ _ = False++prunesTo :: String -> Tree T.Text -> Tree T.Text -> Test+prunesTo label input want =+    TestLabel label . TestCase $+        assertBool+            (label ++ ": got " ++ show (pruneDead input) ++ " want " ++ show want)+            (treeEq (pruneDead input) want)++preserved :: String -> Tree T.Text -> Test+preserved label t = prunesTo label t t++pathEntailment :: [Test]+pathEntailment =+    [ prunesTo+        "ancestor entails child keeps true subtree"+        ( Branch+            (F.col @Double "age" .> F.lit (50 :: Double))+            (Branch (F.col @Double "age" .> F.lit (30 :: Double)) (Leaf "a") (Leaf "b"))+            (Leaf "c")+        )+        (Branch (F.col @Double "age" .> F.lit (50 :: Double)) (Leaf "a") (Leaf "c"))+    , prunesTo+        "ancestor refutes child keeps false subtree"+        ( Branch+            (F.col @Double "age" .> F.lit (50 :: Double))+            (Branch (F.col @Double "age" .< F.lit (40 :: Double)) (Leaf "a") (Leaf "b"))+            (Leaf "c")+        )+        (Branch (F.col @Double "age" .> F.lit (50 :: Double)) (Leaf "b") (Leaf "c"))+    ]++falseEdgeGate :: [Test]+falseEdgeGate =+    [ prunesTo+        "integral false edge entails child"+        ( Branch+            (F.toDouble (F.col @Int "ai") .> F.lit (50 :: Double))+            (Leaf "c")+            ( Branch+                (F.toDouble (F.col @Int "ai") .< F.lit (60 :: Double))+                (Leaf "a")+                (Leaf "b")+            )+        )+        ( Branch+            (F.toDouble (F.col @Int "ai") .> F.lit (50 :: Double))+            (Leaf "c")+            (Leaf "a")+        )+    ]++sameBranchCollapse :: [Test]+sameBranchCollapse =+    [ prunesTo+        "equal leaves collapse the branch"+        (Branch (F.col @Double "age" .> F.lit (50 :: Double)) (Leaf "a") (Leaf "a"))+        (Leaf "a")+    , prunesTo+        "collapse cascades upward"+        ( Branch+            (F.col @Double "age" .> F.lit (50 :: Double))+            (Branch (F.col @Double "hours" .> F.lit (40 :: Double)) (Leaf "a") (Leaf "a"))+            (Leaf "a")+        )+        (Leaf "a")+    ]++preservedTrees :: [Test]+preservedTrees =+    [ preserved+        "child not tight enough is kept"+        ( Branch+            (F.col @Double "age" .> F.lit (50 :: Double))+            (Branch (F.col @Double "age" .> F.lit (60 :: Double)) (Leaf "a") (Leaf "b"))+            (Leaf "c")+        )+    , preserved+        "double false edge is kept (NaN)"+        ( Branch+            (F.col @Double "weight" .> F.lit (50 :: Double))+            (Leaf "c")+            (Branch (F.col @Double "weight" .< F.lit (60 :: Double)) (Leaf "a") (Leaf "b"))+        )+    , preserved+        "cross-column descendant is kept"+        ( Branch+            (F.col @Double "age" .> F.lit (50 :: Double))+            (Branch (F.col @Double "income" .> F.lit (30000 :: Double)) (Leaf "a") (Leaf "b"))+            (Leaf "c")+        )+    ]++tests :: [Test]+tests = concat [pathEntailment, falseEdgeGate, sameBranchCollapse, preservedTrees]
+ tests/Worklist.hs view
@@ -0,0 +1,466 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | Up-front (TDD) spec for the saturation worklist 'saturateCandidates' that+will replace the lazy generate-all 'boolExprsVec'. The existing depth-bounded+'boolExprsVec' is kept as the behaviour-preservation oracle.++State: 'saturateCandidates' is the identity stub, so the structural same-set+and truth-vector floor/collapse cases FAIL (red) — that is the spec PR1 must+meet; base-inclusion / dedup / determinism hold under the stub.+-}+module Worklist (tests, props) where++import qualified DataFrame as D+import DataFrame.DecisionTree (+    CondVec,+    DedupMode (Structural, TruthVector),+    boolExprsVec,+    combineAndVec,+    combineOrVec,+    cvExpr,+    cvVec,+    materializeCondVec,+    saturateCandidates,+ )+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.Expression (+    Expr,+    compareExpr,+    eSize,+    eqExpr,+    normalize,+ )+import DataFrame.Operators++import Data.Function (on)+import Data.List (minimumBy, nubBy)+import qualified Data.Maybe+import qualified Data.Set as Set+import qualified Data.Vector.Unboxed as VU+import Test.HUnit+import Test.QuickCheck++-- Fixture: x = 0..5, y = 5..0 (anti-correlated), z scrambled (independent of both).+-- Note x>2 and y<3 share the truth vector [F,F,F,T,T,T], so the truth-vector mode+-- must collapse them; z gives a third column for non-consolidating cross-column combos.+fixtureDF :: D.DataFrame+fixtureDF =+    D.fromNamedColumns+        [ ("x", DI.fromList ([0, 1, 2, 3, 4, 5] :: [Double]))+        , ("y", DI.fromList ([5, 4, 3, 2, 1, 0] :: [Double]))+        , ("z", DI.fromList ([2, 5, 1, 4, 0, 3] :: [Double]))+        ]++mat :: Expr Bool -> CondVec+mat e =+    Data.Maybe.fromMaybe+        (error "Worklist.mat: could not materialize")+        (materializeCondVec fixtureDF e)++xGt, xLt, yGt, yLt, zGt, zLt :: Double -> CondVec+xGt n = mat (F.col @Double "x" .>. F.lit n)+xLt n = mat (F.col @Double "x" .<. F.lit n)+yGt n = mat (F.col @Double "y" .>. F.lit n)+yLt n = mat (F.col @Double "y" .<. F.lit n)+zGt n = mat (F.col @Double "z" .>. F.lit n)+zLt n = mat (F.col @Double "z" .<. F.lit n)++-- Same truth vector as 'xGt 2' ([F,F,F,T,T,T]) but eSize 4 vs 3 — a non-degenerate+-- truth-vector collision for the min-eSize representative rule.+notLe2 :: CondVec+notLe2 = mat (F.not (F.col @Double "x" .<=. F.lit 2))++litTrue :: CondVec+litTrue = mat (F.lit True)++keyOf :: CondVec -> String+keyOf = show . normalize . cvExpr++keySet :: [CondVec] -> Set.Set String+keySet = Set.fromList . map keyOf++truthSet :: [CondVec] -> Set.Set [Bool]+truthSet = Set.fromList . map (VU.toList . cvVec)++-- Mirrors 'evalWithPenaltyVec' (DecisionTree.hs): score = (#care-point errors, eSize),+-- depending only on the cached vector + size, so distinct same-vector same-size atoms tie.+penBy :: [Bool] -> CondVec -> (Int, Int)+penBy lbls cv =+    ( length (filter id (zipWith (/=) lbls (VU.toList (cvVec cv))))+    , eSize (cvExpr cv)+    )++-- The candidate 'bestDiscreteCandidate' would select: the first 'minimumBy penalty' winner.+argminKey :: [Bool] -> [CondVec] -> String+argminKey lbls = keyOf . minimumBy (compare `on` penBy lbls)++-- Oracle: the current depth-bounded generate-all.+ref :: Int -> [CondVec] -> [CondVec]+ref d base = boolExprsVec base base 0 d++base3 :: [CondVec]+base3 = [xGt 2, xGt 4, yGt 2]++-- x>2 and y<3 share the truth vector [F,F,F,T,T,T], so truth-vector mode collapses+-- this 3-atom base to 2 distinct vectors while structural mode keeps all three.+collBase :: [CondVec]+collBase = [xGt 2, yLt 3, yGt 2]++-- Wider fixture (3 independent-ish columns, 10 rows) yielding many distinct truth+-- vectors — broader coverage for the truth-vector floor / dedup than the 6-row x/y fixture.+wideDF :: D.DataFrame+wideDF =+    D.fromNamedColumns+        [ ("a", DI.fromList ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9] :: [Double]))+        , ("b", DI.fromList ([9, 7, 5, 3, 1, 8, 6, 4, 2, 0] :: [Double]))+        , ("c", DI.fromList ([1, 1, 2, 2, 3, 3, 4, 4, 5, 5] :: [Double]))+        ]++matW :: Expr Bool -> CondVec+matW e =+    Data.Maybe.fromMaybe+        (error "Worklist.matW: could not materialize")+        (materializeCondVec wideDF e)++wideBase :: [CondVec]+wideBase =+    [ matW (F.col @Double "a" .>. F.lit 3)+    , matW (F.col @Double "b" .<. F.lit 5)+    , matW (F.col @Double "c" .>=. F.lit 3)+    ]++------------------------------------------------------------------------+-- HUnit cases+------------------------------------------------------------------------++tests :: [Test]+tests =+    [ TestLabel "structural: same distinct set as oracle" . TestCase $+        assertEqual+            "keySet"+            (keySet (ref 2 base3))+            (keySet (saturateCandidates Structural 2 base3))+    , TestLabel "structural: output deduped (length == distinct keys)" . TestCase $+        let out = saturateCandidates Structural 2 base3+         in assertEqual "no eqExpr duplicates" (Set.size (keySet out)) (length out)+    , TestLabel "structural: base atoms all present" . TestCase $+        assertBool "base subset of output" $+            keySet base3 `Set.isSubsetOf` keySet (saturateCandidates Structural 1 base3)+    , TestLabel "structural: deterministic" . TestCase $+        assertEqual+            "two runs identical"+            (map keyOf (saturateCandidates Structural 2 base3))+            (map keyOf (saturateCandidates Structural 2 base3))+    , TestLabel "structural: consolidation flows through the worklist" . TestCase $+        -- x>2 ∧ x>4 ↦ x>4, x>2 ∨ x>4 ↦ x>2, so the distinct set is just {x>2, x>4}.+        assertEqual+            "consolidated set"+            (keySet [xGt 2, xGt 4])+            (keySet (saturateCandidates Structural 2 [xGt 2, xGt 4]))+    , TestLabel "truth-vector: all output truth vectors distinct" . TestCase $+        let out = saturateCandidates TruthVector 2 [xGt 2, yLt 3]+         in assertEqual "distinct cvVecs" (Set.size (truthSet out)) (length out)+    , TestLabel "truth-vector: collapses same-truth atoms (x>2, y<3)" . TestCase $+        -- x>2 and y<3 are identical on the data; one representative survives.+        assertEqual+            "collapsed to one"+            1+            (length (saturateCandidates TruthVector 1 [xGt 2, yLt 3]))+    , TestLabel "truth-vector: reaches the semantic floor (no split dropped)"+        . TestCase+        $ assertEqual+            "same distinct truth vectors as oracle"+            (truthSet (ref 2 base3))+            (truthSet (saturateCandidates TruthVector 2 base3))+    , TestLabel "truth-vector: strictly fewer candidates when a collision exists"+        . TestCase+        $+        -- collBase has x>2 ≡ y<3, so the truth-vector floor is strictly below the structural set.+        assertBool "|truth| < |structural|"+        $ length (saturateCandidates TruthVector 2 collBase)+            < length (saturateCandidates Structural 2 collBase)+    , TestLabel "truth-vector: keeps the minimum-eSize representative" . TestCase $+        -- x>2 (eSize 3) and not(x<=2) (eSize 4) share a truth vector; the smaller survives.+        let out = saturateCandidates TruthVector 1 [xGt 2, notLe2]+         in do+                assertEqual "min eSize survivor" [3] (map (eSize . cvExpr) out)+                assertEqual "survivor is x>2" [keyOf (xGt 2)] (map keyOf out)+    , TestLabel "truth-vector: tie-break independent of input order" . TestCase $+        -- x>2 and y<3 tie on eSize 3; whichever survives must not depend on base order.+        assertEqual+            "order-independent survivor"+            (map keyOf (saturateCandidates TruthVector 1 [xGt 2, yLt 3]))+            (map keyOf (saturateCandidates TruthVector 1 [yLt 3, xGt 2]))+    , TestLabel "edge: empty base" . TestCase $+        assertEqual+            "empty in, empty out"+            Set.empty+            (keySet (saturateCandidates Structural 2 []))+    , TestLabel "edge: singleton base" . TestCase $+        assertEqual+            "same as oracle"+            (keySet (ref 2 [xGt 2]))+            (keySet (saturateCandidates Structural 2 [xGt 2]))+    , TestLabel "edge: maxDepth 0 is the base only" . TestCase $+        assertEqual+            "no expansion"+            (keySet base3)+            (keySet (saturateCandidates Structural 0 base3))+    , TestLabel "edge: duplicate-seeded base" . TestCase $+        let base = [xGt 2, xGt 2, yGt 2]+         in assertEqual+                "dedups seed, same as oracle"+                (keySet (ref 2 base))+                (keySet (saturateCandidates Structural 2 base))+    , TestLabel "edge: literal operand" . TestCase $+        let base = [xGt 2, litTrue]+         in assertEqual+                "same as oracle"+                (keySet (ref 2 base))+                (keySet (saturateCandidates Structural 2 base))+    , TestLabel "law: cvVec is a homomorphism over AND" . TestCase $+        -- the law justifying one-representative-per-truth-class dedup.+        assertEqual+            "cvVec(a∧b) == cvVec a && cvVec b"+            (VU.toList (VU.zipWith (&&) (cvVec (xGt 2)) (cvVec (yGt 2))))+            (VU.toList (cvVec (combineAndVec (xGt 2) (yGt 2))))+    , TestLabel "law: cvVec is a homomorphism over OR" . TestCase $+        assertEqual+            "cvVec(a∨b) == cvVec a || cvVec b"+            (VU.toList (VU.zipWith (||) (cvVec (xGt 2)) (cvVec (yGt 2))))+            (VU.toList (cvVec (combineOrVec (xGt 2) (yGt 2))))+    , TestLabel "law: consolidated expr re-interprets to its cached vector" . TestCase $+        -- x>2 ∧ x>4 consolidates to x>4; the cached vector must match re-materializing it.+        let c = combineAndVec (xGt 2) (xGt 4)+         in assertEqual+                "cached == re-interpreted"+                (VU.toList (cvVec c))+                (VU.toList (cvVec (mat (cvExpr c))))+    , TestLabel "structural: output order matches the deduped oracle" . TestCase $+        -- byte-identical to today's boolExprsVec, with eqExpr-duplicates removed (first kept):+        -- this is what lets the consumer's first-wins minimumBy pick the same candidate.+        assertEqual+            "deduped-oracle order"+            (map keyOf (nubBy ((==) `on` keyOf) (ref 2 base3)))+            (map keyOf (saturateCandidates Structural 2 base3))+    , TestLabel+        "structural: matches oracle set+order at depth 3+4 (non-consolidating)"+        . TestCase+        $+        -- cross-column base whose closure GROWS with depth (no consolidation); this is where the+        -- frontier:=admitted optimisation could diverge from the oracle's frontier:=all-products.+        let b = [xGt 2, yGt 2, zGt 1]+            deduped d = map keyOf (nubBy ((==) `on` keyOf) (ref d b))+            out d = map keyOf (saturateCandidates Structural d b)+         in do+                assertEqual "set d3" (Set.fromList (deduped 3)) (Set.fromList (out 3))+                assertEqual "order d3" (deduped 3) (out 3)+                assertEqual "set d4" (Set.fromList (deduped 4)) (Set.fromList (out 4))+                assertEqual "order d4" (deduped 4) (out 4)+    , TestLabel "structural: stabilizes at fixpoint (depth cap is a no-op past it)"+        . TestCase+        $+        -- all AND/OR consolidate back into the base ⇒ a genuine fixpoint at round 1, so deeper+        -- depth caps add nothing. (For a non-consolidating base the closure grows with depth,+        -- since 'normalize' does not flatten associativity — there the cap always binds.)+        assertEqual+            "depth 2 == depth 5"+            (keySet (saturateCandidates Structural 2 [xGt 1, xGt 2, xGt 3, xGt 4]))+            (keySet (saturateCandidates Structural 5 [xGt 1, xGt 2, xGt 3, xGt 4]))+    , TestLabel "truth-vector: reaches the floor on a wider fixture" . TestCase $+        assertEqual+            "same distinct truth vectors as oracle"+            (truthSet (ref 2 wideBase))+            (truthSet (saturateCandidates TruthVector 2 wideBase))+    , TestLabel "selection: surfaces the oracle's winning combination" . TestCase $+        -- labels = x>2 ∧ x<5; the unique min-penalty split is that band (not in the base), so+        -- 'minimumBy penalty' over the worklist must pick it just as it does over the oracle.+        let lbls = [False, False, False, True, True, False]+            base = [xGt 2, xLt 5, yGt 2]+         in assertEqual+                "same argmin as oracle"+                (argminKey lbls (ref 2 base))+                (argminKey lbls (saturateCandidates Structural 2 base))+    , TestLabel "selection: tie-winner tracks input order, matching the oracle"+        . TestCase+        $+        -- x>2 and y<3 both score the min (0,3); 'minimumBy' keeps the first, so the winner must+        -- flip with input order exactly as the oracle does. A worklist that imposes its own+        -- (eSize, exprKey) order would pick the same atom for both orders and fail one. (byte-identical)+        let lbls = [False, False, False, True, True, True]+         in do+                assertEqual+                    "x>2-first order"+                    (argminKey lbls (ref 2 [xGt 2, yLt 3]))+                    (argminKey lbls (saturateCandidates Structural 2 [xGt 2, yLt 3]))+                assertEqual+                    "y<3-first order"+                    (argminKey lbls (ref 2 [yLt 3, xGt 2]))+                    (argminKey lbls (saturateCandidates Structural 2 [yLt 3, xGt 2]))+    , TestLabel+        "bounded: output is the distinct closure, below the oracle's materialized count"+        . TestCase+        $+        -- Same-direction thresholds: every AND/OR consolidates, so the closure stays these 4 while+        -- the oracle materializes far more. (Peak residency is the +RTS -s integration check.)+        let base = [xGt 1, xGt 2, xGt 3, xGt 4]+            gen = ref 3 base+         in do+                assertEqual+                    "output bounded to the distinct closure"+                    (Set.size (keySet gen))+                    (length (saturateCandidates Structural 3 base))+                assertBool+                    "oracle materializes more than the closure (the explosion the worklist avoids)"+                    (Set.size (keySet gen) < length gen)+    , TestLabel "structural: maxDepth 1 is base-only (no combination round)"+        . TestCase+        $+        -- boolExprsVec does no combining until depth 2; the worklist must match it depth-for-depth.+        assertEqual+            "no combination at depth 1"+            (keySet (ref 1 [xGt 2, yGt 2]))+            (keySet (saturateCandidates Structural 1 [xGt 2, yGt 2]))+    , TestLabel "structural: base atoms survive the combination round" . TestCase $+        -- a base atom regenerated by a combination must not be dropped.+        -- a base atom regenerated by a combination must not be dropped.+        -- a base atom regenerated by a combination must not be dropped.+        -- a base atom regenerated by a combination must not be dropped.+        -- a base atom regenerated by a combination must not be dropped.+        assertBool "base subset of output at depth 2" $+            keySet base3 `Set.isSubsetOf` keySet (saturateCandidates Structural 2 base3)+    , TestLabel+        "structural: re-saturating a closed base is stable (fixpoint idempotence)"+        . TestCase+        $+        -- on a base whose closure is itself, re-saturating changes nothing. (Depth-bounded+        -- saturation is NOT idempotent on a growing closure — re-feeding goes one round deeper.)+        let b = [xGt 1, xGt 2, xGt 3, xGt 4]+         in assertEqual+                "saturate ∘ saturate == saturate"+                (keySet (saturateCandidates Structural 2 b))+                (keySet (saturateCandidates Structural 2 (saturateCandidates Structural 2 b)))+    , TestLabel "law: combiner key is order-independent (congruence basis)" . TestCase $+        -- combining respects 'normalize', so deduping before combining is sound; also exercises+        -- consolidation in both operand orders.+        do+            assertEqual+                "AND consolidation commutes at the key"+                (keyOf (combineAndVec (xGt 2) (xGt 4)))+                (keyOf (combineAndVec (xGt 4) (xGt 2)))+            assertEqual+                "OR consolidation commutes at the key"+                (keyOf (combineOrVec (xGt 2) (xGt 4)))+                (keyOf (combineOrVec (xGt 4) (xGt 2)))+            assertEqual+                "cross-column AND commutes at the key"+                (keyOf (combineAndVec (xGt 2) (yGt 2)))+                (keyOf (combineAndVec (yGt 2) (xGt 2)))+    , TestLabel+        "truth-vector: section is the (eSize, compareExpr)-minimum of the fiber"+        . TestCase+        $+        -- not merely order-independent: the survivor is the deterministic min, never the max.+        let fiber = [xGt 2, yLt 3]+            cmp a b =+                compare (eSize (cvExpr a)) (eSize (cvExpr b))+                    <> compareExpr (cvExpr a) (cvExpr b)+            want = keyOf (minimumBy cmp fiber)+         in assertEqual+                "min-section survivor"+                [want]+                (map keyOf (saturateCandidates TruthVector 1 fiber))+    ]++------------------------------------------------------------------------+-- QuickCheck properties (over generated base pools and depths)+------------------------------------------------------------------------++genAtom :: Gen CondVec+genAtom =+    elements+        [xGt 1, xGt 2, xGt 3, xLt 2, xLt 4, yGt 1, yGt 3, yLt 3, zGt 1, zGt 3, zLt 4]++genBase :: Gen [CondVec]+genBase = choose (2, 5) >>= \k -> vectorOf k genAtom++-- Random label vector of the fixture's length (6 rows), for selection-preservation.+genLabels :: Gen [Bool]+genLabels = vectorOf 6 (elements [False, True])++prop_structuralSameSet :: Property+prop_structuralSameSet =+    forAllBlind genBase $ \base ->+        forAll (choose (1, 3)) $ \d ->+            counterexample (show (map keyOf base, d)) $+                keySet (saturateCandidates Structural d base) === keySet (ref d base)++prop_truthVectorFloor :: Property+prop_truthVectorFloor =+    forAllBlind genBase $ \base ->+        forAll (choose (1, 3)) $ \d ->+            counterexample (show (map keyOf base, d)) $+                truthSet (saturateCandidates TruthVector d base) === truthSet (ref d base)++-- The candidate *set* depends only on the base as a set, not its input order. (Output++-- * order* tracks input order — that is the byte-identity contract, see the selection tests.)+prop_orderInvariant :: Property+prop_orderInvariant =+    forAllBlind genBase $ \base ->+        forAllBlind (shuffle base) $ \base' ->+            forAll (choose (1, 3)) $ \d ->+                counterexample (show (map keyOf base, map keyOf base', d)) $+                    keySet (saturateCandidates Structural d base)+                        === keySet (saturateCandidates Structural d base')++-- The candidate the consumer's 'minimumBy penaltyCV' selects is byte-identical to the oracle's,+-- for any label vector (d >= 2 so combinations exist). This is the model-preservation contract.+prop_selectionPreserved :: Property+prop_selectionPreserved =+    forAllBlind genBase $ \base ->+        forAllBlind genLabels $ \lbls ->+            forAll (choose (2, 3)) $ \d ->+                counterexample (show (map keyOf base, lbls, d)) $+                    argminKey lbls (saturateCandidates Structural d base)+                        === argminKey lbls (ref d base)++-- The full output (order included) is byte-identical to the deduped oracle, at every depth.+-- Subsumes selection-preservation for ANY (cvVec,eSize)-penalty, and stresses the+-- frontier:=admitted optimisation past the depth where it could first diverge.+prop_orderMatchesOracle :: Property+prop_orderMatchesOracle =+    forAllBlind genBase $ \base ->+        forAll (choose (2, 3)) $ \d ->+            counterexample (show (map keyOf base, d)) $+                map keyOf (saturateCandidates Structural d base)+                    === map keyOf (nubBy ((==) `on` keyOf) (ref d base))++-- The structural key faithfully represents the 'eqExpr' quotient on the candidate domain+-- (atoms and their AND/OR products): show.normalize merges exactly what eqExpr merges.+genCand :: Gen CondVec+genCand =+    oneof+        [ genAtom+        , combineAndVec <$> genAtom <*> genAtom+        , combineOrVec <$> genAtom <*> genAtom+        ]++prop_keyFaithful :: Property+prop_keyFaithful =+    forAllBlind genCand $ \a ->+        forAllBlind genCand $ \b ->+            counterexample (keyOf a ++ "  vs  " ++ keyOf b) $+                (keyOf a == keyOf b) === eqExpr (cvExpr a) (cvExpr b)++props :: [Property]+props =+    [ prop_structuralSameSet+    , prop_truthVectorFloor+    , prop_orderInvariant+    , prop_selectionPreserved+    , prop_orderMatchesOracle+    , prop_keyFaithful+    ]
+ tests/data/transactions.parquet view

binary file changed (absent → 1746 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