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.
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.
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.
openmed.deidentify (HIPAA Safe Harbor or a
stricter policy). SDOH text is dense with PHI (addresses, employer names).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.references/sdoh_zcode_map.md). Keep the span offsets and the model
confidence on every suggestion.(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.Condition, Observation,
Goal) and USCDI v3 SDOH elements.| 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.
openmed.analyze_text(...) output
(PredictionResult dict). Each entity["start"]/["end"] anchors a Z-code
suggestion to source text.openmed.deidentify upstream so no raw PHI reaches
the SDOH store, logs, or coder queue.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.openmed.clinical, resolving-clinical-context) before mapping.