Skip to content

feat(analyzer): add Czech PII recognizers (CZ_*)#2155

Open
santiagotri wants to merge 1 commit into
data-privacy-stack:mainfrom
santiagotri:main
Open

feat(analyzer): add Czech PII recognizers (CZ_*)#2155
santiagotri wants to merge 1 commit into
data-privacy-stack:mainfrom
santiagotri:main

Conversation

@santiagotri

@santiagotri santiagotri commented Jul 11, 2026

Copy link
Copy Markdown

Summary

Adds 5 new pattern-based recognizers for Czech-language PII plus Czech DATE_TIME
date coverage, covering the most relevant entity types under GDPR / zákon č. 110/2019 Sb.,
and fixes the collision where Czech identifiers (rodné číslo, bank accounts) were
mis-detected as PHONE_NUMBER:

Entity Description Legal Basis Validation
CZ_BIRTH_NUMBER Rodné číslo / birth number (YYMMDD/SSS or YYMMDD/SSSS, with or without slash) § 13 z. č. 133/2000 Sb. Mod 11 checksum, incl. the 1954–1985 historical exception; date semantics in regex (month ranges 01–12/21–32/51–62/71–82)
CZ_BANK_ACCOUNT Domestic bank account ([prefix-]number/bank code) vyhláška ČNB č. 169/2011 Sb. Weighted mod 11 checksum on both prefix and account number
CZ_ID_CARD Občanský průkaz / identity card (9 digits) z. č. 269/2021 Sb. Regex + structural checks, score=0.2 (context-driven)
CZ_PASSPORT Cestovní pas (2 letters + 6–7 digits, or 7–8 digits) z. č. 329/1999 Sb. Regex + uppercase enforcement
CZ_DRIVER_LICENSE Řidičský průkaz (2 letters + 6–9 digits) § 103 z. č. 361/2000 Sb. Regex + uppercase enforcement
DATE_TIME Czech date notation ("12. dubna 1985", "12. 4. 1985") via CzDateRecognizer Regex (day/month ranges, genitive + nominative month names)

Also ships a Czech language support recipe (docs/recipes/czech-language-support/)
with a ready-to-use bilingual EN+CS NLP configuration (spacy_en_cs.yaml).

Design decisions

  • All recognizers follow the existing PatternRecognizer pattern and are placed in
    predefined_recognizers/country_specific/czechia/, mirroring the German
    implementation from feat(analyzer): add German PII recognizers (DE_*) #1909
  • All are disabled by default (enabled: false) following the project convention
    for country-specific recognizers; registered for supported_languages: [cs] only,
    with country_code: cz
  • Checksum semantics deliberately differ from the German recognizers: a checksum
    pass promotes to MAX_SCORE, but a checksum failure on an otherwise date-plausible
    number returns None (keep base score, let context words decide) rather than
    False. Rationale: legitimate rodná čísla exist that fail strict mod 11 — 9-digit
    pre-1954 numbers carry no check digit at all, and ~1000 numbers issued 1954–1985
    fall under the historical mod-10 exception — so a hard reject would drop real PII.
    False is reserved for structurally impossible values (all-zero suffix/account,
    bank code 0000, wrong length)
  • PHONE_NUMBER collision fix: PhoneRecognizer scores a flat 0.4 and previously
    won on 850412/0003 and on part of 19-123456789/0800. Base scores plus context
    boosting put the Czech entities at 0.55–0.85 (1.0 on valid checksums), so
    score-based conflict resolution (as applied by presidio-anonymizer) picks the
    Czech entity; the genuine phone number +420 601 234 567 is unaffected
  • Context word lists contain inflected Czech surface forms ("účtu", "průkazu",
    "rodného", …) because the LemmaContextAwareEnhancer substring-matches lemmas and
    no Czech lemmatizer is available in the recommended pipeline
  • Patterns use lookaround anchors ((?<![\w/-]) …) instead of bare \b to prevent
    matches inside bank accounts, IBANs, and longer digit runs
  • CZ_PASSPORT / CZ_DRIVER_LICENSE enforce an uppercase prefix in
    validate_result(): patterns are matched with IGNORECASE (global flags), so
    ordinary two-letter words before a number ("je 123456") would otherwise match
  • CzDateRecognizer emits the standard DATE_TIME entity because no available Czech
    NER model emits DATE labels; CZ_BANK_ACCOUNT invalidates date-like dd/yyyy
    fragments (e.g. 04/2023)
  • NLP engine: neither spaCy nor Stanza publishes a Czech NER model, so the recipe
    uses the official multilingual xx_ent_wiki_sm for PERSON/LOCATION/ORG and
    documents a HuggingFace transformers model as the higher-quality upgrade path
  • All recognizers use global_regex_flags: 26 (IGNORECASE | MULTILINE | DOTALL)
    consistent with the rest of Presidio

