Skip to content

cca_zoo.metrics

Metrics for evaluating fitted multiview CCA models. Functions take already-computed arrays (latent scores, loadings, a correlation matrix) rather than a fitted model, the same convention sklearn.metrics uses.


pairwise_correlations

pairwise_correlations(
    transformed: Sequence[ArrayLike],
) -> np.ndarray

Pearson correlation between every pair of views' latent scores.

Parameters:

Name Type Description Default
transformed Sequence[ArrayLike]

Scores of shape (n_samples, n_components), one per view.

required

Returns:

Type Description
ndarray

Shape (n_views, n_views, n_components); entry [i, j, d]

ndarray

correlates the d-th scores of views i and j.

Examples:

>>> import numpy as np
>>> from cca_zoo.metrics import pairwise_correlations
>>> rng = np.random.default_rng(0)
>>> t1 = rng.standard_normal((20, 1))
>>> t2 = 0.8 * t1 + 0.2 * rng.standard_normal((20, 1))
>>> round(float(pairwise_correlations([t1, t2])[0, 1, 0]), 2)
0.98
Source code in cca_zoo/metrics/_correlation.py
def pairwise_correlations(transformed: Sequence[ArrayLike]) -> np.ndarray:
    """Pearson correlation between every pair of views' latent scores.

    Args:
        transformed: Scores of shape (n_samples, n_components), one per view.

    Returns:
        Shape (n_views, n_views, n_components); entry ``[i, j, d]``
        correlates the d-th scores of views i and j.

    Examples:
        >>> import numpy as np
        >>> from cca_zoo.metrics import pairwise_correlations
        >>> rng = np.random.default_rng(0)
        >>> t1 = rng.standard_normal((20, 1))
        >>> t2 = 0.8 * t1 + 0.2 * rng.standard_normal((20, 1))
        >>> round(float(pairwise_correlations([t1, t2])[0, 1, 0]), 2)
        0.98
    """
    T = np.stack([np.asarray(t) for t in transformed], axis=0)
    T = T - T.mean(axis=1, keepdims=True)
    norms = np.sqrt((T**2).sum(axis=1, keepdims=True))
    T_norm = T / np.where(norms > 1e-12, norms, 1.0)
    corrs: np.ndarray = np.einsum("isd,jsd->ijd", T_norm, T_norm)
    return corrs

average_pairwise_correlations

average_pairwise_correlations(
    correlations: ArrayLike,
) -> np.ndarray

Mean correlation over pairs of distinct views, per dimension.

Parameters:

Name Type Description Default
correlations ArrayLike

Output of :func:pairwise_correlations.

required

Returns:

Type Description
ndarray

Shape (n_components,).

Source code in cca_zoo/metrics/_correlation.py
def average_pairwise_correlations(correlations: ArrayLike) -> np.ndarray:
    """Mean correlation over pairs of distinct views, per dimension.

    Args:
        correlations: Output of :func:`pairwise_correlations`.

    Returns:
        Shape (n_components,).
    """
    corrs = np.asarray(correlations)
    n_views = corrs.shape[0]
    off_diag_sum: np.ndarray = corrs.sum(axis=(0, 1)) - sum(
        corrs[i, i, :] for i in range(n_views)
    )
    n_pairs = n_views * (n_views - 1)
    result: np.ndarray = off_diag_sum / n_pairs
    return result

factor_loadings

factor_loadings(
    views: Sequence[ArrayLike],
    transformed: Sequence[ArrayLike],
) -> list[np.ndarray]

Correlation of each feature with its own view's latent scores.

Parameters:

Name Type Description Default
views Sequence[ArrayLike]

Arrays of shape (n_samples, n_features_i), one per view.

required
transformed Sequence[ArrayLike]

Scores of shape (n_samples, n_components), one per view.

required

Returns:

Type Description
list[ndarray]

One array of shape (n_features_i, n_components) per view.

Examples:

