Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MRG+1] add option to cross_validate to return estimators fitted on each split #9686

Merged
merged 13 commits into from
Feb 27, 2018

Conversation

bellet
Copy link
Contributor

@bellet bellet commented Sep 4, 2017

Reference Issues

Fixes #6827 (alternative to #9496)

What does this implement/fix? Explain your changes.

This PR adds an option to cross_validate so that it returns a list of the estimators fitted on each split. This list is an additional entry to the returned dictionary.

Any other comments?

I simply wrote some basic code implementing the change. If core devs reach some consensus in favor of this solution, I will go ahead and improve the code, add tests and update the doc.

@jnothman
Copy link
Member

jnothman commented Sep 4, 2017 via email

Copy link
Member

@jnothman jnothman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with this kind of change... But I'm not sure if we will find consensus, and @GaelVaroquaux has generally been against giving the user the kitchen sink (to use a strange English expression for "everything and more"). It also has the potential to snowball and become a feature request in grid search. Even if it does not take excessive memory here, it may there. And someone will complain that it falls to return some un-picklable they have created.

Thanks for making this concrete, but let's consider an alternative: Is there a way to instead allow the user to specify a filename format and dump the estimator to disk?

train_scores, test_scores, fit_times, score_times = zip(*scores)
if return_estimator:
(train_scores, test_scores, fit_times, score_times,
fitted_est) = zip(*scores)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For good or bad, all the other return values here are named in plural. This should be consistent unless there's a very good reason not to be.

for train, test in cv.split(X, y, groups))

if return_train_score:
train_scores, test_scores, fit_times, score_times = zip(*scores)
if return_estimator:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There must be a neater way to write this without nested ifs. But alas I'm not sure what it is.

@bellet
Copy link
Contributor Author

bellet commented Sep 5, 2017

Or a callback taking a fitted estimator as input?

@jnothman
Copy link
Member

jnothman commented Sep 5, 2017

I'm afraid the callback approach is plagued by the potential for other parameters: passing the training data, the test data, the fold no, etc. And then we become a framework.

@bellet
Copy link
Contributor Author

bellet commented Sep 5, 2017

Fair enough. I can implement dumping the estimator to disk option if there is some consensus that this is the way to go?

@GaelVaroquaux
Copy link
Member

GaelVaroquaux commented Sep 5, 2017 via email

@bellet
Copy link
Contributor Author

bellet commented Sep 7, 2017

@GaelVaroquaux you are OK with the solution provided by this PR or with the idea of adding an option to dump to file? In the first case we do not need to pickle anything, do we?

@GaelVaroquaux
Copy link
Member

@bellet : I am OK with the solution provided by this PR. I would say that the issues raise by @jnothman need to be addressed, and a test needs to be added, and I would be +1 for merging.

@jnothman
Copy link
Member

jnothman commented Sep 7, 2017 via email

@GaelVaroquaux
Copy link
Member

GaelVaroquaux commented Sep 7, 2017 via email

@jnothman
Copy link
Member

jnothman commented Sep 7, 2017 via email

@bellet
Copy link
Contributor Author

bellet commented Sep 13, 2017

OK I will go ahead with this solution and fix the issues you raised and we can then see who is OK with merging

@bellet
Copy link
Contributor Author

bellet commented Sep 18, 2017

I have addressed the issues raised by @jnothman (not sure whether my alternative to the nested ifs is neater...) and added a test. Comments welcome

@bellet
Copy link
Contributor Author

bellet commented Sep 20, 2017

I do not really understand why the code coverage fails. Unless I misunderstand the report it seems that all of my changes are covered...

@bellet
Copy link
Contributor Author

bellet commented Sep 22, 2017

@jnothman can you see what's wrong?

Copy link
Member

@jnothman jnothman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it's surprising that there's no coverage... perhaps that test is somehow not being run. Put an assert False in with your assertions just to check...?

