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_logdensityturns a distribution and its data into aLogDensityProblemsproblem, 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_distributionfits 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_estimatepost-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
Gammais aGamma.
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.
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])
endparameter_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.
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)2prob 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.
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)2000point_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.
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.
Related packages
Distributions.jl defines the distributions this fits; a type that has a
logpdfand 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.jldistribution.ConvolvedDistributions.jl builds the distribution of a sum, difference or product of independent delays, again as a
Distributions.jldistribution.
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
Want to get started running code? See the getting started guide.
Want to understand the API? See the API reference.
Want to see the code? Check out our GitHub repository.
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.