Predictive densities and Monte Carlo checks¶
An extension beyond the paper: predictive densities of whole time bins and a Monte Carlo predictive p-value with a user-supplied sampler.
predictive_checks
¶
Predictive densities and Monte Carlo predictive checks of whole time bins.
These are extensions beyond the paper. The paper's predictive check is the
rank-based predictive p-value of each spike, computed exactly over the units by
:func:~statespacecheck.mark_predictive_pvalue and
:func:~statespacecheck.event_diagnostics. The functions here compute the
predictive density of all observations in a time bin, and a Monte Carlo p-value
from a user-supplied sampler.
Functions:¶
predictive_density
¶
Compute predictive density by integrating state dist with obs likelihood.
CRITICAL: This function normalizes state_dist ONLY, NOT likelihood. The likelihood p(y|x) is a likelihood function, not a distribution over x. Normalizing it over x would change its value and mask real model misfit.
Formula: f_predictive(y) = ∑_x p(x) * p(y|x)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state_dist
|
(ndarray, shape(n_time, ...))
|
State probability distributions over position at each time point where ... represents arbitrary spatial dimensions. Non-negative values (NaN allowed to mark invalid bins). Will be normalized over spatial dimensions (everything except time). |
required |
observation_likelihood
|
(ndarray, shape(n_time, ...))
|
Observation likelihood p(y|x) of the observed data at each position.
Non-negative values; a NaN bin is excluded from both inputs (the state
is renormalized over the other bins).
Not normalized over positions (unlike the |
required |
Returns:
| Name | Type | Description |
|---|---|---|
predictive_density |
(ndarray, shape(n_time))
|
Predictive density at each time point. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If state_dist and observation_likelihood have different shapes, if they contain negative values, if observation_likelihood contains +inf, or if an input is a masked array (mark bins to exclude with NaN). |
TypeError
|
If an input is complex. |
Examples:
>>> import numpy as np
>>> from statespacecheck import predictive_density
>>> # Simple 1D example with unnormalized state
>>> state = np.array([[3.0, 4.0, 3.0]]) # Unnormalized (sums to 10, not 1)
>>> like = np.array([[2.0, 3.0, 1.0]]) # Likelihood values (not normalized)
>>> pred = predictive_density(state, like)
>>> pred.shape
(1,)
See Also
log_predictive_density : Compute log predictive density for numerical stability kl_divergence : Measure information divergence between distributions hpd_overlap : Compute spatial overlap between HPD regions
Notes
The predictive density is computed via discrete Riemann sum: f_predictive(y_k) = ∑_x p(x_k) * p(y_k | x_k)
Where: - p(x_k) is the state distribution (normalized to sum to 1) - p(y_k | x_k) is the observation likelihood (NOT normalized)
Distributions are validated using validate_paired_distributions: - A bin that is NaN in the likelihood is excluded from the state too - Other NaN/inf values are converted to 0.0 (+inf in the likelihood raises) - Shape and non-negativity are checked - State distribution is normalized after validation - Likelihood is NOT normalized (critical for correct results)
Integration is performed by flattening spatial dimensions and computing row-wise sums over all spatial bins.
Source code in src/statespacecheck/predictive_checks.py
log_predictive_density
¶
log_predictive_density(state_dist: ArrayLike, observation_likelihood: ArrayLike | None = None, *, log_observation_likelihood: ArrayLike | None = None) -> DistributionArray
Compute log predictive density directly in log-space using logsumexp.
CRITICAL: This function normalizes state_dist ONLY, NOT likelihood. Computes log predictive density natively in log-space for numerical stability. DO NOT compute as np.log(predictive_density(...)) - this loses precision.
Formula: log f_predictive(y) = log ∑_x p(x) * p(y|x) = logsumexp(log p(x) + log p(y|x))
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state_dist
|
(ndarray, shape(n_time, ...))
|
State probability distributions over position at each time point where ... represents arbitrary spatial dimensions. Non-negative values (NaN allowed to mark invalid bins). Will be normalized over spatial dimensions (everything except time). |
required |
observation_likelihood
|
(ndarray, shape(n_time, ...))
|
Observation likelihood p(y|x) of the observed data at each position.
Non-negative values; a NaN bin is excluded from both inputs (the state
is renormalized over the other bins). Not normalized over positions.
Must have same shape as state_dist.
Exactly one of |
None
|
log_observation_likelihood
|
(ndarray, shape(n_time, ...))
|
Log observation likelihood log p(y|x), keyword-only. Passing it avoids an exp/log round-trip. -inf is a zero likelihood; a NaN bin is excluded from both inputs. Must have same shape as state_dist. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
log_predictive_density |
(ndarray, shape(n_time))
|
Log predictive density at each time point. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither or both of the likelihood arguments are provided, if shapes don't match, if distributions contain negative values, if the (log) observation likelihood contains +inf, or if an input is a masked array (mark bins to exclude with NaN). |
TypeError
|
If an input is complex. |
Examples:
>>> import numpy as np
>>> from statespacecheck import log_predictive_density
>>> state = np.array([[1.0, 1.0, 1.0]])
>>> like = np.array([[2.0, 3.0, 4.0]])
>>> log_pred = log_predictive_density(state, like)
>>> log_pred.shape
(1,)
>>> # From a log likelihood, for numerical stability
>>> log_pred2 = log_predictive_density(state, log_observation_likelihood=np.log(like))
>>> np.allclose(log_pred, log_pred2)
True
See Also
predictive_density : Compute predictive density in linear space kl_divergence : Measure information divergence between distributions hpd_overlap : Compute spatial overlap between HPD regions
Notes
This function computes log predictive density directly in log-space using scipy.special.logsumexp for numerical stability. This prevents underflow when working with very small probabilities or peaked distributions.
The computation is: log ∑_x p(x) * p(y|x) = logsumexp(log p(x) + log p(y|x))
Where: - p(x) is the state distribution (normalized to sum to 1) - p(y|x) is the observation likelihood (NOT normalized)
For users who already have the log likelihood, passing it via
log_observation_likelihood avoids the exp/log round-trip and is more
efficient and numerically stable.
Source code in src/statespacecheck/predictive_checks.py
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | |
predictive_pvalue
¶
predictive_pvalue(observed_log_pred: ArrayLike, sample_log_pred: Callable[[int], ArrayLike], *, n_samples: int = 1000) -> DistributionArray
Compute predictive p-value via Monte Carlo sampling.
Computes p-values for predictive checks by comparing observed log predictive densities to a distribution of simulated log predictive densities. The p-value at each time point is the proportion of simulated values that are less than or equal to the observed value.
This computes a Monte Carlo predictive check p-value for a user-supplied replicate-generating procedure. If the model is correct and the statistic is continuous, p-values should be approximately uniformly distributed; ties and discreteness can make them conservative. Systematic deviations indicate model misfit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
observed_log_pred
|
(ndarray, shape(n_time))
|
Observed log predictive densities for actual data. Must be 1-dimensional. |
required |
sample_log_pred
|
callable
|
Function that generates samples of log predictive densities under the model.
Must accept a single integer argument |
required |
n_samples
|
int
|
Number of Monte Carlo samples to draw for p-value computation. Higher values give more accurate p-value estimates but take longer. Default is 1000. |
1000
|
Returns:
| Name | Type | Description |
|---|---|---|
p_values |
(ndarray, shape(n_time))
|
P-value at each time point, computed as the proportion of simulated
log predictive densities <= observed value.
Values range from 0 to 1; NaN where |
Raises:
| Type | Description |
|---|---|
ValueError
|
If observed_log_pred is not 1-dimensional, if n_samples <= 0, if sample_log_pred returns an array with the wrong shape or containing NaN, or if observed_log_pred or the sampler's output is a masked array (use NaN for missing observations). |
TypeError
|
If sample_log_pred is not callable, or an input is complex. |
Examples:
>>> import numpy as np
>>> from statespacecheck import predictive_pvalue
>>> # Observed log predictive densities
>>> observed = np.array([-2.0, -1.5, -1.0])
>>> # Sampler with internal random state for reproducibility
>>> def sampler(n_samples):
... rng = np.random.default_rng(42) # Fixed seed for reproducibility
... return rng.normal(loc=-1.5, scale=0.5, size=(n_samples, 3))
>>> # Monte Carlo estimates of the exact values 0.16, 0.5 and 0.84
>>> predictive_pvalue(observed, sampler, n_samples=1000).round(2)
array([0.16, 0.5 , 0.86])
See Also
log_predictive_density : Compute log predictive density for observed data predictive_density : Compute predictive density in linear space aggregate_over_period : Aggregate metrics over time periods
Notes
The p-value at time t is computed as: p_value[t] = (1 / n_samples) * sum(simulated[t] <= observed[t])
Interpretation: the statistic is a log predictive density, so a small
p-value means the observed data were less probable than nearly all
replicates, i.e. unexpected under the model. A p-value near 1 means the
observation was among the most probable outcomes, which is good fit, not
misfit. Flag small values (for example p <= 0.05, as
:func:~statespacecheck.periods.flag_extreme_pvalues does).
The estimate is the fraction of n_samples replicates, so it is a
multiple of 1 / n_samples and can be exactly 0; choose n_samples
large enough to resolve the cutoff you use.
The p-value is r / B: the fraction of B replicates at most as
probable as the observation. It estimates the predictive tail probability,
with standard error sqrt(p (1 - p) / B). A rank test at a finite B
would use (r + 1) / (B + 1) instead, computed from the returned p as
(p * B + 1) / (B + 1). It is valid in finite samples when the observation
and the replicates are exchangeable under the model, and possibly conservative:
it is never below 1 / (B + 1), and its level equals alpha only when
alpha (B + 1) is an integer and there are no ties.
The sampler function should: 1. Generate new data from the model 2. Compute log predictive density for each generated dataset 3. Return array of shape (n_samples, n_time) 4. Use np.random.Generator internally for reproducibility
For reproducible results, create your sampler with a fixed seed: rng = np.random.default_rng(42) sampler = lambda n: rng.normal(size=(n, n_time))
Source code in src/statespacecheck/predictive_checks.py
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 | |