This report analyzes CMS’s Outcome of Care Measures file: 30-day risk-adjusted mortality and readmission rates for three high-stakes conditions — heart attack (AMI), heart failure (HF), and pneumonia (PN) — reported for 4,706 U.S. hospitals. For each condition and each outcome, CMS provides the hospital’s rate, a confidence interval, the patient count behind it, and a categorical comparison to the national rate (Better, No Different, Worse, or Number of Cases Too Small).
This is a quality/outcomes file, distinct from the structural hospital directory analyzed previously — it says how hospitals performed on these three measures, not what type of hospital they are or who owns them.
raw <- read_csv("outcome-of-care-measures.csv", col_types = cols(.default = "c"))
# Six rate measures, each with a matching comparison-to-US-rate column
measures <- tibble::tribble(
~key, ~condition, ~outcome, ~rate_col,
"ha_mort", "Heart Attack", "Mortality", "Hospital 30-Day Death (Mortality) Rates from Heart Attack",
"hf_mort", "Heart Failure", "Mortality", "Hospital 30-Day Death (Mortality) Rates from Heart Failure",
"pn_mort", "Pneumonia", "Mortality", "Hospital 30-Day Death (Mortality) Rates from Pneumonia",
"ha_read", "Heart Attack", "Readmission", "Hospital 30-Day Readmission Rates from Heart Attack",
"hf_read", "Heart Failure", "Readmission", "Hospital 30-Day Readmission Rates from Heart Failure",
"pn_read", "Pneumonia", "Readmission", "Hospital 30-Day Readmission Rates from Pneumonia"
) %>%
mutate(comp_col = str_replace(rate_col, "^Hospital", "Comparison to U.S. Rate - Hospital"))
hospitals <- raw %>%
mutate(across(all_of(measures$rate_col), ~ as.numeric(na_if(., "Not Available"))))
tibble(
Metric = c("Total hospitals", "States / territories represented", "Distinct counties",
"Conditions covered", "Outcome types covered"),
Value = c(
comma(nrow(hospitals)),
n_distinct(hospitals$State),
comma(n_distinct(hospitals$`County Name`)),
"Heart Attack, Heart Failure, Pneumonia",
"30-Day Mortality, 30-Day Readmission"
)
) %>%
kable(caption = "Dataset snapshot") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
| Metric | Value |
|---|---|
| Total hospitals | 4,706 |
| States / territories represented | 54 |
| Distinct counties | 1,498 |
| Conditions covered | Heart Attack, Heart Failure, Pneumonia |
| Outcome types covered | 30-Day Mortality, 30-Day Readmission |
Unlike a facility directory, most of the analytic value here is in the rate columns — and most of those cells are legitimately blank. CMS suppresses a hospital’s rate whenever it has too few qualifying cases to report a statistically reliable number, so “missing” here mostly means small volume, not bad data.
completeness <- hospitals %>%
summarise(across(all_of(measures$rate_col), ~ sum(!is.na(.)))) %>%
pivot_longer(everything(), names_to = "rate_col", values_to = "n_reported") %>%
left_join(measures, by = "rate_col") %>%
mutate(
pct_reported = n_reported / nrow(hospitals),
label = paste(condition, outcome)
)
ggplot(completeness, aes(x = fct_reorder(label, n_reported), y = n_reported, fill = outcome)) +
geom_col() +
geom_text(aes(label = paste0(comma(n_reported), " (", percent(pct_reported, accuracy = 1), ")")),
hjust = -0.05, size = 3.3) +
coord_flip(clip = "off") +
scale_y_continuous(labels = comma, expand = expansion(mult = c(0, 0.25))) +
scale_fill_manual(values = c(Mortality = "#e34a33", Readmission = "#2c7fb8")) +
labs(title = "Hospitals with a reportable rate, by measure",
x = NULL, y = "Hospitals with a non-suppressed rate", fill = NULL) +
theme_minimal(base_size = 12)
Heart attack measures have by far the most suppression — only about 58% of hospitals have a reportable heart-attack mortality rate, and just 50% have a reportable heart-attack readmission rate, because AMI is treated at a comparatively small number of hospitals in high enough volume to meet CMS’s reliability threshold. Pneumonia is the most completely reported measure (90% mortality, 90% readmission), since pneumonia is common and treated at nearly every acute care hospital. Any comparison across conditions should keep this in mind: the heart-attack numbers describe a narrower, larger-volume subset of hospitals than the pneumonia numbers do.
rate_long <- hospitals %>%
select(`Provider Number`, State, all_of(measures$rate_col)) %>%
pivot_longer(all_of(measures$rate_col), names_to = "rate_col", values_to = "rate") %>%
left_join(measures, by = "rate_col") %>%
filter(!is.na(rate))
rate_long %>%
group_by(condition, outcome) %>%
summarise(
n = n(),
mean = mean(rate), median = median(rate),
sd = sd(rate), min = min(rate), max = max(rate),
.groups = "drop"
) %>%
mutate(across(c(mean, median, sd, min, max), ~ round(., 1))) %>%
kable(caption = "Summary statistics for each 30-day outcome measure (%)") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
| condition | outcome | n | mean | median | sd | min | max |
|---|---|---|---|---|---|---|---|
| Heart Attack | Mortality | 2720 | 15.4 | 15.4 | 1.5 | 10.1 | 21.9 |
| Heart Attack | Readmission | 2372 | 19.7 | 19.6 | 1.5 | 15.1 | 27.4 |
| Heart Failure | Mortality | 3947 | 11.6 | 11.6 | 1.5 | 6.7 | 18.1 |
| Heart Failure | Readmission | 4025 | 24.8 | 24.6 | 1.9 | 19.0 | 33.6 |
| Pneumonia | Mortality | 4233 | 12.1 | 11.9 | 1.8 | 6.8 | 21.2 |
| Pneumonia | Readmission | 4247 | 18.5 | 18.4 | 1.6 | 14.1 | 25.8 |
ggplot(rate_long, aes(x = rate, fill = outcome)) +
geom_density(alpha = 0.6, color = NA) +
facet_wrap(~condition, ncol = 1, scales = "free_y") +
scale_fill_manual(values = c(Mortality = "#e34a33", Readmission = "#2c7fb8")) +
labs(title = "Distribution of 30-day rates, by condition and outcome",
x = "Rate (%)", y = "Density", fill = NULL) +
theme_minimal(base_size = 12)
A few patterns stand out:
comp_long <- hospitals %>%
select(`Provider Number`, all_of(measures$comp_col)) %>%
pivot_longer(all_of(measures$comp_col), names_to = "comp_col", values_to = "comparison") %>%
left_join(measures, by = "comp_col")
comp_summary <- comp_long %>%
filter(!is.na(comparison)) %>%
count(condition, outcome, comparison) %>%
group_by(condition, outcome) %>%
mutate(share = n / sum(n)) %>%
ungroup()
comp_summary %>%
mutate(label = paste(condition, "-", outcome)) %>%
mutate(comparison = factor(comparison, levels = c(
"Better than U.S. National Rate", "No Different than U.S. National Rate",
"Worse than U.S. National Rate", "Number of Cases Too Small", "Not Available"
))) %>%
ggplot(aes(x = fct_rev(label), y = share, fill = comparison)) +
geom_col(position = "stack") +
coord_flip() +
scale_y_continuous(labels = percent, expand = c(0, 0)) +
scale_fill_manual(values = c(
"Better than U.S. National Rate" = "#2c7fb8",
"No Different than U.S. National Rate" = "#c7e9c0",
"Worse than U.S. National Rate" = "#e34a33",
"Number of Cases Too Small" = "#bdbdbd",
"Not Available" = "#f0f0f0"
)) +
labs(title = "How each hospital compares to the U.S. national rate, by measure",
x = NULL, y = "Share of hospitals", fill = NULL) +
theme_minimal(base_size = 11) +
theme(legend.position = "bottom")
The overwhelming majority of hospitals are statistically indistinguishable from the national average on every measure — CMS’s methodology is deliberately conservative, flagging a hospital as Better or Worse only when the difference clears a confidence threshold. Because of this:
Pneumonia mortality is the best-populated measure in the file, so it’s the most reliable one for a state-level look.
pn_worse_col <- measures %>% filter(key == "pn_mort") %>% pull(comp_col)
worse_by_state <- hospitals %>%
filter(.data[[pn_worse_col]] == "Worse than U.S. National Rate") %>%
count(State, sort = TRUE) %>%
slice_max(n, n = 10)
ggplot(worse_by_state, aes(x = fct_reorder(State, n), y = n)) +
geom_col(fill = "#e34a33") +
geom_text(aes(label = n), hjust = -0.3, size = 3.5) +
coord_flip(clip = "off") +
scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
labs(title = "States with the most hospitals rated 'Worse than U.S. rate'\nfor 30-day pneumonia mortality",
x = NULL, y = "Number of hospitals") +
theme_minimal(base_size = 12)
California, Louisiana, and Texas have the largest raw counts of hospitals flagged as worse-than-average on pneumonia mortality — though this table is a count of hospitals, not a rate, and larger states naturally contribute more hospitals to any count-based ranking. A fair state-to-state comparison would need to divide by each state’s total number of reporting hospitals, which the appendix table below provides.
hospitals %>%
mutate(pn_flag = .data[[pn_worse_col]]) %>%
filter(!is.na(pn_flag)) %>%
group_by(State) %>%
summarise(
n_reporting = n(),
n_worse = sum(pn_flag == "Worse than U.S. National Rate"),
share_worse = percent(n_worse / n_reporting, accuracy = 0.1)
) %>%
filter(n_reporting >= 20) %>%
arrange(desc(n_worse)) %>%
slice_head(n = 10) %>%
kable(col.names = c("State", "Hospitals Reporting", "Rated Worse", "Share Rated Worse"),
caption = "States (≥20 reporting hospitals) with the highest share of pneumonia-mortality hospitals rated worse than the U.S. rate") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
| State | Hospitals Reporting | Rated Worse | Share Rated Worse |
|---|---|---|---|
| CA | 341 | 17 | 5.0% |
| LA | 114 | 13 | 11.4% |
| TX | 370 | 13 | 3.5% |
| GA | 132 | 11 | 8.3% |
| KY | 96 | 11 | 11.5% |
| NC | 112 | 11 | 9.8% |
| IL | 179 | 10 | 5.6% |
| TN | 116 | 9 | 7.8% |
| PA | 175 | 8 | 4.6% |
| VA | 87 | 8 | 9.2% |
A natural question is whether hospitals with higher mortality on a given condition also tend to have higher readmission rates for it — i.e., whether one outcome predicts the other.
mort_read_pairs <- list(
"Heart Attack" = c("ha_mort", "ha_read"),
"Heart Failure" = c("hf_mort", "hf_read"),
"Pneumonia" = c("pn_mort", "pn_read")
)
pair_data <- purrr::imap_dfr(mort_read_pairs, function(keys, cond) {
mort_col <- measures %>% filter(key == keys[1]) %>% pull(rate_col)
read_col <- measures %>% filter(key == keys[2]) %>% pull(rate_col)
hospitals %>%
select(mortality = all_of(mort_col), readmission = all_of(read_col)) %>%
filter(!is.na(mortality), !is.na(readmission)) %>%
mutate(condition = cond)
})
cor_labels <- pair_data %>%
group_by(condition) %>%
summarise(r = cor(mortality, readmission)) %>%
mutate(label = paste0("r = ", round(r, 2)))
ggplot(pair_data, aes(x = mortality, y = readmission)) +
geom_point(alpha = 0.15, size = 0.8, color = "#2c7fb8") +
geom_smooth(method = "lm", color = "#e34a33", se = FALSE, linewidth = 0.8) +
geom_text(data = cor_labels, aes(x = -Inf, y = Inf, label = label),
hjust = -0.15, vjust = 1.5, size = 4, inherit.aes = FALSE) +
facet_wrap(~condition, scales = "free") +
labs(title = "30-day mortality rate vs. 30-day readmission rate, by hospital",
x = "Mortality rate (%)", y = "Readmission rate (%)") +
theme_minimal(base_size = 12)
The correlation between a hospital’s mortality rate and its readmission rate is essentially zero for every condition (r ≈ 0.02–0.03). This is a genuinely useful finding: a hospital that does well (or poorly) on one 30-day outcome tells you almost nothing about how it will do on the other. Mortality and readmission appear to be measuring largely independent aspects of care quality, which is part of why CMS reports and rewards them as separate measures rather than folding them into a single score.
hospitals %>%
select(`Hospital Name`, City, State, `County Name`,
`Hospital 30-Day Death (Mortality) Rates from Heart Attack`,
`Hospital 30-Day Death (Mortality) Rates from Heart Failure`,
`Hospital 30-Day Death (Mortality) Rates from Pneumonia`,
`Hospital 30-Day Readmission Rates from Heart Attack`,
`Hospital 30-Day Readmission Rates from Heart Failure`,
`Hospital 30-Day Readmission Rates from Pneumonia`) %>%
rename(
"AMI Mortality %" = `Hospital 30-Day Death (Mortality) Rates from Heart Attack`,
"HF Mortality %" = `Hospital 30-Day Death (Mortality) Rates from Heart Failure`,
"PN Mortality %" = `Hospital 30-Day Death (Mortality) Rates from Pneumonia`,
"AMI Readmit %" = `Hospital 30-Day Readmission Rates from Heart Attack`,
"HF Readmit %" = `Hospital 30-Day Readmission Rates from Heart Failure`,
"PN Readmit %" = `Hospital 30-Day Readmission Rates from Pneumonia`
) %>%
datatable(options = list(pageLength = 10, scrollX = TRUE), rownames = FALSE, filter = "top")
This file reports process-adjacent outcome measures, not raw clinical data — rates are already risk-adjusted by CMS, and the underlying patient-level records aren’t available here. It also doesn’t include hospital type, ownership, or bed count, so outcome patterns can’t be directly tied to those structural factors without joining against a separate facility file (such as the hospital directory analyzed previously). Suppressed (“Not Available” / “Number of Cases Too Small”) rates should not be treated as zero or excluded silently from claims about overall U.S. performance — they simply reflect insufficient volume for a reliable estimate.
Report generated in R Markdown. To publish: open this file in
RStudio with outcome-of-care-measures.csv in the same
folder, click Knit, then use the
Publish button (top right of the preview pane) to push
directly to RPubs.