data-sampler 3.4.0__tar.gz → 3.5.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.
Files changed (49) hide show
  1. {data_sampler-3.4.0 → data_sampler-3.5.0}/CHANGELOG.md +66 -0
  2. {data_sampler-3.4.0 → data_sampler-3.5.0}/PKG-INFO +61 -16
  3. {data_sampler-3.4.0 → data_sampler-3.5.0}/README.md +60 -15
  4. data_sampler-3.5.0/docs/img/tui-columns.png +0 -0
  5. data_sampler-3.5.0/docs/img/tui-file.png +0 -0
  6. data_sampler-3.5.0/docs/img/tui-report.png +0 -0
  7. {data_sampler-3.4.0 → data_sampler-3.5.0}/pyproject.toml +1 -1
  8. {data_sampler-3.4.0 → data_sampler-3.5.0}/src/data_sampler/__init__.py +26 -2
  9. data_sampler-3.5.0/src/data_sampler/_names.py +685 -0
  10. {data_sampler-3.4.0 → data_sampler-3.5.0}/src/data_sampler/anonymize.py +246 -34
  11. {data_sampler-3.4.0 → data_sampler-3.5.0}/src/data_sampler/engine.py +50 -5
  12. {data_sampler-3.4.0 → data_sampler-3.5.0}/src/data_sampler/io.py +26 -6
  13. {data_sampler-3.4.0 → data_sampler-3.5.0}/src/data_sampler/stats.py +7 -0
  14. data_sampler-3.5.0/src/data_sampler/tui/app.py +1812 -0
  15. {data_sampler-3.4.0 → data_sampler-3.5.0}/tests/test_anonymize.py +6 -5
  16. {data_sampler-3.4.0 → data_sampler-3.5.0}/tests/test_engine.py +17 -0
  17. {data_sampler-3.4.0 → data_sampler-3.5.0}/tests/test_io.py +36 -1
  18. data_sampler-3.5.0/tests/test_names.py +194 -0
  19. {data_sampler-3.4.0 → data_sampler-3.5.0}/tests/test_stats.py +19 -0
  20. data_sampler-3.5.0/tests/test_tui.py +740 -0
  21. data_sampler-3.4.0/src/data_sampler/_names.py +0 -60
  22. data_sampler-3.4.0/src/data_sampler/tui/app.py +0 -985
  23. data_sampler-3.4.0/tests/test_tui.py +0 -321
  24. {data_sampler-3.4.0 → data_sampler-3.5.0}/.gitattributes +0 -0
  25. {data_sampler-3.4.0 → data_sampler-3.5.0}/.github/workflows/release.yml +0 -0
  26. {data_sampler-3.4.0 → data_sampler-3.5.0}/.gitignore +0 -0
  27. {data_sampler-3.4.0 → data_sampler-3.5.0}/LICENSE +0 -0
  28. {data_sampler-3.4.0 → data_sampler-3.5.0}/ROADMAP.md +0 -0
  29. {data_sampler-3.4.0 → data_sampler-3.5.0/docs}/RELEASING.md +0 -0
  30. {data_sampler-3.4.0 → data_sampler-3.5.0/docs}/TROUBLESHOOTING.md +0 -0
  31. {data_sampler-3.4.0 → data_sampler-3.5.0}/examples/employees.csv +0 -0
  32. {data_sampler-3.4.0 → data_sampler-3.5.0}/examples/using_data_sampler.ipynb +0 -0
  33. {data_sampler-3.4.0 → data_sampler-3.5.0}/requirements.txt +0 -0
  34. {data_sampler-3.4.0 → data_sampler-3.5.0}/scripts/run-tui.bat +0 -0
  35. {data_sampler-3.4.0 → data_sampler-3.5.0}/scripts/run-tui.sh +0 -0
  36. {data_sampler-3.4.0 → data_sampler-3.5.0}/src/data_sampler/__main__.py +0 -0
  37. {data_sampler-3.4.0 → data_sampler-3.5.0}/src/data_sampler/_logging.py +0 -0
  38. {data_sampler-3.4.0 → data_sampler-3.5.0}/src/data_sampler/cli.py +0 -0
  39. {data_sampler-3.4.0 → data_sampler-3.5.0}/src/data_sampler/reduce.py +0 -0
  40. {data_sampler-3.4.0 → data_sampler-3.5.0}/src/data_sampler/report.py +0 -0
  41. {data_sampler-3.4.0 → data_sampler-3.5.0}/src/data_sampler/sampling.py +0 -0
  42. {data_sampler-3.4.0 → data_sampler-3.5.0}/src/data_sampler/tui/__init__.py +0 -0
  43. {data_sampler-3.4.0 → data_sampler-3.5.0}/src/data_sampler/workflow.py +0 -0
  44. {data_sampler-3.4.0 → data_sampler-3.5.0}/tests/conftest.py +0 -0
  45. {data_sampler-3.4.0 → data_sampler-3.5.0}/tests/test_cli.py +0 -0
  46. {data_sampler-3.4.0 → data_sampler-3.5.0}/tests/test_reduce.py +0 -0
  47. {data_sampler-3.4.0 → data_sampler-3.5.0}/tests/test_report.py +0 -0
  48. {data_sampler-3.4.0 → data_sampler-3.5.0}/tests/test_sampling.py +0 -0
  49. {data_sampler-3.4.0 → data_sampler-3.5.0}/tests/test_workflow.py +0 -0
