Prévision avec t0-gamma
Skill de référence : .claude/skills/forecast-t0/SKILL.md. Code : forecast/.
Modèle et runtime
| Élément | Valeur | Où |
|---|---|---|
| Modèle | theforecastingcompany/t0-gamma (poids ouverts, Apache-2.0, environ 256M de paramètres) |
forecast/model.py, MODEL_ID |
| Révision des poids | e08aee083778f1f32da4974c0a34276776a35c52 (commit Hugging Face) |
MODEL_REVISION, ARG T0_MODEL_REVISION de forecast/Dockerfile |
| Runtime | tfc-t0[torch]==0.6.0 |
pyproject.toml (groupe forecast), RUNTIME_VERSION |
| PyTorch | torch==2.14.1, roues CPU (index pytorch-cpu) |
pyproject.toml |
| Exécution | CPU (la carte graphique du NUC est partagée avec d'autres projets) | T0Model.load(device="cpu") |
check_runtime() refuse de charger le modèle si la version installée de tfc-t0 diffère de l'épinglage (RuntimeMismatch) : un runtime trop ancien peut charger les poids sans erreur et appliquer une mauvaise normalisation. Le modèle, la révision et la version du runtime sont écrits avec chaque prévision.
Inférence locale uniquement : les poids sont téléchargés une fois, à la construction de l'image, à la révision épinglée ; à l'exécution HF_HUB_OFFLINE=1 et from_pretrained(..., local_files_only=True). L'API managée Retrocast est interdite (envoi des données à un tiers). Le fine-tuning (forward()) est exclu.
Référence : https://huggingface.co/theforecastingcompany/t0-gamma.
Périmètre retenu
| Paramètre | Valeur |
|---|---|
| Entité | établissement (h001 à h005) |
| Métriques | occupancy (métrique standardized occupancy) et revenue (métrique standardized room_revenue) |
| Fréquence | quotidienne |
| Horizon | 90 jours par défaut (--horizon) ; au-delà de 1024 pas, le runtime fait lui-même le rollout autorégressif (needs_rollout le signale) |
| Contexte | 365 jours par défaut (--context-days) |
| Quantiles | 0,1, 0,5, 0,9 (parmi les 21 niveaux natifs de 0,01 à 0,99) |
| Délai as-of des covariables | 7 jours par défaut (--lead-days) |
Entrées du modèle
forecast/inputs.py, fonction build_inputs(source, entities, metrics, as_of, context_days, horizon, lead_days=7, past_variates=(), covariates=COVARIATES) (numpy seulement, sans torch) :
context:[B, T], ou[B, V, T]sipast_variatesest fourni (la cible en ligne 0). Une ligne de lot par couple (entité, métrique). NaN signifie observation absente : aucune imputation en amont.mask: même forme,MASK_VALID(0),MASK_PAD(1) pour les dates antérieures à la première observation de l'entité,MASK_MISSING(2) pour chaque NaN. Les valeurs numériques sont alignées surt0.MaskType(contrôlé par les tests).future_covariates:[B, F, T + H], connues sur le contexte et sur l'horizon ; le modèle s'en sert pour conditionner, il ne les prévoit pas. Covariables (axe F) :rooms_on_the_books,comp_rate_avg,is_holiday.group_idsn'est jamais combiné avecfuture_covariates:validate_group_idslèveValueError, etForecastInputs.group_idsvaut toujoursNone. Le multivarié avec covariables futures passe par un contexte[B, V, T].
Covariables as-of
Sur la fenêtre passée, une covariable future doit être la valeur connue à l'époque, au même délai, jamais le réalisé final. moment_for(target, as_of, lead_days) fixe l'instant de lecture :
- date future (après
as_of) : ce qui est connu à la fin du jouras_of; - date passée : ce qui était connu
lead_daysjours avant la date, en fin de journée.
Sources des valeurs as-of (DuckDBSource) :
| Covariable | Source | Lecture |
|---|---|---|
rooms_on_the_books |
standardized.snap_bookings_otb (SCD2 dbt) |
Version valide à l'instant : dbt_valid_from <= moment < dbt_valid_to |
comp_rate_avg |
standardized.snap_rate_shop (SCD2 dbt) |
Idem |
is_holiday |
shared_calendar.calendar_days |
Dernier snapshot de calendrier pris au plus tard ce jour-là ; 1.0 si la date y figure, 0.0 sinon, NULL si aucun snapshot n'existait encore |
Les noms de tables et de colonnes SCD2 sont pris dans une liste fermée, jamais chez l'appelant.
ForecastInputs.covariate_availability() classe chaque ligne de lot en full, partial ou none selon la part de cellules de covariables présentes sur l'horizon ; ce classement sert à ventiler le backtest.
Inférence
forecast/model.py, classe T0Model : T0Model.load(device="cpu", local_files_only=True) charge t0.T0Forecaster.from_pretrained à la révision épinglée, en mode évaluation, gradients désactivés. predict(inputs, quantiles=QUANTILES, group_ids=None) renvoie un tableau [B, H, Q] (pour un contexte multivarié, seule la variable cible 0 est gardée).
Table forecast
forecast/writer.py, table Iceberg analytics.forecast.forecast, une ligne par quantile :
| Colonne | Type | Rôle |
|---|---|---|
entity_id |
string | Clé : établissement |
metric |
string | Clé : occupancy ou revenue |
target_date |
date | Clé : date prévue |
as_of_date |
date | Clé : date de la prévision (permet de confronter au réalisé) |
horizon |
long | Clé : pas, à partir de 1 |
quantile |
double | Niveau (0,1, 0,5, 0,9) |
value |
double | Valeur prévue |
model, model_revision, runtime_version |
string | Traçabilité du modèle |
covariates_used |
string (JSON) | Covariables ayant au moins une valeur sur l'horizon pour cette ligne |
confidence_class |
string | Toujours estimated |
_source_system |
string | forecast |
_source_file |
string | forecast-run/<identifiant d'exécution> |
_row_number |
long | Rang dans l'exécution, à partir de 1 |
_loaded_at |
timestamptz | Horodatage d'écriture |
write_forecast refuse de réécrire une prévision existante pour le même (entité, métrique, as_of_date) avec la même révision du modèle (ForecastExistsError).
Backtest
forecast/backtest.py, résultats dans analytics.forecast.backtest_metrics (run_id, evaluated_at, model, model_revision, runtime_version, metric, frequency, horizon, covariate_availability, measure, quantile, value, n_obs).
- Origines :
--originsdates en remontant depuis--as-ofpar pas de--step-days(6 et 14 par défaut). Pour chaque origine, entrées construites as-of, prévision, puis appariement avec la valeur réalisée standardized quand elle existe. - Mesures, ventilées par métrique, fréquence, horizon et disponibilité des covariables :
| Mesure | Calcul |
|---|---|
quantile_loss |
Perte pinball moyenne par niveau |
crps |
Estimation : deux fois la moyenne des pertes quantiles sur les niveaux demandés ; grossière avec trois niveaux |
mase |
Erreur absolue de la médiane divisée par l'erreur naïve saisonnière (saison de 7 jours) sur le contexte |
coverage |
Part des réalisés entre le quantile le plus bas et le plus haut ; quantile porte la largeur nominale (0,8) |
calibration |
Part des réalisés sous chaque quantile |
- Seules les séries quotidiennes sont évaluées (
infer_frequency).
Conteneur
forecast/Dockerfile :
- Base
python:3.12.15-slim-trixie,uv0.11.18 copié depuisghcr.io/astral-sh/uv:0.11.18. uv sync --frozen --no-default-groups --only-group forecastdepuispyproject.tomletuv.lock.- Téléchargement des poids à la révision
T0_MODEL_REVISIONdansHF_HOME=/opt/hf, puisHF_HUB_OFFLINE=1. - Copie de
forecast/etlab/, utilisateur non rootforecast(uid 10001), point d'entréepython -m forecast.run, commande par défautforecast.
Service forecast du compose (profil forecast, lancé à la demande) : POLARIS_URI=http://polaris:8181, identifiants POLARIS_TECH_*, MinIO http://minio-central:9000, LAB_DATABASE, réseaux lab et data. Aucun volume : la variable LAB_DUCKDB_PATH et le volume ./lab_data du compose sont laissés en commentaire, avec la date, la raison et ce qu'il faut remettre. Les extensions DuckDB httpfs et iceberg sont installées à la construction de l'image, pour l'utilisateur forecast (rien à télécharger à l'exécution).
forecast/run.py :
docker compose --profile forecast run --rm forecast forecast --as-of 2026-10-22 [--entities h001,h002] [--metrics occupancy,revenue] [--horizon 90] [--context-days 365] [--lead-days 7]
docker compose --profile forecast run --rm forecast backtest --as-of 2026-10-22 [--origins 6] [--step-days 14]
Déroulé : connexion DuckDB avec Polaris attaché sous analytics (lab.duckdb_connect.connect(cfg, s3=True, polaris=True, cortex=False)), chargement du modèle, construction des entrées, prédiction, écriture Iceberg. Prérequis : la couche standardized (dont les snapshots SCD2) construite par dbt-duckdb dans Polaris (analytics.standardized.*), et au moins un snapshot de calendrier.
Transposition vers Snowpark Container Services
Chez le client, le même conteneur doit tourner dans Snowpark Container Services (ou servir de modèle custom du Model Registry) et écrire dans le même schéma de table forecast.
- Snowpark Container Services : https://docs.snowflake.com/en/developer-guide/snowpark-container-services/overview (vérifié le 2026-10-22).
- Modèle custom du Model Registry (
CustomModel,log_model) : https://docs.snowflake.com/en/developer-guide/snowflake-ml/model-registry/bring-your-own-model-types (vérifié le 2026-10-22).
Ce qui est portable tel quel : l'image et ses épinglages, forecast/inputs.py (contrat d'entrée, as-of), forecast/model.py, forecast/backtest.py, le schéma Arrow de forecast.forecast et forecast.backtest_metrics.
Ce qui est propre au labo dans le code actuel : la lecture de la couche standardized, des snapshots SCD2 et du calendrier par DuckDB sur Polaris (DuckDBSource, noms <base>.<schéma>.<table>), et l'écriture des résultats par PyIceberg via Polaris (lab/iceberg.py, forecast/writer.py). Une implémentation de HistorySource et un écrivain côté Snowflake ne sont pas présents dans le dépôt : leur forme (connexion Snowpark, tables Iceberg gérées par Snowflake ou catalogue externe) est à vérifier au moment de la transposition. Les spécifications de service SPCS (fichier de spécification, compute pool, dépôt d'images) ne sont pas dans le dépôt : leurs contraintes et un squelette sont décrits dans la sous-section suivante.
Contraintes SPCS (décision S5 du 2026-10-25)
Rien de ce qui suit n'est exécutable ni vérifiable au labo : aucun compte Snowflake n'est disponible, une spécification ne peut qu'être relue. Aucun fichier de spécification n'est versionné dans le dépôt (décision S5, option « documentation seule »). Pages lues le 2026-10-24 et relues le 2026-10-25.
| Sujet | Contrainte ou choix | Source | Statut |
|---|---|---|---|
| Forme du service | La prévision est un traitement par lot (forecast et backtest de forecast/run.py) : un job service (EXECUTE JOB SERVICE, disponible en GA) convient mieux qu'un service permanent. Il s'exécute en synchrone par défaut, ou en ASYNC = TRUE ; ses ressources sont libérées à la fin et ses métadonnées restent consultables 30 jours |
https://docs.snowflake.com/en/sql-reference/sql/execute-job-service | Page lue ; choix inféré |
| Syntaxe | EXECUTE JOB SERVICE IN COMPUTE POOL <pool> FROM SPECIFICATION <texte> (ou FROM @<stage> SPECIFICATION_FILE = '...'), avec NAME, ASYNC, REPLICAS, QUERY_WAREHOUSE, EXTERNAL_ACCESS_INTEGRATIONS ; droits CREATE SERVICE sur le schéma, USAGE sur le compute pool, READ sur le dépôt d'images (et sur le stage de la spécification, le cas échéant) |
même page | Lue |
| Point d'entrée | containers.command remplace l'ENTRYPOINT du Dockerfile et containers.args son CMD : l'image a pour ENTRYPOINT python -m forecast.run, la commande par défaut forecast devient donc args: [forecast, ...] |
https://docs.snowflake.com/en/developer-guide/snowpark-container-services/specification-reference | Lue |
| Architecture de l'image | linux/amd64 pour les familles d'instances x86, linux/arm64 pour les familles ARM. uv.lock contient les roues torch 2.14.1+cpu pour manylinux_2_28_x86_64 et manylinux_2_28_aarch64. Les extensions DuckDB (httpfs, iceberg) sont installées à la construction, donc par architecture. Une image arm64 n'est à produire que si le client retient des instances ARM ; disponibilité arm64 du tag python:3.12.15-slim-trixie et construction arm64 sur le NUC (émulation) : non vérifiées |
spécification SPCS, uv.lock, forecast/Dockerfile |
Partiellement vérifié |
| Dépôt d'images | containers.image est un chemin de dépôt d'images du compte (/<base>/<schéma>/<dépôt>/<image>:<tag>), pas une image Docker Hub ni GHCR |
spécification SPCS | Lue |
| Volumes de stage et BCR-2449 | Aucun volume de stage n'est nécessaire : l'image embarque les poids de t0-gamma (HF_HUB_OFFLINE=1) et les extensions DuckDB. Si un volume devenait utile, seule la forme V2 est acceptée (source: stage, stageConfig.name) : BCR-2449 (bundle 2026_08, désactivé par défaut) refuse les nouveaux services et jobs en V1, et V2 n'admet ni écriture aléatoire ni ajout en fin de fichier |
https://docs.snowflake.com/en/release-notes/bcr-bundles/2026_08/bcr-2449 | Lue ; besoin de volume : inféré |
| Points d'accès publics (BCR-2451) | Sans objet : le job n'expose aucun port ni spec.endpoints |
https://docs.snowflake.com/en/release-notes/bcr-bundles/2026_08_bundle | Inféré |
| Secrets | Aucun identifiant dans la spécification : les secrets sont des objets secret Snowflake référencés par containers.secrets (snowflakeSecret, avec envVarName et secretKeyRef, ou directoryPath), jamais par env en clair ; le rôle propriétaire du job a besoin du privilège READ sur ces secrets |
spécification SPCS | Lue |
| Jeton de session | Un jeton OAuth du service est monté par défaut en /snowflake/session/token (mountSessionToken, sessionTokenConfig) |
spécification SPCS | Lue |
| Ressources | containers.resources.requests et limits (memory avec unité M, Mi, G ou Gi, cpu en vCPU) sont relatifs à la famille d'instances du compute pool ; sans limite, elle vaut la capacité du nœud ; l'inférence est sur CPU (décision du plan). Famille d'instances et taille du pool : choix du client |
spécification SPCS | Lue ; valeurs : classe (c) |
Lecture de standardized et écriture de forecast chez le client |
Absentes du dépôt (HistorySource et écrivain côté Snowflake). Deux voies : connexion Snowflake avec le jeton de session (snowflake-connector-python, présent seulement dans le groupe snowflake de uv.lock, version 4.8.0 transitive), ou PyIceberg via Horizon IRC (voir portabilite-snowflake.md). Acceptation du jeton de session SPCS par Horizon IRC, besoin d'une intégration d'accès externe pour joindre le point d'accès du compte depuis le conteneur : non vérifiés |
pages SPCS et Horizon IRC, uv.lock, lab/iceberg.py |
Classe (c) |
| Voie Model Registry | snowflake-ml-python 2.x (run_batch dépend d'EXECUTE INFERENCE JOB SERVICE depuis la 2.0.0, d'après la veille du 2026-10-24) : aucune dépendance au labo, non relue le 2026-10-25 |
veille-snowflake-2026-10.md | Non relu |
Squelette de spécification d'un job service, à compléter chez le client (texte de documentation, non versionné comme fichier exécutable ; les valeurs entre chevrons sont des points classe (c) ; champs conformes à la page de spécification relue le 2026-10-25) :
spec:
containers:
- name: forecast
image: /<base>/<schema>/<image_repository>/forecast:<tag>
args: [forecast, --as-of, <date>]
resources:
requests:
cpu: <vcpu>
memory: <memory, for example 8Gi>
limits:
cpu: <vcpu>
memory: <memory>
secrets:
- snowflakeSecret:
objectName: <db>.<schema>.<secret_name>
envVarName: <ENV_VARIABLE_NAME>
secretKeyRef: secret_string
Pas de volumes, de volumeMounts ni de endpoints : voir le tableau. Les variables d'environnement du labo (POLARIS_URI, POLARIS_TECH_*, MinIO) ne s'appliquent pas chez le client, dont les accès seront ceux de la voie de lecture et d'écriture retenue. Lancement : EXECUTE JOB SERVICE IN COMPUTE POOL <pool> NAME = <db>.<schema>.forecast_job FROM SPECIFICATION $$ ... $$; (ou FROM @<stage> SPECIFICATION_FILE).
SNOWFLAKE.ML.FORECAST est un autre modèle : ses résultats ne sont pas comparables à t0 et il n'est pas reproduit (https://docs.snowflake.com/en/user-guide/ml-functions/forecasting, vérifié le 2026-10-22).