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_lag internally 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 1 aligns 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 both regression_weights and idio_variances; raises ValueError if either is missing. If False (default), uncertainty is not computed.

Returns:
attributionAttribution

The Attribution dataclass containing component-level, factor-level and optionally asset-level attribution results.

See also

predicted_factor_attribution

Predicted (ex-ante) factor model attribution.

Notes

When exposures are time-varying, vol_contrib cannot be exactly reproduced as exposure_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:

exposures and idio_returns may 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 in idio_variances exclude 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, and weights must not contain NaN; a ValueError is 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}")