ngboost 0.5.10.dev0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/PKG-INFO +6 -6
  2. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/api.py +160 -11
  3. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/exponential.py +5 -4
  4. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/laplace.py +4 -2
  5. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/lognormal.py +3 -10
  6. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/normal.py +3 -2
  7. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/t.py +1 -1
  8. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/learners.py +1 -1
  9. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/ngboost.py +217 -25
  10. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/pyproject.toml +10 -7
  11. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/LICENSE +0 -0
  12. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/README.md +0 -0
  13. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/__init__.py +0 -0
  14. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/__init__.py +0 -0
  15. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/beta.py +0 -0
  16. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/beta_bernoulli.py +0 -0
  17. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/betabinomial.py +0 -0
  18. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/categorical.py +0 -0
  19. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/cauchy.py +0 -0
  20. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/distn.py +0 -0
  21. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/gamma.py +0 -0
  22. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/halfnormal.py +0 -0
  23. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/logitnormal.py +0 -0
  24. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/multivariate_normal.py +0 -0
  25. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/poisson.py +0 -0
  26. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/sympy_utils.py +0 -0
  27. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/utils.py +0 -0
  28. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/distns/weibull.py +0 -0
  29. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/evaluation.py +0 -0
  30. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/helpers.py +0 -0
  31. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/manifold.py +0 -0
  32. {ngboost-0.5.10.dev0 → ngboost-0.6.0}/ngboost/scores.py +0 -0
@@ -1,25 +1,25 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ngboost
3
- Version: 0.5.10.dev0
3
+ Version: 0.6.0
4
4
  Summary: Library for probabilistic predictions via gradient boosting.
5
5
  License: Apache-2.0
6
6
  Author: Stanford ML Group
7
7
  Author-email: avati@cs.stanford.edu
8
- Requires-Python: >=3.9,<3.15
8
+ Requires-Python: >=3.10,<3.15
9
9
  Classifier: License :: OSI Approved :: Apache Software License
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.9
13
12
  Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
16
15
  Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Dist: joblib (>=1.2)
17
17
  Requires-Dist: lifelines (>=0.25)
18
- Requires-Dist: matplotlib (>=3.0,<3.10) ; python_version < "3.10"
19
- Requires-Dist: matplotlib (>=3.10.0) ; python_version >= "3.10"
18
+ Requires-Dist: matplotlib (>=3.10.0)
20
19
  Requires-Dist: numpy (>=1.21.2) ; python_version < "3.13"
21
20
  Requires-Dist: numpy (>=2.1.0) ; python_version >= "3.13"
22
- Requires-Dist: scikit-learn (>=1.6,<2.0)
21
+ Requires-Dist: scikit-learn (>=1.6,<1.7) ; python_version < "3.14"
22
+ Requires-Dist: scikit-learn (>=1.8.0,<2.0) ; python_version >= "3.14"
23
23
  Requires-Dist: scipy (>=1.14.1) ; python_version >= "3.13"
24
24
  Requires-Dist: scipy (>=1.7.2) ; python_version < "3.13"
25
25
  Requires-Dist: sympy (>=1.12)
@@ -1,6 +1,9 @@
1
1
  "The NGBoost library API"
2
+
2
3
  # pylint: disable=too-many-arguments
3
- from sklearn.base import BaseEstimator
4
+ import numpy as np
5
+ from sklearn.base import BaseEstimator, ClassifierMixin
6
+ from sklearn.preprocessing import LabelEncoder
4
7
  from sklearn.utils import check_array
5
8
 
