Numpy

Le strutture di Python sono eterogenee, questo e' molto comodo per tante applicazioni, ma poco efficiente per applicazioni numeriche intensive, dato che, per operare su un dizionario od una lista, Python deve, per ogni elemento, cercare di che tipo e' e la sua posizione in memoria.

Numpy e' un pacchetto ausiliario che rimedia a questo, introducendo strutture multidimensionali, con valori tutti dello stesso tipo e quindi stessa lunghezza. In questo modo per scorrere una struttura basta gestire un offset, cosa che si fa velocemente.

Il sito di riferimento di NumPy e': numpy.org qui si trova una estesa documentazione e molti esempi.

La struttura principale di NumPy e' l'oggetto ndarray, il nome significa n-dimensional array. Un ndarray e' una lista o una lista di liste, con elementi tutti dello stesso tipo ed alcuni attributi particolari che ne specificano le caratteristiche. Internamente e' ordinato per righe, cioe' una linea dietro l'altra, analogamente a come sono ordinate le matrici nel linguaggio C, ma si possono creare anche ndarray ordinati per colonne (come nel linguaggio FORTRAN). In genere si utilizzano ndarray ad una o due dimensioni, ma e' possibile avere anche array a molte dimensioni.

Nella tabella seguente gli attributi principali degli ndarray.

Attributo Significato
ndarray.ndim Il numero di dimensioni dell'array 1: per un vettore; 2 per una matrice
ndarray.shape Il numero di righe e colonne espresse con una tupla (righe,colonne)
ndarray.size Il numero totale di elementi
ndarray.dtype Il tipo degli elementi: str,int,float,bool,complex,bytes datetime64, timedelta64 etc. etc.
ndarray.itemsize le dimensioni, in byte, di un elemento

Numpy definisce tipi di dato suoi; ha tipi che ereditano da tutti i tipi di Python ed in piu' ne definisce altri, (una sessantina). Numpy usa delle codifiche brevi per indicare i tipi, di seguito alcuni tipi di dato di numpy.

Per interi, di diverse dimensioni:

  b  byte  (numpy.byte)
  h  short (numpy.short , numpy.int6) interi con segno, a pochi bits
  i  int   (numpy.int32),             intero con segno a 32 bits
  l  int_  (numpy.int_ , numpy.int64) non e' int di python
                                      che ha lunghezza arbitraria
  q  long long del C      (numpy.longlong)
  B  unsigned char del C  (numpy.ubyte)
  H  unsigned short del C (numpy.ushort)
  I  unsigned int del C   (numpy.uint32)
  L  unsigned long        (numpy.uint)
  Q  long long            (numpy.ulonglong)

Per numeri "float" di diverse dimensioni:

  e numpy.float16
  f numpy.single
  d numpy.double, numpy.float_, numpy.float64
  g numpy.longdouble, numpy.longfloat,numpy.float128



Per numeri complessi:

  F numpy.singlecomplex,numpy.complex64
  D numpy.cfloat,numpy.complex_,numpy.complex128
  G numpy.clongdouble,numpy.clongfloat,numpy.longcomplex,numpy.complex256

Per date:

  M numpy.datetime64             per date
  m numpy.timedelta64            per intervalli di tempo

Stringhe e booleani:

  ? numpy.bool8,numpy.bool_      booleani
  S numpy.bytes_
  U numpy.str_ , numpy.unicode_

  V numpy.void : una sequenza di bytes, non altrimenti definita.

Creazione array

La funzione array viene usata per creare ndarray a partire da liste, ha anche argomenti per definire le caratteristiche del ndarray.

Vediamo alcuni esempi, si assume qui che abbiamo importato il modulo con: "import numpy as np"

b=np.array([11,12,13,21,22,23])              # da lista

bb=np.array([11,12,13,21,22,23],np.float64)  # specificando il tipo

x=np.array( [ (1.5,2,3), (4,5,6) ] )         # bidimensionale, da tuple

In caso di ndarray costruiti a partire da stringhe di dimensioni diverse, si assume la lunghezza delle maggiore, per avere oggetti tutti delle stesse dimensioni. Nell'esempio seguente le stringhe sono rappresentate in codifice unicode U5 (lunghezza dell'ultima):

dda=np.array(['aa','ba','caaaa']) # codifica unicode U5

d=np.array(['a','b','c'])      # queste sono invece in codifica U1
dd=np.array(['aa','ba','ca'])  # queste unicode  U2

Si possono anche creare array con valori pre-impostati:

z=np.zeros((3,4))                 # array 3x4 ,con 0
u=np.ones((2,3),dtype=np.int16 )  # array 2x3, interi a 16 bits, con 1
v=np.empty((2,3),dtype=np.int16 ) # array 2x3, di interi, vuoto

