ripartiamo da dove avevamo lasciato.
Ci sono una sessantina di funzioni che permettono di operare con gli array elemento per elemento.
Queste funzioni sono usate anche quando si usano gli operatori matematici, di comparazione etc. in modo tradizionale.
ad esempio quando uso l'operatore + per sommare due array, l'interprete chiama la ufunc (universal function) numpy.add
Passiamo in rassegna gli aspetti meno banali che interessano gli operatori element-wise
la comparazione tra array da luogo a degli array di bool ...
a=np.arange(5*4).reshape(5,4)
a
a>7
#genero un array con shape (5,4) e interi random tra 0 e 20 (escluso, come nei range)
b=np.random.randint(0,20,(5,4))
b
a>b
Le operazioni logiche avvengono elemento per elemento solo se si usano gli operatori bitwise (& , |, ~ , ^) o le corrispondenti ufunc (comparison-functions)
Gli operatori logici and, or , not si riferiscono all'array nella sua interezza.
a
(a > 5) & (a < 14)
(a < 5) | (a > 14)
OCCHIO ALLA precedenza degli operatori logici su quelli di comparazione:
a<5 | a>14
l'operazione che fa scaturire l'errore è 5|a che viene eseguita a causa della precedenza di | sugli operatori di comparazione
Ma cosa succede quando voglio operare con array di dimensioni diverse?
Il broadcasting è l'insieme di regole che permette alla ufunc di operare su array che non hanno esattamente le stesse dimensioni.
Ci ò non è sempre possibile ma il broadcasting amplia di molto le possibilit à di operare.
Prima di cercare di riassumere le regole e le condizioni per l'applicazione del broadcasting, vediamo degli esempi:
a=np.arange(3*4).reshape(3,4)
print('a=\n',a)#(3,4)
b=np.arange(10,40,10).reshape(3,1)
print('b=\n',b)
a+b
l'operazione sopra pu ò essere pensata come eseguita in due fasi:
prima trasformo b in modo da avere le stesse dimensioni di a replicandolo 4 volte (regola 2, vedi sotto)
newb=np.concatenate((b,b,b,b),1)
newb
NOTA BENE: questa trasformazione NON avviene effettivamente, semplicemente la ufunc add fa riferimento ai valori contenuti nella prima ed unica colonna di b per effettuare la somma
poi sommo a con newb
a+newb
a=np.arange(3*4).reshape(4,3)
print('a=\n',a)#(3,4)
b=np.arange(10,40,10).reshape(1,3)
print('b=\n',b)
a+b
al solito è come se prima avessi trasformato b replicandolo 4 volte (regola 2)
newb = np.concatenate((b,b,b,b),0)
newb
e poi:
a+newb
a=np.arange(3).reshape(1,3) # una riga, tre colonne
b=np.arange(10,50,10).reshape(4,1) # (4,1) quattro righe, 1 colonna
print('a=\n',a)
print('b=\n',b)
a+b
In questo caso, per effettuare il broadcasting devo trasformare entrambi i vettori
trasformo a in modo che abbia lo stesso numero di righe di b replicandolo 4 volte (regola 2)
newa = np.concatenate((a,a,a,a),0)
newa
trasformo b in modo che abbia lo stesso numero di colonne di a replicandolo 3 volte (regola 2)
newb = np.concatenate((b,b,b),1)
newb
e quindi sommo
newa+newb
Ci sono due fondamentali regole alla base del broadcasting:
Ne consegue che due o pi ù array sono compatibili per il broadcasting se si verifica una delle tre seguenti condizioni:
Negli esempi che abbiamo visto è stata applicata solo la regola 2 e quindi abbiamo esaminato casi in cui si verificavano le condizioni a e b.
Vediamo un caso nel quale si manifesta la condizione c e si applica la regola 1.
Per capire il broadcasting è utile avere chiaro come fare a modificare la shape di un array aggiungendogli dimensioni ma non elementi (vedi regola 1).
a è un array a 1 dimensione con 5 elementi:
a=arange(5)
print(a.shape)
print(a)
trasformo a in un array di due dimensioni con 1 riga e 5 colonne
a.resize(1,5)
print('a.shape=',a.shape)
print('a=',a)
trasformo a in un array con 5 righe ed una colonna
a.resize(5,1)
print('a.shape=',a.shape)
print('a=',a)
trasformo a in un array con 5 piani, 1 riga ed una colonna
a.resize(5,1,1)
print('a.shape=',a.shape)
print('a=',a)
etc etc
a=arange(5)
print(a.shape)
print(a)
restituisce una vista di a con 1 riga e 5 colonne
a[np.newaxis , :]
restituisce una vista di a con 5 righe e 1 colonna
a[: , None] #NB np.newaxis è equivalente a None
restituisce una vista di a con 5 piani, 1 riga e 1 colonna
a[:,None,None]
Considero la somma in 2D tra un vettore 1D con 3 elementi ed un vettore colonna
Controllate bene le differenze rispetto al caso precedente!!!
a=np.arange(3)# 3 elementi
b=np.arange(10,50,10).reshape(4,1) # (4,1) quattro righe, 1 colonna
print('a=\n',a)
print('b=\n',b)
NOTA BENE: c' è differenza tra i due array v1 e v2:
v1 = np.arange(3)
v2 = np.arange(3).reshape(1,3)
print('v1 = ',v1)
print('v2 = ',v2)
print('v1.shape = ',v1.shape)
print('v2.shape = ',v2.shape)
v1 è un array a 1 dimensione con 3 elementi
v2 è un array con 2 dimensioni e 3 elementi su una riga
il broadcasting in questo caso pu ò essere scomposto in 3 passaggi
Prima applico la regola 1 e aggiungo una dimensione ad a senza per ò aggiungere alcun elemento, usando slice e newaxis:
newa1 = a[np.newaxis , :] # avrei potuto usare anche newa1=a.reshape(1,3) ...
newa1
Poi applico la regola 2 e trasformo newa1 in modo da avere lo stesso numero di righe di b replicandolo 4 volte
newa2 = np.concatenate((newa1,newa1,newa1,newa1),0)
newa2
Quindi applico nuovamente la regola 2 e trasformo b in modo da avere lo stesso numero di colonne di newa2 replicandolo 3 volte lungo l'asse 1
newb = np.concatenate((b,b,b),1)
newb
ed infine sommo
newa2+newb
vi ricordo che io avevo ottenuto lo stesso risultato con a+b !!!!!!!!
a+b
generare il seguente array con 10x10x10 elementi:
#creo un array che rappresenta l'incremento relativo ai diversi piani (1000)
#l'array è strutturato per piani (di un solo elemento)
a=np.arange(0,10000,1000)[:,None,None]
print('a.shape=',a.shape)
print('a=',a)
#creo un array che rappresenta l'incremento relativo alle righe (100)
b=np.arange(0,1000,100)[None,:,None]
print('b.shape=',b.shape)
print('b=',b)
#creo un array che rappresenta l'incremento relativo alle colonne (10)
c=np.arange(0,100,10)[None,None,:]
print('c.shape=',c.shape)
print('c=',c)
np.set_printoptions(threshold=10)
np.set_printoptions(edgeitems=2)
print(a+b+c)
a=np.arange(0,10000,1000)[:,None,None]#uguale al precedente caso
b2=np.arange(0,1000,100)[:,None] #una dimensione meno del precedente caso
c2=np.arange(0,100,10) #due dimensioni in meno
print('a.shape=',a.shape)
print('-'*20)
print('b.shape=',b.shape)
print('b2.shape=',b2.shape)
print('-'*20)
print('c.shape=',c.shape)
print('c2.shape=',c2.shape)
a+b2+c2
se vogliamo sommare due array monodimensionali di N elementi, va e vb, possiamo effettuare un ciclo
nella seguente funzione misuro il tempo necessario ad eseguire l'operazione di somma escuso il tempo necessario a creare l'array dei risultati
import time
def sommaciclo(va,vb):
#determino il numero di elementi
N=min(len(va),len(vb))
#creo ed inizializzo l'array dei risultati
vres = np.zeros(N)
t1 = time.clock()
#somma con un ciclo sugli elementi
#dei vettori
for i in range(N):
vres[i] = va[i] + vb[i]
t2 = time.clock()
retval=t2-t1
print('sommaciclo {1} elementi, t={0:.6f}'.format(retval,N))
return retval
Oppure posso usare la somma tra vettori che richiama implicitamente la ufunc numpy.add
nella seguente funzione misuro il tempo necessario ad eseguire l'operazione di somma compreso il tempo necessario a creare l'array dei risultati che non è scorporabile
def sommavect(va,vb):
t1 = time.clock()
vr = va + vb
t2 = time.clock()
retval=t2-t1
print('sommavect {1} elementi, t={0:.6f}'.format(retval,va.size))
return retval
Adesso confronto il tempo di esecuzione delle due somme al crescere del numero di elementi contenuti
for N in [100,1000,10000,100000,1000000]:
va=np.arange(N)
vb=np.arange(200,200+N)
print('-'*30)
tc=sommaciclo(va,vb)
tv=sommavect(va,vb)
print('ratio = {0}'.format(tc/tv))
Come si pu ò vedere al crescere di N la versione 'vettorizzata' è pi ù conveniente
Tale convenienza dipende principalmente dal fatto che il ciclo sugli elementi è eseguito nella routine compilata e non mediante l'interprete!!!!
data una generica funzione di python non è detto che questa sia compatibile con array passati come argomento.
Ad esempio consideriamo la seguente myfunc
def myfunc(a, b):
"""Return a+b if a>b, otherwise return a-b"""
if a > b:
return a + b
else:
return a - b
definiamo a e b
a=np.arange(5)
b=np.random.randint(0,10,(5))
a,b
se applichiamo myfunc ad a e b l'interprete segnala un errore
myfunc(a,b)
(a>b) è un array di bool che è valutato in un costrutto if che richiede una variabile riconducibile a vero o falso ....
Se vogliamo che myfunc lavori come una ufunc ovvero elemento per elemento sull'array posso crearne una nuova versione studiandola ad-hoc usando le maschere (che vedremo tra poco):
def myfunc2(a,b):
if a.shape == b.shape :
retval = np.zeros_like(a)
i = a>b
# print(i)
retval[i] = a[i] + b[i]
i = ~i
#print(i)
retval[i] = a[i] - b[i]
return retval
else:
return np.array([])
print(a)
print(b)
myfunc2(a,b)
Questa funzione adesso lavora con array che per ò devono avere la stessa forma.
Nel caso a e b siano di forma diversa ma compatibili con il broadcasting la funzione restituisce un array vuoto
a=np.arange(5)
b=np.random.randint(0,10,(5)).reshape(5,1)
print('a=',a)
print('b=',b)
myfunc2(a,b)
esiste un modo semplice per trasformare una funzione di python ordinaria in una ufunc che lavora su array elemento per elemento e supporta il broadcasting e cio è utilizzare la funzione numpy.vectorize:
myfunc_v = np.vectorize(myfunc)
in questo caso la nuova funzione supporter à pienamente anche il broadcasting:
myfunc_v(a,b)
la funzione 'vettorizzata' lavora anche con numeri ma restituisce comunque un array
myfunc_v(3,2)
a=np.arange(2*3*4).reshape(2,3,4)
a
Abbiamo gi à visto che gli operatori di comparazione danno come risultato degli array di bool con la stessa shape degli array di partenza
mask = a>8
mask
se uso una matrice di bool (che chiamo maschera) per indicizzare un array dove la shape della maschera corrisponde a quella dell'array di interesse
ottengo un array monodimensionale che contiene tutti gli elementi dell'array corrispondenti ad un elemento True della maschera
a[mask]
Questa espressione pu ò essere usata per assegnare nuovi valori ad un array
a[mask]=1000
a
NB: se assegno a[mask] ad un nuovo array questo NON si comporta come una vista ma ottengo una copia!!!
b=a[mask]
b[0]=50000
print(a)
print(b.flags.owndata)
L'indicizzazione con bool pu ò essere effettuata anche secondo un'altra modalit à .
Al solito illustriamola con degli esempi.
a=np.arange(2*3*4).reshape(2,3,4)
a
seleziono uno dei due piani (quello che corrisponde a True nell'array di bool)
secondopianodidue = np.array([False,True])
b=a[secondopianodidue,:,:]
print(b)
seleziono una delle righe (per ogni piano)
secondarigaditre = np.array([False , True , False])
print(a[: , secondarigaditre , :])
secondaeterzacolonnadiquattro = np.array( [False , True , True , False])
print(a[: , : , secondaeterzacolonnadiquattro])
print(a[secondopianodidue , secondarigaditre , secondaeterzacolonnadiquattro])
anche in questo caso posso assegnare un nuovo valore agli elementi dell'array originario
a[secondopianodidue,:,:]=200000
a
ma con se assegno un nome al sub-array indicizzato ottengo una copia!!!
b=a[secondopianodidue,:,:]
b[0]=-10000
print(a)
print(b.flags.owndata)
per indicizzare un array 1D come a
a=arange(15)**2
print(a)
posso usare un array 1D ed ottengo un array 1D come risultato
i= np.array([2,2,5,7,14])#NB elementi ripetuti
print(a[i])
pi ù in generale il risultato dell'indicizzazione ha la shape dell'array di indici utilizzato
i=np.array([[1,3],[1,6],[14,14]])
print(a[i])
consideriamo come array da indicizzare il seguente
a=np.arange(5*4).reshape(5,4)
a
se indicizzo a con un array monodimensionale questo viene interpretato come un array di indici che si riferiscono alla prima dimensione di a, in questo caso le righe
indicizzo due volte la seconda riga:
a[np.array([1,1])]
indicizzo tre volte la seconda riga e tre volte la terza e le metto in un array bidimensionale di due colonne
b = a[np.array([ [1,1,1], [2,2,2] ])]
b
ricapitolando, passo un indice che ha shape(2,3):
c = np.array([ [1,1,1], [2,2,2] ])
c.shape
ciascun indice intero si riferisce alla prima dimensione di un array che ha shape
a.shape
ciascuna riga (prima dimensione) di a ha quindi 4 elementi (colonne) e quindi il risultato dell'indicizzazione avr à come shape
b.shape
riferiamoci sempre ad un array bidimensionale di 5x4 elementi
a
estraggo il terzo ed il quinto elemento della seconda colonna (NB indicizzo con array distinti)
a[np.array([2,4]),np.array([1])]
gli array per le diverse dimensioni devono essere array distinti NON integrati in un altro array che verrebbe interpretato come relativo alla sola prima dimensione (vedi paragrafi precedenti):
a[np.array([np.array([2,4]),np.array([1,1])])]
sopra ho estratto la terza e la quinta riga e due volte la seconda riga ....
Anche questo tipo di indicizzazione pu ò essere usata per l'assegnamento di nuovi valori agli elementi dell'array, ma attenzione agli indici ripetuti:
a = np.arange(10)
listaindici = [2 ,2 ,3 ,3 ,4 , 4]
listavalori = [20 ,200 ,30 ,300 ,40 , 400]
a[listaindici] = listavalori
a
... i valori della lista dei valori sono assegnati all'array indicizzato da sinistra a destra ...
a differenza che gli slice, il Fancy indexing restituisce delle copie:
sia nel caso di indici interi
b=a[listaindici]
b.flags.owndata
sia nel caso di indicizzazione con maschere
b=a[a>100]
b.flags.owndata
a
Le modifiche effettuate sugli elementi di b non hanno effetto sugli elementi di a.
Se voglio manipolare e riassegnare i valori indicizzati devo quindi procedere in pi ù passaggi:
np.set_printoptions(threshold=50)
a=np.random.randint(0,200,(7,7))
print(a)
#indicizzo gli elementi di interesse e creo un array che li contiene
i=a>100
print('i=',i)
b=a[i]
print('-'*20)
print('b=',b)
#elaboro
b = -b**2
print('b=',b)
#riassegno
a[i]=b
print('a=',a)
NOTE: