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,178 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from sklearn.base import BaseEstimator
|
|
6
|
+
from sklearn.linear_model import ElasticNetCV, LogisticRegression
|
|
7
|
+
from sklearn.model_selection import cross_val_score
|
|
8
|
+
|
|
9
|
+
from classifier_toolkit.feature_selection.base import (
|
|
10
|
+
BaseFeatureSelector,
|
|
11
|
+
FeatureSelectionError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ElasticNetSelector(BaseFeatureSelector):
|
|
16
|
+
"""
|
|
17
|
+
Feature selector based on Elastic Net regularization.
|
|
18
|
+
|
|
19
|
+
This class implements feature selection using Elastic Net, which combines
|
|
20
|
+
L1 and L2 regularization.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
estimator : Optional[BaseEstimator], optional
|
|
25
|
+
The estimator to use, by default None.
|
|
26
|
+
l1_ratio : float, optional
|
|
27
|
+
The ElasticNet mixing parameter, by default 0.5.
|
|
28
|
+
max_iter : int, optional
|
|
29
|
+
Maximum number of iterations, by default 10000.
|
|
30
|
+
**kwargs
|
|
31
|
+
Additional keyword arguments to be passed to the BaseFeatureSelector.
|
|
32
|
+
|
|
33
|
+
Attributes
|
|
34
|
+
----------
|
|
35
|
+
l1_ratio : float
|
|
36
|
+
The ElasticNet mixing parameter.
|
|
37
|
+
max_iter : int
|
|
38
|
+
Maximum number of iterations.
|
|
39
|
+
model : Optional[ElasticNetCV]
|
|
40
|
+
The fitted ElasticNetCV model.
|
|
41
|
+
feature_importances_ : pd.Series
|
|
42
|
+
The feature importances (coefficients) from the fitted model.
|
|
43
|
+
selected_features_ : List[str]
|
|
44
|
+
The list of selected features.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
estimator: Optional[BaseEstimator] = None,
|
|
50
|
+
l1_ratio: float = 0.5,
|
|
51
|
+
max_iter: int = 10000,
|
|
52
|
+
**kwargs,
|
|
53
|
+
) -> None:
|
|
54
|
+
# Use LogisticRegression as the default estimator if none is provided
|
|
55
|
+
if estimator is None:
|
|
56
|
+
estimator = LogisticRegression(random_state=42)
|
|
57
|
+
super().__init__(estimator=estimator, **kwargs)
|
|
58
|
+
self.l1_ratio = l1_ratio
|
|
59
|
+
self.max_iter = max_iter
|
|
60
|
+
self.model: Optional[ElasticNetCV] = None
|
|
61
|
+
|
|
62
|
+
def fit(self, X: pd.DataFrame, y: pd.Series) -> "ElasticNetSelector":
|
|
63
|
+
"""
|
|
64
|
+
Fit the ElasticNetSelector to the data.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
X : pd.DataFrame
|
|
69
|
+
The input features.
|
|
70
|
+
y : pd.Series
|
|
71
|
+
The target variable.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
ElasticNetSelector
|
|
76
|
+
The fitted ElasticNetSelector instance.
|
|
77
|
+
|
|
78
|
+
Raises
|
|
79
|
+
------
|
|
80
|
+
ValueError
|
|
81
|
+
If model fitting fails or coefficients are not available.
|
|
82
|
+
"""
|
|
83
|
+
self.model = ElasticNetCV(
|
|
84
|
+
l1_ratio=self.l1_ratio, cv=self.cv, max_iter=self.max_iter
|
|
85
|
+
).fit(X, y) # type: ignore
|
|
86
|
+
if self.model is not None and hasattr(self.model, "coef_"):
|
|
87
|
+
self.feature_importances_ = pd.Series(
|
|
88
|
+
abs(self.model.coef_), index=X.columns
|
|
89
|
+
)
|
|
90
|
+
if self.feature_importances_ is not None:
|
|
91
|
+
self.selected_features_ = self.feature_importances_[
|
|
92
|
+
self.feature_importances_ > 0
|
|
93
|
+
].index.tolist()
|
|
94
|
+
else:
|
|
95
|
+
raise ValueError("Feature importances are None after fitting.")
|
|
96
|
+
else:
|
|
97
|
+
raise ValueError("Model fitting failed or coefficients are not available.")
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def get_feature_importances(self) -> pd.Series:
|
|
101
|
+
"""
|
|
102
|
+
Get the feature importances (coefficients) from the fitted model.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
pd.Series
|
|
107
|
+
The feature importances.
|
|
108
|
+
|
|
109
|
+
Raises
|
|
110
|
+
------
|
|
111
|
+
FeatureSelectionError
|
|
112
|
+
If the selector has not been fitted yet.
|
|
113
|
+
"""
|
|
114
|
+
if self.feature_importances_ is None:
|
|
115
|
+
raise FeatureSelectionError(
|
|
116
|
+
"Selector has not been fitted yet. Call 'fit' first."
|
|
117
|
+
)
|
|
118
|
+
return self.feature_importances_
|
|
119
|
+
|
|
120
|
+
def _get_score(self, X: pd.DataFrame, y: pd.Series, features: List[int]) -> float:
|
|
121
|
+
"""
|
|
122
|
+
Calculate the cross-validation score for a subset of features.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
X : pd.DataFrame
|
|
127
|
+
The input features.
|
|
128
|
+
y : pd.Series
|
|
129
|
+
The target variable.
|
|
130
|
+
features : List[int]
|
|
131
|
+
The indices of features to use.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
float
|
|
136
|
+
The mean cross-validation score.
|
|
137
|
+
|
|
138
|
+
Raises
|
|
139
|
+
------
|
|
140
|
+
ValueError
|
|
141
|
+
If the estimator is not set.
|
|
142
|
+
"""
|
|
143
|
+
if self.estimator is None:
|
|
144
|
+
raise ValueError("Estimator is not set. Please provide an estimator.")
|
|
145
|
+
|
|
146
|
+
X_subset = X.iloc[:, features]
|
|
147
|
+
scores = cross_val_score(
|
|
148
|
+
self.estimator,
|
|
149
|
+
X_subset,
|
|
150
|
+
y,
|
|
151
|
+
scoring=self.scorer,
|
|
152
|
+
cv=self.cv,
|
|
153
|
+
n_jobs=-1,
|
|
154
|
+
error_score="raise",
|
|
155
|
+
)
|
|
156
|
+
return float(np.mean(scores))
|
|
157
|
+
|
|
158
|
+
def return_feature_importances(self):
|
|
159
|
+
"""
|
|
160
|
+
Print the feature importances (coefficients) in descending order of absolute value.
|
|
161
|
+
|
|
162
|
+
Raises
|
|
163
|
+
------
|
|
164
|
+
FeatureSelectionError
|
|
165
|
+
If feature importances are not available (i.e., fit hasn't been called).
|
|
166
|
+
"""
|
|
167
|
+
if self.feature_importances_ is None:
|
|
168
|
+
raise FeatureSelectionError(
|
|
169
|
+
"Feature importances are not available. Call 'fit' first."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
sorted_importances = sorted(
|
|
173
|
+
zip(self.feature_importances_.index, self.feature_importances_),
|
|
174
|
+
key=lambda x: abs(x[1]), # Sort by absolute value of coefficients
|
|
175
|
+
reverse=True,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return sorted_importances
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
from typing import List, Literal, Optional, Union
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from tqdm import tqdm
|
|
5
|
+
|
|
6
|
+
from classifier_toolkit.feature_selection.base import (
|
|
7
|
+
BaseFeatureSelector,
|
|
8
|
+
FeatureSelectionError,
|
|
9
|
+
)
|
|
10
|
+
from classifier_toolkit.feature_selection.wrapper_methods.rfe import (
|
|
11
|
+
EnsembleFeatureSelector,
|
|
12
|
+
)
|
|
13
|
+
from classifier_toolkit.feature_selection.wrapper_methods.sequential_selection import (
|
|
14
|
+
SequentialSelector,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MetaSelector(BaseFeatureSelector):
|
|
19
|
+
"""
|
|
20
|
+
A meta-selector that combines multiple feature selection methods.
|
|
21
|
+
|
|
22
|
+
This class allows for the combination of multiple feature selection methods
|
|
23
|
+
using different voting strategies.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
methods : Optional[List[BaseFeatureSelector]], optional
|
|
28
|
+
List of feature selection methods to be combined, by default None.
|
|
29
|
+
voting : str, optional
|
|
30
|
+
The voting strategy to use for combining methods, by default "majority".
|
|
31
|
+
Options are "majority", "union", or "intersection".
|
|
32
|
+
n_features_to_select : Optional[int], optional
|
|
33
|
+
The number of features to select, by default None.
|
|
34
|
+
scoring : str, optional
|
|
35
|
+
The scoring metric to use, by default "accuracy".
|
|
36
|
+
cv : int, optional
|
|
37
|
+
The number of cross-validation folds, by default 5.
|
|
38
|
+
verbose : int, optional
|
|
39
|
+
Verbosity level, by default 0.
|
|
40
|
+
|
|
41
|
+
Attributes
|
|
42
|
+
----------
|
|
43
|
+
methods : List[BaseFeatureSelector]
|
|
44
|
+
The list of feature selection methods.
|
|
45
|
+
voting : str
|
|
46
|
+
The voting strategy used.
|
|
47
|
+
feature_importances_ : pd.Series
|
|
48
|
+
The combined feature importances.
|
|
49
|
+
selected_features_ : List[str]
|
|
50
|
+
The list of selected features.
|
|
51
|
+
|
|
52
|
+
Raises
|
|
53
|
+
------
|
|
54
|
+
ValueError
|
|
55
|
+
If any of the provided methods is not an instance of BaseFeatureSelector.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
methods: Optional[List[BaseFeatureSelector]] = None,
|
|
61
|
+
voting: Literal["majority", "union", "intersection"] = "majority",
|
|
62
|
+
n_features_to_select: int = 1,
|
|
63
|
+
scoring: Literal[
|
|
64
|
+
"accuracy", "f1", "precision", "recall", "roc_auc"
|
|
65
|
+
] = "accuracy",
|
|
66
|
+
cv: int = 5,
|
|
67
|
+
verbose: int = 0,
|
|
68
|
+
):
|
|
69
|
+
super().__init__(
|
|
70
|
+
estimator=None, # MetaSelector doesn't need its own estimator
|
|
71
|
+
n_features_to_select=n_features_to_select,
|
|
72
|
+
scoring=scoring,
|
|
73
|
+
cv=cv,
|
|
74
|
+
verbose=verbose,
|
|
75
|
+
)
|
|
76
|
+
self.methods = methods or [
|
|
77
|
+
EnsembleFeatureSelector(
|
|
78
|
+
estimator="random_forest",
|
|
79
|
+
n_features_to_select=self.n_features_to_select,
|
|
80
|
+
),
|
|
81
|
+
SequentialSelector(
|
|
82
|
+
method="forward",
|
|
83
|
+
estimator_name="lightgbm",
|
|
84
|
+
estimator_params={"n_estimators": 100},
|
|
85
|
+
n_features_to_select=self.n_features_to_select,
|
|
86
|
+
),
|
|
87
|
+
]
|
|
88
|
+
if not all(isinstance(method, BaseFeatureSelector) for method in self.methods):
|
|
89
|
+
raise ValueError("All methods must be instances of BaseFeatureSelector")
|
|
90
|
+
self.voting = voting
|
|
91
|
+
|
|
92
|
+
def fit(self, X: pd.DataFrame, y: pd.Series) -> "MetaSelector":
|
|
93
|
+
"""
|
|
94
|
+
Fit the MetaSelector to the data.
|
|
95
|
+
|
|
96
|
+
This method fits all the individual feature selection methods and then
|
|
97
|
+
combines their results based on the specified voting strategy.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
X : pd.DataFrame
|
|
102
|
+
The input features.
|
|
103
|
+
y : pd.Series
|
|
104
|
+
The target variable.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
MetaSelector
|
|
109
|
+
The fitted MetaSelector instance.
|
|
110
|
+
|
|
111
|
+
Raises
|
|
112
|
+
------
|
|
113
|
+
ValueError
|
|
114
|
+
If an invalid voting strategy is specified.
|
|
115
|
+
"""
|
|
116
|
+
# Create a progress bar
|
|
117
|
+
pbar = tqdm(total=len(self.methods), desc="Fitting methods")
|
|
118
|
+
|
|
119
|
+
for i, method in enumerate(self.methods):
|
|
120
|
+
print(
|
|
121
|
+
f"\nFitting method {i+1}/{len(self.methods)}: {type(method).__name__}"
|
|
122
|
+
)
|
|
123
|
+
method.fit(X, y)
|
|
124
|
+
# Update the progress bar
|
|
125
|
+
pbar.update(1)
|
|
126
|
+
pbar.set_description(f"Fitted {type(method).__name__}")
|
|
127
|
+
|
|
128
|
+
# Close the progress bar
|
|
129
|
+
pbar.close()
|
|
130
|
+
|
|
131
|
+
self._combine_feature_importances()
|
|
132
|
+
|
|
133
|
+
if self.voting == "majority":
|
|
134
|
+
self._majority_voting()
|
|
135
|
+
elif self.voting == "union":
|
|
136
|
+
self._union_voting()
|
|
137
|
+
elif self.voting == "intersection":
|
|
138
|
+
self._intersection_voting()
|
|
139
|
+
else:
|
|
140
|
+
raise ValueError("Voting must be 'majority', 'union', or 'intersection'")
|
|
141
|
+
|
|
142
|
+
return self
|
|
143
|
+
|
|
144
|
+
def _combine_feature_importances(self):
|
|
145
|
+
all_importances = [method.get_feature_importances() for method in self.methods]
|
|
146
|
+
combined_importances = pd.concat(all_importances, axis=1)
|
|
147
|
+
|
|
148
|
+
if self.voting == "majority":
|
|
149
|
+
self.feature_importances_ = combined_importances.mean(axis=1)
|
|
150
|
+
elif self.voting == "union" or self.voting == "intersection":
|
|
151
|
+
self.feature_importances_ = combined_importances.max(axis=1)
|
|
152
|
+
|
|
153
|
+
self.feature_importances_ = self.feature_importances_.sort_values(
|
|
154
|
+
ascending=False
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def print_feature_importances(self):
|
|
158
|
+
if self.feature_importances_ is None:
|
|
159
|
+
raise FeatureSelectionError(
|
|
160
|
+
"Feature importances are not available. Call 'fit' first."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
print("Combined Feature Importances (in descending order):")
|
|
164
|
+
for feature, importance in self.feature_importances_.items():
|
|
165
|
+
print(f"('{feature}', {importance:.6f})")
|
|
166
|
+
|
|
167
|
+
def get_feature_importances(self) -> pd.Series:
|
|
168
|
+
"""
|
|
169
|
+
Get the combined feature importances.
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
pd.Series
|
|
174
|
+
The combined feature importances.
|
|
175
|
+
|
|
176
|
+
Raises
|
|
177
|
+
------
|
|
178
|
+
FeatureSelectionError
|
|
179
|
+
If the selector has not been fitted yet.
|
|
180
|
+
"""
|
|
181
|
+
if self.feature_importances_ is None:
|
|
182
|
+
raise FeatureSelectionError(
|
|
183
|
+
"Selector has not been fitted yet. Call 'fit' first."
|
|
184
|
+
)
|
|
185
|
+
return self.feature_importances_
|
|
186
|
+
|
|
187
|
+
def _majority_voting(self):
|
|
188
|
+
all_importances = [method.get_feature_importances() for method in self.methods]
|
|
189
|
+
combined_importances = pd.concat(all_importances, axis=1)
|
|
190
|
+
threshold = len(self.methods) / 2
|
|
191
|
+
feature_counts = (combined_importances > 0).sum(axis=1)
|
|
192
|
+
self.selected_features_ = list(feature_counts[feature_counts > threshold].index)
|
|
193
|
+
self.feature_importances_ = combined_importances.loc[
|
|
194
|
+
self.selected_features_
|
|
195
|
+
].mean(axis=1)
|
|
196
|
+
|
|
197
|
+
def _union_voting(self):
|
|
198
|
+
all_selected = [set(method.selected_features_) for method in self.methods]
|
|
199
|
+
union_features = set.union(*all_selected)
|
|
200
|
+
self.selected_features_ = list(union_features)
|
|
201
|
+
all_importances = [method.get_feature_importances() for method in self.methods]
|
|
202
|
+
combined_importances = pd.concat(all_importances, axis=1)
|
|
203
|
+
self.feature_importances_ = combined_importances.loc[
|
|
204
|
+
self.selected_features_
|
|
205
|
+
].mean(axis=1)
|
|
206
|
+
|
|
207
|
+
def _intersection_voting(self):
|
|
208
|
+
all_selected = [set(method.selected_features_) for method in self.methods]
|
|
209
|
+
intersection_features = set.intersection(*all_selected)
|
|
210
|
+
self.selected_features_ = list(intersection_features)
|
|
211
|
+
all_importances = [method.get_feature_importances() for method in self.methods]
|
|
212
|
+
combined_importances = pd.concat(all_importances, axis=1)
|
|
213
|
+
self.feature_importances_ = combined_importances.loc[
|
|
214
|
+
self.selected_features_
|
|
215
|
+
].mean(axis=1)
|
|
216
|
+
|
|
217
|
+
def print_results(self):
|
|
218
|
+
"""
|
|
219
|
+
Print the results of the feature selection process.
|
|
220
|
+
|
|
221
|
+
This method prints the results of individual methods, the combined results
|
|
222
|
+
based on voting, and the final feature importances.
|
|
223
|
+
"""
|
|
224
|
+
print("Individual method results:")
|
|
225
|
+
for method in self.methods:
|
|
226
|
+
print(f"{type(method).__name__}: {method.selected_features_}")
|
|
227
|
+
|
|
228
|
+
print("\nCombined results based on voting:")
|
|
229
|
+
print(f"Voting method: {self.voting}")
|
|
230
|
+
print(f"Selected features: {self.selected_features_}")
|
|
231
|
+
|
|
232
|
+
print("\nFinal feature importances:")
|
|
233
|
+
for feature, importance in self.feature_importances_.sort_values(
|
|
234
|
+
ascending=False
|
|
235
|
+
).items():
|
|
236
|
+
print(f"('{feature}', {importance:.6f})")
|
|
237
|
+
|
|
238
|
+
def transform(self, X: pd.DataFrame) -> pd.DataFrame:
|
|
239
|
+
"""
|
|
240
|
+
Transform the input data by selecting the chosen features.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
X : pd.DataFrame
|
|
245
|
+
The input features.
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
pd.DataFrame
|
|
250
|
+
The transformed data containing only the selected features.
|
|
251
|
+
|
|
252
|
+
Raises
|
|
253
|
+
------
|
|
254
|
+
FeatureSelectionError
|
|
255
|
+
If the selector has not been fitted yet.
|
|
256
|
+
"""
|
|
257
|
+
if self.selected_features_ is None:
|
|
258
|
+
raise FeatureSelectionError(
|
|
259
|
+
"Selector has not been fitted yet. Call 'fit' first."
|
|
260
|
+
)
|
|
261
|
+
return X[self.selected_features_]
|
|
262
|
+
|
|
263
|
+
def fit_transform(self, X: pd.DataFrame, y: pd.Series) -> pd.DataFrame:
|
|
264
|
+
"""
|
|
265
|
+
Fit the MetaSelector to the data and then transform it.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
X : pd.DataFrame
|
|
270
|
+
The input features.
|
|
271
|
+
y : pd.Series
|
|
272
|
+
The target variable.
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
pd.DataFrame
|
|
277
|
+
The transformed data containing only the selected features.
|
|
278
|
+
"""
|
|
279
|
+
return self.fit(X, y).transform(X)
|
|
280
|
+
|
|
281
|
+
def get_support(self, indices: bool = False) -> Union[List[bool], List[int]]:
|
|
282
|
+
"""
|
|
283
|
+
Get a boolean mask or integer index array indicating the selected features.
|
|
284
|
+
|
|
285
|
+
Parameters
|
|
286
|
+
----------
|
|
287
|
+
indices : bool, optional
|
|
288
|
+
If True, returns an integer index array, otherwise returns a boolean mask.
|
|
289
|
+
|
|
290
|
+
Returns
|
|
291
|
+
-------
|
|
292
|
+
Union[List[bool], List[int]]
|
|
293
|
+
Boolean mask or integer index array indicating the selected features.
|
|
294
|
+
|
|
295
|
+
Raises
|
|
296
|
+
------
|
|
297
|
+
FeatureSelectionError
|
|
298
|
+
If the selector has not been fitted yet.
|
|
299
|
+
"""
|
|
300
|
+
if self.selected_features_ is None:
|
|
301
|
+
raise FeatureSelectionError(
|
|
302
|
+
"Selector has not been fitted yet. Call 'fit' first."
|
|
303
|
+
)
|
|
304
|
+
assert self.feature_importances_ is not None, ValueError(
|
|
305
|
+
'Feature importances are not available. Call "fit" first.'
|
|
306
|
+
)
|
|
307
|
+
mask = [
|
|
308
|
+
feature in self.selected_features_
|
|
309
|
+
for feature in self.feature_importances_.index
|
|
310
|
+
]
|
|
311
|
+
if indices:
|
|
312
|
+
return [i for i, m in enumerate(mask) if m]
|
|
313
|
+
return mask
|
|
314
|
+
|
|
315
|
+
def get_individual_results(self):
|
|
316
|
+
"""
|
|
317
|
+
Get the results of individual feature selection methods.
|
|
318
|
+
|
|
319
|
+
Returns
|
|
320
|
+
-------
|
|
321
|
+
dict
|
|
322
|
+
A dictionary containing the selected features for each method.
|
|
323
|
+
"""
|
|
324
|
+
return {
|
|
325
|
+
type(method).__name__: method.selected_features_ for method in self.methods
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
def _get_score(self, X: pd.DataFrame, y: pd.Series, features: List[int]) -> float:
|
|
329
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from classifier_toolkit.feature_selection.utils.plottings import (
|
|
2
|
+
plot_feature_importances,
|
|
3
|
+
plot_rfecv_results,
|
|
4
|
+
)
|
|
5
|
+
from classifier_toolkit.feature_selection.utils.scoring import (
|
|
6
|
+
false_positive_rate,
|
|
7
|
+
get_scorer,
|
|
8
|
+
true_positive_rate,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"plot_feature_importances",
|
|
13
|
+
"plot_rfecv_results",
|
|
14
|
+
"get_scorer",
|
|
15
|
+
"false_positive_rate",
|
|
16
|
+
"true_positive_rate",
|
|
17
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from classifier_toolkit.feature_selection.base import BaseFeatureSelector
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def plot_feature_importances(selector: BaseFeatureSelector) -> None:
|
|
8
|
+
importances = selector.get_feature_importances().sort_values(ascending=True)
|
|
9
|
+
|
|
10
|
+
plt.figure(figsize=(10, max(8, len(importances) * 0.3)))
|
|
11
|
+
y_pos = np.arange(len(importances))
|
|
12
|
+
plt.barh(y_pos, np.array(importances.values))
|
|
13
|
+
plt.yticks(y_pos, importances.index.astype(str).tolist())
|
|
14
|
+
plt.xlabel("Importance")
|
|
15
|
+
plt.title("Feature Importances")
|
|
16
|
+
plt.tight_layout()
|
|
17
|
+
plt.show()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def plot_rfecv_results(selector: BaseFeatureSelector) -> None:
|
|
21
|
+
assert hasattr(
|
|
22
|
+
selector, "rfecv"
|
|
23
|
+
), "This plot is only available for RFECV-based selectors."
|
|
24
|
+
assert (
|
|
25
|
+
selector.rfecv is not None
|
|
26
|
+
), "RFECV results are not available. Make sure to fit the selector first."
|
|
27
|
+
assert hasattr(
|
|
28
|
+
selector.rfecv, "cv_results_"
|
|
29
|
+
), "The selector's RFECV doesn't have cv_results_. Make sure to fit the selector first."
|
|
30
|
+
|
|
31
|
+
n_features_total = len(selector.get_feature_importances())
|
|
32
|
+
|
|
33
|
+
grid_scores = selector.rfecv.cv_results_["mean_test_score"]
|
|
34
|
+
std_errors = selector.rfecv.cv_results_["std_test_score"]
|
|
35
|
+
|
|
36
|
+
n_features = np.arange(1, n_features_total + 1)
|
|
37
|
+
|
|
38
|
+
plt.figure(figsize=(12, 6))
|
|
39
|
+
plt.title("Feature Selection using RFECV")
|
|
40
|
+
plt.xlabel("Number of features selected")
|
|
41
|
+
plt.ylabel("Cross-validation score")
|
|
42
|
+
plt.plot(n_features, grid_scores)
|
|
43
|
+
plt.fill_between(
|
|
44
|
+
n_features, grid_scores - std_errors, grid_scores + std_errors, alpha=0.2
|
|
45
|
+
)
|
|
46
|
+
plt.axvline(
|
|
47
|
+
selector.rfecv.n_features_,
|
|
48
|
+
color="red",
|
|
49
|
+
linestyle="--",
|
|
50
|
+
label=f"Optimal number of features ({selector.rfecv.n_features_})",
|
|
51
|
+
)
|
|
52
|
+
plt.legend()
|
|
53
|
+
plt.tight_layout()
|
|
54
|
+
plt.show()
|
|
55
|
+
|
|
56
|
+
# Plot feature importances
|
|
57
|
+
importances = selector.get_feature_importances().sort_values(ascending=False)
|
|
58
|
+
plt.figure(figsize=(12, 8))
|
|
59
|
+
plt.title("Feature Importances")
|
|
60
|
+
importances.plot(kind="bar")
|
|
61
|
+
plt.xlabel("Features")
|
|
62
|
+
plt.ylabel("Importance")
|
|
63
|
+
plt.xticks(rotation=90)
|
|
64
|
+
plt.tight_layout()
|
|
65
|
+
plt.show()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from typing import Callable, Dict, Union
|
|
2
|
+
|
|
3
|
+
from sklearn.metrics import (
|
|
4
|
+
accuracy_score,
|
|
5
|
+
confusion_matrix,
|
|
6
|
+
f1_score,
|
|
7
|
+
make_scorer,
|
|
8
|
+
precision_score,
|
|
9
|
+
recall_score,
|
|
10
|
+
roc_auc_score,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Define a dictionary of scoring functions
|
|
14
|
+
SCORING_FUNCTIONS: Dict[str, Callable] = {
|
|
15
|
+
"accuracy": accuracy_score,
|
|
16
|
+
"f1": f1_score,
|
|
17
|
+
"precision": precision_score,
|
|
18
|
+
"recall": recall_score,
|
|
19
|
+
"roc_auc": roc_auc_score,
|
|
20
|
+
# add prauc to the scoring functions, and make it default in the Literal call
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_scorer(metric: Union[str, Callable]) -> Callable:
|
|
25
|
+
"""
|
|
26
|
+
Get a scorer function for the given metric.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
metric (str or callable): The metric to use for scoring.
|
|
30
|
+
If string, must be one of the keys in SCORING_FUNCTIONS.
|
|
31
|
+
If callable, should be a custom scoring function.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A scorer function that can be used with scikit-learn's cross-validation and model selection tools.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: If the metric is not recognized.
|
|
38
|
+
"""
|
|
39
|
+
if isinstance(metric, str):
|
|
40
|
+
if metric not in SCORING_FUNCTIONS:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Unrecognized metric: {metric}. Available metrics are: {', '.join(SCORING_FUNCTIONS.keys())}"
|
|
43
|
+
)
|
|
44
|
+
return make_scorer(SCORING_FUNCTIONS[metric])
|
|
45
|
+
elif callable(metric):
|
|
46
|
+
return make_scorer(metric)
|
|
47
|
+
else:
|
|
48
|
+
raise ValueError("metric must be either a string or a callable")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def false_positive_rate(y_true: int, y_pred: int) -> float:
|
|
52
|
+
"""
|
|
53
|
+
Calculate the false positive rate.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
y_true: True labels
|
|
57
|
+
y_pred: Predicted labels
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
False positive rate
|
|
61
|
+
"""
|
|
62
|
+
tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
|
|
63
|
+
return fp / (fp + tn)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def true_positive_rate(y_true: int, y_pred: int):
|
|
67
|
+
"""
|
|
68
|
+
Calculate the true positive rate (also known as recall or sensitivity).
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
y_true: True labels
|
|
72
|
+
y_pred: Predicted labels
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True positive rate
|
|
76
|
+
"""
|
|
77
|
+
return recall_score(y_true, y_pred)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Add FPR and TPR to the scoring functions
|
|
81
|
+
SCORING_FUNCTIONS["fpr"] = false_positive_rate
|
|
82
|
+
SCORING_FUNCTIONS["tpr"] = true_positive_rate
|
|
83
|
+
|
|
84
|
+
# Create scorers for FPR and TPR
|
|
85
|
+
fpr_scorer = make_scorer(false_positive_rate, greater_is_better=False)
|
|
86
|
+
tpr_scorer = make_scorer(true_positive_rate, greater_is_better=True)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from classifier_toolkit.feature_selection.wrapper_methods.rfe import (
|
|
2
|
+
EnsembleFeatureSelector,
|
|
3
|
+
)
|
|
4
|
+
from classifier_toolkit.feature_selection.wrapper_methods.sequential_selection import (
|
|
5
|
+
SequentialSelector,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"SequentialSelector",
|
|
10
|
+
"EnsembleFeatureSelector",
|
|
11
|
+
]
|