Social Determinants of Health (SDoH) in RWE
The non-medical, contextual and individual social conditions (economic stability, education, healthcare access, neighborhood environment, social context) operationalized in real-world studies most often as area-level deprivation indices linked to patient geography, or as individual-level screening/Z-code data, and used as a confounder, mediator, effect modifier, or exposure of interest.
In plain language
Social determinants of health (SDoH) are the non-medical conditions in which people are born, grow, and live — things like income, education, and neighborhood quality — that shape whether someone gets sick or stays well. In real-world studies these factors usually cannot be measured directly for each person, so researchers attach a neighborhood-level score (called an area-level deprivation index) to a patient's ZIP code and use that score as a stand-in. Because one neighborhood score is assigned to every patient who lives there, you always inherit some error: a high-deprivation ZIP contains both very poor and working-class households, and you can never know from the score alone which type of person you are actually studying. The most important judgment call is deciding what role SDoH plays in your analysis — if it causes both who gets treated and the outcome, you must account for it; if it is part of how the treatment works, accounting for it would actually hide the effect you are trying to measure.
In real-world evidence, "SDoH" is not one variable but a family of measurement choices, and the methodological work is almost entirely in operationalization. The dominant pattern in US claims and EHR is area-level linkage: geocode the patient's residential address (or, in claims, the member ZIP) to a small geographic unit (census block group or tract), then attach a published composite — the Area Deprivation Index (ADI), the CDC/ATSDR Social Vulnerability Index (SVI), the Robert Graham Center Social Deprivation Index (SDI), or a study-built American Community Survey (ACS) score — and collapse it to a rank (national percentile, state decile, tertile). The less common but richer pattern is individual-level capture: standardized screening (PRAPARE, the CMS Accountable Health Communities HRSN tool) or ICD-10 Z55–Z65 social Z-codes. Whether SDoH belongs in the model at all depends on its causal role, which is the single most consequential and most often-botched decision.
Core conceptual distinction
SDoH can occupy three mutually exclusive causal positions for a given exposure–outcome contrast, and the correct handling is opposite across them. (1) Confounder — neighborhood deprivation drives both who initiates a therapy (access, formulary, cost-sharing) and the outcome (e.g., cardiovascular events): then SDoH belongs in the propensity score or outcome model, and omitting it leaves residual confounding that age/sex/race cannot absorb. (2) Mediator — the exposure operates through a social condition (a copay-assistance program that works by relieving financial strain; a care-access intervention that changes where a patient lives or gets care): conditioning on the mediator blocks the very effect you are trying to estimate (over-adjustment) and can open a collider path. (3) Effect modifier — the treatment effect itself differs by deprivation stratum (a digital-adherence tool that helps only patients with stable housing): here SDoH belongs in an interaction term or a stratified/subgroup estimand, not merely as an additive adjustment. The estimand must name which role SDoH plays before analysis; "we adjusted for ADI" is uninterpretable until the DAG says whether ADI is a backdoor confounder or a pathway variable.
Equally fundamental is the ecological vs individual distinction. An area-level index is a property of a place, not a person; assigning a tract-level ADI to an individual is a deliberate proxy that imports ecological-fallacy error — within-area heterogeneity is enormous, and the bias is non-differential only if measurement error is unrelated to exposure, which is not guaranteed. The area measure is a contextual construct (the effect of living in a deprived neighborhood), not a substitute for an individual's income or food security.
Pros, cons, and trade-offs
(specific and comparative). - Area-level index (ADI/SVI/SDI) vs adjusting on demographics only (age/sex/race): area indices capture access, economic, and environmental drivers of disparities that demographics miss, improving confounding control and enabling equity stratification — and they are available at population scale from ZIP/address alone. Cost: substantial measurement error (a person is not their tract), ecological fallacy, and the temptation to "control away" disparities that are the object of study. Prefer area indices when SDoH is a confounder and individual data are unavailable, which is the usual claims situation. - Area-level vs individual-level (PRAPARE / Z-codes): individual screening measures the actual social need with far less ecological error and supports needs-based targeting. Cost: capture is sparse and differential — Z55–Z65 coding is driven by which systems screen and bill, so "no Z-code" overwhelmingly means "not screened," not "no need," and the completeness correlates with the very deprivation being measured. Prefer individual-level when reliably captured (integrated delivery systems with screening mandates); otherwise area-level is the more honest default. - ADI vs SVI vs SDI: ADI (Singh construction, 17 ACS variables, distributed via the Neighborhood Atlas as national percentiles and state deciles) is the de facto RWE standard but is sensitive to housing-cost variables that skew rankings in high-cost metros; SVI (4 themes, designed for disaster preparedness) emphasizes vulnerability and minority/language; SDI (7 ACS variables) was built explicitly for healthcare utilization research. They are correlated but not interchangeable — pre-specify one and report sensitivity to an alternative index.
When to use
(decision rules). Use SDoH operationalization when (a) deprivation/access plausibly confounds the exposure–outcome relation and is not captured by clinical covariates; (b) the analysis is explicitly about disparities or equity (FDA Diversity Action Plans, HTA equity-weighting); (c) prior evidence shows the treatment effect is modified by social context; or (d) SDoH is the exposure of interest (impact of neighborhood deprivation on adherence, persistence, or access). Geocode to the finest reliable unit (census block group > tract > ZIP), use the index-date address (not the most-recent), and rank within the appropriate reference (national vs state) for the question.
When NOT to use — and when it is actively misleading or dangerous
- When SDoH is a mediator of the exposure effect. Adjusting for neighborhood deprivation when evaluating a copay-assistance or care-navigation intervention removes part of the causal effect (over-adjustment) and can induce collider bias if the mediator shares a common cause with the outcome. Diagnose with the DAG, not reflex. - When the goal is to "explain away" a disparity. Adjusting an exposure–disparity contrast for SDoH can make a real, actionable inequity disappear into a coefficient — statistically tidy, ethically and scientifically wrong if SDoH is on the causal pathway from a structural exposure to the outcome. - Z-code or screening completeness is differential. Treating absent Z55–Z65 as "no social need" misclassifies the majority of patients and biases toward the screened (often sicker, more engaged) population; an unadjusted individual-SDoH analysis here is worse than an honest area-level proxy. - Fine geography on small cells. Block-group linkage on rare outcomes risks re-identification and unstable index values; suppress or coarsen per the data-use agreement.
Data-source operational depth
(by source). - Claims (FFS, MA, commercial): the only SDoH signal is geographic — member ZIP (often ZIP5, sometimes ZIP9) linked to an index. Failure modes: ZIP5 is a postal unit that straddles multiple census tracts, so a ZIP5→tract assignment must use a population-weighted crosswalk (e.g., HUD USPS ZIP–tract) or be flagged ambiguous; PO-box and "unique" (single large-recipient) ZIPs have no meaningful residential geography and must be dropped; addresses are captured at enrollment and go stale — Medicare Advantage and commercial files often carry an enrollment-era address that no longer reflects residence during follow-up, so use the index-date address and treat mid-follow-up moves as a sensitivity analysis. MA-only person-time additionally lacks complete FFS claims, compounding any utilization-based SDoH proxy. No individual social need is observable in claims without supplemental linkage. - EHR: can carry both a geocodable address (richer than claims) and individual screening / Z-codes. The trap is differential capture — Z55–Z65 and PRAPARE fields appear only when a site screens and documents, so completeness is a function of the health system, not the patient, and visit-driven EHR means patients who leave the system are differentially missing. Treat absent SDoH fields as missing-not-at-random; do not impute "0 = no need." - Registry: may collect structured social variables (insurance, education) more completely than claims but rarely at fine geography; link to claims/ACS to add a contextual index and to a mortality source for complete follow-up. - Linked claims–EHR–census: the ideal substrate — individual screening + geocoded contextual index + complete enrollment — but linkage selects the linkable subset (often more urban, more insured), and address/geocode quality must be reconciled before assignment. Report the linked subset's representativeness.
Worked claims example
Question: does residing in a high-deprivation neighborhood predict 12-month non-persistence to a newly initiated chronic therapy, among adults with continuous enrollment? (1) Cohort: first qualifying fill = index date; require continuous medical+pharmacy enrollment for the baseline lookback so covariates are observable. (2) Geographic linkage: take the member residential ZIP as of the index date (not the latest on file). If ZIP9 is present, map ZIP9 → census tract directly; if only ZIP5, apply a population-weighted ZIP5→tract crosswalk and, when one tract holds the clear majority of the ZIP's population, assign it, otherwise flag the member as geographically ambiguous and exclude from the primary analysis (retain for a sensitivity check). Drop PO-box and non-residential ZIPs. (3) Index value: look up the tract's ADI national percentile (1–100) from the Neighborhood Atlas, then cut into tertiles (or use the published decile). (4) Outcome: non-persistence = a gap > 60 days with no fill (last `days_supply` end + grace), measured over the 12-month follow-up using `fill_date` and `days_supply`. (5) Model: because deprivation here is the exposure, do not adjust for downstream mediators (e.g., out-of-pocket cost on the causal path); adjust only for true confounders measured at baseline (age, sex, plan type, comorbidity, calendar year). (6) Report the gradient across tertiles with a sensitivity analysis substituting SVI for ADI and re-running with the most-recent (vs index-date) address to bound address-staleness bias.
Worked example
Scenario
A researcher wants to know whether patients newly started on a blood-pressure medication who live in high-deprivation neighborhoods are less likely to still be taking it 12 months later. Because the claims database only has each patient's ZIP code — not their actual income or education — the researcher uses ZIP-code-level SDoH proxies as area-level stand-ins. The table below shows five patients at treatment start (their index date), the neighborhood deprivation score attached to their ZIP, and whether they were still on medication at 12 months.
Dataset
Five patients at treatment start, with area-level SDoH proxies linked by ZIP code. ADI national percentile runs 1-100; higher = more deprived. Neighborhood deprivation index, median household income, and percent with a college degree all come from census data attached to the patient's ZIP — not from asking the patient directly.
| person_id | index_date | zip_code | adi_national_pct | neighborhood_median_income_usd | pct_college_degree | still_on_med_12mo |
|---|---|---|---|---|---|---|
| 1001 | 2023-02-01 | 02492 | 22 | 98000 | 64 | Yes |
| 1002 | 2023-02-15 | 02136 | 68 | 52000 | 31 | No |
| 1003 | 2023-03-01 | 02136 | 68 | 52000 | 31 | Yes |
| 1004 | 2023-03-10 | 02301 | 81 | 41000 | 19 | No |
| 1005 | 2023-04-01 | 02301 | 81 | 41000 | 19 | No |
Steps
The researcher pulls the residential ZIP code recorded for each patient at their index date (treatment start) — not their most-recent address on file, which may have changed.
Each ZIP is linked to an area-level deprivation score: here the ADI national percentile, plus two underlying census variables (median income and college degree rate) that help show what the ADI is capturing.
Patients 1002 and 1003 live in the same ZIP (02136) and therefore receive the exact same three area scores — but patient 1003 stayed on medication while patient 1002 did not, showing that the shared area score cannot distinguish individual circumstances.
This is the ecological fallacy in action: within a single ZIP code, one patient persisted and one did not, yet both carry identical area-level SDoH values.
Patients 1004 and 1005 live in the highest-deprivation ZIP (ADI 81) and both stopped the medication — suggesting that high neighborhood deprivation may be associated with lower persistence, but the sample is tiny and many other factors could explain this.
Because neighborhood deprivation likely affects both who fills a new prescription (access, cost) and whether they keep filling it (adherence, persistence), ADI acts as a confounder in this study and should be included as a covariate in the analysis model.
If instead the study were evaluating a copay-assistance program that works specifically by reducing the financial strain caused by living in a deprived area, then neighborhood deprivation would sit on the pathway from the program to its effect — adjusting for ADI in that case would block the very mechanism you are trying to measure (over-adjustment).
Result
In this five-patient illustration, 2 of 2 patients (100%) in the highest-deprivation ZIP stopped medication versus 1 of 2 (50%) in the mid-deprivation ZIP and 0 of 1 (0%) in the low-deprivation ZIP — a gradient consistent with deprivation as a confounder of medication persistence. The key limitation is that all three patients in ZIPs 02136 and 02301 share the same area score regardless of their own income or education (ecological fallacy), so the ADI captures neighborhood context, not individual need. Any real analysis would require hundreds of patients and formal adjustment in a regression or propensity model.
Runnable example
python implementation
Operationalize area-level SDoH (ADI national percentile -> tertile) from claims-style inputs. Required inputs (already cleaned): members : person_id, index_date (datetime), resid_zip (str, ZIP5 or ZIP9), zip_type in {'standard','pobox','unique'} zip_xwalk:...
import pandas as pd
import numpy as np
def assign_area_sdoh(members: pd.DataFrame,
zip_xwalk: pd.DataFrame,
adi: pd.DataFrame,
majority_threshold: float = 0.50) -> pd.DataFrame:
m = members.copy()
# Drop non-residential ZIPs (PO boxes, unique/large-recipient ZIPs have no residential geography).
m = m[m["zip_type"] == "standard"].copy()
m["zip5"] = m["resid_zip"].str[:5]
# Population-weighted ZIP5 -> tract: keep the tract holding the largest population share of the ZIP.
xw = zip_xwalk.sort_values(["zip5", "pop_weight"], ascending=[True, False])
top = xw.groupby("zip5", as_index=False).first() # majority tract + its weight
top = top.rename(columns={"census_tract": "census_tract", "pop_weight": "majority_weight"})
m = m.merge(top[["zip5", "census_tract", "majority_weight"]], on="zip5", how="left")
# Flag ZIP5s where no tract holds a clear majority of the population (geographically ambiguous).
m["ambiguous"] = m["majority_weight"].fillna(0) < majority_threshold
# Attach ADI national percentile and cut into deprivation tertiles (1=least, 3=most deprived).
m = m.merge(adi[["census_tract", "adi_natrank"]], on="census_tract", how="left")
primary = m.loc[~m["ambiguous"] & m["adi_natrank"].notna()].copy()
primary["adi_tertile"] = pd.qcut(primary["adi_natrank"], q=3, labels=[1, 2, 3]).astype("Int64")
out = m.merge(primary[["person_id", "adi_tertile"]], on="person_id", how="left")
return out[["person_id", "index_date", "census_tract", "adi_natrank", "adi_tertile", "ambiguous"]]r implementation
Area-level SDoH assignment (ADI national percentile -> tertile) with data.table. Inputs mirror the Python version: members : person_id, index_date (Date), resid_zip (char), zip_type in {'standard','pobox','unique'} zip_xwalk: zip5, census_tract, pop_weight...
library(data.table)
assign_area_sdoh <- function(members, zip_xwalk, adi, majority_threshold = 0.50) {
setDT(members); setDT(zip_xwalk); setDT(adi)
# Keep residential ZIPs only; PO-box / unique ZIPs have no residential geography.
m <- members[zip_type == "standard"]
m[, zip5 := substr(resid_zip, 1L, 5L)]
# Population-weighted ZIP5 -> tract: majority tract per ZIP5.
setorder(zip_xwalk, zip5, -pop_weight)
top <- zip_xwalk[, .SD[1L], by = zip5][, .(zip5, census_tract, majority_weight = pop_weight)]
m <- merge(m, top, by = "zip5", all.x = TRUE)
m[, ambiguous := fifelse(is.na(majority_weight) | majority_weight < majority_threshold, TRUE, FALSE)]
# Attach ADI national percentile; tertile only on unambiguous, non-missing rows (1=least, 3=most deprived).
m <- merge(m, adi[, .(census_tract, adi_natrank)], by = "census_tract", all.x = TRUE)
ok <- !m$ambiguous & !is.na(m$adi_natrank)
m[ok, adi_tertile := cut(adi_natrank,
breaks = quantile(adi_natrank, probs = seq(0, 1, 1/3), na.rm = TRUE),
labels = c(1L, 2L, 3L), include.lowest = TRUE)]
m[, .(person_id, index_date, census_tract, adi_natrank, adi_tertile, ambiguous)]
}