Corrections · Semaine 1 — Linéaire · Logistique
Dataset 200 observations · EDA complète · LinearRegression · MSE/RMSE/MAE/R² · Visualisation prédictions vs réalité + résidus.
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error import matplotlib.pyplot as plt import seaborn as sns np.random.seed(123) n = 200 # ── 1. Créer le dataset ────────────────────────────────────── data = { 'production_tonnes' : np.random.randint(80000, 150000, n), 'pluviometrie_mm' : np.random.randint(400, 1200, n), 'prix_carburant_ksh' : np.random.uniform(100, 150, n), 'demande_export_tonnes': np.random.randint(10000, 50000, n), } df = pd.DataFrame(data) df['prix_mais_ksh_kg'] = ( 30 - 0.0001 * df['production_tonnes'] + 0.01 * df['pluviometrie_mm'] + 0.2 * df['prix_carburant_ksh'] + 0.0003 * df['demande_export_tonnes'] + np.random.normal(0, 3, n) ) # ── 2. EDA complète ────────────────────────────────────────── print("=== Statistiques descriptives ===") print(df.describe().round(2)) print("\nCorrélations avec la cible :") print(df.corr()['prix_mais_ksh_kg'].sort_values(ascending=False).round(4)) plt.figure(figsize=(8, 6)) sns.heatmap(df.corr(), annot=True, cmap='coolwarm', center=0, fmt='.2f') plt.title('Matrice de Corrélation — Maïs Kenya') plt.tight_layout() plt.show() fig, axes = plt.subplots(2, 2, figsize=(12, 8)) for idx, col in enumerate(data.keys()): r, c = idx // 2, idx % 2 axes[r, c].scatter(df[col], df['prix_mais_ksh_kg'], alpha=0.5, color='#2a5298') axes[r, c].set_xlabel(col) axes[r, c].set_ylabel('Prix (KSh/kg)') axes[r, c].set_title(f'Prix vs {col}') plt.tight_layout() plt.show() # ── 3. Train / Test split ──────────────────────────────────── X = df.drop('prix_mais_ksh_kg', axis=1) y = df['prix_mais_ksh_kg'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42) # ── 4. Entraînement + métriques ────────────────────────────── model = LinearRegression() model.fit(X_train, y_train) y_pred = model.predict(X_test) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) mae = mean_absolute_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) print(f"\nRMSE : {rmse:.2f} KSh/kg") print(f"MAE : {mae:.2f} KSh/kg") print(f"R² : {r2:.4f}") print("\nCoefficients :") for feat, coef in zip(X.columns, model.coef_): print(f" {feat:<30} : {coef:+.6f}") # ── 5. Visualisations ──────────────────────────────────────── fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # Prédictions vs Réalité axes[0].scatter(y_test, y_pred, alpha=0.7, color='#2a5298') lims = [min(y_test.min(), y_pred.min()), max(y_test.max(), y_pred.max())] axes[0].plot(lims, lims, 'r--', lw=2, label='Ligne idéale') axes[0].set_xlabel('Valeurs Réelles (KSh/kg)') axes[0].set_ylabel('Prédictions (KSh/kg)') axes[0].set_title('Prédictions vs Réalité') axes[0].legend() # Analyse des résidus residus = y_test - y_pred axes[1].scatter(y_pred, residus, alpha=0.7, color='#764ba2') axes[1].axhline(y=0, color='r', linestyle='--', lw=2) axes[1].set_xlabel('Prédictions (KSh/kg)') axes[1].set_ylabel('Résidus') axes[1].set_title('Analyse des Résidus') plt.tight_layout() plt.savefig('resultats_mais_kenya.png', dpi=300, bbox_inches='tight') plt.show()
Dataset 500 élèves · StandardScaler · LogisticRegression · accuracy/precision/recall/F1 · Matrice de confusion · Courbe ROC.
stratify=y lors du split pour conserver les proportions de classes dans train et test.import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.metrics import (classification_report, confusion_matrix, roc_auc_score, roc_curve, accuracy_score) import matplotlib.pyplot as plt import seaborn as sns np.random.seed(42) n = 500 # ── 1. Dataset — 500 élèves nigérians ──────────────────────── df = pd.DataFrame({ 'distance_km' : np.random.uniform(0, 30, n), 'revenu_naira' : np.random.randint(20000, 500000, n), 'nb_absences' : np.random.randint(0, 50, n), 'note_moyenne' : np.random.uniform(20, 100, n), 'genre' : np.random.choice([0, 1], n), # 0=F, 1=M }) prob = ( 0.15 * df['distance_km'] / 30 + 0.30 * (1 - df['revenu_naira'] / 500000) + 0.30 * df['nb_absences'] / 50 + 0.20 * (1 - df['note_moyenne'] / 100) + 0.05 * df['genre'] + np.random.uniform(-0.1, 0.1, n) ) df['abandon'] = (prob > 0.35).astype(int) print(f"Taux d'abandon : {df['abandon'].mean()*100:.1f}%") # ── 2. Entraînement LogisticRegression ─────────────────────── X = df.drop('abandon', axis=1) y = df['abandon'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42) scaler = StandardScaler() X_train_sc = scaler.fit_transform(X_train) X_test_sc = scaler.transform(X_test) model = LogisticRegression(max_iter=1000, random_state=42) model.fit(X_train_sc, y_train) y_pred = model.predict(X_test_sc) y_pred_proba = model.predict_proba(X_test_sc)[:, 1] # ── 3. Métriques ───────────────────────────────────────────── print("\n=== Rapport de Classification ===") print(classification_report(y_test, y_pred, target_names=["Pas d'abandon", 'Abandon'])) print(f"Accuracy : {accuracy_score(y_test, y_pred):.4f}") print(f"ROC-AUC : {roc_auc_score(y_test, y_pred_proba):.4f}") # ── 4. Matrice de confusion ─────────────────────────────────── cm = confusion_matrix(y_test, y_pred) plt.figure(figsize=(6, 5)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=["Pas d'abandon", 'Abandon'], yticklabels=["Pas d'abandon", 'Abandon']) plt.title('Matrice de Confusion — Abandon Scolaire Nigeria') plt.ylabel('Réel') plt.xlabel('Prédit') plt.tight_layout() plt.savefig('confusion_abandon_nigeria.png', dpi=300) plt.show() # ── 5. Courbe ROC + ROC-AUC ────────────────────────────────── fpr, tpr, _ = roc_curve(y_test, y_pred_proba) auc = roc_auc_score(y_test, y_pred_proba) plt.figure(figsize=(7, 5)) plt.plot(fpr, tpr, color='#2a5298', lw=2, label=f'ROC (AUC = {auc:.4f})') plt.plot([0, 1], [0, 1], 'k--', label='Aléatoire (AUC = 0.5)') plt.xlim([0.0, 1.0]); plt.ylim([0.0, 1.02]) plt.xlabel('Taux Faux Positifs (FPR)') plt.ylabel('Taux Vrais Positifs (TPR)') plt.title('Courbe ROC — Abandon Scolaire Nigeria') plt.legend(loc='lower right') plt.tight_layout() plt.savefig('roc_abandon_nigeria.png', dpi=300) plt.show()
Corrections · Semaine 2 — Random Forest · SMOTE · Grid Search
Dataset 800 sources · One-hot encoding · Vérification déséquilibre → SMOTE automatique · GridSearchCV · Feature Importance.
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, roc_auc_score from imblearn.over_sampling import SMOTE import matplotlib.pyplot as plt np.random.seed(42) n = 800 # ── 1. Dataset ─────────────────────────────────────────────── df = pd.DataFrame({ 'pH' : np.random.uniform(5.5, 9.5, n), 'turbidite_NTU' : np.random.exponential(10, n).clip(max=50), 'chlore_mg_L' : np.random.uniform(0, 2, n), 'coliformes' : np.random.randint(0, 500, n), 'conductivite' : np.random.uniform(50, 2000, n), 'temperature' : np.random.uniform(15, 35, n), 'source' : np.random.choice(['puits', 'riviere', 'robinet', 'source'], n), }) score = ( ((df['pH'] >= 6.5) & (df['pH'] <= 8.5)).astype(float) * 0.30 + (df['turbidite_NTU'] < 5).astype(float) * 0.25 + (df['chlore_mg_L'] < 1).astype(float) * 0.20 + (df['coliformes'] < 50).astype(float) * 0.15 + (df['conductivite'] < 1000).astype(float) * 0.10 + np.random.uniform(-0.1, 0.1, n) ) df['potable'] = (score > 0.50).astype(int) print(f"Eau potable : {df['potable'].mean()*100:.1f}%") # ── 2. Encodage + split ────────────────────────────────────── df_enc = pd.get_dummies(df, columns=['source'], drop_first=True) X = df_enc.drop('potable', axis=1) y = df_enc['potable'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42) # ── 3. SMOTE si déséquilibre > 60/40 ───────────────────────── dist = y_train.value_counts(normalize=True) print(f"Distribution classes : {dist.to_dict()}") if dist.min() < 0.40: sm = SMOTE(random_state=42, k_neighbors=5) X_train, y_train = sm.fit_resample(X_train, y_train) print(f"Après SMOTE : {dict(zip(*np.unique(y_train, return_counts=True)))}") # ── 4. Grid Search ─────────────────────────────────────────── param_grid = { 'n_estimators' : [50, 100, 200], 'max_depth' : [5, 10, None], 'min_samples_split': [2, 5], } gs = GridSearchCV( RandomForestClassifier(random_state=42, n_jobs=-1), param_grid, cv=5, scoring='roc_auc', n_jobs=-1, verbose=0 ) gs.fit(X_train, y_train) print(f"\nMeilleurs paramètres : {gs.best_params_}") print(f"Meilleur ROC-AUC (CV) : {gs.best_score_:.4f}") best_rf = gs.best_estimator_ y_pred = best_rf.predict(X_test) y_pred_proba = best_rf.predict_proba(X_test)[:, 1] print("\n=== Rapport de Classification ===") print(classification_report(y_test, y_pred, target_names=['Non Potable', 'Potable'])) print(f"ROC-AUC test : {roc_auc_score(y_test, y_pred_proba):.4f}") # ── 5. Importance des features ─────────────────────────────── importances = pd.Series(best_rf.feature_importances_, index=X.columns) importances.sort_values(ascending=True).plot( kind='barh', figsize=(9, 6), color='#2a5298') plt.title("Importance des Features — Qualité d'Eau Rwanda") plt.xlabel('Importance (Gini)') plt.tight_layout() plt.savefig('feature_importance_eau_rwanda.png', dpi=300) plt.show() print("\n=== Top 5 Features ===") for feat, imp in importances.sort_values(ascending=False).head(5).items(): print(f" {feat:<25} : {imp:.4f}")
Corrections · Semaine 3 — XGBoost · SHAP · Métriques avancées
Dataset 2 000 zones · XGBClassifier avec scale_pos_weight · ROC-AUC / F1 / Precision / Recall · SHAP summary_plot · Top 3 facteurs.
scale_pos_weight = nb_négatifs / nb_positifs corrige le déséquilibre de classes dans XGBoost sans nécessiter de rééchantillonnage.import pandas as pd import numpy as np import xgboost as xgb from sklearn.model_selection import train_test_split from sklearn.metrics import (classification_report, roc_auc_score, f1_score, precision_score, recall_score) import shap import matplotlib.pyplot as plt np.random.seed(42) n = 2000 # ── 1. Dataset — Tanzanie ──────────────────────────────────── df = pd.DataFrame({ 'temperature' : np.random.uniform(18, 35, n), 'humidite_pct' : np.random.uniform(30, 95, n), 'altitude_m' : np.random.uniform(0, 3000, n), 'densite_pop' : np.random.exponential(100, n).clip(max=1000), 'acces_eau_pct' : np.random.uniform(10, 95, n), 'couverture_moustiq_pct' : np.random.uniform(5, 85, n), }) score = ( (df['temperature'] - 18) / 17 * 0.25 + df['humidite_pct'] / 95 * 0.25 + (1 - df['altitude_m'] / 3000) * 0.15 + (df['densite_pop'] / 500).clip(upper=1) * 0.15 + (1 - df['acces_eau_pct'] / 95) * 0.10 + (1 - df['couverture_moustiq_pct'] / 85) * 0.10 + np.random.uniform(-0.10, 0.10, n) ) df['zone_risque'] = (score > 0.45).astype(int) print(f"Zones à risque : {df['zone_risque'].mean()*100:.1f}%") # ── 2. XGBoost ─────────────────────────────────────────────── X = df.drop('zone_risque', axis=1) y = df['zone_risque'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42) scale_w = (y_train == 0).sum() / (y_train == 1).sum() model = xgb.XGBClassifier( n_estimators=100, max_depth=6, learning_rate=0.1, scale_pos_weight=scale_w, random_state=42, eval_metric='logloss' ) model.fit(X_train, y_train) y_pred = model.predict(X_test) y_pred_proba = model.predict_proba(X_test)[:, 1] # ── 3. Métriques ───────────────────────────────────────────── print("\n=== Rapport de Classification ===") print(classification_report(y_test, y_pred, target_names=['Faible Risque', 'Haut Risque'])) print(f"ROC-AUC : {roc_auc_score(y_test, y_pred_proba):.4f}") print(f"F1-Score : {f1_score(y_test, y_pred):.4f}") print(f"Precision : {precision_score(y_test, y_pred):.4f}") print(f"Recall : {recall_score(y_test, y_pred):.4f}") # ── 4. SHAP Summary Plot ───────────────────────────────────── explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_test) shap.summary_plot(shap_values, X_test, show=False) plt.title('SHAP Summary Plot — Détection Paludisme Tanzanie') plt.tight_layout() plt.savefig('shap_paludisme_tanzanie.png', dpi=300, bbox_inches='tight') plt.show() # ── 5. Top 3 facteurs influents ────────────────────────────── mean_abs_shap = np.abs(shap_values).mean(axis=0) top3 = pd.Series(mean_abs_shap, index=X.columns).sort_values(ascending=False).head(3) print("\n=== Top 3 Facteurs les Plus Influents (SHAP) ===") for rang, (feat, val) in enumerate(top3.items(), 1): print(f" {rang}. {feat:<35} : SHAP moyen = {val:.4f}") print("\n=> Interprétation : une valeur SHAP élevée signifie que") print(" la variable pousse fortement la prédiction vers 'zone à risque'.")
Corrections · Semaine 4 — Flask · joblib · API REST
Deux fichiers : (1) entraîner & sauvegarder le modèle RF avec joblib, (2) API Flask complète avec /health · /api/predict · /api/batch_predict + tests cURL.
📄 Fichier 1 — Entraînement & sauvegarde du modèle
# Fichier : sauvegarde_modele.py # Entraîner et sauvegarder le modèle Random Forest (Étape 1) import pandas as pd, numpy as np from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import roc_auc_score, classification_report import joblib, os np.random.seed(42) n_normal, n_fraud = 4750, 250 normal = pd.DataFrame({ 'montant_ksh' : np.random.lognormal(7, 1.5, n_normal), 'heure' : np.random.randint(6, 22, n_normal), 'nb_transactions_jour': np.random.randint(1, 5, n_normal), 'anciennete_jours' : np.random.randint(30, 1000, n_normal), 'localisation_change' : np.random.choice([0, 1], n_normal, p=[0.9, 0.1]), 'fraude' : np.zeros(n_normal, dtype=int), }) fraud = pd.DataFrame({ 'montant_ksh' : np.random.lognormal(9, 1, n_fraud), 'heure' : np.random.choice(list(range(0, 6)) + list(range(22, 24)), n_fraud), 'nb_transactions_jour': np.random.randint(5, 20, n_fraud), 'anciennete_jours' : np.random.randint(1, 30, n_fraud), 'localisation_change' : np.random.choice([0, 1], n_fraud, p=[0.3, 0.7]), 'fraude' : np.ones(n_fraud, dtype=int), }) df = pd.concat([normal, fraud]).sample(frac=1, random_state=42).reset_index(drop=True) X = df.drop('fraude', axis=1) y = df['fraude'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42) model = RandomForestClassifier( n_estimators=100, class_weight='balanced', random_state=42, n_jobs=-1) model.fit(X_train, y_train) y_pred = model.predict(X_test) y_proba = model.predict_proba(X_test)[:, 1] print(classification_report(y_test, y_pred, target_names=['Normal', 'Fraude'])) print(f"ROC-AUC : {roc_auc_score(y_test, y_proba):.4f}") # Sauvegarde os.makedirs('model', exist_ok=True) metadata = { 'features' : list(X.columns), 'optimal_threshold' : 0.5, 'roc_auc' : roc_auc_score(y_test, y_proba), 'model_name' : 'Random Forest — Fraude M-Pesa', } joblib.dump(model, 'model/random_forest_fraude_mpesa.pkl') joblib.dump(metadata, 'model/model_metadata.pkl') print("✅ Modèle sauvegardé dans model/")
📄 Fichier 2 — Application Flask (API REST)
# Fichier : app.py # API Flask avec /health + /api/predict + /api/batch_predict (Étapes 2/3/4) from flask import Flask, request, jsonify from flask_cors import CORS import joblib, pandas as pd, numpy as np from datetime import datetime app = Flask(__name__) CORS(app) model = joblib.load('model/random_forest_fraude_mpesa.pkl') metadata = joblib.load('model/model_metadata.pkl') FEATURES = metadata['features'] THRESHOLD = metadata['optimal_threshold'] # ── 4. Endpoint /health ───────────────────────────────────── @app.route('/health', methods=['GET']) def health(): return jsonify({ 'status' : 'healthy', 'model_loaded': model is not None, 'features' : FEATURES, 'roc_auc' : round(metadata['roc_auc'], 4), 'timestamp' : datetime.now().isoformat(), }) # ── 2. Endpoint /api/predict (unitaire) ───────────────────── @app.route('/api/predict', methods=['POST']) def predict(): try: data = request.get_json() df_in = pd.DataFrame([data])[FEATURES] proba = float(model.predict_proba(df_in)[:, 1][0]) pred = int(proba >= THRESHOLD) niveau = 'ÉLEVÉ' if proba >= 0.70 else ('MOYEN' if proba >= 0.40 else 'FAIBLE') return jsonify({ 'decision' : 'BLOQUÉE' if pred else 'AUTORISÉE', 'probabilite_fraude': round(proba, 4), 'niveau_risque' : niveau, 'seuil_utilise' : THRESHOLD, 'timestamp' : datetime.now().isoformat(), }) except KeyError as e: return jsonify({'error': f'Feature manquante : {e}'}), 400 except Exception as e: return jsonify({'error': str(e)}), 500 # ── 3. Endpoint /api/batch_predict (en lot) ────────────────── @app.route('/api/batch_predict', methods=['POST']) def batch_predict(): try: data = request.get_json() transactions = data.get('transactions', []) if not transactions: return jsonify({'error': 'Aucune transaction fournie'}), 400 df_in = pd.DataFrame(transactions)[FEATURES] probas = model.predict_proba(df_in)[:, 1] results = [] for i, p in enumerate(probas): results.append({ 'index' : i, 'decision' : 'BLOQUÉE' if p >= THRESHOLD else 'AUTORISÉE', 'probabilite_fraude' : round(float(p), 4), 'niveau_risque' : 'ÉLEVÉ' if p >= 0.70 else ('MOYEN' if p >= 0.40 else 'FAIBLE'), }) nb_fraudes = sum(1 for r in results if r['decision'] == 'BLOQUÉE') return jsonify({ 'total' : len(results), 'fraudes_detectees' : nb_fraudes, 'taux_fraude_pct' : round(nb_fraudes / len(results) * 100, 2), 'resultats' : results, }) except Exception as e: return jsonify({'error': str(e)}), 500 if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000) # ── 5. Tests cURL ──────────────────────────────────────────── # Test santé: # curl http://localhost:5000/health # # Prédiction unitaire: # curl -X POST http://localhost:5000/api/predict \ # -H "Content-Type: application/json" \ # -d '{"montant_ksh":85000,"heure":3,"nb_transactions_jour":12,"anciennete_jours":5,"localisation_change":1}' # # Lot de transactions: # curl -X POST http://localhost:5000/api/batch_predict \ # -H "Content-Type: application/json" \ # -d '{"transactions":[ # {"montant_ksh":1200,"heure":14,"nb_transactions_jour":2,"anciennete_jours":400,"localisation_change":0}, # {"montant_ksh":92000,"heure":2,"nb_transactions_jour":15,"anciennete_jours":3,"localisation_change":1} # ]}' # # Résultat attendu transaction 1 : AUTORISÉE (risque FAIBLE) # Résultat attendu transaction 2 : BLOQUÉE (risque ÉLEVÉ)