index

Python et Visualisation : de l’anatomie de Matplotlib aux surfaces 3D

Python doit une bonne partie de sa popularité scientifique à un trio de bibliothèques : NumPy pour le calcul vectoriel, Matplotlib pour le tracé, et Pandas pour la manipulation de données tabulaires. Ensemble, elles couvrent tout le chemin qui va d’une fonction mathématique abstraite à un graphique lisible.

Cet article suit un fil progressif : d’abord comprendre comment Matplotlib construit un graphique (Figure, Axes, et les deux façons de les piloter), puis l’appliquer à des fonctions d’une variable, à des surfaces en trois dimensions, au choix des couleurs, et enfin aux statistiques descriptives d’un jeu de données.

0. Anatomie d’un graphique Matplotlib

Avant de tracer quoi que ce soit, il est utile de comprendre les deux objets que Matplotlib manipule en permanence :

  • la Figure est la toile entière — le conteneur qui peut accueillir un ou plusieurs graphiques, une légende globale, un titre, etc. ;
  • l’Axes est un graphique individuel à l’intérieur de cette toile, avec ses propres axes X/Y, ses graduations, son titre. C’est sur l’Axes que l’on trace les courbes.

Matplotlib propose alors deux façons d’écrire le même graphique, selon la documentation officielle : l’interface orientée objet (fig, ax = plt.subplots(), en gardant une référence explicite à fig et ax) et l’interface pyplot (un état implicite modifié par des fonctions globales comme plt.plot()). Les deux produisent un résultat identique, mais l’interface orientée objet est plus explicite et plus flexible : elle permet de garder la main sur chaque élément du graphique avant de l’afficher, notamment lorsqu’on combine plusieurs Axes dans une même Figure. C’est celle que nous utiliserons pour les graphiques les plus riches de cet article — les exemples les plus simples ci-dessous gardent volontairement l’interface pyplot, plus courte à lire.

1. Fonctions d’une variable réelle : f:RRf : \mathbb{R} \to \mathbb{R}

Cette section parcourt trois familles de fonctions représentatives — chacune illustre un comportement mathématique différent : croissance, saturation, borne.

Croissance et décroissance exponentielles : exe^{x} et exe^{-x}

L’exponentielle est la fonction qui vérifie f=ff' = f : sa vitesse de variation est à chaque instant proportionnelle à sa propre valeur. C’est le modèle de base de toute croissance (population, intérêts composés) ou décroissance (désintégration radioactive, décharge d’un condensateur) proportionnelle à la quantité présente.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-2, 2, 300)
y_croissance = np.exp(x)
y_decroissance = np.exp(-x)

plt.figure(figsize=(10, 5))
plt.plot(x, y_croissance, color='steelblue', linewidth=2, label='croissance : e^x')
plt.plot(x, y_decroissance, color='tomato', linewidth=2, label='décroissance : e^-x')
plt.title('Croissance et décroissance exponentielles')
plt.xlabel('x')
plt.ylabel('y')
plt.legend(loc='upper center')
plt.grid(True, alpha=0.3)
plt.show()

La fonction logistique et sa dérivée : σ(x)=11+ex\sigma(x) = \dfrac{1}{1+e^{-x}}

La sigmoïde transforme n’importe quel nombre réel en une valeur entre 00 et 11 : elle sature vers 11 quand x+x \to +\infty et vers 00 quand xx \to -\infty, avec un point d’inflexion en (0,0,5)(0, 0{,}5). C’est la fonction d’activation historique des réseaux de neurones, et le cœur de la régression logistique.

Sa dérivée a une propriété remarquable, utile pour la rétropropagation du gradient : σ(x)=σ(x)(1σ(x))\sigma'(x) = \sigma(x)\bigl(1-\sigma(x)\bigr), une courbe en cloche maximale exactement là où σ\sigma varie le plus vite.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-6, 6, 400)
sigmoide = 1 / (1 + np.exp(-x))
derivee = sigmoide * (1 - sigmoide)

plt.figure(figsize=(10, 5))
plt.plot(x, sigmoide, color='steelblue', linewidth=2, label='sigmoïde')
plt.plot(x, derivee, color='tomato', linewidth=2, linestyle='--', label='dérivée')
plt.title('Fonction logistique (sigmoïde)')
plt.xlabel('x')
plt.ylabel('y')
plt.legend(loc='upper left')
plt.grid(True, alpha=0.3)
plt.show()

Une fonction rationnelle bornée : f(x)=11+x2f(x) = \dfrac{1}{1+x^2}

Contrairement à l’exponentielle vue plus haut, qui explose vers l’infini, cette fonction reste bornée (0<f(x)10 < f(x) \leq 1) sur tout R\mathbb{R}, tout en restant infiniment différentiable — sans jamais utiliser d’exponentielle ni de trigonométrie. Son maximum global vaut 11 en x=0x=0, et elle décroît vers 00 de part et d’autre, un peu comme la cloche gaussienne de la section suivante, mais avec une décroissance beaucoup plus lente (en 1/x21/x^2 plutôt qu’en ex2e^{-x^2}).

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-5, 5, 400)
y = 1 / (1 + x**2)

plt.figure(figsize=(10, 5))
plt.plot(x, y, color='steelblue', linewidth=2)
plt.title('Fonction rationnelle bornée')
plt.xlabel('x')
plt.ylabel('y')
plt.grid(True, alpha=0.3)
plt.show()

2. Fonctions de deux variables : f:R2Rf : \mathbb{R}^2 \to \mathbb{R}

Lorsqu’une fonction dépend de deux variables réelles xx et yy, son graphe n’est plus une courbe dans le plan mais une surface dans l’espace R3\mathbb{R}^3. On représente alors z=f(x,y)z = f(x, y) en trois dimensions.

Pour cela, Matplotlib propose le module mpl_toolkits.mplot3d et la fonction plot_surface. Sur cette page, les graphiques 3D ci-dessous sont interactifs : cliquez-glissez pour les faire pivoter et observer la surface sous un autre angle.

Préparation : la grille de points

La visualisation d’une surface nécessite de créer une grille régulière de points (x,y)(x, y) couvrant le domaine souhaité. La fonction np.meshgrid génère cette grille :

import numpy as np

x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)
# X et Y sont des matrices 100×100
# chaque point (X[i,j], Y[i,j]) correspond à un point de la grille

Surface paraboloïde : f(x,y)=x2+y2f(x, y) = x^2 + y^2

Le paraboloïde elliptique est la surface la plus simple à deux variables. Son point minimal est en (0,0,0)(0, 0, 0) et la surface s’élève symétriquement dans toutes les directions. C’est la généralisation en 2D de la parabole.

Cette surface est différentiable partout. Son gradient f=(2x,2y)\nabla f = (2x, 2y) est nul en (0,0)(0,0), qui est un minimum global.

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)
Z = X**2 + Y**2

fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap='viridis', alpha=0.9)
fig.colorbar(surf, ax=ax, shrink=0.5, label='z = f(x, y)')
ax.set_title('Paraboloïde : $f(x, y) = x^2 + y^2$')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.tight_layout()
plt.show()

Selle de cheval (col) : f(x,y)=x2y2f(x, y) = x^2 - y^2

La selle de cheval est l’exemple typique d’un point critique qui n’est ni un minimum ni un maximum. En (0,0)(0,0), la dérivée est nulle dans toutes les directions, mais la surface monte dans la direction xx et descend dans la direction yy : on appelle ce point un point selle ou col.

f=(2x,2y)=(0,0) en (0,0)\nabla f = (2x, -2y) = (0, 0) \text{ en } (0,0)

La matrice hessienne H=(2002)H = \begin{pmatrix} 2 & 0 \\ 0 & -2 \end{pmatrix} a un déterminant négatif (4<0-4 < 0), ce qui confirme le point selle.

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)
Z = X**2 - Y**2

fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap='coolwarm', alpha=0.9)
fig.colorbar(surf, ax=ax, shrink=0.5, label='z = f(x, y)')
ax.set_title('Selle de cheval : $f(x, y) = x^2 - y^2$')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.tight_layout()
plt.show()

Surface ondulée : f(x,y)=sin(x)cos(y)f(x, y) = \sin(x)\cos(y)

La combinaison de fonctions trigonométriques sur deux variables produit une surface périodique dans les deux directions. Cette surface est différentiable partout et possède une infinité de points critiques correspondant aux maxima (+1+1), minima (1-1) et points selles.

f=(cos(x)cos(y),  sin(x)sin(y))\nabla f = (\cos(x)\cos(y),\; -\sin(x)\sin(y))

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

x = np.linspace(-2 * np.pi, 2 * np.pi, 200)
y = np.linspace(-2 * np.pi, 2 * np.pi, 200)
X, Y = np.meshgrid(x, y)
Z = np.sin(X) * np.cos(Y)

fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap='plasma', alpha=0.9)
fig.colorbar(surf, ax=ax, shrink=0.5, label='z = f(x, y)')
ax.set_title('Surface ondulée : $f(x, y) = \\sin(x)\\cos(y)$')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.tight_layout()
plt.show()

Cloche gaussienne : f(x,y)=e(x2+y2)f(x, y) = e^{-(x^2 + y^2)}

