Skip to content

Latest commit

 

History

History
713 lines (522 loc) · 18.9 KB

File metadata and controls

713 lines (522 loc) · 18.9 KB

🔝 Retour au Sommaire

13.1.2 Indexation et slicing avancés

Introduction

L'indexation et le slicing sont des techniques essentielles pour accéder et manipuler les données dans les arrays NumPy. Dans cette section, nous allons explorer les différentes méthodes pour extraire, modifier et manipuler des parties spécifiques d'un array.

import numpy as np

Indexation de base (rappel)

Arrays 1D

Pour les arrays à une dimension, l'indexation fonctionne comme pour les listes Python :

arr = np.array([10, 20, 30, 40, 50])

# Accès à un élément par son index (commence à 0)
print("Premier élément:", arr[0])      # 10  
print("Troisième élément:", arr[2])    # 30  

# Index négatifs (partir de la fin)
print("Dernier élément:", arr[-1])     # 50  
print("Avant-dernier:", arr[-2])       # 40  

Modification d'éléments

arr = np.array([10, 20, 30, 40, 50])

# Modifier un élément
arr[0] = 100  
print("Array modifié:", arr)  # [100 20 30 40 50]  

# Modifier plusieurs éléments
arr[1] = 200  
arr[3] = 400  
print("Array modifié:", arr)  # [100 200 30 400 50]  

Indexation multidimensionnelle

Arrays 2D (matrices)

Pour les arrays à deux dimensions, on utilise la notation [ligne, colonne] :

# Création d'une matrice 3x4
matrice = np.array([[1,  2,  3,  4],
                    [5,  6,  7,  8],
                    [9, 10, 11, 12]])

print("Matrice:\n", matrice)

# Accès à un élément spécifique [ligne, colonne]
print("Élément ligne 0, colonne 2:", matrice[0, 2])    # 3  
print("Élément ligne 1, colonne 1:", matrice[1, 1])    # 6  
print("Élément ligne 2, colonne 3:", matrice[2, 3])    # 12  

# Index négatifs
print("Dernière ligne, dernière colonne:", matrice[-1, -1])  # 12  
print("Première ligne, dernière colonne:", matrice[0, -1])   # 4  

Notation alternative (moins recommandée)

# On peut aussi utiliser des crochets successifs (moins lisible)
print("Élément [1, 2]:", matrice[1][2])  # 7

# Mais la notation [ligne, colonne] est préférée
print("Élément [1, 2]:", matrice[1, 2])  # 7 (recommandé)

Accès à des lignes ou colonnes entières

matrice = np.array([[1,  2,  3,  4],
                    [5,  6,  7,  8],
                    [9, 10, 11, 12]])

# Accès à une ligne entière
print("Ligne 0:", matrice[0])      # [1 2 3 4]  
print("Ligne 1:", matrice[1])      # [5 6 7 8]  
print("Dernière ligne:", matrice[-1])  # [9 10 11 12]  

# Accès à une colonne entière (avec :)
print("Colonne 0:", matrice[:, 0])   # [1 5 9]  
print("Colonne 2:", matrice[:, 2])   # [3 7 11]  
print("Dernière colonne:", matrice[:, -1])  # [4 8 12]  

Slicing (découpage) de base

Le slicing permet d'extraire des portions d'un array. La syntaxe est [début:fin:pas].

Slicing 1D

arr = np.array([0, 10, 20, 30, 40, 50, 60, 70, 80, 90])

# Extraire une portion [début:fin] (fin non incluse)
print("Indices 2 à 5:", arr[2:5])     # [20 30 40]  
print("Indices 0 à 3:", arr[0:3])     # [0 10 20]  

# Omission du début (= 0)
print("Du début à 4:", arr[:4])       # [0 10 20 30]

# Omission de la fin (= jusqu'à la fin)
print("De 6 à la fin:", arr[6:])      # [60 70 80 90]

