# Install any missing packages before loading
required_packages <- c(
"readxl", "dplyr", "tidyr", "ggplot2", "scales",
"pROC", "ResourceSelection", "car",
"gtsummary", "flextable", "knitr", "broom.helpers"
)
installed <- rownames(installed.packages())
to_install <- required_packages[!required_packages %in% installed]
if (length(to_install) > 0) install.packages(to_install)
library(readxl)
library(dplyr)
library(tidyr)
library(ggplot2)
library(scales)
library(pROC)
library(ResourceSelection)
library(car)
library(gtsummary)
library(flextable)
library(knitr)
df <- read_excel("Data for analysis.xlsx",
sheet = "2025 cohort_merged",
na = c("", "NULL", "#VALUE!")) %>%
select(-`...10`, -`...18`) %>%
rename(
id = `MCS ID`,
time_code = `Time code`,
weekday = `Weekday code`,
arrival = `Arrival code`,
triage = `Triage code`,
age = `Age code`,
sex = `Sex code`,
destiny = `Destiny integer`,
prior_adm = `Admission is past 30 days`,
s_age = `S(AGE)`,
s_arrival = `S(ARRIVAL)`,
s_triage = `S(TRIAGE)`,
s_time = `S(TIME)`,
s_prior_adm = `S(Prior admission)`,
s_pp = `S(PRESENTING PROBLEM)`,
start_score = `START SCORE`,
rrc = RRC,
cb = CB,
icu = `ICU Admission`,
mortality = Mortality,
deterioration = Deterioration
)
df <- df %>%
# Remove any rows with missing outcome or score
filter(!is.na(start_score), !is.na(deterioration)) %>%
# Remove 3 rows coded as unknown sex (sex == 0)
filter(!(sex == 0)) %>%
# Set factor types
mutate(
across(c(time_code, weekday, arrival, triage, age, sex,
destiny, prior_adm), as.factor),
across(c(rrc, cb, icu, mortality, deterioration), as.integer)
) %>%
# Apply factor labels
mutate(
age = factor(age,
levels = c(1, 2, 3, 4, 5),
labels = c("16-19", "20-39", "40-59", "60-79", "80+")),
sex = factor(sex,
levels = c(1, 2),
labels = c("Male", "Female")),
triage = factor(triage,
levels = c(3, 4, 5),
labels = c("Urgent", "Semi-Urgent", "Non-Urgent")),
time_code = factor(time_code,
levels = c(1, 2, 3),
labels = c("0800-1759", "1800-2259", "2300-0759")),
weekday = factor(weekday,
levels = c(1, 2, 3, 4, 5, 6, 7),
labels = c("Mon","Tue","Wed","Thu","Fri","Sat","Sun")),
arrival = factor(arrival,
levels = c(1, 2, 3, 4, 5, 6, 7, 9, 10, 11),
labels = c("Ambulance", "Public transport", "Private vehicle",
"Helicopter", "Air ambulance", "Internal ambulance",
"Police", "Walked in", "Retrieval team",
"Internal bed")),
destiny = factor(destiny,
levels = c(1, 2, 3, 4, 5, 6, 9, 10, 11, 12,
13, 14, 15, 17, 18, 19, 20),
labels = c("Abdominal/GI", "Cardiovascular",
"General symptoms", "Febrile illness",
"Injury", "Respiratory", "Musculoskeletal",
"Neurological", "Mental health", "Toxicological",
"ENT/eye/head & neck", "Administrative",
"Genitourinary", "Endocrine",
"Obstetrics/Gynaecology", "Skin/allergy",
"Other medical")),
prior_adm = factor(prior_adm,
levels = c(0, 1),
labels = c("No", "Yes")),
# Binary ambulance variable
# Collapses all ambulance-type arrivals; avoids near-complete separation
# from very small cells (helicopter n=11, air ambulance n=2, etc.)
ambulance = factor(
ifelse(arrival %in% c("Ambulance", "Helicopter", "Air ambulance",
"Internal ambulance", "Retrieval team"),
"Yes", "No"),
levels = c("No", "Yes")
),
# Labelled outcome factor for tables
deterioration_label = factor(deterioration,
levels = c(0, 1),
labels = c("No", "Yes"))
)
cat("Total encounters:", nrow(df), "\n")
## Total encounters: 50152
cat("Deterioration events:", sum(df$deterioration), "\n")
## Deterioration events: 1326
cat("Event rate:", round(mean(df$deterioration) * 100, 2), "%\n")
## Event rate: 2.64 %
table1 <- df %>%
select(age, sex, arrival, triage, time_code, prior_adm,
destiny, start_score, deterioration_label) %>%
tbl_summary(
by = deterioration_label,
label = list(
age ~ "Age group",
sex ~ "Sex",
arrival ~ "Mode of arrival",
triage ~ "Triage category",
time_code ~ "Time of arrival",
prior_adm ~ "Prior hospital admission (30 days)",
destiny ~ "Presenting problem category",
start_score ~ "START score"
),
statistic = list(
all_continuous() ~ "{mean} ({sd})",
all_categorical() ~ "{n} ({p}%)"
),
digits = list(
all_continuous() ~ 1,
all_categorical() ~ c(0, 1)
),
missing = "no"
) %>%
add_overall() %>%
add_p(
test = list(
all_continuous() ~ "t.test",
all_categorical() ~ "chisq.test"
)
) %>%
bold_labels() %>%
bold_p(t = 0.05) %>%
modify_header(
label ~ "**Variable**",
stat_1 ~ "**No deterioration** \nn = {n}",
stat_2 ~ "**Deterioration** \nn = {n}",
stat_0 ~ "**Overall** \nn = {n}"
) %>%
modify_caption("**Table 1.** Baseline characteristics stratified by clinical deterioration")
table1
| Variable | Overall n = 501521 |
No deterioration n = 488261 |
Deterioration n = 13261 |
p-value2 |
|---|---|---|---|---|
| Age group | <0.001 | |||
| Â Â Â Â 16-19 | 3,020 (6.0%) | 2,996 (6.1%) | 24 (1.8%) | |
| Â Â Â Â 20-39 | 19,360 (38.6%) | 19,214 (39.4%) | 146 (11.0%) | |
| Â Â Â Â 40-59 | 13,943 (27.8%) | 13,701 (28.1%) | 242 (18.3%) | |
| Â Â Â Â 60-79 | 9,953 (19.8%) | 9,438 (19.3%) | 515 (38.8%) | |
| Â Â Â Â 80+ | 3,876 (7.7%) | 3,477 (7.1%) | 399 (30.1%) | |
| Sex | 0.004 | |||
| Â Â Â Â Male | 24,148 (48.1%) | 23,458 (48.0%) | 690 (52.0%) | |
| Â Â Â Â Female | 26,004 (51.9%) | 25,368 (52.0%) | 636 (48.0%) | |
| Mode of arrival | <0.001 | |||
| Â Â Â Â Ambulance | 10,135 (20.2%) | 9,436 (19.3%) | 699 (52.7%) | |
| Â Â Â Â Public transport | 232 (0.5%) | 231 (0.5%) | 1 (0.1%) | |
| Â Â Â Â Private vehicle | 38,571 (76.9%) | 37,989 (77.8%) | 582 (43.9%) | |
| Â Â Â Â Helicopter | 11 (0.0%) | 11 (0.0%) | 0 (0.0%) | |
| Â Â Â Â Air ambulance | 2 (0.0%) | 2 (0.0%) | 0 (0.0%) | |
| Â Â Â Â Internal ambulance | 155 (0.3%) | 135 (0.3%) | 20 (1.5%) | |
| Â Â Â Â Police | 515 (1.0%) | 508 (1.0%) | 7 (0.5%) | |
| Â Â Â Â Walked in | 272 (0.5%) | 270 (0.6%) | 2 (0.2%) | |
| Â Â Â Â Retrieval team | 3 (0.0%) | 3 (0.0%) | 0 (0.0%) | |
| Â Â Â Â Internal bed | 256 (0.5%) | 241 (0.5%) | 15 (1.1%) | |
| Triage category | <0.001 | |||
| Â Â Â Â Urgent | 28,326 (56.5%) | 27,250 (55.8%) | 1,076 (81.1%) | |
| Â Â Â Â Semi-Urgent | 17,734 (35.4%) | 17,501 (35.8%) | 233 (17.6%) | |
| Â Â Â Â Non-Urgent | 4,092 (8.2%) | 4,075 (8.3%) | 17 (1.3%) | |
| Time of arrival | <0.001 | |||
| Â Â Â Â 0800-1759 | 29,214 (58.3%) | 28,358 (58.1%) | 856 (64.6%) | |
| Â Â Â Â 1800-2259 | 11,588 (23.1%) | 11,333 (23.2%) | 255 (19.2%) | |
| Â Â Â Â 2300-0759 | 9,350 (18.6%) | 9,135 (18.7%) | 215 (16.2%) | |
| Prior hospital admission (30 days) | 2,705 (5.4%) | 2,546 (5.2%) | 159 (12.0%) | <0.001 |
| Presenting problem category | <0.001 | |||
| Â Â Â Â Abdominal/GI | 9,842 (19.6%) | 9,534 (19.5%) | 308 (23.2%) | |
| Â Â Â Â Cardiovascular | 1,170 (2.3%) | 1,140 (2.3%) | 30 (2.3%) | |
| Â Â Â Â General symptoms | 5,706 (11.4%) | 5,386 (11.0%) | 320 (24.1%) | |
| Â Â Â Â Febrile illness | 1,287 (2.6%) | 1,221 (2.5%) | 66 (5.0%) | |
| Â Â Â Â Injury | 6,621 (13.2%) | 6,559 (13.4%) | 62 (4.7%) | |
| Â Â Â Â Respiratory | 2,228 (4.4%) | 2,070 (4.2%) | 158 (11.9%) | |
| Â Â Â Â Musculoskeletal | 4,163 (8.3%) | 4,080 (8.4%) | 83 (6.3%) | |
| Â Â Â Â Neurological | 3,394 (6.8%) | 3,315 (6.8%) | 79 (6.0%) | |
| Â Â Â Â Mental health | 2,146 (4.3%) | 2,115 (4.3%) | 31 (2.3%) | |
| Â Â Â Â Toxicological | 300 (0.6%) | 297 (0.6%) | 3 (0.2%) | |
| Â Â Â Â ENT/eye/head & neck | 4,941 (9.9%) | 4,898 (10.0%) | 43 (3.2%) | |
| Â Â Â Â Administrative | 1,699 (3.4%) | 1,656 (3.4%) | 43 (3.2%) | |
| Â Â Â Â Genitourinary | 2,038 (4.1%) | 2,001 (4.1%) | 37 (2.8%) | |
| Â Â Â Â Endocrine | 113 (0.2%) | 104 (0.2%) | 9 (0.7%) | |
| Â Â Â Â Obstetrics/Gynaecology | 1,841 (3.7%) | 1,830 (3.7%) | 11 (0.8%) | |
| Â Â Â Â Skin/allergy | 2,650 (5.3%) | 2,608 (5.3%) | 42 (3.2%) | |
| Â Â Â Â Other medical | 13 (0.0%) | 12 (0.0%) | 1 (0.1%) | |
| START score | 11.2 (6.7) | 11.0 (6.6) | 18.3 (5.4) | <0.001 |
| 1 n (%); Mean (SD) | ||||
| 2 Pearson’s Chi-squared test; Welch Two Sample t-test | ||||
outcomes <- df %>%
summarise(
`RRC activation` = sum(rrc),
`Code Blue` = sum(cb),
`ICU admission` = sum(icu),
`In-hospital death` = sum(mortality),
`Composite (any)` = sum(deterioration)
) %>%
pivot_longer(everything(), names_to = "Outcome", values_to = "n") %>%
mutate(
Total = nrow(df),
`Rate (%)` = round(n / Total * 100, 2)
) %>%
select(Outcome, n, `Rate (%)`)
kable(outcomes,
caption = "Table 1b. Composite outcome component frequencies",
align = "lrr")
| Outcome | n | Rate (%) |
|---|---|---|
| RRC activation | 1056 | 2.11 |
| Code Blue | 385 | 0.77 |
| ICU admission | 115 | 0.23 |
| In-hospital death | 219 | 0.44 |
| Composite (any) | 1326 | 2.64 |
roc_start <- roc(deterioration ~ start_score,
data = df,
ci = TRUE)
cat("START score AUC:", round(auc(roc_start), 4), "\n")
## START score AUC: 0.8038
cat("95% CI (DeLong):", round(ci(roc_start)[1], 4),
"-", round(ci(roc_start)[3], 4), "\n")
## 95% CI (DeLong): 0.7927 - 0.8149
plot(roc_start,
print.auc = TRUE,
auc.polygon = TRUE,
grid = TRUE,
col = "#2C7BB6",
auc.polygon.col = "#D7EAF5",
main = "ROC Curve: START Score vs Clinical Deterioration",
xlab = "1 - Specificity",
ylab = "Sensitivity")
# Youden optimal cutpoint
youden_best <- coords(roc_start, "best", best.method = "youden",
ret = c("threshold", "sensitivity", "specificity",
"ppv", "npv"))
# Clinically relevant thresholds for the table
thresh_raw <- coords(roc_start,
x = c(10, 13, 15, 20),
ret = c("threshold", "sensitivity", "specificity",
"ppv", "npv"))
thresh_table <- thresh_raw %>%
mutate(
threshold = as.integer(threshold),
sensitivity = round(sensitivity * 100, 1),
specificity = round(specificity * 100, 1),
ppv = round(ppv * 100, 1),
npv = round(npv * 100, 1)
) %>%
rename(
`Threshold` = threshold,
`Sensitivity (%)` = sensitivity,
`Specificity (%)` = specificity,
`PPV (%)` = ppv,
`NPV (%)` = npv
)
kable(thresh_table,
caption = paste0(
"Table 2. START score diagnostic performance at selected thresholds. ",
"Youden-optimal cutpoint = ", round(youden_best$threshold, 0),
" (sensitivity ", round(youden_best$sensitivity * 100, 1),
"%, specificity ", round(youden_best$specificity * 100, 1), "%). ",
"AUC = ", round(auc(roc_start), 3),
" (95% CI ", round(ci(roc_start)[1], 3),
"-", round(ci(roc_start)[3], 3), ")"
),
align = "rrrrr")
| Threshold | Sensitivity (%) | Specificity (%) | PPV (%) | NPV (%) |
|---|---|---|---|---|
| 10 | 92.5 | 41.9 | 4.1 | 99.5 |
| 13 | 85.1 | 57.3 | 5.1 | 99.3 |
| 15 | 77.7 | 68.6 | 6.3 | 99.1 |
| 20 | 44.3 | 89.7 | 10.5 | 98.3 |
model_cal <- glm(deterioration ~ start_score, data = df, family = binomial)
# Hosmer-Lemeshow test
hl <- hoslem.test(df$deterioration, fitted(model_cal), g = 10)
cat("Hosmer-Lemeshow: X² =", round(hl$statistic, 3),
"| df =", hl$parameter,
"| p =", format.pval(hl$p.value, digits = 3), "\n")
## Hosmer-Lemeshow: X² = 31.977 | df = 8 | p = 9.4e-05
cat("Note: HL p-value is expected to be significant at n ≈", nrow(df),
"— use calibration plot to assess clinical meaningfulness.\n")
## Note: HL p-value is expected to be significant at n ≈ 50152 — use calibration plot to assess clinical meaningfulness.
# Calibration plot (observed vs predicted by decile)
df$predicted_cal <- predict(model_cal, type = "response")
cal_data <- df %>%
mutate(decile = ntile(predicted_cal, 10)) %>%
group_by(decile) %>%
summarise(
mean_predicted = mean(predicted_cal),
observed_rate = mean(deterioration),
n = n(),
.groups = "drop"
)
ggplot(cal_data, aes(x = mean_predicted, y = observed_rate)) +
geom_abline(intercept = 0, slope = 1,
linetype = "dashed", colour = "red", linewidth = 0.8) +
geom_point(size = 3, colour = "#2C7BB6") +
scale_x_continuous(labels = percent_format(accuracy = 0.1)) +
scale_y_continuous(labels = percent_format(accuracy = 0.1)) +
labs(
title = "Calibration Plot: START Score",
subtitle = paste0("Points represent risk deciles. Dashed line = perfect calibration.\n",
"Hosmer-Lemeshow p ", format.pval(hl$p.value, digits = 2),
" (large-sample artefact)"),
x = "Mean Predicted Probability",
y = "Observed Deterioration Rate"
) +
theme_bw(base_size = 12)
Each START component variable entered separately.
uv_table <- df %>%
select(deterioration, age, sex, ambulance, triage,
time_code, prior_adm, destiny) %>%
tbl_uvregression(
method = glm,
y = deterioration,
method.args = list(family = binomial),
exponentiate = TRUE,
label = list(
age ~ "Age group",
sex ~ "Sex",
ambulance ~ "Ambulance arrival",
triage ~ "Triage category",
time_code ~ "Time of arrival",
prior_adm ~ "Prior hospital admission (30 days)",
destiny ~ "Presenting problem category"
),
hide_n = TRUE
) %>%
bold_labels() %>%
bold_p(t = 0.05) %>%
modify_header(estimate ~ "**OR**") %>%
modify_caption(
"**Table 3.** Univariable logistic regression: association of each variable with clinical deterioration"
)
uv_table
| Characteristic | OR | 95% CI | p-value |
|---|---|---|---|
| Age group | |||
|     16-19 | — | — | |
| Â Â Â Â 20-39 | 0.95 | 0.63, 1.50 | 0.8 |
| Â Â Â Â 40-59 | 2.20 | 1.48, 3.45 | <0.001 |
| Â Â Â Â 60-79 | 6.81 | 4.62, 10.6 | <0.001 |
| Â Â Â Â 80+ | 14.3 | 9.68, 22.3 | <0.001 |
| Sex | |||
|     Male | — | — | |
| Â Â Â Â Female | 0.85 | 0.76, 0.95 | 0.004 |
| Ambulance arrival | |||
|     No | — | — | |
| Â Â Â Â Yes | 4.85 | 4.34, 5.41 | <0.001 |
| Triage category | |||
|     Urgent | — | — | |
| Â Â Â Â Semi-Urgent | 0.34 | 0.29, 0.39 | <0.001 |
| Â Â Â Â Non-Urgent | 0.11 | 0.06, 0.17 | <0.001 |
| Time of arrival | |||
|     0800-1759 | — | — | |
| Â Â Â Â 1800-2259 | 0.75 | 0.65, 0.86 | <0.001 |
| Â Â Â Â 2300-0759 | 0.78 | 0.67, 0.91 | 0.001 |
| Prior hospital admission (30 days) | |||
|     No | — | — | |
| Â Â Â Â Yes | 2.48 | 2.08, 2.93 | <0.001 |
| Presenting problem category | |||
|     Abdominal/GI | — | — | |
| Â Â Â Â Cardiovascular | 0.81 | 0.55, 1.17 | 0.3 |
| Â Â Â Â General symptoms | 1.84 | 1.57, 2.16 | <0.001 |
| Â Â Â Â Febrile illness | 1.67 | 1.26, 2.18 | <0.001 |
| Â Â Â Â Injury | 0.29 | 0.22, 0.38 | <0.001 |
| Â Â Â Â Respiratory | 2.36 | 1.94, 2.87 | <0.001 |
| Â Â Â Â Musculoskeletal | 0.63 | 0.49, 0.80 | <0.001 |
| Â Â Â Â Neurological | 0.74 | 0.57, 0.94 | 0.017 |
| Â Â Â Â Mental health | 0.45 | 0.31, 0.65 | <0.001 |
| Â Â Â Â Toxicological | 0.31 | 0.08, 0.82 | 0.046 |
| Â Â Â Â ENT/eye/head & neck | 0.27 | 0.19, 0.37 | <0.001 |
| Â Â Â Â Administrative | 0.80 | 0.57, 1.10 | 0.2 |
| Â Â Â Â Genitourinary | 0.57 | 0.40, 0.80 | 0.001 |
| Â Â Â Â Endocrine | 2.68 | 1.25, 5.05 | 0.005 |
| Â Â Â Â Obstetrics/Gynaecology | 0.19 | 0.10, 0.32 | <0.001 |
| Â Â Â Â Skin/allergy | 0.50 | 0.36, 0.68 | <0.001 |
| Â Â Â Â Other medical | 2.58 | 0.14, 13.2 | 0.4 |
| Abbreviations: CI = Confidence Interval, OR = Odds Ratio | |||
Backwards stepwise selection from full model (AIC criterion). Weekday excluded a priori (p = 0.231 in Table 1; no clinical or statistical justification for inclusion). Mode of arrival collapsed to binary ambulance variable to avoid near-complete separation from small arrival categories.
mv_full <- glm(
deterioration ~ age + sex + ambulance + triage + time_code + prior_adm + destiny,
data = df,
family = binomial
)
# Backwards stepwise
mv_step <- step(mv_full, direction = "backward", trace = 0)
cat("Variables retained after backwards stepwise selection:\n")
## Variables retained after backwards stepwise selection:
print(formula(mv_step))
## deterioration ~ age + sex + ambulance + triage + time_code +
## prior_adm + destiny
vif_result <- vif(mv_step)
kable(
as.data.frame(vif_result),
caption = "Generalised Variance Inflation Factors (GVIF) — values near 1 indicate no multicollinearity",
digits = 2
)
| GVIF | Df | GVIF^(1/(2*Df)) | |
|---|---|---|---|
| age | 1.25 | 4 | 1.03 |
| sex | 1.03 | 1 | 1.01 |
| ambulance | 1.23 | 1 | 1.11 |
| triage | 1.13 | 2 | 1.03 |
| time_code | 1.04 | 2 | 1.01 |
| prior_adm | 1.02 | 1 | 1.01 |
| destiny | 1.30 | 16 | 1.01 |
mv_table <- tbl_regression(
mv_step,
exponentiate = TRUE,
label = list(
age ~ "Age group",
sex ~ "Sex",
ambulance ~ "Ambulance arrival",
triage ~ "Triage category",
time_code ~ "Time of arrival",
prior_adm ~ "Prior hospital admission (30 days)",
destiny ~ "Presenting problem category"
)
) %>%
bold_labels() %>%
bold_p(t = 0.05) %>%
modify_header(estimate ~ "**Adjusted OR**") %>%
modify_caption(
"**Table 4.** Multivariable logistic regression: independent predictors of clinical deterioration"
)
mv_table
| Characteristic | Adjusted OR | 95% CI | p-value |
|---|---|---|---|
| Age group | |||
|     16-19 | — | — | |
| Â Â Â Â 20-39 | 0.87 | 0.58, 1.38 | 0.5 |
| Â Â Â Â 40-59 | 1.73 | 1.15, 2.70 | 0.012 |
| Â Â Â Â 60-79 | 3.95 | 2.66, 6.14 | <0.001 |
| Â Â Â Â 80+ | 5.61 | 3.75, 8.79 | <0.001 |
| Sex | |||
|     Male | — | — | |
| Â Â Â Â Female | 0.84 | 0.75, 0.95 | 0.004 |
| Ambulance arrival | |||
|     No | — | — | |
| Â Â Â Â Yes | 2.52 | 2.23, 2.86 | <0.001 |
| Triage category | |||
|     Urgent | — | — | |
| Â Â Â Â Semi-Urgent | 0.48 | 0.41, 0.56 | <0.001 |
| Â Â Â Â Non-Urgent | 0.18 | 0.11, 0.28 | <0.001 |
| Time of arrival | |||
|     0800-1759 | — | — | |
| Â Â Â Â 1800-2259 | 0.81 | 0.70, 0.94 | 0.005 |
| Â Â Â Â 2300-0759 | 0.76 | 0.65, 0.89 | <0.001 |
| Prior hospital admission (30 days) | |||
|     No | — | — | |
| Â Â Â Â Yes | 1.39 | 1.16, 1.66 | <0.001 |
| Presenting problem category | |||
|     Abdominal/GI | — | — | |
| Â Â Â Â Cardiovascular | 0.55 | 0.36, 0.79 | 0.002 |
| Â Â Â Â General symptoms | 1.64 | 1.38, 1.94 | <0.001 |
| Â Â Â Â Febrile illness | 1.58 | 1.18, 2.09 | 0.002 |
| Â Â Â Â Injury | 0.44 | 0.33, 0.57 | <0.001 |
| Â Â Â Â Respiratory | 1.39 | 1.13, 1.71 | 0.002 |
| Â Â Â Â Musculoskeletal | 0.75 | 0.58, 0.96 | 0.028 |
| Â Â Â Â Neurological | 0.57 | 0.44, 0.73 | <0.001 |
| Â Â Â Â Mental health | 0.53 | 0.35, 0.76 | <0.001 |
| Â Â Â Â Toxicological | 0.29 | 0.07, 0.78 | 0.036 |
| Â Â Â Â ENT/eye/head & neck | 0.44 | 0.31, 0.60 | <0.001 |
| Â Â Â Â Administrative | 0.98 | 0.69, 1.36 | >0.9 |
| Â Â Â Â Genitourinary | 0.44 | 0.31, 0.62 | <0.001 |
| Â Â Â Â Endocrine | 1.56 | 0.72, 3.01 | 0.2 |
| Â Â Â Â Obstetrics/Gynaecology | 0.48 | 0.24, 0.84 | 0.017 |
| Â Â Â Â Skin/allergy | 0.92 | 0.65, 1.27 | 0.6 |
| Â Â Â Â Other medical | 4.78 | 0.25, 27.5 | 0.2 |
| Abbreviations: CI = Confidence Interval, OR = Odds Ratio | |||
df$mv_predicted <- predict(mv_step, type = "response")
roc_mv <- roc(deterioration ~ mv_predicted, data = df, ci = TRUE)
cat("MV model AUC:", round(auc(roc_mv), 4), "\n")
## MV model AUC: 0.828
cat("95% CI (DeLong):", round(ci(roc_mv)[1], 4),
"-", round(ci(roc_mv)[3], 4), "\n")
## 95% CI (DeLong): 0.8176 - 0.8384
DeLong’s test for correlated ROC curves (appropriate when both models are evaluated on the same dataset).
roc_test <- roc.test(roc_start, roc_mv)
# Summary table
comparison <- data.frame(
Model = c("START composite score", "Multivariable model"),
AUC = c(round(auc(roc_start), 4), round(auc(roc_mv), 4)),
`95% CI` = c(
paste0(round(ci(roc_start)[1], 3), " – ", round(ci(roc_start)[3], 3)),
paste0(round(ci(roc_mv)[1], 3), " – ", round(ci(roc_mv)[3], 3))
),
check.names = FALSE
)
kable(comparison,
caption = paste0(
"Table 5. AUC comparison. DeLong's test: Z = ",
round(roc_test$statistic, 2),
", p ", format.pval(roc_test$p.value, digits = 3),
" (difference = ", round(auc(roc_mv) - auc(roc_start), 4),
", 95% CI ",
round(roc_test$conf.int[1], 4), " – ",
round(roc_test$conf.int[2], 4), ")"
),
align = "lrr")
| Model | AUC | 95% CI |
|---|---|---|
| START composite score | 0.8038 | 0.793 – 0.815 |
| Multivariable model | 0.8280 | 0.818 – 0.838 |
# Overlay ROC curves
plot(roc_start,
col = "#2C7BB6",
lwd = 2,
main = "ROC Curves: START Score vs Multivariable Model")
plot(roc_mv,
col = "#D7191C",
lwd = 2,
add = TRUE)
legend("bottomright",
legend = c(
paste0("START score (AUC ", round(auc(roc_start), 3), ")"),
paste0("MV model (AUC ", round(auc(roc_mv), 3), ")")
),
col = c("#2C7BB6", "#D7191C"),
lwd = 2,
bty = "n")
sessionInfo()
## R version 4.6.1 (2026-06-24)
## Platform: aarch64-apple-darwin23
## Running under: macOS Tahoe 26.5.1
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: Australia/Sydney
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] knitr_1.51 flextable_0.9.12 gtsummary_2.5.1
## [4] car_3.1-5 carData_3.0-6 ResourceSelection_0.3-6
## [7] pROC_1.19.0.1 scales_1.4.0 ggplot2_4.0.3
## [10] tidyr_1.3.2 dplyr_1.2.1 readxl_1.5.0
##
## loaded via a namespace (and not attached):
## [1] gtable_0.3.6 xfun_0.59 bslib_0.11.0
## [4] lattice_0.22-9 vctrs_0.7.3 tools_4.6.1
## [7] generics_0.1.4 tibble_3.3.1 pkgconfig_2.0.3
## [10] Matrix_1.7-5 data.table_1.18.4 RColorBrewer_1.1-3
## [13] S7_0.2.2 uuid_1.2-2 gt_1.3.0
## [16] lifecycle_1.0.5 compiler_4.6.1 farver_2.1.2
## [19] stringr_1.6.0 textshaping_1.0.5 litedown_0.9
## [22] fontquiver_0.2.1 fontLiberation_0.1.0 htmltools_0.5.9
## [25] sass_0.4.10 yaml_2.3.12 Formula_1.2-5
## [28] pillar_1.11.1 jquerylib_0.1.4 broom.helpers_1.22.0
## [31] openssl_2.4.2 cachem_1.1.0 abind_1.4-8
## [34] fontBitstreamVera_0.1.1 commonmark_2.0.0 tidyselect_1.2.1
## [37] zip_3.0.0 digest_0.6.39 stringi_1.8.7
## [40] purrr_1.2.2 forcats_1.0.1 labeling_0.4.3
## [43] labelled_2.16.0 fastmap_1.2.0 grid_4.6.1
## [46] cli_3.6.6 magrittr_2.0.5 cards_0.8.0
## [49] broom_1.0.13 withr_3.0.3 gdtools_0.5.1
## [52] backports_1.5.1 cardx_0.3.3 rmarkdown_2.31
## [55] officer_0.7.5 cellranger_1.1.0 hms_1.1.4
## [58] askpass_1.2.1 ragg_1.5.2 evaluate_1.0.5
## [61] haven_2.5.5 markdown_2.0 rlang_1.2.0
## [64] Rcpp_1.1.1-1.1 glue_1.8.1 xml2_1.6.0
## [67] rstudioapi_0.19.0 jsonlite_2.0.0 R6_2.6.1
## [70] systemfonts_1.3.2 fs_2.1.0