scikit-extensions 0.2.2__tar.gz → 0.2.5__tar.gz
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.
- {scikit_extensions-0.2.2 → scikit_extensions-0.2.5}/PKG-INFO +10 -1
- {scikit_extensions-0.2.2 → scikit_extensions-0.2.5}/README.md +9 -0
- {scikit_extensions-0.2.2 → scikit_extensions-0.2.5}/pyproject.toml +1 -1
- {scikit_extensions-0.2.2 → scikit_extensions-0.2.5}/pyproject.toml.orig +1 -1
- scikit_extensions-0.2.5/skext/__init__.py +13 -0
- scikit_extensions-0.2.5/skext/feature_engineering.py +201 -0
- {scikit_extensions-0.2.2 → scikit_extensions-0.2.5}/skext/multilabels.py +28 -10
- scikit_extensions-0.2.2/skext/__init__.py +0 -5
- {scikit_extensions-0.2.2 → scikit_extensions-0.2.5}/skext/linear_model.py +0 -0
- {scikit_extensions-0.2.2 → scikit_extensions-0.2.5}/skext/multicolinerity.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scikit-extensions
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: A collection of Scikit-learn-compatible transformers for feature engineering, preprocessing, and pipeline utilities.
|
|
5
5
|
Keywords: scikit-learn,transformers,preprocessing,pipeline
|
|
6
6
|
Author: sh99
|
|
@@ -95,6 +95,15 @@ mlb = MultiLabelBinarizerTransformer(sparse_output=False)
|
|
|
95
95
|
transformed = mlb.fit_transform(data)
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
Each column can contain label iterables or delimiter-separated strings. String
|
|
99
|
+
values are split on commas by default; pass `delimiter` to use another separator.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
data = pd.DataFrame({'tags': ['python|ml', 'python', 'ml|deep-learning']})
|
|
103
|
+
mlb = MultiLabelBinarizerTransformer(delimiter='|')
|
|
104
|
+
transformed = mlb.fit_transform(data)
|
|
105
|
+
```
|
|
106
|
+
|
|
98
107
|
## Development
|
|
99
108
|
|
|
100
109
|
1. Clone the repository:
|
|
@@ -76,6 +76,15 @@ mlb = MultiLabelBinarizerTransformer(sparse_output=False)
|
|
|
76
76
|
transformed = mlb.fit_transform(data)
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
Each column can contain label iterables or delimiter-separated strings. String
|
|
80
|
+
values are split on commas by default; pass `delimiter` to use another separator.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
data = pd.DataFrame({'tags': ['python|ml', 'python', 'ml|deep-learning']})
|
|
84
|
+
mlb = MultiLabelBinarizerTransformer(delimiter='|')
|
|
85
|
+
transformed = mlb.fit_transform(data)
|
|
86
|
+
```
|
|
87
|
+
|
|
79
88
|
## Development
|
|
80
89
|
|
|
81
90
|
1. Clone the repository:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "scikit-extensions"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.5"
|
|
4
4
|
description = "A collection of Scikit-learn-compatible transformers for feature engineering, preprocessing, and pipeline utilities."
|
|
5
5
|
authors = [{ name = "sh99", email = "shriramh99@gmail.com" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .multilabels import MultiLabelBinarizerTransformer
|
|
2
|
+
from .multicolinerity import MultiCollinearityReducer
|
|
3
|
+
from .linear_model import OLSRegressor
|
|
4
|
+
from .feature_engineering import RareCategoryEncoder, TargetEncoder, TimeFeatureExtractor
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"MultiLabelBinarizerTransformer",
|
|
8
|
+
"MultiCollinearityReducer",
|
|
9
|
+
"OLSRegressor",
|
|
10
|
+
"RareCategoryEncoder",
|
|
11
|
+
"TargetEncoder",
|
|
12
|
+
"TimeFeatureExtractor",
|
|
13
|
+
]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from sklearn.base import BaseEstimator, TransformerMixin
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RareCategoryEncoder(BaseEstimator, TransformerMixin):
|
|
7
|
+
"""Replace infrequent categorical values with a fallback label."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, min_frequency=0.05, replacement="other", columns=None):
|
|
10
|
+
self.min_frequency = min_frequency
|
|
11
|
+
self.replacement = replacement
|
|
12
|
+
self.columns = columns
|
|
13
|
+
|
|
14
|
+
def fit(self, X, y=None):
|
|
15
|
+
X = X.copy() if isinstance(X, pd.DataFrame) else pd.DataFrame(X)
|
|
16
|
+
|
|
17
|
+
if self.columns is None:
|
|
18
|
+
self.columns_ = [
|
|
19
|
+
col for col in X.columns
|
|
20
|
+
if not pd.api.types.is_numeric_dtype(X[col])
|
|
21
|
+
and not pd.api.types.is_bool_dtype(X[col])
|
|
22
|
+
]
|
|
23
|
+
else:
|
|
24
|
+
self.columns_ = list(self.columns)
|
|
25
|
+
missing = [col for col in self.columns_ if col not in X.columns]
|
|
26
|
+
if missing:
|
|
27
|
+
raise ValueError(f"Column '{missing[0]}' is not present in X.")
|
|
28
|
+
|
|
29
|
+
self.rare_categories_ = {}
|
|
30
|
+
for col in self.columns_:
|
|
31
|
+
values = X[col].astype("object").fillna("__missing__")
|
|
32
|
+
counts = values.value_counts(normalize=True)
|
|
33
|
+
threshold = self.min_frequency
|
|
34
|
+
rare = counts[counts < threshold].index
|
|
35
|
+
self.rare_categories_[col] = set(rare)
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def transform(self, X):
|
|
39
|
+
if not hasattr(self, "rare_categories_"):
|
|
40
|
+
raise ValueError("Encoder has not been fitted yet.")
|
|
41
|
+
|
|
42
|
+
X = X.copy() if isinstance(X, pd.DataFrame) else pd.DataFrame(X)
|
|
43
|
+
transformed = X.copy()
|
|
44
|
+
|
|
45
|
+
for col in self.columns_:
|
|
46
|
+
if col not in transformed.columns:
|
|
47
|
+
raise ValueError(f"Column '{col}' is not present in X.")
|
|
48
|
+
values = transformed[col].astype("object").fillna("__missing__")
|
|
49
|
+
transformed[col] = values.map(
|
|
50
|
+
lambda value: self.replacement if value in self.rare_categories_[col] else value
|
|
51
|
+
)
|
|
52
|
+
transformed[col] = transformed[col].replace("__missing__", self.replacement)
|
|
53
|
+
return transformed
|
|
54
|
+
|
|
55
|
+
def get_feature_names_out(self, input_features=None):
|
|
56
|
+
if input_features is None:
|
|
57
|
+
input_features = list(self.columns_)
|
|
58
|
+
return np.asarray(input_features, dtype=object)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TargetEncoder(BaseEstimator, TransformerMixin):
|
|
62
|
+
"""Encode categorical columns using the mean target value."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, columns=None, handle_unknown="mean"):
|
|
65
|
+
self.columns = columns
|
|
66
|
+
self.handle_unknown = handle_unknown
|
|
67
|
+
|
|
68
|
+
def fit(self, X, y):
|
|
69
|
+
X = X.copy() if isinstance(X, pd.DataFrame) else pd.DataFrame(X)
|
|
70
|
+
y = pd.Series(np.asarray(y)).reset_index(drop=True)
|
|
71
|
+
|
|
72
|
+
if self.columns is None:
|
|
73
|
+
self.columns_ = [
|
|
74
|
+
col for col in X.columns
|
|
75
|
+
if not pd.api.types.is_numeric_dtype(X[col])
|
|
76
|
+
and not pd.api.types.is_bool_dtype(X[col])
|
|
77
|
+
]
|
|
78
|
+
else:
|
|
79
|
+
self.columns_ = list(self.columns)
|
|
80
|
+
missing = [col for col in self.columns_ if col not in X.columns]
|
|
81
|
+
if missing:
|
|
82
|
+
raise ValueError(f"Column '{missing[0]}' is not present in X.")
|
|
83
|
+
|
|
84
|
+
self.global_mean_ = float(y.mean()) if len(y) else 0.0
|
|
85
|
+
self.mapping_ = {}
|
|
86
|
+
|
|
87
|
+
for col in self.columns_:
|
|
88
|
+
mapping = pd.Series(y.values, index=X[col].astype("object").fillna("__missing__")).groupby(level=0).mean()
|
|
89
|
+
self.mapping_[col] = mapping.to_dict()
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def transform(self, X):
|
|
93
|
+
if not hasattr(self, "mapping_"):
|
|
94
|
+
raise ValueError("Encoder has not been fitted yet.")
|
|
95
|
+
|
|
96
|
+
X = X.copy() if isinstance(X, pd.DataFrame) else pd.DataFrame(X)
|
|
97
|
+
transformed = X.copy()
|
|
98
|
+
|
|
99
|
+
for col in self.columns_:
|
|
100
|
+
if col not in transformed.columns:
|
|
101
|
+
raise ValueError(f"Column '{col}' is not present in X.")
|
|
102
|
+
values = transformed[col].astype("object").fillna("__missing__")
|
|
103
|
+
transformed[col] = values.map(self.mapping_[col]).fillna(self.global_mean_)
|
|
104
|
+
return transformed
|
|
105
|
+
|
|
106
|
+
def get_feature_names_out(self, input_features=None):
|
|
107
|
+
if input_features is None:
|
|
108
|
+
input_features = list(self.columns_)
|
|
109
|
+
return np.asarray(input_features, dtype=object)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TimeFeatureExtractor(BaseEstimator, TransformerMixin):
|
|
113
|
+
"""Extract calendar features from datetime columns."""
|
|
114
|
+
|
|
115
|
+
VALID_FEATURES = {
|
|
116
|
+
"month",
|
|
117
|
+
"day_of_month",
|
|
118
|
+
"day_of_week",
|
|
119
|
+
"hour",
|
|
120
|
+
"minute",
|
|
121
|
+
"is_weekend",
|
|
122
|
+
"quarter",
|
|
123
|
+
"year",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def __init__(self, features=None, columns=None):
|
|
127
|
+
if features is None:
|
|
128
|
+
features = ["month", "day_of_week", "hour", "is_weekend"]
|
|
129
|
+
self.features = list(features)
|
|
130
|
+
self.columns = columns
|
|
131
|
+
|
|
132
|
+
def fit(self, X, y=None):
|
|
133
|
+
X = X.copy() if isinstance(X, pd.DataFrame) else pd.DataFrame(X)
|
|
134
|
+
|
|
135
|
+
invalid = [feature for feature in self.features if feature not in self.VALID_FEATURES]
|
|
136
|
+
if invalid:
|
|
137
|
+
raise ValueError(f"Unsupported time features: {invalid}")
|
|
138
|
+
|
|
139
|
+
if self.columns is None:
|
|
140
|
+
candidate_columns = []
|
|
141
|
+
for col in X.columns:
|
|
142
|
+
try:
|
|
143
|
+
pd.to_datetime(X[col])
|
|
144
|
+
candidate_columns.append(col)
|
|
145
|
+
except (TypeError, ValueError):
|
|
146
|
+
pass
|
|
147
|
+
self.columns_ = candidate_columns
|
|
148
|
+
else:
|
|
149
|
+
self.columns_ = list(self.columns)
|
|
150
|
+
missing = [col for col in self.columns_ if col not in X.columns]
|
|
151
|
+
if missing:
|
|
152
|
+
raise ValueError(f"Column '{missing[0]}' is not present in X.")
|
|
153
|
+
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
def transform(self, X):
|
|
157
|
+
if not hasattr(self, "columns_"):
|
|
158
|
+
raise ValueError("Transformer has not been fitted yet.")
|
|
159
|
+
|
|
160
|
+
X = X.copy() if isinstance(X, pd.DataFrame) else pd.DataFrame(X)
|
|
161
|
+
output = pd.DataFrame(index=X.index)
|
|
162
|
+
|
|
163
|
+
for col in self.columns_:
|
|
164
|
+
if col not in X.columns:
|
|
165
|
+
raise ValueError(f"Column '{col}' is not present in X.")
|
|
166
|
+
series = pd.to_datetime(X[col], errors="coerce")
|
|
167
|
+
|
|
168
|
+
for feature in self.features:
|
|
169
|
+
if feature == "month":
|
|
170
|
+
values = series.dt.month
|
|
171
|
+
elif feature == "day_of_month":
|
|
172
|
+
values = series.dt.day
|
|
173
|
+
elif feature == "day_of_week":
|
|
174
|
+
values = series.dt.dayofweek
|
|
175
|
+
elif feature == "hour":
|
|
176
|
+
values = series.dt.hour
|
|
177
|
+
elif feature == "minute":
|
|
178
|
+
values = series.dt.minute
|
|
179
|
+
elif feature == "is_weekend":
|
|
180
|
+
values = series.dt.dayofweek.isin([5, 6])
|
|
181
|
+
elif feature == "quarter":
|
|
182
|
+
values = series.dt.quarter
|
|
183
|
+
elif feature == "year":
|
|
184
|
+
values = series.dt.year
|
|
185
|
+
else:
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
name = feature if len(self.columns_) == 1 else f"{col}__{feature}"
|
|
189
|
+
output[name] = values
|
|
190
|
+
|
|
191
|
+
return output
|
|
192
|
+
|
|
193
|
+
def get_feature_names_out(self, input_features=None):
|
|
194
|
+
if not hasattr(self, "columns_"):
|
|
195
|
+
raise ValueError("Transformer has not been fitted yet.")
|
|
196
|
+
|
|
197
|
+
names = []
|
|
198
|
+
for col in self.columns_:
|
|
199
|
+
for feature in self.features:
|
|
200
|
+
names.append(feature if len(self.columns_) == 1 else f"{col}__{feature}")
|
|
201
|
+
return np.asarray(names, dtype=object)
|
|
@@ -9,23 +9,27 @@ class MultiLabelBinarizerTransformer(BaseEstimator, TransformerMixin):
|
|
|
9
9
|
"""
|
|
10
10
|
A custom transformer that wraps sklearn's MultiLabelBinarizer to integrate seamlessly into scikit-learn pipelines.
|
|
11
11
|
|
|
12
|
-
This transformer converts
|
|
13
|
-
|
|
12
|
+
This transformer converts columns containing label iterables or delimiter-separated
|
|
13
|
+
strings into a binary indicator matrix. String values are split using ``delimiter``.
|
|
14
|
+
It is particularly useful for multi-label classification tasks where each sample
|
|
15
|
+
can belong to multiple classes.
|
|
14
16
|
|
|
15
17
|
Parameters
|
|
16
18
|
----------
|
|
17
19
|
sparse_output : bool, default=False
|
|
18
20
|
If True, the output will be a sparse matrix in Compressed Sparse Row (CSR) format. Otherwise, a dense NumPy array is returned.
|
|
21
|
+
delimiter : str, default=","
|
|
22
|
+
Delimiter used to split string values in input columns.
|
|
19
23
|
|
|
20
24
|
Attributes
|
|
21
25
|
----------
|
|
22
|
-
|
|
26
|
+
mlbs_ : dict
|
|
23
27
|
A dictionary mapping column names to fitted MultiLabelBinarizer instances.
|
|
24
|
-
feature_names_in_ :
|
|
25
|
-
|
|
28
|
+
feature_names_in_ : ndarray of shape (n_features_in_,)
|
|
29
|
+
Input feature names seen during fitting, when all column names are strings.
|
|
26
30
|
"""
|
|
27
31
|
|
|
28
|
-
def __init__(self, sparse_output=False):
|
|
32
|
+
def __init__(self, sparse_output=False, delimiter=","):
|
|
29
33
|
"""
|
|
30
34
|
Initializes the transformer with the specified parameters.
|
|
31
35
|
|
|
@@ -33,8 +37,18 @@ class MultiLabelBinarizerTransformer(BaseEstimator, TransformerMixin):
|
|
|
33
37
|
----------
|
|
34
38
|
sparse_output : bool, default=False
|
|
35
39
|
If True, the output will be a sparse matrix in Compressed Sparse Row (CSR) format. Otherwise, a dense NumPy array is returned.
|
|
40
|
+
delimiter : str, default=","
|
|
41
|
+
Delimiter used to split string values in input columns.
|
|
36
42
|
"""
|
|
37
43
|
self.sparse_output = sparse_output
|
|
44
|
+
self.delimiter = delimiter
|
|
45
|
+
|
|
46
|
+
def _prepare_column(self, values):
|
|
47
|
+
return values.map(
|
|
48
|
+
lambda value: [label.strip() for label in value.split(self.delimiter) if label.strip()]
|
|
49
|
+
if isinstance(value, str)
|
|
50
|
+
else value
|
|
51
|
+
)
|
|
38
52
|
|
|
39
53
|
def fit(self, X, y=None):
|
|
40
54
|
"""
|
|
@@ -43,7 +57,8 @@ class MultiLabelBinarizerTransformer(BaseEstimator, TransformerMixin):
|
|
|
43
57
|
Parameters
|
|
44
58
|
----------
|
|
45
59
|
X : pandas DataFrame of shape (n_samples, n_features)
|
|
46
|
-
|
|
60
|
+
Each column contains label iterables or delimiter-separated strings,
|
|
61
|
+
where each cell contains the labels for one sample.
|
|
47
62
|
|
|
48
63
|
y : None
|
|
49
64
|
Ignored. This parameter exists for compatibility with scikit-learn pipelines.
|
|
@@ -62,7 +77,9 @@ class MultiLabelBinarizerTransformer(BaseEstimator, TransformerMixin):
|
|
|
62
77
|
elif hasattr(self, "feature_names_in_"):
|
|
63
78
|
del self.feature_names_in_
|
|
64
79
|
self.mlbs_ = {
|
|
65
|
-
col: MultiLabelBinarizer(sparse_output=self.sparse_output).fit(
|
|
80
|
+
col: MultiLabelBinarizer(sparse_output=self.sparse_output).fit(
|
|
81
|
+
self._prepare_column(X[col])
|
|
82
|
+
)
|
|
66
83
|
for col in self.input_columns_
|
|
67
84
|
}
|
|
68
85
|
return self
|
|
@@ -74,7 +91,8 @@ class MultiLabelBinarizerTransformer(BaseEstimator, TransformerMixin):
|
|
|
74
91
|
Parameters
|
|
75
92
|
----------
|
|
76
93
|
X : pandas DataFrame of shape (n_samples, n_features)
|
|
77
|
-
|
|
94
|
+
Each column contains label iterables or delimiter-separated strings,
|
|
95
|
+
where each cell contains the labels for one sample.
|
|
78
96
|
|
|
79
97
|
Returns
|
|
80
98
|
-------
|
|
@@ -90,7 +108,7 @@ class MultiLabelBinarizerTransformer(BaseEstimator, TransformerMixin):
|
|
|
90
108
|
transformed_parts = []
|
|
91
109
|
for col in self.input_columns_:
|
|
92
110
|
mlb = self.mlbs_[col]
|
|
93
|
-
part = mlb.transform(X[col])
|
|
111
|
+
part = mlb.transform(self._prepare_column(X[col]))
|
|
94
112
|
if not self.sparse_output:
|
|
95
113
|
part = pd.DataFrame(
|
|
96
114
|
part,
|
|
File without changes
|
|
File without changes
|