Skip to content

API Reference

Shrinkage estimators for covariance matrices (Ledoit–Wolf).

from shrinkage.linear import cov1para
from shrinkage.nonlinear import nonlinear_shrinkage

Both estimators are also re-exported from the package root, so from shrinkage import cov1para, nonlinear_shrinkage is equivalent.


Linear Shrinkage

shrinkage.linear

Linear shrinkage estimators for covariance matrices.


Nonlinear Shrinkage

shrinkage.nonlinear

Nonlinear shrinkage estimators for covariance matrices.

nonlinear_shrinkage(Y, k=None)

Quadratic-inverse shrinkage (QIS) of the sample covariance matrix.

Estimates the covariance matrix by shrinking each sample eigenvalue individually toward the value that minimises the expected loss, using the closed-form quadratic-inverse shrinkage estimator of Ledoit and Wolf. QIS is derived under the Frobenius loss (and its inverse-Stein and minimum-variance cousins) and, unlike :func:shrinkage.linear.cov1para — which applies a single scalar intensity toward a scaled-identity target — reshapes the whole eigenvalue spectrum while keeping the sample eigenvectors. It is well defined both when p <= n and when the sample covariance is singular (p > n).

Parameters

Y: Raw data matrix of shape (N, p): N iid observations on p variables. The estimator targets the large-dimensional regime where p is comparable to (or larger than) N. k: Demeaning control. None (default) or a float NaN: demean Y, reduce the effective sample size by 1. 0: no demeaning. 1: Y is already demeaned.

Returns:

Symmetric positive semi-definite covariance estimator of shape (p, p) whose trace matches the sample covariance's trace.

Raises:

ValueError If Y is not a 2-D array, has no variables or observations, contains non-finite values, or the effective sample size n = N - k is not positive. The message names the offending argument/condition.

Notes:

Port of the reference QIS implementation from Ledoit and Wolf's covariance-shrinkage code (https://github.com/pald22/covShrinkage), using :func:numpy.linalg.eigh for the symmetric spectral decomposition.

Examples:

>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> Y = rng.standard_normal((100, 30))
>>> nonlinear_shrinkage(Y).shape
(30, 30)
Source code in src/shrinkage/nonlinear/qis.py
def nonlinear_shrinkage(Y: np.ndarray, k: int | float | None = None) -> np.ndarray:
    """Quadratic-inverse shrinkage (QIS) of the sample covariance matrix.

    Estimates the covariance matrix by shrinking each *sample eigenvalue*
    individually toward the value that minimises the expected loss, using the
    closed-form quadratic-inverse shrinkage estimator of Ledoit and Wolf. QIS is
    derived under the Frobenius loss (and its inverse-Stein and minimum-variance
    cousins) and, unlike :func:`shrinkage.linear.cov1para` — which applies a
    single scalar intensity toward a scaled-identity target — reshapes the whole
    eigenvalue spectrum while keeping the sample eigenvectors. It is well defined
    both when p <= n and when the sample covariance is singular (p > n).

    Parameters
    ----------
    Y:
        Raw data matrix of shape (N, p): N iid observations on p variables. The
        estimator targets the large-dimensional regime where p is comparable to
        (or larger than) N.
    k:
        Demeaning control. None (default) or a float NaN: demean Y, reduce the
        effective sample size by 1. 0: no demeaning. 1: Y is already demeaned.

    Returns:
    -------
    Symmetric positive semi-definite covariance estimator of shape (p, p) whose
    trace matches the sample covariance's trace.

    Raises:
    ------
    ValueError
        If ``Y`` is not a 2-D array, has no variables or observations, contains
        non-finite values, or the effective sample size ``n = N - k`` is not
        positive. The message names the offending argument/condition.

    Notes:
    -----
    Port of the reference ``QIS`` implementation from Ledoit and Wolf's
    covariance-shrinkage code (https://github.com/pald22/covShrinkage), using
    :func:`numpy.linalg.eigh` for the symmetric spectral decomposition.

    Examples:
        >>> import numpy as np
        >>> rng = np.random.default_rng(42)
        >>> Y = rng.standard_normal((100, 30))
        >>> nonlinear_shrinkage(Y).shape
        (30, 30)
    """
    N, p = validate_observation_matrix(Y)

    if k is None or (isinstance(k, float) and np.isnan(k)):
        Y = Y - Y.mean(axis=0)
        k = 1

    n = effective_sample_size(N, k)
    c = p / n  # concentration ratio

    sample = (Y.T @ Y) / n
    sample = (sample + sample.T) / 2.0  # enforce exact symmetry

    # eigh returns eigenvalues in ascending order with orthonormal eigenvectors.
    lam, u = np.linalg.eigh(sample)
    lam = lam.clip(min=0.0)  # reset tiny negative rounding artefacts to zero

    # Inverse of the min(p, n) non-null (largest) eigenvalues.
    pn = min(p, int(n))
    invlambda = 1.0 / lam[p - pn :]

    # Smoothed Stein shrinker and its Hilbert-transform conjugate.
    h = (min(c**2, 1.0 / c**2) ** 0.35) / p**0.35  # smoothing parameter
    Lj = np.tile(invlambda.reshape(-1, 1), (1, pn))
    Lj_i = Lj - Lj.T
    denom = Lj_i**2 + Lj**2 * h**2
    theta = np.mean(Lj * Lj_i / denom, axis=0)
    htheta = np.mean(Lj * Lj * h / denom, axis=0)
    atheta2 = theta**2 + htheta**2

    if p <= n:
        # Non-singular sample covariance: optimally shrunk eigenvalues.
        denom = (1.0 - c) ** 2 * invlambda + 2.0 * c * (1.0 - c) * invlambda * theta + c**2 * invlambda * atheta2
        delta = 1.0 / denom
    else:
        # Singular sample covariance: shrink the p - n null eigenvalues too.
        delta0 = 1.0 / ((c - 1.0) * np.mean(invlambda))
        delta = np.concatenate([np.repeat(delta0, p - pn), 1.0 / (invlambda * atheta2)])

    delta_qis = delta * (lam.sum() / delta.sum())  # preserve the trace
    estimator: np.ndarray = u @ (delta_qis[:, None] * u.T)
    return estimator