Matrici - Numpy 1

Scarica zip esercizi

Naviga file online

Introduzione

Ci sono sostanzialmente due modi in Python di rappresentare matrici: come liste di liste, oppure con la libreria esterna numpy. Vediamo le principali differenze:

Liste di liste - vedere foglio separato

  1. native in Python

  2. non efficienti

  3. le liste sono pervasive in Python, probabilmente incontrerai matrici espresse come liste di liste in ogni caso

  4. forniscono un’idea di come costruire una struttura dati annidata

  5. possono servire per comprendere concetti importanti come puntatori alla memoria e copie

Numpy - questo tutorial

  1. non nativamente disponibile in Python

  2. efficiente

  3. alla base di parecchie librerie di calcolo scientifico (scipy, pandas)

  4. sintassi più comoda per accedere agli elementi (lievemente diversa da quella delle liste di liste)

  5. in alcuni rari casi potrebbe portare problemi di installazione e/o conflitti (l’implementazione non è puro Python)

Qui vedremo i tipi di dati e comandi essenziali della libreria numpy, mostrando molto spesso due versioni dello stesso codice: una prima versione inefficiente con i normali cicli for in Python (lenti), e una seconda senza cicli sfruttando le operazioni su vettori di numpy che spesso permettono di ottenere codice estrememente compatto ed efficiente.

Per ulteriori riferimenti, vedere:

ATTENZIONE: Numpy non funziona in Python Tutor

Che fare

  • scompatta lo zip in una cartella, dovresti ottenere qualcosa del genere:

matrices-numpy
    matrices-numpy1.ipynb
    matrices-numpy1-sol.ipynb
    matrices-numpy2.ipynb
    matrices-numpy2-sol.ipynb
    matrices-numpy3-chal.ipynb
    jupman.py

ATTENZIONE: Per essere visualizzato correttamente, il file del notebook DEVE essere nella cartella szippata.

  • apri il Jupyter Notebook da quella cartella. Due cose dovrebbero aprirsi, prima una console e poi un browser. Il browser dovrebbe mostrare una lista di file: naviga la lista e apri il notebook matrices-numpy1.ipynb

  • Prosegui leggendo il file degli esercizi, ogni tanto al suo interno troverai delle scritte ESERCIZIO, che ti chiederanno di scrivere dei comandi Python nelle celle successive.

Scorciatoie da tastiera:

  • Per eseguire il codice Python dentro una cella di Jupyter, premi Control+Invio

  • Per eseguire il codice Python dentro una cella di Jupyter E selezionare la cella seguente, premi Shift+Invio

  • Per eseguire il codice Python dentro una cella di Jupyter E creare una nuova cella subito dopo, premi Alt+Invio

  • Se per caso il Notebook sembra inchiodato, prova a selezionare Kernel -> Restart

np.array

Innanzitutto importiamo la libreria, e per comodità la rinominiamo in np:

[1]:
import numpy as np

Con le liste di liste abbiamo spesso costruito le matrici una riga alla volta, aggiundo liste all’occorrenza. In numpy invece di solito si crea in un colpo solo tutta la matrice, riempiendola di zeri.

In particolare, questo comando crea un ndarray riempito di zeri:

[2]:
mat = np.zeros( (2,3)  )   # 2 righe, 3 colonne
[3]:
mat
[3]:
array([[0., 0., 0.],
       [0., 0., 0.]])

Nota come all’interno di array( ) il contenuto sembra che venga rappresentato come una lista di liste, MA in realtà nelle memoria fisica i dati sono strutturati in una sequenza lineare che permette a Python di accedere ai numeri in modo molto più rapido.

Possiamo anche crearci un ndarray a partire da una lista di liste:

[4]:
mat = np.array( [ [5.0,8.0,1.0],
                  [4.0,3.0,2.0]])
[5]:
mat
[5]:
array([[5., 8., 1.],
       [4., 3., 2.]])
[6]:
type(mat)
[6]:
numpy.ndarray

Creare una matrice riempita di uno

[7]:
np.ones((3,5))  # 3 righe, 5 colonne
[7]:
array([[1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1.]])

Creare una matrice riempita di un numero k

[8]:
np.full((3,5), 7)
[8]:
array([[7, 7, 7, 7, 7],
       [7, 7, 7, 7, 7],
       [7, 7, 7, 7, 7]])

Dimensioni di una matrice