6
9
  from ngboost.distns import (
@@ -31,8 +34,15 @@ class NGBRegressor(NGBoost, BaseEstimator):
31
34
  A distribution from ngboost.distns, e.g. Normal
32
35
  Score : rule to compare probabilistic predictions P̂ to the observed data y.
33
36
  A score from ngboost.scores, e.g. LogScore
34
- Base : base learner to use in the boosting algorithm.
35
- Any instantiated sklearn regressor, e.g. DecisionTreeRegressor()
37
+ Base : base learner(s) to use in the boosting algorithm.
38
+ Pass a single instantiated sklearn regressor, e.g.
39
+ DecisionTreeRegressor(), to use the same learner for
40
+ every distribution parameter. To use different learners
41
+ per distribution parameter, pass a list/tuple of
42
+ instantiated sklearn regressors with length equal to
43
+ Dist.n_params. The sequence order matches the distribution
44
+ parameter order; for example, Normal uses
45
+ [loc_learner, scale_learner].
36
46
  natural_gradient : logical flag indicating whether the natural gradient should be used
37
47
  n_estimators : the number of boosting iterations to fit
38
48
  learning_rate : the learning rate
@@ -49,12 +59,13 @@ class NGBRegressor(NGBoost, BaseEstimator):
49
59
  loss has to increase before the algorithm stops early.
50
60
  Set to None to disable early stopping and validation.
51
61
  None enables running over the full data set.
62
+ n_jobs : see the ``NGBoost`` base class.
52
63
 
53
64
  Output:
54
65
  An NGBRegressor object that can be fit.
55
66
  """
56
67
 
57
- # pylint: disable=too-many-positional-arguments
68
+ # pylint: disable=too-many-positional-arguments,too-many-locals
58
69
  def __init__(
59
70
  self,
60
71
  Dist=Normal,
@@ -71,6 +82,7 @@ class NGBRegressor(NGBoost, BaseEstimator):
71
82
  random_state=None,
72
83
  validation_fraction=0.1,
73
84
  early_stopping_rounds=None,
85
+ n_jobs=None,
74
86
  ):
75
87
  assert issubclass(
76
88
  Dist, RegressionDistn
@@ -96,6 +108,7 @@ class NGBRegressor(NGBoost, BaseEstimator):
96
108
  random_state,
97
109
  validation_fraction,
98
110
  early_stopping_rounds,
111
+ n_jobs,
99
112
  )
100
113
 
101
114
  self._estimator_type = "regressor"
@@ -114,7 +127,7 @@ class NGBRegressor(NGBoost, BaseEstimator):
114
127
  super().__setstate__(state_dict)
115
128
 
116
129
 
117
- class NGBClassifier(NGBoost, BaseEstimator):
130
+ class NGBClassifier(ClassifierMixin, NGBoost, BaseEstimator):
118
131
  """
119
132
  Constructor for NGBoost classification models.
120
133
 
@@ -127,8 +140,15 @@ class NGBClassifier(NGBoost, BaseEstimator):
127
140
  A distribution from ngboost.distns, e.g. Bernoulli
128
141
  Score : rule to compare probabilistic predictions P̂ to the observed data y.
129
142
  A score from ngboost.scores, e.g. LogScore
130
- Base : base learner to use in the boosting algorithm.
131
- Any instantiated sklearn regressor, e.g. DecisionTreeRegressor()
143
+ Base : base learner(s) to use in the boosting algorithm.
144
+ Pass a single instantiated sklearn regressor, e.g.
145
+ DecisionTreeRegressor(), to use the same learner for
146
+ every distribution parameter. To use different learners
147
+ per distribution parameter, pass a list/tuple of
148
+ instantiated sklearn regressors with length equal to
149
+ Dist.n_params. The sequence order matches the distribution
150
+ parameter order; for example, Normal uses
151
+ [loc_learner, scale_learner].
132
152
  natural_gradient : logical flag indicating whether the natural gradient should be used
133
153
  n_estimators : the number of boosting iterations to fit
134
154
  learning_rate : the learning rate
@@ -139,11 +159,18 @@ class NGBClassifier(NGBoost, BaseEstimator):
139
159
  tol : numerical tolerance to be used in optimization
140
160
  random_state : seed for reproducibility. See
141
161
  https://stackoverflow.com/questions/28064634/random-state-pseudo-random-number-in-scikit-learn
162
+ validation_fraction: Proportion of training data to set
163
+ aside as validation data for early stopping.
164
+ early_stopping_rounds: The number of consecutive boosting iterations during which the
165
+ loss has to increase before the algorithm stops early.
166
+ Set to None to disable early stopping and validation.
167
+ None enables running over the full data set.
168
+ n_jobs : see the ``NGBoost`` base class.
142
169
  Output:
143
170
  An NGBClassifier object that can be fit.
144
171
  """
145
172
 
146
- # pylint: disable=too-many-positional-arguments
173
+ # pylint: disable=too-many-positional-arguments,too-many-locals
147
174
  def __init__(
148
175
  self,
149
176
  Dist=Bernoulli,
@@ -158,6 +185,9 @@ class NGBClassifier(NGBoost, BaseEstimator):
158
185
  verbose_eval=100,
159
186
  tol=1e-4,
160
187
  random_state=None,
188
+ validation_fraction=0.1,
189
+ early_stopping_rounds=None,
190
+ n_jobs=None,
161
191
  ):
162
192
  assert issubclass(
163
193
  Dist, ClassificationDistn
@@ -175,9 +205,108 @@ class NGBClassifier(NGBoost, BaseEstimator):
175
205
  verbose_eval,
176
206
  tol,
177
207
  random_state,
208
+ validation_fraction,
209
+ early_stopping_rounds,
210
+ n_jobs,
178
211
  )
179
212
  self._estimator_type = "classifier"
180
213
 
214
+ def _fit_label_encoder(self, Y):
215
+ le = LabelEncoder().fit(Y)
216
+ self._le = le # pylint: disable=attribute-defined-outside-init
217
+ self.classes_ = le.classes_ # pylint: disable=attribute-defined-outside-init
218
+ n_classes = self.Dist.n_params + 1
219
+ if len(self.classes_) != n_classes:
220
+ raise ValueError(
221
+ "NGBClassifier Dist expects "
222
+ f"{n_classes} classes, got {len(self.classes_)}."
223
+ )
224
+
225
+ def __setstate__(
226
+ self, state_dict
227
+ ): # pylint: disable=attribute-defined-outside-init
228
+ super().__setstate__(state_dict)
229
+ if not hasattr(self, "classes_"):
230
+ self.classes_ = np.arange(self.Dist.n_params + 1)
231
+ if not hasattr(self, "_le"):
232
+ self._le = LabelEncoder()
233
+ self._le.classes_ = self.classes_
234
+
235
+ # fit and partial_fit pass their arguments on to NGBoost unchanged, so
236
+ # pylint reads the calls as copies of the one in NGBoost.fit.
237
+ # pylint: disable=too-many-positional-arguments,duplicate-code
238
+ def fit(
239
+ self,
240
+ X,
241
+ Y,
242
+ X_val=None,
243
+ Y_val=None,
244
+ sample_weight=None,
245
+ val_sample_weight=None,
246
+ train_loss_monitor=None,
247
+ val_loss_monitor=None,
248
+ early_stopping_rounds=None,
249
+ ):
250
+ # Learn the classes on every fit. NGBoost.fit then calls partial_fit,
251
+ # which encodes Y and Y_val with them.
252
+ self._fit_label_encoder(Y)
253
+ super().fit(
254
+ X,
255
+ Y,
256
+ X_val=X_val,
257
+ Y_val=Y_val,
258
+ sample_weight=sample_weight,
259
+ val_sample_weight=val_sample_weight,
260
+ train_loss_monitor=train_loss_monitor,
261
+ val_loss_monitor=val_loss_monitor,
262
+ early_stopping_rounds=early_stopping_rounds,
263
+ )
264
+ return self
265
+
266
+ # pylint: disable=too-many-positional-arguments
267
+ def partial_fit(
268
+ self,
269
+ X,
270
+ Y,
271
+ X_val=None,
272
+ Y_val=None,
273
+ sample_weight=None,
274
+ val_sample_weight=None,
275
+ train_loss_monitor=None,
276
+ val_loss_monitor=None,
277
+ early_stopping_rounds=None,
278
+ ):
279
+ if not hasattr(self, "classes_"):
280
+ self._fit_label_encoder(Y)
281
+ Y = self._le.transform(Y)
282
+ if Y_val is not None:
283
+ Y_val = self._le.transform(Y_val)
284
+ super().partial_fit(
285
+ X,
286
+ Y,
287
+ X_val=X_val,
288
+ Y_val=Y_val,
289
+ sample_weight=sample_weight,
290
+ val_sample_weight=val_sample_weight,
291
+ train_loss_monitor=train_loss_monitor,
292
+ val_loss_monitor=val_loss_monitor,
293
+ early_stopping_rounds=early_stopping_rounds,
294
+ )
295
+ return self
296
+
297
+ def predict(self, X, max_iter=None):
298
+ return self.classes_[super().predict(X, max_iter=max_iter)]
299
+
300
+ def staged_predict(self, X, max_iter=None):
301
+ return [
302
+ self.classes_[pred] for pred in super().staged_predict(X, max_iter=max_iter)
303
+ ]
304
+
305
+ def score(self, X, Y): # for sklearn
306
+ # Call NGBoost.score directly: super() would reach ClassifierMixin.score,
307
+ # which returns accuracy.
308
+ return NGBoost.score(self, X, self._le.transform(Y))
309
+
181
310
  def predict_proba(self, X, max_iter=None):
182
311
  """
183
312
  Probability prediction of Y at the points X=x
@@ -221,8 +350,15 @@ class NGBSurvival(NGBoost, BaseEstimator):
221
350
  A distribution from ngboost.distns, e.g. LogNormal
222
351
  Score : rule to compare probabilistic predictions P̂ to the observed data y.
223
352
  A score from ngboost.scores, e.g. LogScore
224
- Base : base learner to use in the boosting algorithm.
225
- Any instantiated sklearn regressor, e.g. DecisionTreeRegressor()
353
+ Base : base learner(s) to use in the boosting algorithm.
354
+ Pass a single instantiated sklearn regressor, e.g.
355
+ DecisionTreeRegressor(), to use the same learner for
356
+ every distribution parameter. To use different learners
357
+ per distribution parameter, pass a list/tuple of
358
+ instantiated sklearn regressors with length equal to
359
+ Dist.n_params. The sequence order matches the distribution
360
+ parameter order; for example, Normal uses
361
+ [loc_learner, scale_learner].
226
362
  natural_gradient : logical flag indicating whether the natural gradient should be used
227
363
  n_estimators : the number of boosting iterations to fit
228
364
  learning_rate : the learning rate
@@ -233,11 +369,18 @@ class NGBSurvival(NGBoost, BaseEstimator):
233
369
  tol : numerical tolerance to be used in optimization
234
370
  random_state : seed for reproducibility. See
235
371
  https://stackoverflow.com/questions/28064634/random-state-pseudo-random-number-in-scikit-learn
372
+ validation_fraction: Proportion of training data to set
373
+ aside as validation data for early stopping.
374
+ early_stopping_rounds: The number of consecutive boosting iterations during which the
375
+ loss has to increase before the algorithm stops early.
376
+ Set to None to disable early stopping and validation.
377
+ None enables running over the full data set.
378
+ n_jobs : see the ``NGBoost`` base class.
236
379
  Output:
237
380
  An NGBSurvival object that can be fit.
238
381
  """
239
382
 
240
- # pylint: disable=too-many-positional-arguments
383
+ # pylint: disable=too-many-positional-arguments,too-many-locals
241
384
  def __init__(
242
385
  self,
243
386
  Dist=LogNormal,
@@ -252,6 +395,9 @@ class NGBSurvival(NGBoost, BaseEstimator):
252
395
  verbose_eval=100,
253
396
  tol=1e-4,
254
397
  random_state=None,
398
+ validation_fraction=0.1,
399
+ early_stopping_rounds=None,
400
+ n_jobs=None,
255
401
  ):
256
402
 
257
403
  assert issubclass(
@@ -278,6 +424,9 @@ class NGBSurvival(NGBoost, BaseEstimator):
278
424
  verbose_eval,
279
425
  tol,
280
426
  random_state,
427
+ validation_fraction,
428
+ early_stopping_rounds,
429
+ n_jobs,
281
430
  )
282
431
 
283
432
  def __getstate__(self):
@@ -32,16 +32,17 @@ class ExponentialCRPScore(CRPScore):
32
32
  def score(self, Y):
33
33
  E, T = Y["Event"], Y["Time"]
34
34
  score = T + self.scale * (2 * np.exp(-T / self.scale) - 1.5)
35
- score[E == 1] -= (
36
- 0.5 * self.scale[E == 1] * np.exp(-2 * T[E == 1] / self.scale[E == 1])
35
+ # a censored observation only scores the CDF up to the censoring time
36
+ score[E == 0] -= (
37
+ 0.5 * self.scale[E == 0] * np.exp(-2 * T[E == 0] / self.scale[E == 0])
37
38
  )
38
39
  return score
39
40
 
40
41
  def d_score(self, Y):
41
42
  E, T = Y["Event"], Y["Time"]
42
43
  deriv = 2 * np.exp(-T / self.scale) * (self.scale + T) - 1.5 * self.scale
43
- deriv[E == 1] -= np.exp(-2 * T[E == 1] / self.scale[E == 1]) * (
44
- 0.5 * self.scale[E == 1] - T[E == 1]
44
+ deriv[E == 0] -= np.exp(-2 * T[E == 0] / self.scale[E == 0]) * (
45
+ 0.5 * self.scale[E == 0] + T[E == 0]
45
46
  )
46
47
  return deriv.reshape((-1, 1))
47
48
 
@@ -37,8 +37,10 @@ class LaplaceCRPScore(CRPScore):
37
37
  D[:, 0] = np.sign(self.loc - Y) * (
38
38
  1 - np.exp(-np.abs(Y - self.loc) / self.scale)
39
39
  )
40
- D[:, 1] = np.exp(-np.abs(Y - self.loc) / self.scale) * (
41
- self.scale + np.abs(Y - self.loc)
40
+ D[:, 1] = (
41
+ np.exp(-np.abs(Y - self.loc) / self.scale)
42
+ * (self.scale + np.abs(Y - self.loc))
43
+ - 0.75 * self.scale
42
44
  )
43
45
  return D
44
46
 
@@ -30,11 +30,7 @@ class LogNormalLogScoreCensored(LogScore):
30
30
  D_cens[:, 0] = -sp.stats.norm.pdf(lT, loc=self.loc, scale=self.scale) / (
31
31
  1 - self.dist.cdf(T) + self.eps
32
32
  )
33
- D_cens[:, 1] = (
34
- -Z
35
- * sp.stats.norm.pdf(lT, loc=self.loc, scale=self.scale)
36
- / (1 - self.dist.cdf(T) + self.eps)
37
- )
33
+ D_cens[:, 1] = -Z * sp.stats.norm.pdf(Z) / (1 - self.dist.cdf(T) + self.eps)
38
34
 
39
35
  return (1 - E) * D_cens + E * D_uncens
40
36
 
@@ -71,12 +67,9 @@ class LogNormalCRPScoreCensored(CRPScore):
71
67
  Z = (lT - self.loc) / self.scale
72
68
 
73
69
  D = np.zeros((self.loc.shape[0], 2))
70
+ # the censored CRPS is scale * integral of Phi(z)**2 up to Z
74
71
  D[:, 0] = E * -(2 * sp.stats.norm.cdf(Z) - 1) + (1 - E) * -(
75
72
  sp.stats.norm.cdf(Z) ** 2
76
- + 2 * Z * sp.stats.norm.cdf(Z) * sp.stats.norm.pdf(Z)
77
- + 2 * sp.stats.norm.pdf(Z) ** 2
78
- - 2 * sp.stats.norm.cdf(Z) * sp.stats.norm.pdf(Z) ** 2
79
- - np.sqrt(2 / np.pi) * sp.stats.norm.pdf(np.sqrt(2) * Z)
80
73
  )
81
74
  D[:, 1] = self.score(Y) + (lT - self.loc) * D[:, 0]
82
75
  return D
@@ -85,7 +78,7 @@ class LogNormalCRPScoreCensored(CRPScore):
85
78
  I = np.zeros((self.loc.shape[0], 2, 2))
86
79
  I[:, 0, 0] = 2
87
80
  I[:, 1, 1] = self.scale**2
88
- I /= 2 * np.sqrt(np.pi)
81
+ I /= 2 * np.sqrt(np.pi) * self.scale[:, np.newaxis, np.newaxis]
89
82
  return I
90
83
 
91
84
 
@@ -49,7 +49,8 @@ class NormalCRPScore(CRPScore):
49
49
  self.var,
50
50
  ]
51
51
  I = I.reshape((self.var.shape[0], 2, 2))
52
- I = 1 / (2 * np.sqrt(np.pi)) * I
52
+ # 2 * integral of grad F grad F^T, which scales like 1 / scale
53
+ I = 1 / (2 * np.sqrt(np.pi) * self.scale[:, np.newaxis, np.newaxis]) * I
53
54
  return I
54
55
 
55
56
 
@@ -185,7 +186,7 @@ class NormalFixedMeanCRPScore(CRPScore):
185
186
  return D
186
187
 
187
188
  def metric(self):
188
- I = np.c_[self.var]
189
+ I = np.c_[self.scale]
189
190
  I = I.reshape((self.var.shape[0], 1, 1))
190
191
  I = 1 / (2 * np.sqrt(np.pi)) * I
191
192
  return I
@@ -103,7 +103,7 @@ class TFixedDfLogScore(LogScore):
103
103
  def metric(self):
104
104
  FI = np.zeros((self.var.shape[0], 2, 2))
105
105
  FI[:, 0, 0] = (self.df + 1) / ((self.df + 3) * self.var)
106
- FI[:, 1, 1] = (self.df) / (2 * (self.df + 3) * self.var)
106
+ FI[:, 1, 1] = 2 * self.df / (self.df + 3)
107
107
  return FI
108
108
 
109
109
 
@@ -2,7 +2,7 @@ from sklearn.linear_model import Ridge
2
2
  from sklearn.tree import DecisionTreeRegressor
3
3
 
4
4
  default_tree_learner = DecisionTreeRegressor(
5
- criterion="friedman_mse",
5
+ criterion="squared_error",
6
6
  min_samples_split=2,
7
7
  min_samples_leaf=1,
8
8
  min_weight_fraction_leaf=0.0,
@@ -5,6 +5,8 @@
5
5
  # pylint: disable=unused-variable,invalid-unary-operand-type,attribute-defined-outside-init
6
6
  # pylint: disable=redundant-keyword-arg,protected-access,unnecessary-lambda-assignment
7
7
  import numpy as np
8
+ from joblib import Parallel, delayed
9
+ from scipy.sparse import issparse
8
10
  from sklearn.base import clone
9
11
  from sklearn.model_selection import train_test_split
10
12
  from sklearn.tree import DecisionTreeRegressor
@@ -16,6 +18,28 @@ from ngboost.manifold import manifold
16
18
  from ngboost.scores import LogScore
17
19
 
18
20
 
21
+ def _validate_sample_weight(sample_weight, X, input_name="sample_weight"):
22
+ """Return sample_weight as a 1-D float array with one entry per row of X.
23
+
24
+ Weights are matched to rows by position, so a pandas index is ignored.
25
+ """
26
+ if sample_weight is None:
27
+ return None
28
+ sample_weight = check_array(
29
+ sample_weight, ensure_2d=False, dtype=np.float64, input_name=input_name
30
+ )
31
+ if sample_weight.ndim != 1:
32
+ raise ValueError(
33
+ f"{input_name} must be 1-D, got an array of shape {sample_weight.shape}"
34
+ )
35
+ if sample_weight.shape[0] != X.shape[0]:
36
+ raise ValueError(
37
+ f"{input_name} must have one entry per row: got {sample_weight.shape[0]} "
38
+ f"entries for {X.shape[0]} rows"
39
+ )
40
+ return sample_weight
41
+
42
+
19
43
  class NGBoost:
20
44
  """
21
45
  Constructor for all NGBoost models.
@@ -29,8 +53,15 @@ class NGBoost:
29
53
  A distribution from ngboost.distns, e.g. Normal
30
54
  Score : rule to compare probabilistic predictions P̂ to the observed data y.
31
55
  A score from ngboost.scores, e.g. LogScore
32
- Base : base learner to use in the boosting algorithm.
33
- Any instantiated sklearn regressor, e.g. DecisionTreeRegressor()
56
+ Base : base learner(s) to use in the boosting algorithm.
57
+ Pass a single instantiated sklearn regressor, e.g.
58
+ DecisionTreeRegressor(), to use the same learner for
59
+ every distribution parameter. To use different learners
60
+ per distribution parameter, pass a list/tuple of
61
+ instantiated sklearn regressors with length equal to
62
+ Dist.n_params. The sequence order matches the distribution
63
+ parameter order; for example, Normal uses
64
+ [loc_learner, scale_learner].
34
65
  natural_gradient : logical flag indicating whether the natural gradient should be used
35
66
  n_estimators : the number of boosting iterations to fit
36
67
  learning_rate : the learning rate
@@ -46,6 +77,18 @@ class NGBoost:
46
77
  loss has to increase before the algorithm stops early.
47
78
  Set to None to disable early stopping and validation.
48
79
  None enables running over the full data set.
80
+ n_jobs : number of joblib threads used to fit the per parameter
81
+ base learners in parallel on each boosting round. None
82
+ or 1 (the default) fits them serially. A value above 1,
83
+ or ``-1`` for all cores, speeds up fitting when the
84
+ distribution has many parameters, such as a high
85
+ dimensional MultivariateNormal or classification with
86
+ many classes; on small problems the thread overhead can
87
+ make it slower instead. Threads are used so the shared
88
+ data is not copied per worker; when nesting inside an
89
+ outer parallel job keep the product of the thread counts
90
+ near the core count. The fitted model does not depend on
91
+ n_jobs.
49
92
 
50
93
 
51
94
  Output:
@@ -69,6 +112,7 @@ class NGBoost:
69
112
  random_state=None,
70
113
  validation_fraction=0.1,
71
114
  early_stopping_rounds=None,
115
+ n_jobs=None,
72
116
  ):
