ASC Workshop
2026-10-26
Insurance carriers are among the top employers of data scientists (10%).
Note
These are projections and are most certainly impacted by AI and other market changes.
Note
In Utah: Intermountain Health / Select Health, University of Utah Health, Optum, HealthEquity, Regence BCBS, Utah DHHS, County Dept of Health
From seven real postings (accessed Oct 16, 2026).
Provider Data Analyst
Intermountain Health (2 postings)
Payer / Claims Analyst
Optum · USM (HEDIS)
Health Data Scientist
Cohere Health · Dandelion Health · GDIT
Common thread: SQL + a language + healthcare data fluency + explaining results to non-analysts.
Postings: Intermountain Health, Data Analyst, Statistical Senior and Data Analyst-Technical-Staff; Optum, Senior Healthcare Analytics Consultant; USM, HEDIS Data Analyst (SQL); Cohere Health, Senior Data Scientist; Dandelion Health, Research Consultant, Data Scientist; GDIT, Sr. Healthcare Data Scientist (Medicare/Medicaid). Links in Sources.
Data: Highmark claims + AHN EHR (weights)
Design: 30,458 patients with class II/III obesity; propensity-score weighting; mixed-effects models for monthly cost
Findings (2 yrs):
Barrett et al. (2025). JAMA Surgery, 160(11), 1232–1239.
Identify & stratify with claims + EHR (Dx, Rx, PHQ-9, utilization)
Cost of severity change (PMPM, 2019 → 2020)
Popkov, Barrett, et al. (2026). J Behav Health Serv Res; Popkov, Barrett, et al. (2025). J Affect Disord, 368, 41–47.
You’ll try a mini version of this with diabetes in the hands-on section.
$4.5M total savings · hospice use 84% vs 50% · Jaffe, …, Barrett (2025). J Palliat Med.
| Question | Data | Method | Finding |
|---|---|---|---|
| Is adolescent depression screening equitable? | Claims + SVI | Multilevel logistic | Higher SVI → lower screening odds; pediatricians OR = 9.6 |
| What does insomnia cost? | Claims, ~3M members | Propensity matching, GLMs | 9% have insomnia, 21% of total spend |
| Does measurement-based care reduce ED use? | Claims + registry | Negative binomial, survival | 34% lower ED visit rate (IRR = 0.66) |
Galper et al. (2025) AJPH; Karlin et al. (2024) Sleep Adv; Breitzig et al. (2026) Adm Policy Ment Health.
Every one started as a business question from a clinical or finance partner.
Highmark is a payor-provider system: an insurer (Highmark) and a health network (Allegheny Health Network). Similar to Intermountain Health.
| Claims | EHR | Public / Census | Vendor | |
|---|---|---|---|---|
| What | Bills sent to an insurer | Clinical record of care | ACS, CDC SVI, PLACES | Purchased SDOH, consumer, benchmark data |
| Grain | One row per service line | One row per encounter / lab / vital | One row per ZIP, tract, county | One row per person (if matched) |
| Owner | Payers (Highmark, Medicaid) | Providers (Epic, Cerner) | Government | Data companies |
| Strength | Complete billed picture across all providers, with cost | Clinical detail: labs, vitals, PHQ-9 | Free, broad context | Fills social gaps |
| What in the H*ck?! | No results, codes ≠ truth, reversals | Only in-network care, messy entry | Area ≠ person (ecological) | Match rates, black-box methods |
flowchart LR M["<b>members</b><br>member_id · zip · plan"] C["<b>claims</b><br>member_id · dx · $"] X["<b>mrn_crosswalk</b><br>mrn ↔ member_id"] E["<b>ehr_encounters</b><br>mrn · bmi · a1c · phq9"] Z["<b>census_zip</b><br>zip · svi · income"] V["<b>vendor_sdoh</b><br>member_id · food insecurity"] C -- member_id --> M E -- mrn --> X -- member_id --> M Z -- zip --> M V -- "member_id<br>(83% match)" --> M classDef hub fill:#01364C,color:#fff,stroke:#01364C classDef t fill:#F5F5F5,stroke:#01364C,color:#01364C class M hub class C,X,E,Z,V t
The hardest part of healthcare analytics is usually the joins and data prep, not the model.
Juniper Health Plan is fictitious — 3,000 Utah members in 2025.
claims = pd.read_csv("data/claims.csv", parse_dates=["service_date"])
members = pd.read_csv("data/members.csv", parse_dates=["enroll_start", "enroll_end"])
ehr = pd.read_csv("data/ehr_encounters.csv", parse_dates=["encounter_date"])
xwalk = pd.read_csv("data/mrn_crosswalk.csv")
census = pd.read_csv("data/census_zip.csv", dtype={"zip": str})
vendor = pd.read_csv("data/vendor_sdoh.csv")
members["zip"] = members["zip"].astype(str)
members["age"] = 2025 - members["birth_year"]claims 27948
members 3000
ehr 4783
xwalk 1857
census 12
vendor 2494
Name: rows, dtype: int64
| member_id | service_date | place_of_service | dx1 | procedure_or_drug | paid_amt | |
|---|---|---|---|---|---|---|
| 0 | M100754 | 2025-12-24 | 11 Office | E66.01 | 99213 | 151.11 |
| 1 | M100997 | 2025-02-26 | Pharmacy | NaN | GLP-1 RA | 660.51 |
| 2 | M101877 | 2025-04-23 | 11 Office | E11.9 | 99214 | 110.34 |
| 3 | M101270 | 2025-04-26 | 02 Telehealth | R51.9 | 99213 | 126.94 |
| 4 | M101523 | 2025-07-31 | 11 Office | M54.50 | 99213 | 137.19 |
| 5 | M100818 | 2025-12-08 | Pharmacy | NaN | inhaled corticosteroid | 32.93 |
dx1 = ICD-10 diagnosis · procedure_or_drug = CPT code or drug classplace_of_service tells you where: office, ED (23), inpatient (21), telehealth (02)Claims get reversed and resubmitted. Sum dollars by claim before anything else.
claim_status
paid 27535
reversed 413
Name: count, dtype: int64
Per Member Per Month = total paid ÷ member-months
plan_type
Commercial 151.0
Medicaid 183.0
Medicare Advantage 313.0
Name: pmpm, dtype: float64
Why divide by member-months? Not everyone is enrolled all year.
The top 5% of members account for 43% of spend; the top 20% for 76%.
This is why care management programs target “high utilizers.”
| share_of_claims | share_of_$ | |
|---|---|---|
| place_of_service | ||
| 02 Telehealth | 0.07 | 0.04 |
| 11 Office | 0.32 | 0.17 |
| 21 Inpatient Hospital | 0.00 | 0.34 |
| 23 Emergency Room | 0.03 | 0.22 |
| Pharmacy | 0.57 | 0.23 |
Inpatient: few claims, big dollars. Pharmacy: most claims, dollars driven by a few drugs.
5 minutes
dx1) show up most in ED visits (place_of_service == "23 Emergency Room")?procedure_or_drug
ACE inhibitor 0.04
inhaled corticosteroid 0.06
GLP-1 RA 0.86
Name: paid_amt, dtype: float64
Members filling a GLP-1: 191
EHRs use a medical record number (MRN), not a member ID. We need a crosswalk.
62% of members have any EHR data
The rest got care outside this health system: the EHR only sees in-network care.
dx = set(net.loc[net["dx1"] == "E11.9", "member_id"])
rx = set(net.loc[net["procedure_or_drug"] == "metformin", "member_id"])
lab = set(ehr_m.loc[ehr_m["a1c"] >= 6.5, "member_id"])
def found_by(m):
hits = [n for n, ids in [("Dx", dx), ("Rx", rx), ("Lab", lab)]
if m in ids]
return " + ".join(hits) or "none"
linked = ehr_m["member_id"].dropna().unique()
combo = (
pd.Series([found_by(m) for m in linked])
.value_counts()
.drop("none")
)Any single source misses people. Combining claims with EHR labs gives a fuller picture (the same idea as the Highmark depression framework).
5 minutes
Use each linked member’s highest PHQ-9 to assign depression severity (0–4 none/minimal, 5–9 mild, 10–14 moderate, 15–19 mod-severe, 20+ severe). How many with a PHQ-9 ≥ 10 have no depression diagnosis (F32*/F33*) in claims?
{'none': 706, 'mild': 290, 'moderate': 83, 'mod-severe': 31, 'severe': 20}
PHQ-9 ≥ 10 without a dx: 46 of 134
The Social Vulnerability Index (SVI) ranks areas 0–1 on poverty, housing, transportation, etc.
| zip | county | svi | median_hh_income | pct_poverty | pct_uninsured | pct_age65plus | population | |
|---|---|---|---|---|---|---|---|---|
| 0 | 84321 | Cache | 0.42 | 71600 | 13.1 | 8.7 | 20.6 | 31990 |
| 1 | 84341 | Cache | 0.22 | 90800 | 8.7 | 7.5 | 12.5 | 40696 |
| 2 | 84302 | Box Elder | 0.55 | 57000 | 16.3 | 8.9 | 17.7 | 10767 |
⚠️ Ecological fallacy: a ZIP’s SVI describes the area, not the person.
Vendors link consumer records to members and sell person-level social risk.
Ask every vendor: how did you match, how is this score built, and who is missing?
dat = (
mem
.merge(ed_n, on="member_id", how="left")
.fillna({"ed_visits": 0})
.merge(census, on="zip")
.merge(vendor, on="member_id")
.query("match_confidence >= 0.7")
)
dat["fi_q"] = pd.qcut(dat["food_insecurity_index"], 4, labels=["Q1 low", "Q2", "Q3", "Q4 high"])
q = (
dat
.groupby("fi_q", observed=True)
[["ed_visits", "member_months"]]
.sum()
)
q["per_1000"] = 12_000 * q["ed_visits"] / q["member_months"]
q["per_1000"].round(0)fi_q
Q1 low 219.0
Q2 279.0
Q3 367.0
Q4 high 438.0
Name: per_1000, dtype: float64
ED visits are counts with unequal exposure, so use Poisson with an offset for member-months.
5 minutes
housing_instability and transportation_barrier to the model. Do they matter after food insecurity?Keep playing: all data is in data/ and data/make_data.py regenerates it.
bit.ly/47Ywd36 (Slides)
bit.ly/4zxfdg3 (Follow Along Code)