lazypredict 0.3.0a2__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,577 @@
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
+ # Intel Extension for Scikit-learn for better performance
101
+ try:
102
+ from sklearnex import patch_sklearn
103
+ patch_sklearn()
104
+ INTEL_EXTENSION_AVAILABLE = True
105
+ except ImportError:
106
+ INTEL_EXTENSION_AVAILABLE = False
107
+
108
+ # Optional MLflow
109
+ try:
110
+ import mlflow
111
+ except ImportError:
112
+ mlflow = None # type: ignore[assignment]
113
+
114
+ # Kept as module-level lists for backward compatibility but built fresh
115
+ CLASSIFIERS: List[Tuple[str, Any]] = [
116
+ est
117
+ for est in all_estimators()
118
+ if issubclass(est[1], ClassifierMixin) and est[0] not in _REMOVED_CLASSIFIERS
119
+ ]
120
+
121
+ REGRESSORS: List[Tuple[str, Any]] = [
122
+ est
123
+ for est in all_estimators()
124
+ if issubclass(est[1], RegressorMixin) and est[0] not in _REMOVED_REGRESSORS
125
+ ]
126
+
127
+ # Append optional boosting models
128
+ if _XGBOOST_AVAILABLE:
129
+ REGRESSORS.append(("XGBRegressor", xgboost.XGBRegressor))
130
+ CLASSIFIERS.append(("XGBClassifier", xgboost.XGBClassifier))
131
+
132
+ if _LIGHTGBM_AVAILABLE:
133
+ REGRESSORS.append(("LGBMRegressor", lightgbm.LGBMRegressor))
134
+ CLASSIFIERS.append(("LGBMClassifier", lightgbm.LGBMClassifier))
135
+
136
+ if _CATBOOST_AVAILABLE:
137
+ REGRESSORS.append(("CatBoostRegressor", catboost.CatBoostRegressor))
138
+ CLASSIFIERS.append(("CatBoostClassifier", catboost.CatBoostClassifier))
139
+
140
+ if PERPETUAL_AVAILABLE:
141
+ REGRESSORS.append(("PerpetualBooster", PerpetualBooster))
142
+ CLASSIFIERS.append(("PerpetualBooster", PerpetualBooster))
143
+
144
+ # Backward-compatible aliases for removed_ lists
145
+ removed_classifiers = list(_REMOVED_CLASSIFIERS)
146
+ removed_regressors = list(_REMOVED_REGRESSORS)
147
+
148
+
149
+ @contextmanager
150
+ def time_limit(seconds: int):
151
+ """Context manager to limit execution time of a code block.
152
+
153
+ Parameters
154
+ ----------
155
+ seconds : int
156
+ Maximum time in seconds for the code block to execute.
157
+
158
+ Raises
159
+ ------
160
+ TimeoutException
161
+ If the code block exceeds the time limit.
162
+ """
163
+ def signal_handler(signum, frame):
164
+ raise TimeoutException(f"Timed out after {seconds} seconds")
165
+
166
+ if hasattr(signal, "SIGALRM"):
167
+ signal.signal(signal.SIGALRM, signal_handler)
168
+ signal.alarm(seconds)
169
+ try:
170
+ yield
171
+ finally:
172
+ signal.alarm(0)
173
+ else:
174
+ yield
175
+
176
+
177
+ # ---------------------------------------------------------------------------
178
+ # LazyClassifier
179
+ # ---------------------------------------------------------------------------
180
+
181
+
182
+ class LazyClassifier(LazyEstimator):
183
+ """Fit all classification algorithms available in scikit-learn and benchmark them.
184
+
185
+ Parameters
186
+ ----------
187
+ verbose : int, optional (default=0)
188
+ Set to a positive number to enable progress bars and per-model metric output.
189
+ ignore_warnings : bool, optional (default=True)
190
+ When True, warnings and errors from individual models are suppressed.
191
+ custom_metric : callable or None, optional (default=None)
192
+ A function ``f(y_true, y_pred)`` used for additional evaluation.
193
+ predictions : bool, optional (default=False)
194
+ When True, ``fit()`` returns a tuple of (scores, predictions_dataframe).
195
+ random_state : int, optional (default=42)
196
+ Random seed passed to models that accept it.
197
+ classifiers : list or ``"all"``, optional (default="all")
198
+ Specific classifier classes to train, or ``"all"`` for every available one.
199
+ cv : int or None, optional (default=None)
200
+ Number of folds for cross-validation. If None, uses train/test split only.
201
+ timeout : int or float or None, optional (default=None)
202
+ Maximum seconds for each model. Models exceeding this are skipped.
203
+ categorical_encoder : str, optional (default='onehot')
204
+ Encoder for categorical features: ``'onehot'``, ``'ordinal'``,
205
+ ``'target'``, or ``'binary'``.
206
+ n_jobs : int, optional (default=-1)
207
+ Number of parallel jobs for cross-validation. -1 uses all processors.
208
+ max_models : int or None, optional (default=None)
209
+ Maximum number of models to train. None means train all.
210
+ progress_callback : callable or None, optional (default=None)
211
+ Callback ``f(model_name, current, total, metrics)`` called after each model.
212
+ use_gpu : bool, optional (default=False)
213
+ When True, enables GPU acceleration for models that support it
214
+ (e.g., XGBoost, LightGBM, CatBoost). When cuML (RAPIDS) is installed,
215
+ GPU-accelerated scikit-learn equivalents are also added automatically.
216
+ Falls back to CPU if CUDA is unavailable.
217
+
218
+ Examples
219
+ --------
220
+ >>> from lazypredict.Supervised import LazyClassifier
221
+ >>> from sklearn.datasets import load_breast_cancer
222
+ >>> from sklearn.model_selection import train_test_split
223
+ >>> data = load_breast_cancer()
224
+ >>> X = data.data
225
+ >>> y = data.target
226
+ >>> X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.5, random_state=123)
227
+ >>> clf = LazyClassifier(verbose=0, ignore_warnings=True, custom_metric=None)
228
+ >>> models, predictions = clf.fit(X_train, X_test, y_train, y_test)
229
+ """
230
+
231
+ def __init__(
232
+ self,
233
+ verbose: int = 0,
234
+ ignore_warnings: bool = True,
235
+ custom_metric: Optional[Callable] = None,
236
+ predictions: bool = False,
237
+ random_state: int = 42,
238
+ classifiers: Union[str, List] = "all",
239
+ cv: Optional[int] = None,
240
+ timeout: Optional[Union[int, float]] = None,
241
+ categorical_encoder: str = "onehot",
242
+ n_jobs: int = -1,
243
+ max_models: Optional[int] = None,
244
+ progress_callback: Optional[Callable] = None,
245
+ use_gpu: bool = False,
246
+ ):
247
+ super().__init__(
248
+ verbose=verbose,
249
+ ignore_warnings=ignore_warnings,
250
+ custom_metric=custom_metric,
251
+ predictions=predictions,
252
+ random_state=random_state,
253
+ cv=cv,
254
+ timeout=timeout,
255
+ categorical_encoder=categorical_encoder,
256
+ n_jobs=n_jobs,
257
+ max_models=max_models,
258
+ progress_callback=progress_callback,
259
+ use_gpu=use_gpu,
260
+ )
261
+ self.classifiers = classifiers
262
+
263
+ def _estimator_step_name(self) -> str:
264
+ return "classifier"
265
+
266
+ def _get_estimator_list(self) -> List[Tuple[str, Any]]:
267
+ if self.classifiers == "all":
268
+ return list(CLASSIFIERS)
269
+ try:
270
+ return [(cls.__name__, cls) for cls in self.classifiers]
271
+ except Exception as exc:
272
+ logger.error("Invalid classifier(s): %s", exc)
273
+ raise ValueError(f"Invalid classifier(s): {exc}") from exc
274
+
275
+ def _cv_scoring(self) -> Optional[Dict[str, str]]:
276
+ return {
277
+ "accuracy": "accuracy",
278
+ "balanced_accuracy": "balanced_accuracy",
279
+ "f1_weighted": "f1_weighted",
280
+ "precision_weighted": "precision_weighted",
281
+ "recall_weighted": "recall_weighted",
282
+ "roc_auc_ovr_weighted": "roc_auc_ovr_weighted",
283
+ }
284
+
285
+ def _cv_column_names(self) -> List[str]:
286
+ return [
287
+ "Accuracy CV Mean", "Accuracy CV Std",
288
+ "Balanced Accuracy CV Mean", "Balanced Accuracy CV Std",
289
+ "ROC AUC CV Mean", "ROC AUC CV Std",
290
+ "F1 Score CV Mean", "F1 Score CV Std",
291
+ "Precision CV Mean", "Precision CV Std",
292
+ "Recall CV Mean", "Recall CV Std",
293
+ ]
294
+
295
+ def _process_cv_results(
296
+ self, cv_results: Dict[str, Any], X_train: pd.DataFrame
297
+ ) -> Dict[str, Optional[float]]:
298
+ result: Dict[str, Optional[float]] = {
299
+ "Accuracy CV Mean": cv_results["test_accuracy"].mean(),
300
+ "Accuracy CV Std": cv_results["test_accuracy"].std(),
301
+ "Balanced Accuracy CV Mean": cv_results["test_balanced_accuracy"].mean(),
302
+ "Balanced Accuracy CV Std": cv_results["test_balanced_accuracy"].std(),
303
+ "F1 Score CV Mean": cv_results["test_f1_weighted"].mean(),
304
+ "F1 Score CV Std": cv_results["test_f1_weighted"].std(),
305
+ "Precision CV Mean": cv_results["test_precision_weighted"].mean(),
306
+ "Precision CV Std": cv_results["test_precision_weighted"].std(),
307
+ "Recall CV Mean": cv_results["test_recall_weighted"].mean(),
308
+ "Recall CV Std": cv_results["test_recall_weighted"].std(),
309
+ }
310
+ try:
311
+ result["ROC AUC CV Mean"] = cv_results["test_roc_auc_ovr_weighted"].mean()
312
+ result["ROC AUC CV Std"] = cv_results["test_roc_auc_ovr_weighted"].std()
313
+ except Exception:
314
+ result["ROC AUC CV Mean"] = None
315
+ result["ROC AUC CV Std"] = None
316
+ return result
317
+
318
+ def _compute_metrics(
319
+ self, pipe: Pipeline, X_test: pd.DataFrame, y_test: Any, X_train: pd.DataFrame
320
+ ) -> Dict[str, Any]:
321
+ y_pred = pipe.predict(X_test)
322
+ accuracy = accuracy_score(y_test, y_pred, normalize=True)
323
+ b_accuracy = balanced_accuracy_score(y_test, y_pred)
324
+ f1 = f1_score(y_test, y_pred, average="weighted")
325
+ precision = precision_score(y_test, y_pred, average="weighted", zero_division=0)
326
+ recall_val = recall_score(y_test, y_pred, average="weighted")
327
+
328
+ roc_auc = None
329
+ try:
330
+ if hasattr(pipe, "predict_proba"):
331
+ y_pred_proba = pipe.predict_proba(X_test)
332
+ if y_pred_proba.shape[1] == 2:
333
+ roc_auc = roc_auc_score(y_test, y_pred_proba[:, 1])
334
+ else:
335
+ roc_auc = roc_auc_score(
336
+ y_test, y_pred_proba, multi_class="ovr", average="weighted"
337
+ )
338
+ elif hasattr(pipe, "decision_function"):
339
+ roc_auc = roc_auc_score(y_test, pipe.decision_function(X_test))
340
+ else:
341
+ roc_auc = roc_auc_score(y_test, y_pred)
342
+ except Exception as roc_exc:
343
+ if not self.ignore_warnings:
344
+ logger.warning("ROC AUC couldn't be calculated: %s", roc_exc)
345
+
346
+ return {
347
+ "accuracy": accuracy,
348
+ "balanced_accuracy": b_accuracy,
349
+ "roc_auc": roc_auc,
350
+ "f1": f1,
351
+ "precision": precision,
352
+ "recall": recall_val,
353
+ }
354
+
355
+ def _build_scores_dataframe(self, results: List[Dict[str, Any]]) -> pd.DataFrame:
356
+ if not results:
357
+ return pd.DataFrame()
358
+
359
+ rows = []
360
+ for r in results:
361
+ row: Dict[str, Any] = {
362
+ "Model": r["name"],
363
+ "Accuracy": r["accuracy"],
364
+ "Balanced Accuracy": r["balanced_accuracy"],
365
+ "ROC AUC": r["roc_auc"],
366
+ "F1 Score": r["f1"],
367
+ "Precision": r["precision"],
368
+ "Recall": r["recall"],
369
+ }
370
+ if self.custom_metric is not None:
371
+ row[self.custom_metric.__name__] = r.get("custom_metric")
372
+ # CV columns
373
+ for col in self._cv_column_names():
374
+ if col in r:
375
+ row[col] = r[col]
376
+ row["Time Taken"] = r["time"]
377
+ rows.append(row)
378
+
379
+ scores = pd.DataFrame(rows)
380
+ scores = scores.sort_values(by="Balanced Accuracy", ascending=False).set_index("Model")
381
+ return scores
382
+
383
+ def _log_verbose(self, name: str, metrics: Dict[str, Any]) -> None:
384
+ logger.info(
385
+ "Model=%s Accuracy=%.4f BalAcc=%.4f ROC_AUC=%s F1=%.4f Time=%.2fs",
386
+ name,
387
+ metrics["accuracy"],
388
+ metrics["balanced_accuracy"],
389
+ metrics["roc_auc"],
390
+ metrics["f1"],
391
+ metrics["time"],
392
+ )
393
+
394
+
395
+ # ---------------------------------------------------------------------------
396
+ # LazyRegressor
397
+ # ---------------------------------------------------------------------------
398
+
399
+
400
+ class LazyRegressor(LazyEstimator):
401
+ """Fit all regression algorithms available in scikit-learn and benchmark them.
402
+
403
+ Parameters
404
+ ----------
405
+ verbose : int, optional (default=0)
406
+ Set to a positive number to enable progress bars and per-model metric output.
407
+ ignore_warnings : bool, optional (default=True)
408
+ When True, warnings and errors from individual models are suppressed.
409
+ custom_metric : callable or None, optional (default=None)
410
+ A function ``f(y_true, y_pred)`` used for additional evaluation.
411
+ predictions : bool, optional (default=False)
412
+ When True, ``fit()`` returns a tuple of (scores, predictions_dataframe).
413
+ random_state : int, optional (default=42)
414
+ Random seed passed to models that accept it.
415
+ regressors : list or ``"all"``, optional (default="all")
416
+ Specific regressor classes to train, or ``"all"`` for every available one.
417
+ cv : int or None, optional (default=None)
418
+ Number of folds for cross-validation. If None, uses train/test split only.
419
+ timeout : int or float or None, optional (default=None)
420
+ Maximum seconds for each model. Models exceeding this are skipped.
421
+ categorical_encoder : str, optional (default='onehot')
422
+ Encoder for categorical features: ``'onehot'``, ``'ordinal'``,
423
+ ``'target'``, or ``'binary'``.
424
+ n_jobs : int, optional (default=-1)
425
+ Number of parallel jobs for cross-validation. -1 uses all processors.
426
+ max_models : int or None, optional (default=None)
427
+ Maximum number of models to train. None means train all.
428
+ progress_callback : callable or None, optional (default=None)
429
+ Callback ``f(model_name, current, total, metrics)`` called after each model.
430
+ use_gpu : bool, optional (default=False)
431
+ When True, enables GPU acceleration for models that support it
432
+ (e.g., XGBoost, LightGBM). Falls back to CPU if CUDA is unavailable.
433
+
434
+ Examples
435
+ --------
436
+ >>> from lazypredict.Supervised import LazyRegressor
437
+ >>> from sklearn import datasets
438
+ >>> from sklearn.utils import shuffle
439
+ >>> import numpy as np
440
+ >>> diabetes = datasets.load_diabetes()
441
+ >>> X, y = shuffle(diabetes.data, diabetes.target, random_state=13)
442
+ >>> X = X.astype(np.float32)
443
+ >>> offset = int(X.shape[0] * 0.9)
444
+ >>> X_train, y_train = X[:offset], y[:offset]
445
+ >>> X_test, y_test = X[offset:], y[offset:]
446
+ >>> reg = LazyRegressor(verbose=0, ignore_warnings=False, custom_metric=None)
447
+ >>> models, predictions = reg.fit(X_train, X_test, y_train, y_test)
448
+ """
449
+
450
+ def __init__(
451
+ self,
452
+ verbose: int = 0,
453
+ ignore_warnings: bool = True,
454
+ custom_metric: Optional[Callable] = None,
455
+ predictions: bool = False,
456
+ random_state: int = 42,
457
+ regressors: Union[str, List] = "all",
458
+ cv: Optional[int] = None,
459
+ timeout: Optional[Union[int, float]] = None,
460
+ categorical_encoder: str = "onehot",
461
+ n_jobs: int = -1,
462
+ max_models: Optional[int] = None,
463
+ progress_callback: Optional[Callable] = None,
464
+ use_gpu: bool = False,
465
+ ):
466
+ super().__init__(
467
+ verbose=verbose,
468
+ ignore_warnings=ignore_warnings,
469
+ custom_metric=custom_metric,
470
+ predictions=predictions,
471
+ random_state=random_state,
472
+ cv=cv,
473
+ timeout=timeout,
474
+ categorical_encoder=categorical_encoder,
475
+ n_jobs=n_jobs,
476
+ max_models=max_models,
477
+ progress_callback=progress_callback,
478
+ use_gpu=use_gpu,
479
+ )
480
+ self.regressors = regressors
481
+
482
+ def _estimator_step_name(self) -> str:
483
+ return "regressor"
484
+
485
+ def _get_estimator_list(self) -> List[Tuple[str, Any]]:
486
+ if self.regressors == "all":
487
+ return list(REGRESSORS)
488
+ try:
489
+ return [(cls.__name__, cls) for cls in self.regressors]
490
+ except Exception as exc:
491
+ logger.error("Invalid regressor(s): %s", exc)
492
+ raise ValueError(f"Invalid regressor(s): {exc}") from exc
493
+
494
+ def _cv_scoring(self) -> Optional[Dict[str, str]]:
495
+ return {
496
+ "r2": "r2",
497
+ "neg_mean_squared_error": "neg_mean_squared_error",
498
+ }
499
+
500
+ def _cv_column_names(self) -> List[str]:
501
+ return [
502
+ "R-Squared CV Mean", "R-Squared CV Std",
503
+ "Adjusted R-Squared CV Mean", "Adjusted R-Squared CV Std",
504
+ "RMSE CV Mean", "RMSE CV Std",
505
+ ]
506
+
507
+ def _process_cv_results(
508
+ self, cv_results: Dict[str, Any], X_train: pd.DataFrame
509
+ ) -> Dict[str, Optional[float]]:
510
+ rmse_cv = np.sqrt(-cv_results["test_neg_mean_squared_error"])
511
+ adj_r2_cv = [
512
+ adjusted_rsquared(r2, X_train.shape[0], X_train.shape[1])
513
+ for r2 in cv_results["test_r2"]
514
+ ]
515
+ return {
516
+ "R-Squared CV Mean": cv_results["test_r2"].mean(),
517
+ "R-Squared CV Std": cv_results["test_r2"].std(),
518
+ "Adjusted R-Squared CV Mean": np.mean(adj_r2_cv),
519
+ "Adjusted R-Squared CV Std": np.std(adj_r2_cv),
520
+ "RMSE CV Mean": rmse_cv.mean(),
521
+ "RMSE CV Std": rmse_cv.std(),
522
+ }
523
+
524
+ def _compute_metrics(
525
+ self, pipe: Pipeline, X_test: pd.DataFrame, y_test: Any, X_train: pd.DataFrame
526
+ ) -> Dict[str, Any]:
527
+ y_pred = pipe.predict(X_test)
528
+ r_squared = r2_score(y_test, y_pred)
529
+ adj_rsquared = adjusted_rsquared(
530
+ r_squared, X_test.shape[0], X_test.shape[1]
531
+ )
532
+ rmse = np.sqrt(mean_squared_error(y_test, y_pred))
533
+ return {
534
+ "r_squared": r_squared,
535
+ "adjusted_r_squared": adj_rsquared,
536
+ "rmse": rmse,
537
+ }
538
+
539
+ def _build_scores_dataframe(self, results: List[Dict[str, Any]]) -> pd.DataFrame:
540
+ if not results:
541
+ return pd.DataFrame()
542
+
543
+ rows = []
544
+ for r in results:
545
+ row: Dict[str, Any] = {
546
+ "Model": r["name"],
547
+ "Adjusted R-Squared": r["adjusted_r_squared"],
548
+ "R-Squared": r["r_squared"],
549
+ "RMSE": r["rmse"],
550
+ }
551
+ # CV columns
552
+ for col in self._cv_column_names():
553
+ if col in r:
554
+ row[col] = r[col]
555
+ if self.custom_metric is not None:
556
+ row[self.custom_metric.__name__] = r.get("custom_metric")
557
+ row["Time Taken"] = r["time"]
558
+ rows.append(row)
559
+
560
+ scores = pd.DataFrame(rows)
561
+ scores = scores.sort_values(by="Adjusted R-Squared", ascending=False).set_index("Model")
562
+ return scores
563
+
564
+ def _log_verbose(self, name: str, metrics: Dict[str, Any]) -> None:
565
+ logger.info(
566
+ "Model=%s R2=%.4f AdjR2=%.4f RMSE=%.4f Time=%.2fs",
567
+ name,
568
+ metrics["r_squared"],
569
+ metrics["adjusted_r_squared"],
570
+ metrics["rmse"],
571
+ metrics["time"],
572
+ )
573
+
574
+
575
+ # Backward-compatible aliases
576
+ Regression = LazyRegressor
577
+ Classification = LazyClassifier