Test plan

  • 7 test files: one per recognizer (presidio-analyzer/tests/test_cz_*.py) plus
    an end-to-end scenario (test_czech_language_support.py)
  • 130 test cases — positive matches, negative matches, edge cases
  • Separate validate_result() / invalidate_result() tests for the
    checksum-based recognizers (valid, historical-exception, failing, and
    structurally impossible inputs)
  • End-to-end test analyzes a full Czech sample (name, birth date, rodné číslo,
    address, phone, e-mail, ID card, passport, driving licence, bank account,
    IBAN), asserts every span/entity, includes a before-state regression test for
    the PHONE_NUMBER collision, and verifies the exact anonymized output — using
    precomputed NLP artifacts so CI needs no model download
  • All tests pass locally: python -m pytest presidio-analyzer/tests/test_cz*.py
  • Full analyzer suite failure profile identical to main baseline (remaining
    failures are pre-existing optional-dependency/model gaps, verified via stash
    comparison)
  • ruff check passes with no issues
  • default_recognizers.yaml entries follow the existing country-specific
    pattern; default en-only registry verified unaffected via
    RecognizerRegistryProvider load test
  • docs/supported_entities.md updated with Czechia section
  • Czech language support recipe added under docs/recipes/czech-language-support/
    -CHANGELOG.md entry added under [unreleased]

Checklist

  • I have reviewed the contribution guidelines
  • I agree to follow this project's Code of Conduct
  • I confirm that I have the right to submit this contribution and that it does not knowingly contain proprietary or confidential code.
  • My code includes unit tests
  • All unit tests and lint checks pass locally
  • My PR contains documentation updates / additions if required

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings July 11, 2026 11:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds first-class Czech (cs) analyzer support by introducing new pattern-based CZ_* recognizers (plus Czech DATE_TIME notation support), wiring them into the predefined registry/configuration, and documenting how to run a bilingual EN+CS pipeline.

Changes:

  • Added 5 Czech PII recognizers (CZ_BIRTH_NUMBER, CZ_BANK_ACCOUNT, CZ_ID_CARD, CZ_PASSPORT, CZ_DRIVER_LICENSE) and a Czech DATE_TIME recognizer (CzDateRecognizer).
  • Registered the new recognizers in predefined_recognizers/__init__.py and conf/default_recognizers.yaml (disabled by default, country_code: cz).
  • Added documentation and recipes for Czech language support, plus comprehensive unit and end-to-end tests.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
presidio-analyzer/tests/test_czech_language_support.py End-to-end Czech pipeline test + PHONE_NUMBER collision regression coverage
presidio-analyzer/tests/test_cz_passport_recognizer.py Unit tests for CzPassportRecognizer patterns/validation
presidio-analyzer/tests/test_cz_id_card_recognizer.py Unit tests for CzIdCardRecognizer patterns/validation
presidio-analyzer/tests/test_cz_driver_license_recognizer.py Unit tests for CzDriverLicenseRecognizer patterns/validation
presidio-analyzer/tests/test_cz_date_recognizer.py Unit tests for CzDateRecognizer Czech date notation coverage
presidio-analyzer/tests/test_cz_birth_number_recognizer.py Unit tests for CzBirthNumberRecognizer (date semantics + mod-11 / exception)
presidio-analyzer/tests/test_cz_bank_account_recognizer.py Unit tests for CzBankAccountRecognizer (weighted mod-11 + invalidations)
presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/czechia/cz_passport_recognizer.py New Czech passport recognizer implementation
presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/czechia/cz_id_card_recognizer.py New Czech ID card recognizer implementation
presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/czechia/cz_driver_license_recognizer.py New Czech driver license recognizer implementation
presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/czechia/cz_date_recognizer.py New Czech date notation recognizer emitting DATE_TIME
presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/czechia/cz_birth_number_recognizer.py New rodné číslo recognizer with checksum/exception handling
presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/czechia/cz_bank_account_recognizer.py New Czech bank account recognizer with validation + date-fragment invalidation
presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/czechia/init.py Czechia country-specific recognizers package marker
presidio-analyzer/presidio_analyzer/predefined_recognizers/init.py Exposed new Cz*Recognizer classes via public predefined recognizers API
presidio-analyzer/presidio_analyzer/conf/default_recognizers.yaml Added Czech recognizers to default registry config (disabled by default)
docs/supported_entities.md Documented Czechia entity types and Czech DATE_TIME coverage
docs/recipes/czech-language-support/spacy_en_cs.yaml Bilingual EN+CS spaCy engine config (en_core_web_lg + xx_ent_wiki_sm)
docs/recipes/czech-language-support/README.md Usage recipe and guidance for Czech-language pipelines
CHANGELOG.md Added unreleased entry describing Czech recognizers + recipe

Comment on lines +136 to +153
def _resolve_conflicts(results: List[RecognizerResult]) -> List[RecognizerResult]:
"""
Keep the highest-scoring result on overlapping spans.

Mirrors the score-based conflict resolution presidio-anonymizer
applies before operating on the text (higher score wins; on equal
scores the longer, earlier span wins).
"""
ordered = sorted(
results, key=lambda r: (-r.score, -(r.end - r.start), r.start)
)
kept: List[RecognizerResult] = []
for result in ordered:
if all(
result.end <= other.start or result.start >= other.end for other in kept
):
kept.append(result)
return sorted(kept, key=lambda r: r.start)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants