Source code for clustering_mi.mutual_information

# Compute the mutual information and associated entropies
from __future__ import annotations

import logging

import numpy as np
from numpy.typing import ArrayLike

from clustering_mi import _core
from clustering_mi._input_output import _get_contingency_table

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def _stirling_mutual_information(contingency_table: ArrayLike) -> float:
    """
    Compute the Stirling approximated mutual information for the given contingency table.
    This is the mutual information of the corresponding probability distributions (times the number of objects).

    Parameters
    ----------
    contingency_table : ArrayLike
        Contingency table T as a 2D NumPy array where T[r][s] counts the number of objects with label r in the ground truth g and label s in the candidate c.

    Returns
    -------
    float
        Mutual information (base 2)
    """

    return float(_core.stirling_mutual_information(np.asarray(contingency_table)))


def _traditional_mutual_information(contingency_table: ArrayLike) -> float:
    """
    Compute the unreduced microcanonical mutual information and entropies of the ground truth and candidate labelings.

    Parameters
    ----------
    contingency_table : ArrayLike
        Contingency table T as a 2D NumPy array where T[r][s] counts the number of objects with label r in the ground truth g and label s in the candidate c.

    Returns
    -------
    float
        Mutual information (base 2).
    """

    return float(_core.traditional_mutual_information(np.asarray(contingency_table)))


def _adjusted_mutual_information(contingency_table: ArrayLike) -> float:
    """
    Compute the adjusted mutual information, which corrects the mutual information for chance (random permutations of the labels).

    Parameters
    ----------
    contingency_table : ArrayLike
        Contingency table T as a 2D NumPy array where T[r][s] counts the number of objects with label r in the ground truth g and label s in the candidate c.

    Returns
    -------
    float
        Adjusted mutual information (base 2).
    """

    return float(_core.adjusted_mutual_information(np.asarray(contingency_table)))


def _reduced_flat_mutual_information(contingency_table: ArrayLike) -> float:
    """
    Compute the reduced mutual information, using the flat reduction method of https://arxiv.org/pdf/1907.12581.

    Parameters
    ----------
    contingency_table : ArrayLike
        Contingency table T as a 2D NumPy array where T[r][s] counts the number of objects with label r in the ground truth g and label s in the candidate c.

    Returns
    -------
    float
        Reduced mutual information (base 2).
    """

    return float(_core.reduced_flat_mutual_information(np.asarray(contingency_table)))


def _H_ng_G_alpha(ng: ArrayLike, alpha: float) -> float:
    """
    Compute the entropy of a vector of group sizes given the concentration parameter alpha.

    Parameters
    ----------
    ng : list or np.ndarray
        Vector of group sizes.
    alpha : float
        Concentration parameter.

    Returns
    -------
    float
        Entropy of the group sizes. (base e).

    """
    return float(_core.H_ng_G_alpha(np.asarray(ng), float(alpha)))


def _H_ngc_G_nc_alpha(ngc: ArrayLike, alpha: float) -> float:
    """
    Compute the entropy of a contingency table given knowledge of the column sums and the concentration parameter alpha.

    Parameters
    ----------
    ngc : list or np.ndarray
        Vector of column sums.
    alpha : float
        Concentration parameter.

    Returns
    -------
    float
        Entropy of the contingency table. (base e).
    """

    return float(_core.H_ngc_G_nc_alpha(np.asarray(ngc), float(alpha)))


def _reduced_mutual_information(contingency_table: ArrayLike) -> float:
    """
    Compute the reduced mutual information, using the Dirichlet-multinomial reduction of https://arxiv.org/pdf/2405.05393.

    Parameters
    ----------
    contingency_table : ArrayLike
        Contingency table T as a 2D NumPy array where T[r][s] counts the number of objects with label r in the ground truth g and label s in the candidate c.

    Returns
    -------
    float
        Reduced mutual information (base 2).
    """

    return float(_core.reduced_mutual_information(np.asarray(contingency_table)))


