├── 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 | Diagram 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 | image 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 | " \n", 87 | " \n", 88 | " \n", 89 | " \n", 90 | " \n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | " \n", 97 | " \n", 98 | " \n", 99 | " \n", 100 | " \n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | "
unique_iddsy
0AirPassengers1949-01-01112
1AirPassengers1949-02-01118
2AirPassengers1949-03-01132
3AirPassengers1949-04-01129
4AirPassengers1949-05-01121
\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 | --------------------------------------------------------------------------------