Repository: DistrictDataLabs/yellowbrick

Issue:
step: removing 5 features in each iteration
Using RFECV, the graph shows best features with one increment in each iteration.
I want to show each iteration should work with an increment of 5 features.
Example: score  should be plotted against x-axis of [5,10,15,20,25].
I used step=5, but its not working.




Discussion:
@mubashirtuf I'm sorry it's taken so long for us to respond; things got busy at the end of the semester and during the holiday. I can take a look at this, but it would be helpful if you could provide the code you used to generate the error - on my quick first attempt I couldn't reproduce the issue. I see a place where the problem might be occurring, but I'm not totally sure. Would you also upload the figure that was produced?

Resolving patch:
diff --git a/yellowbrick/features/rfecv.py b/yellowbrick/features/rfecv.py
--- a/yellowbrick/features/rfecv.py
+++ b/yellowbrick/features/rfecv.py
@@ -112,6 +112,10 @@ class RFECV(ModelVisualizer):
         functions such as ``predict()`` and ``score()`` are passed through to
         this estimator (it rewraps the original model).
 
+    n_feature_subsets_ : array of shape [n_subsets_of_features]
+        The number of features removed on each iteration of RFE, computed by the
+        number of features in the dataset and the step parameter.
+
     Notes
     -----
     This model wraps ``sklearn.feature_selection.RFE`` and not
@@ -172,7 +176,7 @@ def fit(self, X, y=None):
 
         # Create the RFE model
         rfe = RFE(self.estimator, step=step)
-        n_feature_subsets = np.arange(1, n_features+1)
+        self.n_feature_subsets_ = np.arange(1, n_features+step, step)
 
         # Create the cross validation params
         # TODO: handle random state
@@ -183,7 +187,7 @@ def fit(self, X, y=None):
 
         # Perform cross-validation for each feature subset
         scores = []
-        for n_features_to_select in n_feature_subsets:
+        for n_features_to_select in self.n_feature_subsets_:
             rfe.set_params(n_features_to_select=n_features_to_select)
             scores.append(cross_val_score(rfe, X, y, **cv_params))
 
@@ -192,7 +196,7 @@ def fit(self, X, y=None):
 
         # Find the best RFE model
         bestidx = self.cv_scores_.mean(axis=1).argmax()
-        self.n_features_ = n_feature_subsets[bestidx]
+        self.n_features_ = self.n_feature_subsets_[bestidx]
 
         # Fit the final RFE model for the number of features
         self.rfe_estimator_ = rfe
@@ -214,7 +218,7 @@ def draw(self, **kwargs):
         Renders the rfecv curve.
         """
         # Compute the curves
-        x = np.arange(1, len(self.cv_scores_)+1)
+        x = self.n_feature_subsets_
         means = self.cv_scores_.mean(axis=1)
         sigmas = self.cv_scores_.std(axis=1)
 


Test patch:
diff --git a/tests/baseline_images/test_features/test_rfecv/test_quick_method.png b/tests/baseline_images/test_features/test_rfecv/test_quick_method.png
Binary files a/tests/baseline_images/test_features/test_rfecv/test_quick_method.png and b/tests/baseline_images/test_features/test_rfecv/test_quick_method.png differ
diff --git a/tests/baseline_images/test_features/test_rfecv/test_rfecv_step.png b/tests/baseline_images/test_features/test_rfecv/test_rfecv_step.png
new file mode 100644
Binary files /dev/null and b/tests/baseline_images/test_features/test_rfecv/test_rfecv_step.png differ
diff --git a/tests/test_features/test_rfecv.py b/tests/test_features/test_rfecv.py
--- a/tests/test_features/test_rfecv.py
+++ b/tests/test_features/test_rfecv.py
@@ -16,6 +16,8 @@
 
 import sys
 import pytest
+import numpy as np
+import numpy.testing as npt
 
 from tests.base import VisualTestCase
 from tests.dataset import DatasetMixin, Dataset
@@ -77,7 +79,7 @@ def test_fit(self, mock_draw):
         X, y = self.dataset
         params = (
             "n_features_", "support_", "ranking_",
-            "cv_scores_", "rfe_estimator_",
+            "cv_scores_", "rfe_estimator_", "n_feature_subsets_"
         )
 
         rf = RandomForestClassifier()
@@ -124,7 +126,7 @@ def test_quick_method(self):
         model = LogisticRegression()
         X, y = self.dataset
 
-        ax = rfecv(model, X, y, step=3, cv=cv, scoring='f1_weighted')
+        ax = rfecv(model, X, y, step=2, cv=cv, scoring='f1_weighted')
 
         self.assert_images_similar(ax=ax)
 
@@ -156,11 +158,28 @@ def test_pandas_integration(self):
 
         self.assert_images_similar(oz)
 
-    def test_valid_step(self):
+    def test_invalid_step(self):
         """
         Test step hyperparam validation
         """
         # TODO: parametrize when unittest is removed
-        with pytest.raises(YellowbrickValueError):
-            oz = RFECV(SVC(kernel="lnear"), step=-1)
+        with pytest.raises(YellowbrickValueError, match="step must be >0"):
+            oz = RFECV(SVC(kernel="linear"), step=-1)
             oz.fit(self.dataset.X, self.dataset.y)
+
+    def test_rfecv_step(self):
+        """
+        Test RFECV step=5 with LogisticRegression
+        """
+        X, y = make_classification(
+            n_samples=200, n_features=30, n_informative=18, n_redundant=6,
+            n_repeated=0, n_classes=8, n_clusters_per_class=1, random_state=0
+        )
+
+        oz = RFECV(LogisticRegression(random_state=32), step=5).fit(X, y)
+        assert hasattr(oz, "n_feature_subsets_")
+        npt.assert_array_equal(oz.n_feature_subsets_, np.arange(1,35,5))
+
+        oz.finalize()
+        tol = 1.75 if sys.platform == "win32" else 0.25
+        self.assert_images_similar(oz, tol=tol)
\ No newline at end of file