Module 4 — Définition et versionnage des variables
Une variable dans un magasin n'est pas une colonne, c'est un contrat. Ce module montre comment ce contrat s'écrit, ce qu'il déclare, et surtout comment le faire évoluer sans casser les modèles qui en dépendent — la partie la plus mal traitée par les projets naïfs.
Ce qu'une définition contient
Prenons nb_transactions_24h, l'une des sept variables du scoreur de fraude. Voici la déclaration Feast correspondante :
from feast import Entity, FeatureView, Field, FileSource
from feast.types import Int64
from datetime import timedelta
porteur = Entity(name="porteur", join_keys=["card_id_hash"])
transactions_source = FileSource(
name="transactions_source",
path="s3://banque-fraude/transactions.parquet",
timestamp_field="event_timestamp",
)
porteur_transactions_recentes = FeatureView(
name="porteur_transactions_recentes",
entities=[porteur],
ttl=timedelta(hours=24),
schema=[
Field(name="nb_transactions_1h", dtype=Int64),
Field(name="nb_transactions_24h", dtype=Int64),
Field(name="nb_transactions_7j", dtype=Int64),
],
source=transactions_source,
tags={"proprietaire": "equipe-fraude", "criticite": "haute"},
)
Cette déclaration porte huit informations, dont chacune protège d'une classe de bogues :
- Nom (
nb_transactions_24h) — identifiant stable, il ne change jamais après publication. - Entité (porteur) — sur quoi porte la variable, donc la clé de jointure implicite.
- Type (
Int64) — refusé au chargement s'il ne correspond pas ; empêche l'écart de type entre entraînement (numpyint64) et service (JavaIntegerautoboxed). - Source — d'où viennent les données brutes ; unique, ce qui interdit qu'un service tiers pousse une variante.
- Champ d'horodatage (
event_timestamp) — clé du calcul point-en-temps du module 5. - Durée de validité (
ttl=24h) — au-delà, la valeur en ligne est considérée périmée et une lecture peut renvoyer une valeur par défaut ou déclencher une alerte. - Propriétaire — équipe responsable, à contacter avant de modifier.
- Étiquettes — criticité, réglementation, coût, tout ce qui aide à filtrer un catalogue de 500 variables.
Le principe de la déclaration
Une définition est déclarative, pas impérative. On n'écrit pas comment recalculer la variable — cette logique vit dans le moteur de matérialisation. On écrit ce qu'elle est : son nom, sa source, son type, sa durée de validité. Cela permet trois choses.
D'abord, le moteur peut choisir l'implémentation optimale selon le backend (SQL sur BigQuery, DataFrame sur Spark). Ensuite, la définition sert directement à générer la documentation, l'interface de recherche du catalogue et les contrôles de qualité (module 9). Enfin, deux définitions identiques signifient deux variables identiques : on peut comparer, dépublier, factoriser, ce qui serait impossible sur du code procédural.
Faire évoluer une définition
Le cas le plus difficile — et le mieux ignoré — est celui d'une variable en production qu'on veut changer. Trois scénarios, trois traitements.
Scénario 1 : correction de bogue rétrocompatible. L'équipe s'aperçoit que nb_transactions_24h ne devrait pas compter les transactions annulées. La formule change, les valeurs changent, mais le nom, le type et l'entité ne bougent pas. Traitement : matérialiser la nouvelle version sur tout l'historique (backfill), redéployer, réentraîner les modèles qui en dépendent. Le point critique est que l'historique doit être réécrit, sinon les modèles apprennent l'ancienne définition et servent la nouvelle. C'est un cas d'écart entraînement-service internalisé.
Scénario 2 : évolution non rétrocompatible. L'équipe veut passer montant_moyen_24h de Float32 à Float64, ou changer la source. Ce n'est plus la même variable — elle a un nouveau nom : montant_moyen_24h_v2. L'ancienne cohabite le temps que les modèles migrent. On dépublie la première après avoir vérifié qu'aucun modèle actif ne la consomme.
Scénario 3 : suppression. Une variable devient obsolète (le modèle qui l'utilisait est retiré). Contrôle : le catalogue liste les modèles qui la consomment. Si la liste est vide depuis quelques semaines, la variable est dépubliée, sa matérialisation arrêtée. On ne supprime jamais l'historique tout de suite — au cas où on aurait à reproduire un ancien entraînement.
Le versionnage réel : Git
La grande majorité des magasins ne versionnent pas dans une base : ils versionnent le fichier de définition dans Git. Chaque modification est une pull request, revue par le propriétaire de la vue, et l'application (feast apply) synchronise le registre avec le contenu de la branche principale. C'est modeste — mais cela réutilise toute l'ingénierie logicielle que l'équipe possède déjà (revue, historique, blâme, retour arrière), au lieu de la réinventer côté magasin.
Le corollaire : le registre n'est pas la vérité, Git l'est. Si le registre diverge, on refait apply. Si Git diverge, on reprend la revue.
Un bon nom de variable dit ce qu'elle mesure et sur quelle fenêtre. nb_transactions_24h est explicite ; feat_x_42 ne l'est pas. Le nom fait partie du contrat, il migre dans les tableaux de bord et les journaux d'audit — le renommer est aussi coûteux que renommer une colonne d'une API publique.
En résumé
- Une définition déclare nom, entité, type, source, horodatage, durée de validité et propriétaire — huit champs, huit garde-fous.
- Elle est déclarative : le moteur reste libre de l'implémentation, la définition sert de documentation et de contrat.
- Les évolutions se traitent en trois cas : correction rétrocompatible (avec backfill), version majeure sous nouveau nom, suppression après vérification des dépendances.
- Le versionnage vit dans Git, pas dans le magasin ;
applysynchronise le registre avec la branche principale.
Module suivant : le calcul point-en-temps, qui garantit que la valeur historique servie à l'entraînement est celle qui aurait été observable au moment de l'événement.