Skip to content

Commit d886018

Browse files
rcap107glemaitre
andauthored
MAINT refactoring get_feature_names_out to define it in encoders (#1634)
Co-authored-by: Guillaume Lemaitre <guillaume@probabl.ai>
1 parent 4caf8b4 commit d886018

6 files changed

Lines changed: 132 additions & 57 deletions

File tree

skrub/_apply_to_cols.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -123,30 +123,6 @@ def __init_subclass__(subclass, **kwargs):
123123
wrapped = _wrap_add_check_single_column(getattr(subclass, method))
124124
setattr(subclass, method, wrapped)
125125

126-
def get_feature_names_out(self, input_features=None):
127-
"""Return a list of features generated by the transformer.
128-
129-
Each feature has format ``{input_name}_{n_component}`` where ``input_name``
130-
is the name of the input column, or a default name for the encoder, and
131-
``n_component`` is the idx of the specific feature.
132-
133-
Parameters
134-
----------
135-
input_features : None
136-
The input features. Ignored, only here for compatibility.
137-
138-
Returns
139-
-------
140-
list of str
141-
The list of feature names.
142-
"""
143-
check_is_fitted(self, "n_components_")
144-
num_digits = len(str(self.n_components_ - 1))
145-
return [
146-
f"{self.input_name_}_{str(i).zfill(num_digits)}"
147-
for i in range(self.n_components_)
148-
]
149-
150126

151127
def _wrap_add_check_single_column(f):
152128
# as we have only a few predefined functions to handle, using their exact

skrub/_datetime_encoder.py

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,42 @@ def get_feature_names_out(self, input_features=None):
511511
return self.all_outputs_
512512

513513

514-
class _SplineEncoder(SingleColumnTransformer):
514+
class _BasePeriodicEncoder(SingleColumnTransformer):
515+
"""Base class for periodic encoders."""
516+
517+
def __init__(self, period=24):
518+
self.period = period
519+
520+
def _post_process(self, X, new_features):
521+
result = sbd.make_dataframe_like(X, dict(zip(self.all_outputs_, new_features)))
522+
return sbd.copy_index(X, result)
523+
524+
def get_feature_names_out(self, input_features=None):
525+
"""Return a list of features generated by the transformer.
526+
527+
Each feature has format ``{input_name}_{n_component}`` where ``input_name``
528+
is the name of the input column, or a default name for the encoder, and
529+
``n_component`` is the idx of the specific feature.
530+
531+
Parameters
532+
----------
533+
input_features : None
534+
The input features. Ignored, only here for compatibility.
535+
536+
Returns
537+
-------
538+
list of str
539+
The list of feature names.
540+
"""
541+
check_is_fitted(self, "n_components_")
542+
num_digits = len(str(self.n_components_ - 1))
543+
return [
544+
f"{self.input_name_}_{str(i).zfill(num_digits)}"
545+
for i in range(self.n_components_)
546+
]
547+
548+
549+
class _SplineEncoder(_BasePeriodicEncoder):
515550
"""Generate univariate B-spline bases for features.
516551
517552
This encoder will apply the scikit-learn SplineTransformer to the given
@@ -533,10 +568,21 @@ class _SplineEncoder(SingleColumnTransformer):
533568
"""
534569

535570
def __init__(self, period=24, n_splines=None, degree=3):
536-
self.period = period
571+
super().__init__(period=period)
537572
self.n_splines = n_splines
538573
self.degree = degree
539574

575+
def _periodic_spline_transformer(self):
576+
n_splines = self.period if self.n_splines is None else self.n_splines
577+
n_knots = n_splines + 1 # periodic and include_bias is True
578+
return SplineTransformer(
579+
degree=self.degree,
580+
n_knots=n_knots,
581+
knots=np.linspace(0, self.period, n_knots).reshape(n_knots, 1),
582+
extrapolation="periodic",
583+
include_bias=True,
584+
)
585+
540586
def fit_transform(self, X, y=None):
541587
"""Fit the encoder and transform a column.
542588
@@ -560,7 +606,6 @@ def fit_transform(self, X, y=None):
560606

561607
X_out = self.transformer_.fit_transform(sbd.to_numpy(X).reshape(-1, 1))
562608

563-
self.is_fitted = True
564609
self.n_components_ = X_out.shape[1]
565610

566611
# TODO: this will raise an error if X is None, but it should not happen
@@ -570,7 +615,7 @@ def fit_transform(self, X, y=None):
570615
self.input_name_ = sbd.name(X) + "_spline"
571616
self.all_outputs_ = self.get_feature_names_out()
572617

573-
return self._post_process(X, X_out)
618+
return self._post_process(X, X_out.T)
574619

575620
def transform(self, X):
576621
"""Transform a column.
@@ -588,28 +633,10 @@ def transform(self, X):
588633

589634
X_out = self.transformer_.transform(sbd.to_numpy(X).reshape(-1, 1))
590635

591-
return self._post_process(X, X_out)
592-
593-
def _post_process(self, X, result):
594-
result = sbd.make_dataframe_like(X, dict(zip(self.all_outputs_, result.T)))
595-
result = sbd.copy_index(X, result)
636+
return self._post_process(X, X_out.T)
596637

597-
return result
598638

599-
def _periodic_spline_transformer(self):
600-
if self.n_splines is None:
601-
self.n_splines = self.period
602-
n_knots = self.n_splines + 1 # periodic and include_bias is True
603-
return SplineTransformer(
604-
degree=self.degree,
605-
n_knots=n_knots,
606-
knots=np.linspace(0, self.period, n_knots).reshape(n_knots, 1),
607-
extrapolation="periodic",
608-
include_bias=True,
609-
)
610-
611-
612-
class _CircularEncoder(SingleColumnTransformer):
639+
class _CircularEncoder(_BasePeriodicEncoder):
613640
"""Generate trigonometric features for the given feature.
614641
615642
This encoder will generate two features corresponding to the sine and cosine
@@ -621,9 +648,6 @@ class _CircularEncoder(SingleColumnTransformer):
621648
Period to be used as basis of the trigonometric function.
622649
"""
623650

624-
def __init__(self, period=24):
625-
self.period = period
626-
627651
def fit_transform(self, X, y=None):
628652
"""Fit the encoder and transform a column.
629653
@@ -679,9 +703,3 @@ def transform(self, X):
679703
]
680704

681705
return self._post_process(X, new_features)
682-
683-
def _post_process(self, X, result):
684-
result = sbd.make_dataframe_like(X, dict(zip(self.all_outputs_, result)))
685-
result = sbd.copy_index(X, result)
686-
687-
return result

skrub/_minhash_encoder.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,27 @@ def transform(self, X):
297297
result = sbd.make_dataframe_like(X, dict(zip(names, X_out.T)))
298298
result = sbd.copy_index(X, result)
299299
return result
300+
301+
def get_feature_names_out(self, input_features=None):
302+
"""Return a list of features generated by the transformer.
303+
304+
Each feature has format ``{input_name}_{n_component}`` where ``input_name``
305+
is the name of the input column, or a default name for the encoder, and
306+
``n_component`` is the idx of the specific feature.
307+
308+
Parameters
309+
----------
310+
input_features : None
311+
The input features. Ignored, only here for compatibility.
312+
313+
Returns
314+
-------
315+
list of str
316+
The list of feature names.
317+
"""
318+
check_is_fitted(self, "n_components_")
319+
num_digits = len(str(self.n_components_ - 1))
320+
return [
321+
f"{self.input_name_}_{str(i).zfill(num_digits)}"
322+
for i in range(self.n_components_)
323+
]

skrub/_string_encoder.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
TfidfVectorizer,
88
)
99
from sklearn.pipeline import Pipeline
10+
from sklearn.utils.validation import check_is_fitted
1011

1112
from . import _dataframe as sbd
1213
from ._apply_to_cols import SingleColumnTransformer
@@ -263,3 +264,27 @@ def _post_process(self, X, result):
263264
result = sbd.copy_index(X, result)
264265

265266
return result
267+
268+
def get_feature_names_out(self, input_features=None):
269+
"""Return a list of features generated by the transformer.
270+
271+
Each feature has format ``{input_name}_{n_component}`` where ``input_name``
272+
is the name of the input column, or a default name for the encoder, and
273+
``n_component`` is the idx of the specific feature.
274+
275+
Parameters
276+
----------
277+
input_features : None
278+
The input features. Ignored, only here for compatibility.
279+
280+
Returns
281+
-------
282+
list of str
283+
The list of feature names.
284+
"""
285+
check_is_fitted(self, "n_components_")
286+
num_digits = len(str(self.n_components_ - 1))
287+
return [
288+
f"{self.input_name_}_{str(i).zfill(num_digits)}"
289+
for i in range(self.n_components_)
290+
]

skrub/_text_encoder.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,3 +413,27 @@ def __getstate__(self):
413413
del state[prop]
414414

415415
return state
416+
417+
def get_feature_names_out(self, input_features=None):
418+
"""Return a list of features generated by the transformer.
419+
420+
Each feature has format ``{input_name}_{n_component}`` where ``input_name``
421+
is the name of the input column, or a default name for the encoder, and
422+
``n_component`` is the idx of the specific feature.
423+
424+
Parameters
425+
----------
426+
input_features : None
427+
The input features. Ignored, only here for compatibility.
428+
429+
Returns
430+
-------
431+
list of str
432+
The list of feature names.
433+
"""
434+
check_is_fitted(self, "n_components_")
435+
num_digits = len(str(self.n_components_ - 1))
436+
return [
437+
f"{self.input_name_}_{str(i).zfill(num_digits)}"
438+
for i in range(self.n_components_)
439+
]

skrub/tests/test_datetime_encoder.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,11 @@ def test_error_checking_periodic_encoder(a_datetime_col):
369369
def test_error_dispatch(func):
370370
with pytest.raises(TypeError, match="Expecting a Pandas or Polars Series"):
371371
func(np.array([1]))
372+
373+
374+
def test_n_splines_default_value(df_module):
375+
"""Check that when `n_splines is None`, it defaults to the `period` value."""
376+
period = 15
377+
enc = _SplineEncoder(period=period)
378+
result = enc.fit_transform(df_module.make_column("when", [20, 20, 20]))
379+
assert sbd.column_names(result) == [f"when_spline_{i:02d}" for i in range(period)]

0 commit comments

Comments
 (0)