Repository files navigation

cppp

A package for Calibrated posterior predictive p-values.

⚠️ Work in progress — not ready for use

This is an active research project in early development. It is public so collaborators can follow along, not because it is ready.

There is no released version, and nothing here should be depended on.

If you want to try it, get in touch first.

It provides a general framework for a MCMC engine—to:

  1. Compute calibrated posterior p-values (cppp),
  2. Estimate their Monte Carlo variance using the idea of the transfer effective sample size (ESS)
  3. Can handle different MCMC engines. NIMBLE and R for now, other MCMC engines later.

Concept

Given data $y$, a model $p(\theta \mid y) \propto p(y \mid \theta) \pi (\theta)$, and a discrepancy function $D(y,\theta)$:

  1. Run an long MCMC chain to obtain draws from the posterior $p(\theta \mid y)$. With $M$ draws, we sample new datafrom the posterior predictive of the data $p(y^* \mid \theta_i)$ and compute

$$ \Delta_i = D(y^*_i, \theta_i) - D(y, \theta_i), $$

This chain of $\Delta = { \Delta_i }$ collects the observed discrepancies.

  1. Generate $r$calibration replicates:

    • simulate new datasets $\tilde{y}_j$ from the model,
    • run short chains of length $\tilde{m} $ for $p(\theta \mid \tilde{y}_j)$,
    • compute short-run posterior-predictive p-values $\hat{p}_j$.
  2. Estimate the CPPP:

$$ \widehat{\text{cppp}} = \frac{1}{r}\sum_{j=1}^r \mathbf{1}{\hat{p}_j \le \hat{p}_{\text{obs}}}. $$

  1. Estimate the Monte Carlo variance using the transfer ESS idea: match each $\hat{p}_j$ to a quantile on the observed $\Delta$ -chain, compute the transfer autocorrelation, and estimate the cppp variance.

Package architecture

There is a one-page architecture map: the two routes through the package, what you write, what comes back, and what is not not working yet.

View the architecture map · source

Or, once the package is installed:

browseURL(system.file("cheatsheet", "cppp-architecture.html", package="cppp"))

Main functions

FunctionPurpose
runCalibration()The generic engine. No model in it — just posterior draws and the functions you give it.
runCalibrationNIMBLE()The NIMBLE wrapper. Give it a model and your specifications and it builds the rest itself.

Describing what you want

FunctionRole
discrepancy()Describes one discrepancy: this is a list containing name, the nodes it reads, and optionally a custom nimbleFunction.
simulation()Describes how a replicate dataset is generated. Two options: "conditional" (redraw the data) or "marginal" (redraw named latent nodes too).
discrepancyBasea virtual base class. A custom nimbleFunction discrepancy must contain it.

Five discrepancies are builtin with the package, and naming one is a shortcut for writing it yourself: mean, variance, deviance, chisquared, freemantukey.

Implementation notes

Both simulation() and discrepancy() take a dataNodes argument. The simulation's set has to cover the discrepancy's:

sim <- simulation("conditional", dataNodes = c("y", "z")) # writes y and z
discrepancy("mean", dataNodes = "y") # reads y -- fine, a subset
discrepancy("mean", dataNodes = c("y", "z")) # reads both -- fine
discrepancy("mean", dataNodes = "w") # reads w -- error

The simulation writes first and the discrepancy reads afterwards. A node the simulation never writes still holds the value from the previous draw, so a discrepancy reading it would not actually use a new replicate. The calculator checks the two sets before it starts and stops, naming the nodes at fault.

For paramNodes the story is the same. Before either one does anything, the calculator and the simulator each write a posterior draw into their model. Both look up the values they need by node name, so both must be given the same parameter nodes — and every one of those nodes has to appear as a column in the posterior samples. If one is missing, you get an error naming it.

runCalibrationNIMBLE() arranges all of this for you. It passes your dataNames to the simulation and your paramNames to both the calculator and the simulator, so they cannot disagree. You only need to think about it if you build a calculator or a simulator yourself.

Each piece gets its own copy of the model. Three copies are made in all:

copyused bycompiled?
the originalthe MCMCyes
model$newModel()the discrepancy calculatoryes, together with the discrepancies
model$newModel()the replicate simulatorno, it stays plain R

The three run one after another, never at the same time: simulate a replicate dataset, run a short chain on it, then compute the discrepancies. What the copies avoid is not a clash but the state each one leaves behind. A model remembers the last values written into it, and the MCMC starts its next chain from wherever its model happens to be. Separate copies keep one step from moving another's starting point.

The cost worth knowing about is that the model is compiled twice, once for the MCMC and once with the discrepancies. That is the slow part of setting up a run. The simulator's copy is cheap by comparison, since it is never compiled.

Open question. The third copy may not be needed. Once the calculator is compiled it works on the C model and never touches the R model it was built from, so the simulator could reuse that one. The saving is small and the risk is a state bug that gives wrong numbers rather than an error, so this is written down for discussion rather than acted on.

Watch out for derived parameters. Say the prior is on log(sigma) but you monitor sigma. After writing a draw into the model, the calculator recalculates only the nodes below the parameters. It never recalculates the parameters themselves. If it did, it would rebuild sigma from its parents and throw away the value just drawn — the same sigma for every draw, with no error to warn you. Because it skips them, monitoring sigma and monitoring log_sigma both work.

Turning descriptions into numbers

FunctionRole
makeDiscrepancyCalculator()Builds the calculator that simulate from the posterior predictive and returns $D(y,\theta)$ and $D(y^*,\theta)$ per each replicated dataset, per discrepancy. Discrepancies and loop over the discrepancies compile together in one call using makeDiscrepancyNimbleFun().
makeDiscrepancyNimbleFun()Turns one specification into a nimbleFunction. Mostly used by the calculator.
makeSimulateNewDataFun()Builds the function that simulates one replicate dataset from a draw.
makeDiscrepancyExtractor()Placeholder. The online counterpart — reads discrepancies the MCMC already computed instead of recomputing them.

runCalibrationNIMBLE() calls the first two for you when you pass discrepancies and simulation, so most of the time you never touch them directly.

Results

runCalibration() returns a list with:

  • CPPP — one value per discrepancy
  • obsPPP, repPPP — observed and replicated posterior predictive p-values
  • discrepancies — the observed and replicated discrepancy values
  • drawnIndices — which posterior draws seeded the calibration replicates

runCalibration() returns this as a cpppResult S3 object, built and checked by newCpppResult(). Standard errors, confidence intervals, and print / summary / plot methods are planned but not implemented, so for now it prints as a plain list.

Placeholders

Two pieces are deliberately unfinished, and neither is tested:

FileWhat it is
R/transferAutocorrelation.RtransferAutocorrelation() stops with "Not implemented". Corresponds to step 4 of the concept above, i.e., the transfer-ESS estimate of the cppp variance.
R/makeDiscrepancyExtractor.RmakeDiscrepancyExtractor() reads discrepancy columns that NIMBLE computed during the MCMC run. It is written against the current derived-quantity output (discrepancy_model, discrepancy_simulated), but that format is not settled, and nothing is wired up to feed it yet.

Typical workflow

  1. Build the model — an ordinary NIMBLE model with your data in it.

  2. Describe your discrepanciesdiscrepancy("mean") names one the package ships; discrepancy("asymm", modelNodes = "mu", fun = myAsymm) supplies your own as a nimbleFunction with contains = discrepancyBase.

  3. Describe the replicatessimulation("conditional") redraws the data from each posterior draw. simulation("marginal", simulateNodes = ...) redraws latent nodes as well, and you must name them.

  4. Run the calibration — hand both to runCalibrationNIMBLE(), which runs the long chain, simulates $r$ replicate datasets, runs a short chain on each, and turns the results into a PPP and a CPPP.

  5. Estimate the variancetransferAutocorrelation(), once implemented.


Example

The Newcomb light-speed measurements, with two discrepancies at once: mean, which a normal model should fit, and an asymmetry statistic, which it should not. The full script is in inst/examples/newcomb_spec_offline.R.

library(nimble)
library(cppp)
lightPath<- system.file("examples", "light.txt", package="cppp")
newcombData<-list(y= read.table(lightPath)$V1)
newcombModel<- nimbleModel(
code= nimbleCode({
for (iin1:n) y[i] ~ dnorm(mu, sd=sigma)
mu~ dflat()
log(sigma) ~ dflat()
}),
data=newcombData,
constants=list(n= length(newcombData$y)),
inits=list(mu=0, log_sigma=2)
)
## Your own discrepancy: how lopsided the two tails are around mu.## sort() does not compile, so wrap R's.sortR<- nimbleRcall(prototype=function(x= double(1)) {},
Rfun="sort", returnType= double(1))
asymmDisc<- nimbleFunction(
contains=discrepancyBase,
setup=function(model, dataNodes, modelNodes) {},
run=function() {
ys<- sortR(values(model, dataNodes))
mu<- values(model, modelNodes)[1]
returnType(double(0))
return(abs(ys[61] -mu) - abs(ys[6] -mu))
}
)
res<- runCalibrationNIMBLE(
model=newcombModel,
dataNames="y",
paramNames= c("mu", "sigma"),
discrepancies=list(discrepancy("mean"),
discrepancy("asymm", modelNodes="mu", fun=asymmDisc)),
simulation= simulation("conditional"),
nReps=20
)
res$obsPPP# one per discrepancy; mean near 0.5, asymm far from itres$CPPP# one per discrepancyres$repPPP# nReps x 2

About

Calibrated posterior predictive p-values

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

