Datele tabulare au o formă. Aceleași numere pot sta într-un DataFrame în două layout-uri foarte diferite, iar pe care îl vrei depinde în întregime de ce urmează să faci. Astăzi acoperim reshaping-ul: cele patru funcții (melt, pivot, stack, unstack) care mută datele între layout-uri, capcana în care cad oamenii pe pivot și de ce coloanele MultiIndex sunt uneori utile și de obicei enervante.
Wide vs long
Un tabel wide are un rând pentru fiecare entitate și multe coloane pentru atribute:
order_id Q1 Q2 Q3 Q4
1 120 150 180 210
2 90 100 110 130
Un tabel long are un rând pentru fiecare pereche entitate-atribut:
order_id quarter sales
1 Q1 120
1 Q2 150
1 Q3 180
1 Q4 210
2 Q1 90
...
Aceeași informație, rearanjată. Fiecare formă are locul ei. Wide e ce citesc oamenii într-un raport: trimestrele sus, totalurile jos, ochii scanează de la stânga la dreapta. Long e ce stochează bazele de date, ce așteaptă pipeline-urile de machine learning, ce presupun groupby și majoritatea bibliotecilor de plotting. SQL e fundamental un limbaj de tabele long. Cu cât te descurci mai repede să convertești între cele două, cu atât pierzi mai puțin timp luptându-te cu pandas.
Regula generală: wide pentru raportare, long pentru procesare. Dacă trimiți pe email unui CFO un rezumat trimestrial, mergi wide. Dacă îl bagi într-un model, calculezi agregări sau îl trimiți într-o bază de date, mergi long.
Wide la long: melt
melt ia un DataFrame wide și stivuiește coloanele alese în două coloane noi: una pentru numele vechilor coloane, una pentru 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",
)
Cele patru argumente care merită știute:
id_vars: coloanele care rămân ca identificatori; sunt repetate o dată pentru fiecare rând topit.value_vars: coloanele de topit. Dacă îl omiți, fiecare coloană care nu e înid_varse topită.var_name: numele noii coloane care ține vechile nume de coloană. Implicitul e"variable", ce nu e niciodată ce vrei; setează-l mereu.value_name: numele noii coloane care ține valorile. Implicitul e"value", rareori potrivit.
După asta, long are 12 rânduri (3 comenzi × 4 trimestre) și layout-ul așteptat de majoritatea uneltelor analitice. Poți face groupby pe trimestru, plotezi tendințele de la trimestru la trimestru, faci join cu un tabel de calendar, toate lucruri stângace în layout-ul wide.
O greșeală des întâlnită: să uiți că id_vars contează. Dacă DataFrame-ul tău wide are coloane de metadata (regiune, nume client, monedă) care ar trebui să călătorească cu fiecare rând topit, listează-le pe toate în id_vars sau le pierzi.
Long la wide: pivot
pivot e inversul lui melt. Având un DataFrame long, alegi o coloană să devină noul row index, una să devină noile column headers și una să umple celulele.
back_to_wide = long.pivot(
index="order_id",
columns="quarter",
values="sales",
)
Rezultat:
quarter Q1 Q2 Q3 Q4
order_id
1 120 150 180 210
2 90 100 110 130
3 200 220 240 260
Curat pe un input curat. Dar e o capcană.
Capcana duplicatelor
pivot cere ca perechile (index, columns) să fie unice. În momentul în care ai două rânduri cu același order_id și quarter, să zicem pentru că tabelul original e per-line-item în loc de per-order, pivot eșuează cu ValueError: Index contains duplicate entries, cannot reshape.
Soluția e pivot_table, care primește un aggfunc și agregă duplicatele în loc să dea eroare:
df.pivot_table(
index="order_id",
columns="quarter",
values="sales",
aggfunc="sum", # sau "mean", "max", un callable, o lista de astea
fill_value=0, # ce sa puna in celulele care nu au date
)
Implicit folosesc pivot_table în cod de producție. E strict mai puternic decât pivot și face agregarea explicită. Singurul motiv pentru care apuci pivot e când chiar vrei să dea eroare dacă apar duplicate: o verificare de sanitate că un join în amonte nu a eșuat.
pivot_table acceptă și mai multe coloane values= și mai multe intrări aggfunc=, așa obții un layout ierarhic de raportare (suma vânzărilor, numărul vânzărilor, valoarea medie, toate într-un singur tabel). Aici intră în scenă coloanele MultiIndex.
Coloanele MultiIndex
Când faci pivot_table cu două coloane de valori sau două aggfuncs, coloanele rezultatului devin MultiIndex: coloane cu două niveluri stivuite unul peste altul:
report = sales.pivot_table(
index="region",
columns="quarter",
values=["revenue", "units"],
aggfunc="sum",
)
# coloanele sunt acum: ('revenue', 'Q1'), ('revenue', 'Q2'), ..., ('units', 'Q1'), ...
E grozav pentru rapoarte exportate în Excel, unde header-ul pe două rânduri se citește natural. E o pacoste pentru aproape orice altceva: SQL nu are coloane ierarhice, bibliotecile de ML nici atât, iar accesarea unei singure coloane înseamnă să scrii report[("revenue", "Q1")] în loc de report["revenue_Q1"].
Ca să aplatizezi o coloană MultiIndex în nume single-level:
report.columns = ["_".join(map(str, c)).strip("_") for c in report.columns]
# acum: revenue_Q1, revenue_Q2, ..., units_Q1, units_Q2, ...
Fac asta aproape reflex după orice pivot_table care produce un MultiIndex. Salvează atâta durere în aval încât costul unei linii în plus e neglijabil.
stack și unstack
stack și unstack mută datele între rânduri și coloane la nivelul indexului: sunt echivalentele MultiIndex pentru melt și pivot.
stack() ia cel mai interior nivel al coloanelor și îl coboară într-un nou nivel cel mai interior al row index-ului:
report.stack() # trimestrele se muta din nivel de coloana intr-un row MultiIndex
unstack() e inversul: ia cel mai interior nivel al row index-ului și îl urcă într-un nou nivel de coloană:
long_indexed.unstack() # cel mai interior row index devine coloane
În practică, unstack e mai util: multe operații pandas (în special groupby cu chei multiple) te lasă cu o serie MultiIndex, iar unstack e one-liner-ul care o transformă într-un DataFrame wide curat:
sales.groupby(["region", "quarter"])["amount"].sum().unstack()
Obții un DataFrame indexat după regiune cu o coloană pe trimestru. Tiparul ăsta, groupby pe două coloane, suma valorii, unstack pe a doua cheie, e atât de des întâlnit încât merită memorat.
Poți specifica ce nivel să faci stack/unstack cu argumentul level=: df.unstack(level="quarter") sau df.unstack(level=0). Implicit e cel mai interior.
reset_index: aplatizează MultiIndex-ul înapoi în coloane
După toată manipularea asta de MultiIndex, pasul final tipic e să pui totul înapoi în coloane simple pentru export. reset_index() face exact asta:
flat = report.reset_index()
# index-ul devine o coloana normala, rezultatul e un DataFrame plat
Dacă indexul are mai multe niveluri, toate devin coloane. Dacă vrei doar pe unele aplatizate, trimite level=. Operația oglindă, set_index(), merge invers și e utilă înainte de joins sau unstack.
Un exemplu real: raport ierarhic de vânzări către un export SQL
Să zicem că ai date brute de vânzări, un rând per line item, și vrei să produci două output-uri: un raport ierarhic pentru CFO (trimestrele de-a lungul, regiunile și categoriile de produse imbricate ca rânduri) și un tabel long plat pentru 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],
})
# Raport CFO: regiunile si categoriile in jos, trimestrele de-a lungul
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, plat, un rand per regiune-categorie-trimestru
warehouse = (
sales
.groupby(["region", "category", "quarter"], as_index=False)["amount"]
.sum()
)
warehouse.to_parquet("sales_long.parquet")
Aceeași sursă, două forme, doi consumatori, fiecare mulțumit. CFO-ul primește layout-ul ierarhic wide pe care ochii ei îl pot scana; warehouse-ul primește layout-ul long pe care SQL îl poate îmbina. Reshaping-ul e podul.
Când să folosești fiecare
Un arbore de decizie aproximativ:
- Treci de la „spreadsheet făcut de oameni” la „date pe care le pot analiza”:
melt. - Treci de la „date analizate” la „tabel pentru un om”:
pivot_table. - Ai deja o serie MultiIndex dintr-un
groupbycu chei multiple:unstack. - Trebuie să aplatizezi un MultiIndex înapoi în coloane simple pentru export:
reset_index+ list comprehension-ul de aplatizare a coloanelor.
Majoritatea problemelor de reshape sunt unul din cele patru cazuri. Când te trezești scriind logică complicată de reshape, oprește-te și verifică: aproape mereu există un one-liner pe care l-ai pierdut.
Ce urmează
Lecția 32 e time series: DatetimeIndex, resample, rolling windows și capcana de timezone care mușcă orice pipeline de producție măcar o dată. Pandas a fost construit inițial pentru serii temporale financiare și se vede: API-ul de time-series e una dintre cele mai șlefuite părți din bibliotecă.
Lectură suplimentară
- pandas: Reshaping and pivot tables: referința completă, cu diagrame care în sfârșit fac stack/unstack să se lege.
- pandas: MultiIndex / advanced indexing: când nu poți evita indecșii ierarhici, aici e de citit.
Ne vedem vineri.