Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Dans ce carnet, nous classons des enregistrements sismiques en quatre types de sources — séisme, explosion, événement de surface et bruit — à partir de 61 caractéristiques physiques des formes d’onde (forme spectrale, statistiques d’enveloppe, kurtosis, énergies par bande). Le jeu de données est un ensemble soigneusement constitué d’événements sismiques du Nord-Ouest Pacifique des États-Unis, 1 000 par classe, archivé sur Zenodo : DOI 10.5281/zenodo.14025693.

Nous comparons trois classifieurs classiques : la machine à vecteurs de support (SVM), les K plus proches voisins (KNN) et la forêt aléatoire, et nous les évaluons classe par classe avec des rapports de classification, des matrices de confusion et des courbes ROC un-contre-tous (one-vs-rest).

Ce jeu de données revient dans le carnet 3.9, et il ancre le classement (leaderboard) du cours, défini à la fin de ce carnet.

🖥️ Diapositives du cours — Séance 15 (lun. 2 nov.)

1. Charger les données

Le chargeur ci-dessous télécharge et met en cache les quatre fichiers de classes, les concatène en une seule table et supprime l’unique colonne de caractéristique qui contient des valeurs manquantes.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pooch

SEISMIC_FILES = {
    "1000_earthquakes_physical_features.csv": "md5:28129c8dd1b3e14f655d489577b841b5",
    "1000_explosion_physical_features.csv": "md5:af1342d32e163e961e043364136359b0",
    "1000_noise_physical_features.csv": "md5:16cdb992fed6cf6273d5624f5df905da",
    "1000_surface_physical_features.csv": "md5:9a2c2643030cf058704d68e130654e9d",
}

frames = []
for fname, checksum in SEISMIC_FILES.items():
    path = pooch.retrieve(
        url=f"https://zenodo.org/api/records/14025693/files/{fname}/content",
        known_hash=checksum,
        fname=fname,
        path=pooch.os_cache("mlgeo"),
    )
    frames.append(pd.read_csv(path, index_col=0))
seismic = pd.concat(frames, ignore_index=True)
seismic = seismic.dropna(axis=1)  # drops the one feature column with missing values

2. Explorer

Vérifiez d’abord l’équilibre des classes. Les quatre classes comptent 1 000 événements chacune : cet ensemble soigneusement constitué est donc équilibré. Gardez à l’esprit que les catalogues réels ne le sont pas.

seismic["source"].value_counts().plot(kind="bar")
plt.ylabel("Number of events")
plt.title("Class balance")
plt.tight_layout()
<Figure size 640x480 with 1 Axes>

Construisez la matrice de caractéristiques X (61 caractéristiques numériques des formes d’onde) et le vecteur d’étiquettes y (quatre chaînes de type de source).

X = seismic.drop(columns=["source", "serial_no"])
y = seismic["source"]
print("X:", X.shape, " y:", y.shape)
print("A few feature names:", list(X.columns[:8]))
X: (4000, 61)  y: (4000,)
A few feature names: ['Window_Length', 'RappMaxMean', 'RappMaxMedian', 'AsDec', 'KurtoSig', 'KurtoEnv', 'SkewSig', 'SkewEnv']

3. Découpage entraînement/test

La cellule ci-dessous est le découpage canonique de ce chapitre : cette ligne exacte définit le découpage du classement à la fin du carnet. Tout le monde s’entraîne sur le même X_train et prédit sur le même X_test.

Deux détails comptent :

  • Une version antérieure de cette leçon utilisait shuffle=False. La table étant ordonnée par classe, cela plaçait des classes entières dans l’ensemble de test et aucune dans l’entraînement — le classifieur ne voyait jamais les classes sur lesquelles il était noté.
  • stratify=y préserve les proportions de classes dans les deux moitiés. C’est le plus important quand les classes sont déséquilibrées — et les catalogues sismiques réels le sont : les fenêtres de bruit dépassent les séismes de plusieurs ordres de grandeur.
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=2026, stratify=y)

4. Mettre à l’échelle après le découpage

Nous ajustons la mise à l’échelle (scaler) sur le seul ensemble d’entraînement, puis nous transformons les deux ensembles. Ajuster le scaler sur toutes les données laisserait les statistiques de l’ensemble de test fuir dans l’entraînement.

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

5. Modèle de référence

Avant tout modèle, notez un modèle de référence (baseline) trivial. DummyClassifier(strategy="most_frequent") prédit toujours la classe majoritaire ; avec quatre classes équilibrées, il obtient 0,25. Chaque modèle ci-dessous doit battre ce nombre.

from sklearn.dummy import DummyClassifier
from sklearn import metrics

dummy = DummyClassifier(strategy="most_frequent")
dummy.fit(X_train, y_train)
baseline_acc = metrics.accuracy_score(y_test, dummy.predict(X_test))
print("Baseline accuracy:", baseline_acc)
Baseline accuracy: 0.25

6. Trois classifieurs

La SVM et les K plus proches voisins reposent tous deux sur des distances ou des marges dans l’espace des caractéristiques : ils utilisent donc les caractéristiques mises à l’échelle.

from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier

# Support Vector Machine classifier
clf = SVC(gamma='scale')  # model design
clf.fit(X_train_scaled, y_train)  # learn
svc_prediction = clf.predict(X_test_scaled)  # predict on test
print("SVC test accuracy:", metrics.accuracy_score(y_true=y_test, y_pred=svc_prediction))

# K-nearest Neighbors
knn_clf = KNeighborsClassifier()  # model design
knn_clf.fit(X_train_scaled, y_train)  # learn
knn_prediction = knn_clf.predict(X_test_scaled)  # predict on test
print("K-nearest Neighbors test accuracy:", metrics.accuracy_score(y_true=y_test, y_pred=knn_prediction))
SVC test accuracy: 0.877
K-nearest Neighbors test accuracy: 0.83

La forêt aléatoire travaille sur les caractéristiques non mises à l’échelle : les arbres coupent sur des seuils, et une remise à l’échelle monotone ne change pas de quel côté d’un seuil tombe un point.

# Random Forest, on the unscaled features
rf_clf = RandomForestClassifier(random_state=42)  # model design
rf_clf.fit(X_train, y_train)  # learn
rf_prediction = rf_clf.predict(X_test)  # predict on test
print("Random Forest test accuracy:", metrics.accuracy_score(y_true=y_test, y_pred=rf_prediction))
Random Forest test accuracy: 0.884

7. Évaluation par classe

L’exactitude est un seul nombre. Le rapport de classification donne la précision, le rappel et le F1 par classe, et la matrice de confusion montre quelles classes sont prises les unes pour les autres.

from sklearn.metrics import ConfusionMatrixDisplay

print("Support Vector Machine")
print(f"Classification report for classifier {clf}:\n"
      f"{metrics.classification_report(y_test, svc_prediction)}\n")

disp = ConfusionMatrixDisplay.from_estimator(clf, X_test_scaled, y_test, xticks_rotation=45)
disp.figure_.suptitle("Confusion Matrix: SVC")
plt.tight_layout()
plt.show()
Support Vector Machine
Classification report for classifier SVC():
               precision    recall  f1-score   support

   earthquake       0.82      0.87      0.84       250
    explosion       0.88      0.79      0.83       250
        noise       0.90      0.93      0.92       250
surface event       0.92      0.92      0.92       250

     accuracy                           0.88      1000
    macro avg       0.88      0.88      0.88      1000
 weighted avg       0.88      0.88      0.88      1000


<Figure size 640x480 with 2 Axes>
print("K-nearest neighbors")
print(f"Classification report for classifier {knn_clf}:\n"
      f"{metrics.classification_report(y_test, knn_prediction)}\n")

disp = ConfusionMatrixDisplay.from_estimator(knn_clf, X_test_scaled, y_test, xticks_rotation=45)
disp.figure_.suptitle("Confusion Matrix: KNN")
plt.tight_layout()
plt.show()
K-nearest neighbors
Classification report for classifier KNeighborsClassifier():
               precision    recall  f1-score   support

   earthquake       0.78      0.84      0.81       250
    explosion       0.79      0.68      0.73       250
        noise       0.92      0.91      0.92       250
surface event       0.83      0.89      0.86       250

     accuracy                           0.83      1000
    macro avg       0.83      0.83      0.83      1000
 weighted avg       0.83      0.83      0.83      1000


<Figure size 640x480 with 2 Axes>
print("Random Forest")
print(f"Classification report for classifier {rf_clf}:\n"
      f"{metrics.classification_report(y_test, rf_prediction)}\n")

disp = ConfusionMatrixDisplay.from_estimator(rf_clf, X_test, y_test, xticks_rotation=45)
disp.figure_.suptitle("Confusion Matrix: Random Forest")
plt.tight_layout()
plt.show()
Random Forest
Classification report for classifier RandomForestClassifier(random_state=42):
               precision    recall  f1-score   support

   earthquake       0.83      0.87      0.85       250
    explosion       0.88      0.80      0.83       250
        noise       0.93      0.94      0.93       250
surface event       0.90      0.94      0.92       250

     accuracy                           0.88      1000
    macro avg       0.88      0.88      0.88      1000
 weighted avg       0.88      0.88      0.88      1000


<Figure size 640x480 with 2 Axes>

Quelles paires de classes sont le plus confondues ? Comparez les termes hors diagonale des trois matrices de confusion. Un tableau croisant les étiquettes vraies et les prédictions de la forêt aléatoire rend les comptes faciles à lire.

pd.crosstab(y_test, rf_prediction, rownames=["true"], colnames=["predicted"])
Loading...

La confusion dominante oppose explosions et séismes. C’est physiquement plausible : les tirs de carrière et les séismes superficiels excitent un contenu fréquentiel similaire aux distances régionales, si bien que leurs caractéristiques de forme d’onde se recouvrent.

8. Courbes ROC un-contre-tous

Les courbes ROC sont définies pour les problèmes binaires. Pour un problème multi-classes, nous utilisons la stratégie un-contre-tous (one-vs-rest) : binariser les étiquettes (une colonne par classe) et ajuster un classifieur binaire par classe.

Pour garder un découpage identique au découpage canonique, nous binarisons les y_train et y_test obtenus ci-dessus plutôt que de redécouper les données.

from sklearn.multiclass import OneVsRestClassifier
from sklearn.preprocessing import label_binarize
from sklearn import svm
from sklearn.metrics import roc_curve, auc

classes = ['earthquake', 'explosion', 'noise', 'surface event']
y_train_bin = label_binarize(y_train, classes=classes)
y_test_bin = label_binarize(y_test, classes=classes)

ovr_classifier = OneVsRestClassifier(svm.SVC(kernel='linear'))
y_score = ovr_classifier.fit(X_train_scaled, y_train_bin).decision_function(X_test_scaled)

plt.figure(figsize=(7, 6))
plt.plot([0, 1], [0, 1], 'k--', label='chance')
for i, name in enumerate(classes):
    fpr, tpr, _ = roc_curve(y_test_bin[:, i], y_score[:, i])
    plt.plot(fpr, tpr, label=f'{name} (AUC = {auc(fpr, tpr):.2f})')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.grid(True)
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('One-vs-rest ROC curves, linear SVC')
plt.legend(loc="lower right")
<Figure size 700x600 with 1 Axes>

Classement du cours

Entraînez le classifieur de votre choix sur X_train. L’ingénierie de caractéristiques est autorisée. Ne regardez pas y_test pendant le développement : sélectionnez votre modèle et vos hyperparamètres par validation croisée sur le seul ensemble d’entraînement — en notant les candidats sur des sous-ensembles de validation tournants taillés dans les données d’entraînement (leçon 3.8).

Quand vous avez terminé :

  1. Prédisez sur le X_test canonique (la cellule de découpage de la section 3 le définit).
  2. Sauvegardez vos prédictions dans results/predictions_<uwnetid>.csv au format ci-dessous.
  3. Soumettez le fichier par pull request au dépôt du cours. La CI calcule le macro-F1 — les scores F1 par classe, moyennés en donnant le même poids à chaque classe — sur les étiquettes tenues à l’écart, et publie un classement.

La colonne row_id est la position de la ligne dans la table concaténée canonique ; le découpage la préserve, elle identifie donc chaque échantillon de test. La démonstration ci-dessous écrit un fichier de soumission à partir des prédictions de la forêt aléatoire.

import os
os.makedirs("results", exist_ok=True)
pred_df = pd.DataFrame({"row_id": X_test.index, "prediction": rf_prediction})
pred_df.to_csv("results/predictions_example.csv", index=False)
pred_df.head()
Loading...
References
  1. Kharita, A. (2024). Physical features for small sample of data (1000 events per class). Zenodo. 10.5281/ZENODO.14025693