# Avec un pas
print("Tous les 2 éléments:", arr[::2])       # [0 20 40 60 80]  
print("Indices 1 à 8, par pas de 2:", arr[1:8:2])  # [10 30 50 70]  

# Inverser un array
print("Array inversé:", arr[::-1])    # [90 80 70 60 50 40 30 20 10 0]

Slicing 2D

matrice = np.array([[1,  2,  3,  4,  5],
                    [6,  7,  8,  9, 10],
                    [11, 12, 13, 14, 15],
                    [16, 17, 18, 19, 20]])

print("Matrice complète:\n", matrice)

# Extraire les 2 premières lignes
print("\nDeux premières lignes:\n", matrice[:2])
# [[1 2 3 4 5]
#  [6 7 8 9 10]]

# Extraire les 3 premières colonnes
print("\nTrois premières colonnes:\n", matrice[:, :3])
# [[1  2  3]
#  [6  7  8]
#  [11 12 13]
#  [16 17 18]]

# Extraire une sous-matrice (lignes 1-2, colonnes 2-4)
print("\nSous-matrice [1:3, 2:5]:\n", matrice[1:3, 2:5])
# [[8  9 10]
#  [13 14 15]]

# Lignes 0 et 2, toutes les colonnes
print("\nLignes 0 et 2:\n", matrice[::2])
# [[1  2  3  4  5]
#  [11 12 13 14 15]]

# Toutes les lignes, colonnes paires
print("\nColonnes paires:\n", matrice[:, ::2])
# [[1  3  5]
#  [6  8 10]
#  [11 13 15]
#  [16 18 20]]

Indexation avancée avec des listes

Vous pouvez utiliser des listes ou des arrays pour sélectionner des éléments spécifiques.

Indexation par liste d'indices

arr = np.array([10, 20, 30, 40, 50, 60, 70, 80, 90])

# Sélectionner des éléments aux indices 1, 3, 5
indices = [1, 3, 5]  
print("Éléments aux indices 1, 3, 5:", arr[indices])  # [20 40 60]  

# On peut aussi le faire directement
print("Éléments:", arr[[0, 2, 4, 8]])  # [10 30 50 90]

# Les indices peuvent être répétés
print("Avec répétitions:", arr[[1, 1, 3, 3]])  # [20 20 40 40]

Indexation avancée 2D

matrice = np.array([[1,  2,  3,  4],
                    [5,  6,  7,  8],
                    [9, 10, 11, 12]])

# Sélectionner des lignes spécifiques
lignes = [0, 2]  
print("Lignes 0 et 2:\n", matrice[lignes])  
# [[1 2 3 4]
#  [9 10 11 12]]

# Sélectionner des éléments spécifiques
# Format : lignes et colonnes comme deux listes
lignes = [0, 1, 2]  
colonnes = [1, 2, 3]  
print("Éléments diagonaux:", matrice[lignes, colonnes])  # [2 7 12]  

# Sélectionner les coins de la matrice
lignes = [0, 0, 2, 2]  
colonnes = [0, 3, 0, 3]  
print("Les 4 coins:", matrice[lignes, colonnes])  # [1 4 9 12]  

Indexation booléenne

L'indexation booléenne utilise un array de booléens (True/False) pour sélectionner des éléments.

Principe de base

arr = np.array([10, 25, 30, 15, 40, 5, 35])

# Créer un masque booléen
masque = arr > 20  
print("Masque (> 20):", masque)  
# [False True True False True False True]

# Utiliser le masque pour filtrer
print("Valeurs > 20:", arr[masque])  # [25 30 40 35]

# Écriture directe (plus courante)
print("Valeurs > 20:", arr[arr > 20])  # [25 30 40 35]

Conditions multiples

arr = np.array([10, 25, 30, 15, 40, 5, 35, 20])

# Opérateur ET : &
print("Valeurs entre 15 et 35:", arr[(arr >= 15) & (arr <= 35)])
# [25 30 15 35 20]

