Repository: DistrictDataLabs/yellowbrick

Issue:
Some manifold algorithms do not have transform attribute in sklearn.
**Describe the bug**
Manifold plotting with `manifold=mds` produces correct plot but raises no transform attribute error.

**To Reproduce**

```python
%matplotlib inline
import numpy as np
from yellowbrick.features.manifold import Manifold

y = np.linspace(-1, 1, 100)[:, ]
X = y[:, None] + np.random.random((100, 10))
viz = Manifold(manifold='mds')
viz.fit_transform(X, y)
viz.poof()
```

**Expected behavior**
No transform attribute error.

**Traceback**

```
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-1-7f7457a2faab> in <module>()
      6 X = y[:, None] + np.random.random((100, 10))
      7 viz = Manifold(manifold='mds')
----> 8 viz.fit_transform(X, y)
      9 viz.poof()

~/anaconda3/lib/python3.6/site-packages/sklearn/base.py in fit_transform(self, X, y, **fit_params)
    518         else:
    519             # fit method of arity 2 (supervised transformation)
--> 520             return self.fit(X, y, **fit_params).transform(X)
    521 
    522 

~/anaconda3/lib/python3.6/site-packages/yellowbrick/features/manifold.py in transform(self, X)
    328             Returns the 2-dimensional embedding of the instances.
    329         """
--> 330         return self.manifold.transform(X)
    331 
    332     def draw(self, X, y=None):

AttributeError: 'MDS' object has no attribute 'transform'
```

**Desktop (please complete the following information):**
 - OS: macOS
 - Python 3.6
 - Yellowbrick Version 0.8

**Additional context**
Similarly error for `manifold='spectral'` and `manifold='tsne'`.

**Proposed solution**
Remove the [transform](https://github.com/DistrictDataLabs/yellowbrick/blob/3b3eb63c32bfc86145ec9e130e3a79c9ffc2bec0/yellowbrick/features/manifold.py#L316) method from `yellowbrick.features.manifold` unless it is being used somewhere else.


Discussion:
@zjpoh thanks for the bug report! I think this should be an easy fix; I'm going to leave `transform` in, but will catch `AttributeErrors` in the implementation; then I'll implement a manifold-specific `fit_transform` method rather than relying on the default implementation that's part of `TransformerMixin`. 

Update to follow shortly!

Resolving patch:
diff --git a/yellowbrick/features/manifold.py b/yellowbrick/features/manifold.py
--- a/yellowbrick/features/manifold.py
+++ b/yellowbrick/features/manifold.py
@@ -262,6 +262,14 @@ def manifold(self, transformer):
             self._name = self._manifold.__class__.__name__
 
     def fit(self, X, y=None):
+        """
+        Fits the manifold on X and transforms the data to plot it on the axes.
+        See fit_transform() for more details.
+        """
+        self.fit_transform(X, y)
+        return self
+
+    def fit_transform(self, X, y=None):
         """
         Fits the manifold on X and transforms the data to plot it on the axes.
         The optional y specified can be used to declare discrete colors. If
@@ -311,7 +319,7 @@ def fit(self, X, y=None):
         self.fit_time_ = time.time() - start
 
         self.draw(Xp, y)
-        return self
+        return Xp
 
     def transform(self, X):
         """
@@ -327,7 +335,10 @@ def transform(self, X):
         Xprime : array-like of shape (n, 2)
             Returns the 2-dimensional embedding of the instances.
         """
-        return self.manifold.transform(X)
+        try:
+            return self.manifold.transform(X)
+        except AttributeError as e:
+            raise AttributeError(str(e) + " try using fit_transform instead.")
 
     def draw(self, X, y=None):
         """


Test patch:
diff --git a/tests/test_features/test_manifold.py b/tests/test_features/test_manifold.py
--- a/tests/test_features/test_manifold.py
+++ b/tests/test_features/test_manifold.py
@@ -92,16 +92,30 @@ def test_manifold_instance_construction(self):
         oz = Manifold(manifold=manifold)
         assert oz.manifold is manifold
 
-    @patch('yellowbrick.features.manifold.Manifold.draw', spec=True)
-    def test_manifold_fit(self, mock_draw):
+    @patch('yellowbrick.features.manifold.Manifold.fit_transform', spec=True)
+    def test_manifold_fit(self, mock_fit_transform):
         """
         Test manifold fit method
         """
         X, y = make_s_curve(1000, random_state=888)
         manifold = Manifold(target="auto")
 
-        assert not hasattr(manifold, 'fit_time_')
         assert manifold.fit(X, y) is manifold, "fit did not return self"
+        mock_fit_transform.assert_called_once()
+
+    @patch('yellowbrick.features.manifold.Manifold.draw', spec=True)
+    def test_manifold_fit_transform(self, mock_draw):
+        """
+        Test manifold fit_transform method
+        """
+        X, y = make_s_curve(1000, random_state=888)
+        manifold = Manifold(target="auto")
+
+        assert not hasattr(manifold, 'fit_time_')
+
+        Xp = manifold.fit_transform(X, y)
+        assert Xp.shape == (X.shape[0], 2)
+
         mock_draw.assert_called_once()
         assert hasattr(manifold, 'fit_time_')
         assert manifold._target_color_type == CONTINUOUS
@@ -117,8 +131,12 @@ def test_manifold_classification(self):
         )
 
         oz = Manifold(manifold="spectral", target="discrete", random_state=108)
+        assert not hasattr(oz, 'classes_')
+
         oz.fit(X, y)
 
+        assert hasattr(oz, 'classes_')
+        assert not hasattr(oz, 'range_')
         self.assert_images_similar(oz, tol=0.5)
 
     def test_manifold_regression(self):
@@ -130,8 +148,12 @@ def test_manifold_regression(self):
         )
 
         oz = Manifold(manifold="lle", target="continuous", random_state=1)
+        assert not hasattr(oz, 'range_')
+
         oz.fit(X, y)
 
+        assert not hasattr(oz, 'classes_')
+        assert hasattr(oz, 'range_')
         self.assert_images_similar(oz, tol=1.5)
 
     def test_manifold_single(self):
@@ -233,3 +255,15 @@ def test_determine_target_color_type(self):
         msg = "could not determine target color type"
         with pytest.raises(YellowbrickValueError, match=msg):
             manifold._determine_target_color_type([])
+
+    def test_manifold_no_transform(self):
+        """
+        Test the exception when manifold doesn't implement transform.
+        """
+        X, _ = make_s_curve(1000, random_state=888)
+        manifold = Manifold(manifold='mds', target="auto")
+
+        assert not hasattr(manifold._manifold, 'transform')
+
+        with pytest.raises(AttributeError, match="try using fit_transform instead"):
+            manifold.transform(X)