data-sampler 3.2.1__tar.gz → 3.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {data_sampler-3.2.1 → data_sampler-3.4.0}/CHANGELOG.md +64 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/PKG-INFO +180 -16
- {data_sampler-3.2.1 → data_sampler-3.4.0}/README.md +178 -14
- {data_sampler-3.2.1 → data_sampler-3.4.0}/RELEASING.md +13 -17
- {data_sampler-3.2.1 → data_sampler-3.4.0}/ROADMAP.md +10 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/TROUBLESHOOTING.md +57 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/pyproject.toml +2 -2
- {data_sampler-3.2.1 → data_sampler-3.4.0}/src/data_sampler/__init__.py +9 -2
- {data_sampler-3.2.1 → data_sampler-3.4.0}/src/data_sampler/cli.py +135 -3
- {data_sampler-3.2.1 → data_sampler-3.4.0}/src/data_sampler/engine.py +121 -21
- data_sampler-3.4.0/src/data_sampler/reduce.py +343 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/src/data_sampler/report.py +76 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/src/data_sampler/tui/app.py +85 -4
- {data_sampler-3.2.1 → data_sampler-3.4.0}/tests/test_cli.py +136 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/tests/test_engine.py +128 -0
- data_sampler-3.4.0/tests/test_reduce.py +291 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/tests/test_tui.py +104 -6
- {data_sampler-3.2.1 → data_sampler-3.4.0}/.gitattributes +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/.github/workflows/release.yml +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/.gitignore +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/LICENSE +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/examples/employees.csv +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/examples/using_data_sampler.ipynb +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/requirements.txt +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/scripts/run-tui.bat +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/scripts/run-tui.sh +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/src/data_sampler/__main__.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/src/data_sampler/_logging.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/src/data_sampler/_names.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/src/data_sampler/anonymize.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/src/data_sampler/io.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/src/data_sampler/sampling.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/src/data_sampler/stats.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/src/data_sampler/tui/__init__.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/src/data_sampler/workflow.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/tests/conftest.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/tests/test_anonymize.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/tests/test_io.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/tests/test_report.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/tests/test_sampling.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/tests/test_stats.py +0 -0
- {data_sampler-3.2.1 → data_sampler-3.4.0}/tests/test_workflow.py +0 -0
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.4.0 — 2026-07-24
|
|
4
|
+
|
|
5
|
+
- **Added optional PCA column reduction** (`data_sampler.reduce`): after
|
|
6
|
+
sampling (and anonymization, if any), the numeric block of the outgoing
|
|
7
|
+
sample can be collapsed into its first *k* principal components — so the
|
|
8
|
+
delivered file is narrow as well as short. *k* is controlled one of two
|
|
9
|
+
ways: a component count (`--reduce-components N` /
|
|
10
|
+
`reduce_columns(df, n_components=N)`, capped at the number of usable
|
|
11
|
+
numeric columns) or a variance target (`--reduce-variance R` /
|
|
12
|
+
`variance_ratio=R`: the fewest components whose cumulative
|
|
13
|
+
explained-variance ratio reaches R). Non-numeric columns are always
|
|
14
|
+
preserved; `--reduce-exclude` passes chosen numeric columns (e.g.
|
|
15
|
+
identifiers) through unchanged, `--reduce-prefix` renames the `PC*`
|
|
16
|
+
output columns when the source already has one, and the tool warns when
|
|
17
|
+
an included column looks like an identifier. Reduce flags are validated
|
|
18
|
+
before sampling starts, so a typo never costs a full out-of-core run. Missing values are mean-imputed (the row count
|
|
19
|
+
never changes), constant/all-missing columns pass through with a note, and
|
|
20
|
+
columns are z-scored by default (`--reduce-no-standardize` opts out) so
|
|
21
|
+
large-unit columns cannot dominate. The exact-SVD implementation is pure
|
|
22
|
+
numpy — no new dependency — and deterministic across runs and platforms
|
|
23
|
+
(sign-fixed singular vectors). New public API: `reduce_columns`,
|
|
24
|
+
`ReductionResult`, `format_reduction_report`.
|
|
25
|
+
- **Every reduction reports its rationale**, not just the variance kept: the
|
|
26
|
+
report lists per-component and cumulative explained variance, **the groups
|
|
27
|
+
of correlated columns that move together** (connected components of the
|
|
28
|
+
|r| ≥ 0.7 correlation graph — on standardized data PCA diagonalizes
|
|
29
|
+
exactly that correlation matrix, so the groups are a faithful account of
|
|
30
|
+
what compressed), each component's top driving columns, and any
|
|
31
|
+
imputation/passthrough/identifier notes. The full labelled correlation
|
|
32
|
+
matrix is available on `ReductionResult.correlation_matrix`.
|
|
33
|
+
- Wired into all three surfaces: CLI flags on both the pandas and DuckDB
|
|
34
|
+
engine paths (the reduction always runs on the small sampled frame, never
|
|
35
|
+
the full source), a `reduce` control in the TUI run bar with the rationale
|
|
36
|
+
shown on the report screen, and the Python API. Output files gain a
|
|
37
|
+
`_pca{k}` tag suffix (e.g. `data_sample_500_anon_pca3.csv`).
|
|
38
|
+
|
|
39
|
+
## v3.3.1 — 2026-07-23
|
|
40
|
+
|
|
41
|
+
- **Fixed the second CI-only TUI race** (caught, again, by the release CI —
|
|
42
|
+
it blocked v3.3.0's PyPI publish): a stale `Changed` message (mount-time
|
|
43
|
+
echo or superseded edit, delivered late because every Textual widget runs
|
|
44
|
+
its own message pump) could roll an anonymizer choice back to `none` and
|
|
45
|
+
wipe its options. All Select/Input/Switch handlers now drop any `Changed`
|
|
46
|
+
whose value no longer matches the widget's current value — provably stale
|
|
47
|
+
— which also subsumes the v3.2.1 duplicate-highlight fix. v3.3.0 was never
|
|
48
|
+
published to PyPI; this release carries all of its changes.
|
|
49
|
+
|
|
50
|
+
## v3.3.0 — 2026-07-23
|
|
51
|
+
|
|
52
|
+
- **Two-phase narrow sampling in the DuckDB engine.** The expensive phase of a
|
|
53
|
+
sample (the per-stratum window sort, or the reservoir buffer) now runs over
|
|
54
|
+
only the stratification columns plus a stable row id — `file_row_number` for
|
|
55
|
+
single-file Parquet sources, a positional id for DataFrames — and a second
|
|
56
|
+
pass fetches the winning rows with every column. Measured on 300k rows ×
|
|
57
|
+
401 columns: stratified sampling 2.5× faster on Parquet and 9.5× faster for
|
|
58
|
+
DataFrame sources (whose wide payload never enters the SQL engine; winners
|
|
59
|
+
return as dtype-preserving pandas slices). CSV/TSV/JSON and multi-file
|
|
60
|
+
Parquet globs keep the previous single-pass shape (per-file row numbers are
|
|
61
|
+
not a global id — verification caught that a glob would otherwise be
|
|
62
|
+
silently over-sampled — and text formats must re-parse per scan anyway).
|
|
63
|
+
No behavioral change to counts, allocation, NaN strata, or determinism;
|
|
64
|
+
note that *which* rows a given seed selects changes vs v3.2.1 for
|
|
65
|
+
single-file Parquet and DataFrame sources.
|
|
66
|
+
|
|
3
67
|
## v3.2.1 — 2026-07-23
|
|
4
68
|
|
|
5
69
|
- **Fixed a TUI race that only surfaced on slow machines** (caught by the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: data-sampler
|
|
3
|
-
Version: 3.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 3.4.0
|
|
4
|
+
Summary: Hand someone data that looks and behaves like your production data, isn't your production data, and provably kept its statistical variety — stratified sampling + consistent anonymization, with a terminal UI.
|
|
5
5
|
Project-URL: Homepage, https://github.com/aaronified/data-sampler
|
|
6
6
|
Project-URL: Changelog, https://github.com/aaronified/data-sampler/blob/main/CHANGELOG.md
|
|
7
7
|
Author: aaronified
|
|
@@ -55,23 +55,51 @@ Description-Content-Type: text/markdown
|
|
|
55
55
|
|
|
56
56
|
# data-sampler
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
anonymization and a colorful terminal UI.
|
|
58
|
+
[](https://pypi.org/project/data-sampler/)
|
|
59
|
+
[](https://pypi.org/project/data-sampler/)
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
**Hand someone data that looks and behaves like your production data, isn't
|
|
62
|
+
your production data, and provably kept its statistical variety — in one
|
|
63
|
+
command.**
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
data-sampler customers.xlsx 500 --suggest
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Use it to:
|
|
70
|
+
|
|
71
|
+
- **Share a realistic slice with a vendor or contractor** — names, ids,
|
|
72
|
+
emails, salaries, and dates anonymized, but every distribution, duplicate,
|
|
73
|
+
and group structure intact.
|
|
74
|
+
- **Attach a repro to a bug report** without leaking the real records that
|
|
75
|
+
trigger it.
|
|
76
|
+
- **Cut a 2 GB export down to 500 rows** for a demo or prototype that still
|
|
77
|
+
behaves like the real thing.
|
|
78
|
+
- **Build test fixtures that mirror production skew** instead of uniform toy
|
|
79
|
+
data.
|
|
80
|
+
- **Give a class or workshop realistic data** without a data-sharing
|
|
81
|
+
agreement.
|
|
82
|
+
- **Pull 10,000 representative rows from a 100M-row Parquet file** without
|
|
83
|
+
loading it — the optional DuckDB engine samples out-of-core, in parallel.
|
|
84
|
+
- **Prove the sample is representative**: every run produces a side-by-side
|
|
85
|
+
source-vs-sample distribution report, per column.
|
|
86
|
+
|
|
87
|
+
How: stratified sampling preserves the statistical variety of your data
|
|
88
|
+
(strata are detected automatically), and every anonymizer maps each unique
|
|
89
|
+
original value to exactly one replacement — so repeated values stay repeated
|
|
90
|
+
and the joint distributions survive anonymization. Everything ships as a
|
|
91
|
+
single Python package: a colorful terminal UI for non-programmers, a headless
|
|
92
|
+
CLI, and a plain Python API.
|
|
65
93
|
|
|
66
94
|
## Install
|
|
67
95
|
|
|
68
96
|
```sh
|
|
69
|
-
pip install data-sampler
|
|
70
|
-
#
|
|
71
|
-
pip install -e ".[dev]"
|
|
97
|
+
pip install data-sampler # from PyPI
|
|
98
|
+
pip install "data-sampler[large]" # + the out-of-core DuckDB engine for huge files
|
|
72
99
|
```
|
|
73
100
|
|
|
74
|
-
Requires Python 3.10+.
|
|
101
|
+
Requires Python 3.10+. For development, clone the repo and
|
|
102
|
+
`pip install -e ".[dev]"`.
|
|
75
103
|
|
|
76
104
|
## Terminal UI
|
|
77
105
|
|
|
@@ -99,7 +127,8 @@ The TUI is a panel-based dashboard (think btop / lazydocker):
|
|
|
99
127
|
VS Code extension). Select a column to see full stats and distribution
|
|
100
128
|
bars, choose an anonymizer for it, and toggle whether it should be
|
|
101
129
|
skipped when preserving statistical variety (stratification). Set the
|
|
102
|
-
sample size, output folder, optional seed,
|
|
130
|
+
sample size, output folder, optional seed, an optional PCA column
|
|
131
|
+
reduction (`reduce`: N components or a variance target), and run.
|
|
103
132
|
3. **Report screen** — the stratification comparison and anonymization summary
|
|
104
133
|
on the left, and a **column histograms** panel on the right showing every
|
|
105
134
|
column's source-vs-sample distribution (numeric columns share bin edges;
|
|
@@ -126,6 +155,11 @@ data-sampler <source> <count> [options]
|
|
|
126
155
|
| `--anon COL=KIND[:k=v,...]` | Anonymize a column (repeatable) |
|
|
127
156
|
| `-i`, `--interactive` | Guided workflow: choose an anonymizer type per column from a menu |
|
|
128
157
|
| `--suggest` | Auto-assign a suggested anonymizer type to each column from its stats |
|
|
158
|
+
| `--reduce-components N` | PCA column reduction: replace the numeric columns with the first `N` principal components |
|
|
159
|
+
| `--reduce-variance R` | PCA column reduction: keep the fewest components whose cumulative explained variance reaches `R` (0 < R < 1) |
|
|
160
|
+
| `--reduce-exclude COL[,COL]` | Numeric column(s) to keep out of the reduction, e.g. identifiers (repeatable) |
|
|
161
|
+
| `--reduce-prefix PREFIX` | Name prefix for the component columns (default `PC` → `PC1`, `PC2`, …) |
|
|
162
|
+
| `--reduce-no-standardize` | Skip the per-column z-scoring before PCA |
|
|
129
163
|
| `--engine {auto,pandas,duckdb}` | Sampling engine (default `auto`: DuckDB for Parquet/large inputs, pandas otherwise) |
|
|
130
164
|
| `--threads N` | DuckDB engine: number of threads (default: all cores) |
|
|
131
165
|
| `--memory-limit SIZE` | DuckDB engine: memory limit before spilling to disk (e.g. `8GB`) |
|
|
@@ -144,6 +178,11 @@ data-sampler data.csv 100 --skip region,notes --seed 7 \
|
|
|
144
178
|
|
|
145
179
|
# large / out-of-core: sample a Parquet file in parallel with DuckDB
|
|
146
180
|
data-sampler huge.parquet 10000 --engine duckdb --threads 8 --memory-limit 8GB --suggest
|
|
181
|
+
|
|
182
|
+
# narrow the sample too: collapse the numeric columns into 3 principal
|
|
183
|
+
# components (or keep however many retain 90% of the variance)
|
|
184
|
+
data-sampler wide.csv 500 --reduce-components 3 --reduce-exclude cust_id
|
|
185
|
+
data-sampler wide.csv 500 --reduce-variance 0.9
|
|
147
186
|
```
|
|
148
187
|
|
|
149
188
|
## Python API
|
|
@@ -176,7 +215,11 @@ anon = ds.anonymize(
|
|
|
176
215
|
seed=7,
|
|
177
216
|
)
|
|
178
217
|
|
|
179
|
-
|
|
218
|
+
# optionally collapse the numeric columns into principal components
|
|
219
|
+
red = ds.reduce_columns(anon, variance_ratio=0.9, exclude=["cust_id"])
|
|
220
|
+
print(ds.format_reduction_report(red)) # variance kept + correlated groups
|
|
221
|
+
|
|
222
|
+
ds.save_output(red.data, "data.xlsx", tag="sample_500_anon_pca")
|
|
180
223
|
```
|
|
181
224
|
|
|
182
225
|
## Try it: bundled example
|
|
@@ -344,6 +387,34 @@ category. A side-by-side distribution report is produced for every run.
|
|
|
344
387
|
If no suitable stratification columns exist, the tool falls back to pure
|
|
345
388
|
random sampling automatically.
|
|
346
389
|
|
|
390
|
+
### Reducing columns (PCA)
|
|
391
|
+
|
|
392
|
+
Sampling narrows the *rows*; the optional PCA step narrows the *columns* of
|
|
393
|
+
the outgoing sample. It replaces the numeric block with its first *k*
|
|
394
|
+
principal components (`PC1..PCk`), controlled one of two ways:
|
|
395
|
+
|
|
396
|
+
- **`--reduce-components N`** — `N` components in the output (capped at the
|
|
397
|
+
number of usable numeric columns, with a note when fewer are possible);
|
|
398
|
+
- **`--reduce-variance R`** — the fewest components whose cumulative
|
|
399
|
+
explained-variance ratio reaches `R` (e.g. `0.9` keeps ≥ 90 % of the
|
|
400
|
+
numeric variance).
|
|
401
|
+
|
|
402
|
+
Non-numeric columns (ids, categories, text, booleans, datetimes) are always
|
|
403
|
+
preserved, and `--reduce-exclude` keeps chosen numeric columns out too —
|
|
404
|
+
identifiers should be excluded, since an all-unique id forms its own
|
|
405
|
+
artificial component (the tool warns when it spots one). Missing values are
|
|
406
|
+
mean-imputed so the row count never changes; constant columns carry no signal
|
|
407
|
+
and pass through unchanged. Columns are z-scored first by default (PCA on the
|
|
408
|
+
correlation matrix), so a large-unit column such as a salary cannot dominate
|
|
409
|
+
the components; `--reduce-no-standardize` turns that off.
|
|
410
|
+
|
|
411
|
+
Every reduction prints its rationale: the variance each component retains,
|
|
412
|
+
**the groups of correlated columns** that move together (which is exactly the
|
|
413
|
+
redundancy PCA collapses — on standardized data PCA diagonalizes the
|
|
414
|
+
correlation matrix), and each component's top driving columns. The reduction
|
|
415
|
+
runs after anonymization, on the already-sampled rows, so the stratification
|
|
416
|
+
and histogram reports still describe the original columns.
|
|
417
|
+
|
|
347
418
|
## Large data: the out-of-core DuckDB engine
|
|
348
419
|
|
|
349
420
|
The default pandas path loads the whole file into memory. For inputs that are
|
|
@@ -394,6 +465,97 @@ The pandas path on the same file: 5.6 s total while materializing a ~0.9 GB
|
|
|
394
465
|
frame in RAM — the engine's reservoir sampling is ~50× faster and never
|
|
395
466
|
materializes the source at all.
|
|
396
467
|
|
|
468
|
+
## How it scales: the algorithms
|
|
469
|
+
|
|
470
|
+
Every trick used to handle millions-to-billions of rows and thousands of
|
|
471
|
+
columns, in one place.
|
|
472
|
+
|
|
473
|
+
### Millions (to billions) of rows
|
|
474
|
+
|
|
475
|
+
- **Out-of-core execution.** With the DuckDB engine, loading, stratification,
|
|
476
|
+
and sampling run inside a vectorized, multi-threaded SQL engine with a
|
|
477
|
+
memory limit and a temp directory — it spills to disk instead of OOM-ing,
|
|
478
|
+
and only the resulting sample ever becomes a DataFrame.
|
|
479
|
+
- **Reservoir sampling** for the random case: a single streaming pass with
|
|
480
|
+
O(sample size) memory, an exact row count, and `REPEATABLE(seed)`
|
|
481
|
+
reproducibility that is independent of the file format and thread count.
|
|
482
|
+
- **Two-pass stratified sampling.** Pass 1 is a `GROUP BY` over the
|
|
483
|
+
stratification columns — one row per stratum, tiny regardless of source
|
|
484
|
+
size. Largest-remainder proportional allocation is computed on that tiny
|
|
485
|
+
table in numpy. Pass 2 ranks rows per stratum with
|
|
486
|
+
`row_number() OVER (PARTITION BY strata ORDER BY random())` and keeps the
|
|
487
|
+
first *allocation* rows of each, joining the allocation table with
|
|
488
|
+
`IS NOT DISTINCT FROM` so missing-value strata are sampled too. DuckDB
|
|
489
|
+
parallelizes the partitions and spills the window sort if needed.
|
|
490
|
+
- **Parquet projection pushdown.** Pass 1 and the stats queries touch only
|
|
491
|
+
the columns they reference, so a wide Parquet file is never read in full.
|
|
492
|
+
- **Two-phase narrow sampling.** The expensive phase (the per-stratum window
|
|
493
|
+
sort, or the reservoir buffer) runs over *only the stratification columns
|
|
494
|
+
plus a stable row id* — `file_row_number` for single-file Parquet, a
|
|
495
|
+
positional id for DataFrames — and a second pass fetches just the winning
|
|
496
|
+
rows with every column. The sort never carries the wide payload: measured
|
|
497
|
+
2.5× on a 400-column Parquet file and 9.5× for wide DataFrames (whose
|
|
498
|
+
payload never enters the SQL engine at all — winners come back as
|
|
499
|
+
dtype-preserving pandas slices). CSV/JSON and multi-file Parquet globs
|
|
500
|
+
keep the single-pass shape, which is the correct one there (text must be
|
|
501
|
+
re-parsed per scan, and per-file row numbers aren't a global id).
|
|
502
|
+
- **Determinism engineering.** DuckDB's `GROUP BY` output order is
|
|
503
|
+
nondeterministic, so the stratum order is pinned with `ORDER BY … NULLS
|
|
504
|
+
LAST` before allocation (otherwise remainder ties break differently run to
|
|
505
|
+
run); seeded stratified runs drop to a single thread because `random()`
|
|
506
|
+
ordering is only reproducible that way, then restore the thread count.
|
|
507
|
+
- **Row-count caching.** `count(*)` runs once per source per engine session
|
|
508
|
+
instead of once per operation.
|
|
509
|
+
- **Vectorized anonymizers.** The column is dictionary-encoded once with
|
|
510
|
+
`pd.factorize`, each *unique* value gets one replacement, and the result is
|
|
511
|
+
assembled by a fancy-index gather — the in-process equivalent of a native
|
|
512
|
+
join against a mapping table. Cost scales with unique values plus one
|
|
513
|
+
vectorized pass, never a Python loop over rows: sequential IDs are an
|
|
514
|
+
`np.arange`, numeric/datetime jitter are single vectorized RNG draws.
|
|
515
|
+
|
|
516
|
+
### Thousands of columns
|
|
517
|
+
|
|
518
|
+
- **One scan for all scalar stats.** `DuckDBEngine.stats()` computes count,
|
|
519
|
+
distinct, min/max/mean/std, and median for *every column in a single
|
|
520
|
+
aggregate query* — one streaming pass over the data regardless of how many
|
|
521
|
+
columns there are.
|
|
522
|
+
- **Sketches instead of sorts.** Distinct counts use HyperLogLog
|
|
523
|
+
(`approx_count_distinct`) and medians use `approx_quantile` — streaming
|
|
524
|
+
approximations with fixed memory per column, no per-column sort or full
|
|
525
|
+
hash table. Exact mode (`approximate=False`) exists for small data, and
|
|
526
|
+
approximate results are flagged on the stats object.
|
|
527
|
+
- **`distributions=False`** skips the per-column histogram/top-k passes
|
|
528
|
+
entirely, so very wide tables get exactly one scan (this is what the CLI's
|
|
529
|
+
`--suggest` uses to pick anonymizer types).
|
|
530
|
+
- **Bounded stratification search.** Candidate columns are screened in one
|
|
531
|
+
aggregate pass (HLL cardinality + average text length), then a greedy
|
|
532
|
+
fewest-categories-first selection keeps the *joint* stratum count at or
|
|
533
|
+
below the sample size — so the group count stays bounded no matter how
|
|
534
|
+
many columns the file has.
|
|
535
|
+
- **Pandas-path trims.** Numeric columns skip the top-values stringification
|
|
536
|
+
(~1.9× faster stats), report histograms skip near-unique columns instead
|
|
537
|
+
of hashing millions of ids for a meaningless top-8, and the TUI computes
|
|
538
|
+
stats in a worker thread so the UI never freezes on load.
|
|
539
|
+
- **PCA column reduction on the way out.** `--reduce-components` /
|
|
540
|
+
`--reduce-variance` collapse a wide numeric block into a handful of
|
|
541
|
+
principal components *after* sampling, so the SVD runs on the small sampled
|
|
542
|
+
frame (never the full source) and the delivered file is narrow as well as
|
|
543
|
+
short.
|
|
544
|
+
|
|
545
|
+
### Correctness under scale
|
|
546
|
+
|
|
547
|
+
Details that only bite on real data, all regression-tested: DuckDB treats NaN
|
|
548
|
+
as a value rather than NULL, so every NaN-sensitive aggregate is filtered
|
|
549
|
+
(`FILTER (WHERE NOT isnan(…))`); missing-value strata survive the allocation
|
|
550
|
+
join via `IS NOT DISTINCT FROM`; HyperLogLog estimates are clamped to each
|
|
551
|
+
column's non-null count; and columns-oriented JSON (the pandas `to_json()`
|
|
552
|
+
default, which SQL engines parse as one giant row) is detected and refused
|
|
553
|
+
with guidance.
|
|
554
|
+
|
|
555
|
+
Known trade-offs: CSV sources are re-parsed per query (the streaming design —
|
|
556
|
+
convert to Parquet for repeated work), and a seeded stratified run gives up
|
|
557
|
+
multi-threading for reproducibility (unseeded runs use all cores).
|
|
558
|
+
|
|
397
559
|
## Supported formats
|
|
398
560
|
|
|
399
561
|
| Format | Extensions |
|
|
@@ -406,7 +568,8 @@ materializes the source at all.
|
|
|
406
568
|
|
|
407
569
|
Output keeps the source format and is named
|
|
408
570
|
`{stem}_sample_{count}{ext}` — with an `_anon` suffix when anonymization ran
|
|
409
|
-
|
|
571
|
+
and a `_pca{k}` suffix when PCA column reduction ran
|
|
572
|
+
(e.g. `data_sample_500_anon_pca3.csv`).
|
|
410
573
|
|
|
411
574
|
## Development
|
|
412
575
|
|
|
@@ -420,7 +583,8 @@ Logging is controlled by `DATA_SAMPLER_LOG` (`quiet`/`info`/`verbose`) and
|
|
|
420
583
|
`DATA_SAMPLER_LOG_FILE`. See `ROADMAP.md` for planned work and
|
|
421
584
|
`TROUBLESHOOTING.md` for known failure modes.
|
|
422
585
|
|
|
423
|
-
|
|
586
|
+
Releases go to [PyPI](https://pypi.org/project/data-sampler/) via a
|
|
587
|
+
human-triggered, test-gated GitHub Actions workflow — see `RELEASING.md`.
|
|
424
588
|
|
|
425
589
|
---
|
|
426
590
|
|
|
@@ -1,22 +1,50 @@
|
|
|
1
1
|
# data-sampler
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
anonymization and a colorful terminal UI.
|
|
3
|
+
[](https://pypi.org/project/data-sampler/)
|
|
4
|
+
[](https://pypi.org/project/data-sampler/)
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
**Hand someone data that looks and behaves like your production data, isn't
|
|
7
|
+
your production data, and provably kept its statistical variety — in one
|
|
8
|
+
command.**
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
data-sampler customers.xlsx 500 --suggest
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Use it to:
|
|
15
|
+
|
|
16
|
+
- **Share a realistic slice with a vendor or contractor** — names, ids,
|
|
17
|
+
emails, salaries, and dates anonymized, but every distribution, duplicate,
|
|
18
|
+
and group structure intact.
|
|
19
|
+
- **Attach a repro to a bug report** without leaking the real records that
|
|
20
|
+
trigger it.
|
|
21
|
+
- **Cut a 2 GB export down to 500 rows** for a demo or prototype that still
|
|
22
|
+
behaves like the real thing.
|
|
23
|
+
- **Build test fixtures that mirror production skew** instead of uniform toy
|
|
24
|
+
data.
|
|
25
|
+
- **Give a class or workshop realistic data** without a data-sharing
|
|
26
|
+
agreement.
|
|
27
|
+
- **Pull 10,000 representative rows from a 100M-row Parquet file** without
|
|
28
|
+
loading it — the optional DuckDB engine samples out-of-core, in parallel.
|
|
29
|
+
- **Prove the sample is representative**: every run produces a side-by-side
|
|
30
|
+
source-vs-sample distribution report, per column.
|
|
31
|
+
|
|
32
|
+
How: stratified sampling preserves the statistical variety of your data
|
|
33
|
+
(strata are detected automatically), and every anonymizer maps each unique
|
|
34
|
+
original value to exactly one replacement — so repeated values stay repeated
|
|
35
|
+
and the joint distributions survive anonymization. Everything ships as a
|
|
36
|
+
single Python package: a colorful terminal UI for non-programmers, a headless
|
|
37
|
+
CLI, and a plain Python API.
|
|
10
38
|
|
|
11
39
|
## Install
|
|
12
40
|
|
|
13
41
|
```sh
|
|
14
|
-
pip install data-sampler
|
|
15
|
-
#
|
|
16
|
-
pip install -e ".[dev]"
|
|
42
|
+
pip install data-sampler # from PyPI
|
|
43
|
+
pip install "data-sampler[large]" # + the out-of-core DuckDB engine for huge files
|
|
17
44
|
```
|
|
18
45
|
|
|
19
|
-
Requires Python 3.10+.
|
|
46
|
+
Requires Python 3.10+. For development, clone the repo and
|
|
47
|
+
`pip install -e ".[dev]"`.
|
|
20
48
|
|
|
21
49
|
## Terminal UI
|
|
22
50
|
|
|
@@ -44,7 +72,8 @@ The TUI is a panel-based dashboard (think btop / lazydocker):
|
|
|
44
72
|
VS Code extension). Select a column to see full stats and distribution
|
|
45
73
|
bars, choose an anonymizer for it, and toggle whether it should be
|
|
46
74
|
skipped when preserving statistical variety (stratification). Set the
|
|
47
|
-
sample size, output folder, optional seed,
|
|
75
|
+
sample size, output folder, optional seed, an optional PCA column
|
|
76
|
+
reduction (`reduce`: N components or a variance target), and run.
|
|
48
77
|
3. **Report screen** — the stratification comparison and anonymization summary
|
|
49
78
|
on the left, and a **column histograms** panel on the right showing every
|
|
50
79
|
column's source-vs-sample distribution (numeric columns share bin edges;
|
|
@@ -71,6 +100,11 @@ data-sampler <source> <count> [options]
|
|
|
71
100
|
| `--anon COL=KIND[:k=v,...]` | Anonymize a column (repeatable) |
|
|
72
101
|
| `-i`, `--interactive` | Guided workflow: choose an anonymizer type per column from a menu |
|
|
73
102
|
| `--suggest` | Auto-assign a suggested anonymizer type to each column from its stats |
|
|
103
|
+
| `--reduce-components N` | PCA column reduction: replace the numeric columns with the first `N` principal components |
|
|
104
|
+
| `--reduce-variance R` | PCA column reduction: keep the fewest components whose cumulative explained variance reaches `R` (0 < R < 1) |
|
|
105
|
+
| `--reduce-exclude COL[,COL]` | Numeric column(s) to keep out of the reduction, e.g. identifiers (repeatable) |
|
|
106
|
+
| `--reduce-prefix PREFIX` | Name prefix for the component columns (default `PC` → `PC1`, `PC2`, …) |
|
|
107
|
+
| `--reduce-no-standardize` | Skip the per-column z-scoring before PCA |
|
|
74
108
|
| `--engine {auto,pandas,duckdb}` | Sampling engine (default `auto`: DuckDB for Parquet/large inputs, pandas otherwise) |
|
|
75
109
|
| `--threads N` | DuckDB engine: number of threads (default: all cores) |
|
|
76
110
|
| `--memory-limit SIZE` | DuckDB engine: memory limit before spilling to disk (e.g. `8GB`) |
|
|
@@ -89,6 +123,11 @@ data-sampler data.csv 100 --skip region,notes --seed 7 \
|
|
|
89
123
|
|
|
90
124
|
# large / out-of-core: sample a Parquet file in parallel with DuckDB
|
|
91
125
|
data-sampler huge.parquet 10000 --engine duckdb --threads 8 --memory-limit 8GB --suggest
|
|
126
|
+
|
|
127
|
+
# narrow the sample too: collapse the numeric columns into 3 principal
|
|
128
|
+
# components (or keep however many retain 90% of the variance)
|
|
129
|
+
data-sampler wide.csv 500 --reduce-components 3 --reduce-exclude cust_id
|
|
130
|
+
data-sampler wide.csv 500 --reduce-variance 0.9
|
|
92
131
|
```
|
|
93
132
|
|
|
94
133
|
## Python API
|
|
@@ -121,7 +160,11 @@ anon = ds.anonymize(
|
|
|
121
160
|
seed=7,
|
|
122
161
|
)
|
|
123
162
|
|
|
124
|
-
|
|
163
|
+
# optionally collapse the numeric columns into principal components
|
|
164
|
+
red = ds.reduce_columns(anon, variance_ratio=0.9, exclude=["cust_id"])
|
|
165
|
+
print(ds.format_reduction_report(red)) # variance kept + correlated groups
|
|
166
|
+
|
|
167
|
+
ds.save_output(red.data, "data.xlsx", tag="sample_500_anon_pca")
|
|
125
168
|
```
|
|
126
169
|
|
|
127
170
|
## Try it: bundled example
|
|
@@ -289,6 +332,34 @@ category. A side-by-side distribution report is produced for every run.
|
|
|
289
332
|
If no suitable stratification columns exist, the tool falls back to pure
|
|
290
333
|
random sampling automatically.
|
|
291
334
|
|
|
335
|
+
### Reducing columns (PCA)
|
|
336
|
+
|
|
337
|
+
Sampling narrows the *rows*; the optional PCA step narrows the *columns* of
|
|
338
|
+
the outgoing sample. It replaces the numeric block with its first *k*
|
|
339
|
+
principal components (`PC1..PCk`), controlled one of two ways:
|
|
340
|
+
|
|
341
|
+
- **`--reduce-components N`** — `N` components in the output (capped at the
|
|
342
|
+
number of usable numeric columns, with a note when fewer are possible);
|
|
343
|
+
- **`--reduce-variance R`** — the fewest components whose cumulative
|
|
344
|
+
explained-variance ratio reaches `R` (e.g. `0.9` keeps ≥ 90 % of the
|
|
345
|
+
numeric variance).
|
|
346
|
+
|
|
347
|
+
Non-numeric columns (ids, categories, text, booleans, datetimes) are always
|
|
348
|
+
preserved, and `--reduce-exclude` keeps chosen numeric columns out too —
|
|
349
|
+
identifiers should be excluded, since an all-unique id forms its own
|
|
350
|
+
artificial component (the tool warns when it spots one). Missing values are
|
|
351
|
+
mean-imputed so the row count never changes; constant columns carry no signal
|
|
352
|
+
and pass through unchanged. Columns are z-scored first by default (PCA on the
|
|
353
|
+
correlation matrix), so a large-unit column such as a salary cannot dominate
|
|
354
|
+
the components; `--reduce-no-standardize` turns that off.
|
|
355
|
+
|
|
356
|
+
Every reduction prints its rationale: the variance each component retains,
|
|
357
|
+
**the groups of correlated columns** that move together (which is exactly the
|
|
358
|
+
redundancy PCA collapses — on standardized data PCA diagonalizes the
|
|
359
|
+
correlation matrix), and each component's top driving columns. The reduction
|
|
360
|
+
runs after anonymization, on the already-sampled rows, so the stratification
|
|
361
|
+
and histogram reports still describe the original columns.
|
|
362
|
+
|
|
292
363
|
## Large data: the out-of-core DuckDB engine
|
|
293
364
|
|
|
294
365
|
The default pandas path loads the whole file into memory. For inputs that are
|
|
@@ -339,6 +410,97 @@ The pandas path on the same file: 5.6 s total while materializing a ~0.9 GB
|
|
|
339
410
|
frame in RAM — the engine's reservoir sampling is ~50× faster and never
|
|
340
411
|
materializes the source at all.
|
|
341
412
|
|
|
413
|
+
## How it scales: the algorithms
|
|
414
|
+
|
|
415
|
+
Every trick used to handle millions-to-billions of rows and thousands of
|
|
416
|
+
columns, in one place.
|
|
417
|
+
|
|
418
|
+
### Millions (to billions) of rows
|
|
419
|
+
|
|
420
|
+
- **Out-of-core execution.** With the DuckDB engine, loading, stratification,
|
|
421
|
+
and sampling run inside a vectorized, multi-threaded SQL engine with a
|
|
422
|
+
memory limit and a temp directory — it spills to disk instead of OOM-ing,
|
|
423
|
+
and only the resulting sample ever becomes a DataFrame.
|
|
424
|
+
- **Reservoir sampling** for the random case: a single streaming pass with
|
|
425
|
+
O(sample size) memory, an exact row count, and `REPEATABLE(seed)`
|
|
426
|
+
reproducibility that is independent of the file format and thread count.
|
|
427
|
+
- **Two-pass stratified sampling.** Pass 1 is a `GROUP BY` over the
|
|
428
|
+
stratification columns — one row per stratum, tiny regardless of source
|
|
429
|
+
size. Largest-remainder proportional allocation is computed on that tiny
|
|
430
|
+
table in numpy. Pass 2 ranks rows per stratum with
|
|
431
|
+
`row_number() OVER (PARTITION BY strata ORDER BY random())` and keeps the
|
|
432
|
+
first *allocation* rows of each, joining the allocation table with
|
|
433
|
+
`IS NOT DISTINCT FROM` so missing-value strata are sampled too. DuckDB
|
|
434
|
+
parallelizes the partitions and spills the window sort if needed.
|
|
435
|
+
- **Parquet projection pushdown.** Pass 1 and the stats queries touch only
|
|
436
|
+
the columns they reference, so a wide Parquet file is never read in full.
|
|
437
|
+
- **Two-phase narrow sampling.** The expensive phase (the per-stratum window
|
|
438
|
+
sort, or the reservoir buffer) runs over *only the stratification columns
|
|
439
|
+
plus a stable row id* — `file_row_number` for single-file Parquet, a
|
|
440
|
+
positional id for DataFrames — and a second pass fetches just the winning
|
|
441
|
+
rows with every column. The sort never carries the wide payload: measured
|
|
442
|
+
2.5× on a 400-column Parquet file and 9.5× for wide DataFrames (whose
|
|
443
|
+
payload never enters the SQL engine at all — winners come back as
|
|
444
|
+
dtype-preserving pandas slices). CSV/JSON and multi-file Parquet globs
|
|
445
|
+
keep the single-pass shape, which is the correct one there (text must be
|
|
446
|
+
re-parsed per scan, and per-file row numbers aren't a global id).
|
|
447
|
+
- **Determinism engineering.** DuckDB's `GROUP BY` output order is
|
|
448
|
+
nondeterministic, so the stratum order is pinned with `ORDER BY … NULLS
|
|
449
|
+
LAST` before allocation (otherwise remainder ties break differently run to
|
|
450
|
+
run); seeded stratified runs drop to a single thread because `random()`
|
|
451
|
+
ordering is only reproducible that way, then restore the thread count.
|
|
452
|
+
- **Row-count caching.** `count(*)` runs once per source per engine session
|
|
453
|
+
instead of once per operation.
|
|
454
|
+
- **Vectorized anonymizers.** The column is dictionary-encoded once with
|
|
455
|
+
`pd.factorize`, each *unique* value gets one replacement, and the result is
|
|
456
|
+
assembled by a fancy-index gather — the in-process equivalent of a native
|
|
457
|
+
join against a mapping table. Cost scales with unique values plus one
|
|
458
|
+
vectorized pass, never a Python loop over rows: sequential IDs are an
|
|
459
|
+
`np.arange`, numeric/datetime jitter are single vectorized RNG draws.
|
|
460
|
+
|
|
461
|
+
### Thousands of columns
|
|
462
|
+
|
|
463
|
+
- **One scan for all scalar stats.** `DuckDBEngine.stats()` computes count,
|
|
464
|
+
distinct, min/max/mean/std, and median for *every column in a single
|
|
465
|
+
aggregate query* — one streaming pass over the data regardless of how many
|
|
466
|
+
columns there are.
|
|
467
|
+
- **Sketches instead of sorts.** Distinct counts use HyperLogLog
|
|
468
|
+
(`approx_count_distinct`) and medians use `approx_quantile` — streaming
|
|
469
|
+
approximations with fixed memory per column, no per-column sort or full
|
|
470
|
+
hash table. Exact mode (`approximate=False`) exists for small data, and
|
|
471
|
+
approximate results are flagged on the stats object.
|
|
472
|
+
- **`distributions=False`** skips the per-column histogram/top-k passes
|
|
473
|
+
entirely, so very wide tables get exactly one scan (this is what the CLI's
|
|
474
|
+
`--suggest` uses to pick anonymizer types).
|
|
475
|
+
- **Bounded stratification search.** Candidate columns are screened in one
|
|
476
|
+
aggregate pass (HLL cardinality + average text length), then a greedy
|
|
477
|
+
fewest-categories-first selection keeps the *joint* stratum count at or
|
|
478
|
+
below the sample size — so the group count stays bounded no matter how
|
|
479
|
+
many columns the file has.
|
|
480
|
+
- **Pandas-path trims.** Numeric columns skip the top-values stringification
|
|
481
|
+
(~1.9× faster stats), report histograms skip near-unique columns instead
|
|
482
|
+
of hashing millions of ids for a meaningless top-8, and the TUI computes
|
|
483
|
+
stats in a worker thread so the UI never freezes on load.
|
|
484
|
+
- **PCA column reduction on the way out.** `--reduce-components` /
|
|
485
|
+
`--reduce-variance` collapse a wide numeric block into a handful of
|
|
486
|
+
principal components *after* sampling, so the SVD runs on the small sampled
|
|
487
|
+
frame (never the full source) and the delivered file is narrow as well as
|
|
488
|
+
short.
|
|
489
|
+
|
|
490
|
+
### Correctness under scale
|
|
491
|
+
|
|
492
|
+
Details that only bite on real data, all regression-tested: DuckDB treats NaN
|
|
493
|
+
as a value rather than NULL, so every NaN-sensitive aggregate is filtered
|
|
494
|
+
(`FILTER (WHERE NOT isnan(…))`); missing-value strata survive the allocation
|
|
495
|
+
join via `IS NOT DISTINCT FROM`; HyperLogLog estimates are clamped to each
|
|
496
|
+
column's non-null count; and columns-oriented JSON (the pandas `to_json()`
|
|
497
|
+
default, which SQL engines parse as one giant row) is detected and refused
|
|
498
|
+
with guidance.
|
|
499
|
+
|
|
500
|
+
Known trade-offs: CSV sources are re-parsed per query (the streaming design —
|
|
501
|
+
convert to Parquet for repeated work), and a seeded stratified run gives up
|
|
502
|
+
multi-threading for reproducibility (unseeded runs use all cores).
|
|
503
|
+
|
|
342
504
|
## Supported formats
|
|
343
505
|
|
|
344
506
|
| Format | Extensions |
|
|
@@ -351,7 +513,8 @@ materializes the source at all.
|
|
|
351
513
|
|
|
352
514
|
Output keeps the source format and is named
|
|
353
515
|
`{stem}_sample_{count}{ext}` — with an `_anon` suffix when anonymization ran
|
|
354
|
-
|
|
516
|
+
and a `_pca{k}` suffix when PCA column reduction ran
|
|
517
|
+
(e.g. `data_sample_500_anon_pca3.csv`).
|
|
355
518
|
|
|
356
519
|
## Development
|
|
357
520
|
|
|
@@ -365,7 +528,8 @@ Logging is controlled by `DATA_SAMPLER_LOG` (`quiet`/`info`/`verbose`) and
|
|
|
365
528
|
`DATA_SAMPLER_LOG_FILE`. See `ROADMAP.md` for planned work and
|
|
366
529
|
`TROUBLESHOOTING.md` for known failure modes.
|
|
367
530
|
|
|
368
|
-
|
|
531
|
+
Releases go to [PyPI](https://pypi.org/project/data-sampler/) via a
|
|
532
|
+
human-triggered, test-gated GitHub Actions workflow — see `RELEASING.md`.
|
|
369
533
|
|
|
370
534
|
---
|
|
371
535
|
|
|
@@ -5,23 +5,19 @@ human publishes a GitHub Release (or manually dispatches the workflow). The
|
|
|
5
5
|
workflow uses **PyPI Trusted Publishing** (OIDC), so no API token is stored in
|
|
6
6
|
the repo, in CI secrets, or anywhere else.
|
|
7
7
|
|
|
8
|
-
## One-time setup (PyPI side)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
click-to-approve on every release.
|
|
22
|
-
|
|
23
|
-
After the first successful publish, the pending publisher becomes the project's
|
|
24
|
-
trusted publisher automatically.
|
|
8
|
+
## One-time setup (PyPI side) — DONE (v3.2.1, 2026-07-23)
|
|
9
|
+
|
|
10
|
+
Completed: the PyPI account exists, and the pending publisher (project
|
|
11
|
+
`data-sampler`, owner `aaronified`, repo `data-sampler`, workflow
|
|
12
|
+
`release.yml`, environment `pypi`) became the project's permanent **trusted
|
|
13
|
+
publisher** with the first publish. Nothing to repeat per release.
|
|
14
|
+
|
|
15
|
+
If it ever needs re-registering (new repo/owner/workflow name), manage it at
|
|
16
|
+
<https://pypi.org/manage/project/data-sampler/settings/publishing/>.
|
|
17
|
+
|
|
18
|
+
Still recommended: in GitHub → Settings → Environments → `pypi`, add yourself
|
|
19
|
+
as a required reviewer so every publish waits for your click-to-approve
|
|
20
|
+
(currently publishes run unattended once tests pass).
|
|
25
21
|
|
|
26
22
|
## Per release
|
|
27
23
|
|
|
@@ -89,9 +89,19 @@ its own commit.
|
|
|
89
89
|
the per-column passes for a single cheap scalar pass over very wide inputs.
|
|
90
90
|
`approx_count_distinct` also powers the engine's stratification-column selection.
|
|
91
91
|
|
|
92
|
+
## v3.3
|
|
93
|
+
|
|
94
|
+
- [x] **Block P7 — two-phase narrow sampling.** Rank/reservoir over only the
|
|
95
|
+
stratification columns + a stable row id (single-file Parquet
|
|
96
|
+
`file_row_number`; DataFrame positional id), then fetch the winning full
|
|
97
|
+
rows. 2.5× on wide Parquet, 9.5× on wide DataFrames. CSV/JSON and Parquet
|
|
98
|
+
globs keep the single-pass shape (per-file row ids are not global).
|
|
99
|
+
|
|
92
100
|
## Later
|
|
93
101
|
|
|
94
102
|
- Optional PyInstaller EXE build of the TUI (replaces the old Tkinter EXE).
|
|
103
|
+
- Composite filename+row-number id to extend narrow sampling to Parquet globs.
|
|
104
|
+
- `--columns` output projection (sample only a subset of columns).
|
|
95
105
|
- Rust/Polars-on-Arrow native engine (pyo3) as an alternative to DuckDB.
|
|
96
106
|
- GPU acceleration (RAPIDS cuDF) for the aggregation-heavy paths.
|
|
97
107
|
- Distributed backend (Dask / Ray Data / Spark) for multi-machine scale.
|