Environnement et outils#

La science est ce que nous comprenons suffisamment bien pour l’expliquer à un ordinateur. L’art, c’est tout le reste.

Donald Knuth

Introduction#

L’apprentissage automatique repose sur un écosystème logiciel riche, dont Python est le pivot central. Ce chapitre présente les outils fondamentaux que tout praticien doit maitriser : le calcul numérique avec NumPy, la manipulation de données avec Pandas, la visualisation avec Matplotlib et Seaborn, et l’apprentissage automatique avec Scikit-learn. Nous verrons également les bonnes pratiques de reproductibilité.

Pourquoi Python ?#

Python s’est impose comme le langage de référence en apprentissage automatique pour plusieurs raisons :

  • Lisibilité : la syntaxe est proche du pseudocode, ce qui facilite le prototypage rapide.

  • Ecosystème scientifique : NumPy, SciPy, Pandas, Matplotlib, Scikit-learn, PyTorch, TensorFlow forment un écosystème cohérent et mature.

  • Intéroperabilité : les bibliothèques critiques (NumPy, SciPy) sont implémentées en C/Fortran, ce qui offre des performances proches du code natif.

  • Communauté : la communauté scientifique et industrielle est massive, assurant documentation, tutoriels et support.

import sys
print(f"Python {sys.version}")
Python 3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0]

NumPy#

NumPy (Numerical Python) est la brique fondamentale du calcul scientifique en Python. Il fournit le type ndarray, un tableau multidimensionnel homogène, ainsi qu’un ensemble complet d’operations vectorisées.

import numpy as np
print(f"NumPy {np.__version__}")
NumPy 2.3.5

Tableaux multidimensionnels#

Définition 1 (ndarray)

Un ndarray (ou tableau NumPy) est une structure de données représentant un tableau multidimensionnel homogène. Il est caracterisé par :

  • son type (dtype) : le type de chaque élément (ex. float64, int32)

  • sa forme (shape) : un tuple \((n_1, n_2, \ldots, n_k)\) donnant la taille de chaque dimension

  • son rang (ndim) : le nombre de dimensions \(k\)

Formellement, un ndarray de forme \((n_1, \ldots, n_k)\) représente un élément de \(\mathbb{R}^{n_1 \times n_2 \times \cdots \times n_k}\) (ou d’un autre type numérique).

# Création de tableaux
a = np.array([1, 2, 3, 4, 5])
print(f"Vecteur : {a}")
print(f"Shape : {a.shape}, dtype : {a.dtype}, ndim : {a.ndim}")
Vecteur : [1 2 3 4 5]
Shape : (5,), dtype : int64, ndim : 1
# Matrice 2D
M = np.array([[1, 2, 3],
              [4, 5, 6]])
print(f"Matrice :\n{M}")
print(f"Shape : {M.shape}, ndim : {M.ndim}")
Matrice :
[[1 2 3]
 [4 5 6]]
Shape : (2, 3), ndim : 2
# Fonctions de création courantes
zeros = np.zeros((3, 4))          # matrice 3x4 de zeros
ones = np.ones((2, 3))            # matrice 2x3 de uns
eye = np.eye(3)                   # matrice identité 3x3
lin = np.linspace(0, 1, 5)        # 5 points équidistants dans [0, 1]
arange = np.arange(0, 10, 2)     # équivalent de range, pas de 2
rand = np.random.randn(3, 3)     # matrice 3x3 gaussienne

print(f"linspace : {lin}")
print(f"arange   : {arange}")
print(f"Identite :\n{eye}")
linspace : [0.   0.25 0.5  0.75 1.  ]
arange   : [0 2 4 6 8]
Identite :
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

Operations vectorisées#

Proposition 1 (Operations élément par élément)

Soit \(A, B\) deux tableaux de même forme \((n_1, \ldots, n_k)\). Les opérations arithmétiques \(+, -, \times, /\) sont définies élément par élément :

\[(A \circ B)_{i_1, \ldots, i_k} = A_{i_1, \ldots, i_k} \circ B_{i_1, \ldots, i_k}\]

\(\circ \in \{+, -, \times, /\}\). Ces opérations sont vectorisées : elles sont executées en C, sans boucle Python explicite.

a = np.array([1.0, 2.0, 3.0, 4.0])
b = np.array([10.0, 20.0, 30.0, 40.0])

print(f"a + b = {a + b}")
print(f"a * b = {a * b}")       # produit élément par élément
print(f"a / b = {a / b}")
print(f"a ** 2 = {a ** 2}")     # puissance élément par élément
print(f"np.sqrt(a) = {np.sqrt(a)}")
a + b = [11. 22. 33. 44.]
a * b = [ 10.  40.  90. 160.]
a / b = [0.1 0.1 0.1 0.1]
a ** 2 = [ 1.  4.  9. 16.]
np.sqrt(a) = [1.         1.41421356 1.73205081 2.        ]

Remarque 1

Le produit * entre deux tableaux NumPy est le produit élément par élément (produit de Hadamard), et non le produit matriciel. Pour le produit matriciel, on utilise l’opérateur @ ou la fonction np.dot.