Per ottenere le dimensioni, scriviamo così:

ATTENZIONE: dopo shape non ci sono le parentesi tonde !

shape è un attributo, non una funzione da chiamare

[9]:
mat = np.array( [ [5.0,8.0,1.0],
                  [4.0,3.0,2.0]])

mat.shape
[9]:
(2, 3)

Se vogliamo memorizzare le dimensioni in variabili separate, possiamo usare questo modo più pythonico (notare la virgola tra num_righe e num_colonne):

[10]:
num_righe, num_colonne = mat.shape
[11]:
num_righe
[11]:
2
[12]:
num_colonne
[12]:
3

Lettura e scrittura

Per accedere ai dati o sovrascriverli si utilizza la notazione con le quadre, con l’importante differenza che in Numpy è consentito scrivere entrambi gli indici dentro le stesse quadre, separati da una virgola:

ATTENZIONE: la notazione mat[i,j] è solo per numpy!

Con le liste di liste non funziona.

[13]:
mat = np.array( [ [5.0,8.0,1.0],
                  [4.0,3.0,2.0]])

# mettiamo il numero 9 nella cella alla riga 0 e colonna 1

mat[0,1] = 9
[14]:
mat
[14]:
array([[5., 9., 1.],
       [4., 3., 2.]])

Accediamo alla cella alla riga 0 e colonna 1

[15]:
mat[0,1]
[15]:
9.0

Mettiamo il numero 7 nella cella alla riga 1 e colonna 2:

[16]:
mat[1,2] = 7
[17]:
mat
[17]:
array([[5., 9., 1.],
       [4., 3., 7.]])

✪ ESERCIZIO: prova a scrivere così: che succede?

mat[0,0] = "c"
[18]:
# scrivi qui


✪ ESERCIZIO: Prova a scrivere così e vedere che succede:

mat[1,1.0]
[19]:
# scrivi qui

Riempire tutta la matrice

Possiamo MODIFICARE la matrice scrivendoci dentro un numero con fill()

[20]:
mat = np.array([[3.0, 5.0, 2.0],
                [6.0, 2.0, 9.0]])

mat.fill(7)  # NOTA: non ritorna nulla !!
[21]:
mat
[21]:
array([[7., 7., 7.],
       [7., 7., 7.]])

Slice

Per estrarre dati da un ndarray possiamo usare le slice, con la notazione già usata per le liste normali. La differenza questa volta è che possiamo estrarre sotto-matrici indicando due range tra le stesse quadre:

[22]:
mat = np.array( [ [5, 8, 1],
                  [4, 3, 2],
                  [6, 7, 9],
                  [9, 3, 4],
                  [8, 2, 7]])
[23]:
mat[0:4, 1:3]  # le righe dalla 0 *inclusa* alla 4 *esclusa*
               # e le colonne dalla 1 *inclusa*  alla 3 *esclusa*
[23]:
array([[8, 1],
       [3, 2],
       [7, 9],
       [3, 4]])
[24]:
mat[0:1,0:3]  # tutta la prima riga
[24]:
array([[5, 8, 1]])
[25]:
mat[0:1,:]  # altro modo di estrarre tutta la prima riga
[25]:
array([[5, 8, 1]])
[26]:
mat[0:5, 0:1]  # tutta la prima colonna
[26]:
array([[5],
       [4],
       [6],
       [9],
       [8]])
[27]:
mat[:, 0:1]  # altro modo di estrarre tutta la prima colonna
[27]:
array([[5],
       [4],
       [6],
       [9],
       [8]])

Il passo: Possiamo anche specificare un passo come terzo parametro dopo il :, per esempio per estrarre solo le righe pari possiamo aggiungere un 2 così:

[28]:
mat[0:5:2, :]
[28]:
array([[5, 8, 1],
       [6, 7, 9],
       [8, 2, 7]])

ATTENZIONE: modificando le slice di numpy si modifica anche la matrice originale!

A differenza delle slice di liste che producono sempre nuove liste, per questioni di efficienza con le slice di numpy otteniamo solo una vista sui dati originari, che significa che scrivendo nella vista andiamo a scrivere anche nella matrice originale:

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

[30]:
sotto_mat = mat[0:4, 1:3]
sotto_mat
[30]:
array([[8, 1],
       [3, 2],
       [7, 9],
       [3, 4]])
