🎓 Club D.I.A.M

Programme Machine Learning Complet — 6 Semaines

Supervisé · Non Supervisé · Semi-Supervisé | Focus Afrique 🌍

📚 À propos du programme

Bienvenue dans ce programme intensif de Machine Learning couvrant l'intégralité du spectre du ML — apprentissage supervisé, non supervisé et semi-supervisé — à travers des cas d'usage réels africains.

🎯 Objectifs : Maîtriser la régression, la classification, le clustering, les techniques semi-supervisées, l'interprétabilité des modèles et leur déploiement en production.

🗺️ Parcours :

SemaineThèmeType d'apprentissage
1Régression & RégularisationSupervisé
2Classification ClassiqueSupervisé
3Méthodes EnsemblistesSupervisé
4Boosting & InterprétabilitéSupervisé
5Clustering & Semi-SuperviséNon Supervisé Semi-Supervisé
6Déploiement & Projet FinalMLOps

🚀 Projets Africains du Programme

🌾 Projet 1 : Agriculture Intelligente

Prédiction des Rendements Agricoles au Sahel

Données météo, sol et agricoles pour prédire les rendements de mil et sorgho.

Régression

Données : Mali, Burkina Faso, Niger

💳 Projet 2 : Inclusion Financière

Prédiction de Risque Crédit Mobile Money

Modèle ML pour prédire le défaut de paiement des prêts mobile money.

Classification

Données : Kenya, Côte d'Ivoire, Sénégal

🏥 Projet 3 : Santé Publique

Détection Précoce du Paludisme

Classification des zones à risque basée sur des facteurs environnementaux.

Classification + SHAP

Données : RDC, Nigeria, Tanzanie

🛰️ Projet 4 : Télédétection Agricole

Cartographie des Cultures par Satellite

Segmentation et classification semi-supervisée de parcelles avec peu de labels terrain.

Clustering Semi-Sup.

Données : Sénégal, Côte d'Ivoire

📅 Semaine 1 — Fondamentaux de la Régression Supervisé

🎯 Objectifs de la semaine

  • Comprendre la régression linéaire et logistique
  • Maîtriser la régularisation (Ridge, Lasso)
  • Évaluer les performances avec des métriques appropriées
  • Appliquer à un cas réel africain

📖 Cours — Régression Linéaire

📝 Équation : y = β₀ + β₁x₁ + β₂x₂ + ... + βₙxₙ + ε
Où β sont les coefficients et ε l'erreur résiduelle.
📖
Cours — Régression Linéaire : prédiction du prix du cacao (Afrique)
regression_lineaire.py
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
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

# Dataset simulé — Prix du Cacao (Côte d'Ivoire)
np.random.seed(42)
data = {
    'annee'             : range(2010, 2024),
    'production_tonnes' : [1200000 + i*50000 + np.random.randint(-100000, 100000) for i in range(14)],
    'pluviometrie_mm'   : [1200 + np.random.randint(-200, 300) for _ in range(14)],
    'temperature'       : [26 + np.random.uniform(-1, 2) for _ in range(14)],
    'superficie_ha'     : [3000000 + i*100000 for i in range(14)],
    'prix_fcfa_kg'      : [1200 + i*80 + np.random.randint(-100, 150) for i in range(14)]
}
df = pd.DataFrame(data)

plt.figure(figsize=(10, 8))
sns.heatmap(df.corr(), annot=True, cmap='coolwarm', center=0)
plt.title('Corrélations — Prix du Cacao'); plt.tight_layout(); plt.show()

X = df[['production_tonnes', 'pluviometrie_mm', 'temperature', 'superficie_ha']]
y = df['prix_fcfa_kg']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

print(f"RMSE : {np.sqrt(mean_squared_error(y_test, y_pred)):.2f} FCFA/kg")
print(f"MAE  : {mean_absolute_error(y_test, y_pred):.2f} FCFA/kg")
print(f"R²   : {r2_score(y_test, y_pred):.4f}")
📖
Cours — Régression Logistique : prédiction d'accès à l'électricité
regression_logistique.py
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score

np.random.seed(42)
n = 1000
data_elec = {
    'zone_urbaine'        : np.random.choice([0, 1], n, p=[0.6, 0.4]),
    'revenu_mensuel'      : np.random.randint(20000, 500000, n),
    'distance_reseau_km'  : np.random.uniform(0, 50, n),
    'taille_menage'       : np.random.randint(1, 12, n),
    'niveau_education'    : np.random.randint(0, 5, n),
}
df_elec = pd.DataFrame(data_elec)

df_elec['acces'] = (
    (df_elec['zone_urbaine'] * 0.4) +
    (df_elec['revenu_mensuel'] / 1000000) +
    ((50 - df_elec['distance_reseau_km']) / 50 * 0.3) +
    (df_elec['niveau_education'] / 20) +
    np.random.uniform(-0.2, 0.2, n)
) > 0.6
df_elec['acces'] = df_elec['acces'].astype(int)

X = df_elec.drop('acces', axis=1); y = df_elec['acces']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)

log_model = LogisticRegression(max_iter=1000, random_state=42)
log_model.fit(X_train, y_train)

y_pred       = log_model.predict(X_test)
y_pred_proba = log_model.predict_proba(X_test)[:, 1]

print(classification_report(y_test, y_pred, target_names=["Pas d'accès", 'Accès']))
print(f"ROC-AUC : {roc_auc_score(y_test, y_pred_proba):.4f}")
📖
Cours — Régularisation Ridge & Lasso : lutter contre le surapprentissage
🎯 Pourquoi régulariser ?
  • Ridge (L2) : pénalise les gros coefficients, les réduit sans les annuler → garde toutes les features
  • Lasso (L1) : peut ramener des coefficients exactement à 0 → sélection automatique de variables
  • Utile quand on a beaucoup de features corrélées ou peu de données
ridge_lasso.py
from sklearn.linear_model import Ridge, Lasso, RidgeCV, LassoCV
from sklearn.preprocessing import StandardScaler

# Normalisation obligatoire avant régularisation
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s  = scaler.transform(X_test)

# Comparaison Linéaire / Ridge / Lasso
for alpha in [0.01, 0.1, 1.0, 10.0]:
    ridge = Ridge(alpha=alpha).fit(X_train_s, y_train)
    lasso = Lasso(alpha=alpha).fit(X_train_s, y_train)
    print(f"alpha={alpha} | Ridge R²={ridge.score(X_test_s,y_test):.3f} | "
          f"Lasso R²={lasso.score(X_test_s,y_test):.3f} | coefs non-nuls Lasso={np.sum(lasso.coef_!=0)}")