# Produit matriciel
A = np.array([[1, 2],
              [3, 4]])
B = np.array([[5, 6],
              [7, 8]])

print(f"Produit élément par élément A * B :\n{A * B}")
print(f"\nProduit matriciel A @ B :\n{A @ B}")
Produit élément par élément A * B :
[[ 5 12]
 [21 32]]

Produit matriciel A @ B :
[[19 22]
 [43 50]]

Broadcasting#

Définition 2 (Broadcasting)

Le broadcasting est le mécanisme par lequel NumPy permet d’effectuer des opérations entre tableaux de formes différentes. Deux tableaux sont compatibles pour le broadcasting si, en parcourant leurs dimensions de droite à gauche, chaque paire de dimensions vérifie l’une des conditions suivantes :

  1. Les dimensions sont égales : \(n_i = m_i\)

  2. L’une des dimensions vaut 1 : \(n_i = 1\) ou \(m_i = 1\)

Si l’un des tableaux a moins de dimensions, il est préalablement complété par des dimensions de taille 1 à gauche.

Formellement, si \(A\) a pour forme \((n_1, \ldots, n_k)\) et \(B\) a pour forme \((m_1, \ldots, m_k)\) (après alignement), alors le resultat \(C = A \circ B\) a pour forme \((\max(n_1, m_1), \ldots, \max(n_k, m_k))\).

Exemple 1 (Broadcasting en pratique)

Si \(A\) est de forme \((3, 4)\) et \(b\) de forme \((4,)\) :

  • \(b\) est d’abord vu comme ayant la forme \((1, 4)\)

  • Les formes \((3, 4)\) et \((1, 4)\) sont compatibles (\(3 \neq 1\) mais la seconde dimension est égale)

  • Le résultat est de forme \((3, 4)\) : \(b\) est « répliqué » le long de la première dimension

# Broadcasting : ajouter un vecteur à chaque ligne d'une matrice
M = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]])
v = np.array([10, 20, 30])

print(f"M + v :\n{M + v}")
# Equivalent à : chaque ligne de M reçoit l'addition de v
M + v :
[[11 22 33]
 [14 25 36]
 [17 28 39]]
# Broadcasting : produit extérieur
x = np.array([1, 2, 3]).reshape(3, 1)  # colonne (3, 1)
y = np.array([10, 20, 30])             # ligne  (3,)

print(f"Produit extérieur x * y :\n{x * y}")
# x de forme (3, 1) et y de forme (3,) -> (1, 3) -> resultat (3, 3)
Produit extérieur x * y :
[[10 20 30]
 [20 40 60]
 [30 60 90]]

Indexation et slicing#

Remarque 2

L’indexation NumPy est une généralisation puissante de l’indexation Python. Outre l’indexation classique par position et le slicing, NumPy supporte l”indexation booléenne (masques) et l”indexation avancée (fancy indexing) par tableaux d’indices.

a = np.arange(10)
print(f"a = {a}")
print(f"a[3] = {a[3]}")
print(f"a[2:7] = {a[2:7]}")
print(f"a[::2] = {a[::2]}")       # un élément sur deux
print(f"a[::-1] = {a[::-1]}")     # inversion
a = [0 1 2 3 4 5 6 7 8 9]
a[3] = 3
a[2:7] = [2 3 4 5 6]
a[::2] = [0 2 4 6 8]
a[::-1] = [9 8 7 6 5 4 3 2 1 0]
# Indexation booléenne (masques)
a = np.array([1, -2, 3, -4, 5, -6])
masque = a > 0
print(f"a = {a}")
print(f"Masque (a > 0) : {masque}")
print(f"Eléments positifs : {a[masque]}")

# Modification conditionnelle
a[a < 0] = 0
print(f"Après mise à zero des négatifs : {a}")
a = [ 1 -2  3 -4  5 -6]
Masque (a > 0) : [ True False  True False  True False]
Eléments positifs : [1 3 5]
Après mise à zero des négatifs : [1 0 3 0 5 0]
# Indexation 2D
M = np.arange(12).reshape(3, 4)
print(f"M :\n{M}")
print(f"M[1, 2] = {M[1, 2]}")         # élément (ligne 1, colonne 2)
print(f"M[:, 1] = {M[:, 1]}")         # colonne 1
print(f"M[0:2, 1:3] :\n{M[0:2, 1:3]}")  # sous-matrice
M :
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
M[1, 2] = 6
M[:, 1] = [1 5 9]
M[0:2, 1:3] :
[[1 2]
 [5 6]]

Fonctions d’agrégation#

M = np.random.randn(4, 3)
print(f"M :\n{M}\n")
print(f"Somme totale       : {M.sum():.4f}")
print(f"Somme par colonne  : {M.sum(axis=0)}")  # aggrégation le long des lignes
print(f"Somme par ligne    : {M.sum(axis=1)}")  # aggrégation le long des colonnes
print(f"Moyenne            : {M.mean():.4f}")
print(f"Ecart-type         : {M.std():.4f}")
print(f"Min, Max           : {M.min():.4f}, {M.max():.4f}")
print(f"Argmin (global)    : {M.argmin()}")
M :
[[-0.30354041 -0.86186641  0.63670767]
 [-0.56290309 -1.62428139  1.07495726]
 [-1.92541686 -1.07383455 -0.55276742]
 [ 0.83407289 -0.29538444 -0.7677665 ]]

