11
11
# Thierry Guillemot <[email protected] >
12
12
# Gregory Stupp <[email protected] >
13
13
# Joel Nothman <[email protected] >
14
+ # Arya McCarthy <[email protected] >
14
15
# License: BSD 3 clause
15
16
16
17
from __future__ import division
17
18
18
19
from math import log
20
+ import warnings
19
21
20
22
import numpy as np
21
23
from scipy import sparse as sp
@@ -59,6 +61,21 @@ def check_clusterings(labels_true, labels_pred):
59
61
return labels_true , labels_pred
60
62
61
63
64
+ def _generalized_average (U , V , average_method ):
65
+ """Return a particular mean of two numbers."""
66
+ if average_method == "min" :
67
+ return min (U , V )
68
+ elif average_method == "geometric" :
69
+ return np .sqrt (U * V )
70
+ elif average_method == "arithmetic" :
71
+ return np .mean ([U , V ])
72
+ elif average_method == "max" :
73
+ return max (U , V )
74
+ else :
75
+ raise ValueError ("'average_method' must be 'min', 'geometric', "
76
+ "'arithmetic', or 'max'" )
77
+
78
+
62
79
def contingency_matrix (labels_true , labels_pred , eps = None , sparse = False ):
63
80
"""Build a contingency matrix describing the relationship between labels.
64
81
@@ -245,7 +262,9 @@ def homogeneity_completeness_v_measure(labels_true, labels_pred):
245
262
246
263
V-Measure is furthermore symmetric: swapping ``labels_true`` and
247
264
``label_pred`` will give the same score. This does not hold for
248
- homogeneity and completeness.
265
+ homogeneity and completeness. V-Measure is identical to
266
+ :func:`normalized_mutual_info_score` with the arithmetic averaging
267
+ method.
249
268
250
269
Read more in the :ref:`User Guide <homogeneity_completeness>`.
251
270
@@ -444,7 +463,8 @@ def completeness_score(labels_true, labels_pred):
444
463
def v_measure_score (labels_true , labels_pred ):
445
464
"""V-measure cluster labeling given a ground truth.
446
465
447
- This score is identical to :func:`normalized_mutual_info_score`.
466
+ This score is identical to :func:`normalized_mutual_info_score` with
467
+ the ``'arithmetic'`` option for averaging.
448
468
449
469
The V-measure is the harmonic mean between homogeneity and completeness::
450
470
@@ -459,6 +479,7 @@ def v_measure_score(labels_true, labels_pred):
459
479
measure the agreement of two independent label assignments strategies
460
480
on the same dataset when the real ground truth is not known.
461
481
482
+
462
483
Read more in the :ref:`User Guide <homogeneity_completeness>`.
463
484
464
485
Parameters
@@ -485,6 +506,7 @@ def v_measure_score(labels_true, labels_pred):
485
506
--------
486
507
homogeneity_score
487
508
completeness_score
509
+ normalized_mutual_info_score
488
510
489
511
Examples
490
512
--------
@@ -617,7 +639,8 @@ def mutual_info_score(labels_true, labels_pred, contingency=None):
617
639
return mi .sum ()
618
640
619
641
620
- def adjusted_mutual_info_score (labels_true , labels_pred ):
642
+ def adjusted_mutual_info_score (labels_true , labels_pred ,
643
+ average_method = 'warn' ):
621
644
"""Adjusted Mutual Information between two clusterings.
622
645
623
646
Adjusted Mutual Information (AMI) is an adjustment of the Mutual
@@ -626,7 +649,7 @@ def adjusted_mutual_info_score(labels_true, labels_pred):
626
649
clusters, regardless of whether there is actually more information shared.
627
650
For two clusterings :math:`U` and :math:`V`, the AMI is given as::
628
651
629
- AMI(U, V) = [MI(U, V) - E(MI(U, V))] / [max (H(U), H(V)) - E(MI(U, V))]
652
+ AMI(U, V) = [MI(U, V) - E(MI(U, V))] / [avg (H(U), H(V)) - E(MI(U, V))]
630
653
631
654
This metric is independent of the absolute values of the labels:
632
655
a permutation of the class or cluster label values won't change the
@@ -650,9 +673,17 @@ def adjusted_mutual_info_score(labels_true, labels_pred):
650
673
labels_pred : array, shape = [n_samples]
651
674
A clustering of the data into disjoint subsets.
652
675
676
+ average_method : string, optional (default: 'warn')
677
+ How to compute the normalizer in the denominator. Possible options
678
+ are 'min', 'geometric', 'arithmetic', and 'max'.
679
+ If 'warn', 'max' will be used. The default will change to
680
+ 'arithmetic' in version 0.22.
681
+
682
+ .. versionadded:: 0.20
683
+
653
684
Returns
654
685
-------
655
- ami: float(upperlimited by 1.0)
686
+ ami: float (upperlimited by 1.0)
656
687
The AMI returns a value of 1 when the two partitions are identical
657
688
(ie perfectly matched). Random partitions (independent labellings) have
658
689
an expected AMI around 0 on average hence can be negative.
@@ -691,6 +722,12 @@ def adjusted_mutual_info_score(labels_true, labels_pred):
691
722
<https://en.wikipedia.org/wiki/Adjusted_Mutual_Information>`_
692
723
693
724
"""
725
+ if average_method == 'warn' :
726
+ warnings .warn ("The behavior of AMI will change in version 0.22. "
727
+ "To match the behavior of 'v_measure_score', AMI will "
728
+ "use average_method='arithmetic' by default." ,
729
+ FutureWarning )
730
+ average_method = 'max'
694
731
labels_true , labels_pred = check_clusterings (labels_true , labels_pred )
695
732
n_samples = labels_true .shape [0 ]
696
733
classes = np .unique (labels_true )
@@ -709,17 +746,29 @@ def adjusted_mutual_info_score(labels_true, labels_pred):
709
746
emi = expected_mutual_information (contingency , n_samples )
710
747
# Calculate entropy for each labeling
711
748
h_true , h_pred = entropy (labels_true ), entropy (labels_pred )
712
- ami = (mi - emi ) / (max (h_true , h_pred ) - emi )
749
+ normalizer = _generalized_average (h_true , h_pred , average_method )
750
+ denominator = normalizer - emi
751
+ # Avoid 0.0 / 0.0 when expectation equals maximum, i.e a perfect match.
752
+ # normalizer should always be >= emi, but because of floating-point
753
+ # representation, sometimes emi is slightly larger. Correct this
754
+ # by preserving the sign.
755
+ if denominator < 0 :
756
+ denominator = min (denominator , - np .finfo ('float64' ).eps )
757
+ else :
758
+ denominator = max (denominator , np .finfo ('float64' ).eps )
759
+ ami = (mi - emi ) / denominator
713
760
return ami
714
761
715
762
716
- def normalized_mutual_info_score (labels_true , labels_pred ):
763
+ def normalized_mutual_info_score (labels_true , labels_pred ,
764
+ average_method = 'warn' ):
717
765
"""Normalized Mutual Information between two clusterings.
718
766
719
767
Normalized Mutual Information (NMI) is an normalization of the Mutual
720
768
Information (MI) score to scale the results between 0 (no mutual
721
769
information) and 1 (perfect correlation). In this function, mutual
722
- information is normalized by ``sqrt(H(labels_true) * H(labels_pred))``.
770
+ information is normalized by some generalized mean of ``H(labels_true)``
771
+ and ``H(labels_pred))``, defined by the `average_method`.
723
772
724
773
This measure is not adjusted for chance. Therefore
725
774
:func:`adjusted_mustual_info_score` might be preferred.
@@ -743,13 +792,22 @@ def normalized_mutual_info_score(labels_true, labels_pred):
743
792
labels_pred : array, shape = [n_samples]
744
793
A clustering of the data into disjoint subsets.
745
794
795
+ average_method : string, optional (default: 'warn')
796
+ How to compute the normalizer in the denominator. Possible options
797
+ are 'min', 'geometric', 'arithmetic', and 'max'.
798
+ If 'warn', 'geometric' will be used. The default will change to
799
+ 'arithmetic' in version 0.22.
800
+
801
+ .. versionadded:: 0.20
802
+
746
803
Returns
747
804
-------
748
805
nmi : float
749
806
score between 0.0 and 1.0. 1.0 stands for perfectly complete labeling
750
807
751
808
See also
752
809
--------
810
+ v_measure_score: V-Measure (NMI with arithmetic mean option.)
753
811
adjusted_rand_score: Adjusted Rand Index
754
812
adjusted_mutual_info_score: Adjusted Mutual Information (adjusted
755
813
against chance)
@@ -773,6 +831,12 @@ def normalized_mutual_info_score(labels_true, labels_pred):
773
831
0.0
774
832
775
833
"""
834
+ if average_method == 'warn' :
835
+ warnings .warn ("The behavior of NMI will change in version 0.22. "
836
+ "To match the behavior of 'v_measure_score', NMI will "
837
+ "use average_method='arithmetic' by default." ,
838
+ FutureWarning )
839
+ average_method = 'geometric'
776
840
labels_true , labels_pred = check_clusterings (labels_true , labels_pred )
777
841
classes = np .unique (labels_true )
778
842
clusters = np .unique (labels_pred )
@@ -789,7 +853,10 @@ def normalized_mutual_info_score(labels_true, labels_pred):
789
853
# Calculate the expected value for the mutual information
790
854
# Calculate entropy for each labeling
791
855
h_true , h_pred = entropy (labels_true ), entropy (labels_pred )
792
- nmi = mi / max (np .sqrt (h_true * h_pred ), 1e-10 )
856
+ normalizer = _generalized_average (h_true , h_pred , average_method )
857
+ # Avoid 0.0 / 0.0 when either entropy is zero.
858
+ normalizer = max (normalizer , np .finfo ('float64' ).eps )
859
+ nmi = mi / normalizer
793
860
return nmi
794
861
795
862
0 commit comments