# Distribution Algebra with algebraic.dist

    library(algebraic.dist)

## Introduction

The `algebraic.dist` package provides an algebra over probability distributions. You can combine distribution objects with standard arithmetic operators (`+`, `-`, `*`, `/`, `^`) and mathematical functions (`exp`, `log`, `sqrt`, …), and the package will either simplify the result to a known closed-form distribution or fall back to Monte Carlo sampling when no closed form exists.

This means you can write expressions like `normal(0, 1) + normal(2, 3)` and get back a `normal` object, without manually deriving the result. When the package cannot simplify, the result is an `edist` (expression distribution) that supports all the usual methods – `mean`, `vcov`, `sampler`, `cdf`, and more – via MC approximation.

## Basic Arithmetic

Arithmetic operators on `dist` objects create `edist` (expression distribution) objects, which are then automatically simplified when a closed-form rule applies. When no rule matches, the `edist` is kept as-is.

    x <- normal(0, 1)
    y <- exponential(1)
    z <- x + y

Since the sum of a normal and an exponential has no simple closed form, `z` remains an `edist`:

    is_edist(z)
    #> [1] TRUE
    class(z)
    #> [1] "+_normal_exponential" "x_normal_exponential" "y_normal_exponential"
    #> [4] "edist"                "dist"

The `edist` still supports all distribution methods. Moments are computed via Monte Carlo:

    set.seed(1)
    mean(z)
    #> [1] 0.9944453
    vcov(z)
    #> [1] 2.023444

The theoretical mean is `0 + 1 = 1` and variance is `1 + 1 = 2`, so the MC estimates should be close.

## Automatic Simplification

The real power of the package is automatic simplification. When you compose distributions using operations that have known closed-form results, the package returns the simplified distribution directly – no sampling needed.

### Normal algebra

The sum and difference of independent normals is normal. Scalar multiplication and location shifts are also handled.

Sum of normals:

    z <- normal(1, 4) + normal(2, 5)
    is_normal(z)
    #> [1] TRUE
    z
    #> Normal distribution (mu = 3, var = 9)
    mean(z)
    #> [1] 3
    vcov(z)
    #> [1] 9

The result is Normal(mu = 3, var = 9), since means add and variances add for independent normals.

Difference of normals:

    z <- normal(1, 4) - normal(2, 5)
    z
    #> Normal distribution (mu = -1, var = 9)
    mean(z)
    #> [1] -1
    vcov(z)
    #> [1] 9

Normal(mu = -1, var = 9) – means subtract, but variances still add.

Scalar multiplication:

    z <- 3 * normal(2, 1)
    z
    #> Normal distribution (mu = 6, var = 9)
    mean(z)
    #> [1] 6
    vcov(z)
    #> [1] 9

If `X ~ Normal(mu, v)`, then `c * X ~ Normal(c * mu, c^2 * v)`.

Location shift:

    z <- normal(5, 2) + 10
    z
    #> Normal distribution (mu = 15, var = 2)
    mean(z)
    #> [1] 15
    vcov(z)
    #> [1] 2

Adding a constant shifts the mean but preserves the variance.

### Exponential and Gamma algebra

Sums of independent exponentials (with the same rate) produce gamma distributions. Gamma distributions with the same rate also combine.

Sum of exponentials:

    z <- exponential(2) + exponential(2)
    is_gamma_dist(z)
    #> [1] TRUE
    z
    #> Gamma distribution (shape = 2, rate = 2)
    params(z)
    #> shape  rate
    #>     2     2

Two independent Exp(2) variables sum to Gamma(shape = 2, rate = 2).

Gamma plus exponential:

    z <- gamma_dist(3, 2) + exponential(2)
    z
    #> Gamma distribution (shape = 4, rate = 2)
    params(z)
    #> shape  rate
    #>     4     2

Gamma(3, 2) + Exp(2) = Gamma(4, 2).

Sum of gammas (same rate):

    z <- gamma_dist(3, 2) + gamma_dist(4, 2)
    z
    #> Gamma distribution (shape = 7, rate = 2)
    params(z)
    #> shape  rate
    #>     7     2

Gamma(3, 2) + Gamma(4, 2) = Gamma(7, 2) – shapes add when rates match.

### Transform rules

Certain mathematical transformations of distributions have known closed-form results.

