From 76d4562ee92ba64f9a8f57e7c265877a1f5d347a Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Fri, 26 Dec 2025 03:49:18 +0000 Subject: [PATCH] fix(hgb): decode string labels for scorer --- .../gradient_boosting.py | 17 +++++++- .../tests/test_gradient_boosting.py | 42 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py b/sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py index ad6a5a8ca381b..575db0b490865 100644 --- a/sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py +++ b/sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py @@ -426,13 +426,26 @@ def _check_early_stopping_scorer(self, X_binned_small_train, y_small_train, Scores are computed on validation data or on training data. """ + y_small_train_for_score = y_small_train + y_val_for_score = y_val + + if is_classifier(self): + y_small_train_for_score = self.classes_[ + y_small_train.astype(np.intp, copy=False) + ] + + if y_val is not None: + y_val_for_score = self.classes_[ + y_val.astype(np.intp, copy=False) + ] + self.train_score_.append( - self.scorer_(self, X_binned_small_train, y_small_train) + self.scorer_(self, X_binned_small_train, y_small_train_for_score) ) if self._use_validation_data: self.validation_score_.append( - self.scorer_(self, X_binned_val, y_val) + self.scorer_(self, X_binned_val, y_val_for_score) ) return self._should_stop(self.validation_score_) else: diff --git a/sklearn/ensemble/_hist_gradient_boosting/tests/test_gradient_boosting.py b/sklearn/ensemble/_hist_gradient_boosting/tests/test_gradient_boosting.py index 1eebdefd5288d..937172c9e009b 100644 --- a/sklearn/ensemble/_hist_gradient_boosting/tests/test_gradient_boosting.py +++ b/sklearn/ensemble/_hist_gradient_boosting/tests/test_gradient_boosting.py @@ -132,6 +132,48 @@ def test_early_stopping_classification(data, scoring, validation_fraction, assert gb.n_iter_ == max_iter +def test_early_stopping_string_labels_binary(): + X, y_numeric = make_classification(n_samples=80, n_features=5, + n_informative=4, n_redundant=0, + n_classes=2, random_state=0) + string_labels = np.array(['class_a', 'class_b'], dtype=object) + y = string_labels[y_numeric] + + gb = HistGradientBoostingClassifier( + scoring='accuracy', + validation_fraction=0.2, + n_iter_no_change=5, + max_iter=30, + random_state=0, + ) + gb.fit(X, y) + + assert set(gb.classes_) == {'class_a', 'class_b'} + assert len(gb.validation_score_) > 0 + + +def test_early_stopping_string_labels_multiclass(): + X, y_numeric = make_classification(n_samples=90, n_features=6, + n_informative=5, n_redundant=0, + n_repeated=0, n_classes=3, + n_clusters_per_class=1, + random_state=0) + string_labels = np.array(['class_a', 'class_b', 'class_c'], dtype=object) + y = string_labels[y_numeric] + + gb = HistGradientBoostingClassifier( + scoring='accuracy', + validation_fraction=None, + n_iter_no_change=5, + max_iter=30, + random_state=0, + ) + gb.fit(X, y) + + assert set(gb.classes_) == {'class_a', 'class_b', 'class_c'} + assert len(gb.train_score_) > 0 + + @pytest.mark.parametrize( 'scores, n_iter_no_change, tol, stopping', [