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,20 @@
|
|
|
1
|
+
from typing import Dict, List
|
|
2
|
+
|
|
3
|
+
from .default_warnings import EDAWarning
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WarningSystem:
|
|
7
|
+
def __init__(self, warnings: Dict[str, EDAWarning]):
|
|
8
|
+
self.warnings = warnings
|
|
9
|
+
|
|
10
|
+
def register_warning(self, name: str, warning_obj: EDAWarning):
|
|
11
|
+
"""Register a new warning object."""
|
|
12
|
+
self.warnings[name] = warning_obj
|
|
13
|
+
print(f"Registered warning '{name}'")
|
|
14
|
+
|
|
15
|
+
def run_warnings(self) -> Dict[str, List[Dict[str, str]]]:
|
|
16
|
+
"""Run all registered warning objects and return the results."""
|
|
17
|
+
results = {}
|
|
18
|
+
for name, warning_obj in self.warnings.items():
|
|
19
|
+
results[name] = warning_obj.run()
|
|
20
|
+
return results
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
from typing import Dict, List, Optional, Protocol
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from scipy import stats
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EDAWarning(Protocol):
|
|
9
|
+
df: pd.DataFrame
|
|
10
|
+
|
|
11
|
+
def run(self) -> List[Dict[str, str]]: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NegativeValuesWarning:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
df: pd.DataFrame,
|
|
18
|
+
numerical_columns: Optional[List[str]] = None,
|
|
19
|
+
can_be_negative: Optional[List[str]] = None,
|
|
20
|
+
):
|
|
21
|
+
self.df = df
|
|
22
|
+
self.numerical_columns = numerical_columns
|
|
23
|
+
self.can_be_negative = can_be_negative
|
|
24
|
+
|
|
25
|
+
def run(self) -> List[Dict[str, str]]:
|
|
26
|
+
warnings = []
|
|
27
|
+
if self.numerical_columns:
|
|
28
|
+
for col in self.numerical_columns:
|
|
29
|
+
if (
|
|
30
|
+
self.can_be_negative is None or col not in self.can_be_negative
|
|
31
|
+
) and (self.df[col] < 0).any():
|
|
32
|
+
neg_count = (self.df[col] < 0).sum()
|
|
33
|
+
warnings.append(
|
|
34
|
+
{
|
|
35
|
+
"warning_type": "Negative",
|
|
36
|
+
"message": f"Negative values found in column '{col}' ({neg_count} negative values)",
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
return warnings
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OutliersWarning:
|
|
43
|
+
def __init__(
|
|
44
|
+
self, df: pd.DataFrame, numerical_columns: List[str], distance: float = 1.5
|
|
45
|
+
):
|
|
46
|
+
self.df = df
|
|
47
|
+
self.numerical_columns = numerical_columns
|
|
48
|
+
self.distance = distance
|
|
49
|
+
|
|
50
|
+
def run(self) -> List[Dict[str, str]]:
|
|
51
|
+
warnings = []
|
|
52
|
+
for col in self.numerical_columns:
|
|
53
|
+
Q1 = self.df[col].quantile(0.25)
|
|
54
|
+
Q3 = self.df[col].quantile(0.75)
|
|
55
|
+
IQR = Q3 - Q1
|
|
56
|
+
lower_bound = Q1 - self.distance * IQR
|
|
57
|
+
upper_bound = Q3 + self.distance * IQR
|
|
58
|
+
outliers = self.df[
|
|
59
|
+
(self.df[col] < lower_bound) | (self.df[col] > upper_bound)
|
|
60
|
+
]
|
|
61
|
+
if not outliers.empty:
|
|
62
|
+
warnings.append(
|
|
63
|
+
{
|
|
64
|
+
"warning_type": "Outlier",
|
|
65
|
+
"message": f"Outliers detected in column '{col}' ({len(outliers)} outliers)",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
return warnings
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MissingValuesWarning:
|
|
72
|
+
def __init__(self, df: pd.DataFrame):
|
|
73
|
+
self.df = df
|
|
74
|
+
|
|
75
|
+
def run(self) -> List[Dict[str, str]]:
|
|
76
|
+
missing = self.df.isnull().sum()
|
|
77
|
+
if missing.sum() > 0:
|
|
78
|
+
return [
|
|
79
|
+
{
|
|
80
|
+
"warning_type": "data_quality",
|
|
81
|
+
"message": f"Missing values detected in columns: {missing[missing > 0].index.tolist()}",
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CardinalityWarning:
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
df: pd.DataFrame,
|
|
91
|
+
numerical_columns: List[str],
|
|
92
|
+
exclusion: List[str],
|
|
93
|
+
low_threshold: int = 10,
|
|
94
|
+
):
|
|
95
|
+
self.df = df
|
|
96
|
+
self.numerical_columns = numerical_columns
|
|
97
|
+
self.exclusion = exclusion
|
|
98
|
+
self.low_threshold = low_threshold
|
|
99
|
+
|
|
100
|
+
def run(self) -> List[Dict[str, str]]:
|
|
101
|
+
warnings = []
|
|
102
|
+
for col in self.df.columns:
|
|
103
|
+
if col not in self.numerical_columns and col not in self.exclusion:
|
|
104
|
+
value_counts = len(self.df[col].value_counts())
|
|
105
|
+
if value_counts == 1:
|
|
106
|
+
warnings.append(
|
|
107
|
+
{
|
|
108
|
+
"warning_type": "Cardinality",
|
|
109
|
+
"message": f"Column '{col}' has only one unique value",
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
elif value_counts <= self.low_threshold:
|
|
113
|
+
warnings.append(
|
|
114
|
+
{
|
|
115
|
+
"warning_type": "Cardinality",
|
|
116
|
+
"message": f"Low cardinality in column '{col}' ({value_counts} unique values)",
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
warnings.append(
|
|
121
|
+
{
|
|
122
|
+
"warning_type": "Cardinality",
|
|
123
|
+
"message": f"High cardinality in column '{col}' ({value_counts} unique values)",
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
return warnings
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class DuplicatesWarning:
|
|
130
|
+
def __init__(self, df: pd.DataFrame, id_column: Optional[str] = None):
|
|
131
|
+
self.df = df
|
|
132
|
+
self.id_column = id_column
|
|
133
|
+
|
|
134
|
+
def run(self) -> List[Dict[str, str]]:
|
|
135
|
+
warnings = []
|
|
136
|
+
if self.id_column:
|
|
137
|
+
duplicates = self.df[
|
|
138
|
+
self.df.duplicated(subset=[self.id_column], keep=False)
|
|
139
|
+
]
|
|
140
|
+
if not duplicates.empty:
|
|
141
|
+
warnings.append(
|
|
142
|
+
{
|
|
143
|
+
"warning_type": "Duplicate",
|
|
144
|
+
"message": f"{len(duplicates)} duplicate entries found based on '{self.id_column}'",
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
duplicates = self.df[self.df.duplicated(keep=False)]
|
|
149
|
+
if not duplicates.empty:
|
|
150
|
+
warnings.append(
|
|
151
|
+
{
|
|
152
|
+
"warning_type": "Duplicate",
|
|
153
|
+
"message": f"{len(duplicates)} duplicate rows found",
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
return warnings
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class NormalityWarning:
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
df: pd.DataFrame,
|
|
163
|
+
numerical_columns: List[str],
|
|
164
|
+
significance_level: float = 0.05,
|
|
165
|
+
):
|
|
166
|
+
self.df = df
|
|
167
|
+
self.numerical_columns = numerical_columns
|
|
168
|
+
self.significance_level = significance_level
|
|
169
|
+
|
|
170
|
+
def run(self) -> List[Dict[str, str]]:
|
|
171
|
+
warnings = []
|
|
172
|
+
for col in self.numerical_columns:
|
|
173
|
+
_, p_value = stats.normaltest(self.df[col].dropna())
|
|
174
|
+
if p_value < self.significance_level:
|
|
175
|
+
warnings.append(
|
|
176
|
+
{
|
|
177
|
+
"warning_type": "Normality",
|
|
178
|
+
"message": f"Column '{col}' may not be normally distributed (p-value: {p_value:.4f})",
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
return warnings
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class ImbalanceWarning:
|
|
185
|
+
def __init__(self, df: pd.DataFrame, target_column: str, threshold: float = 0.75):
|
|
186
|
+
self.df = df
|
|
187
|
+
self.target_column = target_column
|
|
188
|
+
self.threshold = threshold
|
|
189
|
+
|
|
190
|
+
def run(self) -> List[Dict[str, str]]:
|
|
191
|
+
warnings = []
|
|
192
|
+
if self.target_column in self.df.columns:
|
|
193
|
+
value_counts = self.df[self.target_column].value_counts(normalize=True)
|
|
194
|
+
if (value_counts > self.threshold).any(): # type: ignore
|
|
195
|
+
majority_class = value_counts.index[0]
|
|
196
|
+
majority_percentage = value_counts.iloc[0] * 100
|
|
197
|
+
warnings.append(
|
|
198
|
+
{
|
|
199
|
+
"warning_type": "Imbalance",
|
|
200
|
+
"message": f"Target variable '{self.target_column}' is imbalanced. "
|
|
201
|
+
f"Majority class '{majority_class}' represents {majority_percentage:.2f}% of the data",
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
return warnings
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class CorrelationsWarning:
|
|
208
|
+
def __init__(
|
|
209
|
+
self, df: pd.DataFrame, numerical_columns: List[str], threshold: float = 0.8
|
|
210
|
+
):
|
|
211
|
+
self.df = df
|
|
212
|
+
self.numerical_columns = numerical_columns
|
|
213
|
+
self.threshold = threshold
|
|
214
|
+
|
|
215
|
+
def run(self) -> List[Dict[str, str]]:
|
|
216
|
+
warnings = []
|
|
217
|
+
if len(self.numerical_columns) > 1:
|
|
218
|
+
corr_matrix = self.df[self.numerical_columns].corr()
|
|
219
|
+
high_corr = np.where(np.abs(corr_matrix) > self.threshold)
|
|
220
|
+
high_corr_pairs = [
|
|
221
|
+
(corr_matrix.index[x], corr_matrix.columns[y])
|
|
222
|
+
for x, y in zip(*high_corr)
|
|
223
|
+
if x != y and x < y
|
|
224
|
+
]
|
|
225
|
+
for pair in high_corr_pairs:
|
|
226
|
+
warnings.append(
|
|
227
|
+
{
|
|
228
|
+
"warning_type": "Correlation",
|
|
229
|
+
"message": f"High correlation ({corr_matrix.loc[pair[0], pair[1]]:.2f}) "
|
|
230
|
+
f"between '{pair[0]}' and '{pair[1]}'",
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
return warnings
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def get_default_warnings(
|
|
237
|
+
df: pd.DataFrame,
|
|
238
|
+
target_column: str,
|
|
239
|
+
can_be_negative: List[str],
|
|
240
|
+
numerical_columns: List[str],
|
|
241
|
+
cardinality_exclusion: List[str],
|
|
242
|
+
correlation_threshold: float = 0.8,
|
|
243
|
+
outlier_distance: float = 1.5,
|
|
244
|
+
imbalance_threshold: float = 0.75,
|
|
245
|
+
normality_significance_level: float = 0.05,
|
|
246
|
+
cardinality_low_threshold: int = 10,
|
|
247
|
+
id_column: Optional[str] = None,
|
|
248
|
+
) -> Dict[str, EDAWarning]:
|
|
249
|
+
return {
|
|
250
|
+
"negative_values": NegativeValuesWarning(
|
|
251
|
+
df,
|
|
252
|
+
numerical_columns=numerical_columns,
|
|
253
|
+
can_be_negative=can_be_negative,
|
|
254
|
+
),
|
|
255
|
+
"outliers": OutliersWarning(
|
|
256
|
+
df,
|
|
257
|
+
numerical_columns=numerical_columns,
|
|
258
|
+
distance=outlier_distance,
|
|
259
|
+
),
|
|
260
|
+
"missing_values": MissingValuesWarning(df),
|
|
261
|
+
"cardinality": CardinalityWarning(
|
|
262
|
+
df,
|
|
263
|
+
numerical_columns=numerical_columns,
|
|
264
|
+
exclusion=cardinality_exclusion,
|
|
265
|
+
low_threshold=cardinality_low_threshold,
|
|
266
|
+
),
|
|
267
|
+
"duplicates": DuplicatesWarning(
|
|
268
|
+
df,
|
|
269
|
+
id_column=id_column,
|
|
270
|
+
),
|
|
271
|
+
"normality": NormalityWarning(
|
|
272
|
+
df,
|
|
273
|
+
numerical_columns=numerical_columns,
|
|
274
|
+
significance_level=normality_significance_level,
|
|
275
|
+
),
|
|
276
|
+
"imbalance": ImbalanceWarning(
|
|
277
|
+
df,
|
|
278
|
+
target_column=target_column,
|
|
279
|
+
threshold=imbalance_threshold,
|
|
280
|
+
),
|
|
281
|
+
"correlations": CorrelationsWarning(
|
|
282
|
+
df,
|
|
283
|
+
numerical_columns=numerical_columns,
|
|
284
|
+
threshold=correlation_threshold,
|
|
285
|
+
),
|
|
286
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from classifier_toolkit.feature_selection.base import BaseFeatureSelector
|
|
2
|
+
from classifier_toolkit.feature_selection.embedded_methods.elastic_net import (
|
|
3
|
+
ElasticNetSelector,
|
|
4
|
+
)
|
|
5
|
+
from classifier_toolkit.feature_selection.meta_selector import MetaSelector
|
|
6
|
+
from classifier_toolkit.feature_selection.utils.plottings import (
|
|
7
|
+
plot_feature_importances,
|
|
8
|
+
plot_rfecv_results,
|
|
9
|
+
)
|
|
10
|
+
from classifier_toolkit.feature_selection.utils.scoring import (
|
|
11
|
+
false_positive_rate,
|
|
12
|
+
get_scorer,
|
|
13
|
+
true_positive_rate,
|
|
14
|
+
)
|
|
15
|
+
from classifier_toolkit.feature_selection.wrapper_methods.rfe import (
|
|
16
|
+
EnsembleFeatureSelector,
|
|
17
|
+
)
|
|
18
|
+
from classifier_toolkit.feature_selection.wrapper_methods.sequential_selection import (
|
|
19
|
+
SequentialSelector,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"BaseFeatureSelector",
|
|
24
|
+
"MetaSelector",
|
|
25
|
+
"ElasticNetSelector",
|
|
26
|
+
"plot_feature_importances",
|
|
27
|
+
"plot_rfecv_results",
|
|
28
|
+
"get_scorer",
|
|
29
|
+
"false_positive_rate",
|
|
30
|
+
"true_positive_rate",
|
|
31
|
+
"EnsembleFeatureSelector",
|
|
32
|
+
"SequentialSelector",
|
|
33
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List, Literal, Optional, Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from sklearn.base import BaseEstimator, TransformerMixin
|
|
7
|
+
from sklearn.feature_selection import RFECV
|
|
8
|
+
from sklearn.model_selection import BaseCrossValidator, cross_val_score
|
|
9
|
+
from sklearn.utils import check_X_y
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseFeatureSelector(BaseEstimator, TransformerMixin, ABC):
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
estimator: Optional[BaseEstimator],
|
|
17
|
+
n_features_to_select: int = 1,
|
|
18
|
+
scoring: Literal[
|
|
19
|
+
"accuracy", "f1", "precision", "recall", "roc_auc"
|
|
20
|
+
] = "accuracy",
|
|
21
|
+
cv: Union[int, BaseCrossValidator] = 5,
|
|
22
|
+
verbose: int = 0,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.estimator: Optional[BaseEstimator] = estimator
|
|
25
|
+
self.n_features_to_select = n_features_to_select
|
|
26
|
+
self.scoring = scoring
|
|
27
|
+
from .utils.scoring import get_scorer
|
|
28
|
+
|
|
29
|
+
self.scorer = get_scorer(scoring)
|
|
30
|
+
self.cv = cv
|
|
31
|
+
self.verbose = verbose
|
|
32
|
+
self.selected_features_: Optional[List[str]] = None
|
|
33
|
+
self.feature_importances_: Optional[pd.Series] = None
|
|
34
|
+
self.rfecv: Optional[RFECV] = None
|
|
35
|
+
self.rfecv_step: int = 1
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def fit(self, X: pd.DataFrame, y: pd.Series) -> "BaseFeatureSelector":
|
|
39
|
+
"""
|
|
40
|
+
Fit the feature selector to the data.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def transform(self, X: pd.DataFrame) -> pd.DataFrame:
|
|
44
|
+
"""
|
|
45
|
+
Transform the data to include only selected features.
|
|
46
|
+
"""
|
|
47
|
+
if self.selected_features_ is None:
|
|
48
|
+
raise FeatureSelectionError(
|
|
49
|
+
"Selector has not been fitted yet. Call 'fit' first."
|
|
50
|
+
)
|
|
51
|
+
if isinstance(self.selected_features_[0], str):
|
|
52
|
+
return X[self.selected_features_]
|
|
53
|
+
else:
|
|
54
|
+
return X.loc[:, self.selected_features_]
|
|
55
|
+
|
|
56
|
+
def fit_transform(self, X: pd.DataFrame, y: pd.Series) -> pd.DataFrame:
|
|
57
|
+
"""
|
|
58
|
+
Fit the selector and transform the data.
|
|
59
|
+
"""
|
|
60
|
+
return self.fit(X, y).transform(X)
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def _get_score(self, X: pd.DataFrame, y: pd.Series, features: List[int]) -> float:
|
|
64
|
+
"""
|
|
65
|
+
Evaluate the score for a subset of features.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
X (pd.DataFrame): The input features.
|
|
69
|
+
y (pd.Series): The target variable.
|
|
70
|
+
features (List[int]): The indices of features to evaluate.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
float: The score for the given features.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def get_feature_importances(self) -> pd.Series:
|
|
78
|
+
"""
|
|
79
|
+
Get the importance scores for each feature.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def get_support(self, indices: bool = False) -> Union[List[bool], List[int]]:
|
|
83
|
+
"""
|
|
84
|
+
Get a mask, or integer index, of the features selected.
|
|
85
|
+
"""
|
|
86
|
+
if self.selected_features_ is None:
|
|
87
|
+
raise FeatureSelectionError(
|
|
88
|
+
"Selector has not been fitted yet. Call 'fit' first."
|
|
89
|
+
)
|
|
90
|
+
assert self.feature_importances_ is not None, ValueError(
|
|
91
|
+
'Feature importances are not available. Call "fit" first.'
|
|
92
|
+
)
|
|
93
|
+
mask = [
|
|
94
|
+
feature in self.selected_features_
|
|
95
|
+
for feature in self.feature_importances_.index
|
|
96
|
+
]
|
|
97
|
+
if indices:
|
|
98
|
+
return [i for i, m in enumerate(mask) if m]
|
|
99
|
+
return mask
|
|
100
|
+
|
|
101
|
+
def _evaluate_features(
|
|
102
|
+
self, X: Union[pd.DataFrame, np.ndarray], y: pd.Series
|
|
103
|
+
) -> pd.Series:
|
|
104
|
+
"""
|
|
105
|
+
Evaluate the features using cross-validation.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
X (Union[pd.DataFrame, np.ndarray]): The input features.
|
|
109
|
+
y (pd.Series): The target variable.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
pd.Series: A series containing the mean cross-validation score for each feature.
|
|
113
|
+
"""
|
|
114
|
+
if self.estimator is None:
|
|
115
|
+
raise ValueError("Estimator is not set. Please provide an estimator.")
|
|
116
|
+
|
|
117
|
+
X, y = check_X_y(X, y, ensure_min_features=0, force_all_finite=False)
|
|
118
|
+
|
|
119
|
+
if isinstance(X, np.ndarray):
|
|
120
|
+
X = pd.DataFrame(X, columns=[f"feature_{i}" for i in range(X.shape[1])])
|
|
121
|
+
|
|
122
|
+
scores = []
|
|
123
|
+
feature_names = X.columns
|
|
124
|
+
|
|
125
|
+
for feature in feature_names:
|
|
126
|
+
X_feature = X[[feature]]
|
|
127
|
+
try:
|
|
128
|
+
feature_scores = cross_val_score(
|
|
129
|
+
self.estimator,
|
|
130
|
+
X_feature,
|
|
131
|
+
y,
|
|
132
|
+
scoring=self.scorer,
|
|
133
|
+
cv=self.cv,
|
|
134
|
+
n_jobs=-1,
|
|
135
|
+
error_score="raise",
|
|
136
|
+
)
|
|
137
|
+
mean_score = np.mean(feature_scores)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
if self.verbose > 0:
|
|
140
|
+
print(f"Error evaluating feature {feature}: {e!s}")
|
|
141
|
+
mean_score = np.nan
|
|
142
|
+
|
|
143
|
+
scores.append(mean_score)
|
|
144
|
+
|
|
145
|
+
return pd.Series(scores, index=feature_names, name="Feature Scores")
|
|
146
|
+
|
|
147
|
+
def fit_rfecv(
|
|
148
|
+
self, X: pd.DataFrame, y: pd.Series, **rfecv_params
|
|
149
|
+
) -> "BaseFeatureSelector":
|
|
150
|
+
"""
|
|
151
|
+
Fit the selector using RFECV.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
X (pd.DataFrame): The input features.
|
|
155
|
+
y (pd.Series): The target variable.
|
|
156
|
+
**rfecv_params: Additional parameters to to RFECV.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
self: The fitted selector.
|
|
160
|
+
"""
|
|
161
|
+
if self.estimator is None:
|
|
162
|
+
raise ValueError("Estimator is not set. Please provide an estimator.")
|
|
163
|
+
|
|
164
|
+
self.rfecv = RFECV(
|
|
165
|
+
estimator=self.estimator,
|
|
166
|
+
step=self.rfecv_step,
|
|
167
|
+
cv=self.cv,
|
|
168
|
+
scoring=self.scoring,
|
|
169
|
+
n_jobs=-1,
|
|
170
|
+
verbose=self.verbose,
|
|
171
|
+
**rfecv_params,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
self.rfecv.fit(X, y)
|
|
175
|
+
self.selected_features_ = X.columns[self.rfecv.support_].tolist()
|
|
176
|
+
self.feature_importances_ = pd.Series(self.rfecv.ranking_, index=X.columns)
|
|
177
|
+
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class FeatureSelectionError(Exception):
|
|
182
|
+
"""Base exception for feature selection errors."""
|