Exponential of a normal is log-normal:

    z <- exp(normal(0, 1))
    is_lognormal(z)
    #> [1] TRUE
    z
    #> Log-normal distribution (meanlog = 0, sdlog = 1)

Logarithm of a log-normal is normal:

    z <- log(lognormal(0, 1))
    is_normal(z)
    #> [1] TRUE
    z
    #> Normal distribution (mu = 0, var = 1)

Standard normal squared is chi-squared:

    z <- normal(0, 1)^2
    is_chi_squared(z)
    #> [1] TRUE
    z
    #> Chi-squared distribution (df = 1)

This rule specifically applies when the normal has mean 0 and variance 1.

### Other simplification rules

Chi-squared addition:

    z <- chi_squared(3) + chi_squared(5)
    is_chi_squared(z)
    #> [1] TRUE
    z
    #> Chi-squared distribution (df = 8)
    params(z)
    #> df
    #>  8

Degrees of freedom add: Chi-squared(3) + Chi-squared(5) = Chi-squared(8).

Poisson addition:

    z <- poisson_dist(2) + poisson_dist(3)
    is_poisson_dist(z)
    #> [1] TRUE
    z
    #> Poisson distribution (lambda = 5)
    params(z)
    #> lambda
    #>      5

Rates add: Poisson(2) + Poisson(3) = Poisson(5).

Product of log-normals:

    z <- lognormal(0, 1) * lognormal(1, 2)
    is_lognormal(z)
    #> [1] TRUE
    z
    #> Log-normal distribution (meanlog = 1, sdlog = 2.23607)
    params(z)
    #>  meanlog    sdlog
    #> 1.000000 2.236068

The product of independent log-normals is log-normal because `log(X * Y) = log(X) + log(Y)`, and the sum of independent normals is normal. So the `meanlog` values add and the `sdlog` values combine as `sqrt(sdlog1^2 + sdlog2^2)`.

Uniform scaling and shifting:

    z <- 2 * uniform_dist(0, 1)
    is_uniform_dist(z)
    #> [1] TRUE
    z
    #> Uniform distribution (min = 0, max = 2)
    params(z)
    #> min max
    #>   0   2

Scaling a Uniform(0, 1) by 2 gives Uniform(0, 2).

    z <- uniform_dist(0, 1) + 5
    is_uniform_dist(z)
    #> [1] TRUE
    z
    #> Uniform distribution (min = 5, max = 6)
    params(z)
    #> min max
    #>   5   6

Shifting Uniform(0, 1) by 5 gives Uniform(5, 6).

## When Simplification Cannot Help

When no algebraic rule matches, the result remains an unsimplified `edist`. All distribution methods still work – they just use Monte Carlo under the hood.

    z <- normal(0, 1) * exponential(1)
    is_edist(z)
    #> [1] TRUE

    set.seed(2)
    mean(z)
    #> [1] 0.02069384

The true mean of `X * Y` where `X ~ Normal(0, 1)` and `Y ~ Exp(1)` is `E[X] * E[Y] = 0 * 1 = 0` (by independence), so the MC estimate should be near zero.

## Realization and MC Fallback

You can explicitly convert any distribution to an empirical distribution via `realize()`. This draws `n` samples and wraps them in an `empirical_dist` that supports exact (discrete) methods for CDF, density, conditional, and more.

    set.seed(3)
    z <- normal(0, 1) * exponential(1)
    rd <- realize(z, n = 10000)
    rd
    #> Realized distribution (10000 samples from: Expression distribution: x * y)
    #>   source:
    #> Expression distribution: x * y
    #>    x  ~  Normal distribution (mu = 0, var = 1)
    #>    y  ~  Exponential distribution (rate = 1)
    mean(rd)
    #> [1] -0.003321398
    vcov(rd)
    #>          [,1]
    #> [1,] 2.048537

For `edist` objects, methods like `cdf()`, `density()`, and `inv_cdf()` automatically materialize samples behind the scenes (with caching, so repeated calls share the same sample). You do not need to call `realize()` manually for these:

    set.seed(4)
    z <- normal(0, 1) + exponential(1)
    Fz <- cdf(z)
    Fz(1)
    #> [1] 0.537
    Fz(2)
    #> [1] 0.7914

## Min, Max, and Summary Operations

The `Summary` group generic is implemented for distributions. The `sum()` and `prod()` generics reduce via `+` and `*`, so they benefit from all the simplification rules. The `min()` and `max()` generics create elementwise `edist` expressions.