cppp

A package for Calibrated posterior predictive p-values.

⚠️ Work in progress — not ready for use

This is an active research project in early development. It is public so collaborators can follow along, not because it is ready.

There is no released version, and nothing here should be depended on.

If you want to try it, get in touch first.

It provides a general framework for a MCMC engine—to:

  1. Compute calibrated posterior p-values (cppp),
  2. Estimate their Monte Carlo variance using the idea of the transfer effective sample size (ESS)
  3. Can handle different MCMC engines. NIMBLE and R for now, other MCMC engines later.

Concept

Given data $y$, a model $p(\theta \mid y) \propto p(y \mid \theta) \pi (\theta)$, and a discrepancy function $D(y,\theta)$:

  1. Run an long MCMC chain to obtain draws from the posterior $p(\theta \mid y)$. With $M$ draws, we sample new datafrom the posterior predictive of the data $p(y^* \mid \theta_i)$ and compute

$$ \Delta_i = D(y^*_i, \theta_i) - D(y, \theta_i), $$

This chain of $\Delta = { \Delta_i }$ collects the observed discrepancies.

  1. Generate $r$calibration replicates:

    • simulate new datasets $\tilde{y}_j$ from the model,
    • run short chains of length $\tilde{m} $ for $p(\theta \mid \tilde{y}_j)$,
    • compute short-run posterior-predictive p-values $\hat{p}_j$.
  2. Estimate the CPPP:

$$ \widehat{\text{cppp}} = \frac{1}{r}\sum_{j=1}^r \mathbf{1}{\hat{p}_j \le \hat{p}_{\text{obs}}}. $$

  1. Estimate the Monte Carlo variance using the transfer ESS idea: match each $\hat{p}_j$ to a quantile on the observed $\Delta$ -chain, compute the transfer autocorrelation, and estimate the cppp variance.

Package architecture

There is a one-page architecture map: the two routes through the package, what you write, what comes back, and what is not not working yet.

View the architecture map · source

Or, once the package is installed:

browseURL(system.file("cheatsheet", "cppp-architecture.html", package="cppp"))

Main functions

FunctionPurpose
runCalibration()The generic engine. No model in it — just posterior draws and the functions you give it.
runCalibrationNIMBLE()The NIMBLE wrapper. Give it a model and your specifications and it builds the rest itself.

Describing what you want

FunctionRole
discrepancy()Describes one discrepancy: this is a list containing name, the nodes it reads, and optionally a custom nimbleFunction.
simulation()Describes how a replicate dataset is generated. Two options: "conditional" (redraw the data) or "marginal" (redraw named latent nodes too).
discrepancyBasea virtual base class. A custom nimbleFunction discrepancy must contain it.

Five discrepancies are builtin with the package, and naming one is a shortcut for writing it yourself: mean, variance, deviance, chisquared, freemantukey.

Implementation notes

Both simulation() and discrepancy() take a dataNodes argument. The simulation's set has to cover the discrepancy's:

sim <- simulation("conditional", dataNodes = c("y", "z")) # writes y and z
discrepancy("mean", dataNodes = "y") # reads y -- fine, a subset
discrepancy("mean", dataNodes = c("y", "z")) # reads both -- fine
discrepancy("mean", dataNodes = "w") # reads w -- error

The simulation writes first and the discrepancy reads afterwards. A node the simulation never writes still holds the value from the previous draw, so a discrepancy reading it would not actually use a new replicate. The calculator checks the two sets before it starts and stops, naming the nodes at fault.

For paramNodes the story is the same. Before either one does anything, the calculator and the simulator each write a posterior draw into their model. Both look up the values they need by node name, so both must be given the same parameter nodes — and every one of those nodes has to appear as a column in the posterior samples. If one is missing, you get an error naming it.

runCalibrationNIMBLE() arranges all of this for you. It passes your dataNames to the simulation and your paramNames to both the calculator and the simulator, so they cannot disagree. You only need to think about it if you build a calculator or a simulator yourself.

Each piece gets its own copy of the model. Three copies are made in all:

copyused bycompiled?
the originalthe MCMCyes
model$newModel()the discrepancy calculatoryes, together with the discrepancies
model$newModel()the replicate simulatorno, it stays plain R

The three run one after another, never at the same time: simulate a replicate dataset, run a short chain on it, then compute the discrepancies. What the copies avoid is not a clash but the state each one leaves behind. A model remembers the last values written into it, and the MCMC starts its next chain from wherever its model happens to be. Separate copies keep one step from moving another's starting point.

The cost worth knowing about is that the model is compiled twice, once for the MCMC and once with the discrepancies. That is the slow part of setting up a run. The simulator's copy is cheap by comparison, since it is never compiled.

Open question. The third copy may not be needed. Once the calculator is compiled it works on the C model and never touches the R model it was built from, so the simulator could reuse that one. The saving is small and the risk is a state bug that gives wrong numbers rather than an error, so this is written down for discussion rather than acted on.

Watch out for derived parameters. Say the prior is on log(sigma) but you monitor sigma. After writing a draw into the model, the calculator recalculates only the nodes below the parameters. It never recalculates the parameters themselves. If it did, it would rebuild sigma from its parents and throw away the value just drawn — the same sigma for every draw, with no error to warn you. Because it skips them, monitoring sigma and monitoring log_sigma both work.

Turning descriptions into numbers

FunctionRole
makeDiscrepancyCalculator()Builds the calculator that simulate from the posterior predictive and returns $D(y,\theta)$ and $D(y^*,\theta)$ per each replicated dataset, per discrepancy. Discrepancies and loop over the discrepancies compile together in one call using makeDiscrepancyNimbleFun().
makeDiscrepancyNimbleFun()Turns one specification into a nimbleFunction. Mostly used by the calculator.
makeSimulateNewDataFun()Builds the function that simulates one replicate dataset from a draw.
makeDiscrepancyExtractor()Placeholder. The online counterpart — reads discrepancies the MCMC already computed instead of recomputing them.

runCalibrationNIMBLE() calls the first two for you when you pass discrepancies and simulation, so most of the time you never touch them directly.

Results

runCalibration() returns a list with:

  • CPPP — one value per discrepancy
  • obsPPP, repPPP — observed and replicated posterior predictive p-values
  • discrepancies — the observed and replicated discrepancy values
  • drawnIndices — which posterior draws seeded the calibration replicates

runCalibration() returns this as a cpppResult S3 object, built and checked by newCpppResult(). Standard errors, confidence intervals, and print / summary / plot methods are planned but not implemented, so for now it prints as a plain list.

Placeholders

Two pieces are deliberately unfinished, and neither is tested:

FileWhat it is
R/transferAutocorrelation.RtransferAutocorrelation() stops with "Not implemented". Corresponds to step 4 of the concept above, i.e., the transfer-ESS estimate of the cppp variance.
R/makeDiscrepancyExtractor.RmakeDiscrepancyExtractor() reads discrepancy columns that NIMBLE computed during the MCMC run. It is written against the current derived-quantity output (discrepancy_model, discrepancy_simulated), but that format is not settled, and nothing is wired up to feed it yet.

Typical workflow

  1. Build the model — an ordinary NIMBLE model with your data in it.

  2. Describe your discrepanciesdiscrepancy("mean") names one the package ships; discrepancy("asymm", modelNodes = "mu", fun = myAsymm) supplies your own as a nimbleFunction with contains = discrepancyBase.

  3. Describe the replicatessimulation("conditional") redraws the data from each posterior draw. simulation("marginal", simulateNodes = ...) redraws latent nodes as well, and you must name them.

  4. Run the calibration — hand both to runCalibrationNIMBLE(), which runs the long chain, simulates $r$ replicate datasets, runs a short chain on each, and turns the results into a PPP and a CPPP.

  5. Estimate the variancetransferAutocorrelation(), once implemented.


Example

The Newcomb light-speed measurements, with two discrepancies at once: mean, which a normal model should fit, and an asymmetry statistic, which it should not. The full script is in inst/examples/newcomb_spec_offline.R.

library(nimble)
library(cppp)
lightPath<- system.file("examples", "light.txt", package="cppp")
newcombData<-list(y= read.table(lightPath)$V1)
newcombModel<- nimbleModel(
code= nimbleCode({
for (iin1:n) y[i] ~ dnorm(mu, sd=sigma)
mu~ dflat()
log(sigma) ~ dflat()
}),
data=newcombData,
constants=list(n= length(newcombData$y)),
inits=list(mu=0, log_sigma=2)
)
## Your own discrepancy: how lopsided the two tails are around mu.## sort() does not compile, so wrap R's.sortR<- nimbleRcall(prototype=function(x= double(1)) {},
Rfun="sort", returnType= double(1))
asymmDisc<- nimbleFunction(
contains=discrepancyBase,
setup=function(model, dataNodes, modelNodes) {},
run=function() {
ys<- sortR(values(model, dataNodes))
mu<- values(model, modelNodes)[1]
returnType(double(0))
return(abs(ys[61] -mu) - abs(ys[6] -mu))
}
)
res<- runCalibrationNIMBLE(
model=newcombModel,
dataNames="y",
paramNames= c("mu", "sigma"),
discrepancies=list(discrepancy("mean"),
discrepancy("asymm", modelNodes="mu", fun=asymmDisc)),
simulation= simulation("conditional"),
nReps=20
)
res$obsPPP# one per discrepancy; mean near 0.5, asymm far from itres$CPPP# one per discrepancyres$repPPP# nReps x 2

About

Calibrated posterior predictive p-values

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

