Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions pretab/transformers/temporal/cyclic.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,21 @@ def transform(self, X):

def _output_sizes(self) -> list[int]:
return [2] * self.n_features_in_

def get_feature_names_out(self, input_features=None):
"""Return output names in the component-major order ``transform`` produces.

``transform`` returns ``hstack([sin, cos])``, each an
``(n_rows, n_features)`` block, so the columns run
``sin_f0, sin_f1, cos_f0, cos_f1``. The inherited feature-major default
would label them the other way round. The ``cyclic{j}`` suffix scheme is
unchanged, so single-feature output is byte-identical; ``j`` is ``0`` for
the sine component and ``1`` for the cosine.
"""
check_is_fitted(self, "n_features_in_")
if input_features is None:
input_features = [f"x{i}" for i in range(self.n_features_in_)]
return np.asarray(
[f"{feature}_cyclic{j}" for j in range(2) for feature in input_features],
dtype=object,
)
20 changes: 20 additions & 0 deletions pretab/transformers/temporal/lag.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,23 @@ def transform(self, X):

def _output_sizes(self) -> list[int]:
return [self.n_lags] * self.n_features_in_

def get_feature_names_out(self, input_features=None):
"""Return output names in the lag-major order ``transform`` produces.

``transform`` hstacks one ``(n_rows, n_features)`` block per lag, so the
columns run ``lag1_f0, lag1_f1, ..., lag2_f0, ...``. The inherited
feature-major default would label them the other way round, mislabelling
every column but the first and last for multi-feature input.
"""
check_is_fitted(self, "n_features_in_")
if input_features is None:
input_features = [f"x{i}" for i in range(self.n_features_in_)]
return np.asarray(
[
f"{feature}_lag{lag}"
for lag in range(self.n_lags)
for feature in input_features
],
dtype=object,
)
21 changes: 21 additions & 0 deletions pretab/transformers/temporal/rolling_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,24 @@ def transform(self, X):

def _output_sizes(self) -> list[int]:
return [len(self.stats)] * self.n_features_in_

def get_feature_names_out(self, input_features=None):
"""Return output names in the stat-major order ``transform`` produces.

``transform`` hstacks one ``(n_rows, n_features)`` block per statistic,
so the columns run ``mean_f0, mean_f1, ..., std_f0, ...``. The inherited
feature-major default would label them the other way round. The
``roll{j}`` suffix scheme is unchanged, so single-feature output is
byte-identical; ``j`` indexes ``self.stats``.
"""
check_is_fitted(self, "n_features_in_")
if input_features is None:
input_features = [f"x{i}" for i in range(self.n_features_in_)]
return np.asarray(
[
f"{feature}_roll{j}"
for j in range(len(self.stats))
for feature in input_features
],
dtype=object,
)
70 changes: 70 additions & 0 deletions tests/test_temporal.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,73 @@ def test_cyclic_feature_names():
np.testing.assert_array_equal(
transformer.get_feature_names_out(["hour"]), ["hour_cyclic0", "hour_cyclic1"]
)


# --------------------------------------------------------------------------- #
# Output names must line up with the columns ``transform`` actually produces.
#
# All three transformers hstack one (n_rows, n_features) block per lag / stat /
# component, so the columns are block-major. The inherited default in
# ``BasePreTabTransformer`` emits feature-major names, which mislabelled every
# column but the first and last whenever there was more than one input feature.
# --------------------------------------------------------------------------- #
@pytest.fixture
def two_features():
# B is always 100x A, so each column is identifiable from its value alone.
return np.column_stack([np.arange(8.0), np.arange(8.0) * 100])


def test_lag_names_match_column_order(two_features):
transformer = LagFeatureTransformer(n_lags=2).fit(two_features)

names = list(transformer.get_feature_names_out(["A", "B"]))
assert names == ["A_lag0", "B_lag0", "A_lag1", "B_lag1"]

row = transformer.transform(two_features)[0]
# Column 1 holds B's lag-1 value (100), so it must be named for B.
assert row[1] == 100.0
assert names[1].startswith("B")


def test_rolling_names_match_column_order(two_features):
transformer = RollingStatsTransformer(window_size=3, stats=("mean", "max")).fit(two_features)

names = list(transformer.get_feature_names_out(["A", "B"]))
assert names == ["A_roll0", "B_roll0", "A_roll1", "B_roll1"]

row = transformer.transform(two_features)[0]
assert row.tolist() == [1.0, 100.0, 2.0, 200.0]


def test_cyclic_names_match_column_order():
X = np.column_stack([np.arange(8.0), np.arange(8.0)])
transformer = CyclicalTimeTransformer(period=8).fit(X)

names = list(transformer.get_feature_names_out(["A", "B"]))
assert names == ["A_cyclic0", "B_cyclic0", "A_cyclic1", "B_cyclic1"]

out = transformer.transform(X)
np.testing.assert_allclose(out[:, :2], np.sin(2 * np.pi * X / 8))
np.testing.assert_allclose(out[:, 2:], np.cos(2 * np.pi * X / 8))


@pytest.mark.parametrize(
("transformer", "expected"),
[
(LagFeatureTransformer(n_lags=2), ["x0_lag0", "x0_lag1"]),
(RollingStatsTransformer(window_size=3, stats=("mean",)), ["x0_roll0"]),
(CyclicalTimeTransformer(period=8), ["x0_cyclic0", "x0_cyclic1"]),
],
)
def test_default_names_are_generated_for_single_feature(transformer, expected):
X = np.arange(8.0).reshape(-1, 1)
assert list(transformer.fit(X).get_feature_names_out()) == expected


def test_names_count_matches_transform_width(two_features):
for transformer in (
LagFeatureTransformer(n_lags=3),
RollingStatsTransformer(window_size=3, stats=("mean", "std", "min")),
):
transformer.fit(two_features)
assert len(transformer.get_feature_names_out()) == transformer.transform(two_features).shape[1]
Loading