Auditing Subgroup Fairness
An aggregate pass can hide a group the model fails. For de-identification that
failure has a name: under-protection — PHI that leaks more often for one
demographic group than another. openmed.eval.fairness_report slices leakage and
recall by gold-span group so disparities surface before deployment, not after a
breach.
When to use this skill
- You want per-subgroup recall and leakage for a de-id or NER model.
- You suspect (or must rule out) that one group is under-protected.
- You need disparity numbers for a clinical AI governance review.
- You need to document which subgroups you couldn't evaluate (the data gap).
What it measures
For each surrogate group fairness_report returns:
- leakage_rate — fraction of that group's gold PHI characters left exposed
(the de-id harm metric).
- recall — fraction of that group's gold spans detected.
- leakage_disparity —
max - min leakage across groups (the gap to close).
- worst_group / worst_group_leakage — the most-failed group.
Group membership comes from a group tag in each gold span's metadata (keys
group, demographic_group, or surrogate_group); ungrouped spans fall into
unspecified.
Quick start
from openmed.eval import fairness_report
# Gold fixtures must tag spans with a surrogate group, e.g.
# {"start": 4, "end": 12, "label": "PERSON", "metadata": {"group": "female"}}
fair = fairness_report(
"OpenMed/Privacy-PII-Detection",
"golden", # named suite, or pass a list of fixtures
device="cpu",
)
print("leakage disparity:", fair.leakage_disparity)
print("worst group :", fair.worst_group, fair.worst_group_leakage)
for group, m in sorted(fair.per_group.items()):
print(f" {group:14s} recall={m.recall:.3f} leakage={m.leakage_rate:.4f}")
# Under-protection alarm: any group leaking more than the rest.
LEAKAGE_GAP_LIMIT = 0.0 # leakage-first: ideally zero leakage everywhere
assert fair.leakage_disparity <= LEAKAGE_GAP_LIMIT or fair.worst_group_leakage == 0
FairnessReport.to_dict() is JSON-ready and PHI-free — drop it straight into a
model card.
Workflow
- Tag the gold corpus by group. Add a synthetic
group to each PHI span's
metadata (sex, age band, race/ethnicity surrogate). Use synthetic surrogates,
not real protected attributes (see building-gold-corpus).
- Run
fairness_report on the model + suite.
- Read leakage first, recall second. For de-id, a group with higher leakage
is under-protected — that is the headline finding.
- Compute the disparity (
leakage_disparity) and locate worst_group.
Equalized-odds framing: equal true-positive (recall) and equal leakage
across groups.
- Document the gap. If race/ethnicity surrogates are absent, report that the
audit could not cover them — most clinical NLP studies omit race entirely, so
silence is the default failure mode, not equity.
- Feed it forward. Put per-group numbers and the gap into the model card and
the governance review.
Hand-off to / from OpenMed
- From
building-gold-corpus: supplies group-tagged synthetic fixtures.
- From
evaluating-with-leakage-gates: an aggregate RELEASABLE decision
should be paired with this audit — overall pass, subgroup fail is exactly the
trap this catches.
- To
authoring-model-cards: FairnessReport.to_dict() fills the
quantitative-analysis / subgroup section.
- Pairs with
benchmarking-clinical-ner: same run, different slice (label vs
group).
Edge cases & gotchas
- Under-protection is the de-id harm; lead with leakage. A group with equal
recall but higher leakage is still failed.
- The race documentation gap is the norm. Most clinical NLP corpora don't
record race/ethnicity, so most fairness audits silently can't measure it.
Report the absence explicitly — don't let missing data read as parity.
unspecified is not a real group. A pile of spans in unspecified means
your gold isn't tagged; fix the corpus before trusting the disparity.
- Small groups give noisy rates. Report span counts (
span_count,
total_chars) alongside rates; a 1-of-2 leak isn't a 50% population rate.
- Synthetic surrogates only. Never store real protected attributes in eval
fixtures; use fabricated group labels for slicing.
- Disparity ≈ 0 with high leakage everywhere is not "fair". Equal failure is
still failure — check absolute leakage, not just the gap.
Standards & references
1---2name: auditing-subgroup-fairness3description: Audit an OpenMed NER or de-identification model for performance disparities across demographic subgroups (sex, age band, race/ethnicity when available) using openmed.eval.fairness_report. Use when the user wants per-subgroup recall and leakage, wants to check whether de-identification under-protects a group, wants to surface a documentation gap where subgroup data is missing, or needs equalized-odds-style disparity numbers for a clinical model. Trigger on "fairness", "subgroup", "bias audit", "disparity", "equalized odds", "under-protected group", "per-group recall", or "STANDING Together" for an OpenMed model.4license: Apache-2.05---67# Auditing Subgroup Fairness89An aggregate pass can hide a group the model fails. For de-identification that10failure has a name: **under-protection** — PHI that leaks more often for one11demographic group than another. `openmed.eval.fairness_report` slices leakage and12recall by gold-span group so disparities surface before deployment, not after a13breach.1415## When to use this skill1617- You want per-subgroup recall and leakage for a de-id or NER model.18- You suspect (or must rule out) that one group is under-protected.19- You need disparity numbers for a clinical AI governance review.20- You need to document *which* subgroups you couldn't evaluate (the data gap).2122## What it measures2324For each surrogate group `fairness_report` returns:2526- **leakage_rate** — fraction of that group's gold PHI characters left exposed27 (the de-id harm metric).28- **recall** — fraction of that group's gold spans detected.29- **leakage_disparity** — `max - min` leakage across groups (the gap to close).30- **worst_group** / **worst_group_leakage** — the most-failed group.3132Group membership comes from a `group` tag in each gold span's `metadata` (keys33`group`, `demographic_group`, or `surrogate_group`); ungrouped spans fall into34`unspecified`.3536## Quick start3738```python39from openmed.eval import fairness_report4041# Gold fixtures must tag spans with a surrogate group, e.g.42# {"start": 4, "end": 12, "label": "PERSON", "metadata": {"group": "female"}}43fair = fairness_report(44 "OpenMed/Privacy-PII-Detection",45 "golden", # named suite, or pass a list of fixtures46 device="cpu",47)4849print("leakage disparity:", fair.leakage_disparity)50print("worst group :", fair.worst_group, fair.worst_group_leakage)51for group, m in sorted(fair.per_group.items()):52 print(f" {group:14s} recall={m.recall:.3f} leakage={m.leakage_rate:.4f}")5354# Under-protection alarm: any group leaking more than the rest.55LEAKAGE_GAP_LIMIT = 0.0 # leakage-first: ideally zero leakage everywhere56assert fair.leakage_disparity <= LEAKAGE_GAP_LIMIT or fair.worst_group_leakage == 057```5859`FairnessReport.to_dict()` is JSON-ready and PHI-free — drop it straight into a60model card.6162## Workflow63641. **Tag the gold corpus by group.** Add a synthetic `group` to each PHI span's65 `metadata` (sex, age band, race/ethnicity surrogate). Use synthetic surrogates,66 not real protected attributes (see `building-gold-corpus`).672. **Run `fairness_report`** on the model + suite.683. **Read leakage first, recall second.** For de-id, a group with higher leakage69 is under-protected — that is the headline finding.704. **Compute the disparity** (`leakage_disparity`) and locate `worst_group`.71 Equalized-odds framing: equal true-positive (recall) *and* equal leakage72 across groups.735. **Document the gap.** If race/ethnicity surrogates are absent, report that the74 audit could not cover them — most clinical NLP studies omit race entirely, so75 silence is the default failure mode, not equity.766. **Feed it forward.** Put per-group numbers and the gap into the model card and77 the governance review.7879## Hand-off to / from OpenMed8081- **From** `building-gold-corpus`: supplies group-tagged synthetic fixtures.82- **From** `evaluating-with-leakage-gates`: an aggregate `RELEASABLE` decision83 should be paired with this audit — overall pass, subgroup fail is exactly the84 trap this catches.85- **To** `authoring-model-cards`: `FairnessReport.to_dict()` fills the86 quantitative-analysis / subgroup section.87- **Pairs with** `benchmarking-clinical-ner`: same run, different slice (label vs88 group).8990## Edge cases & gotchas9192- **Under-protection is the de-id harm; lead with leakage.** A group with equal93 recall but higher leakage is still failed.94- **The race documentation gap is the norm.** Most clinical NLP corpora don't95 record race/ethnicity, so most fairness audits silently can't measure it.96 Report the absence explicitly — don't let missing data read as parity.97- **`unspecified` is not a real group.** A pile of spans in `unspecified` means98 your gold isn't tagged; fix the corpus before trusting the disparity.99- **Small groups give noisy rates.** Report span counts (`span_count`,100 `total_chars`) alongside rates; a 1-of-2 leak isn't a 50% population rate.101- **Synthetic surrogates only.** Never store real protected attributes in eval102 fixtures; use fabricated group labels for slicing.103- **Disparity ≈ 0 with high leakage everywhere is not "fair".** Equal failure is104 still failure — check absolute leakage, not just the gap.105106## Standards & references107108- STANDING Together — reporting standards for health-dataset diversity &109 documentation: https://www.datadiversity.org/110- Hardt, Price, Srebro, *Equality of Opportunity in Supervised Learning*111 (equalized odds): https://arxiv.org/abs/1610.02413112- Chen et al., *Ethical ML in Health Care* (subgroup performance in clinical NLP):113 https://doi.org/10.1146/annurev-biodatasci-092820-114757114- OpenMed source of truth: `openmed/eval/fairness.py`115 (`fairness_report`, `FairnessReport`, `FairnessGroupMetrics`).