Somme totale       : -5.4220
Somme par colonne  : [-1.95778748 -3.85536678  0.391131  ]
Somme par ligne    : [-0.52869915 -1.11222722 -3.55201883 -0.22907805]
Moyenne            : -0.4518
Ecart-type         : 0.8869
Min, Max           : -1.9254, 1.0750
Argmin (global)    : 6

Algèbre linéaire#

NumPy fournit un sous-module numpy.linalg pour les opérations d’algèbre linéaire courantes.

A = np.array([[2, 1],
              [1, 3]])

# Déterminant
det = np.linalg.det(A)
print(f"det(A) = {det:.1f}")

# Valeurs propres et vecteurs propres
valeurs, vecteurs = np.linalg.eig(A)
print(f"Valeurs propres : {valeurs}")
print(f"Vecteurs propres :\n{vecteurs}")

# Inverse
A_inv = np.linalg.inv(A)
print(f"A^(-1) :\n{A_inv}")
print(f"Vérification A @ A^(-1) :\n{A @ A_inv}")
det(A) = 5.0
Valeurs propres : [1.38196601 3.61803399]
Vecteurs propres :
[[-0.85065081 -0.52573111]
 [ 0.52573111 -0.85065081]]
A^(-1) :
[[ 0.6 -0.2]
 [-0.2  0.4]]
Vérification A @ A^(-1) :
[[ 1.00000000e+00  0.00000000e+00]
 [-5.55111512e-17  1.00000000e+00]]
# Résolution d'un système linéaire Ax = b
A = np.array([[3, 1], [1, 2]])
b = np.array([9, 8])
x = np.linalg.solve(A, b)
print(f"Solution de Ax = b : x = {x}")
print(f"Vérification : A @ x = {A @ x}")
Solution de Ax = b : x = [2. 3.]
Vérification : A @ x = [9. 8.]

Pandas#

Pandas est la bibliothèque de référence pour la manipulation et l’analyse de données tabulaires en Python. Elle fournit deux structures fondamentales : les Series et les DataFrame.

import pandas as pd
print(f"Pandas {pd.__version__}")
Pandas 3.0.0

Series#

Définition 3 (Series)

Une Series Pandas est un tableau unidimensionnel étiqueté. Chaque élément est associé à un index (étiquette). Formellement, une Series est une application \(f : I \to V\)\(I\) est l’ensemble des étiquettes (l’index) et \(V\) l’ensemble des valeurs, toutes de même type.

# Création d'une Series
s = pd.Series([10, 20, 30, 40], index=['a', 'b', 'c', 'd'])
print(s)
print(f"\nIndex : {s.index.tolist()}")
print(f"Valeurs : {s.values}")
print(f"s['b'] = {s['b']}")
a    10
b    20
c    30
d    40
dtype: int64

Index : ['a', 'b', 'c', 'd']
Valeurs : [10 20 30 40]
s['b'] = 20

DataFrame#

Définition 4 (DataFrame)

Un DataFrame Pandas est un tableau bidimensionnel étiqueté : chaque ligne possède un index (etiquette de ligne) et chaque colonne possède un nom (étiquette de colonne). Chaque colonne est une Series. Formellement, un DataFrame de \(n\) lignes et \(p\) colonnes représente une matrice de données \(X \in \mathcal{V}_1 \times \cdots \times \mathcal{V}_p\) où chaque \(\mathcal{V}_j\) est le domaine de la \(j\)-ième variable (colonne). Les colonnes peuvent avoir des types différents (numérique, catégoriel, texte, dates…).

# Création d'un DataFrame
df = pd.DataFrame({
    'nom': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'],
    'age': [25, 30, 35, 28, 32],
    'ville': ['Paris', 'Lyon', 'Paris', 'Marseille', 'Lyon'],
    'salaire': [45000, 52000, 61000, 48000, 55000]
})
print(df)
       nom  age      ville  salaire
0    Alice   25      Paris    45000
1      Bob   30       Lyon    52000
2  Charlie   35      Paris    61000
3    Diana   28  Marseille    48000
4      Eve   32       Lyon    55000
# Informations sur le DataFrame
print(df.info())
print(f"\nStatistiques descriptives :\n{df.describe()}")
<class 'pandas.DataFrame'>
RangeIndex: 5 entries, 0 to 4
Data columns (total 4 columns):
 #   Column   Non-Null Count  Dtype
---  ------   --------------  -----
 0   nom      5 non-null      str  
 1   age      5 non-null      int64
 2   ville    5 non-null      str  
 3   salaire  5 non-null      int64
dtypes: int64(2), str(2)
memory usage: 292.0 bytes
None

Statistiques descriptives :
             age       salaire
count   5.000000      5.000000
mean   30.000000  52200.000000
std     3.807887   6220.932406
min    25.000000  45000.000000
25%    28.000000  48000.000000
50%    30.000000  52000.000000
75%    32.000000  55000.000000
max    35.000000  61000.000000

