Let’s talk jobs!

The Field Is Growing

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.

Where the Jobs Are

Payers (insurers)
Claims, cost of care, risk adjustment, HEDIS / Star ratings
Providers (health systems)
EHR reporting, quality, operations, readmissions
Pharma & life sciences
Real-world evidence, outcomes research
Health tech & data vendors
Products built on claims / SDOH data
Consulting
Value-based care, program evaluation
Government & public health
Medicaid, state health departments, CDC


Note

In Utah: Intermountain Health / Select Health, University of Utah Health, Optum, HealthEquity, Regence BCBS, Utah DHHS, County Dept of Health

What Postings Ask For

From seven real postings (accessed Oct 16, 2026).

Provider Data Analyst
Intermountain Health (2 postings)

  • SQL, database design; Python and/or R
  • Epic Clarity / Cerner data
  • Dashboards for clinical leaders
  • Present findings to clinical & business teams
  • BS + 2 yrs · $39–68/hr

Payer / Claims Analyst
Optum · USM (HEDIS)

  • Cost, utilization & reimbursement trends in claims
  • HEDIS measures, care gaps, audits
  • SQL, SAS, Tableau / Power BI, Excel
  • Medicare, Commercial, Medicaid payment models
  • BS + 3 yrs · $92k–164k (Optum)

Health Data Scientist
Cohere Health · Dandelion Health · GDIT

  • Causal inference with claims, EHR & SDOH data
  • ML / AI on Medicare & Medicaid claims
  • ICD, CPT, NDC codes
  • Python, R, SQL, Spark
  • MS+ (or BS + 5 yrs) · $98k–155k


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.

What Did I Do in Healthcare Analytics?

GLP-1s vs Bariatric Surgery

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):

  • Weight loss: 28.3% surgery vs 10.3% GLP-1
  • Total cost: $51,794 surgery vs $63,483 GLP-1
  • Driver: ongoing pharmacy spend

Barrett et al. (2025). JAMA Surgery, 160(11), 1232–1239.

Depression: Finding It and Costing It

Identify & stratify with claims + EHR (Dx, Rx, PHQ-9, utilization)

  • Found 720,882 members with depression
  • 258,206 more than diagnosis codes alone

Cost of severity change (PMPM, 2019 → 2020)

  • Severity ↑ : +$608 PMPM on average
  • Severity ↓ : −$41 PMPM

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.

Palliative Care: Payor-Provider Partnership

$4.5M total savings · hospice use 84% vs 50% · Jaffe, …, Barrett (2025). J Palliat Med.

More from the Same Toolkit

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.

The Healthcare Data Landscape

The Data

Highmark is a payor-provider system: an insurer (Highmark) and a health network (Allegheny Health Network). Similar to Intermountain Health.

Four Sources, Four Lenses

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

Hands-On: “Juniper Health Plan Data”

Meet the Data

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

Claims

What a Claim Looks Like

(
    claims
    .sample(6, random_state=3)
    [["member_id", "service_date", "place_of_service", "dx1", "procedure_or_drug", "paid_amt"]]
    .reset_index(drop=True)
)
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 class
  • place_of_service tells you where: office, ED (23), inpatient (21), telehealth (02)

What in the H*ck?! Reversals

Claims get reversed and resubmitted. Sum dollars by claim before anything else.

claims["claim_status"].value_counts()
claim_status
paid        27535
reversed      413
Name: count, dtype: int64
net = (
    claims
    .groupby(["claim_id", "member_id", "service_date", "claim_type",
              "place_of_service", "dx1", "procedure_or_drug"], dropna=False)
    [["allowed_amt", "paid_amt"]]
    .sum()
    .reset_index()
)
net = net[net["paid_amt"] > 0]          # fully reversed claims drop out
len(claims), len(net)
(27948, 27122)

PMPM: The Payer’s Favorite Metric

Per Member Per Month = total paid ÷ member-months

paid = net.groupby("member_id")["paid_amt"].sum()
mem = members.assign(
    paid=members["member_id"].map(paid).fillna(0))

pmpm = (
    mem
    .groupby("plan_type")
    [["paid", "member_months"]]
    .sum()
)
pmpm["pmpm"] = pmpm["paid"]/pmpm["member_months"]
pmpm["pmpm"].round(0)
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.

Cost Is Concentrated

The top 5% of members account for 43% of spend; the top 20% for 76%.

This is why care management programs target “high utilizers.”

Where Does the Money Go?

by_pos = (
    net
    .groupby("place_of_service")
    ["paid_amt"]
    .agg(["sum", "size"])
)
by_pos["share_of_$"] = by_pos["sum"] / by_pos["sum"].sum()
by_pos["share_of_claims"] = by_pos["size"] / by_pos["size"].sum()
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.

Your Turn: Claims

5 minutes

  1. Which diagnosis codes (dx1) show up most in ED visits (place_of_service == "23 Emergency Room")?
  2. What share of pharmacy dollars goes to GLP-1 RA? How many members fill one?
