Programmation Python — Séance 6 : NumPy

On entre dans la science des données avec un exemple de morphométrie : des mesures relevées sur des spécimens de pinsons (les célèbres pinsons de Darwin).
Télécharger le notebook Ouvrir dans Colab Les sorties sont celles enregistrées dans le notebook : rien n'est réexécuté.

DMI · FST · UCAD

On entre dans la science des données avec un exemple de morphométrie : des mesures relevées sur des spécimens de pinsons (les célèbres pinsons de Darwin). NumPy fournit le tableau (array), sur lequel on calcule sans boucle (la vectorisation). C’est l’outil sur lequel repose pandas (Séance 7). Exécutez chaque cellule avec Maj + Entrée. Les cellules À vous sont à compléter.


1. Le tableau NumPy

On importe NumPy sous le nom conventionnel np, et on crée un tableau avec np.array(...) à partir d’une liste. Ici, la masse (en grammes) de 6 spécimens capturés sur le terrain.

import numpy as np

masse = np.array([15, 16, 21, 22, 28, 27])
print(masse)
[15 16 21 22 28 27]

À vous. Créez un tableau aile contenant les longueurs d’aile (en mm) [67, 68, 74, 75, 80, 79] des mêmes spécimens, puis affichez-le.

# À vous : creez aile = np.array([67, 68, 74, 75, 80, 79]) puis affichez-le

Un tableau contient des éléments de même type. Si l’on mélange entiers et décimaux, tout devient flottant (le point après chaque nombre le signale).

bec = np.array([9.1, 8.0, 12.8])
print(bec)
[ 9.1  8.  12.8]

À vous. Créez un tableau bec_haut contenant les hauteurs de bec [8.0, 8.3, 10.5] et affichez-le.

# À vous : creez bec_haut = np.array([8.0, 8.3, 10.5]) puis affichez-le

2. La vectorisation : calculer sans boucle

Une opération sur un tableau s’applique à chaque élément d’un coup, sans écrire de boucle. C’est plus court, plus lisible, et beaucoup plus rapide.

print(masse + 2)
print(masse * 2)
[17 18 23 24 30 29]
[30 32 42 44 56 54]

Comparons avec la Séance 3. Pour additionner toutes les masses, on écrivait une boucle ; avec NumPy, une seule expression suffit — et donne le même résultat.

# Facon Seance 3 : une boucle
total = 0
for m in masse:
    total = total + m
print(total)

# Facon NumPy : vectorise
print(masse.sum())
129
129

À vous. À partir de masse, ajoutez 5 à chaque valeur sans boucle et affichez le résultat.

# À vous : affichez masse + 5 (sans boucle)

Piège. L’opérateur * agit élément par élément : masse * masse élève chaque masse au carré, ce n’est pas un produit matriciel.

print(masse * masse)
[225 256 441 484 784 729]

3. Les agrégations

Un tableau sait se résumer en un nombre : somme, moyenne, minimum, maximum. Ce sont des méthodes appelées avec un point.

print(masse.sum())
print(masse.mean())
print(masse.min())
print(masse.max())
129
21.5
15
28

La masse moyenne de l’échantillon est 21.5 g. L’écart-type (std) mesure la dispersion des masses ; il tombe rarement rond, on l’arrondit.

print(round(masse.std(), 2))
4.92

À vous. Affichez la moyenne et le maximum du tableau aile que vous avez créé plus haut.

# À vous : affichez aile.mean() et aile.max()

4. Le filtrage booléen : sélectionner des spécimens

Une comparaison sur un tableau renvoie un masque de True/False, un par élément. Placé entre crochets, ce masque sélectionne les éléments qui vérifient la condition — par exemple les spécimens les plus lourds.

print(masse >= 20)
[False False  True  True  True  True]
print(masse[masse >= 20])
print((masse >= 20).sum())
[21 22 28 27]
4

Le masque masse >= 20 marque les spécimens d’au moins 20 g ; masse[masque] ne garde que leurs masses ; .sum() sur un masque compte les True (ici 4 spécimens, sans doute les plus grandes espèces).

À vous. Affichez les masses des spécimens strictement inférieurs à 20 g (les plus petits), puis comptez-les.

# À vous : affichez masse[masse < 20] puis (masse < 20).sum()

5. La table complète : un tableau à deux dimensions

En réalité, on a mesuré 4 caractères par spécimen. On les range dans un tableau 2D : une ligne par spécimen, une colonne par mesure — longueur de bec, hauteur de bec, longueur d’aile, masse. shape donne le format (lignes, colonnes).

# colonnes : bec_long, bec_haut, aile, masse
X = np.array([[ 9.1,  8.0, 67, 15],
              [ 9.4,  8.3, 68, 16],
              [12.8, 10.5, 74, 21],
              [13.2, 10.9, 75, 22],
              [16.1, 14.2, 80, 28],
              [15.7, 13.8, 79, 27]])
print(X)
print(X.shape)
[[ 9.1  8.  67.  15. ]
 [ 9.4  8.3 68.  16. ]
 [12.8 10.5 74.  21. ]
 [13.2 10.9 75.  22. ]
 [16.1 14.2 80.  28. ]
 [15.7 13.8 79.  27. ]]
(6, 4)

