Dotika MLOps · Bloc 2 — Le modèleBlock 2 — The model
▶ Slides▶ Slides ← Sommaire← Contents
Bloc 2 · 3 h · théorie 0h20 · lab 2h40Block 2 · 3 h · theory 0h20 · lab 2h40

Développement
du modèle
Model
development

Le système a besoin d'un cerveau. On le construit ensemble, au clavier : un détecteur de fraude entraîné, évalué, honnête — la matière première de toute la semaine. The system needs a brain. We build it together, at the keyboard: a fraud detector that's trained, evaluated, honest — the raw material for the whole week.

Où on en estWhere we stand

Le flux tourne, personne ne le regardeThe stream flows, nobody watches it

Bloc 1 : environnement prêt, repo Git propre, projet cadré. Mais sur la carte, le compteur des fraudes passées continue de monter. Il manque l'organe central : celui qui décide. Block 1: environment ready, clean Git repo, scoped project. But on the map, the missed-fraud counter keeps climbing. The central organ is missing: the one that decides.

Aujourd'huiToday

Le cerveau apparaîtThe brain appears

Une régression logistique entraînée sur 227 000 transactions réelles. Regardez : les points s'arrêtent, sont scorés, et les fraudes partent dans le bac rouge. Le compteur « fraudes bloquées » tourne enfin. A logistic regression trained on 227,000 real transactions. Watch: dots stop, get scored, and frauds land in the red bin. The "frauds blocked" counter finally moves.

Mais…But…

Un cerveau coincé dans un notebookA brain stuck in a notebook

Le cadre en pointillés dit tout : ce modèle vit sur un laptop, personne ne peut l'interroger, et il s'éteint quand on ferme l'ordinateur. Excellent, et inutilisable. C'est exactement le point de départ du bloc 3 — d'abord, construisons-le bien. The dotted frame says it all: this model lives on a laptop, nobody can query it, and it dies when the lid closes. Excellent, and unusable. That's exactly where block 3 picks up — first, let's build it well.

Acte 1 · les données d'abordAct 1 · data first

Un modèle vaut ce que valent ses donnéesA model is worth what its data is worth

L'algorithme le plus sophistiqué, nourri de données mal comprises, produit des erreurs sophistiquées. On passe donc les 40 premières minutes du lab à regarder : c'est le meilleur investissement de la journée. The most sophisticated algorithm, fed misunderstood data, produces sophisticated errors. So we spend the lab's first 40 minutes looking: the best investment of the day.

284 807
transactions réelles sur deux jours — trop pour Excel, parfait pour pandasreal transactions over two days — too many for Excel, perfect for pandas
0,17 %
de fraudes : 492 aiguilles dans la botte de foin — le chiffre qui dicte toutfraud: 492 needles in the haystack — the number that dictates everything
V1…V28
features anonymisées (PCA) : comme en vraie banque, on ne sait pas toutanonymized features (PCA): like in a real bank, you don't know everything
QUESTION 1
À quel point c'est déséquilibré ?How imbalanced is it?
Compter, visualiser. C'est ce qui décide de la métrique et de la stratégie d'entraînement.Count it, plot it. This decides the metric and the training strategy.
QUESTION 2
Les fraudes ont-elles un montant type ?Do frauds have a typical amount?
Distribution fraude vs légitime — le premier indice de signal exploitable.Fraud vs legit distribution — the first hint of exploitable signal.
QUESTION 3
Y a-t-il des heures à fraude ?Are there fraud hours?
Les patterns temporels parlent souvent — les fraudeurs aussi ont des horaires.Temporal patterns often speak — fraudsters keep hours too.
QUESTION 4
Quelles V séparent le mieux ?Which Vs separate best?
Même anonymisées, certaines V éloignent nettement les deux classes. Nos meilleures alliées.Even anonymized, some Vs clearly split the classes. Our best allies.
Les deux règles sacréesThe two sacred rules

1 · Le test est intouchable : ~20 % des données, jamais vues à l'entraînement, jamais rééquilibrées — elles simulent demain. 2 · Pas de fuite : tout ce qui « apprend » (scaler, features, rééquilibrage) se calcule sur le train seul. Un score trop beau pour être vrai ne l'est jamais — cherchez la fuite. 1 · The test set is untouchable: ~20% of the data, never seen in training, never rebalanced — it simulates tomorrow. 2 · No leakage: anything that "learns" (scaler, features, rebalancing) is fit on the train set only. A score too good to be true never is — hunt the leak.

