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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
À 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.
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:
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.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.
« 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.
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:
src/train/train.py)Baseline — stratified split, scaler on train only, logistic regression + class_weight, as a module (src/train/train.py)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.
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.
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__)"
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()
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%}")
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()
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()
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"
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()
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/
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
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
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
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"
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()
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
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.