Repository: DistrictDataLabs/yellowbrick

Issue:
Add support metric to ClassificationReport
Currently, the ClassificationReport omits support because it is difficult to put into the heatmap scale of (0.0, 1.0). We should still include it, however and _color_ it as the percent of the total number of records, while _labeling_ it with the actual support number. 


Discussion:

Resolving patch:
diff --git a/yellowbrick/classifier/classification_report.py b/yellowbrick/classifier/classification_report.py
--- a/yellowbrick/classifier/classification_report.py
+++ b/yellowbrick/classifier/classification_report.py
@@ -4,6 +4,7 @@
 # Author:   Rebecca Bilbro <rbilbro@districtdatalabs.com>
 # Author:   Benjamin Bengfort <bbengfort@districtdatalabs.com>
 # Author:   Neal Humphrey
+# Author:   Allyssa Riley
 # Created:  Wed May 18 12:39:40 2016 -0400
 #
 # Copyright (C) 2017 District Data Labs
@@ -14,11 +15,12 @@
 """
 Visual classification report for classifier scoring.
 """
-
+ 
 ##########################################################################
 ## Imports
 ##########################################################################
 
+from __future__ import division
 import numpy as np
 import matplotlib.pyplot as plt
 
@@ -36,12 +38,12 @@
 
 CMAP_UNDERCOLOR = 'w'
 CMAP_OVERCOLOR = '#2a7d4f'
-SCORES_KEYS = ('precision', 'recall', 'f1')
+SCORES_KEYS = ('precision', 'recall', 'f1', 'support')
 
 
 class ClassificationReport(ClassificationScoreVisualizer):
     """
-    Classification report that shows the precision, recall, and F1 scores
+    Classification report that shows the precision, recall, F1, and support scores
     for the model. Integrates numerical scores as well as a color-coded heatmap.
 
     Parameters
@@ -74,7 +76,7 @@ class ClassificationReport(ClassificationScoreVisualizer):
     Attributes
     ----------
     scores_ : dict of dicts
-        Outer dictionary composed of precision, recall, and f1 scores with
+        Outer dictionary composed of precision, recall, f1, and support scores with
         inner dictionaries specifiying the values for each class listed.
     """
     def __init__(self, model, ax=None, classes=None, cmap='YlOrRd', **kwargs):
@@ -101,9 +103,18 @@ def score(self, X, y=None, **kwargs):
         y_pred = self.predict(X)
 
         scores = precision_recall_fscore_support(y, y_pred)
-        scores = map(lambda s: dict(zip(self.classes_, s)), scores[0:3])
+
+        # Calculate the percentage for the support metric
+        self.support_score = scores[-1]
+        support_percent = self.support_score / (sum(self.support_score))
+
+        scores = map(lambda s: dict(zip(self.classes_, s)), scores)
         self.scores_ = dict(zip(SCORES_KEYS, scores))
 
+        # Change the support score from the actual support value to the percent
+        # value to be used in the Classification Report.
+        self.scores_['support'] = dict(zip(self.classes_, support_percent))
+
         return self.draw()
 
     def draw(self):
@@ -111,16 +122,17 @@ def draw(self):
         Renders the classification report across each axis.
         """
         # Create display grid
-        cr_display = np.zeros((len(self.classes_), 3))
+        cr_display = np.zeros((len(self.classes_), 4))
 
-        # For each class row, append columns for precision, recall, and f1
+
+        # For each class row, append columns for precision, recall, f1, and support
         for idx, cls in enumerate(self.classes_):
-            for jdx, metric in enumerate(('precision', 'recall', 'f1')):
+            for jdx, metric in enumerate(('precision', 'recall', 'f1', 'support')):
                 cr_display[idx, jdx] = self.scores_[metric][cls]
 
         # Set up the dimensions of the pcolormesh
         # NOTE: pcolormesh accepts grids that are (N+1,M+1)
-        X, Y = np.arange(len(self.classes_)+1), np.arange(4)
+        X, Y = np.arange(len(self.classes_)+1), np.arange(5)
         self.ax.set_ylim(bottom=0, top=cr_display.shape[0])
         self.ax.set_xlim(left=0, right=cr_display.shape[1])
 
@@ -134,6 +146,11 @@ def draw(self):
                 value = cr_display[x,y]
                 svalue = "{:0.3f}".format(value)
 
+                # change the svalue for support (when y == 3) because we want
+                # to label it as the actual support value, not the percentage
+                if y == 3:
+                    svalue = self.support_score[x]
+
                 # Determine the grid and text colors
                 base_color = self.cmap(value)
                 text_color = find_text_color(base_color)
@@ -172,20 +189,21 @@ def finalize(self, **kwargs):
         self.set_title('{} Classification Report'.format(self.name))
 
         # Set the tick marks appropriately
-        self.ax.set_xticks(np.arange(3)+0.5)
+        self.ax.set_xticks(np.arange(4)+0.5)
         self.ax.set_yticks(np.arange(len(self.classes_))+0.5)
 
-        self.ax.set_xticklabels(['precision', 'recall', 'f1-score'], rotation=45)
+        self.ax.set_xticklabels(['precision', 'recall', 'f1-score', 'support'], rotation=45)
         self.ax.set_yticklabels(self.classes_)
 
         plt.tight_layout()
 
 
-def classification_report(model, X, y=None, ax=None, classes=None, **kwargs):
+def classification_report(model, X, y=None, ax=None, classes=None,
+                          random_state=None,**kwargs):
     """Quick method:
 
-    Displays precision, recall, and F1 scores for the model.
-    Integrates numerical scores as well color-coded heatmap.
+    Displays precision, recall, F1, and support scores for the model.
+    Integrates numerical scores as well as color-coded heatmap.
 
     This helper function is a quick wrapper to utilize the ClassificationReport
     ScoreVisualizer for one-off analysis.