Acte 2 · un premier cerveau, simpleAct 2 · a first, simple brain

« Et si on mettait un réseau de neurones ? » Non."What about a neural network?" No.

En production, le bon modèle n'est pas le plus malin — c'est le plus simple qui fait le travail. Notre baseline : la régression logistique, 60 ans d'âge, toujours en service dans la moitié des banques du monde. In production the right model isn't the smartest — it's the simplest one that does the job. Our baseline: logistic regression, 60 years old, still on duty in half the world's banks.

Une balance à soupçonsA scale for suspicion
Chaque variable pèse : montant inhabituel = + lourd, client régulier = − léger. Le modèle apprend le poids de chaque indice, puis écrase le total entre 0 et 1. C'est tout — le reste est du vocabulaire.Every variable weighs in: unusual amount = heavier, regular customer = lighter. The model learns the weight of each clue, then squashes the total between 0 and 1. That's it — the rest is vocabulary.
class_weight='balanced'
Sans consigne, ignorer 492 fraudes est la stratégie gagnante. Ce paramètre fait peser chaque fraude comme ~580 transactions : on encode le coût métier dans l'apprentissage.Left alone, ignoring 492 frauds is the winning strategy. This parameter makes each fraud weigh like ~580 transactions: business cost, encoded into learning.
Des probabilités, pas des verdictsProbabilities, not verdicts
Le modèle dit « 93 % de chances de fraude », jamais oui/non. Où placer la barre ? C'est le seuil — une décision métier déguisée en paramètre. Essayez-le ci-dessous.The model says "93% chance of fraud", never yes/no. Where does the bar go? That's the threshold — a business decision disguised as a parameter. Try it below.
Acte 3 · le juger honnêtementAct 3 · judge it honestly

Quatre cases, toute la véritéFour cells, the whole truth

Toutes les métriques ne sont que des résumés de la matrice de confusion. Quand une métrique vous embrouille, revenez aux quatre cases. Every metric is just a summary of the confusion matrix. When a metric confuses you, go back to the four cells.

le modèle dit : légitimemodel says: legit
le modèle dit : fraudemodel says: fraud
c'était légitimeit was legit
Vrai négatifTrue negative
Personne ne remarque rien — 99 % des cas, et c'est très bien.Nobody notices anything — 99% of cases, and that's fine.
Faux positifFalse positive
Client honnête bloqué : ~15 € + de la confiance perdue.Honest customer blocked: ~€15 + lost trust.
c'était une fraudeit was a fraud
Faux négatifFalse negative
La fraude passe, le montant est perdu — l'erreur la plus chère.The fraud gets through, the amount is lost — the priciest error.
Vrai positifTrue positive
Fraude bloquée — la case pour laquelle on construit tout.Fraud blocked — the cell we build everything for.

Le seuil : déplacez-le, sentez l'arbitrageThe threshold: move it, feel the trade-off

Alerter plus attrape plus de fraudes (recall ↑) mais multiplie les fausses alertes (precision ↓). Chaque position du curseur est un compromis — à vous de le sentir : Alerting more catches more frauds (recall ↑) but multiplies false alarms (precision ↓). Every slider position is a compromise — feel it yourself:

SeuilThreshold
0,50
Fraudes attrapéesFrauds caught
Fraudes ratéesFrauds missed
Bloqués à tortWrongly blocked

PR-AUC : juger tous les seuils d'un coupPR-AUC: judging every threshold at once

Chaque seuil donne un couple precision/recall. La courbe PR les trace tous ; l'aire sous la courbe — la PR-AUC — résume la qualité du modèle indépendamment du seuil. Honnête même à 0,17 % de fraudes, là où la ROC-AUC, noyée sous les 284 315 vrais négatifs, flatte n'importe quel modèle. Et le verdict final se rend dans la devise du métier : coût = fraudes ratées × montant + fausses alertes × 15 €. Every threshold gives one precision/recall pair. The PR curve plots them all; the area under it — the PR-AUC — summarizes model quality regardless of threshold. Honest even at 0.17% fraud, where ROC-AUC, drowned under 284,315 true negatives, flatters any model. And the final verdict is rendered in the business's currency: cost = missed frauds × amount + false alerts × €15.

