Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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 Sep 27, 2026
800c05f
fix(automl): resolve index-name collisions and promote DatetimeIndex …
MeiSiristhebest Sep 28, 2026
c92fc51
fix(automl): preserve aligned indexes for DataFrame targets and fix r…
MeiSiristhebest Sep 28, 2026
7d92b36
fix(automl): align targets by label before DatetimeIndex promotion an…
MeiSiristhebest Sep 29, 2026
26654d3
style: apply black and fix ruff F841 in test
MeiSiristhebest Sep 29, 2026
5c5a023
fix(automl): support positional Series targets with DatetimeIndex fea…
MeiSiristhebest Sep 29, 2026
11fcf86
fix(automl): align datetime labels only when features also have Datet…
MeiSiristhebest Sep 29, 2026
fe9b6bc
Merge branch 'main' into fix/ts-forecast-datetime-index-time-col
thinkall Oct 8, 2026
675fbe9
Merge branch 'main' into fix/ts-forecast-datetime-index-time-col
thinkall Oct 17, 2026
8ba1be7
Merge branch 'main' into fix/ts-forecast-datetime-index-time-col
thinkall Oct 17, 2026
0d53385
fix(automl): handle index-only training, length mismatch, and target …
MeiSiristhebest Oct 17, 2026
456521d
style: apply black formatting to ts_data.py
MeiSiristhebest Oct 17, 2026
9111191
Merge branch 'main' into fix/ts-forecast-datetime-index-time-col
thinkall Oct 18, 2026
3d36d21
fix(automl): align time series inputs at boundary and preserve positi…
MeiSiristhebest Oct 18, 2026
924e458
Merge branch 'main' into fix/ts-forecast-datetime-index-time-col
thinkall Oct 18, 2026
df472f1
Merge branch 'main' into fix/ts-forecast-datetime-index-time-col
thinkall Oct 19, 2026
1964edc
fix(automl): preserve DataFrame target label alignment and disallow i…
MeiSiristhebest Oct 19, 2026
1e3b1f2
test(automl): assert exact target value alignment in reordered target…
MeiSiristhebest Oct 19, 2026
d57af8d
fix(automl): validate explicit time_col on features before joining ta…
MeiSiristhebest Oct 19, 2026
1771c8f
fix(automl): preserve NumPy array normalization while validating Data…
MeiSiristhebest Oct 19, 2026
66ad9c4
fix(automl): preserve non-None DatetimeIndex names and ensure predict…
MeiSiristhebest Oct 20, 2026
e4944d4
docs(test): update test_ts_forecast_datetime_index module docstring t…
MeiSiristhebest Oct 20, 2026
95fb4ce
fix(automl): restore caller input index on predictions for estimators…
MeiSiristhebest Oct 21, 2026
ce5d195
fix(automl): preserve forecast prediction alignment
MeiSiristhebest Oct 21, 2026
c85b11b
fix(ts_forecast): align in-sample predictions by timestamp
MeiSiristhebest Oct 21, 2026
748174b
test(ts_forecast): name timestamp alignment index
MeiSiristhebest Oct 21, 2026
d5fe77c
test(ts-forecast): cover real ARIMA prediction ordering
MeiSiristhebest Oct 21, 2026
73cb38b
fix(ts-forecast): normalize date string array predictions
MeiSiristhebest Oct 21, 2026
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
143 changes: 140 additions & 3 deletions flaml/automl/task/time_series_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ def validate_data(
else:
target_names = label

time_col_inferred = False
if self.time_col is None:
time_col_inferred = True
if isinstance(X_train_all, pd.DataFrame):
assert dataframe is None, "One of dataframe and X arguments must be None"
self.time_col = X_train_all.columns[0]
Comment thread
Copilot marked this conversation as resolved.
Outdated
Expand All @@ -141,6 +143,17 @@ def validate_data(
if X_train_all is not None:
assert y_train_all is not None, "If X_train_all is not None, y_train_all must also be"
assert dataframe is None, "If X_train_all is provided, dataframe must be None"
if isinstance(X_train_all, pd.DataFrame):
X_train_all, y_train_all = self._align_y_to_X_and_promote(X_train_all, y_train_all)
X_train_all, promoted_time_col = self._promote_datetime_index_if_needed(
X_train_all,
time_col=self.time_col,
is_inferred=time_col_inferred,
)
if promoted_time_col is not None:
self.time_col = promoted_time_col
if isinstance(y_train_all, (pd.DataFrame, pd.Series)):
y_train_all = y_train_all.reset_index(drop=True)
dataframe = TimeSeriesDataset.to_dataframe(X_train_all, y_train_all, target_names, self.time_col)

elif dataframe is not None:
Expand All @@ -150,6 +163,14 @@ def validate_data(
else:
raise ValueError("Must supply either X_train_all and y_train_all, or dataframe and label")

dataframe, promoted_time_col = self._promote_datetime_index_if_needed(
dataframe,
time_col=self.time_col,
is_inferred=time_col_inferred,
)
if promoted_time_col is not None:
self.time_col = promoted_time_col

try:
dataframe.loc[:, self.time_col] = pd.to_datetime(dataframe[self.time_col])
except Exception:
Expand All @@ -161,6 +182,16 @@ def validate_data(

if X_val is not None:
assert y_val is not None, "If X_val is not None, y_val must also be"
if isinstance(X_val, pd.DataFrame):
X_val, y_val = self._align_y_to_X_and_promote(X_val, y_val)
X_val, promoted_time_col_val = self._promote_datetime_index_if_needed(
X_val,
time_col=self.time_col,
is_inferred=False,
)
if promoted_time_col_val is not None:
if isinstance(y_val, (pd.DataFrame, pd.Series)):
y_val = y_val.reset_index(drop=True)
val_df = TimeSeriesDataset.to_dataframe(X_val, y_val, target_names, self.time_col)
val_len = len(val_df)
else:
Expand Down Expand Up @@ -366,9 +397,11 @@ def _preprocess(self, X, transformer=None):
X = pd.DataFrame(
dict(
[
(transformer._str_columns[idx], X[idx])
if isinstance(X[0], List)
else (transformer._str_columns[idx], [X[idx]])
(
(transformer._str_columns[idx], X[idx])
if isinstance(X[0], List)
else (transformer._str_columns[idx], [X[idx]])
)
for idx in range(len(X))
]
)
Expand All @@ -385,7 +418,111 @@ def _preprocess(self, X, transformer=None):
X = transformer.transform(X)
return X

@staticmethod
def _align_y_to_X_and_promote(X, y):
"""Align y to X by label before index promotion or resetting.

Only perform datetime-label alignment when X itself has a matching DatetimeIndex
and y also has a DatetimeIndex. If non-matching labels introduce new missing
values, raise ValueError.
Otherwise (e.g. X has a DatetimeIndex and y has a RangeIndex, or X has a
RangeIndex and y is a Series with DatetimeIndex or RangeIndex), preserve
the positional Series contract by assigning X.index to y.
"""
if not isinstance(X, pd.DataFrame) or not isinstance(y, (pd.DataFrame, pd.Series)):
return X, y
Comment thread
Copilot marked this conversation as resolved.

# Only perform datetime-label alignment when both X and y have a DatetimeIndex
if isinstance(X.index, pd.DatetimeIndex) and isinstance(y.index, pd.DatetimeIndex):
if not y.index.equals(X.index):
y_aligned = y.reindex(X.index)
Comment thread
Copilot marked this conversation as resolved.
Outdated
orig_nan_count = int(y.isna().sum().sum() if isinstance(y, pd.DataFrame) else y.isna().sum())
new_nan_count = int(
y_aligned.isna().sum().sum() if isinstance(y_aligned, pd.DataFrame) else y_aligned.isna().sum()
)
if new_nan_count > orig_nan_count:
Comment thread
Copilot marked this conversation as resolved.
Outdated
raise ValueError("Target index labels do not match feature index labels.")
y = y_aligned
return X, y

# Positional pairing: when X has DatetimeIndex and y is a Series without DatetimeIndex (or RangeIndex DataFrame),
# or when X has RangeIndex and y is a Series (even if y has a DatetimeIndex).
if isinstance(y, pd.Series):
if len(y) == len(X):
y = y.copy()
y.index = X.index
return X, y
elif isinstance(y, pd.DataFrame):
if isinstance(X.index, pd.DatetimeIndex) and isinstance(y.index, pd.RangeIndex):
if len(y) == len(X):
y = y.copy()
y.index = X.index
return X, y
elif not y.index.equals(X.index):
# When neither is DatetimeIndex, standard label alignment if indexes differ
y_aligned = y.reindex(X.index)
orig_nan_count = int(y.isna().sum().sum())
new_nan_count = int(y_aligned.isna().sum().sum())
if new_nan_count > orig_nan_count:
raise ValueError("Target index labels do not match feature index labels.")
y = y_aligned

return X, y

@staticmethod
def _promote_datetime_index_if_needed(df, time_col=None, is_inferred=False):
"""Promote a DatetimeIndex to a guaranteed-unique column if needed.

Args:
df: A pandas DataFrame.
time_col: The current timestamp column name (if known).
is_inferred: Whether time_col was inferred rather than explicitly specified.

Returns:
Tuple of (df_processed, promoted_col_name or None).
"""
if not isinstance(df, pd.DataFrame) or not isinstance(df.index, pd.DatetimeIndex):
return df, None

# Precondition check:
# 1. If time_col was inferred: promote if df lacks a datetime-typed time_col.
# 2. If time_col was explicit: promote if time_col is missing from df.columns.
needs_promotion = False
if is_inferred:
if time_col is None or time_col not in df.columns or not pd.api.types.is_datetime64_any_dtype(df[time_col]):
needs_promotion = True
Comment thread
Copilot marked this conversation as resolved.
Outdated
else:
if time_col is not None and time_col not in df.columns:
needs_promotion = True

if not needs_promotion:
return df, None

base_name = df.index.name or "ds"
promoted_col = base_name if not is_inferred and time_col else base_name
if not is_inferred and time_col:
promoted_col = time_col

# Guarantee unique column name avoiding collisions with existing columns
target_col = promoted_col
i = 1
while target_col in df.columns:
Comment thread
Copilot marked this conversation as resolved.
Outdated
target_col = f"{promoted_col}_{i}"
i += 1

df_out = df.copy()
df_out.insert(0, target_col, df.index)
df_out = df_out.reset_index(drop=True)
logger.info(f"Timestamp column not found, promoted DataFrame index as '{target_col}'.")
return df_out, target_col

def preprocess(self, X, transformer=None):
if isinstance(X, pd.DataFrame):
X, _ = self._promote_datetime_index_if_needed(
X,
time_col=self.time_col,
is_inferred=False,
)
Comment thread
Copilot marked this conversation as resolved.
if isinstance(X, (pd.DataFrame, np.ndarray, pd.Series)):
X = normalize_ts_data(X.copy(), self.target_names, self.time_col)
return self._preprocess(X, transformer)
Expand Down
31 changes: 29 additions & 2 deletions flaml/automl/time_series/ts_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,8 +547,35 @@ def normalize_ts_data(X_train_all, target_names, time_col, y_train_all=None):
index=X_train_all.index,
)
elif isinstance(y_train_all, pd.Series):
y_train_all = pd.DataFrame(y_train_all)
y_train_all.index = X_train_all.index
if isinstance(X_train_all.index, pd.DatetimeIndex) and isinstance(y_train_all.index, pd.DatetimeIndex):
if not y_train_all.index.equals(X_train_all.index):
y_aligned = y_train_all.reindex(X_train_all.index)
Comment thread
Copilot marked this conversation as resolved.
Outdated
if y_aligned.isna().sum() > y_train_all.isna().sum():
raise ValueError("Target index labels do not match feature index labels.")
Comment thread
Copilot marked this conversation as resolved.
Outdated
y_train_all = y_aligned
y_train_all = pd.DataFrame(y_train_all)
else:
y_train_all = pd.DataFrame(y_train_all)
y_train_all.index = X_train_all.index
elif isinstance(y_train_all, pd.DataFrame):
if isinstance(X_train_all.index, pd.DatetimeIndex) and isinstance(y_train_all.index, pd.DatetimeIndex):
if not y_train_all.index.equals(X_train_all.index):
y_aligned = y_train_all.reindex(X_train_all.index)
if y_aligned.isna().sum().sum() > y_train_all.isna().sum().sum():
raise ValueError("Target index labels do not match feature index labels.")
y_train_all = y_aligned
elif (
isinstance(X_train_all.index, pd.DatetimeIndex)
and isinstance(y_train_all.index, pd.RangeIndex)
and len(y_train_all) == len(X_train_all)
):
y_train_all = y_train_all.copy()
y_train_all.index = X_train_all.index
elif not y_train_all.index.equals(X_train_all.index):
y_aligned = y_train_all.reindex(X_train_all.index)
if y_aligned.isna().sum().sum() > y_train_all.isna().sum().sum():
raise ValueError("Target index labels do not match feature index labels.")
y_train_all = y_aligned

dataframe = pd.concat([X_train_all, y_train_all], axis=1)

Expand Down
Loading
Loading