Clustering evaluation metrics.
Two kinds of measure live here, and mixing them up is the usual mistake.
External metrics compare a clustering against known ground-truth labels: adjusted_rand_score, rand_score, mutual_info_score, normalized_mutual_info_score, homogeneity_score, completeness_score, v_measure_score and fowlkes_mallows_score. They take (labels_true, labels_pred) and are invariant to how the clusters are named, so permuting labels changes nothing.
Internal metrics judge the clustering from the data geometry alone, with no ground truth: silhouette_score, silhouette_samples, davies_bouldin_score and calinski_harabasz_score. They take (X, labels) and are what you use to pick a number of clusters.
Two directions to watch. davies_bouldin_score is LOWER-is-better, unlike every other metric here. And rand_score and mutual_info_score are uncorrected, so they drift upward as the cluster count grows -- prefer adjusted_rand_score or normalized_mutual_info_score when comparing clusterings of different sizes.
>>> from tuiml.evaluation.metrics import adjusted_rand_score, v_measure_score
>>> true = [0, 0, 1, 1, 2, 2]
>>> pred = [1, 1, 0, 0, 2, 2] # same partition, different names
>>> adjusted_rand_score(true, pred)
1.0
>>> v_measure_score(true, pred)
1.0
Functions
Compute Adjusted Rand Index (ARI).
The Rand index corrected for chance, using the contingency table n_{ij} with row sums a_i and column sums b_j:
Subtracting the expected index makes 0.0 the score of random labelling, so unlike rand_score the value does not drift upward with the number of clusters.
Parameters
labels_true
labels_pred
Returns
score
>>> from tuiml.evaluation.metrics import adjusted_rand_score
>>> adjusted_rand_score([0, 0, 1, 1], [0, 0, 1, 1])
1.0
>>> round(adjusted_rand_score([0, 0, 1, 1], [0, 1, 0, 1]), 2)
-0.5
Compute Rand Index (RI).
The fraction of sample pairs that both labellings agree about, where a counts pairs together in both and b counts pairs apart in both:
Not corrected for chance: random labellings score well above 0. Use adjusted_rand_score when comparing across different cluster counts.
Parameters
labels_true
labels_pred
Returns
score
>>> from tuiml.evaluation.metrics import rand_score
>>> round(rand_score([0, 0, 1, 1], [0, 0, 1, 1]), 3)
0.333
>>> rand_score([0, 0, 1, 1], [0, 1, 0, 1])
0.0
Compute mean Silhouette Coefficient.
The mean silhouette over all samples. For one sample, with a(i) its mean distance to its own cluster and b(i) the mean distance to the nearest other cluster:
+1 means the sample sits well inside its cluster, 0 means it lies on a boundary, and negative means it is closer to a different cluster.
Parameters
X
labels
metric
'euclidean', 'manhattan', or 'cosine'.
Returns
score
>>> import numpy as np
>>> from tuiml.evaluation.metrics import silhouette_score
>>> X = np.array([[1, 2], [1, 4], [1, 0], [4, 2], [4, 4], [4, 0]])
>>> labels = np.array([0, 0, 0, 1, 1, 1])
>>> round(silhouette_score(X, labels), 3)
0.287
Compute Silhouette Coefficient for each sample.
Per-sample version of silhouette_score:
Useful for finding which individual points are badly clustered rather than only the overall average.
Parameters
X
labels
metric
'euclidean', 'manhattan', or 'cosine'.
Returns
scores
>>> import numpy as np
>>> from tuiml.evaluation.metrics import silhouette_samples
>>> X = np.array([[1, 2], [1, 4], [1, 0], [4, 2], [4, 4], [4, 0]])
>>> labels = np.array([0, 0, 0, 1, 1, 1])
>>> np.round(silhouette_samples(X, labels), 3)
array([0.412, 0.225, 0.225, 0.412, 0.225, 0.225])
Compute Davies-Bouldin Index.
Lower values indicate better clustering (minimum is 0).
Average over clusters of the worst-case similarity to any other cluster, where s_i is the mean distance from cluster i to its own centroid and d_{ij} the distance between centroids:
LOWER is better, and 0 is the best possible value: the opposite direction to most metrics in this module.
Parameters
X
labels
Returns
score
>>> import numpy as np
>>> from tuiml.evaluation.metrics import davies_bouldin_score
>>> X = np.array([[1, 2], [1, 4], [1, 0], [4, 2], [4, 4], [4, 0]])
>>> labels = np.array([0, 0, 0, 1, 1, 1])
>>> round(davies_bouldin_score(X, labels), 3)
0.889
Compute Calinski-Harabasz Index (Variance Ratio Criterion).
Higher values indicate better clustering.
Ratio of between-cluster to within-cluster dispersion, each corrected for its degrees of freedom:
Higher is better. The score is unbounded above, so it is meaningful for ranking candidate cluster counts on one dataset, not across datasets.
Parameters
X
labels
Returns
score
>>> import numpy as np
>>> from tuiml.evaluation.metrics import calinski_harabasz_score
>>> X = np.array([[1, 2], [1, 4], [1, 0], [4, 2], [4, 4], [4, 0]])
>>> labels = np.array([0, 0, 0, 1, 1, 1])
>>> round(calinski_harabasz_score(X, labels), 3)
3.375
Compute Mutual Information between two clusterings.
How much knowing the cluster tells you about the true class:
Measured in nats and unbounded above, which makes raw MI hard to compare; normalized_mutual_info_score rescales it to [0, 1].
Parameters
labels_true
labels_pred
Returns
score
>>> from tuiml.evaluation.metrics import mutual_info_score
>>> round(mutual_info_score([0, 0, 1, 1], [0, 0, 1, 1]), 3)
0.693
>>> mutual_info_score([0, 0, 1, 1], [0, 1, 0, 1])
0.0
Compute Normalized Mutual Information (NMI).
Mutual information rescaled by the entropies of the two labellings:
Bounded in [0, 1], reaching 1.0 exactly when the two labellings agree up to relabelling.
Parameters
labels_true
labels_pred
average_method
'arithmetic', 'geometric', 'min', or 'max', computed from the entropies of labels_true and labels_pred.
Returns
score
>>> from tuiml.evaluation.metrics import normalized_mutual_info_score
>>> round(normalized_mutual_info_score([0, 0, 1, 1], [0, 0, 1, 1]), 3)
1.0
>>> normalized_mutual_info_score([0, 0, 1, 1], [0, 1, 0, 1])
0.0
Compute V-measure (harmonic mean of homogeneity and completeness).
The harmonic mean of homogeneity h and completeness c:
Symmetric in the two labellings, and equal to normalized_mutual_info_score under arithmetic-mean normalization.
Parameters
labels_true
labels_pred
beta
Returns
score
>>> from tuiml.evaluation.metrics import v_measure_score
>>> round(v_measure_score([0, 0, 1, 1], [0, 0, 1, 1]), 3)
1.0
>>> v_measure_score([0, 0, 1, 1], [0, 1, 0, 1])
0.0
Compute homogeneity metric (each cluster contains only members of a single class).
Whether each cluster contains only members of a single class:
Splitting one true class across many clusters does not hurt this score -- that is what completeness_score measures.
Parameters
labels_true
labels_pred
Returns
score
>>> from tuiml.evaluation.metrics import homogeneity_score
>>> round(homogeneity_score([0, 0, 1, 1], [0, 0, 1, 1]), 3)
1.0
>>> homogeneity_score([0, 0, 1, 1], [0, 1, 0, 1])
0.0
Compute completeness metric (all members of a class are in the same cluster).
Whether all members of a class land in the same cluster:
The mirror image of homogeneity_score; putting everything in one cluster scores 1.0 here and poorly there.
Parameters
labels_true
labels_pred
Returns
score
>>> from tuiml.evaluation.metrics import completeness_score
>>> round(completeness_score([0, 0, 1, 1], [0, 0, 1, 1]), 3)
1.0
>>> completeness_score([0, 0, 1, 1], [0, 1, 0, 1])
0.0
Compute Fowlkes-Mallows Index.
The geometric mean of the pairwise precision and recall, counting pairs of samples placed in the same cluster:
Bounded in [0, 1]; unlike rand_score it ignores the true-negative pairs that dominate when there are many clusters.
Parameters
labels_true
labels_pred
Returns
score
>>> from tuiml.evaluation.metrics import fowlkes_mallows_score
>>> fowlkes_mallows_score([0, 0, 1, 1], [0, 0, 1, 1])
1.0
>>> fowlkes_mallows_score([0, 0, 1, 1], [0, 1, 0, 1])
0.0