À retenirTakeaway

À déséquilibre extrême, métriques de la classe rare : PR-AUC pour comparer les modèles, coût métier pour choisir le seuil. L'accuracy, elle, ment. Under extreme imbalance, use rare-class metrics: PR-AUC to compare models, business cost to pick the threshold. Accuracy lies.

Acte 4 · itérer, méthodiquementAct 4 · iterate, methodically

Une hypothèse à la foisOne hypothesis at a time

Améliorer un modèle n'est pas « essayer des trucs » : c'est une boucle scientifique — hypothèse (issue de l'analyse d'erreurs) → un seul changement → mesure sur le test → verdict. Trois leviers, du plus au moins rentable : Improving a model is not "trying stuff": it's a scientific loop — hypothesis (born from error analysis) → one single change → measure on the test set → verdict. Three levers, most to least profitable:

LEVIER 1LEVER 1
Fabriquer de meilleurs indicesCraft better clues
amount_log, heure du jour… Deux lignes de feature engineering battent souvent un changement d'algorithme.amount_log, hour of day… Two lines of feature engineering often beat an algorithm swap.
LEVIER 2LEVER 2
Rééquilibrer — train seul !Rebalance — train only!
Under/oversampling, SMOTE : jamais sur le test, et toujours comparé au simple class_weight — qui souvent suffit.Under/oversampling, SMOTE: never on the test set, and always compared against plain class_weight — often enough.
LEVIER 3LEVER 3
Le seuil, levier gratuitThe threshold, a free lever
Balayer tous les seuils, garder celui qui minimise le coût métier. Zéro ré-entraînement, parfois 12 000 € d'écart.Sweep every threshold, keep the one minimizing business cost. Zero retraining, sometimes €12,000 apart.
DANGERDANGER
Overfitting
Train 0,99, test 0,71 : le modèle a appris par cœur. Quand le test redescend alors que le train monte — stop.Train 0.99, test 0.71: the model learned by heart. When test drops while train climbs — stop.
PrémonitionPremonition

Dans une heure : huit modèles, des scores griffonnés, model_final_v2_VRAI_celui_la.pkl — et l'angoisse du « le run 7, c'était quoi déjà ? ». Retenez cette douleur : elle a un remède, il s'appelle MLflow, rendez-vous au bloc 5. An hour from now: eight models, scribbled scores, model_final_v2_REAL_this_one.pkl — and the dread of "wait, what was run 7?". Remember this pain: it has a cure named MLflow, see you in block 5.

La mission du blocThe block's mission

« Construire un modèle honnêtement évalué qui bat sa baseline — et savoir prouver qu'il est bon. » "Build an honestly evaluated model that beats its baseline — and be able to prove it's good."

Pas le meilleur modèle du monde. Un modèle propre, mesuré, expliqué — celui qu'on exposera en API au bloc 3. Not the world's best model. A clean, measured, explained one — the one we'll expose as an API in block 3.

Lab · 2h40Lab · 2h40

À vous de jouerYour turn

Quatre missions, en binômes — un qui code, un qui questionne, on échange à la mi-temps. Les durées viennent du plan de cours : Four missions, in pairs — one codes, one questions, swap at half-time. Durations come straight from the course plan:

0h40
Explorer — charger le CSV, vérifier les chiffres, répondre aux 4 questions de l'acte 1Explore — load the CSV, verify the numbers, answer act 1's four questions
pratiquehands-on
1h00
Baseline — split stratifié, scaler sur train seul, régression logistique + class_weight, en module (src/train/train.py)Baseline — stratified split, scaler on train only, logistic regression + class_weight, as a module (src/train/train.py)
pratiquehands-on
0h40
Itérer — 3 tours de boucle minimum : une feature, un rééquilibrage, le seuil optimisé coût — un changement par tourIterate — at least 3 loop rounds: one feature, one rebalancing, the cost-tuned threshold — one change per round
pratiquehands-on
0h40
Juger — matrices au tableau, courbes PR comparées, analyse d'erreurs : chaque binôme présente sa fraude ratée la plus chèreJudge — matrices on the board, PR curves compared, error analysis: each pair presents their priciest missed fraud
théo + prattheory + lab
Definition of done

Un modèle sauvé (.pkl), sa PR-AUC ≥ baseline (objectif ≥ 0,70 — si vous voyez 0,99, cherchez la fuite), son seuil justifié en euros, ses erreurs analysées, et un tableau de runs commité. C'est ce modèle qu'on branche au monde réel dès le bloc 3. A saved model (.pkl), its PR-AUC ≥ baseline (target ≥ 0.70 — if you see 0.99, hunt the leak), its threshold justified in euros, its errors analyzed, and a committed run table. This is the model we plug into the real world in block 3.

Pas à pas · en autonomieStep by step · self-paced

Faites-le vous-même, de A à ZDo it yourself, start to finish

Prérequis : le pas à pas du bloc 1 terminé — le repo fraud-detection/ existe, Python 3.11 est installé et data/creditcard.csv est vérifié. À la fin de ce guide, vous aurez un notebook d'exploration, un modèle entraîné par un script rejouable (PR-AUC ≥ 0,70), un seuil choisi en euros et une évaluation complète avec analyse d'erreurs.Prerequisites: block 1's walkthrough completed — the fraud-detection/ repo exists, Python 3.11 is installed and data/creditcard.csv is verified. By the end of this guide you'll have an exploration notebook, a model trained by a replayable script (PR-AUC ≥ 0.70), a threshold chosen in euros and a full evaluation with error analysis.

Mission 1 — Explorer les données (0h40)Mission 1 — Explore the data (0h40)

1Créer le venv et installer les dépendancesCreate the venv and install the dependencies

Un environnement virtuel par projet : les versions de vos librairies vivent dans .venv/ (ignoré par Git), pas dans le système. Activez-le au début de chaque session de travail.One virtual environment per project: your library versions live in .venv/ (ignored by Git), not in the system. Activate it at the start of every work session.

cd ~/fraud-detection
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install pandas scikit-learn matplotlib joblib jupyter
python -c "import sklearn, pandas; print(sklearn.__version__, pandas.__version__)"
Vous devriez voir : le prompt préfixé par (.venv) et deux numéros de version qui s'affichent sans erreur.You should see: the prompt prefixed with (.venv) and two version numbers printed without errors.
2Ouvrir le notebook d'exploration et charger le CSVOpen the exploration notebook and load the CSV

Lancez Jupyter, créez un notebook nommé 01_exploration dans notebooks/, puis exécutez la première cellule. Le chemin est relatif au dossier notebooks/, d'où le ../ devant data/.Launch Jupyter, create a notebook named 01_exploration inside notebooks/, then run the first cell. The path is relative to the notebooks/ folder, hence the ../ before data/.

jupyter notebook notebooks/
# in the browser: New, then Notebook (Python 3), rename it 01_exploration
import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv("../data/creditcard.csv")
print(df.shape)
df.head()
Vous devriez voir : (284807, 31) — 31 colonnes : Time, V1 à V28, Amount, Class.You should see: (284807, 31) — 31 columns: Time, V1 to V28, Amount, Class.
3Question 1 — à quel point c'est déséquilibré ?Question 1 — how imbalanced is it?

Le chiffre qui décide de tout : la métrique, la stratégie d'entraînement, le seuil. Comptez-le vous-même.The number that decides everything: the metric, the training strategy, the threshold. Count it yourself.

counts = df["Class"].value_counts()
print(counts)
print(f"fraud rate: {counts[1] / len(df):.3%}")
Vous devriez voir : 284315 légitimes, 492 fraudes — un taux de 0.173%.You should see: 284,315 legit, 492 frauds — a 0.173% rate.
4Question 2 — les fraudes ont-elles un montant type ?Question 2 — do frauds have a typical amount?

Une ligne de pandas suffit pour comparer les distributions de montants des deux classes.One line of pandas is enough to compare the amount distributions of the two classes.

df.groupby("Class")["Amount"].describe()
Vous devriez voir : un montant médian des fraudes d'environ 9.25 (beaucoup de petits « tests de carte ») et un maximum vers 2125.87.You should see: a median fraud amount around 9.25 (many small "card tests") and a maximum near 2125.87.
5Question 3 — y a-t-il des heures à fraude ?Question 3 — are there fraud hours?