# Opérateur OU : |
print("Valeurs < 15 ou > 35:", arr[(arr < 15) | (arr > 35)])
# [10 5 40]

# Opérateur NON : ~
print("Valeurs PAS égales à 25:", arr[arr != 25])
# [10 30 15 40 5 35 20]

print("Valeurs PAS > 30:", arr[~(arr > 30)])
# [10 25 30 15 5 20]

Indexation booléenne 2D

matrice = np.array([[1,  2,  3,  4],
                    [5,  6,  7,  8],
                    [9, 10, 11, 12]])

# Sélectionner toutes les valeurs > 6
print("Valeurs > 6:", matrice[matrice > 6])  # [7 8 9 10 11 12]

# Créer un masque booléen
masque = matrice % 2 == 0  # Valeurs paires  
print("Masque des valeurs paires:\n", masque)  
print("Valeurs paires:", matrice[masque])  # [2 4 6 8 10 12]  

# Sélectionner des lignes selon une condition
# Lignes dont la première colonne est > 5
masque_lignes = matrice[:, 0] > 5  
print("Lignes où première colonne > 5:\n", matrice[masque_lignes])  
# [[9 10 11 12]]

Modification avec indexation

Modification par slicing

arr = np.array([0, 10, 20, 30, 40, 50])

# Modifier une tranche
arr[1:4] = 99  
print("Après modification:", arr)  # [0 99 99 99 40 50]  

# Modifier avec une séquence de valeurs
arr[1:4] = [11, 22, 33]  
print("Après modification:", arr)  # [0 11 22 33 40 50]  

Modification avec indexation booléenne

arr = np.array([10, 25, 30, 15, 40, 5, 35])

# Mettre toutes les valeurs > 30 à 30
arr[arr > 30] = 30  
print("Valeurs plafonnées à 30:", arr)  # [10 25 30 15 30 5 30]  

# Remplacer les valeurs négatives par 0
arr = np.array([5, -2, 8, -7, 3, -1])  
arr[arr < 0] = 0  
print("Négatifs remplacés par 0:", arr)  # [5 0 8 0 3 0]  

# Incrémenter certaines valeurs
arr = np.array([10, 20, 30, 40, 50])  
arr[arr < 35] += 5  
print("Valeurs < 35 incrémentées:", arr)  # [15 25 35 40 50]  

Modification 2D

matrice = np.array([[1, 2, 3],
                    [4, 5, 6],
                    [7, 8, 9]])

# Modifier une ligne
matrice[1] = [40, 50, 60]  
print("Ligne modifiée:\n", matrice)  

# Modifier une colonne
matrice[:, 2] = [300, 600, 900]  
print("Colonne modifiée:\n", matrice)  

# Modifier une sous-matrice
matrice[0:2, 0:2] = [[11, 22], [44, 55]]  
print("Sous-matrice modifiée:\n", matrice)  

Indexation fancy (combinaisons avancées)

L'indexation fancy combine plusieurs techniques pour des sélections complexes.

Combiner lignes et colonnes spécifiques

matrice = np.array([[1,  2,  3,  4],
                    [5,  6,  7,  8],
                    [9, 10, 11, 12],
                    [13, 14, 15, 16]])

# Sélectionner plusieurs lignes et colonnes
lignes = [0, 2, 3]  
colonnes = [1, 3]  

# Méthode 1 : avec np.ix_ (grille)
resultat = matrice[np.ix_(lignes, colonnes)]  
print("Sous-matrice avec np.ix_:\n", resultat)  
# [[2  4]
#  [10 12]
#  [14 16]]

# Méthode 2 : slicing multiple
resultat2 = matrice[lignes][:, colonnes]  
print("Même résultat:\n", resultat2)  

Indexation avec broadcasting

matrice = np.array([[1,  2,  3,  4],
                    [5,  6,  7,  8],
                    [9, 10, 11, 12]])

