ML · Chapitre 1
Machine learning 1 — Premiers pas avec Python et les données
Ce premier chapitre pose les fondations dont nous aurons besoin pour tout le reste du cours. Avant d'aborder les algorithmes d'apprentissage à proprement parler, il est indispensable de savoir manipuler du code Python dans un notebook, charger un jeu de données réel, en extraire quelques statistiques de base et le visualiser sous différentes formes.
Nous commencerons par décrire l'environnement de travail (Kaggle et les notebooks Jupyter), puis nous reverrons les notions de Python strictement nécessaires pour la suite. Nous découvrirons ensuite NumPy pour le calcul vectoriel, pandas pour les tableaux de données, et enfin un panorama d'outils de visualisation, depuis Matplotlib/Seaborn jusqu'à Plotly Express. Le chapitre se conclut sur une introduction à la programmation orientée objet, qui sera utile dès que nous manipulerons des modèles d'apprentissage.
L'objectif n'est pas de faire de vous des développeurs Python aguerris en une séance, mais de vous donner un socle commun, sur lequel les chapitres suivants pourront s'appuyer sans avoir à revenir sur la syntaxe.
L'environnement de travail : Kaggle et Jupyter
Tout le cours s'exécute sur Kaggle, une plateforme en ligne dédiée à la science des données et à l'apprentissage automatique. Travailler sur Kaggle présente plusieurs avantages pédagogiques : aucune installation locale n'est requise, l'environnement Python est standardisé (NumPy, pandas, scikit-learn, Plotly et toutes les bibliothèques utiles sont déjà disponibles), les jeux de données sont accessibles via un chemin standard, et tous les étudiants exécutent leur code dans un environnement strictement identique. Cela garantit que le code qui tourne sur votre notebook tourne aussi sur celui du voisin.
Concrètement, dans un notebook Kaggle, votre code Python s'exécute sur un serveur distant. Les données ajoutées au notebook sont disponibles en lecture seule sous /kaggle/input/nom-du-dataset/, et les fichiers que vous générez sont écrits dans /kaggle/working/.
Cellules, exécution et Markdown
Un notebook Jupyter est un document interactif qui combine texte explicatif, formules mathématiques, code exécutable et visualisations. Il est composé de cellules de deux types : les cellules de code (Python exécutable, dont la dernière instruction est affichée automatiquement) et les cellules Markdown (texte, titres, formules, images).
Pour exécuter une cellule, on utilise les raccourcis clavier suivants :
- Shift + Entrée : exécute la cellule et passe à la suivante
- Ctrl + Entrée : exécute la cellule sans changer de position
- Alt + Entrée : exécute la cellule et insère une nouvelle cellule dessous
Les cellules peuvent en principe être exécutées dans n'importe quel ordre, mais les variables sont stockées dans l'ordre d'exécution réel. En pratique, mieux vaut donc exécuter le notebook de haut en bas, et tout réexécuter après une modification importante : c'est l'unique manière de garantir que le résultat affiché correspond bien au code écrit.
Les cellules Markdown supportent la syntaxe usuelle (# pour les titres, **gras**, *italique*, listes à tirets ou numérotées) ainsi que les formules mathématiques en LaTeX. Une formule en ligne s'écrit $ax + b$ ; une formule centrée s'encadre par $$ ... $$, par exemple :
Les images s'insèrent avec , le chemin pointant typiquement vers un dataset ou une URL externe.
Premier import des bibliothèques
Tout le code du chapitre commence par les imports suivants, qu'on retrouvera dans presque tous les notebooks du cours.
import numpy as np import matplotlib.pyplot as plt import pandas as pd import plotly.express as px
NumPy fournit le calcul numérique vectorisé, Matplotlib la base du tracé statique, pandas les structures de données tabulaires (les DataFrames), et Plotly Express une interface haut niveau pour les graphiques interactifs. Les alias (np, pd, px) sont conventionnels et il est important de les respecter pour rester lisible par toute la communauté.
Rappels essentiels de Python
Avant d'aller plus loin, faisons un tour rapide des constructions Python qu'il faut absolument maîtriser. Python est un langage à typage dynamique : le type est associé à la valeur, pas à la variable. On peut donc écrire :
a = 3 # int b = 2.5 # float c = "Python" # str d = True # bool
Les types les plus courants sont int, float, str et bool. On utilise aussi très souvent les listes, qui stockent une collection ordonnée d'éléments :
values = [1, 2, 3, 4, 5] values[0] # premier élément values[-1] # dernier élément values[1:4] # sous-liste (slice)
L'indexation commence à 0, et les indices négatifs partent de la fin. Les slices du type values[1:4] retournent une nouvelle liste contenant les éléments d'indices 1, 2 et 3 (la borne supérieure est exclue).
Boucles et structures conditionnelles
La boucle for parcourt n'importe quelle collection :
for v in values: print(v)
Pour générer une suite d'entiers, on combine généralement for avec la fonction range, qui s'arrête avant la valeur finale et accepte un pas optionnel :
for i in range(5): # 0, 1, 2, 3, 4 print(i) for i in range(2, 6): # 2, 3, 4, 5 print(i) for i in range(0, 10, 2): # 0, 2, 4, 6, 8 print(i)
range peut également être convertie directement en liste avec list(range(0, 10, 2)). La boucle while répète un bloc tant qu'une condition est vraie, à condition que la condition finisse par devenir fausse pour éviter les boucles infinies :
count = 0 while count < 5: print(count) count += 1
Les structures conditionnelles utilisent if, elif et else, et les conditions peuvent être combinées via les opérateurs booléens and, or et not :
if a > 0: print("a est positif") elif a == 0: print("a est nul") else: print("a est négatif")
En Python, l'indentation est syntaxiquement obligatoire. Toute incohérence dans le nombre d'espaces utilisés provoque une erreur. C'est elle qui délimite les blocs : pas d'accolades, pas de
end, juste l'alignement.
Fonctions, modules et commentaires
Une fonction se définit avec le mot-clé def :
def square(x): return x ** 2
Les fonctionnalités plus avancées sont fournies par des bibliothèques que l'on importe au début du fichier (import math, import numpy as np). Les commentaires sont introduits par # et servent à documenter le code.
Vecteurs, matrices et calcul vectorisé avec NumPy
NumPy est la brique de base du calcul numérique en Python. Il introduit le type ndarray, qui représente aussi bien des vecteurs (tableaux 1D) que des matrices (tableaux 2D), voire des tenseurs de dimension supérieure.
x = np.array([1, 2, 3, 4]) X = np.array([ [1, 2], [3, 4], [5, 6] ])
Ici x est un vecteur de longueur 4, et X une matrice de 3 lignes et 2 colonnes. La forme d'un tableau est accessible par l'attribut shape : un vecteur a une forme (n,) et une matrice une forme (n_lignes, n_colonnes). Vérifier la shape est un réflexe indispensable pour s'assurer que les opérations sont compatibles.
Changer la forme avec reshape
La méthode reshape modifie l'organisation d'un tableau sans en changer les valeurs, à condition que le nombre total d'éléments reste identique.
x = np.arange(1, 11) X = x.reshape(5, 2)
Le vecteur x contient dix éléments ; on le réorganise en une matrice de cinq lignes et deux colonnes. C'est très fréquent en machine learning, où il faut souvent passer d'une représentation linéaire à une représentation tabulaire.
Sommes, moyennes et notion d'axe
NumPy propose une famille de fonctions vectorisées qui opèrent globalement ou le long d'un axe :
np.sum(x) np.mean(x) np.sum(X, axis=0) # somme par colonne np.sum(X, axis=1) # somme par ligne
L'axe 0 parcourt les lignes (donc on agrège les lignes pour obtenir un résultat par colonne), tandis que l'axe 1 parcourt les colonnes (résultat par ligne). C'est une source classique de confusion qu'il vaut mieux clarifier dès maintenant.
Produit scalaire et produit matriciel
Le produit scalaire entre deux vecteurs est l'opération centrale de presque tous les modèles linéaires :
u = np.array([1, 2, 3]) v = np.array([4, 5, 6]) np.dot(u, v)
Mathématiquement :
Pour une matrice multipliée par un vecteur, on utilise l'opérateur @ :
X = np.array([[1, 2], [3, 4], [5, 6]]) w = np.array([0.5, 1.0]) X @ w
Cette opération correspond au calcul fondamental d'une régression linéaire, , où chaque ligne de représente une observation et les coefficients du modèle.
Il faut absolument distinguer
*et@. L'expressionX * weffectue une multiplication élément par élément (en utilisant le mécanisme de broadcasting qui aligne les formes), tandis queX @ wréalise un véritable produit matriciel. Confondre les deux est une erreur classique en début d'apprentissage.
Plus généralement, NumPy vectorise toutes les opérations arithmétiques de base, ce qui évite d'écrire des boucles explicites :
x + 1 x * 2 x ** 2
Chaque opération s'applique simultanément à tous les éléments du tableau, ce qui est à la fois plus concis et drastiquement plus rapide qu'une boucle Python.
Charger et explorer un jeu de données avec pandas
Pandas est la bibliothèque de référence pour manipuler des données tabulaires en Python. Sa structure principale, le DataFrame, est essentiellement une table en mémoire, avec des lignes (les observations) et des colonnes nommées (les variables).
Lire un fichier CSV
La fonction pd.read_csv est la porte d'entrée habituelle pour transformer un fichier texte en DataFrame. Elle détecte automatiquement le séparateur, lit les valeurs, gère les valeurs manquantes et renvoie un objet DataFrame.
df = pd.read_csv('/kaggle/input/mini-datasets/co2_mini.csv')
Sur Kaggle, les datasets ajoutés au notebook sont automatiquement montés sous /kaggle/input/, chacun dans un sous-dossier nommé d'après son identifiant. Le moyen le plus fiable pour récupérer le chemin exact est de passer par le panneau Data à droite du notebook : on déplie le dataset, on survole le nom du fichier CSV, et une icône permet de copier directement le chemin complet.
Les paramètres optionnels les plus utiles à connaître sont sep (caractère séparateur, utile lorsque le fichier utilise ; ou \t) et encoding (par exemple utf-8 pour les fichiers contenant des accents).
Le dataset co2_mini
Tout le chapitre s'appuie sur un petit jeu de données dérivé de « CO2 Emission by Vehicles ». Ce sous-ensemble ne retient que les véhicules utilisant de l'essence Super (type Z) afin de mettre en évidence une corrélation nette. Il contient deux colonnes :
consumption: la consommation, en L/100 kmco2: les émissions de CO₂, en g/km
Premier coup d'œil sur les données
Juste après un read_csv, le réflexe est d'inspecter les premières lignes pour vérifier que les noms de colonnes sont corrects, que les valeurs ressemblent à ce qu'on attend, et que les types sont cohérents.
df.head() # 5 lignes par défaut df.head(10) # 10 lignes
Sur notre jeu de données, on obtient bien deux colonnes numériques cohérentes. Si la sortie laissait apparaître des NaN inattendus ou des valeurs étranges, c'est qu'il faudrait revoir les paramètres du read_csv (sep, decimal, encoding, na_values).
Plusieurs méthodes permettent ensuite de cerner rapidement la taille et la nature du DataFrame :
df.shape # tuple (n_lignes, n_colonnes) df.shape[0] # nombre de lignes df.shape[1] # nombre de colonnes df.columns # noms des colonnes df.info() # types, valeurs non nulles, mémoire utilisée
df.shape retourne par exemple (3202, 2), ce qui se lit immédiatement : 3202 observations, 2 variables. La méthode info() est précieuse pour repérer d'un coup d'œil les valeurs manquantes, les colonnes non numériques, et les éventuelles conversions de type à effectuer.
Sélectionner des colonnes
Pandas distingue deux objets fondamentaux : la Series (un vecteur 1D, une seule colonne) et le DataFrame (un tableau 2D). Selon la syntaxe utilisée pour la sélection, on obtient l'un ou l'autre :
df['co2'] # Series (1D) df[['co2']] # DataFrame à une colonne (2D) df[['consumption', 'co2']] # DataFrame à plusieurs colonnes
Cette distinction n'est pas anecdotique : certaines fonctions, en particulier en machine learning, exigent un tableau 2D. On utilisera alors systématiquement la version df[[...]] avec doubles crochets. La notation pointée df.co2 fonctionne aussi mais elle est plus fragile : elle échoue si le nom de colonne contient des espaces, des caractères spéciaux, ou s'il entre en conflit avec un attribut de pandas.
Statistiques descriptives
Les statistiques de base peuvent se calculer aussi bien avec pandas qu'avec NumPy. Les méthodes pandas, attachées directement aux séries, gèrent automatiquement les valeurs manquantes :
df["co2"].sum() df["co2"].mean() df["co2"].std() df["co2"].min() df["co2"].max()
Les fonctions équivalentes de NumPy (np.sum, np.mean, np.std, np.min, np.max) acceptent aussi bien des tableaux NumPy que des séries pandas et donnent les mêmes résultats sur des données numériques sans valeurs manquantes.
Pour obtenir une synthèse de toutes les colonnes numériques en une seule instruction, on utilise describe :
df.describe()
La sortie regroupe pour chaque colonne :
count: le nombre de valeurs non nullesmean: la moyenne arithmétiquestd: l'écart-type, mesure de la dispersion autour de la moyenneminetmax: les valeurs extrêmes- les quartiles (25 %, 50 %, 75 %)
Les quartiles découpent les données triées en quatre parties égales. Le premier quartile est la valeur en dessous de laquelle se trouvent 25 % des observations ; le troisième quartile correspond aux 75 %. Le deuxième quartile (50 %) est la médiane, qui partage les données en deux moitiés égales. La différence entre et définit l'écart interquartile :
Cet intervalle contient les 50 % centraux des données et joue un rôle clé dans la détection de valeurs aberrantes.
Visualiser les données
Les statistiques agrégées ne suffisent pas : la visualisation permet de découvrir des structures, de repérer des anomalies et de mieux comprendre la distribution avant toute modélisation. Nous utilisons d'abord Seaborn, une bibliothèque construite au-dessus de Matplotlib qui propose une syntaxe orientée DataFrame et des graphiques statistiques expressifs.
Boxplot
Le boxplot synthétise une distribution numérique à partir de cinq statistiques : le minimum, , la médiane , , et le maximum. La boîte s'étend de à , le trait central matérialise la médiane, et les moustaches s'étendent jusqu'aux valeurs extrêmes considérées comme non aberrantes.
import seaborn as sns import matplotlib.pyplot as plt plt.figure(figsize=(6, 4)) sns.boxplot(y=df["co2"]) plt.title("Boxplot de la variable CO2") plt.show()
Une convention courante consiste à qualifier d'aberrante toute observation située au-delà de ou de . Ces points sont alors représentés individuellement. Attention : un point « aberrant » au sens statistique n'est pas nécessairement une erreur. Il peut résulter d'une erreur de mesure ou de saisie, mais aussi d'un événement rare et tout à fait légitime. Une analyse contextuelle est indispensable avant toute suppression.
Histogramme et estimation de densité
L'histogramme regroupe les observations dans des intervalles (bins) et compte combien tombent dans chacun. Le nombre de classes contrôle le compromis entre lissage et finesse : trop peu de bins masquent la structure de la distribution, trop nombreux amplifient le bruit.
plt.figure(figsize=(6, 4)) sns.histplot(data=df, x="co2", bins=100) plt.title("Distribution de la variable CO2") plt.show()
La Kernel Density Estimation (KDE) propose une alternative lissée : au lieu de découper en classes discrètes, elle estime une courbe continue dont l'aire totale vaut 1, et dont la hauteur reflète la densité locale d'observations.
plt.figure(figsize=(6, 4)) sns.kdeplot(data=df, x="co2", fill=True) plt.title("Estimation de densité (KDE)") plt.show()
Les deux représentations sont complémentaires et peuvent même se combiner :
plt.figure(figsize=(6, 4)) sns.histplot(data=df, x="co2", bins=100, kde=True) plt.title("Histogramme avec estimation de densité") plt.show()
Violin plot
Le violin plot combine la philosophie du boxplot et celle de la KDE. Sa largeur en chaque hauteur correspond à la densité de la distribution : les zones larges signalent une forte concentration, les zones étroites des valeurs plus rares. L'option inner="box" superpose la médiane, les quartiles et les valeurs aberrantes au cœur du violon.
plt.figure(figsize=(6, 4)) sns.violinplot(y=df["co2"], inner="box") plt.title("Violin plot de la variable CO2") plt.show()
Au final, ces trois graphiques offrent des angles de vue différents sur une même variable : l'histogramme par classes discrètes, la KDE en continu, et le violin plot qui synthétise les deux. Avant toute modélisation, il est essentiel de prendre le temps de cette analyse exploratoire.
Nuage de points : la relation entre deux variables
Lorsque l'on s'intéresse à la relation entre deux variables numériques, l'outil de référence est le nuage de points (scatter plot). Chaque observation devient un point de coordonnées , ce qui permet d'observer simultanément la tendance globale, la dispersion et d'éventuels points atypiques.
plt.figure(figsize=(6, 4)) sns.scatterplot(data=df, x="consumption", y="co2") plt.title("Relation entre consommation et CO2") plt.show()
Sur le dataset co2_mini, on observe une orientation nettement croissante : plus la consommation est élevée, plus les émissions de CO₂ le sont aussi. C'est précisément ce type de relation linéaire que la régression cherchera à modéliser au chapitre suivant.
Graphiques interactifs avec Plotly Express
Plotly Express est une interface haut niveau qui produit des graphiques interactifs à partir d'un DataFrame en une seule instruction. Contrairement à Matplotlib ou Seaborn, les figures permettent par défaut le zoom, le déplacement et l'affichage de la valeur exacte au survol, ce qui les rend particulièrement adaptées à l'exploration et aux dashboards.
Chaque appel px.* retourne un objet fig, qu'il est ensuite possible de personnaliser. Les principaux types de graphiques s'utilisent ainsi :
fig = px.histogram(df, x="co2", nbins=100) fig.show() fig = px.box(df, y="co2") fig.show() fig = px.violin(df, y="co2", box=True, points="outliers") fig.show() fig = px.scatter(df, x="consumption", y="co2") fig.show()
Les options sont parlantes : nbins contrôle le nombre de classes pour l'histogramme, box=True ajoute un boxplot à l'intérieur du violon, points="outliers" ne dessine que les valeurs aberrantes individuellement. Pour superposer une droite de tendance (régression par moindres carrés) au nuage de points, il suffit d'ajouter trendline="ols" :
fig = px.scatter(df, x="consumption", y="co2", trendline="ols") fig.show()
Une fois la figure créée, on peut ajuster son apparence sans la reconstruire grâce à update_traces (qui modifie les objets graphiques) et update_layout (qui touche à la mise en page) :
fig = px.histogram(df, x="co2", nbins=50) fig.update_traces(opacity=0.7, marker_line_color="black", marker_line_width=1) fig.update_layout(width=600, height=400) fig.show()
Cette séparation entre la création et la personnalisation rend le code plus modulaire : on prototype rapidement avec px.*, puis on raffine.
Programmer avec des objets en Python
Beaucoup de bibliothèques de machine learning, à commencer par scikit-learn, exposent leurs algorithmes sous forme d'objets : on instancie un modèle, on l'entraîne par une méthode, on l'utilise pour prédire par une autre. Comprendre la programmation orientée objet, même superficiellement, est donc indispensable.
Une classe définit un nouveau type d'objet en regroupant des attributs (les données) et des méthodes (les fonctions qui agissent dessus). L'exemple le plus simple est une classe qui modélise une calculatrice avec une mémoire interne.
class Calculatrice: def __init__(self): self.memoire = 10 def add(self, x): self.memoire = self.memoire + x
Le mot-clé class introduit la définition. Le constructeur __init__ est appelé automatiquement lors de la création d'un nouvel objet et sert à initialiser les attributs. Le paramètre self, premier argument de toute méthode d'instance, désigne l'objet courant : c'est lui qui permet d'accéder aux attributs (self.memoire) et de les modifier.
Instancier et utiliser un objet
Pour créer un objet, on appelle simplement la classe comme une fonction :
c = Calculatrice()
Cette instruction réserve un espace mémoire pour le nouvel objet, exécute automatiquement __init__, et stocke une référence à cet objet dans la variable c. À ce stade, l'attribut c.memoire vaut 10.
L'accès aux attributs et l'appel des méthodes se font tous deux via la notation pointée :
c.memoire # lit l'attribut → 10 c.add(20) # appelle la méthode, met memoire à jour c.memoire # → 30
L'argument 20 est passé au paramètre x de la méthode. Le paramètre self, lui, est lié automatiquement par Python à l'objet c et n'a pas à être fourni explicitement. La modification effectuée par add est persistante : tant que l'objet existe, son état interne conserve sa nouvelle valeur.
Il faut bien distinguer la classe (la description abstraite, ici
Calculatrice) et l'objet (une instance concrète, commec). Une même classe peut donner naissance à autant d'objets indépendants que nécessaire, chacun avec son propre état.
Chaînes formatées (f-strings)
Pour afficher proprement des résultats numériques mêlés à du texte, Python propose les f-strings. La lettre f placée devant la chaîne indique que les expressions entre accolades doivent être évaluées :
year = 3 capital = 1234.567 print(f"Année {year} : capital = {capital:.2f}")
La sortie est Année 3 : capital = 1234.57. Le suffixe :.2f indique un nombre réel formaté avec deux chiffres après la virgule. Cette syntaxe sera très utile pour afficher proprement les résultats des simulations.
Exercices
Exercice 1 — Rappels Python (sans NumPy)
Cet exercice vérifie la maîtrise des constructions de base de Python nécessaires pour la suite.
- Créer une liste contenant les nombres de 1 à 10.
- À l'aide d'une boucle
for, afficher uniquement les nombres pairs de cette liste. - À l'aide d'une boucle
for in range, calculer la somme des entiers de 1 à 10. - Écrire une fonction
mean(values)qui prend en entrée une liste de nombres et retourne leur moyenne. - À l'aide d'une boucle
while, afficher les nombres de 5 à 1 dans l'ordre décroissant.
Exercice 2 — Opérations sur vecteurs et matrices (NumPy)
L'objectif est de manipuler des vecteurs et des matrices avec NumPy, sans utiliser de boucle, afin de préparer les calculs employés en régression linéaire. On considère le vecteur :
x = np.array([1, 2, 3, 4, 5, 6])
- Transformer
xen une matriceXde forme(3, 2)à l'aide dereshape. - Calculer la somme totale de
X, la somme par colonne, puis la somme par ligne. - Soit le vecteur de coefficients :
w = np.array([0.5, 1.0])
Calculer le produit matrice-vecteur X @ w.
- Comparer les résultats de
X * wetX @ wet expliquer brièvement la différence observée.
Exercice 3 — Programmation orientée objet : intérêts composés
On souhaite modéliser un capital placé à intérêt composé. Le capital évolue dans le temps selon la formule :
où est le capital à l'année et le taux d'intérêt annuel exprimé en décimal.
- Écrire une classe
CompoundInterestCalculatorcontenant :- un constructeur
__init__(capital_initial, taux) - un attribut
capitalreprésentant le capital courant - un attribut
tauxreprésentant le taux d'intérêt annuel
- un constructeur
- Ajouter une méthode
next_year()qui met à jour le capital en appliquant les intérêts. - Ajouter une méthode
simulate(n)qui simule l'évolution du capital surnannées et affiche le capital après chaque année (utiliser une f-string avec deux décimales). - Instancier un objet avec un capital initial de 1000 et un taux de 5 %.
- Simuler l'évolution du capital sur 5 années.
Pour aller plus loin
La meilleure manière de consolider les notions de ce chapitre est d'aller consulter les documentations officielles, qui sont remarquablement bien rédigées. La documentation NumPy couvre en profondeur le calcul vectorisé, les axes et le broadcasting. Le guide 10 minutes to pandas sur pandas.pydata.org est une excellente porte d'entrée vers les opérations courantes sur les DataFrames. Pour la visualisation, la galerie Seaborn et celle de Plotly Express regorgent d'exemples qu'on peut adapter à ses propres données. Enfin, le tutoriel officiel de Python reste la référence pour approfondir les classes et la programmation orientée objet.