Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
39d410d
fix cross_val_predict for binary classification in decision_function
reiinakano Aug 21, 2017
fc04cee
Add unit tests
reiinakano Aug 22, 2017
9ea3baa
Add unit tests
reiinakano Aug 22, 2017
115e766
Add unit tests
reiinakano Aug 22, 2017
b012790
better fix
reiinakano Aug 22, 2017
a0acb06
fix conflict
reiinakano Aug 22, 2017
1938da5
Merge branch 'master' into fix-cross-val-predict-decision-function
reiinakano Aug 22, 2017
08bc4f9
fix broken
reiinakano Aug 22, 2017
e05fc2e
Merge remote-tracking branch 'origin/fix-cross-val-predict-decision-f…
reiinakano Aug 22, 2017
bfee769
only calculate n_classes if one of 'decision_function', 'predict_prob…
reiinakano Aug 22, 2017
975b5e0
add test for SVC ovo in cross_val_predict
reiinakano Aug 22, 2017
c1d0ef4
flake8 fix
reiinakano Aug 22, 2017
a9aa2b2
fix case of ovo and imbalanced folds for binary classification
reiinakano Aug 31, 2017
f1ebbd2
Merge branch 'master' into fix-cross-val-predict-decision-function
reiinakano Aug 31, 2017
b2da0af
change assert_raises to assert_raise_message for ovo case
reiinakano Aug 31, 2017
dd29e0f
fix flake8 linetoo long
reiinakano Aug 31, 2017
e63b6bc
add comments and clearer tests
reiinakano Sep 5, 2017
c87490b
Merge remote-tracking branch 'upstream/master' into fix-cross-val-pre…
reiinakano Sep 6, 2017
720ec6e
Merge remote-tracking branch 'upstream/master' into fix-cross-val-pre…
reiinakano Sep 7, 2017
624ca2c
improve comments and error message for OvO
reiinakano Sep 27, 2017
a0c7f67
fix .format error with L
reiinakano Sep 27, 2017
c3984b1
Merge branch 'master' into fix-cross-val-predict-decision-function
reiinakano Sep 28, 2017
ed3b6a3
use assert_raises_regex for better error message
reiinakano Sep 28, 2017
331d18f
Merge branch 'master' into fix-cross-val-predict-decision-function
reiinakano Oct 3, 2017
8a71ef3
raise error in decision_function special cases. change predict_log_pr…
reiinakano Oct 11, 2017
4c0bc47
fix broken tests due to special cases of decision_function
reiinakano Oct 11, 2017
de3c3bd
add modified test for decision_function behavior that does not trigge…
reiinakano Oct 11, 2017
b3b350a
fix typos
reiinakano Oct 11, 2017
af5f9ad
fix typos
reiinakano Oct 11, 2017
cee4054
escape regex .
reiinakano Oct 11, 2017
7b65450
escape regex .
reiinakano Oct 11, 2017
9f97b89
address comments. one unaddressed comment
reiinakano Oct 12, 2017
d695b95
simplify code
reiinakano Oct 12, 2017
dc42b27
flake
reiinakano Oct 12, 2017
564bf5e
wrong classes range
reiinakano Oct 12, 2017
3b5cb5e
address comments. adjust error message
reiinakano Oct 13, 2017
e5013bd
add warning
reiinakano Oct 13, 2017
75a0c59
change warning to runtimewarning
reiinakano Oct 13, 2017
042e45f
add test for the warning
reiinakano Oct 13, 2017
bc405c8
Use assert_warns_message rather than assert_warns
lesteve Oct 17, 2017
e3b963d
Note on class-absent replacement values
jnothman Oct 18, 2017
6639b79
Improve error message
jnothman Oct 18, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 52 additions & 6 deletions sklearn/model_selection/_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,15 @@ def cross_val_predict(estimator, X, y=None, groups=None, cv=None, n_jobs=1,
predictions : ndarray
This is the result of calling ``method``

Notes
-----
In the case that one or more classes are absent in a training portion, a
default score needs to be assigned to all instances for that class if
``method`` produces columns per class, as in {'decision_function',
'predict_proba', 'predict_log_proba'}. For ``predict_proba`` this value is
0. In order to ensure finite output, we approximate negative infinity by
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 this behaviour and note is sufficient for a first release.

the minimum finite float value for the dtype in other cases.

Examples
--------
>>> from sklearn import datasets, linear_model
Expand Down Expand Up @@ -727,12 +736,49 @@ def _fit_and_predict(estimator, X, y, train, test, verbose, fit_params,
predictions = func(X_test)
if method in ['decision_function', 'predict_proba', 'predict_log_proba']:
n_classes = len(set(y))
predictions_ = np.zeros((_num_samples(X_test), n_classes))
if method == 'decision_function' and len(estimator.classes_) == 2:
predictions_[:, estimator.classes_[-1]] = predictions
else:
predictions_[:, estimator.classes_] = predictions
predictions = predictions_
if n_classes != len(estimator.classes_):
Copy link
Member

Choose a reason for hiding this comment

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

Should we raise a warning in this case?

Copy link
Member

Choose a reason for hiding this comment

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

This implicitly checks whether n_classes > 2, right? If there was an estimator that would work for 1 class, the bug would still appear, right? Should we add an explicit check that n_classes > 2 to guard against that?

Copy link
Member

Choose a reason for hiding this comment

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

I don't see where the ovo case is addressed. We should also raise an error if predictions.shape[1] != len(estimator.n_classes_), right?

The test for this case is in test_cross_val_predict_with_method. I suggest we add a test with SVC(decision_function_shape='ovo') and make sure a helpful error is thrown.

Copy link
Contributor Author

@reiinakano reiinakano Aug 30, 2017

Choose a reason for hiding this comment

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

@amueller I agree that a warning should be raised in this case.

I don't follow your statement about n_classes > 2. Which bug are you referring to? Could you give a specific example?

For your third point, what I've fixed here is the following regression of cross_val_predict behavior when decision_function_shape=ovo

Code to test:

from sklearn.datasets import load_digits
from sklearn.svm import SVC
from sklearn.model_selection import cross_val_predict

X, y = load_digits(return_X_y=True)
cross_val_predict(SVC(kernel='linear', decision_function_shape='ovo'), X, y, method='decision_function').shape

Expected Results
In previous versions, this is (1797, 45)

Actual Results

ValueError: shape mismatch: value array of shape (602,45) could not be broadcast to indexing result of shape (10,602)

I've also included the test for the ovo case in Line 792-798 in test_validation. Should I move this somewhere else?

Basically, the logic is that if the folds are properly stratified (n_classes == len(estimator.classes_)), it doesn't need to go through all the rest of the code (that handles special cases of n_classes != len(estimator.classes_)) and the predictions can be passed as is without reshaping. This is what caused the two regressions referenced in this PR.

I also acknowledge that the ovo case still throws an error if the folds are not properly stratified. However, this has always been the case even in previous versions, and is due to the weird shape of decision_function for ovo.

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 acknowledge that the ovo case still throws an error if the folds are not properly stratified. However, this has always been the case even in previous versions, and is due to the weird shape of decision_function for ovo.

That was the case I meant. I just wanted a better error in this case, and I want a test for that error.

recommendation = (
'To fix this, use a cross-validation '
'technique resulting in properly '
'stratified folds')
warnings.warn('Number of classes in training fold ({}) does '
'not match total number of classes ({}). '
'Results may not be appropriate for your use case. '
'{}'.format(len(estimator.classes_),
n_classes, recommendation),
RuntimeWarning)
if method == 'decision_function':
if (predictions.ndim == 2 and
predictions.shape[1] != len(estimator.classes_)):
# This handles the case when the shape of predictions
# does not match the number of classes used to train
# it with. This case is found when sklearn.svm.SVC is
# set to `decision_function_shape='ovo'`.
raise ValueError('Output shape {} of {} does not match '
'number of classes ({}) in fold. '
'Irregular decision_function outputs '
'are not currently supported by '
'cross_val_predict'.format(
predictions.shape, method,
len(estimator.classes_),
recommendation))
if len(estimator.classes_) <= 2:
# In this special case, `predictions` contains a 1D array.
raise ValueError('Only {} class/es in training fold, this '
'is not supported for decision_function '
'with imbalanced folds. {}'.format(
len(estimator.classes_),
recommendation))

float_min = np.finfo(predictions.dtype).min
default_values = {'decision_function': float_min,
'predict_log_proba': float_min,
'predict_proba': 0}
predictions_for_all_classes = np.full((_num_samples(predictions),
n_classes),
default_values[method])
predictions_for_all_classes[:, estimator.classes_] = predictions
predictions = predictions_for_all_classes
return predictions, test


Expand Down
104 changes: 95 additions & 9 deletions sklearn/model_selection/tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from sklearn.utils.testing import assert_array_almost_equal
from sklearn.utils.testing import assert_array_equal
from sklearn.utils.testing import assert_warns
from sklearn.utils.testing import assert_warns_message
from sklearn.utils.mocking import CheckingClassifier, MockDataFrame

from sklearn.model_selection import cross_val_score
Expand All @@ -42,6 +43,7 @@
from sklearn.datasets import make_regression
from sklearn.datasets import load_boston
from sklearn.datasets import load_iris
from sklearn.datasets import load_digits
from sklearn.metrics import explained_variance_score
from sklearn.metrics import make_scorer
from sklearn.metrics import accuracy_score
Expand All @@ -52,7 +54,7 @@
from sklearn.metrics.scorer import check_scoring

from sklearn.linear_model import Ridge, LogisticRegression, SGDClassifier
from sklearn.linear_model import PassiveAggressiveClassifier
from sklearn.linear_model import PassiveAggressiveClassifier, RidgeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.cluster import KMeans
Expand Down Expand Up @@ -776,6 +778,89 @@ def split(self, X, y=None, groups=None):

assert_raises(ValueError, cross_val_predict, est, X, y, cv=BadCV())

X, y = load_iris(return_X_y=True)

warning_message = ('Number of classes in training fold (2) does '
'not match total number of classes (3). '
'Results may not be appropriate for your use case.')
assert_warns_message(RuntimeWarning, warning_message,
cross_val_predict, LogisticRegression(),
X, y, method='predict_proba', cv=KFold(2))


def test_cross_val_predict_decision_function_shape():
X, y = make_classification(n_classes=2, n_samples=50, random_state=0)

preds = cross_val_predict(LogisticRegression(), X, y,
method='decision_function')
assert_equal(preds.shape, (50,))

X, y = load_iris(return_X_y=True)
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason you can not use make_classification with n_classes=3?

Copy link
Contributor Author

@reiinakano reiinakano Sep 28, 2017

Choose a reason for hiding this comment

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

No reason, except that it flows naturally into the next test where I need a balanced binary classification dataset sorted by label. Should I just use a make_classification call here?

Copy link
Member

@lesteve lesteve Sep 28, 2017

Choose a reason for hiding this comment

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

Yeah I saw that after I commented, I think it's fine, leave it as it is.


preds = cross_val_predict(LogisticRegression(), X, y,
method='decision_function')
assert_equal(preds.shape, (150, 3))

# This specifically tests imbalanced splits for binary
# classification with decision_function. This is only
# applicable to classifiers that can be fit on a single
# class.
X = X[:100]
y = y[:100]
assert_raise_message(ValueError,
'Only 1 class/es in training fold, this'
' is not supported for decision_function'
' with imbalanced folds. To fix '
'this, use a cross-validation technique '
'resulting in properly stratified folds',
cross_val_predict, RidgeClassifier(), X, y,
method='decision_function', cv=KFold(2))

X, y = load_digits(return_X_y=True)
est = SVC(kernel='linear', decision_function_shape='ovo')

preds = cross_val_predict(est,
X, y,
method='decision_function')
assert_equal(preds.shape, (1797, 45))

ind = np.argsort(y)
X, y = X[ind], y[ind]
assert_raises_regex(ValueError,
'Output shape \(599L?, 21L?\) of decision_function '
'does not match number of classes \(7\) in fold. '
'Irregular decision_function .*',
cross_val_predict, est, X, y,
cv=KFold(n_splits=3), method='decision_function')


def test_cross_val_predict_predict_proba_shape():
X, y = make_classification(n_classes=2, n_samples=50, random_state=0)

preds = cross_val_predict(LogisticRegression(), X, y,
method='predict_proba')
assert_equal(preds.shape, (50, 2))

X, y = load_iris(return_X_y=True)

preds = cross_val_predict(LogisticRegression(), X, y,
method='predict_proba')
assert_equal(preds.shape, (150, 3))


def test_cross_val_predict_predict_log_proba_shape():
X, y = make_classification(n_classes=2, n_samples=50, random_state=0)

preds = cross_val_predict(LogisticRegression(), X, y,
method='predict_log_proba')
assert_equal(preds.shape, (50, 2))

X, y = load_iris(return_X_y=True)

preds = cross_val_predict(LogisticRegression(), X, y,
method='predict_log_proba')
assert_equal(preds.shape, (150, 3))


def test_cross_val_predict_input_types():
iris = load_iris()
Expand Down Expand Up @@ -1217,21 +1302,22 @@ def get_expected_predictions(X, y, cv, classes, est, method):
est.fit(X[train], y[train])
expected_predictions_ = func(X[test])
# To avoid 2 dimensional indexing
exp_pred_test = np.zeros((len(test), classes))
if method is 'decision_function' and len(est.classes_) == 2:
exp_pred_test[:, est.classes_[-1]] = expected_predictions_
if method is 'predict_proba':
exp_pred_test = np.zeros((len(test), classes))
else:
exp_pred_test[:, est.classes_] = expected_predictions_
exp_pred_test = np.full((len(test), classes),
np.finfo(expected_predictions.dtype).min)
exp_pred_test[:, est.classes_] = expected_predictions_
expected_predictions[test] = exp_pred_test

return expected_predictions


def test_cross_val_predict_class_subset():

X = np.arange(8).reshape(4, 2)
y = np.array([0, 0, 1, 2])
classes = 3
X = np.arange(200).reshape(100, 2)
y = np.array([x//10 for x in range(100)])
classes = 10

kfold3 = KFold(n_splits=3)
kfold4 = KFold(n_splits=4)
Expand Down Expand Up @@ -1259,7 +1345,7 @@ def test_cross_val_predict_class_subset():
assert_array_almost_equal(expected_predictions, predictions)

# Testing unordered labels
y = [1, 1, -4, 6]
y = shuffle(np.repeat(range(10), 10), random_state=0)
predictions = cross_val_predict(est, X, y, method=method,
cv=kfold3)
y = le.fit_transform(y)
Expand Down