# TP03 - Synthèse : exposer un modèle ML via une API REST

Fil rouge : prédiction de la consommation électrique (kWh). On sélectionne le meilleur modèle
du TP02, on l'enregistre dans le **MLflow Model Registry**, on le **promeut via un alias**, puis
on l'expose par une **API REST FastAPI**.

## Ce qui a été construit

- **`log_model`** ajouté à chaque run d'entraînement (`lab/modeling/cli.py`, `lab/modeling_ridge/cli.py`) :
  sauvegarde l'artefact complet du modèle sur S3 (`s3://mlflow-artifacts`).
- **Modèle enregistré** : `electricity-consumption` (Registry MLflow). Versions créées avec le flag
  `--register` : v1 = `full` (6 features, RMSE val 6.576), v2 = `mixed` (4 features, RMSE 6.671).
- **Promotion** : alias `champion` -> **v1** (`full`), via `lab/registry/cli.py promote`.
- **API FastAPI** (`lab/serving/`) : `GET /health`, `POST /predict`, `POST /predict/batch`, Swagger `/docs`.
  Le modèle est chargé au démarrage par `models:/electricity-consumption@champion`.
- **Exposition** : `https://api.192-168-122-143.nip.io/docs` (reverse-proxy Caddy -> uvicorn `:8000`).

---

## Partie 1 - Sélection et enregistrement du modèle

**1.1 - Que contiennent les artefacts du modèle sauvegardé par MLflow, pourquoi sont-ils utiles ?**
Sauvegarder un modèle ne se limite pas à ses coefficients : MLflow enregistre tout l'environnement
d'exécution. Contenu observé (`s3://mlflow-artifacts/1/models/<id>/artifacts/`) :
- `model.pkl` : le modèle sérialisé (poids/coefficients) ;
- `MLmodel` : métadonnées (flavors `sklearn`/`pyfunc`, **signature** = schéma entrées/sorties) ;
- `requirements.txt`, `conda.yaml`, `python_env.yaml` : versions exactes des dépendances ;
- `input_example.json`, `serving_input_example.json` : exemple d'entrée.
Utiles pour **recharger le modèle partout** (`load_model`), **reproduire l'environnement** (mêmes
versions -> mêmes prédictions), et connaître le **contrat d'E/S** (signature).

**1.2 - Quel mécanisme vous permet de promouvoir un modèle ?**
L'**alias** du Model Registry : `MlflowClient().set_registered_model_alias(name, "champion", version)`
fait pointer un alias mobile vers une version précise. (Les anciens *stages* Staging/Production sont
dépréciés en MLflow 3.x au profit des alias + tags.)

**1.3 - Plusieurs environnements de production (un par région), plusieurs modèles : comment les identifier ?**
Avec des **alias** et **tags** spécifiques : par ex. des alias `production-eu`, `production-us`,
`champion-north`... sur un même modèle enregistré, et/ou un modèle enregistré par région, complétés par
des **tags** (région, environnement) portés par le modèle ou la version. Le Registry gère plusieurs
alias par modèle et des tags arbitraires : l'API cible alors `models:/<nom>@<alias-région>`.

---

## Partie 2 - Service de prédiction

### Étape 1 - Création de l'API

**2.1 - Quel endpoint de vérification ? Quel code de statut attendu ?**
`GET /health`, qui renvoie **200 OK** avec `{"status":"ok"}`.

**2.2 - À quoi sert un endpoint de santé ?**
À vérifier que le service est vivant et prêt (*liveness/readiness*), sans exécuter de vraie prédiction.
Il est sondé par l'orchestrateur / load-balancer / monitoring pour router le trafic, redémarrer un
conteneur en échec, ou alerter.

### Étape 2 - Requête de prédiction

**2.3 - Quelles informations le client doit-il fournir ? Pourquoi ?**
L'**identifiant client** (`client_id`) et, en paramètre, la **date** de prédiction. Ce sont les seules
informations du **contexte métier** que le client connaît : elles disent *qui* et *quand*. Elles servent
de clé pour retrouver le reste côté serveur.

**2.4 - Quelles informations le client ne peut-il pas fournir ? Comment le service les récupère-t-il ?**
Les **features calculées** (lags, moyennes glissantes) issues de l'historique de consommation : le
client ne les possède/calcule pas. Le service les **récupère lui-même** depuis un feature store / une
base, à partir du `client_id` (+ date). Ici, c'est **simulé par un dictionnaire Python** (`lab/serving/features.py`).

**2.5 - Quelle méthode HTTP pour le endpoint de prédiction ? Pourquoi ?**
**POST** : la requête transporte un **corps JSON structuré** (et potentiellement volumineux en batch),
et déclenche un **calcul** (action, non une simple lecture cacheable de ressource comme le ferait GET).

### Étape 3 - Récupération des features

**2.6 - Rappel des features nécessaires à la prédiction.**
Modèle promu = stratégie `full`, soit les **6 features** :
`lag_1d`, `lag_7d`, `lag_30d`, `lag_365d`, `rolling_mean_7d`, `rolling_mean_30d`.

**2.7 - Dans un système réel, d'où proviennent ces features ?**
D'un **feature store / pipeline de features** : un job (batch ou streaming) calcule lags et moyennes
glissantes depuis la série temporelle brute, les stocke dans une base (ex. Feast), et les sert à
l'inférence. Elles doivent être calculées **de façon identique à l'entraînement** (éviter le
*training/serving skew*).

