data-sampler 3.4.1__tar.gz → 3.5.1__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.
Files changed (47) hide show
  1. {data_sampler-3.4.1 → data_sampler-3.5.1}/CHANGELOG.md +50 -0
  2. {data_sampler-3.4.1 → data_sampler-3.5.1}/PKG-INFO +38 -4
  3. {data_sampler-3.4.1 → data_sampler-3.5.1}/README.md +37 -3
  4. data_sampler-3.5.1/docs/img/tui-columns.png +0 -0
  5. data_sampler-3.5.1/docs/img/tui-file.png +0 -0
  6. data_sampler-3.5.1/docs/img/tui-report.png +0 -0
  7. {data_sampler-3.4.1 → data_sampler-3.5.1}/pyproject.toml +1 -1
  8. {data_sampler-3.4.1 → data_sampler-3.5.1}/src/data_sampler/__init__.py +26 -2
  9. data_sampler-3.5.1/src/data_sampler/_names.py +685 -0
  10. {data_sampler-3.4.1 → data_sampler-3.5.1}/src/data_sampler/anonymize.py +246 -34
  11. {data_sampler-3.4.1 → data_sampler-3.5.1}/src/data_sampler/engine.py +50 -5
  12. {data_sampler-3.4.1 → data_sampler-3.5.1}/src/data_sampler/io.py +26 -6
  13. {data_sampler-3.4.1 → data_sampler-3.5.1}/src/data_sampler/tui/app.py +517 -22
  14. {data_sampler-3.4.1 → data_sampler-3.5.1}/tests/test_anonymize.py +6 -5
  15. {data_sampler-3.4.1 → data_sampler-3.5.1}/tests/test_engine.py +17 -0
  16. {data_sampler-3.4.1 → data_sampler-3.5.1}/tests/test_io.py +36 -1
  17. data_sampler-3.5.1/tests/test_names.py +194 -0
  18. {data_sampler-3.4.1 → data_sampler-3.5.1}/tests/test_tui.py +146 -1
  19. data_sampler-3.4.1/src/data_sampler/_names.py +0 -60
  20. {data_sampler-3.4.1 → data_sampler-3.5.1}/.gitattributes +0 -0
  21. {data_sampler-3.4.1 → data_sampler-3.5.1}/.github/workflows/release.yml +0 -0
  22. {data_sampler-3.4.1 → data_sampler-3.5.1}/.gitignore +0 -0
  23. {data_sampler-3.4.1 → data_sampler-3.5.1}/LICENSE +0 -0
  24. {data_sampler-3.4.1 → data_sampler-3.5.1}/ROADMAP.md +0 -0
  25. {data_sampler-3.4.1 → data_sampler-3.5.1/docs}/RELEASING.md +0 -0
  26. {data_sampler-3.4.1 → data_sampler-3.5.1/docs}/TROUBLESHOOTING.md +0 -0
  27. {data_sampler-3.4.1 → data_sampler-3.5.1}/examples/employees.csv +0 -0
  28. {data_sampler-3.4.1 → data_sampler-3.5.1}/examples/using_data_sampler.ipynb +0 -0
  29. {data_sampler-3.4.1 → data_sampler-3.5.1}/requirements.txt +0 -0
  30. {data_sampler-3.4.1 → data_sampler-3.5.1}/scripts/run-tui.bat +0 -0
  31. {data_sampler-3.4.1 → data_sampler-3.5.1}/scripts/run-tui.sh +0 -0
  32. {data_sampler-3.4.1 → data_sampler-3.5.1}/src/data_sampler/__main__.py +0 -0
  33. {data_sampler-3.4.1 → data_sampler-3.5.1}/src/data_sampler/_logging.py +0 -0
  34. {data_sampler-3.4.1 → data_sampler-3.5.1}/src/data_sampler/cli.py +0 -0
  35. {data_sampler-3.4.1 → data_sampler-3.5.1}/src/data_sampler/reduce.py +0 -0
  36. {data_sampler-3.4.1 → data_sampler-3.5.1}/src/data_sampler/report.py +0 -0
  37. {data_sampler-3.4.1 → data_sampler-3.5.1}/src/data_sampler/sampling.py +0 -0
  38. {data_sampler-3.4.1 → data_sampler-3.5.1}/src/data_sampler/stats.py +0 -0
  39. {data_sampler-3.4.1 → data_sampler-3.5.1}/src/data_sampler/tui/__init__.py +0 -0
  40. {data_sampler-3.4.1 → data_sampler-3.5.1}/src/data_sampler/workflow.py +0 -0
  41. {data_sampler-3.4.1 → data_sampler-3.5.1}/tests/conftest.py +0 -0
  42. {data_sampler-3.4.1 → data_sampler-3.5.1}/tests/test_cli.py +0 -0
  43. {data_sampler-3.4.1 → data_sampler-3.5.1}/tests/test_reduce.py +0 -0
  44. {data_sampler-3.4.1 → data_sampler-3.5.1}/tests/test_report.py +0 -0
  45. {data_sampler-3.4.1 → data_sampler-3.5.1}/tests/test_sampling.py +0 -0
  46. {data_sampler-3.4.1 → data_sampler-3.5.1}/tests/test_stats.py +0 -0
  47. {data_sampler-3.4.1 → data_sampler-3.5.1}/tests/test_workflow.py +0 -0
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.5.1 — 2026-07-24
4
+
5
+ - **The "skip from reduction (PCA)" toggle is hidden for non-numeric columns.**
6
+ PCA reduction only ever operates on continuous numeric columns (booleans and
7
+ datetimes are explicitly rejected; strings/categoricals never qualify), so the
8
+ toggle is meaningless on those columns. It now appears in the detail panel only
9
+ when the selected column is numeric — matching the columns table, which already
10
+ renders `—` in the reduce cell for non-numeric columns. No behavior change to
11
+ the reduction itself: skipped columns were, and remain, held out of the PCA
12
+ math entirely and passed through to the output unchanged.
13
+
14
+ ## v3.5.0 — 2026-07-24
15
+
16
+ - **Names anonymizer is now gender- and ethnicity-aware.** The bundled library
17
+ (`_names.py`) is regrouped into real, hand-checked names keyed by
18
+ `<ethnicity>_<gender>` across **33 ethnic groups** (multiple Indian
19
+ regions/cultures, Chinese, Japanese, Korean, Vietnamese, Anglo, Italian,
20
+ French, German, Russian, Polish, Scandinavian, Greek, Turkish, Persian,
21
+ Arab-Levantine, Sudanese, Algerian, Moroccan, Ethiopian, Rwandan, Yoruba,
22
+ Igbo, Ghanaian-Akan, Hispanic, Brazilian, Filipino, Indonesian). Surnames are
23
+ grouped too — gendered where real (Russian Ivanov/Ivanova, Polish -ski/-ska)
24
+ and with distinctly-female additions (North-Indian Devi/Kumari) over unisex
25
+ bases (Kumar/Das). `NameAnonymizer(gender=…, ethnicity=…)` fixes a gender
26
+ (`male`/`female`/`third`/`undisclosed`) and/or an ethnic group; **third
27
+ gender** mixes given names within an ethnicity, **undisclosed** draws from any
28
+ ethnicity.
29
+ - **Gender/ethnicity can be read from another column** and mapped:
30
+ `NameAnonymizer(gender_column=…, ethnicity_column=…)` with per-value
31
+ `gender_map` / `ethnicity_map` overrides. Values in any encoding (M/F, 0/1 per
32
+ ISO-5218, `male`/`female`, other languages) are auto-detected
33
+ (`suggest_gender_mapping` / `suggest_ethnicity_mapping`) with a manual
34
+ fallback. `randomize_gender=True` reassigns genders and rewrites the gender
35
+ column so both fields are anonymized together.
36
+ - **Custom name libraries.** `export_names_library()` writes the current library
37
+ as an editable module; `load_names_library(path=…)` (or the
38
+ `DATA_SAMPLER_NAMES` env var) activates one for the session; and
39
+ `install_names_library(path=…)` installs one permanently into the package.
40
+ - **Report screen shows a "reproduce this in Python" snippet** — the exact
41
+ `ds.load_file / sample / anonymize / reduce_columns / save_output` calls that
42
+ reproduce the run — and it's included in the saved report `.txt`. Every TUI
43
+ capability now maps to the public API.
44
+ - **Remote inputs.** `load_file` (and the TUI file screen) accept `http(s)` URLs
45
+ (e.g. GitHub raw links). The DuckDB engine loads `httpfs` on demand so it can
46
+ sample remote `s3://` / `https://` **Parquet with HTTP range requests** — a
47
+ multi-GB remote file is sampled out-of-core without downloading it whole.
48
+ - **TUI restyle** — btop-inspired rounded, quiet-until-focused inputs / selects
49
+ / switches and outlined (non-solid) buttons; the columns table's per-stat
50
+ `mean`/`median`/`mode`/`sd` columns; the report save clarifies it includes the
51
+ histograms.
52
+
3
53
  ## v3.4.1 — 2026-07-24
