Composed distributions
One object for the whole case history
Works with individual records
Same representation, multiple uses
using ComposedDistributions, Distributions
# BDBV-inspired delay tree: parallel pathways
# with sequential, resolve, and compete nested together
tree = @uncertain compose((
clinical = sequential(
:onset_admit => Gamma(Normal(1.2, 0.2), 3.0),
:admit_resolve => resolve(
:death => (Gamma(2.0, 3.5), 0.3),
:discharge => (Gamma(1.0, 8.0), 0.7))),
surveillance = sequential(
:onset_notif => Gamma(0.7, 20.0),
:notif_compete => compete(
:confirmed => Gamma(2.0, 1.0),
:lost => Gamma(5.0, 0.5))),
))
# Inspect tree, params, and default priors
event_tree(tree)
params_table(tree)
priors = param_priors(tree)
# Draw a synthetic case, or evaluate a real one's likelihood
case = rand(tree)
logpdf(tree, case)The approach
A case’s event history is a tree. There is a root event (onset), a branch for each later event (admission, notification), and a split where the path forks (death or discharge). Every node is a probability distribution over a delay, and the whole tree is itself a distribution.
The same object works in both directions. rand draws a full event path from the tree; logpdf gives the likelihood of an observed case. Both share one internal representation, so the model you build to simulate is the model you use to fit.
Four layers
The approach is built in four layers, each resting on the one before.
- Leaves are any Distributions.jl
UnivariateDistribution, used directly as the per-event delays, including Distributions.jl’s owntruncated(...)andcensored(...)wrappers — a leaf can be truncated or censored, but wrapping a whole tree the same way errors by design, since the composers read a wrapper’s structure to peel through to the free delay underneath. - Composers wire named leaves into an event tree.
- Combination and lowering join or collapse whole delays with
convolved,difference, andobserved_distribution. - Parameters and edits read and reshape an assembled tree with
params_table,param_priors,uncertain,update,prune, andsplice.
Five composers
Five composers cover the ways an event history can branch.
sequentialchains steps in series.parallelfans branches off one shared origin.resolvegives a one-of outcome as a fixed-probability mixture.competegives a one-of outcome as racing hazards.choosepicks a branch from a field in the data.
The compose front-end lowers a NamedTuple, a Tables.jl table, or a nested matrix onto the same stack, so a tree can be written by hand or built straight from a line list.
Handles partial line-list data
Line-list data is sparse: some delays are observed, some are censored, some are missing entirely. Each case supplies only the delays it has, and the tree accounts for the rest by integrating over the branches it did not see. Because each case is handled on its own, the approach works directly on individual-level data.
The same object handles complete cases, censored cases, and partially observed cases. The model does not change, only the delays each case supplies.
Censoring and modifiers compose as leaves
Real line-list delays are rarely clean, and the corrections are part of the model rather than a preprocessing step. CensoredDistributions.jl supplies censoring leaves for primary-event censoring, interval censoring, and truncation. ModifiedDistributions.jl supplies modifier leaves that rescale, weight, or transform a delay. Both are leaves like any other, so they slot into the tree and the composers peel through them to reach the free delay underneath.
Uncertainty in the parameters themselves
A delay taken from the literature rarely comes with exact parameters. uncertain wraps a leaf whose parameters are themselves distributions, nested to any depth, and the @uncertain macro reads a distribution passed as a constructor argument as that parameter’s prior, so @uncertain Gamma(Normal(2.0, 0.3), 1.0) reads the same as writing it out by hand. The result is still a univariate distribution, so it composes as a leaf everywhere, and has_uncertain guards against forgetting to pin it down before fitting.
uncertain is also the verb for promoting parameters on a tree already built: uncertain(tree; onset_admit = (shape = prior,)) targets one parameter, and bare uncertain(tree) promotes every free parameter with a default prior in one call.
One representation across the workflow
The tree is not tied to one package or one use. The same object that defines a delay distribution for a renewal model drops into a Turing fit unchanged, and the same structure works for differential-equation models. There is no translation step between the delay model, the infection model, and the observation model, because the composed object is the shared vocabulary.
Read the posterior back through the same tree
After fitting, the posterior is read back by applying the parameter draws to the same tree. update(template, chain) reduces the chain to a point summary and returns one fitted tree; update(template, table) does the same from a Tables.jl table of values or priors, for a bulk edit outside a sampling loop. Reading every draw rather than a point summary maps update over param_draws(template, chain), so every delay mean, convolution, or contrast still comes from the fitted object itself, one draw at a time. Nothing is recomputed in separate post-processing that could drift from the model.
Where next
- Run it: the Tutorials gallery collects the runnable walkthroughs for this approach, starting with composing distributions.
- The ComposedDistributions.jl documentation covers the full API and worked examples.
- ConvolvedDistributions.jl handles the convolutions (sums) of delays that arise naturally in event trees.
- ModifiedDistributions.jl lets you rescale, weight, or otherwise transform individual leaves without changing the tree structure.
- CensoredDistributions.jl adds the censoring layer for real line-list data.
- LoweredDistributions.jl lowers the same tree onto a phase-type chain or a CTMC, the bridge to compartmental models.
- Fitting outside Turing, or fitting a plain struct alongside a tree? DistributionsInference.jl hosts the PPL-neutral log-density and readback that
as_logdensityandas_turingbuild on:distribution_priorsassembles priors the same wayparam_priorsdoes for a tree, anddistribution_paramsreads a fitted chain back to the estimated values by name, whatever fittable object it came from. - Ready to fit a full model with infection dynamics? See the composable Turing models approach.