73
117
  self.Dist = Dist
74
118
  self.Score = Score
@@ -87,10 +131,11 @@ class NGBoost:
87
131
  self.scalings = []
88
132
  self.col_idxs = []
89
133
  self.tol = tol
90
- self.random_state = check_random_state(random_state)
134
+ self.random_state = random_state
91
135
  self.best_val_loss_itr = None
92
136
  self.validation_fraction = validation_fraction
93
137
  self.early_stopping_rounds = early_stopping_rounds
138
+ self.n_jobs = n_jobs
94
139
 
95
140
  if hasattr(self.Dist, "multi_output"):
96
141
  self.multi_output = self.Dist.multi_output
@@ -120,6 +165,11 @@ class NGBoost:
120
165
  state_dict["Manifold"] = manifold(state_dict["Score"], state_dict["Dist"])
121
166
  self.__dict__ = state_dict
122
167
 
168
+ def __sklearn_is_fitted__(self):
169
+ # check_is_fitted (used by Pipeline.predict) otherwise looks for
170
+ # attributes that end in "_", and a fitted model has none of those
171
+ return bool(getattr(self, "base_models", None))
172
+
123
173
  def fit_init_params_to_marginal(self, Y, sample_weight=None, iters=1000):
124
174
  self.init_params = self.Manifold.fit(
125
175
  Y
@@ -143,18 +193,14 @@ class NGBoost:
143
193
 
144
194
  if self.minibatch_frac != 1.0:
145
195
  sample_size = int(self.minibatch_frac * len(Y))
146
- idxs = self.random_state.choice(
147
- np.arange(len(Y)), sample_size, replace=False
148
- )
196
+ idxs = self._rng.choice(np.arange(len(Y)), sample_size, replace=False)
149
197
 
150
198
  if self.col_sample != 1.0:
151
199
  if self.col_sample > 0.0:
152
200
  col_size = max(1, int(self.col_sample * X.shape[1]))
153
201
  else:
154
202
  col_size = 0
155
- col_idx = self.random_state.choice(
156
- np.arange(X.shape[1]), col_size, replace=False
157
- )
203
+ col_idx = self._rng.choice(np.arange(X.shape[1]), col_size, replace=False)
158
204
 
159
205
  weight_batch = None if sample_weight is None else sample_weight[idxs]
160
206
 
@@ -167,13 +213,58 @@ class NGBoost:
167
213
  params[idxs, :],
168
214
  )