[31]:
sotto_mat[0,0] = 999
[32]:
mat
[32]:
array([[  5, 999,   1],
       [  4,   3,   2],
       [  6,   7,   9],
       [  9,   3,   4],
       [  8,   2,   7]])

Scrivere una costante in una slice

Possiamo scrivere una costante in tutte le celle di una regione identificando la regione con una slice, e ponendola uguale alla costante:

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

mat[0:4, 1:3]  = 7

mat
[33]:
array([[5, 7, 7],
       [4, 7, 7],
       [6, 7, 7],
       [9, 7, 7],
       [8, 2, 5]])

Scrivere una matrice in una slice

Possiamo scrivere dentro tutte le celle di una regione identificando la regione con una slice, e ponendola uguale ad una matrice da cui vogliamo leggere le celle.

ATTENZIONE: Per evitare problemi, controlla di usare le stesse dimensioni nella slice e nella matrice a destra!

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

mat[0:4, 1:3]  = np.array([
                            [10,50],
                            [11,51],
                            [12,52],
                            [13,53],
                        ])

mat
[34]:
array([[ 5, 10, 50],
       [ 4, 11, 51],
       [ 6, 12, 52],
       [ 9, 13, 53],
       [ 8,  2,  5]])

Assegnazione e copia

Con Numpy dobbiamo fare attenzione a quando usiamo l’operatore di assegnazione =: come accade con le liste normali, se facciamo una assegnazione nella nuova variabile otterremo solo un puntatore all’array originale:

[35]:
va = np.array([1,2,3])
[36]:
va
[36]:
array([1, 2, 3])
[37]:
vb = va
[38]:
vb[0] = 100
[39]:
vb
[39]:
array([100,   2,   3])
[40]:
va
[40]:
array([100,   2,   3])

Se volessimo una copia completa dell’array, dovremmo usare il metodo .copy():

[41]:
va = np.array([1,2,3])
[42]:
vc = va.copy()
[43]:
vc
[43]:
array([1, 2, 3])
[44]:
vc[0] = 100
[45]:
vc
[45]:
array([100,   2,   3])
[46]:
va
[46]:
array([1, 2, 3])

Calcoli

Numpy è estremamente flessibile, e ci permette di usare gli array quasi con le stesse operazioni dei vettori e matrici che conosciamo dall’algebra:

[47]:
va = np.array([5,9,7])
va
[47]:
array([5, 9, 7])
[48]:
vb = np.array([6,8,0])
vb
[48]:
array([6, 8, 0])

Quando effettuiamo un’operazione algebrica, tipicamente viene creato un NUOVO array:

[49]:
vc = va + vb
vc
[49]:
array([11, 17,  7])

Notiamo che la somma non ha cambiato gli input:

[50]:
va
[50]:
array([5, 9, 7])
[51]:
vb
[51]:
array([6, 8, 0])

Moltiplicazione per uno scalare

[52]:
m = np.array([[5, 9, 7],
              [6, 8, 0]])
[53]:
3 * m
[53]:
array([[15, 27, 21],
       [18, 24,  0]])

Somma di uno scalare

[54]:
3 + m
[54]:
array([[ 8, 12, 10],
       [ 9, 11,  3]])

Moltiplicazione

Attenzione alla moltiplicazione con *, che diversamente dalla moltiplicazione classica tra matrici moltiplica elemento per elemento e richiede quindi matrici di dimensioni identiche:

[55]:
ma = np.array([[1,  2,  3],
               [10, 20, 30]])

mb = np.array([[1,  0,  1],
               [4,  5,  6]])

ma * mb
[55]:
array([[  1,   0,   3],
       [ 40, 100, 180]])

Se vogliamo la moltiplicazione tra matrici che troviamo nell’algebra classica, dobbiamo usare l’operatore @ facendo attenzione ad avere matrici di dimensioni compatibili:

[56]:
mc = np.array([[1,  2,  3],
               [10, 20, 30]])
md = np.array([[1, 4],
               [0, 5],
               [1, 6]])

mc @ md
[56]:
array([[  4,  32],
       [ 40, 320]])

Divisione per uno scalare

[57]:
ma = np.array([[1,  2,  0.0],
               [10, 0.0, 30]])

ma / 4
[57]:
array([[0.25, 0.5 , 0.  ],
       [2.5 , 0.  , 7.5 ]])

Attenzione che se dividiamo per 0.0, l’esecuzione del programma continuerà comunque con un warning e ci ritroveremo nella matrice degli strani nan e inf che tendono poi a creare problemi - vedere al riguardo sezione Nan e inifinità

