🪐 TP3 : Classification et Exploration de Données d’Exoplanètes (Kepler)

Bienvenue dans ce TP d’application pratique ! L’objectif de ce travail est de découvrir comment structurer un projet complet de Science des Données (Data Science) en appliquant les 7 étapes du cycle de vie de la donnée.

Nous allons étudier un jeu de données recensant des exoplanètes découvertes par le télescope Kepler (et d’autres instruments), nettoyer ses incertitudes physiques, explorer ses motifs, modéliser les caractéristiques physiques à l’aide d’un algorithme de partitionnement (K-Means), et enfin déployer une interface interactive pour analyser nos résultats.


📋 Rappel : Les 7 Étapes de la Datascience

  1. Acquisition 📥 : Extraction du signal brut depuis des sources externes.
  2. Nettoyage 🧼 : Traitement des anomalies, valeurs aberrantes et formatage (GIGO).
  3. Analyse Exploratoire 🔍 : Recherche de motifs, distributions et corrélations statistiques.
  4. Visualisation 📊 : Représentation visuelle des données (Data Storytelling).
  5. Modélisation 🤖 : Entraînement de modèles statistiques ou d’apprentissage automatique.
  6. Évaluation 🎯 : Audit des performances et interprétation des résultats du modèle.
  7. Communication des résultats 📣 : Présentation et transmission des résultats sous forme interactive ou d’exports de données.

📥 Étape 1 : Acquisition (L’Extraction)

L’objectif de cette première phase est d’importer le signal brut depuis une source publique. Ici, nous allons utiliser l’API kagglehub pour télécharger dynamiquement un jeu de données hébergé sur Kaggle contenant des informations détaillées sur des milliers d’exoplanètes.

Commençons par installer les bibliothèques requises pour l’ensemble du TP.

# 1.1 Installation des dépendances (exécuter une seule fois au début)
%pip install pandas numpy matplotlib seaborn scikit-learn kagglehub dash plotly

Une fois les dépendances installées, nous pouvons importer les modules nécessaires pour les premières manipulations de données et la gestion des chemins.

# 1.2 Importation des bibliothèques fondamentales
import os
import numpy as np
import pandas as pd
import kagglehub

Nous téléchargeons le dataset depuis Kaggle en utilisant son identifiant de dépôt. L’API kagglehub stocke automatiquement les fichiers dans un dossier cache local et retourne le chemin d’accès.

# 1.3 Téléchargement du jeu de données depuis Kaggle
path = kagglehub.dataset_download("diaaessam/exoplanets-planets-outside-our-galaxy")
print("Chemin du dataset téléchargé :", path)

Nous pouvons maintenant charger le fichier CSV exoplanets.csv dans un DataFrame Pandas afin de pouvoir le manipuler en mémoire. Affichons les 5 premières lignes pour comprendre la structure globale.

# 1.4 Chargement et aperçu du dataset brut
csv_path = os.path.join(path, "exoplanets.csv")
df = pd.read_csv(csv_path)

# Affichage du début du DataFrame
display(df.head())

Avant de modifier ou manipuler les données, il est crucial d’auditer leurs métadonnées pour connaître le type de chaque colonne et repérer les valeurs manquantes (valeurs nulles).

# 1.5 Audit des métadonnées et recherche de valeurs manquantes
print("=== Informations Générales ===")
df.info()

print("\n=== Nombre de valeurs manquantes par colonne ===")
print(df.isnull().sum())

🧼 Étape 2 : Nettoyage (Le Décrassage)

Comme vous pouvez le constater, les données physiques brutes (telles que la masse de la planète ou la période orbitale) contiennent des incertitudes de mesure stockées sous forme de chaînes de caractères complexes, par exemple "0.7±0.1" ou "1891+56−48".

Pour pouvoir réaliser des calculs numériques et entraîner un modèle mathématique, nous devons impérativement extraire la valeur centrale (le premier nombre) et ignorer l’incertitude. Si nous laissons les données en l’état, notre modèle ne fonctionnera pas : c’est le principe du Garbage In, Garbage Out (GIGO).

Commençons par préparer un DataFrame propre (clean_df) et formater les colonnes d’identification et de méthode.

# 2.1 Initialisation du DataFrame nettoyé et typage de base
clean_df = pd.DataFrame()

# Conversion explicite des types pour optimiser la mémoire et la cohérence
clean_df['Name'] = df['Name'].astype("str")
clean_df['Discovery method'] = df['Discovery method'].astype("category")

Écrivons maintenant une première fonction utilitaire simple pour extraire l’année de découverte. Parfois, l’année contient des anomalies ou des formats mixtes ; nous ne conserverons que les 4 premiers caractères que nous convertirons en entier.

# 2.2 Fonction de nettoyage de l'année de découverte
def clean_year(val):
    try:
        # On extrait les 4 premiers caractères correspondant à l'année (ex: 2009)
        return int(str(val)[:4])
    except Exception:
        return np.nan

# Application sur la colonne Disc. Year
clean_df["Disc. Year"] = df["Disc. Year"].apply(clean_year).astype('Int64')

Pour traiter les colonnes de mesures physiques, nous allons définir des expressions régulières (Regex) capables de détecter : 1. Les valeurs avec incertitude asymétrique (ex: 123.0+234.2-324.3) 2. Les valeurs avec incertitude symétrique (ex: 123.0±45.6) 3. Les valeurs numériques simples (ex: 123.0)

Définisons d’abord une fonction de conversion en float gérant la virgule décimale française , par le point ..

# 2.3 Définition des fonctions de conversion et d'extraction par Regex
import re

def to_float(s):
    """Convertit une chaîne en float en gérant les formats régionaux."""
    try:
        return float(str(s).replace(',', '.'))
    except Exception:
        return None

def extract_central_value(s):
    """
    Extrait la valeur centrale d'une mesure textuelle contenant des incertitudes.
    Exemples d'entrées : '1.23+0.05-0.03', '2.5±0.1', '14.2'
    """
    # Cas 1 : Format 'valeur+incertitude_sup-incertitude_inf'
    match = re.match(r'^\s*([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)(?:\+([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?))?(?:\-([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?))?\s*$', str(s))
    if not match:
        # Cas 2 : Format avec le symbole ± ou \u00B1
        match_pm = re.match(r'^\s*([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)\u00B1]([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)\s*$', str(s))
        if match_pm:
            return to_float(match_pm.group(1))
        # Cas 3 : Valeur numérique simple sans incertitude
        match_single = re.match(r'^\s*([+-]?\d*\.?\d+)\s*$', str(s))
        if match_single:
            return to_float(match_single.group(1))
        return None
    return to_float(match.group(1))

Nous appliquons cette puissante fonction d’extraction sur nos trois colonnes physiques stratégiques : - La masse de l’exoplanète (Mass (MJ)) - Sa période orbitale (Period (days)) - La masse de son étoile hôte (Host star mass (M☉))

# 2.4 Application de la fonction de nettoyage Regex sur les caractéristiques physiques
float_columns = {
    "Mass (MJ)": "mass",
    "Period (days)": "period",
    "Host star mass (M☉)": "star_mass"
}

for dirty_col, clean_col in float_columns.items():
    clean_df[clean_col] = df[dirty_col].apply(extract_central_value)

Pour terminer l’étape de nettoyage, nous supprimons les lignes qui contiennent des valeurs manquantes dans nos trois caractéristiques physiques principales, car un algorithme de clustering ne peut pas travailler avec des valeurs NaN.

# 2.5 Suppression des valeurs manquantes et aperçu du jeu propre
clean_df.dropna(subset=['mass', 'period', 'star_mass'], inplace=True)

# Affichage des dimensions et des premières lignes nettoyées
print(f"Nombre d'exoplanètes après nettoyage : {len(clean_df)}")
display(clean_df.head())

🔍 Étape 3 : Analyse Exploratoire (L’Interrogatoire)

Maintenant que nos données sont propres et typées correctement, passons à l’Analyse Exploratoire des Données (EDA). Cette étape vise à interroger nos données pour en extraire des premières statistiques globales, comprendre les distributions et identifier les corrélations évidentes.

Commençons par examiner les résumés statistiques (moyennes, écarts-types, quartiles) des variables physiques.

# 3.1 Statistiques descriptives des variables physiques nettoyées
display(clean_df[['mass', 'period', 'star_mass']].describe())

Regardons également comment se répartissent les exoplanètes selon leur méthode de découverte (Discovery method). Cela nous indiquera quelles techniques d’observation (Transit, Vitesse Radiale, etc.) sont majoritaires dans nos données.

# 3.2 Fréquence des différentes méthodes de découverte
discovery_counts = clean_df['Discovery method'].value_counts()
display(discovery_counts)

📊 Étape 4 : Visualisation (Le Témoignage)

Le Data Storytelling consiste à transformer des tableaux bruts en figures parlantes. Pour appréhender la relation complexe entre la masse d’une exoplanète et sa période de révolution (le temps qu’elle met à faire le tour de son étoile), nous allons tracer des visualisations graphiques.

Tout d’abord, créons une figure statique avec Matplotlib en appliquant une échelle logarithmique. Pourquoi ? Parce que les périodes vont de moins d’un jour à des milliers de jours, et les masses de fractions de Terre à plusieurs fois Jupiter !

# 4.1 Visualisation brute des relations physiques (Masse vs Période)
import matplotlib.pyplot as plt
import seaborn as sns

plt.figure(figsize=(10, 6))
sns.scatterplot(
    data=clean_df, 
    x='period', 
    y='mass', 
    alpha=0.6, 
    edgecolor='w', 
    color='#2b5c8f'
)
plt.xscale("log")
plt.yscale("log")
plt.xlabel("Période Orbitale (jours) - Échelle Log")
plt.ylabel("Masse de la Planète (MJ) - Échelle Log")
plt.title("Distribution Physique des Exoplanètes Découvertes")
plt.grid(True, which="both", ls="--", alpha=0.5)
plt.show()

Pour permettre une exploration dynamique où chaque point affiche le nom de l’exoplanète au survol de la souris, nous générons un graphique interactif en utilisant la bibliothèque Plotly Express.

# 4.2 Graphique interactif Plotly Express
import plotly.express as px

fig_raw = px.scatter(
    clean_df,
    x='mass',
    y='period',
    hover_name='Name',
    log_x=True,
    log_y=True,
    labels={'mass': 'Masse de la Planète (MJ)', 'period': 'Période (jours)'},
    title='Exploration Interactive : Masse vs Période orbitale'
)
fig_raw.update_traces(marker=dict(size=6, opacity=0.7))
fig_raw.show()

🤖 Étape 5 : Modélisation (Le Profilage)

L’intelligence artificielle entre en jeu ! Nous voulons partitionner nos exoplanètes en groupes homogènes (clusters) en nous basant sur leur masse et leur période orbitale. Pour cela, nous allons utiliser l’algorithme d’apprentissage non supervisé du K-Means.

🛠️ Feature Engineering : Pourquoi le logarithme ?

L’algorithme K-Means calcule des distances euclidiennes entre les points. Si nous lui fournissons directement la masse (qui varie de 0.001 à 50) et la période (qui varie de 0.1 à 10000), la période écrasera complètement la masse en raison de sa plus grande échelle numérique. En appliquant la transformation \log_{10}(X + 10^{-8}), nous normalisons les échelles afin que chaque variable contribue équitablement au calcul des distances.

# 5.1 Préparation des données (Feature Engineering avec échelle logarithmique)
X = clean_df[['period', 'mass', 'star_mass']].copy()

# Passage à l'échelle log10 pour stabiliser les écarts d'échelle
X_log = np.log10(X + 1e-8)

Nous configurons et entraînons à présent l’algorithme K-Means de la bibliothèque scikit-learn. Nous choisissons de répartir nos exoplanètes en 3 groupes distincts. Les étiquettes de clusters prédites sont ensuite ajoutées à notre DataFrame propre.

# 5.2 Initialisation et entraînement du modèle K-Means
from sklearn.cluster import KMeans

# Paramétrage : 3 clusters, initialisation stable avec n_init=10
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)

# Entraînement et prédiction des clusters sur nos variables cibles
clean_df["cluster"] = kmeans.fit_predict(X_log[['period', 'mass']])

🎯 Étape 6 : Évaluation (L’Audit)

Une fois le modèle entraîné, l’étape d’Évaluation consiste à auditer la cohérence de nos groupes. Est-ce que les 3 clusters formés par le K-Means ont une signification physique réelle du point de vue de l’astrophysique ?

Commençons par tracer graphiquement les clusters avec leurs couleurs respectives pour analyser visuellement la séparation.

# 6.1 Visualisation des clusters générés par K-Means
import matplotlib.pyplot as plt

plt.figure(figsize=(10, 6))
scatter = plt.scatter(
    clean_df['period'], 
    clean_df['mass'], 
    c=clean_df['cluster'], 
    cmap="viridis", 
    alpha=0.6,
    edgecolor='w'
)
plt.xscale("log")
plt.yscale("log")
plt.xlabel("Période (jours)")
plt.ylabel("Masse (MJ)")
plt.title("Résultat du Clustering K-Means des Exoplanètes")
plt.colorbar(scatter, label="Identifiant du Cluster")
plt.grid(True, which="both", ls="--", alpha=0.5)
plt.show()

Pour aller plus loin dans notre audit, calculons le profil moyen (médiane et moyenne) des caractéristiques physiques de chaque cluster. Cela nous permettra d’interpréter scientifiquement les catégories découvertes automatiquement par l’algorithme.

# 6.2 Profilage statistique des clusters d'exoplanètes
for cluster_id in sorted(clean_df['cluster'].unique()):
    subset = clean_df[clean_df['cluster'] == cluster_id]
    print(f"\n🚀 --- CLUSTER {cluster_id} (Nombre de planètes : {len(subset)}) ---")
    print(f"Période orbitale médiane : {subset['period'].median():.2f} jours")
    print(f"Masse planétaire médiane : {subset['mass'].median():.4f} MJ (masses de Jupiter)")
    print(f"Masse médiane de l'étoile hôte : {subset['star_mass'].median():.2f} M☉ (masses solaires)")

[!NOTE] Interprétation Astrophysique des Groupes : - Les Jupiters Chauds (Hot Jupiters) : Planètes géantes très massives situées extrêmement près de leur étoile (période de révolution très courte, de l’ordre de quelques jours seulement). - Les Planètes Tempérées / Périodes Longues : Planètes géantes gazeuses ou telluriques plus éloignées de leur soleil, ayant des périodes orbitales de plusieurs centaines à milliers de jours. - Les Planètes Légères à Période Moyenne / Courte : Planètes de type Super-Terres ou Mini-Neptunes, de faible masse, orbitant relativement près de leur étoile.

📣 Étape 7 : Communication des résultats (La Transmission)

Dernière étape indispensable du cycle de vie consiste à communiquer les résultats de manière claire, concise et exploitable pour des interlocuteurs (décideurs, astrophysiciens ou grand public). Ici, nous communiquons de deux manières : 1. En offrant une application web interactive (Dash) qui permet à tout le monde d’explorer dynamiquement les clusters en direct sans toucher au code. 2. En fournissant une fonction d’exportation pour échanger les données nettoyées et groupées avec d’autres collaborateurs ou logiciels (comme Universe Sandbox).

Créons le graphique Plotly labellisé par nos clusters et lançons l’application web Dash dans notre cellule de notebook.

# 7.1 Lancement de l'application Web interactive Dash en mode inline
import dash
from dash import dcc, html
import plotly.express as px

# Préparation du graphique interactif Plotly
fig = px.scatter(
    clean_df,
    x='mass',
    y='period',
    color=clean_df['cluster'].astype(str),
    hover_name='Name',
    log_x=True,
    log_y=True,
    labels={'mass': 'Masse (MJ)', 'period': 'Période (jours)', 'color': 'Cluster ID'},
    title='Clustering Exoplanétaire : Masse vs Période orbitale'
)
fig.update_traces(marker=dict(size=6, opacity=0.8))

# Configuration de l'application Dash
app = dash.Dash(__name__)
app.layout = html.Div(style={'fontFamily': 'Arial, sans-serif', 'padding': '20px'}, children=[
    html.H2("Visualiseur de Clusters d'Exoplanètes Kepler", style={'color': '#2c3e50'}),
    html.P("Cet explorateur dynamique vous permet de filtrer visuellement les différents groupes physiques d'exoplanètes trouvés par l'algorithme K-Means."),
    dcc.Graph(figure=fig)
])

# Démarrage du serveur web local
app.run(mode='inline')

Pour finir, nous concevons une fonction de déploiement d’export de données sous format JSON ou CSV. Cela permet d’extraire les planètes d’un système stellaire spécifique (comme le célèbre système Kepler-11) afin de les injecter directement dans un simulateur physique d’orbites.

# 7.2 Fonction de déploiement : Exportation de données vers des formats tiers
def export_system_for_universe_sandbox(dataframe, system_name, format_type='json'):
    """
    Filtre et exporte les planètes d'un système stellaire donné.
    Permet l'interopérabilité avec des simulateurs comme Universe Sandbox.
    """
    # Filtrer les planètes appartenant au système ciblé
    system_data = dataframe[dataframe['Name'].str.contains(system_name, na=False, case=False)]
    
    print(f"Exportation du système '{system_name}' ({len(system_data)} planètes trouvées).")
    
    if len(system_data) > 0:
        filename = f"{system_name.replace(' ', '_')}.{format_type}"
        if format_type == 'json':
            system_data.to_json(filename, orient="records")
        elif format_type == 'csv':
            system_data.to_csv(filename, index=False)
            
        print(f"✅ Fichier '{filename}' généré avec succès dans votre espace de travail !")
    else:
        print("❌ Système introuvable dans le jeu de données.")

Appliquons maintenant notre fonction d’exportation pour le système de Kepler-11 afin de générer son fichier d’échange.

# 7.3 Exemple pratique d'exportation pour Kepler-11
export_system_for_universe_sandbox(clean_df, 'Kepler-11', 'json')