"""Derived factor exposure computed from another factor's exposure."""
# Copyright (c) 2023-2026
# Author: Hugo Delatte <hugo.delatte@skfoliolabs.com>
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
from collections.abc import Callable
import numpy as np
import skfolio.typing as skt
from skfolio._constants import _BENCHMARK_WEIGHTS, _PASSTHROUGH
from skfolio.containers import AssetPanel
from skfolio.factor_exposure._base import BaseFactorExposure
from skfolio.preprocessing import BaseCSTransformer, CSStandardScaler, CSWinsorizer
from skfolio.typing import FloatArray
from skfolio.utils.tools import check_estimator
from skfolio.utils.validation import validate_asset_panel
__all__ = ["DerivedFactor"]
[docs]
class DerivedFactor(BaseFactorExposure, stateless=True):
"""Factor exposure derived from another factor's computed exposure.
The derived exposure is computed by applying `func` to the source factor's
exposure, then optionally applying outlier and scoring transformations.
Parameters
----------
source : str
Name of the source factor whose exposure will be transformed. The source factor
must be defined in the factors list of `CharacteristicsFactorModel`. Dependency
ordering is handled automatically via topological sorting.
func : Callable[[np.ndarray], np.ndarray]
Function to apply to the source exposure. Receives a 2D array of shape
(n_observations, n_assets) and should return an array of the same shape.
The source exposure is passed directly. If `func` uses in-place operations,
it should copy the input first unless mutating the source exposure is intended.
family : str, default="style"
The factor family this exposure belongs to (e.g., "market", "style", "industry",
"country"). Factor families group related factors for basket-neutral constraints,
neutralization, attribution and reporting. The default is `"style"`.
outlier_transformer : BaseCSTransformer or "passthrough" or None, default="passthrough"
Cross-sectional transformer for outlier handling applied after `func`. If None,
defaults to `CSWinsorizer()`. Use "passthrough" to skip.
scoring_transformer : BaseCSTransformer or "passthrough", optional
Cross-sectional transformer for scoring applied after outlier handling.
If None, defaults to `CSStandardScaler()`. Use "passthrough" to skip.
transform_by_group : str, optional
Name of a categorical characteristic in the AssetPanel to use for group-wise
transformations. If provided, outlier and scoring transformations are applied
within each group separately.
Attributes
----------
outlier_transformer_ : BaseCSTransformer or str
The fitted outlier transformer.
scoring_transformer_ : BaseCSTransformer or str
The fitted scoring transformer.
n_assets_ : int
Number of assets seen during fitting.
asset_names_ : ndarray of shape (n_assets,)
Asset names seen during fitting.
Examples
--------
>>> from skfolio.factor_exposure import DerivedFactor, FixedWeightedFactor
>>> from skfolio.descriptor import LogMarketCap
>>> from skfolio.prior import CharacteristicsFactorModel
>>>
>>> # Non linear size factor
>>> factors = [
... ("size", FixedWeightedFactor(descriptors=[("log_mcap", LogMarketCap())])),
... ("non_linear_size", DerivedFactor(source="size", func=lambda x: x**3)),
... ]
>>>
>>> # Orthogonalize non_linear_size vs size
>>> model = CharacteristicsFactorModel(
... factors=factors,
... neutralize_against={"non_linear_size": ["size"]},
... )
"""
outlier_transformer_: BaseCSTransformer | str
scoring_transformer_: BaseCSTransformer | str
def __init__(
self,
*,
source: str,
func: Callable[[FloatArray], FloatArray],
family: str = "style",
outlier_transformer: skt.CSTransformer = "passthrough",
scoring_transformer: skt.CSTransformer = None,
transform_by_group: str | None = None,
):
super().__init__(family=family)
self.source = source
self.func = func
self.outlier_transformer = outlier_transformer
self.scoring_transformer = scoring_transformer
self.transform_by_group = transform_by_group