[58]:
print(ma / 0.0)
print("DOPO")
[[inf inf nan]
 [inf nan inf]]
DOPO
/home/da/.local/lib/python3.7/site-packages/ipykernel_launcher.py:1: RuntimeWarning: divide by zero encountered in true_divide
  """Entry point for launching an IPython kernel.
/home/da/.local/lib/python3.7/site-packages/ipykernel_launcher.py:1: RuntimeWarning: invalid value encountered in true_divide
  """Entry point for launching an IPython kernel.

Aggregazione

Numpy fornisce diverse funzioni per calcolare statistiche, noi ne mostriamo solo alcune:

[59]:
m = np.array([[5, 4, 6],
              [3, 7, 1]])
np.sum(m)
[59]:
26
[60]:
np.max(m)
[60]:
7
[61]:
np.min(m)
[61]:
1

Aggregazione su riga o colonna

Aggiungendo il parametro axis possiamo indicare di effettuare l’aggregazione su ciascuna colonna (axis=0) o riga (axis=1):

[62]:
np.max(m, axis=0)  # il massimo di ogni colonna
[62]:
array([5, 7, 6])
[63]:
np.sum(m, axis=0)   # somma ogni colonna
[63]:
array([ 8, 11,  7])
[64]:
np.max(m, axis=1)  # il massimo di ogni riga
[64]:
array([6, 7])
[65]:
np.sum(m, axis=1)   # somma ogni riga
[65]:
array([15, 11])

Filtrare

Numpy mette a disposizione un mini-linguaggio per filtrare i numeri in un array specificando dei criteri di selezione. Vediamo un esempio:

[66]:
mat = np.array([[5, 2, 6],
                [1, 4, 3]])
mat
[66]:
array([[5, 2, 6],
       [1, 4, 3]])

Supponiamo di voler ottenere un array con tutti i numeri da mat che sono maggiori di 2.

Possiamo indicare la matrice mat su cui volevamo operare, poi tra parentesi quadre indichiamo una specie di condizione booleana, riusando la variabile mat così:

[67]:
mat[ mat > 2 ]
[67]:
array([5, 6, 4, 3])

Ma esattamente, che cos’è quella strana espressione che abbiamo messo dentro le quadre? Proviamo ad eseguirla da sola:

[68]:
mat > 2
[68]:
array([[ True, False,  True],
       [False,  True,  True]])

Notiamo che ci restituisce una matrice di booleani, che sono veri quando la corrispondente cella nella matrice originale soddisfa la condizione che abbiamo imposto.

Mettendo poi questa espressione all’interno di mat[   ] otteniamo i valori della matrice originaria che soddisfano l’espressione:

[69]:
mat[ mat > 2 ]
[69]:
array([5, 6, 4, 3])

Non solo, possiamo costruire espressione più complesse usando & per la congiunzione logica and e | (carattere pipe) per la congiunzione logica or:

[70]:
mat = np.array([[5, 2, 6],
                [1, 4, 3]])
mat[(mat > 3) & (mat < 6)]
[70]:
array([5, 4])
[71]:
mat = np.array([[5, 2, 6],
                [1, 4, 3]])
mat[(mat < 2) | (mat > 4)]
[71]:
array([5, 6, 1])

ATTENZIONE: RICORDATI LE PARENTESI TONDE TRA LE VARIE ESPRESSIONI!

ESERCIZIO: prova a riscrivere le espressioni qua sopra ‘dimenticando’ le parentesi tonde nelle varie componenti (sinistra/destra/entrambe) e guarda cosa succede. Ottieni errori o risultati diversi da quelli attesi?

Mostra soluzione
[72]:

mat = np.array([[5, 2, 6], [1, 4, 3]]) # scrivi qui

ATTENZIONE: and E or NON FUNZIONANO!

ESERCIZIO: prova a riscrivere le espressioni qua sopra sostituendo & con and e | con or e guarda cosa succede. Ottieni errori o risultati diversi da quelli attesi?

Mostra soluzione
[73]:

mat = np.array([[5, 2, 6], [1, 4, 3]]) # scrivi qui

Trovare indici con np.where

Abbiamo visto come trovare il contenuto delle celle per cui una condizione è soddisfatta. E se volessimo trovare gli indici di quelle celle? In quel caso useremmo la funzione np.where, passandogli come parametro la condizione espressa nello stesso linguaggio che abbiamo già usato prima.