# Recherche automatique du meilleur alpha (cross-validation intégrée)
ridge_cv = RidgeCV(alphas=np.logspace(-3, 3, 50), cv=5)
ridge_cv.fit(X_train_s, y_train)
print(f"Meilleur alpha Ridge : {ridge_cv.alpha_:.4f}")

lasso_cv = LassoCV(alphas=np.logspace(-3, 3, 50), cv=5, random_state=42)
lasso_cv.fit(X_train_s, y_train)
print(f"Meilleur alpha Lasso : {lasso_cv.alpha_:.4f}")
print(f"Features sélectionnées : {X_train.columns[lasso_cv.coef_ != 0].tolist()}")
Exercice 1.1 ✏️ Régression Linéaire — Prix du Maïs au Kenya 🌽

Contexte : Vous travaillez pour une coopérative agricole au Kenya. Prédisez le prix du maïs.

  1. Créer un dataset de 200 observations : production, pluviométrie, prix carburant, demande export, prix maïs (cible)
  2. Effectuer une EDA complète
  3. Entraîner un modèle de régression linéaire
  4. Évaluer avec MSE, RMSE, MAE et R²
  5. Visualiser prédictions vs réalité + analyse des résidus
solution_ex1_1.py
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

np.random.seed(123); n = 200
data_mais = {
    '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_mais = pd.DataFrame(data_mais)
df_mais['prix_mais_ksh_kg'] = (
    30 - 0.0001 * df_mais['production_tonnes']
       + 0.01   * df_mais['pluviometrie_mm']
       + 0.2    * df_mais['prix_carburant_ksh']
       + 0.0003 * df_mais['demande_export_tonnes']
       + np.random.normal(0, 3, n)
)

X = df_mais.drop('prix_mais_ksh_kg', axis=1); y = df_mais['prix_mais_ksh_kg']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

print(f"RMSE : {np.sqrt(mean_squared_error(y_test, y_pred)):.2f}")
print(f"MAE  : {mean_absolute_error(y_test, y_pred):.2f}")
print(f"R²   : {r2_score(y_test, y_pred):.4f}")
Exercice 1.2 ✏️ Régression Logistique — Abandon Scolaire au Nigeria 🎓

Contexte : Prédire le risque d'abandon scolaire dans les écoles secondaires au Nigeria.

  1. Créer un dataset de 500 élèves : distance école, revenu familial, nb absences, note moyenne, genre, abandon (0/1)
  2. Entraîner une régression logistique
  3. Calculer accuracy, precision, recall, F1-score
  4. Afficher la matrice de confusion
  5. Tracer la courbe ROC et calculer le ROC-AUC
Exercice 1.3 ✏️ Ridge vs Lasso — Prédiction du prix du coton (Burkina Faso) 🧵

Contexte : Vous disposez de 15 variables potentiellement corrélées pour prédire le prix du coton.

  1. Créer un dataset avec 15 features (dont certaines redondantes/corrélées)
  2. Comparer Régression Linéaire, Ridge et Lasso
  3. Utiliser RidgeCV et LassoCV pour trouver l'alpha optimal
  4. Identifier les features sélectionnées par Lasso
  5. Conclure sur le modèle le plus adapté

🎨 Mini-Projet — Prédiction Rendement Agricole (Mali)

📋 Contexte

Vous travaillez pour le Ministère de l'Agriculture du Mali. Mission : estimer les rendements de mil dans différentes régions.

📊 Variables

  • region : Kayes, Koulikoro, Sikasso, Ségou, Mopti…
  • pluviometrie_mm, temperature_moy, superficie_ha, engrais_kg_ha
  • semences_ameliorees (0/1), irrigation (0/1)
  • rendement_kg_ha ← CIBLE
📖
Code Complet — Mini-Projet Rendement Agricole Mali
projet_rendement_mali.py
import pandas as pd, numpy as np, matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.metrics import mean_squared_error, r2_score

np.random.seed(42)
regions   = ['Kayes', 'Koulikoro', 'Sikasso', 'Ségou', 'Mopti', 'Tombouctou']
n         = 300

df = pd.DataFrame({
    'region'              : np.random.choice(regions, n),
    'pluviometrie_mm'     : np.random.randint(300, 1200, n),
    'temperature'         : np.random.uniform(26, 35, n),
    'superficie_ha'       : np.random.randint(1, 100, n),
    'engrais_kg_ha'       : np.random.randint(0, 200, n),
    'semences_ameliorees' : np.random.choice([0, 1], n, p=[0.4, 0.6]),
    'irrigation'          : np.random.choice([0, 1], n, p=[0.7, 0.3]),
})
df['rendement_kg_ha'] = (
    500
    + df['pluviometrie_mm'] * 0.8
    - 30 * (df['temperature'] - 28)
    + df['engrais_kg_ha'] * 3
    + df['semences_ameliorees'] * 200
    + df['irrigation'] * 300
    + np.random.normal(0, 100, n)
).clip(lower=200)

df['region_enc'] = LabelEncoder().fit_transform(df['region'])
X = df.drop(['region', 'rendement_kg_ha'], axis=1)
y = df['rendement_kg_ha']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

scaler         = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled  = scaler.transform(X_test)

for name, model in {'Linéaire': LinearRegression(), 'Ridge': Ridge(), 'Lasso': Lasso()}.items():
    model.fit(X_train_scaled, y_train)
    y_pred = model.predict(X_test_scaled)
    cv     = cross_val_score(model, X_train_scaled, y_train, cv=5, scoring='r2')
    print(f"{name} → R²={r2_score(y_test, y_pred):.4f} | CV={cv.mean():.4f}±{cv.std():.4f}")

📝 Livrables

  1. Rapport d'analyse exploratoire
  2. Modèle entraîné et sauvegardé
  3. Visualisations des résultats
  4. Recommandations pour améliorer les rendements
📅 Semaine 2 — Classification Classique Supervisé

🎯 Objectifs de la semaine

  • Maîtriser les arbres de décision, KNN, SVM et Naive Bayes
  • Comprendre la validation croisée et les courbes d'apprentissage
  • Diagnostiquer overfitting et underfitting
  • Comparer plusieurs algorithmes sur un même problème

📖 Cours — Arbre de Décision

🌳 Principe : l'arbre découpe successivement l'espace des features par des règles simples (seuils) pour séparer les classes. C'est la brique de base de Random Forest (semaine 3).
📖
Cours — Decision Tree : diagnostic de malnutrition infantile (Éthiopie)
decision_tree.py
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
import matplotlib.pyplot as plt

np.random.seed(42); n = 800
df_nutri = pd.DataFrame({
    'age_mois'              : np.random.randint(6, 60, n),
    'poids_kg'               : np.random.uniform(4, 20, n),
    'taille_cm'              : np.random.uniform(60, 110, n),
    'perimetre_brachial_mm' : np.random.uniform(100, 160, n),
    'diversite_alimentaire' : np.random.randint(1, 8, n),
    'acces_eau_potable'     : np.random.choice([0, 1], n, p=[0.35, 0.65]),
})

df_nutri['imc_proxy'] = df_nutri['poids_kg'] / ((df_nutri['taille_cm']/100)**2)
df_nutri['malnutrition'] = (
    (df_nutri['imc_proxy'] < 14) |
    (df_nutri['perimetre_brachial_mm'] < 115) |
    (df_nutri['diversite_alimentaire'] < 3)
).astype(int)

X = df_nutri.drop(['malnutrition', 'imc_proxy'], axis=1)
y = df_nutri['malnutrition']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)

