skfolio.attribution.realized_factor_attribution#
- skfolio.attribution.realized_factor_attribution(*, asset_names, factor_names, factor_families=None, weights, factor_returns, portfolio_returns, exposures, exposure_lag=1, idio_returns, idio_variances=None, regression_weights=None, family_constraint_basis=None, annualization_factor=252.0, compute_asset_breakdowns=True, compute_uncertainty=False)[source]#
Compute realized (ex-post) factor volatility and return attribution.
This function decomposes realized portfolio volatility and return into systematic (factors), idiosyncratic and unattributed contributions.
Time convention (as-of indexing):
Under this convention, all time-varying inputs at observation \(t\) reflect information available up to and including the end of period \(t\). Point-in-time fields and derived values store the latest available value for observation \(t\). Returns stored at observation \(t\) cover the period ending at \(t\), namely \((t-1, t]\).
For time-varying exposures, attribution uses exposures from before the return interval. When
exposure_lag > 0, the function aligns \(B_{t-\ell}\) with returns at \(t\); the first \(\ell\) return observations are discarded. For 2D static exposures, no trimming is needed.\[R_{P,t} = \sum_{k=1}^{K} x_{k,t} f_{k,t} + \varepsilon_{P,t} + \eta_{P,t}\]where \(x_{k,t} = B_{:,k,t-\ell}^\top w_t\), \(\varepsilon_{P,t}\) is the portfolio idiosyncratic return, \(\eta_{P,t}\) is the unattributed portfolio return, and \(\ell\) is
exposure_lag.Unattributed component:
The unattributed return :math:eta_{P,t} is the difference between the observed portfolio return and its systematic-plus-idiosyncratic reconstruction. It captures effects outside that reconstruction, such as costs, cash, intra-period trading and the time-series regression intercept.
Volatility Attribution (Variance Decomposition):
Using the covariance identity, the total portfolio variance decomposes as:
\[\operatorname{Var}(R_P) = \sum_{k=1}^{K} \operatorname{Cov}(x_k f_k, R_P) + \operatorname{Cov}(\varepsilon_P, R_P) + \operatorname{Cov}(\eta_P, R_P)\]Each factor’s variance contribution is \(\operatorname{Cov}(x_k f_k, R_P)\), which captures both the exposure magnitude and the factor’s correlation with portfolio returns. These contributions are additive and sum exactly to total variance.
Volatility Contribution:
The volatility contribution divides the variance contribution by portfolio volatility:
\[\operatorname{VolContrib}_k = \frac{\operatorname{Cov}(x_k f_k, R_P)}{\sigma_P}\]This also satisfies the \(\sigma \cdot \rho\) identity:
\[\operatorname{VolContrib}_k = \operatorname{std}(x_k f_k) \cdot \operatorname{corr}(x_k f_k, R_P)\]Return Attribution:
The mean return contribution of each factor is the average of the exposure-weighted factor returns:
\[\operatorname{MuContrib}_k = \overline{x_k f_k}\]- Parameters:
- asset_namesarray-like of shape (n_assets,)
Names for each asset (e.g., [“AAPL”, “GOOGL”, “MSFT”]).
- factor_namesarray-like of shape (n_factors,)
Names for each factor (e.g., [“Momentum”, “Value”, “Size”]).
- factor_familiesarray-like of shape (n_factors,), optional
Family/category for each factor (e.g., “Style”, “Industry”). If provided, enables family-level aggregation in the output.
- weightsarray-like of shape (n_assets,) or (n_observations, n_assets)
Portfolio weights. If 1D, the same weights are used for all observations (static). If 2D, time-varying weights are used.
- factor_returnsarray-like of shape (n_observations, n_factors)
Factor return time series.
- portfolio_returnsarray-like of shape (n_observations,)
Portfolio return time series.
- exposuresarray-like of shape (n_assets, n_factors) or (n_observations, n_assets, n_factors)
Asset-by-factor exposure (loading) values. If 2D, this is the static loading matrix used for all observations. If 3D, this is a time series of loading matrices following the as-of time-indexing convention (the function applies
exposure_laginternally and trims the returns and weights series accordingly).- exposure_lagint, default=1
Lag applied to time-varying exposures under the as-of time-indexing convention. The default value of
1aligns exposures at \(t-1\) with returns over \((t-1, t]\). Only affects 3D (time-varying) exposures.- idio_returnsarray-like of shape (n_observations, n_assets)
Idiosyncratic returns from the factor model regression. These are the residuals \(\varepsilon_{i,t}\) from the cross-sectional regression.
- idio_variancesarray-like of shape (n_observations, n_assets) or None, optional
Per-asset idiosyncratic (specific) variances \(\sigma^2_{\varepsilon,i,t}\). Required when
compute_uncertainty=True. NaN values are allowed and exclude the corresponding asset-observation pair from the uncertainty estimate.- regression_weightsarray-like of shape (n_observations, n_assets) or None, optional
Per-asset cross-sectional regression weights \(q_{i,t}\) used when estimating factor returns. Required when
compute_uncertainty=True. Must not contain NaN.- family_constraint_basisFamilyConstraintBasis or None, optional
When provided, the uncertainty estimator is computed in the reduced (full-rank) basis defined by the family-constraint change of coordinates. This avoids the singular Gram matrix that arises from collinear constrained families and produces well-conditioned standard errors. Only used when
compute_uncertainty=True.- annualization_factorfloat, default=252.0
Used to annualize expected returns, variances and volatilities. Use 1.0 to disable annualization. Common values: 252 for daily data, 12 for monthly data.
- compute_asset_breakdownsbool, default=True
If True, compute asset-level attribution (systematic/idiosyncratic decomposition). Set to False to skip asset attribution for faster computation.
- compute_uncertaintybool, default=False
If
True, compute attribution uncertainty (standard errors on the factor/idiosyncratic return split). Requires bothregression_weightsandidio_variances; raisesValueErrorif either is missing. IfFalse(default), uncertainty is not computed.
- Returns:
- attributionAttribution
The
Attributiondataclass containing component-level, factor-level and optionally asset-level attribution results.
See also
predicted_factor_attributionPredicted (ex-ante) factor model attribution.
Notes
When exposures are time-varying,
vol_contribcannot be exactly reproduced asexposure_mean * sigma(f) * rho(f, R_P)because the actual contribution is computed from the covariance of the exposure-weighted factor return series. The displayed statistics provide intuitive factor-level information while the contributions reflect the true realized attribution.NaN handling:
exposuresandidio_returnsmay contain NaN entries for assets that are inactive at a given date (delistings, not-yet-listed securities, trading holidays). These NaN values are replaced with 0 before any computation: portfolio weight for an inactive asset is zero, so its return contribution is economically zero.When
compute_uncertainty=True, NaN values inidio_variancesexclude the corresponding asset-observation pair from the uncertainty estimate by setting its effective regression weight to zero. This handles per-asset variance-estimator warmup, inactive assets and sparse histories without changing the attribution sample.factor_returns,portfolio_returns, andweightsmust not contain NaN; aValueErroris raised otherwise.Examples
>>> import numpy as np >>> from skfolio.attribution import realized_factor_attribution >>> # Simulated daily returns: 10 assets, 3 factors. >>> rng = np.random.default_rng(0) >>> factor_returns = rng.standard_normal((252, 3)) * 0.01 >>> loading_matrix = rng.uniform(0.5, 1.5, size=(10, 3)) >>> weights = np.full(10, 1.0 / 10) >>> residuals = rng.standard_normal((252, 10)) * 0.005 >>> asset_returns = factor_returns @ loading_matrix.T + residuals >>> portfolio_returns = asset_returns @ weights >>> asset_names = [f"Asset_{i}" for i in range(10)] >>> factor_names = ["Momentum", "Value", "Size"]
Compute attribution with fixed weights (annualized by default):
>>> attribution = realized_factor_attribution( ... factor_returns=factor_returns, ... portfolio_returns=portfolio_returns, ... exposures=loading_matrix, ... weights=weights, ... idio_returns=residuals, ... asset_names=asset_names, ... factor_names=factor_names, ... ) >>> print(f"Annualized volatility: {attribution.total.vol:.2%}") Annualized volatility: 26.16% >>> print(f"Factor contributions: {attribution.factors.vol_contrib}") Factor contributions: [0.07630795 0.11148071 0.07271759]
Simulate daily rebalancing with a different set of asset weights for each day:
>>> daily_weights = weights + rng.uniform(-0.02, 0.02, size=(252, 10)) >>> # Normalize each day's weights to sum to one. >>> daily_weights /= daily_weights.sum(axis=1, keepdims=True) >>> # Apply each day's weights to that day's asset returns. >>> portfolio_returns = (asset_returns * daily_weights).sum(axis=1) >>> attribution = realized_factor_attribution( ... factor_returns=factor_returns, ... portfolio_returns=portfolio_returns, ... exposures=loading_matrix, ... weights=daily_weights, ... idio_returns=residuals, ... asset_names=asset_names, ... factor_names=factor_names, ... )
Inspect the standard deviation of each factor exposure over time:
>>> print(attribution.factors.exposure_std) [0.0122634 0.00836653 0.01058102]