Aller au contenu

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] si past_variates est 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 sur t0.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_ids n'est jamais combiné avec future_covariates : validate_group_ids lève ValueError, et ForecastInputs.group_ids vaut toujours None. 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 jour as_of ;
  • date passée : ce qui était connu lead_days jours 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 : --origins dates en remontant depuis --as-of par 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, uv 0.11.18 copié depuis ghcr.io/astral-sh/uv:0.11.18.
  • uv sync --frozen --no-default-groups --only-group forecast depuis pyproject.toml et uv.lock.
  • Téléchargement des poids à la révision T0_MODEL_REVISION dans HF_HOME=/opt/hf, puis HF_HUB_OFFLINE=1.
  • Copie de forecast/ et lab/, utilisateur non root forecast (uid 10001), point d'entrée python -m forecast.run, commande par défaut forecast.

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).