169
215
 
216
+ def _base_learners(self):
217
+ if not isinstance(self.Base, (list, tuple)):
218
+ return [self.Base] * self.Manifold.n_params
219
+
220
+ if len(self.Base) != self.Manifold.n_params:
221
+ raise ValueError(
222
+ "Base must be a single estimator or a sequence with one estimator "
223
+ "per distribution parameter "
224
+ f"({self.Manifold.n_params} for {self.Dist.__name__}); "
225
+ f"got {len(self.Base)}."
226
+ )
227
+ return self.Base
228
+
170
229
  def fit_base(self, X, grads, sample_weight=None):
171
- if sample_weight is None:
172
- models = [clone(self.Base).fit(X, g) for g in grads.T]
173
- else:
230
+ base_learners = self._base_learners()
231
+ # Seed only the base learners that have no random_state of their own,
232
+ # drawing from self._rng in this single thread so serial and
233
+ # parallel fits agree. Learners that already fix their own randomness are
234
+ # left untouched, so their RNG stream is not perturbed (this mirrors how
235
+ # scikit learn's forests seed their sub estimators).
236
+ seeds = [
237
+ (
238
+ int(self._rng.randint(np.iinfo(np.int32).max))
239
+ if base.get_params().get("random_state", "set") is None
240
+ else None
241
+ )
242
+ for base in base_learners
243
+ ]
244
+
245
+ def _fit_one(base, g, seed):
246
+ learner = clone(base)
247
+ if seed is not None:
248
+ learner.set_params(random_state=seed)
249
+ if sample_weight is None:
250
+ return learner.fit(X, g)
251
+ return learner.fit(X, g, sample_weight=sample_weight)
252
+
253
+ if self.n_jobs in (None, 1):
174
254
  models = [
175
- clone(self.Base).fit(X, g, sample_weight=sample_weight) for g in grads.T
255
+ _fit_one(base, g, seed)
256
+ for base, g, seed in zip(base_learners, grads.T, seeds)
176
257
  ]
