Source code for skfolio.uncertainty_set._empirical

"""Empirical Uncertainty Set estimators."""

# Copyright (c) 2023-2026
# Author: Hugo Delatte <hugo.delatte@skfoliolabs.com>
# SPDX-License-Identifier: BSD-3-Clause
# Implementation derived from:
# Riskfolio-Lib, Copyright (c) 2020-2023, Dany Cajas, Licensed under BSD 3 clause.
# scikit-learn, Copyright (c) 2007-2010 David Cournapeau, Fabian Pedregosa, Olivier
# Grisel Licensed under BSD 3 clause.

from __future__ import annotations

import numpy as np
import scipy.linalg as sla
import scipy.stats as st
import sklearn.utils.metadata_routing as skm

from skfolio.prior import BasePrior, EmpiricalPrior
from skfolio.typing import ArrayLike
from skfolio.uncertainty_set._base import (
    BaseCovarianceUncertaintySet,
    BaseMuUncertaintySet,
)
from skfolio.uncertainty_set._model import UncertaintySet
from skfolio.utils.stats import commutation_matrix
from skfolio.utils.tools import check_estimator


[docs] class EmpiricalMuUncertaintySet(BaseMuUncertaintySet): r"""Empirical Mu Uncertainty Set. Compute the expected returns ellipsoidal uncertainty set [1]_: .. math:: U_{\mu} = \left\{ \mu : (\mu - \hat{\mu})^\top S^{-1}(\mu - \hat{\mu}) \le \kappa^2 \right\}. Under the assumption that :math:`\Sigma` is given, the distribution of the sample estimator :math:`\hat{\mu}` based on an i.i.d. sample :math:`R_{t}\sim N(\mu, \Sigma), t=1,...,T` is given by :math:`\hat{\mu}\sim N(\mu, \frac{1}{T}\Sigma)`. The radius of the ellipsoid :math:`\kappa` (confidence region) is computed using: .. math:: \kappa^2 = \chi^2_{n_{\text{assets}}}(\beta) with :math:`\chi^2_{n_{\text{assets}}}(\beta)` the inverse cumulative distribution function of the chi-squared distribution with :math:`n_{\text{assets}}` degrees of freedom at the :math:`\beta` confidence level. The shape matrix :math:`S` of the ellipsoid is computed using: .. math:: S = \frac{1}{T}\Sigma with the option to force the non-diagonal elements of the covariance matrix to zero. The estimator stores the square-root factor as the linear geometry map :math:`L = S^{1/2}`. Parameters ---------- prior_estimator : BasePrior, optional The :ref:`prior estimator <prior>` used to estimate the assets covariance matrix. The default (`None`) is to use :class:`~skfolio.prior.EmpiricalPrior`. confidence_level : float, default=0.95 Confidence level :math:`\beta` of the inverse cumulative distribution function of the chi-squared distribution. The default value is `0.95`. diagonal : bool, default=True If `True`, the non-diagonal elements of the covariance matrix are set to zero. n_eff : float, optional Effective number of observations used for the mean estimator. If `None`, the number of observations in `X` is used. This is useful when the expected returns are estimated using a different window length or a weighted estimator (e.g. EWMA), in which case `n_eff` should be set to the corresponding effective sample size. Attributes ---------- uncertainty_set_ : UncertaintySet Mu Uncertainty set :class:`~skfolio.uncertainty_set.UncertaintySet`. prior_estimator_ : BasePrior Fitted `prior_estimator`. n_eff_ : float Effective number of observations actually used to build the uncertainty set. References ---------- .. [1] "Robustness properties of mean-variance portfolios", Optimization: A Journal of Mathematical Programming and Operations Research, Schöttle & Werner (2009). .. [2] "Portfolio Optimization: Theory and Application", Chapter 14, Daniel P. Palomar (2025) """ n_eff_: float def __init__( self, prior_estimator: BasePrior | None = None, confidence_level: float = 0.95, diagonal: bool = True, n_eff: float | None = None, ): super().__init__(prior_estimator=prior_estimator) self.confidence_level = confidence_level self.diagonal = diagonal self.n_eff = n_eff
[docs] def fit( self, X: ArrayLike, y: ArrayLike | None = None, **fit_params ) -> EmpiricalMuUncertaintySet: """Fit the Empirical Mu Uncertainty set estimator. Parameters ---------- X : array-like of shape (n_observations, n_assets) Price returns of the assets. y : array-like of shape (n_observations, n_factors), optional Price returns of factors. The default is `None`. **fit_params : dict Parameters to pass to the underlying estimators. Only available if `enable_metadata_routing=True`, which can be set by using `sklearn.set_config(enable_metadata_routing=True)`. See :ref:`Metadata Routing User Guide <metadata_routing>` for more details. Returns ------- self : EmpiricalMuUncertaintySet Fitted estimator. """ routed_params = skm.process_routing(self, "fit", **fit_params) self.prior_estimator_ = check_estimator( self.prior_estimator, default=EmpiricalPrior(), check_type=BasePrior, ) # fitting estimators self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit) return_distribution = self.prior_estimator_.return_distribution_ n_observations, n_assets = return_distribution.returns.shape self.n_eff_ = n_observations if self.n_eff is None else self.n_eff radius = np.sqrt(st.chi2.ppf(q=self.confidence_level, df=n_assets)) covariance = return_distribution.covariance if self.diagonal: geometry = np.diag(np.sqrt(np.diagonal(covariance) / self.n_eff_)) else: geometry = sla.sqrtm(covariance / self.n_eff_).real self.uncertainty_set_ = UncertaintySet(radius=radius, geometry=geometry, norm=2) return self
[docs] class EmpiricalCovarianceUncertaintySet(BaseCovarianceUncertaintySet): r"""Empirical Covariance Uncertainty set. Compute the covariance ellipsoidal uncertainty set [1]_: .. math:: U_{\Sigma} = \left\{ \Sigma : d^\top S^{-1} d \le \kappa^2, \Sigma \succeq 0 \right\}, \quad d = \operatorname{vec}(\Sigma) - \operatorname{vec}(\hat{\Sigma}). We consider the Wishart distribution for the covariance matrix: .. math:: \hat{\Sigma}\sim W(\frac{1}{T-1}\Sigma, T-1) The radius of the ellipsoid :math:`\kappa` (confidence region) is computed using: .. math:: \kappa^2 = \chi^2_{n_{\text{assets}}^2}(\beta) with :math:`\chi^2_{n_{\text{assets}}^2}(\beta)` the inverse cumulative distribution function of the chi-squared distribution with :math:`n_{\text{assets}}^2` degrees of freedom at the :math:`\beta` confidence level. The shape matrix :math:`S` of the ellipsoid is based on the covariance matrix of the Wishart distributed random variable using the vector notation :math:`\operatorname{vec}(x)`: .. math:: \operatorname{Cov}[\operatorname{vec}(\hat{\Sigma})] = \frac{1}{n_{\text{eff}}} (I_{n^2} + K_{nn})(\Sigma \otimes \Sigma). where :math:`K_{nn}` denotes a commutation matrix and :math:`\otimes` represents the Kronecker product. If `diagonal` is `True`, the asset covariance estimate is diagonalized and the linear geometry map :math:`L` is built directly from the diagonal of :math:`S`. Otherwise, the estimator stores a full square-root factor :math:`L = S^{1/2}`. Parameters ---------- prior_estimator : BasePrior, optional The :ref:`prior estimator <prior>` used to estimate the assets covariance matrix. The default (`None`) is to use :class:`~skfolio.prior.EmpiricalPrior`. confidence_level : float , default=0.95 Confidence level :math:`\beta` of the inverse cumulative distribution function of the chi-squared distribution. The default value is `0.95`. diagonal : bool, default=True If `True`, the non-diagonal elements of the asset covariance matrix are set to zero before building the ellipsoid shape matrix. n_eff : float, optional Effective number of observations used for the covariance estimator. If `None`, the number of observations in `X` is used. This is useful when the covariance matrix is estimated using a different window length or a weighted estimator (e.g. EWMA), in which case `n_eff` should be set to the corresponding effective sample size. Attributes ---------- uncertainty_set_ : UncertaintySet Covariance Uncertainty set :class:`~skfolio.uncertainty_set.UncertaintySet`. prior_estimator_ : BasePrior Fitted `prior_estimator`. n_eff_ : float Effective number of observations actually used to build the uncertainty set. References ---------- .. [1] "Robustness properties of mean-variance portfolios", Optimization: A Journal of Mathematical Programming and Operations Research, Schöttle & Werner (2009). .. [2] "Portfolio Optimization: Theory and Application", Chapter 14, Daniel P. Palomar (2025) """ n_eff_: float def __init__( self, prior_estimator: BasePrior | None = None, confidence_level: float = 0.95, diagonal: bool = True, n_eff: float | None = None, ): super().__init__(prior_estimator=prior_estimator) self.confidence_level = confidence_level self.diagonal = diagonal self.n_eff = n_eff
[docs] def fit( self, X: ArrayLike, y: ArrayLike | None = None, **fit_params ) -> EmpiricalCovarianceUncertaintySet: """Fit the Empirical Covariance Uncertainty set estimator. Parameters ---------- X : array-like of shape (n_observations, n_assets) Price returns of the assets. y : array-like of shape (n_observations, n_factors), optional Price returns of factors. The default is `None`. **fit_params : dict Parameters to pass to the underlying estimators. Only available if `enable_metadata_routing=True`, which can be set by using `sklearn.set_config(enable_metadata_routing=True)`. See :ref:`Metadata Routing User Guide <metadata_routing>` for more details. Returns ------- self : EmpiricalCovarianceUncertaintySet Fitted estimator. """ routed_params = skm.process_routing(self, "fit", **fit_params) self.prior_estimator_ = check_estimator( self.prior_estimator, default=EmpiricalPrior(), check_type=BasePrior, ) # fitting estimators self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit) return_distribution = self.prior_estimator_.return_distribution_ n_observations, n_assets = return_distribution.returns.shape self.n_eff_ = n_observations if self.n_eff is None else self.n_eff radius = np.sqrt(st.chi2.ppf(q=self.confidence_level, df=n_assets**2)) covariance = return_distribution.covariance if self.diagonal: covariance_diag = np.diagonal(covariance) shape_diag = np.kron(covariance_diag, covariance_diag) / self.n_eff_ shape_diag[:: n_assets + 1] *= 2 geometry = np.diag(np.sqrt(shape_diag)) else: shape = ( (np.identity(n_assets**2) + commutation_matrix(covariance)) @ np.kron(covariance, covariance) / self.n_eff_ ) geometry = sla.sqrtm(shape).real self.uncertainty_set_ = UncertaintySet(radius=radius, geometry=geometry, norm=2) return self