Source code for skfolio.attribution._model._asset_by_factor_contribution
"""Asset-by-factor contribution dataclass."""
# Copyright (c) 2023-2026
# Author: Hugo Delatte <hugo.delatte@skfoliolabs.com>
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
from dataclasses import dataclass
import pandas as pd
from skfolio.attribution._utils import _format_percent
from skfolio.typing import FloatArray, StrArray
__all__ = ["AssetByFactorContribution"]
[docs]
@dataclass(frozen=True)
class AssetByFactorContribution:
r"""Asset-by-factor contribution breakdown.
Breaks down factor contributions by asset. Each cell is the contribution of one
asset to one factor's total contribution.
Summing over assets gives per-factor contributions. Summing over factors gives each
asset's systematic contribution.
For single-point attribution, arrays have shape `(n_assets, n_factors)`. For rolling
attribution, arrays have shape `(n_windows, n_assets, n_factors)`.
Attributes
----------
asset_names : ndarray of shape (n_assets,)
Asset names.
factor_names : ndarray of shape (n_factors,)
Factor names.
vol_contrib : ndarray of shape (n_assets, n_factors) or (n_windows, n_assets, n_factors)
Volatility contribution for each asset-factor pair.
mu_contrib : ndarray of shape (n_assets, n_factors) or (n_windows, n_assets, n_factors)
Return contribution for each asset-factor pair.
"""
asset_names: StrArray
factor_names: StrArray
vol_contrib: FloatArray
mu_contrib: FloatArray
def _to_df(
self,
metric: str = "vol_contrib",
formatted: bool = False,
observation_idx: int | None = None,
) -> pd.DataFrame:
"""Return the selected asset-by-factor contribution as a DataFrame.
Parameters
----------
metric : str, default="vol_contrib"
Contribution metric to display: "vol_contrib" or "mu_contrib".
formatted : bool, default=False
If True, format values as percentages.
observation_idx : int, optional
Observation index. Required for rolling attribution.
Returns
-------
df : DataFrame
Matrix with assets as rows and factors as columns.
"""
if metric not in ("vol_contrib", "mu_contrib"):
raise ValueError("`metric` must be 'vol_contrib' or 'mu_contrib'.")
data = getattr(self, metric)
if data.ndim == 3:
if observation_idx is None:
raise ValueError(
"For rolling attribution, must specify `observation_idx`."
)
if not 0 <= observation_idx < data.shape[0]:
raise IndexError(
f"`observation_idx` {observation_idx} is out of range "
f"[0, {data.shape[0]})."
)
data = data[observation_idx]
df = pd.DataFrame(data, index=self.asset_names, columns=self.factor_names)
df.index.name = "Asset"
if formatted:
df = df.map(_format_percent)
return df