**2.8 - Pourquoi séparer récupération des features et calcul de la prédiction ?**
**Séparation des responsabilités** : la source des features peut évoluer (BDD, cache, feature store)
sans toucher au modèle ; le modèle reste une **fonction pure** `features -> prédiction`, testable et
réutilisable. « L'application du modèle n'est qu'une étape de la chaîne de prédiction. »

**2.9 (Bonus) - Si les features ne peuvent pas être calculées/récupérées ?**
L'API ne doit pas planter : elle renvoie une erreur explicite. Client/features introuvables -> **404** ;
payload invalide -> 422 ; feature store indisponible (panne transitoire) -> **503**. Toujours un JSON
d'erreur clair.

### Étape 4 - Chargement du modèle depuis le Registry

**2.10 - Pourquoi un alias plutôt qu'un numéro de version ?**
L'alias (`champion`) est **stable et mobile** : l'API charge toujours `models:/electricity-consumption@champion`
et l'on **re-pointe l'alias** vers une nouvelle version sans modifier ni redéployer le code. Un numéro
de version est **figé** : chaque changement de modèle imposerait d'éditer la config et de redéployer.
L'alias **découple** « quel modèle est en prod » (décision côté MLflow) du code de service.

**2.11 - Avantages du Registry par rapport à un simple fichier modèle ?**
Versioning centralisé (historique de toutes les versions), **alias/tags**, **lignée** vers le run
d'entraînement (params/métriques), chargement par URI depuis n'importe où, workflow de promotion,
traçabilité/audit, environnement (requirements) attaché. Un simple fichier n'offre rien de tout cela
(pas d'historique, pas de métadonnées, distribution manuelle et fragile).

**2.12 (Bonus) - Si les requirements du modèle sont incohérents avec l'environnement de l'API ?**
Risque : incompatibilité de versions (scikit-learn, numpy) -> erreur de désérialisation ou écarts
numériques silencieux. **Architecture** : **isoler le modèle dans son propre runtime** construit à
partir de son `requirements.txt` (image conteneur dédiée par modèle, ex. `mlflow models build-docker` /
MLflow serving), l'API l'appelant via HTTP ; ou figer l'environnement de l'API depuis les requirements
du modèle. On **découple** l'API des dépendances du modèle.

### Étape 5 - Endpoint de prédiction

**2.13 - Étapes lorsqu'une requête de prédiction arrive.**
1. Valider le payload (schéma Pydantic). 2. Récupérer les features du client (404 si inconnu).
3. Assembler le vecteur de features **dans l'ordre attendu** par le modèle. 4. `model.predict`.
5. Formater et renvoyer la réponse JSON (200).

**2.14 - Quel format de réponse ? Quel statut HTTP ?**
JSON : `{client_id, prediction_kwh, model_name, model_version}`, statut **200 OK**.

---

## Partie 3 (Bonus) - Gestion des erreurs

**3.1 - L'application doit-elle échouer ou intercepter cette erreur ?**
**Intercepter.** Un `client_id` inconnu est une **erreur cliente** (mauvaise entrée), pas un bug
serveur : le service reste debout et renvoie une réponse d'erreur propre.

**3.2 - Quel code HTTP est adapté ?**
**404 Not Found** (la ressource/le client demandé n'existe pas). (422 si le payload lui-même est
malformé.) Implémenté via `HTTPException(status_code=404, ...)`.

---

## Partie 4 (Bonus) - Prédictions en batch

**4.1 - Un modèle doit-il forcément être exposé par API ? Dans quel cas ?**
Non. L'API (temps réel) convient aux prédictions **à la demande, individuelles, à faible latence**
(appli interactive). Pour de **gros volumes calculés périodiquement** (ex. tous les clients chaque
nuit), l'**inférence batch** (job planifié qui écrit les résultats en base) est plus adaptée et moins
coûteuse. On expose par API quand on a besoin de prédictions fraîches, unitaires et synchrones.

**4.2 - Quelles briques restent identiques entre batch et temps réel ?**
Le **modèle** (même artefact du Registry), la **logique/définition des features**, le **préprocessing**,
le **code de prédiction** (`features -> prédiction`). Ce qui diffère : le **déclencheur/orchestration**
(requête HTTP vs job planifié), les **E/S** (un JSON unitaire vs une table en masse) et le profil
latence/débit.

**4.3 - Comment récupérer les informations de prédiction ?**
En **masse** : lire les features de tous les clients pour la date depuis le feature store / une table
(BDD ou parquet), appeler `model.predict` sur le **lot entier** (vectorisé), puis écrire les résultats
en base/fichier. Ici, `POST /predict/batch` prend une liste de `client_ids` et renvoie la liste des
prédictions (+ `unknown_client_ids` pour les clients ignorés).

---

## Reproduire

Sur la VM, depuis `/home/user/tp` :

```bash
set -a; source .env; set +a           # MLflow + creds S3 (Garage)

# Partie 1 : enregistrer + promouvoir
python -m lab.modeling.cli full  --register     # v1 (champion)
python -m lab.modeling.cli mixed --register      # v2 (comparaison)
python -m lab.registry.cli versions              # consulter les versions/alias
python -m lab.registry.cli promote --version 1 --alias champion

# Partie 2-4 : servir l'API
./serve.sh                                        # uvicorn :8000  (Swagger /docs)
curl -s localhost:8000/health
curl -s -X POST localhost:8000/predict -H 'content-type: application/json' -d '{"client_id":"MT_124"}'
```

Depuis le poste (certificat ENI de confiance) : **https://api.192-168-122-143.nip.io/docs**.
