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
36 changes: 36 additions & 0 deletions pytorch_forecasting/adapters/scaler_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,39 @@ def fit_transform_sequence(
col = sub.fit_transform(col, X) if sub.fit_per_sequence else col
columns.append(col.unsqueeze(-1))
return torch.cat(columns, dim=-1)

def transform_sequence(
self, data: ArrayLike, X: pd.DataFrame = None
) -> torch.Tensor:
"""Transform with per-sequence sub-normalizers; leave the rest untouched.

Counterpart to :meth:`fit_transform_sequence` for the decoder window,
which must reuse the state fitted on the encoder window rather than
refit. Non-per-sequence normalizers already applied their global state
during preprocessing, so their columns pass through unchanged.

Parameters
----------
data : tensor, ndarray, or Series
Shape ``(pred_length,)`` or ``(pred_length, n_targets)``.

Returns
-------
torch.Tensor
Same shape as input.
"""
if not self.is_multi:
return (
self.transform(data, X) if self.fit_per_sequence else _to_tensor(data)
)

t = _to_tensor(data)
if t.ndim == 1:
t = t.unsqueeze(-1)

columns = []
for idx, sub in enumerate(self._sub_adapters):
col = t[:, idx]
col = sub.transform(col, X) if sub.fit_per_sequence else col
columns.append(col.unsqueeze(-1))
return torch.cat(columns, dim=-1)
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,10 @@ def __getitem__(self, idx):

y = data["target"][decoder_indices]

# reuse the fit from the encoder window, do not refit on y
if normalizer is not None and normalizer.fit_per_sequence:
y = normalizer.transform_sequence(y)

if y.shape[-1] > 1:
y = [y[:, i] for i in range(y.shape[-1])]
else:
Expand Down
30 changes: 30 additions & 0 deletions tests/test_data/test_data_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,3 +726,33 @@ def test_group_normalizer_uses_groups():
mean1 = target1["target"].mean().abs()
assert mean0 < 1.0, "Group 0 target should be normalized near 0"
assert mean1 < 1.0, "Group 1 target should be normalized near 0"


def test_encoder_normalizer_normalizes_y():
"""`y` must be normalized by the encoder-fitted `EncoderNormalizer`."""
n = 200
df = pd.DataFrame(
{
"group": np.repeat([0, 1], n),
"time": np.tile(pd.date_range("2020-01-01", periods=n), 2),
"target": np.tile(np.arange(n, dtype=float) * 10.0 + 100.0, 2),
}
)
ts = TimeSeries(data=df, time="time", target="target", group=["group"])
dm = EncoderDecoderTimeSeriesDataModule(
time_series_dataset=ts,
max_encoder_length=12,
max_prediction_length=6,
batch_size=1,
target_normalizer=EncoderNormalizer(),
)
dm.setup("fit")

x, y = dm.train_dataset[0]
target_past = x["target_past"].squeeze(-1)

assert y.abs().max() < 10.0, "y is still on the raw target scale"

# series is linear, so y carries on from the encoder by one constant step
step = target_past[-1] - target_past[-2]
assert torch.allclose(y[0], target_past[-1] + step, atol=1e-4)
Loading