4
54
 
5
55
  - **TUI columns table now breaks the single summary cell into separate
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-sampler
3
- Version: 3.4.1
3
+ Version: 3.5.1
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
@@ -75,6 +75,10 @@ Use it to:
75
75
  trigger it.
76
76
  - **Cut a 2 GB export down to 500 rows** for a demo or prototype that still
77
77
  behaves like the real thing.
78
+ - **Narrow a wide table, not just shorten it** — optionally collapse the
79
+ numeric columns into a handful of principal components (PCA) on the way out,
80
+ so the shared sample is short *and* narrow, with a report of how much
81
+ variance each component keeps and which columns moved together.
78
82
  - **Build test fixtures that mirror production skew** instead of uniform toy
79
83
  data.
80
84
  - **Give a class or workshop realistic data** without a data-sharing
@@ -103,6 +107,20 @@ Requires Python 3.10+. For development, clone the repo and
103
107
 
104
108
  ## Terminal UI
105
109
 
110
+ *Columns screen — per-column stats (mean / median / mode / sd), anonymizer,
111
+ stratify and reduce config, with multi-select for bulk edits:*
112
+
113
+ ![data-sampler columns screen](docs/img/tui-columns.png)
114
+
115
+ *File screen — type a path or pick one from the directory browser:*
116
+
117
+ ![data-sampler file screen](docs/img/tui-file.png)
118
+
119
+ *Report screen — stratification report on the left, source-vs-sample column
120
+ histograms on the right:*
121
+
122
+ ![data-sampler report screen](docs/img/tui-report.png)
123
+
106
124
  ```sh
107
125
  data-sampler # no arguments → opens the TUI
108
126
  data-sampler-tui # explicit TUI entry point
@@ -202,6 +220,9 @@ data-sampler wide.csv 500 --reduce-variance 0.9
202
220
  import data_sampler as ds
203
221
 
204
222
  df = ds.load_file("data.xlsx", sheet="Sheet2")
223
+ # …or straight from a URL (GitHub raw, S3, etc.); large remote Parquet can be
224
+ # sampled out-of-core with the DuckDB engine, without downloading it whole:
225
+ # df = ds.load_file("https://raw.githubusercontent.com/owner/repo/main/data.csv")
205
226
 
206
227
  # Data Wrangler-style column stats
207
228
  for s in ds.compute_stats(df):
@@ -226,6 +247,18 @@ anon = ds.anonymize(
226
247
  seed=7,
227
248
  )
228
249
 
250
+ # gender- and ethnicity-aware names: fix them, or read them from other columns
251
+ gendered = ds.anonymize(result.data, {
252
+ "name": ds.NameAnonymizer(gender="female", ethnicity="chinese"),
253
+ # or map per row from existing columns (values auto-detected + overridable):
254
+ # "name": ds.NameAnonymizer(gender_column="sex", ethnicity_column="origin"),
255
+ }, seed=7)
256
+
257
+ # bring your own names: export the library, edit it, load it back
258
+ ds.export_names_library("my_names.py") # editable copy of the current library
259
+ ds.load_names_library(path="my_names.py") # activate it for this session
260
+ # ds.install_names_library("my_names.py") # …or install permanently
261
+
229
262
  # optionally collapse the numeric columns into principal components
230
263
  red = ds.reduce_columns(anon, variance_ratio=0.9, exclude=["cust_id"])
231
264
  print(ds.format_reduction_report(red)) # variance kept + correlated groups
@@ -344,7 +377,7 @@ Missing values are left as missing. All anonymizers accept a seed (via
344
377
 
345
378
  | Kind | Replaces values with | Options (defaults) |
346
379
  | --- | --- | --- |
347
- | `names` | Realistic names from a bundled library of first, middle, and last names | `style`: `first_last`, `first_middle_last`, `last_first`, `first`, `last` |
380
+ | `names` | Realistic names from a bundled library grouped by ethnicity + gender (33 groups) | `style`: `first_last`, `first_middle_last`, `last_first`, `first`, `last`; `gender`: `male`/`female`/`third`/`undisclosed`; `ethnicity`; or `gender_column`/`ethnicity_column` (+ `gender_map`/`ethnicity_map`, `randomize_gender`) |
348
381
  | `sequential_id` | `start`, `start+interval`, ... in order of first appearance | `start` (1), `interval` (1), `prefix` (`""`), `width` (0, zero-pads) |
349
382
  | `numeric_jitter` | A random number within ±`pct` of the original | `pct` (0.2 = ±20 %), `round_to` (decimal places) |
350
383
  | `datetime_jitter` | A date/time shifted by a random offset within ±`max_delta` | `max_delta` (`"7D"`; any `pandas.Timedelta` string), `unit` (`"s"`; jitter resolution) |
@@ -592,10 +625,11 @@ python -m build # build the wheel + sdist into dist/
592
625
 
593
626
  Logging is controlled by `DATA_SAMPLER_LOG` (`quiet`/`info`/`verbose`) and
594
627
  `DATA_SAMPLER_LOG_FILE`. See `ROADMAP.md` for planned work and
595
- `TROUBLESHOOTING.md` for known failure modes.
628
+ [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) for known failure modes.
596
629
 
597
630
  Releases go to [PyPI](https://pypi.org/project/data-sampler/) via a
598
- human-triggered, test-gated GitHub Actions workflow — see `RELEASING.md`.
631
+ human-triggered, test-gated GitHub Actions workflow — see
632
+ [`docs/RELEASING.md`](docs/RELEASING.md).
599
633
 
600
634
  ---
601
635
 
@@ -20,6 +20,10 @@ Use it to:
20
20
  trigger it.
21
21
  - **Cut a 2 GB export down to 500 rows** for a demo or prototype that still
22
22
  behaves like the real thing.
23
+ - **Narrow a wide table, not just shorten it** — optionally collapse the
24
+ numeric columns into a handful of principal components (PCA) on the way out,
25
+ so the shared sample is short *and* narrow, with a report of how much
26
+ variance each component keeps and which columns moved together.
23
27
  - **Build test fixtures that mirror production skew** instead of uniform toy
24
28
  data.
25
29
  - **Give a class or workshop realistic data** without a data-sharing
@@ -48,6 +52,20 @@ Requires Python 3.10+. For development, clone the repo and
48
52
 
49
53
  ## Terminal UI
50
54
 
55
+ *Columns screen — per-column stats (mean / median / mode / sd), anonymizer,
56
+ stratify and reduce config, with multi-select for bulk edits:*
57
+
58
+ ![data-sampler columns screen](docs/img/tui-columns.png)
59
+
60
+ *File screen — type a path or pick one from the directory browser:*
61
+
62
+ ![data-sampler file screen](docs/img/tui-file.png)
63
+
64
+ *Report screen — stratification report on the left, source-vs-sample column
65
+ histograms on the right:*
66
+
67
+ ![data-sampler report screen](docs/img/tui-report.png)
68
+
51
69
  ```sh