# Sélectionner la diagonale
indices = np.arange(3)  
diagonale = matrice[indices, indices]  
print("Diagonale:", diagonale)  # [1 6 11]  

# Anti-diagonale
anti_diag = matrice[indices, [2, 1, 0]]  
print("Anti-diagonale:", anti_diag)  # [3 6 9]  

Fonctions utiles pour l'indexation

where() - Trouver les indices

arr = np.array([10, 25, 30, 15, 40, 5, 35])

# Trouver les indices où la condition est vraie
indices = np.where(arr > 20)  
print("Indices des valeurs > 20:", indices)  # (array([1, 2, 4, 6]),)  
print("Valeurs correspondantes:", arr[indices])  # [25 30 40 35]  

# where avec 2D
matrice = np.array([[1, 2, 3],
                    [4, 5, 6],
                    [7, 8, 9]])

lignes, colonnes = np.where(matrice > 5)  
print("Positions (lignes, colonnes) où valeur > 5:")  
print("Lignes:", lignes)      # [1 2 2 2]  
print("Colonnes:", colonnes)  # [2 0 1 2]  
print("Valeurs:", matrice[lignes, colonnes])  # [6 7 8 9]  

argmax() et argmin() - Indices des extrema

arr = np.array([30, 10, 50, 20, 40])

# Index du maximum
idx_max = np.argmax(arr)  
print("Index du max:", idx_max)  # 2  
print("Valeur max:", arr[idx_max])  # 50  

# Index du minimum
idx_min = np.argmin(arr)  
print("Index du min:", idx_min)  # 1  
print("Valeur min:", arr[idx_min])  # 10  

# Avec des arrays 2D
matrice = np.array([[1, 5, 3],
                    [9, 2, 7]])

# Maximum global
idx_max_global = np.argmax(matrice)  
print("Index du max (aplati):", idx_max_global)  # 3  

# Maximum par ligne
idx_max_lignes = np.argmax(matrice, axis=1)  
print("Index max par ligne:", idx_max_lignes)  # [1 0]  

# Maximum par colonne
idx_max_colonnes = np.argmax(matrice, axis=0)  
print("Index max par colonne:", idx_max_colonnes)  # [1 0 1]  

nonzero() - Indices des éléments non nuls

arr = np.array([0, 5, 0, 8, 0, 3, 0])

# Trouver les indices des éléments non nuls
indices_non_nuls = np.nonzero(arr)  
print("Indices non nuls:", indices_non_nuls)  # (array([1, 3, 5]),)  
print("Valeurs non nulles:", arr[indices_non_nuls])  # [5 8 3]  

# Avec 2D
matrice = np.array([[0, 1, 0],
                    [2, 0, 3],
                    [0, 0, 4]])

lignes, colonnes = np.nonzero(matrice)  
print("Lignes des non-nuls:", lignes)      # [0 1 1 2]  
print("Colonnes des non-nuls:", colonnes)  # [1 0 2 2]  

Vues vs Copies

C'est un concept crucial en NumPy : comprendre quand on travaille avec une vue (référence) ou une copie.

Les slices créent des vues

arr = np.array([0, 10, 20, 30, 40])

# Un slice crée une vue, pas une copie
vue = arr[1:4]  
print("Vue:", vue)  # [10 20 30]  

# Modifier la vue modifie l'original !
vue[0] = 999  
print("Vue après modification:", vue)  # [999 20 30]  
print("Array original:", arr)  # [0 999 20 30 40] ← modifié !  

Créer une copie explicite

arr = np.array([0, 10, 20, 30, 40])

# Créer une vraie copie avec .copy()
copie = arr[1:4].copy()  
print("Copie:", copie)  # [10 20 30]  

# Modifier la copie ne modifie PAS l'original
copie[0] = 999  
print("Copie après modification:", copie)  # [999 20 30]  
print("Array original:", arr)  # [0 10 20 30 40] ← inchangé  

