pybear-dask 0.2.0__py3-none-any.whl

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 (33) hide show
  1. pybear_dask/__init__.py +19 -0
  2. pybear_dask/_version.py +38 -0
  3. pybear_dask/base/__init__.py +24 -0
  4. pybear_dask/base/_is_classifier.py +186 -0
  5. pybear_dask/model_selection/GSTCV/_GSTCVDask/GSTCVDask.py +918 -0
  6. pybear_dask/model_selection/GSTCV/_GSTCVDask/__init__.py +5 -0
  7. pybear_dask/model_selection/GSTCV/_GSTCVDask/_fit/__init__.py +5 -0
  8. pybear_dask/model_selection/GSTCV/_GSTCVDask/_fit/_estimator_fit_params_helper.py +106 -0
  9. pybear_dask/model_selection/GSTCV/_GSTCVDask/_fit/_fold_splitter.py +86 -0
  10. pybear_dask/model_selection/GSTCV/_GSTCVDask/_fit/_get_kfold.py +117 -0
  11. pybear_dask/model_selection/GSTCV/_GSTCVDask/_fit/_parallelized_fit.py +127 -0
  12. pybear_dask/model_selection/GSTCV/_GSTCVDask/_fit/_parallelized_scorer.py +228 -0
  13. pybear_dask/model_selection/GSTCV/_GSTCVDask/_fit/_parallelized_train_scorer.py +226 -0
  14. pybear_dask/model_selection/GSTCV/_GSTCVDask/_param_conditioning/__init__.py +5 -0
  15. pybear_dask/model_selection/GSTCV/_GSTCVDask/_param_conditioning/_scheduler.py +88 -0
  16. pybear_dask/model_selection/GSTCV/_GSTCVDask/_type_aliases.py +43 -0
  17. pybear_dask/model_selection/GSTCV/_GSTCVDask/_validation/__init__.py +5 -0
  18. pybear_dask/model_selection/GSTCV/_GSTCVDask/_validation/_cache_cv.py +37 -0
  19. pybear_dask/model_selection/GSTCV/_GSTCVDask/_validation/_dask_estimator.py +84 -0
  20. pybear_dask/model_selection/GSTCV/_GSTCVDask/_validation/_iid.py +38 -0
  21. pybear_dask/model_selection/GSTCV/_GSTCVDask/_validation/_validation.py +54 -0
  22. pybear_dask/model_selection/GSTCV/_GSTCVDask/_validation/_y.py +91 -0
  23. pybear_dask/model_selection/GSTCV/__init__.py +5 -0
  24. pybear_dask/model_selection/__init__.py +28 -0
  25. pybear_dask/model_selection/autogridsearch/AutoGSTCVDask.py +71 -0
  26. pybear_dask/model_selection/autogridsearch/AutoGridSearchCVDask.py +70 -0
  27. pybear_dask/model_selection/autogridsearch/__init__.py +19 -0
  28. pybear_dask/model_selection/autogridsearch/_autogridsearch_wrapper/__init__.py +5 -0
  29. pybear_dask/model_selection/autogridsearch/_autogridsearch_wrapper/_refit_can_be_skipped.py +100 -0
  30. pybear_dask-0.2.0.dist-info/LICENSE +28 -0
  31. pybear_dask-0.2.0.dist-info/METADATA +253 -0
  32. pybear_dask-0.2.0.dist-info/RECORD +33 -0
  33. pybear_dask-0.2.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,918 @@
