FULL RUN REPORT · 500 CASES · 14 CATEGORIES · 12 ANALYTIC TYPES

One cohort, twelve analytics, and how the LLM approached each

Everything from every prior run, combined into a single file: the original backward-lift + closed-loop pipeline, the eight extended analytics (co-occurrence, polypharmacy, time-to-escalation, symptom proximity, risk scoring, readmission drivers, equity check, hub network), and three later LLM-flagged additions (diagnostic ambiguity, high-utilizer cohort, dataset drift) — each followed immediately by a note on what was an LLM judgment call versus deterministic arithmetic.
In plain terms — what this run does and why it matters: a synthetic 500-patient cohort was built, then run through twelve independent statistical passes (comorbidity lift, co-occurrence, polypharmacy, time-to-escalation, symptom overlap, risk calibration, readmission drivers, equity, hub network, diagnostic ambiguity, high-utilizer triage, and a data-drift check) — every number a plain computation, none of them written by a model. The LLM's only job, run the same way twelve times, is to read each ranked table afterward and decide what deserves a reviewer's attention and why. That's the thing this report demonstrates: a repeatable pattern for pairing deterministic analytics with an LLM that interprets results but never invents or overrides them.
500 synthetic cases · 14 SNOMED-coded categories · discovery + independent forward-validation pass
In plain language
·

What was actually done here, and what it's meant to show

A quick, non-technical walkthrough before the full report, for anyone skimming on a phone.

1 · Build a cohort

500 synthetic hospital cases were generated across 14 common diagnoses (pneumonia, heart failure, diabetes, sepsis, etc.), each carrying age, sex, comorbidities, symptoms, medications, length of stay, and whether the case escalated to a higher level of care.

2 · Run twelve analytics

Twelve separate statistical passes were run on that cohort — which comorbidity combinations predict escalation, which symptoms overlap across diagnoses, which patients are the heaviest system users, and more. Every number comes straight out of pandas/numpy — no analytic result is generated by an LLM.

3 · LLM reads, doesn't compute

After each pass finishes, an LLM reads the ranked output and writes a short "so what" — which row matters, why, and what a reviewer should check next. That role is labeled "LLM approach" throughout so it's never confused with the arithmetic above it.

What this project demonstrates: a template for combining deterministic health-data analytics with an LLM in a clearly bounded role — the LLM never computes a rate, invents a patient, or overrides a number; it only interprets, prioritizes, and explains what the arithmetic already found. The same twelve-step pattern here is meant to generalize to any coded clinical dataset, not just this synthetic one.
Read this first
00

What this run actually pulled from, and why

This environment's outbound network is restricted to package registries (PyPI, npm, GitHub) — it cannot reach r4.smarthealthit.org directly, and the general-purpose web tool can only retrieve pages that already appear in a search result, not arbitrary REST/FHIR query responses. So this run did not execute a live Condition?code=... search against the SMART Health IT sandbox.

What it does instead — mirroring the source project's own fallback of shipping a bundled offline cohort for exactly this reason — is generate a 500-case synthetic cohort with the same shape, resource fields, and SNOMED coding pattern as that sandbox's Synthea-derived data, then run the real backward-lift, closed-loop, and eight extended analytics on it end to end. The fetch code section gives the actual, runnable FHIR client call — point it at the live endpoint from a machine with open network access and it replaces the synthetic generator with real sandbox pulls.

In short: the pipeline mechanics, statistics, and report format are real and reproducible. The specific numbers below are computed from a synthetic stand-in cohort, not live patient records — treat every finding here as an illustration of what the pipeline would surface, not a validated clinical claim.
Method
01

The pipeline, as actually run

Fetch/generate Condition cohort
Attach comorbidities, symptoms, meds, outcomes
9 analytic passes (deterministic)
LLM: threshold, framing, narrative
Forward validation pass
Reviewer checkpoint (not simulated)

1 · Cohort assembly

14 SNOMED-coded categories, ~35 cases each on average, totaling 500. Each case carries age, sex, up to 8 tracked comorbidities, presenting symptoms, active medication count, 90-day encounter count, length of stay, escalation outcome, time-to-escalation, and 30-day readmission flag.

2 · Nine analytic passes