Lecture de données#

Remarque 3

Pandas propose de nombreuses fonctions de lecture : read_csv, read_excel, read_json, read_sql, read_parquet, etc. Le format CSV (Comma-Separated Values) est le plus courant pour les jeux de données d’apprentissage automatique.

# Simulation : création d'un CSV en mémoire et lecture
from io import StringIO

csv_data = """nom,age,score,reussite
Alice,25,85.5,True
Bob,30,72.0,False
Charlie,35,91.2,True
Diana,28,68.3,False
Eve,32,88.7,True
Frank,27,75.1,True
"""

df = pd.read_csv(StringIO(csv_data))
print(df)
print(f"\nTypes :\n{df.dtypes}")
       nom  age  score  reussite
0    Alice   25   85.5      True
1      Bob   30   72.0     False
2  Charlie   35   91.2      True
3    Diana   28   68.3     False
4      Eve   32   88.7      True
5    Frank   27   75.1      True

Types :
nom             str
age           int64
score       float64
reussite       bool
dtype: object

Filtrage et selection#

# Selection de colonnes
print("Colonne 'score' :")
print(df['score'])

print("\nPlusieurs colonnes :")
print(df[['nom', 'score']])
Colonne 'score' :
0    85.5
1    72.0
2    91.2
3    68.3
4    88.7
5    75.1
Name: score, dtype: float64

Plusieurs colonnes :
       nom  score
0    Alice   85.5
1      Bob   72.0
2  Charlie   91.2
3    Diana   68.3
4      Eve   88.7
5    Frank   75.1
# Filtrage par condition (indexation booléenne)
reussis = df[df['reussite'] == True]
print("Etudiants ayant réussi :")
print(reussis)

# Conditions combinées
bons = df[(df['score'] > 80) & (df['age'] < 35)]
print("\nScore > 80 et age < 35 :")
print(bons)
Etudiants ayant réussi :
       nom  age  score  reussite
0    Alice   25   85.5      True
2  Charlie   35   91.2      True
4      Eve   32   88.7      True
5    Frank   27   75.1      True

Score > 80 et age < 35 :
     nom  age  score  reussite
0  Alice   25   85.5      True
4    Eve   32   88.7      True
# loc (par étiquettes) et iloc (par position)
print(f"df.loc[0, 'nom'] = {df.loc[0, 'nom']}")
print(f"df.iloc[0, 1] = {df.iloc[0, 1]}")
print(f"\nLignes 1 a 3, colonnes 'nom' et 'score' :")
print(df.loc[1:3, ['nom', 'score']])
df.loc[0, 'nom'] = Alice
df.iloc[0, 1] = 25

Lignes 1 a 3, colonnes 'nom' et 'score' :
       nom  score
1      Bob   72.0
2  Charlie   91.2
3    Diana   68.3

Groupby et agrégation#

Remarque 4

L’operation groupby implémente le paradigme split-apply-combine : les données sont partitionnées en groupes selon une ou plusieurs clés, une fonction d’agrégation est appliquée à chaque groupe, et les résultats sont recombinés en un seul objet.

df_villes = pd.DataFrame({
    'ville': ['Paris', 'Lyon', 'Paris', 'Marseille', 'Lyon', 'Paris'],
    'departement': ['75', '69', '75', '13', '69', '75'],
    'ventes': [120, 80, 150, 90, 110, 130],
    'mois': ['jan', 'jan', 'fev', 'jan', 'fev', 'mar']
})

print("Données :")
print(df_villes)

print("\nVentes moyennes par ville :")
print(df_villes.groupby('ville')['ventes'].mean())

print("\nStatistiques par ville :")
print(df_villes.groupby('ville')['ventes'].agg(['mean', 'sum', 'count']))
Données :
       ville departement  ventes mois
0      Paris          75     120  jan
1       Lyon          69      80  jan
2      Paris          75     150  fev
3  Marseille          13      90  jan
4       Lyon          69     110  fev
5      Paris          75     130  mar

Ventes moyennes par ville :
ville
Lyon          95.000000
Marseille     90.000000
Paris        133.333333
Name: ventes, dtype: float64

Statistiques par ville :
                 mean  sum  count
ville                            
Lyon        95.000000  190      2
Marseille   90.000000   90      1
Paris      133.333333  400      3

Merge (jointures)#

# Deux tables à joindre
clients = pd.DataFrame({
    'client_id': [1, 2, 3, 4],
    'nom': ['Alice', 'Bob', 'Charlie', 'Diana']
})

commandes = pd.DataFrame({
    'commande_id': [101, 102, 103, 104, 105],
    'client_id': [1, 2, 1, 3, 5],
    'montant': [50.0, 30.0, 75.0, 120.0, 45.0]
})

# Inner join (par defaut)
result = pd.merge(clients, commandes, on='client_id')
print("Inner join :")
print(result)

# Left join
result_left = pd.merge(clients, commandes, on='client_id', how='left')
print("\nLeft join :")
print(result_left)
Inner join :
   client_id      nom  commande_id  montant
0          1    Alice          101     50.0
1          1    Alice          103     75.0
2          2      Bob          102     30.0
3          3  Charlie          104    120.0