cppp

A package for Calibrated posterior predictive p-values.

⚠️ Work in progress — not ready for use

This is an active research project in early development. It is public so collaborators can follow along, not because it is ready.

There is no released version, and nothing here should be depended on.

If you want to try it, get in touch first.

It provides a general framework for a MCMC engine—to:

  1. Compute calibrated posterior p-values (cppp),
  2. Estimate their Monte Carlo variance using the idea of the transfer effective sample size (ESS)
  3. Can handle different MCMC engines. NIMBLE and R for now, other MCMC engines later.

Concept

Given data $y$, a model $p(\theta \mid y) \propto p(y \mid \theta) \pi (\theta)$, and a discrepancy function $D(y,\theta)$:

  1. Run an long MCMC chain to obtain draws from the posterior $p(\theta \mid y)$. With $M$ draws, we sample new datafrom the posterior predictive of the data $p(y^* \mid \theta_i)$ and compute

$$ \Delta_i = D(y^*_i, \theta_i) - D(y, \theta_i), $$

This chain of $\Delta = { \Delta_i }$ collects the observed discrepancies.

  1. Generate $r$calibration replicates:

    • simulate new datasets $\tilde{y}_j$ from the model,
    • run short chains of length $\tilde{m} $ for $p(\theta \mid \tilde{y}_j)$,
    • compute short-run posterior-predictive p-values $\hat{p}_j$.
  2. Estimate the CPPP:

$$ \widehat{\text{cppp}} = \frac{1}{r}\sum_{j=1}^r \mathbf{1}{\hat{p}_j \le \hat{p}_{\text{obs}}}. $$

  1. Estimate the Monte Carlo variance using the transfer ESS idea: match each $\hat{p}_j$ to a quantile on the observed $\Delta$ -chain, compute the transfer autocorrelation, and estimate the cppp variance.

Package architecture

There is a one-page architecture map: the two routes through the package, what you write, what comes back, and what is not not working yet.

View the architecture map · source

Or, once the package is installed:

browseURL(system.file("cheatsheet", "cppp-architecture.html", package="cppp"))

Main functions

FunctionPurpose
runCalibration()The generic engine. No model in it — just posterior draws and the functions you give it.
runCalibrationNIMBLE()The NIMBLE wrapper. Give it a model and your specifications and it builds the rest itself.

Describing what you want

FunctionRole
discrepancy()Describes one discrepancy: this is a list containing name, the nodes it reads, and optionally a custom nimbleFunction.
simulation()Describes how a replicate dataset is generated. Two options: "conditional" (redraw the data) or "marginal" (redraw named latent nodes too).
discrepancyBasea virtual base class. A custom nimbleFunction discrepancy must contain it.

Five discrepancies are builtin with the package, and naming one is a shortcut for writing it yourself: mean, variance, deviance, chisquared, freemantukey.

Implementation notes

Both simulation() and discrepancy() take a dataNodes argument. The simulation's set has to cover the discrepancy's:

sim <- simulation("conditional", dataNodes = c("y", "z")) # writes y and z
discrepancy("mean", dataNodes = "y") # reads y -- fine, a subset
discrepancy("mean", dataNodes = c("y", "z")) # reads both -- fine
discrepancy("mean", dataNodes = "w") # reads w -- error

The simulation writes first and the discrepancy reads afterwards. A node the simulation never writes still holds the value from the previous draw, so a discrepancy reading it would not actually use a new replicate. The calculator checks the two sets before it starts and stops, naming the nodes at fault.

For paramNodes the story is the same. Before either one does anything, the calculator and the simulator each write a posterior draw into their model. Both look up the values they need by node name, so both must be given the same parameter nodes — and every one of those nodes has to appear as a column in the posterior samples. If one is missing, you get an error naming it.

runCalibrationNIMBLE() arranges all of this for you. It passes your dataNames to the simulation and your paramNames to both the calculator and the simulator, so they cannot disagree. You only need to think about it if you build a calculator or a simulator yourself.

Each piece gets its own copy of the model. Three copies are made in all:

copyused bycompiled?
the originalthe MCMCyes
model$newModel()the discrepancy calculatoryes, together with the discrepancies
model$newModel()the replicate simulatorno, it stays plain R

The three run one after another, never at the same time: simulate a replicate dataset, run a short chain on it, then compute the discrepancies. What the copies avoid is not a clash but the state each one leaves behind. A model remembers the last values written into it, and the MCMC starts its next chain from wherever its model happens to be. Separate copies keep one step from moving another's starting point.

The cost worth knowing about is that the model is compiled twice, once for the MCMC and once with the discrepancies. That is the slow part of setting up a run. The simulator's copy is cheap by comparison, since it is never compiled.

Open question. The third copy may not be needed. Once the calculator is compiled it works on the C model and never touches the R model it was built from, so the simulator could reuse that one. The saving is small and the risk is a state bug that gives wrong numbers rather than an error, so this is written down for discussion rather than acted on.

Watch out for derived parameters. Say the prior is on log(sigma) but you monitor sigma. After writing a draw into the model, the calculator recalculates only the nodes below the parameters. It never recalculates the parameters themselves. If it did, it would rebuild sigma from its parents and throw away the value just drawn — the same sigma for every draw, with no error to warn you. Because it skips them, monitoring sigma and monitoring log_sigma both work.

Turning descriptions into numbers

FunctionRole
makeDiscrepancyCalculator()Builds the calculator that simulate from the posterior predictive and returns $D(y,\theta)$ and $D(y^*,\theta)$ per each replicated dataset, per discrepancy. Discrepancies and loop over the discrepancies compile together in one call using makeDiscrepancyNimbleFun().
makeDiscrepancyNimbleFun()Turns one specification into a nimbleFunction. Mostly used by the calculator.
makeSimulateNewDataFun()Builds the function that simulates one replicate dataset from a draw.
makeDiscrepancyExtractor()Placeholder. The online counterpart — reads discrepancies the MCMC already computed instead of recomputing them.

runCalibrationNIMBLE() calls the first two for you when you pass discrepancies and simulation, so most of the time you never touch them directly.

Results

runCalibration() returns a list with:

  • CPPP — one value per discrepancy
  • obsPPP, repPPP — observed and replicated posterior predictive p-values
  • discrepancies — the observed and replicated discrepancy values
  • drawnIndices — which posterior draws seeded the calibration replicates

runCalibration() returns this as a cpppResult S3 object, built and checked by newCpppResult(). Standard errors, confidence intervals, and print / summary / plot methods are planned but not implemented, so for now it prints as a plain list.

Placeholders

Two pieces are deliberately unfinished, and neither is tested:

FileWhat it is
R/transferAutocorrelation.RtransferAutocorrelation() stops with "Not implemented". Corresponds to step 4 of the concept above, i.e., the transfer-ESS estimate of the cppp variance.
R/makeDiscrepancyExtractor.RmakeDiscrepancyExtractor() reads discrepancy columns that NIMBLE computed during the MCMC run. It is written against the current derived-quantity output (discrepancy_model, discrepancy_simulated), but that format is not settled, and nothing is wired up to feed it yet.

Typical workflow

  1. Build the model — an ordinary NIMBLE model with your data in it.

  2. Describe your discrepanciesdiscrepancy("mean") names one the package ships; discrepancy("asymm", modelNodes = "mu", fun = myAsymm) supplies your own as a nimbleFunction with contains = discrepancyBase.

  3. Describe the replicatessimulation("conditional") redraws the data from each posterior draw. simulation("marginal", simulateNodes = ...) redraws latent nodes as well, and you must name them.

  4. Run the calibration — hand both to runCalibrationNIMBLE(), which runs the long chain, simulates $r$ replicate datasets, runs a short chain on each, and turns the results into a PPP and a CPPP.

  5. Estimate the variancetransferAutocorrelation(), once implemented.


Example

The Newcomb light-speed measurements, with two discrepancies at once: mean, which a normal model should fit, and an asymmetry statistic, which it should not. The full script is in inst/examples/newcomb_spec_offline.R.

library(nimble)
library(cppp)
lightPath<- system.file("examples", "light.txt", package="cppp")
newcombData<-list(y= read.table(lightPath)$V1)
newcombModel<- nimbleModel(
code= nimbleCode({
for (iin1:n) y[i] ~ dnorm(mu, sd=sigma)
mu~ dflat()
log(sigma) ~ dflat()
}),
data=newcombData,
constants=list(n= length(newcombData$y)),
inits=list(mu=0, log_sigma=2)
)
## Your own discrepancy: how lopsided the two tails are around mu.## sort() does not compile, so wrap R's.sortR<- nimbleRcall(prototype=function(x= double(1)) {},
Rfun="sort", returnType= double(1))
asymmDisc<- nimbleFunction(
contains=discrepancyBase,
setup=function(model, dataNodes, modelNodes) {},
run=function() {
ys<- sortR(values(model, dataNodes))
mu<- values(model, modelNodes)[1]
returnType(double(0))
return(abs(ys[61] -mu) - abs(ys[6] -mu))
}
)
res<- runCalibrationNIMBLE(
model=newcombModel,
dataNames="y",
paramNames= c("mu", "sigma"),
discrepancies=list(discrepancy("mean"),
discrepancy("asymm", modelNodes="mu", fun=asymmDisc)),
simulation= simulation("conditional"),
nReps=20
)
res$obsPPP# one per discrepancy; mean near 0.5, asymm far from itres$CPPP# one per discrepancyres$repPPP# nReps x 2

About

Calibrated posterior predictive p-values

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

