Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Inhomogeneous Poisson Process — From Scratch and With NumPyro

Open In Colab

The inhomogeneous Poisson process (IPP) generalises the HPP by letting the intensity λ(t)\lambda(t) vary in time. The compensator Λ(t)=∫0tλ(s) ds\Lambda(t) = \int_0^t \lambda(s)\, ds replaces the linear λt\lambda t, and the log-likelihood becomes

log⁡L({ti}i=1n)=∑i=1nlog⁡λ(ti)  −  Λ(T).\log L\bigl(\{t_i\}_{i=1}^n\bigr) = \sum_{i=1}^n \log \lambda(t_i) \;-\; \Lambda(T).

This notebook shows both sides of the xtremax stack:

  1. From scratch — call the pure primitives directly: Lewis–Shedler thinning to sample, ipp_log_prob to evaluate the likelihood, time-rescaling residuals for a goodness-of-fit check.

  2. With NumPyro — wrap the IPP as a numpyro.distributions.Distribution and run NUTS over the log-rates of a piecewise-constant intensity.

Setup

/home/azureuser/localfiles/xtremax/.venv/bin/python: No module named pip
Note: you may need to restart the kernel to use updated packages.
/home/azureuser/localfiles/xtremax/.venv/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

1. From scratch — the primitives layer

Pick a piecewise-constant log-intensity. Three bins, three rates. PiecewiseConstantLogIntensity is an eqx.Module that stores bin_edges and log_rates as PyTree leaves; for our purposes here we just need two things from it: it’s a callable log_intensity_fn(t) that returns log⁡λ(t)\log \lambda(t), and it has a closed-form .integrate(a, b) that returns ∫abλ(s) ds\int_a^b \lambda(s)\, ds exactly.

Λ(T) (closed form)   = 56.000
λ_max (closed form)  = 5.000

Sampling via Lewis–Shedler thinning

The ipp_sample_thinning primitive oversamples from a homogeneous process at rate λmax⁡\lambda_{\max} and accepts each candidate with probability λ(t)/λmax⁡\lambda(t) / \lambda_{\max}. It returns (times, mask, n_candidates_uncapped) on a static buffer so the routine stays jit-compatible.

Accepted 42 events out of 85 candidates
<Figure size 900x350 with 1 Axes>

Evaluating the log-likelihood

ipp_log_prob(event_times, mask, log_intensity_fn, integrated_intensity) returns ∑ilog⁡λ(ti)−Λ(T)\sum_i \log \lambda(t_i) - \Lambda(T). We can compute Λ(T)\Lambda(T) once via the closed-form .integrate method and reuse it.

log L at the true intensity: -8.139

Goodness-of-fit via the time-rescaling theorem

Under the time-rescaling theorem, if Λ\Lambda is the correct compensator of the process, the rescaled inter-event times τi=Λ(ti)−Λ(ti−1)\tau_i = \Lambda(t_i) - \Lambda(t_{i-1}) are i.i.d. Exp(1)\mathrm{Exp}(1). Plugging the compensator into time_rescaling_residuals and then through ks_statistic_exp1 gives a one-line goodness-of-fit check that works for any point-process model with a well-defined compensator.

KS statistic (correct compensator) : 0.089
KS statistic (wrong λ≡1)          : 0.462
<Figure size 1000x350 with 2 Axes>

2. With NumPyro — Bayesian inference over log-rates

To recover the bin rates from the event sequence we put a prior on the per-bin log-rates and run NUTS. The IPP likelihood is IppDist(...).log_prob((times, mask)), evaluated with a freshly-constructed intensity at each MCMC step.

The operator (and therefore the distribution wrapper) queries Λ(T)\Lambda(T) from the current intensity module on every log_prob call, so the −Λ(T)-\Lambda(T) term tracks updated log-rates automatically — crucial for Bayesian inference and for any gradient-based downstream use.

Posterior summary (rates):
  bin 0: mean 1.20, 95% CI [0.44, 2.28], true 1.00
  bin 1: mean 3.53, 95% CI [2.29, 5.06], true 5.00
  bin 2: mean 1.36, 95% CI [0.66, 2.32], true 2.00
<Figure size 1100x320 with 3 Axes>

Posterior predictive check

Draw per-bin rate vectors from the posterior, simulate new event sequences, and compare the mean intensity profile with the truth. (We hold the bin edges fixed; the uncertainty is entirely on the rates.)

<Figure size 900x350 with 1 Axes>

Takeaways

  • Primitives are enough to sample, evaluate the likelihood, and run time-rescaling goodness-of-fit for any intensity λ(t)\lambda(t) — no operator or distribution required.

  • PiecewiseConstantLogIntensity is the natural parameterisation for piecewise-constant rates: its log_rates leaf is both a plain eqx.Module parameter (jit/vmap/grad friendly) and a NumPyro-friendly field (samplable via numpyro.sample).

  • The operator’s .log_prob always computes Λ(T)\Lambda(T) from the live intensity, so posteriors from NUTS (or any gradient-based optimiser) see the full likelihood — the ∑log⁡λ(ti)\sum \log \lambda(t_i) term and the −Λ(T)-\Lambda(T) term — rather than a stale cache.