Skip to content

Commit b5f7fd4

Browse files
authored
feat(ordinal): add index_start to OrdinalEncoder for zero-indexed labels (#291) (#479)
1 parent 8b79173 commit b5f7fd4

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

category_encoders/ordinal.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ class OrdinalEncoder( util.UnsupervisedTransformerMixin,util.BaseEncoder):
4848
options are 'error', 'return_nan', and 'value, default to 'value',
4949
which treat nan as a category at fit time,
5050
or -2 at transform time if nan is not a category during fit.
51+
index_start: int
52+
integer at which to start labelling the categories. Defaults to 1.
53+
Set to 0 for zero-indexed labels, which can be convenient when feeding
54+
the encoded values into models that expect zero-indexed inputs such as
55+
embedding layers.
5156
5257
Example
5358
-------
@@ -107,6 +112,7 @@ def __init__(
107112
return_df: bool = True,
108113
handle_unknown: str = 'value',
109114
handle_missing: str = 'value',
115+
index_start: int = 1,
110116
):
111117
super().__init__(
112118
verbose=verbose,
@@ -120,6 +126,7 @@ def __init__(
120126
if self.mapping_supplied:
121127
mapping = self._validate_supplied_mapping(mapping)
122128
self.mapping = mapping
129+
self.index_start = index_start
123130

124131
@property
125132
def category_mapping(self) -> list[dict[str, str | dict | pd.Series]] | None:
@@ -136,6 +143,7 @@ def _fit(self, X: pd.DataFrame, y: pd.Series | None = None, **kwargs) -> None:
136143
cols=self.cols,
137144
handle_unknown=self.handle_unknown,
138145
handle_missing=self.handle_missing,
146+
index_start=self.index_start,
139147
)
140148
self.mapping = categories
141149

@@ -146,6 +154,7 @@ def _transform(self, X: pd.DataFrame) -> pd.DataFrame:
146154
cols=self.cols,
147155
handle_unknown=self.handle_unknown,
148156
handle_missing=self.handle_missing,
157+
index_start=self.index_start,
149158
)
150159
return X
151160

@@ -217,6 +226,7 @@ def ordinal_encoding(
217226
cols: list[str] = None,
218227
handle_unknown: str = 'value',
219228
handle_missing: str = 'value',
229+
index_start: int = 1,
220230
) -> tuple[pd.DataFrame, list[dict]]:
221231
"""Ordinal encoding uses a single column of integers to represent the classes.
222232
@@ -286,7 +296,10 @@ def ordinal_encoding(
286296

287297
index = pd.Series(categories).fillna(nan_identity).unique()
288298

289-
data = pd.Series(index=index, data=range(1, len(index) + 1))
299+
data = pd.Series(
300+
index=index,
301+
data=range(index_start, len(index) + index_start),
302+
)
290303

291304
if handle_missing == 'value' and ~data.index.isna().any():
292305
data.loc[nan_identity] = -2

tests/test_ordinal.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,25 @@ def test_no_gaps(self):
244244
expected_mapping_return_nan, enc_return_nan.mapping[0]['mapping']
245245
)
246246

247+
def test_default_index_start_is_one(self):
248+
"""Test that the default ordinal labels start at 1 (issue #291)."""
249+
train = pd.DataFrame({'city': ['chicago', 'st louis', 'detroit']})
250+
enc = encoders.OrdinalEncoder(cols=['city'])
251+
result = enc.fit_transform(train)['city'].tolist()
252+
self.assertEqual([1, 2, 3], result)
253+
254+
def test_index_start_zero(self):
255+
"""Test that index_start=0 produces zero-indexed ordinal labels (issue #291)."""
256+
train = pd.DataFrame({'city': ['chicago', 'st louis', 'detroit']})
257+
enc = encoders.OrdinalEncoder(cols=['city'], index_start=0)
258+
result = enc.fit_transform(train)['city'].tolist()
259+
self.assertEqual([0, 1, 2], result)
260+
261+
expected_mapping = pd.Series(
262+
[0, 1, 2, -2], index=['chicago', 'st louis', 'detroit', np.nan]
263+
)
264+
pd.testing.assert_series_equal(expected_mapping, enc.mapping[0]['mapping'])
265+
247266
def test_nan_and_none_is_encoded_the_same(self):
248267
"""Test that NaN and None are encoded the same."""
249268
train = pd.DataFrame({'city': [np.nan, None]})

0 commit comments

Comments
 (0)