Source code for skfolio.metrics._scorer

"""Scorer module"""

# Copyright (c) 2023
# Author: Hugo Delatte <delatte.hugo@gmail.com>
# License: BSD 3 clause
# Implementation derived from:
# scikit-portfolio, Copyright (c) 2022, Carlo Nicolini, Licensed under MIT Licence.
# scikit-learn, Copyright (c) 2007-2010 David Cournapeau, Fabian Pedregosa, Olivier
# Grisel Licensed under BSD 3 clause.

from collections.abc import Callable

import numpy.typing as npt

import skfolio.typing as skt
from skfolio.optimization import BaseOptimization
from skfolio.portfolio import Portfolio


class _PortfolioScorer:
    """Portfolio Scorer wrapper"""

    def __init__(self, score_func: Callable, sign: int, kwargs: dict):
        self._score_func = score_func
        self._kwargs = kwargs
        self._sign = sign

    def __repr__(self) -> str:
        """String representation of the `PortfolioScorer`."""
        kwargs_string = "".join([f", {k}={v}" for k, v in self._kwargs.items()])
        return (
            f"make_scorer({self._score_func.__name__}"
            f"{'' if self._sign > 0 else ', greater_is_better=False'}"
            f"{kwargs_string})"
        )

    def __call__(self, estimator: BaseOptimization, X: npt.ArrayLike) -> float:
        """Compute the score of the estimator prediction on X.

        Parameters
        ----------
        estimator : BaseOptimization
            Trained estimator to use for scoring.

        X : array-like of shape (n_observations, n_assets)
            Test data that will be fed to estimator.predict.

        Returns
        -------
        score : float
            Score of the estimator prediction on X.
        """
        pred = estimator.predict(X)
        return self._sign * self._score_func(pred, **self._kwargs)


[docs] def make_scorer( score_func: skt.Measure | Callable, greater_is_better: bool | None = None, **kwargs, ) -> Callable: """Make a scorer from a :ref:`measure <measures_ref>` or from a custom score function. This is a modified version from `scikit-learn` `make_scorer` for enhanced functionalities with `Portfolio` objects. This factory function wraps scoring functions for use in `sklearn.model_selection.GridSearchCV` and `sklearn.model_selection.cross_val_score`. Parameters ---------- score_func : Measure | callable If `score_func` is a :ref:`measure <measures_ref>`, we return the measure of the predicted :class:`~skfolio.portfolio.Portfolio` times `1` or `-1` depending on the `greater_is_better` parameter. Otherwise, `score_func` must be a score function (or loss function) with signature `score_func(pred, **kwargs)`. The argument `pred` is the predicted :class:`~skfolio.portfolio.Portfolio`. Note that you can convert this portfolio object into a numpy array of price returns with `np.asarray(pred)`. greater_is_better : bool, optional If this is set to True, `score_func` is a score function (default) meaning high is good, otherwise it is a loss function, meaning low is good. In the latter case, the scorer object will sign-flip the outcome of the `score_func`. The default (`None`) is to use: * If `score_func` is a :ref:`measure <measures_ref>`: * True for `PerfMeasure` and `RationMeasure` * False for `RiskMeasure` and `ExtraRiskMeasure`. * Otherwise, True. **kwargs : additional arguments Additional parameters to be passed to score_func. Returns ------- scorer : callable Callable object that returns a scalar score. """ if callable(score_func): if greater_is_better is None: greater_is_better = True else: measure = score_func if not isinstance(measure, skt.Measure): raise TypeError("`score_func` must be a callable or a measure") if greater_is_better is None: if measure.is_perf or measure.is_ratio: greater_is_better = True else: greater_is_better = False def score_func(pred: Portfolio) -> float: """Score function""" return getattr(pred, measure.value) sign = 1 if greater_is_better else -1 return _PortfolioScorer(score_func, sign, kwargs)