Repository: scikit-learn/scikit-learn
Pull request: [MRG+1] Change named_steps to Bunch object
<!--
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#Contributing-Pull-Requests
-->
#### Reference Issue
<!-- Example: Fixes #1234 -->
Fix #8481 

#### What does this implement/fix? Explain your changes.
This PR changes `pipeline.named_steps` into bunch object instead of dictionary. 

#### **Any other comments?**
Usage example

```python > anova_filter = SelectKBest(f_regression, k=5)
> clf = svm.SVC(kernel='linear')
> anova_svm = Pipeline([('anova', anova_filter), ('svc', clf)])
> anova_svm.named_steps.anova
SelectKBest(k=5, score_func=<function f_regression at 0x7f1d984d8620>)

```

#### To do
- [x] Update source code
- [x] Test
- [x] Documentation

<!--
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:
API for autocompletable attributes on pipeline
Many people use jupyter for data analysis. I like us to be as responsive and interactive in this environment as possible. I also like API to be discoverable by pressing <tab>.

Unfortunately it's not possible to access attributes on pipelines without writing out full names.
afaik there's two ways to access attributes:
```python
coef = pipeline.steps[1][1].coef_
coef = pipeline.named_steps['logisticregression'].coef_
```
I usually prefer the second one because I like names better than counting and it's more explicit for anyone reading the code.
But that requires me to know two things that I can't get via tab-completion: the names of the steps and the name of the attribute.

I think it would be awesome if I could get to ``coef_`` using a tab-completable, which would make the whole thing much more discoverable.

I'm not sure that's very easy to do, though.
One option would be to either overload ``Pipeline``s ``__getattr__`` and ``__dir__`` so that you could do
```python
pipeline.logisticregression.coef_
```
That requires some magic and it would not work for the names that contain numbers in them. I would be fine if we don't support names with ``-1`` and ``-2`` in them for now.
But more magic in the pipeline is not great.

We could also overload ``__getattr__`` and ``__dir__`` on ```pipeline.named_steps``.
That would limit the magic to ``named_steps`` which is just for convenience and not used internally anywhere afaik. And we already have an implementation of this: the bunch object.

So we could replace ``named_steps`` with a ``Bunch`` object and get an API that is way more discoverable (and easier on my fingertips ;)

Hints from issue discussion:
I would like to work on this issue
Ipython should provide dict key completion for named_steps assist. also
note that there is a setting in ipython that generically enables completion
across complicated expressions. My Pipeline.pop proposal also makes it
easier to access last estimator attributes, and if that's our focus,
Pipeline.last_estimator_ property is also an option

On 2 Mar 2017 2:56 am, "RAKOTOARISON Herilalaina" <notifications@github.com>
wrote:

> I would like to work on this issue
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/scikit-learn/scikit-learn/issues/8481#issuecomment-283380437>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAEz67yxHFHx6ubtPNueI7VAqF6ssaRPks5rhZURgaJpZM4MPyiG>
> .
>

can you link to your pipeline.pop proposal? I don't think the last estimator is necessarily any more important than the other ones.
@amueller #8448

On 4 Mar 2017 8:38 am, "Andreas Mueller" <notifications@github.com> wrote:

> can you link to your pipeline.pop proposal? I don't think the last
> estimator is necessarily any more important than the other ones.
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/scikit-learn/scikit-learn/issues/8481#issuecomment-284077045>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAEz63C4OZNA6Fub66g8--QMMb0WNw3tks5riIg3gaJpZM4MPyiG>
> .
>

Oh also: the keys are auto-completed but nothing that comes after is.
So I can auto-complete ``'logisticregression'`` in the example above, but not ``coef_``.
fair enough. +1 for a bunch-like behaviour

On 7 Mar 2017 4:19 am, "Andreas Mueller" <notifications@github.com> wrote:

> Oh also: the keys are auto-completed but nothing that comes after is.
> So I can auto-complete 'logisticregression' in the example above, but not
> coef_.
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/scikit-learn/scikit-learn/issues/8481#issuecomment-284466412>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAEz64rxX44drcmZ1CoFlNG0pz0cnnaSks5rjEAvgaJpZM4MPyiG>
> .
>

Commit message: Add comment for named_steps usage

Patch to review:
diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py
index 61d7b12b7564d..31aa78cc9054a 100644
--- a/sklearn/pipeline.py
+++ b/sklearn/pipeline.py
@@ -20,6 +20,7 @@
 from .externals import six
 from .utils import tosequence
 from .utils.metaestimators import if_delegate_has_method
+from .datasets.base import Bunch
 
 __all__ = ['Pipeline', 'FeatureUnion']
 
@@ -122,7 +123,7 @@ class Pipeline(_BasePipeline):
 
     Attributes
     ----------
-    named_steps : dict
+    named_steps : bunch object
         Read-only attribute to access any step parameter by user given name.
         Keys are step names and values are steps parameters.
 
@@ -157,7 +158,12 @@ class Pipeline(_BasePipeline):
     array([False, False,  True,  True, False, False, True,  True, False,
            True,  False,  True,  True, False, True,  False, True, True,
            False, False], dtype=bool)
-
+    >>> # Another way to get selected features chosen by anova_filter
+    >>> anova_svm.named_steps.anova.get_support()
+    ... # doctest: +NORMALIZE_WHITESPACE
+    array([False, False,  True,  True, False, False, True,  True, False,
+           True,  False,  True,  True, False, True,  False, True, True,
+           False, False], dtype=bool)
     """
 
     # BaseEstimator interface
@@ -227,7 +233,8 @@ def _estimator_type(self):
 
     @property
     def named_steps(self):
-        return dict(self.steps)
+        # Use Bunch object to improve autocomplete
+        return Bunch(**dict(self.steps))
 
     @property
     def _final_estimator(self):
diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py
index 33e3128931aff..c2449378828e5 100644
--- a/sklearn/tests/test_pipeline.py
+++ b/sklearn/tests/test_pipeline.py
@@ -509,6 +509,18 @@ def test_set_pipeline_steps():
     assert_raises(TypeError, pipeline.fit_transform, [[1]], [1])
 
 
+def test_pipeline_named_steps():
+    transf = Transf()
+    mult2 = Mult(mult=2)
+    pipeline = Pipeline([('mock', transf), ("mult", mult2)])
+
+    # Test access via named_steps bunch object
+    assert_true('mock' in pipeline.named_steps)
+    assert_true('mock2' not in pipeline.named_steps)
+    assert_true(pipeline.named_steps.mock is transf)
+    assert_true(pipeline.named_steps.mult is mult2)
+
+
 def test_set_pipeline_step_none():
     # Test setting Pipeline steps to None
     X = np.array([[1]])


Code review comments:
diff_hunk: @@ -20,6 +20,7 @@
 from .externals import six
 from .utils import tosequence
 from .utils.metaestimators import if_delegate_has_method
+from .datasets.base import Bunch
path: sklearn/pipeline.py
text: @user1:
I don't think we want a dependency on `datasets`. Move `Bunch` to `utils`.