tree_model = DecisionTreeClassifier(max_depth=4, min_samples_leaf=20, random_state=42)
tree_model.fit(X_train, y_train)
y_pred = tree_model.predict(X_test)

print(classification_report(y_test, y_pred, target_names=['Sain', 'Malnutrition']))

plt.figure(figsize=(20, 10))
plot_tree(tree_model, feature_names=X.columns, class_names=['Sain', 'Malnutrition'],
          filled=True, rounded=True, fontsize=10)
plt.savefig('arbre_decision.png', dpi=300, bbox_inches='tight')

importances = pd.Series(tree_model.feature_importances_, index=X.columns).sort_values(ascending=False)
print(importances)
📖
Cours — KNN : classification de zones agroécologiques (Kenya)
⚠️ Important : KNN est sensible à l'échelle des variables → normalisation obligatoire.
knn.py
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import GridSearchCV

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled  = scaler.transform(X_test)

param_grid = {'n_neighbors': range(3, 21, 2), 'weights': ['uniform', 'distance']}
grid_knn = GridSearchCV(KNeighborsClassifier(), param_grid, cv=5, scoring='f1')
grid_knn.fit(X_train_scaled, y_train)

print(f"Meilleur k : {grid_knn.best_params_}")
best_knn = grid_knn.best_estimator_
y_pred_knn = best_knn.predict(X_test_scaled)
print(classification_report(y_test, y_pred_knn))

# Effet du choix de k sur la performance
scores = []
k_range = range(1, 31)
for k in k_range:
    knn = KNeighborsClassifier(n_neighbors=k)
    knn.fit(X_train_scaled, y_train)
    scores.append(knn.score(X_test_scaled, y_test))

plt.plot(k_range, scores, 'o-')
plt.xlabel('k'); plt.ylabel('Accuracy'); plt.title('Impact du choix de k')
plt.savefig('knn_k_effect.png', dpi=300)
📖
Cours — SVM : classification de la qualité du café (Éthiopie)
svm.py
from sklearn.svm import SVC

np.random.seed(42); n = 600
df_cafe = pd.DataFrame({
    'altitude_m'         : np.random.randint(1200, 2200, n),
    'acidite'            : np.random.uniform(5, 9, n),
    'taux_humidite'      : np.random.uniform(9, 14, n),
    'taille_grain_mm'    : np.random.uniform(5, 8, n),
})
df_cafe['qualite_export'] = (
    (df_cafe['altitude_m'] > 1500) &
    (df_cafe['acidite'] > 6.5) &
    (df_cafe['taux_humidite'] < 12)
).astype(int)

X = df_cafe.drop('qualite_export', axis=1); y = df_cafe['qualite_export']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)

for kernel in ['linear', 'rbf', 'poly']:
    svm = SVC(kernel=kernel, C=1.0, probability=True, random_state=42)
    svm.fit(X_train_s, y_train)
    score = svm.score(X_test_s, y_test)
    print(f"Kernel {kernel} → Accuracy: {score:.4f}")
📖
Cours — Naive Bayes : classification de SMS agricoles (Kenya) 📱
💡 Cas d'usage : classer des SMS entrants en "alerte météo utile" vs "spam" pour une plateforme d'info agricole.
naive_bayes.py
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics import classification_report

# Dataset simplifié de SMS (texte + label)
sms_data = [
    ("Alerte pluies fortes prevues demain region Kisumu", 'utile'),
    ("Gagnez 1000000 KSH cliquez ici maintenant", 'spam'),
    ("Prix engrais NPK en baisse cette semaine", 'utile'),
    ("Offre speciale credit rapide sans justificatif", 'spam'),
    ("Periode de semis optimale debute la semaine prochaine", 'utile'),
    # ... (dataset complet : plusieurs centaines d'exemples)
]
textes  = [x[0] for x in sms_data]
labels  = [x[1] for x in sms_data]

vectorizer = TfidfVectorizer(max_features=500)
X = vectorizer.fit_transform(textes)

X_train, X_test, y_train, y_test = train_test_split(X, labels, test_size=0.2, random_state=42)

nb_model = MultinomialNB()
nb_model.fit(X_train, y_train)
y_pred = nb_model.predict(X_test)

print(classification_report(y_test, y_pred))

# Prédiction sur un nouveau SMS
nouveau_sms = vectorizer.transform(["Alerte secheresse zone nord"])
print(f"Prédiction : {nb_model.predict(nouveau_sms)[0]}")
📖
Cours — Validation Croisée & Courbes d'Apprentissage (overfitting/underfitting)
🎯 Diagnostic :
  • Underfitting : score faible sur train ET test → modèle trop simple
  • Overfitting : score élevé sur train, faible sur test → modèle trop complexe / mémorise le bruit
validation_croisee.py
from sklearn.model_selection import cross_val_score, KFold, learning_curve

kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(tree_model, X, y, cv=kf, scoring='f1')
print(f"F1 par fold : {scores}")
print(f"F1 moyen : {scores.mean():.4f} (+/- {scores.std():.4f})")

train_sizes, train_scores, val_scores = learning_curve(
    DecisionTreeClassifier(max_depth=4, random_state=42),
    X, y, cv=5, train_sizes=np.linspace(0.1, 1.0, 10), scoring='f1'
)

