Source code for skfolio.distance._mutual_information

"""Mutual information estimator."""

# Copyright (c) 2023-2026
# Author: Hugo Delatte <hugo.delatte@skfoliolabs.com>
# SPDX-License-Identifier: BSD-3-Clause

from __future__ import annotations

import numpy as np
import scipy.stats as sct
import sklearn.metrics as skmc
import sklearn.utils.validation as skv

from skfolio.distance._base import BaseDistance
from skfolio.typing import ArrayLike
from skfolio.utils.stats import NBinsMethod, n_bins_freedman, n_bins_knuth


[docs] class MutualInformation(BaseDistance): r"""Mutual Information estimator. In information theory, the mutual information is a measure of the mutual dependence between variables. The related distance metric is called the variation of information. For two random variables X and Y, the mutual information I(X,Y) is defined as: .. math:: I(X,Y) = H(X) + H(Y) - H(X,Y) with H(X) and H(Y) the marginal entropies and H(X,Y) the joint entropy. The related distance metric known as the variation of information is defined as: .. math:: d(X,Y) = H(X,Y) - I(X,Y) = H(X) + H(Y) - 2 \times I(X,Y) and its normalization as: .. math:: D(X,Y) = \frac{d(X,Y)}{H(X,Y)} = \frac{H(X) + H(Y) - 2 \times I(X,Y)}{H(X) + H(Y) - I(X,Y)} Parameters ---------- n_bins_method : NBinsMethod, default=NBinsMethod.FREEDMAN Method to compute the number of bins for the contingency matrix estimation used for the computation of the mutual information. Possible values are: * FREEDMAN (`default`) * KNUTH n_bins : int, optional Instead of using `n_bins_method`, you can directly specify the number of bins with `n_bins`. normalize : bool, default=True If this is set to True, the variation of information is normalized. The default is `True`. Attributes ---------- codependence_ : ndarray of shape (n_assets, n_assets) Codependence matrix. distance_ : ndarray of shape (n_assets, n_assets) Distance matrix. n_features_in_ : int Number of assets seen during `fit`. feature_names_in_ : ndarray of shape (`n_features_in_`,) Names of features seen during `fit`. Defined only when `X` has feature names that are all strings. """ def __init__( self, n_bins_method: NBinsMethod = NBinsMethod.FREEDMAN, n_bins: int | None = None, normalize: bool = True, ) -> None: self.n_bins_method = n_bins_method self.n_bins = n_bins self.normalize = normalize
[docs] def fit(self, X: ArrayLike, y: None = None) -> MutualInformation: """Fit the Mutual Information estimator. Parameters ---------- X : array-like of shape (n_observations, n_assets) Price returns of the assets. y : Ignored Not used, present for API consistency by convention. Returns ------- self : MutualInformation Fitted estimator. """ X = skv.validate_data(self, X) n_assets = X.shape[1] if self.n_bins is None: match self.n_bins_method: case NBinsMethod.FREEDMAN: n_bins_func = n_bins_freedman case NBinsMethod.KNUTH: n_bins_func = n_bins_knuth case _: raise ValueError(f"n_bins_method {self.n_bins_method} is not valid") n_bins_list = [n_bins_func(x=X[:, i]) for i in range(n_assets)] else: n_bins_list = [self.n_bins] * n_assets corr = np.full((n_assets, n_assets), np.nan) dist = corr.copy() for i, j in zip(*np.triu_indices(n_assets), strict=True): n_bins = max(n_bins_list[i], n_bins_list[j]) x_i = X[:, i] x_j = X[:, j] contingency = np.histogram2d(x_i, x_j, bins=n_bins)[0] mutual_information = skmc.mutual_info_score( None, None, contingency=contingency ) entropy_x = sct.entropy(np.histogram(x_i, n_bins)[0]) entropy_y = sct.entropy(np.histogram(x_j, n_bins)[0]) if self.normalize: corr[i, j] = mutual_information / min(entropy_x, entropy_y) dist[i, j] = max( 0.0, (entropy_x + entropy_y - 2 * mutual_information) / (entropy_x + entropy_y - mutual_information), ) else: corr[i, j] = mutual_information dist[i, j] = max(0.0, entropy_x + entropy_y - 2 * mutual_information) corr[j, i] = corr[i, j] dist[j, i] = dist[i, j] self.codependence_ = corr self.distance_ = dist return self