[docs] def normalized_mutual_information( input_data_1: ArrayLike | str, input_data_2: ArrayLike | None = None, *, variation: str = "reduced", normalization: str = "second", ) -> ArrayLike: """ Compute the normalized mutual information between two labelings from a pair of lists, the name of a space separated file of labels, or a contingency table. Can specify the variation of mutual information and type of normalization. For the asymmetric (default) normalization, the result is reported as a fraction of the entropy of the second labeling, which is considered the ground truth. Raises AssertionError for invalid inputs. Parameters ---------- input_data_1 : ArrayLike or str First argument. This will either be a 2D array-like which specifies the contingency table whose columns are the first labeling and rows are the second labeling, or a string which is the path to a file containing a list of pairs of labels, or a 1-D array-like of labels. input_data_2 : ArrayLike, optional Second argument. This can only be a 1-D array-like of labels in the case where the first argument is also such a list. variation : str, optional Variation of mutual information to compute. Options are: - "stirling": Stirling's approximation of the traditional mutual information, equal to the mutual information of the corresponding probability distributions (times the number of objects). - "reduced" (default): Reduced mutual information (RMI), Dirichlet-multinomial reduction of https://arxiv.org/pdf/2405.05393 - "reduced_flat": Reduced mutual information (RMI), flat reduction of https://arxiv.org/pdf/1907.12581 - "adjusted": Adjusted mutual information (AMI), correcting for chance: https://jmlr.csail.mit.edu/papers/volume11/vinh10a/vinh10a.pdf - "traditional": Traditional mutual information (MI), microcanonical - "stirling": Stirling's approximation of the traditional mutual information, equal to the mutual information of the corresponding probability distributions (times the number of objects). normalization : str, optional Type of normalization to apply. Options are: - "second" (default): Asymmetric normalization, measures how much the first labeling tells us about the second, as a fraction of all there is to know about the second labeling. - "first": Asymmetric normalization, measures how much the second labeling tells us about the first, as a fraction of all there is to know about the first labeling. - "mean": Symmetric normalization by the arithmetic mean of the two entropies. - "min": Normalize by the minimum of the two entropies. - "max": Normalize by the maximum of the two entropies. - "geometric": Normalize by the geometric mean of the two entropies. - "none": No normalization, returns the mutual information in bits. Returns ------- float Normalized mutual information """ # Get the contingency table contingency_table = _get_contingency_table(input_data_1, input_data_2) # Make the candidate-candidate (labeling 1) and truth-truth (label 2) contingency tables nc = np.sum(contingency_table, axis=0) ng = np.sum(contingency_table, axis=1) ncc = np.diag(nc) # Candidate-candidate ngg = np.diag(ng) # Truth-truth # Compute the mutual information between each pair of labelings MI_c_g, MI_g_c, MI_c_c, MI_g_g = None, None, None, None # Values to be computed if variation == "stirling": MI_c_g = _stirling_mutual_information( contingency_table ) # Mutual information between candidate and ground truth MI_g_c = MI_c_g # Symmetric measure MI_c_c = _stirling_mutual_information( ncc ) # Mutual information between candidate and candidate MI_g_g = _stirling_mutual_information( ngg ) # Mutual information between ground truth and ground truth elif variation == "traditional": MI_c_g = _traditional_mutual_information(contingency_table) MI_g_c = MI_c_g # Symmetric measure MI_c_c = _traditional_mutual_information(ncc) MI_g_g = _traditional_mutual_information(ngg) elif variation == "adjusted": MI_c_g = _adjusted_mutual_information(contingency_table) MI_g_c = MI_c_g # Symmetric measure MI_c_c = _adjusted_mutual_information(ncc) MI_g_g = _adjusted_mutual_information(ngg) elif variation == "reduced": MI_c_g = _reduced_mutual_information(contingency_table) MI_g_c = _reduced_mutual_information( contingency_table.T ) # Transpose to get the ground truth as the first labeling MI_c_c = _reduced_mutual_information(ncc) MI_g_g = _reduced_mutual_information(ngg) elif variation == "reduced_flat": MI_c_g = _reduced_flat_mutual_information(contingency_table) MI_g_c = _reduced_flat_mutual_information(contingency_table.T) MI_c_c = _reduced_flat_mutual_information(ncc) MI_g_g = _reduced_flat_mutual_information(ngg) else: raise ValueError(f"Unknown variation type: {variation}") # Compute the normalized mutual information if normalization == "second": # Asymmetric normalization, measures how much the first labeling tells us about the second, as a fraction of all there is to know about the second labeling return MI_c_g / MI_g_g if MI_g_g > 0 else 0 if normalization == "first": # Asymmetric normalization, measures how much the second labeling tells us about the first, as a fraction of all there is to know about the first labeling return MI_g_c / MI_c_c if MI_c_c > 0 else 0 if ( normalization == "mean" ): # Note that the numerators of these symmetric measures are non-standard in order to account for asymmetries in the calculated MI_c_g vs MI_g_c return (MI_c_g + MI_g_c) / (MI_c_c + MI_g_g) if (MI_c_c + MI_g_g) > 0 else 0 if normalization == "min": return ( min(MI_c_g, MI_g_c) / min(MI_c_c, MI_g_g) if min(MI_c_c, MI_g_g) > 0 else 0 ) if normalization == "max": return ( max(MI_c_g, MI_g_c) / max(MI_c_c, MI_g_g) if max(MI_c_c, MI_g_g) > 0 else 0 ) if normalization == "geometric": return ( np.sqrt(MI_c_g * MI_g_c) / np.sqrt(MI_c_c * MI_g_g) if (MI_c_c * MI_g_g) > 0 else 0 ) if normalization == "none": return MI_c_g # Return the mutual information in bits without normalization (note that this may not be symmetric for the reduced measures) raise ValueError(f"Unknown normalization type: {normalization}")
[docs] def mutual_information( input_data_1: ArrayLike | str, input_data_2: ArrayLike | None = None, *, variation: str = "reduced", ) -> ArrayLike: """ Compute the mutual information between two labelings from a pair of lists, the name of a space separated file of labels, or a contingency table. Can specify the variation of mutual information to compute. Raises AssertionError for invalid inputs. Parameters ---------- input_data_1 : ArrayLike or str First argument. This will either be a 2D array-like which specifies the contingency table whose columns are the first labeling and rows are the second labeling, or a string which is the path to a file containing a list of pairs of labels, or a 1-D array-like of labels. input_data_2 : ArrayLike, optional Second argument. This can only be a 1-D array-like of labels in the case where the first argument is also such a list. variation : str, optional Variation of mutual information to compute. Options are: - "reduced" (default): Reduced mutual information (RMI), reduction of https://arxiv.org/pdf/2405.05393, note that this can be (slightly) asymmetric. - "reduced_flat": Reduced mutual information (RMI), flat reduction of https://arxiv.org/pdf/1907.12581 - "adjusted": Adjusted mutual information (AMI), correcting for chance: https://jmlr.csail.mit.edu/papers/volume11/vinh10a/vinh10a.pdf - "traditional": Traditional mutual information (MI), microcanonical - "stirling": Stirling's approximation of the traditional mutual information, equal to the mutual information of the corresponding probability distributions (times the number of objects). Returns ------- float Mutual information value in bits (base 2). """ return normalized_mutual_information( input_data_1, input_data_2, variation=variation, normalization="none", )