>>> import numpy as np
>>> from cca_zoo.metrics import factor_loadings
>>> rng = np.random.default_rng(0)
>>> t1 = rng.standard_normal((20, 1))
>>> view1 = np.column_stack(
...     [t1[:, 0] + 0.3 * rng.standard_normal(20), rng.standard_normal(20)]
... )
>>> round(float(factor_loadings([view1], [t1])[0][0, 0]), 2)
0.97
Source code in cca_zoo/metrics/_correlation.py
def factor_loadings(
    views: Sequence[ArrayLike], transformed: Sequence[ArrayLike]
) -> list[np.ndarray]:
    """Correlation of each feature with its own view's latent scores.

    Args:
        views: Arrays of shape (n_samples, n_features_i), one per view.
        transformed: Scores of shape (n_samples, n_components), one per view.

    Returns:
        One array of shape (n_features_i, n_components) per view.

    Examples:
        >>> import numpy as np
        >>> from cca_zoo.metrics import factor_loadings
        >>> rng = np.random.default_rng(0)
        >>> t1 = rng.standard_normal((20, 1))
        >>> view1 = np.column_stack(
        ...     [t1[:, 0] + 0.3 * rng.standard_normal(20), rng.standard_normal(20)]
        ... )
        >>> round(float(factor_loadings([view1], [t1])[0][0, 0]), 2)
        0.97
    """
    loadings = []
    for view, variate in zip(views, transformed):
        v = np.asarray(view)
        t = np.asarray(variate)
        v_c = v - v.mean(axis=0)
        t_c = t - t.mean(axis=0)
        cov = v_c.T @ t_c / (v.shape[0] - 1)
        std_v = np.maximum(v_c.std(axis=0, ddof=1), 1e-12)
        std_t = np.maximum(t_c.std(axis=0, ddof=1), 1e-12)
        loadings.append(cov / np.outer(std_v, std_t))
    return loadings

adequacy_coefficient

adequacy_coefficient(
    loadings: Sequence[ArrayLike],
) -> list[np.ndarray]

Variance of each view extracted by its own canonical variates.

The mean squared factor loading per dimension (Cramer & Nicewander, 1979).

Parameters:

Name Type Description Default
loadings Sequence[ArrayLike]

Loadings of shape (n_features_i, n_components), one per view, as from :func:~cca_zoo.metrics.factor_loadings.

required

Returns:

Type Description
list[ndarray]

One array of shape (n_components,) per view.

Examples:

>>> import numpy as np
>>> from cca_zoo.metrics import adequacy_coefficient, factor_loadings
>>> rng = np.random.default_rng(0)
>>> t1 = rng.standard_normal((20, 1))
>>> view1 = np.column_stack(
...     [t1[:, 0] + 0.3 * rng.standard_normal(20), rng.standard_normal(20)]
... )
>>> adequacy = adequacy_coefficient(factor_loadings([view1], [t1]))
>>> round(float(adequacy[0][0]), 2)
0.48
Source code in cca_zoo/metrics/_redundancy.py
def adequacy_coefficient(loadings: Sequence[ArrayLike]) -> list[np.ndarray]:
    """Variance of each view extracted by its own canonical variates.

    The mean squared factor loading per dimension (Cramer & Nicewander, 1979).

    Args:
        loadings: Loadings of shape (n_features_i, n_components), one per
            view, as from :func:`~cca_zoo.metrics.factor_loadings`.

    Returns:
        One array of shape (n_components,) per view.

    Examples:
        >>> import numpy as np
        >>> from cca_zoo.metrics import adequacy_coefficient, factor_loadings
        >>> rng = np.random.default_rng(0)
        >>> t1 = rng.standard_normal((20, 1))
        >>> view1 = np.column_stack(
        ...     [t1[:, 0] + 0.3 * rng.standard_normal(20), rng.standard_normal(20)]
        ... )
        >>> adequacy = adequacy_coefficient(factor_loadings([view1], [t1]))
        >>> round(float(adequacy[0][0]), 2)
        0.48
    """
    return [np.mean(np.asarray(loading) ** 2, axis=0) for loading in loadings]

redundancy_index

redundancy_index(
    loadings: Sequence[ArrayLike], correlations: ArrayLike
) -> np.ndarray

Stewart-Love redundancy: variance of view i explained through view j.

View i's adequacy times the squared canonical correlation between the views' variates, per dimension. Asymmetric in i and j.