On accède à une valeur par [ligne, colonne], soit [specimen, mesure]. X[0] donne toutes les mesures du spécimen 0 ; X[:, 0] donne la colonne des longueurs de bec (le : signifie « tous les spécimens »).

print(X[0, 0])
print(X[0])
print(X[:, 0])
print(X[:, 3])
9.1
[ 9.1  8.  67.  15. ]
[ 9.1  9.4 12.8 13.2 16.1 15.7]
[15. 16. 21. 22. 28. 27.]

À vous. Affichez la hauteur de bec du spécimen 2 (X[2, 1]), puis toute la colonne des ailes (X[:, 2]).

# À vous : affichez X[2, 1] puis X[:, 2]

6. L’argument axis : dans quel sens résumer

Sur un tableau 2D, une agrégation se fait par colonne ou par ligne, selon axis. axis=0 descend les colonnes : une valeur par mesure (moyenne des becs, des ailes…). axis=1 parcourt les lignes : une valeur par spécimen.

print(X.mean(axis=0).round(2))
print(X.mean(axis=1).round(2))
[12.72 10.95 73.83 21.5 ]
[24.78 25.42 29.58 30.28 34.58 33.88]

axis=0 donne la moyenne de chaque mesure sur tous les spécimens (bec 12.72 mm, aile 73.83 mm, masse 21.5 g…) — c’est ce qui a un sens ici. axis=1 mélange des unités différentes (mm et g) : le calcul est correct mais biologiquement peu parlant — d’où l’intérêt de normaliser, juste après.

print(X.sum(axis=0))
[ 76.3  65.7 443.  129. ]

À vous. Affichez le maximum de chaque mesure avec X.max(axis=0), puis le maximum de chaque spécimen avec X.max(axis=1).

# À vous : affichez X.max(axis=0) puis X.max(axis=1)

7. Broadcasting et normalisation (z-score)

Le broadcasting permet d’opérer entre un tableau 2D et un vecteur : le vecteur est appliqué à chaque ligne. Ici, on retire à chaque mesure sa propre moyenne (calculée par axis=0).

print((X - X.mean(axis=0)).round(2))
[[-3.62 -2.95 -6.83 -6.5 ]
 [-3.32 -2.65 -5.83 -5.5 ]
 [ 0.08 -0.45  0.17 -0.5 ]
 [ 0.48 -0.05  1.17  0.5 ]
 [ 3.38  3.25  6.17  6.5 ]
 [ 2.98  2.85  5.17  5.5 ]]

Les mesures sont dans des unités différentes : des mm pour le bec et l’aile, des g pour la masse. Pour les rendre comparables, on normalise chaque colonne — c’est le z-score : retirer la moyenne, puis diviser par l’écart-type.

X_reduit = (X - X.mean(axis=0)) / X.std(axis=0)
print(X_reduit.round(2))
print("ecart-type par mesure :", X_reduit.std(axis=0).round(2))
[[-1.33 -1.23 -1.38 -1.32]
 [-1.22 -1.1  -1.18 -1.12]
 [ 0.03 -0.19  0.03 -0.1 ]
 [ 0.18 -0.02  0.24  0.1 ]
 [ 1.24  1.35  1.25  1.32]
 [ 1.09  1.19  1.04  1.12]]
ecart-type par mesure : [1. 1. 1. 1.]

Après normalisation, chaque mesure a une moyenne de 0 et un écart-type de 1 : toutes sont ramenées à une échelle commune, quelle que soit leur unité. C’est un geste systématique en morphométrie comme en apprentissage automatique — un premier pas concret vers le cours de Machine Learning, où l’on normalisera les variables avant d’entraîner un modèle.

À vous. Normalisez la seule colonne des masses : calculez (X[:, 3] - X[:, 3].mean()) / X[:, 3].std() et affichez le résultat arrondi avec .round(2).

# À vous : affichez ((X[:, 3] - X[:, 3].mean()) / X[:, 3].std()).round(2)

8. Pièges fréquents

  • * n’est pas le produit matriciel : il multiplie élément par élément.
  • Le sens de axis : axis=0 agit sur les colonnes (par mesure), axis=1 sur les lignes (par spécimen).
  • Mélanger des unités : additionner ou moyenner des mesures d’unités différentes (mm et g) n’a pas de sens biologique ; on normalise d’abord.
  • Les types homogènes : un tableau ne contient qu’un seul type ; mélanger entiers et flottants donne des flottants.

Récapitulatif

  • Créer : import numpy as np puis np.array([...]) ; éléments de même type.
  • Vectorisation : une opération s’applique à tout le tableau, sans boucle.
  • Agrégations : .sum(), .mean(), .std(), .min(), .max().
  • Filtrage : un masque booléen (tableau >= v) puis tableau[masque].
  • 2D : shape donne (spécimens, mesures) ; accès [i, j], [:, j] (une mesure), [i, :] (un spécimen).
  • axis : axis=0 par mesure (colonne), axis=1 par spécimen (ligne).
  • Normalisation (z-score) (X - X.mean(axis=0)) / X.std(axis=0) : ramène chaque mesure à une moyenne de 0 et un écart-type de 1 — pont vers le ML.

Prochaine séance — S7 : pandas, pour manipuler la table des pinsons avec des colonnes nommées et une colonne espece.

Retour au sommet