@@ -206,6 +224,9 @@ def classification_report(model, X, y=None, ax=None, classes=None, **kwargs):
     classes : list of strings
         The names of the classes in the target
 
+    random_state: integer
+        The seed value for a random generator
+
     Returns
     -------
     ax : matplotlib axes
@@ -215,7 +236,9 @@ def classification_report(model, X, y=None, ax=None, classes=None, **kwargs):
     visualizer = ClassificationReport(model, ax, classes, **kwargs)
 
     # Create the train and test splits
-    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
+    X_train, X_test, y_train, y_test = train_test_split(
+        X, y, test_size=0.2, random_state=random_state
+    )
 
     # Fit and transform the visualizer (calls draw)
     visualizer.fit(X_train, y_train, **kwargs)


Test patch:
diff --git a/tests/baseline_images/test_classifier/test_classification_report/test_binary_class_report.png b/tests/baseline_images/test_classifier/test_classification_report/test_binary_class_report.png
Binary files a/tests/baseline_images/test_classifier/test_classification_report/test_binary_class_report.png and b/tests/baseline_images/test_classifier/test_classification_report/test_binary_class_report.png differ
diff --git a/tests/baseline_images/test_classifier/test_classification_report/test_multiclass_class_report.png b/tests/baseline_images/test_classifier/test_classification_report/test_multiclass_class_report.png
Binary files a/tests/baseline_images/test_classifier/test_classification_report/test_multiclass_class_report.png and b/tests/baseline_images/test_classifier/test_classification_report/test_multiclass_class_report.png differ
diff --git a/tests/baseline_images/test_classifier/test_classification_report/test_pandas_integration.png b/tests/baseline_images/test_classifier/test_classification_report/test_pandas_integration.png
Binary files a/tests/baseline_images/test_classifier/test_classification_report/test_pandas_integration.png and b/tests/baseline_images/test_classifier/test_classification_report/test_pandas_integration.png differ
diff --git a/tests/baseline_images/test_classifier/test_classification_report/test_quick_method.png b/tests/baseline_images/test_classifier/test_classification_report/test_quick_method.png
Binary files a/tests/baseline_images/test_classifier/test_classification_report/test_quick_method.png and b/tests/baseline_images/test_classifier/test_classification_report/test_quick_method.png differ
diff --git a/tests/test_classifier/test_classification_report.py b/tests/test_classifier/test_classification_report.py
--- a/tests/test_classifier/test_classification_report.py
+++ b/tests/test_classifier/test_classification_report.py
@@ -59,13 +59,14 @@ def test_binary_class_report(self):
         viz.fit(self.binary.X.train, self.binary.y.train)
         viz.score(self.binary.X.test, self.binary.y.test)
 
-        self.assert_images_similar(viz, tol=35)
+        self.assert_images_similar(viz, tol=40)
 
         assert viz.scores_ == {
             'precision': {0: approx(0.7446808), 1: approx(0.8490566)},
             'recall': {0: approx(0.8139534), 1: approx(0.7894736)},
-            'f1': {0: approx(0.7777777), 1: approx(0.8181818)}
-        }
+            'f1': {0: approx(0.7777777), 1: approx(0.8181818)},
+            'support': {0: approx(0.42999999999999999), 1: approx(0.56999999999999995)}
+            }
 
     @pytest.mark.xfail(
         sys.platform == 'win32', reason="images not close on windows"
@@ -80,7 +81,7 @@ def test_multiclass_class_report(self):
         viz.fit(self.multiclass.X.train, self.multiclass.y.train)
         viz.score(self.multiclass.X.test, self.multiclass.y.test)
 
-        self.assert_images_similar(viz)
+        self.assert_images_similar(viz, tol=11.0)
 
         assert viz.scores_ == {
             'precision': {
@@ -93,6 +94,10 @@ def test_multiclass_class_report(self):
                 0: 0.47058823529411764, 1: 0.5294117647058824,
                 2: 0.5294117647058824, 3: 0.35294117647058826,
                 4: 0.38709677419354843, 5: 0.6060606060606061
+            }, 'support': {
+                0: 0.19, 1: 0.16,
+                2: 0.14000000000000001, 3: 0.19,
+                4: 0.16, 5: 0.16
             }}
 
     @pytest.mark.xfail(
@@ -128,9 +133,9 @@ def test_pandas_integration(self):
         viz.fit(X_train, y_train)
         viz.score(X_test, y_test)
 
-        self.assert_images_similar(viz, tol=0.1)
+        self.assert_images_similar(viz, tol=43.0)
 
-        # Ensure correct classification scores under the hood
+        # Ensure correct classification scores under the hood!
         assert viz.scores_ == {
             'precision': {
                 'unoccupied': 0.999347471451876,
@@ -141,9 +146,11 @@ def test_pandas_integration(self):
             }, 'f1': {
                 'unoccupied': 0.9800031994880819,
                 'occupied': 0.9366447034972124
+            }, 'support': {
+                'occupied': 0.22519455252918288,
+                'unoccupied': 0.77480544747081714
             }}
 
-    @pytest.mark.skip(reason="requires random state in quick method")
     def test_quick_method(self):
         """
         Test the quick method with a random dataset
@@ -154,9 +161,10 @@ def test_quick_method(self):
         )
 
         _, ax = plt.subplots()
-        classification_report(DecisionTreeClassifier(), X, y, ax=ax)
+        classification_report(DecisionTreeClassifier(), X, y,
+                              ax=ax, random_state=42)
 
-        self.assert_images_similar(ax=ax)
+        self.assert_images_similar(ax=ax, tol=20.0)
 
     def test_isclassifier(self):
         """