Backward comorbidity lift, closed-loop replication, co-occurrence, polypharmacy burden, time-to-escalation, symptom proximity, risk-score calibration, readmission-driver lift, equity check, and a hub-node network — all computed with pandas/numpy, none with an LLM call.

3 · LLM role

Same role in every one of the nine: choose thresholds and comparisons upfront, then read the ranked/aggregated output and decide what's worth a reviewer's attention — documented explicitly after each analytic below.

Everything up to the ranked table is arithmetic a spreadsheet could do. The LLM's job starts after: reading it, deciding what's worth a sentence, and writing that sentence in language a reviewer can act on.
Cohort
02

500 cases across 14 categories

Sorted by escalation rate. "Escalation" = ICU transfer or acute deterioration flag during the encounter.

CategorynEscalation rateAvg LOS (days)Avg ageAvg comorbidities
LLM approach Chose which five patient-level fields to surface in this overview (rate, LOS, age, comorbidity count — not, say, sex or encounter count) as the ones that explain why a category ranks where it does, and picked escalation rate as the sort key since it's the outcome every downstream analytic keys off. Every number in the table and chart is a pandas groupby().agg() — the LLM computed none of them.
Analytic type 1 · Discovery pass
03

Backward-generated findings, ranked by lift

For each category, the comorbidity most strongly associated with escalation, versus patients in the same category without it.

CategoryComorbidityn (with)Escalation ratevs. withoutLift
Small subgroup sizes (n<10) inflate lift ratios — this is exactly why nothing here is treated as a finding until it clears the forward-replication check next.
LLM approach Set the comparison rule (rate with the comorbidity vs. rate without it, within the same primary-condition cohort, rather than against the whole-cohort baseline) so a finding can't be confounded by one category simply being sicker overall. Excluded any comorbidity name matching the primary condition itself (a bug caught mid-build — hypertension patients were briefly being compared against themselves). Read the resulting 14-row ranked list and picked the top 6 lift ≥1.35 patterns to carry into the replication test — that selection is a judgment call, the lift numbers themselves are arithmetic.
Analytic type 1, continued · Closed loop
04

Which patterns survive an independent pull

Top 6 backward findings, each re-tested against a freshly generated sample of that category. Replicated = lift held at ≥1.3× on the second pull.

Category → comorbidityDiscovery (n / lift)Validation (n / lift)Status
LLM approach Chose the ≥1.3× replication bar and treated it as a hard pass/fail gate rather than a soft signal — a deliberately stricter reading than the discovery lift itself, so a pattern has to clear two independent hurdles, not one, before being called a finding. The decision to simulate the "second FHIR endpoint" as an independently generated same-category sample (rather than a held-out split of the same 500) was also an LLM design choice, made because splitting an already-small per-category cohort in half left too few patients per comorbidity subgroup to test anything meaningfully.
LLM-drafted, arithmetic-grounded
05

Insights from analytic type 1

Written from the two tables above — every number traces to a cell in the backward or replication table, nothing is invented.

Analytic type 2
06

Comorbidity co-occurrence matrix

How often each pair of tracked comorbidities appears together across all 500 cases, regardless of primary diagnosis.

Strongest pairs

Comorbidity AComorbidity BPatients with both
LLM approach Chose the 8-condition tracked set to include (broad enough to catch clustering, narrow enough to stay auditable), then read the ranked pair list and picked which pairs were worth naming as hub candidates versus noise. Every cell in the matrix is a pairwise boolean-AND tally over the whole cohort — arithmetic only.
Analytic type 3
07

Polypharmacy burden

Escalation and 30-day readmission rate by active medication count bucket.

MedsnEscalation rate30-day readmit rate
LLM approach Chose the bucket edges (0–2, 3–4, 5–6, 7–8, 9+) to balance bucket size against clinical meaning — 9+ active medications is a recognized polypharmacy threshold in the geriatric-care literature, not an arbitrary cutpoint — and flagged the 7–8 bucket's escalation jump as the one worth a sentence. No bucket's rate was calculated by the LLM; that's a groupby-mean.
Analytic type 4
08

Time-to-escalation

Among cases that escalated, median days from admission to escalation — by category, and by comorbidity burden.

By category (fastest first)

CategoryMedian daysMean daysn escalated

By comorbidity count