ar=np.arange(10,30,5) # numeri da 10 a 30, passo 5
q=np.linspace(0,2,9)  # numeri da 0 a 2, 9 valori equispaziati

L'oggetto ndarray ha anche una funzione "creatore" del tipo:

nd=np.ndarray(lista,dtype=None)

Si puo' specificare "*order='F'*" per avere un array ordinato per colonne.
La lista da cui creare l'array e' opzionale.

Elementi e slices

Per riferirsi a singoli elementi o parti di una array si puo' usare la stessa sintassi che si usa per le liste di Python:

a=np.array([[1,2,3],[10,20,30],[100,200,300]])

a[2][0] # e' la terza riga, prima colonna
a[2]    # la terza riga, come array monodimensionale

Per gli slices abbiamo la solita sintassi con i due punti, con il primo valore compreso ed il secondo escluso:

a[0:1][2:3] # prima linea (indice 0) , terza colonna.
            # e' un array bidimensionale di shape: (1,1)

Come per le liste indici negativi partono dalla fine di linee o colonne. Si puo' definire un passo per la selezione, se in passo e' negativo si va all'indietro, partendo dalla fine

a[-1][-1]               # vale 300. e' l'ultimo elemento

s=np.array([1,2,3,4,5])
s[0:s.size:2]           # con passo 2, sono i dispari

a=np.array([1,2,3])     # qui si ribalta il vettore
a[::-1]                 # si ottiene: array([3, 2, 1])

Per array a molte dimensione una notazione piu' efficiente di quella che si usa per le liste e' quella che usa gli indici separati da una virgola:

a[2,0]      # invece di   a[2,0]
a[0:1,2:3]  # invece di: a[0:1][2:3]

per indicare tutte le linee, o colonne, oltre a ":" , che si usa per le liste, si puo' usare "..."

a[0:1,...]  # tutte le colonne della prima linea

Funzioni logiche per selezione

Si possono selezionare elementi con espressioni logiche. Ad esempio un array di valori booleani si puo' usare per selezionare elementi:

z=np.array([1,2,3,4,5])
j=np.array([True,False,True,False,True])

z[j]  # produce:   array([1, 3, 5])

Una relazione logica applicata ad un array produce un array booleano, delle stesse dimensioni; per cui possiamo usare espressioni logiche come indici:

a=np.array([1,2,10,20,40,50,100,200])
b=a[a>20] # un array monodimensionale dei valori oltre 20

c = a[(a > 20) & (a < 100)] # array dei valori fra 20 e 100

Le funzioni less, equal, not_equal, greater greater_equal less_equal, confrontano uno ad uno gli elementi di due array, producendo array di valori logici.:

a=np.array([1,100])
aa=np.array([2,10])

np.less(a,aa)   # produce: array([ True, False])

La funzione nonzero produce un array con gli indici degli elementi che non sono nulli (o che sono False). Per matrici produce tuple di indici (una per riga)

g=np.array([0,1,-1,0])
np.nonzero(g)           # e' un array con [1,2]

Per selezionare elementi si possono usare array di indici:

j=np.array([1,1,3,3])
aaa=np.array([10,20,30,40,50,60])

aaa[j]   # e'  array([20, 20, 40, 40])

Si puo' scegliere fra gli elementi di due array in base a condizioni su un terzo array

a = np.array([3,5,7,9])
b = np.array([10,20,30,40])
c = np.array([2,4,6,8])

np.where(a <= 6, b, c)     # condizione elemento per elemento
array([10, 20,  6,  8])    # se vale prende b, altrimenti c

Reshape degli array

Si possono cambiare le dimensioni di un array.

Qui da un array monodimensionale creo una matrice 2x3 (2 linee, 3 colonne):

b=np.array([11,12,13,21,22,23])  # creo ndarray monodimensionale
k=b.reshape(2,3)                 # creo matrice 2x3 (2 linee, 3 colonne)

k.shape # mi da la tupla:  (2, 3)

reshape ha diversi argomenti:

np.reshape(array, newshape=(1, 6), order='C')

order dice come sono messi gli elementi in memoria, C e' come nel linguaggio C, F come nel linguaggio FORTRAN

a2 = b[np.newaxis, :] # converte un monodimensionale in una matrice di una riga
a3 = b[:,np.newaxis]  # converte un monodimensionale in una matrice di una colonna

np.expand_dims(array,axis=num) # aggiunge una dimansione
                               # axis vale 0 od 1, dice se e' linea o colonna.

Trasformare array

Ci sono molte funzioni per trasformare gli array; vstack ed hstack prendono in argomento una tupla di array, e li combinano in verticale od orizzontale.

Nell'esempio seguente otteniamo, rispettivamente: una matrice con gli array come righe, ed un array monodimensionale con i due array di seguito

p=np.array([1,2,3])
w=np.array([10,20,30])

