Come anticipato nella lezione precedente, oltre a poter creare un oggetto ndarray contenente diverse tipologie di dato, è possibile:
- accedere ai contenuti di questo tipo di oggetti;
- modificare gli
ndarray
attraverso le operazioni di indexing o slicing, come già accade per altri tipi di strutture dati in Python.
In questa lezione, vedremo nel dettaglio l’indicizzazione degli elementi di un ndarray
e le principali operazioni di slicing su questo tipo di struttura dati.
Indexing
Come si è potuto vedere in queste lezioni, gli elementi di un ndarray
seguono una indicizzazione in base zero, ossia l’indice in cui si troverà il primo elemento dell’ndarray
è 0
, contrariamente a quanto accade in altri linguaggi di analisi matematica come Matlab.
Vediamo brevemente come poter effettuare l’accesso ai dati di un ndarray
in base al tipo di rappresentazione: vettore, matrice, tensore.
L’accesso ai dati di un vettore è in tutto e per tutto uguale all’accesso ad una lista in Python.
vect = np.array([1, 2, 3, 4])
vect[2]
> 3
Analogamente, per accedere a ndarray
a due dimensioni, ossia una matrice, basterà passare l’indice della riga e della colonna in cui è posizionato l’elemento di interesse come segue.
matrix = np.random.randint(1, 13, (3,3))
matrix[1][2]
> 7
Infine, per accedere a un elemento di un tensore è necessario specificare:
- l’indice della dimensione che contiene il valore di interesse;
- l’indice della riga e della colonna in cui l’elemento è posizionato.
Vediamo un esempio.
tensor = np.random.randint(1,100, (4,4,4))
tensor[1][3][2]
> 29
In questo esempio, abbiamo:
- generato un tensore di soli valori interi randomici compresi tra 1 e 100 e composto da 4 dimensioni;
- effettuato l’accesso al valore in posizione
(3,2)
della seconda dimensione del tensore a 4 dimensioni.
Come già accade con le strutture dati in Python, è possibile utilizzare l’indice negativo per accedere a un elemento. Ad esempio, data una matrice 3x3
, per accedere all'ultimo elemento della seconda riga basterà:
m = np.array([[1,2,3], [4,5,6], [7,8,9]])
m[1][-1]
> 6
Se invece volessimo accedere all’ultimo elemento della terza riga dell’ultima dimensione del tensore precedentemente creato, sarà sufficiente specificare gli indici come segue.
tensor[-1][2][3]
> 50
Come possiamo vedere, l’indicizzazione di un ndarray
permette un accesso facile e veloce agli elementi e alle dimensioni in esso specificati.
Indexing tramite Ellipsis
L’ellipsis (...
) è una costante predefinita in Python3 che può essere utilizzata in NumPy per omettere le dimensioni intermedie quando si devono specificare elementi o intervalli con []
.
Vediamo qualche esempio.
Data una matrice 4x5
, selezioniamo la prima colonna.
m = np.arange(20).reshape(4, 5)
m[...,0]
> array([ 0, 5, 10, 15])
Dato un tensore composto da 4 dimensioni, ognuna delle quali si compone da una matrice 4x4
, selezioniamo la prima colonna di ogni matrice che lo compone.
tensor = np.random.randint(1,100, (4,4,4))
tensor[...,0]
> array([[24, 42, 65, 6],
[12, 70, 82, 17],
[29, 11, 42, 46],
[79, 66, 81, 83]])
Al contrario, per prendere solo la prima matrice del tensore basterà:
tensor[0,...]
> array([[24, 13, 55, 39],
[42, 53, 53, 10],
[65, 47, 47, 96],
[ 6, 95, 46, 42]])
Infine, se volessimo prendere l’ultima riga della prima matrice del tensore, sarà sufficiente impostare i valori come segue.
tensor[1,...,2]
> array([79, 49, 63, 76])
La costante ...
e Ellipsis
sono interscambiabili tra loro. È consigliato per chiarezza di lettura l’utilizzo di ...
.
Slicing
Lo slicing è l’operazione che permette di sezionare un ndarray
su una o più dimensioni estrapolando un set di dati con un determinato step.
La sintassi base dello slicing è rappresentata dalla tripla i:j:k
dove:
i
rappresenta l’indice di inizio;j
rappresenta l’indice di stop;k
è lo step di campionamento preso in considerazione e deve essere diverso da zero.
Tutti e tre questi valori devono essere interi e possono essere positivi e/o negativi.
Vediamo qualche esempio.
Dato un vettore v
composto da 10 elementi, estraiamo dal secondo all’ottavo elemento con un step k=2
.
v = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
v[1:8:2]
> array([1, 3, 5, 7])
Con gli indici i
e j
negativi, invece, il comportamento sarà leggermente diverso. Dato il vettore v selezioniamo dall’elemento in posizione 7 a quello in posizione 9 usando i seguenti valori i=-3, j=10, k=1
.
v[-3:10]
> array([7, 8, 9])
Impostando invece lo step k
ad un valore negativo, avremo come risultato l’array invertito, andando dall’indice maggiore al minore. Vediamo un esempio.
v[-3:3:-1]
> array([7, 6, 5, 4])
Come si può notare, il risultato è un vettore costruito a partire dall’elemento 7 all’elemento 4.
Lo slicing può essere applicato anche a ndarray
con più di una dimensione, come le matrici. In questo caso, la tripla di indici i:j:k
viene applicata sulle righe e sulle colonne della matrice. Vediamo un semplice esempio.
m = np.arange(20).reshape(4, 5)
m[1:4,2:5]
> array([[ 7, 8, 9],
[12, 13, 14],
[17, 18, 19]])
In questo caso, abbiamo selezionato dalla 2° alla 4° riga e dalla 3° alla 5 colonna.
Nonostante la somiglianza, l’operazione di slicing è diversa dall’operazione di indexing. Ad esempio, data la matrice:
m = np.array([[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19],
[20, 21, 22, 23, 24],
[25, 26, 27, 28, 29]])
con l’operazione di indexing viene prodotto un vettore
m[1,2:4]
> [17 18]
con l’operazione di slicing con step k=1
, invece, verrà prodotta una matrice con una sola riga.
m[1:2,2:4]
> [[17 18]]
Boolean Indexing
Fino ad ora abbiamo visto come compiere l’operazione di slicing e selezionare gli elementi di un ndarray
usando gli indici. Ciò è molto utile quando conosciamo gli indici esatti degli elementi che vogliamo selezionare. Tuttavia, ci sono molte situazioni in cui non abbiamo una conoscenza a priori degli indici degli elementi da selezionare.
Il boolean indexing è l’operazione che ci viene in aiuto in questi casi, consentendoci di selezionare elementi utilizzando condizioni booleane invece di indici espliciti.
Ad esempio, supponiamo di avere un ndarray
5x5
di numeri interi compresi tra 0 e 24.
matrix = np.arange(25).reshape(5, 5)
Selezioniamo solo gli elementi maggiori di 10:
matrix[matrix > 10]
> [11 12 13 14 15 16 17 18 19 20 21 22 23 24]
Selezioniamo invece gli elementi minori o uguali a 7:
matrix[matrix <= 7]
> [0 1 2 3 4 5 6 7]
Gli elementi compresi tra 10 e 17:
matrix[(matrix > 10) & (matrix < 17)]
> [11 12 13 14 15 16]
In tutti e tre i casi il risultato della selezione è stato un array contenente tutti gli elementi della selezione. Queste condizioni booleane possono anche essere staticizzate in apposite variabili e riapplicate successivamente. Ad esempio:
matrix[(matrix > 10) & (matrix < 17)]
può essere riscritta come
my_mask=(matrix > 10) & (matrix < 17)
matrix[my_mask]
Questo rende più semplice e ordinata la lettura del codice.
Grazie al boolean indexing possiamo, inoltre, selezionare un set di dati e cambiarne il valore. Ad esempio, selezioniamo tutti i valori compresi tra 10 e 14 e modifichiamoli in -1.
matrix[(matrix > 9) & (matrix < 15)] = -1
> [[ 0 1 2 3 4]
[ 5 6 7 8 9]
[-1 -1 -1 -1 -1]
[15 16 17 18 19]
[20 21 22 23 24]]
Il codice di questa lezione con ulteriori esempi è disponibile su GitHub.