Per esempio, se volessimo trovare gli indici delle celle che contengono numeri minori di 40 o maggiori di 60 scriveremmo così:

[74]:
             #0  1  2  3  4  5
v = np.array([30,60,20,70,40,80])

np.where((v < 40) | (v > 60))
[74]:
(array([0, 2, 3, 5]),)

Scrivere in celle che soddisfano una condizione

Possiamo usare np.where per sostituire i valori nelle celle che soddisfano una condizione con altri valori che saranno indicati in due matrici extra ma e mb. Nel caso la condizione sia soddisfatta, verrà preso un corrispondente valore da ma, altrimenti da mb.

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

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


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

np.where(mat < 50, ma, mb)
[75]:
array([[  1,  -2,   3,  -4],
       [  5,   6,  -7,   8],
       [  9, -10, -11, -12]])

Sequenze arange e linspace

La funzione standard range Python non permette incrementi con la virgola, che possiamo invece ottenere costruendo sequenze di numeri float con np.arange, specificando limite sinistro (incluso), destro (escluso) e l’incremento:

[76]:
np.arange(0.0, 1.0, 0.2)
[76]:
array([0. , 0.2, 0.4, 0.6, 0.8])

Alternativamente, possiamo usare np.linspace, che prende un limite sinistro incluso, un limite destro questa volta incluso, e il numero di ripartizioni in cui suddividere questo spazio:

[77]:
np.linspace(0, 0.8, 5)
[77]:
array([0. , 0.2, 0.4, 0.6, 0.8])
[78]:
np.linspace(0, 0.8, 10)
[78]:
array([0.        , 0.08888889, 0.17777778, 0.26666667, 0.35555556,
       0.44444444, 0.53333333, 0.62222222, 0.71111111, 0.8       ])

NaN e infinità

I numeri float possono essere numeri, non numeri , e anche infinità . A volte durante i calcoli accadono condizioni estreme, come per esempio dividere un numero enorme per un numero enorme. In tali casi, potresti finire con un float particolare che è il temuto Not a Number , NaN in breve, o potresti ottenere una infinità . Questo potrebbe portare a comportamenti imprevedibili, perciò devi saper riconoscere situazioni potenzialmente problematiche. Esempi:

[79]:
10e99999999999999999999999
[79]:
inf
[80]:
10e99999999999999999999999 / 10e99999999999999999999999
[80]:
nan

I comportamenti descritti in seguito sono dettati dallo standard IEEE per l’Aritmetica in virgola mobile binaria (IEEE 754) usato da Numpy e che è implementato in tutti i processori ( CPU ), perciò di fatto riguarda tutti i linguaggi di programmazione.

NaN

NaN significa Non è un Numero. Che è già un nome poco chiaro, visto che il NaN in realtà è un membro molto speciale dei floats, con questa stupefacente proprietà:

NaN NON E’ UGUALE A SE’ STESSO !!!

Hai letto bene, NaN davvero non è uguale a sè stesso.

Sappiamo che la tua mente vuole rifiutare questa nozione, ma la confermeremo a breve.

Per ottenere un NaN, puoi usare il modulo Python math che contiene questo oggetto alieno:

[81]:
import math
math.nan    # nota che stampa 'nan' con n minuscolo
[81]:
nan

Come detto, un NaN è considerato un float:

[82]:
type(math.nan)
[82]:
float

Eppure, si comporta molto diversamente dai suoi compagni float, o da ogni altro oggetto nell’universo conosciuto:

[83]:
math.nan == math.nan   # Eh ????
[83]:
False

Rilevare i NaN

Detto quanto sopra, se vuoi controllare quando una variabile x è un NaN, non puoi scrivere così:

[84]:
x = math.nan
if x == math.nan:  # SBAGLIATO
    print("Sono un NaN ")
else:
    print("x è qualcos'altro ??")
x è qualcos'altro ??

Per gestire correttamente questa situazione, devi usare la funzione math.isnan:

[85]:
x = math.nan
if math.isnan(x):  # CORRETTO
    print("x è un NaN ")
else:
    print("x è qualcos'altro ??")
x è un NaN

Nota che math.isnan funziona anche con NaN negativi:

[86]:
y = -math.nan
if math.isnan(y):  # CORRETTO
    print("y è un NaN ")