Left join :
   client_id      nom  commande_id  montant
0          1    Alice        101.0     50.0
1          1    Alice        103.0     75.0
2          2      Bob        102.0     30.0
3          3  Charlie        104.0    120.0
4          4    Diana          NaN      NaN

Données manquantes#

df_nan = pd.DataFrame({
    'A': [1, 2, np.nan, 4],
    'B': [np.nan, 2, 3, 4],
    'C': [1, 2, 3, np.nan]
})

print("Données avec valeurs manquantes :")
print(df_nan)
print(f"\nNombre de NaN par colonne :\n{df_nan.isna().sum()}")

# Remplissage
print(f"\nRemplissage par la moyenne :\n{df_nan.fillna(df_nan.mean())}")

# Suppression des lignes avec NaN
print(f"\nSuppression des lignes avec NaN :\n{df_nan.dropna()}")
Données avec valeurs manquantes :
     A    B    C
0  1.0  NaN  1.0
1  2.0  2.0  2.0
2  NaN  3.0  3.0
3  4.0  4.0  NaN

Nombre de NaN par colonne :
A    1
B    1
C    1
dtype: int64

Remplissage par la moyenne :
          A    B    C
0  1.000000  3.0  1.0
1  2.000000  2.0  2.0
2  2.333333  3.0  3.0
3  4.000000  4.0  2.0

Suppression des lignes avec NaN :
     A    B    C
1  2.0  2.0  2.0

Matplotlib et Seaborn#

Matplotlib est la bibliothèque de visualisation de base en Python. Seaborn est construite au-dessus de Matplotlib et propose des graphiques statistiques de haut niveau avec un style soigné.

import matplotlib.pyplot as plt
import matplotlib
print(f"Matplotlib {matplotlib.__version__}")
Matplotlib 3.10.8

Graphiques de base avec Matplotlib#

# Courbe simple
x = np.linspace(0, 2 * np.pi, 200)
y1 = np.sin(x)
y2 = np.cos(x)

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, y1, label=r'$\sin(x)$', color='steelblue')
ax.plot(x, y2, label=r'$\cos(x)$', color='coral', linestyle='--')
ax.set_xlabel('$x$')
ax.set_ylabel('$f(x)$')
ax.set_title('Fonctions trigonométriques')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
_images/828beba91862dde9ffebc6fad5c58104eab1ab549dc387af18d3450328669689.png
# Nuage de points
np.random.seed(42)
n = 100
x = np.random.randn(n)
y = 2 * x + 1 + np.random.randn(n) * 0.5

fig, ax = plt.subplots(figsize=(6, 5))
ax.scatter(x, y, alpha=0.6, edgecolors='k', linewidths=0.5)
ax.set_xlabel('$x$')
ax.set_ylabel('$y$')
ax.set_title('Nuage de points : $y = 2x + 1 + \\varepsilon$')
plt.tight_layout()
plt.show()
_images/060b8708c5a9254303651285a23844dc9e1a92db5110c4331a17cb02c03c71bd.png
# Sous-graphiques (subplots)
fig, axes = plt.subplots(3, 1, figsize=(9, 11))

# Histogramme
data = np.random.randn(1000)
axes[0].hist(data, bins=30, color='steelblue', edgecolor='white', alpha=0.8)
axes[0].set_title('Histogramme')

# Diagramme en barres
categories = ['A', 'B', 'C', 'D']
valeurs = [23, 45, 56, 78]
axes[1].bar(categories, valeurs, color=['#3498db', '#e74c3c', '#2ecc71', '#f39c12'])
axes[1].set_title('Diagramme en barres')

# Boite a moustaches
data_box = [np.random.randn(50) + i for i in range(4)]
axes[2].boxplot(data_box, tick_labels=['G1', 'G2', 'G3', 'G4'])
axes[2].set_title('Boites à moustaches')

plt.tight_layout()
plt.show()
_images/14c7db645f848366931b66b86bcf552c2343e770ee340f2979f7817a6590af4e.png

Graphiques statistiques avec Seaborn#

import seaborn as sns
print(f"Seaborn {sns.__version__}")
sns.set_theme(style='whitegrid')
Seaborn 0.13.2
# Chargement d'un jeu de données intègre
tips = sns.load_dataset('tips')
print(tips.head())
   total_bill   tip     sex smoker  day    time  size
0       16.99  1.01  Female     No  Sun  Dinner     2
1       10.34  1.66    Male     No  Sun  Dinner     3
2       21.01  3.50    Male     No  Sun  Dinner     3
3       23.68  3.31    Male     No  Sun  Dinner     2
4       24.59  3.61  Female     No  Sun  Dinner     4
# Distribution
fig, axes = plt.subplots(2, 1, figsize=(9, 7))

sns.histplot(tips['total_bill'], kde=True, ax=axes[0], color='steelblue')
axes[0].set_title('Distribution de total_bill')

sns.boxplot(x='day', y='total_bill', data=tips, ax=axes[1])
axes[1].set_title('total_bill par jour')

