Dacă selecția înseamnă „alege rânduri”, agregarea înseamnă „sumarizează rânduri”. Odată ce ai filtrat până la datele care te interesează, aproape orice analiză se termină cu o întrebare de forma „media per țară”, „maximul per client”, „primul eveniment per sesiune”. Pandas are o operație de bază pentru asta, groupby, și patru metode pe care le poți apela pe rezultat: agg, transform, apply, filter. Arată asemănător și fac lucruri foarte diferite. Astăzi obținem distincția corect, fiindcă alegerea celei greșite e diferența dintre o interogare care rulează în 30 de milisecunde și una care rulează în 30 de secunde.
Vom lucra cu asta pe parcurs:
import pandas as pd
pd.set_option("mode.copy_on_write", True)
df = pd.DataFrame(
{
"country": ["IT", "DE", "FR", "IT", "DE", "FR", "IT", "DE"],
"year": [2024, 2024, 2024, 2025, 2025, 2025, 2025, 2024],
"channel": ["web", "web", "web", "app", "app", "web", "web", "app"],
"revenue": [120.0, 340.0, 210.0, 145.0, 380.0, 225.0, 95.0, 290.0],
"orders": [12, 25, 18, 15, 28, 19, 9, 22],
}
)
Forma de bază
df.groupby("col") întoarce un obiect DataFrameGroupBy: nu un rezultat, un intermediar. Pandas nu iterează efectiv datele până nu apelezi o metodă de agregare pe el:
df.groupby("country").sum(numeric_only=True)
df.groupby("country").mean(numeric_only=True)
df.groupby("country").size() # numarul de randuri per grup
numeric_only=True e o practică bună: fără el, pandas va încerca să sumeze string-uri (ceea ce „funcționează” prin concatenare) sau va da eroare pe tipuri mixte. În pandas 2.x implicit e să dea eroare pe coloane non-numerice pentru sum/mean, ceea ce e implicitul corect dar înseamnă că setezi flag-ul explicit când îl vrei.
Output-ul e un DataFrame indexat după cheia de grup. Cheia de grup s-a mutat de la coloană la index: utilă pentru graficare și pentru join-uri ulterioare, dar o sursă comună de „stai, unde s-a dus coloana mea country?”. Dacă o vrei ca pe o coloană, fie apelează .reset_index(), fie pasează as_index=False:
df.groupby("country", as_index=False).sum(numeric_only=True)
Groupby pe mai multe chei: grupează după un tuplu de coloane:
df.groupby(["country", "year"]).sum(numeric_only=True)
Rezultatul are un MultiIndex pe rânduri. Poți muta ambele chei înapoi la coloane cu .reset_index() sau folosi as_index=False.
.agg: agregări diferite per coloană
sum, mean, min, max, count, nunique, median, std, var, first, last: toate disponibile ca metode pe groupby. Când vrei agregări diferite pentru coloane diferite, asta e .agg:
df.groupby("country").agg(
{
"revenue": "sum",
"orders": "mean",
}
)
Poți pasa o listă pe coloană ca să obții mai multe agregări:
df.groupby("country").agg(
{
"revenue": ["sum", "mean", "max"],
"orders": ["sum", "min"],
}
)
Rezultatul are un MultiIndex pe coloane: ("revenue", "sum"), ("revenue", "mean"), etc. E incomod de manevrat în aval: df["revenue"]["sum"] în loc de df["revenue_sum"]. Ceea ce ne aduce la modul modern.
Agregările denumite: așa se face
Pandas 0.25 a adăugat agregările denumite și odată ce le vezi nu te mai întorci. Forma:
df.groupby("country").agg(
total_revenue=("revenue", "sum"),
avg_revenue=("revenue", "mean"),
total_orders=("orders", "sum"),
n_rows=("revenue", "size"),
)
Fiecare argument cu nume e un tuplu (coloana_sursa, functie_agregare), iar numele argumentului devine numele coloanei din rezultat. Fără MultiIndex. Coloane plate. Auto-documentate. Asta e forma pe care ar trebui s-o scrii în 2026.
Agregări custom? Pasează un callable:
df.groupby("country").agg(
revenue_range=("revenue", lambda s: s.max() - s.min()),
p95_revenue=("revenue", lambda s: s.quantile(0.95)),
)
lambda s: ... primește un Series: valorile acelei coloane pentru un grup. Valoarea de retur poate fi un scalar (devine o celulă) sau un tuplu/listă (rar, avansat).
Pentru „numără rândurile per grup”, preferă ("any_column", "size") în loc de ("any_column", "count"): vezi secțiunea următoare pentru de ce.
size vs count, pe scurt
Două funcții care sună la fel și nu sunt:
.size()întoarce numărul de rânduri din fiecare grup, inclusiv rândurile în care valoarea eNaN. E o proprietate a grupului, nu a vreunei coloane..count()întoarce numărul de valori non-null din fiecare coloană a fiecărui grup. Variază pe coloană.
df.groupby("country").size() # randuri per tara
df.groupby("country").count() # non-null per coloana per tara
df.groupby("country").agg(n=("revenue", "size")) # versiunea cu nume
Dacă vrei „câte rânduri sunt în acest grup”, folosește size. Dacă vrei „câte dintre rânduri au un revenue non-null”, folosește count. Confundarea uneia cu cealaltă produce tăcut dashboard-uri greșite.
.transform: difuzează înapoi la forma originală
agg reduce fiecare grup la un rând. transform face o agregare dar difuzează rezultatul înapoi la fiecare rând din grup, deci output-ul are aceeași lungime ca input-ul. Asta e funcționalitatea-cheie de care nu știai că ai nevoie.
Utilizarea clasică: calculează o deviație per rând de la media grupului.
df["country_avg_revenue"] = df.groupby("country")["revenue"].transform("mean")
df["revenue_vs_avg"] = df["revenue"] - df["country_avg_revenue"]
Sau, normalizează în interiorul grupului:
df["revenue_share_of_country"] = (
df["revenue"] / df.groupby("country")["revenue"].transform("sum")
)
E o singură expresie, vectorizată, rapidă. Alternativa, să construiești un DataFrame agregat separat și apoi să-l aduci înapoi cu merge pe cheia de grup, înseamnă trei rânduri, două obiecte în plus și un ordin de mărime mai lent. Ori de câte ori te trezești că vrei o valoare per rând care depinde de grupul lui, întinde-te după transform.
transform acceptă aceleași agregări string ca agg ("mean", "sum", "max", "min", "count", "std", "rank"), plus orice callable care primește un Series și întoarce un Series de aceeași lungime:
df["revenue_z"] = df.groupby("country")["revenue"].transform(
lambda s: (s - s.mean()) / s.std()
)
.apply: portița de scăpare (folosește cu măsură)
apply e cea mai flexibilă metodă groupby și cea mai lentă. Apelează o funcție o dată per grup, cu DataFrame-ul complet al grupului, și coase rezultatele la un loc:
def top_n(group: pd.DataFrame, n: int = 2) -> pd.DataFrame:
return group.nlargest(n, "revenue")
df.groupby("country").apply(top_n, n=2, include_groups=False)
Pandas 2.2+ cere include_groups=False pe apply când cheile groupby sunt și ele pasate înăuntru: altfel îți va arunca un avertisment de deprecare. Setează-l.
Când e apply răspunsul corect? Când operația are nevoie sincer de DataFrame-ul întreg al grupului și nu poate fi exprimată ca o reducere pe coloană sau o difuzare per rând. „Top N rânduri per grup”, „potrivește o regresie per grup”, „logică complexă pe mai multe coloane care nu se mapează la agg”. Pentru orice altceva, agregare coloană cu coloană, statistici de grup per rând, folosește agg sau transform. Sunt vectorizate; apply e o buclă Python deghizată.
O regulă aproximativă: dacă o poți exprima fără apply, ar trebui s-o faci.
.filter: păstrează doar unele grupuri
filter ia o funcție care întoarce True/False per grup și păstrează rândurile grupurilor în care funcția întoarce True:
df.groupby("country").filter(lambda g: len(g) >= 3)
Asta întoarce DataFrame-ul de formă originală, dar doar cu rândurile din țările care au cel puțin trei înregistrări. Util pentru „elimină categoriile rare înainte de antrenarea unui model” sau „ignoră clienții ocazionali”. Reține că filter aici e metoda groupby, nu metoda DataFrame .filter (care e ceva complet diferit, un selector pe nume de coloană). Numirea pandas, of.
Pivot table-uri: groupby plus reshape
pivot_table e groupby cu un reshape încorporat. Acolo unde groupby([a, b]).agg(...) produce un DataFrame în formă lungă cu un MultiIndex, pivot_table produce unul în formă lată: una dintre chei devine coloane:
df.pivot_table(
values="revenue",
index="country",
columns="year",
aggfunc="sum",
fill_value=0,
)
Obții un DataFrame cu țările ca rânduri, anii ca coloane, venitul însumat în celule. fill_value=0 zice „dacă o combinație (country, year) nu are date, scrie zero în loc de NaN”, de obicei ce vrei pentru un tabel de prezentare.
Mai multe agregări și coloane de valori funcționează și ele:
df.pivot_table(
values=["revenue", "orders"],
index="country",
columns="year",
aggfunc={"revenue": "sum", "orders": "mean"},
)
Rezultatul e lat și prezentabil. Pentru consum în aval prelucrabil de mașini, preferă forma lungă (groupby + named agg) și fă reshape doar la final.
Combinând: filtrare, grupare, agregare, sortare
Analiza reală înlănțuie astea. Pandas se citește de sus în jos când împachetezi un lanț de metode în paranteze:
result = (
df
.loc[df["revenue"].notna()]
.groupby(["country", "year"], as_index=False)
.agg(
total_revenue=("revenue", "sum"),
total_orders=("orders", "sum"),
avg_revenue=("revenue", "mean"),
n_rows=("revenue", "size"),
)
.assign(revenue_per_order=lambda d: d["total_revenue"] / d["total_orders"])
.sort_values(["country", "year"])
.reset_index(drop=True)
)
.assign creează o coloană nouă dintr-un lambda care primește DataFrame-ul în lucru: util în lanțuri fiindcă nu poți scrie df["x"] = ... la mijlocul lanțului. .sort_values sortează rândurile; .reset_index(drop=True) reconstruiește un RangeIndex curat la final, ca rezultatul să fie prezentabil.
Asta e forma a 90% din codul analitic pandas pe care-l scriu: filtrare, groupby cu agregări denumite, coloane derivate prin assign, sortare, reset. Memorează scheletul.
Un sac amestecat de agregări care merită știute
Câteva agregări specifice apar suficient de des încât să le numesc:
first/last: întorc prima/ultima valoare din fiecare grup. Ordinea contează, deci de obicei precedate dedf.sort_values(...). Utile pentru „prima sumă a comenzii clientului” sau „cel mai recent status per ticket”.nunique: numărul de valori distincte.df.groupby("country")["customer_id"].nunique()îți dă numărul de clienți distincți per țară.idxmax/idxmin: eticheta de index a rândului cu valoarea max/min. Combinat cu.loc, așa alegi rândul complet care conține maximul per grup:df.loc[df.groupby("country")["revenue"].idxmax()].quantile:df.groupby("country")["revenue"].quantile(0.95). Percentila 95 per grup, etc. Cu agregări denumite:p95=("revenue", lambda s: s.quantile(0.95)).agg(list): colectează toate valorile din grup într-o listă Python, o celulă per grup. Util pentru „ce articole erau în această comandă?”. Lent pe date mari; în regulă pentru output-uri la scară umană.
Note de performanță
- Agregările vectorizate sunt de 10-100x mai rapide decât
.applycu o funcție Python. „sum”, „mean”, „max” etc. dispatch-uiesc la C; lambdas nu. Dacă te trezești că te întinzi după.applyîntr-o cale fierbinte, întreabă mai întâi dacăaggplus o utilizare creativă a luitransformpoate face aceeași treabă: de obicei poate. as_index=Falsee mai ieftin decât.reset_index()fiindcă pandas nu construiește MultiIndex-ul doar ca să-l arunce. Pentru cod de pipeline care se termină mereu într-un DataFrame plat, implicit foloseșteas_index=False.observed=Truecontează cu chei de grup categorice. Implicit, groupby pe o coloană category produce un rând pentru fiecare valoare a categoriei, chiar și pentru cele fără date. Cuobserved=True, apar doar valorile prezente, ceea ce e aproape mereu ce vrei și e notabil mai rapid pentru categorii cu cardinalitate mare. Pandas 3.0 va schimba implicitul; între timp, setează-l.sort=Falsesare peste sortarea alfabetică a cheilor de grup. Implicitulsort=Truee în regulă pentru rezultate mici dar adaugă overhead notabil pe milioane de grupuri, iar oricum probabil vrei o sortare specifică domeniului la finalul pipeline-ului.- Pentru date cu adevărat mari, pattern-ul groupby-then-agg e ce fac DuckDB și Polars nativ și mai rapid. Dacă DataFrame-ul tău are mai mult de câțiva GB, ia în considerare să faci agregarea în DuckDB (
duckdb.query("SELECT country, SUM(revenue) FROM df GROUP BY country").to_df()) și să citești rezultatul mic înapoi în pandas.
Ce urmează
Lecția 30 e despre join-uri: merge, concat și comportamentul de aliniere a indexului care a salvat sau ruinat multe analize. Lecția 32 va acoperi funcțiile de fereastră (rolling, expanding, ranking): verii cu aromă de serii temporale ai celor făcute azi cu transform.
Lectură suplimentară
- pandas: Group by: ghidul utilizatorului, cu agregări denumite acoperite cum trebuie.
- pandas: Reshaping and pivot tables: pivot, melt, stack, unstack.
- pandas:
transform: pagina de referință; secțiunea din ghidul utilizatorului despretransformmerită în special citită.
Ne vedem vineri.