Time est un nombre de secondes depuis la première transaction : on le convertit en heure du jour, puis on trace le taux de fraude heure par heure.Time is a number of seconds since the first transaction: convert it to hour of day, then plot the fraud rate hour by hour.

df["hour"] = (df["Time"] // 3600) % 24
rate = df.groupby("hour")["Class"].mean() * 100
rate.plot(kind="bar", figsize=(10, 3), title="Fraud rate per hour (%)")
plt.show()
Vous devriez voir : un pic net du taux de fraude au coeur de la nuit, autour de 2 h — les fraudeurs aussi ont des horaires.You should see: a clear fraud-rate spike in the middle of the night, around 2 am — fraudsters keep hours too.
6Question 4 — quelles V séparent le mieux ? Puis commitQuestion 4 — which Vs separate best? Then commit

V14 et V17 sont les alliées classiques de ce dataset. Écrivez vos quatre réponses en cellules markdown, sauvegardez, puis commitez le notebook — c'est le livrable de la mission.V14 and V17 are this dataset's classic allies. Write your four answers in markdown cells, save, then commit the notebook — that's the mission's deliverable.

fig, axes = plt.subplots(1, 2, figsize=(11, 4))
for ax, col in zip(axes, ["V14", "V17"]):
    df.boxplot(column=col, by="Class", ax=ax)
    ax.set_title(col)
plt.suptitle("")
plt.show()
git add notebooks/01_exploration.ipynb
git commit -m "feat: exploration notebook answering the four data questions"
Vous devriez voir : pour la classe 1, des boîtes V14 et V17 nettement décalées vers le bas — du signal exploitable.You should see: for class 1, V14 and V17 boxes clearly shifted downwards — exploitable signal.

Mission 2 — Construire la baseline, en module (1h00)Mission 2 — Build the baseline, as a module (1h00)

7Écrire src/train/train.py — le script rejouableWrite src/train/train.py — the replayable script

Créez le fichier src/train/train.py avec ce contenu exact. Les deux règles sacrées y sont codées : split stratifié 80/20 avec random_state=42, et scaler fitté sur le train seul.Create the file src/train/train.py with this exact content. The two sacred rules are coded in: stratified 80/20 split with random_state=42, and a scaler fitted on the train set only.

"""Fraud detection baseline: logistic regression, honestly evaluated."""
from pathlib import Path

import joblib
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import average_precision_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

DATA_PATH = Path("data/creditcard.csv")
MODELS_DIR = Path("models")
RANDOM_STATE = 42


def load_data(path=DATA_PATH):
    df = pd.read_csv(path)
    X = df.drop(columns=["Class"])
    y = df["Class"]
    return X, y


def split(X, y):
    return train_test_split(
        X, y, test_size=0.2, stratify=y, random_state=RANDOM_STATE
    )


def train(X_train, y_train):
    scaler = StandardScaler()
    X_train_scaled = scaler.fit_transform(X_train)  # fit on train ONLY
    model = LogisticRegression(class_weight="balanced", max_iter=1000)
    model.fit(X_train_scaled, y_train)
    return model, scaler


def evaluate(model, scaler, X_test, y_test):
    X_test_scaled = scaler.transform(X_test)  # transform, never fit
    scores = model.predict_proba(X_test_scaled)[:, 1]
    return average_precision_score(y_test, scores)


def main():
    X, y = load_data()
    X_train, X_test, y_train, y_test = split(X, y)
    model, scaler = train(X_train, y_train)
    pr_auc = evaluate(model, scaler, X_test, y_test)
    MODELS_DIR.mkdir(exist_ok=True)
    joblib.dump(model, MODELS_DIR / "model.pkl")
    joblib.dump(scaler, MODELS_DIR / "scaler.pkl")
    print(f"PR-AUC (test): {pr_auc:.4f}")
    print("saved: models/model.pkl + models/scaler.pkl")


if __name__ == "__main__":
    main()
Vous devriez voir : python -m py_compile src/train/train.py qui ne renvoie rien (aucune erreur de syntaxe).You should see: python -m py_compile src/train/train.py returning nothing (no syntax error).
8Entraîner — et vérifier le checkpoint PR-AUCTrain — and check the PR-AUC checkpoint

On lance en module (-m) depuis la racine du repo : c'est ce qui rend les imports et les chemins stables — le réflexe MLOps du jour.Run it as a module (-m) from the repo root: that's what keeps imports and paths stable — today's MLOps reflex.

cd ~/fraud-detection
source .venv/bin/activate
python -m src.train.train
ls -lh models/
Vous devriez voir : PR-AUC (test) entre 0.70 et 0.80 (objectif ≥ 0.70), et models/model.pkl + models/scaler.pkl créés. Si vous voyez 0.99 : fuite — cherchez un scaler fitté avant le split.You should see: PR-AUC (test) between 0.70 and 0.80 (target ≥ 0.70), and models/model.pkl + models/scaler.pkl created. If you see 0.99: leakage — look for a scaler fitted before the split.

Mission 3 — Itérer méthodiquement (0h40)Mission 3 — Iterate methodically (0h40)

9Tour 1 — fabriquer deux features : amount_log et hourRound 1 — craft two features: amount_log and hour

Hypothèse née de l'exploration : les montants sont très asymétriques (le log les assagit) et la fraude a des horaires. Modifiez src/train/train.py : ajoutez l'import numpy, la fonction add_features, et remplacez load_data.Hypothesis born from the exploration: amounts are heavily skewed (log tames them) and fraud keeps hours. Edit src/train/train.py: add the numpy import, the add_features function, and replace load_data.

# train.py - add to the imports
import numpy as np


# train.py - add this function, and make load_data call it
def add_features(df):
    df = df.copy()
    df["amount_log"] = np.log1p(df["Amount"])
    df["hour"] = (df["Time"] // 3600) % 24
    return df


def load_data(path=DATA_PATH):
    df = pd.read_csv(path)
    df = add_features(df)
    X = df.drop(columns=["Class"])
    y = df["Class"]
    return X, y
python -m src.train.train
Vous devriez voir : un nouveau PR-AUC. Notez-le : un tour de boucle = une hypothèse, un seul changement, une mesure, un verdict.You should see: a new PR-AUC. Write it down: one loop round = one hypothesis, one single change, one measurement, one verdict.
10Tour 2 — le levier gratuit : balayer le seuil au coût métierRound 2 — the free lever: sweep the threshold on business cost

Créez src/train/tune_threshold.py avec ce contenu exact : la fonction cost applique la formule du cadrage — fraudes ratées × montant moyen + fausses alertes × 15 €. Zéro ré-entraînement.Create src/train/tune_threshold.py with this exact content: the cost function applies the scoping formula — missed frauds × average amount + false alerts × €15. Zero retraining.

"""Sweep decision thresholds, keep the one that minimizes business cost."""
import joblib
import numpy as np

from src.train.train import MODELS_DIR, load_data, split

FP_COST = 15.0  # cost of one false alert, in euros


def main():
    X, y = load_data()
    X_train, X_test, y_train, y_test = split(X, y)
    model = joblib.load(MODELS_DIR / "model.pkl")
    scaler = joblib.load(MODELS_DIR / "scaler.pkl")
    scores = model.predict_proba(scaler.transform(X_test))[:, 1]

    y_true = y_test.to_numpy()
    amounts = X_test["Amount"].to_numpy()
    avg_fraud_amount = amounts[y_true == 1].mean()

    def cost(threshold):
        alerts = scores >= threshold
        missed_frauds = int(((y_true == 1) & ~alerts).sum())
        false_alerts = int(((y_true == 0) & alerts).sum())
        return missed_frauds * avg_fraud_amount + false_alerts * FP_COST

    thresholds = np.arange(0.05, 0.95, 0.01)
    costs = [cost(t) for t in thresholds]
    best = float(thresholds[int(np.argmin(costs))])
    print(f"cost at threshold 0.50: {cost(0.50):,.0f} EUR")
    print(f"best threshold: {best:.2f} (cost {min(costs):,.0f} EUR)")


if __name__ == "__main__":
    main()
python -m src.train.tune_threshold
Vous devriez voir : un meilleur seuil différent de 0.50, avec un coût inférieur de plusieurs milliers d'euros. Notez ce seuil — il sert aux étapes 12 et 13.You should see: a best threshold different from 0.50, with a cost several thousand euros lower. Write that threshold down — steps 12 and 13 use it.
11Tour 3 — essayer SMOTE, sur le train seulRound 3 — try SMOTE, on the train set only

SMOTE fabrique des fraudes synthétiques entre les vraies — élégant, et dangereux si appliqué avant le split ou sur le test. Comparez toujours au simple class_weight, et gardez le gagnant.SMOTE crafts synthetic frauds between real ones — elegant, and dangerous if applied before the split or on the test set. Always compare against plain class_weight, and keep the winner.

pip install imbalanced-learn
# train.py - an alternative train() to compare against class_weight
from imblearn.over_sampling import SMOTE  # add to the imports


def train(X_train, y_train):
    scaler = StandardScaler()
    X_train_scaled = scaler.fit_transform(X_train)  # still train ONLY
    X_res, y_res = SMOTE(random_state=RANDOM_STATE).fit_resample(
        X_train_scaled, y_train
    )
    model = LogisticRegression(max_iter=1000)  # no class_weight here
    model.fit(X_res, y_res)
    return model, scaler
Vous devriez voir : un PR-AUC comparable au tour précédent. Si SMOTE ne gagne pas, revenez à class_weight — c'est souvent le cas, et c'est une conclusion valable.You should see: a PR-AUC comparable to the previous round. If SMOTE doesn't win, go back to class_weight — that's often the case, and it's a valid conclusion.
12Tenir le tableau des runs — votre futur MLflowKeep the run table — your future MLflow

Un run par ligne : hypothèse, réglage, score, verdict. Les chiffres ci-dessous sont des exemples — remplacez-les par les vôtres avant de committer.One run per line: hypothesis, setting, score, verdict. The numbers below are examples — replace them with yours before committing.

mkdir -p docs
cat > docs/runs.md << 'EOF'
# Runs - block 2 (replace the example scores with yours)

| run | change                   | PR-AUC | threshold | cost (EUR) | verdict |
|-----|--------------------------|--------|-----------|------------|---------|
| 1   | baseline (class_weight)  | 0.72   | 0.50      | 41300      | ref     |
| 2   | + amount_log, hour       | 0.74   | 0.50      | 39800      | keep    |
| 3   | threshold tuned on cost  | 0.74   | 0.31      | 28750      | keep    |
| 4   | SMOTE vs class_weight    | 0.73   | 0.31      | 29900      | drop    |
EOF

git add src/ docs/runs.md
git commit -m "feat: baseline plus three iteration rounds with run log"
Vous devriez voir : quatre runs documentés avec vos chiffres, et un commit de plus dans git log --oneline.You should see: four documented runs with your numbers, and one more commit in git log --oneline.

Mission 4 — Juger honnêtement (0h40)Mission 4 — Judge honestly (0h40)

13Écrire src/train/evaluate.py — matrice, courbe PR, erreursWrite src/train/evaluate.py — matrix, PR curve, errors

Créez src/train/evaluate.py avec ce contenu exact, en remplaçant THRESHOLD par le seuil retenu à l'étape 10. Trois verdicts : la matrice de confusion au seuil choisi, la courbe PR sauvegardée en image, et le top-10 des fraudes ratées triées par montant — la matière de votre analyse d'erreurs.Create src/train/evaluate.py with this exact content, replacing THRESHOLD with the threshold kept at step 10. Three verdicts: the confusion matrix at the chosen threshold, the PR curve saved as an image, and the top-10 missed frauds sorted by amount — the raw material of your error analysis.

"""Final judgment: confusion matrix, PR curve, priciest missed frauds."""
from pathlib import Path

import joblib
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from sklearn.metrics import (
    average_precision_score,
    confusion_matrix,
    precision_recall_curve,
)

from src.train.train import MODELS_DIR, load_data, split

THRESHOLD = 0.31  # the threshold you kept at step 10
REPORTS_DIR = Path("reports")


def main():
    REPORTS_DIR.mkdir(exist_ok=True)
    X, y = load_data()
    X_train, X_test, y_train, y_test = split(X, y)
    model = joblib.load(MODELS_DIR / "model.pkl")
    scaler = joblib.load(MODELS_DIR / "scaler.pkl")
    scores = model.predict_proba(scaler.transform(X_test))[:, 1]

    # 1. confusion matrix at the chosen threshold
    preds = (scores >= THRESHOLD).astype(int)
    tn, fp, fn, tp = confusion_matrix(y_test, preds).ravel()
    print(f"TN={tn}  FP={fp}  FN={fn}  TP={tp}")

    # 2. precision-recall curve, saved as an image
    precision, recall, _ = precision_recall_curve(y_test, scores)
    pr_auc = average_precision_score(y_test, scores)
    plt.figure(figsize=(7, 5))
    plt.plot(recall, precision, color="#E6332A")
    plt.xlabel("recall")
    plt.ylabel("precision")
    plt.title(f"PR curve - AP = {pr_auc:.3f}")
    plt.grid(True, alpha=0.3)
    plt.savefig(REPORTS_DIR / "pr_curve.png", dpi=150)
    print("saved: reports/pr_curve.png")

    # 3. the ten priciest missed frauds
    test = X_test.copy()
    test["true_class"] = y_test.to_numpy()
    test["score"] = scores
    missed = test[(test["true_class"] == 1) & (test["score"] < THRESHOLD)]
    top10 = missed.sort_values("Amount", ascending=False).head(10)
    print(top10[["Time", "Amount", "score"]].to_string())


if __name__ == "__main__":
    main()
Vous devriez voir : python -m py_compile src/train/evaluate.py qui ne renvoie rien.You should see: python -m py_compile src/train/evaluate.py returning nothing.
14Rendre le verdict — et committer la definition of doneDeliver the verdict — and commit the definition of done

Lisez les trois sorties comme un juge : combien de fraudes ratées (FN) et à quel prix ? Les ratées ont-elles un point commun — heure, montant, profil de V ? Chaque pattern est une hypothèse pour un prochain tour de boucle.Read the three outputs like a judge: how many missed frauds (FN) and at what price? Do the missed ones share a pattern — hour, amount, V profile? Every pattern is a hypothesis for a next loop round.

python -m src.train.evaluate

git add src/train/evaluate.py
git commit -m "feat: evaluation - confusion matrix, PR curve, error analysis"
git log --oneline
Vous devriez voir : TN/FP/FN/TP affichés, reports/pr_curve.png créé, le top-10 des fraudes ratées — et la definition of done atteinte : modèle sauvé, PR-AUC ≥ 0.70, seuil justifié en euros, erreurs analysées.You should see: TN/FP/FN/TP printed, reports/pr_curve.png created, the top-10 missed frauds — and the definition of done reached: saved model, PR-AUC ≥ 0.70, threshold justified in euros, errors analyzed.
Si ça coinceWhen it breaks

PR-AUC à 0.99 : c'est une fuite, pas un exploit. Vérifiez l'ordre des opérations : d'abord le split, ensuite le scaler (et SMOTE) fittés sur le train seul. Un fit_transform sur tout le dataset, ou un rééquilibrage avant le split, et le test a « vu » l'entraînement.PR-AUC at 0.99: it's a leak, not a feat. Check the order of operations: split first, then scaler (and SMOTE) fitted on the train set only. One fit_transform on the whole dataset, or rebalancing before the split, and the test set has "seen" the training.

ConvergenceWarning à l'entraînement. La régression logistique n'a pas fini de converger : vérifiez que max_iter=1000 est bien là, ou montez à 2000. Sans gravité si le PR-AUC reste stable d'un run à l'autre.ConvergenceWarning during training. The logistic regression didn't finish converging: check that max_iter=1000 is really there, or raise it to 2000. Harmless if the PR-AUC stays stable from run to run.

Mémoire saturée ou machine qui rame. Le CSV pèse 144 Mo et float64 double la note : convertissez les colonnes numériques (toutes sauf Class) avec astype("float32") juste après le read_csv, et fermez les notebooks Jupyter inutilisés.Memory maxed out or a sluggish machine. The CSV weighs 144 MB and float64 doubles the bill: convert the numeric columns (all but Class) with astype("float32") right after read_csv, and close unused Jupyter notebooks.

kaggle renvoie 403 Forbidden. Token API manquant ou expiré : régénérez-le sur kaggle.com (Settings, API), placez-le dans ~/.kaggle/kaggle.json, chmod 600 — le pas à pas du bloc 1, étape 9, détaille la manœuvre.kaggle returns 403 Forbidden. Missing or expired API token: regenerate it on kaggle.com (Settings, API), place it at ~/.kaggle/kaggle.json, chmod 600 — block 1's walkthrough, step 9, covers the drill.