Skip to content
Open
Changes from 5 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0e1e446
.ppc_calibration_overlay_data, and ppc_calibration_overlay(_grouped)
TeemuSailynoja Apr 29, 2025
d784405
draft of ppc_calibration plots
TeemuSailynoja May 19, 2025
5cf62f9
Add example for ppc_calibration_overlay()
TeemuSailynoja May 22, 2025
01ac826
Fix ppc_calibration_grouped()
TeemuSailynoja May 22, 2025
f9806eb
fix typo preventing building doc
jgabry May 22, 2025
4f09a00
Merge branch 'master' of github.com:TeemuSailynoja/bayesplot into add…
TeemuSailynoja Jun 12, 2025
5efdf47
Add ppc_calibration plots to namespace and docs.
TeemuSailynoja Jul 3, 2025
14eb2dc
Merge branch 'master' of github.com:stan-dev/bayesplot into add_ppc_c…
TeemuSailynoja Jul 3, 2025
a8b4264
Sync process. WIP. ISSUE: ppc_calibratrion loses the posterior mean.
TeemuSailynoja Jul 15, 2025
56d6662
Merge branch 'master' into add_ppc_calibration
florence-bockting Apr 9, 2026
293151a
update ppc-calibration with consistency and confidence method
Apr 9, 2026
bbd03a3
refactor: merge new changes into existing code
Apr 9, 2026
2d30c46
docs: update ppc-calibration
Apr 9, 2026
82986b4
feature: add psis_object to ppc_loo_calibration; adjust helptext size
Apr 16, 2026
6c665f3
refactor: remove x_scale arg and adj. help_text in ppc-calibration plots
Apr 21, 2026
245dcfa
Merge branch 'master' into add_ppc_calibration
florence-bockting Apr 21, 2026
b0ad5f4
refactor: remove interval_type argument and adjust corresponding tests
May 4, 2026
789de2f
docs: add vignette for ppc-calibration plot
May 4, 2026
1778d02
docs: update function documentation
May 4, 2026
6c75a20
docs: load dplyr in vignette
May 5, 2026
7cff4da
tests: update snapshots
May 5, 2026
6cefc64
docs: update vignette ppc-calibration
May 5, 2026
f19bde9
docs, refactor: update function documentation and refactor ppc_calibr…
May 6, 2026
6e9a5ce
fix: change sorting of ppc-calibration-data
May 6, 2026
a3320c2
tests: remove brms dependent test
May 6, 2026
c6199f7
tests: remove test with dependency
May 6, 2026
8aa2d2d
docs: add ppc-calibration vignette as article-online-only
May 7, 2026
21e4d63
docs: minor adjustments in vignette
May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions R/ppc-calibration.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# x' PPC calibration
#'
#' Assess the calibration of the predictive distributions `yrep` in relation to
#' the data `y'.
#' See the **Plot Descriptions** section, below, for details.
#'
#' @name PPC-calibration
#' @family PPCs
#'
#' @template args-y-yrep
#' @template args-group
#'
#' @template return-ggplot-or-data
#'
#' @section Plot Descriptions:
#' \describe{
#' \item{`ppc_calibration()`,`ppc_calibration_grouped()`}{
#'
#' },
#' \item{`ppc_calibration_overlay()`,`ppc_calibration_overlay_grouped()`}{
#'
#' },
#' \item{`ppc_loo_calibration()`,`ppc_loo_calibration_grouped()`}{
#'
#' }
#' }
#'
#' @examples
#' color_scheme_set("brightblue")
#'
#' # Make an example dataset of binary observations
#' ymin <- range(example_y_data(), example_yrep_draws())[1]
#' ymax <- range(example_y_data(), example_yrep_draws())[2]
#' y <- rbinom(length(example_y_data()), 1, (example_y_data() - ymin) / (ymax - ymin))
#' prep <- (example_yrep_draws() - ymin) / (ymax - ymin)
#'
#' ppc_calibration_overlay(y, prep[1:50, ])
NULL


#' @rdname PPC-calibration
#' @export
ppc_calibration_overlay <- function(
y, prep, ..., linewidth = 0.25, alpha = 0.5) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So for these functions prep is a matrix of probabilities and not actually a matrix of draws of binary outcomes from the posterior predictive distribution, right? I think in that case the argument name prep makes sense. But the description at the top of the file says

Assess the calibration of the predictive distributions yrep in relation to the data `y'

which makes it sound like the user should give us yrep. So I think we just need to reconcile how we describe this to the user.

Copy link
Copy Markdown
Collaborator Author

@TeemuSailynoja TeemuSailynoja Jun 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your interpretation is right. The description needs more clarity. yrep can be made to be accepted by ppc_calibration (without overlay).

check_ignored_arguments(...)
data <- .ppc_calibration_data(y, prep)
ggplot(data) +
geom_abline(color = "black", linetype = 2) +
geom_line(
aes(value, cep, group = rep_id, color = "yrep"),
linewidth = linewidth, alpha = alpha
) +
scale_color_ppc() +
bayesplot_theme_get() +
legend_none() +
coord_equal(xlim = c(0, 1), ylim = c(0, 1), expand = FALSE) +
xlab("Predicted probability") +
ylab("Conditional event probability") +
NULL
}

#' @rdname PPC-calibration
#' @export
ppc_calibration_overlay_grouped <- function(
y, prep, group, ..., linewidth = 0.25, alpha = 0.7) {
check_ignored_arguments(...)
data <- .ppc_calibration_data(y, prep, group)
ggplot(data) +
geom_abline(color = "black", linetype = 2) +
geom_line(aes(value, cep, group = rep_id, color = "yrep"),
linewidth = linewidth, alpha = alpha
) +
facet_wrap(vars(group)) +
scale_color_ppc() +
bayesplot_theme_get() +
legend_none() +
coord_equal(xlim = c(0, 1), ylim = c(0, 1), expand = FALSE) +
xlab("Predicted probability") +
ylab("Conditional event probability") +
NULL
}

#' @rdname PPC-calibration
#' @export
ppc_calibration <- function(
y, prep, prob = .95, show_mean = TRUE, ..., linewidth = 0.5, alpha = 0.7) {
check_ignored_arguments(...)
data <- .ppc_calibration_data(y, prep) %>%
group_by(y_id) %>%
summarise(
value = median(value),
lb = quantile(cep, .5 - .5 * prob),
ub = quantile(cep, .5 + .5 * prob),
cep = median(cep)
)

ggplot(data) +
aes(value, cep) +
geom_abline(color = "black", linetype = 2) +
geom_ribbon(aes(ymin = lb, ymax = ub, fill = "yrep"), alpha = alpha) +
geom_line(
aes(color = "y"),
linewidth = linewidth
) +
scale_color_ppc() +
scale_fill_ppc() +
bayesplot_theme_get() +
legend_none() +
coord_equal(xlim = c(0, 1), ylim = c(0, 1), expand = FALSE) +
xlab("Predicted probability") +
ylab("Conditional event probability") +
NULL
}

#' @rdname PPC-calibration
#' @export
ppc_calibration_grouped <- function(
y, prep, group, prob = .95, show_mean = TRUE, ..., linewidth = 0.5, alpha = 0.7) {
check_ignored_arguments(...)
data <- .ppc_calibration_data(y, prep, group) %>%
group_by(group, y_id) %>%
summarise(
value = median(value),
lb = quantile(cep, .5 - .5 * prob),
ub = quantile(cep, .5 + .5 * prob),
cep = median(cep)
)

ggplot(data) +
aes(value, cep) +
geom_abline(color = "black", linetype = 2) +
geom_ribbon(aes(ymin = lb, ymax = ub, fill = "yrep"), alpha = alpha) +
geom_line(
aes(color = "y"),
linewidth = linewidth
) +
facet_wrap(vars(group)) +
scale_color_ppc() +
scale_fill_ppc() +
bayesplot_theme_get() +
legend_none() +
coord_equal(xlim = c(0, 1), ylim = c(0, 1), expand = FALSE) +
xlab("Predicted probability") +
ylab("Conditional event probability") +
NULL
}

#' @rdname PPC-calibration
#' @export
ppc_loo_calibration <- function(
y, prep, lw, ..., linewidth = 0.25, alpha = 0.7) {
check_ignored_arguments(...)
data <- .ppc_calibration_data(y, prep)
ggplot(data) +
geom_abline(color = "black", linetype = 2) +
geom_line(
aes(value, cep, group = rep_id, color = "yrep"),
linewidth = linewidth, alpha = alpha
) +
scale_color_ppc() +
bayesplot_theme_get() +
legend_none() +
coord_equal(xlim = c(0, 1), ylim = c(0, 1), expand = FALSE) +
xlab("Predicted probability") +
ylab("Conditional event probability") +
NULL
}

#' @rdname PPC-calibration
#' @export
ppc_loo_calibration_grouped <- function(
y, prep, group, lw, ..., linewidth = 0.25, alpha = 0.7) {
check_ignored_arguments(...)
data <- .ppc_calibration_data(y, prep, group)
ggplot(data) +
geom_abline(color = "black", linetype = 2) +
geom_line(aes(value, cep, group = rep_id, color = "yrep"),
linewidth = linewidth, alpha = alpha
) +
facet_wrap(vars(group)) +
scale_color_ppc() +
bayesplot_theme_get() +
legend_none() +
coord_equal(xlim = c(0, 1), ylim = c(0, 1), expand = FALSE) +
xlab("Predicted probability") +
ylab("Conditional event probability") +
NULL
}

.ppc_calibration_data <- function(y, prep, group = NULL) {
y <- validate_y(y)
n_obs <- length(y)
prep <- validate_predictions(prep, n_obs)
if (any(prep > 1 | prep < 0)) {
stop("Values of ´prep´ should be predictive probabilities between 0 and 1.")
}
if (!is.null(group)) {
group <- validate_group(group, n_obs)
} else {
group <- rep(1, n_obs * nrow(prep))
}

if (requireNamespace("monotone", quietly = TRUE)) {
monotone <- monotone::monotone
} else {
monotone <- function(y) {
stats::isoreg(y)$yf
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an advantage to using monotone::monotone instead of stats::isoreg?

Copy link
Copy Markdown
Member

@jgabry jgabry May 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is, does it do something slightly better? Or the same thing more efficiently? I've seen stats::isoreg before but I had never seen the monotone package. If there's no difference then it's probably not worth checking for the monotone package. If it's better then we could put monotone in Suggests and then check for it like you do here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

monotone offers an implementation of the algorithm that is noticeably faster for large samples.
I think it would be good to add it to the suggests.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok sounds good

.ppd_data(prep, group = group) %>%
group_by(group, rep_id) %>%
mutate(
ord = order(value),
value = value[ord],
cep = monotone(y[ord])
) |>
ungroup()
}

.loo_resample_data <- function(prep, lw, psis_object) {
lw <- .get_lw(lw, psis_object)
stopifnot(identical(dim(prep), dim(lw)))
# Work in progress here...
}
Loading