1
+ # Author:
2
+ # Bill Sousa
3
+ #
4
+ # License: BSD 3 clause
5
+ #
6
+
7
+
8
+
9
+ from typing import (
10
+ Callable,
11
+ Iterable,
12
+ Optional
13
+ )
14
+ from typing_extensions import (
15
+ Any,
16
+ Union
17
+ )
18
+ from ._type_aliases import (
19
+ DaskXType,
20
+ DaskYType,
21
+ DaskKFoldType,
22
+ DaskSplitType,
23
+ DaskSchedulerType
24
+ )
25
+ from pybear.model_selection.GSTCV._type_aliases import (
26
+ ClassifierProtocol,
27
+ ErrorScoreType,
28
+ ParamGridInputType,
29
+ ParamGridsInputType,
30
+ ScorerInputType,
31
+ ThresholdsInputType,
32
+ ThresholdsWIPType,
33
+ MaskedHolderType,
34
+ NDArrayHolderType
35
+ )
36
+
37
+
38
+ from copy import deepcopy
39
+ import numbers
40
+
41
+ from dask import compute
42
+ import distributed
43
+
44
+ from ._validation._validation import _validation
45
+ from ._validation._y import _val_y
46
+
47
+ from ._param_conditioning._scheduler import _cond_scheduler
48
+
49
+ from .._GSTCVDask._fit._get_kfold import _get_kfold as _dask_get_kfold
50
+ from .._GSTCVDask._fit._fold_splitter import _fold_splitter as _dask_fold_splitter
51
+ from .._GSTCVDask._fit._estimator_fit_params_helper import \
52
+ _estimator_fit_params_helper as _dask_estimator_fit_params_helper
53
+ from .._GSTCVDask._fit._parallelized_fit import _parallelized_fit
54
+ from .._GSTCVDask._fit._parallelized_scorer import _parallelized_scorer
55
+ from .._GSTCVDask._fit._parallelized_train_scorer import _parallelized_train_scorer
56
+
57
+ from pybear.model_selection.GSTCV._GSTCVMixin._GSTCVMixin import _GSTCVMixin
58
+
59
+
60
+
61
+ class GSTCVDask(_GSTCVMixin):
62
+ """Exhaustive cross-validated search over a grid of hyperparameter
63
+ values and decision thresholds for a binary classifier.
64
+
65
+ The optimal hyperparameters and decision threshold selected are
66
+ those that maximize the average score (and minimize the average
67
+ loss) of the held-out data (test sets).
68
+
69
+ pybear `GSTCVDask` is intended to closely parallel the interface and
70
+ user-experience of dask_ml and scikit-learn `GridSearchCV`. Users
71
+ who are familiar with those GridSearch implementations should find
72
+ that `GSTCVDask` differs with respect to 4 things:
73
+
74
+ 1) the init parameter `thresholds` (which can also be passed as a
75
+ parameter to `param_grid`)
76
+
77
+ 2) additional columns in the :attr:`cv_results_` attribute to report
78
+ the best thresholds for each scorer
79
+
80
+ 3) a new post-run attribute, :attr:`best_threshold_`, which informs
81
+ about the overall best threshold
82
+
83
+ 4) callables passed to `scoring` SHOULD NOT be wrapped in
84
+ 'make_scorer' as would be done with `GridSearchCV`. Pass scoring
85
+ callables in raw metric form. See the dask_ml and scikit-learn docs
86
+ for more information about 'make_scorer' and 'metrics'. Also see the
87
+ 'Parameters' section of the `GSTCVDask` docs.
88
+
89
+ Users who are familiar with the dask_ml implementation of GridSearch
90
+ should focus on the above 4 areas in the `GSTCVDask` 'Parameters' and
91
+ 'Attributes' sections of the docs for mastery of `GSTCVDask`.
92
+
93
+ `GSTCVDask` implements `fit`, `predict_proba`, `predict`, `score`,
94
+ `get_params`, and `set_params` methods. It also implements
95
+ `decision_function`, `predict_log_proba`, `score_samples`,
96
+ `transform` and `inverse_transform` if they are exposed by the
97
+ classifier passed to `estimator`.
98
+
99
+ Parameters
100
+ ----------
101
+ estimator : object
102
+ Required. Must be a binary classifier that conforms to the
103
+ scikit-learn estimator API interface. The classifier must have
104
+ the `fit`, `set_params`, `get_params`, and `predict_proba`
105
+ methods. If the classifier does not have `predict_proba`, try
106
+ to wrap with `CalibratedClassifierCV`. The classifier does not
107
+ need a `score` method, as `GSTCVDask` never accesses the
108
+ estimator `score` method because it always uses a 0.5 threshold.
109
+
110
+ `GSTCVDask` does not strictly prohibit non-dask classifiers, but
111
+ is explicitly designed for use with dask objects (estimators,
112
+ arrays, and dataframes.) pybear `GSTCV` is recommended for
113
+ non-dask classifiers.
114
+ param_grid : Union[ParamGridInputType, ParamGridsInputType]
115
+ Required. A dictionary with hyperparameters names (str) as keys
116
+ and list-likes of respective settings to try as values. Can also
117
+ be a list-like of such dictionaries, and the grids spanned by
118
+ each are explored. This enables searching over any combination
119
+ of hyperparameter settings.
120
+ thresholds : ThresholdsInputType, default=None
121
+ The decision threshold search grid to use when performing the
122
+ hyperparameter search. Other `GridSearchCV` modules only use the
123
+ conventional decision threshold for binary classifiers, which is
124
+ 0.5. This module can search over any set of decision threshold
125
+ values in the 0 to 1 interval (inclusive) in the same manner as
126
+ any other hyperparameter while performing the grid search.
127
+
128
+ The thresholds value passed via the init parameter can be None,
129
+ a single number from 0 to 1 (inclusive) or a list-like of such
130
+ numbers. If None, (and thresholds are not passed directly inside
131
+ the param grid(s)), the default threshold grid is used,
132
+ numpy.linspace(0, 1, 21).
133
+
134
+ Thresholds may also be passed to individual param grids via a
135
+ 'thresholds' key. However, when passed directly to a param grid,
136
+ 'thresholds' cannot be None or a single number, it must be a
137
+ list-like of numbers as is normally done with param grids.
138
+
139
+ Because `thresholds` can be passed in 2 different ways, there
140
+ is a hierarchy that dictates which thresholds are used during
141
+ searching and scoring. Any threshold values passed directly
142
+ within a param grid always supersede any passed (or not passed)
143
+ to the `thresholds` init parameter. When a param grid does not
144
+ have any thresholds passed inside it, the value passed to the
145
+ init parameter is used -- if a value was not passed to the init
146
+ parameter, then the `GSTCVDask` default value is used.
147
+
148
+ When one scorer is used, the best threshold is always exposed
149
+ and is accessible via the :attr:`best_threshold_` attribute.
150
+ When multiple scorers are used, the `best_threshold_` attribute
151
+ is only exposed when a string value is passed to `refit`. The
152
+ best threshold is never reported in the :attr:`best_params_`
153
+ attribute, even if thresholds were passed via a param grid; the
154
+ best threshold is only available via the `best_threshold_`
155
+ attribute. Another way to discover the best threshold for each
156
+ scorer is by inspection of the :attr:`cv_results_` attribute.
157
+
158
+ The scores reported for test data in `cv_results_` are those for
159
+ the best threshold. Also note that when `return_train_score` is
160
+ True, the scores returned for the train data are only for the
161
+ best threshold found for the test data. That is, every threshold
162
+ is scored on the test data, the best score is found, and the
163
+ best threshold is the threshold corresponding to the best score.
164
+ That best score and best threshold is reported for the test data.
165
+ Then when scoring train data, only the best threshold is scored
166
+ and reported in `cv_results_`.
167
+ scoring : ScorerInputType, default='accuracy'
168
+ Strategy to evaluate the performance of the cross-validated model
169
+ on the test set (and also train set, if `return_train_score` is
170
+ True.)
171
+
172
+ For any number of scorers, scoring can be a dictionary with
173
+ user-assigned scorer names as keys and callables as values. See
174
+ below for clarification on allowed callables.
175
+
176
+ For a single scoring metric, a single string or a single callable
177
+ is also allowed. Valid strings that can be passed are 'accuracy',
178
+ 'balanced_accuracy', 'average_precision', 'f1', 'precision', and
179
+ 'recall'.
180
+
181
+ For evaluating multiple metrics, scoring can be a vector-like of
182
+ unique strings, containing a combination of the allowed strings.
183
+
184
+ The default scorer of the estimator cannot be used by this module
185
+ because the decision threshold cannot be manipulated. Therefore,
186
+ `scoring` cannot accept a None argument.
187
+
188
+ About the scorer callable:
189
+ This module's scorers differ from other GSCV implementations
190
+ in an important way. Some of those implementations accept
191
+ 'make_scorer' functions, e.g. `sklearn.metrics.make_scorer`, but
192
+ this module cannot accept this. 'make_scorer' implicitly assumes
193
+ a decision threshold of 0.5, but this module needs to be able to
194
+ calculate predictions based on any user-entered threshold.
195
+ Therefore, in place of 'make_scorer' functions, this module uses
196
+ scoring metrics directly (whereas they would otherwise be passed
197
+ to 'make_scorer'.) An example of a valid scoring metric is
198
+ `sklearn.metrics.accuracy_score`.
199
+
200
+ Additionally, this module can accept any scoring function that
201
+ has signature (y_true, y_pred) and returns a single number. Note
202
+ that, when using a custom scorer, the scorer should return a
203
+ single value. Metric functions returning a list/array of values
204
+ can be wrapped in multiple scorers that return one value each.
205
+
206
+ This module cannot directly accept scorer kwargs and pass them
207
+ to scorers. To pass kwargs to your scoring metric, create a
208
+ wrapper with signature (y_true, y_pred) around the metric and
209
+ hard-code the kwargs into the metric, e.g.,
210
+
211
+ def your_metric_wrapper(y_true, y_pred):
212
+ return your_metric(y_true, y_pred, **hard_coded_kwargs)
213
+ iid : Optional[bool], default=True
214
+ `iid` is ignored when `cv` is an iterable. Indicates whether
215
+ the data's examples are believed to have random distribution
216
+ (True) or if the examples are organized non-randomly in some
217
+ way (False). If the data is not iid, KFold will cross chunk
218
+ boundaries when reading the data in an attempt to randomize it;
219
+ this can be an expensive process. Otherwise, if the data is iid,
220
+ KFold can handle the data as contiguous chunks which is much
221
+ more efficient.
222
+ refit : Optional[Union[bool, str, Callable]], default=True
223
+ After the grid search is done, fit the whole dataset on the
224
+ estimator using the best found hyperparameters and expose this
225
+ fitted estimator via the :attr:`best_estimator_` attribute. Also,
226
+ when the estimator is refit on the best hyperparameters, the
227
+ `GSTCVDask` instance itself becomes the best estimator, exposing
228
+ the :meth:`predict_proba`, :meth:`predict`, and :meth:`score`
229
+ methods (and possibly others.) When refit is not performed, the
230
+ search simply finds the best hyperparameters and exposes them via
231
+ the `best_params_` attribute (unless there are multiple scorers
232
+ and `refit` is False, in which case information about the grid
233
+ search is only available via the `cv_results_` attribute.)
234
+
235
+ The values accepted by `refit` depend on the scoring scheme, that
236
+ is, whether a single or multiple scorers are used. In all cases,
237
+ `refit` can be boolean False (to disable refit), a string that
238
+ indicates the name of the scorer to use (when there is only one
239
+ scorer there is only one possible string value), or a callable.
240
+ See below for more information about the refit callable. When
241
+ one scorer is used, `refit` can be boolean True or False, but
242
+ boolean True cannot be used when there is more than one scorer.
243
+
244
+ Where there are considerations other than maximum score in
245
+ choosing a best estimator, `refit` can be set to a function that
246
+ takes in `cv_results_` and returns :attr:`best_index_` (an
247
+ integer). In that case, the returned `best_index_` sets both the
248
+ `best_estimator_` and `best_params_` attributes. When more than
249
+ one scorer is used, the :attr:`best_score_` and `best_threshold_`
250
+ attributes will not be available, but are available if there is
251
+ only one scorer.
252
+
253
+ See the `scoring` parameter to know more about multiple metric
254
+ evaluation.
255
+ cv : Optional[Union[numbers.Integral, Iterable, None]], default=None
256
+ Sets the cross-validation splitting strategy.
257
+
258
+ Possible inputs for cv are:
259
+
260
+ 1) None, to use the default 5-fold cross validation,
261
+ 2) an integer, must be 2 or greater, to specify the number of
262
+ folds in a KFold split,
263
+ 3) an iterable yielding pairs of (train, test) split indices as
264
+ arrays.
265
+
266
+ For passed iterables:
267
+ This module will convert generators to lists. No validation is
268
+ done beyond verifying that it is an iterable that contains pairs
269
+ of iterables. `GSTCVDask` will catch out of range indices and
270
+ raise an error but any validation beyond that is up to the user
271
+ outside of `GSTCVDask`.
272
+ verbose : Optional[numbers.Real], default=0
273
+ The amount of verbosity to display to screen during the grid
274
+ search. Accepts integers from 0 to 10. 0 means no information
275
+ displayed to the screen, 10 means full verbosity. Non-numbers
276
+ are rejected. Boolean False is set to 0, boolean True is set to
277
+ 10. Negative numbers are rejected. Numbers greater than 10 are
278
+ set to 10. Floats are rounded to integers.
279
+ error_score : Optional[ErrorScoreType], default='raise'
280
+ Score to assign if the estimator raises an error while fitting
281
+ on a train fold. If set to ‘raise’, the error is raised. If a
282
+ numeric value is given, a warning is raised and the error score
283
+ value is inserted into the subsequent calculations in place of
284
+ the missing value(s). This parameter does not affect the refit
285
+ step, which will always raise the error.
286
+ return_train_score : Optional[bool]
287
+ If False, the `cv_results_` attribute will not include training
288
+ scores. If True, the train data is scored using all the scorers
289
+ at the best respective threshold(s) found for the test data.
290
+ Computing training scores is used to assess conditions of under-
291
+ or over-fittedness. However, computing the scores on the training
292
+ set can be computationally expensive and is not required to
293
+ select the hyperparameters that yield the best performance.
294
+ scheduler : Optional[Union[Client, Scheduler, None]], default=None
295
+ A passed scheduler supersedes all other external schedulers.
296
+ When a scheduler is explicitly passed, `GSTCVDask` does not
297
+ perform any validation or verification but allows that to be
298
+ handled by dask at compute time.
299
+
300
+ If 'None' is passed (the default), `GSTCVDask` looks for an
301
+ external context manager or global scheduler using `get_client`.
302
+ If one exists, `GSTCVDask` uses that as the scheduler. If an
303
+ external scheduler does not exist, `GSTCVDask` instantiates a
304
+ multiprocessing distributed.Client() (which defaults to
305
+ LocalCluster) with `n_workers=n_jobs` and 1 thread per worker.
306
+ If `n_jobs` is None `GSTCVDask` uses the default Client behavior
307
+ when `n_workers` is set to None.
308
+
309
+ This module intentionally disallows any shorthand methods for
310
+ internally setting up a scheduler (such as strings like 'thread-
311
+ ing' and 'multiprocessing', which are ultimately passed to
312
+ dask.base.get_scheduler.) All of these types of configurations
313
+ should be handled by the user external to the `GSTCVDask` module.
314
+ As much as possible, dask and distributed objects are allowed to
315
+ flow through without any hard-coded input.
316
+ n_jobs : Optional[Union[int, None]], default=None
317
+ Active only if no scheduler is available. That is, if a
318
+ scheduler is not passed to `scheduler`, if no global scheduler
319
+ is available, and if there is no scheduler context manager, only
320
+ then does `n_jobs` become effectual. In this case, `GSTCVDask`
321
+ creates a Client instance using local multiprocessing with
322
+ `n_workers=n_jobs`.
323
+ cache_cv : Optional[bool], default=True
324
+ Indicates if the train/test folds of the data are to be stored
325
+ when first generated, or if the folds are generated from X, y
326
+ and the KFold indices at each point of need. Caching all the
327
+ folds for each split of the data is memory-intensive.
328
+
329
+ Attributes
330
+ ----------
331
+ cv_results_ : CVResultsType
332
+ A dictionary with column headers as keys and results as values,
333
+ that can be conveniently converted into a pandas DataFrame.
334
+ Always exposed after fit.
335
+
336
+ Below is an example of `cv_results_` for a logistic classifier,
337
+ with:
338
+ cv=3,
339
+
340
+ param_grid={'C': [1e-5, 1e-4]},
341
+
342
+ thresholds=np.linspace(0,1,21),
343
+
344
+ scoring=['accuracy', 'balanced_accuracy']
345
+
346
+ return_train_score=False
347
+
348
+ on random data.
349
+
350
+ {
351
+ 'mean_fit_time': [1.227847, 0.341168]
352
+
353
+ 'std_fit_time': [0.374309, 0.445982]
354
+
355
+ 'mean_score_time': [0.001638, 0.001676]
356
+
357
+ 'std_score_time': [0.000551, 0.000647]
358
+
359
+ 'param_C': [0.00001, 0.0001]
360
+
361
+ 'params': [{'C': 1e-05}, {'C': 0.0001}]
362
+
363
+ 'best_threshold_accuracy': [0.5, 0.51]
364
+
365
+ 'split0_test_accuracy': [0.785243, 0.79844]
366
+
367
+ 'split1_test_accuracy': [0.80228, 0.814281]
368
+
369
+ 'split2_test_accuracy': [0.805881, 0.813381]
370
+
371
+ 'mean_test_accuracy': [0.797801, 0.808701]
372
+
373
+ 'std_test_accuracy': [0.009001, 0.007265]
374
+
375
+ 'rank_test_accuracy': [2, 1]
376
+
377
+ 'best_threshold_balanced_accuracy': [0.5, 0.51]
378
+
379
+ 'split0_test_balanced_accuracy': [0.785164, 0.798407]
380
+
381
+ 'split1_test_balanced_accuracy': [0.802188, 0.814252]
382
+
383
+ 'split2_test_balanced_accuracy': [0.805791, 0.813341]
384
+
385
+ 'mean_test_balanced_accuracy': [0.797714, 0.808667]
386
+
387
+ 'std_test_balanced_accuracy': [0.008995, 0.007264]
388
+
389
+ 'rank_test_balanced_accuracy': [2, 1]
390
+ }
391
+
392
+ Slicing across the dictionary values gives the results for a
393
+ single permutation of grid search. That is, indexing into all of
394
+ the masked arrays at position zero gives the result for the
395
+ first permutation of the grid search, index 1 contains the
396
+ results for the second permutation, and so forth.
397
+
398
+ The key 'params' is used to store a list of parameter settings
399
+ dictionaries for all the hyperparameter candidates. That is,
400
+ the 'params' key holds all the possible permutations of
401
+ hyperparameters for the given search grid(s).
402
+
403
+ 'mean_fit_time', 'std_fit_time', 'mean_score_time' and
404
+ 'std_score_time' are all in seconds.
405
+
406
+ For single-metric evaluation, the scores for the single scorer
407
+ are available in the `cv_results_` dict at the keys ending with
408
+ '_score'. For multi-metric evaluation, the scores for all the
409
+ scorers are available in the `cv_results_` dict at the keys
410
+ ending with that scorer’s name ('_<scorer_name>'). E.g.,
411
+ ‘split0_test_precision’, ‘mean_train_precision’, etc.
412
+ best_estimator_ : object
413
+ The estimator that was chosen by the search, i.e. the estimator
414
+ which gave the highest score (or smallest loss) on the held-out
415
+ (test) data. Only exposed when `refit` is not False; see the
416
+ `refit` parameter for more information on allowed values.
417
+ best_score_ : float
418
+ The mean of the scores of the hold out (test) cv folds for the
419
+ best estimator with the best threshold applied. Always exposed
420
+ when there is one scorer, or when `refit` is specified as a
421
+ string for 2+ scorers.
422
+ best_params_ : dict[str, Any]
423
+ The dictionary found in the :attr:`cv_results_` 'params'
424
+ column in the :attr:`best_index_` position, which gives the
425
+ hyperparameter settings that resulted in the highest mean score
426
+ (best_score_) on the hold out (test) data with the best threshold
427
+ applied.
428
+
429
+ `best_params_` never holds the best threshold. Access the
430
+ best threshold via the :attr:`best_threshold_` attribute (if
431
+ available) or the `cv_results_` attribute.
432
+
433
+ `best_params_` is always exposed when there is one scorer, or
434
+ when `refit` is not False for 2+ scorers.
435
+ best_index_ : int
436
+ The index of the `cv_results_` arrays which corresponds to the
437
+ best hyperparameter settings. Always exposed when there is one
438
+ scorer, or when `refit` is not False for 2+ scorers.
439
+ scorer_ : dict
440
+ Scorer metric(s) used on the held out data to choose the best
441
+ hyperparameters for the model. Always exposed after fit.
442
+
443
+ This attribute holds the validated scoring dictionary which maps
444
+ the scorer key to the scorer metric callable, i.e., a dictionary
445
+ of {scorer_name: scorer_metric}.
446
+ n_splits_ : int
447
+ The number of cross-validation splits (folds). Always exposed
448
+ after fit.
449
+ refit_time_ : float
450
+ Seconds elapsed when refitting the best model on the whole
451
+ dataset. Only exposed when `refit` is not False.
452
+
453
+ multimetric_ : bool
454
+ Whether several scoring metrics were used. False if one scorer
455
+ was used, otherwise True. Always exposed after fit.
456
+ classes_
457
+ n_features_in_
458
+ feature_names_in_
459
+ best_threshold_ : float
460
+ The threshold that, along with the hyperparameter values found
461
+ in `best_params_`, yields the highest score for the given
462
+ estimator and data.
463
+
464
+ The best threshold is only available conditionally via the
465
+ `best_threshold_` attribute. When one scorer is used, the best
466
+ threshold found is always exposed. When multiple scorers are
467
+ used, the `best_threshold_` attribute is only exposed when a
468
+ string value is passed to `refit`. Another way to find the best
469
+ threshold for each scorer and overall is by inspection of
470
+ `cv_results_`.
471
+
472
+ Notes
473
+ -----
474
+
475
+ **Type Aliases**
476
+
477
+ class ClassifierProtocol(Protocol):
478
+ def fit(self, X: Any, y: Any) -> Self
479
+
480
+ def get_params(self, **kwargs) -> dict[str, Any]
481
+
482
+ def set_params(self, **kwargs) -> Self
483
+
484
+ def predict_proba(self, X: Any) -> Any
485
+
486
+ ParamGridInputType:
487
+ dict[str, Sequence[Any]]
488
+
489
+ ParamGridsInputType:
490
+ Sequence[ParamGridInputType]
491
+
492
+ ThresholdsInputType:
493
+ Union[None, numbers.Real, Sequence[numbers.Real]]
494
+
495
+ DaskSlicerType:
496
+ dask.array.core.Array
497
+
498
+ DaskKFoldType:
499
+ tuple[DaskSlicerType, DaskSlicerType]
500
+
501
+ ScorerNameTypes:
502
+ Literal[
503
+ 'accuracy',
504
+ 'balanced_accuracy',
505
+ 'average_precision',
506
+ 'f1',
507
+ 'precision',
508
+ 'recall'
509
+ ]
510
+
511
+ ScorerCallableType:
512
+ Callable[[Iterable, Iterable], numbers.Real]
513
+
514
+ ScorerInputType:
515
+ Union[
516
+ ScorerNameTypes,
517
+ Sequence[ScorerNameTypes],
518
+ ScorerCallableType,
519
+ dict[str, ScorerCallableType]
520
+ ]
521
+
522
+ RefitCallableType:
523
+ Callable[[CVResultsType], numbers.Integral]
524
+
525
+ RefitType:
526
+ Union[bool, ScorerNameTypes, RefitCallableType]
527
+
528
+ DaskSchedulerType:
529
+ Union[distributed.scheduler.Scheduler, distributed.client.Client]
530
+
531
+ DaskXType:
532
+ Iterable
533
+
534
+ DaskYType:
535
+ Union[Sequence[numbers.Integral], None]
536
+
537
+ CVResultsType:
538
+ dict[str, np.ma.masked_array[Any]]
539
+
540
+ FeatureNamesInType:
541
+ numpy.ndarray[str]
542
+
543
+
544
+ Examples
545
+ --------
546
+ >>> import numpy as np
547
+ >>> from pybear_dask.model_selection import GSTCVDask
548
+ >>> from dask_ml.linear_model import LogisticRegression
549
+ >>> from dask_ml.datasets import make_classification
550
+
551
+ >>> clf = LogisticRegression(
552
+ ... solver='lbfgs',
553
+ ... tol=1e-4
554
+ ... )
555
+ >>> X, y = make_classification(
556
+ ... n_samples=90, n_features=5, random_state=19, chunks=(90, 5)
557
+ ... )
558
+ >>> param_grid = {
559
+ ... 'C': [1e-4, 1e-3],
560
+ ... }
561
+ >>> gstcv = GSTCVDask(
562
+ ... estimator=clf,
563
+ ... param_grid=param_grid,
564
+ ... thresholds=[0.1, 0.5, 0.9],
565
+ ... scoring='balanced_accuracy',
566
+ ... iid=True,
567
+ ... refit=False,
568
+ ... cv=3,
569
+ ... verbose=0,
570
+ ... error_score='raise',
571
+ ... return_train_score=False,
572
+ ... scheduler=None,
573
+ ... n_jobs=None,
574
+ ... cache_cv=True
575
+ ... )
576
+ >>> gstcv.fit(X, y)
577
+ GSTCVDask(cv=3, estimator=LogisticRegression(solver='lbfgs'),
578
+ param_grid={'C': [0.0001, 0.001]}, refit=False,
579
+ scoring='balanced_accuracy', thresholds=[0.1, 0.5, 0.9])
580
+
581
+ >>> gstcv.best_params_
582
+ {'C': 0.0001}
583
+
584
+ >>> gstcv.best_threshold_
585
+ 0.5
586
+
587
+ """
588
+
589
+
590
+ def __init__(
591
+ self,
592
+ estimator: ClassifierProtocol,
593
+ param_grid: Union[ParamGridInputType, ParamGridsInputType],
594
+ *,
595
+ thresholds: ThresholdsInputType=None,
596
+ scoring: ScorerInputType='accuracy',
597
+ iid: Optional[bool]=True,
598
+ refit: Optional[Union[bool, str, Callable]] = True,
599
+ cv: Optional[Union[numbers.Integral, Iterable, None]]=None,
600
+ verbose: Optional[numbers.Real]=0,
601
+ error_score: Optional[ErrorScoreType]='raise',
602
+ return_train_score: Optional[bool]=False,
603
+ scheduler: Optional[
604
+ Union[distributed.Client, distributed.scheduler.Scheduler, None]
605
+ ]=None,
606
+ n_jobs: Optional[Union[numbers.Integral, None]]=None,
607
+ cache_cv: Optional[bool]=True
608
+ ) -> None:
609
+ """Initialize the `GSTCVDask` instance."""
610
+
611
+ self.estimator = estimator
612
+ self.param_grid = param_grid
613
+ self.thresholds = thresholds
614
+ self.scoring = scoring
615
+ self.n_jobs = n_jobs
616
+ self.cv = cv
617
+ self.refit = refit
618
+ self.verbose = verbose
619
+ self.error_score = error_score
620
+ self.return_train_score = return_train_score
621
+ self.iid = iid
622
+ self.scheduler = scheduler
623
+ self.cache_cv = cache_cv
624
+
625
+
626
+ def _val_y(self, _y: DaskYType) -> None:
627
+ """Implements `GSTCVDask` _val_y in methods in `_GSTCVMixin`.
628
+
629
+ See the docs for `GSTCVDask` _val_y.
630
+
631
+ Parameters
632
+ ----------
633
+ _y : DaskYType
634
+ The target for the data.
635
+
636
+ Returns
637
+ -------
638
+ None
639
+
640
+ """
641
+
642
+ # KEEP val y separate
643
+ _val_y(_y)
644
+
645
+
646
+ def _val_params(self) -> None:
647
+ """Validate init params that are unique to `GSTCVDask`.
648
+
649
+ Returns
650
+ -------
651
+ None
652
+
653
+ """
654
+
655
+ # KEEP val of y separate
656
+ _validation(self.estimator, self.iid, self.cache_cv)
657
+
658
+
659
+ def _condition_params(
660
+ self,
661
+ _X: DaskXType,
662
+ _y: DaskYType
663
+ ) -> None:
664
+ """Condition GSTCVDask-only init params into format for internal
665
+ processing.
666
+
667
+ Parameters
668
+ ----------
669
+ _X : DaskXType
670
+ The data.
671
+ _y : DaskYType
672
+ The target for the data.
673
+
674
+ Returns
675
+ -------
676
+ None
677
+
678
+ """
679
+
680
+
681
+ self._scheduler: DaskSchedulerType = \
682
+ _cond_scheduler(self.scheduler, self.n_jobs)
683
+
684
+ self._KFOLD: list[DaskKFoldType]
685
+ if isinstance(self._cv, numbers.Integral):
686
+ self._KFOLD = list(_dask_get_kfold(
687
+ _X, self.n_splits_, self.iid, self._verbose, _y=_y
688
+ ))
689
+ else: # _cv is an iterable, _cond_cv should have made list[tuple]
690
+ self._KFOLD = self._cv
691
+
692
+ self._CACHE_CV: Union[None, list[DaskSplitType]] = None
693
+ if self.cache_cv:
694
+ self._CACHE_CV = []
695
+ for (train_idxs, test_idxs) in self._KFOLD:
696
+ self._CACHE_CV.append(_dask_fold_splitter(train_idxs, test_idxs, _X, _y))
697
+
698
+
699
+ def _fit_all_folds(
700
+ self,
701
+ _X:DaskXType,
702
+ _y:DaskYType,
703
+ _grid:dict[str, Any],
704
+ _fit_params
705
+ ) -> list[tuple[ClassifierProtocol, float, bool], ...]:
706
+ """Fit on each train/test split for one single set of
707
+ hyperparameter values (one permutation of GSCV).
708
+
709
+ Parameters
710
+ ----------
711
+ _X : DaskXType
712
+ The data.
713
+ _y : DaskYType
714
+ The target for the data.
715
+ _grid : dict[str, Any]
716
+ The values for the hyperparameters for this permutation of
717
+ grid search.
718
+
719
+ Returns
720
+ -------
721
+ FIT_OUTPUT : list[tuple[ClassifierProtocol, float, bool], ...]
722
+ A list of tuples, one tuple for each fold, with each tuple
723
+ holding the respective fitted estimator for that fold of
724
+ train/test data, the fit time, and a bool indicating whether
725
+ the fit raised an error.
726
+
727
+ """
728
+
729
+ # **** IMPORTANT NOTES ABOUT estimator & n_jobs ****
730
+ # this is from sklearn notes, where the problem was discovered.
731
+ # there was a (sometimes large, >> 0.10) anomaly in scores
732
+ # when n_jobs was set to (1, None) vs. (-1, 2, 3, 4) when using
733
+ # SK Logistic. While running the fits under a regular for-loop,
734
+ # the SK estimator itself was found to be the cause, from being
735
+ # fit on repeatedly without reconstruct. There appears be some
736
+ # form of state information being retained in the estimator that
737
+ # is altered with fit and alters subsequent fits slightly. there
738
+ # is very little on google and ChatGPT about this (ChatGPT also
739
+ # thinks that 'state' is the problem.) Locking random_state made
740
+ # no difference, and locking other things with randomness (KFold,
741
+ # etc.) made no difference. There was no experimentation done to
742
+ # see if that problem extends beyond SK Logistic or SK. joblib
743
+ # with n_jobs (1, None) behaves exactly the same as a regular
744
+ # for-loop. The solution is to reconstruct the estimator with each
745
+ # fit and fit it once - then the results agree exactly with the
746
+ # results when n_jobs > 1 and with SK gscv. 'estimator' is being
747
+ # rebuilt at each call with the class constructor
748
+ # type(_estimator)(**_estimator.get_params(deep=True)).
749
+
750
+ # must use shallow params to construct estimator
751
+ s_p = self._estimator.get_params(deep=False) # shallow_params
752
+ # must use deep params for pipeline to set GSCV params (depth
753
+ # doesnt matter for an estimator when not pipeline.)
754
+ d_p = self._estimator.get_params(deep=True) # deep_params
755
+
756
+ _fold_fit_params = _dask_estimator_fit_params_helper(
757
+ [*compute(len(_y))][0], # n_samples
758
+ _fit_params,
759
+ self._KFOLD
760
+ )
761
+
762
+ FIT_OUTPUT = []
763
+ for f_idx in range(self.n_splits_):
764
+
765
+ # train only!
766
+ if self.cache_cv:
767
+ _X_y_train = list(zip(*self._CACHE_CV[f_idx]))[0]
768
+ elif not self.cache_cv:
769
+ _X_y_train = list(zip(*_dask_fold_splitter(*self._KFOLD[f_idx], _X, _y)))[0]
770
+
771
+ FIT_OUTPUT.append(
772
+ _parallelized_fit(
773
+ f_idx,
774
+ *_X_y_train,
775
+ type(self._estimator)(**s_p).set_params(**deepcopy(d_p)),
776
+ _grid,
777
+ self.error_score,
778
+ **_fold_fit_params[f_idx]
779
+ )
780
+ )
781
+
782
+ del s_p, d_p, _fold_fit_params, _X_y_train
783
+
784
+ return FIT_OUTPUT
785
+
786
+
787
+ def _score_all_folds_and_thresholds(
788
+ self,
789
+ _X:DaskXType,
790
+ _y:DaskYType,
791
+ _FIT_OUTPUT:list[tuple[ClassifierProtocol, float, bool], ...],
792
+ _THRESHOLDS:ThresholdsWIPType
793
+ ) -> list[tuple[MaskedHolderType, MaskedHolderType], ...]:
794
+ """For each fitted estimator associated with each fold, produce
795
+ the y_pred vector for that fold's test data and score it against
796
+ the actual y.
797
+
798
+ Parameters
799
+ ----------
800
+ _X : DaskXType
801
+ The data.
802
+ _y : DaskYType
803
+ The target for the data.
804
+ _FIT_OUTPUT : list[tuple[ClassifierProtocol, float, bool], ...]
805
+ A list of tuples, one tuple for each fold, with each tuple
806
+ holding the respective fitted estimator for that fold of
807
+ train/test data, the fit time, and a bool indicating whether
808
+ the fit raised an error.
809
+ _THRESHOLDS : ThresholdsWIPType
810
+ The thresholds for which to calculate scores.
811
+
812
+ Returns
813
+ -------
814
+ TEST_SCORER_OUT : list[tuple[MaskedHolderType, MaskedHolderType], ...]
815
+ TEST_THRESHOLD_x_SCORER__SCORE_LAYER : MaskedHolderType
816
+ Masked array of shape (n_thresholds, n_scorers) holding
817
+ the scores for each scorer on each threshold for one
818
+ fold of test data.
819
+ TEST_THRESHOLD_x_SCORER__SCORE_TIME_LAYER : MaskedHolderType
820
+ Masked array of shape (n_thresholds, n_scorers) holding
821
+ the times to score each scorer on each threshold for one
822
+ fold of test data.
823
+
824
+ """
825
+
826
+ TEST_SCORER_OUT = []
827
+ for f_idx in range(self.n_splits_):
828
+
829
+ # test only!
830
+ if self.cache_cv:
831
+ _X_y_test = list(zip(*self._CACHE_CV[f_idx]))[1]
832
+ elif not self.cache_cv:
833
+ _X_y_test = list(zip(*_dask_fold_splitter(*self._KFOLD[f_idx], _X, _y)))[1]
834
+
835
+ TEST_SCORER_OUT.append(
836
+ _parallelized_scorer(
837
+ *_X_y_test,
838
+ _FIT_OUTPUT[f_idx],
839
+ f_idx,
840
+ self.scorer_,
841
+ _THRESHOLDS,
842
+ self.error_score,
843
+ self._verbose
844
+ )
845
+ )
846
+
847
+ del _X_y_test
848
+
849
+ return TEST_SCORER_OUT
850
+ # END SCORE ALL FOLDS & THRESHOLDS #################################
851
+
852
+
853
+ def _score_train(
854
+ self,
855
+ _X:DaskXType,
856
+ _y:DaskYType,
857
+ _FIT_OUTPUT:list[tuple[ClassifierProtocol, float, bool], ...],
858
+ _BEST_THRESHOLDS_BY_SCORER:NDArrayHolderType
859
+ ) -> list[MaskedHolderType]:
860
+ # TRAIN_SCORER_OUT is TRAIN_SCORER__SCORE_LAYER
861
+ """Using the fitted estimator for each fold, all the scorers,
862
+ and the best thresholds for each scorer, score the train data
863
+ for each fold using all the scorers and the single best threshold
864
+ for the respective scorer.
865
+
866
+ Parameters
867
+ ----------
868
+ _X : DaskXType
869
+ The data.
870
+ _y : DaskYType
871
+ The target for the data.
872
+ _FIT_OUTPUT : list[tuple[ClassifierProtocol, float, bool], ...]
873
+ A list of tuples, one tuple for each fold, with each tuple
874
+ holding the respective fitted estimator for that fold of
875
+ train/test data, the fit time, and a bool indicating whether
876
+ the fit raised an error.
877
+ _BEST_THRESHOLDS_BY_SCORER : NDArrayHolderType
878
+ The best threshold for each scorer as found by averaging the
879
+ scores across each fold of test data.
880
+
881
+ Returns
882
+ -------
883
+ TRAIN_SCORER_OUT : list[MaskedHolderType]
884
+ List of masked arrays where each masked array holds the
885
+ scores for a fold of train data using every scorer and the
886
+ best threshold associated with that scorer.
887
+
888
+ """
889
+
890
+ TRAIN_SCORER_OUT = []
891
+ for f_idx in range(self.n_splits_):
892
+
893
+ # train only!
894
+ if self.cache_cv:
895
+ _X_y_train = list(zip(*self._CACHE_CV[f_idx]))[0]
896
+ elif not self.cache_cv:
897
+ _X_y_train = list(zip(*_dask_fold_splitter(*self._KFOLD[f_idx], _X, _y)))[0]
898
+
899
+ TRAIN_SCORER_OUT.append(
900
+ _parallelized_train_scorer(
901
+ *_X_y_train,
902
+ _FIT_OUTPUT[f_idx],
903
+ f_idx,
904
+ self.scorer_,
905
+ _BEST_THRESHOLDS_BY_SCORER,
906
+ self.error_score,
907
+ self._verbose
908
+ )
909
+ )
910
+
911
+ del _X_y_train
912
+
913
+ return TRAIN_SCORER_OUT
914
+
915
+
916
+
917
+
918
+