I dati tabellari hanno una forma. Gli stessi numeri possono stare in un DataFrame in due layout molto diversi, e quale dei due vuoi dipende interamente da cosa stai per fare dopo. Oggi vediamo il reshape: le quattro funzioni (melt, pivot, stack, unstack) che spostano i dati tra layout, la trappola che frega chi usa pivot, e perché le colonne MultiIndex sono talvolta utili e di solito fastidiose.
Wide vs long
Una tabella wide ha una riga per entità e molte colonne per gli attributi:
order_id Q1 Q2 Q3 Q4
1 120 150 180 210
2 90 100 110 130
Una tabella long ha una riga per coppia entità-attributo:
order_id quarter sales
1 Q1 120
1 Q2 150
1 Q3 180
1 Q4 210
2 Q1 90
...
Stessa informazione, riorganizzata. Ognuna delle due forme ha il suo posto. Wide è quello che gli umani leggono in un report: trimestri in alto, totali in fondo, gli occhi scorrono da sinistra a destra. Long è quello che i database memorizzano, quello che le pipeline di machine learning si aspettano, quello che groupby e la maggior parte delle librerie di plotting assumono. SQL è fondamentalmente un linguaggio long-table. più diventi veloce a convertire tra le due, meno tempo passi a lottare con pandas.
La regola generale: wide per il reporting, long per il processing. Se stai mandando per email a un CFO un riepilogo trimestrale, usa wide. Se lo stai dando in pasto a un modello, calcolando aggregazioni, o spedendolo a un database, usa long.
Da wide a long: melt
melt prende un DataFrame wide e impila le colonne scelte in due nuove: una per i vecchi nomi di colonna, una per i valori.
import pandas as pd
wide = pd.DataFrame({
"order_id": [1, 2, 3],
"Q1": [120, 90, 200],
"Q2": [150, 100, 220],
"Q3": [180, 110, 240],
"Q4": [210, 130, 260],
})
long = wide.melt(
id_vars=["order_id"],
value_vars=["Q1", "Q2", "Q3", "Q4"],
var_name="quarter",
value_name="sales",
)
I quattro argomenti che vale la pena conoscere:
id_vars: colonne che restano come colonne identificative; vengono ripetute una volta per ogni riga melted.value_vars: colonne da fare melt. Se la ometti, ogni colonna che non è inid_varsviene melted.var_name: il nome della nuova colonna che contiene i vecchi nomi di colonna. Il default è"variable", che non è mai quello che vuoi; impostalo sempre.value_name: il nome della nuova colonna che contiene i valori. Il default è"value", anche questo raramente giusto.
Dopo, long ha 12 righe (3 ordini per 4 trimestri) e il layout che si aspetta la maggior parte degli strumenti analitici. Puoi raggruppare per trimestre, plottare l’andamento trimestre su trimestre, fare join contro una calendar table: tutte cose scomode nel layout wide.
Un errore comune: dimenticare che id_vars conta. Se il tuo DataFrame wide ha colonne di metadati (regione, nome cliente, valuta) che dovrebbero viaggiare con ogni riga melted, elencale tutte in id_vars o le perderai.
Da long a wide: pivot
pivot è l’inverso di melt. Dato un DataFrame long, scegli una colonna che diventa il nuovo indice di riga, una che diventa le nuove intestazioni di colonna, e una che riempie le celle.
back_to_wide = long.pivot(
index="order_id",
columns="quarter",
values="sales",
)
Risultato:
quarter Q1 Q2 Q3 Q4
order_id
1 120 150 180 210
2 90 100 110 130
3 200 220 240 260
Abbastanza pulito su un input pulito. Ma c’è una trappola.
La trappola dei duplicati
pivot richiede che le coppie (index, columns) siano uniche. Nel momento in cui hai due righe con lo stesso order_id e quarter, ad esempio perché la tabella originale è per riga d’ordine invece che per ordine, pivot fallisce con ValueError: Index contains duplicate entries, cannot reshape.
La soluzione è pivot_table, che prende una aggfunc e aggrega i duplicati invece di dare errore:
df.pivot_table(
index="order_id",
columns="quarter",
values="sales",
aggfunc="sum", # oppure "mean", "max", un callable custom, una lista di questi
fill_value=0, # cosa mettere nelle celle senza dati
)
In codice di produzione vado di default su pivot_table. È strettamente più potente di pivot e rende l’aggregazione esplicita. L’unico motivo per usare pivot è quando vuoi davvero che dia errore se compaiono duplicati, come sanity check sul fatto che il join a monte non sia andato male.
pivot_table accetta anche più colonne values= e più voci di aggfunc=, ed è così che ottieni un layout di reporting gerarchico (somma vendite, conteggio vendite, valore medio per vendita tutto in una tabella). È lì che entrano in scena le colonne MultiIndex.
Colonne MultiIndex
Quando fai pivot_table con due colonne value o due aggfunc, le colonne del risultato diventano un MultiIndex: colonne con due livelli impilati uno sopra l’altro:
report = sales.pivot_table(
index="region",
columns="quarter",
values=["revenue", "units"],
aggfunc="sum",
)
# le colonne ora sono: ('revenue', 'Q1'), ('revenue', 'Q2'), ..., ('units', 'Q1'), ...
È ottimo per i report da esportare in Excel, dove l’intestazione su due righe si legge naturalmente. È una rottura per quasi tutto il resto: SQL non ha colonne gerarchiche, le librerie ML neanche, e accedere a una singola colonna significa scrivere report[("revenue", "Q1")] invece di report["revenue_Q1"].
Per appiattire una colonna MultiIndex in nomi a singolo livello:
report.columns = ["_".join(map(str, c)).strip("_") for c in report.columns]
# ora: revenue_Q1, revenue_Q2, ..., units_Q1, units_Q2, ...
Lo faccio quasi per riflesso dopo ogni pivot_table che produce un MultiIndex. Risparmia così tanto dolore a valle che il costo di una riga in più è trascurabile.
stack e unstack
stack e unstack spostano i dati tra righe e colonne a livello di indice: sono gli equivalenti MultiIndex di melt e pivot.
stack() prende il livello più interno delle colonne e lo sposta giù in un nuovo livello più interno dell’indice di riga:
report.stack() # i trimestri si spostano dal livello colonna a un MultiIndex di riga
unstack() è l’inverso: prende il livello più interno dell’indice di riga e lo sposta su in un nuovo livello di colonna:
long_indexed.unstack() # l'indice di riga più interno diventa colonne
In pratica, unstack è più utile: molte operazioni pandas (specie groupby con più chiavi) ti lasciano con una Serie a MultiIndex, e unstack è l’one-liner che la trasforma in un DataFrame wide pulito:
sales.groupby(["region", "quarter"])["amount"].sum().unstack()
Ottieni un DataFrame indicizzato per regione con una colonna per trimestre. Questo schema, groupby su due colonne, somma del valore, unstack della seconda chiave, è così comune che vale la pena memorizzarlo.
Puoi specificare quale livello fare stack/unstack con l’argomento level=: df.unstack(level="quarter") o df.unstack(level=0). Il default è il più interno.
reset_index: appiattire il MultiIndex e riportarlo a colonne
Dopo tutta questa manipolazione di MultiIndex, il passo finale tipico è rimettere tutto in colonne semplici per l’export. reset_index() fa esattamente questo:
flat = report.reset_index()
# l'indice diventa una colonna normale, il risultato è un DataFrame piatto
Se l’indice ha più livelli, diventano tutti colonne. Se vuoi appiattirne solo alcuni, passa level=. L’operazione speculare, set_index(), va nell’altra direzione ed è utile prima di join o unstack.
Un esempio reale: report vendite gerarchico verso un export SQL
Supponi di avere dati di vendita grezzi, una riga per riga d’ordine, e di voler produrre due output: un report gerarchico per il CFO (trimestri in orizzontale, regioni e categorie prodotto annidate come righe), e una tabella long piatta per il data warehouse.
import pandas as pd
sales = pd.DataFrame({
"region": ["EU", "EU", "US", "US", "EU", "US"],
"category": ["A", "B", "A", "B", "A", "A"],
"quarter": ["Q1", "Q1", "Q1", "Q1", "Q2", "Q2"],
"amount": [100, 50, 200, 150, 120, 220],
})
# Report CFO: regioni e categorie in verticale, trimestri in orizzontale
report = sales.pivot_table(
index=["region", "category"],
columns="quarter",
values="amount",
aggfunc="sum",
fill_value=0,
)
report.to_excel("cfo_report.xlsx")
# Export warehouse: long, piatto, una riga per regione-categoria-trimestre
warehouse = (
sales
.groupby(["region", "category", "quarter"], as_index=False)["amount"]
.sum()
)
warehouse.to_parquet("sales_long.parquet")
Stessa fonte, due forme, due consumatori, ognuno contento. Il CFO ottiene il layout gerarchico wide che i suoi occhi possono scorrere; il warehouse ottiene il layout long con cui SQL può fare join. Il reshape è il ponte.
Quando usare cosa
Un albero decisionale approssimativo:
- Si passa da “foglio di calcolo fatto da umani” a “dati che posso analizzare”:
melt. - Si passa da “dati che ho analizzato” a “tabella per un umano”:
pivot_table. - Hai già una Serie con MultiIndex da un
groupbya più chiavi:unstack. - Devi appiattire un MultiIndex in colonne semplici per l’export:
reset_index+ la list comprehension che appiattisce le colonne.
La maggior parte dei problemi di reshape è uno di questi quattro casi. Quando ti ritrovi a scrivere logica di reshape complicata, fermati e controlla: quasi sempre c’è un one-liner che ti sei perso.
Cosa viene dopo
La lezione 32 è time series: DatetimeIndex, resample, rolling window, e la trappola del fuso orario che azzanna almeno una volta ogni pipeline di produzione. Pandas è nato originariamente per le time series finanziarie, e si vede: l’API time-series è una delle parti più rifinite della libreria.
Letture di approfondimento
- pandas: Reshaping and pivot tables: il riferimento completo, con i diagrammi che fanno finalmente scattare il click su stack/unstack.
- pandas: MultiIndex / advanced indexing: quando non puoi evitare gli indici gerarchici, è qui che si legge.
Ci vediamo venerdì.