plt.figure(figsize=(10, 6))
plt.plot(train_sizes, train_scores.mean(axis=1), 'o-', label='Score entraînement')
plt.plot(train_sizes, val_scores.mean(axis=1), 'o-', label='Score validation')
plt.xlabel("Taille du jeu d'entraînement"); plt.ylabel('F1-Score')
plt.title("Courbe d'apprentissage"); plt.legend(); plt.grid(True)
plt.savefig('learning_curve.png', dpi=300)
Exercice 2.1 ✏️ Comparaison multi-algorithmes — Accès aux soins (Burkina Faso) 🏥

Contexte : Comparer Decision Tree, KNN, SVM et Naive Bayes pour prédire l'accès aux soins primaires en zone rurale.

  1. Créer un dataset : distance centre santé, revenu, nb enfants, niveau éducation mère, saison, accès_soins (0/1)
  2. Entraîner les 4 algorithmes avec validation croisée 5-fold
  3. Comparer via un tableau récapitulatif (Accuracy, F1, temps d'entraînement)
  4. Tracer les courbes ROC des 4 modèles sur un même graphique
  5. Justifier le choix du meilleur modèle
Exercice 2.2 ✏️ Classification Qualité d'Eau au Rwanda 💧

Contexte : Classifier la potabilité de l'eau dans différentes sources au Rwanda.

  1. Créer un dataset avec pH, turbidité, chlore, coliformes, conductivité, température
  2. Entraîner un Decision Tree puis un SVM
  3. Comparer avec validation croisée
  4. Tracer une courbe d'apprentissage pour diagnostiquer le sur/sous-apprentissage

🎨 Mini-Projet — Churn Télécom au Ghana

📋 Description

Prédire le risque de désabonnement (churn) des clients d'un opérateur télécom au Ghana en comparant plusieurs algorithmes de classification classique.

📊 Variables

  • Durée d'abonnement (mois), Consommation data (GB), Montant facture (GHS)
  • Appels au service client, Type de forfait, Région, Churn (0/1)

📝 Livrables

  1. Comparaison Decision Tree / KNN / SVM / Naive Bayes
  2. Validation croisée et courbes d'apprentissage pour chaque modèle
  3. Sélection et justification du modèle final
📅 Semaine 3 — Méthodes Ensemblistes Supervisé

🎯 Objectifs de la semaine

  • Comprendre le principe des méthodes ensemblistes (bagging)
  • Maîtriser Random Forest
  • Gérer les déséquilibres de classes avec SMOTE
  • Optimiser les hyperparamètres (Grid/Random Search)

📖 Cours — Random Forest

🌳 Principe du Bagging : Random Forest entraîne de nombreux arbres de décision sur des sous-échantillons aléatoires puis moyenne leurs prédictions → réduit la variance et l'overfitting d'un arbre unique.
📖
Cours — Random Forest : détection de fraude M-Pesa (Kenya)
random_forest.py
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, roc_auc_score

np.random.seed(42); n_fraud = 250; n_normal = 4750

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)

rf = RandomForestClassifier(n_estimators=100, max_depth=10, class_weight='balanced', random_state=42, n_jobs=-1)
rf.fit(X_train, y_train)
y_pred = rf.predict(X_test)

print(classification_report(y_test, y_pred, target_names=['Normal', 'Fraude']))
print(f"ROC-AUC : {roc_auc_score(y_test, rf.predict_proba(X_test)[:,1]):.4f}")
📖
Cours — SMOTE & Hyperparameter Tuning (Grid Search / Random Search)
smote_gridsearch.py
from imblearn.over_sampling import SMOTE
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
from scipy.stats import randint

smote = SMOTE(random_state=42, k_neighbors=5)
X_train_sm, y_train_sm = smote.fit_resample(X_train, y_train)
print(f"Avant SMOTE : {np.bincount(y_train)}")
print(f"Après SMOTE : {np.bincount(y_train_sm)}")

param_grid = {
    'n_estimators'    : [50, 100, 200],
    'max_depth'       : [5, 10, 15, None],
    'min_samples_split': [2, 5, 10],
}
grid_search = GridSearchCV(
    RandomForestClassifier(random_state=42, n_jobs=-1),
    param_grid, cv=5, scoring='roc_auc', n_jobs=-1
)
grid_search.fit(X_train_sm, y_train_sm)
print(f"Meilleurs params : {grid_search.best_params_}")
print(f"Meilleur ROC-AUC  : {grid_search.best_score_:.4f}")

random_search = RandomizedSearchCV(
    RandomForestClassifier(random_state=42, n_jobs=-1),
    param_distributions={'n_estimators': randint(50, 300), 'max_depth': [5,10,15,20,None]},
    n_iter=30, cv=5, scoring='roc_auc', random_state=42, n_jobs=-1
)
random_search.fit(X_train_sm, y_train_sm)
print(f"Random Search meilleur score : {random_search.best_score_:.4f}")
Exercice 3.1 ✏️ Random Forest + SMOTE — Qualité d'Eau au Rwanda 💧

Contexte : Reprendre le dataset qualité d'eau (Semaine 2) avec Random Forest.

  1. Entraîner un Random Forest
  2. Appliquer SMOTE si la classe est déséquilibrée
  3. Optimiser avec Grid Search
  4. Afficher l'importance des features
  5. Comparer avec les résultats de la Semaine 2 (Decision Tree seul)

🎨 Mini-Projet — Segmentation Risque Assurance Agricole (Sénégal)

📋 Description

Prédire le risque de sinistre pour une assurance agricole indicielle basée sur la météo.

📊 Variables

  • Pluviométrie, indice de végétation (NDVI), historique sinistres, type de culture, superficie assurée, sinistre (0/1)

Consigne : comparer Random Forest optimisé (Grid Search + SMOTE) avec le Decision Tree simple de la semaine précédente.

📅 Semaine 4 — Boosting & Interprétabilité Supervisé

🎯 Objectifs de la semaine

  • Maîtriser XGBoost et LightGBM
  • Comprendre les métriques avancées (ROC-AUC, F1)
  • Interpréter les modèles avec SHAP
  • Construire un pipeline complet Scikit-learn

📖 Cours — XGBoost & LightGBM

🚀 Avantages du Boosting : performance supérieure, résistance au surapprentissage, gestion des valeurs manquantes, données déséquilibrées.
📖
Cours — XGBoost : prédiction de défaut crédit microfinance (Sénégal)
xgboost_credit.py
import xgboost as xgb
from sklearn.metrics import classification_report, roc_auc_score, f1_score