plt.tight_layout()
plt.show()
_images/df835b1ba18b07c522fd9bcda995e38bb5e5ded9d6c7a3e3bd5023a9bae45bd7.png
# Relation entre variables
fig, axes = plt.subplots(2, 1, figsize=(9, 9))

sns.scatterplot(x='total_bill', y='tip', hue='smoker', data=tips, ax=axes[0])
axes[0].set_title('Pourboire vs. addition')

sns.heatmap(tips[['total_bill', 'tip', 'size']].corr(),
            annot=True, cmap='coolwarm', vmin=-1, vmax=1, ax=axes[1])
axes[1].set_title('Matrice de correlation')

plt.tight_layout()
plt.show()
_images/e6f92927261d85aef6db3404a71e66aed1ae9d32df44226566c9269f3ffb4d20.png
# Pairplot : vision globale des relations entre variables
sns.pairplot(tips, hue='smoker', height=2.5,
             plot_kws={'alpha': 0.6, 'edgecolor': 'k', 'linewidth': 0.3})
plt.suptitle('Pairplot du jeu de données tips', y=1.02)
plt.show()
_images/1e381dd7900675ec7a48ac6757274571d363ebf2375f74cc45a596de789c4707.png

Scikit-learn#

Scikit-learn est la bibliothèque de reference pour l’apprentissage automatique classique en Python. Sa force reside dans une API cohérente et une architecture modulaire.

import sklearn
print(f"Scikit-learn {sklearn.__version__}")
Scikit-learn 1.8.0

Philosophie et API#

Définition 5 (Estimateur (Scikit-learn))

Un estimateur est tout objet qui apprend à partir de données. Dans Scikit-learn, un estimateur est une classe Python implémentant la méthode fit(X, y=None) qui ajuste le modèle aux données d’entrainement.

  • Classifieurs et regresseurs (apprentissage supervisé) implémentent en plus predict(X) pour produire des prédictions.

  • Transformateurs implémentent transform(X) pour transformer les données (et souvent fit_transform(X) pour combiner les deux en une seule passe).

  • Pipelines chainent plusieurs transformateurs et un estimateur final.

Remarque 5

L’API uniforme de Scikit-learn suit toujours le même schema :

  1. Instancier le modèle avec ses hyperparamètres : model = MonModele(param=valeur)

  2. Entrainer sur les données : model.fit(X_train, y_train)

  3. Prédire sur de nouvelles données : y_pred = model.predict(X_test)

  4. Evaluer les performances : score = model.score(X_test, y_test)

Cette uniformité est l’un des atouts majeurs de la bibliothèque : changer de modèle ne modifie qu’une seule ligne de code.

Exemple complet : classification#

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, classification_report

# 1. Charger les données
iris = load_iris()
X, y = iris.data, iris.target
print(f"Forme de X : {X.shape}")
print(f"Classes : {iris.target_names}")
Forme de X : (150, 4)
Classes : ['setosa' 'versicolor' 'virginica']
# 2. Séparer en entrainement / test
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)
print(f"Entrainement : {X_train.shape[0]} echantillons")
print(f"Test         : {X_test.shape[0]} echantillons")
Entrainement : 105 echantillons
Test         : 45 echantillons
# 3. Prétraiter (standardisation)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)  # on utilise les stats de l'entrainement

print(f"Moyenne avant standardisation : {X_train[:, 0].mean():.2f}")
print(f"Moyenne après standardisation : {X_train_scaled[:, 0].mean():.6f}")
print(f"Ecart-type après standardisation : {X_train_scaled[:, 0].std():.6f}")
Moyenne avant standardisation : 5.87
Moyenne après standardisation : 0.000000
Ecart-type après standardisation : 1.000000
# 4. Entrainer un classifieur
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X_train_scaled, y_train)

# 5. Prédire et évaluer
y_pred = knn.predict(X_test_scaled)
print(f"Précision : {accuracy_score(y_test, y_pred):.2%}")
print(f"\nRapport de classification :\n{classification_report(y_test, y_pred, target_names=iris.target_names)}")
Précision : 91.11%

Rapport de classification :
              precision    recall  f1-score   support

      setosa       1.00      1.00      1.00        15
  versicolor       0.79      1.00      0.88        15
   virginica       1.00      0.73      0.85        15

    accuracy                           0.91        45
   macro avg       0.93      0.91      0.91        45
weighted avg       0.93      0.91      0.91        45

Pipelines#

Définition 6 (Pipeline (Scikit-learn))

Un Pipeline est une séquence ordonnée d’étapes, chacune étant un couple (nom, estimateur). Toutes les étapes sauf la dernière doivent être des transformateurs (implémenter transform). La dernière étape peut être un estimateur quelconque. L’appel à fit du pipeline enchaine les fit_transform des étapes intermédiaires, puis le fit de la dernière étape.

from sklearn.pipeline import Pipeline

# Pipeline = standardisation + KNN
pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('knn', KNeighborsClassifier(n_neighbors=5))
])

# En une seule ligne : entrainement et évaluation
pipe.fit(X_train, y_train)
score = pipe.score(X_test, y_test)
print(f"Score du pipeline : {score:.2%}")
Score du pipeline : 91.11%

