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
>>> from skfolio.attribution import realized_factor_attribution >>> import numpy as np >>> >>> # Static exposures and weights >>> attribution = realized_factor_attribution( ... factor_returns=factor_returns, # (252, 3) ... portfolio_returns=portfolio_returns, # (252,) ... exposures=loading_matrix, # (10, 3) ... weights=weights, # (10,) ... idio_returns=residuals, # (252, 10) ... factor_names=["Momentum", "Value", "Size"], ... ) >>> print(f"Total volatility: {attribution.total.vol:.2%}") >>> print(f"Factor contributions: {attribution.factors.vol_contrib}") >>> >>> # Time-varying weights (e.g., from rebalancing) >>> attribution = realized_factor_attribution( ... factor_returns=factor_returns, ... portfolio_returns=portfolio_returns, ... exposures=loading_matrix, ... weights=daily_weights, # (252, 10) ... idio_returns=residuals, ... factor_names=["Momentum", "Value", "Size"], ... ) >>> print(f"Exposure std (shows position dynamism): {attribution.factors.exposure_std}")