np.random.seed(42); n = 3000
df_credit = pd.DataFrame({
    'age'                 : np.random.randint(18, 70, n),
    'revenu_mensuel_fcfa'  : np.random.lognormal(11.5, 0.8, n),
    'montant_pret_fcfa'   : np.random.lognormal(12.5, 0.7, n),
    'score_historique'    : np.random.uniform(0, 100, n),
    'nb_personnes_charge' : np.random.randint(0, 8, n),
    'epargne_fcfa'        : np.random.lognormal(10, 1, n),
    'zone_rurale'         : np.random.choice([0,1], n, p=[0.4,0.6]),
    'nb_prets_anterieurs' : np.random.randint(0, 5, n),
})

ratio = df_credit['montant_pret_fcfa'] / df_credit['revenu_mensuel_fcfa']
df_credit['defaut'] = (ratio/10 + (100-df_credit['score_historique'])/200 + df_credit['zone_rurale']*0.1 + np.random.uniform(-0.1,0.1,n) > 0.3).astype(int)

X = df_credit.drop('defaut', axis=1); y = df_credit['defaut']
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()
xgb_model = xgb.XGBClassifier(n_estimators=100, max_depth=6, learning_rate=0.1,
                                 scale_pos_weight=scale_w, random_state=42, eval_metric='logloss')
xgb_model.fit(X_train, y_train)

y_pred       = xgb_model.predict(X_test)
y_pred_proba = xgb_model.predict_proba(X_test)[:, 1]

print(classification_report(y_test, y_pred, target_names=['Pas Défaut', 'Défaut']))
print(f"F1  : {f1_score(y_test, y_pred):.4f}")
print(f"AUC : {roc_auc_score(y_test, y_pred_proba):.4f}")
📖
Cours — LightGBM & comparaison XGBoost vs LightGBM
lightgbm_compare.py
import lightgbm as lgb

lgb_train = lgb.Dataset(X_train, label=y_train)
lgb_test  = lgb.Dataset(X_test,  label=y_test, reference=lgb_train)

params = {
    'objective'       : 'binary',
    'metric'          : 'auc',
    'boosting_type'   : 'gbdt',
    'num_leaves'      : 31,
    'learning_rate'   : 0.05,
    'feature_fraction': 0.8,
    'is_unbalance'    : True,
    'verbose'         : -1,
}

lgb_model = lgb.train(params, lgb_train, num_boost_round=100,
                      valid_sets=[lgb_train, lgb_test],
                      callbacks=[lgb.early_stopping(10)])

y_proba_lgb = lgb_model.predict(X_test, num_iteration=lgb_model.best_iteration)
y_pred_lgb  = (y_proba_lgb >= 0.5).astype(int)

print(f"XGBoost  AUC : {roc_auc_score(y_test, y_pred_proba):.4f}")
print(f"LightGBM AUC : {roc_auc_score(y_test, y_proba_lgb):.4f}")

📖 Cours — SHAP & Interprétabilité

🎯 Pourquoi SHAP ? Comprendre pourquoi un client est à risque, identifier les facteurs de décision, assurer la transparence réglementaire.
📖
Cours — SHAP : summary plot, force plot, waterfall, pipelines Scikit-learn
shap_pipeline.py
import shap
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

explainer   = shap.TreeExplainer(xgb_model)
shap_values = explainer.shap_values(X_test)

shap.summary_plot(shap_values, X_test, show=False)
plt.savefig('shap_summary.png', dpi=300, bbox_inches='tight')

shap.summary_plot(shap_values, X_test, plot_type='bar', show=False)
plt.savefig('shap_importance.png', dpi=300)

idx = np.where((y_pred==1) & (y_test==1))[0][0]
shap.force_plot(explainer.expected_value, shap_values[idx],
               X_test.iloc[idx], matplotlib=True, show=False)
plt.savefig(f'shap_force_{idx}.png', dpi=300)

pipeline = Pipeline([
    ('scaler'    , StandardScaler()),
    ('classifier', xgb.XGBClassifier(n_estimators=100, random_state=42, eval_metric='logloss'))
])
pipeline.fit(X_train, y_train)
print(f"Pipeline AUC : {roc_auc_score(y_test, pipeline.predict_proba(X_test)[:,1]):.4f}")
Exercice 4.1 ✏️ XGBoost + SHAP — Détection Paludisme (Tanzanie) 🏥

Contexte : Classifier les zones à risque de paludisme selon des facteurs environnementaux et démographiques.

  1. Créer un dataset : température, humidité, altitude, densité population, accès eau, couverture moustiquaires, zone à risque (0/1)
  2. Entraîner un modèle XGBoost
  3. Évaluer avec ROC-AUC, F1, precision, recall
  4. Générer un SHAP summary plot
  5. Interpréter les 3 facteurs les plus influents

🚀 Projet Principal — Risque Crédit Mobile Money (Kenya, Côte d'Ivoire, Sénégal)

📋 Description

Client : Opérateur Mobile Money multi-pays. Problématique : réduire le taux de défaut de 15 % à 8 %.

🎯 Objectifs Business

  • Identifier les clients à risque avant l'octroi du crédit
  • Optimiser les montants et durées par profil
  • Maintenir un taux d'approbation > 60 %
📖
Code Complet — Pipeline Mobile Money (10 000 clients, XGBoost, SHAP)
projet_mobile_money.py
import pandas as pd, numpy as np, xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, roc_auc_score, f1_score
import shap, joblib

np.random.seed(42); n = 10000
pays = np.random.choice(['Kenya','Cote_Ivoire','Senegal'], n, p=[0.4,0.35,0.25])

df = pd.DataFrame({
    'anciennete_jours'      : np.random.randint(30, 1095, n),
    'nb_transactions_3mois'  : np.random.randint(5, 200, n),
    'solde_moyen'            : np.random.lognormal(9, 1.5, n),
    'montant_demande'        : np.random.lognormal(10, 0.8, n),
    'nb_retards_paiement'    : np.random.randint(0, 5, n),
    'score_comportement'     : np.random.uniform(0, 100, n),
    'utilise_epargne'        : np.random.choice([0,1], n, p=[0.6,0.4]),
    'zone_urbaine'           : np.random.choice([0,1], n, p=[0.45,0.55]),
})

df['ratio_pret_solde'] = df['montant_demande'] / (df['solde_moyen'] + 1)
df['anciennete_annees'] = df['anciennete_jours'] / 365
df = pd.get_dummies(df.assign(pays=pays), columns=['pays'], drop_first=True)

prob = (0.3 + df['ratio_pret_solde']*0.15 + df['nb_retards_paiement']/20
        - df['anciennete_annees']*0.1 - df['utilise_epargne']*0.1
        + np.random.uniform(-0.15,0.15,n))
df['defaut'] = (prob > 0.35).astype(int)

X = df.drop('defaut', axis=1); y = df['defaut']
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)

