Skip to content

Stats: copulas and dependence

simweave.stats lets a simulation draw dependent random quantities: an inter-arrival gap together with a job's service time, or successive gaps that cluster into bursts. A copula describes the dependence; marginals describe each quantity's own distribution; you choose them separately.

It needs only numpy. Gaussian, Clayton, Gumbel and Frank copulas work on a base install; the Student-t copula and method="mle" fitting need scipy (pip install simweave[optim]) and say so if it is missing. The design is in Copula sampling.

Joint draws: gap and service time

import numpy as np
import simweave as sw
from simweave.stats import Exponential, JointSampler

joint = JointSampler(
    sw.GumbelCopula.from_kendall_tau(0.5).flipped((0,)),   # negative dependence
    {"interarrival": Exponential(1.0), "service_time": Exponential(0.8)},
)

def factory(env, draw):                       # draw = {"interarrival": ..., "service_time": ...}
    job = sw.Entity()
    job.sim_properties = sw.EntityProperties.from_draw(draw)   # fixed service time + extras
    return job

gen = sw.ArrivalGenerator(None, factory, service, rng=rng, joint=joint, gap_alignment="after")

ArrivalGenerator(joint=...) is opt-in; without it nothing changes. The generator draws from a child generator seeded from rng, so adding a copula does not disturb your other random draws.

Which entity gets the attribute? gap_alignment="before" (default) attaches the attributes to the entity that arrives after the drawn gap (an attribute vs the time since the last arrival). "after" attaches them to the entity just created, with the gap being the time to the next arrival. They change queue behaviour differently, so pick the one that matches your story.

Bursty arrivals with identical marginals

from simweave.stats import MarkovCopulaProcess

def scenario(seed):
    gaps = MarkovCopulaProcess(sw.GumbelCopula.from_kendall_tau(0.6), Exponential(1.0))
    gen = sw.ArrivalGenerator(gaps, factory, service, rng=np.random.default_rng(seed))
    ...

Each gap keeps exactly the Exponential(1.0) marginal, but consecutive gaps have the copula's dependence. The process is stateful: build it inside the scenario so each Monte Carlo replicate gets its own. See demo 30.

Copulas

Copula Tail dependence Notes
GaussianCopula none any dimension
StudentTCopula symmetric scipy
ClaytonCopula lower exchangeable, any dimension
GumbelCopula upper exchangeable, any dimension
FrankCopula none exchangeable, any dimension

All have from_kendall_tau(tau). Negative dependence and the opposite tails use .flipped(axes) and .survival(): GumbelCopula.from_kendall_tau(0.5).flipped((1,)) is large on the first axis with small on the second. copula.tail_dependence() returns the corner coefficients (ll, uu, lu, ul).

Marginals

Exponential, Uniform, Normal, LogNormal (from_mean_cv), Weibull, Deterministic, Empirical(data), and ScipyMarginal / as_marginal for any frozen scipy.stats distribution. The existing sw.exponential, sw.uniform, sw.normal and sw.deterministic helpers also carry ppf/cdf and can be used directly as marginals.

Fitting from data

fit = sw.fit_copula(observations)            # (n, 2) array; ranks only
print(fit.family, fit.tau, fit.table)        # candidates ranked by AIC
joint = sw.JointSampler.from_data(observations, ["interarrival", "size"])

fit_copula inverts Kendall's tau (numpy only) and ranks the families, with survival variants for upper-tail shapes and a flip for negative dependence. method="mle" refines the parameter by maximum likelihood (scipy).

API

stats

Dependence modelling: marginals, copulas, joint and serial sampling, fitting.

Pure numpy at import time. Gaussian, Clayton, Gumbel and Frank copulas work on a base install; the Student-t copula and method="mle" fitting need scipy (pip install simweave[optim]).

ClaytonCopula

ClaytonCopula(theta: float, dim: int = 2)

Bases: _Archimedean

Clayton copula: lower-tail dependence 2**(-1/theta).

Copula

Bases: ABC

Base class. dim is the number of coupled variables.

sample_u abstractmethod

sample_u(n: int, rng: Generator) -> ndarray

Draw n points, shape (n, dim).

log_pdf abstractmethod

log_pdf(u) -> ndarray

Log copula density at the rows of u.

tail_dependence

tail_dependence() -> dict

Corner tail-dependence coefficients (bivariate).

