Repository: DistrictDataLabs/yellowbrick

Issue:
YellowBrick not compatible with statsmodels estimators (and a trivial wrapper as a light fix)
I'm comparing GLMs in `statsmodels` and other models in `sklearn`. I wanted to use a consistent visual output but realised that `yellowbrick` only understands the `sklearn BaseEstimator`. 

I've written a trivial wrapper (i.e. - it works for me, it may not do what you need!) around a `statsmodels GLM` that makes it 'look like' a `BaseEstimator`, in the eyes of `yellowbrick`. As a result I can create a `partial` GLM, push it into this wrapper, then call `yb.ResidualsPlot` and `yb.PredictionError`. 

I present this code more as a placeholder in case someone else has the same need and they'd like to build a stronger solution. 

This is tested using `yellowbrick 0.5`, `statsmodels 0.8`, `sklearn 0.19` in Python 3.6.

```
class StatsModelsWrapper(BaseEstimator):
    """Wrap a statsmodels GLM as a sklearn (fake) BaseEstimator for YellowBrick

    To use it, first create a partial 
    ```
    glm_gaussian_partial = functools.partial(sm.GLM, family=sm.families.Gaussian())
    #gaussian_model = glm_gaussian_partial(y_train, X_train) # for statsmodels usage, calling .summary() etc
    fake_est = StatsModelsWrapper(glm_gaussian_partial)
    visualizer = PredictionError(fake_est)
    #subsequent calls to e.g. fake_est.fit(X, y) will be passed through
    ```
    
    NOTE that this wrapper is trivial, options and extra things like weights are not handled
    """
    def __init__(self, 
                 glm_partial, 
                 stated_estimator_type="regressor",
                 scorer=sklearn.metrics.r2_score):
        # YellowBrick checks the attribute to see if it is a regressor/clusterer/classifier
        self._estimator_type = stated_estimator_type
        # assume user passes in a partial which we can subsequently instantiate
        self.glm_partial = glm_partial
        # score() needs a default scoring function, regression uses r^2 in sklearn
        self.scorer = scorer

    def fit(self, X, y):
        """Pretend to be a sklearn estimator, fit in this case is called on creation"""
        # note that GLM takes endog (y) and then exog (X), this is the reverse of sklearn's methods
        self.glm_model = self.glm_partial(y, X)
        self.glm_results = self.glm_model.fit()
        
    def predict(self, X):
        return self.glm_results.predict(X)
    
    def score(self, X, y):
        return self.scorer(y, self.predict(X))
    

def test_stats_models_wrapper():
    """A trivial test of the StatsModelsWrapper defined above"""
    X = np.array([[1], [2], [3]])
    y = np.array([1.1, 2, 3])
    glm_gaussian_partial = functools.partial(sm.GLM, family=sm.families.Gaussian())
    fake_est = StatsModelsWrapper(glm_gaussian_partial)
    fake_est.fit(X, y)
    fake_est.predict(X)
    fake_est.score(X, y)
    print("All the fake methods ran")
test_stats_models_wrapper()
```


Discussion:
@ianozsvald thank you! this is a very helpful wrapper that you've contributed here. at our next maintainers meeting, we'll discuss how we might be able to potentially incorporate this in. Maybe in a `contrib` module to start
See #341 -- once we get the `contrib` module in place, we're planning on including this as well as possibly Keras visualizers in it; as well as some of our more prototype-y implementations. 