ComorbiditiesMedian daysMean daysn escalated
Escalations arrive fastest in categories that are already acute at presentation and slower in chronic-onset categories — and within any category, more comorbidities compress the time-to-escalation, not just its probability.
LLM approach Decided median (not mean) was the right summary given a right-skewed, small-n-per-category distribution, and drew the "acute-onset categories escalate faster, and comorbidity burden compresses timing further" narrative connecting the two tables — a comparison the tables alone don't state on their own. No median/mean was computed by the LLM — that's pandas.agg() on cases already flagged as escalated.
Analytic type 5
09

Symptom → diagnosis proximity

For each category, the presenting symptom most enriched relative to its background rate across the whole cohort.

CategoryMost distinctive symptom% of category% of full cohortEnrichment
LLM approach Designed the enrichment metric itself (% of category ÷ % of full cohort, rather than a raw count or a formal chi-square test) as the simplest ratio a reviewer could sanity-check by hand, set the ≥4-patient floor to suppress noise, and used the result as a validation step — checking the pipeline recovers known symptom-diagnosis pairs (palpitations/AFib, dysuria/UTI, polyuria/diabetes) before trusting it on anything genuinely ambiguous. No percentage or enrichment ratio was computed by the LLM.
Analytic type 6
10

Composite risk score — and its calibration

A transparent, hand-weighted score split into quintiles, checked against the actual observed escalation rate in each quintile.

Risk quintilenAvg scoreObserved escalation rate
LLM approach Picked which four inputs to weight and their relative weights (comorbidity count heaviest, then medication burden, then age, then two condition-specific flags) — a deliberately transparent, hand-set scheme rather than a fitted model, chosen so every weight can be read and challenged by a clinician rather than trusted as a black box. After scoring, checked the calibration table for monotonicity as the explicit pass/fail test for whether the score is worth using at all — it passed. No patient's score, quintile cutpoint, or observed rate was computed by the LLM.
Analytic type 7
11

30-day readmission drivers

The same backward-lift arithmetic from analytic type 1, re-run against a different outcome variable — 30-day readmission instead of in-stay escalation.

CategoryComorbiditynReadmit ratevs. withoutLift
Notice the leading comorbidity here differs from the escalation-lift table (atrial fibrillation and hypertension dominate here vs. CKD there) — "risk" isn't one number; which comorbidity matters depends on which outcome you're asking about.
LLM approach Re-ran the identical backward-lift code from analytic type 1 against a different outcome column, on purpose, specifically to test whether the same comorbidities lead for every outcome or whether leadership changes with the question — then wrote up the fact that it changes as the main finding of this analytic. No lift ratio in either table was computed by the LLM.
Analytic type 8
12

Subgroup equity check

Per the source report's own roadmap item ("Equity & bias review") — escalation rate checked across sex and age band before trusting any finding operationally.

SexnEscalation rateAvg LOS
Age bandnEscalation rate
Sex shows no meaningful gap here (26.2% vs 25.4%) — expected, since this synthetic generator didn't encode a sex effect. Age shows a real, gradual gradient (17%→28%), which is clinically plausible and worth stratifying for rather than treating as a single population-wide rate.
LLM approach Chose to run this analytic at all, unprompted by any specific finding upstream — pulled directly from the source report's own roadmap item as a check that should happen before any pattern above is trusted operationally — and interpreted the 1-point sex gap as noise versus the 11-point age gradient as a real, plausible signal worth stratifying for. Neither group's escalation rate was computed by the LLM.
Analytic type 9
13

Hub-node comorbidity network

Primary conditions (teal) and comorbidities (amber) as nodes; an edge is drawn where a comorbidity appears in ≥8 patients of that primary-condition cohort.

Essential hypertension and chronic kidney disease sit as the clearest hub nodes — high-degree comorbidities threading through multiple unrelated primary diagnoses — exactly the kind of node the source report's "hub-node identification" feature is designed to surface for a reviewer.
LLM approach Set the ≥8-patient edge-inclusion threshold to keep the graph legible rather than a fully connected mess, chose the circular two-ring layout (primaries outer, comorbidities inner) so edges read cleanly without a physics-based force layout, and named the hub nodes by reading which nodes had the most and heaviest edges. No edge weight or node degree was computed by the LLM — those are groupby counts against a fixed threshold.
Analytic type 10 · LLM-flagged
16a

