Source code for mantispy.metrics._silhouette

"""Silhouette-based integration metrics, following scib's definitions."""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
import pandas as pd
from sklearn.metrics import silhouette_samples, silhouette_score

from mantispy.metrics._common import embedding, tidy

if TYPE_CHECKING:
    from anndata import AnnData


[docs] def silhouette_label(adata: AnnData, label_key: str, use_rep: str = "X_pca") -> pd.DataFrame: """How well separated the biological labels are, rescaled to ``[0, 1]``. Higher means tighter, better separated groups. """ labels = adata.obs[label_key].to_numpy() value = (silhouette_score(embedding(adata, use_rep), labels) + 1.0) / 2.0 return tidy("silhouette_label", use_rep, label_key, value)
[docs] def silhouette_batch(adata: AnnData, label_key: str, batch_key: str, use_rep: str = "X_pca") -> pd.DataFrame: """How well mixed the batches are within each biological label. Computed per label as ``1 - mean|silhouette over batch|`` and averaged over labels. Higher is better; a silhouette near zero means the batches are indistinguishable within that label. A label is skipped when mixing is undefined for it, which is when it has fewer than two batches or as many batches as rows (one well per plate on three plates, for example). The value is NaN when every label is skipped. """ values = embedding(adata, use_rep) labels = adata.obs[label_key].to_numpy() batches = adata.obs[batch_key].to_numpy() scores = [] for label in pd.unique(labels): rows = labels == label n_rows, n_batches = int(rows.sum()), len(np.unique(batches[rows])) if n_rows < 3 or not 2 <= n_batches <= n_rows - 1: continue scores.append(float(np.mean(1.0 - np.abs(silhouette_samples(values[rows], batches[rows]))))) return tidy("silhouette_batch", use_rep, batch_key, float(np.mean(scores)) if scores else np.nan)