Skip to content
Merged
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
2 changes: 2 additions & 0 deletions foundationforecast/_foundation_forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ def __init__(
fallback_model: Forecaster | None = None,
clean_cache: bool = False,
):
if not models:
raise ValueError("At least one model is required.")
self._validate_unique_aliases(models)
self.models = models
self.fallback_model = fallback_model
Expand Down
27 changes: 27 additions & 0 deletions foundationforecast/core/forecaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,32 @@ def maybe_convert_col_to_datetime(df: pd.DataFrame, col_name: str) -> pd.DataFra
class Forecaster:
alias: str

@staticmethod
def validate_input(
df: pd.DataFrame,
h: int | None,
) -> None:
"""Validate that the input DataFrame and horizon are suitable for forecasting.

Args:
df: DataFrame containing the time series. Must include the columns
`unique_id`, `ds`, and `y`.
h: Forecast horizon. If provided, must be a positive integer.
"""
if not isinstance(df, pd.DataFrame):
raise ValueError("df must be a pandas DataFrame.")
required_cols = ["unique_id", "ds", "y"]
missing = [c for c in required_cols if c not in df.columns]
if missing:
raise ValueError(
f"Input df is missing required columns: {missing}. "
"Expected columns are: 'unique_id', 'ds', 'y'."
)
if h is not None and (not isinstance(h, int | np.integer) or h <= 0):
raise ValueError("h must be a positive integer.")
if len(df) == 0:
raise ValueError("df must contain at least one row.")

@staticmethod
def plot(
df: pd.DataFrame | None = None,
Expand Down Expand Up @@ -161,6 +187,7 @@ def cross_validation(
level: list[int | float] | None = None,
quantiles: list[float] | None = None,
) -> pd.DataFrame:
self.validate_input(df, h)
freq = self._maybe_infer_freq(df, freq)
df = maybe_convert_col_to_datetime(df, "ds")
results = []
Expand Down
1 change: 1 addition & 0 deletions foundationforecast/core/multi_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def _call_models(
quantiles: list[float] | None,
**kwargs,
) -> pd.DataFrame:
Forecaster.validate_input(df, h)
freq = maybe_infer_freq(df, freq)
res_df: pd.DataFrame | None = None
for model in self.models:
Expand Down
37 changes: 37 additions & 0 deletions tests/core/test_forecaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,40 @@ def test_detect_anomalies_short_series_error():
)
with pytest.raises(ValueError, match="Cannot perform anomaly detection"):
model.detect_anomalies(df, h=5, freq="D")


def test_validate_input():
df = generate_series(n_series=1, freq="D", min_length=10, max_length=10)
DummyModel().validate_input(df, h=2)


@pytest.mark.parametrize(
"df,h,match",
[
(
pd.DataFrame({"ds": pd.date_range("2023-01-01", periods=2, freq="D")}),
2,
"missing required columns",
),
(pd.DataFrame(), 2, "missing required columns"),
(
pd.DataFrame(columns=["unique_id", "ds", "y"]),
2,
"must contain at least one row",
),
("not a dataframe", 2, "must be a pandas DataFrame"),
(
generate_series(n_series=1, freq="D", min_length=5, max_length=5),
0,
"h must be a positive integer",
),
(
generate_series(n_series=1, freq="D", min_length=5, max_length=5),
-1,
"h must be a positive integer",
),
],
)
def test_validate_input_errors(df, h, match):
with pytest.raises(ValueError, match=match):
DummyModel().validate_input(df, h)
12 changes: 12 additions & 0 deletions tests/test_foundation_forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,15 @@ def test_foundation_forecast_duplicate_aliases_with_moirai():
ValueError, match="Duplicate model aliases found: \\['Moirai'\\]"
):
FoundationForecast(models=[model1, model2])


def test_foundation_forecast_rejects_empty_models():
with pytest.raises(ValueError, match="At least one model is required"):
FoundationForecast(models=[])


def test_foundation_forecast_validates_input():
df = generate_series(n_series=1, freq="D", min_length=5, max_length=5)
forecaster = FoundationForecast(models=[DummyModel()])
with pytest.raises(ValueError, match="h must be a positive integer"):
forecaster.forecast(df=df, h=0, freq="D")
Loading