Nella prima parte di questo tutorial (🔗Tutorial NumPy: Il Concetto di ndarray e dtype), abbiamo scoperto cosa sono gli ndarray e perché il loro tipo di dato (dtype) è così importante per le performance. Ora che abbiamo le basi teoriche, è il momento di passare alla pratica.
In questa guida, metteremo le mani in pasta per capire come creare e manipolare questi potenti oggetti. Imparare a dare forma ai dati è una competenza fondamentale che ti servirà ogni giorno, in qualsiasi campo che richieda analisi numerica. Vedremo come:
- Trasformare semplici liste Python in array veloci e strutturati.
- Generare array da zero usando funzioni predefinite come
np.zeros,np.arangeenp.linspace. - Ispezionare e modificare le loro dimensioni.
Imparare queste operazioni è il primo, vero passo per sfruttare tutta la potenza di NumPy.
Mettiamoci al lavoro!
Creazione di Array da Liste Python (np.array())
Abbiamo già visto brevemente come creare un array NumPy da una lista Python usando np.array(). Questa è la modalità più comune per iniziare a lavorare con NumPy quando i tuoi dati sono già in una struttura Python standard.
Ricorda che tutti gli elementi dell’array risultante avranno lo stesso tipo di dato.
Esempio: Creiamo array di diverse dimensioni.
import numpy as np
# Array 1D (vettore)
dati_1d = [10, 20, 30, 40, 50]
ar_1d = np.array(dati_1d)
print(f"Array 1D: {ar_1d}\n")
# Array 2D (matrice)
dati_2d = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
ar_2d = np.array(dati_2d)
print(f"Array 2D:\n{ar_2d}\n")
# Array 3D (tensore)
dati_3d = [
[[1, 2], [3, 4]],
[[5, 6], [7, 8]],
[[9, 10], [11, 12]]
]
ar_3d = np.array(dati_3d)
print(f"Array 3D:\n{ar_3d}\n")
Osservazione Strategica: Quando crei array da liste annidate, assicurati che tutte le sottoliste abbiano la stessa lunghezza. NumPy si aspetta una struttura rettangolare (o cubica, o N-dimensionale) per poter creare un ndarray omogeneo. Se le lunghezze non corrispondono, potresti ottenere un array di tipo object, che è molto meno efficiente e perde i vantaggi di NumPy.
Contesto Applicativo: Questo è il punto di partenza più comune. Immagina di aver raccolto dati da un file CSV, da un’API o da un database. Spesso, questi dati vengono caricati inizialmente in liste Python. La funzione np.array() è il ponte che ti permette di trasferire questi dati nel mondo performante di NumPy.
Peculiarità dell’Esempio: La creazione dell’array 3D è particolarmente interessante. Pensa a un’immagine a colori: potrebbe essere rappresentata come un array 3D (altezza, larghezza, canali di colore RGB). Oppure a dati di serie storiche per più sensori: (numero di sensori, numero di giorni, misurazioni giornaliere). Questo esempio ti abitua a pensare ai dati in più di due dimensioni.
Creazione di Array con Funzioni Built-in
NumPy offre diverse funzioni predefinite per creare array con valori iniziali specifici, il che è estremamente utile per inizializzare matrici per calcoli, placeholder o test.
np.zeros(shape, dtype=float): Crea un array riempito di zeri.shapeè una tupla che definisce le dimensioni dell’array.np.ones(shape, dtype=float): Crea un array riempito di uno.np.empty(shape, dtype=float): Crea un array con valori casuali (non inizializzati). È leggermente più veloce dizerosoonesperché non deve impostare i valori, ma il contenuto iniziale è imprevedibile.np.arange(start, stop, step, dtype=None): Simile alla funzionerange()di Python, crea un array con valori distribuiti uniformemente all’interno di un intervallo specificato.stopè esclusivo.np.linspace(start, stop, num, endpoint=True, retstep=False, dtype=None): Crea un array con un numero specificato (num) di campioni distribuiti uniformemente in un intervallo.stopè inclusivo per default.
Esempio: Utilizziamo le funzioni built-in.
import numpy as np
# Array di zeri (2 righe, 3 colonne)
zeri = np.zeros((2, 3))
print(f"Array di zeri:\n{zeri}\n")
# Array di uno (3 righe, 2 colonne, tipo intero)
uni = np.ones((3, 2), dtype=int)
print(f"Array di uno (int):\n{uni}\n")
# Array vuoto (1 riga, 4 colonne)
vuoto = np.empty((1, 4))
print(f"Array vuoto (valori casuali):\n{vuoto}\n")
# Array con valori da 0 a 9 (escluso)
sequenza = np.arange(10)
print(f"Sequenza con arange: {sequenza}\n")
# Sequenza con step di 2
sequenza_step = np.arange(0, 10, 2)
print(f"Sequenza con step 2: {sequenza_step}\n")
# 50 punti equispaziati tra 0 e 10
punti_equispaziati = np.linspace(0, 10, 50)
print(f"Punti equispaziati (primi 5): {punti_equispaziati[:5]} ... ultimi 5: {punti_equispaziati[-5:]}\n")
# 5 punti equispaziati tra 0 e 1, inclusi gli estremi
punti_equispaziati_piccoli = np.linspace(0, 1, 5)
print(f"5 punti tra 0 e 1: {punti_equispaziati_piccoli}\n")
Richiamo Teorico (linspace vs arange):
np.arange()è utile quando conosci il passo (step) tra i valori e vuoi generare una sequenza che si avvicina a un certo limite. È simile arange()di Python.np.linspace()è preferibile quando hai bisogno di un numero preciso di punti (num) equispaziati tra due estremi, ed è molto comune in grafici e simulazioni dove la densità dei punti è importante.
Contesto Applicativo:
np.zeros(): È fondamentale per l’inizializzazione. Immagina di dover contare le occorrenze di eventi in diverse categorie; puoi iniziare con un array di zeri e incrementare i valori man mano che elabori i dati. In machine learning, si usa per inizializzare i pesi di una rete neurale.
np.linspace() vs np.arange(): arange è perfetto quando ti serve un contatore con un passo definito (es. “ogni 2 secondi”). linspace è il re della visualizzazione dati: se devi disegnare una funzione matematica, linspace ti garantisce di avere esattamente N punti per creare una curva morbida e precisa, indipendentemente dal passo.
Peculiarità dell’Esempio: La differenza tra arange e linspace è uno dei primi ostacoli per chi impara. L’esempio che mostra linspace(0, 1, 5) è potente nella sua semplicità: mostra come ottenere frazioni perfette ([0., 0.25, 0.5, 0.75, 1.]), un’operazione meno intuitiva da fare con arange.
Dimensioni e Forma degli Array (.shape, .ndim, .size)
Gli array NumPy hanno attributi che descrivono la loro struttura.
Questi attributi sono fondamentali per capire come i dati sono organizzati e per eseguire operazioni corrette.
.shape: Una tupla di interi che indica la dimensione di ogni asse dell’array. Per una matrice 2×3,shapesarà(2, 3)..ndim: Il numero di dimensioni (o assi) dell’array. Un vettore handim=1, una matricendim=2, ecc..size: Il numero totale di elementi nell’array. È uguale al prodotto degli elementi dishape.
Esempio: Analizziamo la forma degli array.
import numpy as np
ar_1d = np.array([1, 2, 3, 4, 5])
ar_2d = np.array([[1, 2, 3], [4, 5, 6]])
ar_3d = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
print(f"Array 1D: {ar_1d}")
print(f" Shape: {ar_1d.shape}") # (5,) -> 5 elementi, 1 dimensione
print(f" Ndim: {ar_1d.ndim}") # 1
print(f" Size: {ar_1d.size}\n") # 5
print(f"Array 2D:\n{ar_2d}")
print(f" Shape: {ar_2d.shape}") # (2, 3) -> 2 righe, 3 colonne
print(f" Ndim: {ar_2d.ndim}") # 2
print(f" Size: {ar_2d.size}\n") # 6 (2*3)
print(f"Array 3D:\n{ar_3d}")
print(f" Shape: {ar_3d.shape}") # (2, 2, 2) -> 2 'piani', 2 righe, 2 colonne
print(f" Ndim: {ar_3d.ndim}") # 3
print(f" Size: {ar_3d.size}\n") # 8 (2*2*2)
Osservazione Strategica: Comprendere shape è fondamentale per il debugging e per assicurarsi che le operazioni tra array siano compatibili. Molti errori in NumPy derivano da array con forme inaspettate.
Contesto Applicativo: Questa non è un’operazione, ma un’attività di diagnostica. Prima di moltiplicare due matrici, devi controllare che le loro shape siano compatibili. Se una funzione di una libreria (es. Scikit-learn, TensorFlow) ti restituisce un errore, la prima cosa da fare è quasi sempre stampare la shape degli array coinvolti.
Peculiarità dell’Esempio: Notare come la shape di un array 1D sia una tupla con un solo elemento, (5,). Questo è un dettaglio cruciale che spesso confonde i principianti, i quali si aspetterebbero (1, 5). Comprendere questa differenza è fondamentale per operazioni come il reshaping o la concatenazione.
Indicizzazione e Slicing
Accedere a elementi specifici o a sottoinsiemi di un array è un’operazione comune e potente in NumPy, molto simile a come si fa con le liste Python, ma con capacità estese per le dimensioni multiple.
- Indicizzazione (Indexing): Si usano le parentesi quadre
[]per accedere agli elementi. L’indicizzazione in Python (e NumPy) è basata su zero, quindi il primo elemento ha indice0. - Slicing: Permette di estrarre sotto-array (o fette) specificando un intervallo di indici. La sintassi è
[start:stop:step], dovestopè esclusivo.
Indicizzazione 1D
import numpy as np
ar_1d = np.array([10, 20, 30, 40, 50, 60, 70])
# Accesso a un singolo elemento
print(f"Primo elemento: {ar_1d[0]}")
print(f"Terzo elemento: {ar_1d[2]}")
print(f"Ultimo elemento (indicizzazione negativa): {ar_1d[-1]}\n")
# Slicing
print(f"Elementi dal 2° al 4° (indici 1, 2, 3): {ar_1d[1:4]}")
print(f"Elementi dall'inizio al 3° (indici 0, 1, 2): {ar_1d[:3]}")
print(f"Elementi dal 5° alla fine: {ar_1d[4:]}")
print(f"Tutti gli elementi: {ar_1d[:]}")
print(f"Elementi con passo 2: {ar_1d[::2]}")
print(f"Array invertito: {ar_1d[::-1]}\n")
Indicizzazione 2D (Matrici)
Per le matrici, si usano due indici: il primo per la riga, il secondo per la colonna. Possono essere separati da una virgola o usati in parentesi quadre separate.
import numpy as np
ar_2d = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
print(f"Array 2D:\n{ar_2d}\n")
# Accesso a un singolo elemento (riga, colonna)
print(f"Elemento in riga 0, colonna 1: {ar_2d[0, 1]} (che è 2)")
print(f"Elemento in riga 2, colonna 2: {ar_2d[2][2]} (che è 9)\n") # Sintassi alternativa
# Slicing di righe e colonne
print(f"Prima riga: {ar_2d[0, :]}") # O semplicemente ar_2d[0]
print(f"Seconda colonna: {ar_2d[:, 1]}\n")
# Slicing per estrarre una sottomatrice
sottomatrice = ar_2d[0:2, 1:3] # Righe 0 e 1, Colonne 1 e 2
print(f"Sottomatrice (righe 0-1, colonne 1-2):\n{sottomatrice}\n")
# Estrazione di elementi non contigui (indicizzazione avanzata - verrà approfondita)
# Per ora, un esempio semplice: estrarre elementi specifici
print(f"Elementi (0,0), (1,1), (2,2) (diagonale): {ar_2d[[0,1,2], [0,1,2]]}\n")
Indicizzazione 3D (Tensori)
Per array con più di due dimensioni, il concetto si estende. Ogni indice si riferisce a una dimensione specifica.
import numpy as np
ar_3d = np.array([
[[1, 2], [3, 4]],
[[5, 6], [7, 8]],
[[9, 10], [11, 12]]])
print(f"Array 3D:\n{ar_3d}\n")
# Accesso a un singolo elemento
print(f"Elemento ar_3d[0, 0, 1]: {ar_3d[0, 0, 1]} (che è 2)\n")
# Slicing per estrarre un 'piano' (la prima 'fetta' lungo la prima dimensione)
print(f"Primo piano:\n{ar_3d[0, :, :]}\n") # O semplicemente ar_3d[0]
# Slicing per estrarre una 'colonna' attraverso i piani
print(f"Seconda colonna di ogni piano:\n{ar_3d[:, :, 1]}\n")
Contesto Applicativo: Questo è il “bisturi” dell’analista di dati.
2D: Estrarre intere colonne (ar_2d[:, 1]) è un’operazione quotidiana: “prendi la colonna dei prezzi di chiusura da un dataset finanziario” o “seleziona solo la feature ‘età’ dei pazienti”. Estrarre una sottomatrice (ar_2d[0:2, 1:3]) equivale a “focalizzati sui primi 2 pazienti e sulle loro colonne da 1 a 2”.
3D: Lo slicing ar_3d[:, :, 1] è potentissimo. Se ar_3d rappresenta i dati di vendita di più negozi (piani, prodotti, giorni), questo slicing potrebbe significare: “dammi le vendite del secondo giorno per tutti i prodotti e tutti i negozi”.
Peculiarità dell’Esempio: L’indicizzazione avanzata per estrarre la diagonale (ar_2d[[0,1,2], [0,1,2]]) anticipa una tecnica incredibilmente flessibile, che permette di “cucire insieme” elementi da posizioni arbitrarie dell’array, una capacità che le liste Python non hanno.
Richiamo Teorico (Viste vs Copie):
Quando si esegue lo slicing su un array NumPy, il risultato è spesso una vista (view) dell’array originale, non una copia (copy). Ciò significa che se modifichi gli elementi della vista, modificherai anche gli elementi corrispondenti nell’array originale. Questo è un aspetto importante per l’efficienza della memoria, ma può portare a comportamenti inaspettati se non ne sei consapevole.
Per ottenere una copia indipendente, devi usare esplicitamente il metodo .copy().
import numpy as np
original_array = np.array([1, 2, 3, 4, 5])
# Creazione di una vista
view_array = original_array[1:4]
print(f"Originale: {original_array}")
print(f"Vista: {view_array}")
# Modifica della vista
view_array[0] = 99
print(f"\nDopo modifica della vista:")
print(f"Originale: {original_array}") # L'array originale è cambiato!
print(f"Vista: {view_array}\n")
# Creazione di una copia
copy_array = original_array[1:4].copy()
print(f"Originale (dopo la modifica precedente): {original_array}")
print(f"Copia: {copy_array}")
# Modifica della copia
copy_array[0] = 100
print(f"\nDopo modifica della copia:")
print(f"Originale: {original_array}") # L'array originale NON è cambiato!
print(f"Copia: {copy_array}\n")
Contesto Applicativo: Questo concetto è legato all’efficienza. Immagina di lavorare con un dataset di miliardi di punti (es. astronomia, genomica). Creare una copia di ogni sottoinsieme che analizzi sarebbe un disastro per la memoria RAM. Le viste permettono a NumPy di essere veloce e leggero.
Peculiarità dell’Esempio: L’esempio è cruciale perché mostra un potenziale “bug” nel codice di un principiante. Se non si è consapevoli di questo comportamento, si potrebbe modificare una “fetta” di dati pensando di non alterare l’originale, introducendo errori difficili da scovare. Sapere quando usare .copy() è segno di una comprensione più profonda di NumPy.
Reshaping degli Array (.reshape())
Il reshaping ti permette di cambiare la forma (le dimensioni) di un array senza modificarne i dati.
È un’operazione molto potente per riorganizzare i dati per diverse analisi o per adattarli a funzioni che richiedono una forma specifica.
Il numero totale di elementi nell’array deve rimanere lo stesso dopo il reshaping.
Puoi usare -1 come una delle dimensioni, e NumPy calcolerà automaticamente la dimensione mancante.
import numpy as np
# Array 1D con 12 elementi
ar_1d = np.arange(12)
print(f"Array 1D originale: {ar_1d}\n")
# Reshape a una matrice 3x4
matrice_3x4 = ar_1d.reshape((3, 4))
print(f"Matrice 3x4:\n{matrice_3x4}\n")
# Reshape a una matrice 4x3
matrice_4x3 = ar_1d.reshape(4, 3) # Le parentesi per la tupla sono opzionali se ci sono solo 2 dimensioni
print(f"Matrice 4x3:\n{matrice_4x3}\n")
# Reshape a un array 2x2x3 (3D)
tensore_2x2x3 = ar_1d.reshape((2, 2, 3))
print(f"Tensore 2x2x3:\n{tensore_2x2x3}\n")
# Usare -1 per inferire una dimensione
matrice_inferita = ar_1d.reshape((3, -1)) # NumPy calcola che la seconda dimensione deve essere 4
print(f"Matrice con dimensione inferita (-1):\n{matrice_inferita}\n")
# Attenzione: il numero di elementi deve essere compatibile
try:
ar_1d.reshape((5, 3)) # 12 elementi non possono formare una 5x3 (15 elementi)
except ValueError as e:
print(f"Errore di Reshape: {e}\n")
Osservazione Strategica: Il reshaping non modifica l’array originale, ma restituisce un nuovo array con la forma desiderata. Se vuoi modificare l’array in-place, devi riassegnare il risultato (es. ar_1d = ar_1d.reshape(...)).
Contesto Applicativo: L’applicazione per eccellenza è nel machine learning. Un modello di computer vision potrebbe richiedere che un’immagine piatta di 784 pixel sia rimodellata in una matrice 28×28. Un modello di linguaggio potrebbe richiedere che una sequenza di parole sia strutturata in “batch” di una certa dimensione.
Peculiarità dell’Esempio: L’uso di -1 (ar_1d.reshape((3, -1))) è una delle feature più comode di NumPy. Ti libera dal dover calcolare a mente le dimensioni, riducendo gli errori e rendendo il codice più leggibile e flessibile: se la dimensione dell’array originale cambia, il reshape con -1 si adatterà automaticamente.
Concatenazione e Divisione degli Array
Spesso è necessario combinare più array o dividere un array grande in sotto-array.
NumPy fornisce funzioni dedicate per queste operazioni.
Concatenazione (np.concatenate(), np.vstack(), np.hstack())
np.concatenate((array1, array2, ...), axis=0): Concatena una sequenza di array lungo un asse esistente. L’asse di default è0(righe).np.vstack((array1, array2, ...)): Stack verticale (per riga). Equivalente anp.concatenate(..., axis=0)per array 2D.np.hstack((array1, array2, ...)): Stack orizzontale (per colonna). Equivalente anp.concatenate(..., axis=1)per array 2D.
import numpy as np
ar1 = np.array([1, 2, 3])
ar2 = np.array([4, 5, 6])
# Concatenazione 1D
concat_1d = np.concatenate((ar1, ar2))
print(f"Concatenazione 1D: {concat_1d}\n")
mat1 = np.array([[1, 2],
[3, 4]])
mat2 = np.array([[5, 6],
[7, 8]])
# Concatenazione verticale (per riga) - axis=0
concat_v = np.concatenate((mat1, mat2), axis=0)
print(f"Concatenazione verticale (axis=0):\n{concat_v}\n")
# Stack verticale (equivalente a concatenate axis=0 per 2D)
vstack_res = np.vstack((mat1, mat2))
print(f"Stack verticale (vstack):\n{vstack_res}\n")
# Concatenazione orizzontale (per colonna) - axis=1
concat_h = np.concatenate((mat1, mat2), axis=1)
print(f"Concatenazione orizzontale (axis=1):\n{concat_h}\n")
# Stack orizzontale (equivalente a concatenate axis=1 per 2D)
hstack_res = np.hstack((mat1, mat2))
print(f"Stack orizzontale (hstack):\n{hstack_res}\n")
# Attenzione: le dimensioni devono essere compatibili lungo l'asse di concatenazione
mat3 = np.array([[10, 11, 12]])
try:
np.concatenate((mat1, mat3), axis=0) # mat1 ha 2 colonne, mat3 ne ha 3
except ValueError as e:
print(f"Errore di concatenazione (dimensioni incompatibili): {e}\n")
Divisione (np.split(), np.hsplit(), np.vsplit())
np.split(array, indices_or_sections, axis=0): Divide un array in più sotto-array.indices_or_sectionspuò essere un intero (numero di sezioni uguali) o una lista di indici dove avviene la divisione.np.hsplit(array, indices_or_sections): Divide un array orizzontalmente (per colonna). Equivalente anp.split(..., axis=1).np.vsplit(array, indices_or_sections): Divide un array verticalmente (per riga). Equivalente anp.split(..., axis=0).
import numpy as np
ar_to_split = np.arange(16).reshape((4, 4))
print(f"Array da dividere:\n{ar_to_split}\n")
# Divisione in 2 sezioni uguali verticalmente (per riga)
split_v = np.vsplit(ar_to_split, 2)
print(f"Divisione verticale in 2:\n{split_v[0]}\n---\n{split_v[1]}\n")
# Divisione in 4 sezioni uguali orizzontalmente (per colonna)
split_h = np.hsplit(ar_to_split, 4)
print(f"Divisione orizzontale in 4 (primo e secondo blocco):\n{split_h[0]}\n---\n{split_h[1]}\n")
# Divisione in sezioni non uguali usando indici
# Dividi dopo la riga con indice 1 e dopo la riga con indice 3
split_by_indices = np.vsplit(ar_to_split, [1, 3])
print(f"Divisione verticale per indici [1, 3]:\n{split_by_indices[0]}\n---\n{split_by_indices[1]}\n---\n{split_by_indices[2]}\n")
# Attenzione: il numero di sezioni deve essere compatibile con la dimensione dell'asse
try:
np.vsplit(ar_to_split, 3) # 4 righe non possono essere divise in 3 sezioni uguali
except ValueError as e:
print(f"Errore di divisione (numero di sezioni incompatibile): {e}\n")
Contesto Applicativo:
Concatenazione: vstack è comunemente usato per unire dati raccolti in momenti diversi (es. log giornalieri) o provenienti da file diversi. hstack è utile per aggiungere nuove “colonne” di feature a un dataset esistente.
Divisione: vsplit è la base per creare set di dati per il machine learning. Prendi un grande dataset, lo mescoli e poi lo dividi in “training set” (es. 80%) e “test set” (es. 20%) usando np.split.
Peculiarità dell’Esempio: L’esempio di vsplit con una lista di indici ([1, 3]) è molto potente. Mostra che non sei limitato a divisioni di dimensioni uguali. Puoi tagliare l’array esattamente dove vuoi, permettendo di creare suddivisioni complesse e non uniformi, una necessità comune in scenari di dati reali.
Questo conclude Il tutorial sugli Array i. Hai imparato a creare array in diversi modi, a capirne la struttura e a manipolarli attraverso indicizzazione, slicing, reshaping, concatenazione e divisione. Queste sono le basi su cui costruiremo operazioni più complesse. Nel prossimo articolo, ci immergeremo nelle operazioni matematiche e statistiche che rendono NumPy così potente per l’analisi dei dati.
🔗Tutorial NumPy: Operazioni Matematiche e Statistiche Avanzate





