✅ Corrections Bootcamp Data Science

Solutions complètes & expliquées — 7 exercices

Club D.I.A.M 💎 Données Africaines Réelles 🌍
📋 Guide d'utilisation — Cliquez sur chaque carte pour révéler la solution complète. Essayez d'abord l'exercice par vous-même, puis comparez votre approche.
7
Exercices corrigés
4
Semaines couvertes
5
Bibliothèques utilisées
100%
Code exécutable

📊 Corrections — Semaine 1 : NumPy & Pandas

Fondations · Arrays · DataFrames · Sélection

Semaine 1/4
Corr. 1.1 Analyse des précipitations en Afrique

Array NumPy de 12 mois · argmin/argmax · indexation booléenne · statistiques vectorielles.

💡 Point clé : np.argmin() et np.argmax() retournent l'indice du min/max — parfait pour retrouver le mois correspondant.
solution_ex1_1.py
import numpy as np

precipitations = np.array([200, 180, 220, 150, 80, 20, 10, 15, 50, 160, 240, 210])
mois = ['Jan','Fév','Mar','Avr','Mai','Jun','Jul','Aoû','Sep','Oct','Nov','Déc']

# 1. Précipitation totale annuelle
total = np.sum(precipitations)
print(f"1. Total annuel          : {total} mm")

# 2. Mois le plus sec et le plus pluvieux
idx_sec      = np.argmin(precipitations)
idx_pluvieux = np.argmax(precipitations)
print(f"2. Mois le + sec         : {mois[idx_sec]} ({precipitations[idx_sec]} mm)")
print(f"   Mois le + pluvieux    : {mois[idx_pluvieux]} ({precipitations[idx_pluvieux]} mm)")

# 3. Moyenne mensuelle
moyenne = np.mean(precipitations)
print(f"3. Moyenne mensuelle     : {moyenne:.1f} mm")

# 4. Mois de saison des pluies (> 150 mm)
masque      = precipitations > 150
mois_pluies = [mois[i] for i in range(12) if masque[i]]
print(f"4. Saison des pluies     : {mois_pluies}  ({np.sum(masque)} mois)")

# 5. Différence max - min
diff = np.max(precipitations) - np.min(precipitations)
print(f"5. Différence max - min  : {diff} mm")
Corr. 1.2 Analyse des données économiques africaines

DataFrame 6 pays · PIB/habitant calculé · filtrage · corrélation · colonne conditionnelle · tri.

💡 Point clé : apply(lambda x: ...) est le moyen le plus lisible pour créer une colonne catégorielle à partir d'une condition.
solution_ex1_2.py
import pandas as pd
import numpy as np

data = {
    'Pays'              : ['Gabon', 'Cameroun', "Côte d'Ivoire", 'Sénégal', 'Mali', 'Ghana'],
    'PIB_Mds'           : [20.3, 44.1, 61.4, 27.6, 18.1, 72.3],
    'Population_M'      : [2.3, 27.9, 26.9, 17.2, 22.4, 33.5],
    'Taux_Scolarisation': [88, 78, 62, 57, 52, 74],
    'Esperance_Vie'     : [65, 59, 58, 62, 55, 64],
}
df = pd.DataFrame(data)

# 1. PIB par habitant le plus élevé
df['PIB_par_hab'] = df['PIB_Mds'] / df['Population_M'] * 1000
idx = df['PIB_par_hab'].idxmax()
print(f"1. Meilleur PIB/hab : {df.loc[idx, 'Pays']} ({df.loc[idx, 'PIB_par_hab']:.0f} $/hab)")

# 2. Pays avec taux de scolarisation > 75 %
scol = df[df['Taux_Scolarisation'] > 75][['Pays', 'Taux_Scolarisation']]
print(f"2. Scolarisation > 75 % :\n{scol.to_string(index=False)}")

# 3. Corrélation PIB / scolarisation
corr = df['PIB_par_hab'].corr(df['Taux_Scolarisation'])
print(f"3. Corrélation PIB ↔ Scolarisation : {corr:.3f}")

# 4. Colonne Catégorie
df['Categorie'] = df['PIB_par_hab'].apply(
    lambda x: 'Riche' if x > 5000 else 'Émergent')
print(f"4. Catégories :\n{df[['Pays','PIB_par_hab','Categorie']].to_string(index=False)}")

# 5. Tri par espérance de vie décroissante
df_trie = df.sort_values('Esperance_Vie', ascending=False)
print(f"5. Tri espérance de vie :\n{df_trie[['Pays','Esperance_Vie']].to_string(index=False)}")

🔍 Corrections — Semaine 2 : Pandas Avancé

Groupby · Pivot · Merge · Valeurs manquantes

Semaine 2/4
Corr. 2.1 Analyse des résultats scolaires par région

groupby multi-colonnes · pivot_table · gestion des NaN avec fillna(médiane) · classement.

💡 Point clé : pivot_table() = groupby en 2D. La médiane est préférable à la moyenne pour remplacer les NaN car elle est robuste aux valeurs extrêmes.
solution_ex2_1.py
import pandas as pd
import numpy as np

np.random.seed(42)
regions  = ['Nord', 'Sud', 'Est', 'Ouest', 'Centre'] * 10
matieres = ['Maths', 'Science', 'Français', 'Histoire'] * 12 + ['Maths', 'Science']

rows = []
for i in range(50):
    rows.append({
        'Region'    : regions[i],
        'Matiere'   : ['Maths','Science','Français','Histoire'][i % 4],
        'Score'     : np.random.randint(50, 96),
        'Nb_eleves' : np.random.randint(80, 151),
    })
df = pd.DataFrame(rows)
df.loc[df.sample(5).index, 'Score'] = np.nan   # valeurs manquantes

# 1. Moyenne par région pour chaque matière
print("1. Moyenne par région x matière :")
print(df.groupby(['Region', 'Matiere'])['Score'].mean().round(1).unstack())

# 2. Région avec le meilleur taux de réussite
moy_region = df.groupby('Region')['Score'].mean().sort_values(ascending=False)
print(f"\n2. Meilleure région : {moy_region.idxmax()} ({moy_region.max():.1f}/100)")

# 3. Pivot table
print("\n3. Pivot table (Score × Région × Matière) :")
pivot = df.pivot_table(
    values='Score', index='Region', columns='Matiere', aggfunc='mean').round(1)
print(pivot)

# 4. Gérer les valeurs manquantes (médiane)
mediane = df['Score'].median()
df['Score'] = df['Score'].fillna(mediane)
print(f"\n4. NaN remplacés par médiane = {mediane:.1f}")
print(f"   NaN restants : {df['Score'].isnull().sum()}")

# 5. Classement régions par score moyen décroissant
classement = df.groupby('Region')['Score'].mean().sort_values(ascending=False).round(1)
print("\n5. Classement des régions :")
for rang, (region, score) in enumerate(classement.items(), 1):
    print(f"   {rang}. {region:<8} : {score}")
Corr. 2.2 Fusion de datasets économiques et sanitaires

inner join · left join · détection de pays manquants · corrélation inter-datasets.

💡 Point clé : Le left join est idéal pour "auditer" un dataset : les NaN dans les colonnes droites révèlent exactement les lignes sans correspondance.
solution_ex2_2.py
import pandas as pd

# Dataset économique (7 pays)
df_eco = pd.DataFrame({
    &#x27;Pays'       : ['Gabon', 'Cameroun', "Côte d'Ivoire", 'Sénégal', 'Mali', 'Ghana', 'Burkina'],
    &#x27;PIB_Mds'    : [20.3, 44.1, 61.4, 27.6, 18.1, 72.3, 16.1],
    &#x27;Dep_Sante_%': [4.2, 3.1, 2.8, 3.5, 2.2, 3.8, 5.1],
})

# Dataset sanitaire (6 pays — sans Burkina, +Nigeria)
df_sante = pd.DataFrame({
    &#x27;Pays'         : ['Gabon', 'Cameroun', "Côte d'Ivoire", 'Sénégal', 'Ghana', 'Nigeria'],
    &#x27;Esperance_Vie': [65, 59, 58, 62, 64, 53],
    &#x27;Medecins_10k' : [5.8, 2.3, 1.4, 0.7, 1.1, 0.4],
})

# 1. Inner join
inner = pd.merge(df_eco, df_sante, on=&#x27;Pays', how='inner')
print(f"1. INNER JOIN — {len(inner)} pays communs :")
print(inner[[&#x27;Pays', 'PIB_Mds', 'Esperance_Vie']].to_string(index=False))

# 2. Left join — pays sans données sanitaires
left       = pd.merge(df_eco, df_sante, on=&#x27;Pays', how='left')
sans_sante = left[left[&#x27;Esperance_Vie'].isnull()]['Pays'].tolist()
print(f"\n2. LEFT JOIN — pays sans données sanitaires : {sans_sante}")

# 3. PIB par médecin (relatif : Mds $ / nb médecins pour 10 000 hab.)
inner[&#x27;PIB_par_med'] = (inner['PIB_Mds'] / inner['Medecins_10k']).round(2)
print("\n3. PIB relatif par médecin :")
print(inner[[&#x27;Pays', 'PIB_Mds', 'Medecins_10k', 'PIB_par_med']].to_string(index=False))

# 4. Corrélation dépenses santé ↔ espérance de vie
corr = inner[&#x27;Dep_Sante_%'].corr(inner['Esperance_Vie'])
print(f"\n4. Corrélation dépenses santé ↔ espérance de vie : {corr:.3f}")
interp = "positive forte" if corr > 0.5 else "positive modérée" if corr > 0 else "faible ou négative"
print(f"   → Corrélation {interp}")

📈 Corrections — Semaine 3 : Visualisation & EDA

Matplotlib · Seaborn · 6 étapes EDA · Heatmap

Semaine 3/4
Corr. 3.1 Dashboard Santé Publique Africaine

Subplots 2×2 · barres annotées · pie chart · line avec fill_between · scatter avec colorbar · export PNG.

💡 Point clé : fig.suptitle() donne un titre global au dashboard. plt.tight_layout() évite les chevauchements entre sous-graphiques.
solution_ex3_1.py
import matplotlib.pyplot as plt
import numpy as np

# Données santé publique africaine
pays     = [&#x27;Gabon', 'Cameroun', 'Congo', 'Sénégal', "C. d'Ivoire"]
cas_palu = [1250, 890, 650, 720, 580]
budgets  = [4.2, 3.1, 2.8, 3.5, 2.6]      # % du PIB
annees   = list(range(2014, 2024))
cas_evo  = [2100, 1950, 1800, 1650, 1500, 1380, 1280, 1150, 1050, 890]
bud_abs  = [35, 45, 62, 28, 72, 40]         # budget santé absolu (Mds $)
esperance= [65, 59, 58, 62, 61, 63]
couleurs = [&#x27;#009e60', '#3a75c4', '#fcd116', '#009e60', '#3a75c4']

fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle(&#x27;Dashboard Santé Publique — Afrique', fontsize=16, fontweight='bold')

# 1. Barres : cas de paludisme
bars = axes[0,0].bar(pays, cas_palu, color=couleurs, edgecolor=&#x27;white', linewidth=1.2)
axes[0,0].set_title(&#x27;Cas de Paludisme par Pays (2024)', fontweight='bold')
axes[0,0].set_ylabel(&#x27;Nombre de cas')
for bar, val in zip(bars, cas_palu):
    axes[0,0].text(bar.get_x() + bar.get_width()/2,
                   bar.get_height() + 15, f&#x27;{val:,}', ha='center', fontsize=9, fontweight='bold')
axes[0,0].set_ylim(0, max(cas_palu) * 1.15)

# 2. Pie : répartition des budgets santé
axes[0,1].pie(budgets, labels=pays, autopct=&#x27;%1.1f%%',
              colors=couleurs, startangle=90, pctdistance=0.82)
axes[0,1].set_title(&#x27;Répartition des Budgets Santé (% PIB)', fontweight='bold')

# 3. Line : évolution des cas sur 10 ans
axes[1,0].plot(annees, cas_evo, color=&#x27;#009e60', linewidth=2.5,
               marker=&#x27;o', markersize=6, markerfacecolor='white', markeredgewidth=2)
axes[1,0].fill_between(annees, cas_evo, alpha=0.12, color=&#x27;#009e60')
axes[1,0].set_title(&#x27;Évolution des Cas de Paludisme (2014-2023)', fontweight='bold')
axes[1,0].set_xlabel(&#x27;Année'); axes[1,0].set_ylabel('Nombre de cas')
axes[1,0].grid(alpha=0.3, linestyle=&#x27;--')
axes[1,0].tick_params(axis=&#x27;x', rotation=45)

# 4. Scatter : budget santé vs espérance de vie
sc = axes[1,1].scatter(bud_abs, esperance, c=bud_abs, cmap=&#x27;YlGn',
                        s=100, alpha=0.85, edgecolors=&#x27;#555', linewidths=0.8)
plt.colorbar(sc, ax=axes[1,1], label=&#x27;Budget (Mds $)')
axes[1,1].set_xlabel(&#x27;Budget Santé (Mds $)')
axes[1,1].set_ylabel(&#x27;Espérance de vie (ans)')
axes[1,1].set_title(&#x27;Budget Santé vs Espérance de Vie', fontweight='bold')
axes[1,1].grid(alpha=0.3)

plt.tight_layout()
plt.savefig(&#x27;dashboard_sante_afrique.png', dpi=150, bbox_inches='tight')
plt.show()
print("✅ Dashboard sauvegardé : dashboard_sante_afrique.png")
Corr. 3.2 EDA Complète — Données Éducatives Africaines

6 étapes méthodologiques : chargement, nettoyage (NaN+doublons), describe, 4 visualisations, heatmap Seaborn, 5 insights.

💡 Point clé : La structure en 6 étapes est un standard professionnel. Commencer par drop_duplicates() avant fillna() évite de biaiser les statistiques de remplacement.
solution_ex3_2.py
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme(style=&#x27;whitegrid', palette='viridis')
np.random.seed(42)

# ═══ ÉTAPE 1 — CHARGEMENT & INSPECTION ═══
regions = [&#x27;Nord', 'Sud', 'Est', 'Ouest', 'Centre'] * 10
pays_l  = [&#x27;Gabon', 'Cameroun', "C. d'Ivoire", 'Sénégal', 'Mali'] * 10

df = pd.DataFrame({
    &#x27;Region'         : regions,
    &#x27;Pays'           : pays_l,
    &#x27;Taux_Scolaris'  : np.random.randint(40, 95, 50),
    &#x27;Budget_Educ_pct': np.random.uniform(2.0, 8.0, 50).round(1),
    &#x27;Score_PISA'     : np.random.randint(280, 450, 50).astype(float),
    &#x27;Ratio_Elev_Prof': np.random.randint(20, 65, 50),
})
# Introduire NaN + doublon
df.loc[df.sample(4).index, &#x27;Score_PISA']    = np.nan
df.loc[df.sample(2).index, &#x27;Taux_Scolaris'] = np.nan
df = pd.concat([df, df.iloc[[0]]], ignore_index=True)

print(f"Shape : {df.shape} | Colonnes : {list(df.columns)}")
print(df.head(3))

# ═══ ÉTAPE 2 — NETTOYAGE ═══
print(f"\nNaN :\n{df.isnull().sum()}")
print(f"Doublons : {df.duplicated().sum()}")
df = df.drop_duplicates().reset_index(drop=True)
df[&#x27;Score_PISA']    = df['Score_PISA'].fillna(df['Score_PISA'].median())
df[&#x27;Taux_Scolaris'] = df['Taux_Scolaris'].fillna(df['Taux_Scolaris'].mean())
print(f"Shape après nettoyage : {df.shape} | NaN restants : {df.isnull().sum().sum()}")

# ═══ ÉTAPE 3 — STATISTIQUES DESCRIPTIVES ═══
print("\nStatistiques descriptives :")
print(df.describe().round(1))
print("\nRépartition par région :")
print(df[&#x27;Region'].value_counts())

# ═══ ÉTAPE 4 — VISUALISATIONS ═══
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle(&#x27;EDA — Données Éducatives Africaines', fontsize=14, fontweight='bold')

axes[0,0].hist(df[&#x27;Taux_Scolaris'], bins=12, color='#009e60', edgecolor='white', alpha=0.8)
axes[0,0].set_title(&#x27;Distribution du Taux de Scolarisation')
axes[0,0].set_xlabel(&#x27;Taux (%)')

df.boxplot(column=&#x27;Score_PISA', by='Region', ax=axes[0,1], patch_artist=True)
axes[0,1].set_title(&#x27;Score PISA par Région')
axes[0,1].set_xlabel(&#x27;')

moy_bud = df.groupby(&#x27;Pays')['Budget_Educ_pct'].mean().sort_values(ascending=False)
axes[1,0].bar(moy_bud.index, moy_bud.values, color=&#x27;#3a75c4', edgecolor='white')
axes[1,0].set_title(&#x27;Budget Éducation Moyen (% PIB) par Pays')
axes[1,0].set_ylabel(&#x27;% du PIB')

axes[1,1].scatter(df[&#x27;Budget_Educ_pct'], df['Score_PISA'],
                  alpha=0.6, color=&#x27;#fcd116', edgecolors='#333', s=60)
axes[1,1].set_xlabel(&#x27;Budget Éducation (% PIB)')
axes[1,1].set_ylabel(&#x27;Score PISA')
axes[1,1].set_title(&#x27;Budget Éducation vs Score PISA')

plt.tight_layout()
plt.savefig(&#x27;eda_education_afrique.png', dpi=150, bbox_inches='tight')
plt.show()

# ═══ ÉTAPE 5 — HEATMAP DES CORRÉLATIONS ═══
plt.figure(figsize=(8, 6))
num_cols = [&#x27;Taux_Scolaris', 'Budget_Educ_pct', 'Score_PISA', 'Ratio_Elev_Prof']
corr_mat = df[num_cols].corr()
sns.heatmap(corr_mat, annot=True, cmap=&#x27;RdYlGn', fmt='.2f',
            linewidths=0.5, square=True,
            xticklabels=[&#x27;Scolaris.', 'Budget', 'PISA', 'Ratio'],
            yticklabels=[&#x27;Scolaris.', 'Budget', 'PISA', 'Ratio'])
plt.title(&#x27;Heatmap des Corrélations — Éducation Africaine', fontweight='bold')
plt.tight_layout()
plt.show()

# ═══ ÉTAPE 6 — INSIGHTS ═══
print("\n5 INSIGHTS ACTIONNABLES :")
insights = [
    f"Budget ↔ PISA : corr = {corr_mat.loc[&#x27;Budget_Educ_pct','Score_PISA']:.3f} "
    f"— {&#x27;relation positive' if corr_mat.loc['Budget_Educ_pct','Score_PISA'] > 0 else 'relation faible'}",
    f"Ratio élèves/prof ↔ PISA : corr = {corr_mat.loc[&#x27;Ratio_Elev_Prof','Score_PISA']:.3f} "
    f"— plus le ratio est élevé, {&#x27;plus le score baisse' if corr_mat.loc['Ratio_Elev_Prof','Score_PISA'] < 0 else 'moins prévisible'}",
    f"Meilleure région : {df.groupby(&#x27;Region')['Score_PISA'].mean().idxmax()} "
    f"({df.groupby(&#x27;Region')['Score_PISA'].mean().max():.0f}/450)",
    f"Taux scolarisation moyen : {df[&#x27;Taux_Scolaris'].mean():.1f}%",
    f"Budget éducation moyen : {df[&#x27;Budget_Educ_pct'].mean():.1f}% du PIB",
]
for i, insight in enumerate(insights, 1):
    print(f"  💡 {i}. {insight}")

🎯 Corrections — Semaine 4 : Analyse Avancée

Régression · Scipy · Intervalles de confiance

Semaine 4/4
Corr. 4.1 Régression : Précipitations → Rendements Agricoles

scipy.stats.pearsonr · linregress · droite de régression · IC à 95% avec fill_between · analyse des résidus · prédiction.

💡 Point clé : Le R² mesure la proportion de variance expliquée (0 = nul, 1 = parfait). Toujours vérifier le graphe des résidus : s'ils sont aléatoires autour de 0, le modèle est bien spécifié.
solution_ex4_1.py
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats

np.random.seed(42)

# Données : précipitations (mm) et rendements agricoles (kg/ha)
precipitations = np.array([350, 420, 480, 510, 560, 620, 680, 720, 780, 830, 890, 950])
rendements     = (precipitations * 0.85 + np.random.normal(0, 40, 12)).clip(min=200)
regions        = [&#x27;Sahel N', 'Sahel S', 'Guinée N', 'Guinée C', 'Guinée S',
                  &#x27;Côtier N', 'Côtier C', 'Côtier S', 'Central N', 'Central C',
                  &#x27;Forêt N',  'Forêt S']

# 1. Corrélation de Pearson
r, p_val = stats.pearsonr(precipitations, rendements)
print(f"1. Corrélation de Pearson : r = {r:.4f}  (p = {p_val:.6f})")
force = "très forte" if abs(r) > 0.8 else "forte" if abs(r) > 0.6 else "modérée"
print(f"   → Corrélation {force}")

# 2. Régression linéaire
slope, intercept, r_value, p_value, std_err = stats.linregress(precipitations, rendements)
print(f"\n2. Modèle : Rendement = {slope:.3f} × Précip. + {intercept:.1f}")
print(f"   R²          : {r_value**2:.4f}  ({r_value**2*100:.1f}% de la variance expliquée)")
print(f"   p-value     : {p_value:.6f}  ({&#x27;significatif' if p_value < 0.05 else 'non significatif'})")

# 3. Visualisation avec droite de régression et IC à 95 %
x_range  = np.linspace(precipitations.min() - 30, precipitations.max() + 30, 200)
y_pred_l = slope * x_range + intercept
n, se    = len(precipitations), std_err * np.sqrt(n)

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# Scatter + régression + IC
axes[0].scatter(precipitations, rendements, color=&#x27;#009e60', s=90, zorder=4,
                label=&#x27;Données observées', edgecolors='white', linewidths=0.8)
axes[0].plot(x_range, y_pred_l, color=&#x27;#3a75c4', linewidth=2.5,
             label=f&#x27;Régression  R² = {r_value**2:.3f}')
axes[0].fill_between(x_range, y_pred_l - 1.96*se, y_pred_l + 1.96*se,
                     alpha=0.12, color=&#x27;#3a75c4', label='IC à 95 %')
for i, reg in enumerate(regions):
    axes[0].annotate(reg, (precipitations[i], rendements[i]),
                     textcoords=&#x27;offset points', xytext=(4, 3), fontsize=7, color='#555')
axes[0].set_xlabel(&#x27;Précipitations (mm/an)', fontsize=11)
axes[0].set_ylabel(&#x27;Rendement agricole (kg/ha)', fontsize=11)
axes[0].set_title(&#x27;Précipitations → Rendements Agricoles', fontweight='bold')
axes[0].legend(); axes[0].grid(alpha=0.3, linestyle=&#x27;--')

# Analyse des résidus
y_pts    = slope * precipitations + intercept
residus  = rendements - y_pts
axes[1].scatter(y_pts, residus, color=&#x27;#fcd116', s=80, edgecolors='#333', zorder=3)
axes[1].axhline(y=0, color=&#x27;red', linestyle='--', linewidth=1.5)
axes[1].set_xlabel(&#x27;Valeurs ajustées'); axes[1].set_ylabel('Résidus')
axes[1].set_title(&#x27;Analyse des Résidus', fontweight='bold')
axes[1].grid(alpha=0.3)

plt.tight_layout()
plt.savefig(&#x27;regression_agricole.png', dpi=150, bbox_inches='tight')
plt.show()

# 4. Interprétation du R²
print(f"\n4. R² = {r_value**2:.4f} → le modèle explique {r_value**2*100:.1f}% de la variance")
fiab = "fiable" if r_value**2 > 0.75 else "à améliorer (variables climatiques manquantes ?)"
print(f"   → Modèle {fiab}")

# 5. Prédiction pour 800 mm
precip_cible = 800
pred         = slope * precip_cible + intercept
ic_low       = pred - 1.96 * se
ic_high      = pred + 1.96 * se
print(f"\n5. Prédiction pour {precip_cible} mm de précipitations :")
print(f"   Rendement estimé  : {pred:.0f} kg/ha")
print(f"   IC à 95 %         : [{ic_low:.0f} ; {ic_high:.0f}] kg/ha")