Repository: scikit-learn/scikit-learn
Pull request: [MRG] BUG Fixes voting named_estiamtor bug
<!--
Thanks for contributing a pull request! Please ensure you have taken a look at
the contribution guidelines: https://github.com/scikit-learn/scikit-learn/blob/master/CONTRIBUTING.md#pull-request-checklist
-->

#### Reference Issues/PRs
<!--
Example: Fixes #1234. See also #3456.
Please use keywords (e.g., Fixes) to create link to the issues or pull requests
you resolved, so that they will automatically be closed when your pull request
is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests
-->
Fixes https://github.com/scikit-learn/scikit-learn/issues/15374

<!--
Please be aware that we are a loose team of volunteers so patience is
necessary; assistance handling other issues is very welcome. We value
all user contributions, no matter how minor they are. If we are slow to
review, either the pull request needs some benchmarking, tinkering,
convincing, etc. or more likely the reviewers are simply busy. In either
case, we ask for your understanding during the review process.
For more information, see our FAQ on this topic:
http://scikit-learn.org/dev/faq.html#why-is-my-pull-request-not-getting-any-attention.

Thanks for contributing!
-->


Problem statement:
VotingClassifier.named_estimators_ has a wrong value if some estimator is None
#### Steps/Code to Reproduce

```python
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import GaussianNB
from sklearn.ensemble import RandomForestClassifier, VotingClassifier

X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
y = np.array([1, 1, 1, 2, 2, 2])

clf1 = LogisticRegression(solver='lbfgs', multi_class='multinomial', random_state=1)
clf2 = RandomForestClassifier(n_estimators=50, random_state=1)
clf3 = GaussianNB()

eclf1 = VotingClassifier(estimators=[('lr', clf1), ('rf', None), ('gnb', clf3)], voting='hard')

eclf = eclf1.fit(X, y)
print(eclf.named_estimators_)
```

#### Expected Results
```
{'lr': LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
                   intercept_scaling=1, l1_ratio=None, max_iter=100,
                   multi_class='multinomial', n_jobs=None, penalty='l2',
                   random_state=1, solver='lbfgs', tol=0.0001, verbose=0,
                   warm_start=False), 'gnb: GaussianNB(priors=None, var_smoothing=1e-09)}
```

#### Actual Results

```
{'lr': LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
                   intercept_scaling=1, l1_ratio=None, max_iter=100,
                   multi_class='multinomial', n_jobs=None, penalty='l2',
                   random_state=1, solver='lbfgs', tol=0.0001, verbose=0,
                   warm_start=False), 'rf': GaussianNB(priors=None, var_smoothing=1e-09)}
```

#### Versions
System:
    python: 3.6.8 (default, Oct  7 2019, 12:59:55)  [GCC 8.3.0]
executable: /usr/local/bin/python
   machine: Linux-4.15.0-1039-aws-x86_64-with-Ubuntu-18.04-bionic

Python deps:
       pip: 19.3.1
setuptools: 41.5.0
   sklearn: 0.21.3
     numpy: 1.17.3
     scipy: 1.3.1
    Cython: None
    pandas: 0.25.2

Hints from issue discussion:
it seems the issue is due to [this code block](https://github.com/scikit-learn/scikit-learn/blob/0.21.X/sklearn/ensemble/voting.py#L104:L106)

```python
self.named_estimators_ = Bunch()
for k, e in zip(self.estimators, self.estimators_):
    self.named_estimators_[k[0]] = e
```
`self.estimators` should be replaced by something like `self.estimators_no_none`

Commit message: DOC Adds to whats_new

Patch to review:
diff --git a/doc/whats_new/v0.22.rst b/doc/whats_new/v0.22.rst
index 91a1d9eb90dcd..f1ff286ba4c07 100644
--- a/doc/whats_new/v0.22.rst
+++ b/doc/whats_new/v0.22.rst
@@ -714,6 +714,13 @@ Changelog
   - ``optimize.newton_cg`
   - ``random.random_choice_csc``
 
+:mod:`sklearn.voting`
+.....................
+
+- |Fix| The `named_estimators_` attribute in :class:`voting.VotingClassifier`
+  and :class:`voting.VotingRegressor` now correctly maps to dropped estimators.
+  :pr:`15375` by `Thomas Fan`_.
+
 :mod:`sklearn.isotonic`
 ..................................
 
diff --git a/sklearn/ensemble/_voting.py b/sklearn/ensemble/_voting.py
index de2880166c0cf..bddf5f00b10af 100644
--- a/sklearn/ensemble/_voting.py
+++ b/sklearn/ensemble/_voting.py
@@ -71,8 +71,13 @@ def fit(self, X, y, sample_weight=None):
             )
 
         self.named_estimators_ = Bunch()
-        for k, e in zip(self.estimators, self.estimators_):
-            self.named_estimators_[k[0]] = e
+
+        # Uses None or 'drop' as placeholder for dropped estimators
+        est_iter = iter(self.estimators_)
+        for name, est in self.estimators:
+            current_est = est if est in (None, 'drop') else next(est_iter)
+            self.named_estimators_[name] = current_est
+
         return self
 
 
diff --git a/sklearn/ensemble/tests/test_voting.py b/sklearn/ensemble/tests/test_voting.py
index d1021fbce28b5..bf18a0e7e0cfd 100644
--- a/sklearn/ensemble/tests/test_voting.py
+++ b/sklearn/ensemble/tests/test_voting.py
@@ -560,3 +560,21 @@ def test_deprecate_none_transformer(Voter, BaseEstimator):
            "Use the string 'drop' instead.")
     with pytest.warns(DeprecationWarning, match=msg):
         est.fit(X, y)
+
+
+# TODO: Remove drop pparametrize in 0.24 when None is removed in Voting*
+@pytest.mark.parametrize(
+    "Voter, BaseEstimator",
+    [(VotingClassifier, DecisionTreeClassifier),
+     (VotingRegressor, DecisionTreeRegressor)]
+)
+@pytest.mark.parametrize("drop", [None, 'drop'])
+def test_correct_named_estimator_with_drop(Voter, BaseEstimator, drop):
+    est = Voter(estimators=[('lr', drop),
+                            ('tree', BaseEstimator(random_state=0))])
+
+    with pytest.warns(None) as rec:
+        est.fit(X, y)
+    assert rec if drop is None else not rec
+
+    assert est.named_estimators_['lr'] == drop


Code review comments:
diff_hunk: @@ -560,3 +560,21 @@ def test_deprecate_none_transformer(Voter, BaseEstimator):
            "Use the string 'drop' instead.")
     with pytest.warns(DeprecationWarning, match=msg):
         est.fit(X, y)
+
+
+# TODO: Remove drop pparametrize in 0.24 when None is removed in Voting*
path: sklearn/ensemble/tests/test_voting.py
text: @user1:
typo parametrize :)