Repository: DistrictDataLabs/yellowbrick

Issue:
Precision-Recall Curve does not show class labels
**Describe the bug**

When labels are passed into the `PrecisionRecallCurve` the visualization is not drawn correctly.

**To Reproduce**

```python
from yellowbrick.classifier import PrecisionRecallCurve 
from yellowbrick.dataset import load_game 

from sklearn.preprocessing import LabelEncoder 
from sklearn.naive_bayes import MultinomialNB 
from sklearn.model_selection import train_test_split as tts 

# Load the dataset and label encode the target 
data = load_game() 
X = data.iloc[:, data.columns != 'outcome']
y = LabelEncoder().fit_transform(data['outcome'])

# Create train test splits 
X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, shuffle=True)

oz = PrecisionRecallCurve(
    MultinomialNB(), per_class=True, iso_f1_curves=True, fill_area=False, 
    micro=False, classes=["loss", "draw", "win"]
)
oz.fit(X_train, y_train)
oz.score(X_test, y_test)
oz.poof()
```

**Dataset**

I used the game multi-label dataset from the UCI Machine Learning repository, as wrangled by the yellowbrick datasets module.

**Expected behavior**

When the target, `y` is label encoded (e.g. via the `LabelEncoder`) to classes 0, 1, and 2 and class names are passed in via the `classes` param of the visualizer, the legend should display the class names. However, in this case the visualization does not appear at all:

