Repository navigation
fix(automl): treat a DatetimeIndex as the ts_forecast time column #1614
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Nefelibata (MeiSiristhebest)
wants to merge
28
commits into
microsoft:main
Choose a base branch
from
MeiSiristhebest:fix/ts-forecast-datetime-index-time-col
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,114
−13
Open
Changes from 5 commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
347d211
fix(automl): use a DatetimeIndex as the ts_forecast time column
MeiSiristhebest 800c05f
fix(automl): resolve index-name collisions and promote DatetimeIndex …
MeiSiristhebest c92fc51
fix(automl): preserve aligned indexes for DataFrame targets and fix r…
MeiSiristhebest 7d92b36
fix(automl): align targets by label before DatetimeIndex promotion an…
MeiSiristhebest 26654d3
style: apply black and fix ruff F841 in test
MeiSiristhebest 5c5a023
fix(automl): support positional Series targets with DatetimeIndex fea…
MeiSiristhebest 11fcf86
fix(automl): align datetime labels only when features also have Datet…
MeiSiristhebest fe9b6bc
Merge branch 'main' into fix/ts-forecast-datetime-index-time-col
thinkall 675fbe9
Merge branch 'main' into fix/ts-forecast-datetime-index-time-col
thinkall 8ba1be7
Merge branch 'main' into fix/ts-forecast-datetime-index-time-col
thinkall 0d53385
fix(automl): handle index-only training, length mismatch, and target …
MeiSiristhebest 456521d
style: apply black formatting to ts_data.py
MeiSiristhebest 9111191
Merge branch 'main' into fix/ts-forecast-datetime-index-time-col
thinkall 3d36d21
fix(automl): align time series inputs at boundary and preserve positi…
MeiSiristhebest 924e458
Merge branch 'main' into fix/ts-forecast-datetime-index-time-col
thinkall df472f1
Merge branch 'main' into fix/ts-forecast-datetime-index-time-col
thinkall 1964edc
fix(automl): preserve DataFrame target label alignment and disallow i…
MeiSiristhebest 1e3b1f2
test(automl): assert exact target value alignment in reordered target…
MeiSiristhebest d57af8d
fix(automl): validate explicit time_col on features before joining ta…
MeiSiristhebest 1771c8f
fix(automl): preserve NumPy array normalization while validating Data…
MeiSiristhebest 66ad9c4
fix(automl): preserve non-None DatetimeIndex names and ensure predict…
MeiSiristhebest e4944d4
docs(test): update test_ts_forecast_datetime_index module docstring t…
MeiSiristhebest 95fb4ce
fix(automl): restore caller input index on predictions for estimators…
MeiSiristhebest ce5d195
fix(automl): preserve forecast prediction alignment
MeiSiristhebest c85b11b
fix(ts_forecast): align in-sample predictions by timestamp
MeiSiristhebest 748174b
test(ts_forecast): name timestamp alignment index
MeiSiristhebest d5fe77c
test(ts-forecast): cover real ARIMA prediction ordering
MeiSiristhebest 73cb38b
fix(ts-forecast): normalize date string array predictions
MeiSiristhebest File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| """Regression tests for ts_forecast input where the timestamp is the DataFrame index. | ||
|
|
||
| Flaml expects the timestamp to live in a *column* (`time_col`). When a user passes the | ||
| idiomatic pandas shape instead -- timestamps as a `DatetimeIndex` -- `validate_data` | ||
| defaults `time_col` to `dataframe.columns[0]`, which is a value column (often the label | ||
| itself), so the real timestamps are never looked at. | ||
| """ | ||
|
|
||
| import numpy as np | ||
| import pandas as pd | ||
| import pytest | ||
|
|
||
| from flaml import AutoML | ||
|
|
||
|
|
||
| def _make_train_df(periods=120): | ||
| return pd.DataFrame( | ||
| {"y": np.sin(np.arange(periods) / 6) * 10 + 50}, | ||
| index=pd.date_range("2018-01-01", periods=periods, freq="MS"), | ||
| ) | ||
|
|
||
|
|
||
| def _fit(automl, **kwargs): | ||
| settings = { | ||
| "task": "ts_forecast", | ||
| "label": "y", | ||
| "period": 12, | ||
| "time_budget": 5, | ||
| "estimator_list": ["lgbm"], | ||
| "metric": "mape", | ||
| "log_file_name": False, | ||
| "verbose": 0, | ||
| } | ||
| settings.update(kwargs) | ||
| automl.fit(**settings) | ||
|
|
||
|
|
||
| def test_unnamed_datetime_index_becomes_the_time_column(): | ||
| automl = AutoML() | ||
| _fit(automl, dataframe=_make_train_df()) | ||
| assert automl._state.task.time_col == "ds" | ||
|
|
||
|
|
||
| def test_named_datetime_index_keeps_its_name(): | ||
| df = _make_train_df() | ||
| df.index.name = "month" | ||
| automl = AutoML() | ||
| _fit(automl, dataframe=df) | ||
| assert automl._state.task.time_col == "month" | ||
|
|
||
|
|
||
| def test_promoted_index_keeps_every_training_row(): | ||
| """Pre-promotion the label column was read as epoch nanoseconds, collapsing the 120 real | ||
| timestamps onto a handful of 1970-01-01 values and dropping the duplicates.""" | ||
| df = _make_train_df() | ||
| automl = AutoML() | ||
| _fit(automl, dataframe=df) | ||
| assert automl._state.data_size[0] == len(df) | ||
|
|
||
|
|
||
| def test_explicit_time_col_still_wins_over_the_index(): | ||
| df = _make_train_df() | ||
| df["ts"] = df.index | ||
| automl = AutoML() | ||
| _fit(automl, dataframe=df, time_col="ts") | ||
| assert automl._state.task.time_col == "ts" | ||
|
|
||
|
|
||
| def test_genuine_missing_timestamp_still_raises(): | ||
| df = pd.DataFrame({"note": ["not-a-date"] * 120, "y": np.arange(120, dtype=float)}) | ||
| automl = AutoML() | ||
| with pytest.raises(ValueError) as exc: | ||
| _fit(automl, dataframe=df) | ||
| assert "must contain timestamp values" in str(exc.value) | ||
|
|
||
|
|
||
| def test_timestamp_column_input_is_unchanged(): | ||
| df = _make_train_df().reset_index().rename(columns={"index": "ds"}) | ||
| automl = AutoML() | ||
| _fit(automl, dataframe=df, time_col="ds") | ||
| assert automl._state.task.time_col == "ds" | ||
| assert len(automl.predict(df[["ds"]].tail(12))) == 12 | ||
|
|
||
|
|
||
| def test_index_name_collision_resolves_to_unique_column(): | ||
| """When dataframe already has a column named 'ds' (or 'index') that is not datetime, | ||
| promoting an unnamed DatetimeIndex must not overwrite or conflict with the existing column.""" | ||
| df = _make_train_df() | ||
| df["ds"] = np.arange(len(df), dtype=float) # Collision candidate | ||
| df["ds_1"] = np.arange(len(df), dtype=float) # Double collision candidate | ||
| automl = AutoML() | ||
| _fit(automl, dataframe=df) | ||
| # Target column should be uniquely chosen as ds_2 avoiding collision | ||
| assert automl._state.task.time_col == "ds_2" | ||
| assert "ds" in automl._feature_names_in_ | ||
| assert "ds_1" in automl._feature_names_in_ | ||
| assert "ds_2" in automl._feature_names_in_ | ||
| assert automl.data_size_full == len(df) | ||
|
|
||
|
|
||
| def test_validation_data_with_datetime_index(): | ||
| """Verify that validation data passed as DataFrame with DatetimeIndex is properly promoted.""" | ||
| df_train = _make_train_df(periods=100) | ||
| val_index = pd.date_range("2026-05-01", periods=20, freq="MS") | ||
| df_val = pd.DataFrame({"y": np.sin(np.arange(100, 120) / 6) * 10 + 50}, index=val_index) | ||
| X_val = pd.DataFrame(index=val_index) | ||
| y_val = df_val["y"] | ||
| automl = AutoML() | ||
| _fit( | ||
| automl, | ||
| dataframe=df_train, | ||
| X_val=X_val, | ||
| y_val=y_val, | ||
| ) | ||
| assert automl._state.task.time_col == "ds" | ||
| assert automl._state.eval_method == "holdout" | ||
| assert len(automl.predict(X_val)) == len(val_index) | ||
|
|
||
|
|
||
| def test_validation_data_with_dataframe_target_and_datetime_index(): | ||
| """Verify that validation data with DataFrame y_val and DatetimeIndex preserves aligned indexes.""" | ||
| df_train = _make_train_df(periods=100) | ||
| val_index = pd.date_range("2026-05-01", periods=20, freq="MS") | ||
| df_val = pd.DataFrame({"y": np.sin(np.arange(100, 120) / 6) * 10 + 50}, index=val_index) | ||
| X_val = pd.DataFrame(index=val_index) | ||
| y_val = df_val[["y"]] # DataFrame target with DatetimeIndex | ||
| automl = AutoML() | ||
| _fit( | ||
| automl, | ||
| dataframe=df_train, | ||
| X_val=X_val, | ||
| y_val=y_val, | ||
| ) | ||
| assert automl._state.task.time_col == "ds" | ||
| assert automl._state.eval_method == "holdout" | ||
| assert len(automl.predict(X_val)) == len(val_index) | ||
|
|
||
|
|
||
| def test_xtrain_ytrain_with_datetime_index(): | ||
| """Verify that (X_train, y_train) inputs with DatetimeIndex are promoted and work seamlessly.""" | ||
| train_idx = pd.date_range("2018-01-01", periods=100, freq="MS") | ||
| val_idx = pd.date_range("2026-05-01", periods=20, freq="MS") | ||
| X_train = pd.DataFrame({"feat": np.arange(100, dtype=float)}, index=train_idx) | ||
| y_train = pd.Series(np.sin(np.arange(100) / 6) * 10 + 50, index=train_idx, name="y") | ||
| X_val = pd.DataFrame({"feat": np.arange(100, 120, dtype=float)}, index=val_idx) | ||
| y_val = pd.DataFrame({"y": np.sin(np.arange(100, 120) / 6) * 10 + 50}, index=val_idx) | ||
| automl = AutoML() | ||
| _fit(automl, X_train=X_train, y_train=y_train, X_val=X_val, y_val=y_val) | ||
| assert automl._state.task.time_col == "ds" | ||
| assert len(automl.predict(X_val)) == 20 | ||
|
|
||
|
|
||
| def test_predict_with_datetime_index(): | ||
| """Verify that future prediction data passed with DatetimeIndex works without explicit time_col.""" | ||
| df = _make_train_df(periods=120) | ||
| automl = AutoML() | ||
| _fit(automl, dataframe=df) | ||
|
|
||
| # Future prediction input using DatetimeIndex | ||
| future_index = pd.date_range("2028-01-01", periods=12, freq="MS") | ||
| future_df = pd.DataFrame(index=future_index) | ||
| preds = automl.predict(future_df) | ||
| assert len(preds) == 12 | ||
|
|
||
|
|
||
| def test_training_with_reordered_dataframe_target_aligns_by_label(): | ||
| """Verify that when y_train is passed with shuffled timestamps, values align by label rather than position.""" | ||
| train_idx = pd.date_range("2018-01-01", periods=60, freq="MS") | ||
| X_train = pd.DataFrame({"feat": np.arange(60, dtype=float)}, index=train_idx) | ||
| # Shuffled index for y_train | ||
| shuffled_idx = train_idx[::-1] | ||
| y_values = np.sin(np.arange(60)[::-1] / 6) * 10 + 50 | ||
| y_train = pd.DataFrame({"y": y_values}, index=shuffled_idx) | ||
|
|
||
| automl = AutoML() | ||
| _fit(automl, X_train=X_train, y_train=y_train) | ||
|
|
||
| # When y_train is reordered, AutoML must correctly align y by index rather than crashing or distorting data | ||
| preds = automl.predict(X_train.tail(12)) | ||
| assert len(preds) == 12 | ||
| assert automl._state.task.time_col == "ds" | ||
|
Copilot marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| def test_validation_with_reordered_dataframe_target_aligns_by_label(): | ||
| """Verify that when y_val is passed with shuffled timestamps, values align by label rather than position.""" | ||
| df_train = _make_train_df(periods=80) | ||
| val_idx = pd.date_range("2024-09-01", periods=20, freq="MS") | ||
| X_val = pd.DataFrame(index=val_idx) | ||
| # Shuffled index for y_val | ||
| shuffled_val_idx = val_idx[::-1] | ||
| y_val = pd.DataFrame({"y": (np.arange(20)[::-1] + 100.0)}, index=shuffled_val_idx) | ||
|
|
||
| automl = AutoML() | ||
| _fit(automl, dataframe=df_train, X_val=X_val, y_val=y_val) | ||
|
|
||
| preds = automl.predict(X_val) | ||
| assert len(preds) == 20 | ||
| assert automl._state.eval_method == "holdout" | ||
|
|
||
|
|
||
| def test_mismatched_target_index_raises_value_error(): | ||
| """Verify that passing targets with non-matching labels raises ValueError instead of silent corruption.""" | ||
| train_idx = pd.date_range("2018-01-01", periods=60, freq="MS") | ||
| mismatched_idx = pd.date_range("2019-01-01", periods=60, freq="MS") | ||
| X_train = pd.DataFrame({"feat": np.arange(60, dtype=float)}, index=train_idx) | ||
| y_train = pd.DataFrame({"y": np.arange(60, dtype=float)}, index=mismatched_idx) | ||
|
|
||
| automl = AutoML() | ||
| with pytest.raises(ValueError, match="Target index labels do not match feature index labels"): | ||
| _fit(automl, X_train=X_train, y_train=y_train) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.