model = xgb.XGBClassifier(n_estimators=200, max_depth=6, learning_rate=0.05,
                             scale_pos_weight=(y_tr==0).sum()/(y_tr==1).sum(),
                             random_state=42, eval_metric='logloss')
model.fit(X_tr, y_tr)
y_p = model.predict(X_te)
y_pp = model.predict_proba(X_te)[:,1]
print(f"AUC={roc_auc_score(y_te,y_pp):.4f}  F1={f1_score(y_te,y_p):.4f}")

exp = shap.TreeExplainer(model); sv = exp.shap_values(X_te)
shap.summary_plot(sv, X_te, show=False); plt.savefig('shap_mm.png', dpi=300)

joblib.dump(model, 'xgb_mobile_money.pkl')
print("✅ Modèle sauvegardé")
📅 Semaine 5 — Clustering & Semi-Supervisé Non Supervisé Semi-Supervisé

🎯 Objectifs de la semaine

  • Comprendre les différences entre supervisé, non supervisé et semi-supervisé
  • Maîtriser K-Means, CAH et DBSCAN
  • Utiliser PCA pour la réduction de dimension
  • Appliquer le Self-Training et le Label Propagation quand peu de données sont labellisées
🧭 Différence clé :
  • Non supervisé : aucune donnée labellisée, on cherche des structures cachées (clusters)
  • Semi-supervisé : peu de données labellisées + beaucoup de données non labellisées → on exploite les deux
  • Cas d'usage terrain : très fréquent en Afrique où l'annotation manuelle (agents de terrain, experts) est coûteuse et rare
🔵 Partie A — Apprentissage Non Supervisé

📖 Cours — K-Means

📖
Cours — K-Means : segmentation clients Mobile Money (Ouganda)
kmeans.py
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score

np.random.seed(42); n = 2000
df_clients = pd.DataFrame({
    'nb_transactions_mois'  : np.random.gamma(5, 4, n),
    'montant_moyen_ugx'     : np.random.lognormal(9, 1.2, n),
    'anciennete_mois'       : np.random.randint(1, 60, n),
    'diversite_services'    : np.random.randint(1, 8, n),
    'taux_epargne'          : np.random.uniform(0, 0.5, n),
})

scaler = StandardScaler()
X_scaled = scaler.fit_transform(df_clients)

# Méthode du coude + silhouette pour choisir k
inertias, silhouettes = [], []
K_range = range(2, 10)
for k in K_range:
    km = KMeans(n_clusters=k, random_state=42, n_init=10)
    km.fit(X_scaled)
    inertias.append(km.inertia_)
    silhouettes.append(silhouette_score(X_scaled, km.labels_))

fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].plot(K_range, inertias, 'o-'); axes[0].set_title('Méthode du coude')
axes[1].plot(K_range, silhouettes, 'o-', color='orange'); axes[1].set_title('Score de Silhouette')
plt.savefig('elbow_silhouette.png', dpi=300)

# Clustering final
kmeans_final = KMeans(n_clusters=4, random_state=42, n_init=10)
df_clients['segment'] = kmeans_final.fit_predict(X_scaled)

# Profilage business des segments — ÉTAPE ESSENTIELLE
profil_segments = df_clients.groupby('segment').mean()
print(profil_segments)

noms_segments = {
    0: "Petits utilisateurs occasionnels",
    1: "Power users fidèles",
    2: "Nouveaux clients à fort potentiel",
    3: "Clients dormants à risque de churn"
}
df_clients['segment_nom'] = df_clients['segment'].map(noms_segments)
📖
Cours — Clustering Hiérarchique (CAH) : profils agricoles (Mali)
cah.py
from scipy.cluster.hierarchy import dendrogram, linkage
from sklearn.cluster import AgglomerativeClustering

np.random.seed(42); n = 50
df_communes = pd.DataFrame({
    'pluviometrie_moy'      : np.random.uniform(300, 1200, n),
    'superficie_cultivee'   : np.random.uniform(500, 5000, n),
    'rendement_moyen'       : np.random.uniform(500, 2500, n),
    'taux_irrigation'       : np.random.uniform(0, 0.6, n),
})

scaler = StandardScaler()
X_scaled = scaler.fit_transform(df_communes)

linkage_matrix = linkage(X_scaled, method='ward')
plt.figure(figsize=(14, 6))
dendrogram(linkage_matrix)
plt.title('Dendrogramme — Communes agricoles du Mali')
plt.savefig('dendrogramme.png', dpi=300)

agglo = AgglomerativeClustering(n_clusters=3, linkage='ward')
df_communes['cluster'] = agglo.fit_predict(X_scaled)
print(df_communes.groupby('cluster').mean())
📖
Cours — DBSCAN : détection de foyers épidémiques (RDC)
💡 Pourquoi DBSCAN ici ? Contrairement à K-Means, DBSCAN ne nécessite pas de fixer k à l'avance, détecte des formes irrégulières et identifie automatiquement les points isolés (bruit).
dbscan.py
from sklearn.cluster import DBSCAN

np.random.seed(42); n = 500
df_cas = pd.DataFrame({
    'latitude'   : np.random.uniform(-4, -2, n),
    'longitude'  : np.random.uniform(15, 17, n),
})
foyer1 = pd.DataFrame({'latitude': np.random.normal(-3, 0.05, 80), 'longitude': np.random.normal(15.5, 0.05, 80)})
foyer2 = pd.DataFrame({'latitude': np.random.normal(-2.5, 0.05, 60), 'longitude': np.random.normal(16.2, 0.05, 60)})
df_cas = pd.concat([df_cas, foyer1, foyer2], ignore_index=True)

dbscan = DBSCAN(eps=0.1, min_samples=10)
df_cas['cluster'] = dbscan.fit_predict(df_cas[['latitude', 'longitude']])

n_foyers = len(set(df_cas['cluster'])) - (1 if -1 in df_cas['cluster'].values else 0)
print(f"Nombre de foyers détectés : {n_foyers}")
print(f"Cas isolés (bruit) : {(df_cas['cluster'] == -1).sum()}")

plt.figure(figsize=(10, 8))
plt.scatter(df_cas['longitude'], df_cas['latitude'], c=df_cas['cluster'], cmap='viridis', s=20)
plt.title('Foyers épidémiques détectés (DBSCAN)')
plt.savefig('dbscan_foyers.png', dpi=300)
📖
Cours — PCA : réduction de dimension avant clustering
pca.py
from sklearn.decomposition import PCA

pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)

print(f"Variance expliquée : {pca.explained_variance_ratio_}")
print(f"Variance cumulée : {sum(pca.explained_variance_ratio_):.2%}")

plt.figure(figsize=(10, 8))
scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=df_clients['segment'], cmap='viridis')
plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.1%})')
plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.1%})')
plt.title('Segments clients visualisés après PCA')
plt.colorbar(scatter)
plt.savefig('pca_clusters.png', dpi=300)
Exercice 5.1 ✏️ Segmentation Exploitations Agricoles (Tanzanie) 🚜

Contexte : Segmenter les exploitations agricoles pour cibler des programmes d'aide différenciés.

  1. Créer un dataset : superficie, rendement, accès crédit, usage intrants, niveau mécanisation (10+ features)
  2. Appliquer PCA pour réduire la dimension
  3. Déterminer le k optimal (coude + silhouette)
  4. Appliquer K-Means puis comparer avec CAH
  5. Profiler chaque segment et proposer une stratégie d'intervention adaptée
🟠 Partie B — Apprentissage Semi-Supervisé

📖 Cours — Semi-Supervisé

🎯 Contexte terrain africain : Vous avez des milliers d'images satellite ou de relevés terrain, mais seulement quelques centaines ont été annotées par des experts (coût, temps). Le semi-supervisé exploite intelligemment les données non labellisées pour améliorer le modèle.
📖
Cours — Self-Training : classification de cultures par satellite (Sénégal) 🛰️
🔄 Principe du Self-Training :
  1. Entraîner un modèle sur les quelques données labellisées disponibles
  2. Prédire les labels des données non labellisées
  3. Ajouter au jeu d'entraînement les prédictions les plus confiantes (pseudo-labels)
  4. Répéter jusqu'à convergence
self_training.py
from sklearn.semi_supervised import SelfTrainingClassifier
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# Dataset : indices spectraux satellite (NDVI, NDWI, etc.) par parcelle
np.random.seed(42); n = 3000
df_cultures = pd.DataFrame({
    'ndvi_moyen'       : np.random.uniform(0.2, 0.9, n),
    'ndwi_moyen'       : np.random.uniform(-0.3, 0.5, n),
    'texture_ir'       : np.random.uniform(0, 1, n),
    'humidite_sol'     : np.random.uniform(10, 40, n),
})
# 0 = mil, 1 = arachide, 2 = riz
conditions = [
    (df_cultures['ndvi_moyen'] < 0.45),
    (df_cultures['ndvi_moyen'] >= 0.45) & (df_cultures['ndwi_moyen'] < 0.1),
]
df_cultures['culture'] = np.select(conditions, [0, 1], default=2)

X = df_cultures.drop('culture', axis=1).values
y_true = df_cultures['culture'].values

# Simuler seulement 5% de labels disponibles (réalité terrain)
rng = np.random.RandomState(42)
y_semi = np.copy(y_true)
mask_unlabeled = rng.rand(len(y_true)) > 0.05
y_semi[mask_unlabeled] = -1  # -1 = non labellisé (convention sklearn)
print(f"Données labellisées : {(y_semi != -1).sum()} / {len(y_semi)}")

X_train, X_test, y_train_semi, y_test = train_test_split(X, y_semi, test_size=0.2, random_state=42)
_, _, y_train_true, _ = train_test_split(X, y_true, test_size=0.2, random_state=42)

# Self-Training avec SVM comme estimateur de base
base_model = SVC(probability=True, kernel='rbf', random_state=42)
self_training = SelfTrainingClassifier(base_model, threshold=0.8, verbose=True)
self_training.fit(X_train, y_train_semi)

# Comparaison avec un modèle entraîné uniquement sur les données labellisées
mask_labeled = y_train_semi != -1
baseline = SVC(probability=True, kernel='rbf', random_state=42)
baseline.fit(X_train[mask_labeled], y_train_semi[mask_labeled])