ed = net[net["place_of_service"].str.startswith("23")]
print(ed["dx1"].value_counts().head(3))
dx1
R07.9    156
J06.9    153
R55      145
Name: count, dtype: int64
rx = net[net["claim_type"] == "pharmacy"]
print(rx.groupby("procedure_or_drug")["paid_amt"].sum().div(rx["paid_amt"].sum()).round(2).sort_values().tail(3))
print("Members filling a GLP-1:", rx.loc[rx["procedure_or_drug"] == "GLP-1 RA", "member_id"].nunique())
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

Electronic Health Records

Linking EHR to Claims

EHRs use a medical record number (MRN), not a member ID. We need a crosswalk.

ehr_m = ehr.merge(xwalk, on="mrn", how="left")
cov = members["member_id"].isin(ehr_m["member_id"]).mean()
62% of members have any EHR data

The rest got care outside this health system: the EHR only sees in-network care.

What in the H*ck?! Data Entry Errors

ehr_m.loc[~ehr_m["bmi"].between(12, 70), "bmi"] = np.nan   # set impossible values to missing

Who Has Diabetes? Depends Who You Ask

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).

Your Turn: EHR

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?

phq = ehr_m.groupby("member_id")["phq9"].max().dropna()
sev = pd.cut(phq, [-1, 4, 9, 14, 19, 27], labels=["none", "mild", "moderate", "mod-severe", "severe"])
dep_dx = set(net.loc[net["dx1"].str.match(r"F3[23]", na=False), "member_id"])
high = phq[phq >= 10].index
{'none': 706, 'mild': 290, 'moderate': 83, 'mod-severe': 31, 'severe': 20}
PHQ-9 ≥ 10 without a dx: 46 of 134

Public / Census Data

Adding Neighborhood Context

The Social Vulnerability Index (SVI) ranks areas 0–1 on poverty, housing, transportation, etc.

census.head(3)
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
ed_n = ed.groupby("member_id").size().rename("ed_visits")
by_zip = (mem.merge(ed_n, on="member_id", how="left").fillna({"ed_visits": 0})
             .groupby("zip")[["ed_visits", "member_months"]].sum()
             .merge(census, on="zip"))
by_zip["ed_per_1000"] = 12_000 * by_zip["ed_visits"] / by_zip["member_months"]

ED Visits Rise with Vulnerability


⚠️ Ecological fallacy: a ZIP’s SVI describes the area, not the person.

Vendor Data

Buying What You Can’t Observe

Vendors link consumer records to members and sell person-level social risk.

v = members[["member_id"]].merge(
    vendor, on="member_id", how="left")
conf = v["match_confidence"]
print(f"Matched:          {conf.notna().mean():.0%}")
print(f"High-confidence:  {(conf >= .7).mean():.0%}")
Matched:          83%
High-confidence:  67%

Ask every vendor: how did you match, how is this score built, and who is missing?

Food Insecurity and the ED

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

Putting It All Together: A Poisson Model

ED visits are counts with unequal exposure, so use Poisson with an offset for member-months.

dat["diabetes_dx"] = dat["member_id"].isin(dx).astype(int)
fit = smf.glm(
    "ed_visits ~ food_insecurity_index + svi + age + diabetes_dx + C(plan_type)",
    data = dat,
    family = sm.families.Poisson(),
    offset = np.log(dat["member_months"])
).fit()

Your Turn: Vendor + Model

5 minutes

  1. Add housing_instability and transportation_barrier to the model. Do they matter after food insecurity?
  2. Refit using all matched members (no confidence filter). Does the food-insecurity IRR change?
fit2 = smf.glm("ed_visits ~ food_insecurity_index + housing_instability + transportation_barrier"
               " + svi + age + diabetes_dx + C(plan_type)", data=dat,
               family=sm.families.Poisson(), offset=np.log(dat["member_months"])).fit()
np.exp(fit2.params).round(2)

Takeaways

  1. Claims = cost & breadth · EHR = clinical depth · Census = context · Vendor = gaps filled (at a price)
  2. Most of the work is linking and cleaning: reversals, crosswalks, implausible values, match rates
  3. No single source finds everyone. Combine them.
  4. The field is growing, and it rewards people who can explain results to clinicians and executives

Keep playing: all data is in data/ and data/make_data.py regenerates it.

Sources: Background & Careers

Sources: Highmark Work

  • Barrett TS, et al. Obesity treatment with bariatric surgery vs GLP-1 receptor agonists. JAMA Surg. 2025;160(11):1232–1239. PMID 40960852
  • Popkov AA, Barrett TS, et al. Depression symptom severity and total cost of care. J Affect Disord. 2025;368:41–47. PMID 39271070
  • Popkov AA, Barrett TS, et al. Claims-based depression identification and stratification. J Behav Health Serv Res. 2026;53(2):237–253. PMID 40962946
  • Jaffe E, …, Barrett TS. Payor-provider partnership in palliative care. J Palliat Med. 2025;28(8):1079–1086. PMID 40323731
  • Galper K, …, Barrett TS, et al. Social vulnerability and depression screening. Am J Public Health. 2025;115(9):1436–1444. PMID 40609057
  • Karlin BE, …, Barrett TS. Insomnia in a payor-provider system. Sleep Adv. 2024;5(1):zpae054. PMID 39372542
  • Breitzig MT, …, Barrett TS, et al. Measurement-based care and ED visits. Adm Policy Ment Health. 2026;53(4):462–474. PMID 42096017