else:
    print("y è quacos'altro ??")
y è un NaN

Sequenze con i NaN

Per fortuna, non tutto è completamente assurdo. Se compari sequenze che contengono NaN ad altre, ottieni risultati ragionevoli:

[87]:
[math.nan, math.nan] == [math.nan, math.nan]
[87]:
True
[88]:
[math.nan, math.nan] == [math.nan, 5.0]
[88]:
False

Esercizio - NaN due variabili

Date due variabili x e y, scrivi del codice che stampa "stessa cosa" quando sono lo stesso, anche quando sono NaN. Altrimenti, stampa "non sono la stessa cosa"

Mostra soluzione
[89]:
import math

# output atteso: stessa cosa
x = math.nan
y = math.nan

# output atteso: non sono la stessa cosa
#x = 3
#y = math.nan

# output atteso: non sono la stessa cosa
#x = math.nan
#y = 5

# output atteso: non sono la stessa cosa
#x = 2
#y = 7

# output atteso: stessa cosa
#x = 4
#y = 4

# scrivi qui


stessa cosa

Operazioni sui NaN

Qualunque operazione sui NaN genera un altro NaN:

[90]:
5 * math.nan
[90]:
nan
[91]:
math.nan + math.nan
[91]:
nan
[92]:
math.nan / math.nan
[92]:
nan

L’unica cosa che non puoi fare è dividere per zero un NaN ‘fuori scatola’:

math.nan / 0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-94-1da38377fac4> in <module>
----> 1 math.nan / 0

ZeroDivisionError: float division by zero

NaN corrisponde al valore logico booleano True:

[93]:
if math.nan:
    print("That's True")
That's True

I NaN e Numpy

Quando usi Numpy è abbastanza probabile incontrare NaN, al punto che sono ridefiniti dentro Numpy - ma di fatto sono esattamente gli stessi che nel modulo math:

[94]:
np.nan
[94]:
nan
[95]:
math.isnan(np.nan)
[95]:
True
[96]:
np.isnan(math.nan)
[96]:
True

In Numpy quando hai numeri sconosciuti potresti essere tentato di mettere un None. Puoi anche farlo, ma guarda attentamente il risultato:

[97]:
import numpy as np
np.array([4.9,None,3.2,5.1])
[97]:
array([4.9, None, 3.2, 5.1], dtype=object)

L’array risultante non è un array di float64 che permette calcoli veloci, invece è un array che contiene generici object , perchè Numpy assume che l’array contenga dati eterogenei. Perciò quello che guadagni in generalità lo perdi in performance, che dovrebbe essere il motivo principale di usare Numpy.

Per quanto appaiano strani, i NaN sono considerati come dei float e quindi possono essere salvati nell’array:

[98]:
np.array([4.9,np.nan,3.2,5.1])   # NOTA: il `dtype=object` è sparito
[98]:
array([4.9, nan, 3.2, 5.1])

Dove sono i NaN ?

Vediamo dove possiamo incontrare dei NaN e altri oggetti strani come le infinità.

Prima, controlliamo cosa succede quando chiamiamo la funzione log del modulo standard math. Dalle lezioni di matematica, sappiamo che la funzione log si comporta così:

  • \(x < 0\): non definita

  • \(x = 0\): tende a meno infinito

  • \(x > 0\): definita

log function u9u9u9

Perciò possiamo chiederci cosa succede se gli passiamo un valore per il quale non è definita. Proviamo prima con math.log della libreria standard di Python:

>>> math.log(-1)
ValueError                                Traceback (most recent call last)
<ipython-input-38-d6e02ba32da6> in <module>
----> 1 math.log(-1)

ValueError: math domain error

In questo caso viene sollevato ValueError e l’esecuzione viene interrotta.

Vediamo ora l’equivalente in Numpy:

[99]:
np.log(-1)
/home/da/.local/lib/python3.7/site-packages/ipykernel_launcher.py:1: RuntimeWarning: invalid value encountered in log
  """Entry point for launching an IPython kernel.
[99]:
nan

In questo caso abbiamo ottenuto come risultato np.nan, quindi l’esecuzione non si è interrotta, Jupyter ci ha solo informato con una stampa addizionale che abbiamo compiuto qualcosa di pericoloso.

Quando incontra calcoli pericolosi, Numpy di default effettua in ogni caso il calcolo e salva il risultato come NaN o altri oggetti limite. Questo vale anche per i calcoli sugli array:

[100]:
np.log(np.array([3,7,-1,9]))
/home/da/.local/lib/python3.7/site-packages/ipykernel_launcher.py:1: RuntimeWarning: invalid value encountered in log
  """Entry point for launching an IPython kernel.