y_test_true = y_train_true  # pour l'exemple, en pratique garder un vrai jeu de test labellisé
print("── Baseline (5% labels seulement) ──")
print(classification_report(y_test, baseline.predict(X_test)))
print("── Self-Training (5% labels + pseudo-labeling) ──")
print(classification_report(y_test, self_training.predict(X_test)))
📖
Cours — Label Propagation : détection maladie du cacao (Côte d'Ivoire) 🍫
🔗 Principe : Label Propagation construit un graphe de similarité entre tous les points et propage les labels connus vers les points non labellisés proches dans ce graphe.
label_propagation.py
from sklearn.semi_supervised import LabelPropagation, LabelSpreading
from sklearn.preprocessing import StandardScaler

# Dataset : détection de la pourriture brune du cacao via photos terrain (features extraites)
np.random.seed(42); n = 1500
df_cacao = pd.DataFrame({
    'taux_brun_pct'     : np.random.uniform(0, 100, n),
    'texture_surface'   : np.random.uniform(0, 1, n),
    'humidite_ambiante' : np.random.uniform(60, 95, n),
    'age_cabosse_jours' : np.random.randint(10, 150, n),
})
df_cacao['malade'] = ((df_cacao['taux_brun_pct'] > 40) & (df_cacao['humidite_ambiante'] > 75)).astype(int)

X = StandardScaler().fit_transform(df_cacao.drop('malade', axis=1))
y_true = df_cacao['malade'].values

# Seulement 8% annoté par les agents terrain
rng = np.random.RandomState(42)
y_semi = np.copy(y_true)
mask_unlabeled = rng.rand(len(y_true)) > 0.08
y_semi[mask_unlabeled] = -1

# Label Spreading (version robuste au bruit de Label Propagation)
label_spread = LabelSpreading(kernel='knn', n_neighbors=7, alpha=0.2)
label_spread.fit(X, y_semi)

y_pred_propagated = label_spread.transduction_
accuracy_globale = (y_pred_propagated == y_true).mean()
print(f"Précision de propagation sur tout le dataset : {accuracy_globale:.4f}")
print(f"Labels initialement connus : {(y_semi != -1).sum()} → Labels finaux inférés : {len(y_true)}")
Exercice 5.2 ✏️ Semi-Supervisé — Maladie du Manioc (Nigeria) 🌿

Contexte : Une ONG dispose de 10 000 photos de feuilles de manioc mais seulement 300 ont été annotées par un phytopathologiste (sain/malade).

  1. Simuler un dataset avec features visuelles (couleur, texture, taches) et seulement 3% de labels
  2. Entraîner un modèle baseline (labels seuls) avec Random Forest
  3. Appliquer SelfTrainingClassifier et comparer les performances
  4. Appliquer LabelSpreading et comparer
  5. Conclure sur le gain apporté par le semi-supervisé avec si peu de labels

🎨 Mini-Projet — Cartographie des Cultures par Télédétection (Sénégal)

📋 Contexte

Une agence agricole dispose d'images satellite Sentinel-2 couvrant toute une région, mais seules quelques parcelles ont été vérifiées au sol (vérité terrain). Combinez clustering et semi-supervisé pour cartographier l'ensemble des cultures.

📊 Étapes attendues

  1. Réduire les indices spectraux avec PCA
  2. Effectuer un premier clustering K-Means exploratoire (sans labels) pour identifier les grandes zones homogènes
  3. Utiliser les quelques points de vérité terrain disponibles avec Self-Training pour affiner la classification par type de culture
  4. Comparer la carte obtenue par clustering pur vs semi-supervisé
  5. Estimer les superficies par culture sur toute la région

📝 Livrables

  1. Carte de clustering exploratoire
  2. Carte de classification semi-supervisée finale
  3. Rapport comparatif des deux approches
  4. Estimation des superficies par culture

🎨 Mini-Projet — Segmentation Campagne de Vaccination (Nigeria)

📋 Contexte

ONG de santé publique souhaitant optimiser une campagne de vaccination en segmentant la population selon le risque et l'accessibilité.

📊 Variables

  • Âge, distance centre santé, statut vaccinal antérieur, taille du foyer, zone (urbaine/rurale), niveau de sensibilisation

📝 Livrables

  1. Comparaison K-Means vs DBSCAN vs CAH
  2. Profilage des segments avec visualisations
  3. Recommandations de stratégie de communication par segment
  4. Estimation du nombre d'agents de terrain nécessaires par segment
📅 Semaine 6 — Déploiement & Projet Final MLOps

🎯 Objectifs de la semaine

  • Créer une API Flask pour le modèle
  • Déployer le modèle en production
  • Mettre en place le monitoring
  • Présenter le projet final

📖 Cours — API Flask pour ML

📖
Cours — Structure du projet & code Flask complet (predict + batch + health)
structure.txt
projet_api/
├── app.py                  # Application Flask principale
├── model/
│   ├── xgboost_model.pkl  # Modèle sauvegardé
│   └── metadata.pkl        # Métadonnées
├── requirements.txt
├── templates/
│   └── index.html
└── static/
app.py
from flask import Flask, request, jsonify, render_template
from flask_cors import CORS
import joblib, pandas as pd
from datetime import datetime

app      = Flask(__name__)
CORS(app)
model    = joblib.load('model/xgboost_credit_mobile_money.pkl')
metadata = joblib.load('model/model_metadata.pkl')

@app.route('/')
def home():
    return render_template('index.html')

@app.route('/api/predict', methods=['POST'])
def predict():
    try:
        data      = request.get_json()
        threshold = metadata['optimal_threshold']
        df_in     = pd.DataFrame([data])[metadata['features']]
        proba     = model.predict_proba(df_in)[:, 1][0]
        decision  = 'REFUSÉ' if proba >= threshold else 'APPROUVÉ'
        niveau    = 'ÉLEVÉ' if proba >= 0.7 else 'MOYEN' if proba >= 0.4 else 'FAIBLE'
        return jsonify({
            'decision'          : decision,
            'probabilite_defaut': float(proba),
            'niveau_risque'     : niveau,
            'timestamp'         : datetime.now().isoformat(),
        })
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/batch_predict', methods=['POST'])
def batch_predict():
    try:
        data   = request.get_json()['clients']
        df_in  = pd.DataFrame(data)[metadata['features']]
        probas = model.predict_proba(df_in)[:, 1]
        results = [{'index': i, 'probabilite_defaut': float(p)} for i, p in enumerate(probas)]
        return jsonify({'resultats': results, 'nb_traites': len(results)})
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/health', methods=['GET'])
def health():
    return jsonify({'status': 'healthy', 'model_loaded': model is not None})

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)
Exercice 6.1 ✏️ Déploiement — API Flask pour votre modèle de détection de fraude

Contexte : Mettre en production votre modèle Random Forest de détection de fraude M-Pesa.

  1. Sauvegarder le modèle avec joblib.dump()
  2. Créer un endpoint /api/predict
  3. Ajouter un endpoint /api/batch_predict
  4. Implémenter un /health check
  5. Tester avec curl ou Postman
💡 Test rapide : curl -X POST http://localhost:5000/api/predict -H "Content-Type: application/json" -d '{"montant_ksh": 5000, "heure": 14}'

🚀 Projet Final — Plateforme Complète de Prédiction

🎯 Objectif

Créer une plateforme complète intégrant au choix un volet supervisé, non supervisé ou semi-supervisé, avec API REST, interface web, monitoring et documentation.

  • ✅ API REST fonctionnelle
  • ✅ Interface web interactive
  • ✅ Monitoring des prédictions
  • ✅ Dashboard d'administration
  • ✅ Documentation API

📋 Options de Projet Final

OptionTypeExemple
ASuperviséScoring crédit ou détection fraude (XGBoost + SHAP + API)
BNon superviséSegmentation clients avec dashboard de profilage interactif
CSemi-superviséClassification avec peu de labels + pipeline d'annotation active

📋 Livrables Finaux

  1. Code source complet (GitHub)
  2. API déployée et testée
  3. Documentation technique
  4. Présentation PowerPoint
  5. Vidéo de démonstration (5 min)

🎓 Évaluation

CritèrePoints
Qualité du modèle ML (métrique adaptée au type de projet)25
API fonctionnelle et documentée20
Interface utilisateur15
Analyse business et recommandations15
Code propre et documenté10
Présentation orale15
TOTAL100