258
+ else:
259
+ if issparse(X) and X.format in ("csr", "csc", "bsr"):
260
+ # Canonicalize once so the threaded fits do not race on an in
261
+ # place sort_indices of this shared matrix. Only the compressed
262
+ # formats have sortable indices; sklearn copies the rest.
263
+ X.sort_indices()
264
+ models = Parallel(n_jobs=self.n_jobs, backend="threading")(
265
+ delayed(_fit_one)(base, g, seed)
266
+ for base, g, seed in zip(base_learners, grads.T, seeds)
267
+ )
177
268
  fitted = np.array([m.predict(X) for m in models]).T
178
269
  self.base_models.append(models)
179
270
  return fitted
@@ -235,9 +326,12 @@ class NGBoost:
235
326
  Y_val : DataFrame object or List or
236
327
  numpy array of validation-set outcomes in numeric format
237
328
  sample_weight : how much to weigh each example in the training set.
238
- numpy array of size (n) (defaults to None)
329
+ List, pandas Series or numpy array of size (n)
330
+ (defaults to None). Weights are matched to rows by
331
+ position, so the index of a Series is ignored.
239
332
  val_sample_weight : how much to weigh each example in the validation set.
240
- (defaults to None)
333
+ List, pandas Series or numpy array of size (n_val),
334
+ matched to rows by position (defaults to None)
241
335
  train_loss_monitor : a custom score or set of scores to track on the training set
