Skip to content

proof of concept for posterior_pit#1857

Open
avehtari wants to merge 39 commits intopaul-buerkner:masterfrom
avehtari:posterior_pit
Open

proof of concept for posterior_pit#1857
avehtari wants to merge 39 commits intopaul-buerkner:masterfrom
avehtari:posterior_pit

Conversation

@avehtari
Copy link
Copy Markdown
Contributor

@avehtari avehtari commented Feb 20, 2026

This is a proof of concept branch and far from ready to be merged, but easier to show a possible way to solve issue #1855

To avoid duplication of code in brms, the idea is to use posterior_predict(), but add a new argument type with default value "r" referring to random draws. New type="p" would instead return CDFs / PITs.

PITs = posterior_predict(fit, type = "p")
PIT_post = colMeans(PITs)
PIT_loo = E_loo(PITs, loo(fit)$psis_object)$value

These PIT values are more useful than the ones based on ranks.

The proof of concept works with families "gaussian" and "student_t" without truncation. If the general idea is acceptable then support for the rest of the families and truncation can be added.

Adding here that natural other types are "d" for posterior predictive densities

ppd = posterior_predict(fit, type = "d")

and
"q" for quantiles that are more accurate than current rank based quantiles (used e.g. by predictive_interval())

ppqs = posterior_predict(fit, type = "q", probs = c(0.05, 0.95))
ppq = apply(ppqs, 1, mean)

Copy link
Copy Markdown
Owner

@paul-buerkner paul-buerkner left a comment

Choose a reason for hiding this comment

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

Thanks! I like the idea. Please find some comments to code structure and naming in my review.

Comment thread R/posterior_predict.R Outdated
object, newdata = NULL, re_formula = NULL, re.form = NULL,
transform = NULL, resp = NULL, negative_rt = FALSE,
ndraws = NULL, draw_ids = NULL, sort = FALSE, ntrys = 5,
ndraws = NULL, draw_ids = NULL, sort = FALSE, ntrys = 5, type = "r",
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Can we think of a more informative name than "type" and more informative options that just "r" and "p"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was thinking that "r", "p", "q", and "d" would naturally refer to how the distribution functions are named in R. Looking at R documentation these could be also named "random", "probability", "quantile", and "density".

I've not been able to come up with better name than "type"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

How about "kind"?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

similarily unspecific. Let' s go with "type" for now until we have a better idea.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Alternative argument names could be output, return_type, or mode.

