<a id="skfolio-attribution-realized-factor-attribution"></a>

# skfolio.attribution.realized_factor_attribution

<a id="skfolio.attribution.realized_factor_attribution"></a>

### 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)

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_names** *array-like of shape (n_assets,)*
  : Names for each asset (e.g., [“AAPL”, “GOOGL”, “MSFT”]).

  **factor_names** *array-like of shape (n_factors,)*
  : Names for each factor (e.g., [“Momentum”, “Value”, “Size”]).

  **factor_families** *array-like of shape (n_factors,), optional*
  : Family/category for each factor (e.g., “Style”, “Industry”).  If provided,
    enables family-level aggregation in the output.

  **weights** *array-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_returns** *array-like of shape (n_observations, n_factors)*
  : Factor return time series.

  **portfolio_returns** *array-like of shape (n_observations,)*
  : Portfolio return time series.

  **exposures** *array-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_lag** *int, 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_returns** *array-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_variances** *array-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_weights** *array-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_basis** *FamilyConstraintBasis 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_factor** *float, 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_breakdowns** *bool, default=True*
  : If True, compute asset-level attribution (systematic/idiosyncratic
    decomposition). Set to False to skip asset attribution for faster computation.

  **compute_uncertainty** *bool, 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:**
  **attribution** *Attribution*
  : The [`Attribution`](https://skfolio.org/generated/skfolio.attribution.Attribution.html.md#skfolio.attribution.Attribution) dataclass containing component-level,  factor-level
    and optionally asset-level attribution results.

#### SEE ALSO
[`predicted_factor_attribution`](https://skfolio.org/generated/skfolio.attribution.predicted_factor_attribution.html.md#skfolio.attribution.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

```pycon
>>> 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}")
```