La cloche gaussienne en deux dimensions est la généralisation de la distribution normale. Elle possède un maximum global en (0,0)(0, 0)f(0,0)=1f(0,0) = 1, et tend vers 00 dans toutes les directions. Elle est différentiable partout et ses courbes de niveau sont des cercles concentriques.

Cette surface intervient notamment en probabilités (densité de la loi normale bivariée) et en traitement du signal (filtre gaussien).

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

x = np.linspace(-3, 3, 200)
y = np.linspace(-3, 3, 200)
X, Y = np.meshgrid(x, y)
Z = np.exp(-(X**2 + Y**2))

fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap='magma', alpha=0.9)
fig.colorbar(surf, ax=ax, shrink=0.5, label='z = f(x, y)')
ax.set_title('Cloche gaussienne : $f(x, y) = e^{-(x^2+y^2)}$')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.tight_layout()
plt.show()

Fonction non différentiable : f(x,y)=x+yf(x, y) = |x| + |y|

Toutes les fonctions ne sont pas différentiables. La fonction f(x,y)=x+yf(x, y) = |x| + |y| est continue partout mais non différentiable le long des axes x=0x = 0 et y=0y = 0, car la dérivée partielle n’y existe pas (la pente change brutalement).

Son graphe forme un croisement de plis : quatre faces planes qui se rejoignent sur les axes avec des arêtes nettes. Les courbes de niveau sont des carrés, et le minimum global est en (0,0)(0, 0).

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

x = np.linspace(-5, 5, 200)
y = np.linspace(-5, 5, 200)
X, Y = np.meshgrid(x, y)
Z = np.abs(X) + np.abs(Y)

fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap='YlOrRd', alpha=0.9)
fig.colorbar(surf, ax=ax, shrink=0.5, label='z = f(x, y)')
ax.set_title('Non différentiable : $f(x, y) = |x| + |y|$')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.tight_layout()
plt.show()

Courbes de niveau : une autre façon de lire une surface

Les courbes de niveau (ou isocourbes) sont les projections horizontales de la surface : pour chaque valeur cc, elles représentent l’ensemble des points (x,y)(x, y) tels que f(x,y)=cf(x, y) = c. C’est la représentation utilisée sur les cartes topographiques — souvent plus lisible qu’une surface 3D pour repérer précisément un extremum ou comparer deux valeurs.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-3, 3, 300)
y = np.linspace(-3, 3, 300)
X, Y = np.meshgrid(x, y)
Z = np.sin(X) * np.cos(Y)

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

# Surface en couleur
c1 = axes[0].contourf(X, Y, Z, levels=30, cmap='plasma')
fig.colorbar(c1, ax=axes[0], label='f(x, y)')
axes[0].set_title('Carte de chaleur : $f(x,y) = \\sin(x)\\cos(y)$')
axes[0].set_xlabel('x')
axes[0].set_ylabel('y')

# Courbes de niveau
c2 = axes[1].contour(X, Y, Z, levels=20, cmap='plasma')
axes[1].clabel(c2, inline=True, fontsize=8)
axes[1].set_title('Courbes de niveau')
axes[1].set_xlabel('x')
axes[1].set_ylabel('y')

plt.tight_layout()
plt.show()

3. Bien choisir ses couleurs

Le choix d’une carte de couleurs (cmap) n’est pas qu’une question esthétique : une mauvaise carte peut faire apparaître de faux motifs ou masquer de vraies variations. Jusqu’en 2015, la carte par défaut de Matplotlib était jet, un dégradé arc-en-ciel dont la luminosité varie de façon irrégulière — deux valeurs proches peuvent y sembler très différentes, et inversement.

En 2015, Nathaniel Smith et Stéfan van der Walt ont conçu quatre cartes perceptuellement uniformesviridis, plasma, magma, inferno — où la luminosité croît de façon monotone avec la valeur représentée. Elles restent lisibles en niveaux de gris et pour les daltoniens, et viridis est devenue la carte par défaut de Matplotlib à partir de la version 2.0 (voir le guide officiel des colormaps).

On distingue en pratique trois grandes familles :

FamilleUsageExemplesUtilisée plus haut pour
Séquentielleune grandeur qui croît du plus faible au plus fort, sans valeur centrale particulièreviridis, plasma, magma, YlOrRdle paraboloïde (toujours 0\geq 0), la cloche gaussienne, la surface x+y\lvert x\rvert+\lvert y\rvert
Divergenteune grandeur avec un centre neutre significatif (souvent zéro), qui s’écarte dans deux directionscoolwarm, RdBu, PiYGla selle de cheval x2y2x^2-y^2, positive d’un côté, négative de l’autre
Qualitativedes catégories sans ordre natureltab10, Set2, Pairedcourbes multiples (voir steelblue / tomato plus haut)

C’est pourquoi la selle de cheval utilise coolwarm plutôt que viridis : sa valeur change de signe autour de (0,0)(0,0), et une carte divergente rend ce basculement immédiatement visible.

4. Statistiques descriptives avec NumPy et Pandas

La visualisation ne se limite pas aux fonctions mathématiques : elle est aussi au cœur de l’analyse de données. Avant toute modélisation, il est essentiel de comprendre la distribution des variables.

La distribution normale

La distribution normale (ou gaussienne) est caractérisée par deux paramètres : la moyenne μ\mu (centre de la distribution) et l’écart-type σ\sigma (dispersion). Sa densité de probabilité est :

ϕ(x)=1σ2πe(xμ)22σ2\phi(x) = \frac{1}{\sigma\sqrt{2\pi}}\,e^{-\frac{(x-\mu)^2}{2\sigma^2}}

L’histogramme permet de visualiser la distribution empirique d’un échantillon et de la comparer à la courbe théorique :

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm

mu, sigma = 100, 15
data = np.random.normal(mu, sigma, 1000)

x_range = np.linspace(mu - 4*sigma, mu + 4*sigma, 300)

fig, ax = plt.subplots(figsize=(10, 5))
ax.hist(data, bins=40, density=True, color='steelblue', alpha=0.6, label='Échantillon empirique')
ax.plot(x_range, norm.pdf(x_range, mu, sigma), color='tomato', linewidth=2,
        label=r'Densité théorique $\mathcal{N}(100, 15^2)$')
ax.set_title('Distribution normale : échantillon vs densité théorique')
ax.set_xlabel('Valeur')
ax.set_ylabel('Densité')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Plus l’échantillon est grand, plus l’histogramme empirique (en bleu) se rapproche de la courbe théorique (en rouge) — c’est une illustration directe de la loi des grands nombres.

Résumé statistique avec Pandas

La méthode describe() de Pandas fournit en une seule commande les principales mesures de tendance centrale et de dispersion :

import numpy as np
import pandas as pd

data = np.random.normal(100, 15, 1000)
serie = pd.Series(data, name='mesures')
print(serie.describe())
StatistiqueSignification
countNombre d’observations
meanMoyenne arithmétique xˉ\bar{x}
stdÉcart-type empirique ss
min / maxValeurs extrêmes
25% / 50% / 75%Quartiles Q1Q_1, médiane, Q3Q_3

Fonctions statistiques NumPy

NumPy propose un ensemble de fonctions directement applicables sur des tableaux :

import numpy as np

notes = np.random.normal(12, 3, 50)

print(f"Moyenne   : {np.mean(notes):.2f}")
print(f"Écart-type: {np.std(notes):.2f}")
print(f"Minimum   : {np.min(notes):.2f}")
print(f"Maximum   : {np.max(notes):.2f}")
print(f"Médiane   : {np.median(notes):.2f}")

La moyenne mesure le centre de gravité des données. L’écart-type quantifie leur dispersion autour de cette moyenne. La médiane est la valeur qui sépare les 50% inférieurs des 50% supérieurs : elle est plus robuste que la moyenne en présence de valeurs aberrantes.

Inspection rapide : head() et tail()

Pour un premier aperçu d’un jeu de données, head() affiche les premières lignes et tail() les dernières :

import numpy as np
import pandas as pd

temperatures = np.random.uniform(15, 35, 100)
serie = pd.Series(temperatures, name='température (°C)')

print("Premières valeurs :")
print(serie.head(8))

print("\nDernières valeurs :")
print(serie.tail(5))

Récapitulatif

Type de visualisationFonction cléCas d’usage
Courbe 2Dplt.plot()Fonctions f:RRf : \mathbb{R} \to \mathbb{R}
Histogrammeplt.hist()Distribution d’un échantillon
Surface 3Dax.plot_surface()Fonctions f:R2Rf : \mathbb{R}^2 \to \mathbb{R}
Carte de chaleurplt.contourf()Vue en surplomb d’une surface
Courbes de niveauplt.contour()Isocourbes, topographie
describe()PandasRésumé statistique d’une série

Trois réflexes suffisent pour aborder n’importe quel nouveau jeu de données ou fonction : préférer l’interface orientée objet (fig, ax) dès que le graphique se complexifie, choisir une carte de couleurs séquentielle ou divergente selon que la grandeur a ou non un centre neutre, et toujours commencer l’analyse d’un échantillon par describe() avant de se lancer dans un modèle.

Python offre ainsi un environnement complet pour explorer les fonctions mathématiques dans toutes leurs dimensions : des oscillations périodiques aux surfaces complexes, des comportements différentiables aux singularités, des données brutes aux résumés statistiques. La visualisation n’est pas une étape accessoire : c’est souvent le premier geste qui révèle la structure d’un problème.

Pour aller plus loin