Notions d'indexing, de slicing et de subsetting avec NumPy¶
Rappels de ces concepts en pur Python¶
Ces trois concepts, intimement liés, sont fondamentaux pour la manipulation de données en Python. Et ils sont essentiels pour comprendre des bibliothèques plus avancées comme NumPy.
Utilisation de l'indexation (Indexing)¶
L'indexation permet d'accéder à un élément spécifique d'une séquence en utilisant son indice. En Python, les indices commencent à 0.
data = [10, 20, 30, 40, 50] # Une liste de cinqs éléments
print("Indice 0 ->", data[0]) # Les indices possibles vont donc de 0 Ã 4
print("Indice 2 ->", data[2])
print("Indice 4 ->", data[4])
Indice 0 -> 10 Indice 2 -> 30 Indice 4 -> 50
Si vous dépassez la longueur de la liste, une erreur IndexError
sera levée.
print("Il n'y a pas d'indice 5 ->", data[5])
--------------------------------------------------------------------------- IndexError Traceback (most recent call last) Cell In[2], line 1 ----> 1 print("Il n'y a pas d'indice 5 ->", data[5]) IndexError: list index out of range
Notez enfin, qu'une indexation négative permet d'accéder aux éléments depuis la fin de la liste (par exemple, -1 est le dernier élément).
print("Indice -1 ->", data[-1]) # L'index du dernier élément
print("Indice -3 ->", data[-3])
print("Indice -5 ->", data[-5]) # L'index du premier élément
print("Il n'y a pas d'indice -6 ->", data[-6])
Indice -1 -> 50 Indice -3 -> 30 Indice -5 -> 10
--------------------------------------------------------------------------- IndexError Traceback (most recent call last) Cell In[3], line 5 2 print("Indice -3 ->", data[-3]) 3 print("Indice -5 ->", data[-5]) # L'index du premier élément ----> 5 print("Il n'y a pas d'indice -6 ->", data[-6]) IndexError: list index out of range
Utilisation de l'opérateur de Slicing¶
L'opérateur de slicing (tranchage, en français) permet de récupérer un sous ensemble d'une collection. Voici la syntaxe générale de cet opérateur : [début:fin:pas]
, en sachant que :
- Si vous ne mentionnez pas l'opérande
début
, c'est l'indice 0 qui sera considéré. - Si vous ne mentionnez pas l'opérande
fin
, c'est la taille de la collection qui sera considérée. - Si vous ne mentionnez pas l'opérande
pas
, c'est un pas (un step) de 1 qui sera considéré.
Note : il est aussi très important de garder en mémoire que l'index de fin est exclusif : la donnée présente à cet indice ne sera pas extraite.
Voici quelques exemples d'utilisation de cet opérateur.
data = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
print(data[2:6]) # 70, la donnée à l'indice 6 ne sera pas considérée (on extrait 6-2 (soit 4) éléments).
# L'indice 6 correspond à la position d'arrêt pour l'extraction de nos données.
[30, 40, 50, 60]
print(data[:6]) # On réalise l'extraction à partir de l'indice 0
[10, 20, 30, 40, 50, 60]
print(data[2:]) # On réalise l'extraction jusqu'à la fin de la collection
[30, 40, 50, 60, 70, 80, 90, 100]
# Imaginons un ensemble de coordonnées x, y qui se suivent directement dans une liste.
coords = [10, 11, 20, 21, 30, 31, 40, 41, 50, 51]
# Il est possible d'en extraire tous les x en jouant sur le pas
abscisses = coords[::2]
# Et il est aussi possible d'en extraire tous les y en commençant à l'indice 1 et avec un pas de 2
ordonnees = coords[1::2]
print("Abscisses : ", abscisses)
print("Ordonnées : ", ordonnees)
Abscisses : [10, 20, 30, 40, 50] Ordonnées : [11, 21, 31, 41, 51]
Enfin, notez que cet opérateur de travailler sur des collections de natures diverses et variées.
print((10, 20, 30, 40, 50)[2:5]) # Une tranche d'un tuple
print("Hello World"[2:8]) # Une partie de la chaîne de caractères
import numpy as np
v = np.array([10, 20, 30, 40, 50])
print(v[2:5]) # Et bien entendu, cela fonctionne aussi sur un tableau NumPy
(30, 40, 50) llo Wo [30 40 50]
Autres techniques d'extraction de sous-ensembles (Subsetting)¶
En fait, l'opérateur de slicing est un cas particulier permettant d'extraire un sous ensemble d'une collection. Mais il existe un multitude d'autres possibilités et notamment l'utilisateur de listes en intention (list comprehensions, en anglais).
Voici un exemple d'extraction d'un sous-ensembles d'une liste en utilisant une liste en intention.
data = [40, 28, 53, 11, 7, 34, 23, 3, 18, 60]
# On cherche à extraire les valeurs contenues dans la liste data, à condition qu'elle soit >= 25.
big_values = [value for value in data if value >= 25]
print(big_values) # Nous avons bien réussit à extraire une sous-liste.
[40, 28, 53, 34, 60]
Voici un autre exemple d'extraction de données à partir de la fonction filter
.
little_values = list(filter(lambda value: value < 25, data))
print(little_values)
[11, 7, 23, 3, 18]
Note : filter
est une classe de type iterateur. Si vous souhaitez récupérer une collection et non un itérateur, il vous appartient de reconstituer la collection. C'est pour cette raison que je passe l'objet de filtre produit au constructeur de ma nouvelle liste.
Maintenant que nous sommes au clair sur ces quelques rappels, nous pouvons passer à la transposition de ces possibilités à Numpy.
Application de ces concepts à Numpy¶
L'indexation avec Numpy¶
Bien entendu, vous pouvez utiliser l'opérateur d'indexation sur un ndarray
. Voici un premier exemple permettant de manipuler les données d'un vecteur. Notez que l'indexation négative fonctionne de manière similaire à l'indexation d'une liste.
import numpy as np
v = np.array([10, 20, 30, 40, 50])
print(type(v))
print("Indice 0 ->", v[0]) # Les indices possibles vont donc de 0 Ã 4
print("Indice 2 ->", v[2])
print("Indice 4 ->", v[4])
print("Indice -1 ->", v[-1]) # L'index du dernier élément
print("Indice -3 ->", v[-3])
print("Indice -5 ->", v[-5]) # L'index du premier élément
<class 'numpy.ndarray'> Indice 0 -> 10 Indice 2 -> 30 Indice 4 -> 50 Indice -1 -> 50 Indice -3 -> 30 Indice -5 -> 10
Néanmoins quelques différences existent entre l'indexation d'une liste et celle d'un tableau Numpy :
- Un tableau Numpy ne peut contenir que des données d'un même type.
- Un tableau Numpy pouvant avoir plusieurs dimensions, l'opérateur d'indexation accepte autant d'index que nécessaire.
Voici quelques exemples.
v = np.array([10, 20, 30, 40, 50])
print("Type de données supporté par le vecteur v :", v.dtype)
v[2] = 3000
print(v)
v[2] = 3.14
print(v)
v[2] = "Ouille"
Type de données supporté par le vecteur v : int32 [ 10 20 3000 40 50] [10 20 3 40 50]
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[12], line 7 5 v[2] = 3.14 6 print(v) ----> 7 v[2] = "Ouille" ValueError: invalid literal for int() with base 10: 'Ouille'
# On instancie maintenant une matrice
m = np.array([[10, 20, 30], [40, 50, 60], [70, 80, 90]], dtype=np.float64)
print(m)
m[1][1] = 5000 # Ca marche quand même : on récupère la seconde ligne puis on en extrait la second valeur.
print(m)
m[1, 1] = 500 # Mais c'est quand même mieux comme cela.
print(m)
[[10. 20. 30.] [40. 50. 60.] [70. 80. 90.]] [[ 10. 20. 30.] [ 40. 5000. 60.] [ 70. 80. 90.]] [[ 10. 20. 30.] [ 40. 500. 60.] [ 70. 80. 90.]]
Le slicing avec Numpy¶
L'opérateur de slicing est lui aussi supporté par Numpy en sachant qu'il a été étendu, lui aussi, pour accepter des dimensions multiples.
Note : bien entendu, l'index de fin sur l'expression d'une tranche est exclusif.
v = np.array([10, 20, 30, 40, 50])
print("Index 1 :", v[1])
print("Index 4 :", v[4])
v2 = v[1:4] # On extrait trois valeurs (4 - 1 = 3)
print(v2)
Index 1 : 20 Index 4 : 50 [20 30 40]
m = np.array([[10, 20, 30], [40, 50, 60], [70, 80, 90]], dtype=np.float64)
print(m)
coin_haut_gauche = m[0:2, 0:2] # On ne considère que les deux premières lignes et les deux premières colonnes
coin_haut_droit = m[0:2, 1:3] # 0:2 => les deux premières lignes ; 1:3 => les deux dernières colonnes
coin_bas_gauche = m[1:3, 0:2] # 1:3 => les deux dernières lignes ; 0:2 => les deux premières colonnes
coin_bas_droit = m[1:3, 1:3] # On ne considère que les deux dernières lignes et les deux dernières colonnes
print("Coin haut gauche : ", coin_haut_gauche, sep="\n")
print("Coin haut droit : ", coin_haut_droit, sep="\n")
print("Coin bas gauche : ", coin_bas_gauche, sep="\n")
print("Coin bas droit : ", coin_bas_droit, sep="\n")
[[10. 20. 30.] [40. 50. 60.] [70. 80. 90.]] Coin haut gauche : [[10. 20.] [40. 50.]] Coin haut droit : [[20. 30.] [50. 60.]] Coin bas gauche : [[40. 50.] [70. 80.]] Coin bas droit : [[50. 60.] [80. 90.]]
Il est aussi possible d'extraire un sous-ensemble de valeurs en utilisant un pas.
v = np.array([1, 11, 2, 12, 3, 13, 4, 14, 5, 15, 6, 16, 7, 17, 8, 18, 9, 19])
v1 = v[::2] # Extraction des valeurs pour les index paires.
v2 = v[1::2] # Extraction des valeurs pour les index impaires.
print(v, v1, v2, sep="\n")
[ 1 11 2 12 3 13 4 14 5 15 6 16 7 17 8 18 9 19] [1 2 3 4 5 6 7 8 9] [11 12 13 14 15 16 17 18 19]
Le subsetting avec Numpy¶
Comme nous l'avons déjà dis, le slicing est une forme de subsetting, mais il existe d'autres possibilités pour extraire un sous ensemble de valeur.
- Utilisation d'index multiples
- Utilisation de l'indexation booléenne
- Utilisation de la fonction where
- ...
Voici un exemple plus chaque possibilité proposée.
# Exemple d'utilisation d'index multiples
v = np.array([10, 20, 30, 40, 50, 60, 70, 80, 90])
print("Indexation simple :", v[0])
print("Indexation via plusieurs index : ", v[[0, 1, 3, 5, 8]]) # Une liste d'index est proposées dans l'opérateur []
Indexation simple : 10 Indexation via plusieurs index : [10 20 40 60 90]
# Exemple d'indexation booléenne
# Pour comprendre le concept, il faut déjà noter qu'on peut produire des matrices booléennes via les opérateurs de comparaison
v = np.array([7, 25, 30, 12, 14, 23, 26, 8, 1, 40])
mat_bol = v < 25
print("Exemple de matrice booléenne :", mat_bol)
# On peut se servir d'une matrice booléenne pour indexer toutes les valeurs à extraire.
v2 = v[mat_bol] # On de manière plus compacte : v2 = v[v < 25]
v3 = v[v >= 25]
print("Exemple d'indexation booléenne :", v2)
print("Un autre exemple :", v3)
Exemple de matrice booléenne : [ True False False True True True False True True False] Exemple d'indexation booléenne : [ 7 12 14 23 8 1] Un autre exemple : [25 30 26 40]
# Exemple d'utilisation de la méthode where
v = np.array([7, 25, 30, 12, 14, 23, 26, 8, 1, 40])
# Extraction des index pour lesquels la condition est respectée.
indexes = np.where(v < 25) # Produit une liste d'index à partir de la matrice booléenne
print("Index :", indexes)
# Extraction des données à partir de ces index
v2 = v[indexes] # On en revient à utiliser des index multiples
print("v2 =", v2)
Index : (array([0, 3, 4, 5, 7, 8], dtype=int64),) v2 = [ 7 12 14 23 8 1]
Application des ces possibilités à la manipulation d'images¶
Nous l'avons déjà vue dans le notebook précédent : une image peut être considérée comme étant une matrice. De plus MatPlotLib (MPL) peut vous aider à charger une image dans une matrice Numpy.
Pour rappel, voici comment charger une image JPG avec MPL.
import numpy as np
import matplotlib.pyplot as plt
img = plt.imread("bact2.jpg")
print("shape :", img.shape) # La matrice est en 3 dimensions afin de stocker les 3 plans RGB des couleur de pixels
print("dtype :", img.dtype) # Une image JPG renvoie des valeurs de pixels comprises entre 0 et 255 (d'où le np.uint8)
print("size :", img.size)
shape : (1024, 1024, 3) dtype : uint8 size : 3145728
Attention : si vous utilisez une image au format PNG, les dimensions de votre matrice vont changer. En effet, outre la taille, le format PNG peut aussi gérer la transparence. Il y pourra donc y avoir un quatrième plan pour y stocker ces informations de transparence. De plus, les valeurs des composantes de couleurs (et de transparence) ne seront plus comprises entre 0 et 255, mais entre 0 et 1 en valeurs flottantes.
import numpy as np
import matplotlib.pyplot as plt
img = plt.imread("bact2.png")
print("shape :", img.shape) # La matrice est en 3 dimensions afin de stocker les 3 plans RGB des couleur de pixels
print("dtype :", img.dtype) # Une image JPG renvoie des valeurs de pixels comprises entre 0 et 255 (d'où le np.uint8)
print("size :", img.size)
shape : (1024, 1024, 4) dtype : float32 size : 4194304
Ensuite vous pouvez utiliser Numpy pour manipuler votre image. Par exemple, tentons d'extraire le quart haut-gauche de l'image sur un seul plan de couleur (donc en noir et blanc).
img = plt.imread("bact2.png")
img2 = img[0:img.shape[0] // 2, 0:img.shape[1] // 2, 0] # L'opérateur // réalise une division entière
print("Image d'origine :", img.shape)
print("Image finale :", img2.shape, "(il n'y a plus que deux dimensions, car il ne reste plus qu'un plan de couleurs)")
plt.subplot(1, 2, 1) # On met l'image original à gauche (grille 1, 2 => 1 ligne et 2 colonnes).
plt.imshow(img)
plt.subplot(1, 2, 2) # Et sous-partie extraite à droite.
plt.imshow(img2, cmap="gray")
plt.show()
Image d'origine : (1024, 1024, 4) Image finale : (512, 512) (il n'y a plus que deux dimensions, car il ne reste plus qu'un plan de couleurs)
Cet autre exemple, vous montre comment réduire par 12 la dimensionnalité d'une image. Par exemple, dans le domaine du deep learning, la réduction de la dimensionnalité des images est une pratique courante, principalement pour optimiser les performances et l'efficacité des modèles. En diminuant la résolution des images, on réduit la complexité computationnelle, ce qui accélère à la fois l'entraînement et l'inférence des réseaux de neurones. Cette approche aide également à limiter le risque de suradaptation (ou overfitting), où le modèle apprend trop spécifiquement les détails des données d'entraînement au lieu de généraliser. De plus, en travaillant avec des images moins détaillées, les modèles nécessitent moins de mémoire pour le stockage et le traitement, ce qui est crucial pour les systèmes à ressources limitées. La réduction de dimensionnalité peut aussi être vue comme une forme de filtrage, où l'accent est mis sur les caractéristiques les plus significatives des images, permettant aux modèles d'apprentissage profond de se concentrer sur les aspects les plus pertinents des données.
Note : pourquoi on réduit par 12 ? On réduit par 2 sur la hauteur, par 2 sur la largeur et par 3 (ou 4, s'il y a un plan d'opacité) sur les plans de couleurs. Donc, au final l'image est bien réduite par 12..
img = plt.imread("bact2.jpg")
img2 = img[::2, ::2, 0] # L'opérateur // réalise une division entière
print("Image d'origine :", img.shape)
print("Image finale :", img2.shape, "(il n'y a plus que deux dimensions, car il ne reste plus qu'un plan de couleurs)")
plt.subplot(1, 2, 1) # On met l'image original à gauche (grille 1, 2 => 1 ligne et 2 colonnes).
plt.imshow(img)
plt.subplot(1, 2, 2) # Et sous-partie extraite à droite.
plt.imshow(img2, cmap="gray")
plt.show()
Image d'origine : (1024, 1024, 3) Image finale : (512, 512) (il n'y a plus que deux dimensions, car il ne reste plus qu'un plan de couleurs)