Thanks to visit codestin.com
Credit goes to Github.com

Skip to content

Conversation

@EmanAbdelhaleem
Copy link
Contributor

Reference Issues/PRs

Fixes #8787

What does this implement/fix? Explain your changes.

This PR addresses a test failure affecting several estimators that require contiguous exogenous data (X) when making predictions at non-contiguous forecasting horizons (e.g., fh=[2, 5]).

Changes implemented:

  1. Added new capability tag capability:non_contiguous_X:
    • Default value: True (most forecasters can handle non-contiguous X)
    • Set to False for forecasters that require contiguous X
    • Registered in sktime/registry/_tags.py with full documentation
  2. Added _is_contiguous_fh() helper function in sktime/forecasting/base/_fh.py:
    • Checks whether a forecasting horizon contains contiguous time points
    • Handles both relative integer horizons and absolute datetime/period horizons
  3. Updated test_predict_time_index_with_X in sktime/forecasting/tests/test_all_forecasters.py:
    • Skips non-contiguous X test cases for forecasters with capability:non_contiguous_X=False
    • Allows all forecasters to be tested with contiguous X
  4. Removed "tests:skip_by_name": ["test_predict_time_index_with_X"] tag, and Set capability:non_contiguous_X=False for affected forecasters:
    • ARDL (sktime/forecasting/ardl.py)
    • DynamicFactor (sktime/forecasting/dynamic_factor.py)
    • SkforecastRecursive (sktime/forecasting/compose/_skforecast_reduce.py)
    • StatsForecastAutoARIMA (sktime/forecasting/statsforecast.py)
    • StatsModelsARIMA (sktime/forecasting/arima/_statsmodels.py)
    • UnobservedComponents (sktime/forecasting/structural.py)

Does your contribution introduce a new dependency? If yes, which one?

No

Did you add any tests for the change?

The existing test_predict_time_index_with_X test now properly handles estimators with this limitation by skipping non-contiguous test cases.

PR checklist

  • I've added myself to the list of contributors
  • The PR title starts with either [ENH], [MNT], [DOC], or [BUG].

@yarnabrina
Copy link
Member

Thanks for your contribution, much appreciated.

Can you please elaborate how are you identifying which forecasters support non-contiguous X and what does not? For example, if you check #8740, StatsForecastAutoARIMA does not actually fail the predict call even if passed horizon (fh) and exogenous data (X) shapes don't align, and all it checks (I think, did not verify, may be wrong) is the shape and not actual time identifiers.

So, how did you determine what all forecasters face this issue? Is it from just test failures, or did you verify by checking one by one, or some other way?

@EmanAbdelhaleem
Copy link
Contributor Author

They were failing the test_predict_time_index_with_X test when fh= [2,5] , and were listed in the issue I am solving. Check #8787
I have reproduced the bug using the same [2,5] example and tried a different contiguous example to make sure the estimator actually works.

For the example you have mentioned, I believe it doesn't fail cuz from 1:3 (what the estimator managed to predict) were contiguous already, no gaps, it couldn't predict 4 and 5 cuz it doesn't have X for them. I think this might mean theat the estimator behavior when passed fh, X that doesn't align in shape is taking the min, and won't fail as long as what it is trying to predict is contiguous.

@fkiraly fkiraly added module:forecasting forecasting module: forecasting, incl probabilistic and hierarchical forecasting enhancement Adding new functionality labels Nov 22, 2025
# CI and test flags
# -----------------
"tests:skip_by_name": ["test_predict_time_index_with_X"],
# known failure in case of non-contiguous X, see issue #8787
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a minor nitpick regarding documentation - remove the "CI and test flags" section, since this is now a capability tag.

``fh``, the forecaster may receive exogenous data ``X`` that corresponds
only to the specific time points in ``fh``.
If the forecasting horizon is non-contiguous (e.g., ``fh=[2, 5]``),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not true. The full X will be passed? Can you kindly check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have checked and it's true that only X values that correspond to the specific time points in fh will be passed. This is due to passing fh instead of test_size to the temporal_train_test_split in the test_predict_time_index_with_X function.

We can find in the ForecastingHorizonSplitter at sktime\split\fh.py a note saying:

Users should note that, for non-contiguous forecasting horizons,
the union of training and test sets will not cover the entire time series.

* Require contiguous data for their recursive prediction algorithms
If a forecaster has this tag set to ``False`` and receives non-contiguous
exogenous data, it will raise an error during prediction.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you maybe also add a paragraph that references ForecastX, which can be used to make forecasts for the missing X-indices in case the tag is False?

