🚀 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
🎯 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
Où β sont les coefficients et ε l'erreur résiduelle.
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}")
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}")
- 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
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()}")
Contexte : Vous travaillez pour une coopérative agricole au Kenya. Prédisez le prix du maïs.
- Créer un dataset de 200 observations : production, pluviométrie, prix carburant, demande export, prix maïs (cible)
- Effectuer une EDA complète
- Entraîner un modèle de régression linéaire
- Évaluer avec MSE, RMSE, MAE et R²
- Visualiser prédictions vs réalité + analyse des 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 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}")
Contexte : Prédire le risque d'abandon scolaire dans les écoles secondaires au Nigeria.
- Créer un dataset de 500 élèves : distance école, revenu familial, nb absences, note moyenne, genre, abandon (0/1)
- Entraîner une régression logistique
- Calculer accuracy, precision, recall, F1-score
- Afficher la matrice de confusion
- Tracer la courbe ROC et calculer le ROC-AUC
Contexte : Vous disposez de 15 variables potentiellement corrélées pour prédire le prix du coton.
- Créer un dataset avec 15 features (dont certaines redondantes/corrélées)
- Comparer Régression Linéaire, Ridge et Lasso
- Utiliser RidgeCV et LassoCV pour trouver l'alpha optimal
- Identifier les features sélectionnées par Lasso
- 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
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
- Rapport d'analyse exploratoire
- Modèle entraîné et sauvegardé
- Visualisations des résultats
- Recommandations pour améliorer les rendements
🎯 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
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)
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)
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}")
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]}")
- 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
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)
Contexte : Comparer Decision Tree, KNN, SVM et Naive Bayes pour prédire l'accès aux soins primaires en zone rurale.
- Créer un dataset : distance centre santé, revenu, nb enfants, niveau éducation mère, saison, accès_soins (0/1)
- Entraîner les 4 algorithmes avec validation croisée 5-fold
- Comparer via un tableau récapitulatif (Accuracy, F1, temps d'entraînement)
- Tracer les courbes ROC des 4 modèles sur un même graphique
- Justifier le choix du meilleur modèle
Contexte : Classifier la potabilité de l'eau dans différentes sources au Rwanda.
- Créer un dataset avec pH, turbidité, chlore, coliformes, conductivité, température
- Entraîner un Decision Tree puis un SVM
- Comparer avec validation croisée
- 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
- Comparaison Decision Tree / KNN / SVM / Naive Bayes
- Validation croisée et courbes d'apprentissage pour chaque modèle
- Sélection et justification du modèle final
🎯 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
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}")
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}")
Contexte : Reprendre le dataset qualité d'eau (Semaine 2) avec Random Forest.
- Entraîner un Random Forest
- Appliquer SMOTE si la classe est déséquilibrée
- Optimiser avec Grid Search
- Afficher l'importance des features
- 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.
🎯 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
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}")
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é
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}")
Contexte : Classifier les zones à risque de paludisme selon des facteurs environnementaux et démographiques.
- Créer un dataset : température, humidité, altitude, densité population, accès eau, couverture moustiquaires, zone à risque (0/1)
- Entraîner un modèle XGBoost
- Évaluer avec ROC-AUC, F1, precision, recall
- Générer un SHAP summary plot
- 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 %
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é")
🎯 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
- 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
📖 Cours — K-Means
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)
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())
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)
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)
Contexte : Segmenter les exploitations agricoles pour cibler des programmes d'aide différenciés.
- Créer un dataset : superficie, rendement, accès crédit, usage intrants, niveau mécanisation (10+ features)
- Appliquer PCA pour réduire la dimension
- Déterminer le k optimal (coude + silhouette)
- Appliquer K-Means puis comparer avec CAH
- Profiler chaque segment et proposer une stratégie d'intervention adaptée
📖 Cours — Semi-Supervisé
- Entraîner un modèle sur les quelques données labellisées disponibles
- Prédire les labels des données non labellisées
- Ajouter au jeu d'entraînement les prédictions les plus confiantes (pseudo-labels)
- Répéter jusqu'à convergence
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)))
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)}")
Contexte : Une ONG dispose de 10 000 photos de feuilles de manioc mais seulement 300 ont été annotées par un phytopathologiste (sain/malade).
- Simuler un dataset avec features visuelles (couleur, texture, taches) et seulement 3% de labels
- Entraîner un modèle baseline (labels seuls) avec Random Forest
- Appliquer SelfTrainingClassifier et comparer les performances
- Appliquer LabelSpreading et comparer
- 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
- Réduire les indices spectraux avec PCA
- Effectuer un premier clustering K-Means exploratoire (sans labels) pour identifier les grandes zones homogènes
- Utiliser les quelques points de vérité terrain disponibles avec Self-Training pour affiner la classification par type de culture
- Comparer la carte obtenue par clustering pur vs semi-supervisé
- Estimer les superficies par culture sur toute la région
📝 Livrables
- Carte de clustering exploratoire
- Carte de classification semi-supervisée finale
- Rapport comparatif des deux approches
- 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
- Comparaison K-Means vs DBSCAN vs CAH
- Profilage des segments avec visualisations
- Recommandations de stratégie de communication par segment
- Estimation du nombre d'agents de terrain nécessaires par segment
🎯 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
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/
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)
Contexte : Mettre en production votre modèle Random Forest de détection de fraude M-Pesa.
- Sauvegarder le modèle avec
joblib.dump() - Créer un endpoint
/api/predict - Ajouter un endpoint
/api/batch_predict - Implémenter un
/healthcheck - Tester avec
curlou Postman
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
| Option | Type | Exemple |
|---|---|---|
| A | Supervisé | Scoring crédit ou détection fraude (XGBoost + SHAP + API) |
| B | Non supervisé | Segmentation clients avec dashboard de profilage interactif |
| C | Semi-supervisé | Classification avec peu de labels + pipeline d'annotation active |
📋 Livrables Finaux
- Code source complet (GitHub)
- API déployée et testée
- Documentation technique
- Présentation PowerPoint
- Vidéo de démonstration (5 min)
🎓 Évaluation
| Critère | Points |
|---|---|
| Qualité du modèle ML (métrique adaptée au type de projet) | 25 |
| API fonctionnelle et documentée | 20 |
| Interface utilisateur | 15 |
| Analyse business et recommandations | 15 |
| Code propre et documenté | 10 |
| Présentation orale | 15 |
| TOTAL | 100 |
📚 Ressources Complémentaires
🔗 Liens Utiles
- Documentation Scikit-learn
- Documentation Semi-Supervisé (sklearn)
- Documentation XGBoost
- Documentation LightGBM
- Documentation SHAP
- Documentation Flask