# Fine-grained RP breaks for CDI (3-5), extended range for SEAS5 (3-7)
rp_cdi_fine <- seq(3, 5, by = 0.2)
rp_seas5_fine <- c(seq(3, 5, by = 0.2), 6, 7) # Fine 3-5, then 6, 7
# Compute our tuned threshold's equivalent RP
n_trigger_tuned <- sum(df_historical$prob_drought > best_prob_threshold)
tuned_rp <- round((nrow(df_historical) + 1) / n_trigger_tuned, 1)
df_zoom <- expand_grid(
cdi_rp_thresh = rp_cdi_fine,
seas5_rp_thresh = rp_seas5_fine
) |>
pmap_dfr(function(cdi_rp_thresh, seas5_rp_thresh) {
combined_flag <- factor(
if_else(
df_cerf_analysis$cdi_rp >= cdi_rp_thresh |
df_cerf_analysis$seas5_rp >= seas5_rp_thresh,
"yes", "no"
),
levels = c("yes", "no")
)
f1 <- yardstick::f_meas_vec(df_cerf_analysis$cerf, combined_flag)
tibble(
cdi_rp_thresh = cdi_rp_thresh,
seas5_rp_thresh = seas5_rp_thresh,
f1 = if_else(is.na(f1), 0, f1)
)
})
# Marginal F1 for CDI alone (zoomed range)
df_cdi_zoom <- map_dfr(rp_cdi_fine, function(rp) {
flag <- factor(
if_else(df_cerf_analysis$cdi_rp >= rp, "yes", "no"),
levels = c("yes", "no")
)
f1 <- yardstick::f_meas_vec(df_cerf_analysis$cerf, flag)
tibble(rp = rp, f1 = if_else(is.na(f1), 0, f1))
})
# Marginal F1 for SEAS5 alone (extended range to 7)
df_seas5_zoom <- map_dfr(rp_seas5_fine, function(rp) {
flag <- factor(
if_else(df_cerf_analysis$seas5_rp >= rp, "yes", "no"),
levels = c("yes", "no")
)
f1 <- yardstick::f_meas_vec(df_cerf_analysis$cerf, flag)
tibble(rp = rp, f1 = if_else(is.na(f1), 0, f1))
})
# Find position of our tuned threshold (snap to nearest grid point)
tuned_cdi_snap <- rp_cdi_fine[which.min(abs(rp_cdi_fine - tuned_rp))]
# Main heatmap
p_heat_zoom <- df_zoom |>
ggplot(aes(x = factor(cdi_rp_thresh), y = factor(seas5_rp_thresh), fill = f1)) +
geom_tile(color = "white", linewidth = 0.3) +
geom_text(aes(label = round(f1, 2)), size = 2.2) +
# Highlight our tuned threshold
annotate("rect",
xmin = which(rp_cdi_fine == tuned_cdi_snap) - 0.5,
xmax = which(rp_cdi_fine == tuned_cdi_snap) + 0.5,
ymin = which(rp_seas5_fine == 4) - 0.5,
ymax = which(rp_seas5_fine == 4) + 0.5,
fill = NA, color = "black", linewidth = 1.5) +
scale_fill_gradient2(
low = "white", mid = "khaki", high = hdx_hex("mint-hdx"),
midpoint = 0.4,
limits = c(0, max(df_zoom$f1)),
name = "F1"
) +
labs(x = "CDI RP Threshold", y = "SEAS5 RP Threshold") +
theme(
panel.grid = element_blank(),
legend.position = "none",
plot.margin = margin(0, 0, 5, 5),
axis.text.x = element_text(size = 7, angle = 45, hjust = 1),
axis.text.y = element_text(size = 7)
)
# Top marginal bar (CDI)
p_top_zoom <- df_cdi_zoom |>
ggplot(aes(x = factor(rp), y = f1)) +
geom_col(fill = hdx_hex("sapphire-hdx"), width = 0.7) +
geom_text(aes(label = round(f1, 2)), vjust = -0.3, size = 2) +
scale_y_continuous(limits = c(0, max(df_cdi_zoom$f1) * 1.15), expand = c(0, 0)) +
labs(x = NULL, y = NULL, title = "CDI alone") +
theme(
axis.text.x = element_blank(),
axis.ticks.x = element_blank(),
axis.text.y = element_blank(),
axis.ticks.y = element_blank(),
axis.line = element_blank(),
panel.grid = element_blank(),
plot.margin = margin(0, 0, -10, 0)
)
# Right marginal bar (SEAS5)
p_right_zoom <- df_seas5_zoom |>
ggplot(aes(x = f1, y = factor(rp))) +
geom_col(fill = hdx_hex("tomato-hdx"), width = 0.7) +
geom_text(aes(label = round(f1, 2)), hjust = -0.2, size = 2) +
scale_x_continuous(limits = c(0, max(df_seas5_zoom$f1) * 1.15), expand = c(0, 0)) +
labs(x = NULL, y = NULL, title = "SEAS5 alone") +
theme(
axis.text.x = element_blank(),
axis.ticks.x = element_blank(),
axis.text.y = element_blank(),
axis.ticks.y = element_blank(),
axis.line = element_blank(),
panel.grid = element_blank(),
plot.margin = margin(0, 0, 0, -10)
)
# Empty corner
p_empty_zoom <- ggplot() + theme_void()
# Combine with patchwork
(p_top_zoom + p_empty_zoom + p_heat_zoom + p_right_zoom) +
plot_layout(
widths = c(4, 1),
heights = c(1, 4)
) +
plot_annotation(
title = "Combined Trigger F1: CDI (3–5) × SEAS5 (3–7)",
subtitle = glue("Black border = proposed threshold (CDI ≈ RP {tuned_rp}, SEAS5 RP 4)")
)