Multivariate¶
multivariate
¶
Multivariate heavy-tailed distributions, in the elliptical family.
Correlation is not the question when heavy-tailed variables are modelled jointly. The question is whether their extremes arrive together, and that is a property of the joint tail rather than of the covariance. Two series can be almost uncorrelated and still crash on the same day.
The distributions here are normal scale mixtures: a Gaussian vector divided by the square root of an independent positive variable. That single construction gives both members below, and the mixing variable is what makes the tail heavy::
X = mu + L Z / sqrt(S)
with L a Cholesky factor of the scale matrix, Z standard normal, and
S the mixing variable. S = 1 gives the multivariate normal; S = W/nu
with W chi-square on nu degrees of freedom gives the multivariate
Student-t.
Two things this deliberately does not do:
There is no distribution function. The multivariate t has no closed-form
cdf in dimension two or above; every implementation computes it by numerical
integration or simulation. :meth:MultivariateStudentT.cdf_monte_carlo is
offered instead, and it reports a standard error, because a probability
estimated by simulation without one is not usable.
The linear algebra is NumPy's, but only just. Cholesky is cubic in the dimension and the dimension here is two or three, so the factorisation is still written out rather than delegated -- it is the one place a hand-written routine can give a better error than a library one, naming the pivot that failed instead of reporting that a matrix was not positive definite. What is vectorised is everything that runs once per observation: the quadratic form, the density, the sampler and the EM step, where the sample is thousands of rows and the interpreter was the whole cost.
Elliptical
dataclass
¶
Bases: ABC
A normal scale mixture: X = mu + L Z / sqrt(S).
Subclasses supply two things: the density kernel in the quadratic form, and how to draw the mixing variable. Everything else -- the Cholesky factor, the Mahalanobis distance, sampling, marginals -- follows from the construction and is shared.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mu
|
Vector
|
Location vector. |
required |
sigma
|
Matrix
|
Scale matrix, symmetric positive definite. For the normal this is
the covariance; for the Student-t it is not, since the
covariance is |
required |
Raises:
| Type | Description |
|---|---|
ParameterError
|
If the dimensions disagree or the scale matrix is not positive definite. |
logpdf
¶
Log density at x, for one point or many.
The log is the primitive rather than an afterthought: a multivariate density in even a few dimensions underflows to zero well before the point stops being interesting, and fitting works on the log anyway.
Source code in heavytails/multivariate.py
mahalanobis
¶
The quadratic form (x - mu)' Sigma^-1 (x - mu).
Computed by forward substitution on the Cholesky factor rather than by inverting the matrix, which is both faster and better conditioned.
Takes one point or many, and mirrors what it was given: a single point
gives a float, an (n, dim) array of them gives an array of n. The
substitution loops over the dimension, which is small, and does each of
its steps across every observation at once -- the opposite of the
arrangement it replaced, which looped over the observations and did
each one's substitution in the interpreter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Any
|
A point with as many components as the distribution, or an array of such points whose last axis is the components. |
required |
Returns:
| Type | Description |
|---|---|
Any
|
The squared Mahalanobis distance, which is non-negative. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the last axis does not match the dimension. |
Source code in heavytails/multivariate.py
marginal
¶
The distribution of the named components.
Marginals of an elliptical distribution stay in the family, with the corresponding sub-block of the scale matrix and the same mixing variable. That is an exact identity rather than an approximation, and the test suite checks it against simulation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
indices
|
Sequence[int]
|
Which components to keep, in the order wanted. |
required |
Returns:
| Type | Description |
|---|---|
Elliptical
|
The same kind of distribution, of lower dimension. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If an index is out of range or repeated. |
Source code in heavytails/multivariate.py
pdf
¶
Density at x, or zero where it underflows.
Source code in heavytails/multivariate.py
rvs
¶
Draw n independent vectors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Number of draws, positive. |
required |
seed
|
int | None
|
Seed for reproducibility. |
None
|
Returns:
| Type | Description |
|---|---|
list[Vector]
|
A list of |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in heavytails/multivariate.py
MultivariateNormal
dataclass
¶
Bases: Elliptical
Multivariate normal, present as the reference member of the family.
Not heavy-tailed, and included anyway: it is the nu -> infinity limit of
the Student-t and therefore the thing to check that limit against, and its
zero tail dependence is the contrast that makes the t interesting. Two
jointly normal variables with correlation 0.9 still have asymptotically
independent extremes.
Examples:
>>> normal = MultivariateNormal(mu=[0.0, 0.0], sigma=[[1.0, 0.0], [0.0, 1.0]])
>>> round(normal.pdf([0.0, 0.0]), 6)
0.159155
>>> round(1 / (2 * math.pi), 6)
0.159155
MultivariateStudentT
dataclass
¶
Bases: Elliptical
Multivariate Student-t, the workhorse of joint heavy-tail modelling.
The tail index is nu in every direction, and -- unlike the normal --
its extremes arrive together: the coefficient of tail dependence is
positive for any correlation, including zero. That is the property the
whole elliptical apparatus is here to provide, and
:func:tail_dependence_coefficient gives it in closed form.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nu
|
float
|
Degrees of freedom, positive. Moments of order |
1.0
|
mu
|
Vector
|
Location vector. |
required |
sigma
|
Matrix
|
Scale matrix, not the covariance. The covariance is
|
required |
Raises:
| Type | Description |
|---|---|
ParameterError
|
If |
Examples:
>>> t = MultivariateStudentT(nu=4.0, mu=[0.0, 0.0],
... sigma=[[1.0, 0.5], [0.5, 1.0]])
>>> t.dim
2
>>> round(t.mahalanobis([1.0, 1.0]), 6)
1.333333
Its marginals are univariate t with the same degrees of freedom:
>>> from heavytails import StudentT
>>> round(t.marginal([0]).pdf([1.5]), 8) == round(StudentT(nu=4.0).pdf(1.5), 8)
True
cdf_monte_carlo
¶
Estimate P(X <= upper) by simulation, with a standard error.
The multivariate t has no closed-form distribution function in dimension two or above. Every library that offers one computes it numerically, and this says so in its name rather than presenting an estimate as an exact value.
The standard error is returned alongside, because a simulated probability without one cannot be acted on -- and it is largest in relative terms exactly where the probability is smallest, which is the corner anyone modelling joint extremes is asking about.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
upper
|
Sequence[float]
|
Componentwise upper limits. |
required |
n
|
int
|
Number of draws. |
100000
|
seed
|
int | None
|
Seed for reproducibility. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> t = MultivariateStudentT(nu=5.0, mu=[0.0, 0.0],
... sigma=[[1.0, 0.0], [0.0, 1.0]])
>>> result = t.cdf_monte_carlo([0.0, 0.0], n=20000, seed=1)
>>> abs(result["probability"] - 0.25) < 4 * result["standard_error"]
True
Source code in heavytails/multivariate.py
covariance
¶
The covariance matrix, which is not the scale matrix.
Returns:
| Type | Description |
|---|---|
Matrix
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in heavytails/multivariate.py
cholesky
¶
Lower-triangular L with L L' = matrix.
The factorisation doubles as the validity check on a scale matrix: it succeeds exactly when the matrix is symmetric positive definite, which is what a covariance or scale matrix has to be. A separate eigenvalue test would be more work and would answer the same question less directly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
Sequence[Sequence[float]]
|
Square, symmetric, positive definite. |
required |
Returns:
| Type | Description |
|---|---|
Matrix
|
The lower-triangular factor, as a list of rows. |
Raises:
| Type | Description |
|---|---|
ParameterError
|
If the matrix is not square, not symmetric, or not positive definite -- with which entry failed, since "not positive definite" on its own is a hard message to act on. |
Examples:
Source code in heavytails/multivariate.py
fit_multivariate_t
¶
Fit a multivariate Student-t by expectation-maximisation.
The scale-mixture construction is what makes this easy: treat the mixing
variable as missing data, and each iteration is a weighted mean and
covariance with weights (nu + d) / (nu + q_i). Those weights are the
whole robustness story -- a point far out in Mahalanobis distance gets less
say, automatically, which is why a t fit is not dragged around by outliers
the way a Gaussian one is.
With nu left unset it is chosen by profile likelihood over a grid. That
is a coarser answer than the other two parameters get, and it is honest
about the shape of the problem: the profile likelihood in nu is flat
enough that a fitted value of 6 rather than 8 usually says more about the
sample than about the population.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Sequence[Sequence[float]]
|
Rows of observations, all the same length. |
required |
nu
|
float | None
|
Degrees of freedom, or None to select by profile likelihood. |
None
|
max_iter
|
int
|
Maximum EM iterations. |
200
|
tol
|
float
|
Convergence tolerance on the location and scale. |
1e-08
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
|
dict[str, Any]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the data is empty, ragged, or has fewer rows than columns -- in which case the scale matrix is singular and no fit exists. |
Examples:
>>> source = MultivariateStudentT(nu=5.0, mu=[1.0, -1.0],
... sigma=[[1.0, 0.3], [0.3, 2.0]])
>>> fit = fit_multivariate_t(source.rvs(4000, seed=1), nu=5.0)
>>> fit["converged"]
True
>>> all(abs(a - b) < 0.15 for a, b in zip(fit["distribution"].mu, [1.0, -1.0]))
True
Source code in heavytails/multivariate.py
571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 | |
tail_dependence_coefficient
¶
Coefficient of tail dependence for a bivariate Student-t, in closed form.
The probability that one component is extreme given the other is, in the limit::
lambda = 2 T_{nu+1}( -sqrt( (nu+1)(1-rho) / (1+rho) ) )
with T the univariate t distribution function. Upper and lower are
equal by symmetry.
Two properties are worth knowing before using correlation as a proxy for joint risk:
It is positive even at zero correlation. Uncorrelated t variables still have extremes that arrive together, because they share the same mixing variable -- the market-wide shock, in the usual reading.
It goes to zero as nu grows. The Gaussian limit has no tail dependence at any correlation short of one, which is exactly why a Gaussian copula understates joint extremes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nu
|
float
|
Degrees of freedom, positive. |
required |
rho
|
float
|
Correlation of the scale matrix, in [-1, 1]. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The coefficient, in [0, 1]. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
Positive even when the components are uncorrelated:
And vanishing as the tail lightens: