data-sampler 3.3.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.3.1 → data_sampler-3.4.0}/CHANGELOG.md +36 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/PKG-INFO +53 -4
- {data_sampler-3.3.1 → data_sampler-3.4.0}/README.md +52 -3
- {data_sampler-3.3.1 → data_sampler-3.4.0}/pyproject.toml +1 -1
- {data_sampler-3.3.1 → data_sampler-3.4.0}/src/data_sampler/__init__.py +9 -2
- {data_sampler-3.3.1 → data_sampler-3.4.0}/src/data_sampler/cli.py +135 -3
- data_sampler-3.4.0/src/data_sampler/reduce.py +343 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/src/data_sampler/report.py +76 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/src/data_sampler/tui/app.py +65 -4
- {data_sampler-3.3.1 → data_sampler-3.4.0}/tests/test_cli.py +136 -0
- data_sampler-3.4.0/tests/test_reduce.py +291 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/tests/test_tui.py +38 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/.gitattributes +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/.github/workflows/release.yml +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/.gitignore +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/LICENSE +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/RELEASING.md +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/ROADMAP.md +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/TROUBLESHOOTING.md +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/examples/employees.csv +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/examples/using_data_sampler.ipynb +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/requirements.txt +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/scripts/run-tui.bat +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/scripts/run-tui.sh +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/src/data_sampler/__main__.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/src/data_sampler/_logging.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/src/data_sampler/_names.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/src/data_sampler/anonymize.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/src/data_sampler/engine.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/src/data_sampler/io.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/src/data_sampler/sampling.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/src/data_sampler/stats.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/src/data_sampler/tui/__init__.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/src/data_sampler/workflow.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/tests/conftest.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/tests/test_anonymize.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/tests/test_engine.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/tests/test_io.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/tests/test_report.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/tests/test_sampling.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/tests/test_stats.py +0 -0
- {data_sampler-3.3.1 → data_sampler-3.4.0}/tests/test_workflow.py +0 -0
|
@@ -1,5 +1,41 @@
|
|
|
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
|
+
|
|
3
39
|
## v3.3.1 — 2026-07-23
|
|
4
40
|
|
|
5
41
|
- **Fixed the second CI-only TUI race** (caught, again, by the release CI —
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: data-sampler
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
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
|
|
@@ -127,7 +127,8 @@ The TUI is a panel-based dashboard (think btop / lazydocker):
|
|
|
127
127
|
VS Code extension). Select a column to see full stats and distribution
|
|
128
128
|
bars, choose an anonymizer for it, and toggle whether it should be
|
|
129
129
|
skipped when preserving statistical variety (stratification). Set the
|
|
130
|
-
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.
|
|
131
132
|
3. **Report screen** — the stratification comparison and anonymization summary
|
|
132
133
|
on the left, and a **column histograms** panel on the right showing every
|
|
133
134
|
column's source-vs-sample distribution (numeric columns share bin edges;
|
|
@@ -154,6 +155,11 @@ data-sampler <source> <count> [options]
|
|
|
154
155
|
| `--anon COL=KIND[:k=v,...]` | Anonymize a column (repeatable) |
|
|
155
156
|
| `-i`, `--interactive` | Guided workflow: choose an anonymizer type per column from a menu |
|
|
156
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 |
|
|
157
163
|
| `--engine {auto,pandas,duckdb}` | Sampling engine (default `auto`: DuckDB for Parquet/large inputs, pandas otherwise) |
|
|
158
164
|
| `--threads N` | DuckDB engine: number of threads (default: all cores) |
|
|
159
165
|
| `--memory-limit SIZE` | DuckDB engine: memory limit before spilling to disk (e.g. `8GB`) |
|
|
@@ -172,6 +178,11 @@ data-sampler data.csv 100 --skip region,notes --seed 7 \
|
|
|
172
178
|
|
|
173
179
|
# large / out-of-core: sample a Parquet file in parallel with DuckDB
|
|
174
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
|
|
175
186
|
```
|
|
176
187
|
|
|
177
188
|
## Python API
|
|
@@ -204,7 +215,11 @@ anon = ds.anonymize(
|
|
|
204
215
|
seed=7,
|
|
205
216
|
)
|
|
206
217
|
|
|
207
|
-
|
|
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")
|
|
208
223
|
```
|
|
209
224
|
|
|
210
225
|
## Try it: bundled example
|
|
@@ -372,6 +387,34 @@ category. A side-by-side distribution report is produced for every run.
|
|
|
372
387
|
If no suitable stratification columns exist, the tool falls back to pure
|
|
373
388
|
random sampling automatically.
|
|
374
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
|
+
|
|
375
418
|
## Large data: the out-of-core DuckDB engine
|
|
376
419
|
|
|
377
420
|
The default pandas path loads the whole file into memory. For inputs that are
|
|
@@ -493,6 +536,11 @@ columns, in one place.
|
|
|
493
536
|
(~1.9× faster stats), report histograms skip near-unique columns instead
|
|
494
537
|
of hashing millions of ids for a meaningless top-8, and the TUI computes
|
|
495
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.
|
|
496
544
|
|
|
497
545
|
### Correctness under scale
|
|
498
546
|
|
|
@@ -520,7 +568,8 @@ multi-threading for reproducibility (unseeded runs use all cores).
|
|
|
520
568
|
|
|
521
569
|
Output keeps the source format and is named
|
|
522
570
|
`{stem}_sample_{count}{ext}` — with an `_anon` suffix when anonymization ran
|
|
523
|
-
|
|
571
|
+
and a `_pca{k}` suffix when PCA column reduction ran
|
|
572
|
+
(e.g. `data_sample_500_anon_pca3.csv`).
|
|
524
573
|
|
|
525
574
|
## Development
|
|
526
575
|
|
|
@@ -72,7 +72,8 @@ The TUI is a panel-based dashboard (think btop / lazydocker):
|
|
|
72
72
|
VS Code extension). Select a column to see full stats and distribution
|
|
73
73
|
bars, choose an anonymizer for it, and toggle whether it should be
|
|
74
74
|
skipped when preserving statistical variety (stratification). Set the
|
|
75
|
-
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.
|
|
76
77
|
3. **Report screen** — the stratification comparison and anonymization summary
|
|
77
78
|
on the left, and a **column histograms** panel on the right showing every
|
|
78
79
|
column's source-vs-sample distribution (numeric columns share bin edges;
|
|
@@ -99,6 +100,11 @@ data-sampler <source> <count> [options]
|
|
|
99
100
|
| `--anon COL=KIND[:k=v,...]` | Anonymize a column (repeatable) |
|
|
100
101
|
| `-i`, `--interactive` | Guided workflow: choose an anonymizer type per column from a menu |
|
|
101
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 |
|
|
102
108
|
| `--engine {auto,pandas,duckdb}` | Sampling engine (default `auto`: DuckDB for Parquet/large inputs, pandas otherwise) |
|
|
103
109
|
| `--threads N` | DuckDB engine: number of threads (default: all cores) |
|
|
104
110
|
| `--memory-limit SIZE` | DuckDB engine: memory limit before spilling to disk (e.g. `8GB`) |
|
|
@@ -117,6 +123,11 @@ data-sampler data.csv 100 --skip region,notes --seed 7 \
|
|
|
117
123
|
|
|
118
124
|
# large / out-of-core: sample a Parquet file in parallel with DuckDB
|
|
119
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
|
|
120
131
|
```
|
|
121
132
|
|
|
122
133
|
## Python API
|
|
@@ -149,7 +160,11 @@ anon = ds.anonymize(
|
|
|
149
160
|
seed=7,
|
|
150
161
|
)
|
|
151
162
|
|
|
152
|
-
|
|
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")
|
|
153
168
|
```
|
|
154
169
|
|
|
155
170
|
## Try it: bundled example
|
|
@@ -317,6 +332,34 @@ category. A side-by-side distribution report is produced for every run.
|
|
|
317
332
|
If no suitable stratification columns exist, the tool falls back to pure
|
|
318
333
|
random sampling automatically.
|
|
319
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
|
+
|
|
320
363
|
## Large data: the out-of-core DuckDB engine
|
|
321
364
|
|
|
322
365
|
The default pandas path loads the whole file into memory. For inputs that are
|
|
@@ -438,6 +481,11 @@ columns, in one place.
|
|
|
438
481
|
(~1.9× faster stats), report histograms skip near-unique columns instead
|
|
439
482
|
of hashing millions of ids for a meaningless top-8, and the TUI computes
|
|
440
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.
|
|
441
489
|
|
|
442
490
|
### Correctness under scale
|
|
443
491
|
|
|
@@ -465,7 +513,8 @@ multi-threading for reproducibility (unseeded runs use all cores).
|
|
|
465
513
|
|
|
466
514
|
Output keeps the source format and is named
|
|
467
515
|
`{stem}_sample_{count}{ext}` — with an `_anon` suffix when anonymization ran
|
|
468
|
-
|
|
516
|
+
and a `_pca{k}` suffix when PCA column reduction ran
|
|
517
|
+
(e.g. `data_sample_500_anon_pca3.csv`).
|
|
469
518
|
|
|
470
519
|
## Development
|
|
471
520
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "data-sampler"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.4.0"
|
|
8
8
|
description = "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."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -9,20 +9,24 @@ Public API::
|
|
|
9
9
|
result = ds.sample(df, 500, exclude_columns=["notes"])
|
|
10
10
|
print(ds.format_stratification_report(df, result))
|
|
11
11
|
anon = ds.anonymize(result.data, {"name": "names", "id": ("sequential_id", {"start": 1000})})
|
|
12
|
-
ds.
|
|
12
|
+
red = ds.reduce_columns(anon, variance_ratio=0.9, exclude=["id"]) # optional PCA
|
|
13
|
+
print(ds.format_reduction_report(red))
|
|
14
|
+
ds.save_output(red.data, "data.csv", tag="sample_500_anon_pca")
|
|
13
15
|
|
|
14
16
|
ds.run_tui() # launch the terminal UI
|
|
15
17
|
"""
|
|
16
18
|
|
|
17
19
|
from __future__ import annotations
|
|
18
20
|
|
|
19
|
-
__version__ = "3.
|
|
21
|
+
__version__ = "3.4.0"
|
|
20
22
|
|
|
21
23
|
from .io import SUPPORTED_EXTENSIONS, list_sheets, load_file, save_output
|
|
24
|
+
from .reduce import ReductionResult, reduce_columns
|
|
22
25
|
from .report import (
|
|
23
26
|
column_histogram_data,
|
|
24
27
|
format_column_histograms,
|
|
25
28
|
format_distribution,
|
|
29
|
+
format_reduction_report,
|
|
26
30
|
format_stratification_report,
|
|
27
31
|
)
|
|
28
32
|
from .sampling import (
|
|
@@ -47,7 +51,10 @@ __all__ = [
|
|
|
47
51
|
"sample",
|
|
48
52
|
"stratified_sample",
|
|
49
53
|
"find_stratification_columns",
|
|
54
|
+
"ReductionResult",
|
|
55
|
+
"reduce_columns",
|
|
50
56
|
"format_stratification_report",
|
|
57
|
+
"format_reduction_report",
|
|
51
58
|
"format_distribution",
|
|
52
59
|
"column_histogram_data",
|
|
53
60
|
"format_column_histograms",
|
|
@@ -13,6 +13,12 @@ which picks DuckDB for Parquet/large files) samples out-of-core in parallel:
|
|
|
13
13
|
|
|
14
14
|
data-sampler huge.parquet 10000 --engine duckdb --threads 8 \\
|
|
15
15
|
--memory-limit 8GB --suggest
|
|
16
|
+
|
|
17
|
+
Optionally reduce the numeric columns of the outgoing sample to K principal
|
|
18
|
+
components — or to however many retain a target share of the variance:
|
|
19
|
+
|
|
20
|
+
data-sampler data.csv 500 --reduce-components 3 --reduce-exclude id
|
|
21
|
+
data-sampler data.csv 500 --reduce-variance 0.9
|
|
16
22
|
"""
|
|
17
23
|
|
|
18
24
|
from __future__ import annotations
|
|
@@ -24,7 +30,12 @@ from . import __version__
|
|
|
24
30
|
from ._logging import get_logger
|
|
25
31
|
from .anonymize import anonymize, make_anonymizer
|
|
26
32
|
from .io import load_file, save_output
|
|
27
|
-
from .
|
|
33
|
+
from .reduce import reduce_columns
|
|
34
|
+
from .report import (
|
|
35
|
+
format_column_histograms,
|
|
36
|
+
format_reduction_report,
|
|
37
|
+
format_stratification_report,
|
|
38
|
+
)
|
|
28
39
|
from .sampling import sample
|
|
29
40
|
|
|
30
41
|
log = get_logger(__name__)
|
|
@@ -41,6 +52,24 @@ def _coerce_value(value: str):
|
|
|
41
52
|
return value
|
|
42
53
|
|
|
43
54
|
|
|
55
|
+
def _positive_int(text: str) -> int:
|
|
56
|
+
"""argparse type: an integer >= 1 (rejects bad values at parse time)."""
|
|
57
|
+
value = int(text)
|
|
58
|
+
if value < 1:
|
|
59
|
+
raise argparse.ArgumentTypeError(f"must be a positive integer, got {text}")
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _unit_interval(text: str) -> float:
|
|
64
|
+
"""argparse type: a float strictly between 0 and 1."""
|
|
65
|
+
value = float(text)
|
|
66
|
+
if not 0.0 < value < 1.0:
|
|
67
|
+
raise argparse.ArgumentTypeError(
|
|
68
|
+
f"must be strictly between 0 and 1, got {text}"
|
|
69
|
+
)
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
44
73
|
def parse_anon_option(text: str) -> tuple[str, str, dict]:
|
|
45
74
|
"""Parse ``COL=KIND[:key=value,key=value]`` into (column, kind, options)."""
|
|
46
75
|
col, sep, rhs = text.partition("=")
|
|
@@ -110,6 +139,46 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
110
139
|
"stats (columns set via --anon are left as given)"
|
|
111
140
|
),
|
|
112
141
|
)
|
|
142
|
+
reduce_group = parser.add_mutually_exclusive_group()
|
|
143
|
+
reduce_group.add_argument(
|
|
144
|
+
"--reduce-components", type=_positive_int, metavar="N",
|
|
145
|
+
dest="reduce_components",
|
|
146
|
+
help=(
|
|
147
|
+
"PCA column reduction: replace the numeric columns with the first "
|
|
148
|
+
"N principal components (non-numeric columns are preserved; N is "
|
|
149
|
+
"capped at the number of usable numeric columns)"
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
reduce_group.add_argument(
|
|
153
|
+
"--reduce-variance", type=_unit_interval, metavar="R",
|
|
154
|
+
dest="reduce_variance",
|
|
155
|
+
help=(
|
|
156
|
+
"PCA column reduction: keep the fewest principal components whose "
|
|
157
|
+
"cumulative explained-variance ratio reaches R (0 < R < 1)"
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
parser.add_argument(
|
|
161
|
+
"--reduce-exclude", action="append", default=[], metavar="COL[,COL...]",
|
|
162
|
+
dest="reduce_exclude",
|
|
163
|
+
help=(
|
|
164
|
+
"numeric column(s) to exclude from PCA reduction and pass through "
|
|
165
|
+
"unchanged, e.g. identifiers (repeatable)"
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
"--reduce-prefix", default="PC", metavar="PREFIX", dest="reduce_prefix",
|
|
170
|
+
help=(
|
|
171
|
+
"name prefix for the principal-component columns (default PC → "
|
|
172
|
+
"PC1, PC2, …); change it if the source already has such a column"
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
parser.add_argument(
|
|
176
|
+
"--reduce-no-standardize", action="store_true", dest="reduce_no_standardize",
|
|
177
|
+
help=(
|
|
178
|
+
"PCA column reduction: skip the per-column z-scoring (by default "
|
|
179
|
+
"columns are standardized so large-unit columns cannot dominate)"
|
|
180
|
+
),
|
|
181
|
+
)
|
|
113
182
|
parser.add_argument(
|
|
114
183
|
"--engine", choices=("auto", "pandas", "duckdb"), default="auto",
|
|
115
184
|
help=(
|
|
@@ -136,6 +205,65 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
136
205
|
return parser
|
|
137
206
|
|
|
138
207
|
|
|
208
|
+
def _reduce_requested(args) -> bool:
|
|
209
|
+
return args.reduce_components is not None or args.reduce_variance is not None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _validate_reduce_args(args, parser, columns) -> list[str]:
|
|
213
|
+
"""Validate the reduce flags as soon as the source's columns are known —
|
|
214
|
+
BEFORE sampling, so a typo never costs a full (possibly out-of-core) run.
|
|
215
|
+
|
|
216
|
+
Returns the parsed ``--reduce-exclude`` column list.
|
|
217
|
+
"""
|
|
218
|
+
exclude = [
|
|
219
|
+
c.strip() for chunk in args.reduce_exclude for c in chunk.split(",") if c.strip()
|
|
220
|
+
]
|
|
221
|
+
if not _reduce_requested(args):
|
|
222
|
+
if exclude:
|
|
223
|
+
parser.error(
|
|
224
|
+
"--reduce-exclude requires --reduce-components or --reduce-variance"
|
|
225
|
+
)
|
|
226
|
+
if args.reduce_no_standardize:
|
|
227
|
+
parser.error(
|
|
228
|
+
"--reduce-no-standardize requires --reduce-components or "
|
|
229
|
+
"--reduce-variance"
|
|
230
|
+
)
|
|
231
|
+
return exclude
|
|
232
|
+
# compare stringified: Excel sources can carry non-string column labels
|
|
233
|
+
names = {str(c) for c in columns}
|
|
234
|
+
unknown = [c for c in exclude if c not in names]
|
|
235
|
+
if unknown:
|
|
236
|
+
parser.error(
|
|
237
|
+
f"--reduce-exclude: column(s) not found in file: {', '.join(unknown)}"
|
|
238
|
+
)
|
|
239
|
+
return exclude
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _apply_reduction(args, parser, data, exclude):
|
|
243
|
+
"""Run the optional PCA column reduction on the outgoing sample.
|
|
244
|
+
|
|
245
|
+
Returns ``(data, tag_suffix)`` — unchanged when no reduction was requested.
|
|
246
|
+
"""
|
|
247
|
+
if not _reduce_requested(args):
|
|
248
|
+
return data, ""
|
|
249
|
+
try:
|
|
250
|
+
reduction = reduce_columns(
|
|
251
|
+
data,
|
|
252
|
+
n_components=args.reduce_components,
|
|
253
|
+
variance_ratio=args.reduce_variance,
|
|
254
|
+
exclude=exclude,
|
|
255
|
+
standardize=not args.reduce_no_standardize,
|
|
256
|
+
prefix=args.reduce_prefix,
|
|
257
|
+
seed=args.seed,
|
|
258
|
+
)
|
|
259
|
+
except ValueError as exc:
|
|
260
|
+
parser.error(f"column reduction: {exc}")
|
|
261
|
+
print()
|
|
262
|
+
print(format_reduction_report(reduction))
|
|
263
|
+
suffix = f"_pca{reduction.n_components}" if reduction.n_components else ""
|
|
264
|
+
return reduction.data, suffix
|
|
265
|
+
|
|
266
|
+
|
|
139
267
|
def _build_engine_spec(args, parser, engine, cols) -> dict:
|
|
140
268
|
"""Build the anonymizer spec for the engine path (validates against ``cols``,
|
|
141
269
|
supports explicit --anon and --suggest via the engine's approximate stats)."""
|
|
@@ -189,6 +317,7 @@ def _run_with_engine(args, parser, skip) -> int:
|
|
|
189
317
|
parser.error(
|
|
190
318
|
f"--skip: column(s) not found in file: {', '.join(unknown_skip)}"
|
|
191
319
|
)
|
|
320
|
+
reduce_exclude = _validate_reduce_args(args, parser, cols)
|
|
192
321
|
spec = _build_engine_spec(args, parser, engine, cols)
|
|
193
322
|
result = engine.sample(
|
|
194
323
|
args.source, args.count,
|
|
@@ -213,7 +342,8 @@ def _run_with_engine(args, parser, skip) -> int:
|
|
|
213
342
|
parser.error(f"anonymization failed: {exc}")
|
|
214
343
|
print(f"Anonymized columns: {', '.join(spec)}")
|
|
215
344
|
|
|
216
|
-
|
|
345
|
+
data, reduce_tag = _apply_reduction(args, parser, data, reduce_exclude)
|
|
346
|
+
tag = f"sample_{args.count}" + ("_anon" if spec else "") + reduce_tag
|
|
217
347
|
out_path = save_output(data, args.source, tag, output_folder=args.outdir)
|
|
218
348
|
print(f"\nSampled {len(data):,} rows.")
|
|
219
349
|
print(f"Output saved to: {out_path}")
|
|
@@ -288,6 +418,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
288
418
|
unknown_skip = [c for c in skip if c not in df.columns]
|
|
289
419
|
if unknown_skip:
|
|
290
420
|
parser.error(f"--skip: column(s) not found in file: {', '.join(unknown_skip)}")
|
|
421
|
+
reduce_exclude = _validate_reduce_args(args, parser, df.columns)
|
|
291
422
|
|
|
292
423
|
# parse explicit --anon flags into (column, kind, options) triples
|
|
293
424
|
anon_specs: list[tuple[str, str, dict]] = []
|
|
@@ -352,7 +483,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
352
483
|
parser.error(f"anonymization failed: {exc}")
|
|
353
484
|
print(f"Anonymized columns: {', '.join(spec)}")
|
|
354
485
|
|
|
355
|
-
|
|
486
|
+
data, reduce_tag = _apply_reduction(args, parser, data, reduce_exclude)
|
|
487
|
+
tag = f"sample_{args.count}" + ("_anon" if spec else "") + reduce_tag
|
|
356
488
|
out_path = save_output(data, args.source, tag, output_folder=args.outdir)
|
|
357
489
|
print(f"\nSampled {len(data)} rows.")
|
|
358
490
|
print(f"Output saved to: {out_path}")
|