[100]:
array([1.09861229, 1.94591015,        nan, 2.19722458])

Infinità

Come abbiamo detto in precedenza, Numpy usa lo standard IEEE per l’aritmetica binaria in virgola mobile (IEEE 754). Dato che qualcuno all’IEEE ha deciso di racchiudere i misteri dell’infinito nei numeri float, abbiamo ancora un’altro cittadino da considerare quando facciamo calcoli (per altre informazioni, vedere Numpy documentation on constants):

Infinità positiva np.inf

[101]:
 np.array( [ 5 ] ) / 0
/home/da/.local/lib/python3.7/site-packages/ipykernel_launcher.py:1: RuntimeWarning: divide by zero encountered in true_divide
  """Entry point for launching an IPython kernel.
[101]:
array([inf])
[102]:
np.array( [ 6,9,5,7 ] ) / np.array( [ 2,0,0,4 ] )
/home/da/.local/lib/python3.7/site-packages/ipykernel_launcher.py:1: RuntimeWarning: divide by zero encountered in true_divide
  """Entry point for launching an IPython kernel.
[102]:
array([3.  ,  inf,  inf, 1.75])

Attenzione che:

  • Not a Number non è equivalente all’infinità

  • l’infinità positiva non è equivalente all’infinità negativa

  • l’infinità è equivalente all’infinità positiva

Per fortuna, l’infinità è equivalente all’infinità:

[103]:
np.inf == np.inf
[103]:
True

perciò possiamo in sicurezza equiparare due infinità con ==:

[104]:
x = np.inf

if x == np.inf:
    print("x è infinito")
else:
    print("x è finito")
x è infinito

Alternativamente, possiamo usare la funzione np.isinf:

[105]:
np.isinf(np.inf)
[105]:
True

Infinità negativa

Possiamo anche avere un’infinità negativa, che è differente dall’inifinità positiva:

[106]:
-np.inf == np.inf
[106]:
False

Nota che isinf può rilevare sia infinità positive che negative:

[107]:
np.isinf(-np.inf)
[107]:
True

Per rilevare specificamente un’infinità negativa dei usare isneginf:

[108]:
np.isneginf(-np.inf)
[108]:
True
[109]:
np.isneginf(np.inf)
[109]:
False

Dove possiamo trovarle? Come esempio, proviamo la funzione np.log:

[110]:
np.log(0)
/home/da/.local/lib/python3.7/site-packages/ipykernel_launcher.py:1: RuntimeWarning: divide by zero encountered in log
  """Entry point for launching an IPython kernel.
[110]:
-inf

Combinare infinità e NaN

Quando esegui operazioni che riguardano le infinità e i NaN, l’aritmetica IEEE prova a imitare l’analisi classica, a volte includendo NaN come risultato:

[111]:
np.inf + np.inf
[111]:
inf
[112]:
- np.inf - np.inf
[112]:
-inf
[113]:
np.inf * -np.inf
[113]:
-inf

Un risultato che in analisi classica sarebbe non definito, qui diventa NaN:

[114]:
np.inf - np.inf
[114]:
nan
[115]:
np.inf / np.inf
[115]:
nan

Come al solito, combinare con un NaN risulta in NaN:

[116]:
np.inf + np.nan
[116]:
nan
[117]:
np.inf / np.nan
[117]:
nan

Zero negativo

Puoi persino avere uno zero negativo - chi l’avrebbe pensato?

[118]:
np.NZERO
[118]:
-0.0

Lo zero negativo naturalmente fa coppia con il più conosciuto e apprezzato zero positivo:

[119]:
np.PZERO
[119]:
0.0

NOTA: Scrivere np.NZERO o -0.0 è esattamente la stessa cosa. Lo stesso vale per lo zero positivo.

A questo punto, potresti cominciare a chiederti con qualche se sono davvero considerati uguali. Verifichiamo:

[120]:
0.0 == -0.0
[120]:
True

Grandioso! Finalmente qualcosa che ha senso.

Dato quanto sopra, potresti pensare che in una formula puoi sostituire uno per l’altro e ottenere gli stessi risultati, in armonia con le regole dell’universo.

Facciamo un tentativo di sostituzione, come esempio prima cercheremo di dividere un numero per uno zero positivo (persino se gli insegnanti di matematica ci dicono che tali divisioni siano vietate). Cosa potremmo mai ottenere?

\(\large \frac{5.0}{0.0}= ???\)

In termini di Numpy, potremmo scrivere così per ‘inscatolare’ tutto in arrays:

[121]:
np.array( [ 5.0 ] ) / np.array( [ 0.0 ] )
/home/da/.local/lib/python3.7/site-packages/ipykernel_launcher.py:1: RuntimeWarning: divide by zero encountered in true_divide
  """Entry point for launching an IPython kernel.
