Le cerveau du bloc 2 est excellent — et muet. On lui donne une adresse : une API FastAPI blindée, testée, documentée, que n'importe quelle application de la banque peut appeler. Block 2's brain is excellent — and mute. We give it an address: a FastAPI API that's armored, tested, documented, callable by any application in the bank.
Bloc 2 : le modèle est bon, et vous savez le prouver. Mais il vit dans un cadre en pointillés — un notebook, sur un laptop. À 3 h 47, quand l'app de la banque a 300 ms pour décider, personne ne peut lui demander un verdict. Block 2: the model is good, and you can prove it. But it lives inside a dotted frame — a notebook, on a laptop. At 3:47 am, when the bank's app has 300 ms to decide, nobody can ask it for a verdict.
Une API FastAPI se dresse devant le modèle. Regardez la carte : les transactions ne vont plus directement au cerveau — elles passent par la porte. POST /predict, entrées validées par Pydantic, modèle chargé une fois au démarrage. A FastAPI API now stands in front of the model. Watch the map: transactions no longer reach the brain directly — they go through the door. POST /predict, inputs validated by Pydantic, model loaded once at startup.
N'importe quelle application envoie un JSON et reçoit un verdict en quelques millisecondes — et le verdict humain revient par /feedback. Le cerveau n'est plus muet. Mais tout ça tourne sur une machine : c'est le point de départ du bloc 4. Any application sends a JSON and gets a verdict back in a few milliseconds — and the human verdict comes home through /feedback. The brain is no longer mute. But all of it runs on one machine: that's where block 4 picks up.
Servir un modèle, c'est rendre ses prédictions accessibles au moment où on en a besoin — sans humain dans la boucle. Toute la théorie du bloc tient en deux questions : quand répondre, et comment ne pas se trahir entre l'entraînement et le serving. Serving a model means making its predictions available at the moment they're needed — with no human in the loop. The block's whole theory fits in two questions: when to answer, and how not to betray yourself between training and serving.
Une seule source de vérité pour le calcul des features (dans src/, importée par le train ET l'API), le préprocesseur sauvé avec le modèle, un test qui vérifie le pipeline complet — et au bloc 8, le monitoring des distributions. Un score parfait au test et faux en prod : cherchez le skew. One single source of truth for feature computation (in src/, imported by training AND the API), the preprocessor saved with the model, one test checking the full pipeline — and in block 8, distribution monitoring. Perfect at test time, wrong in production: hunt the skew.
« Envoie-moi une transaction sous cette forme, je te renvoie un verdict sous cette forme. » L'app de paiement ne sait pas que c'est du scikit-learn derrière — et n'a pas besoin de le savoir. Tant que le contrat tient, on peut tout changer derrière la porte. "Send me a transaction in this shape, I'll send back a verdict in this shape." The payment app doesn't know there's scikit-learn behind the door — and doesn't need to. As long as the contract holds, everything behind the door can change.
La même démo que dans les slides : une transaction valide reçoit un verdict en quelques millisecondes ; une transaction cassée est refusée par le videur — proprement, précisément, automatiquement : The same demo as in the slides: a valid transaction gets a verdict in a few milliseconds; a broken one is rejected by the bouncer — cleanly, precisely, automatically:
Requête → POST /predictRequest → POST /predict
RéponseResponse
TestClient fait tourner l'API en mémoire : quatre tests couvrent le contrat — le cas nominal, le 422, le batch, le pouls. Et /docs expose une documentation générée depuis le code, interactive, qui ne ment jamais : la doc manuelle ment au bout de deux semaines, celle-ci est reconstruite à chaque démarrage. Au bloc 6, la CI rejouera ces tests à chaque commit. TestClient runs the API in memory: four tests cover the contract — the happy path, the 422, the batch, the pulse. And /docs exposes documentation generated from the code, interactive, that never lies: hand-written docs start lying after two weeks, these are rebuilt at every startup. In block 6, the CI will replay these tests on every commit.
Le mode de serving vient du métier ; le modèle se charge au démarrage ; on valide à la porte ; mêmes features au train et au serving ; on teste le contrat, pas les entrailles. The serving mode comes from the business; the model loads at startup; validate at the door; same features in training and serving; test the contract, not the guts.
« Donner une adresse au modèle : une API blindée et testée que n'importe quelle application de la banque peut appeler. » "Give the model an address: an armored, tested API that any application in the bank can call."
Votre modèle du bloc 2, servi en temps réel — quatre guichets, un videur, des tests, une doc qui ne ment pas. C'est cette API qu'on mettra en boîte au bloc 4. Your block 2 model, served in real time — four counters, a bouncer, tests, docs that don't lie. This is the API we'll box up in block 4.
Une théorie courte, puis trois missions au clavier — à cheval sur les jours 1 et 2. Les durées viennent du plan de cours : A short theory, then three hands-on missions — spread over days 1 and 2. Durations come straight from the course plan:
`uvicorn src.api.main:app` tourne ; les quatre guichets répondent ; une transaction invalide reçoit un 422 avec le champ fautif ; pytest vert (4 tests minimum) ; /docs décrit toute l'API — le tout commité. C'est cette API qu'on conteneurise au bloc 4. `uvicorn src.api.main:app` runs; all four counters answer; an invalid transaction gets a 422 naming the offending field; pytest green (4 tests minimum); /docs describes the whole API — all committed. This is the API we containerize in block 4.
Ce guide reprend les trois missions du lab, commande par commande, pour tout refaire seul. Prérequis : le bloc 2 terminé — le dépôt fraud-detection/, le venv .venv (Python 3.11), et les deux fichiers models/model.pkl et models/scaler.pkl produits par votre entraînement. À la fin : une API FastAPI à quatre guichets, validée par Pydantic, testée par pytest, documentée par Swagger — prête à être mise en boîte au bloc 4.
This guide replays the lab's three missions, command by command, so you can redo everything on your own. Prerequisite: block 2 finished — the fraud-detection/ repo, the .venv virtualenv (Python 3.11), and the two files models/model.pkl and models/scaler.pkl produced by your training. At the end: a FastAPI API with four counters, validated by Pydantic, tested with pytest, documented by Swagger — ready to be boxed up in block 4.
Placez-vous à la racine du dépôt, activez le venv du bloc 2, vérifiez que le modèle et le scaler sont bien là, puis installez les quatre briques du bloc. Move to the repo root, activate block 2's virtualenv, check that the model and the scaler are there, then install the block's four building bricks.
cd fraud-detection
source .venv/bin/activate
python --version
ls models/
pip install fastapi "uvicorn[standard]" pydantic pytest httpx
L'API vit dans src/serve/, les tests dans tests/, le feedback dans data/. Les fichiers __init__.py font de src un vrai package Python — sans eux, l'import src.serve.app échoue.
The API lives in src/serve/, the tests in tests/, the feedback in data/. The __init__.py files make src a real Python package — without them, the src.serve.app import fails.
mkdir -p src/serve tests data
touch src/__init__.py src/serve/__init__.py tests/__init__.py
find src tests -name "__init__.py"
Le fichier entier, en une fois : le lifespan charge le modèle et le scaler une seule fois au démarrage ; le videur Pydantic décrit les 30 features (time, amount, v1 à v28 — mêmes colonnes, même ordre qu'à l'entraînement) ; le seuil 0.31 est une constante, pas un chiffre enfoui. The whole file, in one go: the lifespan loads the model and the scaler once at startup; the Pydantic bouncer describes all 30 features (time, amount, v1 through v28 — same columns, same order as in training); the 0.31 threshold is a named constant, not a buried number.
import json
from contextlib import asynccontextmanager
from pathlib import Path
import joblib
import numpy as np
from fastapi import FastAPI
from pydantic import BaseModel, Field
THRESHOLD = 0.31
MODEL_PATH = Path("models/model.pkl")
SCALER_PATH = Path("models/scaler.pkl")
FEEDBACK_PATH = Path("data/feedback.jsonl")
ml = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
# load once at startup, not on every request
ml["model"] = joblib.load(MODEL_PATH)
ml["scaler"] = joblib.load(SCALER_PATH)
yield
ml.clear()
app = FastAPI(title="Fraud Detection API", version="0.1.0", lifespan=lifespan)
class Transaction(BaseModel):
time: float = Field(ge=0)
amount: float = Field(ge=0)
v1: float = 0.0
v2: float = 0.0
v3: float = 0.0
v4: float = 0.0
v5: float = 0.0
v6: float = 0.0
v7: float = 0.0
v8: float = 0.0
v9: float = 0.0
v10: float = 0.0
v11: float = 0.0
v12: float = 0.0
v13: float = 0.0
v14: float = 0.0
v15: float = 0.0
v16: float = 0.0
v17: float = 0.0
v18: float = 0.0
v19: float = 0.0
v20: float = 0.0
v21: float = 0.0
v22: float = 0.0
v23: float = 0.0
v24: float = 0.0
v25: float = 0.0
v26: float = 0.0
v27: float = 0.0
v28: float = 0.0
class Feedback(BaseModel):
transaction_id: str
fraud_confirmed: bool
def to_row(t: Transaction) -> list:
# same column order as training: Time, V1..V28, Amount
return [t.time] + [getattr(t, "v" + str(i)) for i in range(1, 29)] + [t.amount]
def score(rows: np.ndarray) -> np.ndarray:
x = ml["scaler"].transform(rows)
return ml["model"].predict_proba(x)[:, 1]
def as_verdict(s: float) -> dict:
return {"fraud_score": round(float(s), 4),
"verdict": "ALERT" if s >= THRESHOLD else "OK"}
@app.post("/predict")
def predict(transaction: Transaction):
s = score(np.array([to_row(transaction)]))[0]
return as_verdict(s)
@app.post("/batch")
def batch(transactions: list[Transaction]):
rows = np.array([to_row(t) for t in transactions])
return {"results": [as_verdict(s) for s in score(rows)]}
@app.post("/feedback")
def feedback(fb: Feedback):
FEEDBACK_PATH.parent.mkdir(parents=True, exist_ok=True)
with FEEDBACK_PATH.open("a", encoding="utf-8") as f:
f.write(json.dumps(fb.model_dump()) + "\n")
return {"stored": True}
@app.get("/health")
def health():
return {"status": "ok", "model_loaded": "model" in ml}
Toujours depuis la racine du dépôt — c'est de là que partent les chemins models/ et data/. Le flag --reload redémarre le serveur à chaque sauvegarde du fichier. Always from the repo root — that is where the models/ and data/ paths resolve from. The --reload flag restarts the server on every file save.
uvicorn src.serve.app:app --reload --port 8000
Dans un second terminal (laissez uvicorn tourner dans le premier) : une transaction valide entre, un verdict sort. Les v manquants prennent leur valeur par défaut 0.0. In a second terminal (leave uvicorn running in the first): a valid transaction goes in, a verdict comes out. Missing v fields take their 0.0 default.
curl -s -X POST http://127.0.0.1:8000/predict \
-H "Content-Type: application/json" \
-d '{"time": 3600, "amount": 129.90, "v14": -8.31, "v12": -5.20}'
Envoyez exprès un montant négatif : Pydantic doit la refuser avant qu'elle touche le modèle, en désignant le champ fautif. Deliberately send a negative amount: Pydantic must reject it before it touches the model, naming the offending field.
curl -s -X POST http://127.0.0.1:8000/predict \
-H "Content-Type: application/json" \
-w "\nHTTP %{http_code}\n" \
-d '{"time": 3600, "amount": -50.00}'
Même API, mode analyste : une liste de transactions, un passage vectorisé NumPy, une liste de verdicts. Same API, analyst mode: a list of transactions, one vectorized NumPy pass, a list of verdicts.
curl -s -X POST http://127.0.0.1:8000/batch \
-H "Content-Type: application/json" \
-d '[{"time": 3600, "amount": 129.90, "v14": -8.31},
{"time": 7200, "amount": 12.50}]'
L'analyste renvoie la vérité terrain ; l'API l'ajoute en JSONL dans data/feedback.jsonl — une ligne JSON par verdict, le futur carburant du ré-entraînement. The analyst sends ground truth back; the API appends it as JSONL to data/feedback.jsonl — one JSON line per verdict, the future fuel for retraining.
curl -s -X POST http://127.0.0.1:8000/feedback \
-H "Content-Type: application/json" \
-d '{"transaction_id": "tx-000042", "fraud_confirmed": true}'
cat data/feedback.jsonl
Le guichet lu par des machines — dès le bloc 4 par Docker, au bloc 7 par Kubernetes, toutes les dix secondes. The counter read by machines — by Docker from block 4 on, by Kubernetes every ten seconds from block 7.
curl -s http://127.0.0.1:8000/health
TestClient fait tourner l'API en mémoire, sans serveur. Le bloc with est important : c'est lui qui déclenche le lifespan, donc le chargement du modèle. Quatre tests couvrent le contrat : le pouls, le cas nominal, le 422, le batch.
TestClient runs the API in memory, no server needed. The with block matters: it is what triggers the lifespan, hence the model loading. Four tests cover the contract: the pulse, the happy path, the 422, the batch.
from fastapi.testclient import TestClient
from src.serve.app import app
VALID = {"time": 3600, "amount": 129.90, "v14": -8.31, "v12": -5.20}
def test_health():
with TestClient(app) as client:
r = client.get("/health")
assert r.status_code == 200
assert r.json() == {"status": "ok", "model_loaded": True}
def test_predict_valid():
with TestClient(app) as client:
r = client.post("/predict", json=VALID)
assert r.status_code == 200
body = r.json()
assert 0.0 <= body["fraud_score"] <= 1.0
assert body["verdict"] in ("ALERT", "OK")
def test_predict_negative_amount_is_rejected():
with TestClient(app) as client:
r = client.post("/predict", json={"time": 3600, "amount": -50.00})
assert r.status_code == 422
def test_batch_returns_one_verdict_per_transaction():
with TestClient(app) as client:
payload = [VALID, {"time": 7200, "amount": 12.50}]
r = client.post("/batch", json=payload)
assert r.status_code == 200
assert len(r.json()["results"]) == 2
Depuis la racine, avec python -m pour que le dossier courant soit sur le chemin d'import.
From the root, using python -m so the current folder is on the import path.
python -m pytest -q
Uvicorn toujours lancé, ouvrez Swagger dans le navigateur et essayez chaque guichet avec "Try it out" — la doc est générée depuis le code, reconstruite à chaque démarrage. With uvicorn still running, open Swagger in the browser and try every counter with "Try it out" — the docs are generated from the code, rebuilt at every startup.
open http://127.0.0.1:8000/docs
# Linux: xdg-open http://127.0.0.1:8000/docs
ModuleNotFoundError: No module named 'src' — vous n'êtes pas à la racine de fraud-detection/, ou il manque un des trois __init__.py de l'étape 2. FileNotFoundError: models/model.pkl — le modèle du bloc 2 n'est pas exporté, ou uvicorn est lancé depuis un sous-dossier : rejouez la sauvegarde joblib du bloc 2 et relancez depuis la racine. 422 inattendu sur une transaction valide — les noms de champs doivent correspondre exactement au modèle Pydantic : time, amount, v1 à v28, en minuscules. ERROR: [Errno 48] address already in use — un ancien uvicorn occupe le port : arrêtez-le (lsof -ti :8000 puis kill) ou lancez avec --port 8001. ModuleNotFoundError: No module named 'src' — you are not at the root of fraud-detection/, or one of step 2's three __init__.py files is missing. FileNotFoundError: models/model.pkl — block 2's model was never exported, or uvicorn was launched from a subfolder: replay block 2's joblib save and restart from the root. Unexpected 422 on a valid transaction — field names must match the Pydantic model exactly: time, amount, v1 through v28, lowercase. ERROR: [Errno 48] address already in use — an old uvicorn holds the port: stop it (lsof -ti :8000 then kill) or launch with --port 8001.