Extracting SDOH and Mapping to ICD-10-CM Z-Codes
Social determinants of health (SDOH) — the conditions in which people live,
work, and age — drive an estimated 80% of health outcomes, yet they live almost
entirely in free-text narrative. Multiple chart-review studies find SDOH
documented in notes but coded with a Z-code under ~2% of the time. The
information is there; the structured signal is not. This skill recovers it: run
OpenMed NER over de-identified notes, then map the resulting spans to the
ICD-10-CM Z55–Z65 family.
When to use
- A note clearly describes a social risk ("lives in her car", "skips meals to
afford insulin", "no ride to dialysis") and you want a coded, queryable signal.
- You are building health-equity dashboards, risk stratification, or
closed-loop referral feeds and need SDOH as discrete data.
- You want to reconcile what the chart says against what was coded, and flag
Z-code gaps for a coder or care team to confirm.
This is a decision-support step. It proposes Z-codes; a human assigns them.
SDOH coding is sensitive — never expose individual SDOH inferences outside the
care/coding workflow, and never feed them to coverage or pricing decisions.
Quick start
De-identify first, run NER, then map spans to Z-codes:
import openmed
from sdoh_zcode_map import SDOH_ZCODES # see references/sdoh_zcode_map.md
note = (
"62F with CHF. Reports she lost her apartment last month and is "
"staying in a shelter. Often runs out of food before month-end. "
"No car; misses appointments because the bus does not run to clinic."
)
# 1) Strip PHI before any downstream processing or storage.
deid = openmed.deidentify(note, method="replace", policy="hipaa_safe_harbor")
# 2) Run clinical NER. Use an SDOH/clinical model from the registry; discover
# available keys with openmed.get_models_by_category(...).
result = openmed.analyze_text(deid.text, output_format="dict")
# 3) Map each entity span to a candidate Z-code.
for ent in result["entities"]:
code = SDOH_ZCODES.get(ent["label"].lower())
if code:
print(f"{ent['text']!r:40} {ent['label']:18} -> {code}")
analyze_text returns entities shaped as
{"text", "label", "confidence", "start", "end", "metadata"}. The start/end
offsets index into the text you passed in, so you can anchor every suggested
Z-code back to its exact source span for human review.
Workflow
- De-identify the note with
openmed.deidentify (HIPAA Safe Harbor or a
stricter policy). SDOH text is dense with PHI (addresses, employer names).
- Extract entities with
openmed.analyze_text. Pick a model whose label
set covers social concepts; if your model only emits clinical findings, run a
second pass with a zero-shot model (openmed zero) using SDOH labels such as
housing_instability, food_insecurity, unemployment,
transportation_barrier, social_isolation, financial_strain.
- Map spans to Z-codes using a curated lookup keyed by label
(
references/sdoh_zcode_map.md). Keep the span offsets and the model
confidence on every suggestion.
- Stage for confirmation. Emit
(span, label, suggested_code, confidence)
tuples for a coder or the Gravity Project pipeline to accept or reject. Do not
auto-bill a Z-code from an inference alone.
- Normalize to value sets. Align labels to the Gravity Project SDOH
domains so codes are interoperable with FHIR (
Condition, Observation,
Goal) and USCDI v3 SDOH elements.
Z-code families you will hit most (ICD-10-CM Z55–Z65)
| Domain |
Range |
Example |
| Education / literacy |
Z55 |
Z55.0 illiteracy |
| Employment |
Z56 |
Z56.0 unemployment |
| Occupational exposure |
Z57 |
— |
| Housing / economic |
Z59 |
Z59.0 homelessness, Z59.41 food insecurity, Z59.82 transportation insecurity |
| Social environment |
Z60 |
Z60.2 living alone, Z60.4 social exclusion |
| Upbringing |
Z62 |
— |
| Family / support circumstances |
Z63 |
Z63.4 disappearance/death of family member |
| Psychosocial circumstances |
Z64–Z65 |
Z65.1 imprisonment |
The full curated label→code table lives in
references/sdoh_zcode_map.md.
Hand-off to / from OpenMed
- From OpenMed: this skill consumes
openmed.analyze_text(...) output
(PredictionResult dict). Each entity["start"]/["end"] anchors a Z-code
suggestion to source text.
- To OpenMed: always run
openmed.deidentify upstream so no raw PHI reaches
the SDOH store, logs, or coder queue.
- Onward: emit suggestions into a FHIR
Condition/Observation with the
Z-code as code.coding (system http://hl7.org/fhir/sid/icd-10-cm). OpenMed's
openmed.clinical.exporters.fhir helpers (to_bundle, to_operation_outcome)
assemble the envelope; ICD-10-CM itself is public-domain in the US release.
Edge cases & gotchas
- Negation and history. "Denies food insecurity" or "previously homeless,
now housed" must not produce an active Z-code. Run negation/temporality
resolution (
openmed.clinical, resolving-clinical-context) before mapping.
- Hypotheticals and screening prompts. Template text ("Do you have stable
housing?") and family-member SDOH ("his mother is unhoused") are common false
positives — check the subject and modality.
- One span, one domain. Do not stack multiple Z-codes onto one phrase; map
to the most specific single code and let the coder add others.
- Granularity drift. ICD-10-CM adds SDOH codes most fiscal years (e.g.
Z59.4x food, Z59.82 transportation). Pin your code set to a release year and
re-validate annually.
- Do not infer protected attributes. Surface only what the note states;
never derive race, immigration status, or income bracket as an SDOH "finding".
- Restricted terminology. SNOMED CT SDOH refsets and LOINC SDOH panels are
licensed separately — OpenMed does not bundle them; load the user's own copy
out-of-process if you cross-map beyond ICD-10-CM.
Standards & references
1---2name: extracting-sdoh3description: Extracts social determinants of health (SDOH) — housing instability, food insecurity, unemployment, transportation barriers, social isolation, financial strain — from clinical narrative and maps the spans to ICD-10-CM Z-codes (Z55–Z65). Use after running OpenMed NER when the user wants SDOH surfacing, Z-code suggestion, health-equity analytics, or to recover SDOH that is documented in free text but not coded. Pairs with OpenMed analyze_text output. Standards: ICD-10-CM Z55–Z65, Gravity Project value sets, n2c2 2022 SDOH track. Trigger keywords: SDOH, social determinants, Z-codes, housing, food insecurity, health equity, Gravity Project.4license: Apache-2.05---67# Extracting SDOH and Mapping to ICD-10-CM Z-Codes89Social determinants of health (SDOH) — the conditions in which people live,10work, and age — drive an estimated 80% of health outcomes, yet they live almost11entirely in free-text narrative. Multiple chart-review studies find SDOH12**documented in notes but coded with a Z-code under ~2% of the time**. The13information is there; the structured signal is not. This skill recovers it: run14OpenMed NER over de-identified notes, then map the resulting spans to the15ICD-10-CM **Z55–Z65** family.1617## When to use1819- A note clearly describes a social risk ("lives in her car", "skips meals to20 afford insulin", "no ride to dialysis") and you want a coded, queryable signal.21- You are building health-equity dashboards, risk stratification, or22 closed-loop referral feeds and need SDOH as discrete data.23- You want to reconcile what the chart *says* against what was *coded*, and flag24 Z-code gaps for a coder or care team to confirm.2526This is a **decision-support** step. It proposes Z-codes; a human assigns them.27SDOH coding is sensitive — never expose individual SDOH inferences outside the28care/coding workflow, and never feed them to coverage or pricing decisions.2930## Quick start3132De-identify first, run NER, then map spans to Z-codes:3334```python35import openmed36from sdoh_zcode_map import SDOH_ZCODES # see references/sdoh_zcode_map.md3738note = (39 "62F with CHF. Reports she lost her apartment last month and is "40 "staying in a shelter. Often runs out of food before month-end. "41 "No car; misses appointments because the bus does not run to clinic."42)4344# 1) Strip PHI before any downstream processing or storage.45deid = openmed.deidentify(note, method="replace", policy="hipaa_safe_harbor")4647# 2) Run clinical NER. Use an SDOH/clinical model from the registry; discover48# available keys with openmed.get_models_by_category(...).49result = openmed.analyze_text(deid.text, output_format="dict")5051# 3) Map each entity span to a candidate Z-code.52for ent in result["entities"]:53 code = SDOH_ZCODES.get(ent["label"].lower())54 if code:55 print(f"{ent['text']!r:40} {ent['label']:18} -> {code}")56```5758`analyze_text` returns entities shaped as59`{"text", "label", "confidence", "start", "end", "metadata"}`. The `start`/`end`60offsets index into the text you passed in, so you can anchor every suggested61Z-code back to its exact source span for human review.6263## Workflow64651. **De-identify** the note with `openmed.deidentify` (HIPAA Safe Harbor or a66 stricter policy). SDOH text is dense with PHI (addresses, employer names).672. **Extract entities** with `openmed.analyze_text`. Pick a model whose label68 set covers social concepts; if your model only emits clinical findings, run a69 second pass with a zero-shot model (`openmed zero`) using SDOH labels such as70 `housing_instability`, `food_insecurity`, `unemployment`,71 `transportation_barrier`, `social_isolation`, `financial_strain`.723. **Map spans to Z-codes** using a curated lookup keyed by label73 (`references/sdoh_zcode_map.md`). Keep the **span offsets** and the model74 `confidence` on every suggestion.754. **Stage for confirmation.** Emit `(span, label, suggested_code, confidence)`76 tuples for a coder or the Gravity Project pipeline to accept or reject. Do not77 auto-bill a Z-code from an inference alone.785. **Normalize to value sets.** Align labels to the **Gravity Project** SDOH79 domains so codes are interoperable with FHIR (`Condition`, `Observation`,80 `Goal`) and USCDI v3 SDOH elements.8182### Z-code families you will hit most (ICD-10-CM Z55–Z65)8384| Domain | Range | Example |85| --- | --- | --- |86| Education / literacy | Z55 | Z55.0 illiteracy |87| Employment | Z56 | Z56.0 unemployment |88| Occupational exposure | Z57 | — |89| Housing / economic | Z59 | Z59.0 homelessness, Z59.41 food insecurity, Z59.82 transportation insecurity |90| Social environment | Z60 | Z60.2 living alone, Z60.4 social exclusion |91| Upbringing | Z62 | — |92| Family / support circumstances | Z63 | Z63.4 disappearance/death of family member |93| Psychosocial circumstances | Z64–Z65 | Z65.1 imprisonment |9495The full curated label→code table lives in96[references/sdoh_zcode_map.md](references/sdoh_zcode_map.md).9798## Hand-off to / from OpenMed99100- **From OpenMed:** this skill consumes `openmed.analyze_text(...)` output101 (`PredictionResult` dict). Each `entity["start"]/["end"]` anchors a Z-code102 suggestion to source text.103- **To OpenMed:** always run `openmed.deidentify` upstream so no raw PHI reaches104 the SDOH store, logs, or coder queue.105- **Onward:** emit suggestions into a FHIR `Condition`/`Observation` with the106 Z-code as `code.coding` (system `http://hl7.org/fhir/sid/icd-10-cm`). OpenMed's107 `openmed.clinical.exporters.fhir` helpers (`to_bundle`, `to_operation_outcome`)108 assemble the envelope; ICD-10-CM itself is public-domain in the US release.109110## Edge cases & gotchas111112- **Negation and history.** "Denies food insecurity" or "previously homeless,113 now housed" must not produce an active Z-code. Run negation/temporality114 resolution (`openmed.clinical`, `resolving-clinical-context`) before mapping.115- **Hypotheticals and screening prompts.** Template text ("Do you have stable116 housing?") and family-member SDOH ("his mother is unhoused") are common false117 positives — check the subject and modality.118- **One span, one domain.** Do not stack multiple Z-codes onto one phrase; map119 to the most specific single code and let the coder add others.120- **Granularity drift.** ICD-10-CM adds SDOH codes most fiscal years (e.g.121 Z59.4x food, Z59.82 transportation). Pin your code set to a release year and122 re-validate annually.123- **Do not infer protected attributes.** Surface only what the note states;124 never derive race, immigration status, or income bracket as an SDOH "finding".125- **Restricted terminology.** SNOMED CT SDOH refsets and LOINC SDOH panels are126 licensed separately — OpenMed does not bundle them; load the user's own copy127 out-of-process if you cross-map beyond ICD-10-CM.128129## Standards & references130131- ICD-10-CM official guidelines, Z55–Z65 SDOH codes (CDC/CMS, public domain):132 https://www.cdc.gov/nchs/icd/icd-10-cm.htm133- Gravity Project (HL7 SDOH Clinical Care value sets & FHIR IG):134 https://www.hl7.org/gravity/ and https://confluence.hl7.org/display/GRAV135- n2c2 2022 Track 2 — SDOH extraction shared task (Social History Annotation136 Corpus): https://n2c2.dbmi.hms.harvard.edu/137- CMS ICD-10-CM Z-code SDOH resources:138 https://www.cms.gov/files/document/zcodes-infographic.pdf139- USCDI SDOH data classes: https://www.healthit.gov/isa/uscdi-data-class/sdoh