242
336
  during training. Defaults to the score defined in the NGBoost
243
337
  constructor
@@ -254,6 +348,11 @@ class NGBoost:
254
348
  self.base_models = []
255
349
  self.scalings = []
256
350
  self.col_idxs = []
351
+ # Start a fresh random stream on every fit so that refitting with an
352
+ # integer random_state gives the same model
353
+ self._rng = check_random_state(self.random_state)
354
+
355
+ self.best_val_loss_itr = None
257
356
 
258
357
  return self.partial_fit(
259
358
  X,
@@ -298,9 +397,12 @@ class NGBoost:
298
397
  Y_val : DataFrame object or List or
299
398
  numpy array of validation-set outcomes in numeric format
300
399
  sample_weight : how much to weigh each example in the training set.
301
- numpy array of size (n) (defaults to None)
400
+ List, pandas Series or numpy array of size (n)
401
+ (defaults to None). Weights are matched to rows by
402
+ position, so the index of a Series is ignored.
302
403
  val_sample_weight : how much to weigh each example in the validation set.
303
- (defaults to None)
404
+ List, pandas Series or numpy array of size (n_val),
405
+ matched to rows by position (defaults to None)
304
406
  train_loss_monitor : a custom score or set of scores to track on the training set
305
407
  during training. Defaults to the score defined in the NGBoost
306
408
  constructor
@@ -320,6 +422,9 @@ class NGBoost:
320
422
  raise RuntimeError(
321
423
  "Base models, scalings, and col_idxs are not the same length"
322
424
  )
425
+ if getattr(self, "_rng", None) is None:
426
+ self._rng = check_random_state(self.random_state)
427
+ self._base_learners()
323
428
 
324
429
  # if early stopping is specified, split X,Y and sample weights (if given) into training and validation sets
325
430
  # This will overwrite any X_val and Y_val values passed by the user directly.
@@ -337,7 +442,7 @@ class NGBoost:
337
442
  X,
338
443
  Y,
339
444
  test_size=self.validation_fraction,
340
- random_state=self.random_state,
445
+ random_state=self._rng,
341
446
  )