Comment thread R/posterior_predict.R Outdated
prep <- prepare_predictions(
object, newdata = newdata, re_formula = re_formula, resp = resp,
ndraws = ndraws, draw_ids = draw_ids, check_response = FALSE, ...
ndraws = ndraws, draw_ids = draw_ids, check_response = FALSE, type = type, ...
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

why does prepare predictions need to know the type?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's likely that it doesn't, but I was struggling to see the structure of the code, and possibly put it in more places than needed

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I don't think it does

Comment thread R/posterior_predict.R Outdated
mean = mu, sd = sigma,
lb = prep$data$lb[i], ub = prep$data$ub[i],
ntrys = ntrys
switch(type,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

whe should solve this with a better abstraction. Changing the code of each of the families using switch is unnecessary. Add another layer of a function that handles the two types once and can be called (instead of just rcontinous) in the family-specific functions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't understand. Can explain more or modify this PR to show it with an example?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Should we have a short zoom call about this?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

yes. will write you on slack

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How about something like the following? We have a helper function that prepares the switching and can be called within the corresponding distributions. In the following a (non-polished) example for a helper function and its function call in the two example distributions (gaussian and student). (Note I changed the argument type to output in the following code... just to get an idea how it would look like)

.predict_continuous_helper <- function(output, prep, i, dist, ntrys, ...) {
  lb <- prep$data$lb[i]
  ub <- prep$data$ub[i]
  
  if (output == "probability") {
    q <- prep$data$Y[i]
    return(pcontinuous(
      q = q, dist = dist, lb = lb, ub = ub, ntrys = ntrys,
      ndraws = prep$ndraws, ...
    ))
  } else if (output == "random") {
    return(rcontinuous(
      n = prep$ndraws, dist = dist, lb = lb, ub = ub, ntrys = ntrys, ...
    ))
  }
}

posterior_predict_gaussian <- function(i, prep, ntrys = 5, output = "random", ...) {
  mu <- get_dpar(prep, "mu", i = i)
  sigma <- get_dpar(prep, "sigma", i = i)
  sigma <- add_sigma_se(sigma, prep, i = i)
  
  .predict_continuous_helper(
    output = output, prep = prep, i = i, ntrys = ntrys,
    dist = "norm", mean = mu, sd = sigma
  )
}

posterior_predict_student <- function(i, prep, ntrys = 5, output = "random", ...) {
  nu <- get_dpar(prep, "nu", i = i)
  mu <- get_dpar(prep, "mu", i = i)
  sigma <- get_dpar(prep, "sigma", i = i)
  sigma <- add_sigma_se(sigma, prep, i = i)
  
  .predict_continuous_helper(
    output = output, prep = prep, i = i, ntrys = ntrys,
    dist = "student_t", df = nu, mu = mu, sigma = sigma
  )
}

Comment thread R/posterior_predict.R Outdated
pdist <- paste0("p", dist)
out <- do_call(pdist, c(list(n), args))
} else {
error("not implemented yet")
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

are you planning to implement this part too as part of this PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I can, if we go forward with the plan

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Okay. Let's discuss the plan first and then move this one forward too

@paul-buerkner
Copy link
Copy Markdown
Owner

paul-buerkner commented Feb 25, 2026 via email

Comment thread R/posterior_predict.R Outdated
F_q <- do_call(pdist, c(list(q), args))
# include (lb - 1) to treat lb as inclusive
# this is only relevant for discrete distributions
F_lb <- do_call(pdist, c(list(lb - 1), args))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Here I am a bit unsure how to treat this correctly. I would have treated the lower bound as exclusive but this would have been inconsistent with the rdiscrete function.
Thus, I set lb - 1 in order to ensure that the lower bound is inclusive but I was uncertain here.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

It should be consistent with how Stan handles it. I am not 100% anymore how it does. Can you double check to match the behavior?

Comment thread tests/testthat/tests.posterior_predict.R
@florence-bockting
Copy link
Copy Markdown
Collaborator

I have now updated the proof of concept for 4 functions:

  • posterior_predict_gaussian()
  • posterior_predict_student()
  • posterior_predict_binomial()
  • posterior_predict_poisson()

They include two additional new arguments:

  • output (which can take currently the values random or probability) and
  • randomized (which can be NULL or a boolean).

The current implementation follows the idea that we can call:

  • rpred <- posterior_predict(fit_norm)
    • computes random draws from the normal predictive
    • output = "random" and randomized = NULL by default
  • ppred <- posterior_predict(fit_norm, output = "probability")
    • computes cdf of the normal predictive
    • randomized = NULL by default
  • rpred <- posterior_predict(fit_pois)
    • computes random draws from the poisson predictive
    • output = "random" and randomized = NULL by default
  • ppred <- posterior_predict(fit_pois, output = "probability")
    • computes randomized PITs from the poisson predictive
    • randomized = TRUE by default
  • ppred <- posterior_predict(fit_pois, output = "probability", randomized = FALSE)
    • computes the non-randomized PITs from the poisson predictive, which are currently defined as the mid points (F(x) +F(x-1)) * 0.5

@avehtari
Copy link
Copy Markdown
Contributor Author

computes the non-randomized PITs from the poisson predictive, which are currently defined as the mid points (F(x) +F(x-1)) * 0.5

I think these should be just F(x) to follow what ppois etc. do. For the same reason, I think that it might be more logical to have non-randomized as the default

@avehtari
Copy link
Copy Markdown
Contributor Author

Or we could have separately output options probability (not randomized F(x)) and pit randomized for discrete

@florence-bockting
Copy link
Copy Markdown
Collaborator

Or we could have separately output options probability (not randomized F(x)) and pit randomized for discrete

Okay, one option could be to have for outcome the values random, probability, density, quantile, and pit. Whereby pit is only a valid option for discrete cases. Cons are that there is a conceptual overlap between pit and probability and pit is only a valid choice for a subclass of inputs. Pro is that we have all alternatives in one function and no additional argument (like randomized).

An alternative is to outsource the pit case entirely and to have an additional function such as posterior_pit(fit_pois) that applies only to discrete cases and additionally, we have posterior_predict(fit_pois, outcome = "probability") supporting the default implementations like ppois(). In my opinion, this option is from a design point relatively clean, as we don't have the problem that there are "cases" which work/don't work. However, we would have to introduce an additional function.

@paul-buerkner
Copy link
Copy Markdown
Owner

paul-buerkner commented Mar 16, 2026 via email

@avehtari
Copy link
Copy Markdown
Contributor Author

Okay, one option could be to have for outcome the values random, probability, density, quantile, and pit. Whereby pit is only a valid option for discrete cases

In this case pit for continuous would be valid, but the result would be the same as with probability

  • PITs (with randomization for discrete) are needed fot PIT and LOO-PIT uniformity checks (pp_check(..., type = "pit_ecdf|loo_pit_ecdf"))
  • Probabilities (non-randomized) are needed for calibration plots (reliabilitydiag type plots), for which we have PR in bayesplot PPC Calibration plots stan-dev/bayesplot#352
  • Probabilities are useful in general for making predictions and modeling visualization
  • If the outcome options are random, probability, density, quantile, and pit, we don't need separate argument for randomization, and pit computes such PIT values which are useful for uniformity tests both for continuous and discrete case

@paul-buerkner
Copy link
Copy Markdown
Owner

I am fine with option random, probability, density, quantile, and pit (or whatever subset we go with first), as long all all is realized in the same function (posterior_predict). Otherwise code overhead is too high. If we want, we can still have lightweight wrappers around posterior_predict with output argument fixed.

@florence-bockting
Copy link
Copy Markdown
Collaborator

I am fine with option random, probability, density, quantile, and pit (or whatever subset we go with first), as long all all is realized in the same function (posterior_predict).

Alright. We definitely stick with one function - the idea of an extra function is out.
I will change the implementation to having posterior_predict with output = (random, probability, density, quantile, pit). Whereby pit is the randomized pit for discrete distributions and the "normal" cdf for continuous distributions.

@florence-bockting
Copy link
Copy Markdown
Collaborator

I'm fine with this. I usually prefer smaller PRs anyway.

@avehtari
Copy link
Copy Markdown
Contributor Author

How is the log_lik method related to the envisioned density option?

log_lik was meant to compute log likelihood which is used to remove information from the posterior to get leave-one-*-out posterior. log_lik as a function name is confusing when we want to compute predictive density. There is a predictive diagnostic topic in my research plan that requires predictive densities and having this functionality would make experiments easier. Having posterior_predict(..., output="density") would 1) be more logical when we want predictive densities, 2) be logical option to be in the same function as "random", "probability", and "quantile" to complete the usual quartet of distributions (r, p, q, d).