cppp

A package for Calibrated posterior predictive p-values.

⚠️ Work in progress — not ready for use

This is an active research project in early development. It is public so collaborators can follow along, not because it is ready.

There is no released version, and nothing here should be depended on.

If you want to try it, get in touch first.

It provides a general framework for a MCMC engine—to:

  1. Compute calibrated posterior p-values (cppp),
  2. Estimate their Monte Carlo variance using the idea of the transfer effective sample size (ESS)
  3. Can handle different MCMC engines. NIMBLE and R for now, other MCMC engines later.

Concept

Given data $y$, a model $p(\theta \mid y) \propto p(y \mid \theta) \pi (\theta)$, and a discrepancy function $D(y,\theta)$:

  1. Run an long MCMC chain to obtain draws from the posterior $p(\theta \mid y)$. With $M$ draws, we sample new datafrom the posterior predictive of the data $p(y^* \mid \theta_i)$ and compute

$$ \Delta_i = D(y^*_i, \theta_i) - D(y, \theta_i), $$

This chain of $\Delta = { \Delta_i }$ collects the observed discrepancies.

  1. Generate $r$calibration replicates:

    • simulate new datasets $\tilde{y}_j$ from the model,
    • run short chains of length $\tilde{m} $ for $p(\theta \mid \tilde{y}_j)$,
    • compute short-run posterior-predictive p-values $\hat{p}_j$.
  2. Estimate the CPPP:

$$ \widehat{\text{cppp}} = \frac{1}{r}\sum_{j=1}^r \mathbf{1}{\hat{p}_j \le \hat{p}_{\text{obs}}}. $$

  1. Estimate the Monte Carlo variance using the transfer ESS idea: match each $\hat{p}_j$ to a quantile on the observed $\Delta$ -chain, compute the transfer autocorrelation, and estimate the cppp variance.

Package architecture

There is a one-page architecture map: the two routes through the package, what you write, what comes back, and what is not not working yet.

View the architecture map · source

Or, once the package is installed:

browseURL(system.file("cheatsheet", "cppp-architecture.html", package="cppp"))

Main functions

FunctionPurpose
runCalibration()The generic engine. No model in it — just posterior draws and the functions you give it.
runCalibrationNIMBLE()The NIMBLE wrapper. Give it a model and your specifications and it builds the rest itself.

Describing what you want

FunctionRole
discrepancy()Describes one discrepancy: this is a list containing name, the nodes it reads, and optionally a custom nimbleFunction.
simulation()Describes how a replicate dataset is generated. Two options: "conditional" (redraw the data) or "marginal" (redraw named latent nodes too).
discrepancyBasea virtual base class. A custom nimbleFunction discrepancy must contain it.

Five discrepancies are builtin with the package, and naming one is a shortcut for writing it yourself: mean, variance, deviance, chisquared, freemantukey.

Implementation notes

Both simulation() and discrepancy() take a dataNodes argument. The simulation's set has to cover the discrepancy's:

sim <- simulation("conditional", dataNodes = c("y", "z")) # writes y and z
discrepancy("mean", dataNodes = "y") # reads y -- fine, a subset
discrepancy("mean", dataNodes = c("y", "z")) # reads both -- fine
discrepancy("mean", dataNodes = "w") # reads w -- error

The simulation writes first and the discrepancy reads afterwards. A node the simulation never writes still holds the value from the previous draw, so a discrepancy reading it would not actually use a new replicate. The calculator checks the two sets before it starts and stops, naming the nodes at fault.

For paramNodes the story is the same. Before either one does anything, the calculator and the simulator each write a posterior draw into their model. Both look up the values they need by node name, so both must be given the same parameter nodes — and every one of those nodes has to appear as a column in the posterior samples. If one is missing, you get an error naming it.

runCalibrationNIMBLE() arranges all of this for you. It passes your dataNames to the simulation and your paramNames to both the calculator and the simulator, so they cannot disagree. You only need to think about it if you build a calculator or a simulator yourself.

Each piece gets its own copy of the model. Three copies are made in all:

copyused bycompiled?
the originalthe MCMCyes
model$newModel()the discrepancy calculatoryes, together with the discrepancies
model$newModel()the replicate simulatorno, it stays plain R

The three run one after another, never at the same time: simulate a replicate dataset, run a short chain on it, then compute the discrepancies. What the copies avoid is not a clash but the state each one leaves behind. A model remembers the last values written into it, and the MCMC starts its next chain from wherever its model happens to be. Separate copies keep one step from moving another's starting point.

The cost worth knowing about is that the model is compiled twice, once for the MCMC and once with the discrepancies. That is the slow part of setting up a run. The simulator's copy is cheap by comparison, since it is never compiled.

Open question. The third copy may not be needed. Once the calculator is compiled it works on the C model and never touches the R model it was built from, so the simulator could reuse that one. The saving is small and the risk is a state bug that gives wrong numbers rather than an error, so this is written down for discussion rather than acted on.

Watch out for derived parameters. Say the prior is on log(sigma) but you monitor sigma. After writing a draw into the model, the calculator recalculates only the nodes below the parameters. It never recalculates the parameters themselves. If it did, it would rebuild sigma from its parents and throw away the value just drawn — the same sigma for every draw, with no error to warn you. Because it skips them, monitoring sigma and monitoring log_sigma both work.

Turning descriptions into numbers

FunctionRole
makeDiscrepancyCalculator()Builds the calculator that simulate from the posterior predictive and returns $D(y,\theta)$ and $D(y^*,\theta)$ per each replicated dataset, per discrepancy. Discrepancies and loop over the discrepancies compile together in one call using makeDiscrepancyNimbleFun().
makeDiscrepancyNimbleFun()Turns one specification into a nimbleFunction. Mostly used by the calculator.
makeSimulateNewDataFun()Builds the function that simulates one replicate dataset from a draw.
makeDiscrepancyExtractor()Placeholder. The online counterpart — reads discrepancies the MCMC already computed instead of recomputing them.

runCalibrationNIMBLE() calls the first two for you when you pass discrepancies and simulation, so most of the time you never touch them directly.

Results

runCalibration() returns a list with:

  • CPPP — one value per discrepancy
  • obsPPP, repPPP — observed and replicated posterior predictive p-values
  • discrepancies — the observed and replicated discrepancy values
  • drawnIndices — which posterior draws seeded the calibration replicates

runCalibration() returns this as a cpppResult S3 object, built and checked by newCpppResult(). Standard errors, confidence intervals, and print / summary / plot methods are planned but not implemented, so for now it prints as a plain list.

Placeholders

Two pieces are deliberately unfinished, and neither is tested:

FileWhat it is
R/transferAutocorrelation.RtransferAutocorrelation() stops with "Not implemented". Corresponds to step 4 of the concept above, i.e., the transfer-ESS estimate of the cppp variance.
R/makeDiscrepancyExtractor.RmakeDiscrepancyExtractor() reads discrepancy columns that NIMBLE computed during the MCMC run. It is written against the current derived-quantity output (discrepancy_model, discrepancy_simulated), but that format is not settled, and nothing is wired up to feed it yet.

Typical workflow

  1. Build the model — an ordinary NIMBLE model with your data in it.

  2. Describe your discrepanciesdiscrepancy("mean") names one the package ships; discrepancy("asymm", modelNodes = "mu", fun = myAsymm) supplies your own as a nimbleFunction with contains = discrepancyBase.

  3. Describe the replicatessimulation("conditional") redraws the data from each posterior draw. simulation("marginal", simulateNodes = ...) redraws latent nodes as well, and you must name them.

  4. Run the calibration — hand both to runCalibrationNIMBLE(), which runs the long chain, simulates $r$ replicate datasets, runs a short chain on each, and turns the results into a PPP and a CPPP.

  5. Estimate the variancetransferAutocorrelation(), once implemented.


Example

The Newcomb light-speed measurements, with two discrepancies at once: mean, which a normal model should fit, and an asymmetry statistic, which it should not. The full script is in inst/examples/newcomb_spec_offline.R.

library(nimble)
library(cppp)
lightPath<- system.file("examples", "light.txt", package="cppp")
newcombData<-list(y= read.table(lightPath)$V1)
newcombModel<- nimbleModel(
code= nimbleCode({
for (iin1:n) y[i] ~ dnorm(mu, sd=sigma)
mu~ dflat()
log(sigma) ~ dflat()
}),
data=newcombData,
constants=list(n= length(newcombData$y)),
inits=list(mu=0, log_sigma=2)
)
## Your own discrepancy: how lopsided the two tails are around mu.## sort() does not compile, so wrap R's.sortR<- nimbleRcall(prototype=function(x= double(1)) {},
Rfun="sort", returnType= double(1))
asymmDisc<- nimbleFunction(
contains=discrepancyBase,
setup=function(model, dataNodes, modelNodes) {},
run=function() {
ys<- sortR(values(model, dataNodes))
mu<- values(model, modelNodes)[1]
returnType(double(0))
return(abs(ys[61] -mu) - abs(ys[6] -mu))
}
)
res<- runCalibrationNIMBLE(
model=newcombModel,
dataNames="y",
paramNames= c("mu", "sigma"),
discrepancies=list(discrepancy("mean"),
discrepancy("asymm", modelNodes="mu", fun=asymmDisc)),
simulation= simulation("conditional"),
nReps=20
)
res$obsPPP# one per discrepancy; mean near 0.5, asymm far from itres$CPPP# one per discrepancyres$repPPP# nReps x 2

About

Calibrated posterior predictive p-values

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

