sd-auto-encoder 0.3.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.
- sd_auto_encoder-0.3.0.dist-info/METADATA +120 -0
- sd_auto_encoder-0.3.0.dist-info/RECORD +8 -0
- sd_auto_encoder-0.3.0.dist-info/WHEEL +5 -0
- sd_auto_encoder-0.3.0.dist-info/licenses/LICENSE +21 -0
- sd_auto_encoder-0.3.0.dist-info/top_level.txt +1 -0
- sdencoder/__init__.py +4 -0
- sdencoder/encoder.py +230 -0
- sdencoder/target.py +66 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sd-auto-encoder
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: auto-picks onehot/ordinal/binary encoding per categorical column
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pandas>=1.2
|
|
9
|
+
Dynamic: license-file
|
|
10
|
+
|
|
11
|
+
# SDEncoder
|
|
12
|
+
|
|
13
|
+
Auto-picks an encoding method per categorical column instead of you manually
|
|
14
|
+
deciding onehot vs ordinal vs label encoding for each one. Companion to
|
|
15
|
+
SDScaler, same idea but for categorical data instead of numeric.
|
|
16
|
+
|
|
17
|
+
Two things in here:
|
|
18
|
+
- `SDEncoder` - for feature columns (X)
|
|
19
|
+
- `SDTargetEncoder` - for your target column (y)
|
|
20
|
+
|
|
21
|
+
They're kept separate on purpose. Label-style integer codes are fine for a
|
|
22
|
+
target (a model just needs distinct class ids, order doesn't matter), but
|
|
23
|
+
doing that on a *feature* column tricks a lot of models into thinking there's
|
|
24
|
+
a numeric relationship between categories that don't actually have one.
|
|
25
|
+
`SDEncoder` will refuse to touch a Series for this reason, and `SDTargetEncoder`
|
|
26
|
+
will refuse a DataFrame.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
cd SDEncoder
|
|
32
|
+
pip install -e .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import pandas as pd
|
|
39
|
+
from sdencoder import SDEncoder, SDTargetEncoder
|
|
40
|
+
|
|
41
|
+
df = pd.DataFrame({
|
|
42
|
+
"color": ["red", "blue", "green", "blue"],
|
|
43
|
+
"size": ["S", "M", "L", "M"],
|
|
44
|
+
"is_member": ["yes", "no", "yes", "yes"],
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
encoder = SDEncoder(ordinal_maps={"size": ["S", "M", "L"]})
|
|
48
|
+
X_encoded = encoder.fit_transform(df)
|
|
49
|
+
print(X_encoded)
|
|
50
|
+
print(encoder.summary())
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For the target:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
y = pd.Series(["approved", "denied", "approved"])
|
|
57
|
+
target_encoder = SDTargetEncoder()
|
|
58
|
+
y_encoded = target_encoder.fit_transform(y)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Same train/test rule as SDScaler - fit on train, just transform on test:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
X_train_enc = encoder.fit_transform(X_train)
|
|
65
|
+
X_test_enc = encoder.transform(X_test)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## How SDEncoder decides
|
|
69
|
+
|
|
70
|
+
For each object/category column:
|
|
71
|
+
- if you gave it an `ordinal_maps` entry for that column -> uses your order
|
|
72
|
+
- 2 categories -> binary (0/1)
|
|
73
|
+
- more than 2 but under `onehot_cutoff` (default 10) -> onehot
|
|
74
|
+
- more than `onehot_cutoff` -> falls back to ordinal, since onehot on
|
|
75
|
+
something like a "city" column with 200 values would blow up your
|
|
76
|
+
feature count for not much benefit
|
|
77
|
+
|
|
78
|
+
Numeric columns are left completely alone.
|
|
79
|
+
|
|
80
|
+
`ordinal_maps` is the important one to use correctly - only pass a real
|
|
81
|
+
order there for columns that actually have one (like S/M/L, or
|
|
82
|
+
low/medium/high). Don't use it just to avoid getting a onehot column.
|
|
83
|
+
|
|
84
|
+
## Unseen categories
|
|
85
|
+
|
|
86
|
+
If `.transform()` sees a category that wasn't there during `.fit()`, it
|
|
87
|
+
becomes `NaN` for ordinal/binary columns, or an all-zero row for onehot
|
|
88
|
+
columns. That's intentional - better to notice missing data than have it
|
|
89
|
+
silently mapped to some made-up number.
|
|
90
|
+
|
|
91
|
+
## ColumnTransformer
|
|
92
|
+
|
|
93
|
+
If you'd rather use this inside an sklearn `Pipeline`:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
encoder = SDEncoder(ordinal_maps={"size": ["S", "M", "L"]}).fit(df)
|
|
97
|
+
ct = encoder.to_column_transformer()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This just builds an sklearn `ColumnTransformer` using the same decisions
|
|
101
|
+
SDEncoder already made. Needs scikit-learn installed, only used if you
|
|
102
|
+
actually call this method.
|
|
103
|
+
|
|
104
|
+
## Files
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
sdencoder/
|
|
108
|
+
encoder.py - SDEncoder, for X
|
|
109
|
+
target.py - SDTargetEncoder, for y
|
|
110
|
+
examples/
|
|
111
|
+
example_usage.py
|
|
112
|
+
tests/
|
|
113
|
+
test_encoder.py
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Todo / ideas
|
|
117
|
+
|
|
118
|
+
- frequency encoding as another fallback option for high-cardinality columns
|
|
119
|
+
- inverse_transform for onehot columns (currently raises NotImplementedError)
|
|
120
|
+
- handle missing values (NaN) as their own category instead of erroring later
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
sd_auto_encoder-0.3.0.dist-info/licenses/LICENSE,sha256=ESYyLizI0WWtxMeS7rGVcX3ivMezm-HOd5WdeOh-9oU,1056
|
|
2
|
+
sdencoder/__init__.py,sha256=k1MoH1x2uyLHrWxU-HDWGhxqJHOUGjGLg2--puD1Qes,90
|
|
3
|
+
sdencoder/encoder.py,sha256=Uon-kmLrrhdz3RkRyGtimx1JBB6VAJESwayiDLEmADc,9533
|
|
4
|
+
sdencoder/target.py,sha256=7VbtfkHSm_8eWOgCh8_O6UyHPT88yxHFaVp0EZ8a22A,2392
|
|
5
|
+
sd_auto_encoder-0.3.0.dist-info/METADATA,sha256=9UIDaC50N-lKJfvMV7ECTEvFW5iMewgrURfX8anMYGA,3616
|
|
6
|
+
sd_auto_encoder-0.3.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
7
|
+
sd_auto_encoder-0.3.0.dist-info/top_level.txt,sha256=FjkiQcQPbRudyfba0RAq7EgeDUnVJc4b1LJFiYfVsM0,10
|
|
8
|
+
sd_auto_encoder-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sdencoder
|
sdencoder/__init__.py
ADDED
sdencoder/encoder.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
METHODS = ("onehot", "ordinal", "binary", "target")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SDEncoder:
|
|
7
|
+
"""
|
|
8
|
+
encodes categorical feature columns - picks onehot, ordinal, or binary
|
|
9
|
+
per column depending on how many categories it has.
|
|
10
|
+
|
|
11
|
+
encoder = SDEncoder()
|
|
12
|
+
X_encoded = encoder.fit_transform(X)
|
|
13
|
+
encoder.summary() # see what got picked per column
|
|
14
|
+
|
|
15
|
+
pass ordinal_maps for columns that actually have a real order, like
|
|
16
|
+
ordinal_maps={"education": ["high school", "bachelors", "masters", "phd"]}
|
|
17
|
+
anything not listed there just gets auto-picked based on category count.
|
|
18
|
+
|
|
19
|
+
don't use this on your target (y), use SDTargetEncoder for that instead.
|
|
20
|
+
label-style integer codes are fine for a target since the model just
|
|
21
|
+
needs distinct class ids, but doing that on a feature column makes the
|
|
22
|
+
model think there's a numeric order between categories that don't
|
|
23
|
+
actually have one (unless you gave it an ordinal_map on purpose).
|
|
24
|
+
|
|
25
|
+
pass target_encode_cols if you want specific high-cardinality columns
|
|
26
|
+
replaced with the (smoothed) mean of y per category instead of falling
|
|
27
|
+
back to ordinal. this one's opt-in only, never auto-picked, since it
|
|
28
|
+
needs y and carries more overfitting risk than the other three. if you
|
|
29
|
+
use it, fit() needs y: encoder.fit(X, y)
|
|
30
|
+
|
|
31
|
+
note on the target method: this does simple mean + smoothing, not the
|
|
32
|
+
full cross-fold fitting sklearn's own TargetEncoder does internally, so
|
|
33
|
+
it's a bit more overfit-prone on the training set itself. fine for a
|
|
34
|
+
class project, but for anything more serious use sklearn's TargetEncoder
|
|
35
|
+
directly with fit_transform() on train.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, onehot_cutoff=10, ordinal_maps=None, drop_first=False,
|
|
39
|
+
target_encode_cols=None, target_smoothing=10):
|
|
40
|
+
self.onehot_cutoff = onehot_cutoff
|
|
41
|
+
self.ordinal_maps = ordinal_maps or {}
|
|
42
|
+
self.drop_first = drop_first
|
|
43
|
+
self.target_encode_cols = target_encode_cols or []
|
|
44
|
+
self.target_smoothing = target_smoothing
|
|
45
|
+
|
|
46
|
+
self.stats_ = {}
|
|
47
|
+
self.columns_ = None
|
|
48
|
+
self.cat_columns_ = None
|
|
49
|
+
self.fitted = False
|
|
50
|
+
|
|
51
|
+
def fit(self, X, y=None):
|
|
52
|
+
if isinstance(X, pd.Series):
|
|
53
|
+
raise TypeError(
|
|
54
|
+
"SDEncoder wants a DataFrame of features, not a Series. "
|
|
55
|
+
"if this is your target column, use SDTargetEncoder instead."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if self.target_encode_cols and y is None:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"target_encode_cols is set but no y was given - "
|
|
61
|
+
"target encoding needs the target column, call fit(X, y)"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
X = pd.DataFrame(X)
|
|
65
|
+
self.columns_ = list(X.columns)
|
|
66
|
+
# anything not numeric gets treated as categorical - covers "object"
|
|
67
|
+
# dtype, the newer pandas "str" dtype, and category dtype all at
|
|
68
|
+
# once instead of checking dtype names one by one (those checks
|
|
69
|
+
# broke on pandas 3.x, which reports plain strings as dtype "str")
|
|
70
|
+
self.cat_columns_ = [
|
|
71
|
+
c for c in self.columns_ if not pd.api.types.is_numeric_dtype(X[c])
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
self.stats_ = {}
|
|
75
|
+
for col in self.cat_columns_:
|
|
76
|
+
if col in self.target_encode_cols:
|
|
77
|
+
self.stats_[col] = self._fit_target(X[col], y)
|
|
78
|
+
else:
|
|
79
|
+
self.stats_[col] = self._fit_one(X[col])
|
|
80
|
+
|
|
81
|
+
self.fitted = True
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def transform(self, X):
|
|
85
|
+
if not self.fitted:
|
|
86
|
+
raise RuntimeError("call fit() before transform()")
|
|
87
|
+
|
|
88
|
+
X = pd.DataFrame(X)
|
|
89
|
+
out = X.copy()
|
|
90
|
+
|
|
91
|
+
for col in self.cat_columns_:
|
|
92
|
+
stat = self.stats_[col]
|
|
93
|
+
|
|
94
|
+
if stat["method"] == "onehot":
|
|
95
|
+
dummies = pd.get_dummies(X[col], prefix=col)
|
|
96
|
+
# line up columns with what we saw during fit, in case a
|
|
97
|
+
# category is missing/new in this batch of data
|
|
98
|
+
dummies = dummies.reindex(columns=stat["dummy_columns"], fill_value=0)
|
|
99
|
+
out = out.drop(columns=[col])
|
|
100
|
+
out = pd.concat([out, dummies], axis=1)
|
|
101
|
+
elif stat["method"] == "target":
|
|
102
|
+
out[col] = X[col].map(stat["mapping"]).fillna(stat["global_mean"])
|
|
103
|
+
# unseen categories get the global mean here instead of NaN -
|
|
104
|
+
# that's the standard fallback for target encoding, "we have
|
|
105
|
+
# no info about this category so just guess the average"
|
|
106
|
+
else:
|
|
107
|
+
out[col] = X[col].map(stat["mapping"])
|
|
108
|
+
# categories not seen during fit turn into NaN on purpose,
|
|
109
|
+
# better than silently making up a code for them
|
|
110
|
+
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
def fit_transform(self, X, y=None):
|
|
114
|
+
return self.fit(X, y).transform(X)
|
|
115
|
+
|
|
116
|
+
def inverse_transform(self, X):
|
|
117
|
+
if not self.fitted:
|
|
118
|
+
raise RuntimeError("call fit() before inverse_transform()")
|
|
119
|
+
|
|
120
|
+
X = pd.DataFrame(X).copy()
|
|
121
|
+
for col in self.cat_columns_:
|
|
122
|
+
stat = self.stats_[col]
|
|
123
|
+
if stat["method"] in ("onehot", "target"):
|
|
124
|
+
raise NotImplementedError(
|
|
125
|
+
f"can't auto-reverse a {stat['method']} column ('{col}') through this method"
|
|
126
|
+
)
|
|
127
|
+
reverse = {v: k for k, v in stat["mapping"].items()}
|
|
128
|
+
X[col] = X[col].map(reverse)
|
|
129
|
+
|
|
130
|
+
return X
|
|
131
|
+
|
|
132
|
+
def summary(self):
|
|
133
|
+
if not self.fitted:
|
|
134
|
+
raise RuntimeError("call fit() before summary()")
|
|
135
|
+
|
|
136
|
+
rows = []
|
|
137
|
+
for col in self.columns_:
|
|
138
|
+
if col in self.stats_:
|
|
139
|
+
rows.append({
|
|
140
|
+
"column": col,
|
|
141
|
+
"method": self.stats_[col]["method"],
|
|
142
|
+
"categories": self.stats_[col]["n_categories"],
|
|
143
|
+
})
|
|
144
|
+
else:
|
|
145
|
+
rows.append({"column": col, "method": "passthrough", "categories": None})
|
|
146
|
+
|
|
147
|
+
return pd.DataFrame(rows)
|
|
148
|
+
|
|
149
|
+
def to_column_transformer(self):
|
|
150
|
+
"""
|
|
151
|
+
builds an sklearn ColumnTransformer that mirrors whatever this
|
|
152
|
+
encoder decided, in case you'd rather plug it into an sklearn
|
|
153
|
+
Pipeline instead of calling .transform() directly. needs sklearn.
|
|
154
|
+
|
|
155
|
+
note: target-encoded columns get dropped from the ColumnTransformer
|
|
156
|
+
version since sklearn's transformers don't take y at transform time
|
|
157
|
+
the way our target method does - if you're using target encoding,
|
|
158
|
+
just call SDEncoder.transform() directly instead of going through
|
|
159
|
+
this method for those columns.
|
|
160
|
+
"""
|
|
161
|
+
if not self.fitted:
|
|
162
|
+
raise RuntimeError("call fit() before to_column_transformer()")
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
from sklearn.compose import ColumnTransformer
|
|
166
|
+
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
|
|
167
|
+
except ImportError:
|
|
168
|
+
raise ImportError("to_column_transformer() needs scikit-learn installed")
|
|
169
|
+
|
|
170
|
+
transformers = []
|
|
171
|
+
for col, stat in self.stats_.items():
|
|
172
|
+
if stat["method"] == "onehot":
|
|
173
|
+
transformers.append((f"{col}_onehot", OneHotEncoder(handle_unknown="ignore"), [col]))
|
|
174
|
+
elif stat["method"] == "target":
|
|
175
|
+
continue
|
|
176
|
+
else:
|
|
177
|
+
order = [list(stat["mapping"].keys())]
|
|
178
|
+
transformers.append((f"{col}_ordinal", OrdinalEncoder(categories=order), [col]))
|
|
179
|
+
|
|
180
|
+
return ColumnTransformer(transformers, remainder="passthrough")
|
|
181
|
+
|
|
182
|
+
# internal, don't call directly
|
|
183
|
+
|
|
184
|
+
def _fit_one(self, series):
|
|
185
|
+
col = series.name
|
|
186
|
+
n_unique = series.nunique(dropna=True)
|
|
187
|
+
|
|
188
|
+
if col in self.ordinal_maps:
|
|
189
|
+
order = self.ordinal_maps[col]
|
|
190
|
+
mapping = {cat: i for i, cat in enumerate(order)}
|
|
191
|
+
return {"method": "ordinal", "mapping": mapping, "n_categories": len(order)}
|
|
192
|
+
|
|
193
|
+
if n_unique == 2:
|
|
194
|
+
cats = sorted(series.dropna().unique())
|
|
195
|
+
mapping = {cats[0]: 0, cats[1]: 1}
|
|
196
|
+
return {"method": "binary", "mapping": mapping, "n_categories": 2}
|
|
197
|
+
|
|
198
|
+
if n_unique <= self.onehot_cutoff:
|
|
199
|
+
dummies = pd.get_dummies(series, prefix=col)
|
|
200
|
+
dummy_cols = list(dummies.columns)
|
|
201
|
+
if self.drop_first and len(dummy_cols) > 1:
|
|
202
|
+
dummy_cols = dummy_cols[1:]
|
|
203
|
+
return {"method": "onehot", "dummy_columns": dummy_cols, "n_categories": n_unique}
|
|
204
|
+
|
|
205
|
+
# too many categories for onehot to be practical, fall back to ordinal
|
|
206
|
+
cats = sorted(series.dropna().unique())
|
|
207
|
+
mapping = {cat: i for i, cat in enumerate(cats)}
|
|
208
|
+
return {"method": "ordinal", "mapping": mapping, "n_categories": n_unique}
|
|
209
|
+
|
|
210
|
+
def _fit_target(self, series, y):
|
|
211
|
+
y = pd.Series(y).reset_index(drop=True)
|
|
212
|
+
series = series.reset_index(drop=True)
|
|
213
|
+
|
|
214
|
+
global_mean = float(y.mean())
|
|
215
|
+
smoothing = self.target_smoothing
|
|
216
|
+
|
|
217
|
+
grouped = y.groupby(series).agg(["mean", "count"])
|
|
218
|
+
# blend each category's own mean with the global mean - categories
|
|
219
|
+
# with few rows lean more on the global mean, categories with lots
|
|
220
|
+
# of rows lean more on their own mean. classic smoothing trick so
|
|
221
|
+
# a category that only shows up once or twice doesn't get a wildly
|
|
222
|
+
# overfit value.
|
|
223
|
+
smoothed = (grouped["count"] * grouped["mean"] + smoothing * global_mean) / (grouped["count"] + smoothing)
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
"method": "target",
|
|
227
|
+
"mapping": smoothed.to_dict(),
|
|
228
|
+
"global_mean": global_mean,
|
|
229
|
+
"n_categories": series.nunique(dropna=True),
|
|
230
|
+
}
|
sdencoder/target.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SDTargetEncoder:
|
|
5
|
+
"""
|
|
6
|
+
encodes a target column (y) into integer class labels, basically what
|
|
7
|
+
LabelEncoder does. only meant for a single column, not a full feature
|
|
8
|
+
dataframe - that's what SDEncoder is for.
|
|
9
|
+
|
|
10
|
+
by default it just sorts categories alphabetically and numbers them,
|
|
11
|
+
which is fine if you don't care which class gets which number. but if
|
|
12
|
+
your target actually has a real order (low/medium/high) or you care
|
|
13
|
+
which class ends up as 1 for a binary target (metrics like precision/
|
|
14
|
+
recall treat 1 as the "positive" class), pass class_order yourself:
|
|
15
|
+
|
|
16
|
+
SDTargetEncoder(class_order=["low", "medium", "high"])
|
|
17
|
+
SDTargetEncoder(class_order=["denied", "approved"]) # approved -> 1
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, class_order=None):
|
|
21
|
+
self.class_order = class_order
|
|
22
|
+
self.mapping_ = None
|
|
23
|
+
self.classes_ = None
|
|
24
|
+
self.fitted = False
|
|
25
|
+
|
|
26
|
+
def fit(self, y):
|
|
27
|
+
if isinstance(y, pd.DataFrame):
|
|
28
|
+
raise TypeError(
|
|
29
|
+
"SDTargetEncoder wants a single column, not a DataFrame. "
|
|
30
|
+
"for feature columns use SDEncoder instead."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
y = pd.Series(y)
|
|
34
|
+
actual_classes = set(y.dropna().unique())
|
|
35
|
+
|
|
36
|
+
if self.class_order is not None:
|
|
37
|
+
given_classes = set(self.class_order)
|
|
38
|
+
if given_classes != actual_classes:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"class_order {self.class_order} doesn't match the classes "
|
|
41
|
+
f"actually found in y: {sorted(actual_classes)}"
|
|
42
|
+
)
|
|
43
|
+
self.classes_ = list(self.class_order)
|
|
44
|
+
else:
|
|
45
|
+
self.classes_ = sorted(actual_classes)
|
|
46
|
+
|
|
47
|
+
self.mapping_ = {c: i for i, c in enumerate(self.classes_)}
|
|
48
|
+
self.fitted = True
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def transform(self, y):
|
|
52
|
+
if not self.fitted:
|
|
53
|
+
raise RuntimeError("call fit() before transform()")
|
|
54
|
+
return pd.Series(y).map(self.mapping_)
|
|
55
|
+
|
|
56
|
+
def fit_transform(self, y):
|
|
57
|
+
return self.fit(y).transform(y)
|
|
58
|
+
|
|
59
|
+
def inverse_transform(self, y_encoded):
|
|
60
|
+
if not self.fitted:
|
|
61
|
+
raise RuntimeError("call fit() before inverse_transform()")
|
|
62
|
+
reverse = {v: k for k, v in self.mapping_.items()}
|
|
63
|
+
return pd.Series(y_encoded).map(reverse)
|
|
64
|
+
|
|
65
|
+
def __repr__(self):
|
|
66
|
+
return f"SDTargetEncoder(classes={self.classes_})"
|