![figure_1](https://user-images.githubusercontent.com/745966/45109335-3c59a800-b10d-11e8-9386-e74de86282eb.png)


**Traceback**

No exception is raised, however, the following warning is issued:

```
/Users/benjamin/.pyenv/versions/3.6.2/envs/yellowbrick3/lib/python3.6/site-packages/numpy/lib/arraysetops.py:522: FutureWarning: elementwise comparison failed; returning scalar instead, but in the future will perform elementwise comparison
  mask |= (ar1 == a)
/Users/benjamin/.pyenv/versions/3.6.2/envs/yellowbrick3/lib/python3.6/site-packages/sklearn/metrics/ranking.py:444: RuntimeWarning: invalid value encountered in true_divide
  recall = tps / tps[-1]
objc[46460]: Class FIFinderSyncExtensionHost is implemented in both /System/Library/PrivateFrameworks/FinderKit.framework/Versions/A/FinderKit (0x7fffb473cc90) and /System/Library/PrivateFrameworks/FileProvider.framework/OverrideBundles/FinderSyncCollaborationFileProviderOverride.bundle/Contents/MacOS/FinderSyncCollaborationFileProviderOverride (0x12814ecd8). One of the two will be used. Which one is undefined.
```

**Desktop (please complete the following information):**
 - OS: macOS High Sierra 10.13.6
 - Python Version 3.6.2
 - Yellowbrick Version 0.9 develop 

**Additional context**

This was a known bug during development but was left in as a rare case. 



Discussion:
@bbengfort I would like to take a stab at this. Btw, what is the meaning of the hacktoberfest label?
@zjpoh that would be great - though you'll have to wait until #599 is merged before you can do a fix! Do you want to work with @rebeccabilbro in doing a code review of that PR?

The [hacktoberfest label](https://github.com/DistrictDataLabs/yellowbrick/issues?q=is%3Aopen+is%3Aissue+label%3Ahacktoberfest) is meant to designate issues that would be good for folks who are participating in [Hacktoberfest](https://hacktoberfest.digitalocean.com/), particularly [PyData Manchester folks](https://twitter.com/_JAStark/status/1037431605890048000).
@bbengfort Sure. I’ll take a look at that tonight. 
@zjpoh still interested in fixing this bug?
@bbengfort Sorry again for being so irresponsive for such a long time. 

I think the problem is that the `y` in `score` has values `[0, 1, 2]`, but the classes are `classes=['loss' 'draw' 'win']`. This breaks the `label_binarize`. I'm working on a fix and will open a PR when I have it ready.

Resolving patch:
diff --git a/yellowbrick/classifier/prcurve.py b/yellowbrick/classifier/prcurve.py
--- a/yellowbrick/classifier/prcurve.py
+++ b/yellowbrick/classifier/prcurve.py
@@ -180,7 +180,8 @@ def fit(self, X, y=None):
             self.estimator = OneVsRestClassifier(self.estimator)
 
             # Use label_binarize to create multi-label ouptut for OneVsRestClassifier
-            Y = label_binarize(y, classes=np.unique(y))
+            self._target_labels = np.unique(y)
+            Y = label_binarize(y, classes=self._target_labels)
         elif ttype.startswith(BINARY):
             self.target_type_ = BINARY
 
@@ -226,7 +227,7 @@ def score(self, X, y=None):
             self.score_ = average_precision_score(y, y_scores)
         else:
             # Use label_binarize to create multi-label ouptut for OneVsRestClassifier
-            Y = label_binarize(y, classes=self.classes_)
+            Y = label_binarize(y, classes=self._target_labels)
 
             self.precision_, self.recall_, self.score_ = {}, {}, {}
 


Test patch:
diff --git a/tests/baseline_images/test_classifier/test_prcurve/test_multiclass_probability_with_class_labels.png b/tests/baseline_images/test_classifier/test_prcurve/test_multiclass_probability_with_class_labels.png
new file mode 100644
Binary files /dev/null and b/tests/baseline_images/test_classifier/test_prcurve/test_multiclass_probability_with_class_labels.png differ
diff --git a/tests/test_classifier/test_prcurve.py b/tests/test_classifier/test_prcurve.py
--- a/tests/test_classifier/test_prcurve.py
+++ b/tests/test_classifier/test_prcurve.py
@@ -14,6 +14,7 @@
 ## Imports
 ##########################################################################
 
+import matplotlib
 import sys
 import pytest
 
@@ -29,7 +30,6 @@
 from sklearn.datasets import make_regression
 from sklearn.tree import DecisionTreeClassifier
 from sklearn.linear_model import RidgeClassifier
-from sklearn.model_selection import train_test_split as tts
 from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier
 
 
@@ -239,6 +239,72 @@ def test_multiclass_probability(self):
         tol = 6.6 if sys.platform == 'win32' else 1.0 # fails with RMSE 6.583 on AppVeyor
         self.assert_images_similar(oz, tol=tol)
 
+    def test_multiclass_probability_with_class_labels(self):
+        """Visual similarity of multiclass classifier with class labels."""
+        # Create and fit the visualizer
+        oz = PrecisionRecallCurve(
+            GaussianNB(), per_class=True, micro=False, fill_area=False,
+            iso_f1_curves=True, ap_score=False,
+            classes=["a", "b", "c", "d", "e", "f"]
+        )
+        assert_not_fitted(oz)
+
+        # Fit returns self
+        assert oz.fit(self.multiclass.X.train, self.multiclass.y.train) is oz
+
+        # Score the visualizer
+        s = oz.score(self.multiclass.X.test, self.multiclass.y.test)
+        assert_fitted(oz)
+
+        # Score should be between 0 and 1
+        assert 0.0 <= s <= 1.0
+
+        # Check the multiclass classification properties
+        assert oz.target_type_ == MULTICLASS
+        assert isinstance(oz.score_, dict)
+        assert oz.score_[MICRO] == s
+        assert isinstance(oz.precision_, dict)
+        assert isinstance(oz.recall_, dict)
+        assert len(oz.score_) == len(oz.classes_) + 1
+        assert len(oz.precision_) == len(oz.classes_) + 1
+        assert len(oz.recall_) == len(oz.classes_) + 1
+
+        # Finalize image
+        oz.finalize()
+
+        # Compare texts of the images.
+        # Labels
+        assert oz.ax.get_xlabel() == "Recall"
+        oz.ax.set_xlabel("")
+        assert oz.ax.get_ylabel() == "Precision"
+        oz.ax.set_ylabel("")
+        assert oz.ax.get_title() == "Precision-Recall Curve for GaussianNB"
+        oz.ax.set_title("")
+        # Legend
+        oz_legend_txt = [x.get_text() for x in oz.ax.legend().get_texts()]
+        expected_legend_txt = [
+            "PR for class a (area=0.42)",
+            "PR for class b (area=0.36)",
+            "PR for class c (area=0.44)",
+            "PR for class d (area=0.52)",
+            "PR for class e (area=0.37)",
+            "PR for class f (area=0.49)",
+        ]
+        assert oz_legend_txt == expected_legend_txt
+        handles, _ = oz.ax.get_legend_handles_labels()
+        empty_labels = [""] * len(handles)
+        oz.ax.legend(handles=handles, labels=empty_labels, loc='lower left',
+                     frameon=True)
+        # Text in iso_f1_curves.
+        # Will not check for these as they appears okay in other test images.
+        for child in oz.ax.get_children():
+            if isinstance(child, matplotlib.text.Annotation):
+                oz.ax.texts.remove(child)
+
+        # Compare the images
+        tol = 6.6 if sys.platform == 'win32' else 1.0 # fails with RMSE 6.583 on AppVeyor
+        self.assert_images_similar(oz, tol=tol)
+
     @pytest.mark.filterwarnings("ignore:From version 0.21")
     def test_quick_method(self):
         """
@@ -314,4 +380,4 @@ def test_missing_test_data_in_quick_method(self):
             precision_recall_curve(RandomForestClassifier(random_state=27),x_train, y_train,y_test=y_test,random_state=7)
 
         with pytest.raises(YellowbrickValueError, match="both X_test and y_test are required if one is specified"):
-            precision_recall_curve(RandomForestClassifier(random_state=27),x_train, y_train,X_test=x_test,random_state=7)
\ No newline at end of file
+            precision_recall_curve(RandomForestClassifier(random_state=27),x_train, y_train,X_test=x_test,random_state=7)