return None


def _is_contiguous_fh(fh):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add it to ForecastingHorizon as a method, and call it _is_contiguous

if not isinstance(fh, ForecastingHorizon):
fh = ForecastingHorizon(fh)

try:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should avoid using try/except in checks, since it masks actual bugs or failures. Instead, check for the exact condition that you want to check.

Copy link
Collaborator

@fkiraly fkiraly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great contribution, this is really useful!

I have left some comments above.

@EmanAbdelhaleem
Copy link
Contributor Author

Thanks for your contribution, much appreciated.

Can you please elaborate how are you identifying which forecasters support non-contiguous X and what does not? For example, if you check #8740, StatsForecastAutoARIMA does not actually fail the predict call even if passed horizon (fh) and exogenous data (X) shapes don't align, and all it checks (I think, did not verify, may be wrong) is the shape and not actual time identifiers.

So, how did you determine what all forecasters face this issue? Is it from just test failures, or did you verify by checking one by one, or some other way?

@yarnabrina @fkiraly Regarding passing horizon (fh) and exogenous data (X) shapes that don't align, I have investigated and actually it doesn't take the min as I said, to be more precise:

  • If we are using the sktime wrapper, it would actually fail, as with the wrapper implementation it requires fh and X to align. In your code you haven't used the sktime wrapper, instead you used the model directly from statsforecast.models , that's why it worked.

  • This behaviour is not the same for all models btw, for example: 'ARDL' model actually doesn't require 'fh' and 'X' to align, whether you are using the sktime wrapper or not


note: the syntax I am using is arbitrary
Another interesting point that I have noticed:
fitted_model.predict(5) has different bahaviour whether you are using sktime wrapper or not

  1. Without the sktime wrapper, you need to pass 'start' , 'end' parameters, it would be something like this:
y_pred = fitted_model.predict(
    start = start,
    end=end,
    exog_oos=X_oos
)
  • the default value for start is the first element in the training data, and for end it's the last element in the training data, so even if we don't pass a start, given that we have X, we will manage to predict with fh=5, cuz there is no gap (in the inner calculations), for simplicity:
    fh(5) = fh([1,2,3,4,5])
  1. In the other hand, with the sktime wrapper, when you say fitted_model.predict(5)
    the default values for start and end would be min , max of the passed hf respectively
    for the case: fh = 5, both start and end would equal 5, That's why it's gonna fail to predict, as now we have gaps, for simplicity:
    fh(5) = fh([5])

@EmanAbdelhaleem
Copy link
Contributor Author

EmanAbdelhaleem commented Nov 23, 2025

Just now I have tested fh = ForecastingHorizon([3,4]) with the sktime wrapper, and it worked! although if we are gonna follow the previous rules I have stated above, it's gonna fail cuz we have gaps 1,2

but what is way more weird is that I have tested fh = ForecastingHorizon([3,5]) and it also worked perfectly fine :)
I no longer make sense of what's happening, if fh=[3,5] can actually work, then I am afraid the problem wasn't actually about non-contiguous fh from the beginning, AAA, not sure any more.

@fkiraly could you please check if I am missing something? I have also checked with the exact fh = np.array([2, 5], dtype="int64") value we have in our 'TEST_OOS_FHS' that made the estimators fail in #8787 and it still worked!
here is the code I tested the above examples with

from sktime.datasets import load_longley    
from sktime.forecasting.ardl import ARDL    
from sktime.forecasting.base import ForecastingHorizon    
from sktime.split import temporal_train_test_split  
import numpy as np
  
# Load data with exogenous variables    
y, X = load_longley()    
    
# Split into train and test using temporal_train_test_split  
y_train, y_test, X_train, X_test = temporal_train_test_split(y, X, test_size=5)  

# Select specific exogenous variables    
X_train = X_train[["GNPDEFL", "GNP"]]    
X_test = X_test[["GNPDEFL", "GNP"]]    
# Create ARDL model with lags and order for exogenous variables    
ardl = ARDL(lags=2, order={"GNPDEFL": 1, "GNP": 2}, trend="c")    
    
# Fit the model    
ardl.fit(y=y_train, X=X_train)    
    
fh = ForecastingHorizon(np.array([2, 5], dtype="int64"))    
y_pred = ardl.predict(fh=fh, X=X_test)    
    
print(y_pred)

@fkiraly
Copy link
Collaborator