cppp

A package for Calibrated posterior predictive p-values.

⚠️ Work in progress — not ready for use

This is an active research project in early development. It is public so collaborators can follow along, not because it is ready.

There is no released version, and nothing here should be depended on.

If you want to try it, get in touch first.

It provides a general framework for a MCMC engine—to:

  1. Compute calibrated posterior p-values (cppp),
  2. Estimate their Monte Carlo variance using the idea of the transfer effective sample size (ESS)
  3. Can handle different MCMC engines. NIMBLE and R for now, other MCMC engines later.

Concept

Given data $y$, a model $p(\theta \mid y) \propto p(y \mid \theta) \pi (\theta)$, and a discrepancy function $D(y,\theta)$:

  1. Run an long MCMC chain to obtain draws from the posterior $p(\theta \mid y)$. With $M$ draws, we sample new datafrom the posterior predictive of the data $p(y^* \mid \theta_i)$ and compute

$$ \Delta_i = D(y^*_i, \theta_i) - D(y, \theta_i), $$

This chain of $\Delta = { \Delta_i }$ collects the observed discrepancies.

  1. Generate $r$calibration replicates:

    • simulate new datasets $\tilde{y}_j$ from the model,
    • run short chains of length $\tilde{m} $ for $p(\theta \mid \tilde{y}_j)$,
    • compute short-run posterior-predictive p-values $\hat{p}_j$.
  2. Estimate the CPPP:

$$ \widehat{\text{cppp}} = \frac{1}{r}\sum_{j=1}^r \mathbf{1}{\hat{p}_j \le \hat{p}_{\text{obs}}}. $$

  1. Estimate the Monte Carlo variance using the transfer ESS idea: match each $\hat{p}_j$ to a quantile on the observed $\Delta$ -chain, compute the transfer autocorrelation, and estimate the cppp variance.

Package architecture

There is a one-page architecture map: the two routes through the package, what you write, what comes back, and what is not not working yet.

View the architecture map · source

Or, once the package is installed:

browseURL(system.file("cheatsheet", "cppp-architecture.html", package="cppp"))

Main functions

FunctionPurpose
runCalibration()The generic engine. No model in it — just posterior draws and the functions you give it.
runCalibrationNIMBLE()The NIMBLE wrapper. Give it a model and your specifications and it builds the rest itself.

Describing what you want

FunctionRole
discrepancy()Describes one discrepancy: this is a list containing name, the nodes it reads, and optionally a custom nimbleFunction.
simulation()Describes how a replicate dataset is generated. Two options: "conditional" (redraw the data) or "marginal" (redraw named latent nodes too).
discrepancyBasea virtual base class. A custom nimbleFunction discrepancy must contain it.

Five discrepancies are builtin with the package, and naming one is a shortcut for writing it yourself: mean, variance, deviance, chisquared, freemantukey.

Implementation notes

Both simulation() and discrepancy() take a dataNodes argument. The simulation's set has to cover the discrepancy's:

sim <- simulation("conditional", dataNodes = c("y", "z")) # writes y and z
discrepancy("mean", dataNodes = "y") # reads y -- fine, a subset
discrepancy("mean", dataNodes = c("y", "z")) # reads both -- fine
discrepancy("mean", dataNodes = "w") # reads w -- error

The simulation writes first and the discrepancy reads afterwards. A node the simulation never writes still holds the value from the previous draw, so a discrepancy reading it would not actually use a new replicate. The calculator checks the two sets before it starts and stops, naming the nodes at fault.

For paramNodes the story is the same. Before either one does anything, the calculator and the simulator each write a posterior draw into their model. Both look up the values they need by node name, so both must be given the same parameter nodes — and every one of those nodes has to appear as a column in the posterior samples. If one is missing, you get an error naming it.

runCalibrationNIMBLE() arranges all of this for you. It passes your dataNames to the simulation and your paramNames to both the calculator and the simulator, so they cannot disagree. You only need to think about it if you build a calculator or a simulator yourself.

Each piece gets its own copy of the model. Three copies are made in all:

copyused bycompiled?
the originalthe MCMCyes
model$newModel()the discrepancy calculatoryes, together with the discrepancies
model$newModel()the replicate simulatorno, it stays plain R

The three run one after another, never at the same time: simulate a replicate dataset, run a short chain on it, then compute the discrepancies. What the copies avoid is not a clash but the state each one leaves behind. A model remembers the last values written into it, and the MCMC starts its next chain from wherever its model happens to be. Separate copies keep one step from moving another's starting point.

The cost worth knowing about is that the model is compiled twice, once for the MCMC and once with the discrepancies. That is the slow part of setting up a run. The simulator's copy is cheap by comparison, since it is never compiled.

Open question. The third copy may not be needed. Once the calculator is compiled it works on the C model and never touches the R model it was built from, so the simulator could reuse that one. The saving is small and the risk is a state bug that gives wrong numbers rather than an error, so this is written down for discussion rather than acted on.

Watch out for derived parameters. Say the prior is on log(sigma) but you monitor sigma. After writing a draw into the model, the calculator recalculates only the nodes below the parameters. It never recalculates the parameters themselves. If it did, it would rebuild sigma from its parents and throw away the value just drawn — the same sigma for every draw, with no error to warn you. Because it skips them, monitoring sigma and monitoring log_sigma both work.

Turning descriptions into numbers

FunctionRole
makeDiscrepancyCalculator()Builds the calculator that simulate from the posterior predictive and returns $D(y,\theta)$ and $D(y^*,\theta)$ per each replicated dataset, per discrepancy. Discrepancies and loop over the discrepancies compile together in one call using makeDiscrepancyNimbleFun().
makeDiscrepancyNimbleFun()Turns one specification into a nimbleFunction. Mostly used by the calculator.
makeSimulateNewDataFun()Builds the function that simulates one replicate dataset from a draw.
makeDiscrepancyExtractor()Placeholder. The online counterpart — reads discrepancies the MCMC already computed instead of recomputing them.

runCalibrationNIMBLE() calls the first two for you when you pass discrepancies and simulation, so most of the time you never touch them directly.

Results

runCalibration() returns a list with:

  • CPPP — one value per discrepancy
  • obsPPP, repPPP — observed and replicated posterior predictive p-values
  • discrepancies — the observed and replicated discrepancy values
  • drawnIndices — which posterior draws seeded the calibration replicates

runCalibration() returns this as a cpppResult S3 object, built and checked by newCpppResult(). Standard errors, confidence intervals, and print / summary / plot methods are planned but not implemented, so for now it prints as a plain list.

Placeholders

Two pieces are deliberately unfinished, and neither is tested:

FileWhat it is
R/transferAutocorrelation.RtransferAutocorrelation() stops with "Not implemented". Corresponds to step 4 of the concept above, i.e., the transfer-ESS estimate of the cppp variance.
R/makeDiscrepancyExtractor.RmakeDiscrepancyExtractor() reads discrepancy columns that NIMBLE computed during the MCMC run. It is written against the current derived-quantity output (discrepancy_model, discrepancy_simulated), but that format is not settled, and nothing is wired up to feed it yet.

Typical workflow

  1. Build the model — an ordinary NIMBLE model with your data in it.

  2. Describe your discrepanciesdiscrepancy("mean") names one the package ships; discrepancy("asymm", modelNodes = "mu", fun = myAsymm) supplies your own as a nimbleFunction with contains = discrepancyBase.

  3. Describe the replicatessimulation("conditional") redraws the data from each posterior draw. simulation("marginal", simulateNodes = ...) redraws latent nodes as well, and you must name them.

  4. Run the calibration — hand both to runCalibrationNIMBLE(), which runs the long chain, simulates $r$ replicate datasets, runs a short chain on each, and turns the results into a PPP and a CPPP.

  5. Estimate the variancetransferAutocorrelation(), once implemented.


Example

The Newcomb light-speed measurements, with two discrepancies at once: mean, which a normal model should fit, and an asymmetry statistic, which it should not. The full script is in inst/examples/newcomb_spec_offline.R.

library(nimble)
library(cppp)
lightPath<- system.file("examples", "light.txt", package="cppp")
newcombData<-list(y= read.table(lightPath)$V1)
newcombModel<- nimbleModel(
code= nimbleCode({
for (iin1:n) y[i] ~ dnorm(mu, sd=sigma)
mu~ dflat()
log(sigma) ~ dflat()
}),
data=newcombData,
constants=list(n= length(newcombData$y)),
inits=list(mu=0, log_sigma=2)
)
## Your own discrepancy: how lopsided the two tails are around mu.## sort() does not compile, so wrap R's.sortR<- nimbleRcall(prototype=function(x= double(1)) {},
Rfun="sort", returnType= double(1))
asymmDisc<- nimbleFunction(
contains=discrepancyBase,
setup=function(model, dataNodes, modelNodes) {},
run=function() {
ys<- sortR(values(model, dataNodes))
mu<- values(model, modelNodes)[1]
returnType(double(0))
return(abs(ys[61] -mu) - abs(ys[6] -mu))
}
)
res<- runCalibrationNIMBLE(
model=newcombModel,
dataNames="y",
paramNames= c("mu", "sigma"),
discrepancies=list(discrepancy("mean"),
discrepancy("asymm", modelNodes="mu", fun=asymmDisc)),
simulation= simulation("conditional"),
nReps=20
)
res$obsPPP# one per discrepancy; mean near 0.5, asymm far from itres$CPPP# one per discrepancyres$repPPP# nReps x 2

About

Calibrated posterior predictive p-values

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