342
447
 
343
448
  else:
@@ -356,7 +461,7 @@ class NGBoost:
356
461
  Y,
357
462
  sample_weight,
358
463
  test_size=self.validation_fraction,
359
- random_state=self.random_state,
464
+ random_state=self._rng,
360
465
  )
361
466
  elif X_val is not None and Y_val is not None:
362
467
  if sample_weight is not None and val_sample_weight is None:
@@ -389,6 +494,7 @@ class NGBoost:
389
494
  multi_output=self.multi_output,
390
495
  y_numeric=True,
391
496
  )
497
+ sample_weight = _validate_sample_weight(sample_weight, X)
392
498
 
393
499
  self.n_features = X.shape[1]
394
500
 
@@ -406,6 +512,9 @@ class NGBoost:
406
512
  multi_output=self.multi_output,
407
513
  y_numeric=True,
408
514
  )
515
+ val_sample_weight = _validate_sample_weight(
516
+ val_sample_weight, X_val, input_name="val_sample_weight"
517
+ )
409
518
  val_params = self.pred_param(X_val)
410
519
  val_loss_list = []
411
520
  best_val_loss = np.inf
@@ -491,16 +600,65 @@ class NGBoost:
491
600
 
492
601
  return self
493
602
 
603
+ def _set_base_params(self, parameters):
604
+ if not isinstance(self.Base, (list, tuple)):
605
+ self.Base.set_params(**parameters)
606
+ return
607
+
608
+ base_learners = list(self.Base)
609
+ for key, value in parameters.items():
610
+ index, delim, sub_key = key.partition("__")
611
+ try:
612
+ index = int(index)
613
+ except ValueError as exc:
614
+ raise ValueError(
615
+ "Base sequence parameters must use integer positions, "
616
+ f"for example Base__0__max_depth. Got Base__{key}."
617
+ ) from exc
618
+ if index < 0 or index >= len(base_learners):
619
+ raise ValueError(
620
+ f"Base sequence index {index} is out of range for "
621
+ f"{len(base_learners)} estimators."
622
+ )
623
+ if delim:
624
+ base_learners[index].set_params(**{sub_key: value})
625
+ else:
626
+ base_learners[index] = value
627
+ self.Base = type(self.Base)(base_learners)
628
+
494
629
  def set_params(self, **parameters):
495
- for parameter, value in parameters.items():
496
- setattr(self, parameter, value)
630
+ if not parameters:
631
+ return self
632
+
633
+ valid_params = self.get_params(deep=True)
634
+ nested_params = {}
635
+ for key, value in parameters.items():
636
+ key, delim, sub_key = key.partition("__")
637
+ if key not in valid_params:
638
+ local_valid_params = self.get_params(deep=False).keys()
639
+ raise ValueError(
640
+ f"Invalid parameter {key!r} for estimator {self}. "
641
+ f"Valid parameters are: {sorted(local_valid_params)!r}."
642
+ )
643
+ if delim:
644
+ nested_params.setdefault(key, {})[sub_key] = value
645
+ else:
646
+ setattr(self, key, value)
647
+ valid_params[key] = value
648
+
649
+ for key, sub_params in nested_params.items():
650
+ if key == "Base":
651
+ self._set_base_params(sub_params)
652
+ else:
653
+ valid_params[key].set_params(**sub_params)
497
654
  return self
