Beginner tutorial · composition · JDistlib 0.6.0+

Design a new probability model from distributions you already trust

A composition design is a pipeline. Start with one or more base distributions, then mix populations, condition on an allowed range, change units or scale, and finally represent observation limits. Each step produces another ordinary JDistlib distribution.

Available from 0.6.0

Start with the scientific story

Choose the operation that matches what happened

StoryOperationWhat changes
An observation comes from one of several populations.Distributions.mixtureDensity and CDF become weighted averages.
Only values inside a range belong to the population.Distributions.truncateOutside values are removed and the retained range is renormalized.
A device reports every low or high value at a limit.Distributions.censorTail probability becomes point mass at the limits.
Units or calibration change by shift + scale*x.Distributions.affineSupport, density, CDF, quantiles, and draws are transformed.
A differentiable one-to-one formula maps X to Y.Distributions.transformThe inverse and its Jacobian define the new density.

Complete design

Give every intermediate step a meaningful name

MixtureDistribution latent = Distributions.mixture(
    new double[] {0.8, 0.2},
    new Normal(50.0, 8.0),
    new Normal(75.0, 12.0));

TruncatedContinuousDistribution physicallyPossible =
    Distributions.truncate(
        latent, 0.0, Double.POSITIVE_INFINITY);

MonotoneTransformDistribution calibrated =
    Distributions.affine(physicallyPossible, 2.0, 1.1);

CensoredDistribution observed =
    Distributions.censor(calibrated, 5.0, 100.0);

This design says that latent measurements come from two subpopulations, negative latent values are physically impossible, calibration reports 2 + 1.1*x, and the instrument records anything beyond its range at 5 or 100. The final object still supports density or mass, CDF, quantile, and random generation.

double below60 = observed.cumulative(60.0);
double median = observed.quantile(0.5);
double atCeiling = observed.getUpperAtomProbability();
double simulated = observed.random();

The complete snippet is compiled during the release build in CompositionExamples.java.

Several populations

Mixture weights describe how often each component is chosen

\[ f(x)=\sum_{j=1}^{k}\pi_j f_j(x),\qquad F(x)=\sum_{j=1}^{k}\pi_j F_j(x). \]
MixtureDistribution response = Distributions.mixture(
    new double[] {3.0, 1.0},
    new Normal(100.0, 15.0),
    new Normal(150.0, 20.0));

Weights need only be nonnegative and have a positive finite sum; JDistlib normalizes them. Here, 3:1 means 75% and 25%. A mixture is not the same as averaging two random variables: one component is selected for each draw.

Two different boundary stories

Truncation removes values; censoring records them at a boundary

Truncation

If only (a<X<b) can enter the population, probability outside the range is discarded. The retained density is

\[f_T(x)=\frac{f(x)}{F(b)-F(a)}.\]
Distributions.truncate(base, a, b)

Censoring

If values outside the range still occur but the instrument reports the nearest limit, their probability accumulates as atoms at (a) and (b).

CensoredDistribution measured =
    Distributions.censor(base, a, b);
double lowerMass = measured.getLowerAtomProbability();

Units and calibration

Use the affine shortcut for Y = shift + scale × X

// Celsius to Fahrenheit
MonotoneTransformDistribution fahrenheit =
    Distributions.affine(celsius, 32.0, 9.0 / 5.0);

// Reflection is supported because scale may be negative.
MonotoneTransformDistribution reflected =
    Distributions.affine(base, 0.0, -1.0);

The affine factory derives the inverse, Jacobian, direction, and output support automatically. Prefer it over a general transformation whenever it expresses the design.

One-to-one formulas

For Y = h(X), supply the inverse and its log-Jacobian

For a differentiable strictly monotone transformation,

\[ f_Y(y)=f_X(h^{-1}(y)) \left|\frac{d h^{-1}(y)}{dy}\right|. \]

This example constructs (Y=\exp(X)) explicitly:

MonotoneTransformDistribution positive =
    Distributions.transform(
        new Normal(0.0, 0.5),
        Math::exp,             // h(x)
        Math::log,             // h^-1(y)
        y -> -Math.log(y),    // log |d log(y)/dy| = -log(y)
        true,                  // h is increasing
        0.0,
        Double.POSITIVE_INFINITY);

The derivative is supplied in log-absolute form because transformed densities can span an extreme numerical range. Output bounds describe the support of Y, not X.

Worked vignette · mixture + nonlinear transform

Analyze two response-time regimes through one distribution object

Suppose normal traffic has a median response time of 4 seconds, while 15% of requests arrive during a degraded regime with a median of 20 seconds. Model the logarithm of response time as a normal mixture, then exponentiate the complete mixture:

\[ Z\sim0.85\,N(\log 4,0.30^2)+0.15\,N(\log 20,0.45^2), \qquad T=e^Z. \]
MixtureDistribution logSeconds = Distributions.mixture(
    new double[] {0.85, 0.15},
    new Normal(Math.log(4.0), 0.30),
    new Normal(Math.log(20.0), 0.45));

MonotoneTransformDistribution responseTime =
    Distributions.transform(
        logSeconds,
        Math::exp,
        Math::log,
        y -> -Math.log(y),
        true,
        0.0,
        Double.POSITIVE_INFINITY);

The transformed mixture has the same scalar API as its components. Density uses both the mixture weights and the transformation Jacobian; the CDF reverses through the inverse transformation; quantiles invert the mixture CDF numerically; and random draws follow the transformed mixture law.

// Analytical questions in seconds.
double densityAtTen = responseTime.density(10.0, false);
double withinTen = responseTime.cumulative(10.0);
double percentile95 = responseTime.quantile(0.95);

// Reproducible random scenarios from the same model.
// import jdistlib.rng.MersenneTwister;
responseTime.setRandomEngine(new MersenneTwister(20260826L));
double oneScenario = responseTime.random();
double[] scenarios = responseTime.random(1_000);

Applying one common monotone transformation after mixing is equivalent to transforming every component first and then mixing with the same weights. Transforming the mixture directly keeps the shared change of scale in one place. The complete example is compiled during every check build in CompositionExamples.java.

Direction matters

A decreasing transformation reverses probability tails

For (Y=\exp(-X)), the inverse is (-\log y), the absolute inverse derivative is still (1/y), and increasing must be false:

MonotoneTransformDistribution decreasing =
    Distributions.transform(
        base,
        x -> Math.exp(-x),
        y -> -Math.log(y),
        y -> -Math.log(y),
        false,
        0.0,
        Double.POSITIVE_INFINITY);

The direction flag lets CDF and quantile operations reverse tails correctly. The Jacobian uses an absolute derivative, so its sign is never included in the density.

Before relying on the result

Check each layer, then check the complete design

Composition is preferable to rewriting a custom kernel when these operations match the scientific design: it preserves exact component CDFs and quantiles, exposes intermediate assumptions, and reduces the amount of numerical integration required.

Next

Build a component only when the catalog is not enough

Use the custom-distribution tutorial to create a component from a kernel or discrete weights. Use the copula tutorial (0.7.0+) when the goal is a multivariate model rather than a one-dimensional composition.