Healthcare Resource Utilization (HCRU)
The quantification of the volume and type of healthcare services a patient uses over a defined observation window — hospitalizations and length of stay, emergency department visits, ambulatory/physician encounters, procedures, tests, and pharmacy fills — operationalized from administrative claims, EHR, or registry data and typically standardized to person-time (per-patient-per-month/year) for comparison.
In plain language
Healthcare Resource Utilization (HCRU) counts how often patients use medical services — hospital stays, emergency room visits, doctor's office visits, and prescription fills — over a defined observation window. Because patients are not all watched for the same length of time, the raw counts are divided by how long each patient was observed, producing a rate such as "admissions per patient per year" that makes different groups fairly comparable. HCRU answers the question: how much health care did these patients actually consume, and in which settings? One honest limitation: HCRU counts events but does not measure how severe each event was — a two-day hospital stay and a two-week stay both count as one admission.
Healthcare resource utilization (HCRU) is a count/volume outcome, not a dollar outcome. It enumerates the services a patient consumes (inpatient admissions, ICU days, ED visits, office visits, infusions, procedures, pharmacy fills) over an observation window, using standardized coding: ICD diagnosis/procedure codes, CPT/HCPCS, UB-04 revenue codes, CMS place-of-service (POS) codes, DRGs, and NDCs. HCRU is the substrate from which costs, burden-of-disease estimates, and budget-impact inputs are later built — but it is measured and modeled in its own units (events, days, fills) before any monetization.
Core conceptual distinction
. Three orthogonal design choices define any HCRU measure, and they must be pre-specified in the estimand because each maps to a different model. (1) Setting / place of service: inpatient (POS 21, UB-04 revenue 0100–0219, with DRGs and LOS), ED (POS 23, revenue 045x), on-campus outpatient (POS 22), office (POS 11), SNF/home health/hospice, and pharmacy (NDC). Inpatient HCRU is low-frequency but cost-dominant; office HCRU is high-frequency and cheap — collapsing them hides where burden accrues. (2) Attribution: all-cause (every claim in the window — correct for total budget and for capturing off-target harms) vs disease-specific/attributable (claims carrying a qualifying diagnosis in the primary, or any, position, or a validated algorithm) vs incremental (the difference vs a matched comparator). (3) Endpoint form: a binary "any use" indicator (proportion with ≥1 admission), a count (number of visits), or a rate standardized to person-time. These are not interchangeable: a binary endpoint uses logistic/log-binomial regression; a count or rate uses Poisson or, far more often, negative binomial with an `offset(log(person_time))` because utilization data are over-dispersed and zero-inflated. Reporting a mean count without the denominator or the distribution is uninterpretable when follow-up varies by arm.
Pros, cons, and trade-offs
. - vs healthcare-costs-pppm-pppy-pmpm (monetized utilization): HCRU counts isolate the driver of burden (which setting, which service) and are robust to price variation, negotiated rates, and the absence of true paid amounts — a hospitalization is a hospitalization regardless of the contract. Cost: counts ignore intensity (a 1-day and a 14-day admission are both "1 admission"), so they understate the economic gap between arms. Prefer HCRU counts when describing service mix or identifying high-utilization phenotypes, and report them alongside costs, never instead of them. - vs poisson-negative-binomial-count-models (the analytic layer): HCRU is the outcome those models consume; naive comparison of mean counts across arms with unequal follow-up or over-dispersion gives wrong standard errors. Prefer NB with a person-time offset over Poisson whenever the variance exceeds the mean (the norm). - vs cost-outlier-handling-rwe: Raw HCRU counts are the input that must be inspected before deciding on winsorization or robust models; a handful of frequent-ED super-utilizers can dominate an unadjusted mean. Prefer examining the count distribution first, then choosing a count model or trimming rule.
When to use
. Describing disease burden and service mix; identifying high-cost/high-utilization patients and the settings that drive them; building the volume inputs for cost, budget-impact, and cost-of-illness analyses; comparing utilization between treatment arms in comparative-effectiveness or safety studies; and PQA/CMS-style payer reporting where PPPM utilization is a standard deliverable.
When NOT to use — and when it is actively misleading or dangerous
. - When the question is economic value and you report counts alone. Counts treat a brief observation stay and a prolonged ICU admission as equal; a therapy that shortens LOS without reducing admission count looks null on crude HCRU yet saves real money. Always pair counts with LOS/days and with costs. - When follow-up differs by arm and you compare raw means. Longer-followed patients accrue more events mechanically; failing to standardize to person-time (PPPM/PPPY) or to use a person-time offset manufactures a spurious difference. This is the single most common HCRU error in payer decks. - When person-time is unobservable. In Medicare Advantage (MA-only) enrollment, encounter data are incomplete and fee-for-service (FFS) claims are absent, so "zero utilization" is missingness, not true non-use. Counting MA person-time in the denominator while events are under-captured biases every rate downward — dangerous when arms differ in MA mix. - When attribution is unvalidated. Disease-specific HCRU built on rule-out codes or on diagnoses that also flag comorbid or screening encounters over-attributes; conversely, requiring a primary-position code under-attributes when the condition is coded secondarily. Use a validated algorithm and report the all-cause figure for context. - When competing risks differ by exposure. In elderly claims cohorts, the arm with higher mortality accumulates less downstream HCRU simply because patients die — a survivorship artifact that makes the more harmful drug look "lower utilization." Account for death as a competing event, not as censoring that inflates rates.
Data-source operational depth
. - Claims (FFS commercial or Medicare A/B/D): The strongest substrate for standardized events. Inpatient via UB-04 revenue codes + DRG; outpatient/procedures via CPT/HCPCS + POS; pharmacy via NDC on the Part D / pharmacy file. Failure modes: (a) MA-only person-time lacks FFS claims — restrict to enrollees with the relevant medical+pharmacy benefit and exclude MA-only spans, or you divide real events by inflated, unobservable denominators; (b) site-of-service shift — a procedure moving from inpatient to hospital-outpatient changes the POS bucket without changing total volume, so trend analyses must collapse sites or model them jointly; (c) bundled/episode payments (BPCI, CJR, oncology bundles) — line-level HCRU is often still observable inside the episode window, but the single bundled payment decouples observed counts from paid cost, so cost attribution needs allowed amounts or shadow pricing; (d) immortal time in procedure studies — starting follow-up at diagnosis but defining "treated" by a later procedure guarantees the treated arm survives to be treated, inflating its pre-procedure HCRU; align time zero to the procedure or use a landmark. - EHR: Captures clinically real events (visits attended, labs ordered) and severity that claims lack, but is visit-driven — sicker, more engaged patients generate more observed encounters, biasing utilization upward, and a patient who leaves the system is differentially lost. EHR rarely sees out-of-network or cross-system care, so totals under-capture. Best linked to claims for a complete denominator. - Registry: Protocol-driven capture of disease-specific HCRU (specialist visits, defined tests) with strong severity/staging, but incomplete for all-cause or primary-care use. Excellent for validating claims-based HCRU algorithms when linked. - Linked claims–EHR–registry–vital records: The ideal substrate (EHR severity + claims completeness + reliable mortality for competing-risk handling), but linkage selects the linkable subset and introduces date-discrepancy issues (order vs fill vs service dates) that must be reconciled before binning person-time.
Worked claims example
Question: all-cause and diabetes-attributable HCRU in the first year after initiating a new GLP-1 agonist, by place of service, among adults in a commercial + Medicare FFS database. (1) Cohort & observable time: require 365 days of continuous medical + pharmacy enrollment before the index fill (washout + baseline) and follow from the index date forward; exclude MA-only spans so absence of claims is true non-utilization, not missingness. (2) Person-time: for each patient, accrue enrolled days from index to the earliest of disenrollment, death, +365 days, or end of data; convert to person-months = days / 30.44. A patient enrolled 200 days contributes 6.57 person-months — and contributes to the denominator even with zero events. (3) Event counting by POS: inpatient admissions = distinct admission stays after collapsing same-day transfers and bridging stays into one episode (revenue 0100–0219 / POS 21); ED = POS 23 / revenue 045x not followed by an inpatient admission on the same/next day (else it rolls into the admission); office = POS 11 CPT E/M; pharmacy = distinct NDC fills. (4) Attribution: all-cause = every event; diabetes-attributable = events with a type-2 diabetes ICD-10 code (E11.x) in any position, reported alongside the all-cause figure. (5) Standardize: PPPM rate = total events across the cohort / total person-months; e.g., 1,820 inpatient admissions over 38,400 person-months = 0.0474 admissions PPPM ≈ 0.57 PPPY. (6) Model: negative-binomial regression of the event count with `offset(log(person_months))` and arm + baseline covariates, because the inpatient count is over-dispersed and zero-inflated; treat death as a competing event when comparing arms with differential mortality. (7) Sensitivity: repeat with a primary-position-only attribution, a 30-day vs 7-day ED-to-admission rollup window, and FFS-only vs FFS+observable-MA cohorts to bound the denominator assumption.
Interpreting the output
A study of GLP-1 initiators reports all-cause HCRU in the first year across 2.800 total person-years: inpatient admissions 1.07 per patient per year (PPPY), ED visits 2.50 PPPY, and outpatient encounters 6.07 PPPY. These three rates are the standardized event counts per full year of observed patient-time.
(1) Formal interpretation. The 1.07 inpatient admissions PPPY means that, across the observed person-time, patients averaged just over one hospital admission per year of follow-up; the denominator is 2.800 person-years, not a headcount of 2.8 patients, so patients with partial follow-up are weighted by their observed time. These are all-cause rates: every admission regardless of diagnosis is counted. The rates are not adjusted for baseline differences across arms; any between-arm comparison would require regression with a person-time offset and covariate adjustment. Counts treat a 1-day and a 14-day stay as one admission each — LOS data must accompany the count to reflect severity.
(2) Practical interpretation. For a budget-impact team, the outpatient rate (6.07 visits PPPY) is the highest-volume driver of utilization, while the inpatient rate (1.07 PPPY) likely dominates costs despite its lower volume. Reporting all three rates with their person-time base allows a payer to multiply each rate by its unit cost and project total expected spend per patient per year — which is why the HCRU table is the standard companion to any dollar-denominated cost estimate.
Worked example
Scenario
A researcher wants to describe how often patients with type 2 diabetes used the hospital, emergency room, and outpatient clinic in the year after starting a new medication. Three patients are enrolled. Patient P-001 was observed for the full 365-day follow-up year. Patient P-002 was also observed the full year. Patient P-003 enrolled later and was observed for only 292 days before the study ended. The analyst needs to report utilization rates that are fair to compare across patients, even though one patient was watched for less time.
Dataset
Raw event counts per patient during each patient's individual observation window
| person_id | inpatient_admissions | er_visits | outpatient_visits | observed_days |
|---|---|---|---|---|
| P-001 | 2 | 4 | 10 | 365 |
| P-002 | 1 | 3 | 365 | |
| P-003 | 1 | 2 | 4 | 292 |
Steps
Convert each patient's observed days to observed years by dividing by 365. P-001: 365 / 365 = 1.000 year. P-002: 365 / 365 = 1.000 year. P-003: 292 / 365 = 0.800 year.
For each patient, divide their event count by their observed years to get a per-patient-per-year (PPPY) rate for each setting. This annualizes the counts so a patient watched for less than a year is not penalized for having fewer raw events.
P-001 inpatient PPPY: 2 admissions / 1.000 year = 2.00 admissions/year. ER PPPY: 4 / 1.000 = 4.00 visits/year. Outpatient PPPY: 10 / 1.000 = 10.00 visits/year.
P-002 inpatient PPPY: 0 / 1.000 = 0.00. ER PPPY: 1 / 1.000 = 1.00 visit/year. Outpatient PPPY: 3 / 1.000 = 3.00 visits/year.
P-003 inpatient PPPY: 1 / 0.800 = 1.25 admissions/year. ER PPPY: 2 / 0.800 = 2.50 visits/year. Outpatient PPPY: 4 / 0.800 = 5.00 visits/year.
Notice why annualizing matters: P-003 had only 4 outpatient visits, fewer raw events than P-001's 10, yet her annualized rate (5.00/year) is higher than P-002's (3.00/year). Without dividing by observed time, raw counts would unfairly make P-003 look like the lightest user simply because she was watched for less time.
To report a single cohort-level rate, sum all events and divide by total observed years. Inpatient: (2 + 0 + 1) = 3 admissions / (1.000 + 1.000 + 0.800) = 2.800 total person-years = 1.07 admissions PPPY. ER: (4 + 1 + 2) = 7 visits / 2.800 = 2.50 visits PPPY. Outpatient: (10 + 3 + 4) = 17 visits / 2.800 = 6.07 visits PPPY.
Result
Per-patient-per-year utilization rates — P-001: 2.00 inpatient, 4.00 ER, 10.00 outpatient. P-002: 0.00 inpatient, 1.00 ER, 3.00 outpatient. P-003: 1.25 inpatient, 2.50 ER, 5.00 outpatient. Cohort-level rates (all 3 patients, 2.800 total person-years): 1.07 inpatient admissions PPPY, 2.50 ER visits PPPY, 6.07 outpatient visits PPPY. All arithmetic derived from raw counts and observed days in the table above.
Runnable example
python implementation
Computes all-cause and POS-stratified HCRU rates (PPPM/PPPY) and fits a negative-binomial count model with a person-time offset. Required inputs (already cleaned, de-duplicated, MA-only spans excluded): claims : person_id, service_date (datetime), pos (CMS...
import numpy as np
import pandas as pd
import patsy
import statsmodels.api as sm
DAYS_PER_MONTH = 30.44
def attributable(dx_series: pd.Series, code_prefixes=("E11",)) -> pd.Series:
# True if any diagnosis on the claim starts with a condition prefix (any-position attribution).
return dx_series.fillna("").str.upper().str.contains("|".join(code_prefixes), regex=True)
def hcru_rates(claims: pd.DataFrame, cohort: pd.DataFrame, attributable_only: bool = False) -> pd.DataFrame:
c = claims.merge(cohort[["person_id", "index_date", "enroll_end", "arm"]], on="person_id", how="inner")
# Keep only events inside the follow-up window [index_date, enroll_end].
c = c[(c["service_date"] >= c["index_date"]) & (c["service_date"] <= c["enroll_end"])]
if attributable_only:
c = c[attributable(c["dx1"])]
# Person-months per patient (denominator includes zero-utilization patients).
pt = cohort.copy()
pt["person_months"] = ((pt["enroll_end"] - pt["index_date"]).dt.days.clip(lower=0)) / DAYS_PER_MONTH
# Event counts by setting per patient, then merge onto the full cohort (fill non-utilizers with 0).
counts = (c.groupby(["person_id", "claim_type"]).size()
.unstack(fill_value=0).reset_index())
out = pt.merge(counts, on="person_id", how="left").fillna(0)
# Cohort-level PPPM / PPPY by setting.
settings = [s for s in ["inpatient", "ed", "outpatient", "pharmacy"] if s in out.columns]
total_pm = out["person_months"].sum()
rates = {s: out[s].sum() / total_pm for s in settings} # events per person-month
rates = pd.DataFrame({"setting": settings,
"pppm": [rates[s] for s in settings],
"pppy": [rates[s] * 12 for s in settings]})
return out, rates
# --- Negative-binomial model for the inpatient count with a log person-time offset ---
# Use the discrete NB MLE so the dispersion (alpha) is ESTIMATED from the data, matching
# R's MASS::glm.nb and SAS PROC GENMOD dist=negbin (not fixed at alpha=1.0).
patient_level, cohort_rates = hcru_rates(claims, cohort)
patient_level["log_pt"] = np.log(patient_level["person_months"].replace(0, np.nan))
model_df = patient_level.dropna(subset=["log_pt"])
y, X = patsy.dmatrices("inpatient ~ C(arm) + age + sex", data=model_df, return_type="dataframe")
nb = sm.NegativeBinomial(y, X, offset=model_df["log_pt"]).fit() # estimates alpha by MLE
print(cohort_rates)
print(nb.summary()) # exp(coef) on arm = adjusted rate ratio of inpatient HCRU; alpha = dispersionr implementation
All-cause / POS HCRU PPPM-PPPY and a negative-binomial rate model (MASS::glm.nb) with a person-time offset. Inputs mirror the Python version: claims : person_id, service_date (Date), pos, revenue_code, dx1, claim_type in...
library(data.table)
library(MASS)
DAYS_PER_MONTH <- 30.44
hcru_rates <- function(claims, cohort, attributable_only = FALSE, code_prefix = "^E11") {
setDT(claims); setDT(cohort)
c <- merge(claims, cohort[, .(person_id, index_date, enroll_end, arm)], by = "person_id")
c <- c[service_date >= index_date & service_date <= enroll_end]
if (attributable_only) c <- c[grepl(code_prefix, toupper(dx1))]
# Person-months per patient; zero-utilization patients retain their denominator.
pt <- cohort[, person_months := as.numeric(pmax(enroll_end - index_date, 0)) / DAYS_PER_MONTH]
counts <- dcast(c[, .N, by = .(person_id, claim_type)],
person_id ~ claim_type, value.var = "N", fill = 0)
out <- merge(pt, counts, by = "person_id", all.x = TRUE)
settings <- intersect(c("inpatient","ed","outpatient","pharmacy"), names(out))
for (s in settings) out[is.na(get(s)), (s) := 0]
total_pm <- sum(out$person_months)
rates <- data.table(setting = settings,
pppm = sapply(settings, function(s) sum(out[[s]]) / total_pm))
rates[, pppy := pppm * 12]
list(patient_level = out, rates = rates)
}
res <- hcru_rates(claims, cohort)
print(res$rates)
# Negative-binomial rate model: exp(coef) for arm = adjusted inpatient rate ratio.
d <- res$patient_level[person_months > 0]
nb <- glm.nb(inpatient ~ arm + age + sex + offset(log(person_months)), data = d)
summary(nb)