Skip to content

Instantly share code, notes, and snippets.

@traversc
Created August 15, 2025 23:58
Show Gist options
  • Select an option

  • Save traversc/c4fd6be52bb308829bbb0862104001c0 to your computer and use it in GitHub Desktop.

Select an option

Save traversc/c4fd6be52bb308829bbb0862104001c0 to your computer and use it in GitHub Desktop.
geom_smooth_monotonic
# ggplot2 + scam monotonic smoother
# alpha works on the line when se = FALSE
# Depends: ggplot2, scam
StatSmoothMonotonic <- ggplot2::ggproto(
"StatSmoothMonotonic", ggplot2::Stat,
required_aes = c("x", "y"),
compute_group = function(data, scales,
se = TRUE, level = 0.95, n = 100,
k = 10, dir = c("increasing","decreasing"),
family = gaussian(), ...) {
dir <- match.arg(dir)
bs <- if (dir == "increasing") "mpi" else "mpd"
d <- data[stats::complete.cases(data$x, data$y), c("x","y")]
if (nrow(d) < 2L) return(data.frame())
fam <- if (is.character(family)) get(family)() else family
form <- stats::as.formula(paste0('y ~ s(x, k=', k, ', bs="', bs, '")'))
fit <- scam::scam(form, data = d, family = fam)
gridx <- seq(min(d$x), max(d$x), length.out = n)
pr <- stats::predict(fit, newdata = data.frame(x = gridx), se.fit = se, type = "link")
if (se) {
ylink <- as.numeric(pr$fit)
se_link <- as.numeric(pr$se.fit)
} else {
ylink <- as.numeric(pr)
se_link <- NA_real_
}
if (identical(fam$link, "identity")) {
yresp <- ylink
se_resp <- se_link
} else {
yresp <- fam$linkinv(ylink)
se_resp <- if (se) se_link * abs(fam$mu.eta(ylink)) else NA_real_
}
if (se) {
df_res <- tryCatch(fit$df.residual, error = function(e) NA_real_)
if (is.na(df_res)) df_res <- max(1, nrow(d) - length(stats::coef(fit)))
tcrit <- stats::qt((1 + level) / 2, df = df_res)
ymin <- yresp - tcrit * se_resp
ymax <- yresp + tcrit * se_resp
} else {
ymin <- ymax <- NA_real_
}
data.frame(x = gridx, y = yresp, ymin = ymin, ymax = ymax)
}
)
stat_smooth_monotonic <- function(mapping = NULL, data = NULL,
geom = ggplot2::GeomSmooth, position = "identity",
..., se = TRUE, level = 0.95, n = 100,
k = 10, dir = c("increasing","decreasing"),
family = gaussian(),
na.rm = FALSE, show.legend = NA, inherit.aes = TRUE) {
ggplot2::layer(
stat = StatSmoothMonotonic, data = data, mapping = mapping,
geom = geom, position = position, show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(se = se, level = level, n = n, k = k, dir = dir,
family = family, na.rm = na.rm, ...)
)
}
geom_smooth_monotonic <- function(mapping = NULL, data = NULL,
position = "identity", ...,
se = TRUE, level = 0.95, n = 100, k = 10,
dir = c("increasing","decreasing"),
family = gaussian(),
line_alpha = NULL, ribbon_alpha = 0.2,
na.rm = FALSE, show.legend = NA, inherit.aes = TRUE) {
dots <- list(...)
alpha_in <- NULL
if ("alpha" %in% names(dots)) {
alpha_in <- dots$alpha
dots$alpha <- NULL # avoid duplicated aesthetics
}
if (!se) {
alpha_param <- if (!is.null(line_alpha)) line_alpha else alpha_in
return(
do.call(
stat_smooth_monotonic,
c(list(mapping = mapping, data = data, geom = "line", position = position,
se = FALSE, level = level, n = n, k = k, dir = dir, family = family,
na.rm = na.rm, show.legend = show.legend, inherit.aes = inherit.aes,
alpha = alpha_param),
dots)
)
)
}
# se = TRUE: ribbon and line as separate layers; control alphas independently
ribbon_alpha_param <- if (!is.null(ribbon_alpha)) ribbon_alpha else alpha_in
line_alpha_param <- if (!is.null(line_alpha)) line_alpha else NULL
list(
do.call(
stat_smooth_monotonic,
c(list(mapping = mapping, data = data, geom = "ribbon", position = position,
se = TRUE, level = level, n = n, k = k, dir = dir, family = family,
na.rm = na.rm, show.legend = show.legend, inherit.aes = inherit.aes,
alpha = ribbon_alpha_param),
dots)
),
do.call(
stat_smooth_monotonic,
c(list(mapping = mapping, data = data, geom = "line", position = position,
se = TRUE, level = level, n = n, k = k, dir = dir, family = family,
na.rm = na.rm, show.legend = show.legend, inherit.aes = inherit.aes,
alpha = line_alpha_param),
dots)
)
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment