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,4 @@
1
+ from .automated_warnings import WarningSystem
2
+ from .default_warnings import EDAWarning, get_default_warnings
3
+
4
+ __all__ = ["WarningSystem", "EDAWarning", "get_default_warnings"]
@@ -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."""
@@ -0,0 +1,7 @@
1
+ from classifier_toolkit.feature_selection.embedded_methods.elastic_net import (
2
+ ElasticNetSelector,
3
+ )
4
+
5
+ __all__ = [
6
+ "ElasticNetSelector",
7
+ ]