fkiraly commented Nov 23, 2025

@EmanAbdelhaleem, sorry, can you be precise what you exactly tested with what? You keep saying "I have tested fh= sth sth etc", but in which context did you test it, and where, and for what?

@EmanAbdelhaleem
Copy link
Contributor Author

EmanAbdelhaleem commented Nov 23, 2025

@EmanAbdelhaleem, sorry, can you be precise what you exactly tested with what? You keep saying "I have tested fh= sth sth etc", but in which context did you test it, and where, and for what?

Just a normal case like the above code I provided, this is what I mean by I tested, I initialize a model, fit it, then try to predict with various fh values.

For what?
I have removed try,except from is_contiguous() function, and tried to cover every single case with if conditions
One of them was what if we have fh = one integer value, like fh=3, so I started checking with examples, when would it work and when it would not, I noticed a different bahviour with different error messages, so I investigated more.

@fkiraly
Copy link
Collaborator

fkiraly commented Nov 25, 2025

Sorry to ask again, I have difficulties following you.

Can you please be very precise under which conditions you tested what, and what is failing?

@EmanAbdelhaleem
Copy link
Contributor Author

EmanAbdelhaleem commented Nov 26, 2025

Sorry to ask again, I have difficulties following you.

Can you please be very precise under which conditions you tested what, and what is failing?

Setup:

  • Model: ARDL(lags=2, order={"GNPDEFL": 1, "GNP": 2}, trend="c")
  • Data: Longley dataset, split with last 5 observations as test set (X_test has 5 rows)
  • Training data: 11 observations (indices 0-10)
### Used Code:
from sktime.datasets import load_longley    
from sktime.forecasting.ardl import ARDL    
from sktime.forecasting.base import ForecastingHorizon    
from sktime.split import temporal_train_test_split  
import numpy as np
  
# Load data with exogenous variables    
y, X = load_longley()    
    
# Split into train and test using temporal_train_test_split  
y_train, y_test, X_train, X_test = temporal_train_test_split(y, X, test_size=5)  

# Select specific exogenous variables    
X_train = X_train[["GNPDEFL", "GNP"]]    
X_test = X_test[["GNPDEFL", "GNP"]]    
# Create ARDL model with lags and order for exogenous variables    
ardl = ARDL(lags=2, order={"GNPDEFL": 1, "GNP": 2}, trend="c")    
    
# Fit the model    
ardl.fit(y=y_train, X=X_train)    
    
fh = ForecastingHorizon(np.array([2, 5], dtype="int64"))    
y_pred = ardl.predict(fh=fh, X=X_test)    
    
print(y_pred)

Test Results:

by manually testing different fh values I got:

Testing single-element horizons:

  • ForecastingHorizon(1) → ✓ Works
  • ForecastingHorizon(2) → ✓ Works
  • ForecastingHorizon(3) → ✗ FAILS with IndexError: index -2 is out of bounds for axis 0 with size 1
  • ForecastingHorizon(4) → ✗ FAILS with IndexError: index -3 is out of bounds for axis 0 with size 1
  • ForecastingHorizon(5) → ✗ FAILS with IndexError: index -4 is out of bounds for axis 0 with size 1

Testing multi-element horizons:

  • ForecastingHorizon([1,2,3]) → ✓ Works
  • ForecastingHorizon([3,4]) → ✓ Works
  • ForecastingHorizon([3,5]) → ✓ Works
  • ForecastingHorizon([2,5]) → ✓ Works
  • ForecastingHorizon([4,5]) → ✗ FAILS with IndexError: index -3 is out of bounds for axis 0 with size 2
  • ForecastingHorizon([1,2,3,4,5]) → ✓ Works
  • ForecastingHorizon([1,4,5]) → ✓ Works
  • ForecastingHorizon([1,3,5]) → ✓ Works
  • ForecastingHorizon([3,4,5]) → ✓ Works

Initial Confusion:

I initially thought the issue was about non-contiguous horizons (e.g., [3] skipping 1 and 2). However, [3,5] works despite skipping 1, 2, and 4, Even [2,5] works, which was weird cuz that was the failed test case in #8787

Given that, the first hypothesis that the issue is about non-contiguous horizons is contradicted, hence, declined.

I've conducted an investigation into this behavior by examining the ARDL implementation in both sktime.forecasting.ardl and statsforecast.models. You can find some details in my following submitted issue: https://github.com/statsmodels/statsmodels/issues/9699#issue-3665426646

The key finding is that the contiguous nature of predictions is not determined by whether the forecasting horizon is contiguous or non-contiguous. Instead, the start and end parameter values exclusively control this behavior. Additionally, I identified differences in how start and end defaults are handled between statsforecast.models and the sktime wrapper, which may warrant further examination.

@EmanAbdelhaleem
Copy link
Contributor Author

After ruling out the non-contiguous horizon hypothesis, and knowing that the error I got from reproducing the exact failing test from #8787 was:

ValueError: exog_oos must have at least 5 observations to produce 5 forecasts based on the model specification.

I checked the X_test data being passed to the forecasters and discovered the problem: when using fh=[2, 5], X_test only contains 2 rows (for steps 2 and 5), but the forecasters need all 5 rows (steps 1 through 5).

Why This Happens:

The test uses:

y_train, _, X_train, X_test = temporal_train_test_split(y, X, fh=fh)

When fh is passed (instead of test_size), temporal_train_test_split uses ForecastingHorizonSplitter, which has this documented behavior in sktime/split/fh.py:

"Users should note that, for non-contiguous forecasting horizons, the union of training and test sets will not cover the entire time series."

So with fh=[2, 5], it only returns data for those specific indices, creating gaps. The forecasters need contiguous X data covering all steps from 1 to 5, but they're only receiving 2 rows.

Summary:

The test fails because it's passing incomplete X_test data to forecasters that require contiguous exogenous variables.


I used some LLM help to make the text clearer, but if anything is still unclear, please point out the exact part or tell me how you would test it, and I’ll follow your steps. I realize I may be using the wrong terms as it's my first PR, my apologies for that.

@fkiraly
Copy link
Collaborator

fkiraly commented Nov 28, 2025

I see, thanks.

So we are probably dealing with two problems:

  • the "non-contiguous" issue, which I think is valid as a contribution and a fix
  • additional bugs in estimators if the horizon is not contiguous, where probably the internal translation of fh to the actial indices is not properly implemented

I would do as follows:

  • add the non-contiguous tag (capability not there) at least for the estimator where some non-contiguous fh fail
  • open issues for estimators where you suspect there is an additional bug, e.g., some non-contiguous fh work and so do not

@EmanAbdelhaleem
Copy link
Contributor Author

EmanAbdelhaleem commented Nov 28, 2025

@fkiraly I have made some changes. Kindly, review them.
May I ask why removing capability?

@fkiraly
Copy link
Collaborator

fkiraly commented Dec 2, 2025

May I ask why removing capability?

Sorry, can you explain what you mean?

fkiraly
fkiraly previously approved these changes Dec 2, 2025
Copy link
Collaborator

@fkiraly fkiraly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great contribution!

@fkiraly fkiraly changed the title [BUG] Add capability:non_contiguous_X tag and skip non-contiguous X tests for affected estimators [ENH] Add capability:non_contiguous_X tag and skip non-contiguous X tests for affected estimators Dec 2, 2025
@fkiraly fkiraly changed the title [ENH] Add capability:non_contiguous_X tag and skip non-contiguous X tests for affected estimators [ENH] capability:non_contiguous_X tag and skip non-contiguous X tests for affected estimators Dec 2, 2025
@EmanAbdelhaleem
Copy link
Contributor Author

May I ask why removing capability?

Sorry, can you explain what you mean?

@fkiraly Never mind. I think I misunderstood things, I thought you meant to remove capability from the tag name when you said "add the non-contiguous tag (capability not there)".

I see that there was a trailing whitespace and you already added a commit to remove it. We can run the checks again, right?
Sorry for the messy errors. I have pip installed pre-commit after committing my changes. I also work on a windows environment, so I kept having pre-commit modifications for all the repo files due to LF/CRLF. That's why I had to run pre-commit for each file separately and maybe I missed the statsforcast.py file.

@fkiraly fkiraly merged commit ed4b1c7 into sktime:main Dec 20, 2025
82 of 84 checks passed
@EmanAbdelhaleem EmanAbdelhaleem deleted the fix/8787_non_contiguous_X branch December 21, 2025 23:52
@EmanAbdelhaleem
Copy link
Contributor Author

Wow, this is my first merged PR, very happy for it. Thank you so much for the review and guidance Dr. @fkiraly !

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

Labels

enhancement Adding new functionality module:forecasting forecasting module: forecasting, incl probabilistic and hierarchical forecasting

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] resolve many failure cases where estimators require contiguous exogenous data for forecast

3 participants