Pandas
Padroneggia Pandas per l'analisi dati in Python: dalle strutture Series e DataFrame alle operazioni SQL-like, fino alle best practice per la vettorizzazione, il method chaining e l'ottimizzazione della memoria.
Pandas, estensione vettorizzata di NumPy, offre strutture dati per l'analisi dati come Series (array 1D etichettato) e DataFrame (tabelle 2D), supportando operazioni intuitive su dati strutturati. La manipolazione avviene tramite sintassi simile a dizionari per selezioni, aggiunte e cancellazioni di colonne, confermando Pandas come standard _de facto_ per la data science.
Introduzione
Pandas è una libreria open-source per Python che fornisce strutture dati e strumenti di analisi ad alte prestazioni. Nata come estensione di NumPy, ne eredita la potenza di calcolo vettorializzato, aggiungendo un'interfaccia intuitiva e flessibile per la manipolazione di dati strutturati. È lo standard de facto per il data science e l'analisi dati, supportando l'integrazione con librerie come SciPy per l'analisi statistica, Matplotlib per la visualizzazione e Scikit-learn per il machine learning.
Per installare Pandas, è sufficiente utilizzare pip:
pip install pandas
L'importazione standard, insieme a NumPy, è:
import numpy as np
import pandas as pd
Strutture dati fondamentali
Pandas si basa su due strutture dati principali, pensate per gestire dati rispettivamente in una e due dimensioni, ed è proprio dalla loro comprensione che parte ogni analisi efficace.
La Series
Una Series è un array monodimensionale etichettato, in grado di contenere dati di qualsiasi tipo. Ogni elemento è associato a un indice, che può essere personalizzato. Una Series può essere creata a partire da diversi tipi di dati (liste, array NumPy, dizionari). Se l'indice non viene specificato, verrà creato automaticamente un indice posizionale.
serie1 = pd.Series(np.random.randn(4), index=['a', 'b', 'c', 'd'])
dizionario = {"b": 1, "a": 0, "c": 2}
serie2 = pd.Series(dizionario)
Operare con le Series
Le Series sono estremamente flessibili e possono essere utilizzate come array NumPy o come dizionari Python. Questo duplice comportamento le rende uno strumento potente per lo sviluppatore.
- Come array NumPy: supportano operazioni vettoriali e lo slicing. La funzione
to_numpy()restituisce l'array sottostante: a seconda del dtype della Series, questo può essere una vista o una copia dei dati, quindi è buona pratica non fare affidamento implicito sull'uno o sull'altro comportamento quando si scrive codice manutenibile. - Come dizionario: è possibile accedere agli elementi tramite l'indice (es.
serie["a"]) e verificare l'appartenenza con l'operatorein.
Il DataFrame
Un DataFrame è una struttura dati bidimensionale, simile a un foglio di calcolo o a una tabella di un database. È composto da righe e colonne, dove ogni colonna è una Series e può contenere dati di tipo diverso. La sintassi per creare un DataFrame è molto flessibile.
dati = {
"one": pd.Series([1.0, 2.0, 3.0], index=["a", "b", "c"]),
"two": pd.Series([1.0, 2.0, 3.0, 4.0], index=["a", "b", "c", "d"])
}
df = pd.DataFrame(dati)
Operazioni sui DataFrame
La vera potenza di Pandas risiede nella facilità con cui si possono manipolare i dati in un DataFrame. Le operazioni di selezione, aggiunta e cancellazione di colonne seguono la sintassi dei dizionari.
colonna_one = df["one"]
colonna_one = df.one
df["three"] = df["one"] * df["two"]
del df["three"]
df.pop("two")
Accesso ai dati: loc vs iloc
La selezione di righe e celle può essere effettuata tramite due metodi principali, che servono a scopi leggermente diversi. La chiarezza nell'uso di questi metodi è fondamentale per scrivere codice pulito e privo di errori.
loc: basato sulle etichette. L'indice della riga viene specificato.df.loc['a']seleziona la riga con indice 'a'.iloc: basato sulla posizione. L'indice intero della riga viene specificato.df.iloc[0]seleziona la prima riga.
Entrambi i metodi accettano singoli valori, liste, slice (fette) e array booleani, offrendo massima flessibilità.
| Operazione | Sintassi | Risultato |
|---|---|---|
| Selezionare colonna/e | df[colonne] | Series / DataFrame |
| Selezionare riga/e da etichetta | df.loc[indici] | Series / DataFrame |
| Selezionare riga/e da posizione | df.iloc[posizioni] | Series / DataFrame |
| Slice di righe | df[start:end] | DataFrame |
Iterazione su un DataFrame
Sebbene l'iterazione esplicita non sia l'approccio più performante, a volte è necessaria. Pandas fornisce due metodi principali:
iterrows(): itera sulle righe restituendo una Series per ciascuna. Nota, può essere meno efficiente e non sempre garantisce la conservazione del tipo di dato originale.itertuples(): itera sulle righe restituendo una tupla con nome. È generalmente più performante diiterrows().
for index, row in df.iterrows():
print(f"Indice: {index}, Valore colonna 'one': {row['one']}")
for row in df.itertuples():
print(f"Indice: {row.Index}, Valore colonna 'one': {row.one}")
Gestione dei valori mancanti
Uno degli scenari più frequenti nell'analisi di dati reali è la presenza di valori mancanti, spesso rappresentati come NaN. Gestirli correttamente è un requisito di base per garantire l'affidabilità di qualsiasi pipeline di analisi.
Pandas mette a disposizione un set di funzioni dedicate:
isna()/notna(): restituiscono una maschera booleana che indica, rispettivamente, quali valori sono mancanti o validi.dropna(): rimuove le righe o le colonne contenenti valori mancanti.fillna(): sostituisce i valori mancanti con un valore specifico, con il valore precedente/successivo, o con un valore calcolato (es. la media della colonna).
df["voti"].isna()
df.dropna(axis=0)
df["voti"] = df["voti"].fillna(df["voti"].mean())
Scegliere tra dropna() e fillna() non è mai una decisione puramente tecnica, ma dipende dal contesto del dominio, va sempre valutato se un valore mancante rappresenta un dato assente per errore o un'informazione essa stessa significativa.
Un caso d'uso: esplorazione del dataset Titanic
Un classico scenario di data science prevede il caricamento di un dataset, la sua esplorazione e la sua visualizzazione. Con Pandas, l'intero flusso di lavoro può essere gestito in un unico ambiente, senza la necessità di strumenti esterni come DBMS o fogli di calcolo.
L'approccio tradizionale prevederebbe, in sequenza, di caricare i dati su un DBMS, utilizzare un linguaggio come Java o Python per leggerli, usare SQL per trasformarli e infine esportarli in Excel per la visualizzazione. Con Pandas, tutto questo viene semplificato in un unico notebook, sfruttando funzioni già pronte per leggere il dato, esplorarlo, elaborarlo e visualizzarlo.
Operazioni SQL-like in Pandas
Pandas offre funzionalità che replicano le operazioni tipiche dell'SQL, permettendo ai professionisti di sfruttare la loro conoscenza del linguaggio di query.
Join e concatenazione
La libreria mette a disposizione tre metodi principali per combinare dati da DataFrame diversi.
join()
Perfetto quando si vogliono unire due DataFrame basandosi sugli indici delle righe. Parametri chiave come how ('inner', 'left', 'right', 'outer') permettono di specificare il tipo di join.
df1 = pd.DataFrame({'Nome': ['Mario', 'Luigi']}, index=['CF1', 'CF2'])
df2 = pd.DataFrame({'Cognome': ['Rossi', 'Verdi']}, index=['CF1', 'CF3'])
df3 = df1.join(df2, how='outer')
merge()
È una versione più generale di join. È la scelta ideale quando la colonna su cui effettuare il join non è l'indice.
on: nome della colonna comune.left_on/right_on: nomi delle colonne quando questi sono diversi nei due DataFrame.
df1 = pd.DataFrame({'Nome': ['Mario', 'Luigi'], 'Codice': ['CF1', 'CF2']})
df2 = pd.DataFrame({'Cognome': ['Rossi', 'Verdi'], 'Codice': ['CF1', 'CF3']})
df3 = df1.merge(df2, on='Codice', how='inner')
concat()
Permette di concatenare più DataFrame, sia per righe (axis=0) che per colonne (axis=1). È utile per aggiungere nuovi dati o feature al dataset.
df_righe = pd.concat([df1, df2], axis=0)
df_colonne = pd.concat([df1, df2], axis=1)
Grouping (GROUP BY)
L'operazione di raggruppamento, simile al GROUP BY di SQL, è fondamentale per l'aggregazione dei dati. Si utilizza la funzione groupby() a cui viene passata una lista di colonne su cui raggruppare.
raggruppato = df.groupby(['colonna1', 'colonna2'])
Sul risultato, si possono applicare funzioni di aggregazione come sum(), mean(), count(), std(), e molte altre.
Importazione ed esportazione dei dati
Pandas supporta un'ampia gamma di formati di file per importare ed esportare dati, semplificando l'integrazione con diversi sistemi.
| Tipo | Lettura | Salvataggio |
|---|---|---|
| CSV | pd.read_csv() | df.to_csv() |
| JSON | pd.read_json() | df.to_json() |
| Excel | pd.read_excel() | df.to_excel() |
| SQL | pd.read_sql() | df.to_sql() |
Un aspetto spesso trascurato riguarda la sicurezza in fase di lettura dei dati: formati come il pickle (pd.read_pickle()) eseguono deserializzazione di oggetti Python arbitrari e, se applicati a file provenienti da fonti non fidate, possono comportare l'esecuzione di codice non controllato. Come buona pratica di sicurezza del software, è opportuno limitare l'uso di questo formato a file generati e controllati internamente, preferendo formati come CSV, JSON o Parquet quando i dati provengono da fonti esterne.
Esplorazione rapida dei dati
Prima di iniziare qualsiasi analisi, è cruciale comprendere la struttura e le caratteristiche del dataset. Pandas offre funzioni dedicate a questo scopo.
head()/tail(): visualizza le prime/ultime righe del DataFrame.shape: restituisce le dimensioni (righe, colonne).info(): fornisce un riepilogo del DataFrame, inclusi i tipi di dato e il numero di valori non nulli.describe(): genera statistiche descrittive per le colonne numeriche (conteggio, media, deviazione standard, min, max, quartili).
A queste si aggiungono funzioni come sum(), mean(), std(), value_counts() per un'analisi più mirata.
Visualizzazione dei dati con Matplotlib
Pandas si integra perfettamente con Matplotlib, la libreria di plotting più popolare in Python. Il metodo plot() di Series e DataFrame è un wrapper che semplifica la creazione di grafici.
import matplotlib.pyplot as plt
df['colonna'].plot(kind='bar')
plt.show()
La potenza di questo approccio risiede nella possibilità di specificare il tipo di grafico attraverso il parametro kind. Per visualizzazioni più avanzate, come i diagrammi di dispersione a matrice, è disponibile il modulo pandas.plotting.
| kind | Grafico |
|---|---|
'bar' / 'barh' | Bar plot |
'hist' | Istogramma |
'box' | Boxplot |
'scatter' | Scatter plot |
'pie' | Grafico a torta |
Vettorizzazione: il cuore dell'efficienza
Le operazioni vettoriali sono uno dei pilastri su cui si basa la potenza di Pandas. Invece di iterare elemento per elemento, si applicano operazioni a intere serie di dati, sfruttando l'ottimizzazione in C e le capacità di NumPy.
Vettorizzazione su Series
Le operazioni aritmetiche e di confronto tra Series vengono eseguite componente per componente, allineando gli elementi in base all'indice. Se un indice non è presente in una delle due Series, il risultato sarà NaN.
serie = pd.Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd'])
print(serie * 2)
print(serie[1:] + serie[:-1])
Vettorizzazione su DataFrame
Anche per i DataFrame è possibile applicare operazioni vettoriali. Il concetto di broadcast di NumPy permette di eseguire operazioni tra un DataFrame e un valore scalare, un array o un altro DataFrame di forma compatibile.
L'asse di applicazione può essere controllato tramite il parametro axis in molte funzioni, axis=0 (o axis='index') applica l'operazione per ogni colonna ed è il comportamento predefinito, mentre axis=1 (o axis='columns') applica l'operazione per ogni riga.
Per applicare una funzione personalizzata, si utilizza il metodo apply().
def valuta_voto(x):
if x >= 18.0:
return "Promosso"
else:
return "Bocciato"
df["risultati"] = df["voti"].apply(valuta_voto)
Best practice: chiarezza architetturale con il method chaining
Quando una trasformazione dei dati richiede più passaggi in sequenza (filtraggio, creazione di nuove colonne, raggruppamento), scrivere ogni passaggio come una riassegnazione separata rende il codice difficile da leggere e da manutenere. Il metodo pipe() permette di incapsulare ogni trasformazione in una funzione pura e di comporle in una catena leggibile dall'alto verso il basso, con evidenti benefici in termini di modularità e linearità logica del codice.
def rimuovi_mancanti(dataframe):
return dataframe.dropna()
def aggiungi_colonna_risultati(dataframe):
return dataframe.assign(risultati=dataframe["voti"].apply(valuta_voto))
df_finale = (
df
.pipe(rimuovi_mancanti)
.pipe(aggiungi_colonna_risultati)
)
Ogni funzione della catena ha una singola responsabilità e può essere testata in isolamento, un principio di progettazione che si applica altrettanto bene al codice di analisi dati quanto a un qualsiasi altro sistema software.
Copie e viste: un'attenzione alla manutenibilità
La distinzione tra copie e viste in Pandas può creare comportamenti inattesi se non gestita correttamente. Comprendere questo concetto è fondamentale per evitare bug e garantire la manutenibilità del codice. Il principio "explicit is better than implicit" guida la gestione di questo aspetto.
Scenario 1: creare un DataFrame indipendente
Quando si vuole lavorare su una copia del dato originale senza modificarlo, si utilizza il metodo copy().
df2 = df1.copy()
Scenario 2: aggiornare un DataFrame da un altro
Se l'obiettivo è aggiornare i valori di un DataFrame (df1) con quelli di un altro (df2), il metodo update() è la soluzione più chiara e sicura. Anche in questo caso, è consigliabile creare una copia per l'elaborazione intermedia.
df2 = df1.copy()
df1.update(df2)
Questo approccio evita l'uso di catene di indicizzazione che potrebbero generare il famoso SettingWithCopyWarning, rendendo il codice più robusto e prevedibile.
Ottimizzazione delle prestazioni e memoria
Oltre alla vettorizzazione, un'altra leva di ottimizzazione spesso sottovalutata riguarda la scelta dei tipi di dato. Colonne testuali con un numero limitato di valori distinti (es. categorie, stati, etichette) possono essere convertite al tipo category tramite astype('category'), riducendo sensibilmente l'occupazione di memoria e velocizzando operazioni come groupby() su dataset di grandi dimensioni.
df["categoria"] = df["categoria"].astype("category")
Questa è una delle ottimizzazioni con il miglior rapporto tra sforzo di implementazione e beneficio ottenuto, particolarmente rilevante quando si lavora con dataset che non entrano comodamente in memoria.
Conclusioni
Pandas si conferma come uno strumento indispensabile per chiunque lavori con dati in Python. La sua curva di apprendimento è dolce, ma nasconde una potenza enorme che, se sfruttata correttamente, permette di scrivere codice efficiente, leggibile e manutenibile. L'adozione di best practice come l'uso delle operazioni vettoriali, la comprensione delle strutture dati, la corretta gestione di copie e viste, e un'attenzione mirata a prestazioni e sicurezza, sono fondamentali per trasformare un semplice script di analisi in un sistema robusto e scalabile. L'integrazione nativa con SQL, Excel e le librerie di visualizzazione lo rende un coltellino svizzero per il data scientist moderno.