np.vstack((p, w))     # array con 2 righe
np.hstack((p, w))     # array con una lunga riga

Anche concatenate unisce array.

p=np.array([1,2,3,4])
w=np.array([10,20,30,40])

aa=np.concatenate((p, w))

#ottengo: array([1,2,3,4,10,20,30,40])

Le funzioni vsplit, hsplit dividono un array in orizzontale o verticale. Nell'esempio che segue separiamo una matrice in una lista di array (sempre a 2 dimensioni) con le righe o le colonne

s=np.array([[1,2],[10,20]])
np.vsplit(s,2)    # lista di 2 array bidimensionali, a una riga
                  #  [array([[1, 2]]), array([[10, 20]])]

np.hsplit(s,2)    # lista di 2 array bidimensionali, a una colonna

Per copiare un array:

s2=s.copy()
s2=s        # NON fa una copia, ma da due nomi all'array

Altre funzioni utili sono: sort, per ordinare un array, flip che scambia righe o colonne, (la prima diventa l'ultima e viceversa).

s.T  # e' la trasposta

a.flatten() # un vettore con gli elementi in fila

La funzione unique fa un vettore senza doppioni

ff=np.array([1,1,2,2])
v=np.unique(ff)

Algebra con Array

Si possono fare operazioni algebriche con gli array, senza dover iterare sugli elementi, che, in un linguaggio interpretato, e' un'operazione relativamente lenta. Vediamo alcuni esempi:

n=np.array([[0,0,1],[0,1,0],[1,0,0]])
a=np.array([[1,2,3],[10,20,30],[100,200,300]])
b=np.array([1.1,2.1,3.1])
c=np.array([10.1,20.1,30.1])

b+c   # somma gli elementi: array([11.2, 22.2, 33.2])
b*10  # moltiplica ogni elementi per 10
b*c   # moltiplica elementi fra loro:  array([11.11, 42.21, 93.31])

a[0]-1 # la prima riga, meno uno:  array([0, 1, 2])
a-b    # toglie il vettore b ad ogni linea di a
n*b    # moltiplica ogni riga di a per b, non e' il prodotto matriciale

Il prodotto di un vettore per una matrice si fa con la funzione dot; vdot e' il prodotto vettoriale:

np.dot(n,b)
np.dot(n,n)  # prodotto fra matrici: righe per colonne

Ci sono un sacco di funzioni per gli arrays, alcune possono avere come parametro axis=0 oppure axis=1, per dare un valore per ogni riga o per ogni colonna di una matrice;

a=np.array([[1,2,3],[10,20,30],[100,200,300]])

a.sum()           # somma di tutti gli elementi
a.min()           # l'elemento minimo
a.max()           # l'elemento massimo

a.min(axis=0)     # array di elementi minimi di ogni colonna:
a.min(axis=1)     # array di elementi minimi di ogni riga:
np.sum(a,axis=0)  # array delle somme di ogni colonna

a.cumsum()       # cumulativa
a.cumsum(axis=1) # cumulativa per righe
a.cumprod()      # prodotto cumulativo (produce array 1-dim)

a.mean()    #  media
a.var()     #  varianza
a.std()     #  deviazione standard
a.trace()   #  traccia (somma sulla diagonale)

broadcasting

Questo termine esprime il fatto che numpy puo' fare operazioni fra oggetti di dimensioni diverse; internamente ripete quello con dimensione minore fino alle dimensioni del maggiore. Numpy, quando puo', fa conversioni automatiche fra diversi tipi di dato.

a=np.array([[1,2,3],[10,20,30],[100,200,300]])

a+0.5           # somma 0.5 ad ogni elemento di a
a[0]=7          # 7 e' assegnato ad ogni elemento della prima riga
a+[100,200,300] # la lista, intesa come riga, e' sommato ad ogni riga di a
                  ( ma deve avere 3 colonne o da errore)

Operare su array con funzioni

Si puo' applicare un funzione ad un array, ottenendo un array di risultati, che operano sui i valori corrispondenti degli array in agomento.

Per far questo occorre vettorializzare la funzione.

Esempio:

def myfunc(a,b):
   return a*b

vfun=np.vectorize(myfunc) # ora ha in argomenti array e produce array

a=np.array([1,2,3])
b=np.array([10,20,30])

vfun(a,b)   # produce:  array([10, 40, 90])

Iteratori

Quando si puo', conviene evitare di iterare sugli elementi di un array, ma si puo' fare. Per iterare sugli elementi di un vettore si usa il for di Python, come per le liste. Per una matrice restituisce ad ogni giro una riga, come array monodimensionale:

a=np.array([[1,2],[10,20]])
for i in a:
  print(i)

Per iterare sui singoli elementi della matrice si usa l'attributo flat (che e' un iteratore non una funzione):

for i in a.flat:
  print(i)