cppp

A package for Calibrated posterior predictive p-values.

⚠️ Work in progress — not ready for use

This is an active research project in early development. It is public so collaborators can follow along, not because it is ready.

There is no released version, and nothing here should be depended on.

If you want to try it, get in touch first.

It provides a general framework for a MCMC engine—to:

  1. Compute calibrated posterior p-values (cppp),
  2. Estimate their Monte Carlo variance using the idea of the transfer effective sample size (ESS)
  3. Can handle different MCMC engines. NIMBLE and R for now, other MCMC engines later.

Concept

Given data $y$, a model $p(\theta \mid y) \propto p(y \mid \theta) \pi (\theta)$, and a discrepancy function $D(y,\theta)$:

  1. Run an long MCMC chain to obtain draws from the posterior $p(\theta \mid y)$. With $M$ draws, we sample new datafrom the posterior predictive of the data $p(y^* \mid \theta_i)$ and compute

$$ \Delta_i = D(y^*_i, \theta_i) - D(y, \theta_i), $$

This chain of $\Delta = { \Delta_i }$ collects the observed discrepancies.

  1. Generate $r$calibration replicates:

    • simulate new datasets $\tilde{y}_j$ from the model,
    • run short chains of length $\tilde{m} $ for $p(\theta \mid \tilde{y}_j)$,
    • compute short-run posterior-predictive p-values $\hat{p}_j$.
  2. Estimate the CPPP:

$$ \widehat{\text{cppp}} = \frac{1}{r}\sum_{j=1}^r \mathbf{1}{\hat{p}_j \le \hat{p}_{\text{obs}}}. $$

  1. Estimate the Monte Carlo variance using the transfer ESS idea: match each $\hat{p}_j$ to a quantile on the observed $\Delta$ -chain, compute the transfer autocorrelation, and estimate the cppp variance.

Package architecture

There is a one-page architecture map: the two routes through the package, what you write, what comes back, and what is not not working yet.

View the architecture map · source

Or, once the package is installed:

browseURL(system.file("cheatsheet", "cppp-architecture.html", package="cppp"))

Main functions

FunctionPurpose
runCalibration()The generic engine. No model in it — just posterior draws and the functions you give it.
runCalibrationNIMBLE()The NIMBLE wrapper. Give it a model and your specifications and it builds the rest itself.

Describing what you want

FunctionRole
discrepancy()Describes one discrepancy: this is a list containing name, the nodes it reads, and optionally a custom nimbleFunction.
simulation()Describes how a replicate dataset is generated. Two options: "conditional" (redraw the data) or "marginal" (redraw named latent nodes too).
discrepancyBasea virtual base class. A custom nimbleFunction discrepancy must contain it.

Five discrepancies are builtin with the package, and naming one is a shortcut for writing it yourself: mean, variance, deviance, chisquared, freemantukey.

Implementation notes

Both simulation() and discrepancy() take a dataNodes argument. The simulation's set has to cover the discrepancy's:

sim <- simulation("conditional", dataNodes = c("y", "z")) # writes y and z
discrepancy("mean", dataNodes = "y") # reads y -- fine, a subset
discrepancy("mean", dataNodes = c("y", "z")) # reads both -- fine
discrepancy("mean", dataNodes = "w") # reads w -- error

The simulation writes first and the discrepancy reads afterwards. A node the simulation never writes still holds the value from the previous draw, so a discrepancy reading it would not actually use a new replicate. The calculator checks the two sets before it starts and stops, naming the nodes at fault.

For paramNodes the story is the same. Before either one does anything, the calculator and the simulator each write a posterior draw into their model. Both look up the values they need by node name, so both must be given the same parameter nodes — and every one of those nodes has to appear as a column in the posterior samples. If one is missing, you get an error naming it.

runCalibrationNIMBLE() arranges all of this for you. It passes your dataNames to the simulation and your paramNames to both the calculator and the simulator, so they cannot disagree. You only need to think about it if you build a calculator or a simulator yourself.

Each piece gets its own copy of the model. Three copies are made in all:

copyused bycompiled?
the originalthe MCMCyes
model$newModel()the discrepancy calculatoryes, together with the discrepancies
model$newModel()the replicate simulatorno, it stays plain R

The three run one after another, never at the same time: simulate a replicate dataset, run a short chain on it, then compute the discrepancies. What the copies avoid is not a clash but the state each one leaves behind. A model remembers the last values written into it, and the MCMC starts its next chain from wherever its model happens to be. Separate copies keep one step from moving another's starting point.

The cost worth knowing about is that the model is compiled twice, once for the MCMC and once with the discrepancies. That is the slow part of setting up a run. The simulator's copy is cheap by comparison, since it is never compiled.

Open question. The third copy may not be needed. Once the calculator is compiled it works on the C model and never touches the R model it was built from, so the simulator could reuse that one. The saving is small and the risk is a state bug that gives wrong numbers rather than an error, so this is written down for discussion rather than acted on.

Watch out for derived parameters. Say the prior is on log(sigma) but you monitor sigma. After writing a draw into the model, the calculator recalculates only the nodes below the parameters. It never recalculates the parameters themselves. If it did, it would rebuild sigma from its parents and throw away the value just drawn — the same sigma for every draw, with no error to warn you. Because it skips them, monitoring sigma and monitoring log_sigma both work.

Turning descriptions into numbers

FunctionRole
makeDiscrepancyCalculator()Builds the calculator that simulate from the posterior predictive and returns $D(y,\theta)$ and $D(y^*,\theta)$ per each replicated dataset, per discrepancy. Discrepancies and loop over the discrepancies compile together in one call using makeDiscrepancyNimbleFun().
makeDiscrepancyNimbleFun()Turns one specification into a nimbleFunction. Mostly used by the calculator.
makeSimulateNewDataFun()Builds the function that simulates one replicate dataset from a draw.
makeDiscrepancyExtractor()Placeholder. The online counterpart — reads discrepancies the MCMC already computed instead of recomputing them.

runCalibrationNIMBLE() calls the first two for you when you pass discrepancies and simulation, so most of the time you never touch them directly.

Results

runCalibration() returns a list with:

  • CPPP — one value per discrepancy
  • obsPPP, repPPP — observed and replicated posterior predictive p-values
  • discrepancies — the observed and replicated discrepancy values
  • drawnIndices — which posterior draws seeded the calibration replicates

runCalibration() returns this as a cpppResult S3 object, built and checked by newCpppResult(). Standard errors, confidence intervals, and print / summary / plot methods are planned but not implemented, so for now it prints as a plain list.

Placeholders

Two pieces are deliberately unfinished, and neither is tested:

FileWhat it is
R/transferAutocorrelation.RtransferAutocorrelation() stops with "Not implemented". Corresponds to step 4 of the concept above, i.e., the transfer-ESS estimate of the cppp variance.
R/makeDiscrepancyExtractor.RmakeDiscrepancyExtractor() reads discrepancy columns that NIMBLE computed during the MCMC run. It is written against the current derived-quantity output (discrepancy_model, discrepancy_simulated), but that format is not settled, and nothing is wired up to feed it yet.

Typical workflow

  1. Build the model — an ordinary NIMBLE model with your data in it.

  2. Describe your discrepanciesdiscrepancy("mean") names one the package ships; discrepancy("asymm", modelNodes = "mu", fun = myAsymm) supplies your own as a nimbleFunction with contains = discrepancyBase.

  3. Describe the replicatessimulation("conditional") redraws the data from each posterior draw. simulation("marginal", simulateNodes = ...) redraws latent nodes as well, and you must name them.

  4. Run the calibration — hand both to runCalibrationNIMBLE(), which runs the long chain, simulates $r$ replicate datasets, runs a short chain on each, and turns the results into a PPP and a CPPP.

  5. Estimate the variancetransferAutocorrelation(), once implemented.


Example

The Newcomb light-speed measurements, with two discrepancies at once: mean, which a normal model should fit, and an asymmetry statistic, which it should not. The full script is in inst/examples/newcomb_spec_offline.R.

library(nimble)
library(cppp)
lightPath<- system.file("examples", "light.txt", package="cppp")
newcombData<-list(y= read.table(lightPath)$V1)
newcombModel<- nimbleModel(
code= nimbleCode({
for (iin1:n) y[i] ~ dnorm(mu, sd=sigma)
mu~ dflat()
log(sigma) ~ dflat()
}),
data=newcombData,
constants=list(n= length(newcombData$y)),
inits=list(mu=0, log_sigma=2)
)
## Your own discrepancy: how lopsided the two tails are around mu.## sort() does not compile, so wrap R's.sortR<- nimbleRcall(prototype=function(x= double(1)) {},
Rfun="sort", returnType= double(1))
asymmDisc<- nimbleFunction(
contains=discrepancyBase,
setup=function(model, dataNodes, modelNodes) {},
run=function() {
ys<- sortR(values(model, dataNodes))
mu<- values(model, modelNodes)[1]
returnType(double(0))
return(abs(ys[61] -mu) - abs(ys[6] -mu))
}
)
res<- runCalibrationNIMBLE(
model=newcombModel,
dataNames="y",
paramNames= c("mu", "sigma"),
discrepancies=list(discrepancy("mean"),
discrepancy("asymm", modelNodes="mu", fun=asymmDisc)),
simulation= simulation("conditional"),
nReps=20
)
res$obsPPP# one per discrepancy; mean near 0.5, asymm far from itres$CPPP# one per discrepancyres$repPPP# nReps x 2

About

Calibrated posterior predictive p-values

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

