Skip to content

[BUG] Normalize decoder target when using EncoderNormalizer in EncoderDecoderDataModule - #2361

Open
Zuhef wants to merge 1 commit into
sktime:mainfrom
Zuhef:fix/beta-issue-2360-normalize-decoder-target
Open

[BUG] Normalize decoder target when using EncoderNormalizer in EncoderDecoderDataModule#2361
Zuhef wants to merge 1 commit into
sktime:mainfrom
Zuhef:fix/beta-issue-2360-normalize-decoder-target

Conversation

@Zuhef

@Zuhef Zuhef commented Jul 29, 2026

Copy link
Copy Markdown

LLM generated content, by claude-opus-5

(Disclosed per the note in this repository's pull request template. The change was
reviewed, executed and verified locally by a human-directed workflow; all test output
quoted below was produced on this branch.)

Reference Issues/PRs

Fixes #2360.

Touches the same code path as #2302, which introduced the per-sequence scaler handling.

What does this implement/fix? Explain your changes.

With an EncoderNormalizer, EncoderDecoderTimeSeriesDataModule returned a decoder
target y that was never normalized, so y sat in a different space than
x["target_past"].

The reason it is skipped entirely rather than merely inconsistent: _normalize_target
applies the global transform only when the normalizer is not per-sequence.

if not self._target_normalizer.fit_per_sequence:
    target = self._target_normalizer.transform(target, X)

So for an EncoderNormalizer the cached data["target"] stays raw for the whole
series, and normalization is deferred to __getitem__. There, target_past is
fitted-and-transformed on the encoder window, but y was taken straight from that
still-raw cache. Net effect: the encoder input is standardized while the training
target is not.

Reproduction on main (deterministic series with a strong trend, encoder length 20,
prediction length 5):

target_past: mean=-0.0000 std=1.0000 min=-1.598 max=+1.556
y[0]       : mean=+941.7448 std=32.8716 min=+903.716 max=+983.395

The fix adds ScalerAdapter.transform_sequence, a companion to the existing
fit_transform_sequence, and applies it to y:

  • it transforms using the parameters fit_transform_sequence just fitted on the
    encoder window, rather than re-fitting on the decoder window. Re-fitting would leak
    future values into the scaling and would put y in a different space than
    target_past — arguably worse than the original bug;
  • it mirrors fit_transform_sequence column-by-column for MultiNormalizer, so
    non-per-sequence sub-normalizers are passed through untouched and never
    double-normalized.

After the fix the same window gives y[0]: mean=+2.1108 std=0.2769, which is the
arithmetically expected result: the encoder window spans 500→880 (mean ≈ 691,
std ≈ 118.7) and the decoder window sits at ≈ 941, so
(941 − 691) / 118.7 ≈ 2.11. std stays at 0.277 rather than collapsing to 1.0,
confirming the encoder parameters were reused instead of re-fitted.

What should a reviewer concentrate their feedback on?

  • Transform vs re-fit. I took "normalize y" to mean "apply the encoder
    window's fitted parameters", not "fit on the decoder window". Please confirm that
    matches your intent, since it is the one genuinely semantic decision here.
  • Placement of the new method. transform_sequence lives on ScalerAdapter
    next to fit_transform_sequence. Happy to inline it in the data module instead if
    you would rather not widen the adapter's public surface.
  • Pre-existing gap I deliberately did not touch. For a MultiNormalizer that
    mixes per-sequence and global normalizers, ScalerAdapter.fit_per_sequence is
    any(...), so _normalize_target skips the global transform for every column —
    meaning the non-per-sequence targets are currently never normalized at all. That is
    independent of this issue, so I left it alone and only kept the decoder side
    symmetric with the encoder side. Happy to open a separate issue if that is a real
    bug rather than intended.

Did you add any tests for the change?

Yes, three in tests/test_data/test_data_module.py:

Test Purpose
test_encoder_normalizer_normalizes_decoder_target y equals the raw decoder target transformed with the encoder window's fitted parameters. Fails on main.
test_encoder_normalizer_normalizes_decoder_target_multivariate MultiNormalizer: per-sequence column transformed, non-per-sequence column left untouched. Fails on main.
test_encoder_normalizer_decoder_target_does_not_refit Guards against a re-fit implementation. Passes on main too — it constrains the fix rather than reproducing the bug.

Expectations are derived from the data module's own target_original cache for the
same window, so they do not depend on two data modules happening to select the same
series.

Verification run locally on Windows / Python 3.13 / torch 2.13.0+cpu:

# full suite, with the fix
python -m pytest tests -q
429 passed, 15 skipped in 133.22s
# (15 skips are environmental: cpflows/matplotlib absent, known Windows-only skips)

# new tests with the source change reverted, tests kept
python -m pytest tests/test_data/test_data_module.py -k decoder_target
2 failed, 1 passed
  E  AssertionError: y is still raw - decoder target was not normalized by EncoderNormalizer
  E  AssertionError: per-sequence decoder target not transformed with encoder-window parameters

# repo pre-commit hooks, as the code-quality job runs them
pre-commit run --files <changed files>
  trim trailing whitespace...Passed   fix end of files...Passed
  check python ast...Passed           ruff...Passed        ruff-format...Passed

The new tests were run three times consecutively to check they are not order- or
seed-dependent.

Any other comments?

The diff is 181 added lines and no deletions or modifications to existing logic. I did
not add a CHANGELOG.md entry, following #2302 / #2300 / #2256 which leave it to
maintainers at release time — glad to add one if you prefer.

PR checklist

  • The PR title starts with either [ENH], [MNT], [DOC], or [BUG]. [BUG] - bugfix, [MNT] - CI, test framework, [ENH] - adding or improving code, [DOC] - writing or improving documentation or docstrings.
  • Added/modified tests
  • Used pre-commit hooks when committing to ensure that code is compliant with hooks. Install hooks with pre-commit install.
    To run hooks independent of commit, execute pre-commit run --all-files

`EncoderDecoderTimeSeriesDataModule.__getitem__` normalized `target_past`
through the per-sequence normalizer but returned `y` straight from the
still-raw cached target. `_normalize_target` deliberately skips global
normalization when the normalizer is per-sequence, so with an
`EncoderNormalizer` the decoder target was never normalized at all and sat
in a different space than the encoder input.

Add `ScalerAdapter.transform_sequence`, a companion to
`fit_transform_sequence` that transforms only per-sequence sub-normalizers
using their already-fitted state, and apply it to `y`. Transforming rather
than re-fitting keeps `y` on the encoder window's parameters and avoids
leaking future values into the scaling.

Fixes sktime#2360
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.85714% with 1 line in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@e7c7965). Learn more about missing BASE report.

Files with missing lines Patch % Lines
pytorch_forecasting/adapters/scaler_adapters.py 91.66% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2361   +/-   ##
=======================================
  Coverage        ?   87.34%           
=======================================
  Files           ?      171           
  Lines           ?    10094           
  Branches        ?        0           
=======================================
  Hits            ?     8817           
  Misses          ?     1277           
  Partials        ?        0           
Flag Coverage Δ
cpu 87.34% <92.85%> (?)
pytest 87.34% <92.85%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] y not being normalized when using EncoderNormalizer in EncoderDecoderDataModule

1 participant