52
70
  data-sampler # no arguments → opens the TUI
53
71
  data-sampler-tui # explicit TUI entry point
@@ -147,6 +165,9 @@ data-sampler wide.csv 500 --reduce-variance 0.9
147
165
  import data_sampler as ds
148
166
 
149
167
  df = ds.load_file("data.xlsx", sheet="Sheet2")
168
+ # …or straight from a URL (GitHub raw, S3, etc.); large remote Parquet can be
169
+ # sampled out-of-core with the DuckDB engine, without downloading it whole:
170
+ # df = ds.load_file("https://raw.githubusercontent.com/owner/repo/main/data.csv")
150
171
 
151
172
  # Data Wrangler-style column stats
152
173
  for s in ds.compute_stats(df):
@@ -171,6 +192,18 @@ anon = ds.anonymize(
171
192
  seed=7,
172
193
  )
173
194
 
195
+ # gender- and ethnicity-aware names: fix them, or read them from other columns
196
+ gendered = ds.anonymize(result.data, {
197
+ "name": ds.NameAnonymizer(gender="female", ethnicity="chinese"),
198
+ # or map per row from existing columns (values auto-detected + overridable):
199
+ # "name": ds.NameAnonymizer(gender_column="sex", ethnicity_column="origin"),
200
+ }, seed=7)
201
+
202
+ # bring your own names: export the library, edit it, load it back
203
+ ds.export_names_library("my_names.py") # editable copy of the current library
204
+ ds.load_names_library(path="my_names.py") # activate it for this session
205
+ # ds.install_names_library("my_names.py") # …or install permanently
206
+
174
207
  # optionally collapse the numeric columns into principal components