Keys: ll = P(U2<q | U1uu = P(U2>1-q | U1>1-q), lu = P(U2>1-q | U1<q), ul = P(U21-q), as q -> 0.

conditional_ppf

conditional_ppf(w, u)

Quantile of U2 | U1 = u at probability w (bivariate).

flipped

flipped(axes=(1,)) -> 'Copula'

Reflect the listed axes: u_k -> 1 - u_k.

survival

survival() -> 'Copula'

Survival copula (all axes reflected); swaps lower and upper tails.

Flipped

Flipped(base: Copula, axes=(1,))

Bases: Copula

A copula with some axes reflected (u_k -> 1 - u_k).

FrankCopula

FrankCopula(theta: float, dim: int = 2)

Bases: _Archimedean

Frank copula (theta > 0): symmetric, no tail dependence.

GaussianCopula

GaussianCopula(corr)

Bases: Copula

Gaussian copula with correlation matrix (or scalar rho for two variables).

GumbelCopula

GumbelCopula(theta: float, dim: int = 2)

Bases: _Archimedean

Gumbel copula: upper-tail dependence 2 - 2**(1/theta).

conditional_ppf

conditional_ppf(w, u)

Invert h(v|u) = dC/du by vectorised bisection.

StudentTCopula

StudentTCopula(corr, df: float = 4.0)

Bases: Copula

Student-t copula (requires scipy). Symmetric tail dependence.

FitResult dataclass

FitResult(copula: Copula, family: str, tau: float, loglik: float, aic: float, n: int, table: list[dict] = list())

Outcome of :func:fit_copula.

JointSampler

JointSampler(copula: Copula, marginals: Mapping[str, object], *, block: int = 256)

Draw dependent values: copula supplies the dependence, marginals the shapes.

Parameters:

Name Type Description Default
copula Copula

A :class:~simweave.stats.copulas.Copula with dim == len(marginals).

required
marginals Mapping[str, object]

Mapping name -> marginal (insertion order = copula axis order). Anything :func:~simweave.stats.marginals.as_marginal accepts.

required
block int

Scalar :meth:draw calls are served from blocks of this many vectorised samples. The buffer is tied to the rng passed in, so a given seed and call order is reproducible; a shared rng advances in blocks.

256

sample_array

sample_array(rng: Generator, n: int) -> ndarray

(n, dim) array of values, columns in names order.

draw

draw(rng: Generator) -> dict[str, float]

One joint draw as {name: value}.

from_data classmethod

from_data(data, names: Sequence[str], *, marginals: Mapping[str, object] | str = 'empirical', family: str = 'auto', **fit_kwargs) -> 'JointSampler'

Calibrate from observations (rows) with empirical or supplied marginals.

Empirical

Empirical(data)

Bases: Marginal

Empirical marginal: linear-interpolated quantile function of a sample.

Exponential

Exponential(mean: float)

Bases: Marginal

Exponential with the given mean (same convention as sw.exponential).

LogNormal

LogNormal(mu: float, sigma: float)

Bases: Marginal

exp(N(mu, sigma^2)).

Marginal

Bases: ABC

Base class: subclasses implement ppf and cdf.

rvs

rvs(rng: Generator, size=None)

Independent draws (inverse-transform of uniforms).

Normal

Normal(mean: float, std: float, *, clip_nonnegative: bool = True)

Bases: Marginal

Normal; clipped at zero by default, like sw.normal.

ScipyMarginal

ScipyMarginal(frozen)

Bases: Marginal

Wrap a frozen scipy.stats distribution.

MarkovCopulaProcess

MarkovCopulaProcess(copula: Copula, marginal)

Stationary sequence with a chosen marginal and lag-1 dependence copula.

U_t = h^{-1}(W | U_{t-1}) with W ~ U(0, 1), so each value has exactly the requested marginal and consecutive values have exactly the copula's dependence. It is a callable (rng) -> float, so it plugs straight into ArrivalGenerator(interarrival=...).

The process carries state, so build one per replicate inside the scenario function; do not share it across Monte Carlo runs.

sample

sample(rng: Generator, n: int) -> ndarray

Convenience: n consecutive values (continues the chain).

fit_copula

fit_copula(data, family: str = 'auto', *, method: str = 'tau', df: float = 5.0) -> FitResult

Fit a bivariate copula (or a Gaussian/t copula of higher dimension).

Negative dependence is handled by reflecting the second axis; the returned copula is then a flipped copula.

Parameters:

Name Type Description Default
data

(n, d) observations. Only ranks are used, so marginals do not matter.

required
family str

"auto" (rank candidates by AIC) or one of "gaussian", "t", "clayton", "gumbel", "frank", optionally with a "_survival" suffix ("clayton_survival") for upper-tail shapes.

'auto'
method str

"tau" (default; Kendall-tau inversion, numpy only) or "mle" (refine the parameter by maximum likelihood on pseudo-observations; needs scipy).

'tau'
df float

Degrees of freedom for the t copula (held fixed).

5.0

Returns:

Type Description
FitResult

The best candidate, with all candidates ranked in table.

kendall_tau

kendall_tau(x, y, *, max_n: int = 10000) -> float

Kendall's tau-b.

Uses scipy when present; otherwise an O(n^2) numpy pass (subsampled with a warning above max_n points).

pseudo_obs

pseudo_obs(data) -> ndarray

Rank-transform each column to rank / (n + 1) (open unit interval).

as_marginal

as_marginal(obj) -> Marginal

Coerce obj to a :class:Marginal.

Accepts a :class:Marginal, a frozen scipy.stats distribution, a raw array of observations (-> :class:Empirical), or any object with a ppf attribute, including sw.exponential(...) and friends.

tail_dependence_empirical

tail_dependence_empirical(u, q: float = 0.01) -> dict

Empirical corner tail-dependence at level q for bivariate uniforms.

Keys match :meth:Copula.tail_dependence: ll, uu, lu, ul.