@@ -489,6 +511,8 @@ def _fit_and_score(estimator, X, y, scorer, train, test, verbose,
ret.extend([fit_time, score_time])
if return_parameters:
ret.append(parameters)
if return_estimator:
ret.append(estimator)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently coverage is missing from this line!

Copy link
Contributor Author

@bellet bellet Sep 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added an assert False along with my assert_almost_equal on the estimator parameters and this makes the test fail, so they are run. Hence it doesn't make sense that this line is not covered, otherwise how can the test pass?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps put the assert False here just to prove the coverage tool wrong? If it does not fail, you've got some investigation to do...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, just did this and the test now fails. Should I commit this for the sake of it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How strange...

@jnothman
Copy link
Member

Do you believe there's more to do here before full review? If not, relabel WIP -> MRG please.

@bellet bellet changed the title [WIP] add option to cross_validate to return estimators fitted on each split [MRG] add option to cross_validate to return estimators fitted on each split Oct 17, 2017
@bellet bellet changed the title [MRG] add option to cross_validate to return estimators fitted on each split [WIP] add option to cross_validate to return estimators fitted on each split Oct 17, 2017
@bellet
Copy link
Contributor Author

bellet commented Oct 17, 2017

I actually need to add the new option to the doc

Copy link
Member

@jnothman jnothman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise LGTM

@@ -140,6 +144,8 @@ def cross_validate(estimator, X, y=None, groups=None, scoring=None, cv=None,
The time for scoring the estimator on the test set for each
cv split. (Note time for scoring on the train set is not
included even if ``return_train_score`` is set to ``True``
``estimator``
The list of estimator objects for each cv split.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop "list of" for consistency

if return_train_score:
train_scores, test_scores, fit_times, score_times = zip(*scores)
train_scores = zipped_scores[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think = zipped_scores.pop(0) and similar below will help simplify this logic.

@bellet bellet changed the title [WIP] add option to cross_validate to return estimators fitted on each split [MRG] add option to cross_validate to return estimators fitted on each split Oct 17, 2017
@bellet
Copy link
Contributor Author

bellet commented Oct 17, 2017

Thanks for the comment, this indeed simplifies things a bit. I also briefly mentioned the new option in the doc.

@@ -196,6 +196,8 @@ following keys -
for all the scorers. If train scores are not needed, this should be set to
``False`` explicitly.

``return_estimator`` is set to ``False`` by default. When set to ``True``, it adds an ``estimator`` key containing the estimators fitted on each split.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please limit lines to under 80 characters

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think this is somewhat unnecessary. Or I'd state it more in terms of use case rather than API: "you can also retain to estimator fitted for each training set with return_estimator=True"

train_scores = _aggregate_score_dicts(train_scores)
if return_estimator:
(test_scores, fit_times, score_times,
fitted_estimators) = zipped_scores
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use pop here too and remove the else

@@ -196,6 +196,8 @@ following keys -
for all the scorers. If train scores are not needed, this should be set to
``False`` explicitly.

``return_estimator`` is set to ``False`` by default. When set to ``True``, it adds an ``estimator`` key containing the estimators fitted on each split.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think this is somewhat unnecessary. Or I'd state it more in terms of use case rather than API: "you can also retain to estimator fitted for each training set with return_estimator=True"

@jnothman jnothman changed the title [MRG] add option to cross_validate to return estimators fitted on each split [MRG+1] add option to cross_validate to return estimators fitted on each split Oct 17, 2017
@bellet
Copy link
Contributor Author

bellet commented Oct 24, 2017

@GaelVaroquaux do you have additional comments? thanks!

@amueller
Copy link
Member

travis is failing.

Copy link
Member

@amueller amueller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM apart from minor nitpicks and doctest failures.

@@ -182,7 +182,7 @@ The ``cross_validate`` function differs from ``cross_val_score`` in two ways -

- It allows specifying multiple metrics for evaluation.

- It returns a dict containing training scores, fit-times and score-times in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optionally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -149,6 +153,8 @@ def cross_validate(estimator, X, y=None, groups=None, scoring=None, cv=None,
The time for scoring the estimator on the test set for each
cv split. (Note time for scoring on the train set is not
included even if ``return_train_score`` is set to ``True``
``estimator``
The estimator objects for each cv split.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if return_estimator is True.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@bellet
Copy link
Contributor Author

bellet commented Oct 26, 2017

@amueller for travis failure see above discussion #9686 (review) with @jnothman
I could not find an explanation for this behavior. The test passes on my machine for both Python 2 and 3.

@@ -227,8 +231,9 @@ Here is an example of ``cross_validate`` using a single metric::

>>> scores = cross_validate(clf, iris.data, iris.target,
... scoring='precision_macro')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should have a comma at the end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, nice catch!

@bellet
Copy link
Contributor Author

bellet commented Nov 16, 2017

@amueller do you have other requests? thanks!

@amueller
Copy link
Member

travis is failing because of doctests.

@bellet
Copy link
Contributor Author

bellet commented Dec 16, 2017

How do I locate which doctests fail? All tests pass when I run nosetests on sklearn/model_selection/_validation.py (except for a warning which is not related to this PR).

nosetests sklearn/model_selection/_validation.py  --with-doctest -v
Doctest: sklearn.model_selection._validation._aggregate_score_dicts ... ok
Doctest: sklearn.model_selection._validation.cross_val_predict ... ok
Doctest: sklearn.model_selection._validation.cross_val_score ... ok
Doctest: sklearn.model_selection._validation.cross_validate ... deprecation.py:122: FutureWarning: You are accessing a training score ('train_r2'), which will not be available by default any more in 0.21. If you need training scores, please set return_train_score=True
  warnings.warn(*warn_args, **warn_kwargs)
ok

----------------------------------------------------------------------
Ran 4 tests in 0.109s

OK

@bellet
Copy link
Contributor Author

bellet commented Dec 16, 2017

OK it seems it was only the documentation thing. All checks pass now @amueller

@bellet
Copy link
Contributor Author

bellet commented Feb 12, 2018

@amueller @jnothman @GaelVaroquaux anything else should be done here?

@jnothman
Copy link
Member

Please add an Enhancements entry to the change log at doc/whats_new/v0.20.rst. Like the other entries there, please reference this pull request with :issue: and credit yourself (and other contributors if applicable) with :user:

@jnothman
Copy link
Member

or maybe this is a New Feature. Otherwise, this is good to merge.

@bellet
Copy link
Contributor Author

bellet commented Feb 27, 2018

@jnothman done, thanks!

@jnothman jnothman merged commit d9c2122 into scikit-learn:master Feb 27, 2018
@jnothman
Copy link
Member

Thanks @bellet!

@bellet bellet deleted the i/6827-2 branch February 27, 2018 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow cross_val_score to return model from each fit?
4 participants