lazypredict 0.3.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.
@@ -0,0 +1,633 @@
1
+ """
2
+ Supervised Models — LazyClassifier and LazyRegressor for rapid model benchmarking.
3
+
4
+ Provides LazyClassifier and LazyRegressor classes that train multiple
5
+ scikit-learn models with minimal code to quickly identify which algorithms
6
+ perform best on a given dataset.
7
+ """
8
+ # Author: Shankar Rao Pandala <shankar.pandala@live.com>
9
+
10
+ import logging
11
+ import signal
12
+ from contextlib import contextmanager
13
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
14
+
15
+ import numpy as np
16
+ import pandas as pd
17
+ from sklearn.base import ClassifierMixin, RegressorMixin
18
+ from sklearn.metrics import (
19
+ accuracy_score,
20
+ balanced_accuracy_score,
21
+ f1_score,
22
+ mean_squared_error,
23
+ precision_score,
24
+ r2_score,
25
+ recall_score,
26
+ roc_auc_score,
27
+ )
28
+ from sklearn.pipeline import Pipeline
29
+ from sklearn.utils import all_estimators
30
+
31
+ # Re-export from submodules for backward compatibility
32
+ from lazypredict._base import ( # noqa: F401
33
+ LazyEstimator,
34
+ _validate_fit_inputs,
35
+ _validate_init_params,
36
+ )
37
+ from lazypredict.config import (
38
+ REMOVED_CLASSIFIERS,
39
+ REMOVED_REGRESSORS,
40
+ VALID_ENCODERS,
41
+ )
42
+ from lazypredict.exceptions import TimeoutException
43
+ from lazypredict.integrations.mlflow import ( # noqa: F401
44
+ MLFLOW_AVAILABLE,
45
+ is_mlflow_tracking_enabled,
46
+ setup_mlflow,
47
+ )
48
+ from lazypredict.metrics import adjusted_rsquared
49
+ from lazypredict.preprocessing import ( # noqa: F401
50
+ CATEGORY_ENCODERS_AVAILABLE,
51
+ build_preprocessor,
52
+ categorical_transformer_high,
53
+ categorical_transformer_low,
54
+ get_card_split,
55
+ get_categorical_encoder,
56
+ numeric_transformer,
57
+ prepare_dataframes,
58
+ )
59
+
60
+ # Module-level logger — users can configure via logging.getLogger("lazypredict")
61
+ logger = logging.getLogger("lazypredict")
62
+
63
+ # Keep private aliases used in the module
64
+ _VALID_ENCODERS = VALID_ENCODERS
65
+ _REMOVED_CLASSIFIERS = REMOVED_CLASSIFIERS
66
+ _REMOVED_REGRESSORS = REMOVED_REGRESSORS
67
+
68
+ # Private aliases for prepare/build used within fit()
69
+ _prepare_dataframes = prepare_dataframes
70
+ _build_preprocessor = build_preprocessor
71
+
72
+ # Optional xgboost
73
+ try:
74
+ import xgboost
75
+ _XGBOOST_AVAILABLE = True
76
+ except ImportError:
77
+ _XGBOOST_AVAILABLE = False
78
+
79
+ # Optional lightgbm
80
+ try:
81
+ import lightgbm
82
+ _LIGHTGBM_AVAILABLE = True
83
+ except ImportError:
84
+ _LIGHTGBM_AVAILABLE = False
85
+
86
+ # Optional catboost
87
+ try:
88
+ import catboost
89
+ _CATBOOST_AVAILABLE = True
90
+ except ImportError:
91
+ _CATBOOST_AVAILABLE = False
92
+
93
+ # Optional perpetual
94
+ try:
95
+ from perpetual import PerpetualBooster
96
+ PERPETUAL_AVAILABLE = True
97
+ except ImportError:
98
+ PERPETUAL_AVAILABLE = False
99
+
100
+ # Optional InterpretML (EBM)
101
+ try:
102
+ from interpret.glassbox import (
103
+ ExplainableBoostingClassifier,
104
+ ExplainableBoostingRegressor,
105
+ )
106
+ _INTERPRET_AVAILABLE = True
107
+ except ImportError:
108
+ _INTERPRET_AVAILABLE = False
109
+
110
+ # Intel Extension for Scikit-learn for better performance
111
+ try:
112
+ from sklearnex import patch_sklearn
113
+ patch_sklearn()
114
+ INTEL_EXTENSION_AVAILABLE = True
115
+ except ImportError:
116
+ INTEL_EXTENSION_AVAILABLE = False
117
+
118
+ # Optional MLflow
119
+ try:
120
+ import mlflow
121
+ except ImportError:
122
+ mlflow = None # type: ignore[assignment]
123
+
124
+ # Kept as module-level lists for backward compatibility but built fresh
125
+ CLASSIFIERS: List[Tuple[str, Any]] = [
126
+ est
127
+ for est in all_estimators()
128
+ if issubclass(est[1], ClassifierMixin) and est[0] not in _REMOVED_CLASSIFIERS
129
+ ]
130
+
131
+ REGRESSORS: List[Tuple[str, Any]] = [
132
+ est
133
+ for est in all_estimators()
134
+ if issubclass(est[1], RegressorMixin) and est[0] not in _REMOVED_REGRESSORS
135
+ ]
136
+
137
+ # Append optional boosting models
138
+ if _XGBOOST_AVAILABLE:
139
+ REGRESSORS.append(("XGBRegressor", xgboost.XGBRegressor))
140
+ CLASSIFIERS.append(("XGBClassifier", xgboost.XGBClassifier))
141
+
142
+ if _LIGHTGBM_AVAILABLE:
143
+ REGRESSORS.append(("LGBMRegressor", lightgbm.LGBMRegressor))
144
+ CLASSIFIERS.append(("LGBMClassifier", lightgbm.LGBMClassifier))
145
+
146
+ if _CATBOOST_AVAILABLE:
147
+ REGRESSORS.append(("CatBoostRegressor", catboost.CatBoostRegressor))
148
+ CLASSIFIERS.append(("CatBoostClassifier", catboost.CatBoostClassifier))
149
+
150
+ if PERPETUAL_AVAILABLE:
151
+ REGRESSORS.append(("PerpetualBooster", PerpetualBooster))
152
+ CLASSIFIERS.append(("PerpetualBooster", PerpetualBooster))
153
+
154
+ if _INTERPRET_AVAILABLE:
155
+ CLASSIFIERS.append(("ExplainableBoostingClassifier", ExplainableBoostingClassifier))
156
+ REGRESSORS.append(("ExplainableBoostingRegressor", ExplainableBoostingRegressor))
157
+
158
+ # Backward-compatible aliases for removed_ lists
159
+ removed_classifiers = list(_REMOVED_CLASSIFIERS)
160
+ removed_regressors = list(_REMOVED_REGRESSORS)
161
+
162
+
163
+ @contextmanager
164
+ def time_limit(seconds: int):
165
+ """Context manager to limit execution time of a code block.
166
+
167
+ Parameters
168
+ ----------
169
+ seconds : int
170
+ Maximum time in seconds for the code block to execute.
171
+
172
+ Raises
173
+ ------
174
+ TimeoutException
175
+ If the code block exceeds the time limit.
176
+ """
177
+ def signal_handler(signum, frame):
178
+ raise TimeoutException(f"Timed out after {seconds} seconds")
179
+
180
+ if hasattr(signal, "SIGALRM"):
181
+ signal.signal(signal.SIGALRM, signal_handler)
182
+ signal.alarm(seconds)
183
+ try:
184
+ yield
185
+ finally:
186
+ signal.alarm(0)
187
+ else:
188
+ yield
189
+
190
+
191
+ # ---------------------------------------------------------------------------
192
+ # LazyClassifier
193
+ # ---------------------------------------------------------------------------
194
+
195
+
196
+ class LazyClassifier(LazyEstimator):
197
+ """Fit all classification algorithms available in scikit-learn and benchmark them.
198
+
199
+ Parameters
200
+ ----------
201
+ verbose : int, optional (default=0)
202
+ Set to a positive number to enable progress bars and per-model metric output.
203
+ ignore_warnings : bool, optional (default=True)
204
+ When True, warnings and errors from individual models are suppressed.
205
+ custom_metric : callable or None, optional (default=None)
206
+ A function ``f(y_true, y_pred)`` used for additional evaluation.
207
+ predictions : bool, optional (default=False)
208
+ When True, ``fit()`` returns a tuple of (scores, predictions_dataframe).
209
+ random_state : int, optional (default=42)
210
+ Random seed passed to models that accept it.
211
+ classifiers : list or ``"all"``, optional (default="all")
212
+ Specific classifier classes to train, or ``"all"`` for every available one.
213
+ cv : int or None, optional (default=None)
214
+ Number of folds for cross-validation. If None, uses train/test split only.
215
+ timeout : int or float or None, optional (default=None)
216
+ Maximum seconds for each model. Models exceeding this are skipped.
217
+ categorical_encoder : str, optional (default='onehot')
218
+ Encoder for categorical features: ``'onehot'``, ``'ordinal'``,
219
+ ``'target'``, or ``'binary'``.
220
+ n_jobs : int, optional (default=-1)
221
+ Number of parallel jobs for cross-validation. -1 uses all processors.
222
+ max_models : int or None, optional (default=None)
223
+ Maximum number of models to train. None means train all.
224
+ progress_callback : callable or None, optional (default=None)
225
+ Callback ``f(model_name, current, total, metrics)`` called after each model.
226
+ use_gpu : bool, optional (default=False)
227
+ When True, enables GPU acceleration.
228
+ tune : bool, optional (default=False)
229
+ When True, tunes the top-k models after initial benchmarking.
230
+ tune_top_k : int, optional (default=5)
231
+ Number of top models to tune.
232
+ tune_trials : int, optional (default=50)
233
+ Number of optimization trials per model.
234
+ tune_timeout : int, float, or None, optional (default=None)
235
+ Maximum seconds per model tuning.
236
+ tune_backend : str, optional (default='optuna')
237
+ Tuning backend: ``'optuna'``, ``'sklearn'``, or ``'flaml'``.
238
+
239
+ Examples
240
+ --------
241
+ >>> from lazypredict.Supervised import LazyClassifier
242
+ >>> from sklearn.datasets import load_breast_cancer
243
+ >>> from sklearn.model_selection import train_test_split
244
+ >>> data = load_breast_cancer()
245
+ >>> X = data.data
246
+ >>> y = data.target
247
+ >>> X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.5, random_state=123)
248
+ >>> clf = LazyClassifier(verbose=0, ignore_warnings=True, custom_metric=None)
249
+ >>> models, predictions = clf.fit(X_train, X_test, y_train, y_test)
250
+ """
251
+
252
+ def __init__(
253
+ self,
254
+ verbose: int = 0,
255
+ ignore_warnings: bool = True,
256
+ custom_metric: Optional[Callable] = None,
257
+ predictions: bool = False,
258
+ random_state: int = 42,
259
+ classifiers: Union[str, List] = "all",
260
+ cv: Optional[int] = None,
261
+ timeout: Optional[Union[int, float]] = None,
262
+ categorical_encoder: str = "onehot",
263
+ n_jobs: int = -1,
264
+ max_models: Optional[int] = None,
265
+ progress_callback: Optional[Callable] = None,
266
+ use_gpu: bool = False,
267
+ tune: bool = False,
268
+ tune_top_k: int = 5,
269
+ tune_trials: int = 50,
270
+ tune_timeout: Optional[Union[int, float]] = None,
271
+ tune_backend: str = "optuna",
272
+ ):
273
+ super().__init__(
274
+ verbose=verbose,
275
+ ignore_warnings=ignore_warnings,
276
+ custom_metric=custom_metric,
277
+ predictions=predictions,
278
+ random_state=random_state,
279
+ cv=cv,
280
+ timeout=timeout,
281
+ categorical_encoder=categorical_encoder,
282
+ n_jobs=n_jobs,
283
+ max_models=max_models,
284
+ progress_callback=progress_callback,
285
+ use_gpu=use_gpu,
286
+ tune=tune,
287
+ tune_top_k=tune_top_k,
288
+ tune_trials=tune_trials,
289
+ tune_timeout=tune_timeout,
290
+ tune_backend=tune_backend,
291
+ )
292
+ self.classifiers = classifiers
293
+
294
+ def _estimator_step_name(self) -> str:
295
+ return "classifier"
296
+
297
+ def _get_estimator_list(self) -> List[Tuple[str, Any]]:
298
+ if self.classifiers == "all":
299
+ return list(CLASSIFIERS)
300
+ try:
301
+ return [(cls.__name__, cls) for cls in self.classifiers]
302
+ except Exception as exc:
303
+ logger.error("Invalid classifier(s): %s", exc)
304
+ raise ValueError(f"Invalid classifier(s): {exc}") from exc
305
+
306
+ def _tune_scoring(self) -> str:
307
+ return "balanced_accuracy"
308
+
309
+ def _cv_scoring(self) -> Optional[Dict[str, str]]:
310
+ return {
311
+ "accuracy": "accuracy",
312
+ "balanced_accuracy": "balanced_accuracy",
313
+ "f1_weighted": "f1_weighted",
314
+ "precision_weighted": "precision_weighted",
315
+ "recall_weighted": "recall_weighted",
316
+ "roc_auc_ovr_weighted": "roc_auc_ovr_weighted",
317
+ }
318
+
319
+ def _cv_column_names(self) -> List[str]:
320
+ return [
321
+ "Accuracy CV Mean", "Accuracy CV Std",
322
+ "Balanced Accuracy CV Mean", "Balanced Accuracy CV Std",
323
+ "ROC AUC CV Mean", "ROC AUC CV Std",
324
+ "F1 Score CV Mean", "F1 Score CV Std",
325
+ "Precision CV Mean", "Precision CV Std",
326
+ "Recall CV Mean", "Recall CV Std",
327
+ ]
328
+
329
+ def _process_cv_results(
330
+ self, cv_results: Dict[str, Any], X_train: pd.DataFrame
331
+ ) -> Dict[str, Optional[float]]:
332
+ result: Dict[str, Optional[float]] = {
333
+ "Accuracy CV Mean": cv_results["test_accuracy"].mean(),
334
+ "Accuracy CV Std": cv_results["test_accuracy"].std(),
335
+ "Balanced Accuracy CV Mean": cv_results["test_balanced_accuracy"].mean(),
336
+ "Balanced Accuracy CV Std": cv_results["test_balanced_accuracy"].std(),
337
+ "F1 Score CV Mean": cv_results["test_f1_weighted"].mean(),
338
+ "F1 Score CV Std": cv_results["test_f1_weighted"].std(),
339
+ "Precision CV Mean": cv_results["test_precision_weighted"].mean(),
340
+ "Precision CV Std": cv_results["test_precision_weighted"].std(),
341
+ "Recall CV Mean": cv_results["test_recall_weighted"].mean(),
342
+ "Recall CV Std": cv_results["test_recall_weighted"].std(),
343
+ }
344
+ try:
345
+ result["ROC AUC CV Mean"] = cv_results["test_roc_auc_ovr_weighted"].mean()
346
+ result["ROC AUC CV Std"] = cv_results["test_roc_auc_ovr_weighted"].std()
347
+ except Exception:
348
+ result["ROC AUC CV Mean"] = None
349
+ result["ROC AUC CV Std"] = None
350
+ return result
351
+
352
+ def _compute_metrics(
353
+ self, pipe: Pipeline, X_test: pd.DataFrame, y_test: Any, X_train: pd.DataFrame
354
+ ) -> Dict[str, Any]:
355
+ y_pred = pipe.predict(X_test)
356
+ accuracy = accuracy_score(y_test, y_pred, normalize=True)
357
+ b_accuracy = balanced_accuracy_score(y_test, y_pred)
358
+ f1 = f1_score(y_test, y_pred, average="weighted")
359
+ precision = precision_score(y_test, y_pred, average="weighted", zero_division=0)
360
+ recall_val = recall_score(y_test, y_pred, average="weighted")
361
+
362
+ roc_auc = None
363
+ try:
364
+ if hasattr(pipe, "predict_proba"):
365
+ y_pred_proba = pipe.predict_proba(X_test)
366
+ if y_pred_proba.shape[1] == 2:
367
+ roc_auc = roc_auc_score(y_test, y_pred_proba[:, 1])
368
+ else:
369
+ roc_auc = roc_auc_score(
370
+ y_test, y_pred_proba, multi_class="ovr", average="weighted"
371
+ )
372
+ elif hasattr(pipe, "decision_function"):
373
+ roc_auc = roc_auc_score(y_test, pipe.decision_function(X_test))
374
+ else:
375
+ roc_auc = roc_auc_score(y_test, y_pred)
376
+ except Exception as roc_exc:
377
+ if not self.ignore_warnings:
378
+ logger.warning("ROC AUC couldn't be calculated: %s", roc_exc)
379
+
380
+ return {
381
+ "accuracy": accuracy,
382
+ "balanced_accuracy": b_accuracy,
383
+ "roc_auc": roc_auc,
384
+ "f1": f1,
385
+ "precision": precision,
386
+ "recall": recall_val,
387
+ }
388
+
389
+ def _build_scores_dataframe(self, results: List[Dict[str, Any]]) -> pd.DataFrame:
390
+ if not results:
391
+ return pd.DataFrame()
392
+
393
+ rows = []
394
+ for r in results:
395
+ row: Dict[str, Any] = {
396
+ "Model": r["name"],
397
+ "Accuracy": r["accuracy"],
398
+ "Balanced Accuracy": r["balanced_accuracy"],
399
+ "ROC AUC": r["roc_auc"],
400
+ "F1 Score": r["f1"],
401
+ "Precision": r["precision"],
402
+ "Recall": r["recall"],
403
+ }
404
+ if self.custom_metric is not None:
405
+ row[self.custom_metric.__name__] = r.get("custom_metric")
406
+ # CV columns
407
+ for col in self._cv_column_names():
408
+ if col in r:
409
+ row[col] = r[col]
410
+ row["Time Taken"] = r["time"]
411
+ rows.append(row)
412
+
413
+ scores = pd.DataFrame(rows)
414
+ scores = scores.sort_values(by="Balanced Accuracy", ascending=False).set_index("Model")
415
+ return scores
416
+
417
+ def _log_verbose(self, name: str, metrics: Dict[str, Any]) -> None:
418
+ logger.info(
419
+ "Model=%s Accuracy=%.4f BalAcc=%.4f ROC_AUC=%s F1=%.4f Time=%.2fs",
420
+ name,
421
+ metrics["accuracy"],
422
+ metrics["balanced_accuracy"],
423
+ metrics["roc_auc"],
424
+ metrics["f1"],
425
+ metrics["time"],
426
+ )
427
+
428
+
429
+ # ---------------------------------------------------------------------------
430
+ # LazyRegressor
431
+ # ---------------------------------------------------------------------------
432
+
433
+
434
+ class LazyRegressor(LazyEstimator):
435
+ """Fit all regression algorithms available in scikit-learn and benchmark them.
436
+
437
+ Parameters
438
+ ----------
439
+ verbose : int, optional (default=0)
440
+ Set to a positive number to enable progress bars and per-model metric output.
441
+ ignore_warnings : bool, optional (default=True)
442
+ When True, warnings and errors from individual models are suppressed.
443
+ custom_metric : callable or None, optional (default=None)
444
+ A function ``f(y_true, y_pred)`` used for additional evaluation.
445
+ predictions : bool, optional (default=False)
446
+ When True, ``fit()`` returns a tuple of (scores, predictions_dataframe).
447
+ random_state : int, optional (default=42)
448
+ Random seed passed to models that accept it.
449
+ regressors : list or ``"all"``, optional (default="all")
450
+ Specific regressor classes to train, or ``"all"`` for every available one.
451
+ cv : int or None, optional (default=None)
452
+ Number of folds for cross-validation. If None, uses train/test split only.
453
+ timeout : int or float or None, optional (default=None)
454
+ Maximum seconds for each model. Models exceeding this are skipped.
455
+ categorical_encoder : str, optional (default='onehot')
456
+ Encoder for categorical features: ``'onehot'``, ``'ordinal'``,
457
+ ``'target'``, or ``'binary'``.
458
+ n_jobs : int, optional (default=-1)
459
+ Number of parallel jobs for cross-validation. -1 uses all processors.
460
+ max_models : int or None, optional (default=None)
461
+ Maximum number of models to train. None means train all.
462
+ progress_callback : callable or None, optional (default=None)
463
+ Callback ``f(model_name, current, total, metrics)`` called after each model.
464
+ use_gpu : bool, optional (default=False)
465
+ When True, enables GPU acceleration. Falls back to CPU if CUDA is unavailable.
466
+ tune : bool, optional (default=False)
467
+ When True, tunes the top-k models after initial benchmarking.
468
+ tune_top_k : int, optional (default=5)
469
+ Number of top models to tune.
470
+ tune_trials : int, optional (default=50)
471
+ Number of optimization trials per model.
472
+ tune_timeout : int, float, or None, optional (default=None)
473
+ Maximum seconds per model tuning.
474
+ tune_backend : str, optional (default='optuna')
475
+ Tuning backend: ``'optuna'``, ``'sklearn'``, or ``'flaml'``.
476
+
477
+ Examples
478
+ --------
479
+ >>> from lazypredict.Supervised import LazyRegressor
480
+ >>> from sklearn import datasets
481
+ >>> from sklearn.utils import shuffle
482
+ >>> import numpy as np
483
+ >>> diabetes = datasets.load_diabetes()
484
+ >>> X, y = shuffle(diabetes.data, diabetes.target, random_state=13)
485
+ >>> X = X.astype(np.float32)
486
+ >>> offset = int(X.shape[0] * 0.9)
487
+ >>> X_train, y_train = X[:offset], y[:offset]
488
+ >>> X_test, y_test = X[offset:], y[offset:]
489
+ >>> reg = LazyRegressor(verbose=0, ignore_warnings=False, custom_metric=None)
490
+ >>> models, predictions = reg.fit(X_train, X_test, y_train, y_test)
491
+ """
492
+
493
+ def __init__(
494
+ self,
495
+ verbose: int = 0,
496
+ ignore_warnings: bool = True,
497
+ custom_metric: Optional[Callable] = None,
498
+ predictions: bool = False,
499
+ random_state: int = 42,
500
+ regressors: Union[str, List] = "all",
501
+ cv: Optional[int] = None,
502
+ timeout: Optional[Union[int, float]] = None,
503
+ categorical_encoder: str = "onehot",
504
+ n_jobs: int = -1,
505
+ max_models: Optional[int] = None,
506
+ progress_callback: Optional[Callable] = None,
507
+ use_gpu: bool = False,
508
+ tune: bool = False,
509
+ tune_top_k: int = 5,
510
+ tune_trials: int = 50,
511
+ tune_timeout: Optional[Union[int, float]] = None,
512
+ tune_backend: str = "optuna",
513
+ ):
514
+ super().__init__(
515
+ verbose=verbose,
516
+ ignore_warnings=ignore_warnings,
517
+ custom_metric=custom_metric,
518
+ predictions=predictions,
519
+ random_state=random_state,
520
+ cv=cv,
521
+ timeout=timeout,
522
+ categorical_encoder=categorical_encoder,
523
+ n_jobs=n_jobs,
524
+ max_models=max_models,
525
+ progress_callback=progress_callback,
526
+ use_gpu=use_gpu,
527
+ tune=tune,
528
+ tune_top_k=tune_top_k,
529
+ tune_trials=tune_trials,
530
+ tune_timeout=tune_timeout,
531
+ tune_backend=tune_backend,
532
+ )
533
+ self.regressors = regressors
534
+
535
+ def _estimator_step_name(self) -> str:
536
+ return "regressor"
537
+
538
+ def _get_estimator_list(self) -> List[Tuple[str, Any]]:
539
+ if self.regressors == "all":
540
+ return list(REGRESSORS)
541
+ try:
542
+ return [(cls.__name__, cls) for cls in self.regressors]
543
+ except Exception as exc:
544
+ logger.error("Invalid regressor(s): %s", exc)
545
+ raise ValueError(f"Invalid regressor(s): {exc}") from exc
546
+
547
+ def _tune_scoring(self) -> str:
548
+ return "r2"
549
+
550
+ def _cv_scoring(self) -> Optional[Dict[str, str]]:
551
+ return {
552
+ "r2": "r2",
553
+ "neg_mean_squared_error": "neg_mean_squared_error",
554
+ }
555
+
556
+ def _cv_column_names(self) -> List[str]:
557
+ return [
558
+ "R-Squared CV Mean", "R-Squared CV Std",
559
+ "Adjusted R-Squared CV Mean", "Adjusted R-Squared CV Std",
560
+ "RMSE CV Mean", "RMSE CV Std",
561
+ ]
562
+
563
+ def _process_cv_results(
564
+ self, cv_results: Dict[str, Any], X_train: pd.DataFrame
565
+ ) -> Dict[str, Optional[float]]:
566
+ rmse_cv = np.sqrt(-cv_results["test_neg_mean_squared_error"])
567
+ adj_r2_cv = [
568
+ adjusted_rsquared(r2, X_train.shape[0], X_train.shape[1])
569
+ for r2 in cv_results["test_r2"]
570
+ ]
571
+ return {
572
+ "R-Squared CV Mean": cv_results["test_r2"].mean(),
573
+ "R-Squared CV Std": cv_results["test_r2"].std(),
574
+ "Adjusted R-Squared CV Mean": np.mean(adj_r2_cv),
575
+ "Adjusted R-Squared CV Std": np.std(adj_r2_cv),
576
+ "RMSE CV Mean": rmse_cv.mean(),
577
+ "RMSE CV Std": rmse_cv.std(),
578
+ }
579
+
580
+ def _compute_metrics(
581
+ self, pipe: Pipeline, X_test: pd.DataFrame, y_test: Any, X_train: pd.DataFrame
582
+ ) -> Dict[str, Any]:
583
+ y_pred = pipe.predict(X_test)
584
+ r_squared = r2_score(y_test, y_pred)
585
+ adj_rsquared = adjusted_rsquared(
586
+ r_squared, X_test.shape[0], X_test.shape[1]
587
+ )
588
+ rmse = np.sqrt(mean_squared_error(y_test, y_pred))
589
+ return {
590
+ "r_squared": r_squared,
591
+ "adjusted_r_squared": adj_rsquared,
592
+ "rmse": rmse,
593
+ }
594
+
595
+ def _build_scores_dataframe(self, results: List[Dict[str, Any]]) -> pd.DataFrame:
596
+ if not results:
597
+ return pd.DataFrame()
598
+
599
+ rows = []
600
+ for r in results:
601
+ row: Dict[str, Any] = {
602
+ "Model": r["name"],
603
+ "Adjusted R-Squared": r["adjusted_r_squared"],
604
+ "R-Squared": r["r_squared"],
605
+ "RMSE": r["rmse"],
606
+ }
607
+ # CV columns
608
+ for col in self._cv_column_names():
609
+ if col in r:
610
+ row[col] = r[col]
611
+ if self.custom_metric is not None:
612
+ row[self.custom_metric.__name__] = r.get("custom_metric")
613
+ row["Time Taken"] = r["time"]
614
+ rows.append(row)
615
+
616
+ scores = pd.DataFrame(rows)
617
+ scores = scores.sort_values(by="Adjusted R-Squared", ascending=False).set_index("Model")
618
+ return scores
619
+
620
+ def _log_verbose(self, name: str, metrics: Dict[str, Any]) -> None:
621
+ logger.info(
622
+ "Model=%s R2=%.4f AdjR2=%.4f RMSE=%.4f Time=%.2fs",
623
+ name,
624
+ metrics["r_squared"],
625
+ metrics["adjusted_r_squared"],
626
+ metrics["rmse"],
627
+ metrics["time"],
628
+ )
629
+
630
+
631
+ # Backward-compatible aliases
632
+ Regression = LazyRegressor
633
+ Classification = LazyClassifier