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.
- classifier_toolkit/eda/__init__.py +19 -0
- classifier_toolkit/eda/bivariate_analysis.py +413 -0
- classifier_toolkit/eda/eda_toolkit.py +549 -0
- classifier_toolkit/eda/feature_engineering.py +1310 -0
- classifier_toolkit/eda/first_glance.py +253 -0
- classifier_toolkit/eda/univariate_analysis.py +778 -0
- classifier_toolkit/eda/visualizations.py +1248 -0
- classifier_toolkit/eda/warnings/__init__.py +4 -0
- classifier_toolkit/eda/warnings/automated_warnings.py +20 -0
- classifier_toolkit/eda/warnings/default_warnings.py +286 -0
- classifier_toolkit/feature_selection/__init__.py +33 -0
- classifier_toolkit/feature_selection/base.py +182 -0
- classifier_toolkit/feature_selection/embedded_methods/__init__.py +7 -0
- classifier_toolkit/feature_selection/embedded_methods/elastic_net.py +178 -0
- classifier_toolkit/feature_selection/meta_selector.py +329 -0
- classifier_toolkit/feature_selection/utils/__init__.py +17 -0
- classifier_toolkit/feature_selection/utils/plottings.py +65 -0
- classifier_toolkit/feature_selection/utils/scoring.py +86 -0
- classifier_toolkit/feature_selection/wrapper_methods/__init__.py +11 -0
- classifier_toolkit/feature_selection/wrapper_methods/rfe.py +345 -0
- classifier_toolkit/feature_selection/wrapper_methods/sequential_selection.py +566 -0
- classifier_toolkit/model_fitting/__init__.py +0 -0
- classifier_toolkit/model_fitting/model_search.py +3 -0
- classifier_toolkit-0.1.0.dist-info/METADATA +99 -0
- classifier_toolkit-0.1.0.dist-info/RECORD +26 -0
- classifier_toolkit-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
from typing import Dict, List, Literal, Optional, Set, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from lightgbm import LGBMClassifier
|
|
6
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
7
|
+
from sklearn.linear_model import LogisticRegression
|
|
8
|
+
from sklearn.metrics import get_scorer
|
|
9
|
+
from sklearn.model_selection import cross_val_score, train_test_split
|
|
10
|
+
from xgboost import XGBClassifier
|
|
11
|
+
|
|
12
|
+
from classifier_toolkit.feature_selection.base import (
|
|
13
|
+
BaseFeatureSelector,
|
|
14
|
+
FeatureSelectionError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SequentialSelector(BaseFeatureSelector):
|
|
19
|
+
"""
|
|
20
|
+
Sequential feature selector using various estimators.
|
|
21
|
+
|
|
22
|
+
This class implements forward, backward, and bidirectional feature selection
|
|
23
|
+
using different estimators.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
estimator_params : Optional[Dict[str, float]]
|
|
28
|
+
Parameters for the estimator.
|
|
29
|
+
estimator_name : {'random_forest', 'lightgbm', 'xgboost', 'logistic_regression'}, optional
|
|
30
|
+
The name of the estimator to use, by default 'random_forest'.
|
|
31
|
+
n_features_to_select : int, optional
|
|
32
|
+
The number of features to select, by default 1.
|
|
33
|
+
scoring : {'accuracy', 'f1', 'precision', 'recall', 'roc_auc'}, optional
|
|
34
|
+
The scoring metric to use, by default 'roc_auc'.
|
|
35
|
+
cv : int, optional
|
|
36
|
+
The number of cross-validation folds, by default 5.
|
|
37
|
+
method : {'forward', 'backward', 'bidirectional'}, optional
|
|
38
|
+
The selection method to use, by default 'forward'.
|
|
39
|
+
verbose : int, optional
|
|
40
|
+
Verbosity level, by default 0.
|
|
41
|
+
tolerance : float, optional
|
|
42
|
+
Tolerance for early stopping, by default 1e-4.
|
|
43
|
+
n_jobs : int, optional
|
|
44
|
+
The number of jobs to run in parallel, by default -1.
|
|
45
|
+
early_stopping_rounds : int, optional
|
|
46
|
+
The number of rounds for early stopping, by default 5.
|
|
47
|
+
|
|
48
|
+
Attributes
|
|
49
|
+
----------
|
|
50
|
+
estimator_params : Dict[str, float]
|
|
51
|
+
Parameters for the estimator.
|
|
52
|
+
method : str
|
|
53
|
+
The selection method used.
|
|
54
|
+
feature_scores_ : Dict[str, float]
|
|
55
|
+
Scores for each feature.
|
|
56
|
+
tolerance : float
|
|
57
|
+
Tolerance for early stopping.
|
|
58
|
+
n_jobs : int
|
|
59
|
+
The number of jobs to run in parallel.
|
|
60
|
+
early_stopping_rounds : int
|
|
61
|
+
The number of rounds for early stopping.
|
|
62
|
+
estimator_name : str
|
|
63
|
+
The name of the estimator used.
|
|
64
|
+
estimator : object
|
|
65
|
+
The estimator object.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
estimator_params: Optional[Dict[str, Union[int, float, str]]],
|
|
71
|
+
estimator_name: Literal[
|
|
72
|
+
"random_forest", "lightgbm", "xgboost", "logistic_regression"
|
|
73
|
+
] = "random_forest",
|
|
74
|
+
n_features_to_select: int = 1,
|
|
75
|
+
scoring: Literal[
|
|
76
|
+
"accuracy", "f1", "precision", "recall", "roc_auc"
|
|
77
|
+
] = "roc_auc",
|
|
78
|
+
cv: int = 5,
|
|
79
|
+
method: Literal["forward", "backward", "bidirectional"] = "forward",
|
|
80
|
+
verbose: int = 0,
|
|
81
|
+
tolerance: float = 1e-4,
|
|
82
|
+
n_jobs: int = -1,
|
|
83
|
+
early_stopping_rounds: int = 5,
|
|
84
|
+
) -> None:
|
|
85
|
+
super().__init__(
|
|
86
|
+
estimator=None,
|
|
87
|
+
n_features_to_select=n_features_to_select,
|
|
88
|
+
scoring=scoring,
|
|
89
|
+
cv=cv,
|
|
90
|
+
verbose=verbose,
|
|
91
|
+
)
|
|
92
|
+
self.estimator_params = estimator_params or {}
|
|
93
|
+
self.method = method
|
|
94
|
+
self.feature_scores_: Dict[str, float] = {}
|
|
95
|
+
self.tolerance = tolerance
|
|
96
|
+
self.n_jobs = n_jobs
|
|
97
|
+
self.early_stopping_rounds = early_stopping_rounds
|
|
98
|
+
self.estimator_name = estimator_name
|
|
99
|
+
self.estimator = self.initialize_estimator() # type: ignore
|
|
100
|
+
|
|
101
|
+
# Handle the scoring parameter
|
|
102
|
+
self.scorer = get_scorer(scoring)
|
|
103
|
+
|
|
104
|
+
def initialize_estimator(self):
|
|
105
|
+
"""
|
|
106
|
+
Initialize the estimator based on the provided name and parameters.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
object
|
|
111
|
+
The initialized estimator.
|
|
112
|
+
|
|
113
|
+
Raises
|
|
114
|
+
------
|
|
115
|
+
ValueError
|
|
116
|
+
If the estimator name is not recognized.
|
|
117
|
+
"""
|
|
118
|
+
if self.estimator_name == "random_forest":
|
|
119
|
+
return RandomForestClassifier(**self.estimator_params)
|
|
120
|
+
elif self.estimator_name == "lightgbm":
|
|
121
|
+
return LGBMClassifier(**self.estimator_params)
|
|
122
|
+
elif self.estimator_name == "xgboost":
|
|
123
|
+
return XGBClassifier(**self.estimator_params)
|
|
124
|
+
elif self.estimator_name == "logistic_regression":
|
|
125
|
+
return LogisticRegression(**self.estimator_params)
|
|
126
|
+
else:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
"Estimator must be 'random_forest', 'lightgbm', 'xgboost', or 'logistic_regression'"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def fit(self, X: pd.DataFrame, y: pd.Series) -> "SequentialSelector":
|
|
132
|
+
"""
|
|
133
|
+
Fit the SequentialSelector to the data.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
X : pd.DataFrame
|
|
138
|
+
The input features.
|
|
139
|
+
y : pd.Series
|
|
140
|
+
The target variable.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
SequentialSelector
|
|
145
|
+
The fitted SequentialSelector instance.
|
|
146
|
+
|
|
147
|
+
Raises
|
|
148
|
+
------
|
|
149
|
+
ValueError
|
|
150
|
+
If the selection method is not recognized.
|
|
151
|
+
"""
|
|
152
|
+
self.feature_names_ = X.columns.tolist()
|
|
153
|
+
|
|
154
|
+
if self.method == "forward":
|
|
155
|
+
self._forward_selection(X, y)
|
|
156
|
+
elif self.method == "backward":
|
|
157
|
+
self._backward_selection(X, y)
|
|
158
|
+
elif self.method == "bidirectional":
|
|
159
|
+
self._bidirectional_selection(X, y)
|
|
160
|
+
else:
|
|
161
|
+
raise ValueError("Method must be 'forward', 'backward', or 'bidirectional'")
|
|
162
|
+
|
|
163
|
+
self.feature_importances_ = self.get_feature_importances()
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
def _get_score(self, X: pd.DataFrame, y: pd.Series, features: List[int]) -> float:
|
|
167
|
+
"""
|
|
168
|
+
Calculate the cross-validation score for a subset of features.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
X : pd.DataFrame
|
|
173
|
+
The input features.
|
|
174
|
+
y : pd.Series
|
|
175
|
+
The target variable.
|
|
176
|
+
features : List[int]
|
|
177
|
+
The indices of features to use.
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
float
|
|
182
|
+
The mean cross-validation score.
|
|
183
|
+
|
|
184
|
+
Raises
|
|
185
|
+
------
|
|
186
|
+
ValueError
|
|
187
|
+
If the estimator is not set.
|
|
188
|
+
"""
|
|
189
|
+
X_subset = X.iloc[:, features]
|
|
190
|
+
|
|
191
|
+
if self.estimator is not None:
|
|
192
|
+
scores = cross_val_score(
|
|
193
|
+
self.estimator, X_subset, y, scoring=self.scorer, cv=self.cv, n_jobs=-1
|
|
194
|
+
)
|
|
195
|
+
return float(np.mean(scores))
|
|
196
|
+
else:
|
|
197
|
+
raise ValueError("Estimator is not set.")
|
|
198
|
+
|
|
199
|
+
def _get_score_fast(self, X_train, X_val, y_train, y_val, features: List[int]):
|
|
200
|
+
"""
|
|
201
|
+
Calculate the score for a subset of features using a fast method.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
X_train : pd.DataFrame
|
|
206
|
+
The training input features.
|
|
207
|
+
X_val : pd.DataFrame
|
|
208
|
+
The validation input features.
|
|
209
|
+
y_train : pd.Series
|
|
210
|
+
The training target variable.
|
|
211
|
+
y_val : pd.Series
|
|
212
|
+
The validation target variable.
|
|
213
|
+
features : List[int]
|
|
214
|
+
The indices of features to use.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
float
|
|
219
|
+
The score for the given features.
|
|
220
|
+
|
|
221
|
+
Raises
|
|
222
|
+
------
|
|
223
|
+
ValueError
|
|
224
|
+
If the estimator is not set.
|
|
225
|
+
"""
|
|
226
|
+
X_train_subset = X_train.iloc[:, features]
|
|
227
|
+
X_val_subset = X_val.iloc[:, features]
|
|
228
|
+
|
|
229
|
+
if self.estimator is not None:
|
|
230
|
+
self.estimator.fit(X_train_subset, y_train) # type: ignore
|
|
231
|
+
return self.scorer(self.estimator, X_val_subset, y_val)
|
|
232
|
+
else:
|
|
233
|
+
raise ValueError("Estimator is not set.")
|
|
234
|
+
|
|
235
|
+
def _forward_selection(self, X: pd.DataFrame, y: pd.Series) -> None:
|
|
236
|
+
"""
|
|
237
|
+
Perform forward feature selection.
|
|
238
|
+
|
|
239
|
+
Parameters
|
|
240
|
+
----------
|
|
241
|
+
X : pd.DataFrame
|
|
242
|
+
The input features.
|
|
243
|
+
y : pd.Series
|
|
244
|
+
The target variable.
|
|
245
|
+
"""
|
|
246
|
+
selected: List[int] = []
|
|
247
|
+
remaining = list(range(X.shape[1]))
|
|
248
|
+
feature_scores: Dict[int, float] = {}
|
|
249
|
+
|
|
250
|
+
X_train, X_val, y_train, y_val = train_test_split(
|
|
251
|
+
X, y, test_size=0.2, random_state=42
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
max_features = (
|
|
255
|
+
self.n_features_to_select
|
|
256
|
+
if self.n_features_to_select is not None
|
|
257
|
+
else X.shape[1]
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
while len(selected) < max_features:
|
|
261
|
+
scores = []
|
|
262
|
+
for feature in remaining:
|
|
263
|
+
temp_selected = [*selected, feature]
|
|
264
|
+
score = self._get_score_fast(
|
|
265
|
+
X_train, X_val, y_train, y_val, temp_selected
|
|
266
|
+
)
|
|
267
|
+
scores.append((score, feature))
|
|
268
|
+
|
|
269
|
+
best_score, best_feature = max(scores)
|
|
270
|
+
selected.append(best_feature)
|
|
271
|
+
remaining.remove(best_feature)
|
|
272
|
+
feature_scores[best_feature] = best_score
|
|
273
|
+
|
|
274
|
+
print(
|
|
275
|
+
f"Selected feature: {X.columns[best_feature]}, Score: {best_score:.6f}"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if self.n_features_to_select is None and len(selected) == X.shape[1]:
|
|
279
|
+
break
|
|
280
|
+
|
|
281
|
+
self.selected_features_ = [X.columns[i] for i in selected]
|
|
282
|
+
self.feature_scores_ = {X.columns[k]: v for k, v in feature_scores.items()}
|
|
283
|
+
|
|
284
|
+
def _backward_selection(self, X: pd.DataFrame, y: pd.Series) -> None:
|
|
285
|
+
"""
|
|
286
|
+
Perform backward feature selection.
|
|
287
|
+
|
|
288
|
+
Parameters
|
|
289
|
+
----------
|
|
290
|
+
X : pd.DataFrame
|
|
291
|
+
The input features.
|
|
292
|
+
y : pd.Series
|
|
293
|
+
The target variable.
|
|
294
|
+
"""
|
|
295
|
+
selected = list(range(X.shape[1]))
|
|
296
|
+
feature_scores: Dict[int, float] = {
|
|
297
|
+
feature: 0.0 for feature in selected
|
|
298
|
+
} # Initialize with floats
|
|
299
|
+
|
|
300
|
+
X_train, X_val, y_train, y_val = train_test_split(
|
|
301
|
+
X, y, test_size=0.2, random_state=42
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
min_features = (
|
|
305
|
+
self.n_features_to_select if self.n_features_to_select is not None else 1
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
while len(selected) > min_features:
|
|
309
|
+
scores = []
|
|
310
|
+
for feature in selected:
|
|
311
|
+
temp_selected = [f for f in selected if f != feature]
|
|
312
|
+
score = self._get_score_fast(
|
|
313
|
+
X_train, X_val, y_train, y_val, temp_selected
|
|
314
|
+
)
|
|
315
|
+
scores.append((score, feature))
|
|
316
|
+
|
|
317
|
+
best_score, worst_feature = max(scores)
|
|
318
|
+
selected.remove(worst_feature)
|
|
319
|
+
feature_scores[worst_feature] = best_score
|
|
320
|
+
|
|
321
|
+
print(
|
|
322
|
+
f"Removed feature: {X.columns[worst_feature]}, Score: {best_score:.6f}"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
if self.n_features_to_select is None and len(selected) == 1:
|
|
326
|
+
break
|
|
327
|
+
|
|
328
|
+
self.selected_features_ = [X.columns[i] for i in selected]
|
|
329
|
+
self.feature_scores_ = {X.columns[k]: v for k, v in feature_scores.items()}
|
|
330
|
+
|
|
331
|
+
def _bidirectional_selection(self, X: pd.DataFrame, y: pd.Series) -> None:
|
|
332
|
+
"""
|
|
333
|
+
Perform bidirectional feature selection.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
X : pd.DataFrame
|
|
338
|
+
The input features.
|
|
339
|
+
y : pd.Series
|
|
340
|
+
The target variable.
|
|
341
|
+
"""
|
|
342
|
+
selected: List[int] = []
|
|
343
|
+
remaining: List[int] = list(range(X.shape[1]))
|
|
344
|
+
feature_scores: Dict[int, List[float]] = {
|
|
345
|
+
feature: [] for feature in range(X.shape[1])
|
|
346
|
+
}
|
|
347
|
+
recently_added: Set[int] = set()
|
|
348
|
+
recently_removed: Set[int] = set()
|
|
349
|
+
|
|
350
|
+
X_train, X_val, y_train, y_val = train_test_split(
|
|
351
|
+
X, y, test_size=0.2, random_state=42
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Start with the best single feature
|
|
355
|
+
initial_scores = [
|
|
356
|
+
(self._get_score_fast(X_train, X_val, y_train, y_val, [f]), f)
|
|
357
|
+
for f in remaining
|
|
358
|
+
]
|
|
359
|
+
best_score, best_feature = max(initial_scores)
|
|
360
|
+
selected.append(best_feature)
|
|
361
|
+
remaining.remove(best_feature)
|
|
362
|
+
feature_scores[best_feature].append(best_score)
|
|
363
|
+
recently_added.add(best_feature)
|
|
364
|
+
|
|
365
|
+
print(f"Initial feature: {X.columns[best_feature]}, Score: {best_score:.6f}")
|
|
366
|
+
|
|
367
|
+
max_iterations = (
|
|
368
|
+
X.shape[1] * 2
|
|
369
|
+
if self.n_features_to_select is None
|
|
370
|
+
else self.n_features_to_select * 2
|
|
371
|
+
)
|
|
372
|
+
iteration = 0
|
|
373
|
+
|
|
374
|
+
while iteration < max_iterations:
|
|
375
|
+
iteration += 1
|
|
376
|
+
# Forward step
|
|
377
|
+
forward_scores = [
|
|
378
|
+
(
|
|
379
|
+
self._get_score_fast(
|
|
380
|
+
X_train,
|
|
381
|
+
X_val,
|
|
382
|
+
y_train,
|
|
383
|
+
y_val,
|
|
384
|
+
[*selected, f],
|
|
385
|
+
),
|
|
386
|
+
f,
|
|
387
|
+
)
|
|
388
|
+
for f in remaining
|
|
389
|
+
if f not in recently_removed
|
|
390
|
+
]
|
|
391
|
+
best_forward_score, best_forward_feature = (
|
|
392
|
+
max(forward_scores) if forward_scores else (-np.inf, None)
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Backward step (if there's more than one feature)
|
|
396
|
+
backward_scores = []
|
|
397
|
+
if len(selected) > 1:
|
|
398
|
+
backward_scores = [
|
|
399
|
+
(
|
|
400
|
+
self._get_score_fast(
|
|
401
|
+
X_train,
|
|
402
|
+
X_val,
|
|
403
|
+
y_train,
|
|
404
|
+
y_val,
|
|
405
|
+
[f for f in selected if f != feat],
|
|
406
|
+
),
|
|
407
|
+
feat,
|
|
408
|
+
)
|
|
409
|
+
for feat in selected
|
|
410
|
+
if feat not in recently_added
|
|
411
|
+
]
|
|
412
|
+
best_backward_score, best_backward_feature = (
|
|
413
|
+
max(backward_scores) if backward_scores else (-np.inf, None)
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Update feature scores
|
|
417
|
+
for score, feature in forward_scores:
|
|
418
|
+
feature_scores[feature].append(score)
|
|
419
|
+
for score, feature in backward_scores:
|
|
420
|
+
feature_scores[feature].append(score)
|
|
421
|
+
|
|
422
|
+
# Decide whether to add or remove a feature
|
|
423
|
+
if (
|
|
424
|
+
best_forward_score > best_backward_score
|
|
425
|
+
and best_forward_feature is not None
|
|
426
|
+
):
|
|
427
|
+
selected.append(best_forward_feature)
|
|
428
|
+
remaining.remove(best_forward_feature)
|
|
429
|
+
recently_added.add(best_forward_feature)
|
|
430
|
+
recently_removed.clear()
|
|
431
|
+
print(
|
|
432
|
+
f"Added feature: {X.columns[best_forward_feature]}, Score: {best_forward_score:.6f}"
|
|
433
|
+
)
|
|
434
|
+
elif best_backward_feature is not None:
|
|
435
|
+
selected.remove(best_backward_feature)
|
|
436
|
+
remaining.append(best_backward_feature)
|
|
437
|
+
recently_removed.add(best_backward_feature)
|
|
438
|
+
recently_added.clear()
|
|
439
|
+
print(
|
|
440
|
+
f"Removed feature: {X.columns[best_backward_feature]}, Score: {best_backward_score:.6f}"
|
|
441
|
+
)
|
|
442
|
+
else:
|
|
443
|
+
print("No improvement possible. Stopping.")
|
|
444
|
+
break
|
|
445
|
+
|
|
446
|
+
# Clear recent sets after a few iterations to allow reconsideration
|
|
447
|
+
if len(selected) % 5 == 0:
|
|
448
|
+
recently_added.clear()
|
|
449
|
+
recently_removed.clear()
|
|
450
|
+
|
|
451
|
+
print(f"Iteration {iteration}, Selected features: {len(selected)}")
|
|
452
|
+
|
|
453
|
+
# Check if we've reached the desired number of features
|
|
454
|
+
if len(selected) == self.n_features_to_select:
|
|
455
|
+
print(
|
|
456
|
+
f"Reached desired number of features ({self.n_features_to_select}). Stopping."
|
|
457
|
+
)
|
|
458
|
+
break
|
|
459
|
+
|
|
460
|
+
# Check if we've reached the maximum number of iterations
|
|
461
|
+
if iteration >= max_iterations:
|
|
462
|
+
print(
|
|
463
|
+
f"Reached maximum number of iterations ({max_iterations}). Stopping."
|
|
464
|
+
)
|
|
465
|
+
break
|
|
466
|
+
|
|
467
|
+
self.selected_features_ = [
|
|
468
|
+
X.columns[i] for i in selected[: self.n_features_to_select]
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
# Calculate average scores for all features
|
|
472
|
+
if self.feature_scores_ is None:
|
|
473
|
+
self.feature_scores_ = {}
|
|
474
|
+
|
|
475
|
+
for feature_idx, scores in feature_scores.items():
|
|
476
|
+
feature_name = X.columns[feature_idx]
|
|
477
|
+
self.feature_scores_[feature_name] = (
|
|
478
|
+
float(np.mean(scores)) if scores else 0.0
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
print("\nFeature selection completed.")
|
|
482
|
+
print(f"Total iterations: {iteration}")
|
|
483
|
+
print(f"Selected features: {len(self.selected_features_)}")
|
|
484
|
+
print("Feature importances (average scores):")
|
|
485
|
+
for feature, score in sorted( # type: ignore
|
|
486
|
+
self.feature_scores_.items(), key=lambda x: x[1], reverse=True
|
|
487
|
+
):
|
|
488
|
+
print(f"{feature}: {score:.6f}")
|
|
489
|
+
|
|
490
|
+
def get_feature_importances(self) -> pd.Series:
|
|
491
|
+
"""
|
|
492
|
+
Get the feature importances (scores) from the fitted model.
|
|
493
|
+
|
|
494
|
+
Returns
|
|
495
|
+
-------
|
|
496
|
+
pd.Series
|
|
497
|
+
The feature importances.
|
|
498
|
+
|
|
499
|
+
Raises
|
|
500
|
+
------
|
|
501
|
+
FeatureSelectionError
|
|
502
|
+
If the selector has not been fitted yet.
|
|
503
|
+
"""
|
|
504
|
+
if self.feature_scores_ is None:
|
|
505
|
+
raise FeatureSelectionError(
|
|
506
|
+
"Selector has not been fitted yet. Call 'fit' first."
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
importances = pd.Series(self.feature_scores_)
|
|
510
|
+
|
|
511
|
+
# Normalize importances
|
|
512
|
+
if importances.sum() > 0:
|
|
513
|
+
importances = importances / importances.sum()
|
|
514
|
+
else:
|
|
515
|
+
print("Warning: Sum of importances is zero. Skipping normalization.")
|
|
516
|
+
|
|
517
|
+
return importances
|
|
518
|
+
|
|
519
|
+
def transform(self, X: pd.DataFrame) -> pd.DataFrame:
|
|
520
|
+
"""
|
|
521
|
+
Transform the input data by selecting the chosen features.
|
|
522
|
+
|
|
523
|
+
Parameters
|
|
524
|
+
----------
|
|
525
|
+
X : pd.DataFrame
|
|
526
|
+
The input features.
|
|
527
|
+
|
|
528
|
+
Returns
|
|
529
|
+
-------
|
|
530
|
+
pd.DataFrame
|
|
531
|
+
The transformed data containing only the selected features.
|
|
532
|
+
|
|
533
|
+
Raises
|
|
534
|
+
------
|
|
535
|
+
FeatureSelectionError
|
|
536
|
+
If the selector has not been fitted yet.
|
|
537
|
+
"""
|
|
538
|
+
if self.selected_features_ is None:
|
|
539
|
+
raise FeatureSelectionError(
|
|
540
|
+
"Selector has not been fitted yet. Call 'fit' first."
|
|
541
|
+
)
|
|
542
|
+
return X[self.selected_features_]
|
|
543
|
+
|
|
544
|
+
def print_feature_importances(self):
|
|
545
|
+
"""
|
|
546
|
+
Print the feature importances (scores) in descending order.
|
|
547
|
+
|
|
548
|
+
Raises
|
|
549
|
+
------
|
|
550
|
+
FeatureSelectionError
|
|
551
|
+
If feature importances are not available (i.e., fit hasn't been called).
|
|
552
|
+
"""
|
|
553
|
+
if self.feature_importances_ is None:
|
|
554
|
+
raise FeatureSelectionError(
|
|
555
|
+
"Feature importances are not available. Call 'fit' first."
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
sorted_importances = sorted(
|
|
559
|
+
zip(self.feature_importances_.index, self.feature_importances_),
|
|
560
|
+
key=lambda x: x[1],
|
|
561
|
+
reverse=True,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
print("Feature importances (in descending order):")
|
|
565
|
+
for feature, importance in sorted_importances:
|
|
566
|
+
print(f"('{feature}', {importance:.6f})")
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: classifier-toolkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
Author: gauthier.marquand
|
|
6
|
+
Author-email: gauthier.marquand@qonto.com
|
|
7
|
+
Requires-Python: >=3.9,<3.11
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Requires-Dist: catboost (>=1.2.2,<2.0.0)
|
|
12
|
+
Requires-Dist: category-encoders (>=2.6.3,<3.0.0)
|
|
13
|
+
Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
|
14
|
+
Requires-Dist: graphviz (>=0.20.3,<0.21.0)
|
|
15
|
+
Requires-Dist: ipywidgets (>=8.1.5,<9.0.0)
|
|
16
|
+
Requires-Dist: lightgbm (>=4.5.0,<5.0.0)
|
|
17
|
+
Requires-Dist: matplotlib (>=3.9.2,<4.0.0)
|
|
18
|
+
Requires-Dist: nbformat (>=5.10.4,<6.0.0)
|
|
19
|
+
Requires-Dist: numpy (<1.26)
|
|
20
|
+
Requires-Dist: optuna (>=3.5.0,<4.0.0)
|
|
21
|
+
Requires-Dist: pandas (>=2.2.0,<3.0.0)
|
|
22
|
+
Requires-Dist: polars (>=1.2.1,<2.0.0)
|
|
23
|
+
Requires-Dist: pyarrow (>=17.0.0,<18.0.0)
|
|
24
|
+
Requires-Dist: pydantic-settings (>=2.3.4,<3.0.0)
|
|
25
|
+
Requires-Dist: pydot (>=3.0.1,<4.0.0)
|
|
26
|
+
Requires-Dist: pylint (>=3.2.6,<4.0.0)
|
|
27
|
+
Requires-Dist: scikit-learn (>=1.4.0,<2.0.0)
|
|
28
|
+
Requires-Dist: scipy (<1.12)
|
|
29
|
+
Requires-Dist: seaborn (>=0.10.1)
|
|
30
|
+
Requires-Dist: shap (>=0.44.1,<0.45.0)
|
|
31
|
+
Requires-Dist: snowflake (>=0.10.0,<0.11.0)
|
|
32
|
+
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
33
|
+
Requires-Dist: xgboost (>=2.1.1,<3.0.0)
|
|
34
|
+
Requires-Dist: ydata-profiling (>=4.6.4,<5.0.0)
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# Classifier Toolkit
|
|
38
|
+
|
|
39
|
+
[](https://python-poetry.org/)
|
|
40
|
+
[](https://github.com/astral-sh/ruff)
|
|
41
|
+
[](https://github.com/psf/black)
|
|
42
|
+
|
|
43
|
+
This is a new project.
|
|
44
|
+
|
|
45
|
+
-----
|
|
46
|
+
|
|
47
|
+
## Table of Content
|
|
48
|
+
|
|
49
|
+
<!-- [[_TOC_]] -->
|
|
50
|
+
|
|
51
|
+
1. [Installation](#installation)
|
|
52
|
+
2. [Usage](#usage)
|
|
53
|
+
3. [Modules Overview](#modules-overview)
|
|
54
|
+
4. [Future Work](#future-work)
|
|
55
|
+
|
|
56
|
+
### Installation
|
|
57
|
+
|
|
58
|
+
This library is published in the PyPI directory. To install, users can run pip install 'classifier_toolkit' command.
|
|
59
|
+
|
|
60
|
+
### Usage
|
|
61
|
+
|
|
62
|
+
This library automates binary classification tasks in the finance domain, specifically for default and fraud labeling. It includes several packages designed to address the main steps in any machine learning/data science task:
|
|
63
|
+
|
|
64
|
+
1. EDA: which is accessible by EDA_Toolkit. This package provides the EDA and feature engineering functionality alongside with all the necessary visualizations.
|
|
65
|
+
2. Feature Selection: To be implemented.
|
|
66
|
+
3. Model fitting and hyperparameter tuning: To be implemented.
|
|
67
|
+
4. Evaluation and reporting: To be implemented.
|
|
68
|
+
|
|
69
|
+
In the future, the package architectures will be included here. However, for now please consult the docstrings in the specific methods in the relevant modules.
|
|
70
|
+
|
|
71
|
+
**Note**: that this library does not contain data wrangling steps (although it contains feature engineering), it's an intermediate step between EDA and feature engineering where users should fix any data quality related issues. Therefore, conducting the EDA is crucial to mitigate any issues before moving onto the feature engineering and the subsequent steps.
|
|
72
|
+
|
|
73
|
+
### Modules Overview
|
|
74
|
+
|
|
75
|
+
- **EDA Toolkit**: This module includes classes and methods for performing comprehensive exploratory data analysis. It provides automated warnings for data quality issues, univariate and bivariate analysis, and various data visualizations to help understand the dataset.
|
|
76
|
+
|
|
77
|
+
- **Univariate Analysis**: This class focuses on the analysis of individual variables. It includes methods for calculating statistical measures, visualizing distributions, and assessing relationships between variables and a target through techniques like Cramer's V and Information Value. This helps in understanding the significance and distribution of each feature independently.
|
|
78
|
+
|
|
79
|
+
- **Bivariate Analysis**: This class deals with the analysis of two variables to understand their relationship. It includes functionalities for generating correlation heatmaps, performing ANOVA tests between numerical and categorical variables, and computing pairwise Cramer's V for categorical features. This aids in identifying patterns and correlations between pairs of variables, which is crucial for feature selection and engineering.
|
|
80
|
+
|
|
81
|
+
- **Feature Engineering**: This module assists in transforming features, handling missing values, encoding categorical variables, and more. It aims to enhance the dataset's quality for better model performance.
|
|
82
|
+
|
|
83
|
+
- **Visualizations**: This module offers a wide range of plotting capabilities to visually analyze data distributions, relationships, and other crucial aspects of the dataset.
|
|
84
|
+
|
|
85
|
+
- **Automated Warnings**: A utility to automatically check the dataset for common issues such as missing or duplicate values, outliers, and more, providing warnings to guide data cleaning efforts.
|
|
86
|
+
|
|
87
|
+
- **Feature Selection**: This module provides various feature selection techniques:
|
|
88
|
+
- **Embedded Methods**: Includes ElasticNet for regularization-based feature selection.
|
|
89
|
+
- **Wrapper Methods**:
|
|
90
|
+
- Recursive Feature Elimination (RFE) with support for various ensemble methods (Random Forest, XGBoost, LightGBM, CatBoost).
|
|
91
|
+
- Sequential Feature Selection (forward, backward, floating, and bidirectional).
|
|
92
|
+
- **Meta Selector**: Combines multiple feature selection methods to provide a robust selection.
|
|
93
|
+
- **Utility Functions**: Includes scoring functions and plotting utilities for feature importance visualization.
|
|
94
|
+
|
|
95
|
+
### Future Work
|
|
96
|
+
The next planned improvements and additions to the library include:
|
|
97
|
+
* Adding model fitting and hyperparameter tuning functionalities.
|
|
98
|
+
* Developing comprehensive evaluation and reporting tools to assist with model assessment.
|
|
99
|
+
* Expanding documentation to include architecture diagrams and detailed usage examples.
|