Exemple : régression linéaire#

from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score

# Données synthétiques
np.random.seed(42)
X_reg = np.random.randn(100, 1) * 2
y_reg = 3 * X_reg.ravel() + 7 + np.random.randn(100) * 1.5

X_train_r, X_test_r, y_train_r, y_test_r = train_test_split(
    X_reg, y_reg, test_size=0.3, random_state=42
)

# Entrainement
lr = LinearRegression()
lr.fit(X_train_r, y_train_r)
y_pred_r = lr.predict(X_test_r)

print(f"Coefficient : {lr.coef_[0]:.3f} (attendu : 3)")
print(f"Ordonnée à l'origine : {lr.intercept_:.3f} (attendu : 7)")
print(f"R^2 : {r2_score(y_test_r, y_pred_r):.4f}")
print(f"RMSE : {np.sqrt(mean_squared_error(y_test_r, y_pred_r)):.4f}")
Coefficient : 2.880 (attendu : 3)
Ordonnée à l'origine : 7.139 (attendu : 7)
R^2 : 0.9087
RMSE : 1.3861
# Visualisation de la régression
fig, ax = plt.subplots(figsize=(7, 5))
ax.scatter(X_test_r, y_test_r, alpha=0.6, label='Observations', edgecolors='k', linewidths=0.5)
X_plot = np.linspace(X_reg.min(), X_reg.max(), 100).reshape(-1, 1)
ax.plot(X_plot, lr.predict(X_plot), color='red', linewidth=2, label='Régression linéaire')
ax.set_xlabel('$x$')
ax.set_ylabel('$y$')
ax.set_title(f'Régression linéaire : $y = {lr.coef_[0]:.2f}x + {lr.intercept_:.2f}$')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
_images/3c8e5109c0eaf5460b5fb7438fd0c208a448277c5c963aa58c2853563196d49a.png

Jupyter#

Les notebooks Jupyter sont l’environnement de travail privilegié pour l’exploration de données et le prototypage en apprentissage automatique.

Notebooks et kernels#

Définition 7 (Notebook Jupyter)

Un notebook Jupyter est un document intéractif composé d’une séquence de cellules. Chaque cellule est soit :

  • une cellule de code : contient du code exécutable (Python, R, Julia, etc.)

  • une cellule Markdown : contient du texte mis en forme, des équations LaTeX, des images

Le notebook est connecté à un kernel, un processus qui exécute le code. Le kernel maintient l’état (variables, imports) entre les exécutions de cellules.

Remarque 6

L’exécution des cellules dans un notebook n’est pas nécessairement séquentielle : l’utilisateur peut exécuter les cellules dans n’importe quel ordre. Cela peut mener à des états incohérents (variables définies dans un ordre différent de l’affichage). C’est une source classique de bugs ; il est recommandé de régulièrement redémarrer le kernel et de re-exécuter toutes les cellules depuis le debut (Restart & Run All).

Commandes magiques#

Les commandes magiques (magic commands) sont des extensions spécifiques à IPython/Jupyter qui facilitent le travail intéractif.

# %timeit : mesure du temps d'exécution (ligne)
%timeit np.dot(np.random.randn(100), np.random.randn(100))
7.48 μs ± 40.3 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
%%timeit
# Mesure du temps d'exécution (cellule entière)
a = np.random.randn(1000)
b = np.random.randn(1000)
c = a + b
46.2 μs ± 47.2 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

Remarque 7

Parmi les commandes magiques les plus utiles :

  • %timeit / %%timeit : benchmark

  • %matplotlib inline : affichage des graphiques dans le notebook

  • %who / %whos : lister les variables définies

  • %load_ext : charger une extension

  • %%writefile : écrire le contenu d’une cellule dans un fichier

  • %run : exécuter un script Python

Comparaison : boucle Python vs. NumPy vectorisé#

Un exemple classique qui illustre l’intérêt du calcul vectorisé.

n = 100_000
a = np.random.randn(n)
b = np.random.randn(n)

# Version boucle Python
def dot_python(a, b):
    s = 0.0
    for i in range(len(a)):
        s += a[i] * b[i]
    return s

# Comparaison
import time

start = time.perf_counter()
for _ in range(10):
    dot_python(a, b)
temps_python = (time.perf_counter() - start) / 10

start = time.perf_counter()
for _ in range(10):
    np.dot(a, b)
temps_numpy = (time.perf_counter() - start) / 10

print(f"Boucle Python : {temps_python*1000:.2f} ms")
print(f"NumPy np.dot  : {temps_numpy*1000:.4f} ms")
print(f"Accélération  : x{temps_python/temps_numpy:.0f}")
Boucle Python : 34.15 ms
NumPy np.dot  : 0.0682 ms
Accélération  : x500

Reproductibilité#

La reproductibilité est un enjeu majeur en apprentissage automatique. Un résultat scientifique doit pouvoir être reproduit par un tiers disposant du même code et des mêmes données.

Graines aléatoires#

Définition 8 (Graine aléatoire)

Une graine aléatoire (random seed) est un entier qui initialise le générateur de nombres pseudo-aléatoires (PRNG). Fixer la graine garantit que la même séquence de nombres sera produite à chaque exécution, assurant la reproductibilité des resultats.

# Sans graine : résultats différents à chaque exécution
print("Sans graine :")
print(np.random.randn(3))
print(np.random.randn(3))

# Avec graine : résultats identiques
print("\nAvec graine (42) :")
np.random.seed(42)
print(np.random.randn(3))
np.random.seed(42)
print(np.random.randn(3))
Sans graine :
[-0.94098177 -0.96139537  0.76457362]
[ 0.00344685 -1.13088264  0.38307273]

Avec graine (42) :
[ 0.49671415 -0.1382643   0.64768854]
[ 0.49671415 -0.1382643   0.64768854]

Remarque 8

En pratique, il est recommandé d’utiliser un objet np.random.Generator plutôt que la graine globale np.random.seed. Cela évite les effets de bord entre différentes parties du code.

# Bonne pratique : utiliser un Generator
rng = np.random.default_rng(seed=42)
print(f"Echantillon 1 : {rng.standard_normal(3)}")

rng2 = np.random.default_rng(seed=42)
print(f"Echantillon 2 : {rng2.standard_normal(3)}")  # identique
Echantillon 1 : [ 0.30471708 -1.03998411  0.7504512 ]
Echantillon 2 : [ 0.30471708 -1.03998411  0.7504512 ]

Environnements virtuels#

Remarque 9

Un environnement virtuel isole les dépendances d’un projet. Cela garantit que les versions des bibliothèques utilisées sont fixées et que le projet ne souffre pas de conflits avec d’autres projets.

Deux outils courants :

  • venv (integré a Python) : python -m venv mon_env

  • conda (Anaconda/Miniconda) : conda create -n mon_env python=3.11

Le fichier requirements.txt ou environment.yml enregistre les dépendances et leurs versions, permettant à un tiers de reconstruire l’environnement exact.

# Afficher les versions des packages clés
import importlib

packages = ['numpy', 'pandas', 'matplotlib', 'seaborn', 'sklearn']
for pkg in packages:
    try:
        mod = importlib.import_module(pkg)
        version = getattr(mod, '__version__', 'N/A')
        print(f"{pkg:15s} {version}")
    except ImportError:
        print(f"{pkg:15s} non installé")
numpy           2.3.5
pandas          3.0.0
matplotlib      3.10.8
seaborn         0.13.2
sklearn         1.8.0

Versionnement du code#

Remarque 10

Le versionnement du code avec Git est indispensable en pratique. Quelques bonnes pratiques :

  • Versionner le code et les notebooks (attention : les notebooks sont des fichiers JSON, les diffs sont difficiles à lire ; des outils comme nbstripout ou jupytext aident à gérer cela).

  • Ne pas versionner les données volumineuses (utiliser Git LFS ou DVC).

  • Documenter les expériences (hyperparamètres, résultats) dans un fichier de suivi ou un outil dedié (MLflow, Weights & Biases).

Résumé des bonnes pratiques#

Proposition 2 (Checklist de reproductibilite)

Pour qu’une expérience d’apprentissage automatique soit reproductible, il convient de :

  1. Fixer les graines aléatoires de tous les générateurs (NumPy, Python, framework de deep learning)

  2. Geler les versions des dépendances (requirements.txt, environment.yml)

  3. Versionner le code (Git) et documenter les modifications

  4. Séparer les données du code et documenter leur provenance

  5. Automatiser le pipeline (scripts, Makefiles, pipelines CI/CD)

  6. Documenter les hyperparamètres et les résultats de chaque expérience

# Exemple : fixer toutes les graines pour une reproductibilité maximale
import random

SEED = 42

# Python
random.seed(SEED)

# NumPy
np.random.seed(SEED)
rng = np.random.default_rng(SEED)

# Scikit-learn utilise le paramètre random_state
# ex : train_test_split(..., random_state=SEED)
# ex : RandomForestClassifier(random_state=SEED)

print(f"Graine globale fixée a {SEED}")
print(f"random.random()     = {random.random():.6f}")
print(f"np.random.randn(1)  = {np.random.randn(1)[0]:.6f}")
print(f"rng.standard_normal()= {rng.standard_normal():.6f}")
Graine globale fixée a 42
random.random()     = 0.639427
np.random.randn(1)  = 0.496714
rng.standard_normal()= 0.304717

Résumé#

Ce chapitre a presenté les outils fondamentaux de l’ecosystème Python pour l’apprentissage automatique :

Outil

Role

Import

NumPy

Calcul numérique, tableaux multidimensionnels

import numpy as np

Pandas

Manipulation de données tabulaires

import pandas as pd

Matplotlib

Visualisation de base

import matplotlib.pyplot as plt

Seaborn

Visualisation statistique

import seaborn as sns

Scikit-learn

Apprentissage automatique classique

import sklearn

Jupyter

Environnement intéractif

Ces outils seront utilisés tout au long de cet ouvrage. La maitrise de NumPy et Pandas est un prérequis pour manipuler efficacement les données, et la compréhension de l’API Scikit-learn est essentielle pour aborder les chapitres suivants sur les algorithmes d’apprentissage.