cppp

A package for Calibrated posterior predictive p-values.

⚠️ Work in progress — not ready for use

This is an active research project in early development. It is public so collaborators can follow along, not because it is ready.

There is no released version, and nothing here should be depended on.

If you want to try it, get in touch first.

It provides a general framework for a MCMC engine—to:

  1. Compute calibrated posterior p-values (cppp),
  2. Estimate their Monte Carlo variance using the idea of the transfer effective sample size (ESS)
  3. Can handle different MCMC engines. NIMBLE and R for now, other MCMC engines later.

Concept

Given data $y$, a model $p(\theta \mid y) \propto p(y \mid \theta) \pi (\theta)$, and a discrepancy function $D(y,\theta)$:

  1. Run an long MCMC chain to obtain draws from the posterior $p(\theta \mid y)$. With $M$ draws, we sample new datafrom the posterior predictive of the data $p(y^* \mid \theta_i)$ and compute

$$ \Delta_i = D(y^*_i, \theta_i) - D(y, \theta_i), $$

This chain of $\Delta = { \Delta_i }$ collects the observed discrepancies.

  1. Generate $r$calibration replicates:

    • simulate new datasets $\tilde{y}_j$ from the model,
    • run short chains of length $\tilde{m} $ for $p(\theta \mid \tilde{y}_j)$,
    • compute short-run posterior-predictive p-values $\hat{p}_j$.
  2. Estimate the CPPP:

$$ \widehat{\text{cppp}} = \frac{1}{r}\sum_{j=1}^r \mathbf{1}{\hat{p}_j \le \hat{p}_{\text{obs}}}. $$

  1. Estimate the Monte Carlo variance using the transfer ESS idea: match each $\hat{p}_j$ to a quantile on the observed $\Delta$ -chain, compute the transfer autocorrelation, and estimate the cppp variance.

Package architecture

There is a one-page architecture map: the two routes through the package, what you write, what comes back, and what is not not working yet.

View the architecture map · source

Or, once the package is installed:

browseURL(system.file("cheatsheet", "cppp-architecture.html", package="cppp"))

Main functions

FunctionPurpose
runCalibration()The generic engine. No model in it — just posterior draws and the functions you give it.
runCalibrationNIMBLE()The NIMBLE wrapper. Give it a model and your specifications and it builds the rest itself.

Describing what you want

FunctionRole
discrepancy()Describes one discrepancy: this is a list containing name, the nodes it reads, and optionally a custom nimbleFunction.
simulation()Describes how a replicate dataset is generated. Two options: "conditional" (redraw the data) or "marginal" (redraw named latent nodes too).
discrepancyBasea virtual base class. A custom nimbleFunction discrepancy must contain it.

Five discrepancies are builtin with the package, and naming one is a shortcut for writing it yourself: mean, variance, deviance, chisquared, freemantukey.

Implementation notes

Both simulation() and discrepancy() take a dataNodes argument. The simulation's set has to cover the discrepancy's:

sim <- simulation("conditional", dataNodes = c("y", "z")) # writes y and z
discrepancy("mean", dataNodes = "y") # reads y -- fine, a subset
discrepancy("mean", dataNodes = c("y", "z")) # reads both -- fine
discrepancy("mean", dataNodes = "w") # reads w -- error

The simulation writes first and the discrepancy reads afterwards. A node the simulation never writes still holds the value from the previous draw, so a discrepancy reading it would not actually use a new replicate. The calculator checks the two sets before it starts and stops, naming the nodes at fault.

For paramNodes the story is the same. Before either one does anything, the calculator and the simulator each write a posterior draw into their model. Both look up the values they need by node name, so both must be given the same parameter nodes — and every one of those nodes has to appear as a column in the posterior samples. If one is missing, you get an error naming it.

runCalibrationNIMBLE() arranges all of this for you. It passes your dataNames to the simulation and your paramNames to both the calculator and the simulator, so they cannot disagree. You only need to think about it if you build a calculator or a simulator yourself.

Each piece gets its own copy of the model. Three copies are made in all:

copyused bycompiled?
the originalthe MCMCyes
model$newModel()the discrepancy calculatoryes, together with the discrepancies
model$newModel()the replicate simulatorno, it stays plain R

The three run one after another, never at the same time: simulate a replicate dataset, run a short chain on it, then compute the discrepancies. What the copies avoid is not a clash but the state each one leaves behind. A model remembers the last values written into it, and the MCMC starts its next chain from wherever its model happens to be. Separate copies keep one step from moving another's starting point.

The cost worth knowing about is that the model is compiled twice, once for the MCMC and once with the discrepancies. That is the slow part of setting up a run. The simulator's copy is cheap by comparison, since it is never compiled.

Open question. The third copy may not be needed. Once the calculator is compiled it works on the C model and never touches the R model it was built from, so the simulator could reuse that one. The saving is small and the risk is a state bug that gives wrong numbers rather than an error, so this is written down for discussion rather than acted on.

Watch out for derived parameters. Say the prior is on log(sigma) but you monitor sigma. After writing a draw into the model, the calculator recalculates only the nodes below the parameters. It never recalculates the parameters themselves. If it did, it would rebuild sigma from its parents and throw away the value just drawn — the same sigma for every draw, with no error to warn you. Because it skips them, monitoring sigma and monitoring log_sigma both work.

Turning descriptions into numbers

FunctionRole
makeDiscrepancyCalculator()Builds the calculator that simulate from the posterior predictive and returns $D(y,\theta)$ and $D(y^*,\theta)$ per each replicated dataset, per discrepancy. Discrepancies and loop over the discrepancies compile together in one call using makeDiscrepancyNimbleFun().
makeDiscrepancyNimbleFun()Turns one specification into a nimbleFunction. Mostly used by the calculator.
makeSimulateNewDataFun()Builds the function that simulates one replicate dataset from a draw.
makeDiscrepancyExtractor()Placeholder. The online counterpart — reads discrepancies the MCMC already computed instead of recomputing them.

runCalibrationNIMBLE() calls the first two for you when you pass discrepancies and simulation, so most of the time you never touch them directly.

Results

runCalibration() returns a list with:

  • CPPP — one value per discrepancy
  • obsPPP, repPPP — observed and replicated posterior predictive p-values
  • discrepancies — the observed and replicated discrepancy values
  • drawnIndices — which posterior draws seeded the calibration replicates

runCalibration() returns this as a cpppResult S3 object, built and checked by newCpppResult(). Standard errors, confidence intervals, and print / summary / plot methods are planned but not implemented, so for now it prints as a plain list.

Placeholders

Two pieces are deliberately unfinished, and neither is tested:

FileWhat it is
R/transferAutocorrelation.RtransferAutocorrelation() stops with "Not implemented". Corresponds to step 4 of the concept above, i.e., the transfer-ESS estimate of the cppp variance.
R/makeDiscrepancyExtractor.RmakeDiscrepancyExtractor() reads discrepancy columns that NIMBLE computed during the MCMC run. It is written against the current derived-quantity output (discrepancy_model, discrepancy_simulated), but that format is not settled, and nothing is wired up to feed it yet.

Typical workflow

  1. Build the model — an ordinary NIMBLE model with your data in it.

  2. Describe your discrepanciesdiscrepancy("mean") names one the package ships; discrepancy("asymm", modelNodes = "mu", fun = myAsymm) supplies your own as a nimbleFunction with contains = discrepancyBase.

  3. Describe the replicatessimulation("conditional") redraws the data from each posterior draw. simulation("marginal", simulateNodes = ...) redraws latent nodes as well, and you must name them.

  4. Run the calibration — hand both to runCalibrationNIMBLE(), which runs the long chain, simulates $r$ replicate datasets, runs a short chain on each, and turns the results into a PPP and a CPPP.

  5. Estimate the variancetransferAutocorrelation(), once implemented.


Example

The Newcomb light-speed measurements, with two discrepancies at once: mean, which a normal model should fit, and an asymmetry statistic, which it should not. The full script is in inst/examples/newcomb_spec_offline.R.

library(nimble)
library(cppp)
lightPath<- system.file("examples", "light.txt", package="cppp")
newcombData<-list(y= read.table(lightPath)$V1)
newcombModel<- nimbleModel(
code= nimbleCode({
for (iin1:n) y[i] ~ dnorm(mu, sd=sigma)
mu~ dflat()
log(sigma) ~ dflat()
}),
data=newcombData,
constants=list(n= length(newcombData$y)),
inits=list(mu=0, log_sigma=2)
)
## Your own discrepancy: how lopsided the two tails are around mu.## sort() does not compile, so wrap R's.sortR<- nimbleRcall(prototype=function(x= double(1)) {},
Rfun="sort", returnType= double(1))
asymmDisc<- nimbleFunction(
contains=discrepancyBase,
setup=function(model, dataNodes, modelNodes) {},
run=function() {
ys<- sortR(values(model, dataNodes))
mu<- values(model, modelNodes)[1]
returnType(double(0))
return(abs(ys[61] -mu) - abs(ys[6] -mu))
}
)
res<- runCalibrationNIMBLE(
model=newcombModel,
dataNames="y",
paramNames= c("mu", "sigma"),
discrepancies=list(discrepancy("mean"),
discrepancy("asymm", modelNodes="mu", fun=asymmDisc)),
simulation= simulation("conditional"),
nReps=20
)
res$obsPPP# one per discrepancy; mean near 0.5, asymm far from itres$CPPP# one per discrepancyres$repPPP# nReps x 2

About

Calibrated posterior predictive p-values

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

