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 = compose((
clinical = sequential(
:onset_admit => Gamma(1.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 = build_priors(params_table(tree))
# Draw a synthetic case, or evaluate a real one's likelihood
rand(tree)
logpdf(tree, ans)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. - 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,build_priors,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. 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.
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) walks each draw through the tree and returns a distribution per draw, so every delay mean, convolution, or contrast comes from the fitted object itself. 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.
- Ready to fit a full model with infection dynamics? See the composable Turing models approach.