From 1cb237fa22a3bee8408fe46e9a8ad59902ebe3d3 Mon Sep 17 00:00:00 2001 From: ryan-wolbeck Date: Mon, 23 Mar 2026 22:12:14 -0500 Subject: [PATCH 1/3] fix validation fraction api --- RELEASE_NOTES.md | 4 ++++ ngboost/api.py | 10 ++++++++++ pyproject.toml | 2 +- tests/test_basic.py | 19 +++++++++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7d2a6297..bc82022b 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,9 @@ # RELEASE NOTES +## Version 0.5.11 + +* Fix `NGBClassifier` API parity by adding `validation_fraction` and `early_stopping_rounds`, with regression coverage for the validation-split path (issue #402) + ## Version 0.5.10 * Add `load_ngboost_model` compatibility loader for models saved with scikit-learn < 1.3 and loaded under newer scikit-learn versions (issue #389) diff --git a/ngboost/api.py b/ngboost/api.py index 085bd942..a13f09f7 100644 --- a/ngboost/api.py +++ b/ngboost/api.py @@ -139,6 +139,12 @@ class NGBClassifier(NGBoost, BaseEstimator): tol : numerical tolerance to be used in optimization random_state : seed for reproducibility. See https://stackoverflow.com/questions/28064634/random-state-pseudo-random-number-in-scikit-learn + validation_fraction: Proportion of training data to set + aside as validation data for early stopping. + early_stopping_rounds: The number of consecutive boosting iterations during which the + loss has to increase before the algorithm stops early. + Set to None to disable early stopping and validation. + None enables running over the full data set. Output: An NGBClassifier object that can be fit. """ @@ -158,6 +164,8 @@ def __init__( verbose_eval=100, tol=1e-4, random_state=None, + validation_fraction=0.1, + early_stopping_rounds=None, ): assert issubclass( Dist, ClassificationDistn @@ -175,6 +183,8 @@ def __init__( verbose_eval, tol, random_state, + validation_fraction, + early_stopping_rounds, ) self._estimator_type = "classifier" diff --git a/pyproject.toml b/pyproject.toml index 8733ac79..eda7554e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ngboost" -version = "0.5.10dev" +version = "0.5.11dev" description = "Library for probabilistic predictions via gradient boosting." authors = ["Stanford ML Group "] readme = "README.md" diff --git a/tests/test_basic.py b/tests/test_basic.py index 59c9ea73..bfe437a6 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -54,3 +54,22 @@ def test_regression(california_housing_data): score = mean_squared_error(y_test, preds) assert score <= 15 + + +def test_classifier_validation_fraction_is_supported(breast_cancer_data): + x_train, x_test, y_train, _ = breast_cancer_data + ngb = NGBClassifier( + Dist=Bernoulli, + n_estimators=25, + verbose=False, + random_state=1, + validation_fraction=0.2, + early_stopping_rounds=2, + ) + + assert ngb.get_params()["validation_fraction"] == 0.2 + assert ngb.get_params()["early_stopping_rounds"] == 2 + + ngb.fit(x_train, y_train) + preds = ngb.predict(x_test) + assert preds.shape[0] == x_test.shape[0] From 7622bc3708edbf5f945da47b174cac7411c601ca Mon Sep 17 00:00:00 2001 From: ryan-wolbeck Date: Tue, 23 Jun 2026 16:49:06 -0500 Subject: [PATCH 2/3] update release notes for PRs 409 and 411 --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index bc82022b..7f9509aa 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,6 +3,8 @@ ## Version 0.5.11 * Fix `NGBClassifier` API parity by adding `validation_fraction` and `early_stopping_rounds`, with regression coverage for the validation-split path (issue #402) +* Replace the deprecated `friedman_mse` criterion with `squared_error` for the default tree learner and distribution tests (issue #408, PR #409) +* Support a distinct base learner for each distribution parameter, including scikit-learn nested parameters and feature importances for all-tree configurations (issue #338, PR #411) ## Version 0.5.10 From a4aaa91a48fc92b66f183aa73da2dc3697a39ed0 Mon Sep 17 00:00:00 2001 From: ryan-wolbeck Date: Tue, 23 Jun 2026 17:00:05 -0500 Subject: [PATCH 3/3] update NGBSurvivial --- RELEASE_NOTES.md | 2 +- ngboost/api.py | 12 +++++++++++- tests/test_basic.py | 22 +++++++++++++++++++++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7f9509aa..4ce1721f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,7 +2,7 @@ ## Version 0.5.11 -* Fix `NGBClassifier` API parity by adding `validation_fraction` and `early_stopping_rounds`, with regression coverage for the validation-split path (issue #402) +* Fix `NGBClassifier` and `NGBSurvival` API parity by adding `validation_fraction` and `early_stopping_rounds`, with regression coverage for their validation-split paths (issue #402) * Replace the deprecated `friedman_mse` criterion with `squared_error` for the default tree learner and distribution tests (issue #408, PR #409) * Support a distinct base learner for each distribution parameter, including scikit-learn nested parameters and feature importances for all-tree configurations (issue #338, PR #411) diff --git a/ngboost/api.py b/ngboost/api.py index a980d3d1..84d5413e 100644 --- a/ngboost/api.py +++ b/ngboost/api.py @@ -265,11 +265,17 @@ class NGBSurvival(NGBoost, BaseEstimator): tol : numerical tolerance to be used in optimization random_state : seed for reproducibility. See https://stackoverflow.com/questions/28064634/random-state-pseudo-random-number-in-scikit-learn + validation_fraction: Proportion of training data to set + aside as validation data for early stopping. + early_stopping_rounds: The number of consecutive boosting iterations during which the + loss has to increase before the algorithm stops early. + Set to None to disable early stopping and validation. + None enables running over the full data set. Output: An NGBSurvival object that can be fit. """ - # pylint: disable=too-many-positional-arguments + # pylint: disable=too-many-positional-arguments,too-many-locals def __init__( self, Dist=LogNormal, @@ -284,6 +290,8 @@ def __init__( verbose_eval=100, tol=1e-4, random_state=None, + validation_fraction=0.1, + early_stopping_rounds=None, ): assert issubclass( @@ -310,6 +318,8 @@ def __init__( verbose_eval, tol, random_state, + validation_fraction, + early_stopping_rounds, ) def __getstate__(self): diff --git a/tests/test_basic.py b/tests/test_basic.py index ed00ca3d..2f65ff15 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -5,7 +5,7 @@ from sklearn.linear_model import Ridge from sklearn.tree import DecisionTreeRegressor -from ngboost import NGBClassifier, NGBRegressor +from ngboost import NGBClassifier, NGBRegressor, NGBSurvival from ngboost.distns import Bernoulli, Normal, k_categorical @@ -93,6 +93,26 @@ def test_classifier_validation_fraction_is_supported(breast_cancer_data): assert preds.shape[0] == x_test.shape[0] +def test_survival_validation_fraction_is_supported( + california_housing_survival_data, +): + x_train, x_test, t_train, e_train, _ = california_housing_survival_data + ngb = NGBSurvival( + n_estimators=5, + verbose=False, + random_state=1, + validation_fraction=0.2, + early_stopping_rounds=2, + ) + + assert ngb.get_params()["validation_fraction"] == 0.2 + assert ngb.get_params()["early_stopping_rounds"] == 2 + + ngb.fit(x_train, t_train, e_train) + preds = ngb.predict(x_test) + assert preds.shape[0] == x_test.shape[0] + + def test_regression_accepts_base_learner_per_distribution_parameter(): rng = np.random.default_rng(0) X = rng.normal(size=(80, 4))