175
208
  red = ds.reduce_columns(anon, variance_ratio=0.9, exclude=["cust_id"])
176
209
  print(ds.format_reduction_report(red)) # variance kept + correlated groups
@@ -289,7 +322,7 @@ Missing values are left as missing. All anonymizers accept a seed (via
289
322
 
290
323
  | Kind | Replaces values with | Options (defaults) |
291
324
  | --- | --- | --- |
292
- | `names` | Realistic names from a bundled library of first, middle, and last names | `style`: `first_last`, `first_middle_last`, `last_first`, `first`, `last` |
325
+ | `names` | Realistic names from a bundled library grouped by ethnicity + gender (33 groups) | `style`: `first_last`, `first_middle_last`, `last_first`, `first`, `last`; `gender`: `male`/`female`/`third`/`undisclosed`; `ethnicity`; or `gender_column`/`ethnicity_column` (+ `gender_map`/`ethnicity_map`, `randomize_gender`) |
293
326
  | `sequential_id` | `start`, `start+interval`, ... in order of first appearance | `start` (1), `interval` (1), `prefix` (`""`), `width` (0, zero-pads) |
294
327
  | `numeric_jitter` | A random number within ±`pct` of the original | `pct` (0.2 = ±20 %), `round_to` (decimal places) |
295
328
  | `datetime_jitter` | A date/time shifted by a random offset within ±`max_delta` | `max_delta` (`"7D"`; any `pandas.Timedelta` string), `unit` (`"s"`; jitter resolution) |
@@ -537,10 +570,11 @@ python -m build # build the wheel + sdist into dist/
537
570
 
538
571
  Logging is controlled by `DATA_SAMPLER_LOG` (`quiet`/`info`/`verbose`) and
539
572
  `DATA_SAMPLER_LOG_FILE`. See `ROADMAP.md` for planned work and
540
- `TROUBLESHOOTING.md` for known failure modes.
573
+ [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) for known failure modes.
541
574
 
542
575
  Releases go to [PyPI](https://pypi.org/project/data-sampler/) via a
543
- human-triggered, test-gated GitHub Actions workflow — see `RELEASING.md`.
576
+ human-triggered, test-gated GitHub Actions workflow — see
577
+ [`docs/RELEASING.md`](docs/RELEASING.md).
544
578
 
545
579
  ---
546
580
 
Binary file
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "data-sampler"
7
- version = "3.4.1"
7
+ version = "3.5.1"
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" }
@@ -18,7 +18,7 @@ Public API::
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
- __version__ = "3.4.1"
21
+ __version__ = "3.5.1"
22
22
 
23
23
  from .io import SUPPORTED_EXTENSIONS, list_sheets, load_file, save_output
24
24
  from .reduce import ReductionResult, reduce_columns
@@ -60,9 +60,16 @@ __all__ = [
60
60
  "format_column_histograms",
61
61
  "anonymize",
62
62
  "make_anonymizer",
63
+ "NameAnonymizer",
64
+ "suggest_gender_mapping",
65
+ "suggest_ethnicity_mapping",
63
66
  "AnonymizationPlan",
64
67
  "suggest_type",
65
68
  "TYPE_OPTIONS",
69
+ "export_names_library",
70
+ "load_names_library",
71
+ "install_names_library",
72
+ "name_ethnicities",
66
73
  "run_tui",
67
74
  ]
68
75
 
@@ -70,13 +77,30 @@ __all__ = [
70
77
  def __getattr__(name: str):
71
78
  # Lazy imports: keep pandas-only workflows fast and let the core API work
72
79
  # even while optional pieces (TUI) are not yet importable.
73
- if name in ("anonymize", "make_anonymizer"):
80
+ if name in (
81
+ "anonymize", "make_anonymizer", "NameAnonymizer",
82
+ "suggest_gender_mapping", "suggest_ethnicity_mapping",
83
+ ):
74
84
  # importlib, not `from . import anonymize`: the fromlist form calls
75
85
  # hasattr(package, "anonymize"), which re-enters this __getattr__.
76
86
  import importlib
77
87
 
78
88
  _anon = importlib.import_module(".anonymize", __name__)
79
89
  return getattr(_anon, name)
90
+ if name in (
91
+ "export_names_library", "load_names_library",
92
+ "install_names_library", "name_ethnicities",
93
+ ):
94
+ import importlib
95
+
96
+ _n = importlib.import_module("._names", __name__)
97
+ _alias = {
98
+ "export_names_library": "export_library",
99
+ "load_names_library": "load_library",
100
+ "install_names_library": "install_library",
101
+ "name_ethnicities": "ETHNICITIES",
102
+ }[name]
103
+ return getattr(_n, _alias)
80
104
  if name in ("AnonymizationPlan", "suggest_type", "TYPE_OPTIONS"):
81
105
  import importlib
82
106