Parameters:

Name Type Description Default
loadings Sequence[ArrayLike]

Loadings of shape (n_features_i, n_components), one per view, as from :func:~cca_zoo.metrics.factor_loadings.

required
correlations ArrayLike

Shape (n_views, n_views, n_components), as from :func:~cca_zoo.metrics.pairwise_correlations.

required

Returns:

Type Description
ndarray

Shape (n_views, n_views, n_components); the diagonal is each view's

ndarray

adequacy.

Examples:

>>> import numpy as np
>>> from cca_zoo.metrics import (
...     factor_loadings,
...     pairwise_correlations,
...     redundancy_index,
... )
>>> rng = np.random.default_rng(0)
>>> t1 = rng.standard_normal((20, 1))
>>> t2 = 0.8 * t1 + 0.2 * rng.standard_normal((20, 1))
>>> view1 = np.column_stack(
...     [t1[:, 0] + 0.1 * rng.standard_normal(20), rng.standard_normal(20)]
... )
>>> view2 = np.column_stack(
...     [t2[:, 0] + 0.1 * rng.standard_normal(20), rng.standard_normal(20)]
... )
>>> redundancy = redundancy_index(
...     factor_loadings([view1, view2], [t1, t2]),
...     pairwise_correlations([t1, t2]),
... )
>>> round(float(redundancy[0, 1, 0]), 2)
0.52
Source code in cca_zoo/metrics/_redundancy.py
def redundancy_index(
    loadings: Sequence[ArrayLike], correlations: ArrayLike
) -> np.ndarray:
    """Stewart-Love redundancy: variance of view i explained through view j.

    View i's adequacy times the squared canonical correlation between the
    views' variates, per dimension. Asymmetric in ``i`` and ``j``.

    Args:
        loadings: Loadings of shape (n_features_i, n_components), one per
            view, as from :func:`~cca_zoo.metrics.factor_loadings`.
        correlations: Shape (n_views, n_views, n_components), as from
            :func:`~cca_zoo.metrics.pairwise_correlations`.

    Returns:
        Shape (n_views, n_views, n_components); the diagonal is each view's
        adequacy.

    Examples:
        >>> import numpy as np
        >>> from cca_zoo.metrics import (
        ...     factor_loadings,
        ...     pairwise_correlations,
        ...     redundancy_index,
        ... )
        >>> rng = np.random.default_rng(0)
        >>> t1 = rng.standard_normal((20, 1))
        >>> t2 = 0.8 * t1 + 0.2 * rng.standard_normal((20, 1))
        >>> view1 = np.column_stack(
        ...     [t1[:, 0] + 0.1 * rng.standard_normal(20), rng.standard_normal(20)]
        ... )
        >>> view2 = np.column_stack(
        ...     [t2[:, 0] + 0.1 * rng.standard_normal(20), rng.standard_normal(20)]
        ... )
        >>> redundancy = redundancy_index(
        ...     factor_loadings([view1, view2], [t1, t2]),
        ...     pairwise_correlations([t1, t2]),
        ... )
        >>> round(float(redundancy[0, 1, 0]), 2)
        0.52
    """
    adequacy = np.stack(adequacy_coefficient(loadings), axis=0)  # (n_views, k)
    corrs = np.asarray(correlations)
    result: np.ndarray = adequacy[:, np.newaxis, :] * corrs**2
    return result

total_redundancy

total_redundancy(redundancy: ArrayLike) -> np.ndarray

Redundancy summed over dimensions, shape (n_views, n_views).

Parameters:

Name Type Description Default
redundancy ArrayLike

Output of :func:redundancy_index.

required

Returns:

Type Description
ndarray

Entry [i, j] is the variance of view i explained through view j.

Source code in cca_zoo/metrics/_redundancy.py
def total_redundancy(redundancy: ArrayLike) -> np.ndarray:
    """Redundancy summed over dimensions, shape (n_views, n_views).

    Args:
        redundancy: Output of :func:`redundancy_index`.

    Returns:
        Entry ``[i, j]`` is the variance of view i explained through view j.
    """
    result: np.ndarray = np.asarray(redundancy).sum(axis=-1)
    return result