Min of exponentials has a special rule – the minimum of independent exponentials is exponential with the sum of rates:

    z <- min(exponential(1), exponential(2))
    is_exponential(z)
    #> [1] TRUE
    z
    #> Exponential distribution (rate = 3)
    params(z)
    #> rate
    #>    3

This is the well-known result: if `X ~ Exp(r1)` and `Y ~ Exp(r2)`, then `min(X, Y) ~ Exp(r1 + r2)`.

Sum reduces via the `+` operator, so simplification rules apply:

    z <- sum(normal(0, 1), normal(2, 3), normal(-1, 2))
    is_normal(z)
    #> [1] TRUE
    z
    #> Normal distribution (mu = 1, var = 6)
    mean(z)
    #> [1] 1
    vcov(z)
    #> [1] 6

Three normals sum to a single normal: mean = 0 + 2 - 1 = 1, var = 1 + 3 + 2 = 6.

Max and other combinations that lack closed forms remain as `edist`:

    set.seed(5)
    z <- max(exponential(1), exponential(2))
    is_edist(z)
    #> [1] TRUE
    mean(z)
    #> [1] 1.170383

## Limiting Distributions

The package provides functions for common asymptotic results from probability theory.

### Central Limit Theorem

`clt(base_dist)` returns the limiting distribution of the standardized sample mean, `sqrt(n) * (Xbar_n - mu)`. For a univariate distribution with variance `sigma^2`, this is `Normal(0, sigma^2)`.

    z <- clt(exponential(1))
    z
    #> Normal distribution (mu = 0, var = 1)
    mean(z)
    #> [1] 0
    vcov(z)
    #> [1] 1

For Exp(1), the mean is 1 and the variance is 1, so the CLT limit is Normal(0, 1).

### Law of Large Numbers

`lln(base_dist)` returns the degenerate distribution at the population mean – the limit of `Xbar_n` as `n -> infinity`. It is represented as a normal with zero variance.

    d <- lln(exponential(1))
    d
    #> Normal distribution (mu = 1, var = 0)
    mean(d)
    #> [1] 1
    vcov(d)
    #> [1] 0

### Delta Method

`delta_clt(base_dist, g, dg)` returns the limiting distribution of `sqrt(n) * (g(Xbar_n) - g(mu))` under the delta method. You supply the function `g` and its derivative `dg`.

    z <- delta_clt(exponential(1), g = exp, dg = exp)
    z
    #> Normal distribution (mu = 0, var = 7.38906)
    mean(z)
    #> [1] 0
    vcov(z)
    #> [1] 7.389056

For `g = exp` applied to Exp(1) (mean = 1, var = 1), the delta method gives `Normal(0, (exp(1))^2 * 1) = Normal(0, e^2)`.

### Moment-Matching Normal Approximation

`normal_approx(x)` constructs a normal distribution that matches the mean and variance of any input distribution:

    g <- gamma_dist(5, 2)
    n <- normal_approx(g)
    n
    #> Normal distribution (mu = 2.5, var = 1.25)
    mean(n)
    #> [1] 2.5
    vcov(n)
    #> [1] 1.25

The Gamma(5, 2) has mean 2.5 and variance 1.25, so the normal approximation is Normal(2.5, 1.25).

### Empirical CLT Convergence

To see the CLT in action, we can draw replicated standardized sample means at increasing sample sizes and watch them converge to the predicted distribution:

    set.seed(42)
    base <- exponential(rate = 1)
    ns <- c(10, 100, 1000)
    for (n in ns) {
      samp_means <- replicate(5000, {
        x <- sampler(base)(n)
        sqrt(n) * (mean(x) - mean(base))
      })
      cat(sprintf("n=%4d: mean=%.3f, var=%.3f\n",
                  n, mean(samp_means), var(samp_means)))
    }
    #> n=  10: mean=-0.006, var=1.002
    #> n= 100: mean=-0.003, var=1.022
    #> n=1000: mean=0.007, var=0.995

Compare with the CLT prediction:

    z <- clt(base)
    cat(sprintf("CLT:    mean=%.3f, var=%.3f\n", mean(z), vcov(z)))
    #> CLT:    mean=0.000, var=1.000

As `n` grows, the empirical mean converges to 0 and the empirical variance converges to 1, matching the CLT limit of Normal(0, 1).