deidkit 0.1.4__tar.gz → 0.1.6__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.
- {deidkit-0.1.4/src/deidkit.egg-info → deidkit-0.1.6}/PKG-INFO +50 -24
- {deidkit-0.1.4 → deidkit-0.1.6}/README.md +46 -21
- {deidkit-0.1.4 → deidkit-0.1.6}/examples/quickstart.md +4 -3
- {deidkit-0.1.4 → deidkit-0.1.6}/pyproject.toml +15 -5
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/__init__.py +1 -1
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/cli.py +14 -9
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/config.py +10 -7
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/detect/pipeline.py +3 -5
- deidkit-0.1.6/src/deidkit/detect/spacy_ner.py +131 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/version.py +1 -1
- {deidkit-0.1.4 → deidkit-0.1.6/src/deidkit.egg-info}/PKG-INFO +50 -24
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit.egg-info/requires.txt +1 -0
- deidkit-0.1.6/tests/test_ner_model_helpers.py +43 -0
- deidkit-0.1.4/src/deidkit/detect/spacy_ner.py +0 -63
- deidkit-0.1.4/tests/test_ner_model_helpers.py +0 -30
- {deidkit-0.1.4 → deidkit-0.1.6}/LICENSE +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/MANIFEST.in +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/examples/benchmark.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/examples/make_sample.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/examples/policy.example.yaml +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/examples/policy.meridian.yaml +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/setup.cfg +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/__main__.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/api.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/data/NOTICES.md +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/data/context_triggers_en.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/data/context_triggers_es.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/data/en_given_names.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/data/en_surnames.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/data/es_given_names.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/data/es_surnames.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/data/honorifics_en.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/data/honorifics_es.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/data/medical_stoplist_en.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/data/medical_stoplist_es.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/data/medical_vocab.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/dates.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/detect/__init__.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/detect/checksums.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/detect/context.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/detect/gazetteer.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/detect/patterns.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/detect/types.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/engine.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/generators/__init__.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/generators/names.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/generators/surrogates.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/io.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/learn.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/mapping.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/report.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/resources.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/schema.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/secret.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit/textnorm.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit.egg-info/SOURCES.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit.egg-info/dependency_links.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit.egg-info/entry_points.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/src/deidkit.egg-info/top_level.txt +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_api.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_checksums.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_dates.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_db.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_detect.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_engine.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_hardening.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_known_phi.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_learn.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_medical_vocab.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_names.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_qa_fixes.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_qa_freetext_leaks.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_qa_hardening.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_schema_lang.py +0 -0
- {deidkit-0.1.4 → deidkit-0.1.6}/tests/test_scoring.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deidkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Schema-driven pseudonymization for clinical/tabular datasets: synthetic names, interval-preserving date shifting, multi-stage free-text PII detection, and before/after audit exports.
|
|
5
5
|
Author: Meridian Data
|
|
6
6
|
License: MIT
|
|
@@ -26,13 +26,14 @@ Requires-Dist: openpyxl>=3.0
|
|
|
26
26
|
Requires-Dist: PyYAML>=5.4
|
|
27
27
|
Requires-Dist: SQLAlchemy>=1.4
|
|
28
28
|
Requires-Dist: pyodbc>=4.0
|
|
29
|
-
|
|
30
|
-
Requires-Dist: spacy>=3.4; extra == "ner"
|
|
29
|
+
Requires-Dist: spacy>=3.4
|
|
31
30
|
Provides-Extra: parquet
|
|
32
31
|
Requires-Dist: pyarrow>=7.0; extra == "parquet"
|
|
33
32
|
Provides-Extra: sql
|
|
34
33
|
Requires-Dist: SQLAlchemy>=1.4; extra == "sql"
|
|
35
34
|
Requires-Dist: pyodbc>=4.0; extra == "sql"
|
|
35
|
+
Provides-Extra: ner
|
|
36
|
+
Requires-Dist: spacy>=3.4; extra == "ner"
|
|
36
37
|
Provides-Extra: all
|
|
37
38
|
Requires-Dist: spacy>=3.4; extra == "all"
|
|
38
39
|
Requires-Dist: pyarrow>=7.0; extra == "all"
|
|
@@ -283,8 +284,7 @@ REPLACE PHONE 0.99 regex '300 123 4567'
|
|
|
283
284
|
```
|
|
284
285
|
`REPLACE` = will be scrubbed. `review` = queued for a human instead.
|
|
285
286
|
|
|
286
|
-
*In code:* `dk.Detector("es"
|
|
287
|
-
detections.
|
|
287
|
+
*In code:* `dk.Detector(lang="es").detect(text)` returns the same detections.
|
|
288
288
|
|
|
289
289
|
### The audit workbook — evidence of every change
|
|
290
290
|
|
|
@@ -408,29 +408,43 @@ extra_stoplist_files=["learned/learned_stoplist.txt"])`.
|
|
|
408
408
|
|
|
409
409
|
## 8. Tuning recall & precision
|
|
410
410
|
|
|
411
|
+
**By default deidkit auto-redacts** (`balanced` mode) **with the statistical NER
|
|
412
|
+
stage on**, so it catches names automatically — including novel names with no
|
|
413
|
+
surrounding cue — **with no setup and no flags**. spaCy ships with deidkit, and
|
|
414
|
+
the ~500 MB model (too big for the pip package) is **downloaded automatically on
|
|
415
|
+
the first run** that needs it:
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
deidkit run <input> deid_out
|
|
419
|
+
# first run only: "[deidkit] NER model not found — downloading once (~500 MB)…"
|
|
420
|
+
# then it's cached and every later run uses it instantly.
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Nothing to remember, nothing to install separately. Fallbacks, if the auto-
|
|
424
|
+
download can't happen (no network, read-only box):
|
|
425
|
+
|
|
426
|
+
- deidkit **never fails** — it warns once and runs on the deterministic rules
|
|
427
|
+
(recall on bare/novel names is lower until the model is present).
|
|
428
|
+
- Pre-warm it deliberately with `deidkit download-model`, or install the model
|
|
429
|
+
wheel offline (see the offline note below).
|
|
430
|
+
- Turn auto-download off with `DEIDKIT_AUTO_DOWNLOAD=0` (then it's rules-only
|
|
431
|
+
unless the model is already installed), or drop NER entirely with `--no-ner`.
|
|
432
|
+
|
|
433
|
+
Knobs, only if you want to change the defaults:
|
|
434
|
+
|
|
411
435
|
| Lever | What it does |
|
|
412
436
|
| --- | --- |
|
|
413
|
-
| `--mode
|
|
414
|
-
| `--mode
|
|
415
|
-
| `--
|
|
437
|
+
| `--mode balanced` (default) | auto-redact confident detections, incl. single NER hits |
|
|
438
|
+
| `--mode conservative` | stricter — only clear names; send the ambiguous tail to `review.csv` |
|
|
439
|
+
| `--mode aggressive` | highest recall (also lone dictionary tokens); more to review |
|
|
440
|
+
| `--no-ner` | turn the NER stage off (deterministic rules only) |
|
|
441
|
+
| `--spacy-model NAME` | use a different spaCy model (default `es_core_news_lg`) |
|
|
416
442
|
| `--id-checksums DNI,NIE` | catch *bare* national IDs (no cue word) with near-zero false positives |
|
|
417
443
|
| `--name-style tagged\|placeholder\|token\|realistic` | how fakes look — `tagged` = "Juan Gómez [SINTÉTICO]" (obvious); `realistic` = unmarked |
|
|
418
444
|
| `--no-medical-vocab` | (advanced) stop treating ~21k drug/disease terms as safe non-names |
|
|
419
445
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
**Enabling NER** is a one-time model install (the model is ~500 MB and ships
|
|
424
|
-
separately from the package, not bundled):
|
|
425
|
-
|
|
426
|
-
```bash
|
|
427
|
-
pip install "deidkit[ner]" # installs spaCy
|
|
428
|
-
deidkit download-model es_core_news_lg # fetches the model (once)
|
|
429
|
-
deidkit run <input> deid_out --spacy-model es_core_news_lg
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
If the model isn't installed, deidkit still runs on the deterministic rules and
|
|
433
|
-
prints a clear warning — it never fails, but free-text name recall is lower.
|
|
446
|
+
Work the `review.csv` queue for whatever the automatic pass leaves uncertain — the
|
|
447
|
+
audit is how you prove the result.
|
|
434
448
|
|
|
435
449
|
*In code*, the same knobs are `Policy` fields:
|
|
436
450
|
|
|
@@ -439,6 +453,17 @@ dk.Policy(mode="balanced", spacy_model="es_core_news_lg",
|
|
|
439
453
|
id_checksums=["DNI", "NIE"], name_style="tagged")
|
|
440
454
|
```
|
|
441
455
|
|
|
456
|
+
**Offline / locked-down machine** (no internet on the box that runs deidkit): the
|
|
457
|
+
model is a normal pip wheel, so fetch it once on a connected machine and copy it
|
|
458
|
+
over — no runtime download needed.
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
# on a machine with internet:
|
|
462
|
+
pip download es_core_news_lg -d ./models
|
|
463
|
+
# on the offline runtime box:
|
|
464
|
+
pip install ./models/es_core_news_lg-*.whl
|
|
465
|
+
```
|
|
466
|
+
|
|
442
467
|
---
|
|
443
468
|
|
|
444
469
|
## 9. Reading from a database (Fabric / Azure SQL)
|
|
@@ -704,7 +729,8 @@ Run `deidkit <command> --help` for any command's flags.
|
|
|
704
729
|
### Python `Policy` fields
|
|
705
730
|
|
|
706
731
|
`entity_key` (default `"patient_id"`), `date_max_days` (365), `lang` (`"es"`),
|
|
707
|
-
`mode` (`"
|
|
732
|
+
`mode` (`"balanced"`), `name_style` (`"tagged"`), `spacy_model`
|
|
733
|
+
(`"es_core_news_lg"`, NER on by default), `shift_dates_in_text`
|
|
708
734
|
(True), `only`, `ignore`, `rules` (`list[FieldRule]`), `spacy_model`,
|
|
709
735
|
`known_name_columns`, `known_id_columns`, `extra_known_names`,
|
|
710
736
|
`known_token_min_len` (5), `id_checksums`, `use_medical_vocab` (True),
|
|
@@ -744,7 +770,7 @@ per-patient date offsets, and write the audit yourself:
|
|
|
744
770
|
```python
|
|
745
771
|
import deidkit as dk
|
|
746
772
|
|
|
747
|
-
policy = dk.Policy(lang="es",
|
|
773
|
+
policy = dk.Policy(lang="es", entity_key="patient_id", # mode defaults to balanced + NER
|
|
748
774
|
known_id_columns=["patient_id"], ignore={"blood_type"})
|
|
749
775
|
|
|
750
776
|
deid = dk.Deidentifier(
|
|
@@ -239,8 +239,7 @@ REPLACE PHONE 0.99 regex '300 123 4567'
|
|
|
239
239
|
```
|
|
240
240
|
`REPLACE` = will be scrubbed. `review` = queued for a human instead.
|
|
241
241
|
|
|
242
|
-
*In code:* `dk.Detector("es"
|
|
243
|
-
detections.
|
|
242
|
+
*In code:* `dk.Detector(lang="es").detect(text)` returns the same detections.
|
|
244
243
|
|
|
245
244
|
### The audit workbook — evidence of every change
|
|
246
245
|
|
|
@@ -364,29 +363,43 @@ extra_stoplist_files=["learned/learned_stoplist.txt"])`.
|
|
|
364
363
|
|
|
365
364
|
## 8. Tuning recall & precision
|
|
366
365
|
|
|
366
|
+
**By default deidkit auto-redacts** (`balanced` mode) **with the statistical NER
|
|
367
|
+
stage on**, so it catches names automatically — including novel names with no
|
|
368
|
+
surrounding cue — **with no setup and no flags**. spaCy ships with deidkit, and
|
|
369
|
+
the ~500 MB model (too big for the pip package) is **downloaded automatically on
|
|
370
|
+
the first run** that needs it:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
deidkit run <input> deid_out
|
|
374
|
+
# first run only: "[deidkit] NER model not found — downloading once (~500 MB)…"
|
|
375
|
+
# then it's cached and every later run uses it instantly.
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Nothing to remember, nothing to install separately. Fallbacks, if the auto-
|
|
379
|
+
download can't happen (no network, read-only box):
|
|
380
|
+
|
|
381
|
+
- deidkit **never fails** — it warns once and runs on the deterministic rules
|
|
382
|
+
(recall on bare/novel names is lower until the model is present).
|
|
383
|
+
- Pre-warm it deliberately with `deidkit download-model`, or install the model
|
|
384
|
+
wheel offline (see the offline note below).
|
|
385
|
+
- Turn auto-download off with `DEIDKIT_AUTO_DOWNLOAD=0` (then it's rules-only
|
|
386
|
+
unless the model is already installed), or drop NER entirely with `--no-ner`.
|
|
387
|
+
|
|
388
|
+
Knobs, only if you want to change the defaults:
|
|
389
|
+
|
|
367
390
|
| Lever | What it does |
|
|
368
391
|
| --- | --- |
|
|
369
|
-
| `--mode
|
|
370
|
-
| `--mode
|
|
371
|
-
| `--
|
|
392
|
+
| `--mode balanced` (default) | auto-redact confident detections, incl. single NER hits |
|
|
393
|
+
| `--mode conservative` | stricter — only clear names; send the ambiguous tail to `review.csv` |
|
|
394
|
+
| `--mode aggressive` | highest recall (also lone dictionary tokens); more to review |
|
|
395
|
+
| `--no-ner` | turn the NER stage off (deterministic rules only) |
|
|
396
|
+
| `--spacy-model NAME` | use a different spaCy model (default `es_core_news_lg`) |
|
|
372
397
|
| `--id-checksums DNI,NIE` | catch *bare* national IDs (no cue word) with near-zero false positives |
|
|
373
398
|
| `--name-style tagged\|placeholder\|token\|realistic` | how fakes look — `tagged` = "Juan Gómez [SINTÉTICO]" (obvious); `realistic` = unmarked |
|
|
374
399
|
| `--no-medical-vocab` | (advanced) stop treating ~21k drug/disease terms as safe non-names |
|
|
375
400
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
**Enabling NER** is a one-time model install (the model is ~500 MB and ships
|
|
380
|
-
separately from the package, not bundled):
|
|
381
|
-
|
|
382
|
-
```bash
|
|
383
|
-
pip install "deidkit[ner]" # installs spaCy
|
|
384
|
-
deidkit download-model es_core_news_lg # fetches the model (once)
|
|
385
|
-
deidkit run <input> deid_out --spacy-model es_core_news_lg
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
If the model isn't installed, deidkit still runs on the deterministic rules and
|
|
389
|
-
prints a clear warning — it never fails, but free-text name recall is lower.
|
|
401
|
+
Work the `review.csv` queue for whatever the automatic pass leaves uncertain — the
|
|
402
|
+
audit is how you prove the result.
|
|
390
403
|
|
|
391
404
|
*In code*, the same knobs are `Policy` fields:
|
|
392
405
|
|
|
@@ -395,6 +408,17 @@ dk.Policy(mode="balanced", spacy_model="es_core_news_lg",
|
|
|
395
408
|
id_checksums=["DNI", "NIE"], name_style="tagged")
|
|
396
409
|
```
|
|
397
410
|
|
|
411
|
+
**Offline / locked-down machine** (no internet on the box that runs deidkit): the
|
|
412
|
+
model is a normal pip wheel, so fetch it once on a connected machine and copy it
|
|
413
|
+
over — no runtime download needed.
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
# on a machine with internet:
|
|
417
|
+
pip download es_core_news_lg -d ./models
|
|
418
|
+
# on the offline runtime box:
|
|
419
|
+
pip install ./models/es_core_news_lg-*.whl
|
|
420
|
+
```
|
|
421
|
+
|
|
398
422
|
---
|
|
399
423
|
|
|
400
424
|
## 9. Reading from a database (Fabric / Azure SQL)
|
|
@@ -660,7 +684,8 @@ Run `deidkit <command> --help` for any command's flags.
|
|
|
660
684
|
### Python `Policy` fields
|
|
661
685
|
|
|
662
686
|
`entity_key` (default `"patient_id"`), `date_max_days` (365), `lang` (`"es"`),
|
|
663
|
-
`mode` (`"
|
|
687
|
+
`mode` (`"balanced"`), `name_style` (`"tagged"`), `spacy_model`
|
|
688
|
+
(`"es_core_news_lg"`, NER on by default), `shift_dates_in_text`
|
|
664
689
|
(True), `only`, `ignore`, `rules` (`list[FieldRule]`), `spacy_model`,
|
|
665
690
|
`known_name_columns`, `known_id_columns`, `extra_known_names`,
|
|
666
691
|
`known_token_min_len` (5), `id_checksums`, `use_medical_vocab` (True),
|
|
@@ -700,7 +725,7 @@ per-patient date offsets, and write the audit yourself:
|
|
|
700
725
|
```python
|
|
701
726
|
import deidkit as dk
|
|
702
727
|
|
|
703
|
-
policy = dk.Policy(lang="es",
|
|
728
|
+
policy = dk.Policy(lang="es", entity_key="patient_id", # mode defaults to balanced + NER
|
|
704
729
|
known_id_columns=["patient_id"], ignore={"blood_type"})
|
|
705
730
|
|
|
706
731
|
deid = dk.Deidentifier(
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
pip install -e .
|
|
7
|
-
#
|
|
8
|
-
|
|
7
|
+
# NER runs by default; its ~500 MB model auto-downloads on the first run that
|
|
8
|
+
# needs it (spaCy already ships with deidkit). Pre-fetch it with:
|
|
9
|
+
# deidkit download-model
|
|
9
10
|
```
|
|
10
11
|
|
|
11
12
|
If the `deidkit` command isn't on your PATH, use `python -m deidkit ...`
|
|
@@ -66,6 +67,6 @@ python examples/benchmark.py --n 400 --spacy es_core_news_lg
|
|
|
66
67
|
| tokens instead of fake names | `--names-token` |
|
|
67
68
|
| don't shift dates inside free text | `--no-date-text` |
|
|
68
69
|
| wider/narrower date window | `--date-max-days 30` |
|
|
69
|
-
|
|
|
70
|
+
| turn OFF the default NER stage | `--no-ner` |
|
|
70
71
|
| Parquet output | `--format parquet` |
|
|
71
72
|
| load a YAML policy | `--policy examples/policy.example.yaml` |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "deidkit"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6"
|
|
8
8
|
description = "Schema-driven pseudonymization for clinical/tabular datasets: synthetic names, interval-preserving date shifting, multi-stage free-text PII detection, and before/after audit exports."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -40,16 +40,17 @@ dependencies = [
|
|
|
40
40
|
# from the Microsoft Fabric SQL analytics endpoint (the primary data source).
|
|
41
41
|
"SQLAlchemy>=1.4",
|
|
42
42
|
"pyodbc>=4.0",
|
|
43
|
+
# spaCy is core so NER is on by default with no extra install step or bracket.
|
|
44
|
+
# The model itself is separate (too big for PyPI): `deidkit download-model`.
|
|
45
|
+
"spacy>=3.4",
|
|
43
46
|
]
|
|
44
47
|
|
|
45
48
|
[project.optional-dependencies]
|
|
46
|
-
# Statistical NER stage (second-opinion recall). After install run:
|
|
47
|
-
# python -m spacy download es_core_news_lg
|
|
48
|
-
ner = ["spacy>=3.4"]
|
|
49
49
|
# Read/write Parquet.
|
|
50
50
|
parquet = ["pyarrow>=7.0"]
|
|
51
|
-
# Back-compat
|
|
51
|
+
# Back-compat aliases: these are now core, so `deidkit[sql]` / `deidkit[ner]` == `deidkit`.
|
|
52
52
|
sql = ["SQLAlchemy>=1.4", "pyodbc>=4.0"]
|
|
53
|
+
ner = ["spacy>=3.4"]
|
|
53
54
|
# Everything except the heavy spaCy model download.
|
|
54
55
|
all = ["spacy>=3.4", "pyarrow>=7.0", "SQLAlchemy>=1.4", "pyodbc>=4.0"]
|
|
55
56
|
dev = ["pytest>=7.0"]
|
|
@@ -70,3 +71,12 @@ where = ["src"]
|
|
|
70
71
|
|
|
71
72
|
[tool.setuptools.package-data]
|
|
72
73
|
deidkit = ["data/*.txt", "data/*.md"]
|
|
74
|
+
|
|
75
|
+
[tool.pytest.ini_options]
|
|
76
|
+
filterwarnings = [
|
|
77
|
+
# The NER model / spaCy aren't present in CI; NER's graceful-fallback warnings
|
|
78
|
+
# are expected there and are exercised directly in test_ner_model_helpers.py.
|
|
79
|
+
"ignore:spaCy is missing.*:UserWarning",
|
|
80
|
+
"ignore:NER model.*auto-download is off.*:UserWarning",
|
|
81
|
+
"ignore:could not auto-download.*:UserWarning",
|
|
82
|
+
]
|
|
@@ -4,7 +4,7 @@ Quick start (library)::
|
|
|
4
4
|
|
|
5
5
|
import deidkit as dk
|
|
6
6
|
|
|
7
|
-
policy = dk.Policy(lang="es",
|
|
7
|
+
policy = dk.Policy(lang="es", ignore=["blood_type"]) # balanced + NER by default
|
|
8
8
|
deid = dk.Deidentifier(
|
|
9
9
|
policy,
|
|
10
10
|
secret="keep-this-secret",
|
|
@@ -50,6 +50,8 @@ def build_policy(args) -> Policy:
|
|
|
50
50
|
policy.date_max_days = args.date_max_days
|
|
51
51
|
if getattr(args, "spacy_model", None):
|
|
52
52
|
policy.spacy_model = args.spacy_model
|
|
53
|
+
if getattr(args, "no_ner", False):
|
|
54
|
+
policy.spacy_model = None
|
|
53
55
|
if getattr(args, "names_token", False):
|
|
54
56
|
policy.names_synthetic = False
|
|
55
57
|
if getattr(args, "no_date_text", False):
|
|
@@ -222,14 +224,14 @@ def cmd_run_db(args) -> int:
|
|
|
222
224
|
|
|
223
225
|
|
|
224
226
|
def cmd_download_model(args) -> int:
|
|
225
|
-
"""Download the spaCy model
|
|
227
|
+
"""Download the spaCy NER model (used by default). One-time, ~500 MB."""
|
|
226
228
|
model = args.model
|
|
227
229
|
try:
|
|
228
230
|
import spacy # noqa: F401
|
|
229
231
|
from spacy.cli import download
|
|
230
232
|
except ImportError:
|
|
231
|
-
print(
|
|
232
|
-
file=sys.stderr)
|
|
233
|
+
print("error: spaCy is missing (it ships with deidkit) — reinstall with "
|
|
234
|
+
"`pip install --force-reinstall deidkit`.", file=sys.stderr)
|
|
233
235
|
return 2
|
|
234
236
|
print(f"Downloading spaCy model '{model}' (~500 MB, one-time)…")
|
|
235
237
|
try:
|
|
@@ -239,8 +241,8 @@ def cmd_download_model(args) -> int:
|
|
|
239
241
|
print(f"error: could not download '{model}'. Check the model name and "
|
|
240
242
|
f"your network.", file=sys.stderr)
|
|
241
243
|
return 2
|
|
242
|
-
print(
|
|
243
|
-
|
|
244
|
+
print("Done. NER now runs by default; no flag needed. "
|
|
245
|
+
"(Turn it off with --no-ner if you ever want rules-only.)")
|
|
244
246
|
return 0
|
|
245
247
|
|
|
246
248
|
|
|
@@ -282,7 +284,8 @@ def cmd_run(args) -> int:
|
|
|
282
284
|
print(f" secret : deidkit-secret.key (KEEP — needed to re-run consistently)")
|
|
283
285
|
print(f"\n cells changed : {summ['cells_changed']:,}")
|
|
284
286
|
print(f" to review : {summ['review_detections']:,}")
|
|
285
|
-
print(f"
|
|
287
|
+
print(f" NER stage : {summ['ner_status']} (deterministic rules always run; "
|
|
288
|
+
f"{'add --no-ner for rules-only' if summ['ner_status'].startswith('active') else 'download-model to enable'})")
|
|
286
289
|
for etype, cnt in sorted(summ["entity_counts"].items(), key=lambda x: -x[1]):
|
|
287
290
|
print(f" {etype:<14} {cnt:,}")
|
|
288
291
|
return 0
|
|
@@ -294,7 +297,7 @@ def _add_policy_args(p):
|
|
|
294
297
|
p.add_argument("--lang", choices=["es", "en", "multi"],
|
|
295
298
|
help="Language(s) for names/detection ('multi' = ES+EN).")
|
|
296
299
|
p.add_argument("--mode", choices=sorted(ALL_MODES),
|
|
297
|
-
help="Free-text detection aggressiveness (default
|
|
300
|
+
help="Free-text detection aggressiveness (default balanced = auto-redact).")
|
|
298
301
|
p.add_argument("--name-style", choices=["tagged", "placeholder", "token", "realistic"],
|
|
299
302
|
help="How fake names look (default tagged = obviously synthetic).")
|
|
300
303
|
p.add_argument("--only", help="Comma list: transform ONLY these columns.")
|
|
@@ -308,7 +311,9 @@ def _add_policy_args(p):
|
|
|
308
311
|
p.add_argument("--no-date-text", action="store_true",
|
|
309
312
|
help="Do not shift date strings found inside free text.")
|
|
310
313
|
p.add_argument("--spacy-model",
|
|
311
|
-
help="
|
|
314
|
+
help="Override the NER model (default es_core_news_lg; on by default).")
|
|
315
|
+
p.add_argument("--no-ner", action="store_true",
|
|
316
|
+
help="Turn off the statistical NER stage (deterministic rules only).")
|
|
312
317
|
p.add_argument("--known-name-column", action="append",
|
|
313
318
|
help="Column whose value is a known person name to force-scrub "
|
|
314
319
|
"from that row's free text (repeatable).")
|
|
@@ -404,7 +409,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
404
409
|
|
|
405
410
|
pdm = sub.add_parser(
|
|
406
411
|
"download-model",
|
|
407
|
-
help="Download the spaCy NER model
|
|
412
|
+
help="Download the spaCy NER model (used by default; one-time ~500 MB).")
|
|
408
413
|
pdm.add_argument("model", nargs="?", default="es_core_news_lg",
|
|
409
414
|
help="spaCy model name (default es_core_news_lg).")
|
|
410
415
|
pdm.set_defaults(func=cmd_download_model)
|
|
@@ -49,9 +49,9 @@ ALL_STRATEGIES = frozenset(
|
|
|
49
49
|
)
|
|
50
50
|
|
|
51
51
|
# Detection aggressiveness for the free-text stage.
|
|
52
|
-
MODE_CONSERVATIVE = "conservative" # only replace when clearly a name (
|
|
53
|
-
MODE_BALANCED = "balanced" # also
|
|
54
|
-
MODE_AGGRESSIVE = "aggressive" # accept any plausible person span (
|
|
52
|
+
MODE_CONSERVATIVE = "conservative" # only replace when clearly a name (review the rest)
|
|
53
|
+
MODE_BALANCED = "balanced" # also auto-redact single-signal / NER hits (default)
|
|
54
|
+
MODE_AGGRESSIVE = "aggressive" # accept any plausible person span (highest recall)
|
|
55
55
|
ALL_MODES = frozenset({MODE_CONSERVATIVE, MODE_BALANCED, MODE_AGGRESSIVE})
|
|
56
56
|
|
|
57
57
|
# How replacement person names look. All are OBVIOUSLY synthetic except
|
|
@@ -129,21 +129,24 @@ class Policy:
|
|
|
129
129
|
rules:
|
|
130
130
|
Explicit per-column overrides evaluated before the schema auto-plan.
|
|
131
131
|
spacy_model:
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
The statistical NER model, ON by default (``"es_core_news_lg"``) so novel
|
|
133
|
+
names are caught with no extra flags. It loads lazily and only if present
|
|
134
|
+
— if the model isn't downloaded yet, deidkit warns once and runs on the
|
|
135
|
+
deterministic stages. Download it with ``deidkit download-model``; set to
|
|
136
|
+
``None`` to turn NER off.
|
|
134
137
|
"""
|
|
135
138
|
|
|
136
139
|
entity_key: str = "patient_id"
|
|
137
140
|
date_max_days: int = 365
|
|
138
141
|
lang: str = "es"
|
|
139
|
-
mode: str =
|
|
142
|
+
mode: str = MODE_BALANCED
|
|
140
143
|
name_style: str = NAME_TAGGED
|
|
141
144
|
names_synthetic: bool = True
|
|
142
145
|
shift_dates_in_text: bool = True
|
|
143
146
|
only: Optional[set] = None
|
|
144
147
|
ignore: set = field(default_factory=set)
|
|
145
148
|
rules: List[FieldRule] = field(default_factory=list)
|
|
146
|
-
spacy_model: Optional[str] =
|
|
149
|
+
spacy_model: Optional[str] = "es_core_news_lg"
|
|
147
150
|
# Advanced: extra gazetteer/stoplist files to merge on top of the bundled ones.
|
|
148
151
|
extra_name_files: List[str] = field(default_factory=list)
|
|
149
152
|
extra_stoplist_files: List[str] = field(default_factory=list)
|
|
@@ -52,7 +52,7 @@ class Detector:
|
|
|
52
52
|
def __init__(
|
|
53
53
|
self,
|
|
54
54
|
lang: str = "es",
|
|
55
|
-
mode: str = "
|
|
55
|
+
mode: str = "balanced",
|
|
56
56
|
spacy_model: Optional[str] = None,
|
|
57
57
|
extra_name_files=(),
|
|
58
58
|
extra_stoplist_files=(),
|
|
@@ -70,9 +70,7 @@ class Detector:
|
|
|
70
70
|
|
|
71
71
|
@property
|
|
72
72
|
def ner_status(self) -> str:
|
|
73
|
-
if self.ner is None
|
|
74
|
-
return "disabled"
|
|
75
|
-
return "active" if self.ner.available else f"unavailable ({self.ner.error})"
|
|
73
|
+
return "disabled" if self.ner is None else self.ner.ner_status
|
|
76
74
|
|
|
77
75
|
# ------------------------------------------------------------------ #
|
|
78
76
|
def detect(self, text: str) -> List[Detection]:
|
|
@@ -94,7 +92,7 @@ class Detector:
|
|
|
94
92
|
raw += checksums.find_valid_ids(text, self.id_checksums)
|
|
95
93
|
raw += context.find(text, tokens, self.gaz)
|
|
96
94
|
raw += gazetteer.find(text, tokens, self.gaz)
|
|
97
|
-
if self.ner is not None and
|
|
95
|
+
if self.ner is not None: # find() lazy-loads the model and returns [] if absent
|
|
98
96
|
raw += self.ner.find(text)
|
|
99
97
|
|
|
100
98
|
idents = [d for d in raw if d.entity_type in _IDENTIFIER_TYPES]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Statistical NER stage (spaCy).
|
|
2
|
+
|
|
3
|
+
On by default (``Policy.spacy_model`` = ``"es_core_news_lg"``). The model loads
|
|
4
|
+
**lazily** on first use, and if it isn't installed yet deidkit **auto-downloads
|
|
5
|
+
it once** — so there is no separate install step for anyone to forget. Everything
|
|
6
|
+
degrades gracefully: if the download can't happen (no network, read-only env),
|
|
7
|
+
``available`` is ``False``, :meth:`find` returns nothing, a one-time warning names
|
|
8
|
+
the fix, and the tool still runs on the deterministic stages.
|
|
9
|
+
|
|
10
|
+
Auto-download is on by default; set ``DEIDKIT_AUTO_DOWNLOAD=0`` to disable it (for
|
|
11
|
+
offline/locked-down machines, pre-install the model wheel instead).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import importlib
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import warnings
|
|
20
|
+
from typing import List
|
|
21
|
+
|
|
22
|
+
from .types import Detection, PERSON
|
|
23
|
+
|
|
24
|
+
_PERSON_LABELS = {"PER", "PERSON"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _auto_download_enabled() -> bool:
|
|
28
|
+
return os.environ.get("DEIDKIT_AUTO_DOWNLOAD", "1").strip().lower() not in (
|
|
29
|
+
"0", "false", "no", "off",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SpacyNER:
|
|
34
|
+
def __init__(self, model: str):
|
|
35
|
+
self.model = model
|
|
36
|
+
self._nlp = None
|
|
37
|
+
self._loaded = False
|
|
38
|
+
self.available = False
|
|
39
|
+
self.error = None
|
|
40
|
+
|
|
41
|
+
def _try_load(self) -> bool:
|
|
42
|
+
try:
|
|
43
|
+
import spacy
|
|
44
|
+
|
|
45
|
+
self._nlp = spacy.load(self.model, disable=["lemmatizer", "textcat"])
|
|
46
|
+
self.available = True
|
|
47
|
+
return True
|
|
48
|
+
except Exception as exc: # OSError (model absent) / ImportError (spaCy absent)
|
|
49
|
+
self.error = str(exc)
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
def _ensure_loaded(self) -> None:
|
|
53
|
+
"""Load on first use; auto-download the model once if it isn't installed."""
|
|
54
|
+
if self._loaded:
|
|
55
|
+
return
|
|
56
|
+
self._loaded = True
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
import spacy # noqa: F401
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
self.error = str(exc)
|
|
62
|
+
warnings.warn(
|
|
63
|
+
f"spaCy is missing (it ships with deidkit); NER is off. Reinstall "
|
|
64
|
+
f"with `pip install --force-reinstall deidkit`. ({exc})",
|
|
65
|
+
stacklevel=3,
|
|
66
|
+
)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
if self._try_load():
|
|
70
|
+
return # already installed
|
|
71
|
+
|
|
72
|
+
if not _auto_download_enabled():
|
|
73
|
+
warnings.warn(
|
|
74
|
+
f"NER model {self.model!r} is not installed and auto-download is off "
|
|
75
|
+
f"(DEIDKIT_AUTO_DOWNLOAD=0); running rules-only. Pre-install it with "
|
|
76
|
+
f"`deidkit download-model {self.model}`.",
|
|
77
|
+
stacklevel=3,
|
|
78
|
+
)
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Auto-download once (this run), then load in-process.
|
|
82
|
+
try:
|
|
83
|
+
print(f"[deidkit] NER model {self.model!r} not found — downloading once "
|
|
84
|
+
f"(~500 MB, first run only)…", file=sys.stderr, flush=True)
|
|
85
|
+
from spacy.cli import download
|
|
86
|
+
|
|
87
|
+
download(self.model)
|
|
88
|
+
importlib.invalidate_caches() # make the just-installed package importable now
|
|
89
|
+
if self._try_load():
|
|
90
|
+
print("[deidkit] NER model ready.", file=sys.stderr, flush=True)
|
|
91
|
+
return
|
|
92
|
+
except SystemExit as exc: # spacy.cli.download can sys.exit on failure
|
|
93
|
+
self.error = f"download exited ({exc.code})"
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
self.error = str(exc)
|
|
96
|
+
|
|
97
|
+
warnings.warn(
|
|
98
|
+
f"could not auto-download NER model {self.model!r} ({self.error}); running "
|
|
99
|
+
f"on the deterministic rules only. Fixes: ensure network access and rerun, "
|
|
100
|
+
f"run `deidkit download-model {self.model}`, or set DEIDKIT_AUTO_DOWNLOAD=0 "
|
|
101
|
+
f"to stay rules-only quietly.",
|
|
102
|
+
stacklevel=3,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def ner_status(self) -> str:
|
|
107
|
+
if not self._loaded:
|
|
108
|
+
return "pending (loads on first use)"
|
|
109
|
+
return "active" if self.available else f"unavailable ({self.error})"
|
|
110
|
+
|
|
111
|
+
def find(self, text: str) -> List[Detection]:
|
|
112
|
+
if not text:
|
|
113
|
+
return []
|
|
114
|
+
self._ensure_loaded()
|
|
115
|
+
if not self.available:
|
|
116
|
+
return []
|
|
117
|
+
doc = self._nlp(text)
|
|
118
|
+
out: List[Detection] = []
|
|
119
|
+
for ent in doc.ents:
|
|
120
|
+
if ent.label_ in _PERSON_LABELS:
|
|
121
|
+
out.append(
|
|
122
|
+
Detection(
|
|
123
|
+
start=ent.start_char,
|
|
124
|
+
end=ent.end_char,
|
|
125
|
+
text=ent.text,
|
|
126
|
+
entity_type=PERSON,
|
|
127
|
+
methods={"ner"},
|
|
128
|
+
meta={"gender": "u"},
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
return out
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deidkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Schema-driven pseudonymization for clinical/tabular datasets: synthetic names, interval-preserving date shifting, multi-stage free-text PII detection, and before/after audit exports.
|
|
5
5
|
Author: Meridian Data
|
|
6
6
|
License: MIT
|
|
@@ -26,13 +26,14 @@ Requires-Dist: openpyxl>=3.0
|
|
|
26
26
|
Requires-Dist: PyYAML>=5.4
|
|
27
27
|
Requires-Dist: SQLAlchemy>=1.4
|
|
28
28
|
Requires-Dist: pyodbc>=4.0
|
|
29
|
-
|
|
30
|
-
Requires-Dist: spacy>=3.4; extra == "ner"
|
|
29
|
+
Requires-Dist: spacy>=3.4
|
|
31
30
|
Provides-Extra: parquet
|
|
32
31
|
Requires-Dist: pyarrow>=7.0; extra == "parquet"
|
|
33
32
|
Provides-Extra: sql
|
|
34
33
|
Requires-Dist: SQLAlchemy>=1.4; extra == "sql"
|
|
35
34
|
Requires-Dist: pyodbc>=4.0; extra == "sql"
|
|
35
|
+
Provides-Extra: ner
|
|
36
|
+
Requires-Dist: spacy>=3.4; extra == "ner"
|
|
36
37
|
Provides-Extra: all
|
|
37
38
|
Requires-Dist: spacy>=3.4; extra == "all"
|
|
38
39
|
Requires-Dist: pyarrow>=7.0; extra == "all"
|
|
@@ -283,8 +284,7 @@ REPLACE PHONE 0.99 regex '300 123 4567'
|
|
|
283
284
|
```
|
|
284
285
|
`REPLACE` = will be scrubbed. `review` = queued for a human instead.
|
|
285
286
|
|
|
286
|
-
*In code:* `dk.Detector("es"
|
|
287
|
-
detections.
|
|
287
|
+
*In code:* `dk.Detector(lang="es").detect(text)` returns the same detections.
|
|
288
288
|
|
|
289
289
|
### The audit workbook — evidence of every change
|
|
290
290
|
|
|
@@ -408,29 +408,43 @@ extra_stoplist_files=["learned/learned_stoplist.txt"])`.
|
|
|
408
408
|
|
|
409
409
|
## 8. Tuning recall & precision
|
|
410
410
|
|
|
411
|
+
**By default deidkit auto-redacts** (`balanced` mode) **with the statistical NER
|
|
412
|
+
stage on**, so it catches names automatically — including novel names with no
|
|
413
|
+
surrounding cue — **with no setup and no flags**. spaCy ships with deidkit, and
|
|
414
|
+
the ~500 MB model (too big for the pip package) is **downloaded automatically on
|
|
415
|
+
the first run** that needs it:
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
deidkit run <input> deid_out
|
|
419
|
+
# first run only: "[deidkit] NER model not found — downloading once (~500 MB)…"
|
|
420
|
+
# then it's cached and every later run uses it instantly.
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Nothing to remember, nothing to install separately. Fallbacks, if the auto-
|
|
424
|
+
download can't happen (no network, read-only box):
|
|
425
|
+
|
|
426
|
+
- deidkit **never fails** — it warns once and runs on the deterministic rules
|
|
427
|
+
(recall on bare/novel names is lower until the model is present).
|
|
428
|
+
- Pre-warm it deliberately with `deidkit download-model`, or install the model
|
|
429
|
+
wheel offline (see the offline note below).
|
|
430
|
+
- Turn auto-download off with `DEIDKIT_AUTO_DOWNLOAD=0` (then it's rules-only
|
|
431
|
+
unless the model is already installed), or drop NER entirely with `--no-ner`.
|
|
432
|
+
|
|
433
|
+
Knobs, only if you want to change the defaults:
|
|
434
|
+
|
|
411
435
|
| Lever | What it does |
|
|
412
436
|
| --- | --- |
|
|
413
|
-
| `--mode
|
|
414
|
-
| `--mode
|
|
415
|
-
| `--
|
|
437
|
+
| `--mode balanced` (default) | auto-redact confident detections, incl. single NER hits |
|
|
438
|
+
| `--mode conservative` | stricter — only clear names; send the ambiguous tail to `review.csv` |
|
|
439
|
+
| `--mode aggressive` | highest recall (also lone dictionary tokens); more to review |
|
|
440
|
+
| `--no-ner` | turn the NER stage off (deterministic rules only) |
|
|
441
|
+
| `--spacy-model NAME` | use a different spaCy model (default `es_core_news_lg`) |
|
|
416
442
|
| `--id-checksums DNI,NIE` | catch *bare* national IDs (no cue word) with near-zero false positives |
|
|
417
443
|
| `--name-style tagged\|placeholder\|token\|realistic` | how fakes look — `tagged` = "Juan Gómez [SINTÉTICO]" (obvious); `realistic` = unmarked |
|
|
418
444
|
| `--no-medical-vocab` | (advanced) stop treating ~21k drug/disease terms as safe non-names |
|
|
419
445
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
**Enabling NER** is a one-time model install (the model is ~500 MB and ships
|
|
424
|
-
separately from the package, not bundled):
|
|
425
|
-
|
|
426
|
-
```bash
|
|
427
|
-
pip install "deidkit[ner]" # installs spaCy
|
|
428
|
-
deidkit download-model es_core_news_lg # fetches the model (once)
|
|
429
|
-
deidkit run <input> deid_out --spacy-model es_core_news_lg
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
If the model isn't installed, deidkit still runs on the deterministic rules and
|
|
433
|
-
prints a clear warning — it never fails, but free-text name recall is lower.
|
|
446
|
+
Work the `review.csv` queue for whatever the automatic pass leaves uncertain — the
|
|
447
|
+
audit is how you prove the result.
|
|
434
448
|
|
|
435
449
|
*In code*, the same knobs are `Policy` fields:
|
|
436
450
|
|
|
@@ -439,6 +453,17 @@ dk.Policy(mode="balanced", spacy_model="es_core_news_lg",
|
|
|
439
453
|
id_checksums=["DNI", "NIE"], name_style="tagged")
|
|
440
454
|
```
|
|
441
455
|
|
|
456
|
+
**Offline / locked-down machine** (no internet on the box that runs deidkit): the
|
|
457
|
+
model is a normal pip wheel, so fetch it once on a connected machine and copy it
|
|
458
|
+
over — no runtime download needed.
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
# on a machine with internet:
|
|
462
|
+
pip download es_core_news_lg -d ./models
|
|
463
|
+
# on the offline runtime box:
|
|
464
|
+
pip install ./models/es_core_news_lg-*.whl
|
|
465
|
+
```
|
|
466
|
+
|
|
442
467
|
---
|
|
443
468
|
|
|
444
469
|
## 9. Reading from a database (Fabric / Azure SQL)
|
|
@@ -704,7 +729,8 @@ Run `deidkit <command> --help` for any command's flags.
|
|
|
704
729
|
### Python `Policy` fields
|
|
705
730
|
|
|
706
731
|
`entity_key` (default `"patient_id"`), `date_max_days` (365), `lang` (`"es"`),
|
|
707
|
-
`mode` (`"
|
|
732
|
+
`mode` (`"balanced"`), `name_style` (`"tagged"`), `spacy_model`
|
|
733
|
+
(`"es_core_news_lg"`, NER on by default), `shift_dates_in_text`
|
|
708
734
|
(True), `only`, `ignore`, `rules` (`list[FieldRule]`), `spacy_model`,
|
|
709
735
|
`known_name_columns`, `known_id_columns`, `extra_known_names`,
|
|
710
736
|
`known_token_min_len` (5), `id_checksums`, `use_medical_vocab` (True),
|
|
@@ -744,7 +770,7 @@ per-patient date offsets, and write the audit yourself:
|
|
|
744
770
|
```python
|
|
745
771
|
import deidkit as dk
|
|
746
772
|
|
|
747
|
-
policy = dk.Policy(lang="es",
|
|
773
|
+
policy = dk.Policy(lang="es", entity_key="patient_id", # mode defaults to balanced + NER
|
|
748
774
|
known_id_columns=["patient_id"], ignore={"blood_type"})
|
|
749
775
|
|
|
750
776
|
deid = dk.Deidentifier(
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""The NER model is separate from the package (too big for PyPI); it must be a
|
|
2
|
+
one-command install, and a missing model must warn loudly (not silently reduce
|
|
3
|
+
recall). These tests cover the helper command and the warning — without ever
|
|
4
|
+
downloading a model."""
|
|
5
|
+
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
from deidkit.cli import build_parser, cmd_download_model
|
|
9
|
+
from deidkit.detect.spacy_ner import SpacyNER
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_missing_model_degrades_gracefully(monkeypatch):
|
|
13
|
+
monkeypatch.setenv("DEIDKIT_AUTO_DOWNLOAD", "0") # never hit the network here
|
|
14
|
+
ner = SpacyNER("definitely_not_a_real_model_zzz")
|
|
15
|
+
assert ner.available is False # lazy: not loaded yet
|
|
16
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
17
|
+
warnings.simplefilter("always")
|
|
18
|
+
result = ner.find("Paciente Juan Perez consulta.") # loads lazily -> warns
|
|
19
|
+
assert result == [] # degrades, no crash
|
|
20
|
+
assert ner.available is False # load failed
|
|
21
|
+
msgs = " ".join(str(w.message) for w in caught)
|
|
22
|
+
assert "NER" in msgs and "deidkit" in msgs # loud + actionable
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_auto_download_enabled_env_toggle(monkeypatch):
|
|
26
|
+
from deidkit.detect.spacy_ner import _auto_download_enabled
|
|
27
|
+
|
|
28
|
+
monkeypatch.delenv("DEIDKIT_AUTO_DOWNLOAD", raising=False)
|
|
29
|
+
assert _auto_download_enabled() is True # on by default
|
|
30
|
+
for off in ("0", "false", "no", "off"):
|
|
31
|
+
monkeypatch.setenv("DEIDKIT_AUTO_DOWNLOAD", off)
|
|
32
|
+
assert _auto_download_enabled() is False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_cli_wires_download_model_with_default():
|
|
36
|
+
args = build_parser().parse_args(["download-model"])
|
|
37
|
+
assert args.func is cmd_download_model
|
|
38
|
+
assert args.model == "es_core_news_lg"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_cli_download_model_accepts_explicit_name():
|
|
42
|
+
args = build_parser().parse_args(["download-model", "en_core_web_sm"])
|
|
43
|
+
assert args.model == "en_core_web_sm"
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
"""Optional statistical NER stage (spaCy).
|
|
2
|
-
|
|
3
|
-
Enabled only when ``Policy.spacy_model`` is set and spaCy + the model are
|
|
4
|
-
installed. It is used as an independent PERSON detector and to corroborate
|
|
5
|
-
gazetteer hits. Everything degrades gracefully: if the model is missing,
|
|
6
|
-
``available`` is ``False`` and :meth:`find` returns nothing, so the tool still
|
|
7
|
-
runs on the deterministic stages alone.
|
|
8
|
-
|
|
9
|
-
pip install "deidkit[ner]"
|
|
10
|
-
python -m spacy download es_core_news_lg
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
from typing import List
|
|
16
|
-
|
|
17
|
-
from .types import Detection, PERSON
|
|
18
|
-
|
|
19
|
-
_PERSON_LABELS = {"PER", "PERSON"}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class SpacyNER:
|
|
23
|
-
def __init__(self, model: str):
|
|
24
|
-
self.model = model
|
|
25
|
-
self._nlp = None
|
|
26
|
-
self.available = False
|
|
27
|
-
self.error = None
|
|
28
|
-
try:
|
|
29
|
-
import spacy
|
|
30
|
-
|
|
31
|
-
self._nlp = spacy.load(model, disable=["lemmatizer", "textcat"])
|
|
32
|
-
self.available = True
|
|
33
|
-
except Exception as exc: # ImportError or OSError (model not found)
|
|
34
|
-
self.error = str(exc)
|
|
35
|
-
import warnings
|
|
36
|
-
|
|
37
|
-
warnings.warn(
|
|
38
|
-
f"NER requested but spaCy model {model!r} is not available "
|
|
39
|
-
f"({type(exc).__name__}). The NER stage is OFF, so free-text name "
|
|
40
|
-
f"recall is reduced — names not in the gazetteer/anchored may be "
|
|
41
|
-
f"missed. Install it once with: deidkit download-model {model} "
|
|
42
|
-
f'(after: pip install "deidkit[ner]").',
|
|
43
|
-
stacklevel=3,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
def find(self, text: str) -> List[Detection]:
|
|
47
|
-
if not self.available or not text:
|
|
48
|
-
return []
|
|
49
|
-
doc = self._nlp(text)
|
|
50
|
-
out: List[Detection] = []
|
|
51
|
-
for ent in doc.ents:
|
|
52
|
-
if ent.label_ in _PERSON_LABELS:
|
|
53
|
-
out.append(
|
|
54
|
-
Detection(
|
|
55
|
-
start=ent.start_char,
|
|
56
|
-
end=ent.end_char,
|
|
57
|
-
text=ent.text,
|
|
58
|
-
entity_type=PERSON,
|
|
59
|
-
methods={"ner"},
|
|
60
|
-
meta={"gender": "u"},
|
|
61
|
-
)
|
|
62
|
-
)
|
|
63
|
-
return out
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"""The NER model is separate from the package (too big for PyPI); it must be a
|
|
2
|
-
one-command install, and a missing model must warn loudly (not silently reduce
|
|
3
|
-
recall). These tests cover the helper command and the warning — without ever
|
|
4
|
-
downloading a model."""
|
|
5
|
-
|
|
6
|
-
import warnings
|
|
7
|
-
|
|
8
|
-
from deidkit.cli import build_parser, cmd_download_model
|
|
9
|
-
from deidkit.detect.spacy_ner import SpacyNER
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def test_missing_model_warns_and_disables():
|
|
13
|
-
with warnings.catch_warnings(record=True) as caught:
|
|
14
|
-
warnings.simplefilter("always")
|
|
15
|
-
ner = SpacyNER("definitely_not_a_real_model_zzz")
|
|
16
|
-
assert ner.available is False
|
|
17
|
-
assert ner.find("Paciente Juan Perez consulta.") == [] # degrades, no crash
|
|
18
|
-
msgs = " ".join(str(w.message) for w in caught)
|
|
19
|
-
assert "download-model" in msgs and "NER" in msgs # loud + actionable
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def test_cli_wires_download_model_with_default():
|
|
23
|
-
args = build_parser().parse_args(["download-model"])
|
|
24
|
-
assert args.func is cmd_download_model
|
|
25
|
-
assert args.model == "es_core_news_lg"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def test_cli_download_model_accepts_explicit_name():
|
|
29
|
-
args = build_parser().parse_args(["download-model", "en_core_web_sm"])
|
|
30
|
-
assert args.model == "en_core_web_sm"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|