├── tests
├── __init__.py
├── utils
│ └── __init__.py
├── models
│ ├── __init__.py
│ ├── utils
│ │ └── __init__.py
│ ├── foundation
│ │ ├── __init__.py
│ │ └── test_timesfm.py
│ └── conftest.py
├── gift_eval
│ ├── conftest.py
│ ├── test_gift_eval.py
│ └── test_evaluation.py
├── test_agent.py
├── docs
│ └── test_docs.py
├── test_live.py
└── test_forecaster.py
├── experiments
├── __init__.py
├── fev
│ ├── src
│ │ ├── __init__.py
│ │ ├── download_results.py
│ │ ├── evaluate_model_modal.py
│ │ └── evaluate_model.py
│ ├── pyproject.toml
│ └── README.md
└── gift-eval
│ ├── src
│ ├── __init__.py
│ ├── download_results.py
│ ├── run_timecopilot.py
│ └── run_modal.py
│ ├── .python-version
│ ├── Makefile
│ ├── pyproject.toml
│ └── README.md
├── .python-version
├── docs
├── blog
│ ├── index.md
│ ├── .meta.yml
│ ├── .authors.yml
│ └── posts
│ │ └── forecasting-the-agentic-way.md
├── experiments
│ ├── fev.md
│ └── gift-eval.md
├── api
│ ├── models
│ │ ├── ml.md
│ │ ├── prophet.md
│ │ ├── ensembles.md
│ │ ├── neural.md
│ │ ├── utils
│ │ │ └── forecaster.md
│ │ ├── stats.md
│ │ └── foundation
│ │ │ └── models.md
│ ├── forecaster.md
│ ├── agent.md
│ └── gift-eval
│ │ └── gift-eval.md
├── index.md
├── community
│ ├── roadmap.md
│ └── help.md
├── changelogs
│ ├── v0.0.16.md
│ ├── index.md
│ ├── v0.0.21.md
│ ├── v0.0.22.md
│ ├── v0.0.15.md
│ ├── v0.0.14.md
│ ├── v0.0.20.md
│ ├── v0.0.19.md
│ ├── v0.0.18.md
│ ├── v0.0.10.md
│ ├── v0.0.11.md
│ ├── v0.0.13.md
│ ├── v0.0.17.md
│ └── v0.0.12.md
├── getting-started
│ ├── installation.md
│ └── introduction.md
├── stylesheets
│ └── extra.css
├── contributing.md
├── forecasting-parameters.md
├── model-hub.md
└── examples
│ └── agent-quickstart.ipynb
├── timecopilot
├── gift_eval
│ ├── __init__.py
│ ├── utils.py
│ ├── gluonts_predictor.py
│ └── data.py
├── utils
│ └── __init__.py
├── models
│ ├── utils
│ │ ├── __init__.py
│ │ ├── parallel_forecaster.py
│ │ └── gluonts_forecaster.py
│ ├── ensembles
│ │ ├── __init__.py
│ │ └── median.py
│ ├── foundation
│ │ ├── __init__.py
│ │ ├── utils.py
│ │ ├── moirai.py
│ │ └── timegpt.py
│ ├── __init__.py
│ ├── ml.py
│ └── prophet.py
└── __init__.py
├── Makefile
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yaml
│ ├── feature.yaml
│ ├── question.yaml
│ └── bug.yaml
├── deploy_docs.py
├── workflows
│ ├── deploy-docs.yaml
│ ├── preview-docs.yaml
│ ├── release.yaml
│ ├── notify-release.yaml
│ └── ci.yaml
└── FUNDING.yml
├── .pre-commit-config.yaml
├── LICENSE
├── .gitignore
├── pyproject.toml
└── mkdocs.yml
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/experiments/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.11
2 |
--------------------------------------------------------------------------------
/tests/models/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/blog/index.md:
--------------------------------------------------------------------------------
1 | # Blog
2 |
--------------------------------------------------------------------------------
/experiments/fev/src/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/models/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/timecopilot/gift_eval/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/timecopilot/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/experiments/gift-eval/src/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/models/foundation/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/timecopilot/models/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/timecopilot/models/ensembles/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/blog/.meta.yml:
--------------------------------------------------------------------------------
1 | hide:
2 | - feedback
3 |
--------------------------------------------------------------------------------
/experiments/gift-eval/.python-version:
--------------------------------------------------------------------------------
1 | 3.11
2 |
--------------------------------------------------------------------------------
/docs/experiments/fev.md:
--------------------------------------------------------------------------------
1 | {% include-markdown "../../experiments/fev/README.md" %}
--------------------------------------------------------------------------------
/docs/experiments/gift-eval.md:
--------------------------------------------------------------------------------
1 | {% include-markdown "../../experiments/gift-eval/README.md" %}
--------------------------------------------------------------------------------
/timecopilot/models/foundation/__init__.py:
--------------------------------------------------------------------------------
1 | from .timesfm import TimesFM
2 |
3 | __all__ = [
4 | "TimesFM",
5 | ]
6 |
--------------------------------------------------------------------------------
/docs/api/models/ml.md:
--------------------------------------------------------------------------------
1 |
2 | # `timecopilot.models.ml`
3 |
4 | ::: timecopilot.models.ml
5 | options:
6 | members:
7 | - AutoLGBM
--------------------------------------------------------------------------------
/docs/api/forecaster.md:
--------------------------------------------------------------------------------
1 | # `timecopilot.forecaster`
2 |
3 | ::: timecopilot.forecaster
4 | options:
5 | members:
6 | - TimeCopilotForecaster
--------------------------------------------------------------------------------
/docs/api/models/prophet.md:
--------------------------------------------------------------------------------
1 | # `timecopilot.models.prophet`
2 |
3 | ::: timecopilot.models.prophet
4 | options:
5 | members:
6 | - Prophet
--------------------------------------------------------------------------------
/docs/api/models/ensembles.md:
--------------------------------------------------------------------------------
1 | # `timecopilot.models.ensembles`
2 |
3 | ::: timecopilot.models.ensembles.median
4 | options:
5 | members:
6 | - MedianEnsemble
--------------------------------------------------------------------------------
/docs/api/models/neural.md:
--------------------------------------------------------------------------------
1 |
2 | # `timecopilot.models.neural`
3 |
4 | ::: timecopilot.models.neural
5 | options:
6 | members:
7 | - AutoNHITS
8 | - AutoTFT
--------------------------------------------------------------------------------
/timecopilot/__init__.py:
--------------------------------------------------------------------------------
1 | from .agent import AsyncTimeCopilot, TimeCopilot
2 | from .forecaster import TimeCopilotForecaster
3 |
4 | __all__ = ["AsyncTimeCopilot", "TimeCopilot", "TimeCopilotForecaster"]
5 |
--------------------------------------------------------------------------------
/docs/api/agent.md:
--------------------------------------------------------------------------------
1 | # `timecopilot.agent`
2 |
3 | ::: timecopilot.agent
4 | options:
5 | members:
6 | - TimeCopilot
7 | - AsyncTimeCopilot
8 | - ForecastAgentOutput
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | hide:
3 | - toc
4 | - navigation
5 | ---
6 |
7 |
8 |
14 |
15 | --8<-- "README.md"
16 |
--------------------------------------------------------------------------------
/docs/api/models/utils/forecaster.md:
--------------------------------------------------------------------------------
1 | # `timecopilot.models.utils.forecaster`
2 |
3 | ::: timecopilot.models.utils.forecaster
4 | options:
5 | members:
6 | - get_seasonality
7 | - maybe_infer_freq
8 | - Forecaster
--------------------------------------------------------------------------------
/docs/community/roadmap.md:
--------------------------------------------------------------------------------
1 | TimeCopilot is under active development with a clear roadmap ahead. Please visit our [issue tracker](https://github.com/TimeCopilot/timecopilot/issues) on GitHub to stay updated on the latest features, report issues, and contribute to the project.
2 |
--------------------------------------------------------------------------------
/docs/api/gift-eval/gift-eval.md:
--------------------------------------------------------------------------------
1 | # `timecopilot.gift_eval`
2 |
3 | ::: timecopilot.gift_eval.eval
4 | options:
5 | members:
6 | - GIFTEval
7 |
8 | ::: timecopilot.gift_eval.gluonts_predictor
9 | options:
10 | members:
11 | - GluonTSPredictor
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: serve-docs
2 | serve-docs:
3 | uv run --group docs mkdocs build
4 | uv run --group docs modal serve .github/deploy_docs.py
5 |
6 | .PHONY: deploy-docs
7 | deploy-docs:
8 | uv run --group docs mkdocs build
9 | uv run --group docs modal deploy .github/deploy_docs.py
10 |
--------------------------------------------------------------------------------
/experiments/gift-eval/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: download-gift-eval-data upload-data-to-s3
2 |
3 | download-gift-eval-data:
4 | @huggingface-cli download Salesforce/GiftEval --repo-type=dataset --local-dir=./data/gift-eval
5 |
6 | upload-data-to-s3: download-gift-eval-data
7 | @aws s3 sync ./data/gift-eval s3://timecopilot-gift-eval/data/gift-eval
8 |
--------------------------------------------------------------------------------
/experiments/gift-eval/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | dependencies = [
3 | "modal>=1.0.5",
4 | "s3fs>=2023.12.1",
5 | "timecopilot>=0.0.21",
6 | "transformers<4.54",
7 | "transformers==4.40.1 ; python_full_version < '3.12'",
8 | "typer>=0.16.0",
9 | ]
10 | description = "TimeCopilot experiments for GIFT-Eval"
11 | name = "timecopilot-gift-eval"
12 | readme = "README.md"
13 | requires-python = ">=3.11"
14 | version = "0.2.0"
15 |
--------------------------------------------------------------------------------
/experiments/fev/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | dependencies = [
3 | "fev>=0.5.0",
4 | "modal>=1.0.5",
5 | "pyarrow<=20.0.0",
6 | "s3fs>=2023.12.1",
7 | "timecopilot>=0.0.17",
8 | "typer>=0.16.0",
9 | ]
10 | description = "TimeCopilot fev experiments"
11 | name = "timecopilot-fev"
12 | readme = "README.md"
13 | requires-python = ">=3.11"
14 | version = "0.1.0"
15 |
16 | [tool.uv]
17 | override-dependencies = ["datasets[s3]>=2.15,<4.0"]
18 |
--------------------------------------------------------------------------------
/docs/api/models/stats.md:
--------------------------------------------------------------------------------
1 |
2 | # `timecopilot.models.stats`
3 |
4 | ::: timecopilot.models.stats
5 | options:
6 | members:
7 | - ADIDA
8 | - AutoARIMA
9 | - AutoCES
10 | - AutoETS
11 | - CrostonClassic
12 | - DynamicOptimizedTheta
13 | - HistoricAverage
14 | - IMAPA
15 | - SeasonalNaive
16 | - Theta
17 | - ZeroModel
--------------------------------------------------------------------------------
/docs/changelogs/v0.0.16.md:
--------------------------------------------------------------------------------
1 | ### Fixes
2 |
3 | * **OpenAI Package Version Pinned**: Recent releases of the OpenAI package have been causing a `TypeError: Cannot instantiate typing.Union`. For more information, see [this issue](https://github.com/pydantic/pydantic-ai/issues/2476). Details can be found in [#167](https://github.com/TimeCopilot/timecopilot/pull/167).
4 |
5 | ---
6 |
7 | **Full Changelog**: https://github.com/TimeCopilot/timecopilot/compare/v0.0.15...v0.0.16
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yaml:
--------------------------------------------------------------------------------
1 | # Enable blank issues
2 | blank_issues_enabled: true
3 |
4 | # Contact links
5 | contact_links:
6 | - name: 💬 Join Discord
7 | url: "https://discord.gg/7GEdHR6Pfg"
8 | about: Ask questions and get help about TimeCopilot outside of GitHub issues.
9 | - name: 📖 Documentation
10 | url: "https://timecopilot.dev/"
11 | about: Check the docs before opening an issue.
12 | - name: 🙌 Contributing Guide
13 | url: "https://timecopilot.dev/contributing/"
14 | about: Learn how to contribute to TimeCopilot.
--------------------------------------------------------------------------------
/experiments/fev/src/download_results.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 |
3 | from .evaluate_model import tasks
4 |
5 |
6 | def download_results():
7 | summaries = []
8 | for task in tasks():
9 | csv_path = f"s3://timecopilot-fev/results/{task.dataset_config}.csv"
10 | df = pd.read_csv(csv_path)
11 | summaries.append(df)
12 | # Show and save the results
13 | df = pd.concat(summaries)
14 | print(df)
15 | df.to_csv("timecopilot.csv", index=False)
16 |
17 |
18 | if __name__ == "__main__":
19 | download_results()
20 |
--------------------------------------------------------------------------------
/timecopilot/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .stats import (
2 | ADIDA,
3 | IMAPA,
4 | AutoARIMA,
5 | AutoCES,
6 | AutoETS,
7 | CrostonClassic,
8 | DynamicOptimizedTheta,
9 | HistoricAverage,
10 | SeasonalNaive,
11 | Theta,
12 | ZeroModel,
13 | )
14 |
15 | __all__ = [
16 | "ADIDA",
17 | "IMAPA",
18 | "AutoARIMA",
19 | "AutoCES",
20 | "AutoETS",
21 | "CrostonClassic",
22 | "DynamicOptimizedTheta",
23 | "HistoricAverage",
24 | "SeasonalNaive",
25 | "Theta",
26 | "ZeroModel",
27 | ]
28 |
--------------------------------------------------------------------------------
/docs/blog/.authors.yml:
--------------------------------------------------------------------------------
1 | authors:
2 | azulgarza:
3 | name: Azul Garza
4 | description: Co-Creator
5 | avatar: https://avatars.githubusercontent.com/u/10517170
6 | url: https://github.com/AzulGarza
7 | reneerosillo:
8 | name: Renee Rosillo
9 | description: Co-Creator
10 | avatar: https://media.licdn.com/dms/image/v2/D4E03AQGN9Z-UddZReg/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1727908622546?e=1766016000&v=beta&t=zDLuE5S7jSlE_xDfQwPSTJnZjIi-BL_fzO44f8VnMkg
11 | url: https://www.linkedin.com/in/reneerosillo/
12 |
--------------------------------------------------------------------------------
/docs/getting-started/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | TimeCopilot is available on PyPI as [timecopilot](https://pypi.org/project/timecopilot/) and can be installed with a single command:
4 |
5 | === "pip"
6 |
7 | ```bash
8 | pip install timecopilot
9 | ```
10 |
11 | === "uv"
12 |
13 | ```bash
14 | uv add timecopilot
15 | ```
16 |
17 |
18 | Requires Python 3.10 or later.
19 |
20 | !!! tip
21 |
22 | If you don't have a prior experience with `uv`, go to [uv getting started](https://docs.astral.sh/uv/getting-started/) section.
23 |
24 |
--------------------------------------------------------------------------------
/docs/getting-started/introduction.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | TimeCopilot is a generative agent that applies a systematic forecasting approach using large language models (LLMs) to:
4 |
5 | - Interpret statistical features and patterns
6 | - Guide model selection based on data characteristics
7 | - Explain technical decisions in natural language
8 | - Answer domain-specific questions about forecasts
9 |
10 | Here is an schematic of TimeCopilot's architecture:
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.github/deploy_docs.py:
--------------------------------------------------------------------------------
1 | import os
2 | import subprocess
3 |
4 | import modal
5 |
6 | app_name = "timecopilot.dev"
7 | preview = os.environ.get("PREVIEW_DEPLOY", "false").lower() == "true"
8 | if preview:
9 | app_name = f"preview.{app_name}"
10 |
11 |
12 | app = modal.App(name=app_name)
13 |
14 |
15 | @app.function(
16 | image=modal.Image.debian_slim()
17 | .add_local_dir("site", remote_path="/root/site", copy=True)
18 | .workdir("/root/site")
19 | )
20 | @modal.web_server(8000, custom_domains=[app_name])
21 | def run():
22 | cmd = "python -m http.server 8000"
23 | subprocess.Popen(cmd, shell=True)
24 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | fail_fast: true
2 | repos:
3 | - repo: https://github.com/astral-sh/ruff-pre-commit
4 | rev: v0.2.1
5 | hooks:
6 | - id: ruff
7 | args: [--fix]
8 | - id: ruff-format
9 | - repo: https://github.com/pre-commit/mirrors-mypy
10 | rev: v1.8.0
11 | hooks:
12 | - id: mypy
13 | args: [--ignore-missing-imports, --check-untyped-defs]
14 | additional_dependencies: [types-PyYAML, types-requests]
15 | - repo: https://github.com/pappasam/toml-sort
16 | rev: v0.24.2
17 | hooks:
18 | - id: toml-sort
19 | args: ["--all", "--trailing-comma-inline-array", "--in-place"]
20 |
--------------------------------------------------------------------------------
/docs/stylesheets/extra.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --md-primary-fg-color: #2080ff;
3 | --md-primary-fg-color--light: #4c9aff;
4 | --md-primary-fg-color--dark: #1165e5;
5 |
6 | --md-accent-fg-color: var(--md-primary-fg-color);
7 | }
8 |
9 | [data-md-color-scheme="slate"] {
10 | --md-primary-fg-color: #2080ff;
11 | --md-primary-fg-color--light: #4c9aff;
12 | --md-primary-fg-color--dark: #1165e5;
13 |
14 | --md-default-bg-color: #0d1117;
15 | --md-code-bg-color: #161b22;
16 | --md-footer-bg-color: #0d1117;
17 | --md-sidebar-bg-color: #161b22;
18 | --md-header-bg-color: #0d1117;
19 |
20 | --md-typeset-color: #c9d1d9;
21 | --md-typeset-a-color: var(--md-primary-fg-color);
22 | }
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-docs.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy Docs
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 | workflow_dispatch:
8 |
9 | jobs:
10 | deploy:
11 | name: Deploy Docs
12 | runs-on: ubuntu-latest
13 | env:
14 | MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
15 | MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
16 | MODAL_ENVIRONMENT: ${{ secrets.MODAL_ENVIRONMENT }}
17 | steps:
18 | - name: Clone repo
19 | uses: actions/checkout@v4
20 |
21 | - name: Set up uv
22 | uses: astral-sh/setup-uv@v6
23 |
24 | - name: Build docs
25 | run: uv run --group docs mkdocs build
26 |
27 | - name: Deploy docs
28 | run: uv run --group docs modal deploy .github/deploy_docs.py
29 |
--------------------------------------------------------------------------------
/docs/changelogs/index.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | Welcome to the TimeCopilot Changelog. Here, you will find a comprehensive list of all the changes, updates, and improvements made to the TimeCopilot project. This section is designed to keep you informed about the latest features, bug fixes, and enhancements as we continue to develop and refine the TimeCopilot experience. Stay tuned for regular updates and feel free to explore the details of each release below.
4 |
5 |
6 | - [v0.0.22](v0.0.22.md)
7 | - [v0.0.21](v0.0.21.md)
8 | - [v0.0.20](v0.0.20.md)
9 | - [v0.0.19](v0.0.19.md)
10 | - [v0.0.18](v0.0.18.md)
11 | - [v0.0.17](v0.0.17.md)
12 | - [v0.0.16](v0.0.16.md)
13 | - [v0.0.15](v0.0.15.md)
14 | - [v0.0.14](v0.0.14.md)
15 | - [v0.0.13](v0.0.13.md)
16 | - [v0.0.12](v0.0.12.md)
17 | - [v0.0.11](v0.0.11.md)
18 | - [v0.0.10](v0.0.10.md)
--------------------------------------------------------------------------------
/.github/workflows/preview-docs.yaml:
--------------------------------------------------------------------------------
1 | name: Preview Docs
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | tags:
8 | - v*
9 | workflow_dispatch:
10 |
11 | jobs:
12 | preview:
13 | name: Preview Docs
14 | runs-on: ubuntu-latest
15 | env:
16 | MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
17 | MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
18 | MODAL_ENVIRONMENT: ${{ secrets.MODAL_ENVIRONMENT }}
19 | PREVIEW_DEPLOY: true
20 | steps:
21 | - name: Clone repo
22 | uses: actions/checkout@v4
23 |
24 | - name: Set up uv
25 | uses: astral-sh/setup-uv@v6
26 |
27 | - name: Build docs
28 | run: uv run --group docs mkdocs build
29 |
30 | - name: Preview docs
31 | run: uv run --group docs modal deploy .github/deploy_docs.py
32 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [AzulGarza] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/docs/changelogs/v0.0.21.md:
--------------------------------------------------------------------------------
1 | ### Features
2 |
3 | * **AWS's Chronos-2 foundational model**: [Chronos-2](https://arxiv.org/abs/2510.15821) has been added to the foundational models hub. Chronos-2 is a 120M parameter time series foundation model from AWS that provides state-of-the-art forecasting capabilities. You can now use Chronos-2 through the existing Chronos interface. Refer to [#245](https://github.com/TimeCopilot/timecopilot/pull/245) for more details.
4 |
5 | ```python
6 | import pandas as pd
7 | from timecopilot.models.foundation.chronos import Chronos
8 |
9 | df = pd.read_csv(
10 | "https://timecopilot.s3.amazonaws.com/public/data/events_pageviews.csv",
11 | parse_dates=["ds"],
12 | )
13 | # Use Chronos-2
14 | model = Chronos(repo_id="s3://autogluon/chronos-2")
15 | fcst = model.forecast(df, h=12)
16 | print(fcst)
17 | ```
18 |
19 | ---
20 |
21 | **Full Changelog**: https://github.com/TimeCopilot/timecopilot/compare/v0.0.20...v0.0.21
22 |
--------------------------------------------------------------------------------
/docs/changelogs/v0.0.22.md:
--------------------------------------------------------------------------------
1 | ### Documentation
2 |
3 | * **Windows __main__ guard note**: Added documentation note on using `__main__` guard for Windows users to ensure proper script execution. Thanks to @Rafipilot for the contribution! See [#250](https://github.com/TimeCopilot/timecopilot/pull/250).
4 |
5 | ### Fixes
6 |
7 | * **GPU support**: Fixed GPU device mapping to use `cuda:0` instead of `gpu` for better compatibility with PyTorch's device handling. See [#252](https://github.com/TimeCopilot/timecopilot/pull/252).
8 |
9 | * **Chronos-2 batch unpacking**: Fixed an issue where Chronos-2 model predictions weren't properly unpacked from batches, ensuring correct forecast output. See [#253](https://github.com/TimeCopilot/timecopilot/pull/253).
10 |
11 | ## New Contributors
12 |
13 | * @Rafipilot made their first contribution in [#250](https://github.com/TimeCopilot/timecopilot/pull/250)
14 |
15 | ---
16 |
17 | **Full Changelog**: https://github.com/TimeCopilot/timecopilot/compare/v0.0.21...v0.0.22
18 |
19 |
--------------------------------------------------------------------------------
/tests/gift_eval/conftest.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import pandas as pd
4 | import pytest
5 |
6 | from timecopilot.gift_eval.eval import GIFTEval
7 |
8 |
9 | @pytest.fixture(scope="session")
10 | def cache_path() -> Path:
11 | cache_path = Path(".pytest_cache") / "gift_eval"
12 | cache_path.mkdir(parents=True, exist_ok=True)
13 | return cache_path
14 |
15 |
16 | @pytest.fixture(scope="session")
17 | def all_results_df(cache_path: Path) -> pd.DataFrame:
18 | all_results_file = cache_path / "seasonal_naive_all_results.csv"
19 | if not all_results_file.exists():
20 | all_results_df = pd.read_csv(
21 | "https://huggingface.co/spaces/Salesforce/GIFT-Eval/raw/main/results/seasonal_naive/all_results.csv"
22 | )
23 | all_results_df.to_csv(all_results_file, index=False)
24 | return pd.read_csv(all_results_file)
25 |
26 |
27 | @pytest.fixture(scope="session")
28 | def storage_path(cache_path: Path) -> Path:
29 | GIFTEval.download_data(cache_path)
30 | return cache_path
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature.yaml:
--------------------------------------------------------------------------------
1 | # Template adapted from Pydantic-AI issue templates
2 | # Source: https://github.com/pydantic/pydantic-ai
3 |
4 | name: TimeCopilot - Feature Request
5 | description: Suggest a new feature to implement
6 | title: "feat: "
7 | labels: ["enhancement"]
8 |
9 | body:
10 | - type: markdown
11 | attributes:
12 | value: We appreciate your contribution to TimeCopilot!
13 |
14 | - type: textarea
15 | id: description
16 | attributes:
17 | label: Description
18 | description: |
19 | Tell us about the feature you'd like to implement. You might include:
20 | * A demo or example
21 | * Relevant use case(s)
22 | * References to similar projects or features
23 | validations:
24 | required: true
25 |
26 | - type: textarea
27 | id: references
28 | attributes:
29 | label: Additional context
30 | description: |
31 | Describe any alternative solutions or features you've considered.
32 | Include any other context about the feature request here.
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.yaml:
--------------------------------------------------------------------------------
1 | # Template adapted from Pydantic-AI issue templates
2 | # Source: https://github.com/pydantic/pydantic-ai
3 |
4 | name: TimeCopilot - Question
5 | description: "Ask a question about TimeCopilot"
6 | title: "question: "
7 | labels: ["question"]
8 |
9 | body:
10 | - type: markdown
11 | attributes:
12 | value: How can we support you? 🤝
13 |
14 | - type: textarea
15 | id: question
16 | attributes:
17 | label: Description
18 | description: |
19 | Provide details about your question.
20 | * Code snippets showing what you've tried
21 | * Error messages you're encountering (if any)
22 | * Expected vs. actual behavior
23 | * Your use case and what you're trying to achieve
24 | validations:
25 | required: true
26 |
27 | - type: textarea
28 | id: context
29 | attributes:
30 | label: Context
31 | description: |
32 | Provide additional information.
33 | * Python and TimeCopilot's version
34 | * Relevant configuration details
--------------------------------------------------------------------------------
/docs/api/models/foundation/models.md:
--------------------------------------------------------------------------------
1 | # `timecopilot.models.foundation`
2 |
3 | ::: timecopilot.models.foundation.chronos
4 | options:
5 | members:
6 | - Chronos
7 |
8 | ::: timecopilot.models.foundation.flowstate
9 | options:
10 | members:
11 | - FlowState
12 |
13 | ::: timecopilot.models.foundation.moirai
14 | options:
15 | members:
16 | - Moirai
17 |
18 | ::: timecopilot.models.foundation.sundial
19 | options:
20 | members:
21 | - Sundial
22 |
23 | ::: timecopilot.models.foundation.tabpfn
24 | options:
25 | members:
26 | - TabPFN
27 |
28 | ::: timecopilot.models.foundation.timegpt
29 | options:
30 | members:
31 | - TimeGPT
32 |
33 | ::: timecopilot.models.foundation.timesfm
34 | options:
35 | members:
36 | - TimesFM
37 |
38 | ::: timecopilot.models.foundation.tirex
39 | options:
40 | members:
41 | - TiRex
42 |
43 | ::: timecopilot.models.foundation.toto
44 | options:
45 | members:
46 | - Toto
--------------------------------------------------------------------------------
/docs/community/help.md:
--------------------------------------------------------------------------------
1 | # Getting help
2 |
3 | We aim to provide comprehensive support for TimeCopilot users. Whether you need technical assistance, want to report a bug, or just want to connect with other users, we have several channels available to help you.
4 |
5 | ## GitHub
6 |
7 | ### Issues
8 |
9 | If you encounter any bugs or have feature requests, please report them on our [issue tracker](https://github.com/TimeCopilot/timecopilot/issues).
10 |
11 | ### Discussions
12 |
13 | For general questions, ideas, and community discussions, join our [discussions](https://github.com/TimeCopilot/timecopilot/discussions) forum.
14 |
15 | ## Discord Server
16 |
17 | For real-time chat and community support, join our [Discord server](https://discord.gg/7GEdHR6Pfg).
18 |
19 | ## Note
20 | When using Windows, ensure to use the ` if __name__ == "__main__": ` guard to ensure that the code which starts new processes only runs when the script is executed directly, preventing child processes from re-importing the main module and causing a RuntimeError related to Python’s multiprocessing bootstrapping phase.
21 |
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Azul Garza
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/experiments/gift-eval/src/download_results.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from pathlib import Path
3 |
4 | import pandas as pd
5 |
6 | from timecopilot.gift_eval.utils import DATASETS_WITH_TERMS
7 |
8 | logging.basicConfig(level=logging.INFO)
9 |
10 |
11 | def download_results():
12 | bucket = "timecopilot-gift-eval"
13 |
14 | dfs = []
15 |
16 | for dataset_name, term in DATASETS_WITH_TERMS:
17 | csv_path = (
18 | f"s3://{bucket}/results/timecopilot/{dataset_name}/{term}/all_results.csv"
19 | )
20 | logging.info(f"Downloading {csv_path}")
21 | try:
22 | df = pd.read_csv(csv_path, storage_options={"anon": False})
23 | dfs.append(df)
24 | except Exception as e:
25 | logging.error(f"Error downloading {csv_path}: {e}")
26 |
27 | df = pd.concat(dfs, ignore_index=True)
28 | output_dir = Path("results/timecopilot")
29 | output_dir.mkdir(parents=True, exist_ok=True)
30 | df.to_csv(output_dir / "all_results.csv", index=False)
31 | logging.info(f"Saved results to {output_dir / 'all_results.csv'}")
32 |
33 |
34 | if __name__ == "__main__":
35 | download_results()
36 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 | workflow_dispatch:
8 |
9 | jobs:
10 | pypi:
11 | name: Publish to PyPI
12 | runs-on: ubuntu-latest
13 | environment:
14 | name: release
15 | permissions:
16 | contents: read
17 | id-token: write
18 | steps:
19 | - name: Clone repo
20 | uses: actions/checkout@v4
21 |
22 | - name: Set up uv
23 | uses: astral-sh/setup-uv@v6
24 |
25 | - name: Build package
26 | run: uv build
27 |
28 | - name: Perform import test (wheel)
29 | run: uv run --isolated --no-project -p 3.13 --with dist/*.whl -- python -c "from timecopilot import TimeCopilot"
30 |
31 | - name: Perform import test (source distribution)
32 | run: uv run --isolated --no-project -p 3.13 --with dist/*.tar.gz -- python -c "from timecopilot import TimeCopilot"
33 |
34 | - name: Publish package
35 | run: uv publish --trusted-publishing always
36 |
37 | - name: After publishing test
38 | run: uv run --isolated --no-project -p 3.13 --with timecopilot -- python -c "from timecopilot import TimeCopilot"
--------------------------------------------------------------------------------
/docs/changelogs/v0.0.15.md:
--------------------------------------------------------------------------------
1 | ### Features
2 |
3 | * **Sundial foundational model**: The [Sundial model](https://github.com/thuml/Sundial) has been added to the foundational models hub. Refer to [#157](https://github.com/TimeCopilot/timecopilot/pull/157) for more details.
4 |
5 | ```python
6 | import pandas as pd
7 | from timecopilot.models.foundational.sundial import Sundial
8 |
9 | df = pd.read_csv(
10 | "https://timecopilot.s3.amazonaws.com/public/data/algeria_exports.csv",
11 | parse_dates=["ds"],
12 | )
13 | model = Sundial()
14 | fcst = model.forecast(df, h=12)
15 | print(fcst)
16 | ```
17 |
18 | ### Fixes
19 |
20 | * **Enhanced GIFT-Eval Experiment**: The experiment now omits models identified as having potential data leakage, following [the latest update](https://github.com/SalesforceAIResearch/gift-eval?tab=readme-ov-file#2025-08-05) to the official evaluation criteria. For more information, refer to [#158](https://github.com/TimeCopilot/timecopilot/pull/158) and [experiments/gift-eval](https://github.com/TimeCopilot/timecopilot/tree/main/experiments/gift-eval).
21 |
22 | ---
23 |
24 | **Full Changelog**: https://github.com/TimeCopilot/timecopilot/compare/v0.0.14...v0.0.15
--------------------------------------------------------------------------------
/.github/workflows/notify-release.yaml:
--------------------------------------------------------------------------------
1 | name: Notify Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 | workflow_dispatch:
8 | inputs:
9 | tag:
10 | description: "Release tag to simulate"
11 | required: true
12 |
13 | jobs:
14 | discord:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | - name: Prepare env
20 | run: |
21 | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
22 | echo "TAG=${{ github.event.inputs.tag }}" >> "$GITHUB_ENV"
23 | else
24 | echo "TAG=${GITHUB_REF_NAME}" >> "$GITHUB_ENV"
25 | fi
26 |
27 | - name: Read changelog
28 | id: changelog
29 | uses: andstor/file-reader-action@v1
30 | with:
31 | path: 'docs/changelogs/${{ env.TAG }}.md'
32 |
33 | - name: Notify Discord
34 | uses: rjstone/discord-webhook-notify@v2
35 | with:
36 | webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
37 | username: TimeCopilot Releases
38 | severity: info
39 | title: "🚀 New release ${{ env.TAG }}"
40 | description: "Changelog also available at https://timecopilot.dev/changelogs/${{ env.TAG }}/"
41 | details: ${{ steps.changelog.outputs.contents }}
42 | footer: "pushed via GitHub Actions"
43 |
--------------------------------------------------------------------------------
/docs/changelogs/v0.0.14.md:
--------------------------------------------------------------------------------
1 | ### Experiments
2 |
3 | * **Median Ensemble Experiment on GIFT-Eval**: Introduced a median ensemble experiment on GIFT-Eval. For more details, refer to [#152](https://github.com/TimeCopilot/timecopilot/pull/152).
4 |
5 | ### Features
6 |
7 | * **Custom Forecaster Integration**: Users can now integrate specific models into the agent for evaluation and utilization through the `forecasters` argument in TimeCopilot's constructor. See [#153](https://github.com/TimeCopilot/timecopilot/pull/153).
8 | ```python
9 | from timecopilot import TimeCopilot
10 | from timecopilot.models.benchmarks import SeasonalNaive
11 | from timecopilot.models.foundational.toto import Toto
12 |
13 | tc = TimeCopilot(
14 | llm="openai:gpt-4o",
15 | forecasters=[
16 | SeasonalNaive(),
17 | Toto(),
18 | ]
19 | )
20 | tc.forecast(
21 | df="https://timecopilot.s3.amazonaws.com/public/data/air_passengers.csv",
22 | h=12,
23 | )
24 | result = tc.query("What is the best model for monthly data?")
25 | print(result.output)
26 | ```
27 |
28 | ### Fixes
29 |
30 | * **Restore CLI Prettify Functionality**: Resolved an issue with the `prettify` method affecting the CLI. This fix is included in the current release. See [#154](https://github.com/TimeCopilot/timecopilot/pull/154).
31 |
32 | ---
33 |
34 | **Full Changelog**: https://github.com/TimeCopilot/timecopilot/compare/v0.0.13...v0.0.14
--------------------------------------------------------------------------------
/experiments/fev/src/evaluate_model_modal.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import fev
4 | import modal
5 |
6 | app = modal.App(name="timecopilot-fev")
7 | image = (
8 | modal.Image.from_registry(
9 | "nvidia/cuda:12.8.1-devel-ubuntu24.04",
10 | add_python="3.11",
11 | )
12 | # uploaded to s3 by makefile
13 | .apt_install("git")
14 | .pip_install("uv")
15 | .add_local_file("pyproject.toml", "/root/pyproject.toml", copy=True)
16 | .add_local_file(".python-version", "/root/.python-version", copy=True)
17 | .add_local_file("uv.lock", "/root/uv.lock", copy=True)
18 | .workdir("/root")
19 | .run_commands("uv pip install . --system --compile-bytecode")
20 | )
21 | aws_secret = modal.Secret.from_name(
22 | "aws-secret",
23 | required_keys=["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
24 | )
25 | volume = {
26 | "/s3-bucket": modal.CloudBucketMount(
27 | bucket_name="timecopilot-fev",
28 | secret=aws_secret,
29 | )
30 | }
31 |
32 |
33 | @app.function(
34 | image=image,
35 | volumes=volume,
36 | # 3 hours timeout
37 | timeout=60 * 60 * 3,
38 | gpu="A10G",
39 | # as my local
40 | cpu=8,
41 | secrets=[modal.Secret.from_name("hf-secret")],
42 | )
43 | def evaluate_task_modal(task: fev.Task):
44 | from .evaluate_model import evaluate_task
45 |
46 | evaluation_summary = evaluate_task(task=task)
47 | save_path = Path(f"/s3-bucket/results/{task.dataset_config}.csv")
48 | save_path.parent.mkdir(parents=True, exist_ok=True)
49 | evaluation_summary.to_csv(save_path, index=False)
50 |
51 |
52 | @app.local_entrypoint()
53 | def main():
54 | from .evaluate_model import tasks
55 |
56 | list(evaluate_task_modal.map(tasks()[:2]))
57 |
--------------------------------------------------------------------------------
/tests/test_agent.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import pytest
4 | from pydantic_ai.messages import ModelMessage, ModelResponse, ToolCallPart
5 | from pydantic_ai.models.function import AgentInfo, FunctionModel
6 | from utilsforecast.data import generate_series
7 |
8 | from timecopilot.agent import ForecastAgentOutput, TimeCopilot
9 |
10 |
11 | def build_stub_llm(output: dict) -> FunctionModel: # noqa: D401
12 | def _response_fn(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse: # noqa: D401
13 | payload = json.dumps(output)
14 | return ModelResponse(
15 | parts=[ToolCallPart(tool_name="final_result", args=payload)]
16 | )
17 |
18 | return FunctionModel(_response_fn)
19 |
20 |
21 | @pytest.mark.parametrize("query", [None, "dummy"])
22 | def test_forecast_returns_expected_output(query):
23 | df = generate_series(n_series=1, freq="D", min_length=30)
24 | expected_output = {
25 | "tsfeatures_analysis": "ok",
26 | "selected_model": "ZeroModel",
27 | "model_details": "details",
28 | "model_comparison": "cmp",
29 | "is_better_than_seasonal_naive": True,
30 | "reason_for_selection": "reason",
31 | "forecast_analysis": "analysis",
32 | "anomaly_analysis": "anomaly",
33 | "user_query_response": query,
34 | }
35 | tc = TimeCopilot(llm=build_stub_llm(expected_output))
36 | tc.fcst_df = None
37 | tc.eval_df = None
38 | tc.features_df = None
39 | tc.anomalies_df = None
40 | result = tc.forecast(df=df, h=2, freq="D", seasonality=7, query=query)
41 |
42 | assert result.output == ForecastAgentOutput(**expected_output)
43 |
44 |
45 | def test_constructor_rejects_model_kwarg():
46 | with pytest.raises(ValueError):
47 | TimeCopilot(llm="test", model="something")
48 |
--------------------------------------------------------------------------------
/docs/changelogs/v0.0.20.md:
--------------------------------------------------------------------------------
1 | ### Features
2 |
3 | * **IBM's FlowState foundational model**: [FlowState](https://github.com/ibm-granite/granite-tsfm) has been added to the foundational models hub. FlowState is the first time-scale adjustable Time Series Foundation Model (TSFM), combining a State Space Model (SSM) Encoder with a Functional Basis Decoder for time-scale invariant forecasting. Refer to [#234](https://github.com/TimeCopilot/timecopilot/pull/234) for more details.
4 |
5 | ```python
6 | import pandas as pd
7 | from timecopilot.models.foundation.flowstate import FlowState
8 |
9 | df = pd.read_csv(
10 | "https://timecopilot.s3.amazonaws.com/public/data/events_pageviews.csv",
11 | parse_dates=["ds"],
12 | )
13 | # Use the commercial model
14 | model = FlowState(repo_id="ibm-granite/granite-timeseries-flowstate-r1")
15 | # Or use the research model
16 | # model = FlowState(repo_id="ibm-research/flowstate")
17 | fcst = model.forecast(df, h=12)
18 | print(fcst)
19 | ```
20 |
21 | ### Fixes
22 |
23 | * **TimesFM 2.5 integration**: Fixed compatibility issues with TimesFM 2.5 that were introduced in recent updates. The implementation now properly handles the new API changes in TimesFM 2.5. See [#235](https://github.com/TimeCopilot/timecopilot/pull/235).
24 |
25 | * **TimesFM loading from local path**: Fixed an issue where TimesFM models couldn't be loaded from local file paths. The model loading mechanism has been updated to properly handle both local and remote model sources. Thanks to @KilianZimmerer for the contribution! See [#230](https://github.com/TimeCopilot/timecopilot/pull/230).
26 |
27 | ## New Contributors
28 | * @KilianZimmerer made their first contribution in [#230](https://github.com/TimeCopilot/timecopilot/pull/230)
29 |
30 | ---
31 |
32 | **Full Changelog**: https://github.com/TimeCopilot/timecopilot/compare/v0.0.19...v0.0.20
33 |
--------------------------------------------------------------------------------
/tests/gift_eval/test_gift_eval.py:
--------------------------------------------------------------------------------
1 | import tempfile
2 | from pathlib import Path
3 |
4 | import pandas as pd
5 |
6 | from timecopilot.gift_eval.eval import GIFTEval
7 | from timecopilot.gift_eval.gluonts_predictor import GluonTSPredictor
8 | from timecopilot.models.stats import SeasonalNaive
9 |
10 |
11 | def test_concat_results(storage_path: Path):
12 | predictor = GluonTSPredictor(
13 | forecaster=SeasonalNaive(),
14 | batch_size=512,
15 | )
16 |
17 | def _evaluate_predictor(
18 | dataset_name: str,
19 | term: str,
20 | output_path: Path | str,
21 | overwrite_results: bool = False,
22 | ):
23 | gifteval = GIFTEval(
24 | dataset_name=dataset_name,
25 | term=term,
26 | output_path=output_path,
27 | storage_path=storage_path,
28 | )
29 | gifteval.evaluate_predictor(
30 | predictor,
31 | batch_size=512,
32 | overwrite_results=overwrite_results,
33 | )
34 |
35 | combinations = [
36 | ("m4_weekly", "short"),
37 | ("m4_hourly", "short"),
38 | ]
39 |
40 | with tempfile.TemporaryDirectory() as temp_dir:
41 | for i, (dataset_name, term) in enumerate(combinations):
42 | _evaluate_predictor(
43 | dataset_name=dataset_name,
44 | term=term,
45 | output_path=temp_dir,
46 | )
47 | eval_df = pd.read_csv(Path(temp_dir) / "all_results.csv")
48 | print(eval_df)
49 | assert len(eval_df) == i + 1
50 |
51 | _evaluate_predictor(
52 | dataset_name="m4_hourly",
53 | term="short",
54 | output_path=temp_dir,
55 | overwrite_results=True,
56 | )
57 | eval_df = pd.read_csv(Path(temp_dir) / "all_results.csv")
58 | assert len(eval_df) == 1
59 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.yaml:
--------------------------------------------------------------------------------
1 | # Template adapted from Pydantic-AI issue templates
2 | # Source: https://github.com/pydantic/pydantic-ai
3 |
4 | name: TimeCopilot - Bug Report
5 | description: Report a bug or unexpected behavior
6 | title: "bug: "
7 | labels: bug
8 |
9 | body:
10 | - type: markdown
11 | attributes:
12 | value: We appreciate your contribution to TimeCopilot!
13 |
14 | - type: checkboxes
15 | id: checks
16 | attributes:
17 | label: Initial Checklist
18 | description: Ensure you comply with the following before moving ahead.
19 | options:
20 | - label: I confirm that I'm using the latest version of TimeCopilot.
21 | required: true
22 | - label: I confirm that I've reviewed my issue at https://github.com/AzulGarza/timecopilot/issues before opening this one.
23 | required: true
24 |
25 | - type: textarea
26 | id: description
27 | attributes:
28 | label: What happened?
29 | description: |
30 | Explain what you're seeing and the outcome you expect.
31 | placeholder: Tell us what you see!
32 | value: |
33 | Steps to reproduce the behavior...
34 | 1. Go to '...'
35 | 2. Click on '....'
36 | 3. Scroll down to '....'
37 | 4. See error
38 | validations:
39 | required: true
40 |
41 | - type: textarea
42 | id: logs
43 | attributes:
44 | label: Relevant log output
45 | description: Copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
46 | render: shell
47 |
48 | - type: textarea
49 | id: environment
50 | attributes:
51 | label: Environment
52 | description: |
53 | Which version of TimeCopilot are you using? Specify details of your local environment such as OS, architecture, Python and TimeCopilot's version.
54 | render: Text
55 | validations:
56 | required: true
--------------------------------------------------------------------------------
/timecopilot/models/foundation/utils.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Iterable
2 |
3 | import pandas as pd
4 | import torch
5 | from utilsforecast.processing import make_future_dataframe
6 |
7 |
8 | class TimeSeriesDataset:
9 | def __init__(
10 | self,
11 | data: torch.Tensor,
12 | uids: Iterable,
13 | last_times: Iterable,
14 | batch_size: int,
15 | ):
16 | self.data = data
17 | self.uids = uids
18 | self.last_times = last_times
19 | self.batch_size = batch_size
20 | self.n_batches = len(data) // self.batch_size + (
21 | 0 if len(data) % self.batch_size == 0 else 1
22 | )
23 | self.current_batch = 0
24 |
25 | @classmethod
26 | def from_df(
27 | cls,
28 | df: pd.DataFrame,
29 | batch_size: int,
30 | dtype: torch.dtype = torch.bfloat16,
31 | ):
32 | tensors = []
33 | df_sorted = df.sort_values(by=["unique_id", "ds"])
34 | for _, group in df_sorted.groupby("unique_id"):
35 | tensors.append(torch.tensor(group["y"].values, dtype=dtype))
36 | uids = df_sorted["unique_id"].unique()
37 | last_times = df_sorted.groupby("unique_id")["ds"].tail(1)
38 | return cls(tensors, uids, last_times, batch_size)
39 |
40 | def __len__(self):
41 | return self.n_batches
42 |
43 | def make_future_dataframe(self, h: int, freq: str) -> pd.DataFrame:
44 | return make_future_dataframe(
45 | uids=self.uids,
46 | last_times=pd.to_datetime(self.last_times),
47 | h=h,
48 | freq=freq,
49 | ) # type: ignore
50 |
51 | def __iter__(self):
52 | self.current_batch = 0 # Reset for new iteration
53 | return self
54 |
55 | def __next__(self):
56 | if self.current_batch < self.n_batches:
57 | start_idx = self.current_batch * self.batch_size
58 | end_idx = start_idx + self.batch_size
59 | self.current_batch += 1
60 | return self.data[start_idx:end_idx]
61 | else:
62 | raise StopIteration
63 |
--------------------------------------------------------------------------------
/docs/changelogs/v0.0.19.md:
--------------------------------------------------------------------------------
1 | ### Features
2 |
3 | * **Google's TimesFM 2.5 foundational model**: [TimesFM 2.5](https://github.com/google-research/timesfm) has been added to the foundational models hub. Refer to [#224](https://github.com/TimeCopilot/timecopilot/pull/224) for more details.
4 |
5 | ```python
6 | import pandas as pd
7 | from timecopilot.models.foundation.timesfm import TimesFM
8 |
9 | df = pd.read_csv(
10 | "https://timecopilot.s3.amazonaws.com/public/data/events_pageviews.csv",
11 | parse_dates=["ds"],
12 | )
13 | model = TimesFM(repo_id="google/timesfm-2.5-200m-pytorch")
14 | fcst = model.forecast(df, h=12)
15 | print(fcst)
16 | ```
17 |
18 | * **Improved agent prompt**: Enhanced the agent prompt for better performance and accuracy. See [#218](https://github.com/TimeCopilot/timecopilot/pull/218).
19 |
20 | ### Fixes
21 |
22 | * **Correct pydantic-ai library**: Fixed the pydantic-ai library dependency to ensure proper functionality. See [#221](https://github.com/TimeCopilot/timecopilot/pull/221).
23 |
24 | * **TimesFM quantiles**: Fixed quantile handling for TimesFM models to ensure correct probabilistic forecasts. See [#225](https://github.com/TimeCopilot/timecopilot/pull/225).
25 |
26 | ### Documentation
27 |
28 | * **Time series foundation models comparison example**: Added comprehensive example on how to compare time series foundational models. See [#227](https://github.com/TimeCopilot/timecopilot/pull/227).
29 |
30 | * **Revamped static site**: Major improvements to the static documentation site with better design and navigation. See [#226](https://github.com/TimeCopilot/timecopilot/pull/226).
31 |
32 | ### Infrastructure
33 |
34 | * **Documentation tests across Python versions**: Added testing for documentation examples across multiple Python versions to ensure compatibility. See [#220](https://github.com/TimeCopilot/timecopilot/pull/220).
35 |
36 | * **S3 URL standardization**: Updated to use S3 URLs instead of URIs for better consistency. See [#222](https://github.com/TimeCopilot/timecopilot/pull/222).
37 |
38 | ---
39 |
40 | **Full Changelog**: https://github.com/TimeCopilot/timecopilot/compare/v0.0.18...v0.0.19
--------------------------------------------------------------------------------
/experiments/gift-eval/src/run_timecopilot.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import Annotated
3 |
4 | import typer
5 |
6 | from timecopilot.gift_eval.eval import GIFTEval
7 | from timecopilot.gift_eval.gluonts_predictor import GluonTSPredictor
8 | from timecopilot.models.ensembles.median import MedianEnsemble
9 | from timecopilot.models.foundation.chronos import Chronos
10 | from timecopilot.models.foundation.timesfm import TimesFM
11 | from timecopilot.models.foundation.tirex import TiRex
12 |
13 | logging.basicConfig(level=logging.INFO)
14 |
15 |
16 | app = typer.Typer()
17 |
18 |
19 | @app.command()
20 | def run_timecopilot(
21 | dataset_name: Annotated[
22 | str,
23 | typer.Option(help="The name of the dataset to evaluate"),
24 | ],
25 | term: Annotated[
26 | str,
27 | typer.Option(help="The term to evaluate"),
28 | ],
29 | output_path: Annotated[
30 | str,
31 | typer.Option(help="The directory to save the results"),
32 | ],
33 | storage_path: Annotated[
34 | str,
35 | typer.Option(help="The directory were the GIFT data is stored"),
36 | ],
37 | ):
38 | logging.info(f"Running {dataset_name} {term} {output_path}")
39 | batch_size = 64
40 | predictor = GluonTSPredictor(
41 | forecaster=MedianEnsemble(
42 | models=[
43 | Chronos(
44 | repo_id="amazon/chronos-2",
45 | batch_size=batch_size,
46 | ),
47 | TimesFM(
48 | repo_id="google/timesfm-2.5-200m-pytorch",
49 | batch_size=batch_size,
50 | ),
51 | TiRex(
52 | batch_size=batch_size,
53 | ),
54 | ],
55 | alias="TimeCopilot",
56 | ),
57 | max_length=4_096,
58 | # data batch size
59 | batch_size=1_024,
60 | )
61 | gifteval = GIFTEval(
62 | dataset_name=dataset_name,
63 | term=term,
64 | output_path=output_path,
65 | storage_path=storage_path,
66 | )
67 | gifteval.evaluate_predictor(predictor, batch_size=512)
68 |
69 |
70 | if __name__ == "__main__":
71 | app()
72 |
--------------------------------------------------------------------------------
/tests/docs/test_docs.py:
--------------------------------------------------------------------------------
1 | import re
2 | import sys
3 | from pathlib import Path
4 |
5 | import pytest
6 | from mktestdocs import check_md_file
7 |
8 |
9 | @pytest.mark.docs
10 | @pytest.mark.parametrize(
11 | "fpath",
12 | [p for p in Path("docs").rglob("*.md") if "changelogs" not in p.parts],
13 | ids=str,
14 | )
15 | @pytest.mark.flaky(reruns=3, reruns_delay=80)
16 | def test_docs(fpath):
17 | check_md_file(fpath=fpath, memory=True)
18 |
19 |
20 | @pytest.mark.docs
21 | @pytest.mark.flaky(reruns=3, reruns_delay=80)
22 | def test_readme():
23 | check_md_file("README.md", memory=True)
24 |
25 |
26 | @pytest.mark.docs
27 | def test_latest_changelog():
28 | def version_key(filename):
29 | match = re.search(r"(\d+\.\d+\.\d+)", str(filename))
30 | if match:
31 | version_string = match.group(1)
32 | return tuple(map(int, version_string.split(".")))
33 | return (0, 0, 0)
34 |
35 | changelog_dir = Path("docs/changelogs")
36 | changelogs = sorted(changelog_dir.glob("v*.md"), key=version_key)
37 | latest_changelog = changelogs[-1] if changelogs else None
38 | check_md_file(latest_changelog, memory=True)
39 |
40 |
41 | @pytest.mark.docs
42 | @pytest.mark.flaky(reruns=3, reruns_delay=80)
43 | @pytest.mark.parametrize(
44 | "fpath",
45 | Path("timecopilot").glob("**/*.py"),
46 | ids=str,
47 | )
48 | def test_py_examples(fpath):
49 | check_md_file(fpath=fpath, memory=True)
50 |
51 |
52 | skip_gift_eval_mark = pytest.mark.skipif(
53 | sys.version_info >= (3, 13),
54 | reason="gift-eval notebook not supported on Python 3.13",
55 | )
56 |
57 |
58 | def maybe_skip_gift_eval(fpath):
59 | out = str(fpath)
60 | if out == "docs/examples/gift-eval.ipynb":
61 | out = pytest.param(out, marks=skip_gift_eval_mark)
62 | return out
63 |
64 |
65 | # skipping notebooks for now, as they rise a no space error
66 | # see: https://github.com/TimeCopilot/timecopilot/actions/runs/18858375517/job/53811527062?pr=245
67 | # @pytest.mark.docs
68 | # @pytest.mark.parametrize(
69 | # "fpath",
70 | # [maybe_skip_gift_eval(f) for f in Path("docs").rglob("*.ipynb")],
71 | # )
72 | # def test_notebooks(fpath):
73 | # nb = nbformat.read(fpath, as_version=4)
74 | # client = NotebookClient(nb, timeout=600, kernel_name="python3")
75 | # client.execute()
76 |
--------------------------------------------------------------------------------
/timecopilot/gift_eval/utils.py:
--------------------------------------------------------------------------------
1 | from itertools import product
2 |
3 | QUANTILE_LEVELS = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
4 |
5 | SHORT_DATASETS = [
6 | "m4_yearly",
7 | "m4_quarterly",
8 | "m4_monthly",
9 | "m4_weekly",
10 | "m4_daily",
11 | "m4_hourly",
12 | "electricity/15T",
13 | "electricity/H",
14 | "electricity/D",
15 | "electricity/W",
16 | "solar/10T",
17 | "solar/H",
18 | "solar/D",
19 | "solar/W",
20 | "hospital",
21 | "covid_deaths",
22 | "us_births/D",
23 | "us_births/M",
24 | "us_births/W",
25 | "saugeenday/D",
26 | "saugeenday/M",
27 | "saugeenday/W",
28 | "temperature_rain_with_missing",
29 | "kdd_cup_2018_with_missing/H",
30 | "kdd_cup_2018_with_missing/D",
31 | "car_parts_with_missing",
32 | "restaurant",
33 | "hierarchical_sales/D",
34 | "hierarchical_sales/W",
35 | "LOOP_SEATTLE/5T",
36 | "LOOP_SEATTLE/H",
37 | "LOOP_SEATTLE/D",
38 | "SZ_TAXI/15T",
39 | "SZ_TAXI/H",
40 | "M_DENSE/H",
41 | "M_DENSE/D",
42 | "ett1/15T",
43 | "ett1/H",
44 | "ett1/D",
45 | "ett1/W",
46 | "ett2/15T",
47 | "ett2/H",
48 | "ett2/D",
49 | "ett2/W",
50 | "jena_weather/10T",
51 | "jena_weather/H",
52 | "jena_weather/D",
53 | "bitbrains_fast_storage/5T",
54 | "bitbrains_fast_storage/H",
55 | "bitbrains_rnd/5T",
56 | "bitbrains_rnd/H",
57 | "bizitobs_application",
58 | "bizitobs_service",
59 | "bizitobs_l2c/5T",
60 | "bizitobs_l2c/H",
61 | ]
62 |
63 | MED_LONG_DATASETS = [
64 | "electricity/15T",
65 | "electricity/H",
66 | "solar/10T",
67 | "solar/H",
68 | "kdd_cup_2018_with_missing/H",
69 | "LOOP_SEATTLE/5T",
70 | "LOOP_SEATTLE/H",
71 | "SZ_TAXI/15T",
72 | "M_DENSE/H",
73 | "ett1/15T",
74 | "ett1/H",
75 | "ett2/15T",
76 | "ett2/H",
77 | "jena_weather/10T",
78 | "jena_weather/H",
79 | "bitbrains_fast_storage/5T",
80 | "bitbrains_rnd/5T",
81 | "bizitobs_application",
82 | "bizitobs_service",
83 | "bizitobs_l2c/5T",
84 | "bizitobs_l2c/H",
85 | ]
86 |
87 | ALL_DATASETS = SHORT_DATASETS + MED_LONG_DATASETS
88 |
89 |
90 | DATASETS_WITH_TERMS = [(dataset_name, "short") for dataset_name in SHORT_DATASETS]
91 | DATASETS_WITH_TERMS += [
92 | (dataset_name, term)
93 | for dataset_name, term in product(MED_LONG_DATASETS, ["medium", "long"])
94 | ]
95 |
--------------------------------------------------------------------------------
/tests/gift_eval/test_evaluation.py:
--------------------------------------------------------------------------------
1 | import random
2 | import tempfile
3 | from pathlib import Path
4 |
5 | import pandas as pd
6 | import pytest
7 |
8 | from timecopilot.gift_eval.eval import GIFTEval
9 | from timecopilot.gift_eval.gluonts_predictor import GluonTSPredictor
10 | from timecopilot.gift_eval.utils import DATASETS_WITH_TERMS
11 | from timecopilot.models.stats import SeasonalNaive
12 |
13 | TARGET_COLS = [
14 | "dataset",
15 | "model",
16 | "eval_metrics/MSE[mean]",
17 | "eval_metrics/MSE[0.5]",
18 | "eval_metrics/MAE[0.5]",
19 | "eval_metrics/MASE[0.5]",
20 | # can be unstable, due to division by zero
21 | # "eval_metrics/MAPE[0.5]",
22 | "eval_metrics/sMAPE[0.5]",
23 | "eval_metrics/MSIS",
24 | "eval_metrics/RMSE[mean]",
25 | "eval_metrics/NRMSE[mean]",
26 | "eval_metrics/ND[0.5]",
27 | "eval_metrics/mean_weighted_sum_quantile_loss",
28 | "domain",
29 | "num_variates",
30 | ]
31 |
32 |
33 | @pytest.mark.gift_eval
34 | def test_number_of_datasets(all_results_df: pd.DataFrame):
35 | assert len(DATASETS_WITH_TERMS) == len(all_results_df)
36 |
37 |
38 | @pytest.mark.gift_eval
39 | @pytest.mark.parametrize(
40 | "dataset_name, term",
41 | # testing 20 random datasets
42 | # each time to prevent longer running tests
43 | random.sample(DATASETS_WITH_TERMS, 20),
44 | )
45 | def test_evaluation(
46 | dataset_name: str,
47 | term: str,
48 | all_results_df: pd.DataFrame,
49 | storage_path: Path,
50 | ):
51 | predictor = GluonTSPredictor(
52 | forecaster=SeasonalNaive(
53 | # alias used by the official evaluation
54 | alias="Seasonal_Naive",
55 | ),
56 | batch_size=512,
57 | )
58 | with tempfile.TemporaryDirectory() as temp_dir:
59 | gifteval = GIFTEval(
60 | dataset_name=dataset_name,
61 | term=term,
62 | output_path=temp_dir,
63 | storage_path=storage_path,
64 | )
65 | gifteval.evaluate_predictor(
66 | predictor,
67 | batch_size=512,
68 | )
69 | eval_df = pd.read_csv(Path(temp_dir) / "all_results.csv")
70 | expected_eval_df = all_results_df.query("dataset == @gifteval.ds_config")
71 | assert not eval_df.isna().any().any()
72 | pd.testing.assert_frame_equal(
73 | eval_df.reset_index(drop=True)[TARGET_COLS],
74 | expected_eval_df.reset_index(drop=True)[TARGET_COLS],
75 | atol=1e-2,
76 | rtol=1e-2,
77 | check_dtype=False,
78 | )
79 |
--------------------------------------------------------------------------------
/experiments/fev/README.md:
--------------------------------------------------------------------------------
1 | # TimeCopilot `fev` Experiments
2 |
3 | This project demonstrates the evaluation of a foundation model ensemble built using the [TimeCopilot](https://timecopilot.dev) library on the [fev](https://github.com/autogluon/fev/) benchmark.
4 |
5 | TimeCopilot is an open‑source AI agent for time series forecasting that provides a unified interface to multiple forecasting approaches, from foundation models to classical statistical, machine learning, and deep learning methods, along with built‑in ensemble capabilities for robust and explainable forecasting.
6 |
7 | ## Model Description
8 |
9 | This ensemble leverages [**TimeCopilot's MedianEnsemble**](https://timecopilot.dev/api/models/ensembles/#timecopilot.models.ensembles.median.MedianEnsemble) feature, which combines two state-of-the-art foundation models:
10 |
11 | - [**TiRex** (NX-AI)](https://timecopilot.dev/api/models/foundation/models/#timecopilot.models.foundation.tirex.TiRex)
12 | - [**Chronos** (AWS AI Labs)](https://timecopilot.dev/api/models/foundation/models/#timecopilot.models.foundation.chronos.Chronos)
13 |
14 | ## Setup
15 |
16 | ### Prerequisites
17 | - Python 3.11+
18 | - [uv](https://docs.astral.sh/uv/) package manager
19 | - AWS CLI configured (for distributed evaluation)
20 | - [Modal](https://modal.com/) account (for distributed evaluation)
21 |
22 | ### Installation
23 |
24 | ```bash
25 | # Install dependencies
26 | uv sync
27 | ```
28 |
29 | ## Evaluation Methods
30 |
31 | ### 1. Local Evaluation
32 |
33 | Run evaluation sequentially (locally):
34 |
35 | ```bash
36 | uv run -m src.evaluate_model --num-tasks 2
37 | ```
38 |
39 | Remove `--num-tasks` parameter to run on all tasks. Results are saved to `timecopilot.csv` in `fev` format.
40 |
41 | ### 2. Distributed Evaluation (Recommended)
42 |
43 | #### 2.1 Evaluate ensemble
44 |
45 | Evaluate all dataset configurations in parallel using [modal](https://modal.com/):
46 |
47 | ```bash
48 | # Run distributed evaluation on Modal cloud
49 | uv run modal run --detach -m src.evaluate_model_modal
50 | ```
51 |
52 | This creates one GPU job per dataset configuration, significantly reducing evaluation time.
53 |
54 | **Infrastructure:**
55 | - **GPU**: A10G per job
56 | - **CPU**: 8 cores per job
57 | - **Timeout**: 3 hours per job
58 | - **Storage**: S3 bucket for data and results
59 |
60 | #### 2.2 Collect Results
61 |
62 | Download and consolidate results from distributed evaluation:
63 |
64 | ```bash
65 | # Download all results from S3 and create consolidated CSV
66 | uv run python -m src.download_results
67 | ```
68 |
69 | Results are saved to `timecopilot.csv` in `fev` format.
70 |
--------------------------------------------------------------------------------
/experiments/gift-eval/src/run_modal.py:
--------------------------------------------------------------------------------
1 | import modal
2 |
3 | app = modal.App(name="timecopilot-gift-eval")
4 | image = (
5 | modal.Image.from_registry(
6 | "nvidia/cuda:12.8.1-devel-ubuntu24.04",
7 | add_python="3.11",
8 | )
9 | # uploaded to s3 by makefile
10 | .apt_install("git")
11 | .pip_install("uv")
12 | .add_local_file("pyproject.toml", "/root/pyproject.toml", copy=True)
13 | .add_local_file(".python-version", "/root/.python-version", copy=True)
14 | .add_local_file("uv.lock", "/root/uv.lock", copy=True)
15 | .workdir("/root")
16 | .run_commands("uv pip install . --system --compile-bytecode")
17 | )
18 | secret = modal.Secret.from_name(
19 | "aws-secret",
20 | required_keys=["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
21 | )
22 | volume = {
23 | "/s3-bucket": modal.CloudBucketMount(
24 | bucket_name="timecopilot-gift-eval",
25 | secret=secret,
26 | )
27 | }
28 |
29 |
30 | @app.function(
31 | image=image,
32 | volumes=volume,
33 | # 6 hours timeout
34 | timeout=60 * 60 * 6,
35 | gpu="A10G",
36 | # as my local
37 | cpu=8,
38 | )
39 | def run_timecopilot_modal(dataset_name: str, term: str):
40 | import logging
41 | from pathlib import Path
42 |
43 | from .run_timecopilot import run_timecopilot
44 |
45 | output_path = Path(f"/s3-bucket/results/timecopilot/{dataset_name}/{term}/")
46 | if output_path.exists():
47 | logging.info(f"Output dir {output_path} already exists")
48 | return
49 | run_timecopilot(
50 | dataset_name=dataset_name,
51 | term=term,
52 | output_path=output_path,
53 | storage_path="/s3-bucket/data/gift-eval",
54 | )
55 |
56 |
57 | @app.local_entrypoint()
58 | def main():
59 | import logging
60 |
61 | import fsspec
62 |
63 | from timecopilot.gift_eval.utils import DATASETS_WITH_TERMS
64 |
65 | logging.basicConfig(level=logging.INFO)
66 |
67 | fs = fsspec.filesystem("s3")
68 | missing_datasets_with_terms = [
69 | (dataset_name, term)
70 | for dataset_name, term in DATASETS_WITH_TERMS
71 | if not fs.exists(
72 | f"s3://timecopilot-gift-eval/results/timecopilot/{dataset_name}/{term}/all_results.csv"
73 | )
74 | ]
75 | logging.info(f"Running {len(missing_datasets_with_terms)} datasets")
76 | results = list(
77 | run_timecopilot_modal.starmap(
78 | missing_datasets_with_terms,
79 | return_exceptions=True,
80 | wrap_returned_exceptions=False,
81 | )
82 | )
83 | logging.info(f"errors: {[r for r in results if r is not None]}")
84 |
--------------------------------------------------------------------------------
/docs/changelogs/v0.0.18.md:
--------------------------------------------------------------------------------
1 | ### Features
2 |
3 | * **Anomaly Detection Capabilities**: Added comprehensive anomaly detection functionality to the forecaster, enabling identification of outliers and unusual patterns in time series data. See [#213](https://github.com/TimeCopilot/timecopilot/pull/213).
4 |
5 | ```python
6 | import pandas as pd
7 | from timecopilot import TimeCopilotForecaster
8 | from timecopilot.models.stats import SeasonalNaive, Theta
9 | from timecopilot.models.foundation.chronos import Chronos
10 |
11 | # Load your time series data
12 | df = pd.read_csv(
13 | "https://timecopilot.s3.amazonaws.com/public/data/taylor_swift_pageviews.csv",
14 | parse_dates=["ds"],
15 | )
16 |
17 | # Create forecaster with multiple models
18 | tcf = TimeCopilotForecaster(
19 | models=[
20 | Chronos(repo_id="amazon/chronos-bolt-mini"),
21 | SeasonalNaive(),
22 | Theta(),
23 | ]
24 | )
25 |
26 | # Detect anomalies with 95% confidence level
27 | anomalies_df = tcf.detect_anomalies(df=df, h=7, level=95)
28 |
29 | # Visualize the results
30 | tcf.plot(df, anomalies_df)
31 | ```
32 |
33 | * **fev Experiments**: Added new [fev](https://github.com/autogluon/fev) experiments to expand the evaluation results. See [#211](https://github.com/TimeCopilot/timecopilot/pull/211).
34 |
35 | * **Chat-like CLI Capabilities**: Introduced an interactive, conversational CLI interface that enables natural language interaction with TimeCopilot. The CLI now supports seamless model switching, anomaly detection integration, and real-time plotting capabilities. See [#215](https://github.com/TimeCopilot/timecopilot/pull/215).
36 |
37 | ```bash
38 | # Start the interactive CLI
39 | uv run timecopilot
40 |
41 | # Natural conversation examples:
42 | > "forecast the next 12 months"
43 | > "now try this with Chronos"
44 | > "highlight anomalies in this series"
45 | > "show me the plot"
46 | > "explain the results"
47 | ```
48 |
49 | ### Fixes
50 |
51 | * **GIFT-Eval Import Corrections**: Fixed import statements after refactoring in the GIFT-Eval experiment to ensure proper functionality. See [#209](https://github.com/TimeCopilot/timecopilot/pull/209).
52 |
53 | * **Documentation Link Updates**: Corrected links throughout the documentation after the recent refactoring to maintain proper navigation. See [#210](https://github.com/TimeCopilot/timecopilot/pull/210).
54 |
55 | ### Documentation
56 |
57 | * **README Improvements**: Enhanced README.md with updated information and improved clarity. See [#207](https://github.com/TimeCopilot/timecopilot/pull/207).
58 |
59 | ---
60 |
61 | **Full Changelog**: https://github.com/TimeCopilot/timecopilot/compare/v0.0.17...v0.0.18
--------------------------------------------------------------------------------
/docs/changelogs/v0.0.10.md:
--------------------------------------------------------------------------------
1 |
2 | ### Features
3 | * **TimeCopilotForecaster Class**: Introduced the `TimeCopilotForecaster` class to enhance forecasting capabilities. See [#48](https://github.com/TimeCopilot/timecopilot/pull/48). Example:
4 |
5 | ```python
6 | import pandas as pd
7 |
8 | from timecopilot import TimeCopilotForecaster
9 | from timecopilot.models.benchmarks import SeasonalNaive
10 | from timecopilot.models.foundational import TimesFM
11 |
12 | df = pd.read_csv(
13 | "https://timecopilot.s3.amazonaws.com/public/data/algeria_exports.csv",
14 | parse_dates=["ds"],
15 | )
16 | forecaster = TimeCopilotForecaster(models=[TimesFM(), SeasonalNaive()])
17 | fcsts_df = forecaster.forecast(df=df, h=12, freq="MS")
18 | ```
19 |
20 | * **Probabilistic Forecasts**: Added support for probabilistic forecasts in the forecaster class. See [#50](https://github.com/TimeCopilot/timecopilot/pull/50). Example:
21 |
22 | ```python
23 | import pandas as pd
24 |
25 | from timecopilot import TimeCopilotForecaster
26 | from timecopilot.models.benchmarks import SeasonalNaive, Prophet
27 | from timecopilot.models.foundational import TimesFM
28 |
29 | df = pd.read_csv(
30 | "https://timecopilot.s3.amazonaws.com/public/data/algeria_exports.csv",
31 | parse_dates=["ds"],
32 | )
33 | forecaster = TimeCopilotForecaster(models=[TimesFM(), SeasonalNaive()])
34 | fcsts_df_level = forecaster.forecast(
35 | df=df,
36 | h=12,
37 | freq="MS",
38 | level=[80, 90],
39 | )
40 | fcsts_df_quantiles = forecaster.forecast(
41 | df=df,
42 | h=12,
43 | freq="MS",
44 | quantiles=[0.1, 0.9],
45 | )
46 | ```
47 |
48 | * **Integration with External Libraries**:
49 | - **timesfm**: Added Google's foundation model [TimesFM](https://github.com/google-research/timesfm). See [#55](https://github.com/TimeCopilot/timecopilot/pull/55).
50 | - **chronos**: Added AWS AI Labs's foundation model [Chronos](https://arxiv.org/abs/2403.07815). See [#59](https://github.com/TimeCopilot/timecopilot/pull/59).
51 | - **Prophet**: Added Facebook's [Prophet](https://facebook.github.io/prophet/) to available models. See [#61](https://github.com/TimeCopilot/timecopilot/pull/61).
52 |
53 |
54 | * **Multi-series Support**: Enhanced the agent to handle multiple time series. See [#64](https://github.com/TimeCopilot/timecopilot/pull/64).
55 | - **Example**:
56 | ```python
57 | from timecopilot import TimeCopilot
58 |
59 | tc = TimeCopilot()
60 | # now the forecast method can handle multiple time series
61 | tc.forecast(...)
62 | ```
63 |
64 | * **Agent Integration**: Utilized the TimeCopilotForecaster class within the agent. See [#65](https://github.com/TimeCopilot/timecopilot/pull/65).
65 |
66 | ### Tests
67 | * **Basic Functionality Tests**: Added tests for basic functionality to ensure reliability. See [#43](https://github.com/TimeCopilot/timecopilot/pull/43).
68 |
69 | ### Fixes
70 | * **CI Improvements**: Implemented a fix to cancel concurrent CI runs, optimizing the CI process. See [#63](https://github.com/TimeCopilot/timecopilot/pull/63).
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | workflow_dispatch:
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}-${{ github.ref }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | test:
15 | name: test on ${{ matrix.python-version }}
16 | runs-on: ubuntu-latest
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | python-version: ["3.10", "3.11", "3.12", "3.13"]
21 | env:
22 | UV_PYTHON: ${{ matrix.python-version }}
23 |
24 | steps:
25 | - name: Clone repo
26 | uses: actions/checkout@v4
27 |
28 | - name: Set up uv
29 | uses: astral-sh/setup-uv@v6
30 | with:
31 | enable-cache: true
32 |
33 | - name: Run tests
34 | run: uv run pytest
35 | env:
36 | HF_TOKEN: ${{ secrets.HF_TOKEN }}
37 |
38 | - name: Test import class
39 | run: uv run -- python -c "from timecopilot import TimeCopilot, TimeCopilotForecaster"
40 |
41 | test-live:
42 | name: test live
43 | runs-on: ubuntu-latest
44 | strategy:
45 | fail-fast: false
46 |
47 | steps:
48 | - name: Clone repo
49 | uses: actions/checkout@v4
50 |
51 | - name: Set up uv
52 | uses: astral-sh/setup-uv@v6
53 | with:
54 | enable-cache: true
55 |
56 | - name: Run tests
57 | run: uv run pytest -m live
58 | env:
59 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
60 |
61 | test-gift-eval:
62 | name: test gift eval
63 | runs-on: ubuntu-latest
64 | strategy:
65 | fail-fast: true
66 |
67 | steps:
68 | - name: Clone repo
69 | uses: actions/checkout@v4
70 |
71 | - name: Set up uv
72 | uses: astral-sh/setup-uv@v6
73 | with:
74 | enable-cache: true
75 |
76 | - name: Run tests
77 | run: uv run pytest -m gift_eval -x -n 0
78 |
79 | test-docs:
80 | name: test docs
81 | runs-on: ubuntu-latest
82 | strategy:
83 | fail-fast: true
84 | env:
85 | UV_PYTHON: 3.11
86 | steps:
87 | - name: Clone repo
88 | uses: actions/checkout@v4
89 |
90 | - name: Set up uv
91 | uses: astral-sh/setup-uv@v6
92 | with:
93 | enable-cache: true
94 |
95 | - name: Run tests
96 | run: uv run pytest -m docs -x -n 0
97 | env:
98 | HF_TOKEN: ${{ secrets.HF_TOKEN }}
99 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
100 | OLLAMA_BASE_URL: "URL_PLACEHOLDER"
101 |
102 | build-docs:
103 | name: build docs
104 | runs-on: ubuntu-latest
105 | steps:
106 | - name: Clone repo
107 | uses: actions/checkout@v4
108 |
109 | - name: Set up uv
110 | uses: astral-sh/setup-uv@v6
111 |
112 | - name: Build docs
113 | run: uv run --group docs mkdocs build
114 |
115 | lint:
116 | name: lint and style checks
117 | runs-on: ubuntu-latest
118 | steps:
119 | - name: Clone repo
120 | uses: actions/checkout@v4
121 |
122 | - name: Set up uv
123 | uses: astral-sh/setup-uv@v6
124 |
125 | - name: Run pre-commit
126 | run: uvx pre-commit run --all-files
127 |
--------------------------------------------------------------------------------
/experiments/fev/src/evaluate_model.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import time
3 | import warnings
4 |
5 | import datasets
6 | import fev
7 | import pandas as pd
8 | import typer
9 |
10 | from timecopilot.models.ensembles.median import MedianEnsemble
11 | from timecopilot.models.foundation.chronos import Chronos
12 | from timecopilot.models.foundation.tirex import TiRex
13 |
14 | app = typer.Typer()
15 | logging.basicConfig(level=logging.INFO)
16 | datasets.disable_progress_bars()
17 |
18 |
19 | def predict_with_model(task: fev.Task) -> tuple[datasets.Dataset, float, dict]:
20 | past_df, *_ = fev.convert_input_data(task, "nixtla", trust_remote_code=True)
21 | # Forward fill NaNs + zero-fill leading NaNs
22 | past_df = (
23 | past_df.set_index("unique_id")
24 | .groupby("unique_id")
25 | .ffill()
26 | .reset_index()
27 | .fillna(0.0)
28 | )
29 |
30 | forecaster = MedianEnsemble(
31 | models=[
32 | Chronos(
33 | repo_id="amazon/chronos-bolt-base",
34 | batch_size=256,
35 | ),
36 | TiRex(batch_size=256),
37 | ],
38 | alias="TimeCopilot",
39 | )
40 | start_time = time.monotonic()
41 | with warnings.catch_warnings():
42 | warnings.simplefilter("ignore")
43 | forecast_df = forecaster.forecast(
44 | df=past_df,
45 | h=task.horizon,
46 | quantiles=task.quantile_levels,
47 | freq=task.freq,
48 | )
49 | inference_time = time.monotonic() - start_time
50 | renamer = {
51 | forecaster.alias: "predictions",
52 | }
53 | if task.quantile_levels is not None:
54 | renamer.update(
55 | {
56 | f"{forecaster.alias}-q-{int(100 * q)}": str(q)
57 | for q in task.quantile_levels
58 | }
59 | )
60 | forecast_df = forecast_df.rename(columns=renamer)
61 | selected_columns = [fev.constants.PREDICTIONS]
62 | if task.quantile_levels is not None:
63 | selected_columns += [str(q) for q in task.quantile_levels]
64 | predictions_list = []
65 | for _, forecast in forecast_df.groupby("unique_id"):
66 | predictions_list.append(forecast[selected_columns].to_dict("list"))
67 | predictions = datasets.Dataset.from_list(predictions_list)
68 |
69 | return predictions, inference_time, {}
70 |
71 |
72 | def evaluate_task(task: fev.Task):
73 | predictions, inference_time, extra_info = predict_with_model(task)
74 | evaluation_summary = task.evaluation_summary(
75 | predictions,
76 | model_name="timecopilot",
77 | inference_time_s=inference_time,
78 | extra_info=extra_info,
79 | )
80 | print(evaluation_summary)
81 | return pd.DataFrame([evaluation_summary])
82 |
83 |
84 | def tasks():
85 | benchmark = fev.Benchmark.from_yaml(
86 | "https://raw.githubusercontent.com/autogluon/fev/refs/heads/main/benchmarks/chronos_zeroshot/tasks.yaml"
87 | )
88 | return benchmark.tasks
89 |
90 |
91 | @app.command()
92 | def main(num_tasks: int | None = None):
93 | _tasks = tasks()[:num_tasks]
94 | logging.info(f"Evaluating {len(_tasks)} tasks")
95 | summaries = []
96 | for task in _tasks:
97 | evaluation_summary = evaluate_task(task)
98 | summaries.append(evaluation_summary)
99 | # Show and save the results
100 | summary_df = pd.concat(summaries)
101 | print(summary_df)
102 | summary_df.to_csv("timecopilot.csv", index=False)
103 |
104 |
105 | if __name__ == "__main__":
106 | app()
107 |
--------------------------------------------------------------------------------
/tests/models/conftest.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import pytest
4 |
5 | from timecopilot.models.ensembles.median import MedianEnsemble
6 | from timecopilot.models.foundation.chronos import Chronos
7 | from timecopilot.models.foundation.flowstate import FlowState
8 | from timecopilot.models.foundation.moirai import Moirai
9 | from timecopilot.models.foundation.timesfm import TimesFM
10 | from timecopilot.models.foundation.toto import Toto
11 | from timecopilot.models.ml import AutoLGBM
12 | from timecopilot.models.neural import AutoNHITS, AutoTFT
13 | from timecopilot.models.prophet import Prophet
14 | from timecopilot.models.stats import (
15 | ADIDA,
16 | AutoARIMA,
17 | SeasonalNaive,
18 | ZeroModel,
19 | )
20 |
21 |
22 | @pytest.fixture(autouse=True)
23 | def disable_mps_session(monkeypatch):
24 | # Make torch.backends.mps report unavailable
25 | try:
26 | import torch
27 |
28 | monkeypatch.setattr(
29 | torch.backends.mps, "is_available", lambda: False, raising=False
30 | )
31 | monkeypatch.setattr(
32 | torch.backends.mps, "is_built", lambda: False, raising=False
33 | )
34 | except Exception:
35 | # torch might not be installed in some envs; ignore
36 | pass
37 |
38 |
39 | models = [
40 | AutoLGBM(num_samples=2, cv_n_windows=2),
41 | AutoNHITS(
42 | num_samples=2,
43 | config=dict(
44 | max_steps=1,
45 | val_check_steps=1,
46 | input_size=12,
47 | mlp_units=3 * [[8, 8]],
48 | ),
49 | ),
50 | AutoTFT(
51 | num_samples=2,
52 | config=dict(
53 | max_steps=1,
54 | val_check_steps=1,
55 | input_size=12,
56 | hidden_size=8,
57 | ),
58 | ),
59 | AutoARIMA(),
60 | SeasonalNaive(),
61 | ZeroModel(),
62 | ADIDA(),
63 | Prophet(),
64 | Chronos(repo_id="amazon/chronos-bolt-tiny", alias="Chronos-Bolt"),
65 | Chronos(repo_id="amazon/chronos-2", alias="Chronos-2"),
66 | Chronos(repo_id="amazon/chronos-2", alias="Chronos-2", batch_size=2),
67 | FlowState(repo_id="ibm-research/flowstate"),
68 | FlowState(
69 | repo_id="ibm-granite/granite-timeseries-flowstate-r1",
70 | alias="FlowState-Granite",
71 | ),
72 | Toto(context_length=256, batch_size=2),
73 | Moirai(
74 | context_length=256,
75 | batch_size=2,
76 | repo_id="Salesforce/moirai-1.1-R-small",
77 | ),
78 | TimesFM(
79 | repo_id="google/timesfm-1.0-200m-pytorch",
80 | context_length=256,
81 | ),
82 | TimesFM(
83 | repo_id="google/timesfm-2.5-200m-pytorch",
84 | context_length=256,
85 | ),
86 | MedianEnsemble(
87 | models=[
88 | Chronos(repo_id="amazon/chronos-t5-tiny", alias="Chronos-T5"),
89 | Chronos(repo_id="amazon/chronos-bolt-tiny", alias="Chronos-Bolt"),
90 | SeasonalNaive(),
91 | ],
92 | ),
93 | Moirai(
94 | context_length=256,
95 | batch_size=2,
96 | repo_id="Salesforce/moirai-2.0-R-small",
97 | ),
98 | ]
99 | if sys.version_info >= (3, 11):
100 | from timecopilot.models.foundation.tirex import TiRex
101 |
102 | models.append(TiRex())
103 |
104 | if sys.version_info < (3, 13):
105 | from tabpfn_time_series import TabPFNMode
106 |
107 | from timecopilot.models.foundation.sundial import Sundial
108 | from timecopilot.models.foundation.tabpfn import TabPFN
109 |
110 | models.append(TabPFN(mode=TabPFNMode.MOCK))
111 | models.append(Sundial(context_length=256, num_samples=10))
112 |
--------------------------------------------------------------------------------
/docs/changelogs/v0.0.11.md:
--------------------------------------------------------------------------------
1 | ### Features
2 |
3 | * **TiRex Foundation Model**: Added the [TiRex](https://github.com/NX-AI/tirex) time series foundation model. See [#77](https://github.com/TimeCopilot/timecopilot/pull/77). Example:
4 |
5 | ```python
6 | import pandas as pd
7 | from timecopilot.models.foundational.tirex import TiRex
8 |
9 | df = pd.read_csv(
10 | "https://timecopilot.s3.amazonaws.com/public/data/algeria_exports.csv",
11 | parse_dates=["ds"],
12 | )
13 | model = TiRex()
14 | fcst = model.forecast(df, h=12)
15 | ```
16 |
17 | * **Toto Model**: Added the [Toto](https://github.com/DataDog/toto) time series model. See [#78](https://github.com/TimeCopilot/timecopilot/pull/78). Example:
18 |
19 | ```python
20 | import pandas as pd
21 | from timecopilot.models.foundational.toto import Toto
22 |
23 | df = pd.read_csv(
24 | "https://timecopilot.s3.amazonaws.com/public/data/algeria_exports.csv",
25 | parse_dates=["ds"],
26 | )
27 | model = Toto()
28 | fcst = model.forecast(df, h=12)
29 | ```
30 |
31 | * **Optional `freq` Parameter**: The `freq` parameter is now optional in all forecast and cross-validation methods. If not provided, frequency is inferred automatically from the data. See [#96](https://github.com/TimeCopilot/timecopilot/pull/96). Example:
32 |
33 | ```python
34 | import pandas as pd
35 | from timecopilot.models.benchmarks import SeasonalNaive
36 |
37 | df = pd.read_csv(
38 | "https://timecopilot.s3.amazonaws.com/public/data/algeria_exports.csv",
39 | parse_dates=["ds"],
40 | )
41 | model = SeasonalNaive()
42 | # freq is now optional
43 | fcst = model.forecast(df, h=12)
44 | ```
45 |
46 | * **Improved Model Docstrings**: All foundational and statistical model constructors are now fully documented, with clear parameter explanations and references to official sources. See [#93](https://github.com/TimeCopilot/timecopilot/pull/93) and [#94](https://github.com/TimeCopilot/timecopilot/pull/94).
47 |
48 | * **Comprehensive Module Docstrings**: Added module-level docstrings to improve API documentation and usability. See [#82](https://github.com/TimeCopilot/timecopilot/pull/82).
49 |
50 | * **TimeCopilotForecaster Documentation**: Documented the `TimeCopilotForecaster` class, including its constructor and methods, to clarify its unified, multi-model forecasting and cross-validation interface. See [#97](https://github.com/TimeCopilot/timecopilot/pull/97). Example:
51 |
52 |
53 | ```python
54 | import pandas as pd
55 | from timecopilot.forecaster import TimeCopilotForecaster
56 | from timecopilot.models.benchmarks.prophet import Prophet
57 | from timecopilot.models.benchmarks.stats import AutoARIMA, SeasonalNaive
58 | from timecopilot.models.foundational.toto import Toto
59 |
60 | df = pd.read_csv(
61 | "https://timecopilot.s3.amazonaws.com/public/data/air_passengers.csv",
62 | parse_dates=["ds"],
63 | )
64 | tcf = TimeCopilotForecaster(
65 | models=[
66 | AutoARIMA(),
67 | SeasonalNaive(),
68 | Prophet(),
69 | Toto(context_length=256),
70 | ]
71 | )
72 |
73 | fcst_df = tcf.forecast(df=df, h=12)
74 | cv_df = tcf.cross_validation(df=df, h=12)
75 | ```
76 |
77 | ### Tests
78 | * **Parallel Test Execution**: Added `pytest-xdist` to enable running tests in parallel, speeding up CI and local test runs. See [#75](https://github.com/TimeCopilot/timecopilot/pull/75).
79 |
80 | ### Fixes
81 |
82 | * **Documentation Improvements**: Enhanced documentation for all models and constructors, ensuring clarity and consistency across the codebase. See [#93](https://github.com/TimeCopilot/timecopilot/pull/93), [#94](https://github.com/TimeCopilot/timecopilot/pull/94), and [#82](https://github.com/TimeCopilot/timecopilot/pull/82).
83 |
84 | * **S3 Data Source**: All example and test data now use S3 URLs for consistency and reproducibility. See [#73](https://github.com/TimeCopilot/timecopilot/pull/73).
85 |
86 |
87 |
88 |
89 | **Full Changelog**: https://github.com/TimeCopilot/timecopilot/compare/v0.0.10...v0.0.11
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | #.idea/
161 |
162 | *.DS_Store
163 |
164 | # VS Code
165 | .vscode/
166 |
167 | # logs
168 | lightning_logs/
169 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | build-backend = "hatchling.build"
3 | requires = ["hatchling"]
4 |
5 | [dependency-groups]
6 | dev = [
7 | "mktestdocs>=0.2.5",
8 | "pre-commit",
9 | "pytest",
10 | "pytest-asyncio>=1.1.0",
11 | "pytest-cov",
12 | "pytest-mock>=3.15.1",
13 | "pytest-rerunfailures>=15.1",
14 | "pytest-xdist>=3.8.0",
15 | "s3fs>=2025.3.0",
16 | ]
17 | docs = [
18 | "mkdocs-include-markdown-plugin>=7.1.6",
19 | "mkdocs-jupyter>=0.25.1",
20 | "mkdocs-material>=9.6.14",
21 | "mkdocs>=1.6.1",
22 | "mkdocstrings[python]>=0.29.1",
23 | "mktestdocs>=0.2.5",
24 | "modal>=1.0.4",
25 | "ruff>=0.12.1",
26 | ]
27 |
28 | [project]
29 | authors = [
30 | {email = "azul.garza.r@gmail.com", name = "Azul Garza"},
31 | ]
32 | classifiers = [
33 | "Development Status :: 2 - Pre-Alpha",
34 | "Intended Audience :: Developers",
35 | "Intended Audience :: Information Technology",
36 | "License :: OSI Approved :: MIT License",
37 | "Operating System :: OS Independent",
38 | "Programming Language :: Python :: 3 :: Only",
39 | "Programming Language :: Python :: 3",
40 | "Programming Language :: Python :: 3.10",
41 | "Programming Language :: Python :: 3.11",
42 | "Programming Language :: Python :: 3.12",
43 | "Programming Language :: Python :: 3.13",
44 | "Programming Language :: Python",
45 | "Topic :: Internet",
46 | "Topic :: Scientific/Engineering :: Artificial Intelligence",
47 | "Topic :: Software Development :: Libraries :: Python Modules",
48 | ]
49 | dependencies = [
50 | "fire",
51 | "gluonts[torch]",
52 | "huggingface-hub>=0.34.4",
53 | "lightgbm>=4.6.0",
54 | "logfire>=4.7.0",
55 | "mlforecast>=1.0.2",
56 | "neuralforecast>=3.0.2",
57 | "nixtla>=0.6.6",
58 | "openai>=1.99.7",
59 | "pandas>=2.2.0 ; python_full_version >= '3.13'",
60 | "prophet>=1.1.7",
61 | "pydantic-ai>=0.7.0",
62 | "scipy<=1.15.3",
63 | "statsforecast",
64 | "tabpfn-time-series==1.0.3 ; python_full_version < '3.13'",
65 | "timecopilot-chronos-forecasting>=0.2.0",
66 | "timecopilot-granite-tsfm>=0.1.1",
67 | "timecopilot-timesfm>=0.2.1",
68 | "timecopilot-tirex>=0.1.0 ; python_full_version >= '3.11'",
69 | "timecopilot-toto>=0.1.3",
70 | "timecopilot-uni2ts>=0.1.2 ; python_full_version < '3.14'",
71 | "transformers==4.40.1 ; python_full_version < '3.13'",
72 | "transformers>=4.48,<5 ; python_full_version >= '3.13'",
73 | "tsfeatures",
74 | "utilsforecast[plotting]",
75 | ]
76 | description = "The GenAI Forecasting Agent · LLMs × Time Series Foundation Models"
77 | license = "MIT"
78 | name = "timecopilot"
79 | readme = "README.md"
80 | requires-python = ">=3.10"
81 | version = "0.0.22"
82 |
83 | [project.scripts]
84 | timecopilot = "timecopilot._cli:main"
85 |
86 | [tool.coverage]
87 | branch = true
88 | source = ["timecopilot"]
89 |
90 | [tool.coverage.report]
91 | fail_under = 80
92 | show_missing = true
93 |
94 | [tool.coverage.run]
95 | omit = [
96 | "tests/*",
97 | ]
98 |
99 | [tool.hatch.metadata]
100 | allow-direct-references = true
101 |
102 | [tool.mypy]
103 | disable_error_code = ["no-redef"] # for fasthtml
104 |
105 | [tool.pytest.ini_options]
106 | addopts = "-m 'not docs and not live and not gift_eval' -n auto"
107 | markers = [
108 | "docs: marks tests related to documentation",
109 | "flaky: rerun failures (provided by pytest-rerunfailures)",
110 | "gift_eval: marks tests related to gift eval results replication",
111 | "live: marks tests that require calls to llm providers",
112 | ]
113 | testpaths = ["tests"]
114 |
115 | [tool.ruff]
116 | fix = true
117 | line-length = 88
118 | src = ["timecopilot"]
119 |
120 | [tool.ruff.format]
121 | docstring-code-format = true
122 | docstring-code-line-length = 20
123 |
124 | [tool.ruff.lint]
125 | ignore = [
126 | "F811", # redefined function, this is helpful for fasthtml
127 | ]
128 | select = ["B", "E", "F", "I", "SIM", "UP"]
129 |
130 | [tool.ruff.lint.isort]
131 | known-local-folder = ["timecopilot"]
132 | no-lines-before = ["local-folder"]
133 |
--------------------------------------------------------------------------------
/docs/changelogs/v0.0.13.md:
--------------------------------------------------------------------------------
1 | ### Features
2 |
3 | * **Custom seasonalitites**. Added `custom_seasonalities` argument to [`get_seasonality`][timecopilot.models.utils.forecaster.get_seasonality]. See [#147](https://github.com/TimeCopilot/timecopilot/pull/147).
4 | ```python
5 | from timecopilot.models.utils.forecaster import get_seasonality
6 |
7 | print(get_seasonality("D", custom_seasonalities={"D": 7}))
8 | # 7
9 | print(get_seasonality("D")) # default seasonalities are used
10 | # 1
11 | ```
12 |
13 | * **GIFTEval results concatenation**. Added functionality to concatenate results for different datasets when `GIFTEval(...).evaluate_predictor(...)` is used. See [#148](https://github.com/TimeCopilot/timecopilot/pull/148).
14 | ```python
15 | import pandas as pd
16 | from timecopilot.gift_eval.eval import GIFTEval
17 | from timecopilot.gift_eval.gluonts_predictor import GluonTSPredictor
18 | from timecopilot.models.benchmarks import SeasonalNaive
19 |
20 | storage_path = ".pytest_cache/gift_eval"
21 | GIFTEval.download_data(storage_path)
22 |
23 | predictor = GluonTSPredictor(
24 | forecaster=SeasonalNaive(),
25 | batch_size=512,
26 | )
27 |
28 | def evaluate_predictor(
29 | dataset_name: str,
30 | term: str,
31 | overwrite_results: bool = False,
32 | ):
33 | gifteval = GIFTEval(
34 | dataset_name=dataset_name,
35 | term=term,
36 | output_path="./seasonal_naive",
37 | storage_path=storage_path,
38 | )
39 | gifteval.evaluate_predictor(
40 | predictor,
41 | batch_size=512,
42 | overwrite_results=overwrite_results,
43 | )
44 |
45 | combinations = [
46 | ("m4_weekly", "short"),
47 | ("m4_hourly", "short"),
48 | ]
49 |
50 | for i, (dataset_name, term) in enumerate(combinations):
51 | evaluate_predictor(
52 | dataset_name=dataset_name,
53 | term=term,
54 | )
55 | eval_df = pd.read_csv("./seasonal_naive/all_results.csv")
56 | print(eval_df) # it includes eval for the two datasets
57 |
58 | # you can use overwrite_results to generate a new file of results
59 | evaluate_predictor(
60 | dataset_name="m4_weekly",
61 | term="short",
62 | overwrite_results=True
63 | )
64 | eval_df = pd.read_csv("./seasonal_naive/all_results.csv")
65 | print(eval_df) # it includes eval just for m4_weekly
66 | ```
67 |
68 | ### Refactorings
69 |
70 | * **GIFTEval Module**: Refactored the [GIFTEval](https://github.com/SalesforceAIResearch/gift-eval/) module to infer horizon `h` and frequency `freq` directly from the dataset when calling `GIFTEval(...).evaluate_predictor(...)`. See [#147](https://github.com/TimeCopilot/timecopilot/pull/147). New usage:
71 | ```python
72 | import pandas as pd
73 | from timecopilot.gift_eval.eval import GIFTEval
74 | from timecopilot.gift_eval.gluonts_predictor import GluonTSPredictor
75 | from timecopilot.models.benchmarks import SeasonalNaive
76 |
77 | storage_path = ".pytest_cache/gift_eval"
78 | GIFTEval.download_data(storage_path)
79 |
80 | predictor = GluonTSPredictor(
81 | # you can use any forecaster from TimeCopilot
82 | # and create your own forecaster by subclassing
83 | # [Forecaster][timecopilot.models.utils.forecaster.Forecaster]
84 | forecaster=SeasonalNaive(),
85 | batch_size=512,
86 | )
87 | gift_eval = GIFTEval(
88 | dataset_name="m4_daily",
89 | term="short",
90 | output_path="./seasonal_naive",
91 | storage_path=storage_path,
92 | )
93 | gift_eval.evaluate_predictor(
94 | predictor,
95 | batch_size=512,
96 | )
97 | eval_df = pd.read_csv("./seasonal_naive/all_results.csv")
98 | print(eval_df)
99 | ```
100 |
101 | * **Median ensemble**: Now it applies median plus isotonic regression for probabilistic forecasts. See [#149](https://github.com/TimeCopilot/timecopilot/pull/149).
102 |
103 | ---
104 |
105 | **Full Changelog**: https://github.com/TimeCopilot/timecopilot/compare/v0.0.12...v0.0.13
--------------------------------------------------------------------------------
/tests/models/foundation/test_timesfm.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 |
5 | from timecopilot.models.foundation.timesfm import _TimesFMV1, _TimesFMV2_p5
6 |
7 | MODEL_PARAMS = [
8 | (
9 | _TimesFMV1,
10 | [
11 | "timecopilot.models.foundation.timesfm.timesfm_v1.TimesFmCheckpoint",
12 | "timecopilot.models.foundation.timesfm.timesfm_v1.TimesFm",
13 | ],
14 | ),
15 | (
16 | _TimesFMV2_p5,
17 | [
18 | "timecopilot.models.foundation.timesfm.TimesFM_2p5_200M_torch",
19 | ],
20 | ),
21 | ]
22 |
23 |
24 | @pytest.mark.parametrize("model_class, mock_paths", MODEL_PARAMS)
25 | def test_load_model_from_local_path(mocker, model_class, mock_paths):
26 | """Tests loading from a local path."""
27 | module_path = "timecopilot.models.foundation.timesfm"
28 | mock_os_exists = mocker.patch(f"{module_path}.os.path.exists", return_value=True)
29 | mock_loader = [mocker.patch(i) for i in mock_paths]
30 |
31 | local_path = "/fake/local/path"
32 |
33 | model_instance = model_class(
34 | repo_id=local_path,
35 | context_length=64,
36 | batch_size=32,
37 | alias="test",
38 | )
39 |
40 | with model_instance._get_predictor(prediction_length=12) as p:
41 | predictor = p
42 |
43 | mock_os_exists.assert_called_once_with(local_path)
44 |
45 | if model_class is _TimesFMV1:
46 | assert predictor is mock_loader[1].return_value
47 | expected_path = os.path.join(local_path, "torch_model.ckpt")
48 | mock_loader[0].assert_called_once_with(path=expected_path)
49 | elif model_class is _TimesFMV2_p5:
50 | expected_predictor = mock_loader[
51 | 0
52 | ].return_value.model.load_checkpoint.return_value
53 | assert predictor is expected_predictor
54 | mock_loader[0].return_value.model.load_checkpoint.assert_called_once_with(
55 | os.path.join(local_path, "model.safetensors")
56 | )
57 |
58 |
59 | @pytest.mark.parametrize("model_class, mock_paths", MODEL_PARAMS)
60 | def test_load_model_from_hf_repo(mocker, model_class, mock_paths):
61 | """Tests loading from a Hugging Face repo."""
62 | module_path = "timecopilot.models.foundation.timesfm"
63 | mock_os_exists = mocker.patch(f"{module_path}.os.path.exists", return_value=False)
64 | mock_repo_exists = mocker.patch(f"{module_path}.repo_exists", return_value=True)
65 | mock_loader = [mocker.patch(i) for i in mock_paths]
66 |
67 | repo_id = "/fake/google/repo-id"
68 |
69 | model_instance = model_class(
70 | repo_id=repo_id,
71 | context_length=64,
72 | batch_size=32,
73 | alias="test",
74 | )
75 |
76 | with model_instance._get_predictor(prediction_length=12) as p:
77 | predictor = p
78 |
79 | mock_os_exists.assert_called_once_with(repo_id)
80 | mock_repo_exists.assert_called_once_with(repo_id)
81 |
82 | if model_class is _TimesFMV1:
83 | assert predictor is mock_loader[1].return_value
84 | mock_loader[0].assert_called_once_with(huggingface_repo_id=repo_id)
85 | elif model_class is _TimesFMV2_p5:
86 | assert predictor is mock_loader[0].from_pretrained.return_value
87 | mock_loader[0].from_pretrained.assert_called_once_with(repo_id)
88 |
89 |
90 | @pytest.mark.parametrize("model_class, _", MODEL_PARAMS)
91 | def test_model_raises_OSError_on_failed_load(mocker, model_class, _):
92 | """Tests that an OSError is raised on a failed load attempt."""
93 |
94 | module_path = "timecopilot.models.foundation.timesfm"
95 | mock_os_exists = mocker.patch(f"{module_path}.os.path.exists", return_value=False)
96 | mock_repo_exists = mocker.patch(f"{module_path}.repo_exists", return_value=False)
97 |
98 | repo_id = "/this-is-a-fake/google/repo-id"
99 |
100 | model_instance = model_class(
101 | repo_id=repo_id,
102 | context_length=64,
103 | batch_size=32,
104 | alias="test",
105 | )
106 | with (
107 | pytest.raises(OSError, match="Failed to load model"),
108 | model_instance._get_predictor(prediction_length=12),
109 | ):
110 | pass
111 |
112 | mock_os_exists.assert_called_once_with(repo_id)
113 | mock_repo_exists.assert_called_once_with(repo_id)
114 |
--------------------------------------------------------------------------------
/timecopilot/models/ml.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pandas as pd
4 | from mlforecast.auto import AutoLightGBM, AutoMLForecast
5 |
6 | from .utils.forecaster import Forecaster, get_seasonality
7 |
8 | os.environ["NIXTLA_ID_AS_COL"] = "true"
9 |
10 |
11 | class AutoLGBM(Forecaster):
12 | """AutoLGBM forecaster using AutoMLForecast with LightGBM.
13 |
14 | Notes:
15 | - Level and quantiles are not supported for AutoLGBM yet. Please open
16 | an issue if you need this feature.
17 | - AutoLGBM requires a minimum length for some frequencies.
18 | """
19 |
20 | def __init__(
21 | self,
22 | alias: str = "AutoLGBM",
23 | num_samples: int = 10,
24 | cv_n_windows: int = 5,
25 | ):
26 | self.alias = alias
27 | self.num_samples = num_samples
28 | self.cv_n_windows = cv_n_windows
29 |
30 | def forecast(
31 | self,
32 | df: pd.DataFrame,
33 | h: int,
34 | freq: str | None = None,
35 | level: list[int | float] | None = None,
36 | quantiles: list[float] | None = None,
37 | ) -> pd.DataFrame:
38 | """Generate forecasts for time series data using the model.
39 |
40 | This method produces point forecasts and, optionally, prediction
41 | intervals or quantile forecasts. The input DataFrame can contain one
42 | or multiple time series in stacked (long) format.
43 |
44 | Args:
45 | df (pd.DataFrame):
46 | DataFrame containing the time series to forecast. It must
47 | include as columns:
48 |
49 | - "unique_id": an ID column to distinguish multiple series.
50 | - "ds": a time column indicating timestamps or periods.
51 | - "y": a target column with the observed values.
52 |
53 | h (int):
54 | Forecast horizon specifying how many future steps to predict.
55 | freq (str, optional):
56 | Frequency of the time series (e.g. "D" for daily, "M" for
57 | monthly). See [Pandas frequency aliases](https://pandas.pydata.org/
58 | pandas-docs/stable/user_guide/timeseries.html#offset-aliases) for
59 | valid values. If not provided, the frequency will be inferred
60 | from the data.
61 | level (list[int | float], optional):
62 | Confidence levels for prediction intervals, expressed as
63 | percentages (e.g. [80, 95]). If provided, the returned
64 | DataFrame will include lower and upper interval columns for
65 | each specified level.
66 | quantiles (list[float], optional):
67 | List of quantiles to forecast, expressed as floats between 0
68 | and 1. Should not be used simultaneously with `level`. When
69 | provided, the output DataFrame will contain additional columns
70 | named in the format "model-q-{percentile}", where {percentile}
71 | = 100 × quantile value.
72 |
73 | Returns:
74 | pd.DataFrame:
75 | DataFrame containing forecast results. Includes:
76 |
77 | - point forecasts for each timestamp and series.
78 | - prediction intervals if `level` is specified.
79 | - quantile forecasts if `quantiles` is specified.
80 |
81 | For multi-series data, the output retains the same unique
82 | identifiers as the input DataFrame.
83 | """
84 | if level is not None or quantiles is not None:
85 | raise ValueError("Level and quantiles are not supported for AutoLGBM yet.")
86 |
87 | freq = self._maybe_infer_freq(df, freq)
88 | mf = AutoMLForecast(
89 | models=[AutoLightGBM()],
90 | freq=freq,
91 | season_length=get_seasonality(freq),
92 | num_threads=-1,
93 | )
94 | mf.fit(
95 | df=df,
96 | n_windows=self.cv_n_windows,
97 | h=h,
98 | num_samples=self.num_samples,
99 | )
100 | fcst_df = mf.predict(h=h)
101 | fcst_df = fcst_df.rename(columns={"AutoLightGBM": self.alias})
102 | return fcst_df
103 |
--------------------------------------------------------------------------------
/timecopilot/models/utils/parallel_forecaster.py:
--------------------------------------------------------------------------------
1 | import os
2 | from collections.abc import Callable
3 | from multiprocessing import Pool
4 |
5 | import pandas as pd
6 |
7 | from .forecaster import Forecaster
8 |
9 |
10 | class ParallelForecaster(Forecaster):
11 | def _process_group(
12 | self,
13 | df: pd.DataFrame,
14 | func: Callable,
15 | **kwargs,
16 | ) -> pd.DataFrame:
17 | uid = df["unique_id"].iloc[0]
18 | _df = df.drop("unique_id", axis=1)
19 | res_df = func(_df, **kwargs)
20 | res_df.insert(0, "unique_id", uid)
21 | return res_df
22 |
23 | def _apply_parallel(
24 | self,
25 | df_grouped: pd.DataFrame,
26 | func: Callable,
27 | **kwargs,
28 | ) -> pd.DataFrame:
29 | with Pool(max(1, (os.cpu_count() or 1) - 1)) as executor:
30 | futures = [
31 | executor.apply_async(
32 | self._process_group,
33 | args=(df, func),
34 | kwds=kwargs,
35 | )
36 | for _, df in df_grouped
37 | ]
38 | results = [future.get() for future in futures]
39 | return pd.concat(results)
40 |
41 | def _local_forecast(
42 | self,
43 | df: pd.DataFrame,
44 | h: int,
45 | freq: str,
46 | level: list[int | float] | None = None,
47 | quantiles: list[float] | None = None,
48 | ) -> pd.DataFrame:
49 | raise NotImplementedError
50 |
51 | def forecast(
52 | self,
53 | df: pd.DataFrame,
54 | h: int,
55 | freq: str | None = None,
56 | level: list[int | float] | None = None,
57 | quantiles: list[float] | None = None,
58 | ) -> pd.DataFrame:
59 | """Generate forecasts for time series data using the model.
60 |
61 | This method produces point forecasts and, optionally, prediction
62 | intervals or quantile forecasts. The input DataFrame can contain one
63 | or multiple time series in stacked (long) format.
64 |
65 | Args:
66 | df (pd.DataFrame):
67 | DataFrame containing the time series to forecast. It must
68 | include as columns:
69 |
70 | - "unique_id": an ID column to distinguish multiple series.
71 | - "ds": a time column indicating timestamps or periods.
72 | - "y": a target column with the observed values.
73 |
74 | h (int):
75 | Forecast horizon specifying how many future steps to predict.
76 | freq (str, optional):
77 | Frequency of the time series (e.g. "D" for daily, "M" for
78 | monthly). See [Pandas frequency aliases](https://pandas.pydata.org/
79 | pandas-docs/stable/user_guide/timeseries.html#offset-aliases) for
80 | valid values. If not provided, the frequency will be inferred
81 | from the data.
82 | level (list[int | float], optional):
83 | Confidence levels for prediction intervals, expressed as
84 | percentages (e.g. [80, 95]). If provided, the returned
85 | DataFrame will include lower and upper interval columns for
86 | each specified level.
87 | quantiles (list[float], optional):
88 | List of quantiles to forecast, expressed as floats between 0
89 | and 1. Should not be used simultaneously with `level`. When
90 | provided, the output DataFrame will contain additional columns
91 | named in the format "model-q-{percentile}", where {percentile}
92 | = 100 × quantile value.
93 |
94 | Returns:
95 | pd.DataFrame:
96 | DataFrame containing forecast results. Includes:
97 |
98 | - point forecasts for each timestamp and series.
99 | - prediction intervals if `level` is specified.
100 | - quantile forecasts if `quantiles` is specified.
101 |
102 | For multi-series data, the output retains the same unique
103 | identifiers as the input DataFrame.
104 | """
105 | freq = self._maybe_infer_freq(df, freq)
106 | fcst_df = self._apply_parallel(
107 | df.groupby("unique_id"),
108 | self._local_forecast,
109 | h=h,
110 | freq=freq,
111 | level=level,
112 | quantiles=quantiles,
113 | )
114 | return fcst_df
115 |
--------------------------------------------------------------------------------
/experiments/gift-eval/README.md:
--------------------------------------------------------------------------------
1 | # First-Place Results on the GIFT-Eval Benchmark
2 |
3 | This section documents the evaluation of a foundation model ensemble built using the [TimeCopilot](https://timecopilot.dev) library on the [GIFT-Eval](https://huggingface.co/spaces/Salesforce/GIFT-Eval) benchmark.
4 |
5 | !!! success ""
6 | With less than $30 in compute cost, TimeCopilot achieved first place in probabilistic accuracy (CRPS) among open-source solution on this large-scale benchmark, which spans 24 datasets, 144k+ time series, and 177M data points.
7 |
8 |
9 | TimeCopilot is an open‑source AI agent for time series forecasting that provides a unified interface to multiple forecasting approaches, from foundation models to classical statistical, machine learning, and deep learning methods, along with built‑in ensemble capabilities for robust and explainable forecasting.
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ## Description
18 |
19 | This ensemble leverages [**TimeCopilot's MedianEnsemble**](https://timecopilot.dev/api/models/ensembles/#timecopilot.models.ensembles.median.MedianEnsemble) feature, which combines three state-of-the-art foundation models:
20 |
21 | - [**Chronos-2** (AWS)](https://timecopilot.dev/api/models/foundation/models/#timecopilot.models.foundation.chronos.Chronos).
22 | - [**TimesFM-2.5** (Google Research)](https://timecopilot.dev/api/models/foundation/models/#timecopilot.models.foundation.timesfm.TimesFM).
23 | - [**TiRex** (NXAI)](https://timecopilot.dev/api/models/foundation/models/#timecopilot.models.foundation.tirex.TiRex).
24 |
25 | ## Setup
26 |
27 | ### Prerequisites
28 | - Clone [TimeCopilot's repo](https://github.com/TimeCopilot/timecopilot) and go to `experiments/gift-eval`.
29 | - Python 3.11+
30 | - [uv](https://docs.astral.sh/uv/) package manager
31 | - AWS CLI configured (for distributed evaluation)
32 | - [Modal](https://modal.com/) account (for distributed evaluation)
33 |
34 | ### Installation
35 |
36 | ```bash
37 | # Install dependencies
38 | uv sync
39 | ```
40 |
41 | ## Dataset Management
42 |
43 | ### Download GIFT-Eval Dataset
44 |
45 | ```bash
46 | # Download the complete GIFT-Eval dataset
47 | make download-gift-eval-data
48 | ```
49 |
50 | This downloads all 97 dataset configurations to `./data/gift-eval/`.
51 |
52 | ### Upload to S3 (Optional)
53 |
54 | For distributed evaluation, upload the dataset to S3:
55 |
56 | ```bash
57 | # Upload dataset to S3 for distributed access
58 | make upload-data-to-s3
59 | ```
60 |
61 | ## Evaluation Methods
62 |
63 | ### 1. Local Evaluation
64 |
65 | Run evaluation on a single dataset locally:
66 |
67 | ```bash
68 | uv run -m src.run_timecopilot \
69 | --dataset-name "m4_weekly" \
70 | --term "short" \
71 | --output-path "./results/timecopilot/" \
72 | --storage-path "./data/gift-eval"
73 | ```
74 |
75 | **Parameters:**
76 |
77 | - `--dataset-name`: GIFT-Eval dataset name (e.g., "m4_weekly", "bizitobs_l2c/H")
78 | - `--term`: Forecasting horizon ("short", "medium", "long")
79 | - `--output-path`: Directory to save evaluation results
80 | - `--storage-path`: Path to GIFT-Eval dataset
81 |
82 | ### 2. Distributed Evaluation (Recommended)
83 |
84 | Evaluate all 97 dataset configurations in parallel using [modal](https://modal.com/):
85 |
86 | ```bash
87 | # Run distributed evaluation on Modal cloud
88 | uv run modal run --detach -m src.run_modal::main
89 | ```
90 |
91 | This creates one GPU job per dataset configuration, significantly reducing evaluation time.
92 |
93 | **Infrastructure:**
94 |
95 | - **GPU**: A10G per job
96 | - **CPU**: 8 cores per job
97 | - **Timeout**: 3 hours per job
98 | - **Storage**: S3 bucket for data and results
99 |
100 | ### 3. Collect Results
101 |
102 | Download and consolidate results from distributed evaluation:
103 |
104 | ```bash
105 | # Download all results from S3 and create consolidated CSV
106 | uv run python -m src.download_results
107 | ```
108 |
109 | Results are saved to `results/timecopilot/all_results.csv` in GIFT-Eval format.
110 |
111 |
112 | ## Changelog
113 |
114 | ### **2025-11-06**
115 |
116 | We introduced newer models based on the most recent progress in the field: Chronos-2, TimesFM-2.5 and TiRex.
117 |
118 | ### **2025-08-05**
119 |
120 | GIFT‑Eval recently [enhanced its evaluation dashboard](https://github.com/SalesforceAIResearch/gift-eval?tab=readme-ov-file#2025-08-05) with a new flag that identifies models likely affected by data leakage (i.e., having seen parts of the test set during training). While the test set itself hasn’t changed, this new insight helps us better interpret model performance. To keep our results focused on truly unseen data, we’ve excluded any flagged models from this experiment and added the Sundial model to the ensemble. The previous experiment details remain available [here](https://github.com/TimeCopilot/timecopilot/tree/v0.0.14/experiments/gift-eval).
121 |
--------------------------------------------------------------------------------
/docs/contributing.md:
--------------------------------------------------------------------------------
1 | Your contributions are highly appreciated!
2 |
3 | ## Prerequisites
4 | Before proceeding, ensure the following tools and credentials are set up:
5 |
6 | - Install [uv](https://docs.astral.sh/uv/getting-started/installation/).
7 | - Install [pre-commit](https://pre-commit.com/#install).
8 |
9 | !!! tip "Tip"
10 | Once `uv` is installed, you can easily install `pre-commit` by running:
11 | ```
12 | uv tool install pre-commit
13 | ```
14 |
15 | - Set up `pre-commit` hook:
16 | ```
17 | pre-commit install --install-hooks
18 | ```
19 | - Generate an OpenAI API Key:
20 | 1. Create an [openai](https://auth.openai.com/log-in) account.
21 | 2. Visit the [API key](https://platform.openai.com/api-keys) page.
22 | 3. Generate a new secret key.
23 | You'll need this key in the setup section below.
24 |
25 | ## Installation and Setup
26 | To run timecopilot in your local environment:
27 |
28 | 1. Fork and clone the repository:
29 | ```
30 | git clone git@github.com:/timecopilot.git
31 | ```
32 | 2. Navigate into the project folder:
33 | ```
34 | cd timecopilot
35 | ```
36 | 3. Install the required dependencies for local development:
37 | ```
38 | uv sync --frozen --all-extras --all-packages --group docs
39 | ```
40 | 4. Export your OpenAI API key as an environment variable:
41 | ```
42 | export OPENAI_API_KEY=""
43 | ```
44 | 5. Test timecopilot with a sample forecast:
45 | ```
46 | uvx timecopilot forecast https://otexts.com/fpppy/data/AirPassengers.csv
47 | ```
48 |
49 | ✅ You're ready to start contributing!
50 |
51 | ## Running Tests
52 |
53 | To run tests, run:
54 |
55 | ```bash
56 | uv run pytest
57 | ```
58 |
59 | ## Documentation Changes
60 |
61 | To run the documentation page in your local environment, run:
62 |
63 | ```bash
64 | uv run mkdocs serve
65 | ```
66 |
67 |
68 | ### Documentation Notes
69 |
70 | - Each pull request is tested to ensure it can successfully build the documentation, preventing potential errors.
71 | - Merging into the main branch triggers a deployment of a documentation preview, accessible at [preview.timecopilot.dev](https://preview.timecopilot.dev).
72 | - When a new version of the library is released, the documentation is deployed to [timecopilot.dev](https://timecopilot.dev).
73 |
74 | ### File Naming Convention
75 |
76 | All documentation files should use **kebab-case** (e.g., `model-hub.md`, `forecasting-parameters.md`). Kebab-case is widely used in static site generators and web contexts because it is URL-friendly, consistent, and avoids ambiguity with underscores or `camelCase`. Using a single convention improves readability and prevents broken links in deployment.
77 |
78 | For further reference, see the [Google Developer Documentation Style Guide on file names](https://developers.google.com/style/filenames).
79 |
80 | ## Adding New Datasets
81 |
82 | The datasets utilized in our documentation are hosted on AWS at `https://timecopilot.s3.amazonaws.com/public/data/`. If you wish to contribute additional datasets for your changes, please contact [@AzulGarza](http://github.com/AzulGarza) for guidance.
83 |
84 | ## Forked Dependencies
85 |
86 | TimeCopilot uses some forked Python packages, maintained under custom names on PyPI:
87 |
88 |
89 | - **chronos-forecasting**
90 | - Forked from: [amazon-science/chronos-forecasting](https://github.com/amazon-science/chronos-forecasting)
91 | - TimeCopilot fork: [AzulGarza/chronos-forecasting](https://github.com/AzulGarza/chronos-forecasting/tree/feat/timecopilot-chronos-forecasting)
92 | - Published on PyPI as: [`timecopilot-chronos-forecasting`](https://pypi.org/project/timecopilot-chronos-forecasting/)
93 |
94 |
95 | - **granite-tsfm**
96 | - Forked from: [ibm-granite/granite-tsfm](https://github.com/ibm-granite/granite-tsfm)
97 | - TimeCopilot fork: [AzulGarza/granite-tsfm](https://github.com/AzulGarza/granite-tsfm)
98 | - Published on PyPI as: [`timecopilot-granite-tsfm`](https://pypi.org/project/timecopilot-granite-tsfm/)
99 |
100 | - **timesfm**
101 | - Forked from: [google-research/timesfm](https://github.com/google-research/timesfm)
102 | - TimeCopilot fork: [AzulGarza/timesfm](https://github.com/AzulGarza/timesfm)
103 | - Published on PyPI as: [`timecopilot-timesfm`](https://pypi.org/project/timecopilot-timesfm/)
104 |
105 | - **tirex**
106 | - Forked from: [NX-AI/tirex](https://github.com/NX-AI/tirex)
107 | - TimeCopilot fork: [AzulGarza/tirex](https://github.com/AzulGarza/tirex)
108 | - Published on PyPI as: [`timecopilot-tirex`](https://pypi.org/project/timecopilot-tirex/)
109 |
110 | - **toto**
111 | - Forked from: [DataDog/toto](https://github.com/DataDog/toto)
112 | - TimeCopilot fork: [AzulGarza/toto](https://github.com/AzulGarza/toto)
113 | - Published on PyPI as: [`timecopilot-toto`](https://pypi.org/project/timecopilot-toto/)
114 |
115 | - **uni2ts**:
116 | - Forked from: [SalesforceAIResearch/uni2ts](https://github.com/SalesforceAIResearch/uni2ts)
117 | - TimeCopilot fork: [AzulGarza/uni2ts](https://github.com/AzulGarza/uni2ts)
118 | - Published on PyPI as: [`timecopilot-uni2ts`](https://pypi.org/project/timecopilot-uni2ts/)
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: ""
2 | site_description: The GenAI Forecasting Agent · LLMs x Foundation Time Series Models
3 | site_author: AzulGarza
4 | strict: true
5 | site_url: https://timecopilot.dev
6 |
7 | repo_name: TimeCopilot/timecopilot
8 | repo_url: https://github.com/TimeCopilot/timecopilot
9 | edit_uri: edit/main/docs
10 |
11 | watch:
12 | - "README.md"
13 | - timecopilot
14 |
15 | nav:
16 | - Home: index.md
17 | - Getting Started:
18 | - Introduction: getting-started/introduction.md
19 | - Quickstart: getting-started/quickstart.md
20 | - Installation: getting-started/installation.md
21 | - Examples:
22 | - examples/agent-quickstart.ipynb
23 | - examples/llm-providers.ipynb
24 | - examples/aws-bedrock.ipynb
25 | - examples/forecaster-quickstart.ipynb
26 | - examples/anomaly-detection-forecaster-quickstart.ipynb
27 | - examples/ts-foundation-models-comparison-quickstart.ipynb
28 | - examples/gift-eval.ipynb
29 | - examples/chronos-family.ipynb
30 | - examples/cryptocurrency-quickstart.ipynb
31 | - Experiments:
32 | - experiments/gift-eval.md
33 | - experiments/fev.md
34 | - Documentation:
35 | - Time Series Model Hub: model-hub.md
36 | - Forecasting Parameters: forecasting-parameters.md
37 | - API Reference:
38 | - api/agent.md
39 | - api/forecaster.md
40 | - api/models/foundation/models.md
41 | - api/models/stats.md
42 | - api/models/prophet.md
43 | - api/models/ml.md
44 | - api/models/neural.md
45 | - api/models/ensembles.md
46 | - api/models/utils/forecaster.md
47 | - api/gift-eval/gift-eval.md
48 | - Changelogs:
49 | - changelogs/index.md
50 | - changelogs/v0.0.22.md
51 | - changelogs/v0.0.21.md
52 | - changelogs/v0.0.20.md
53 | - changelogs/v0.0.19.md
54 | - changelogs/v0.0.18.md
55 | - changelogs/v0.0.17.md
56 | - changelogs/v0.0.16.md
57 | - changelogs/v0.0.15.md
58 | - changelogs/v0.0.14.md
59 | - changelogs/v0.0.13.md
60 | - changelogs/v0.0.12.md
61 | - changelogs/v0.0.11.md
62 | - changelogs/v0.0.10.md
63 | - Community:
64 | - Getting Help: community/help.md
65 | - Contributing: contributing.md
66 | - Roadmap: community/roadmap.md
67 | - Blog:
68 | - blog/index.md
69 |
70 | theme:
71 | name: "material"
72 | logo: https://timecopilot.s3.amazonaws.com/public/logos/logo-white.svg
73 | favicon: https://timecopilot.s3.amazonaws.com/public/logos/favicon-white.svg
74 | palette:
75 | - media: "(prefers-color-scheme)"
76 | primary: custom
77 | accent: light-blue
78 | toggle:
79 | icon: material/brightness-auto
80 | name: "Switch to light mode"
81 | - media: "(prefers-color-scheme: light)"
82 | scheme: default
83 | primary: custom
84 | accent: light-blue
85 | toggle:
86 | icon: material/brightness-7
87 | name: "Switch to dark mode"
88 | - media: "(prefers-color-scheme: dark)"
89 | scheme: slate
90 | primary: custom
91 | accent: light-blue
92 | toggle:
93 | icon: material/brightness-4
94 | name: "Switch to system preference"
95 | features:
96 | - search.suggest
97 | - search.highlight
98 | - content.tabs.link
99 | - content.code.annotate
100 | - content.code.copy
101 | - content.code.select
102 | - navigation.path
103 | - navigation.indexes
104 | - navigation.sections
105 | - navigation.tracking
106 | - navigation.tabs
107 | - navigation.tabs.sticky
108 | - navigation.footer
109 | - toc.follow
110 |
111 | extra_css:
112 | - stylesheets/extra.css
113 |
114 | markdown_extensions:
115 | - attr_list
116 | - admonition
117 | - pymdownx.details
118 | - footnotes
119 | - pymdownx.emoji:
120 | emoji_index: !!python/name:material.extensions.emoji.twemoji
121 | emoji_generator: !!python/name:material.extensions.emoji.to_svg
122 | - pymdownx.magiclink
123 | - pymdownx.snippets:
124 | base_path: [!relative $config_dir]
125 | check_paths: true
126 | - pymdownx.superfences
127 | - pymdownx.tabbed:
128 | alternate_style: true
129 | slugify: !!python/object/apply:pymdownx.slugs.slugify
130 | kwds:
131 | case: lower
132 | - def_list
133 | - pymdownx.tasklist:
134 | custom_checkbox: true
135 | - tables
136 |
137 | plugins:
138 | - blog:
139 | post_url_format: "{slug}"
140 | - search
141 | - autorefs
142 | - mkdocs-jupyter
143 | - include-markdown
144 | - mkdocstrings:
145 | handlers:
146 | python:
147 | paths: [timecopilot]
148 | options:
149 | relative_crossrefs: true
150 | members_order: source
151 | separate_signature: true
152 | show_signature_annotations: true
153 | signature_crossrefs: true
154 | group_by_category: false
155 | heading_level: 3
156 | inherited_members: true
157 | members: true
158 | merge_init_into_class: true
159 | extensions:
160 | - griffe_inherited_docstrings:
161 | merge: true
162 |
163 | copyright: 2025 - TimeCopilot
164 |
--------------------------------------------------------------------------------
/docs/forecasting-parameters.md:
--------------------------------------------------------------------------------
1 | This section describes how [`TimeCopilot`][timecopilot.agent.TimeCopilot] determines the three core forecasting parameters: `freq`, `h`, and `seasonality` when you call
2 | [`TimeCopilot.forecast(...)`][timecopilot.agent.TimeCopilot.forecast].
3 |
4 | You can:
5 |
6 | - let the assistant infer everything automatically (recommended for a quick
7 | start),
8 | - provide the values in plain-English inside the `query`, or
9 | - override them explicitly with keyword arguments.
10 |
11 | ### What do these terms mean?
12 |
13 | * **`freq`**: the pandas frequency string that describes the spacing of your
14 | timestamps (`"H"` for hourly, `"D"` for daily, `"MS"` for monthly-start,
15 | etc.). It tells the models how many observations occur in one unit of
16 | *seasonality*.
17 | * **`seasonality`**: the length of the dominant seasonal cycle expressed in
18 | number of `freq` periods (24 for hourly data with a daily cycle, 12 for
19 | monthly‐start data with a yearly cycle, …). See
20 | [`get_seasonality`][timecopilot.models.utils.forecaster.get_seasonality] for the default mapping.
21 | * **`h` (horizon)**: how many future periods you want to forecast.
22 |
23 | !!! tip "Pandas available frequencies"
24 | You can see [here](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases) the complete list of available frequencies.
25 |
26 | With these concepts in mind, let's see how [`TimeCopilot`][timecopilot.agent.TimeCopilot] chooses their values.
27 |
28 | ## Where do the parameters come from?
29 |
30 | `TimeCopilot` follows these precedence rules:
31 |
32 | 1. **Natural-language query wins.**
33 | If the query text mentions any of the three parameters they are extracted by
34 | an LLM agent and used first.
35 | 2. **Explicit keyword arguments are next.**
36 | Any argument you pass directly to `parse()` ( `freq=`, `h=`,
37 | `seasonality=` ) fills the gaps left by the query.
38 | 3. **Automatic inference is the fallback.**
39 | If a value is still unknown it is inferred from the data frame:
40 | * `freq`: [`maybe_infer_freq(df)`][timecopilot.models.utils.forecaster.maybe_infer_freq]
41 | * `seasonality`: [`get_seasonality(freq)`][timecopilot.models.utils.forecaster.get_seasonality]
42 | * `h`: `2 * seasonality`
43 |
44 | !!! tip "Summary"
45 | Text -> kwargs -> automatic inference (from your data, `df`)
46 |
47 | ## Passing parameters in a natural-language query
48 |
49 | Sometimes it's easier to embed the settings directly in your query:
50 |
51 | ```python
52 | import pandas as pd
53 | from timecopilot.agent import TimeCopilot
54 |
55 | df = pd.read_csv("https://timecopilot.s3.amazonaws.com/public/data/air_passengers.csv")
56 | query = """
57 | Which months will have peak passenger traffic in the next 24 months?
58 | use 12 as seasonality and MS as freq
59 | """
60 |
61 | tc = TimeCopilot(llm="gpt-5-mini", retries=3)
62 |
63 | # Passing `None` simply uses the defaults; they are shown
64 | # here for clarity but can be omitted.
65 | result = tc.forecast(
66 | df=df,
67 | query=query,
68 | freq=None, # default, infer from query/df
69 | h=None, # default, infer from query/df
70 | seasonality=None, # default, infer from query/df
71 | )
72 |
73 | print(result.output.user_query_response)
74 | # Based on the forecast, peak passenger traffic
75 | # in the next 24 months is expected to occur in the months of July and August
76 | # both in 1961 and 1962.
77 | ```
78 |
79 | ??? note "How does the inference happen?"
80 | Under the hood the LLM receives a system prompt like:
81 |
82 | > "Extract the following fields if they appear in the user text…"
83 |
84 | …and returns a JSON tool call that is validated against the
85 | `DatasetParams` schema.
86 |
87 |
88 | ## Supplying the parameters programmatically (skip the LLM)
89 |
90 | If you already know the values you can skip the LLM entirely:
91 |
92 | ```python
93 | result = tc.forecast(
94 | df=df,
95 | freq="MS", # monthly-start
96 | h=12, # one year ahead
97 | seasonality=12, # yearly
98 | query=None, # no natural-language query
99 | )
100 | print(result.output)
101 | ```
102 |
103 | Because every field is supplied, no inference or LLM call happens.
104 |
105 | ## Mixed approach (query + kwargs)
106 |
107 | You can combine both techniques. The parser fills the *missing* fields from the
108 | kwargs or, if still empty, infers them:
109 |
110 | ```python
111 | query = "Which months will have peak passenger traffic in the next 24 months?"
112 | result = tc.forecast(
113 | df=df,
114 | freq="MS", # explicit override
115 | h=None, # default, pulled from query (24)
116 | seasonality=None, # default, inferred as 12
117 | query=query,
118 | )
119 | print(result.output.user_query_response)
120 | ```
121 |
122 | ## Choosing sensible defaults
123 |
124 | When you let [`TimeCopilot`][timecopilot.agent.TimeCopilot] infer the parameters:
125 |
126 | * `freq` should be either present in the query **or** directly deducible from
127 | your `ds` column (regular timestamps with no gaps).
128 | * `seasonality` defaults to the conventional period for the frequency
129 | (e.g. 7 for daily, 12 for monthly). Override it if your data behaves
130 | differently.
131 | * `h` defaults to twice the seasonal period—large enough for
132 | meaningful evaluation while staying quick to compute.
133 |
134 | !!! note
135 | These defaults aim to keep the *first run* friction-free. Fine-tune them
136 | as soon as you have more insight into your particular dataset.
--------------------------------------------------------------------------------
/docs/model-hub.md:
--------------------------------------------------------------------------------
1 | # Time Series Model Hub
2 |
3 |
4 | TimeCopilot provides a unified API for time series forecasting, integrating foundation models, classical statistical models, machine learning, and neural network families of models. This approach lets you experiment, benchmark, and deploy a wide range of forecasting models with minimal code changes, so you can choose the best tool for your data and use case.
5 |
6 | Here you'll find all the time series forecasting models available in TimeCopilot, organized by family. Click on any model name to jump to its detailed API documentation.
7 |
8 | !!! tip "Forecast multiple models using a unified API"
9 |
10 | With the [TimeCopilotForecaster][timecopilot.forecaster.TimeCopilotForecaster] class, you can generate and cross-validate forecasts using a unified API. Here's an example:
11 |
12 | ```python
13 | import pandas as pd
14 | from timecopilot.forecaster import TimeCopilotForecaster
15 | from timecopilot.models.prophet import Prophet
16 | from timecopilot.models.stats import AutoARIMA, SeasonalNaive
17 | from timecopilot.models.foundation.toto import Toto
18 |
19 | df = pd.read_csv(
20 | "https://timecopilot.s3.amazonaws.com/public/data/air_passengers.csv",
21 | parse_dates=["ds"],
22 | )
23 | tcf = TimeCopilotForecaster(
24 | models=[
25 | AutoARIMA(),
26 | SeasonalNaive(),
27 | Prophet(),
28 | Toto(context_length=256),
29 | ]
30 | )
31 |
32 | fcst_df = tcf.forecast(df=df, h=12)
33 | cv_df = tcf.cross_validation(df=df, h=12)
34 | ```
35 |
36 | ---
37 |
38 | ## Foundation Models
39 |
40 | TimeCopilot provides a unified interface to state-of-the-art foundation models for time series forecasting. These models are designed to handle a wide range of forecasting tasks, from classical seasonal patterns to complex, high-dimensional data. Below you will find a list of all available foundation models, each with a dedicated section describing its API and usage.
41 |
42 | - [Chronos](api/models/foundation/models.md#timecopilot.models.foundation.chronos) ([arXiv:2403.07815](https://arxiv.org/abs/2403.07815))
43 | - [FlowState](api/models/foundation/models.md#timecopilot.models.foundation.flowstate) ([arXiv:2508.05287](https://arxiv.org/abs/2508.05287))
44 | - [Moirai](api/models/foundation/models.md#timecopilot.models.foundation.moirai) ([arXiv:2402.02592](https://arxiv.org/abs/2402.02592))
45 | - [Sundial](api/models/foundation/models.md#timecopilot.models.foundation.sundial) ([arXiv:2502.00816](https://arxiv.org/pdf/2502.00816))
46 | - [TabPFN](api/models/foundation/models.md#timecopilot.models.foundation.tabpfn) ([arXiv:2501.02945](https://arxiv.org/abs/2501.02945))
47 | - [TiRex](api/models/foundation/models.md#timecopilot.models.foundation.tirex) ([arXiv:2505.23719](https://arxiv.org/abs/2505.23719))
48 | - [TimeGPT](api/models/foundation/models.md#timecopilot.models.foundation.timegpt) ([arXiv:2310.03589](https://arxiv.org/abs/2310.03589))
49 | - [TimesFM](api/models/foundation/models.md#timecopilot.models.foundation.timesfm) ([arXiv:2310.10688](https://arxiv.org/abs/2310.10688))
50 | - [Toto](api/models/foundation/models.md#timecopilot.models.foundation.toto) ([arXiv:2505.14766](https://arxiv.org/abs/2505.14766))
51 |
52 | ---
53 |
54 | ## Statistical & Classical Models
55 |
56 | TimeCopilot includes a suite of classical and statistical forecasting models, providing robust baselines and interpretable alternatives to foundation models. These models are ideal for quick benchmarking, transparent forecasting, and scenarios where simplicity and speed are paramount. Below is a list of all available statistical models, each with a dedicated section describing its API and usage.
57 |
58 | - [ADIDA](api/models/stats.md#timecopilot.models.stats.ADIDA)
59 | - [AutoARIMA](api/models/stats.md#timecopilot.models.stats.AutoARIMA)
60 | - [AutoCES](api/models/stats.md#timecopilot.models.stats.AutoCES)
61 | - [AutoETS](api/models/stats.md#timecopilot.models.stats.AutoETS)
62 | - [CrostonClassic](api/models/stats.md#timecopilot.models.stats.CrostonClassic)
63 | - [DynamicOptimizedTheta](api/models/stats.md#timecopilot.models.stats.DynamicOptimizedTheta)
64 | - [HistoricAverage](api/models/stats.md#timecopilot.models.stats.HistoricAverage)
65 | - [IMAPA](api/models/stats.md#timecopilot.models.stats.IMAPA)
66 | - [SeasonalNaive](api/models/stats.md#timecopilot.models.stats.SeasonalNaive)
67 | - [Theta](api/models/stats.md#timecopilot.models.stats.Theta)
68 | - [ZeroModel](api/models/stats.md#timecopilot.models.stats.ZeroModel)
69 |
70 |
71 | ### Prophet Model
72 |
73 | TimeCopilot integrates the popular Prophet model for time series forecasting, developed by Facebook. Prophet is well-suited for business time series with strong seasonal effects and several seasons of historical data. Below you will find the API reference for the Prophet model.
74 |
75 |
76 | - [Prophet](api/models/prophet.md/#timecopilot.models.prophet.Prophet)
77 |
78 | ## Machine Learning Models
79 |
80 | TimeCopilot provides access to automated machine learning models for time series forecasting. These models leverage gradient boosting and other ML techniques to automatically select features and optimize hyperparameters for your specific time series data.
81 |
82 | - [AutoLGBM](api/models/ml.md#timecopilot.models.ml.AutoLGBM)
83 |
84 | ## Neural Network Models
85 |
86 | TimeCopilot integrates state-of-the-art neural network models for time series forecasting. These models leverage deep learning architectures specifically designed for temporal data, offering powerful capabilities for complex pattern recognition and long-range dependency modeling.
87 |
88 | - [AutoNHITS](api/models/neural.md#timecopilot.models.neural.AutoNHITS)
89 | - [AutoTFT](api/models/neural.md#timecopilot.models.neural.AutoTFT)
--------------------------------------------------------------------------------
/timecopilot/models/prophet.py:
--------------------------------------------------------------------------------
1 | from copy import deepcopy
2 | from typing import Any
3 |
4 | import pandas as pd
5 | from prophet import Prophet as ProphetBase
6 | from threadpoolctl import threadpool_limits
7 |
8 | from .utils.forecaster import QuantileConverter
9 | from .utils.parallel_forecaster import ParallelForecaster
10 |
11 |
12 | class Prophet(ProphetBase, ParallelForecaster):
13 | """
14 | Wrapper for Facebook Prophet for time series forecasting.
15 |
16 | Prophet is a procedure for forecasting time series data based on an additive model
17 | where non-linear trends are fit with yearly, weekly, and daily seasonality, plus
18 | holiday effects. It works best with time series that have strong seasonal effects
19 | and several seasons of historical data. Prophet is robust to missing data and
20 | shifts in the trend, and typically handles outliers well.
21 |
22 | See the [official documentation](https://github.com/facebook/prophet)
23 | for more details.
24 | """
25 |
26 | def __init__(
27 | self,
28 | alias: str = "Prophet",
29 | *args: Any,
30 | **kwargs: Any,
31 | ):
32 | """
33 | Args:
34 | alias (str, optional): Custom name for the model instance.
35 | Default is "Prophet".
36 | *args: Additional positional arguments passed to ProphetBase.
37 | **kwargs: Additional keyword arguments passed to ProphetBase.
38 |
39 | Raises:
40 | ValueError: If 'interval_width' is provided in kwargs. Use 'level' or
41 | 'quantiles' instead when using 'forecast' or 'cross_validation'.
42 | """
43 | super().__init__(*args, **kwargs)
44 | self.alias = alias
45 | if "interval_width" in kwargs:
46 | raise ValueError(
47 | "interval_width is not supported, "
48 | "use `level` or `quantiles` instead when using "
49 | "`forecast` or `cross_validation`"
50 | )
51 |
52 | def predict_uncertainty(
53 | self,
54 | df: pd.DataFrame,
55 | vectorized: bool,
56 | quantiles: list[float],
57 | ) -> pd.DataFrame:
58 | # adapted from https://github.com/facebook/prophet/blob/e64606036325bfb225333ef0991e41bdfb66f7c1/python/prophet/forecaster.py#L1431-L1455
59 | sim_values = self.sample_posterior_predictive(df, vectorized)
60 | series = {}
61 | for q in quantiles:
62 | series[f"yhat-q-{int(q * 100)}"] = self.percentile(
63 | sim_values["yhat"], 100 * q, axis=1
64 | )
65 | return pd.DataFrame(series)
66 |
67 | def predict(
68 | self,
69 | df: pd.DataFrame = None,
70 | vectorized: bool = True,
71 | quantiles: list[float] | None = None,
72 | ) -> pd.DataFrame:
73 | # Predict using the prophet model.
74 | # adapted from https://github.com/facebook/prophet/blob/e64606036325bfb225333ef0991e41bdfb66f7c1/python/prophet/forecaster.py#L1249C2-L1295C1
75 | # to allow for quantiles
76 | if self.history is None:
77 | raise Exception("Model has not been fit.")
78 | if df is None:
79 | df = self.history.copy()
80 | else:
81 | if df.shape[0] == 0:
82 | raise ValueError("Dataframe has no rows.")
83 | df = self.setup_dataframe(df.copy())
84 | df["trend"] = self.predict_trend(df)
85 | seasonal_components = self.predict_seasonal_components(df)
86 | if self.uncertainty_samples and quantiles is not None:
87 | intervals = self.predict_uncertainty(df, vectorized, quantiles)
88 | else:
89 | intervals = None
90 | # Drop columns except ds, cap, floor, and trend
91 | cols = ["ds", "trend"]
92 | if "cap" in df:
93 | cols.append("cap")
94 | if self.logistic_floor:
95 | cols.append("floor")
96 | # Add in forecast components
97 | df2 = pd.concat((df[cols], intervals, seasonal_components), axis=1)
98 | df2["yhat"] = (
99 | df2["trend"] * (1 + df2["multiplicative_terms"]) + df2["additive_terms"]
100 | )
101 | df2.columns = [col.replace("yhat", self.alias) for col in df2.columns]
102 | cols = [col for col in df2.columns if self.alias in col or "ds" in col]
103 | return df2[cols]
104 |
105 | def _local_forecast_impl(
106 | self,
107 | df: pd.DataFrame,
108 | h: int,
109 | freq: str,
110 | level: list[int | float] | None = None,
111 | quantiles: list[float] | None = None,
112 | ) -> pd.DataFrame:
113 | qc = QuantileConverter(level=level, quantiles=quantiles)
114 | model = deepcopy(self)
115 | model.fit(df=df)
116 | future_df = model.make_future_dataframe(
117 | periods=h,
118 | include_history=False,
119 | freq=freq,
120 | )
121 | fcst_df = model.predict(future_df, quantiles=qc.quantiles)
122 | fcst_df = qc.maybe_convert_quantiles_to_level(fcst_df, models=[self.alias])
123 | return fcst_df
124 |
125 | def _local_forecast(
126 | self,
127 | df: pd.DataFrame,
128 | h: int,
129 | freq: str,
130 | level: list[int | float] | None = None,
131 | quantiles: list[float] | None = None,
132 | ) -> pd.DataFrame:
133 | with threadpool_limits(limits=1):
134 | return self._local_forecast_impl(
135 | df=df,
136 | h=h,
137 | freq=freq,
138 | level=level,
139 | quantiles=quantiles,
140 | )
141 |
--------------------------------------------------------------------------------
/tests/test_live.py:
--------------------------------------------------------------------------------
1 | """
2 | Test that the agent works with a live LLM.
3 | Keeping it separate from the other tests because costs and requires a live LLM.
4 | """
5 |
6 | import logfire
7 | import pytest
8 | from dotenv import load_dotenv
9 | from utilsforecast.data import generate_series
10 |
11 | from timecopilot import TimeCopilot
12 | from timecopilot.agent import AsyncTimeCopilot
13 | from timecopilot.models.stats import SeasonalNaive, ZeroModel
14 |
15 | load_dotenv()
16 | logfire.configure(send_to_logfire="if-token-present")
17 | logfire.instrument_pydantic_ai()
18 |
19 | default_forecasters = [ZeroModel(), SeasonalNaive()]
20 |
21 |
22 | @pytest.mark.live
23 | @pytest.mark.flaky(reruns=3, reruns_delay=2)
24 | def test_forecast_custom_forecasters():
25 | h = 2
26 | df = generate_series(
27 | n_series=1,
28 | freq="D",
29 | min_length=30,
30 | static_as_categorical=False,
31 | with_trend=True,
32 | )
33 | tc = TimeCopilot(
34 | llm="openai:gpt-4o-mini",
35 | forecasters=[
36 | ZeroModel(),
37 | ],
38 | )
39 | result = tc.forecast(
40 | df=df,
41 | query=f"Please forecast the series with a horizon of {h} and frequency D.",
42 | )
43 | assert len(result.fcst_df) == h
44 | assert result.features_df is not None
45 | assert result.eval_df is not None
46 |
47 |
48 | @pytest.mark.live
49 | @pytest.mark.parametrize("n_series", [1, 2])
50 | @pytest.mark.flaky(reruns=3, reruns_delay=2)
51 | def test_forecast_returns_expected_output(n_series):
52 | h = 2
53 | df = generate_series(
54 | n_series=n_series,
55 | freq="D",
56 | min_length=30,
57 | static_as_categorical=False,
58 | )
59 | tc = TimeCopilot(
60 | llm="openai:gpt-4o-mini",
61 | forecasters=default_forecasters,
62 | retries=3,
63 | )
64 | result = tc.forecast(
65 | df=df,
66 | query=f"Please forecast the series with a horizon of {h} and frequency D.",
67 | )
68 | assert len(result.fcst_df) == n_series * h
69 | assert result.features_df is not None
70 | assert result.eval_df is not None
71 | assert result.output.is_better_than_seasonal_naive
72 | assert result.output.forecast_analysis is not None
73 | assert result.output.reason_for_selection is not None
74 |
75 |
76 | @pytest.mark.live
77 | @pytest.mark.flaky(reruns=3, reruns_delay=2)
78 | def test_is_queryable():
79 | h = 2
80 | df = generate_series(
81 | n_series=2,
82 | freq="D",
83 | min_length=30,
84 | static_as_categorical=False,
85 | )
86 | tc = TimeCopilot(
87 | llm="openai:gpt-4o-mini",
88 | retries=3,
89 | )
90 | assert not tc.is_queryable()
91 | result = tc.forecast(
92 | df=df,
93 | query=f"Please forecast the series with a horizon of {h} and frequency D.",
94 | )
95 | assert tc.is_queryable()
96 | result = tc.query("how much will change the series with id 0?")
97 | print(result.output)
98 |
99 |
100 | @pytest.mark.live
101 | @pytest.mark.asyncio
102 | @pytest.mark.parametrize("n_series", [1, 2])
103 | @pytest.mark.flaky(reruns=3, reruns_delay=2)
104 | async def test_async_forecast_returns_expected_output(n_series):
105 | h = 2
106 | df = generate_series(
107 | n_series=n_series,
108 | freq="D",
109 | min_length=30,
110 | static_as_categorical=False,
111 | )
112 | tc = AsyncTimeCopilot(
113 | llm="openai:gpt-4o-mini",
114 | forecasters=default_forecasters,
115 | retries=3,
116 | )
117 | result = await tc.forecast(
118 | df=df,
119 | query=f"Please forecast the series with a horizon of {h} and frequency D.",
120 | )
121 | assert len(result.fcst_df) == n_series * h
122 | assert result.features_df is not None
123 | assert result.eval_df is not None
124 | assert result.output.is_better_than_seasonal_naive
125 | assert result.output.forecast_analysis is not None
126 | assert result.output.reason_for_selection is not None
127 |
128 |
129 | @pytest.mark.live
130 | @pytest.mark.asyncio
131 | @pytest.mark.flaky(reruns=3, reruns_delay=2)
132 | async def test_async_is_queryable():
133 | h = 2
134 | df = generate_series(
135 | n_series=2,
136 | freq="D",
137 | min_length=30,
138 | static_as_categorical=False,
139 | )
140 | tc = AsyncTimeCopilot(
141 | llm="openai:gpt-4o-mini",
142 | forecasters=default_forecasters,
143 | retries=3,
144 | )
145 | assert not tc.is_queryable()
146 | await tc.forecast(
147 | df=df,
148 | query=f"Please forecast the series with a horizon of {h} and frequency D.",
149 | )
150 | assert tc.is_queryable()
151 | answer = await tc.query("how much will change the series with id 0?")
152 | print(answer.output)
153 |
154 |
155 | @pytest.mark.live
156 | @pytest.mark.asyncio
157 | @pytest.mark.flaky(reruns=3, reruns_delay=2)
158 | async def test_async_query_stream():
159 | h = 2
160 | df = generate_series(
161 | n_series=1,
162 | freq="D",
163 | min_length=30,
164 | static_as_categorical=False,
165 | )
166 | tc = AsyncTimeCopilot(
167 | llm="openai:gpt-4o-mini",
168 | forecasters=default_forecasters,
169 | retries=3,
170 | )
171 | await tc.forecast(
172 | df=df,
173 | query=f"Please forecast the series with a horizon of {h} and frequency D.",
174 | )
175 | async with tc.query_stream("What is the forecast for the next 2 days?") as result:
176 | # This will yield a StreamedRunResult, which can be streamed for text
177 | async for text in result.stream(debounce_by=0.01):
178 | print(text, end="", flush=True)
179 |
--------------------------------------------------------------------------------
/docs/changelogs/v0.0.17.md:
--------------------------------------------------------------------------------
1 | ### Features
2 |
3 | * **Moirai2 Foundation Model**: Added support for the [Moirai2](https://github.com/SalesforceAIResearch/uni2ts) model, a new state-of-the-art foundation model for time series forecasting. See [#177](https://github.com/TimeCopilot/timecopilot/pull/177).
4 |
5 | ```python
6 | import pandas as pd
7 | from timecopilot.models.foundation.moirai import Moirai
8 |
9 | df = pd.read_csv(
10 | "https://timecopilot.s3.amazonaws.com/public/data/air_passengers.csv",
11 | parse_dates=["ds"]
12 | )
13 | model = Moirai(repo_id="Salesforce/moirai-2.0-R-small")
14 | fcst = model.forecast(df, h=12)
15 | print(fcst)
16 | ```
17 |
18 | * **Machine Learning and Neural Forecasting Methods**: Expanded the forecasting capabilities with new ML and neural methods including `AutoLightGBM`, `AutoNHITS` y `AutoTFT`. See [#181](https://github.com/TimeCopilot/timecopilot/pull/181).
19 |
20 | * **Static Plot Method**: Added a static plotting method for visualizing forecasts without requiring an agent instance. See [#183](https://github.com/TimeCopilot/timecopilot/pull/183).
21 |
22 | ```python
23 | import pandas as pd
24 | from timecopilot import TimeCopilotForecaster
25 | from timecopilot.models.foundation.moirai import Moirai
26 | from timecopilot.models.prophet import Prophet
27 | from timecopilot.models.stats import AutoARIMA, AutoETS, SeasonalNaive
28 |
29 | df = pd.read_csv(
30 | "https://timecopilot.s3.amazonaws.com/public/data/air_passengers.csv",
31 | parse_dates=["ds"],
32 | )
33 | tcf = TimeCopilotForecaster(
34 | models=[
35 | AutoARIMA(),
36 | AutoETS(),
37 | Moirai(),
38 | Prophet(),
39 | SeasonalNaive(),
40 | ]
41 | )
42 | fcst_df = tcf.forecast(df=df, h=12, level=[80, 90])
43 | tcf.plot(df, fcst_df, level=[80, 90])
44 | ```
45 |
46 | * **Enhanced Documentation with Examples**: Added comprehensive examples section using mkdocs-jupyter, including interactive notebooks for [agent quickstart](https://timecopilot.dev/examples/agent-quickstart/) and [forecaster usage](https://timecopilot.dev/examples/forecaster-quickstart/). See [#176](https://github.com/TimeCopilot/timecopilot/pull/176) and [#198](https://github.com/TimeCopilot/timecopilot/pull/198).
47 |
48 | * **GIFT-Eval Plotting**: Added plots for the [GIFT-Eval experiment](https://timecopilot.dev/experiments/gift-eval/) to better visualize model performance across different datasets. See [#180](https://github.com/TimeCopilot/timecopilot/pull/180).
49 |
50 | * **Improved Date and Target Column Handling**: Specify to the agent the handling of date (`ds`) and target (`y`) columns. See [#139](https://github.com/TimeCopilot/timecopilot/pull/139).
51 |
52 | ### Refactorings
53 |
54 | * **Clearer Models Structure**: Reorganized the models module for better clarity and maintainability. Models are now organized into logical categories: `stats`, `ml`, `neural`, `foundation`, and `ensembles`. See [#203](https://github.com/TimeCopilot/timecopilot/pull/203).
55 | - Prophet moved from `models.benchmarks.prophet` to `models.prophet`
56 | - Statistical models moved from `models.benchmarks.stats` to `models.stats`
57 | - ML models moved from `models.benchmarks.ml` to `models.ml`
58 | - Neural models moved from `models.benchmarks.neural` to `models.neural`
59 |
60 | * **Improved DataFrame Concatenation**: Optimized DataFrame concatenation in feature extraction loops for better performance. See [#105](https://github.com/TimeCopilot/timecopilot/pull/105).
61 |
62 | ### Fixes
63 |
64 | * **OpenAI Version Compatibility**: Unpinned OpenAI version to resolve compatibility issues with recent releases. See [#171](https://github.com/TimeCopilot/timecopilot/pull/171).
65 |
66 | * **Median Ensemble Level Test**: Relaxed test constraints for median ensemble levels to improve test reliability. See [#175](https://github.com/TimeCopilot/timecopilot/pull/175).
67 |
68 | * **Documentation URL Format**: Updated documentation to use kebab-case URLs for better consistency. See [#200](https://github.com/TimeCopilot/timecopilot/pull/200).
69 |
70 | * **Explicit Keyword Arguments**: Added explicit override handling for keyword arguments to prevent unexpected behavior. See [#202](https://github.com/TimeCopilot/timecopilot/pull/202).
71 |
72 | ### Documentation
73 |
74 | * **Enhanced README**: Improved README content with additional information and fixed various typos. See [#172](https://github.com/TimeCopilot/timecopilot/pull/172), [#187](https://github.com/TimeCopilot/timecopilot/pull/187), [#188](https://github.com/TimeCopilot/timecopilot/pull/188).
75 |
76 | * **New Logo and Branding**: Added new logos and favicon for improved visual identity. See [#185](https://github.com/TimeCopilot/timecopilot/pull/185), [#186](https://github.com/TimeCopilot/timecopilot/pull/186).
77 |
78 | * **Issue Templates**: Added GitHub issue templates to streamline bug reporting and feature requests. See [#193](https://github.com/TimeCopilot/timecopilot/pull/193).
79 |
80 | * **Documentation Testing**: Added comprehensive tests for documentation to ensure code examples work correctly. See [#194](https://github.com/TimeCopilot/timecopilot/pull/194).
81 |
82 | ### Infrastructure
83 |
84 | * **CI/CD Improvements**: Moved linting action to the main CI workflow for better organization. See [#174](https://github.com/TimeCopilot/timecopilot/pull/174).
85 |
86 | * **Discord Release Notifications**: Added automated Discord notifications for new releases. See [#195](https://github.com/TimeCopilot/timecopilot/pull/195), [#196](https://github.com/TimeCopilot/timecopilot/pull/196), [#197](https://github.com/TimeCopilot/timecopilot/pull/197).
87 |
88 | * **Improved Experiment Naming**: Better naming conventions for GIFT-Eval experiments. See [#199](https://github.com/TimeCopilot/timecopilot/pull/199).
89 |
90 | ## New Contributors
91 | * @elmartinj made their first contribution in [#187](https://github.com/TimeCopilot/timecopilot/pull/187)
92 | * @friscobrisco made their first contribution in [#139](https://github.com/TimeCopilot/timecopilot/pull/139)
93 |
94 | ---
95 |
96 | **Full Changelog**: https://github.com/TimeCopilot/timecopilot/compare/v0.0.16...v0.0.17
97 |
--------------------------------------------------------------------------------
/docs/blog/posts/forecasting-the-agentic-way.md:
--------------------------------------------------------------------------------
1 | ---
2 | date: 2025-12-01
3 | authors:
4 | - azulgarza
5 | - reneerosillo
6 | categories:
7 | - General
8 | description: >
9 | We are thrilled to announce TimeCopilot
10 | title: Forecasting, the Agentic Way
11 | slug: forecasting-the-agentic-way
12 | ---
13 |
14 |
15 | # Forecasting, the Agentic Way
16 |
17 | TimeCopilot is open-source agentic forecasting with LLMs and the strongest time series models. We started TimeCopilot with a clear goal. We want to democratize time series forecasting and make it accessible not only to humans but to the next wave of automated agentic systems. Accurate, automated, and easy forecasts.
18 |
19 | ## We are all grounded by temporal data
20 |
21 | In 2005, Steve Jobs [told the Stanford graduating class](https://www.youtube.com/watch?v=UF8uR6Z6KLc) that we can only connect the dots looking backwards. At a fundamental level, he named one of the forces that shapes how we live. He explained how we move through the world without knowing what our actions will create, and why trusting those actions matters when the outcome is uncertain. For professionals who work with forecasts every day, this is not an abstract idea. Our work supports planning, risk, demand, supply, energy, finance, operations, and more. And as Steve said, the success of our work depends on how well we use past outcomes in the form of time series data to see what the future might look like through the lens of our own context.
22 |
23 | Daily forecasting work is not glamorous. It is the nitty-gritty of managing pipelines, models, baselines, assumptions, overrides, and schedules. It means moving between scripts, dashboards, notebooks, and model families. Many forecasters know that explaining a forecast often takes more time than producing one.
24 |
25 | Why go to so much trouble to understand the future? As a field we know that [small shifts in starting conditions can create very different outcomes](https://en.wikipedia.org/wiki/Lorenz_system). And we know that uncertainty has structure. When we understand that structure, we make better decisions. There is satisfaction in taking something complex and turning it into a pattern we can recognize. In simple terms, we do it because we enjoy predicting the future. TimeCopilot is our rendition to the field. It reflects both technical progress and an ideal. We want to use all the technology we have to make better predictions.
26 |
27 | ## We believe the future opens up with LLMs
28 |
29 | Forecasting has moved through several eras. Classical statistics. Probabilistic approaches. Deep learning. And now time series foundation models trained on large and diverse datasets. Each method brings value, but none solves the full problem. Different inputs and assumptions need different tools. Every forecaster knows: [no single model, no matter how powerful, can solve every problem](https://en.wikipedia.org/wiki/No_free_lunch_theorem). Forecasting fuel needs for a high tide water platform is not the same as forecasting train schedules for a small province in Germany. Our work as forecasters has always relied on understanding context and resetting the constraints that make specific pipelines work for specific use cases. But what if we had an automated helper that understood our particular needs? With the rise of agentic systems, we finally have an extra set of hands to take on the repetitive work we have to deal with every day.
30 |
31 | As we see it, there’s a need to build a layer above all the foundation models and the systems that came before. A layer that knows when to use each method, how to combine them, and how to explain the reasoning behind a forecast.
32 |
33 | TimeCopilot is that layer. It uses a wide set of time series foundation models and classical models and libraries. It applies LLMs to add context, constraints, and reasoning. It explains its assumptions in natural language. It updates forecasts as new information arrives. It lets people work through text while keeping the granularity that teams need of hand-tuning pipelines: a coordinated reasoning layer that uses many tools and adapts as context shifts.
34 |
35 | With the dawn of large language models, it became evident that written word matters, and plain English may be the most natural interface for reasoning. Andrej Karpathy often notes that [language is becoming the new interface for software](https://x.com/karpathy/status/1617979122625712128?lang=en). Forecasting fits that idea. The work is built on uncertainty, intent, and assumptions. Text gives us a simple way to express that. Text translates our needs and helps us define the guardrails for our context. TimeCopilot is the agent for forecasting in the same way [Clay](https://www.clay.com/) helps marketing teams move faster and [Cursor](https://cursor.com/) helps engineering teams ship code with less friction.
36 |
37 | ## And so we asked ourselves a simple question: can we use agents for time series forecasting?
38 |
39 | TimeCopilot began as a first principles question about forecasting and context. Early prototypes led to a research effort. Our early project attracted collaborators, engineers, and contributors who cared about the same ideas. In a short time, it has become a meaningful open source time series project. We have assembled [the largest unified collection](https://timecopilot.dev/model-hub/) of time series foundation models with more than 30 models across more than 7 families. We have shown practical examples of [agentic forecasting](https://timecopilot.dev/examples/llm-providers/). We reached the top position on the [GIFT Eval benchmark](https://timecopilot.dev/experiments/gift-eval/) above AWS, Salesforce, IBM, and top universities. We [published at NeurIPS](https://berts-workshop.github.io/accepted-papers/). We passed 12k downloads, 280 GitHub stars, and continue to grow a community around this work. Everything is open source and built with a scientific mindset.
40 |
41 | We now have early support that helps us move quickly and work with organizations exploring where agentic forecasting can be useful.
42 |
43 | We believe the next generation of forecasting will be grounded in data, expressed through text, orchestrated across many models, clear about uncertainty, and built in the open.
44 |
45 | TimeCopilot is our next step towards that.
46 |
47 | More soon.
48 |
49 | Azul & Renée
50 |
51 |
--------------------------------------------------------------------------------
/timecopilot/models/foundation/moirai.py:
--------------------------------------------------------------------------------
1 | from contextlib import contextmanager
2 |
3 | import torch
4 | from gluonts.torch.model.predictor import PyTorchPredictor
5 | from uni2ts.model.moirai import MoiraiForecast, MoiraiModule
6 | from uni2ts.model.moirai2 import Moirai2Forecast, Moirai2Module
7 | from uni2ts.model.moirai_moe import MoiraiMoEForecast, MoiraiMoEModule
8 |
9 | from ..utils.gluonts_forecaster import GluonTSForecaster
10 |
11 |
12 | class Moirai(GluonTSForecaster):
13 | """
14 | Moirai is a universal foundation model for time series forecasting, designed to
15 | handle a wide range of frequencies, multivariate series, and covariates. It uses
16 | a masked encoder-based transformer architecture with multi-patch size projection
17 | layers and Any-Variate Attention, enabling zero-shot and probabilistic
18 | forecasting. See the [official repo](https://github.com/
19 | SalesforceAIResearch/uni2ts),
20 | [Hugging Face](https://huggingface.co/collections/
21 | Salesforce/moirai-r-models-65c8d3a94c51428c300e0742), and
22 | [arXiv:2402.02592](https://arxiv.org/abs/2402.02592) for more details.
23 | """
24 |
25 | def __init__(
26 | self,
27 | repo_id: str = "Salesforce/moirai-1.0-R-large",
28 | filename: str = "model.ckpt",
29 | context_length: int = 4096,
30 | patch_size: int = 32,
31 | num_samples: int = 100,
32 | target_dim: int = 1,
33 | feat_dynamic_real_dim: int = 0,
34 | past_feat_dynamic_real_dim: int = 0,
35 | batch_size: int = 32,
36 | alias: str = "Moirai",
37 | ):
38 | """
39 | Args:
40 | repo_id (str, optional): The Hugging Face Hub model ID or local path to
41 | load the Moirai model from. Examples include
42 | "Salesforce/moirai-1.0-R-large". Defaults to
43 | "Salesforce/moirai-1.0-R-large". See the full list of models at
44 | [Hugging Face](https://huggingface.co/collections/Salesforce/
45 | moirai-r-models-65c8d3a94c51428c300e0742).
46 | filename (str, optional): Checkpoint filename for the model weights.
47 | Defaults to "model.ckpt".
48 | context_length (int, optional): Maximum context length (input window size)
49 | for the model. Controls how much history is used for each forecast.
50 | Defaults to 4096.
51 | patch_size (int, optional): Patch size for patch-based input encoding.
52 | Can be set to "auto" or a specific value (e.g., 8, 16, 32, 64, 128).
53 | Defaults to 32. See the Moirai paper for recommended values by
54 | frequency. Not used for Moirai-2.0.
55 | num_samples (int, optional): Number of samples for probabilistic
56 | forecasting. Controls the number of forecast samples drawn for
57 | uncertainty estimation. Defaults to 100.
58 | Not used for Moirai-2.0.
59 | target_dim (int, optional): Number of target variables (for multivariate
60 | forecasting). Defaults to 1.
61 | feat_dynamic_real_dim (int, optional): Number of dynamic real covariates
62 | known in the future. Defaults to 0.
63 | past_feat_dynamic_real_dim (int, optional): Number of past dynamic real
64 | covariates. Defaults to 0.
65 | batch_size (int, optional): Batch size to use for inference. Defaults to
66 | 32. Adjust based on available memory and model size.
67 | alias (str, optional): Name to use for the model in output DataFrames and
68 | logs. Defaults to "Moirai".
69 |
70 | Notes:
71 | **Academic Reference:**
72 |
73 | - Paper: [Unified Training of Universal Time Series Forecasting Transformers](https://arxiv.org/abs/2402.02592)
74 |
75 | **Resources:**
76 |
77 | - GitHub: [SalesforceAIResearch/uni2ts](https://github.com/SalesforceAIResearch/uni2ts)
78 | - HuggingFace: [Salesforce/moirai-r-models](https://huggingface.co/collections/Salesforce/moirai-r-models-65c8d3a94c51428c300e0742)
79 |
80 | **Technical Details:**
81 |
82 | - The model is loaded onto the best available device (GPU if available,
83 | otherwise CPU).
84 | """
85 | super().__init__(
86 | repo_id=repo_id,
87 | filename=filename,
88 | alias=alias,
89 | num_samples=num_samples,
90 | )
91 | self.context_length = context_length
92 | self.patch_size = patch_size
93 | self.target_dim = target_dim
94 | self.feat_dynamic_real_dim = feat_dynamic_real_dim
95 | self.past_feat_dynamic_real_dim = past_feat_dynamic_real_dim
96 | self.batch_size = batch_size
97 |
98 | @contextmanager
99 | def get_predictor(self, prediction_length: int) -> PyTorchPredictor:
100 | kwargs = {
101 | "prediction_length": prediction_length,
102 | "context_length": self.context_length,
103 | "patch_size": self.patch_size,
104 | "num_samples": self.num_samples,
105 | "target_dim": self.target_dim,
106 | "feat_dynamic_real_dim": self.feat_dynamic_real_dim,
107 | "past_feat_dynamic_real_dim": self.past_feat_dynamic_real_dim,
108 | }
109 | if "moe" in self.repo_id:
110 | model_cls, model_module = MoiraiMoEForecast, MoiraiMoEModule
111 | elif "moirai-2.0" in self.repo_id:
112 | model_cls, model_module = Moirai2Forecast, Moirai2Module
113 | del kwargs["patch_size"]
114 | del kwargs["num_samples"]
115 | else:
116 | model_cls, model_module = MoiraiForecast, MoiraiModule
117 | model = model_cls(
118 | module=model_module.from_pretrained(self.repo_id),
119 | **kwargs,
120 | )
121 | predictor = model.create_predictor(batch_size=self.batch_size)
122 |
123 | try:
124 | yield predictor
125 | finally:
126 | del predictor, model
127 | torch.cuda.empty_cache()
128 |
--------------------------------------------------------------------------------
/timecopilot/models/foundation/timegpt.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pandas as pd
4 | from nixtla import NixtlaClient
5 |
6 | from ..utils.forecaster import Forecaster
7 |
8 |
9 | class TimeGPT(Forecaster):
10 | """
11 | TimeGPT is a pre-trained foundation model for time series forecasting and anomaly
12 | detection, developed by Nixtla. It is based on a large encoder-decoder transformer
13 | architecture trained on over 100 billion data points from diverse domains.
14 | See the [official repo](https://github.com/nixtla/nixtla),
15 | [docs](https://www.nixtla.io/docs),
16 | and [arXiv:2310.03589](https://arxiv.org/abs/2310.03589) for more details.
17 | """
18 |
19 | def __init__(
20 | self,
21 | api_key: str | None = None,
22 | base_url: str | None = None,
23 | max_retries: int = 1,
24 | model: str = "timegpt-1",
25 | alias: str = "TimeGPT",
26 | ):
27 | """
28 | Args:
29 | api_key (str, optional): API key for authenticating with the Nixtla TimeGPT
30 | API. If not provided, will use the `NIXTLA_API_KEY`
31 | environment variable.
32 | base_url (str, optional): Base URL for the TimeGPT API. Defaults to the
33 | official Nixtla endpoint.
34 | max_retries (int, optional): Maximum number of retries for API requests.
35 | Defaults to 1.
36 | model (str, optional): Model name or version to use. Defaults to
37 | "timegpt-1". See the [Nixtla docs](https://www.nixtla.io/docs) for
38 | available models.
39 | alias (str, optional): Name to use for the model in output DataFrames and
40 | logs. Defaults to "TimeGPT".
41 |
42 | Notes:
43 | **Academic Reference:**
44 |
45 | - Paper: [TimeGPT-1](https://arxiv.org/abs/2310.03589)
46 |
47 | **Resources:**
48 |
49 | - GitHub: [Nixtla/nixtla](https://github.com/Nixtla/nixtla)
50 |
51 | **Technical Details:**
52 |
53 | - TimeGPT is a foundation model for time series forecasting designed for
54 | production-ready forecasting with minimal setup.
55 | - Provides zero-shot forecasting capabilities across various
56 | domains and frequencies.
57 | - Requires a valid API key from Nixtla to use.
58 | - For more information, see the
59 | [TimeGPT documentation](https://www.nixtla.io/docs).
60 | """
61 | self.api_key = api_key
62 | self.base_url = base_url
63 | self.max_retries = max_retries
64 | self.model = model
65 | self.alias = alias
66 |
67 | def _get_client(self) -> NixtlaClient:
68 | if self.api_key is None: # noqa: SIM108
69 | api_key = os.environ["NIXTLA_API_KEY"]
70 | else:
71 | api_key = self.api_key
72 | return NixtlaClient(
73 | api_key=api_key,
74 | base_url=self.base_url,
75 | max_retries=self.max_retries,
76 | )
77 |
78 | def forecast(
79 | self,
80 | df: pd.DataFrame,
81 | h: int,
82 | freq: str | None = None,
83 | level: list[int | float] | None = None,
84 | quantiles: list[float] | None = None,
85 | ) -> pd.DataFrame:
86 | """Generate forecasts for time series data using the model.
87 |
88 | This method produces point forecasts and, optionally, prediction
89 | intervals or quantile forecasts. The input DataFrame can contain one
90 | or multiple time series in stacked (long) format.
91 |
92 | Args:
93 | df (pd.DataFrame):
94 | DataFrame containing the time series to forecast. It must
95 | include as columns:
96 |
97 | - "unique_id": an ID column to distinguish multiple series.
98 | - "ds": a time column indicating timestamps or periods.
99 | - "y": a target column with the observed values.
100 |
101 | h (int):
102 | Forecast horizon specifying how many future steps to predict.
103 | freq (str, optional):
104 | Frequency of the time series (e.g. "D" for daily, "M" for
105 | monthly). See [Pandas frequency aliases](https://pandas.pydata.org/
106 | pandas-docs/stable/user_guide/timeseries.html#offset-aliases) for
107 | valid values. If not provided, the frequency will be inferred
108 | from the data.
109 | level (list[int | float], optional):
110 | Confidence levels for prediction intervals, expressed as
111 | percentages (e.g. [80, 95]). If provided, the returned
112 | DataFrame will include lower and upper interval columns for
113 | each specified level.
114 | quantiles (list[float], optional):
115 | List of quantiles to forecast, expressed as floats between 0
116 | and 1. Should not be used simultaneously with `level`. When
117 | provided, the output DataFrame will contain additional columns
118 | named in the format "model-q-{percentile}", where {percentile}
119 | = 100 × quantile value.
120 |
121 | Returns:
122 | pd.DataFrame:
123 | DataFrame containing forecast results. Includes:
124 |
125 | - point forecasts for each timestamp and series.
126 | - prediction intervals if `level` is specified.
127 | - quantile forecasts if `quantiles` is specified.
128 |
129 | For multi-series data, the output retains the same unique
130 | identifiers as the input DataFrame.
131 | """
132 | freq = self._maybe_infer_freq(df, freq)
133 | client = self._get_client()
134 | fcst_df = client.forecast(
135 | df=df,
136 | h=h,
137 | freq=freq,
138 | model=self.model,
139 | level=level,
140 | quantiles=quantiles,
141 | )
142 | fcst_df["ds"] = pd.to_datetime(fcst_df["ds"])
143 | cols = [col.replace("TimeGPT", self.alias) for col in fcst_df.columns]
144 | fcst_df.columns = cols
145 | return fcst_df
146 |
--------------------------------------------------------------------------------
/docs/changelogs/v0.0.12.md:
--------------------------------------------------------------------------------
1 | ### Features
2 |
3 | * **Query Method**: Added a `query` method to the forecaster for flexible, programmatic access to model capabilities. See [#134](https://github.com/TimeCopilot/timecopilot/pull/134).
4 | ```python
5 | from timecopilot import TimeCopilot
6 |
7 | tc = TimeCopilot(llm="openai:gpt-4o")
8 | tc.forecast(
9 | df="https://timecopilot.s3.amazonaws.com/public/data/air_passengers.csv",
10 | h=12,
11 | )
12 | result = tc.query("What is the best model for monthly data?")
13 | print(result.output)
14 | ```
15 |
16 | * **Async TimeCopilot Agent**: Introduced the `AsyncTimeCopilot` class for asynchronous forecasting and querying. See [#135](https://github.com/TimeCopilot/timecopilot/pull/135) and [#138](https://github.com/TimeCopilot/timecopilot/pull/138).
17 | ```python
18 | import asyncio
19 | from timecopilot import AsyncTimeCopilot
20 |
21 | async def main():
22 | tc = AsyncTimeCopilot(llm="openai:gpt-4o")
23 | await tc.forecast(
24 | df="https://timecopilot.s3.amazonaws.com/public/data/air_passengers.csv",
25 | h=12
26 | )
27 | answer = await tc.query("Which model performed best?")
28 | print(answer.output)
29 |
30 | asyncio.run(main())
31 | ```
32 |
33 | * **Fallback Model Support**: The `TimeCopilotForecaster` now supports a fallback model, which is used if the primary model fails. See [#123](https://github.com/TimeCopilot/timecopilot/pull/123).
34 | ```python
35 | from timecopilot.forecaster import TimeCopilotForecaster
36 | from timecopilot.models.foundational.timesfm import TimesFM
37 | from timecopilot.models.benchmarks.stats import SeasonalNaive
38 |
39 | forecaster = TimeCopilotForecaster(
40 | models=[TimesFM()],
41 | fallback_model=SeasonalNaive()
42 | )
43 | ```
44 |
45 | * **TimesFM 2.0 Support**: Added support for TimesFM 2.0, enabling the use of the latest version of Google's TimesFM model. See [#128](https://github.com/TimeCopilot/timecopilot/pull/128).
46 | ```python
47 | from timecopilot.models.foundational.timesfm import TimesFM
48 |
49 | model = TimesFM(
50 | # default value
51 | repo_id="google/timesfm-2.0-500m-pytorch",
52 | )
53 | ```
54 |
55 | * **TabPFN Foundation Model**: Added the [TabPFN](https://github.com/PriorLabs/TabPFN) time series foundation model. See [#113](https://github.com/TimeCopilot/timecopilot/pull/113).
56 | ```python
57 | import pandas as pd
58 | from timecopilot.models.foundational.tabpfn import TabPFN
59 |
60 | df = pd.read_csv("https://timecopilot.s3.amazonaws.com/public/data/algeria_exports.csv", parse_dates=["ds"])
61 | model = TabPFN()
62 | fcst = model.forecast(df, h=12)
63 | print(fcst)
64 | ```
65 |
66 | * **Median Ensemble**: Introduced a new Median Ensemble model that combines predictions from multiple models to improve forecast accuracy. See [#144](https://github.com/TimeCopilot/timecopilot/pull/144).
67 | ```python
68 | import pandas as pd
69 | from timecopilot.models.benchmarks import SeasonalNaive
70 | from timecopilot.models.ensembles.median import MedianEnsemble
71 | from timecopilot.models.foundational.chronos import Chronos
72 |
73 |
74 | df = pd.read_csv(
75 | "https://timecopilot.s3.amazonaws.com/public/data/air_passengers.csv",
76 | parse_dates=["ds"],
77 | )
78 |
79 | models = [
80 | Chronos(
81 | repo_id="amazon/chronos-t5-tiny",
82 | alias="Chronos-T5",
83 | ),
84 | Chronos(
85 | repo_id="amazon/chronos-bolt-tiny",
86 | alias="Chronos-Bolt",
87 | ),
88 | SeasonalNaive(),
89 | ]
90 | median_ensemble = MedianEnsemble(models=models)
91 | fcst_df = median_ensemble.forecast(
92 | df=df,
93 | h=12,
94 | )
95 | print(fcst_df)
96 | ```
97 |
98 | * **GIFTEval Module**: Added the [GIFTEval](https://github.com/SalesforceAIResearch/gift-eval/) module for advanced evaluation of forecasting models. See [#140](https://github.com/TimeCopilot/timecopilot/pull/140).
99 | ```python
100 | import pandas as pd
101 | from timecopilot.gift_eval.eval import GIFTEval, QUANTILE_LEVELS
102 | from timecopilot.gift_eval.gluonts_predictor import GluonTSPredictor
103 | from timecopilot.models.benchmarks import SeasonalNaive
104 |
105 | storage_path = ".pytest_cache/gift_eval"
106 | GIFTEval.download_data(storage_path)
107 |
108 | gifteval = GIFTEval(
109 | dataset_name="m4_weekly",
110 | term="short",
111 | output_path="./seasonal_naive",
112 | storage_path=storage_path,
113 | )
114 | predictor = GluonTSPredictor(
115 | forecaster=SeasonalNaive(),
116 | h=gifteval.dataset.prediction_length,
117 | freq=gifteval.dataset.freq,
118 | quantiles=QUANTILE_LEVELS,
119 | batch_size=512,
120 | )
121 | gifteval.evaluate_predictor(
122 | predictor,
123 | batch_size=512,
124 | )
125 | eval_df = pd.read_csv("./seasonal_naive/all_results.csv")
126 | print(eval_df)
127 | ```
128 |
129 | ### Fixes
130 |
131 | * **Model Compatibility**: Added support for the Moirai and TimeGPT models. See [#115](https://github.com/TimeCopilot/timecopilot/pull/115), [#117](https://github.com/TimeCopilot/timecopilot/pull/117).
132 | * **GluonTS Forecaster**: Improved frequency handling and now uses the median for forecasts. See [#124](https://github.com/TimeCopilot/timecopilot/pull/124), [#127](https://github.com/TimeCopilot/timecopilot/pull/127).
133 | * **TimesFM Quantile Names**: TimesFM now returns correct quantile names. See [#131](https://github.com/TimeCopilot/timecopilot/pull/131).
134 | * **Removed Lag Llama**: The Lag Llama model has been removed. See [#116](https://github.com/TimeCopilot/timecopilot/pull/116).
135 | * **DataFrame Handling**: Fixed DataFrame copying to avoid index side effects. See [#120](https://github.com/TimeCopilot/timecopilot/pull/120).
136 |
137 | ### Docs
138 |
139 | * **Foundation Model Documentation**: Added comprehensive documentation for foundation models, including paper citations and repository links. See [#118](https://github.com/TimeCopilot/timecopilot/pull/118).
140 | * **Unique Alias Validation**: Added validation to prevent column conflicts in `TimeCopilotForecaster`. See [#122](https://github.com/TimeCopilot/timecopilot/pull/122).
141 |
142 | ---
143 |
144 | **Full Changelog**: https://github.com/TimeCopilot/timecopilot/compare/v0.0.11...v0.0.12
--------------------------------------------------------------------------------
/tests/test_forecaster.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from utilsforecast.data import generate_series
3 |
4 | from timecopilot.forecaster import TimeCopilotForecaster
5 | from timecopilot.models import SeasonalNaive, ZeroModel
6 | from timecopilot.models.foundation.moirai import Moirai
7 |
8 |
9 | @pytest.fixture
10 | def models():
11 | return [SeasonalNaive(), ZeroModel()]
12 |
13 |
14 | @pytest.mark.parametrize(
15 | "freq,h",
16 | [
17 | ("D", 2),
18 | ("W-MON", 3),
19 | ],
20 | )
21 | def test_forecaster_forecast(models, freq, h):
22 | n_uids = 3
23 | df = generate_series(n_series=n_uids, freq=freq, min_length=30)
24 | forecaster = TimeCopilotForecaster(models=models)
25 | fcst_df = forecaster.forecast(df=df, h=h, freq=freq)
26 | assert len(fcst_df.columns) == 2 + len(models)
27 | assert len(fcst_df) == h * n_uids
28 | for model in models:
29 | assert model.alias in fcst_df.columns
30 |
31 |
32 | @pytest.mark.parametrize(
33 | "freq,h,n_windows,step_size",
34 | [
35 | ("D", 2, 2, 1),
36 | ("W-MON", 3, 2, 2),
37 | ],
38 | )
39 | def test_forecaster_cross_validation(models, freq, h, n_windows, step_size):
40 | n_uids = 3
41 | df = generate_series(n_series=n_uids, freq=freq, min_length=30)
42 | forecaster = TimeCopilotForecaster(models=models)
43 | fcst_df = forecaster.cross_validation(
44 | df=df,
45 | h=h,
46 | freq=freq,
47 | n_windows=n_windows,
48 | step_size=step_size,
49 | )
50 | assert len(fcst_df.columns) == 4 + len(models)
51 | uids = df["unique_id"].unique()
52 | for uid in uids: # noqa: B007
53 | fcst_df_uid = fcst_df.query("unique_id == @uid")
54 | assert fcst_df_uid["cutoff"].nunique() == n_windows
55 | assert len(fcst_df_uid) == n_windows * h
56 | for model in models:
57 | assert model.alias in fcst_df.columns
58 |
59 |
60 | def test_forecaster_forecast_with_level(models):
61 | n_uids = 3
62 | level = [80, 90]
63 | df = generate_series(n_series=n_uids, freq="D", min_length=30)
64 | forecaster = TimeCopilotForecaster(models=models)
65 | fcst_df = forecaster.forecast(df=df, h=2, freq="D", level=level) # type: ignore
66 | assert len(fcst_df) == 2 * n_uids
67 | assert len(fcst_df.columns) == 2 + len(models) * (1 + 2 * len(level))
68 | for model in models:
69 | assert model.alias in fcst_df.columns
70 | for lv in level:
71 | assert f"{model.alias}-lo-{lv}" in fcst_df.columns
72 | assert f"{model.alias}-hi-{lv}" in fcst_df.columns
73 |
74 |
75 | def test_forecaster_forecast_with_quantiles(models):
76 | n_uids = 3
77 | quantiles = [0.1, 0.9]
78 | df = generate_series(n_series=n_uids, freq="D", min_length=30)
79 | forecaster = TimeCopilotForecaster(models=models)
80 | fcst_df = forecaster.forecast(df=df, h=2, freq="D", quantiles=quantiles)
81 | assert len(fcst_df) == 2 * n_uids
82 | assert len(fcst_df.columns) == 2 + len(models) * (1 + len(quantiles))
83 | for model in models:
84 | assert model.alias in fcst_df.columns
85 | for q in quantiles:
86 | assert f"{model.alias}-q-{int(100 * q)}" in fcst_df.columns
87 |
88 |
89 | def test_forecaster_fallback_model():
90 | from timecopilot.models.utils.forecaster import Forecaster
91 |
92 | class FailingModel(Forecaster):
93 | alias = "FailingModel"
94 |
95 | def forecast(self, df, h, freq=None, level=None, quantiles=None):
96 | raise RuntimeError("Intentional failure")
97 |
98 | class DummyModel(Forecaster):
99 | alias = "DummyModel"
100 |
101 | def forecast(self, df, h, freq=None, level=None, quantiles=None):
102 | # Return a DataFrame with the expected columns
103 | import pandas as pd
104 |
105 | n = len(df["unique_id"].unique()) * h
106 | return pd.DataFrame(
107 | {
108 | "unique_id": ["A"] * n,
109 | "ds": pd.date_range("2020-01-01", periods=n, freq="D"),
110 | "DummyModel": range(n),
111 | }
112 | )
113 |
114 | df = generate_series(n_series=1, freq="D", min_length=10)
115 | forecaster = TimeCopilotForecaster(
116 | models=[FailingModel()],
117 | fallback_model=DummyModel(),
118 | )
119 | fcst_df = forecaster.forecast(df=df, h=2, freq="D")
120 | # Should use DummyModel's output
121 | # and rename the columns to the original model's alias
122 | assert "FailingModel" in fcst_df.columns
123 | assert "DummyModel" not in fcst_df.columns
124 | assert len(fcst_df) == 2
125 |
126 |
127 | def test_forecaster_no_fallback_raises():
128 | from timecopilot.models.utils.forecaster import Forecaster
129 |
130 | class FailingModel(Forecaster):
131 | alias = "FailingModel"
132 |
133 | def forecast(self, df, h, freq=None, level=None, quantiles=None):
134 | raise RuntimeError("Intentional failure")
135 |
136 | df = generate_series(n_series=1, freq="D", min_length=10)
137 | forecaster = TimeCopilotForecaster(models=[FailingModel()])
138 | with pytest.raises(RuntimeError, match="Intentional failure"):
139 | forecaster.forecast(df=df, h=2, freq="D")
140 |
141 |
142 | def test_duplicate_aliases_raises_error():
143 | """Test that TimeCopilotForecaster raises error with duplicate aliases."""
144 | # Create two models with the same alias
145 | model1 = Moirai(repo_id="Salesforce/moirai-1.0-R-small", alias="Moirai")
146 | model2 = Moirai(repo_id="Salesforce/moirai-1.0-R-large", alias="Moirai")
147 |
148 | with pytest.raises(
149 | ValueError, match="Duplicate model aliases found: \\['Moirai'\\]"
150 | ):
151 | TimeCopilotForecaster(models=[model1, model2])
152 |
153 |
154 | def test_unique_aliases_works():
155 | """Test that TimeCopilotForecaster works when models have unique aliases."""
156 | # Create two models with different aliases
157 | model1 = Moirai(repo_id="Salesforce/moirai-1.0-R-small", alias="MoiraiSmall")
158 | model2 = Moirai(repo_id="Salesforce/moirai-1.0-R-large", alias="MoiraiLarge")
159 |
160 | # This should not raise an error
161 | forecaster = TimeCopilotForecaster(models=[model1, model2])
162 | assert len(forecaster.models) == 2
163 | assert forecaster.models[0].alias == "MoiraiSmall"
164 | assert forecaster.models[1].alias == "MoiraiLarge"
165 |
166 |
167 | def test_mixed_models_unique_aliases():
168 | """Test that different model classes with unique aliases work together."""
169 | model1 = SeasonalNaive()
170 | model2 = ZeroModel()
171 | model3 = Moirai(repo_id="Salesforce/moirai-1.0-R-small", alias="MoiraiTest")
172 |
173 | # This should not raise an error
174 | forecaster = TimeCopilotForecaster(models=[model1, model2, model3])
175 | assert len(forecaster.models) == 3
176 |
--------------------------------------------------------------------------------
/timecopilot/models/ensembles/median.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | from sklearn.isotonic import IsotonicRegression
3 |
4 | from ... import TimeCopilotForecaster
5 | from ..utils.forecaster import Forecaster, QuantileConverter
6 |
7 |
8 | class MedianEnsemble(Forecaster):
9 | def __init__(self, models: list[Forecaster], alias: str = "MedianEnsemble"):
10 | # fmt: off
11 | """
12 | Initialize a MedianEnsemble forecaster.
13 |
14 | This ensemble combines the forecasts of multiple models by taking the
15 | median of their predictions for each time step and series.
16 | For probabilistic forecasts (quantiles and levels),
17 | it uses isotonic regression to ensure monotonicity of the quantile outputs
18 | across the ensemble. Optionally, you can set a custom alias
19 | for the ensemble.
20 |
21 | Args:
22 | models (list[Forecaster]):
23 | List of instantiated forecaster models to be ensembled. Each model must
24 | implement the forecast method and have a unique alias.
25 | alias (str, optional):
26 | Name to use for the ensemble in output DataFrames and logs. Defaults to
27 | "MedianEnsemble".
28 |
29 | Example:
30 | ```python
31 | import pandas as pd
32 | from timecopilot.models.ensembles.median import MedianEnsemble
33 | from timecopilot.models.foundation.chronos import Chronos
34 | from timecopilot.models.stats import SeasonalNaive
35 |
36 |
37 | df = pd.read_csv(
38 | "https://timecopilot.s3.amazonaws.com/public/data/air_passengers.csv",
39 | parse_dates=["ds"],
40 | )
41 |
42 | models = [
43 | Chronos(
44 | repo_id="amazon/chronos-t5-tiny",
45 | alias="Chronos-T5",
46 | ),
47 | Chronos(
48 | repo_id="amazon/chronos-bolt-tiny",
49 | alias="Chronos-Bolt",
50 | ),
51 | SeasonalNaive(),
52 | ]
53 | median_ensemble = MedianEnsemble(models=models)
54 | fcst_df = median_ensemble.forecast(
55 | df=df,
56 | h=12,
57 | )
58 | print(fcst_df)
59 | ```
60 | """
61 | # fmt: on
62 | self.tcf = TimeCopilotForecaster(models=models, fallback_model=None)
63 | self.alias = alias
64 |
65 | def forecast(
66 | self,
67 | df: pd.DataFrame,
68 | h: int,
69 | freq: str | None = None,
70 | level: list[int | float] | None = None,
71 | quantiles: list[float] | None = None,
72 | ) -> pd.DataFrame:
73 | """Generate forecasts for time series data using the model.
74 |
75 | This method produces point forecasts and, optionally, prediction
76 | intervals or quantile forecasts. The input DataFrame can contain one
77 | or multiple time series in stacked (long) format.
78 |
79 | Args:
80 | df (pd.DataFrame):
81 | DataFrame containing the time series to forecast. It must
82 | include as columns:
83 |
84 | - "unique_id": an ID column to distinguish multiple series.
85 | - "ds": a time column indicating timestamps or periods.
86 | - "y": a target column with the observed values.
87 |
88 | h (int):
89 | Forecast horizon specifying how many future steps to predict.
90 | freq (str, optional):
91 | Frequency of the time series (e.g. "D" for daily, "M" for
92 | monthly). See [Pandas frequency aliases](https://pandas.pydata.org/
93 | pandas-docs/stable/user_guide/timeseries.html#offset-aliases) for
94 | valid values. If not provided, the frequency will be inferred
95 | from the data.
96 | level (list[int | float], optional):
97 | Confidence levels for prediction intervals, expressed as
98 | percentages (e.g. [80, 95]). If provided, the returned
99 | DataFrame will include lower and upper interval columns for
100 | each specified level.
101 | quantiles (list[float], optional):
102 | List of quantiles to forecast, expressed as floats between 0
103 | and 1. Should not be used simultaneously with `level`. When
104 | provided, the output DataFrame will contain additional columns
105 | named in the format "model-q-{percentile}", where {percentile}
106 | = 100 × quantile value.
107 |
108 | Returns:
109 | pd.DataFrame:
110 | DataFrame containing forecast results. Includes:
111 |
112 | - point forecasts for each timestamp and series.
113 | - prediction intervals if `level` is specified.
114 | - quantile forecasts if `quantiles` is specified.
115 |
116 | For multi-series data, the output retains the same unique
117 | identifiers as the input DataFrame.
118 | """
119 | qc = QuantileConverter(level=level, quantiles=quantiles)
120 | _fcst_df = self.tcf._call_models(
121 | "forecast",
122 | merge_on=["unique_id", "ds"],
123 | df=df,
124 | h=h,
125 | freq=freq,
126 | level=None,
127 | quantiles=qc.quantiles,
128 | )
129 | fcst_df = _fcst_df[["unique_id", "ds"]]
130 | model_cols = [model.alias for model in self.tcf.models]
131 | fcst_df[self.alias] = _fcst_df[model_cols].median(axis=1)
132 | if qc.quantiles is not None:
133 | qs = sorted(qc.quantiles)
134 | q_cols = []
135 | for q in qs:
136 | pct = int(q * 100)
137 | models_q_cols = [f"{col}-q-{pct}" for col in model_cols]
138 | q_col = f"{self.alias}-q-{pct}"
139 | fcst_df[q_col] = _fcst_df[models_q_cols].median(axis=1)
140 | q_cols.append(q_col)
141 | # enforce monotonicity
142 | ir = IsotonicRegression(increasing=True)
143 |
144 | def apply_isotonic(row):
145 | return ir.fit_transform(qs, row)
146 |
147 | # @Azul: this can be parallelized later
148 | vals_monotonic = fcst_df[q_cols].apply(
149 | apply_isotonic,
150 | axis=1,
151 | result_type="expand",
152 | )
153 | fcst_df[q_cols] = vals_monotonic
154 | if 0.5 in qc.quantiles:
155 | fcst_df[self.alias] = fcst_df[f"{self.alias}-q-50"].values
156 | fcst_df = qc.maybe_convert_quantiles_to_level(
157 | fcst_df,
158 | models=[self.alias],
159 | )
160 | return fcst_df
161 |
--------------------------------------------------------------------------------
/timecopilot/models/utils/gluonts_forecaster.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Iterable
2 | from contextlib import contextmanager
3 | from typing import Any
4 |
5 | import pandas as pd
6 | import torch
7 | import utilsforecast.processing as ufp
8 | from gluonts.dataset.pandas import PandasDataset
9 | from gluonts.model.forecast import Forecast
10 | from gluonts.torch.model.predictor import PyTorchPredictor
11 | from huggingface_hub import hf_hub_download
12 | from tqdm import tqdm
13 |
14 | from .forecaster import Forecaster, QuantileConverter
15 |
16 |
17 | def fix_freq(freq: str) -> str:
18 | # see https://github.com/awslabs/gluonts/pull/2462/files
19 | replacer = {"MS": "M", "ME": "M"}
20 | return replacer.get(freq, freq)
21 |
22 |
23 | def maybe_convert_col_to_float32(df: pd.DataFrame, col_name: str) -> pd.DataFrame:
24 | if df[col_name].dtype != "float32":
25 | df = df.copy()
26 | df[col_name] = df[col_name].astype("float32")
27 | return df
28 |
29 |
30 | class GluonTSForecaster(Forecaster):
31 | def __init__(
32 | self,
33 | repo_id: str,
34 | filename: str,
35 | alias: str,
36 | num_samples: int = 100,
37 | ):
38 | self.repo_id = repo_id
39 | self.filename = filename
40 | self.alias = alias
41 | self.num_samples = num_samples
42 |
43 | @property
44 | def checkpoint_path(self) -> str:
45 | return hf_hub_download(
46 | repo_id=self.repo_id,
47 | filename=self.filename,
48 | )
49 |
50 | @property
51 | def map_location(self) -> str:
52 | map_location = "cuda:0" if torch.cuda.is_available() else "cpu"
53 | return map_location
54 |
55 | def load(self) -> Any:
56 | return torch.load(
57 | self.checkpoint_path,
58 | map_location=self.map_location,
59 | )
60 |
61 | @contextmanager
62 | def get_predictor(self, prediction_length: int) -> PyTorchPredictor:
63 | raise NotImplementedError
64 |
65 | def gluonts_instance_fcst_to_df(
66 | self,
67 | fcst: Forecast,
68 | freq: str,
69 | model_name: str,
70 | quantiles: list[float] | None,
71 | ) -> pd.DataFrame:
72 | point_forecast = fcst.median
73 | h = len(point_forecast)
74 | dates = pd.date_range(
75 | fcst.start_date.to_timestamp(),
76 | freq=freq,
77 | periods=h,
78 | )
79 | fcst_df = pd.DataFrame(
80 | {
81 | "ds": dates,
82 | "unique_id": fcst.item_id,
83 | model_name: point_forecast,
84 | }
85 | )
86 | if quantiles is not None:
87 | for q in quantiles:
88 | fcst_df = ufp.assign_columns(
89 | fcst_df,
90 | f"{model_name}-q-{int(q * 100)}",
91 | fcst.quantile(q),
92 | )
93 | return fcst_df
94 |
95 | def gluonts_fcsts_to_df(
96 | self,
97 | fcsts: Iterable[Forecast],
98 | freq: str,
99 | model_name: str,
100 | quantiles: list[float] | None,
101 | ) -> pd.DataFrame:
102 | df = []
103 | for fcst in tqdm(fcsts):
104 | fcst_df = self.gluonts_instance_fcst_to_df(
105 | fcst=fcst,
106 | freq=freq,
107 | model_name=model_name,
108 | quantiles=quantiles,
109 | )
110 | df.append(fcst_df)
111 | return pd.concat(df).reset_index(drop=True)
112 |
113 | def forecast(
114 | self,
115 | df: pd.DataFrame,
116 | h: int,
117 | freq: str | None = None,
118 | level: list[int | float] | None = None,
119 | quantiles: list[float] | None = None,
120 | ) -> pd.DataFrame:
121 | """Generate forecasts for time series data using the model.
122 |
123 | This method produces point forecasts and, optionally, prediction
124 | intervals or quantile forecasts. The input DataFrame can contain one
125 | or multiple time series in stacked (long) format.
126 |
127 | Args:
128 | df (pd.DataFrame):
129 | DataFrame containing the time series to forecast. It must
130 | include as columns:
131 |
132 | - "unique_id": an ID column to distinguish multiple series.
133 | - "ds": a time column indicating timestamps or periods.
134 | - "y": a target column with the observed values.
135 |
136 | h (int):
137 | Forecast horizon specifying how many future steps to predict.
138 | freq (str, optional):
139 | Frequency of the time series (e.g. "D" for daily, "M" for
140 | monthly). See [Pandas frequency aliases](https://pandas.pydata.org/
141 | pandas-docs/stable/user_guide/timeseries.html#offset-aliases) for
142 | valid values. If None, the frequency will be inferred from the data.
143 | level (list[int | float], optional):
144 | Confidence levels for prediction intervals, expressed as
145 | percentages (e.g. [80, 95]). If provided, the returned
146 | DataFrame will include lower and upper interval columns for
147 | each specified level.
148 | quantiles (list[float], optional):
149 | List of quantiles to forecast, expressed as floats between 0
150 | and 1. Should not be used simultaneously with `level`. When
151 | provided, the output DataFrame will contain additional columns
152 | named in the format "model-q-{percentile}", where {percentile}
153 | = 100 × quantile value.
154 |
155 | Returns:
156 | pd.DataFrame:
157 | DataFrame containing forecast results. Includes:
158 |
159 | - point forecasts for each timestamp and series.
160 | - prediction intervals if `level` is specified.
161 | - quantile forecasts if `quantiles` is specified.
162 |
163 | For multi-series data, the output retains the same unique
164 | identifiers as the input DataFrame.
165 | """
166 | df = maybe_convert_col_to_float32(df, "y")
167 | freq = self._maybe_infer_freq(df, freq)
168 | qc = QuantileConverter(level=level, quantiles=quantiles)
169 | gluonts_dataset = PandasDataset.from_long_dataframe(
170 | df.copy(deep=False),
171 | target="y",
172 | item_id="unique_id",
173 | timestamp="ds",
174 | freq=fix_freq(freq),
175 | )
176 | with self.get_predictor(prediction_length=h) as predictor:
177 | fcsts = predictor.predict(
178 | gluonts_dataset,
179 | num_samples=self.num_samples,
180 | )
181 | fcst_df = self.gluonts_fcsts_to_df(
182 | fcsts,
183 | freq=freq,
184 | model_name=self.alias,
185 | quantiles=qc.quantiles,
186 | )
187 | if qc.quantiles is not None:
188 | fcst_df = qc.maybe_convert_quantiles_to_level(
189 | fcst_df,
190 | models=[self.alias],
191 | )
192 |
193 | return fcst_df
194 |
--------------------------------------------------------------------------------
/timecopilot/gift_eval/gluonts_predictor.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import numpy as np
4 | import pandas as pd
5 | import tqdm
6 | import utilsforecast.processing as ufp
7 | from gluonts.dataset import Dataset
8 | from gluonts.dataset.util import forecast_start
9 | from gluonts.model import Forecast
10 | from gluonts.model.forecast import QuantileForecast
11 | from gluonts.model.predictor import RepresentablePredictor
12 | from gluonts.transform.feature import LastValueImputation, MissingValueImputation
13 |
14 | from ..models.utils.forecaster import Forecaster
15 | from .utils import QUANTILE_LEVELS
16 |
17 |
18 | class GluonTSPredictor(RepresentablePredictor):
19 | """
20 | Adapter to use a TimeCopilot Forecaster as a GluonTS Predictor.
21 |
22 | This class wraps a TimeCopilot Forecaster and exposes the GluonTS Predictor
23 | interface, allowing it to be used with GluonTS evaluation and processing utilities.
24 | """
25 |
26 | def __init__(
27 | self,
28 | forecaster: Forecaster,
29 | h: int | None = None,
30 | freq: str | None = None,
31 | level: list[int | float] | None = None,
32 | quantiles: list[float] | None = None,
33 | max_length: int | None = None,
34 | imputation_method: MissingValueImputation | None = None,
35 | batch_size: int | None = 1024,
36 | ):
37 | """
38 | Initialize a GluonTSPredictor.
39 |
40 | Args:
41 | forecaster (Forecaster): The TimeCopilot forecaster to wrap.
42 | You can use any forecaster from TimeCopilot, and create your own
43 | forecaster by subclassing
44 | [Forecaster][timecopilot.models.utils.forecaster.Forecaster].
45 | h (int | None): Forecast horizon. If None (default), the horizon is
46 | inferred from the dataset.
47 | freq (str | None): Frequency string (e.g., 'D', 'H').
48 | If None (default), the frequency is inferred from the dataset.
49 | level (list[int | float] | None): Not supported; use quantiles instead.
50 | quantiles (list[float] | None): Quantiles to forecast. If None (default),
51 | the default quantiles [0.1, 0.2, ..., 0.9] are used.
52 | max_length (int | None): Maximum length of input series.
53 | imputation_method (MissingValueImputation | None): Imputation method for
54 | missing values. If None (default), the last value is used
55 | with LastValueImputation().
56 | batch_size (int | None): Batch size for prediction.
57 |
58 | Raises:
59 | NotImplementedError: If level is provided (use quantiles instead).
60 | """
61 | self.forecaster = forecaster
62 | self.h = h
63 | self.freq = freq
64 | self.level = level
65 | if level is not None:
66 | raise NotImplementedError("level is not supported, use quantiles instead")
67 | self.quantiles = quantiles or QUANTILE_LEVELS
68 | self.max_length = max_length
69 | self.imputation_method = imputation_method or LastValueImputation()
70 | self.batch_size = batch_size
71 | self.alias = forecaster.alias
72 |
73 | def _gluonts_dataset_to_df(
74 | self,
75 | dataset: Dataset,
76 | ) -> tuple[pd.DataFrame, dict[str, Any]]:
77 | dfs: list[pd.DataFrame] = []
78 | metadata: dict[str, Any] = {}
79 | for _, entry in enumerate(dataset):
80 | target = np.asarray(entry["target"], dtype=np.float32)
81 | if self.max_length is not None and len(target) > self.max_length:
82 | entry["start"] += len(target[: -self.max_length])
83 | target = target[-self.max_length :]
84 | if np.isnan(target).any():
85 | target = self.imputation_method(target)
86 | if target.ndim > 1:
87 | raise ValueError("only for univariate time series")
88 | fcst_start = forecast_start(entry)
89 | uid = f"{entry['item_id']}-{fcst_start}"
90 | ds_start = entry["start"]
91 | ds = pd.date_range(
92 | start=ds_start.to_timestamp(),
93 | freq=ds_start.freq,
94 | periods=len(target),
95 | )
96 | uid_df = pd.DataFrame(
97 | {
98 | "unique_id": uid,
99 | "ds": ds,
100 | "y": target,
101 | }
102 | )
103 | dfs.append(uid_df)
104 | metadata[uid] = {
105 | "item_id": entry["item_id"],
106 | "fcst_start": fcst_start,
107 | }
108 | df = pd.concat(dfs, ignore_index=True)
109 | return df, metadata
110 |
111 | def _predict_df(
112 | self,
113 | df: pd.DataFrame,
114 | metadata: dict[str, Any],
115 | h: int,
116 | freq: str,
117 | ) -> list[Forecast]:
118 | fcst_df = self.forecaster.forecast(
119 | df=df,
120 | h=h,
121 | freq=freq,
122 | level=self.level,
123 | quantiles=self.quantiles,
124 | )
125 | fcst_df = fcst_df.set_index("unique_id")
126 | fcsts: list[Forecast] = []
127 | for uid, metadata_uid in metadata.items():
128 | fcst_df_uid = fcst_df.loc[uid]
129 | forecast_arrays = ufp.value_cols_to_numpy(
130 | df=fcst_df_uid,
131 | id_col="unique_id",
132 | time_col="ds",
133 | target_col=None,
134 | )
135 | forecast_keys = ["mean"]
136 | if self.quantiles is not None:
137 | forecast_keys += [f"{q}" for q in self.quantiles]
138 | q_fcst = QuantileForecast(
139 | forecast_arrays=forecast_arrays.T,
140 | forecast_keys=forecast_keys,
141 | item_id=metadata_uid["item_id"],
142 | start_date=metadata_uid["fcst_start"],
143 | )
144 | fcsts.append(q_fcst)
145 | return fcsts
146 |
147 | def _predict_batch(
148 | self,
149 | batch: list[Dataset],
150 | h: int,
151 | freq: str,
152 | ) -> list[Forecast]:
153 | df, metadata = self._gluonts_dataset_to_df(batch)
154 | return self._predict_df(df=df, metadata=metadata, h=h, freq=freq)
155 |
156 | def predict(self, dataset: Dataset, **kwargs: Any) -> list[Forecast]:
157 | """
158 | Predict forecasts for a GluonTS Dataset.
159 |
160 | Args:
161 | dataset (Dataset): GluonTS Dataset to forecast.
162 | **kwargs: Additional keyword arguments (unused).
163 |
164 | Returns:
165 | list[Forecast]: List of GluonTS Forecast objects for the dataset.
166 | """
167 | fcsts: list[Forecast] = []
168 | batch: list[Dataset] = []
169 | h = self.h or dataset.test_data.prediction_length
170 | if h is None:
171 | raise ValueError("horizon `h` must be provided")
172 | freq = self.freq
173 | for _, entry in tqdm.tqdm(enumerate(dataset), total=len(dataset)):
174 | if freq is None:
175 | freq = entry["freq"]
176 | batch.append(entry)
177 | if len(batch) == self.batch_size:
178 | fcsts.extend(self._predict_batch(batch=batch, h=h, freq=freq))
179 | batch = []
180 | if len(batch) > 0:
181 | if freq is None:
182 | raise ValueError("frequency `freq` must be provided")
183 | fcsts.extend(self._predict_batch(batch=batch, h=h, freq=freq))
184 | return fcsts
185 |
--------------------------------------------------------------------------------
/timecopilot/gift_eval/data.py:
--------------------------------------------------------------------------------
1 | # Adapted from https://github.com/SalesforceAIResearch/gift-eval
2 |
3 | import math
4 | import os
5 | from collections.abc import Iterable, Iterator
6 | from enum import Enum
7 | from functools import cached_property
8 | from pathlib import Path
9 |
10 | import datasets
11 | import pyarrow.compute as pc
12 | from dotenv import load_dotenv
13 | from gluonts.dataset import DataEntry
14 | from gluonts.dataset.common import ProcessDataEntry
15 | from gluonts.dataset.split import TestData, TrainingDataset, split
16 | from gluonts.itertools import Map
17 | from gluonts.time_feature import norm_freq_str
18 | from gluonts.transform import Transformation
19 | from pandas.tseries.frequencies import to_offset
20 | from toolz import compose
21 |
22 | TEST_SPLIT = 0.1
23 | MAX_WINDOW = 20
24 |
25 | M4_PRED_LENGTH_MAP = {
26 | "A": 6,
27 | "Q": 8,
28 | "M": 18,
29 | "W": 13,
30 | "D": 14,
31 | "H": 48,
32 | }
33 |
34 | PRED_LENGTH_MAP = {
35 | "M": 12,
36 | "W": 8,
37 | "D": 30,
38 | "H": 48,
39 | "T": 48,
40 | "S": 60,
41 | }
42 |
43 | TFB_PRED_LENGTH_MAP = {
44 | "A": 6,
45 | "H": 48,
46 | "Q": 8,
47 | "D": 14,
48 | "M": 18,
49 | "W": 13,
50 | "U": 8,
51 | "T": 8,
52 | }
53 |
54 |
55 | class Term(Enum):
56 | SHORT = "short"
57 | MEDIUM = "medium"
58 | LONG = "long"
59 |
60 | @property
61 | def multiplier(self) -> int:
62 | if self == Term.SHORT:
63 | return 1
64 | elif self == Term.MEDIUM:
65 | return 10
66 | elif self == Term.LONG:
67 | return 15
68 |
69 |
70 | def itemize_start(data_entry: DataEntry) -> DataEntry:
71 | data_entry["start"] = data_entry["start"].item()
72 | return data_entry
73 |
74 |
75 | def maybe_reconvert_freq(freq: str) -> str:
76 | """if the freq is one of the newest pandas freqs, convert it to the old freq"""
77 | deprecated_map = {
78 | "Y": "A",
79 | "YE": "A",
80 | "QE": "Q",
81 | "ME": "M",
82 | "h": "H",
83 | "min": "T",
84 | "s": "S",
85 | "us": "U",
86 | }
87 | if freq in deprecated_map:
88 | return deprecated_map[freq]
89 | return freq
90 |
91 |
92 | class MultivariateToUnivariate(Transformation):
93 | def __init__(self, field):
94 | self.field = field
95 |
96 | def __call__(
97 | self, data_it: Iterable[DataEntry], is_train: bool = False
98 | ) -> Iterator:
99 | for data_entry in data_it:
100 | item_id = data_entry["item_id"]
101 | val_ls = list(data_entry[self.field])
102 | for id, val in enumerate(val_ls):
103 | univariate_entry = data_entry.copy()
104 | univariate_entry[self.field] = val
105 | univariate_entry["item_id"] = item_id + "_dim" + str(id)
106 | yield univariate_entry
107 |
108 |
109 | class Dataset:
110 | def _storage_path_from_env_var(self, env_var: str) -> Path:
111 | load_dotenv()
112 | env_var_value = os.getenv(env_var)
113 | if env_var_value is None:
114 | raise ValueError(f"Environment variable {env_var} is not set")
115 | return Path(env_var_value)
116 |
117 | def __init__(
118 | self,
119 | name: str,
120 | term: Term | str = Term.SHORT,
121 | to_univariate: bool = False,
122 | storage_path: Path | str | None = None,
123 | storage_env_var: str = "GIFT_EVAL",
124 | ):
125 | if storage_path is None:
126 | storage_path = self._storage_path_from_env_var(storage_env_var)
127 | else:
128 | storage_path = Path(storage_path)
129 | self.hf_dataset = datasets.load_from_disk(str(storage_path / name)).with_format(
130 | "numpy"
131 | )
132 | process = ProcessDataEntry(
133 | self.freq,
134 | one_dim_target=self.target_dim == 1,
135 | )
136 |
137 | self.gluonts_dataset = Map(compose(process, itemize_start), self.hf_dataset)
138 | if to_univariate:
139 | self.gluonts_dataset = MultivariateToUnivariate("target").apply(
140 | self.gluonts_dataset
141 | )
142 |
143 | self.term = Term(term)
144 | self.name = name
145 |
146 | @cached_property
147 | def prediction_length(self) -> int:
148 | freq = norm_freq_str(to_offset(self.freq).name)
149 | freq = maybe_reconvert_freq(freq)
150 | pred_len = (
151 | M4_PRED_LENGTH_MAP[freq] if "m4" in self.name else PRED_LENGTH_MAP[freq]
152 | )
153 | return self.term.multiplier * pred_len
154 |
155 | @cached_property
156 | def freq(self) -> str:
157 | return self.hf_dataset[0]["freq"]
158 |
159 | @cached_property
160 | def target_dim(self) -> int:
161 | return (
162 | target.shape[0]
163 | if len((target := self.hf_dataset[0]["target"]).shape) > 1
164 | else 1
165 | )
166 |
167 | @cached_property
168 | def past_feat_dynamic_real_dim(self) -> int:
169 | if "past_feat_dynamic_real" not in self.hf_dataset[0]:
170 | return 0
171 | elif (
172 | len(
173 | (
174 | past_feat_dynamic_real := self.hf_dataset[0][
175 | "past_feat_dynamic_real"
176 | ]
177 | ).shape
178 | )
179 | > 1
180 | ):
181 | return past_feat_dynamic_real.shape[0]
182 | else:
183 | return 1
184 |
185 | @cached_property
186 | def windows(self) -> int:
187 | if "m4" in self.name:
188 | return 1
189 | w = math.ceil(TEST_SPLIT * self._min_series_length / self.prediction_length)
190 | return min(max(1, w), MAX_WINDOW)
191 |
192 | @cached_property
193 | def _min_series_length(self) -> int:
194 | if self.hf_dataset[0]["target"].ndim > 1:
195 | lengths = pc.list_value_length(
196 | pc.list_flatten(
197 | pc.list_slice(self.hf_dataset.data.column("target"), 0, 1)
198 | )
199 | )
200 | else:
201 | lengths = pc.list_value_length(self.hf_dataset.data.column("target"))
202 | return min(lengths.to_numpy())
203 |
204 | @cached_property
205 | def sum_series_length(self) -> int:
206 | if self.hf_dataset[0]["target"].ndim > 1:
207 | lengths = pc.list_value_length(
208 | pc.list_flatten(self.hf_dataset.data.column("target"))
209 | )
210 | else:
211 | lengths = pc.list_value_length(self.hf_dataset.data.column("target"))
212 | return sum(lengths.to_numpy())
213 |
214 | @property
215 | def training_dataset(self) -> TrainingDataset:
216 | training_dataset, _ = split(
217 | self.gluonts_dataset, offset=-self.prediction_length * (self.windows + 1)
218 | )
219 | return training_dataset
220 |
221 | @property
222 | def validation_dataset(self) -> TrainingDataset:
223 | validation_dataset, _ = split(
224 | self.gluonts_dataset, offset=-self.prediction_length * self.windows
225 | )
226 | return validation_dataset
227 |
228 | @property
229 | def test_data(self) -> TestData:
230 | _, test_template = split(
231 | self.gluonts_dataset, offset=-self.prediction_length * self.windows
232 | )
233 | test_data = test_template.generate_instances(
234 | prediction_length=self.prediction_length,
235 | windows=self.windows,
236 | distance=self.prediction_length,
237 | )
238 | return test_data
239 |
--------------------------------------------------------------------------------
/docs/examples/agent-quickstart.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "767e2143",
6 | "metadata": {},
7 | "source": [
8 | "# TimeCopilot Agent"
9 | ]
10 | },
11 | {
12 | "cell_type": "code",
13 | "execution_count": 9,
14 | "id": "02826629",
15 | "metadata": {},
16 | "outputs": [],
17 | "source": [
18 | "import nest_asyncio\n",
19 | "nest_asyncio.apply()"
20 | ]
21 | },
22 | {
23 | "cell_type": "markdown",
24 | "id": "85e1ddbe",
25 | "metadata": {},
26 | "source": [
27 | "## Import libraries"
28 | ]
29 | },
30 | {
31 | "cell_type": "code",
32 | "execution_count": 10,
33 | "id": "5e2acc09",
34 | "metadata": {},
35 | "outputs": [],
36 | "source": [
37 | "import pandas as pd\n",
38 | "from timecopilot import TimeCopilot"
39 | ]
40 | },
41 | {
42 | "cell_type": "markdown",
43 | "id": "c2b4d0bb",
44 | "metadata": {},
45 | "source": [
46 | "\n",
47 | "## Load the dataset. \n",
48 | "\n",
49 | "The DataFrame must include at least the following columns:\n",
50 | "- unique_id: Unique identifier for each time series (string)\n",
51 | "- ds: Date column (datetime format)\n",
52 | "- y: Target variable for forecasting (float format)\n",
53 | "\n",
54 | "The pandas frequency will be inferred from the ds column, if not provided.\n",
55 | "If the seasonality is not provided, it will be inferred based on the frequency. \n",
56 | "If the horizon is not set, it will default to 2 times the inferred seasonality."
57 | ]
58 | },
59 | {
60 | "cell_type": "code",
61 | "execution_count": 11,
62 | "id": "5b690413",
63 | "metadata": {},
64 | "outputs": [
65 | {
66 | "data": {
67 | "text/html": [
68 | "\n",
69 | "\n",
82 | "
\n",
83 | " \n",
84 | " \n",
85 | " | \n",
86 | " unique_id | \n",
87 | " ds | \n",
88 | " y | \n",
89 | "
\n",
90 | " \n",
91 | " \n",
92 | " \n",
93 | " | 0 | \n",
94 | " AirPassengers | \n",
95 | " 1949-01-01 | \n",
96 | " 112 | \n",
97 | "
\n",
98 | " \n",
99 | " | 1 | \n",
100 | " AirPassengers | \n",
101 | " 1949-02-01 | \n",
102 | " 118 | \n",
103 | "
\n",
104 | " \n",
105 | " | 2 | \n",
106 | " AirPassengers | \n",
107 | " 1949-03-01 | \n",
108 | " 132 | \n",
109 | "
\n",
110 | " \n",
111 | " | 3 | \n",
112 | " AirPassengers | \n",
113 | " 1949-04-01 | \n",
114 | " 129 | \n",
115 | "
\n",
116 | " \n",
117 | " | 4 | \n",
118 | " AirPassengers | \n",
119 | " 1949-05-01 | \n",
120 | " 121 | \n",
121 | "
\n",
122 | " \n",
123 | "
\n",
124 | "
"
125 | ],
126 | "text/plain": [
127 | " unique_id ds y\n",
128 | "0 AirPassengers 1949-01-01 112\n",
129 | "1 AirPassengers 1949-02-01 118\n",
130 | "2 AirPassengers 1949-03-01 132\n",
131 | "3 AirPassengers 1949-04-01 129\n",
132 | "4 AirPassengers 1949-05-01 121"
133 | ]
134 | },
135 | "execution_count": 11,
136 | "metadata": {},
137 | "output_type": "execute_result"
138 | }
139 | ],
140 | "source": [
141 | "df = pd.read_csv(\"https://timecopilot.s3.amazonaws.com/public/data/air_passengers.csv\")\n",
142 | "df.head()"
143 | ]
144 | },
145 | {
146 | "cell_type": "markdown",
147 | "id": "59be5da5",
148 | "metadata": {},
149 | "source": [
150 | "Initialize the forecasting agent. You can use any LLM by specifying the llm parameter.\n"
151 | ]
152 | },
153 | {
154 | "cell_type": "code",
155 | "execution_count": 12,
156 | "id": "821101f6",
157 | "metadata": {},
158 | "outputs": [],
159 | "source": [
160 | "\n",
161 | "tc = TimeCopilot(\n",
162 | " llm=\"openai:gpt-4o\",\n",
163 | " retries=3,\n",
164 | ")\n"
165 | ]
166 | },
167 | {
168 | "cell_type": "markdown",
169 | "id": "b3bc3a45",
170 | "metadata": {},
171 | "source": [
172 | "\n",
173 | "## Generate forecast \n",
174 | "\n",
175 | "You can optionally specify the following parameters:\n",
176 | "- freq: The frequency of your data (e.g., 'D' for daily, 'M' for monthly)\n",
177 | "- h: The forecast horizon, which is the number of periods to predict\n",
178 | "- seasonality: The seasonal period of your data, which can be inferred if not provided\n"
179 | ]
180 | },
181 | {
182 | "cell_type": "code",
183 | "execution_count": 13,
184 | "id": "0ccf5f6f",
185 | "metadata": {},
186 | "outputs": [
187 | {
188 | "name": "stderr",
189 | "output_type": "stream",
190 | "text": [
191 | "1it [00:00, 8.31it/s]\n",
192 | "1it [00:00, 7.24it/s]\n",
193 | "1it [00:05, 5.77s/it]\n",
194 | "1it [00:00, 17.29it/s]\n",
195 | "1it [00:00, 188.78it/s]\n",
196 | "1it [00:00, 144.74it/s]\n",
197 | "1it [00:00, 201.90it/s]\n",
198 | "1it [00:00, 203.62it/s]\n",
199 | "1it [00:00, 144.84it/s]\n",
200 | "1it [00:00, 211.99it/s]\n",
201 | "0it [00:00, ?it/s]Importing plotly failed. Interactive plots will not work.\n",
202 | "16:33:10 - cmdstanpy - INFO - Chain [1] start processing\n",
203 | "16:33:10 - cmdstanpy - INFO - Chain [1] done processing\n",
204 | "1it [00:03, 3.86s/it]\n"
205 | ]
206 | }
207 | ],
208 | "source": [
209 | "result = tc.forecast(df=df)"
210 | ]
211 | },
212 | {
213 | "cell_type": "markdown",
214 | "id": "b7263888",
215 | "metadata": {},
216 | "source": [
217 | "## Ask a question about the future"
218 | ]
219 | },
220 | {
221 | "cell_type": "code",
222 | "execution_count": 14,
223 | "id": "4713d65c",
224 | "metadata": {},
225 | "outputs": [
226 | {
227 | "name": "stdout",
228 | "output_type": "stream",
229 | "text": [
230 | "The model that performed best is the one with the lowest MASE (Mean Absolute Scaled Error) score. According to the evaluation results:\n",
231 | "\n",
232 | "- AutoCES has the lowest MASE score of 0.6225634591577892.\n",
233 | "\n",
234 | "This indicates that the AutoCES model provided the most accurate forecasts relative to the other models tested, as a lower MASE score signifies better performance.\n"
235 | ]
236 | }
237 | ],
238 | "source": [
239 | "answer = tc.query(\"Which model performed best?\")\n",
240 | "print(answer.output)"
241 | ]
242 | },
243 | {
244 | "cell_type": "markdown",
245 | "id": "6a8ae523",
246 | "metadata": {},
247 | "source": []
248 | }
249 | ],
250 | "metadata": {
251 | "kernelspec": {
252 | "display_name": ".venv",
253 | "language": "python",
254 | "name": "python3"
255 | },
256 | "language_info": {
257 | "codemirror_mode": {
258 | "name": "ipython",
259 | "version": 3
260 | },
261 | "file_extension": ".py",
262 | "mimetype": "text/x-python",
263 | "name": "python",
264 | "nbconvert_exporter": "python",
265 | "pygments_lexer": "ipython3",
266 | "version": "3.11.12"
267 | }
268 | },
269 | "nbformat": 4,
270 | "nbformat_minor": 5
271 | }
272 |
--------------------------------------------------------------------------------