Skip to content

DistributionsInference ​

The inference layer for the EpiAware composable-modelling stack. A distribution names its own parameters and becomes fittable through a plain log-density that any sampler can read.

Why DistributionsInference? ​

  • Fitting a distribution usually means rewriting it inside one probabilistic programming language's macros. Here it names its own scalar parameters once and every sampler reads that declaration.

  • distribution_to_logdensity turns a distribution and its data into a LogDensityProblems problem, which any consumer of that interface can drive.

  • A distribution declares its parameters as a table of rows, one row per scalar parameter carrying its name, value, prior and support. Attaching a prior to a row is what makes that parameter estimated. The rows are plain NamedTuples, so the inventory is a row table any Tables.jl consumer reads, without this package depending on Tables.jl.

  • optimise_distribution fits a distribution to data with an optimiser of your choosing in one call, for a point estimate rather than a posterior, and the pieces it composes stay available for a fit you drive yourself.

  • point_estimate post-processes sampler output onto a fitted distribution in one call.

  • Supporting a new chain type takes one added method.

  • The distribution that comes back is the same kind of object that went in, so a fitted Gamma is a Gamma.

Getting started ​

See documentation for a full walkthrough.

Start with a plain Distributions.jl Gamma. The package ships no parameter_rows/reconstruct methods for Distributions.jl types, so write those two once per family. Nothing else about the distribution changes.

julia
using DistributionsInference, Distributions, Random

function DistributionsInference.parameter_rows(d::Gamma)
    return [(name = :shape, value = shape(d),
            prior = LogNormal(log(2.0), 0.5), support = (0.0, Inf)),
        (name = :scale, value = scale(d),
            prior = LogNormal(0.0, 0.5), support = (0.0, Inf))]
end

function DistributionsInference.reconstruct(::Gamma, x::AbstractVector)
    return Gamma(x[1], x[2])
end

parameter_rows sets the estimation boundary. A row carrying a prior is estimated; a row with prior = nothing stays at its value. distribution_to_logdensity packages a template distribution and the data into a log-density over the estimated rows.

julia
template = Gamma(2.0, 1.0)
data = rand(Xoshiro(1), Gamma(2.0, 1.0), 200)
prob = distribution_to_logdensity(template, data)
DistributionsInference.flat_dimension(template)
2

prob is a LogDensityProblems problem, so any sampler that consumes that interface can drive it. Here that is AdvancedMH's random-walk Metropolis, wrapped in a guard because a random-walk proposal does not respect positive support on its own.

julia
using AdvancedMH
using LinearAlgebra: I

model = AdvancedMH.DensityModel() do x
    any(<=(0), x) ? -Inf : DistributionsInference.logdensity(prob, x)
end
sampler = RWMH(MvNormal(zeros(2), 0.05^2 * I))
transitions = sample(Xoshiro(1), model, sampler, 4000;
    param_names = ["shape", "scale"], progress = false)
draws = [t.params for t in transitions][2001:end]
length(draws)
2000

point_estimate reads the draws back onto the distribution through the same dotted-name chain a real PPL's sampler would hand back. What it returns is a Gamma. The chain readback is a package extension, so add and load FlexiChains for this last step. The chain itself is FlexiChains' own type, keyed by the names parameter_rows declared, and building one from a sampler's raw draws is FlexiChains' constructor rather than a step this package owns.

julia
using FlexiChains: FlexiChain, Parameter

niter = length(draws)
chain = FlexiChain{Symbol}(niter, 1,
    Dict(Parameter(:shape) => reshape(first.(draws), niter, 1),
        Parameter(:scale) => reshape(last.(draws), niter, 1)))
point_estimate(template, chain)
Distributions.Gamma{Float64}(α=1.798102650014111, θ=1.1221712509134354)

The getting started guide goes further, with a distribution type of your own and sampling through Turing.

  • Distributions.jl defines the distributions this fits; a type that has a logpdf and can name its scalar parameters is a candidate.

  • ComposedDistributions.jl builds a distribution by composing others into chains, branches and outcomes; a package extension here reads a composed tree's generated codec directly, so its estimated leaves, pooled and shared parameters included, are fittable as they stand.

  • ModifiedDistributions.jl wraps a distribution to change one behaviour, such as rescaling, likelihood weighting or a hazard shift; a modifier used as a leaf inside a composed tree is already fittable, but the extension for fitting a standalone modifier is parked until that package registers in General.

  • ReparameterisedDistributions.jl switches a family between parameter conventions, so a distribution can be fitted in the coordinates its priors were elicited in rather than the family's native ones.

  • CensoredDistributions.jl applies primary event censoring, interval censoring and right truncation to a delay, and returns a Distributions.jl distribution.

  • ConvolvedDistributions.jl builds the distribution of a sum, difference or product of independent delays, again as a Distributions.jl distribution.

ComposedDistributions is the only one of these covered by an extension today. For the rest, as for a plain Distributions.jl distribution, the two protocol methods are yours to write.

Where to learn more ​

Part of the EpiAware ecosystem ​

DistributionsInference is part of EpiAware, a set of composable tools for infectious disease modelling. See the other packages in the ecosystem.

Contributing ​

We welcome contributions and new contributors! Please open an issue or pull request on GitHub. This package follows ColPrac and the SciML style.

How to cite ​

If you use DistributionsInference in your work, please cite it. Citation metadata lives in CITATION.cff, which GitHub renders as a "Cite this repository" button on the repository page.

Code of conduct ​

Please note that the DistributionsInference project is released with a Contributor Code of Conduct. By contributing, you agree to abide by its terms.