L'indexation fancy crée des copies

arr = np.array([0, 10, 20, 30, 40])

# L'indexation avec une liste crée une copie
indices = [1, 3]  
copie = arr[indices]  
copie[0] = 999  
print("Array original:", arr)  # [0 10 20 30 40] ← inchangé  

# L'indexation booléenne crée aussi une copie
arr = np.array([10, 20, 30, 40, 50])  
copie = arr[arr > 25]  
copie[0] = 999  
print("Array original:", arr)  # [10 20 30 40 50] ← inchangé  

Vérifier si c'est une vue

arr = np.array([0, 10, 20, 30, 40])

# Slice = vue
vue = arr[1:4]  
print("Vue partage la base:", vue.base is arr)  # True  

# Copie explicite
copie = arr[1:4].copy()  
print("Copie partage la base:", copie.base is arr)  # False  

# L'array original n'a pas de base
print("Original a une base:", arr.base is None)  # True

Exemples pratiques

Exemple 1 : Normalisation min-max

# Normaliser des données entre 0 et 1
donnees = np.array([10, 25, 15, 30, 20, 35])

min_val = np.min(donnees)  
max_val = np.max(donnees)  

# Formule : (x - min) / (max - min)
donnees_normalisees = (donnees - min_val) / (max_val - min_val)  
print("Données normalisées:", donnees_normalisees)  
# [0.   0.6  0.2  0.8  0.4  1.  ]

Exemple 2 : Remplacement conditionnel

# Remplacer les valeurs aberrantes (outliers)
donnees = np.array([15, 18, 200, 19, -50, 17, 20, 16], dtype=float)

# Définir des seuils
seuil_bas = 10  
seuil_haut = 100  

# Créer une copie pour ne pas modifier l'original
donnees_clean = donnees.copy()

# Remplacer les valeurs hors limites par la médiane
mediane = np.median(donnees[(donnees >= seuil_bas) & (donnees <= seuil_haut)])  
donnees_clean[(donnees_clean < seuil_bas) | (donnees_clean > seuil_haut)] = mediane  

print("Données originales:", donnees)  
print("Données nettoyées:", donnees_clean)  
# [15 18 17.5 19 17.5 17 20 16]

Exemple 3 : Extraction de sous-matrices

# Matrice représentant des scores d'étudiants
# Lignes = étudiants, Colonnes = matières
scores = np.array([[85, 90, 78, 92],
                   [88, 75, 95, 87],
                   [70, 85, 80, 88],
                   [92, 88, 85, 90],
                   [78, 82, 88, 84]])

# Noms des matières
matieres = ['Math', 'Physique', 'Chimie', 'Bio']

# Extraire les scores en Math et Chimie (colonnes 0 et 2)
math_chimie = scores[:, [0, 2]]  
print("Scores Math et Chimie:\n", math_chimie)  

# Trouver les étudiants avec plus de 85 en Math
bons_en_math = scores[scores[:, 0] > 85]  
print("\nÉtudiants avec > 85 en Math:\n", bons_en_math)  

# Moyenne de chaque étudiant
moyennes = np.mean(scores, axis=1)  
print("\nMoyennes des étudiants:", moyennes)  

# Étudiants avec moyenne > 85
elite = scores[moyennes > 85]  
print("\nÉtudiants d'élite (moyenne > 85):\n", elite)  

Exemple 4 : Grille de données

# Créer une grille de coordonnées
x = np.arange(0, 5)  # [0 1 2 3 4]  
y = np.arange(0, 3)  # [0 1 2]  

# Créer un meshgrid
X, Y = np.meshgrid(x, y)

print("Grille X:\n", X)
# [[0 1 2 3 4]
#  [0 1 2 3 4]
#  [0 1 2 3 4]]

print("Grille Y:\n", Y)
# [[0 0 0 0 0]
#  [1 1 1 1 1]
#  [2 2 2 2 2]]

