Skip to content

Getting started

Welcome to the ComposedDistributions documentation. This page is the quickstart. The home page is generated from the README, so it stays short; put the walkthrough a new user needs here and grow it into tutorials as the package develops.

Installation

julia
using Pkg
Pkg.add("ComposedDistributions")

Load the package:

julia
using ComposedDistributions

What ComposedDistributions does

A composed distribution is a multi-state event process: named events linked by delays, wired into a tree. The same object scores an observed record with logpdf and simulates a new one with rand, so a model is built once and used in both directions. See Why ComposedDistributions? on the home page for the full motivation, and Concepts for the four layers and the verb that builds each one.

A first example

A hospital pathway: an admission delay with literature uncertainty on its typical duration, then a death-versus-discharge split where the death probability is the case-fatality ratio, alongside a reporting delay truncated at a 21-day cutoff (reports arriving later are excluded) and a referral delay censored at 14 days (a referral still pending at day 14 is recorded as arriving then) — both plain Distributions.jl wrappers used here as ordinary leaves.

julia
using ComposedDistributions
using ComposedDistributions: update
using Distributions, Random

cfr = 0.12   # case-fatality ratio among admitted cases

admission = @uncertain compose((
    path = sequential(
        :onset_admit => LogNormal(Normal(0.0, 0.2), 0.4),
        :admit_outcome => resolve(:death => (Gamma(1.5, 1.0), cfr),
            :discharge => Gamma(2.0, 1.5))),
    onset_report = truncated(Gamma(1.5, 1.0); upper = 21.0),
    onset_referral = censored(Gamma(1.0, 2.0); upper = 14.0)))
Parallel (3 branches)
├─ path: Sequential (2 steps)
│  ├─ onset_admit: uncertain(Distributions.LogNormal{Float64}(μ=0.0, σ=0.4); mu = Distributions.Normal{Float64}(μ=0.0, σ=0.2))
│  └─ admit_outcome: Resolve (2 outcomes)
│     ├─ death (p = 0.12): Distributions.Gamma{Float64}(α=1.5, θ=1.0)
│     └─ discharge (p = 0.88): Distributions.Gamma{Float64}(α=2.0, θ=1.5)
├─ onset_report: Truncated(Distributions.Gamma{Float64}(α=1.5, θ=1.0); upper=21.0)
└─ onset_referral: Censored(Distributions.Gamma{Float64}(α=1.0, θ=2.0); upper=14.0)

admission prints as the tree it is: three branches off the onset, the admission branch itself a two-step chain ending in the death/discharge split, and the reporting and referral branches keeping their truncated() and censored() wrappers visible in the printed tree.

The same object simulates a structured record and scores one straight back.

julia
record = rand(Xoshiro(1), admission)
(path_onset_admit = 1.219540408861313, path_admit_outcome = 3.054755933253062, onset_report = 0.48257329798973975, onset_referral = 0.5073200149288529)
julia
logpdf(admission, record)
-3.786590242826544

Its free parameters read as a flat table, keyed by edge and parameter name; onset_admit's mu carries the uncertainty prior attached above (its reported value, 0.0, is the LogNormal family's own default, which here also happens to be the prior's centre), and the death/discharge split shows up as its own branch_probs rows.

julia
params_table(admission)
params_table (12 rows)
  edge                             param      value  support     prior                                      
  ───────────────────────────────  ─────────  ─────  ──────────  ───────────────────────────────────────────
  path.onset_admit                 mu         0.0    (0.0, Inf)  Distributions.Normal{Float64}(μ=0.0, σ=0.2)
  path.onset_admit                 sigma      0.4    (0.0, Inf)                                             
  path.admit_outcome.death         shape      1.5    (0.0, Inf)                                             
  path.admit_outcome.death         scale      1.0    (0.0, Inf)                                             
  path.admit_outcome.discharge     shape      2.0    (0.0, Inf)                                             
  path.admit_outcome.discharge     scale      1.5    (0.0, Inf)                                             
  path.admit_outcome.branch_probs  death      0.12   (0.0, 1.0)                                             
  path.admit_outcome.branch_probs  discharge  0.88   (0.0, 1.0)                                             
  onset_report                     shape      1.5    (0.0, Inf)                                             
  onset_report                     scale      1.0    (0.0, Inf)                                             
  onset_referral                   shape      1.0    (0.0, Inf)                                             
  onset_referral                   scale      2.0    (0.0, Inf)

event fetches any node by its dotted path, so the death outcome's delay is reachable directly.

julia
event(admission, :path, :admit_outcome, :death)
Distributions.Gamma{Float64}(α=1.5, θ=1.0)

The tree still has an uncertain leaf, so it estimates rather than scores at one fixed value until that leaf is pinned or fitted.

julia
has_uncertain(admission)
true

Pinning onset_admit to a concrete delay collapses the admission chain to one convolved total via observed_distribution, integrating the intermediate admission event out.

julia
pinned = update(admission, (:path, :onset_admit) => LogNormal(1.5, 0.4))
total = observed_distribution(event(pinned, :path))
mean(total)
7.674955811237434

Fitting admission itself — estimating the uncertain leaf from data rather than pinning it by hand — is DistributionsInference.jl's job; see its composed-tree tutorial for the full walkthrough.

Two spellings for named children

Every composer above names its children two equivalent ways: name => dist arrow pairs, or a single positional named tuple (name = dist, …). Both spellings lower to the same pairs and build the identical node.

julia
using ComposedDistributions, Distributions

arrow = sequential(:onset_admit => Gamma(2.0, 1.0),
    :admit_death => LogNormal(0.5, 0.4))
equals = sequential((onset_admit = Gamma(2.0, 1.0),
    admit_death = LogNormal(0.5, 0.4)))
arrow == equals
true

Use the equals form for hand-written, fixed names, and the arrow form for data-driven or computed names (a Symbol built programmatically). The same pairing holds for parallel, resolve, compete and choose.

Uncertain distributions

A literature-reported delay rarely comes with exact parameters. Write the uncertainty inline with @uncertain (as onset_admit does above): a distribution literal in a parameter slot reads as that parameter's prior, the natural spelling of the positional uncertain family form. The result is still a univariate distribution, so it composes as a leaf everywhere, and rand draws the marginal (a fresh parameter draw each call); the rest of the surface reports a fixed placeholder for each uncertain parameter until concrete values are pinned with update (guard against a forgotten collapse with has_uncertain).

An uncertain leaf is one of two deferred leaves, resolved to a concrete distribution later rather than fixed at build time. See Concepts for how it relates to its sibling Varying.

Learning more

Getting help

For usage questions, ask on the Julia Discourse (the SciML or usage categories) or the epinowcast community forum, our home for epidemiological modelling questions. Please use GitHub issues for bug reports and feature requests only.