498
655
 
499
656
  def get_params(self, deep=True):
500
657
  """
501
658
  Parameters
502
659
  ----------
503
- deep : Ignored. (for compatibility with sklearn)
660
+ deep : bool, default=True
661
+ If True, return nested parameters for the base learner or learners.
504
662
  Returns
505
663
  ----------
506
664
  params : returns an dictionary of parameters.
@@ -515,15 +673,45 @@ class NGBoost:
515
673
  "minibatch_frac": self.minibatch_frac,
516
674
  "col_sample": self.col_sample,
517
675
  "verbose": self.verbose,
676
+ "verbose_eval": self.verbose_eval,
677
+ "tol": self.tol,
518
678
  "random_state": self.random_state,
519
679
  "validation_fraction": self.validation_fraction,
520
680
  "early_stopping_rounds": self.early_stopping_rounds,
681
+ "n_jobs": self.n_jobs,
521
682
  }
522
683
 
684
+ if not deep:
685
+ return params
686
+
687
+ if isinstance(self.Base, (list, tuple)):
688
+ for i, base in enumerate(self.Base):
689
+ params[f"Base__{i}"] = base
690
+ if hasattr(base, "get_params"):
691
+ for key, value in base.get_params(deep=True).items():
692
+ params[f"Base__{i}__{key}"] = value
693
+ elif hasattr(self.Base, "get_params"):
694
+ for key, value in self.Base.get_params(deep=True).items():
695
+ params[f"Base__{key}"] = value
696
+
523
697
  return params
524
698
 
525
699
  def score(self, X, Y): # for sklearn
526
- return self.Manifold(self.pred_dist(X)._params).total_score(Y)
700
+ """
701
+ Score the predicted distributions at the points X=x against the outcomes Y.
702
+ Higher is better, as scikit-learn expects, so this is the negative of the
703
+ average scoring rule: the mean log-likelihood for LogScore and the negative
704
+ mean CRPS for CRPScore.
705
+
706
+ Parameters:
707
+ X : DataFrame object or List or
708
+ numpy array of predictors (n x p) in numeric format.
709
+ Y : numpy array of outcomes (n)
710
+
711
+ Output:
712
+ A float, the negative of the average score
713
+ """
714
+ return -self.Manifold(self.pred_dist(X)._params).total_score(Y)
527
715
 
528
716
  def pred_dist(self, X, max_iter=None):
529
717
  """
@@ -616,8 +804,12 @@ class NGBoost:
616
804
  # Check whether the model is fitted
617
805
  if not self.base_models:
618
806
  return None
619
- # Check whether the base model is DecisionTreeRegressor
620
- if not isinstance(self.base_models[0][0], DecisionTreeRegressor):
807
+ # Check whether all base models are DecisionTreeRegressor
808
+ if any(
809
+ not isinstance(model, DecisionTreeRegressor)
810
+ for models in self.base_models
811
+ for model in models
812
+ ):
621
813
  return None
622
814
  # Reshape the base_models
623
815
  params_trees = zip(*self.base_models)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ngboost"
3
- version = "0.5.10dev"
3
+ version = "0.6.0"
4
4
  description = "Library for probabilistic predictions via gradient boosting."
5
5
  authors = ["Stanford ML Group <avati@cs.stanford.edu>"]
6
6
  readme = "README.md"
@@ -13,8 +13,11 @@ classifiers = [
13
13
  license = "Apache License 2.0"
14
14
 
15
15
  [tool.poetry.dependencies]
16
- python = ">=3.9, <3.15"
17
- scikit-learn = "^1.6"
16
+ python = ">=3.10, <3.15"
17
+ scikit-learn = [
18
+ {version = ">=1.6,<1.7", markers = "python_version < '3.14'"},
19
+ {version = ">=1.8.0,<2.0", markers = "python_version >= '3.14'"}
20
+ ]
18
21
  numpy = [
19
22
  {version = ">=1.21.2", markers = "python_version < '3.13'"},
20
23
  {version = ">=2.1.0", markers = "python_version >= '3.13'"}
@@ -26,18 +29,18 @@ scipy = [
26
29
  tqdm = ">=4.3"
27
30
  lifelines = ">=0.25"
28
31
  sympy = ">=1.12"
29
- matplotlib = [
30
- {version = ">=3.0,<3.10", markers = "python_version < '3.10'"},
31
- {version = ">=3.10.0", markers = "python_version >= '3.10'"}
32
- ]
32
+ joblib = ">=1.2"
33
+ matplotlib = ">=3.10.0"
33
34
 
34
35
  [tool.poetry.group.dev.dependencies]
35
36
  pytest = "^8.0.0"
37
+ pandas = ">=1.0"
36
38
  black = "^24.0.0"
37
39
  pre-commit = "^4.0.0"
38
40
  isort = "^5.13.0"
39
41
  pylint = "^3.2.0"
40
42
  flake8 = "^7.1.0"
43
+ skops = ">=0.16"
41
44
 
42
45
 
43
46
  [build-system]
File without changes
File without changes
File without changes