Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ comparison-model minus reference-model by @florence-bockting in #391
* Fix `crps()` and `scrps()` ignoring the `permutations` argument for numeric
input by @thanasibakis in #398
* Speed up the Bayesian bootstrap in `pseudobma_weights()` by @VisruthSK in #368
* Signal each class of posterior warning once per call in `relative_eff()`,
`E_loo()`, and `loo_moment_match()` by @florence-bockting in #416
Comment on lines +14 to +15

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.

Right now this only applies if the user has the GitHub version of posterior. Should we mention that it's conditional on posterior > 1.7.1?


# loo 2.10.1

Expand Down
20 changes: 11 additions & 9 deletions R/E_loo.R
Original file line number Diff line number Diff line change
Expand Up @@ -271,15 +271,17 @@ E_loo_khat.default <- function(x, psis_object, log_ratios, ...) {
#' @export
E_loo_khat.matrix <- function(x, psis_object, log_ratios, ...) {
tail_lengths <- attr(psis_object, "tail_len")
if (is.null(x)) {
sapply(seq_len(ncol(log_ratios)), function(i) {
.E_loo_khat_i(x, log_ratios[, i], tail_lengths[i])
})
} else {
sapply(seq_len(ncol(log_ratios)), function(i) {
.E_loo_khat_i(x[, i], log_ratios[, i], tail_lengths[i])
})
}
.collect_warnings(
if (is.null(x)) {
sapply(seq_len(ncol(log_ratios)), function(i) {
.E_loo_khat_i(x, log_ratios[, i], tail_lengths[i])
})
} else {
sapply(seq_len(ncol(log_ratios)), function(i) {
.E_loo_khat_i(x[, i], log_ratios[, i], tail_lengths[i])
})
}
)
}

#' Compute function-specific khat estimates
Expand Down
5 changes: 3 additions & 2 deletions R/effective_sample_sizes.R
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ relative_eff.array <- function(x, ..., cores = getOption("mc.cores", 1)) {
S <- prod(dim(x)[1:2]) # posterior sample size = iter * chains

if (cores == 1) {
n_eff_vec <- apply(x, 3, posterior::ess_mean)
n_eff_vec <- .collect_warnings(apply(x, 3, posterior::ess_mean))
} else {
if (!os_is_windows()) {
n_eff_list <-
Expand Down Expand Up @@ -105,14 +105,15 @@ relative_eff.function <-
N <- dim(data)[1]

if (cores == 1) {
n_eff_list <-
n_eff_list <- .collect_warnings(
lapply(
X = seq_len(N),
FUN = function(i) {
val_i <- f_i(data_i = data[i, , drop = FALSE], draws = draws, ...)
relative_eff.default(as.vector(val_i), chain_id = chain_id, cores = 1)
}
)
)
} else {
if (!os_is_windows()) {
n_eff_list <-
Expand Down
20 changes: 20 additions & 0 deletions R/helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,23 @@ release_questions <- function() {
is_constant <- function(x, tol = .Machine$double.eps) {
abs(max(x) - min(x)) < tol
}

#' Collect posterior warnings and signal each class once
#'
#' @noRd
#' @param expr Expression to evaluate.
#' @return The value of `expr`.
.collect_warnings <- function(expr) {
collected <- list()
out <- withCallingHandlers(
expr,
posterior_warning = function(cnd) {
collected[[class(cnd)[1]]] <<- cnd

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.

I think this keeps the last warning of each class, not the first. The only class this really affects is pareto_tail_exceeds_draws, since those messages include actual numbers. I'm not sure that this really matters, though. What do you think?

invokeRestart("muffleWarning")
}
)
for (cnd in collected) {
warning(cnd)
}
out
}
Comment on lines +289 to +302

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.

I think if the function the user calls errors for whatever reason they won't see the warnings. What about this slightly modified version that moves the loop into an on.exit. I think this would allow the user to see the warnings even after an error, although I haven't tested it.

.collect_warnings <- function(expr) {
  collected <- list()
  on.exit(for (cnd in collected) warning(cnd))
  withCallingHandlers(
    expr,
    posterior_warning = function(cnd) {
      collected[[class(cnd)[1]]] <<- cnd
      invokeRestart("muffleWarning")
    }
  )
}

4 changes: 3 additions & 1 deletion R/loo_moment_matching.R
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ loo_moment_match.default <- function(x, loo, post_draws, log_lik_i,
}

if (cores == 1) {
mm_list <- lapply(X = I, FUN = function(i) loo_moment_match_i_fun(i))
mm_list <- .collect_warnings(
lapply(X = I, FUN = function(i) loo_moment_match_i_fun(i))
)
}
else {
if (!os_is_windows()) {
Expand Down
26 changes: 26 additions & 0 deletions tests/testthat/test_0_helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,29 @@ test_that("loo_cores works", {
expect_warning(expect_equal(loo_cores(10), 2), "deprecated")
options(loo.cores = NULL)
})

test_that(".collect_warnings() signals each posterior class once", {

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.

With CRAN posterior 1.7.1 there are no warning classes yet, so in CI no posterior warning ever gets to .collect_warnings() from relative_eff(), E_loo(), or loo_moment_match().

I think this test for the helper itself is good and we should keep it, but maybe add some tests that the functions users call actually signal each warning only once?

How about adding these two tests?

  1. One that calls the real loo function but with posterior mocked, e.g. local_mocked_bindings(ess_mean = ..., .package = "posterior"), with a mock that signals a classed warning, so it runs in CI now. (local_mocked_bindings() needs testthat >= 3.1.7, so we'd need to bump that in DESCRIPTION.)
  2. One that uses the real posterior functions without mocking, but we skip it until the next posterior release is available, e.g. skip_if_not(packageVersion("posterior") > "1.7.1").

warn_classed <- function(class) {
warning(structure(
class = c(class, "posterior_warning", "warning", "condition"),
list(message = class, call = NULL)
))
}
caught <- character()
out <- withCallingHandlers(
.collect_warnings({
warn_classed("a")
warn_classed("a")
.collect_warnings(warn_classed("b"))
warn_classed("b")
warning("other")
1
}),
warning = function(cnd) {
caught <<- c(caught, conditionMessage(cnd))
invokeRestart("muffleWarning")
}
)
expect_equal(out, 1)
expect_equal(caught, c("other", "a", "b"))
})
Loading