We could start with random, probability, pit. And the worry about the others afterwards? This could simplify this PR and reviewing it.

I'm fine that it would be added later, but wanted to keep in the plans, so that it will be easier to implement later.

@paul-buerkner
Copy link
Copy Markdown
Owner

Okay, that's fine and sounds like a good plan. @florence-bockting please let me know when (now already?) you want my initial review on your draft PR. Just having support for a few families is sufficient for me to initially review it.

@florence-bockting
Copy link
Copy Markdown
Collaborator

Okay, that's fine and sounds like a good plan. @florence-bockting please let me know when (now already?) you want my initial review on your draft PR. Just having support for a few families is sufficient for me to initially review it.

Alright, go ahead @paul-buerkner

@florence-bockting
Copy link
Copy Markdown
Collaborator

Okay, that's fine and sounds like a good plan. @florence-bockting please let me know when (now already?) you want my initial review on your draft PR. Just having support for a few families is sufficient for me to initially review it.

Alright, go ahead @paul-buerkner

Just a quick check-in and reminder @paul-buerkner

Copy link
Copy Markdown
Owner

@paul-buerkner paul-buerkner left a comment

Choose a reason for hiding this comment

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

Getting there, thank you! Here are my comments and change requests

Comment thread R/posterior_predict.R Outdated
Comment thread R/posterior_predict.R Outdated
Comment thread R/posterior_predict.R Outdated
Comment thread R/posterior_predict.R Outdated
Comment thread R/posterior_predict.R Outdated
Comment thread R/posterior_predict.R Outdated
Comment thread tests/testthat/tests.posterior_predict.R Outdated
Comment thread vignettes/brms_posterior_predict.Rmd
Comment thread DESCRIPTION Outdated
Comment thread DESCRIPTION Outdated
@paul-buerkner
Copy link
Copy Markdown
Owner

paul-buerkner commented Apr 13, 2026 via email

@avehtari
Copy link
Copy Markdown
Contributor Author

Would it be too much trouble to allow argument lower.tail=FALSE for output="probability"? And if that, then maybe log for density and log.p for probability and quantile?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants