classifier-toolkit 0.1.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 (26) hide show
  1. classifier_toolkit/eda/__init__.py +19 -0
  2. classifier_toolkit/eda/bivariate_analysis.py +413 -0
  3. classifier_toolkit/eda/eda_toolkit.py +549 -0
  4. classifier_toolkit/eda/feature_engineering.py +1310 -0
  5. classifier_toolkit/eda/first_glance.py +253 -0
  6. classifier_toolkit/eda/univariate_analysis.py +778 -0
  7. classifier_toolkit/eda/visualizations.py +1248 -0
  8. classifier_toolkit/eda/warnings/__init__.py +4 -0
  9. classifier_toolkit/eda/warnings/automated_warnings.py +20 -0
  10. classifier_toolkit/eda/warnings/default_warnings.py +286 -0
  11. classifier_toolkit/feature_selection/__init__.py +33 -0
  12. classifier_toolkit/feature_selection/base.py +182 -0
  13. classifier_toolkit/feature_selection/embedded_methods/__init__.py +7 -0
  14. classifier_toolkit/feature_selection/embedded_methods/elastic_net.py +178 -0
  15. classifier_toolkit/feature_selection/meta_selector.py +329 -0
  16. classifier_toolkit/feature_selection/utils/__init__.py +17 -0
  17. classifier_toolkit/feature_selection/utils/plottings.py +65 -0
  18. classifier_toolkit/feature_selection/utils/scoring.py +86 -0
  19. classifier_toolkit/feature_selection/wrapper_methods/__init__.py +11 -0
  20. classifier_toolkit/feature_selection/wrapper_methods/rfe.py +345 -0
  21. classifier_toolkit/feature_selection/wrapper_methods/sequential_selection.py +566 -0
  22. classifier_toolkit/model_fitting/__init__.py +0 -0
  23. classifier_toolkit/model_fitting/model_search.py +3 -0
  24. classifier_toolkit-0.1.0.dist-info/METADATA +99 -0
  25. classifier_toolkit-0.1.0.dist-info/RECORD +26 -0
  26. classifier_toolkit-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,1310 @@
1
+ import math
2
+ from typing import List, Literal, NamedTuple, Optional
3
+
4
+ import matplotlib.pyplot as plt
5
+ import numpy as np
6
+ import numpy.typing as npt
7
+ import pandas as pd
8
+ from category_encoders import CatBoostEncoder
9
+ from scipy.stats import anderson, probplot
10
+ from sklearn.cluster import KMeans
11
+ from sklearn.decomposition import PCA, TruncatedSVD
12
+ from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
13
+ from sklearn.impute import KNNImputer, SimpleImputer
14
+ from sklearn.model_selection import cross_val_score
15
+ from sklearn.preprocessing import (
16
+ OneHotEncoder,
17
+ OrdinalEncoder,
18
+ PowerTransformer,
19
+ StandardScaler,
20
+ )
21
+
22
+
23
+ class AndersonResult(NamedTuple):
24
+ statistic: float
25
+ critical_values: npt.NDArray
26
+ significance_level: npt.NDArray
27
+
28
+
29
+ class FeatureEngineering:
30
+ def __init__(
31
+ self,
32
+ dataframe: pd.DataFrame,
33
+ target_column: str,
34
+ numerical_columns: List[str],
35
+ categorical_columns: List[str],
36
+ to_be_encoded: Optional[List[str]] = None,
37
+ transformations: Optional[List[dict]] = None,
38
+ ):
39
+ """
40
+ Initialize the FeatureEngineering class with data and configuration.
41
+
42
+ Parameters
43
+ ----------
44
+ dataframe : pd.DataFrame
45
+ The data to be processed.
46
+ target_column : str
47
+ The target column for analysis.
48
+ numerical_columns : List[str]
49
+ List of numerical columns in the data.
50
+ categorical_columns : List[str]
51
+ List of categorical columns in the data.
52
+ to_be_encoded : Optional[List[str]], optional
53
+ Columns to be encoded, by default None.
54
+ transformations : Optional[List[dict]], optional
55
+ List of transformations to be applied, by default None.
56
+ """
57
+ self.data = dataframe.copy()
58
+ self.target_column = target_column
59
+ self.numerical_columns = numerical_columns
60
+ self.categorical_columns = categorical_columns
61
+ self.encode_list = to_be_encoded if to_be_encoded else categorical_columns
62
+ self.transformations = (
63
+ transformations if isinstance(transformations, list) else []
64
+ )
65
+ self.transformed_data: Optional[pd.DataFrame] = None
66
+
67
+ def _log_transformation(self, method_name: str, params: dict):
68
+ """
69
+ Log the transformation applied to the data.
70
+
71
+ Parameters
72
+ ----------
73
+ method_name : str
74
+ The name of the transformation method.
75
+ params : dict
76
+ Parameters used in the transformation.
77
+ """
78
+ self.transformations.append({"method": method_name, "params": params})
79
+
80
+ def apply_stored_transformations(self, new_data: pd.DataFrame) -> pd.DataFrame:
81
+ """
82
+ Apply stored transformations to new data.
83
+
84
+ Parameters
85
+ ----------
86
+ new_data : pd.DataFrame
87
+ The new data to apply transformations to.
88
+
89
+ Returns
90
+ -------
91
+ pd.DataFrame
92
+ The transformed data.
93
+ """
94
+ backup = self.data.copy()
95
+ self.data = new_data
96
+
97
+ for transformation in self.transformations:
98
+ method = transformation["method"]
99
+ params = transformation["params"]
100
+
101
+ if method == "handle_missing_values":
102
+ new_data = self.handle_missing_values(**params)
103
+ elif method == "normality_transform":
104
+ new_data = self.apply_normality_transformations(**params)
105
+ elif method == "winsorization":
106
+ new_data = self.apply_winsorization(**params)
107
+ elif method == "encoding_categorical":
108
+ new_data = self.encoding_categorical(**params)
109
+ else:
110
+ print(f"Transformation method '{method}' not found.")
111
+
112
+ self.data = backup
113
+ return new_data
114
+
115
+ def handle_missing_values(
116
+ self,
117
+ method: Literal["knn", "mean", "median", "most_frequent", "quantile"] = "knn",
118
+ n_neighbors: Optional[int] = 5,
119
+ ) -> pd.DataFrame:
120
+ """
121
+ Handle missing values in the DataFrame using the specified method.
122
+
123
+ Parameters
124
+ ----------
125
+ method : {'knn', 'mean', 'median', 'most_frequent', 'quantile'}, optional
126
+ The imputation method to use, by default 'knn'.
127
+ n_neighbors : Optional[int], optional
128
+ Number of neighbors to use for KNN imputation, by default 5.
129
+
130
+ Returns
131
+ -------
132
+ pd.DataFrame
133
+ DataFrame with imputed values.
134
+ """
135
+ # Get all columns except the target
136
+ features = self.data.drop(columns=[self.target_column])
137
+ numeric_columns = self.numerical_columns.copy()
138
+ if self.target_column in numeric_columns:
139
+ numeric_columns.remove(self.target_column)
140
+
141
+ # Validate numerical columns
142
+ missing_numerical_columns = [
143
+ col for col in numeric_columns if col not in features.columns
144
+ ]
145
+ if missing_numerical_columns:
146
+ raise KeyError(
147
+ f"Numerical columns not found in DataFrame: {missing_numerical_columns}"
148
+ )
149
+
150
+ numerical_features = features[numeric_columns]
151
+
152
+ # Validate categorical columns
153
+ missing_categorical_columns = [
154
+ col for col in self.categorical_columns if col not in features.columns
155
+ ]
156
+ if missing_categorical_columns:
157
+ raise KeyError(
158
+ f"Categorical columns not found in DataFrame: {missing_categorical_columns}"
159
+ )
160
+
161
+ categorical_features = features[self.categorical_columns].copy()
162
+ encoders = {}
163
+ for column in self.categorical_columns:
164
+ encoder = OrdinalEncoder(
165
+ handle_unknown="use_encoded_value", unknown_value=-1
166
+ )
167
+ categorical_features[column] = encoder.fit_transform(
168
+ categorical_features[[column]]
169
+ ).ravel()
170
+ encoders[column] = encoder
171
+
172
+ # Combine numeric and encoded categorical features
173
+ combined_features = pd.concat(
174
+ [numerical_features, categorical_features], axis=1
175
+ )
176
+
177
+ # Check if the method is one of the SimpleImputer strategies
178
+ simple_imputer_methods = ["mean", "median", "most_frequent", "quantile"]
179
+
180
+ if method.lower() == "knn":
181
+ # Initialize the KNN imputer
182
+ if n_neighbors is None:
183
+ raise ValueError("n_neighbors must be specified for KNN imputation.")
184
+ imputer = KNNImputer(n_neighbors=n_neighbors)
185
+ elif method.lower() in simple_imputer_methods:
186
+ # Initialize the SimpleImputer based on the method
187
+ if method.lower() == "quantile":
188
+ imputer = SimpleImputer(strategy="constant", fill_value=0.5)
189
+ else:
190
+ imputer = SimpleImputer(strategy=method.lower())
191
+ else:
192
+ raise ValueError(
193
+ f"Method '{method}' is not supported. Choose from 'knn', 'mean', 'median', 'most_frequent', or 'quantile'."
194
+ )
195
+
196
+ # Fit and transform the combined features
197
+ imputed_features = imputer.fit_transform(combined_features)
198
+
199
+ # Convert the imputed array back to a DataFrame
200
+ imputed_df = pd.DataFrame(
201
+ imputed_features, columns=combined_features.columns, index=self.data.index
202
+ )
203
+
204
+ # Decode categorical columns
205
+ for column in self.categorical_columns:
206
+ imputed_df[column] = (
207
+ encoders[column].inverse_transform(imputed_df[[column]]).ravel()
208
+ )
209
+
210
+ # Add the target column back
211
+ imputed_df[self.target_column] = self.data[self.target_column]
212
+
213
+ # Update the current DataFrame with the imputed values
214
+ self.data[imputed_df.columns] = imputed_df
215
+
216
+ print(f"\nMissing values have been imputed using {method} imputation.")
217
+ print("Note: The target column was excluded from imputation.")
218
+ print("Categorical columns were encoded before imputation and then decoded.")
219
+
220
+ # Log the transformation
221
+ self._log_transformation(
222
+ "handle_missing", {"method": method, "n_neighbors": n_neighbors}
223
+ )
224
+ return self.data
225
+
226
+ def diagnose_normality_transform(
227
+ self,
228
+ method: Literal["power", "standard"] = "power",
229
+ alpha: float = 0.05,
230
+ test_mode: bool = False,
231
+ ) -> dict:
232
+ transformed_data = self.data.copy()
233
+ results = {}
234
+ for column in self.numerical_columns:
235
+ if column == self.target_column:
236
+ print(
237
+ f"{column} is the target column. Skipping normality check and transformation."
238
+ )
239
+ continue
240
+ # Perform Anderson-Darling test before transformation
241
+ ad_result: AndersonResult = anderson(self.data[column].dropna()) # type: ignore
242
+ ad_statistic = ad_result.statistic
243
+ ad_critical_values = ad_result.critical_values
244
+ ad_significance_level = ad_result.significance_level
245
+ is_normal_ad = (
246
+ ad_statistic
247
+ < ad_critical_values[np.searchsorted(ad_significance_level, alpha)]
248
+ )
249
+
250
+ results[column] = {
251
+ "ad_statistic": ad_statistic,
252
+ "ad_critical_values": ad_critical_values,
253
+ "is_normal_ad": is_normal_ad,
254
+ "transformation": "None",
255
+ "lambda": None,
256
+ }
257
+
258
+ # Create Q-Q plot before transformation
259
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
260
+ probplot(self.data[column], dist="norm", plot=ax1)
261
+ ax1.set_title(f"{column} Before Transformation")
262
+
263
+ # If not normal, apply selected transformation
264
+ if not is_normal_ad:
265
+ if method.lower() == "power":
266
+ transformer = PowerTransformer(method="yeo-johnson")
267
+ elif method.lower() == "standard":
268
+ transformer = StandardScaler()
269
+
270
+ # Mask for non-missing values
271
+ non_missing_mask = ~self.data[column].isna()
272
+
273
+ # Fit and transform only non-missing values
274
+ transformed_data_col = transformer.fit_transform(
275
+ self.data.loc[non_missing_mask, [column]]
276
+ )
277
+ transformed_data_col = transformed_data_col.flatten()
278
+
279
+ # Perform Anderson-Darling test after transformation
280
+ ad_result_after: AndersonResult = anderson(transformed_data_col) # type: ignore
281
+ ad_statistic_after = ad_result_after.statistic
282
+ ad_critical_values_after = ad_result_after.critical_values
283
+ is_normal_ad_after = (
284
+ ad_statistic_after
285
+ < ad_critical_values_after[
286
+ np.searchsorted(ad_significance_level, alpha)
287
+ ]
288
+ )
289
+
290
+ # Create Q-Q plot after transformation
291
+ probplot(transformed_data_col, dist="norm", plot=ax2)
292
+ ax2.set_title(f"{column} After Transformation")
293
+
294
+ # Update the column with transformed data in the transformed DataFrame
295
+ transformed_data[column] = self.data[column].astype(float)
296
+ transformed_data.loc[non_missing_mask, column] = transformed_data_col
297
+ results[column]["transformation"] = method.capitalize() + "Transformer"
298
+ if method == "power":
299
+ results[column]["lambda"] = transformer.lambdas_[0] # type: ignore
300
+ results[column]["ad_statistic_after"] = ad_statistic_after
301
+ results[column]["ad_critical_values_after"] = ad_critical_values_after
302
+ results[column]["is_normal_ad_after"] = is_normal_ad_after
303
+ else:
304
+ ax2.axis("off")
305
+
306
+ plt.tight_layout()
307
+ if not test_mode:
308
+ plt.show()
309
+
310
+ # Store the transformed data
311
+ self.transformed_data = transformed_data
312
+
313
+ # Print results
314
+ print("\nNormality Test Results and Transformations:")
315
+ for column, result in results.items():
316
+ print(f"\n{column}:")
317
+ print(f" Anderson-Darling statistic: {result['ad_statistic']:.4f}")
318
+ print(f" Initially normal (Anderson-Darling): {result['is_normal_ad']}")
319
+ if result["transformation"] != "None":
320
+ print(f" Transformation applied: {result['transformation']}")
321
+ if result["transformation"] == "PowerTransformer":
322
+ print(f" Lambda value: {result['lambda']:.4f}")
323
+ print(
324
+ f" Anderson-Darling statistic after transformation: {result['ad_statistic_after']:.4f}"
325
+ )
326
+ print(
327
+ f" Normal after transformation (Anderson-Darling): {result['is_normal_ad_after']}"
328
+ )
329
+ else:
330
+ print(" No transformation applied")
331
+
332
+ self._log_transformation(
333
+ "normality_transform", {"method": method, "alpha": alpha}
334
+ )
335
+ return results
336
+
337
+ def apply_normality_transformations(
338
+ self, columns_to_keep: List[str]
339
+ ) -> pd.DataFrame:
340
+ """
341
+ Apply the transformations to the specified columns and keep them in the DataFrame.
342
+
343
+ Parameters
344
+ ----------
345
+ columns_to_keep : List[str]
346
+ List of columns to keep after applying transformations.
347
+
348
+ Returns
349
+ -------
350
+ pd.DataFrame
351
+ DataFrame with applied transformations.
352
+ """
353
+ if self.transformed_data is None:
354
+ raise ValueError(
355
+ "No transformations have been applied yet. Please run normality_transform first."
356
+ )
357
+
358
+ # Create a new DataFrame with the selected columns
359
+ updated_data = self.data.copy()
360
+ for column in columns_to_keep:
361
+ if column in self.transformed_data.columns:
362
+ updated_data[column] = self.transformed_data[column]
363
+ else:
364
+ raise ValueError(f"Column {column} not found in transformed data")
365
+
366
+ self.data = updated_data
367
+ self._log_transformation(
368
+ "apply_transformations", {"columns_to_keep": columns_to_keep}
369
+ )
370
+ return self.data
371
+
372
+ def handle_high_cardinality(
373
+ self,
374
+ high_cardinality_columns: List[str],
375
+ method: Literal["gathering", "woe", "perlich"] = "gathering",
376
+ ) -> None:
377
+ """
378
+ Handle high cardinality in categorical columns using the specified method.
379
+
380
+ Parameters
381
+ ----------
382
+ high_cardinality_columns : List[str]
383
+ List of columns with high cardinality.
384
+ method : {'gathering', 'woe', 'perlich'}, optional
385
+ The method to use for handling high cardinality, by default 'gathering'.
386
+ """
387
+ self.high_cardinality_columns = high_cardinality_columns
388
+ self.method = method.lower()
389
+
390
+ for column in high_cardinality_columns:
391
+ if self.method == "gathering":
392
+ self._gathering_method(column)
393
+ elif self.method == "woe":
394
+ self._woe_method(column)
395
+ elif self.method == "perlich":
396
+ pass # To be implemented later
397
+ else:
398
+ print("Please choose the correct method: gathering | woe | perlich")
399
+ return None
400
+
401
+ def _gathering_method(self, column: str, threshold: float = 0.01) -> None:
402
+ """
403
+ Gather all categories representing less than a specified percentage of the population into 'other'.
404
+
405
+ Parameters
406
+ ----------
407
+ column : str
408
+ The column to apply the method to.
409
+ threshold : float, optional
410
+ The threshold for the percentage of the population, by default 0.01.
411
+ """
412
+ value_counts = self.data[column].value_counts(normalize=True)
413
+ small_categories = value_counts[value_counts < threshold].index # type: ignore
414
+ self.data[column] = self.data[column].apply(
415
+ lambda x: "other" if x in small_categories else x
416
+ )
417
+
418
+ def _woe_method(self, column: str) -> None:
419
+ """
420
+ Calculate the Weight of Evidence (WOE) for high-cardinality columns.
421
+
422
+ Parameters
423
+ ----------
424
+ column : str
425
+ The column to apply the method to.
426
+ """
427
+ if self.data[self.target_column].dtype.name == "category":
428
+ self.data[self.target_column] = self.data[self.target_column].cat.codes
429
+ elif not np.issubdtype(self.data[self.target_column].dtype, np.number): # type: ignore
430
+ self.data[self.target_column] = self.data[self.target_column].astype(float)
431
+
432
+ target = self.data[self.target_column]
433
+ df = pd.DataFrame({column: self.data[column], "target": target})
434
+
435
+ total_good = df["target"].sum()
436
+ total_bad = df["target"].count() - total_good
437
+
438
+ grouped = df.groupby(column)["target"].agg(["sum", "count"])
439
+ grouped["good"] = grouped["sum"]
440
+ grouped["bad"] = grouped["count"] - grouped["sum"]
441
+
442
+ epsilon = 1e-10
443
+ grouped["good_pct"] = (grouped["good"] + epsilon) / (total_good + epsilon)
444
+ grouped["bad_pct"] = (grouped["bad"] + epsilon) / (total_bad + epsilon)
445
+
446
+ grouped["woe"] = np.log(grouped["good_pct"] / grouped["bad_pct"])
447
+
448
+ woe_dict = grouped["woe"].to_dict()
449
+ self.data[column] = (
450
+ self.data[column].map(woe_dict).fillna(0)
451
+ ) # Fill missing values with 0
452
+
453
+ def transform_and_plot_high_cardinality_histograms(
454
+ self,
455
+ high_cardinality_columns: List[str],
456
+ method: Literal["gathering", "woe", "perlich"] = "gathering",
457
+ ) -> Optional[pd.DataFrame]:
458
+ """
459
+ Transform high cardinality columns and plot histograms before and after transformation.
460
+
461
+ Parameters
462
+ ----------
463
+ high_cardinality_columns : List[str]
464
+ List of columns with high cardinality.
465
+ method : {'gathering', 'woe', 'perlich'}, optional
466
+ The method to use for handling high cardinality, by default 'gathering'.
467
+
468
+ Returns
469
+ -------
470
+ pd.DataFrame
471
+ DataFrame with transformed columns.
472
+ """
473
+ self.high_cardinality_columns = high_cardinality_columns
474
+ self.method = method.lower()
475
+ for column in high_cardinality_columns:
476
+ if column not in self.data.columns:
477
+ print(f"Column '{column}' does not exist in the DataFrame.")
478
+ continue
479
+
480
+ original_data = self.data[column].copy()
481
+
482
+ plt.figure(figsize=(12, 6))
483
+ plt.subplot(1, 2, 1)
484
+ original_data.value_counts().plot(kind="bar")
485
+ plt.title(f"{column} - Before Transformation")
486
+ plt.xlabel(column)
487
+ plt.ylabel("Frequency")
488
+
489
+ if self.method == "gathering":
490
+ self._gathering_method(column)
491
+ elif self.method == "woe":
492
+ self._woe_method(column)
493
+ elif self.method == "perlich":
494
+ pass # To be implemented later
495
+ else:
496
+ print("Please choose the correct method: gathering | woe | perlich")
497
+ return None
498
+
499
+ plt.subplot(1, 2, 2)
500
+ if self.method == "woe":
501
+ transformed_data = self.data[column]
502
+ transformed_data.plot(kind="kde", color="#1e78b5")
503
+ plt.title(f"{column} - After Transformation ({method.capitalize()})")
504
+ plt.xlabel(f"{column} (WOE)")
505
+ plt.ylabel("Density")
506
+ else:
507
+ transformed_data = self.data[column].value_counts()
508
+ colors = [
509
+ "#23eb1c" if x == "other" else "#1e78b5"
510
+ for x in transformed_data.index
511
+ ]
512
+ transformed_data.plot(kind="bar", color=colors)
513
+ plt.title(f"{column} - After Transformation ({method.capitalize()})")
514
+ plt.xlabel(column)
515
+ plt.ylabel("Frequency")
516
+
517
+ plt.tight_layout()
518
+ plt.show()
519
+
520
+ return self.data
521
+
522
+ def perform_low_cardinality_checks_numerical(
523
+ self, threshold: int = 10
524
+ ) -> List[str]:
525
+ """
526
+ Check for low cardinality numerical columns.
527
+
528
+ Parameters
529
+ ----------
530
+ threshold : int, optional
531
+ The threshold for low cardinality, by default 10.
532
+
533
+ Returns
534
+ -------
535
+ List[str]
536
+ List of low cardinality numerical columns.
537
+ """
538
+ low_cardinality_numeric = [
539
+ col
540
+ for col in self.numerical_columns
541
+ if self.data[col].nunique() < threshold
542
+ ]
543
+ return low_cardinality_numeric
544
+
545
+ def _kmeans_binning(self, column: str, n_bins: int) -> pd.Series:
546
+ """
547
+ Apply k-means binning to the specified column.
548
+
549
+ Parameters
550
+ ----------
551
+ column : str
552
+ The column to bin.
553
+ n_bins : int
554
+ The number of bins to create.
555
+
556
+ Returns
557
+ -------
558
+ pd.Series
559
+ Series with binned data.
560
+ """
561
+ kmeans = KMeans(n_clusters=n_bins)
562
+ self.data[column + "_binned"] = kmeans.fit_predict(self.data[[column]])
563
+ print(f"KMeans Bins for {column}: {self.data[column + '_binned'].unique()}")
564
+ print(
565
+ f"KMeans Cluster Centers for {column}: {kmeans.cluster_centers_.flatten()}"
566
+ )
567
+ return self.data[column + "_binned"]
568
+
569
+ def _fixed_width_binning(self, column: str, n_bins: int) -> pd.Series:
570
+ """
571
+ Apply fixed-width binning to the specified column.
572
+
573
+ Parameters
574
+ ----------
575
+ column : str
576
+ The column to bin.
577
+ n_bins : int
578
+ The number of bins to create.
579
+
580
+ Returns
581
+ -------
582
+ pd.Series
583
+ Series with binned data.
584
+ """
585
+ min_val = self.data[column].min()
586
+ max_val = self.data[column].max()
587
+
588
+ bin_edges = np.linspace(min_val, max_val, n_bins + 1)
589
+ bin_edges = np.ceil(bin_edges).astype(int)
590
+
591
+ print(f"Fixed Width Bins for {column}: {bin_edges}")
592
+
593
+ self.data[column + "_binned"] = np.digitize(
594
+ self.data[column], bin_edges, right=False
595
+ )
596
+
597
+ print(
598
+ f"Bin counts for {column} with fixed_width binning: {self.data[column + '_binned'].value_counts()}"
599
+ )
600
+
601
+ return self.data[column + "_binned"]
602
+
603
+ def _exponential_binning(self, column: str, n_bins: int) -> pd.Series:
604
+ """
605
+ Apply exponential binning to the specified column.
606
+
607
+ Parameters
608
+ ----------
609
+ column : str
610
+ The column to bin.
611
+ n_bins : int
612
+ The number of bins to create.
613
+
614
+ Returns
615
+ -------
616
+ pd.Series
617
+ Series with binned data.
618
+ """
619
+ min_val = self.data[column].min()
620
+ max_val = self.data[column].max()
621
+ bins = [min_val]
622
+ for i in range(1, n_bins):
623
+ bins.append(
624
+ min_val
625
+ + (math.exp(i) - 1) * (max_val - min_val) / (math.exp(n_bins) - 1)
626
+ )
627
+ bins.append(max_val)
628
+ self.data[column + "_binned"] = (
629
+ np.digitize(self.data[column], bins, right=True) - 1
630
+ )
631
+ print(f"Exponential Bins for {column}: {bins}")
632
+ return self.data[column + "_binned"]
633
+
634
+ def _quantile_binning(self, column: str, n_bins: int) -> pd.Series:
635
+ """
636
+ Apply quantile binning to the specified column.
637
+
638
+ Parameters
639
+ ----------
640
+ column : str
641
+ The column to bin.
642
+ n_bins : int
643
+ The number of bins to create.
644
+
645
+ Returns
646
+ -------
647
+ pd.Series
648
+ Series with binned data.
649
+ """
650
+ try:
651
+ self.data[column + "_binned"] = pd.qcut(
652
+ self.data[column], q=n_bins, labels=False, duplicates="drop"
653
+ )
654
+ print(
655
+ f"Quantile Bins for {column}: {pd.qcut(self.data[column], q=n_bins, duplicates='drop').unique()}"
656
+ )
657
+ except ValueError:
658
+ self.data[column + "_binned"] = pd.qcut(
659
+ self.data[column].rank(method="first"),
660
+ q=n_bins,
661
+ labels=False,
662
+ duplicates="drop",
663
+ )
664
+ print(
665
+ f"Quantile Bins (with ranking) for {column}: {pd.qcut(self.data[column].rank(method='first'), q=n_bins, duplicates='drop').unique()}"
666
+ )
667
+ return self.data[column + "_binned"]
668
+
669
+ def _equal_frequency_binning(self, column: str, n_bins: int) -> pd.Series:
670
+ """
671
+ Apply equal frequency binning to the specified column.
672
+
673
+ Parameters
674
+ ----------
675
+ column : str
676
+ The column to bin.
677
+ n_bins : int
678
+ The number of bins to create.
679
+
680
+ Returns
681
+ -------
682
+ pd.Series
683
+ Series with binned data.
684
+ """
685
+ try:
686
+ self.data[column + "_binned"] = pd.qcut(
687
+ self.data[column], q=n_bins, labels=False, duplicates="drop"
688
+ )
689
+ print(
690
+ f"Equal Frequency Bins for {column}: {pd.qcut(self.data[column], q=n_bins, duplicates='drop').unique()}"
691
+ )
692
+ except ValueError:
693
+ self.data[column + "_binned"] = pd.qcut(
694
+ self.data[column].rank(method="first"),
695
+ q=n_bins,
696
+ labels=False,
697
+ duplicates="drop",
698
+ )
699
+ print(
700
+ f"Equal Frequency Bins (with ranking) for {column}: {pd.qcut(self.data[column].rank(method='first'), q=n_bins, duplicates='drop').unique()}"
701
+ )
702
+ return self.data[column + "_binned"]
703
+
704
+ def binnings(
705
+ self,
706
+ methods: Literal[
707
+ "kmeans", "fixed_width", "exponential", "quantile", "equal_frequency"
708
+ ] = "equal_frequency",
709
+ n_bins: int = 5,
710
+ state: Literal["prod", "dev"] = "prod",
711
+ ) -> pd.DataFrame:
712
+ """
713
+ Apply binning to low cardinality numerical columns using the specified methods.
714
+
715
+ Parameters
716
+ ----------
717
+ methods : {'kmeans', 'fixed_width', 'exponential', 'quantile', 'equal_frequency'}, optional
718
+ The binning methods to use, by default 'equal_frequency'.
719
+ n_bins : int, optional
720
+ The number of bins to create, by default 5.
721
+ state : {'prod', 'dev'}, optional
722
+ The state is to indicate whether we test the function or not, if testing, plots are not shown.
723
+ Returns
724
+ -------
725
+ pd.DataFrame
726
+ DataFrame with binned columns.
727
+ """
728
+ low_cardinality_numeric = self.perform_low_cardinality_checks_numerical()
729
+ print(f"Low cardinality numerical features: {low_cardinality_numeric}")
730
+
731
+ for col in low_cardinality_numeric:
732
+ if state == "prod":
733
+ self.plot_distribution(col, "before transform", binned=False)
734
+
735
+ print(f"\nApplying {methods} binning for column: {col}")
736
+
737
+ if methods == "kmeans":
738
+ self.data[col + "_binned"] = self._kmeans_binning(col, n_bins)
739
+ elif methods == "fixed_width":
740
+ self.data[col + "_binned"] = self._fixed_width_binning(col, n_bins)
741
+ elif methods == "exponential":
742
+ self.data[col + "_binned"] = self._exponential_binning(col, n_bins)
743
+ elif methods == "quantile":
744
+ self.data[col + "_binned"] = self._quantile_binning(col, n_bins)
745
+ elif methods == "equal_frequency":
746
+ self.data[col + "_binned"] = self._equal_frequency_binning(col, n_bins)
747
+ else:
748
+ raise ValueError("Please check the name of the method")
749
+
750
+ print(
751
+ f"Bin counts for {col} with {methods} binning: {self.data[col + '_binned'].value_counts()}"
752
+ )
753
+
754
+ if state == "prod":
755
+ self.plot_distribution(col, method_name=methods, binned=True)
756
+
757
+ return self.data
758
+
759
+ def plot_distribution(
760
+ self,
761
+ column: str,
762
+ method_name: str,
763
+ binned: bool = False,
764
+ log_scale: bool = False,
765
+ ) -> None:
766
+ """
767
+ Plot the distribution of the specified column.
768
+
769
+ Parameters
770
+ ----------
771
+ column : str
772
+ The column to plot.
773
+ method_name : str
774
+ The name of the method used for binning.
775
+ binned : bool, optional
776
+ Whether the data has been binned, by default False.
777
+ log_scale : bool, optional
778
+ Whether to use a log scale for the y-axis, by default False.
779
+ """
780
+ plt.figure(figsize=(10, 6))
781
+ if binned:
782
+ binned_data = self.data[column + "_binned"]
783
+ bin_counts = binned_data.value_counts().sort_index()
784
+
785
+ bars = plt.bar(
786
+ bin_counts.index,
787
+ list(bin_counts.values),
788
+ color="blue",
789
+ alpha=0.7,
790
+ label="Binned",
791
+ )
792
+
793
+ plt.xticks(bin_counts.index, bin_counts.index.to_list(), rotation=45)
794
+
795
+ plt.title(
796
+ f"Distribution of {column} (Binned) with {method_name.capitalize()} Method"
797
+ )
798
+ plt.xlabel(f"{column}")
799
+
800
+ for bar, count in zip(bars, bin_counts.values):
801
+ plt.text(
802
+ bar.get_x() + bar.get_width() / 2,
803
+ bar.get_height() / 2,
804
+ f"{count}",
805
+ ha="center",
806
+ va="center",
807
+ color="white",
808
+ fontsize=10,
809
+ weight="bold",
810
+ )
811
+
812
+ else:
813
+ plt.hist(
814
+ self.data[column], bins="auto", alpha=0.7, color="red", label="Original"
815
+ )
816
+ plt.title(f"Distribution of {column}")
817
+ plt.xlabel(column)
818
+
819
+ plt.ylabel("Frequency")
820
+ if log_scale:
821
+ plt.yscale("log")
822
+ plt.legend()
823
+ plt.tight_layout()
824
+ plt.show()
825
+
826
+ def _encoding_categorical_one_hot(self, data: pd.DataFrame) -> pd.DataFrame:
827
+ """
828
+ Perform one-hot encoding for the specified categorical columns.
829
+
830
+ Parameters
831
+ ----------
832
+ data : pd.DataFrame
833
+ The data containing the categorical columns to encode.
834
+
835
+ Returns
836
+ -------
837
+ pd.DataFrame
838
+ DataFrame with one-hot encoded columns.
839
+ """
840
+ encoder = OneHotEncoder(
841
+ sparse_output=False, drop="first", handle_unknown="ignore"
842
+ )
843
+ encoded_array = encoder.fit_transform(data[self.encode_list])
844
+
845
+ encoded_df = pd.DataFrame(
846
+ encoded_array,
847
+ columns=encoder.get_feature_names_out(self.encode_list),
848
+ index=data.index,
849
+ )
850
+
851
+ data = data.drop(columns=self.encode_list)
852
+ data = pd.concat([data, encoded_df], axis=1)
853
+
854
+ return data
855
+
856
+ def _encoding_categorical_distribution(self, data: pd.DataFrame) -> pd.DataFrame:
857
+ """
858
+ Perform distribution encoding for the specified categorical columns.
859
+
860
+ Parameters
861
+ ----------
862
+ data : pd.DataFrame
863
+ The data containing the categorical columns to encode.
864
+
865
+ Returns
866
+ -------
867
+ pd.DataFrame
868
+ DataFrame with distribution encoded columns.
869
+ """
870
+ for column in self.encode_list:
871
+ counts = data[column].value_counts(normalize=True)
872
+ data[column] = data[column].map(counts)
873
+ return data
874
+
875
+ def _encoding_categorical_woe(self, data: pd.DataFrame) -> pd.DataFrame:
876
+ """
877
+ Perform Weight of Evidence (WOE) encoding for the specified categorical columns.
878
+
879
+ Parameters
880
+ ----------
881
+ data : pd.DataFrame
882
+ The data containing the categorical columns to encode.
883
+
884
+ Returns
885
+ -------
886
+ pd.DataFrame
887
+ DataFrame with WOE encoded columns.
888
+ """
889
+ for column in self.encode_list:
890
+ target = data[self.target_column]
891
+ df = pd.DataFrame({column: data[column], "target": target})
892
+
893
+ total_good = df["target"].sum()
894
+ total_bad = df["target"].count() - total_good
895
+
896
+ grouped = df.groupby(column, observed=False)["target"].agg(["sum", "count"])
897
+ grouped["good"] = grouped["sum"]
898
+ grouped["bad"] = grouped["count"] - grouped["sum"]
899
+
900
+ epsilon = 1e-10
901
+ grouped["good_pct"] = (grouped["good"] + epsilon) / (total_good + epsilon)
902
+ grouped["bad_pct"] = (grouped["bad"] + epsilon) / (total_bad + epsilon)
903
+
904
+ grouped["woe"] = np.log(grouped["good_pct"] / grouped["bad_pct"])
905
+
906
+ woe_dict = grouped["woe"].to_dict()
907
+
908
+ data[column] = data[column].astype(str).map(woe_dict).fillna(0)
909
+ return data
910
+
911
+ def _encoding_categorical_catboost(
912
+ self, data: pd.DataFrame, sigma_value: float = 0.05
913
+ ) -> pd.DataFrame:
914
+ """
915
+ Perform CatBoost encoding for the specified categorical columns.
916
+
917
+ Parameters
918
+ ----------
919
+ data : pd.DataFrame
920
+ The DataFrame containing the data to encode.
921
+ sigma_value : float, optional
922
+ The regularization parameter for the encoding, by default 0.05.
923
+
924
+ Returns
925
+ -------
926
+ pd.DataFrame
927
+ DataFrame with CatBoost encoded columns.
928
+ """
929
+ encoder = CatBoostEncoder(cols=self.encode_list, sigma=sigma_value)
930
+ data[self.encode_list] = encoder.fit_transform(
931
+ data[self.encode_list], data[self.target_column]
932
+ )
933
+ return data
934
+
935
+ def encoding_categorical(
936
+ self,
937
+ method: Literal["one_hot", "distribution", "woe", "catboost"] = "one_hot",
938
+ ) -> pd.DataFrame:
939
+ """
940
+ Perform encoding for the specified categorical columns using the chosen method.
941
+
942
+ The columns to be encoded are specified during the initialization of the FeatureEngineering class.
943
+
944
+ Parameters
945
+ ----------
946
+ method : {'one_hot', 'distribution', 'woe', 'catboost'}, optional
947
+ The encoding method to use, by default 'one_hot'.
948
+ """
949
+ if self.encode_list is None:
950
+ print("No columns specified for encoding.")
951
+
952
+ missing_columns = [
953
+ col for col in self.encode_list if col not in self.data.columns
954
+ ]
955
+ if missing_columns:
956
+ raise ValueError(
957
+ f"The following columns are not in the DataFrame: {missing_columns}"
958
+ )
959
+
960
+ data = self.data.copy()
961
+
962
+ if method == "one_hot":
963
+ data = self._encoding_categorical_one_hot(data)
964
+ print(f"One-hot encoding performed on columns: {self.encode_list}")
965
+ # Get the names of the new encoded columns
966
+ new_columns = [
967
+ col for col in data.columns if col.startswith(tuple(self.encode_list))
968
+ ]
969
+ print("\nHead of encoded columns:")
970
+ print(data[new_columns].head())
971
+ elif method == "distribution":
972
+ data = self._encoding_categorical_distribution(data)
973
+ print(f"Distribution encoding performed on columns: {self.encode_list}")
974
+ print("\nHead of encoded columns:")
975
+ print(data[self.encode_list].head())
976
+ elif method == "woe":
977
+ data = self._encoding_categorical_woe(data)
978
+ print(f"WOE encoding performed on columns: {self.encode_list}")
979
+ print("\nHead of encoded columns:")
980
+ print(data[self.encode_list].head())
981
+ elif method == "catboost":
982
+ data = self._encoding_categorical_catboost(data)
983
+ print(f"CatBoost encoding performed on columns: {self.encode_list}")
984
+ print("\nHead of encoded columns:")
985
+ print(data[self.encode_list].head())
986
+ else:
987
+ raise ValueError(
988
+ f"Encoding method '{method}' is not supported. Choose from 'one_hot', 'distribution', 'woe', or 'catboost'."
989
+ )
990
+
991
+ self.data = data
992
+ self.data.info()
993
+ self._log_transformation("encoding_categorical", {"method": method})
994
+ return self.data
995
+
996
+ ####### FEATURE ENGINEERING: OPTIONAL FUNCTIONS #######
997
+ def search_pca_components(self):
998
+ """
999
+ Search for the optimal number of principal components to retain.
1000
+ """
1001
+ numerical_data = self.data[self.numerical_columns]
1002
+
1003
+ pca = PCA()
1004
+ pca.fit(numerical_data)
1005
+
1006
+ explained_variance = pca.explained_variance_ratio_
1007
+ cumulative_variance = explained_variance.cumsum()
1008
+
1009
+ plt.figure(figsize=(10, 6))
1010
+ plt.plot(
1011
+ range(1, len(explained_variance) + 1),
1012
+ explained_variance,
1013
+ marker="o",
1014
+ label="Explained Variance",
1015
+ )
1016
+ plt.plot(
1017
+ range(1, len(cumulative_variance) + 1),
1018
+ cumulative_variance,
1019
+ marker="o",
1020
+ label="Cumulative Variance",
1021
+ )
1022
+ plt.title("Explained Variance by Principal Components")
1023
+ plt.xlabel("Number of Principal Components")
1024
+ plt.ylabel("Variance Explained")
1025
+ plt.legend()
1026
+ plt.grid(True)
1027
+ plt.show()
1028
+
1029
+ for i, (ev, cv) in enumerate(zip(explained_variance, cumulative_variance), 1):
1030
+ print(
1031
+ f"Principal Component {i}: Explained Variance = {ev:.4f}, Cumulative Variance = {cv:.4f}"
1032
+ )
1033
+
1034
+ print(
1035
+ "Use the cumulative variance to decide the number of components to retain and then call implement_pca with the desired number of components."
1036
+ )
1037
+
1038
+ def implement_pca(self, n_components: int):
1039
+ """
1040
+ Implement PCA with the specified number of components.
1041
+
1042
+ Parameters
1043
+ ----------
1044
+ n_components : int
1045
+ The number of principal components to retain.
1046
+ """
1047
+ numerical_data = self.data[self.numerical_columns]
1048
+
1049
+ pca = PCA(n_components=n_components)
1050
+ principal_components = pca.fit_transform(numerical_data)
1051
+
1052
+ pca_columns = [f"PC{i+1}" for i in range(n_components)]
1053
+ pca_df = pd.DataFrame(
1054
+ principal_components, columns=pca_columns, index=self.data.index
1055
+ )
1056
+
1057
+ self.data.drop(columns=self.numerical_columns, inplace=True)
1058
+ self.data[pca_columns] = pca_df
1059
+
1060
+ print(
1061
+ f"PCA has been applied, reducing the data to {n_components} principal components."
1062
+ )
1063
+
1064
+ def search_svd_components(self, max_components: int):
1065
+ """
1066
+ Search for the optimal number of singular value components to retain.
1067
+
1068
+ Parameters
1069
+ ----------
1070
+ max_components : int
1071
+ The maximum number of components to evaluate.
1072
+ """
1073
+ numerical_data = self.data[self.numerical_columns]
1074
+
1075
+ if max_components <= 0:
1076
+ max_components = min(numerical_data.shape[1], numerical_data.shape[0])
1077
+
1078
+ explained_variances = []
1079
+ cumulative_variances = []
1080
+
1081
+ for n in range(1, max_components + 1):
1082
+ svd = TruncatedSVD(n_components=n)
1083
+ svd.fit(numerical_data)
1084
+ explained_variance = svd.explained_variance_ratio_
1085
+ cumulative_variance = explained_variance.sum()
1086
+ explained_variances.append(explained_variance[-1])
1087
+ cumulative_variances.append(cumulative_variance)
1088
+ print(
1089
+ f"Number of components: {n}, Explained Variance: {explained_variance[-1]:.4f}, Cumulative Variance: {cumulative_variance:.4f}"
1090
+ )
1091
+
1092
+ plt.figure(figsize=(10, 6))
1093
+ plt.plot(
1094
+ range(1, max_components + 1),
1095
+ explained_variances,
1096
+ marker="o",
1097
+ label="Explained Variance",
1098
+ )
1099
+ plt.plot(
1100
+ range(1, max_components + 1),
1101
+ cumulative_variances,
1102
+ marker="o",
1103
+ label="Cumulative Variance",
1104
+ )
1105
+ plt.title("Explained Variance by SVD Components")
1106
+ plt.xlabel("Number of SVD Components")
1107
+ plt.ylabel("Variance Explained")
1108
+ plt.legend()
1109
+ plt.grid(True)
1110
+ plt.show()
1111
+
1112
+ print(
1113
+ "Use the explained and cumulative variance to decide the number of components to retain, then call implement_svd with the desired number of components."
1114
+ )
1115
+
1116
+ def implement_svd(self, n_components: int):
1117
+ """
1118
+ Implement Singular Value Decomposition (SVD) with the specified number of components.
1119
+
1120
+ Parameters
1121
+ ----------
1122
+ n_components : int
1123
+ The number of singular value components to retain.
1124
+ """
1125
+ numerical_data = self.data[self.numerical_columns]
1126
+
1127
+ svd = TruncatedSVD(n_components=n_components)
1128
+ svd_components = svd.fit_transform(numerical_data)
1129
+
1130
+ svd_columns = [f"SVD{i+1}" for i in range(n_components)]
1131
+ svd_df = pd.DataFrame(
1132
+ svd_components, columns=svd_columns, index=self.data.index
1133
+ )
1134
+
1135
+ self.data.drop(columns=self.numerical_columns, inplace=True)
1136
+ self.data[svd_columns] = svd_df
1137
+
1138
+ print(
1139
+ f"SVD has been applied, reducing the data to {n_components} singular value components."
1140
+ )
1141
+
1142
+ def evaluate_lda_components(self):
1143
+ """
1144
+ Evaluate the performance of Linear Discriminant Analysis (LDA) with different numbers of components.
1145
+ """
1146
+ numerical_data = self.data[self.numerical_columns]
1147
+ target_data = self.data[self.target_column]
1148
+
1149
+ num_classes = len(target_data.unique())
1150
+ num_features = numerical_data.shape[1]
1151
+ max_components = min(num_classes - 1, num_features)
1152
+
1153
+ scores = []
1154
+ for n in range(1, max_components + 1):
1155
+ lda = LDA(n_components=n)
1156
+ score = cross_val_score(lda, numerical_data, target_data, cv=5).mean()
1157
+ scores.append(score)
1158
+ print(f"Number of components: {n}, Cross-validation score: {score:.4f}")
1159
+
1160
+ plt.figure(figsize=(10, 6))
1161
+ plt.plot(range(1, max_components + 1), scores, marker="o")
1162
+ plt.title("LDA Components vs. Cross-Validation Score")
1163
+ plt.xlabel("Number of LDA Components")
1164
+ plt.ylabel("Cross-Validation Score")
1165
+ plt.grid(True)
1166
+ plt.show()
1167
+
1168
+ print(
1169
+ "Use the plot to decide the optimal number of components and then call implement_lda with the desired number of components."
1170
+ )
1171
+
1172
+ def implement_lda(self, n_components: int):
1173
+ """
1174
+ Implement Linear Discriminant Analysis (LDA) with the specified number of components.
1175
+
1176
+ Parameters
1177
+ ----------
1178
+ n_components : int
1179
+ The number of LDA components to retain.
1180
+ """
1181
+ numerical_data = self.data[self.numerical_columns]
1182
+ target_data = self.data[self.target_column]
1183
+
1184
+ lda = LDA(n_components=n_components)
1185
+ lda_components = lda.fit_transform(numerical_data, target_data)
1186
+
1187
+ lda_columns = [f"LD{i+1}" for i in range(n_components)]
1188
+ lda_df = pd.DataFrame(
1189
+ lda_components, columns=lda_columns, index=self.data.index
1190
+ )
1191
+
1192
+ self.data.drop(columns=self.numerical_columns, inplace=True)
1193
+ self.data[lda_columns] = lda_df
1194
+
1195
+ print(
1196
+ f"LDA has been applied, reducing the data to {n_components} linear discriminants."
1197
+ )
1198
+
1199
+ def apply_winsorization(
1200
+ self, selected_columns: List[str], percentile: float = 0.05
1201
+ ) -> pd.DataFrame:
1202
+ """
1203
+ Apply winsorization to numerical columns to limit extreme values.
1204
+
1205
+ Parameters
1206
+ ----------
1207
+ selected_columns : List[str]
1208
+ List of columns to apply winsorization to.
1209
+ percentile : float, optional
1210
+ The percentage of data to be considered as extreme values on each side, by default 0.05.
1211
+
1212
+ Returns
1213
+ -------
1214
+ pd.DataFrame
1215
+ DataFrame with winsorized columns.
1216
+ """
1217
+ if len(selected_columns) == 0:
1218
+ selected_columns = self.numerical_columns
1219
+
1220
+ for column in selected_columns:
1221
+ if column == self.target_column:
1222
+ print(f"{column} is the target column. Skipping winsorization.")
1223
+ continue
1224
+
1225
+ lower_bound = np.percentile(self.data[column], percentile * 100)
1226
+ upper_bound = np.percentile(self.data[column], (1 - percentile) * 100)
1227
+ self.data[column] = np.clip(self.data[column], lower_bound, upper_bound)
1228
+
1229
+ print(f"\n{' WINSORIZATION RESULTS '.center(100, '=')}")
1230
+ print(
1231
+ f"Winsorization applied with {percentile * 100:.1f}% capping on each side."
1232
+ )
1233
+ print("=" * 100)
1234
+
1235
+ # Prepare the summary statistics
1236
+ summary = self.data[selected_columns].describe()
1237
+
1238
+ # Determine the width for each column based on the longest column name
1239
+ col_width = max(max(len(col) for col in selected_columns), 15) + 2
1240
+
1241
+ # Print column names
1242
+ print(
1243
+ f"{'Statistic':<15}"
1244
+ + "".join(f"{col:^{col_width}}" for col in selected_columns)
1245
+ )
1246
+ print("-" * (15 + col_width * len(selected_columns)))
1247
+
1248
+ # Print the summary statistics
1249
+ for stat in summary.index:
1250
+ print(
1251
+ f"{stat:<15}"
1252
+ + "".join(
1253
+ f"{summary.loc[stat, col]:^{col_width}.6f}"
1254
+ for col in selected_columns
1255
+ )
1256
+ )
1257
+
1258
+ print("=" * (15 + col_width * len(selected_columns)))
1259
+
1260
+ self._log_transformation(
1261
+ "winsorization",
1262
+ {"selected_columns": selected_columns, "percentile": percentile},
1263
+ )
1264
+ return self.data
1265
+
1266
+ percentile_capping = apply_winsorization
1267
+
1268
+
1269
+ '''
1270
+ def apply_winsorization(
1271
+ self, selected_columns: List[str], percentile: float = 0.05
1272
+ ) -> pd.DataFrame:
1273
+ """
1274
+ Apply winsorization to numerical columns to limit extreme values.
1275
+
1276
+ Parameters
1277
+ ----------
1278
+ selected_columns : List[str]
1279
+ List of columns to apply winsorization to.
1280
+ percentile : float, optional
1281
+ The percentage of data to be considered as extreme values on each side, by default 0.05.
1282
+
1283
+ Returns
1284
+ -------
1285
+ pd.DataFrame
1286
+ DataFrame with winsorized columns.
1287
+ """
1288
+ if len(selected_columns) == 0:
1289
+ selected_columns = self.numerical_columns
1290
+
1291
+ for column in selected_columns:
1292
+ if column == self.target_column:
1293
+ print(f"{column} is the target column. Skipping winsorization.")
1294
+ continue
1295
+
1296
+ lower_bound = np.percentile(self.data[column], percentile * 100)
1297
+ upper_bound = np.percentile(self.data[column], (1 - percentile) * 100)
1298
+ self.data[column] = np.clip(self.data[column], lower_bound, upper_bound)
1299
+
1300
+ print(f"\nWinsorization applied with {percentile * 100}% capping on each side.")
1301
+ print(self.data[selected_columns].describe())
1302
+
1303
+ self._log_transformation(
1304
+ "winsorization",
1305
+ {"selected_columns": selected_columns, "percentile": percentile},
1306
+ )
1307
+ return self.data
1308
+
1309
+ percentile_capping = apply_winsorization
1310
+ '''