Resolving patch:
diff --git a/yellowbrick/contrib/statsmodels/__init__.py b/yellowbrick/contrib/statsmodels/__init__.py
new file mode 100644
--- /dev/null
+++ b/yellowbrick/contrib/statsmodels/__init__.py
@@ -0,0 +1,17 @@
+# yellowbrick.contrib.statsmodels
+# Implements wrappers around hte statsmodels library to use Yellowbrick with.
+#
+# Author:  Benjamin Bengfort <benjamin@bengfort.com>
+# Created: Wed Apr 04 13:13:24 2018 -0400
+#
+# ID: __init__.py [] benjamin@bengfort.com $
+
+"""
+Implements wrappers around hte statsmodels library to use Yellowbrick with.
+"""
+
+##########################################################################
+## Imports
+##########################################################################
+
+from .base import StatsModelsWrapper
diff --git a/yellowbrick/contrib/statsmodels/base.py b/yellowbrick/contrib/statsmodels/base.py
new file mode 100644
--- /dev/null
+++ b/yellowbrick/contrib/statsmodels/base.py
@@ -0,0 +1,84 @@
+# yellowbrick.contrib.statsmodels.base
+# A basic wrapper for statsmodels that emulates a scikit-learn estimator.
+#
+# Author:  Ian Ozsvald
+# Created: Wed Jan 10 12:47:00 2018 -0500
+#
+# ID: base.py [] benjamin@bengfort.com $
+
+"""
+A basic wrapper for statsmodels that emulates a scikit-learn estimator.
+"""
+
+##########################################################################
+## Imports
+##########################################################################
+
+from sklearn.metrics import r2_score
+from sklearn.base import BaseEstimator
+
+
+##########################################################################
+## statsmodels Estimator
+##########################################################################
+
+class StatsModelsWrapper(BaseEstimator):
+    """
+    Wrap a statsmodels GLM as a sklearn (fake) BaseEstimator for YellowBrick.
+
+    Examples
+    --------
+    First import the external libraries and helper utilities:
+
+    >>> import statsmodels as sm
+    >>> from functools import partial
+
+    Instantiate a partial with the statsmodels API:
+
+    >>> glm_gaussian_partial = partial(sm.GLM, family=sm.families.Gaussian())
+    >>> sm_est = StatsModelsWrapper(glm_gaussian_partial)
+
+    Create a Yellowbrick visualizer to visualize prediction error:
+
+    >>> visualizer = PredictionError(sm_est)
+    >>> visualizer.fit(X_train, y_train)
+    >>> visualizer.score(X_test, y_test)
+
+    For statsmodels usage, calling .summary() etc:
+
+    >>> gaussian_model = glm_gaussian_partial(y_train, X_train)
+
+    Note
+    ----
+    .. note:: This wrapper is trivial, options and extra things like weights
+        are not currently handled.
+    """
+    def __init__(self, glm_partial, stated_estimator_type="regressor",
+                 scorer=r2_score):
+
+        # YellowBrick checks the attribute to see if it is a
+        # regressor/clusterer/classifier
+        self._estimator_type = stated_estimator_type
+
+        # assume user passes in a partial which we can instantiate later
+        self.glm_partial = glm_partial
+
+        # needs a default scoring function, regression uses r^2 in sklearn
+        self.scorer = scorer
+
+    def fit(self, X, y):
+        """
+        Pretend to be a sklearn estimator, fit is called on creation
+        """
+
+        # note that GLM takes endog (y) and then exog (X):
+        # this is the reverse of sklearn's methods
+        self.glm_model = self.glm_partial(y, X)
+        self.glm_results = self.glm_model.fit()
+        return self
+
+    def predict(self, X):
+        return self.glm_results.predict(X)
+
+    def score(self, X, y):
+        return self.scorer(y, self.predict(X))


Test patch:
diff --git a/tests/test_contrib/test_statsmodels/__init__.py b/tests/test_contrib/test_statsmodels/__init__.py
new file mode 100644
--- /dev/null
+++ b/tests/test_contrib/test_statsmodels/__init__.py
@@ -0,0 +1,15 @@
+# tests.test_contrib.test_statsmodels
+# Tests for the statsmodels contrib package
+#
+# Author:  Benjamin Bengfort <benjamin@bengfort.com>
+# Created: Wed Apr 04 13:28:13 2018 -0400
+#
+# ID: __init__.py [] benjamin@bengfort.com $
+
+"""
+Tests for the statsmodels contrib package
+"""
+
+##########################################################################
+## Imports
+##########################################################################
diff --git a/tests/test_contrib/test_statsmodels/test_base.py b/tests/test_contrib/test_statsmodels/test_base.py
new file mode 100644
--- /dev/null
+++ b/tests/test_contrib/test_statsmodels/test_base.py
@@ -0,0 +1,46 @@
+# tests.test_contrib.test_statsmodels.test_base
+# Tests for the statsmodels estimator wrapper.
+#
+# Author:  Ian Ozsvald
+# Created: Wed Jan 10 12:47:00 2018 -0500
+#
+# ID: test_base.py [] benjamin@bengfort.com $
+
+"""
+Tests for the statsmodels estimator wrapper.
+"""
+
+##########################################################################
+## Imports
+##########################################################################
+
+import pytest
+import functools
+import numpy as np
+
+from yellowbrick.contrib.statsmodels import StatsModelsWrapper
+
+try:
+    import statsmodels as sm
+except ImportError:
+    sm = None
+
+
+##########################################################################
+## Test Cases
+##########################################################################
+
+@pytest.mark.skipif(sm is None, reason="test requires statsmodels")
+def test_stats_models_wrapper():
+    """
+    A trivial test of the StatsModelsWrapper
+    """
+    X = np.array([[1], [2], [3]])
+    y = np.array([1.1, 2, 3])
+
+    glm_gaussian = functools.partial(sm.GLM, family=sm.families.Gaussian())
+    sm_est = StatsModelsWrapper(glm_gaussian)
+
+    assert sm_est.fit(X, y) is sm_est, "fit did not return self"
+    assert sm_est.predict(X).shape == (3,)
+    assert 0.0 <= sm_est.score(X, y) <= 1.0