cppp

A package for Calibrated posterior predictive p-values.

⚠️ Work in progress — not ready for use

This is an active research project in early development. It is public so collaborators can follow along, not because it is ready.

There is no released version, and nothing here should be depended on.

If you want to try it, get in touch first.

It provides a general framework for a MCMC engine—to:

  1. Compute calibrated posterior p-values (cppp),
  2. Estimate their Monte Carlo variance using the idea of the transfer effective sample size (ESS)
  3. Can handle different MCMC engines. NIMBLE and R for now, other MCMC engines later.

Concept

Given data $y$, a model $p(\theta \mid y) \propto p(y \mid \theta) \pi (\theta)$, and a discrepancy function $D(y,\theta)$:

  1. Run an long MCMC chain to obtain draws from the posterior $p(\theta \mid y)$. With $M$ draws, we sample new datafrom the posterior predictive of the data $p(y^* \mid \theta_i)$ and compute

$$ \Delta_i = D(y^*_i, \theta_i) - D(y, \theta_i), $$

This chain of $\Delta = { \Delta_i }$ collects the observed discrepancies.

  1. Generate $r$calibration replicates:

    • simulate new datasets $\tilde{y}_j$ from the model,
    • run short chains of length $\tilde{m} $ for $p(\theta \mid \tilde{y}_j)$,
    • compute short-run posterior-predictive p-values $\hat{p}_j$.
  2. Estimate the CPPP:

$$ \widehat{\text{cppp}} = \frac{1}{r}\sum_{j=1}^r \mathbf{1}{\hat{p}_j \le \hat{p}_{\text{obs}}}. $$

  1. Estimate the Monte Carlo variance using the transfer ESS idea: match each $\hat{p}_j$ to a quantile on the observed $\Delta$ -chain, compute the transfer autocorrelation, and estimate the cppp variance.

Package architecture

There is a one-page architecture map: the two routes through the package, what you write, what comes back, and what is not not working yet.

View the architecture map · source

Or, once the package is installed:

browseURL(system.file("cheatsheet", "cppp-architecture.html", package="cppp"))

Main functions

FunctionPurpose
runCalibration()The generic engine. No model in it — just posterior draws and the functions you give it.
runCalibrationNIMBLE()The NIMBLE wrapper. Give it a model and your specifications and it builds the rest itself.

Describing what you want

FunctionRole
discrepancy()Describes one discrepancy: this is a list containing name, the nodes it reads, and optionally a custom nimbleFunction.
simulation()Describes how a replicate dataset is generated. Two options: "conditional" (redraw the data) or "marginal" (redraw named latent nodes too).
discrepancyBasea virtual base class. A custom nimbleFunction discrepancy must contain it.

Five discrepancies are builtin with the package, and naming one is a shortcut for writing it yourself: mean, variance, deviance, chisquared, freemantukey.

Implementation notes

Both simulation() and discrepancy() take a dataNodes argument. The simulation's set has to cover the discrepancy's:

sim <- simulation("conditional", dataNodes = c("y", "z")) # writes y and z
discrepancy("mean", dataNodes = "y") # reads y -- fine, a subset
discrepancy("mean", dataNodes = c("y", "z")) # reads both -- fine
discrepancy("mean", dataNodes = "w") # reads w -- error

The simulation writes first and the discrepancy reads afterwards. A node the simulation never writes still holds the value from the previous draw, so a discrepancy reading it would not actually use a new replicate. The calculator checks the two sets before it starts and stops, naming the nodes at fault.

For paramNodes the story is the same. Before either one does anything, the calculator and the simulator each write a posterior draw into their model. Both look up the values they need by node name, so both must be given the same parameter nodes — and every one of those nodes has to appear as a column in the posterior samples. If one is missing, you get an error naming it.

runCalibrationNIMBLE() arranges all of this for you. It passes your dataNames to the simulation and your paramNames to both the calculator and the simulator, so they cannot disagree. You only need to think about it if you build a calculator or a simulator yourself.

Each piece gets its own copy of the model. Three copies are made in all:

copyused bycompiled?
the originalthe MCMCyes
model$newModel()the discrepancy calculatoryes, together with the discrepancies
model$newModel()the replicate simulatorno, it stays plain R

The three run one after another, never at the same time: simulate a replicate dataset, run a short chain on it, then compute the discrepancies. What the copies avoid is not a clash but the state each one leaves behind. A model remembers the last values written into it, and the MCMC starts its next chain from wherever its model happens to be. Separate copies keep one step from moving another's starting point.

The cost worth knowing about is that the model is compiled twice, once for the MCMC and once with the discrepancies. That is the slow part of setting up a run. The simulator's copy is cheap by comparison, since it is never compiled.

Open question. The third copy may not be needed. Once the calculator is compiled it works on the C model and never touches the R model it was built from, so the simulator could reuse that one. The saving is small and the risk is a state bug that gives wrong numbers rather than an error, so this is written down for discussion rather than acted on.

Watch out for derived parameters. Say the prior is on log(sigma) but you monitor sigma. After writing a draw into the model, the calculator recalculates only the nodes below the parameters. It never recalculates the parameters themselves. If it did, it would rebuild sigma from its parents and throw away the value just drawn — the same sigma for every draw, with no error to warn you. Because it skips them, monitoring sigma and monitoring log_sigma both work.

Turning descriptions into numbers

FunctionRole
makeDiscrepancyCalculator()Builds the calculator that simulate from the posterior predictive and returns $D(y,\theta)$ and $D(y^*,\theta)$ per each replicated dataset, per discrepancy. Discrepancies and loop over the discrepancies compile together in one call using makeDiscrepancyNimbleFun().
makeDiscrepancyNimbleFun()Turns one specification into a nimbleFunction. Mostly used by the calculator.
makeSimulateNewDataFun()Builds the function that simulates one replicate dataset from a draw.
makeDiscrepancyExtractor()Placeholder. The online counterpart — reads discrepancies the MCMC already computed instead of recomputing them.

runCalibrationNIMBLE() calls the first two for you when you pass discrepancies and simulation, so most of the time you never touch them directly.

Results

runCalibration() returns a list with:

  • CPPP — one value per discrepancy
  • obsPPP, repPPP — observed and replicated posterior predictive p-values
  • discrepancies — the observed and replicated discrepancy values
  • drawnIndices — which posterior draws seeded the calibration replicates

runCalibration() returns this as a cpppResult S3 object, built and checked by newCpppResult(). Standard errors, confidence intervals, and print / summary / plot methods are planned but not implemented, so for now it prints as a plain list.

Placeholders

Two pieces are deliberately unfinished, and neither is tested:

FileWhat it is
R/transferAutocorrelation.RtransferAutocorrelation() stops with "Not implemented". Corresponds to step 4 of the concept above, i.e., the transfer-ESS estimate of the cppp variance.
R/makeDiscrepancyExtractor.RmakeDiscrepancyExtractor() reads discrepancy columns that NIMBLE computed during the MCMC run. It is written against the current derived-quantity output (discrepancy_model, discrepancy_simulated), but that format is not settled, and nothing is wired up to feed it yet.

Typical workflow

  1. Build the model — an ordinary NIMBLE model with your data in it.

  2. Describe your discrepanciesdiscrepancy("mean") names one the package ships; discrepancy("asymm", modelNodes = "mu", fun = myAsymm) supplies your own as a nimbleFunction with contains = discrepancyBase.

  3. Describe the replicatessimulation("conditional") redraws the data from each posterior draw. simulation("marginal", simulateNodes = ...) redraws latent nodes as well, and you must name them.

  4. Run the calibration — hand both to runCalibrationNIMBLE(), which runs the long chain, simulates $r$ replicate datasets, runs a short chain on each, and turns the results into a PPP and a CPPP.

  5. Estimate the variancetransferAutocorrelation(), once implemented.


Example

The Newcomb light-speed measurements, with two discrepancies at once: mean, which a normal model should fit, and an asymmetry statistic, which it should not. The full script is in inst/examples/newcomb_spec_offline.R.

library(nimble)
library(cppp)
lightPath<- system.file("examples", "light.txt", package="cppp")
newcombData<-list(y= read.table(lightPath)$V1)
newcombModel<- nimbleModel(
code= nimbleCode({
for (iin1:n) y[i] ~ dnorm(mu, sd=sigma)
mu~ dflat()
log(sigma) ~ dflat()
}),
data=newcombData,
constants=list(n= length(newcombData$y)),
inits=list(mu=0, log_sigma=2)
)
## Your own discrepancy: how lopsided the two tails are around mu.## sort() does not compile, so wrap R's.sortR<- nimbleRcall(prototype=function(x= double(1)) {},
Rfun="sort", returnType= double(1))
asymmDisc<- nimbleFunction(
contains=discrepancyBase,
setup=function(model, dataNodes, modelNodes) {},
run=function() {
ys<- sortR(values(model, dataNodes))
mu<- values(model, modelNodes)[1]
returnType(double(0))
return(abs(ys[61] -mu) - abs(ys[6] -mu))
}
)
res<- runCalibrationNIMBLE(
model=newcombModel,
dataNames="y",
paramNames= c("mu", "sigma"),
discrepancies=list(discrepancy("mean"),
discrepancy("asymm", modelNodes="mu", fun=asymmDisc)),
simulation= simulation("conditional"),
nReps=20
)
res$obsPPP# one per discrepancy; mean near 0.5, asymm far from itres$CPPP# one per discrepancyres$repPPP# nReps x 2

About

Calibrated posterior predictive p-values

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages