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
4 changes: 2 additions & 2 deletions docs/examples/tabpfn-family.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@
"source": [
"## Generate forecast\n",
"\n",
"TabPFN outputs nine fixed quantiles (0.1 through 0.9). Pass `level=[0, 20, 40, 60, 80]` to request all supported prediction intervals.\n"
"TabPFN outputs nine fixed quantiles (0.1 through 0.9). Pass `level=[20, 40, 60, 80]` for common decile-style prediction intervals.\n"
]
},
{
Expand Down Expand Up @@ -480,7 +480,7 @@
}
],
"source": [
"level = [0, 20, 40, 60, 80]\n",
"level = [20, 40, 60, 80]\n",
"cv_df = tcf.cross_validation(df=df, h=12, freq=\"MS\", level=level)\n"
]
},
Expand Down
4 changes: 2 additions & 2 deletions docs/examples/timesfm-family.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"\n",
"In this example we compare all four generations on event pageview data.\n",
"\n",
"**Prediction intervals:** TimesFM outputs nine fixed quantiles (0.1 through 0.9). Pass `level=[0, 20, 40, 60, 80]` to request all supported prediction intervals; custom levels or quantiles are not supported."
"**Prediction intervals:** TimesFM outputs nine fixed quantiles (0.1 through 0.9). Pass `level=[20, 40, 60, 80]` for common decile-style prediction intervals; custom levels or quantiles are not supported."
]
},
{
Expand Down Expand Up @@ -360,7 +360,7 @@
}
],
"source": [
"level = [0, 20, 40, 60, 80] # required to use all TimesFM-supported quantiles\n",
"level = [20, 40, 60, 80]\n",
"cv_df = tcf.cross_validation(df=df, h=12, level=level)"
]
},
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/tirex-family.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@
"metadata": {},
"outputs": [],
"source": [
"level = [0, 20, 40, 60, 80] # we need to pass 0 level since specific quantiles are required by TiRex\n",
"level = [20, 40, 60, 80]\n",
"cv_df = tcf.cross_validation(df=df, h=12, level=level)\n"
]
},
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ classifiers = [
# - transformers>=5 (CVE-2026-1839 fix) requires huggingface-hub>=1; the upper
# bounds were widened together (transformers<6, huggingface-hub<2). Reaching
# transformers 5 also requires tfc-t0>=0.2 (older tfc-t0 capped hub<1).
# - scipy>=1.17.1 on Python>=3.11; scipy<=1.15.3 remains on 3.10 (no 1.17 wheels).
# - lightning and pytorch-lightning are the same library
# NOTE: dependencies pinned for faster pip installs
# versions chosen from uv's dependency resolution
Expand All @@ -69,7 +70,7 @@ dependencies = [
"catboost>=1.2.10",
"datasets>=4.1.1",
"fire",
"foundationforecast>=0.1.8",
"foundationforecast>=0.1.12",
"fsspec>=2025.9.0",
"huggingface-hub>=0.36.2,<2.0",
"hydra-core>=1.3.2",
Expand All @@ -93,7 +94,8 @@ dependencies = [
"prophet>=1.1.7",
"pydantic-ai>=0.7.0",
"pytorch-lightning==2.4.0",
"scipy<=1.15.3",
"scipy<=1.15.3 ; python_full_version < '3.11'",
"scipy>=1.17.1 ; python_full_version >= '3.11'",
"statsforecast>=2.0.2",
"tensorboard>=2.20.0",
"torch>=2.10,<2.11 ; python_full_version >= '3.11'",
Expand Down
2 changes: 1 addition & 1 deletion tests/models/foundation/test_tabpfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
pytestmark = pytest.mark.models

DEFAULT_QUANTILES = [round(i * 0.1, 1) for i in range(1, 10)]
DEFAULT_LEVEL = [0, 20, 40, 60, 80]
DEFAULT_LEVEL = [20, 40, 60, 80]

TABPFN_CASES = [
pytest.param(TABPFN_V2_MODEL, 4096, "TabPFN-2", id="v2"),
Expand Down
3 changes: 1 addition & 2 deletions tests/models/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ def test_using_quantiles(model):

@pytest.mark.parametrize("model", models)
def test_using_level(model):
level = [0, 20, 40, 60, 80] # corresponds to qs [0.1, 0.2, ..., 0.9]
level = [20, 40, 60, 80] # decile-style levels (maps to native quantile knots)
df = generate_series(n_series=2, freq="D")
_level_unsupported = {
"AutoLGBM",
Expand Down Expand Up @@ -323,7 +323,6 @@ def test_using_level(model):
assert all(col in fcst_df.columns for col in exp_lv_cols)
assert not any(("-q-" in col) for col in fcst_df.columns)
# test monotonicity of levels
exp_lv_cols = exp_lv_cols[2:] # remove level 0
for c1, c2 in zip(exp_lv_cols[:-1:2], exp_lv_cols[1::2], strict=False):
if model.alias == "ZeroModel":
# ZeroModel is a constant model, so all levels should be the same
Expand Down
35 changes: 14 additions & 21 deletions tests/models/utils/test_forecaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def test_maybe_convert_level_to_quantiles(n_models, quantiles):
"n_models,level",
[
(1, [80]),
(2, [0, 80]),
(2, [20, 80]),
(2, [60, 80]),
],
)
Expand All @@ -167,26 +167,19 @@ def test_maybe_convert_quantiles_to_level(n_models, level):
assert result_df.shape[1] == exp_n_cols
for model in models:
for lv in level:
if lv == 0:
pd.testing.assert_series_equal(
result_df[model],
df[f"{model}-q-50"],
check_names=False,
)
else:
alpha = round(1 - lv / 100, 2)
q_lo = int((alpha / 2) * 100)
q_hi = int((1 - alpha / 2) * 100)
pd.testing.assert_series_equal(
result_df[f"{model}-lo-{lv}"],
df[f"{model}-q-{q_lo}"],
check_names=False,
)
pd.testing.assert_series_equal(
result_df[f"{model}-hi-{lv}"],
df[f"{model}-q-{q_hi}"],
check_names=False,
)
alpha = round(1 - lv / 100, 2)
q_lo = int((alpha / 2) * 100)
q_hi = int((1 - alpha / 2) * 100)
pd.testing.assert_series_equal(
result_df[f"{model}-lo-{lv}"],
df[f"{model}-q-{q_lo}"],
check_names=False,
)
pd.testing.assert_series_equal(
result_df[f"{model}-hi-{lv}"],
df[f"{model}-q-{q_hi}"],
check_names=False,
)
# check that maybe convert level to quantiles returns the same result
pd.testing.assert_frame_equal(
df,
Expand Down
6 changes: 0 additions & 6 deletions tests/test_distributed.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ def test_cross_validation_spark(simple_models, spark_df):

@pytest.mark.distributed
def test_using_level_spark(foundation_model, spark_session):
# level = [0, 20, 40, 60, 80] # corresponds to qs [0.1, 0.2, ..., 0.9]
level: list[int | float] = [20, 80]
df = generate_series(
n_series=2, freq="D", max_length=100, static_as_categorical=False
Expand Down Expand Up @@ -217,7 +216,6 @@ def test_using_level_spark(foundation_model, spark_session):
assert all(col in fcst_df_pd.columns for col in exp_lv_cols)
assert not any(("-q-" in col) for col in fcst_df_pd.columns)
# test monotonicity of levels
exp_lv_cols = exp_lv_cols[2:] # remove level 0
for c1, c2 in zip(exp_lv_cols[:-1:2], exp_lv_cols[1::2], strict=False):
for model in foundation_model:
if model.alias == "ZeroModel":
Expand Down Expand Up @@ -312,7 +310,6 @@ def test_cross_validation_dask(simple_models, dask_df):
def test_using_level_dask(foundation_model):
import dask.dataframe as dd

# level = [0, 20, 40, 60, 80] # corresponds to qs [0.1, 0.2, ..., 0.9]
level: list[int | float] = [20, 80]
n_partitions = 2
df = generate_series(
Expand Down Expand Up @@ -352,7 +349,6 @@ def test_using_level_dask(foundation_model):
assert all(col in fcst_df_pd.columns for col in exp_lv_cols)
assert not any(("-q-" in col) for col in fcst_df_pd.columns)
# test monotonicity of levels
exp_lv_cols = exp_lv_cols[2:] # remove level 0
for c1, c2 in zip(exp_lv_cols[:-1:2], exp_lv_cols[1::2], strict=False):
for model in foundation_model:
if model.alias == "ZeroModel":
Expand Down Expand Up @@ -425,7 +421,6 @@ def test_cross_validation_ray(simple_models, ray_dataset):
def test_using_level_ray(foundation_model):
import ray

# level = [0, 20, 40, 60, 80] # corresponds to qs [0.1, 0.2, ..., 0.9]
level: list[int | float] = [20, 80]
df = generate_series(
n_series=2, freq="D", max_length=100, static_as_categorical=False
Expand Down Expand Up @@ -473,7 +468,6 @@ def test_using_level_ray(foundation_model):
assert all(col in fcst_df_pd.columns for col in exp_lv_cols)
assert not any(("-q-" in col) for col in fcst_df_pd.columns)
# test monotonicity of levels
exp_lv_cols = exp_lv_cols[2:] # remove level 0
for c1, c2 in zip(exp_lv_cols[:-1:2], exp_lv_cols[1::2], strict=False):
for model in foundation_model:
if model.alias == "ZeroModel":
Expand Down
9 changes: 9 additions & 0 deletions timecopilot/forecaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ def _cross_validation_pandas(
step_size: int | None = None,
level: list[int | float] | None = None,
quantiles: list[float] | None = None,
*,
futr_exog_list: list[str] | None = None,
) -> pd.DataFrame:
"""
Internal pandas-only cross-validation implementation.
Expand All @@ -285,6 +287,7 @@ def _cross_validation_pandas(
step_size=step_size,
level=level,
quantiles=quantiles,
futr_exog_list=futr_exog_list,
)

def _cross_validation_distributed(
Expand All @@ -296,6 +299,8 @@ def _cross_validation_distributed(
step_size: int | None = None,
level: list[int | float] | None = None,
quantiles: list[float] | None = None,
*,
futr_exog_list: list[str] | None = None,
num_partitions: int | None = None,
) -> DistributedDataFrame:
"""
Expand Down Expand Up @@ -368,6 +373,8 @@ def cross_validation(
step_size: int | None = None,
level: list[int | float] | None = None,
quantiles: list[float] | None = None,
*,
futr_exog_list: list[str] | None = None,
num_partitions: int | None = None,
) -> AnyDataFrame:
"""
Expand Down Expand Up @@ -443,6 +450,7 @@ def cross_validation(
step_size=step_size,
level=level,
quantiles=quantiles,
futr_exog_list=futr_exog_list,
num_partitions=num_partitions,
)

Expand All @@ -455,6 +463,7 @@ def cross_validation(
step_size=step_size,
level=level,
quantiles=quantiles,
futr_exog_list=futr_exog_list,
)

def _detect_anomalies_pandas(
Expand Down
31 changes: 31 additions & 0 deletions timecopilot/models/utils/forecaster.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pandas as pd
from foundationforecast.core.exog.covariates import normalize_exog_strategy
from foundationforecast.core.forecaster import Forecaster as _Forecaster
from foundationforecast.core.forecaster import (
QuantileConverter,
Expand All @@ -21,6 +22,36 @@
class Forecaster(_Forecaster):
"""TimeCopilot Forecaster base (extends foundationforecast)."""

def _ensure_forecaster_state(self) -> None:
if not hasattr(self, "exog_strategy"):
self.exog_strategy = normalize_exog_strategy(False)
if not hasattr(self, "reuse_loaded_model"):
self.reuse_loaded_model = True

def cross_validation(
self,
df: pd.DataFrame,
h: int,
freq: str | None = None,
n_windows: int = 1,
step_size: int | None = None,
level: list[int | float] | None = None,
quantiles: list[float] | None = None,
*,
futr_exog_list: list[str] | None = None,
) -> pd.DataFrame:
self._ensure_forecaster_state()
return super().cross_validation(
df,
h,
freq=freq,
n_windows=n_windows,
step_size=step_size,
level=level,
quantiles=quantiles,
futr_exog_list=futr_exog_list,
)

@staticmethod
def _maybe_infer_freq(df: pd.DataFrame, freq: str | None) -> str:
return maybe_infer_freq(df, freq)
Loading
Loading