library(tidyverse)
library(purrr)
library(gsheet)
library(raster)
library(ncdf4)
library(lubridate)
library(readxl)
library(writexl)
library(caret)
library(tidyr)
library(r4pde)
library(refund)
library(readr)
library(fdatest)
library(dplyr)
library(rlang)
library(rms)
library(pROC)
library(PresenceAbsence)
library(OptimalCutpoints)
library(ggtext)
library(scales)
library(PRROC)
library(patchwork)
Logistic and Ensemble Models
Libraries
Load Required Libraries
Data
# Read datasets
<- read_xlsx("plan/weather_data_final.xlsx")
data <- read_csv("plan/weather_data_nasa.csv")
data_nasa
# Remove studies 126 to 150
<- data %>% filter(!study %in% 126:150)
data <- data_nasa %>% filter(!study %in% 126:150)
data_nasa
# Read predictor
<- read_xlsx("plan/df_predictors.xlsx") df_predictors
Logistic Models
# Set up datadist for rms
<- datadist(df_predictors)
dd options(datadist = "dd")
# Convert epidemic to numeric
<- as.numeric(as.character(df_predictors$epidemic))
obs
<- nrow(df_predictors) n
Fit logistic regression models using predictors of interest. Restricted cubic splines are applied where appropriate to allow for non-linear effects.
Logistic model 1 (LM1)
# Fit logistic model with restricted cubic splines
<- lrm(factor(epidemic) ~ tmin + rcs(rh, 4),
m_logistic data = df_predictors, x = TRUE, y = TRUE)
Logistic model 2 (LM2)
# Fit logistic model with restricted cubic splines
<- lrm(factor(epidemic) ~ rcs(rh, 4) + rcs(dew, 3),
m_logistic2 data = df_predictors, x = TRUE, y = TRUE)
Logistic model 3 (LM3)
# Fit logistic model with restricted cubic splines
<- lrm(factor(epidemic) ~ tmin + prec2,
m_logistic3 data = df_predictors, x = TRUE, y = TRUE)
LM performance
Evaluate models fit using Cox-Snell and Nagelkerke R², Brier Score, ROC-AUC, optimal classification threshold, accuracy, and confusion matrix.
<- function(model, data, resp_col, B_boot = 1000) {
evaluate_logistic <- nrow(data)
n <- data[[resp_col]]
actual <- predict(model, type = "fitted")
predicted_prob
# Log-likelihoods
<- logLik(glm(as.formula(paste(resp_col, "~ 1")), data = data, family = binomial()))
ll_null <- logLik(model)
ll_full
# R²
<- 1 - exp((2 / n) * (ll_null - ll_full))
cs_r2 <- cs_r2 / (1 - exp((2 / n) * as.numeric(ll_null)))
nag_r2
# Brier
<- mean((predicted_prob - actual)^2)
brier
# ROC-AUC
<- pROC::roc(actual, predicted_prob)
roc_obj <- pROC::auc(roc_obj)
auc_val
# Optimal threshold
<- data.frame(1, actual, predicted_prob)
preds <- optimal.thresholds(preds)$predicted_prob[3]
opt_thresh <- ifelse(predicted_prob > opt_thresh, 1, 0)
predicted_class <- mean(predicted_class == actual)
accuracy
# Confusion matrix
<- caret::confusionMatrix(
conf factor(predicted_class),
factor(actual),
mode = "everything",
positive = "1"
)
# PR-AUC via bootstrap
<- function(y, p) {
pr_auc_fun <- as.integer(y)
y if (length(unique(y)) < 2) return(NA_real_)
::pr.curve(scores.class0 = p[y == 1],
PRROCscores.class1 = p[y == 0],
curve = FALSE)$auc.integral
}
<- pr_auc_fun(actual, predicted_prob)
pr_apparent <- numeric(B_boot)
opt_vec for (b in 1:B_boot) {
<- sample.int(n, replace = TRUE)
idx_boot <- data[idx_boot, , drop = FALSE]
dat_boot <- update(model, data = dat_boot)
fit_b <- dat_boot[[resp_col]]
y_boot <- predict(fit_b, type = "fitted")
p_boot <- predict(fit_b, newdata = data, type = "fitted")
p_test_orig <- pr_auc_fun(y_boot, p_boot) - pr_auc_fun(actual, p_test_orig)
opt_vec[b]
}<- pr_apparent - mean(na.omit(opt_vec))
pr_corrected
list(
cs_r2 = cs_r2,
nag_r2 = nag_r2,
brier = brier,
auc_roc = auc_val,
accuracy = accuracy,
conf_matrix = conf,
pr_auc = pr_corrected,
opt_threshold = opt_thresh
) }
LM1
<- evaluate_logistic(m_logistic, df_predictors, "epidemic") res_LM1
LM2
<- evaluate_logistic(m_logistic2, df_predictors, "epidemic") res_LM2
LM3
<- evaluate_logistic(m_logistic3, df_predictors, "epidemic") res_LM3
Models validation
Perform internal validation using bootstrap and cross-validation. PR-AUC is calculated with bootstrap optimism correction to estimate the expected performance on new data.
LM1
<- predict(m_logistic, type = "fitted")
predicted_prob <- df_predictors$epidemic
actual
# Calibration: bootstrap and cross-validation
<- calibrate(m_logistic, method = "boot", B = 1000)
cal_boot plot(cal_boot, main = "Calibration Plot (Bootstrap)", col = "red")
n=125 Mean absolute error=0.028 Mean squared error=0.00096
0.9 Quantile of absolute error=0.043
<- calibrate(m_logistic, method = "crossvalidation", B = 10)
cal_cv plot(cal_cv, main = "Calibration Plot (Cross-validation)")
n=125 Mean absolute error=0.052 Mean squared error=0.0036
0.9 Quantile of absolute error=0.092
# rms-style calibration plot
val.prob(predicted_prob, actual, pl = TRUE, smooth = TRUE)
Dxy C (ROC) R2 D D:Chi-sq
6.362667e-01 8.181333e-01 3.845901e-01 3.267619e-01 4.184524e+01
D:p U U:Chi-sq U:p Q
9.879086e-11 -1.600000e-02 5.684342e-14 1.000000e+00 3.427619e-01
Brier Intercept Slope Emax E90
1.670537e-01 -9.221220e-16 1.000000e+00 8.967492e-02 5.126278e-02
Eavg S:z S:p
2.899140e-02 -7.742772e-02 9.382833e-01
# Internal validation
<- validate(m_logistic, method = "boot", B = 1000)
validation_boot print(validation_boot)
index.orig training test optimism index.corrected n
Dxy 0.6363 0.6590 0.6137 0.0453 0.5910 1000
R2 0.3846 0.4196 0.3499 0.0697 0.3149 1000
Intercept 0.0000 0.0000 -0.0439 0.0439 -0.0439 1000
Slope 1.0000 1.0000 0.8507 0.1493 0.8507 1000
Emax 0.0000 0.0000 0.0434 0.0434 0.0434 1000
D 0.3268 0.3671 0.2925 0.0745 0.2522 1000
U -0.0160 -0.0160 0.0231 -0.0391 0.0231 1000
Q 0.3428 0.3831 0.2694 0.1136 0.2291 1000
B 0.1671 0.1589 0.1752 -0.0163 0.1834 1000
g 1.7761 2.0259 1.6374 0.3885 1.3876 1000
gp 0.3095 0.3188 0.2918 0.0270 0.2825 1000
<- validate(m_logistic, method = "crossvalidation", B = 10)
validation_cv print(validation_cv)
index.orig training test optimism index.corrected n
Dxy 0.6363 0.6353 0.6327 0.0026 0.6336 10
R2 0.3846 0.3863 0.4279 -0.0416 0.4261 10
Intercept 0.0000 0.0000 3.6218 -3.6218 3.6218 10
Slope 1.0000 1.0000 34.7253 -33.7253 34.7253 10
Emax 0.0000 0.0000 0.4834 0.4834 0.4834 10
D 0.3268 0.3278 0.3535 -0.0257 0.3525 10
U -0.0160 -0.0177 0.0612 -0.0789 0.0629 10
Q 0.3428 0.3456 0.2924 0.0532 0.2896 10
B 0.1671 0.1664 0.1795 -0.0131 0.1802 10
g 1.7761 1.7886 38.9117 -37.1230 38.8992 10
gp 0.3095 0.3095 0.3039 0.0056 0.3039 10
<- function(model, data, response, B = 1000, seed = 123) {
pr_auc_bootstrap set.seed(seed)
# Internal function to compute PR-AUC
<- function(y, p) {
pr_auc_fun <- as.integer(y)
y if (length(unique(y)) < 2) return(NA_real_)
::pr.curve(
PRROCscores.class0 = p[y == 1],
scores.class1 = p[y == 0],
curve = FALSE
$auc.integral
)
}
# Extract response and predicted probabilities
<- data[[response]]
y_full <- predict(model, type = "fitted")
p_full
# Apparent PR-AUC
<- pr_auc_fun(y_full, p_full)
pr_apparent
# Bootstrap to estimate optimism
<- nrow(data)
n <- numeric(B)
opt_vec for (b in 1:B) {
<- sample.int(n, replace = TRUE)
idx_boot <- data[idx_boot, , drop = FALSE]
dat_boot <- update(model, data = dat_boot)
fit_b
<- dat_boot[[response]]
y_boot <- predict(fit_b, type = "fitted")
p_boot <- pr_auc_fun(y_boot, p_boot)
pr_apparent_b
<- predict(fit_b, newdata = data, type = "fitted")
p_test_on_orig <- pr_auc_fun(y_full, p_test_on_orig)
pr_test_b
<- pr_apparent_b - pr_test_b
opt_vec[b]
}
<- mean(na.omit(opt_vec))
opt_mean <- pr_apparent - opt_mean
pr_corrected
# Bootstrap percentile confidence interval
<- numeric(B)
pr_test_vec for (b in 1:B) {
<- sample.int(n, replace = TRUE)
idx_boot <- data[idx_boot, , drop = FALSE]
dat_boot <- update(model, data = dat_boot)
fit_b <- predict(fit_b, newdata = data, type = "fitted")
p_test_on_orig <- pr_auc_fun(y_full, p_test_on_orig)
pr_test_vec[b]
}
<- quantile(na.omit(pr_test_vec), c(0.025, 0.975))
ci
# Return results as a list
list(
pr_apparent = pr_apparent,
pr_corrected = pr_corrected,
optimism = opt_mean,
ci_95 = ci
) }
<- pr_auc_bootstrap(m_logistic, df_predictors, "epidemic")
res_LM1 res_LM1
$pr_apparent
[1] 0.7690194
$pr_corrected
[1] 0.7418514
$optimism
[1] 0.027168
$ci_95
2.5% 97.5%
0.7087109 0.7719495
LM2
<- predict(m_logistic2, type = "fitted")
predicted_prob <- df_predictors$epidemic
actual
# Calibration: bootstrap and cross-validation
<- calibrate(m_logistic2, method = "boot", B = 1000)
cal_boot plot(cal_boot, main = "Calibration Plot (Bootstrap)", col = "red")
n=125 Mean absolute error=0.036 Mean squared error=0.002
0.9 Quantile of absolute error=0.074
<- calibrate(m_logistic2, method = "crossvalidation", B = 10)
cal_cv plot(cal_cv, main = "Calibration Plot (Cross-validation)")
n=125 Mean absolute error=0.104 Mean squared error=0.01442
0.9 Quantile of absolute error=0.181
# rms-style calibration plot
val.prob(predicted_prob, actual, pl = TRUE, smooth = TRUE)
Dxy C (ROC) R2 D D:Chi-sq
6.357333e-01 8.178667e-01 3.911504e-01 3.335673e-01 4.269592e+01
D:p U U:Chi-sq U:p Q
6.394563e-11 -1.600000e-02 1.421085e-14 1.000000e+00 3.495673e-01
Brier Intercept Slope Emax E90
1.641585e-01 -4.158460e-14 1.000000e+00 7.827309e-02 6.439991e-02
Eavg S:z S:p
2.963018e-02 -5.250303e-02 9.581279e-01
# Internal validation
<- validate(m_logistic2, method = "boot", B = 1000)
validation_boot print(validation_boot)
index.orig training test optimism index.corrected n
Dxy 0.6357 0.6618 0.6032 0.0586 0.5771 1000
R2 0.3912 0.4328 0.3476 0.0853 0.3059 1000
Intercept 0.0000 0.0000 -0.0627 0.0627 -0.0627 1000
Slope 1.0000 1.0000 0.8253 0.1747 0.8253 1000
Emax 0.0000 0.0000 0.0535 0.0535 0.0535 1000
D 0.3336 0.3812 0.2902 0.0910 0.2425 1000
U -0.0160 -0.0160 0.0244 -0.0404 0.0244 1000
Q 0.3496 0.3972 0.2658 0.1314 0.2182 1000
B 0.1642 0.1547 0.1735 -0.0189 0.1830 1000
g 1.6600 1.9367 1.5239 0.4128 1.2473 1000
gp 0.3083 0.3206 0.2872 0.0334 0.2749 1000
<- validate(m_logistic2, method = "crossvalidation", B = 10)
validation_cv print(validation_cv)
index.orig training test optimism index.corrected n
Dxy 0.6357 0.6374 0.5730 0.0644 0.5713 10
R2 0.3912 0.3961 0.3802 0.0160 0.3752 10
Intercept 0.0000 0.0000 3.3391 -3.3391 3.3391 10
Slope 1.0000 1.0000 9.2640 -8.2640 9.2640 10
Emax 0.0000 0.0000 0.4664 0.4664 0.4664 10
D 0.3336 0.3381 0.3085 0.0296 0.3039 10
U -0.0160 -0.0177 0.0845 -0.1023 0.0863 10
Q 0.3496 0.3558 0.2239 0.1319 0.2177 10
B 0.1642 0.1629 0.1873 -0.0244 0.1886 10
g 1.6600 1.6869 13.2673 -11.5804 13.2404 10
gp 0.3083 0.3099 0.2876 0.0223 0.2860 10
<- pr_auc_bootstrap(m_logistic2, df_predictors, "epidemic")
res_LM2 res_LM2
$pr_apparent
[1] 0.7815969
$pr_corrected
[1] 0.7537298
$optimism
[1] 0.02786702
$ci_95
2.5% 97.5%
0.7294202 0.7809791
LM3
<- predict(m_logistic3, type = "fitted")
predicted_prob <- df_predictors$epidemic
actual
# Calibration: bootstrap and cross-validation
<- calibrate(m_logistic3, method = "boot", B = 1000)
cal_boot plot(cal_boot, main = "Calibration Plot (Bootstrap)", col = "red")
n=125 Mean absolute error=0.042 Mean squared error=0.00246
0.9 Quantile of absolute error=0.077
<- calibrate(m_logistic3, method = "crossvalidation", B = 10)
cal_cv plot(cal_cv, main = "Calibration Plot (Cross-validation)")
n=125 Mean absolute error=0.063 Mean squared error=0.00576
0.9 Quantile of absolute error=0.097
# rms-style calibration plot
val.prob(predicted_prob, actual, pl = TRUE, smooth = TRUE)
Dxy C (ROC) R2 D D:Chi-sq
6.378667e-01 8.189333e-01 3.580264e-01 2.996694e-01 3.845868e+01
D:p U U:Chi-sq U:p Q
5.592540e-10 -1.600000e-02 2.842171e-14 1.000000e+00 3.156694e-01
Brier Intercept Slope Emax E90
1.687973e-01 -1.716688e-15 1.000000e+00 9.278250e-02 8.381359e-02
Eavg S:z S:p
4.633975e-02 -3.152830e-01 7.525468e-01
# Internal validation
<- validate(m_logistic3, method = "boot", B = 1000)
validation_boot print(validation_boot)
index.orig training test optimism index.corrected n
Dxy 0.6379 0.6414 0.6271 0.0144 0.6235 1000
R2 0.3580 0.3762 0.3500 0.0262 0.3318 1000
Intercept 0.0000 0.0000 -0.0077 0.0077 -0.0077 1000
Slope 1.0000 1.0000 0.9624 0.0376 0.9624 1000
Emax 0.0000 0.0000 0.0098 0.0098 0.0098 1000
D 0.2997 0.3217 0.2917 0.0300 0.2697 1000
U -0.0160 -0.0160 0.0063 -0.0223 0.0063 1000
Q 0.3157 0.3377 0.2854 0.0523 0.2634 1000
B 0.1688 0.1636 0.1728 -0.0092 0.1780 1000
g 1.6805 1.8006 1.6475 0.1531 1.5274 1000
gp 0.2970 0.2999 0.2929 0.0070 0.2899 1000
<- validate(m_logistic3, method = "crossvalidation", B = 10)
validation_cv print(validation_cv)
index.orig training test optimism index.corrected n
Dxy 0.6379 0.6361 0.6250 0.0111 0.6268 10
R2 0.3580 0.3603 0.4339 -0.0735 0.4316 10
Intercept 0.0000 0.0000 0.2946 -0.2946 0.2946 10
Slope 1.0000 1.0000 1.9885 -0.9885 1.9885 10
Emax 0.0000 0.0000 0.1786 0.1786 0.1786 10
D 0.2997 0.3015 0.3482 -0.0467 0.3464 10
U -0.0160 -0.0177 0.0828 -0.1005 0.0845 10
Q 0.3157 0.3192 0.2654 0.0538 0.2619 10
B 0.1688 0.1680 0.1799 -0.0120 0.1808 10
g 1.6805 1.6938 3.2882 -1.5944 3.2749 10
gp 0.2970 0.2977 0.3130 -0.0154 0.3124 10
<- pr_auc_bootstrap(m_logistic3, df_predictors, "epidemic")
res_LM3 res_LM3
$pr_apparent
[1] 0.7319088
$pr_corrected
[1] 0.7198495
$optimism
[1] 0.01205921
$ci_95
2.5% 97.5%
0.7179385 0.7445676
Ensemble Models
# Fit base models and get probabilities
<- list(
models model1 = lrm(factor(epidemic) ~ tmin + rcs(rh, 4), data = df_predictors, x = TRUE, y = TRUE),
model2 = lrm(factor(epidemic) ~ rcs(rh, 4) + rcs(dew, 3), data = df_predictors, x = TRUE, y = TRUE),
model3 = lrm(factor(epidemic) ~ tmin + prec2, data = df_predictors, x = TRUE, y = TRUE)
)
# Predicted probabilities
<- predict(models$model1, type = "fitted")
p1 <- predict(models$model2, type = "fitted")
p2 <- predict(models$model3, type = "fitted") p3
Create ensemble predictions:
Unweighted
# Simple mean of predicted probabilities
<- (p1 + p2 + p3) / 3 ensemble_unw
Majorityard vote
# Define cutpoints for each base model
<- 0.530
cut_p1 <- 0.51
cut_p2 <- 0.460
cut_p3
# Convert predicted probabilities to binary classification
<- ifelse(p1 >= cut_p1, 1, 0)
class_p1 <- ifelse(p2 >= cut_p2, 1, 0)
class_p2 <- ifelse(p3 >= cut_p3, 1, 0)
class_p3
# Majority vote function
<- function(...) {
hard_vote <- c(...)
votes if (sum(votes) >= ceiling(length(votes)/2)) {
return(1)
else {
} return(0)
}
}
# Apply hard vote across observations
<- mapply(hard_vote, class_p1, class_p2, class_p3) ensemble_hard
Stacked ensemble
# Use base model predictions as features in a meta logistic regression model
<- data.frame(p1 = p1, p2 = p2, p3 = p3, epidemic = factor(df_predictors$epidemic))
stack_data
<- glm(epidemic ~ p1 + p2 + p3, data = stack_data, family = binomial)
meta_model <- predict(meta_model, type = "response")
ensemble_stack print(meta_model)
Call: glm(formula = epidemic ~ p1 + p2 + p3, family = binomial, data = stack_data)
Coefficients:
(Intercept) p1 p2 p3
-2.8931 0.8888 2.7411 2.3118
Degrees of Freedom: 124 Total (i.e. Null); 121 Residual
Null Deviance: 168.3
Residual Deviance: 121.6 AIC: 129.6
<- data.frame(
df_preds epidemic = df_predictors$epidemic, # variável resposta observada
LM1 = predict(models$model1, type = "fitted"),
LM2 = predict(models$model2, type = "fitted"),
LM3 = predict(models$model3, type = "fitted"),
UNW = ensemble_unw,
MJT = ensemble_hard,
STACK = predict(meta_model, type = "response")
)
write_xlsx(df_preds, "plan/df_preds.xlsx")
Ensenble models metrics
Metrics: Brier score, optimal threshold, accuracy, confusion matrix, Youden index
<- function(predicted_prob, actual) {
evaluate_ensemble <- mean((predicted_prob - actual)^2)
brier_score
# Optimal threshold
<- data.frame(1, actual, predicted_prob)
preds <- optimal.thresholds(preds)
o <- o$predicted_prob[3]
threshold
# Binary classification and accuracy
<- ifelse(predicted_prob > threshold, 1, 0)
predicted <- mean(predicted == actual)
accuracy
# Confusion matrix
<- confusionMatrix(
cmax data = as.factor(predicted),
reference = as.factor(actual),
mode = "everything",
positive = "1"
)<- cmax$byClass["Sensitivity"]
Sensitivity <- cmax$byClass["Specificity"]
Specificity <- Sensitivity + Specificity - 1
Youden
list(
Brier = brier_score,
Threshold = threshold,
Accuracy = accuracy,
Sensitivity = Sensitivity,
Specificity = Specificity,
Youden = Youden
) }
Unweighted
<- df_predictors$epidemic
actual <- evaluate_ensemble(ensemble_unw, actual)
res_unw res_unw
$Brier
[1] 0.1605968
$Threshold
[1] 0.475
$Accuracy
[1] 0.8
$Sensitivity
Sensitivity
0.7
$Specificity
Specificity
0.8666667
$Youden
Sensitivity
0.5666667
Majority vote
<- evaluate_ensemble(ensemble_hard, actual)
res_hard res_hard
$Brier
[1] 0.184
$Threshold
[1] 0.5
$Accuracy
[1] 0.816
$Sensitivity
Sensitivity
0.72
$Specificity
Specificity
0.88
$Youden
Sensitivity
0.6
Stacked
<- evaluate_ensemble(ensemble_stack, actual)
res_stack res_stack
$Brier
[1] 0.1560908
$Threshold
[1] 0.47
$Accuracy
[1] 0.824
$Sensitivity
Sensitivity
0.74
$Specificity
Specificity
0.88
$Youden
Sensitivity
0.62
Ensemble models validation
Bootstrap procedure to estimate variability of AUC, Brier score, and PR-AUC.
Function adapted for ensemble bootstrap metrics (Unweighted / Hard vote)
<- function(data, type = c("unweighted", "hard"),
ensemble_metrics_bootstrap B = 1000, cutpoints = c(0.53, 0.51, 0.46), seed = 123) {
set.seed(seed)
<- match.arg(type)
type
<- nrow(data)
n <- numeric(B)
aucs <- numeric(B)
briers <- numeric(B)
pr_aucs
for (b in 1:B) {
# 1️⃣ Resample
<- sample(1:n, size = n, replace = TRUE)
idx <- data[idx, ]
boot_data
# 2️⃣ Fit base models
<- lrm(factor(epidemic) ~ tmin + rcs(rh, 4), data = boot_data, x=TRUE, y=TRUE)
m1 <- lrm(factor(epidemic) ~ rcs(rh, 4) + rcs(dew, 3), data = boot_data, x=TRUE, y=TRUE)
m2 <- lrm(factor(epidemic) ~ tmin + prec2, data = boot_data, x=TRUE, y=TRUE)
m3
# 3️⃣ Predictions
<- predict(m1, type = "fitted")
p1b <- predict(m2, type = "fitted")
p2b <- predict(m3, type = "fitted")
p3b
# 4️⃣ Ensemble
if (type == "unweighted") {
<- (p1b + p2b + p3b)/3
ens_b else if (type == "hard") {
} <- ifelse(p1b >= cutpoints[1], 1, 0)
class_p1 <- ifelse(p2b >= cutpoints[2], 1, 0)
class_p2 <- ifelse(p3b >= cutpoints[3], 1, 0)
class_p3 # Softified probability for metrics (proportion of votes)
<- (class_p1 + class_p2 + class_p3)/3
ens_b
}
# 5️⃣ Metrics
<- as.numeric(roc(boot_data$epidemic, ens_b)$auc)
aucs[b] <- mean((ens_b - as.numeric(boot_data$epidemic))^2)
briers[b] <- pr.curve(scores.class0 = ens_b[boot_data$epidemic==1],
pr scores.class1 = ens_b[boot_data$epidemic==0],
curve = FALSE)
<- pr$auc.integral
pr_aucs[b]
}
list(AUC = aucs, Brier = briers, PR_AUC = pr_aucs)
}
Unweighted
<- ensemble_metrics_bootstrap(df_predictors, type = "unweighted", B = 1000)
res_unweighted mean(res_unweighted$AUC)
[1] 0.8421867
mean(res_unweighted$Brier)
[1] 0.1534931
mean(res_unweighted$PR_AUC)
[1] 0.7956525
Majority vote
<- ensemble_metrics_bootstrap(df_predictors, type = "hard", B = 1000)
res_hard mean(res_hard$AUC)
[1] 0.8117832
mean(res_hard$Brier)
[1] 0.1735476
mean(res_hard$PR_AUC)
[1] 0.7582948
Stacked
Stacked ensemble calibration and validation
<- datadist(stack_data)
dd
options(datadist = "dd")
<- lrm(epidemic ~ p1 + p2 + p3, data = stack_data, x = TRUE, y = TRUE)
fit_stacked
# Calibration: bootstrap and cross-validation
<- calibrate(fit_stacked, method = "boot", B = 1000)
cal_boot plot(cal_boot, main = "Calibration Plot (Bootstrap)", col = "red")
n=125 Mean absolute error=0.032 Mean squared error=0.0015
0.9 Quantile of absolute error=0.065
<- calibrate(fit_stacked, method = "crossvalidation", B = 10)
cal_cv plot(cal_cv, main = "Calibration Plot (Cross-validation)")
n=125 Mean absolute error=0.041 Mean squared error=0.00335
0.9 Quantile of absolute error=0.08
# rms-style calibration plot
val.prob(predicted_prob, actual, pl = TRUE, smooth = TRUE)
Dxy C (ROC) R2 D D:Chi-sq
6.378667e-01 8.189333e-01 3.580264e-01 2.996694e-01 3.845868e+01
D:p U U:Chi-sq U:p Q
5.592540e-10 -1.600000e-02 2.842171e-14 1.000000e+00 3.156694e-01
Brier Intercept Slope Emax E90
1.687973e-01 -1.716688e-15 1.000000e+00 9.278250e-02 8.381359e-02
Eavg S:z S:p
4.633975e-02 -3.152830e-01 7.525468e-01
# Internal validation
<- validate(fit_stacked, method = "boot", B = 1000)
validation_boot print(validation_boot)
index.orig training test optimism index.corrected n
Dxy 0.6688 0.6730 0.6505 0.0225 0.6463 1000
R2 0.4212 0.4393 0.4060 0.0332 0.3880 1000
Intercept 0.0000 0.0000 -0.0126 0.0126 -0.0126 1000
Slope 1.0000 1.0000 0.9422 0.0578 0.9422 1000
Emax 0.0000 0.0000 0.0153 0.0153 0.0153 1000
D 0.3654 0.3880 0.3493 0.0387 0.3267 1000
U -0.0160 -0.0160 0.0047 -0.0207 0.0047 1000
Q 0.3814 0.4040 0.3447 0.0593 0.3220 1000
B 0.1561 0.1509 0.1621 -0.0112 0.1673 1000
g 1.7290 1.8189 1.6663 0.1526 1.5764 1000
gp 0.3236 0.3259 0.3167 0.0092 0.3144 1000
<- validate(fit_stacked, method = "crossvalidation", B = 10)
validation_cv print(validation_cv)
index.orig training test optimism index.corrected n
Dxy 0.6688 0.6712 0.6119 0.0593 0.6095 10
R2 0.4212 0.4246 0.4497 -0.0252 0.4464 10
Intercept 0.0000 0.0000 4.9438 -4.9438 4.9438 10
Slope 1.0000 1.0000 6.7315 -5.7315 6.7315 10
Emax 0.0000 0.0000 0.5253 0.5253 0.5253 10
D 0.3654 0.3686 0.3884 -0.0197 0.3851 10
U -0.0160 -0.0177 0.1104 -0.1281 0.1121 10
Q 0.3814 0.3864 0.2780 0.1084 0.2730 10
B 0.1561 0.1552 0.1720 -0.0168 0.1729 10
g 1.7290 1.7457 13.4171 -11.6714 13.4004 10
gp 0.3236 0.3247 0.3145 0.0102 0.3134 10
# Extract corrected AUC from Dxy
<- validation_boot["Dxy", "index.corrected"]
Dxy <- validation_boot["B", "index.corrected"]
B <- (Dxy + 1) / 2
auc B
[1] 0.1673119