@@ -1,5 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.5.0 — 2026-07-24
4
+
5
+ - **Names anonymizer is now gender- and ethnicity-aware.** The bundled library
6
+ (`_names.py`) is regrouped into real, hand-checked names keyed by
7
+ `<ethnicity>_<gender>` across **33 ethnic groups** (multiple Indian
8
+ regions/cultures, Chinese, Japanese, Korean, Vietnamese, Anglo, Italian,
9
+ French, German, Russian, Polish, Scandinavian, Greek, Turkish, Persian,
10
+ Arab-Levantine, Sudanese, Algerian, Moroccan, Ethiopian, Rwandan, Yoruba,
11
+ Igbo, Ghanaian-Akan, Hispanic, Brazilian, Filipino, Indonesian). Surnames are
12
+ grouped too — gendered where real (Russian Ivanov/Ivanova, Polish -ski/-ska)
13
+ and with distinctly-female additions (North-Indian Devi/Kumari) over unisex
14
+ bases (Kumar/Das). `NameAnonymizer(gender=…, ethnicity=…)` fixes a gender
15
+ (`male`/`female`/`third`/`undisclosed`) and/or an ethnic group; **third
16
+ gender** mixes given names within an ethnicity, **undisclosed** draws from any
17
+ ethnicity.
18
+ - **Gender/ethnicity can be read from another column** and mapped:
19
+ `NameAnonymizer(gender_column=…, ethnicity_column=…)` with per-value
20
+ `gender_map` / `ethnicity_map` overrides. Values in any encoding (M/F, 0/1 per
21
+ ISO-5218, `male`/`female`, other languages) are auto-detected
22
+ (`suggest_gender_mapping` / `suggest_ethnicity_mapping`) with a manual
23
+ fallback. `randomize_gender=True` reassigns genders and rewrites the gender
24
+ column so both fields are anonymized together.
25
+ - **Custom name libraries.** `export_names_library()` writes the current library
26
+ as an editable module; `load_names_library(path=…)` (or the
27
+ `DATA_SAMPLER_NAMES` env var) activates one for the session; and
28
+ `install_names_library(path=…)` installs one permanently into the package.
29
+ - **Report screen shows a "reproduce this in Python" snippet** — the exact
30
+ `ds.load_file / sample / anonymize / reduce_columns / save_output` calls that
31
+ reproduce the run — and it's included in the saved report `.txt`. Every TUI
32
+ capability now maps to the public API.
33
+ - **Remote inputs.** `load_file` (and the TUI file screen) accept `http(s)` URLs
34
+ (e.g. GitHub raw links). The DuckDB engine loads `httpfs` on demand so it can
35
+ sample remote `s3://` / `https://` **Parquet with HTTP range requests** — a
36
+ multi-GB remote file is sampled out-of-core without downloading it whole.
37
+ - **TUI restyle** — btop-inspired rounded, quiet-until-focused inputs / selects
38
+ / switches and outlined (non-solid) buttons; the columns table's per-stat
39
+ `mean`/`median`/`mode`/`sd` columns; the report save clarifies it includes the
40
+ histograms.
41
+
42
+ ## v3.4.1 — 2026-07-24
43
+
44
+ - **TUI columns table now breaks the single summary cell into separate
45
+ `mean` / `median` / `mode` / `sd` columns.** Numeric columns fill all four;
46
+ non-numeric columns show `mode` (their most frequent value) with dashes for
47
+ the numeric-only stats. `ColumnStats` gained a `mode` field, populated for
48
+ every column kind by `compute_column_stats`.
49
+ - **Bulk configuration via multi-select on the columns screen.** Ctrl-click
50
+ toggles individual rows, shift-click selects a contiguous range (keyboard:
51
+ `space` toggles the cursor row, `x` clears the selection). Any anonymizer
52
+ choice, stratification-skip, or reduction-skip then applies to every
53
+ selected column at once; the panel title shows the selection count.
54
+ - **Columns can now be excluded from the PCA reduction from the TUI.** A new
55
+ “skip from reduction (PCA)” switch (and the `d` key) sets a per-column
56
+ `skip_reduce` flag that is passed to `reduce_columns(exclude=…)`, so chosen
57
+ numeric columns pass through the reduction unchanged. The columns table
58
+ gained a `reduce` column showing each column's candidacy / skip state, and
59
+ excluded columns are listed in the run report.
60
+ - **Undo / redo for column configuration** (`ctrl+z` / `ctrl+y`), keeping well
61
+ over the last ten steps. Bursts of edits to one option field coalesce into a
62
+ single undo step.
63
+ - **File screen: refresh the directory browser** with `ctrl+r` or the new
64
+ ⟳ refresh button, so files created after launch appear without restarting.
65
+ - **Report screen: save the report + column histograms to a text file** with
66
+ `ctrl+s` or the 💾 save .txt button; it writes `<sample-name>_report.txt`
67
+ next to the sample output.
68
+
3
69
  ## v3.4.0 — 2026-07-24