Diagnostic ambiguity zones

Symptom sets that recur across more than one primary condition, ranked by how many distinct diagnoses they're compatible with. This is the closest this cohort gets to naming genuine differential-diagnosis pressure points rather than clean per-condition signatures.

Symptom setn# conditions sharing itDistributionTop dx share
Analytic type 11 · LLM-flagged
16b

High-utilizer cohort (top decile by 90-day encounters)

The 10% of patients with the most encounters in the prior 90 days, isolated and compared against everyone else, the way a case-management program would triage its outreach list.

CohortnEscalation rate30-day readmitAvg comorbiditiesAvg medsAvg LOS
Primary conditionHigh-utilizers, n
Analytic type 12 · LLM-flagged
16c

Dataset A vs. B drift check

This cohort was assembled from two source pulls, tagged A and B. Before trusting any cross-cohort finding, it's worth checking whether A and B actually look like the same population — an equity/QA habit that generalizes to comparing any two source EHRs or sites.

MetricDataset ADataset B
Conditionn (A / B)Escalation AEscalation BAbs. diff
Reproduce with live data
14

The actual FHIR fetch (run this where network access allows it)

This is the real client call against the open SMART Health IT R4 sandbox. It was not executed in this environment (see the section above titled “What this run actually pulled from, and why”) — drop it into the cohort-generation step on any machine with outbound internet access.

import requests

BASE = "https://r4.smarthealthit.org"

SEED_CODES = {
    "Pneumonia": "233604007", "Type 2 diabetes": "44054006",
    "Chronic kidney disease": "709044004", "COPD": "13645005",
    "Congestive heart failure": "84114007", "Sepsis": "91302008",
    "Urinary tract infection": "68566005", "Asthma": "195967001",
    "Essential hypertension": "59621000", "Acute stroke / CVA": "230690007",
    "Myocardial infarction": "22298006", "Major depressive disorder": "370143000",
    "Osteoarthritis": "396275006", "Atrial fibrillation": "49436004",
}

def fetch_cohort(code, count=40):
    """Pull Condition resources for a SNOMED seed code, then the
    subject Patient + related Conditions (comorbidities) + Encounter
    outcome for each match."""
    conditions = requests.get(f"{BASE}/Condition", params={
        "code": code, "_count": count
    }).json()
    cases = []
    for entry in conditions.get("entry", []):
        cond = entry["resource"]
        patient_id = cond["subject"]["reference"].split("/")[-1]
        comorb = requests.get(f"{BASE}/Condition", params={
            "patient": patient_id, "_count": 50
        }).json()
        encounters = requests.get(f"{BASE}/Encounter", params={
            "patient": patient_id, "_count": 20
        }).json()
        cases.append({"patient_id": patient_id, "seed_code": code,
                       "comorbidities": comorb, "encounters": encounters})
    return cases

# cohort = []
# for name, code in SEED_CODES.items():
#     cohort.extend(fetch_cohort(code, count=36))   # ~36 x 14 ≈ 500 cases

This mirrors what the source project's Phase-2 tool does live in-browser against this same sandbox — plain-language query → SNOMED concept → Condition search → patient match — run here as a batch pull instead of a single interactive query.

Read the fine print
15

What this run does not claim

Not causal. Lift is a screening signal, computed on synthetic data with a deliberately injected — not discovered-from-nature — association, purely to demonstrate that the pipeline can recover a known signal.

Not clinically validated. No real patient data was accessed. Nothing here should inform an actual clinical, billing, or research decision.

Small-n throughout. With 500 cases across 14 categories, most subgroup comparisons sit at n=6–20 — enough to demonstrate the mechanics, not enough for statistical confidence.

Human checkpoints, on paper only. No reviewer signed off on any "replicated" finding above — that step is simulated by an arithmetic threshold, not a clinician.
Next, built
17

Per-case explorer

The build-ahead plan, completed on the same 500-case cohort: each case opened into its own coded timeline, linked back into the analytics above — comorbidity co-occurrence, readmission-driver lift, and risk-score quintile — and located inside the hub-node network.

Timelines below are ordered by pipeline stage, not a real timestamp — the underlying fields don't carry event-level time (see the build-ahead section, step 6). Every number on this page is a direct lookup against the same computed tables above; nothing here is re-derived or LLM-estimated.