Skip to content

proof of concept for posterior_pit - #1857

Open
avehtari wants to merge 75 commits into
paul-buerkner:masterfrom
avehtari:posterior_pit
Open

proof of concept for posterior_pit#1857
avehtari wants to merge 75 commits into
paul-buerkner:masterfrom
avehtari:posterior_pit

Conversation

@avehtari

@avehtari avehtari commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #1855

  • Extend posterior_predict() beyond random draws (PIT / CDF / density / quantile) for calibration and predictive checks.
  • More detailed description of this PR and its status can be found in notes/pr-summary.md

Current API

  • new argument output with values: "random", "probability", "pit", "density", "quantile" (default is "random")
  • additional support of q / p / lower.tail / log.p / log.
# example usage
pits <- posterior_predict(fit, output = "pit")
ppc_pit_ecdf(pit = colMeans(pits), method = "correlated")

What’s implemented now

Known limitations

  • Some families still "random" only (cox none; custom only if implemented)
  • Discrete truncated "random": rejection sampling

TODOs

Original PR text (click to expand)

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)

@paul-buerkner paul-buerkner left a comment

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.

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

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
@paul-buerkner

paul-buerkner commented Feb 25, 2026 via email

Copy link
Copy Markdown
Owner

Comment thread R/posterior_predict.R Outdated
Comment thread tests/testthat/tests.posterior_predict.R Outdated
@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

paul-buerkner commented Mar 16, 2026 via email

Copy link
Copy Markdown
Owner

@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

Graphics from the knitted vignettes\brms_distribution_output_cheatsheet.Rmd:

output = "random" (click to expand)

image

output = "probability" (click to expand)

image

output = "pit" (click to expand)

image

output = "density" (click to expand)

image

output = "quantile" (click to expand)

image

@florence-bockting

Copy link
Copy Markdown
Collaborator

PR is currently failing because of some add_subsampling_vars_to_pointwise(pointwise = x$pointwise, idxs, elpd_loo_approx), which seems to be unrelated to my changes. Any idea @paul-buerkner where this comes from?

@paul-buerkner

paul-buerkner commented Jul 20, 2026 via email

Copy link
Copy Markdown
Owner

@florence-bockting

Copy link
Copy Markdown
Collaborator

@paul-buerkner When you have time, you can probably take a look at the PR.
I implemented not with the support of posterior_predict for most families.
See PR description for reference to relevant files.

@florence-bockting

Copy link
Copy Markdown
Collaborator

About the growing argument list of posterior_predict(); we discussed wrapping p, q, lower.tail, log.p, and log in something like output_args = list(...).

After thinking it through, I think we should keep the flat structure. These arguments are only used for specific output modes, and typical calls stay short, e.g. posterior_predict(fit, output = "density", log = TRUE) or posterior_predict(fit, output = "quantile", p = 0.5). With output_args, those become posterior_predict(fit, output = "density", output_args = list(log = TRUE)) or posterior_predict(fit, output = "quantile", output_args = list(p = 0.5)), which feels more awkward than helpful for the common case. So I’d suggest sticking with the flat design and document clearly which arguments apply to which output values (as we already do in the (updated) docs). In the upcoming vignette (#1900), we can provide a detailed overview which shows the structure of the object a bit more explicit e.g., with tables or so.

What do you think @paul-buerkner?

@paul-buerkner

Copy link
Copy Markdown
Owner

Makes sense. I support your decision.

@florence-bockting

Copy link
Copy Markdown
Collaborator

@paul-buerkner When you have time, you can probably take a look at the PR. I implemented not with the support of posterior_predict for most families. See PR description for reference to relevant files.

I implemented now all the aspects we discussed today. You can have now a look at it =)

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.

New posterior_pit and loo_pit functions

3 participants