4
70
 
5
71
  - **Added optional PCA column reduction** (`data_sampler.reduce`): after
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-sampler
3
- Version: 3.4.0
3
+ Version: 3.5.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
@@ -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
@@ -120,24 +138,35 @@ data_sampler.run_tui("data.csv") # pre-load a file
120
138
 
121
139
  The TUI is a panel-based dashboard (think btop / lazydocker):
122
140
 
123
- 1. **File screen** — type a path or pick a file from the directory browser;
124
- Excel files take an optional sheet name.
141
+ 1. **File screen** — type a path or pick a file from the directory browser
142
+ (**`ctrl+r`** or the **⟳ refresh** button re-scans the folder so files
143
+ created since launch show up); Excel files take an optional sheet name.
125
144
  2. **Columns screen** — every column with its type, missing %, unique count,
126
- distribution sparkline, and summary (modelled after the Data Wrangler
127
- VS Code extension). Select a column to see full stats and distribution
128
- bars, choose an anonymizer for it, and toggle whether it should be
129
- skipped when preserving statistical variety (stratification). Set the
130
- sample size, output folder, optional seed, an optional PCA column
131
- reduction (`reduce`: N components or a variance target), and run.
145
+ distribution sparkline, and per-stat **mean / median / mode / sd** columns
146
+ (modelled after the Data Wrangler VS Code extension). Select a column to
147
+ see full stats and distribution bars, choose an anonymizer for it, and
148
+ toggle whether it should be skipped when preserving statistical variety
149
+ (stratification) or excluded from the PCA reduction. **Multi-select** rows
150
+ to configure them in bulk: **ctrl-click** toggles individual rows,
151
+ **shift-click** selects a range (**space** toggles the cursor row from the
152
+ keyboard, **`x`** clears) — then any anonymizer / skip / reduce choice
153
+ applies to every selected column at once. **`ctrl+z` / `ctrl+y`** undo and
154
+ redo column-config changes (at least the last ten steps). Set the sample
155
+ size, output folder, optional seed, an optional PCA column reduction
156
+ (`reduce`: N components or a variance target), and run.
132
157
  3. **Report screen** — the stratification comparison and anonymization summary
133
158
  on the left, and a **column histograms** panel on the right showing every
134
159
  column's source-vs-sample distribution (numeric columns share bin edges;
135
160
  others use the source's top categories) so you can see at a glance how well
136
- the sample preserved each column. The output path is shown too.
161
+ the sample preserved each column. The output path is shown too, and
162
+ **`ctrl+s`** (or **💾 save .txt**) writes the report and histograms to a
163
+ `*_report.txt` beside the sample.
137
164
 
138
- Key bindings: `ctrl+r` run sample, `a` auto-suggest anonymizer types,
139
- `s` toggle stratification skip,
140
- `escape` back, `ctrl+q` quit.
165
+ Key bindings — columns screen: `ctrl+r` run sample, `a` auto-suggest
166
+ anonymizer types, `s` toggle stratification skip, `d` toggle reduction skip,
167
+ `space` select row, `x` clear selection, `ctrl+z`/`ctrl+y` undo/redo,
168
+ `escape` back. Report screen: `ctrl+s` save report to text, `n` new file.
169
+ Everywhere: `ctrl+q` quit.
141
170
 
142
171
  ## CLI (headless)
143
172
 
@@ -191,6 +220,9 @@ data-sampler wide.csv 500 --reduce-variance 0.9
191
220
  import data_sampler as ds
192
221
 
193
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")
194
226
 
195
227
  # Data Wrangler-style column stats
196
228
  for s in ds.compute_stats(df):
@@ -215,6 +247,18 @@ anon = ds.anonymize(
215
247
  seed=7,
216
248
  )
217
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
+
218
262
  # optionally collapse the numeric columns into principal components
219
263
  red = ds.reduce_columns(anon, variance_ratio=0.9, exclude=["cust_id"])
220
264
  print(ds.format_reduction_report(red)) # variance kept + correlated groups
@@ -333,7 +377,7 @@ Missing values are left as missing. All anonymizers accept a seed (via
333
377
 
334
378
  | Kind | Replaces values with | Options (defaults) |
335
379
  | --- | --- | --- |
336
- | `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`) |
337
381
  | `sequential_id` | `start`, `start+interval`, ... in order of first appearance | `start` (1), `interval` (1), `prefix` (`""`), `width` (0, zero-pads) |
338
382
  | `numeric_jitter` | A random number within ±`pct` of the original | `pct` (0.2 = ±20 %), `round_to` (decimal places) |
339
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) |
@@ -581,10 +625,11 @@ python -m build # build the wheel + sdist into dist/
581
625
 
582
626
  Logging is controlled by `DATA_SAMPLER_LOG` (`quiet`/`info`/`verbose`) and
583
627
  `DATA_SAMPLER_LOG_FILE`. See `ROADMAP.md` for planned work and
584
- `TROUBLESHOOTING.md` for known failure modes.
628
+ [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) for known failure modes.
585
629
 
586
630
  Releases go to [PyPI](https://pypi.org/project/data-sampler/) via a
587
- 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).
588
633
 
589
634
  ---
590
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
@@ -65,24 +83,35 @@ data_sampler.run_tui("data.csv") # pre-load a file
65
83
 
66
84
  The TUI is a panel-based dashboard (think btop / lazydocker):
67
85
 
68
- 1. **File screen** — type a path or pick a file from the directory browser;
69
- Excel files take an optional sheet name.
86
+ 1. **File screen** — type a path or pick a file from the directory browser
87
+ (**`ctrl+r`** or the **⟳ refresh** button re-scans the folder so files
88
+ created since launch show up); Excel files take an optional sheet name.
70
89
  2. **Columns screen** — every column with its type, missing %, unique count,
71
- distribution sparkline, and summary (modelled after the Data Wrangler
72
- VS Code extension). Select a column to see full stats and distribution
73
- bars, choose an anonymizer for it, and toggle whether it should be
74
- skipped when preserving statistical variety (stratification). Set the
75
- sample size, output folder, optional seed, an optional PCA column
76
- reduction (`reduce`: N components or a variance target), and run.
90
+ distribution sparkline, and per-stat **mean / median / mode / sd** columns
91
+ (modelled after the Data Wrangler VS Code extension). Select a column to
92
+ see full stats and distribution bars, choose an anonymizer for it, and
93
+ toggle whether it should be skipped when preserving statistical variety
94
+ (stratification) or excluded from the PCA reduction. **Multi-select** rows
95
+ to configure them in bulk: **ctrl-click** toggles individual rows,
96
+ **shift-click** selects a range (**space** toggles the cursor row from the
97
+ keyboard, **`x`** clears) — then any anonymizer / skip / reduce choice
98
+ applies to every selected column at once. **`ctrl+z` / `ctrl+y`** undo and
99
+ redo column-config changes (at least the last ten steps). Set the sample
100
+ size, output folder, optional seed, an optional PCA column reduction
101
+ (`reduce`: N components or a variance target), and run.
77
102
  3. **Report screen** — the stratification comparison and anonymization summary
78
103
  on the left, and a **column histograms** panel on the right showing every
79
104
  column's source-vs-sample distribution (numeric columns share bin edges;
80
105
  others use the source's top categories) so you can see at a glance how well
81
- the sample preserved each column. The output path is shown too.
106
+ the sample preserved each column. The output path is shown too, and
107
+ **`ctrl+s`** (or **💾 save .txt**) writes the report and histograms to a
108
+ `*_report.txt` beside the sample.
82
109
 
83
- Key bindings: `ctrl+r` run sample, `a` auto-suggest anonymizer types,
84
- `s` toggle stratification skip,
85
- `escape` back, `ctrl+q` quit.
110
+ Key bindings — columns screen: `ctrl+r` run sample, `a` auto-suggest
111
+ anonymizer types, `s` toggle stratification skip, `d` toggle reduction skip,
112
+ `space` select row, `x` clear selection, `ctrl+z`/`ctrl+y` undo/redo,
113
+ `escape` back. Report screen: `ctrl+s` save report to text, `n` new file.
114
+ Everywhere: `ctrl+q` quit.
86
115
 
87
116
  ## CLI (headless)
88
117
 
@@ -136,6 +165,9 @@ data-sampler wide.csv 500 --reduce-variance 0.9
136
165
  import data_sampler as ds
137
166
 
138
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")
139
171
 
140
172
  # Data Wrangler-style column stats
141
173
  for s in ds.compute_stats(df):
@@ -160,6 +192,18 @@ anon = ds.anonymize(
160
192
  seed=7,
161
193
  )
162
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
+
163
207
  # optionally collapse the numeric columns into principal components
164
208
  red = ds.reduce_columns(anon, variance_ratio=0.9, exclude=["cust_id"])
165
209
  print(ds.format_reduction_report(red)) # variance kept + correlated groups
@@ -278,7 +322,7 @@ Missing values are left as missing. All anonymizers accept a seed (via
278
322
 
279
323
  | Kind | Replaces values with | Options (defaults) |
280
324
  | --- | --- | --- |
281
- | `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`) |
282
326
  | `sequential_id` | `start`, `start+interval`, ... in order of first appearance | `start` (1), `interval` (1), `prefix` (`""`), `width` (0, zero-pads) |
283
327
  | `numeric_jitter` | A random number within ±`pct` of the original | `pct` (0.2 = ±20 %), `round_to` (decimal places) |
284
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) |
@@ -526,10 +570,11 @@ python -m build # build the wheel + sdist into dist/
526
570
 
527
571
  Logging is controlled by `DATA_SAMPLER_LOG` (`quiet`/`info`/`verbose`) and
528
572
  `DATA_SAMPLER_LOG_FILE`. See `ROADMAP.md` for planned work and
529
- `TROUBLESHOOTING.md` for known failure modes.
573
+ [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) for known failure modes.
530
574
 
531
575
  Releases go to [PyPI](https://pypi.org/project/data-sampler/) via a
532
- 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).
533
578
 
534
579
  ---
535
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.0"
7
+ version = "3.5.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" }
@@ -18,7 +18,7 @@ Public API::
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
- __version__ = "3.4.0"
21
+ __version__ = "3.5.0"
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