← back to Unclaimed Property Platform
tests/test_cycle4_fairness.py
69 lines
"""Fairness / coverage tests for candidate generation (referenced by entity_match docstring).
The fairness property that matters at the BLOCKING stage is coverage: no name distribution
may be systematically un-indexable, or those owners become structurally un-findable (they'd
never enter a candidate set, so their property is never matched to them). This is distinct
from scoring fairness (calibration parity), which belongs to the trained production model.
These tests assert the coverage floor holds across diverse name distributions AND document
the known Soundex limitation so it can't be forgotten. Stdlib only, $0.
Run: python -m tests.test_cycle4_fairness
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from services.common.normalize import phonetic_key
from services.matching.entity_match import MatchInput, blocking_keys
# Deliberately diverse surnames — Anglo, Polish, Vietnamese, Arabic, Hispanic, hyphenated.
DIVERSE_NAMES = [
"Catherine O'Neil", "Kathryn ONeill", "Grzegorz Brzęczyszczykiewicz",
"Nguyễn Thị Hương", "محمد بن سلمان", "José García-Márquez",
"Xochitl Ramírez", "Þórunn Jónsdóttir", "李伟", "O", " ",
]
def ok(msg: str) -> None:
print(f" ✓ {msg}")
def main() -> int:
print("1) Coverage floor: every record with an address is indexable (name-key OR geo-key)")
unindexable = []
for name in DIVERSE_NAMES:
keys = blocking_keys(MatchInput(name, postal_code="12345", region="SM"))
# A record with an address is ALWAYS indexable via zip/region even when the name
# script produces no phonetic/token key (pure CJK/Arabic) — the coverage guarantee.
if not keys:
unindexable.append(name)
assert not unindexable, f"these records produced NO blocking key (un-findable): {unindexable}"
ok(f"all {len(DIVERSE_NAMES)} records indexable (incl. non-latin scripts via geo)")
print("2) A name with NO geo still needs a name-derived key when it has letters")
# A latin-script name with no address must still be indexable by a name key.
keys = blocking_keys(MatchInput("Catherine O'Neil"))
assert any(k.startswith(("ph:", "tok0:")) for k in keys), keys
ok(f"name-only record indexable via {[k for k in keys if k.startswith(('ph:','tok0:'))][:1]}")
print("3) Known limitation is real, not hidden: non-latin scripts get no phonetic key")
# This DOCUMENTS the gap the docstring warns about — Soundex is latin-only. The test
# asserts the CURRENT behavior so a future Double-Metaphone upgrade visibly changes it.
cjk = phonetic_key("李伟")
assert cjk == "", f"expected empty phonetic key for CJK today, got {cjk!r}"
# ...but such a record is STILL indexable when it has geo (see test 1), so coverage holds.
cjk_keys = blocking_keys(MatchInput("李伟", postal_code="12345"))
assert any(k.startswith("zip:") for k in cjk_keys), cjk_keys
ok("CJK name has no phonetic key today (limitation logged) but stays geo-indexable")
print("\nALL FAIRNESS/COVERAGE ASSERTIONS PASSED ✅")
return 0
if __name__ == "__main__":
raise SystemExit(main())