# Calculer des distances depuis l'origine
distances = np.sqrt(X**2 + Y**2)  
print("Distances:\n", distances)  

# Sélectionner les points à distance < 2.5
masque = distances < 2.5  
print("\nPoints proches (distance < 2.5):")  
print("X:", X[masque])  
print("Y:", Y[masque])  

Exemple 5 : Traitement d'image simplifié

# Simuler une image en niveaux de gris (8x8)
image = np.random.randint(0, 256, size=(8, 8))  
print("Image originale:\n", image)  

# Extraire une région d'intérêt (ROI) - centre 4x4
roi = image[2:6, 2:6]  
print("\nRégion d'intérêt:\n", roi)  

# Augmenter la luminosité de la ROI
image[2:6, 2:6] = np.clip(image[2:6, 2:6] + 50, 0, 255)  
print("\nImage avec ROI éclaircie:\n", image)  

# Créer un masque pour les pixels sombres
sombre = image < 100  
print("\nMasque pixels sombres:\n", sombre)  

# Éclaircir les pixels sombres
image[sombre] = image[sombre] + 50  
print("\nImage après éclaircissement des zones sombres:\n", image)  

Astuces et pièges à éviter

⚠️ Piège 1 : Modifier une vue modifie l'original

# ❌ Attention à ce piège courant
arr = np.array([1, 2, 3, 4, 5])  
sous_arr = arr[1:4]  # Vue, pas copie !  
sous_arr[:] = 0      # Modifie l'original  
# arr devient [1, 0, 0, 0, 5]

# ✅ Solution : faire une copie
arr = np.array([1, 2, 3, 4, 5])  
sous_arr = arr[1:4].copy()  
sous_arr[:] = 0  
# arr reste [1, 2, 3, 4, 5]

⚠️ Piège 2 : Conditions multiples sans parenthèses

arr = np.array([10, 20, 30, 40])

# ❌ Erreur : oubli des parenthèses
# arr[arr > 15 & arr < 35]  # Erreur !

# ✅ Correct : avec parenthèses
resultat = arr[(arr > 15) & (arr < 35)]

⚠️ Piège 3 : Taille incompatible lors de l'assignation

arr = np.array([1, 2, 3, 4, 5])

# ❌ Erreur : nombre d'éléments différent
# arr[1:4] = [10, 20]  # Erreur !

# ✅ Correct : même taille
arr[1:4] = [10, 20, 30]

# ✅ Ou : broadcasting avec une valeur unique
arr[1:4] = 99

Bonnes pratiques

  1. Utilisez .copy() quand nécessaire : Si vous devez modifier un sous-array sans affecter l'original
  2. Préférez l'indexation booléenne : Plus lisible que les boucles pour filtrer
  3. Attention aux vues : Souvenez-vous que les slices sont des vues
  4. Utilisez des noms explicites : arr[arr > 0] plutôt que créer un masque intermédiaire si simple
  5. Évitez les boucles : L'indexation avancée et booléenne remplace souvent les boucles

Résumé

L'indexation et le slicing dans NumPy offrent des outils puissants pour :

  • Indexation de base : Accéder à des éléments individuels avec [i] ou [i, j]
  • Slicing : Extraire des portions avec [début:fin:pas]
  • Indexation booléenne : Filtrer avec des conditions arr[arr > 5]
  • Indexation fancy : Sélectionner avec des listes d'indices
  • Modification : Changer des valeurs avec toutes ces techniques
  • Vues vs copies : Comprendre quand l'original est affecté

Ces techniques permettent de manipuler efficacement les données sans écrire de boucles, ce qui rend le code plus rapide et plus lisible.

Dans la section suivante, nous explorerons la manipulation de données avec Pandas, qui s'appuie sur ces concepts NumPy pour offrir des fonctionnalités encore plus puissantes.

⏭️ Manipulation de données avec Pandas