[121]:
array([inf])

Mmm, abbiamo ottenuto un array con dentro np.inf (e un warning stampato)

Se 0.0 e -0.0 sono davvero la stessa cosa, dividendo un numero per -0.0 dovremmo ottenere lo stesso identico risultato, no?

Proviamo:

[122]:
np.array( [ 5.0 ] ) / np.array( [ -0.0 ] )
/home/da/.local/lib/python3.7/site-packages/ipykernel_launcher.py:1: RuntimeWarning: divide by zero encountered in true_divide
  """Entry point for launching an IPython kernel.
[122]:
array([-inf])

Ecchecaspita. Questa volta ci ritroviamo con una infinità negativa -np.inf

Se tutto ciò ti pare strano, non dare la colpa a Numpy o Python. Questo è il modo con cui praticamente ogni processore ( CPU ) compie operazioni in virgola mobile, perciò lo troverai in quasi TUTTI i linguaggi di programmazione.

Quello che i linguaggi di programmazione possono fare è aggiungere ulteriori controlli per proteggerti da queste situazioni paradossali, come per esempio lanciare ZeroDivisionError quando scrivi direttamente 1.0/0.0 (bloccando quindi l’esecuzione) o stampare un warning nel caso di operazioni su array Numpy.

Esercizio: rilevare numeri propri

Scrivi del codice che STAMPA numeri uguali se due numeri x e y sono uguali e veri numeri, e STAMPA non uguali altrimenti.

NOTA: numeri non uguali va stampato se uno qualunque dei numeri è infinito o NaN.

Per risolverlo, sentiti libero di chiamare funzioni indicate nella documentazione di Numpy riguardo le costanti

Mostra soluzione
[123]:
import numpy as np

# atteso: numeri uguali
x = 5
y = 5

# atteso: numeri non uguali
#x = np.inf
#y = 3

# atteso: numeri non uguali
#x = 3
#y = np.inf

# atteso: numeri non uguali
#x = np.inf
#y = np.nan

# atteso: numeri non uguali
#x = np.nan
#y = np.inf

# atteso: numeri non uguali
#x = np.nan
#y = 7

# atteso: numeri non uguali
#x = 9
#y = np.nan

# atteso: numeri non uguali
#x = np.nan
#y = np.nan


# scrivi qui


numeri uguali
numeri uguali

Domande - NaN

Per ciascuna delle espressioni seguenti, prova ad indovinare il risultato

ATTENZIONE: ciò che segue può causare nausea e gravi convulsioni

Durante test clinici, sia pazienti con inclinazioni matematiche che soggetti con repulsione per le scienze esatte hanno lamentato malessere per ragioni differenti che sono ancora oggetto di ricerca.

a.  0.0 * -0.0
b.  (-0.0)**3
c.  np.log(-7) == math.log(-7)
d.  np.log(-7) == np.log(-7)
e.  np.isnan( 1 / np.log(1) )
f.  np.sqrt(-1) * np.sqrt(-1)   # sqrt = radice quadrata
g.  3 ** np.inf
h   3 ** -np.inf
i.  1/np.sqrt(-3)
j.  1/np.sqrt(-0.0)
m.  np.sqrt(np.inf) - np.sqrt(-np.inf)
n.  np.sqrt(np.inf) + ( 1 / np.sqrt(-0.0) )
o.  np.isneginf(np.log(np.e) / np.sqrt(-0.0))
p.  np.isinf(np.log(np.e) / np.sqrt(-0.0))
q.  [np.nan, np.inf] == [np.nan, np.inf]
r.  [np.nan, -np.inf] == [np.nan, np.inf]
s.  [np.nan, np.inf] == [-np.nan, np.inf]

Prosegui

Continua ora con gli esercizi.