Pandas GroupBy: Potpuni vodič za grupiranje i agregaciju podataka u Pythonu

Naučite kako koristiti pandas GroupBy za grupiranje i agregaciju podataka u Pythonu. Vodič pokriva agg(), transform(), apply(), NamedAgg, optimizaciju performansi i novosti u pandas 3.0 s praktičnim primjerima.

Pandas GroupBy: Potpuni vodič za grupiranje i agregaciju podataka

Ako radite s podatcima u Pythonu, gotovo sigurno ste se već susreli s ovim scenarijem — imate hrpu redaka u tablici i trebate odgovore na pitanja poput: "Koja je prosječna plaća po odjelu?", "Kolika je ukupna prodaja po regiji i mjesecu?" ili "Koji proizvod ima najveći broj reklamacija?"

Tu na scenu stupa pandas GroupBy.

Iskreno, ovo je jedna od onih pandas značajki koju, kad jednom svladate, koristite gotovo svaki dan. Mehanizam je jednostavan u teoriji — grupirate podatke prema jednom ili više stupaca, a onda na svaku grupu primijenite agregacijsku funkciju. U praksi, međutim, postoji hrpa nijansi koje mogu zbuniti i iskusnije korisnike.

U ovom vodiču prolazimo kroz sve aspekte groupby() metode — od osnovnih primjera do naprednijih tehnika poput imenovanog agregiranja, razlike između agg(), transform() i apply(), te optimizacije performansi za velike skupove podataka. Svi primjeri prilagođeni su pandas 3.0 verziji (objavljenoj u siječnju 2026.), pa nećete naletjeti na zastarjele obrasce.

Što je GroupBy i kako funkcionira?

Metoda groupby() u pandasu slijedi paradigmu poznatu kao "split-apply-combine" — podijeli, primijeni, kombiniraj. Tri su jasna koraka:

  1. Split (Podjela) — DataFrame se dijeli u grupe na temelju jedinstvenih vrijednosti u jednom ili više stupaca.
  2. Apply (Primjena) — Na svaku grupu zasebno primjenjuje se neka funkcija — agregacija, transformacija ili filtriranje.
  3. Combine (Kombiniranje) — Rezultati svih grupa spajaju se natrag u jedinstvenu strukturu podataka.

Ako ste ikad pisali SQL, koncept je praktički identičan GROUP BY klauzuli. Ali pandas GroupBy nudi puno veću fleksibilnost — osim klasične agregacije, podržava transformacije, filtriranje i proizvoljne operacije nad grupama. To je nešto što vam čisti SQL ne može pružiti (barem ne tako elegantno).

Postavljanje radnog okruženja

Za praćenje primjera trebat će vam Python 3.10+ i pandas 3.0. Dakle, prvo instalirajmo sve potrebno:

# Instalacija pandas 3.0
pip install pandas>=3.0 numpy

# Provjera verzije
import pandas as pd
print(pd.__version__)  # 3.0.1 ili novije

Sad kreirajmo realistični skup podataka koji ćemo koristiti kroz cijeli vodič. Osobno volim raditi s podatcima koji barem malo nalikuju na stvarni svijet — lakše je pratiti logiku kad stupci imaju smislena imena:

import pandas as pd
import numpy as np

# Podatci o prodaji za lanac trgovina
np.random.seed(42)

prodaja = pd.DataFrame({
    "datum": pd.date_range("2025-01-01", periods=100, freq="D"),
    "regija": np.random.choice(["Zagreb", "Split", "Rijeka", "Osijek"], 100),
    "kategorija": np.random.choice(["Elektronika", "Odjeća", "Hrana", "Sport"], 100),
    "prodavac": np.random.choice(["Ana", "Marko", "Ivan", "Petra", "Luka"], 100),
    "iznos": np.random.randint(100, 5000, 100).astype(float),
    "kolicina": np.random.randint(1, 20, 100),
    "povrat": np.random.choice([True, False], 100, p=[0.1, 0.9])
})

print(prodaja.head(10))
print(f"\nOblik podataka: {prodaja.shape}")
print(f"Stupci: {list(prodaja.columns)}")

Grupiranje po jednom stupcu

Krenimo od najjednostavnijeg oblika — odaberete jedan stupac i primijenite agregacijsku funkciju:

# Ukupna prodaja po regiji
po_regiji = prodaja.groupby("regija")["iznos"].sum()
print(po_regiji)

# Izlaz:
# regija
# Osijek     28450.0
# Rijeka     31200.0
# Split      35800.0
# Zagreb     42150.0
# Name: iznos, dtype: float64

Jedna stvar koju vrijedi zapamtiti: groupby("regija") samo stvara DataFrameGroupBy objekt — on sam po sebi ne izvršava nikakav izračun. Pandas koristi tzv. lijenu evaluaciju (lazy evaluation), pa se izračun pokreće tek kad pozovete neku funkciju poput sum(), mean() ili count().

# GroupBy objekt — još bez izračuna
grupa = prodaja.groupby("regija")
print(type(grupa))
# <class 'pandas.core.groupby.generic.DataFrameGroupBy'>

# Broj grupa
print(f"Broj grupa: {grupa.ngroups}")

# Pregled grupa
print(grupa.groups.keys())

Ovo je korisno znati jer možete lančano nadovezivati operacije bez brige o performansama — pandas neće ništa izračunati dok eksplicitno ne zatražite rezultat.

Grupiranje po više stupaca

Kad trebate detaljniju analizu, grupirate po kombinaciji stupaca. Recimo, želite vidjeti prodaju po regiji i kategoriji proizvoda:

# Prosječna prodaja po regiji i kategoriji
po_regiji_kat = prodaja.groupby(["regija", "kategorija"])["iznos"].mean()
print(po_regiji_kat)

# Rezultat je Series s MultiIndex-om
# Pretvaranje u čitljiviji DataFrame
print(po_regiji_kat.reset_index())

Rezultat grupiranja po više stupaca uvijek ima MultiIndex. Pozivanjem .reset_index() pretvarate ga u obični DataFrame s odvojenim stupcima — što je obično praktičnije za daljnju obradu ili vizualizaciju. Ja osobno gotovo uvijek odmah pozovem reset_index() jer mi je tako lakše raditi.

Ugrađene agregacijske funkcije

Pandas dolazi s cijelim nizom ugrađenih agregacijskih funkcija koje su optimizirane u Cythonu i stoga se izvršavaju znatno brže od prilagođenih Python funkcija:

# Razne agregacije na jednom stupcu
print("Suma:", prodaja.groupby("regija")["iznos"].sum())
print("Prosjek:", prodaja.groupby("regija")["iznos"].mean())
print("Medijan:", prodaja.groupby("regija")["iznos"].median())
print("Minimum:", prodaja.groupby("regija")["iznos"].min())
print("Maksimum:", prodaja.groupby("regija")["iznos"].max())
print("Broj:", prodaja.groupby("regija")["iznos"].count())
print("Std. devijacija:", prodaja.groupby("regija")["iznos"].std())
print("Varijanca:", prodaja.groupby("regija")["iznos"].var())

Ove funkcije pokrivaju većinu svakodnevnih potreba. Ali za složenije scenarije — recimo kad trebate više agregacija odjednom ili prilagođene izračune — tu dolazi metoda agg().

Metoda agg() — višestruke agregacije odjednom

Metoda agg() (skraćeno od aggregate) omogućuje primjenu više funkcija na jedan ili više stupaca u istom pozivu. Po mom iskustvu, ovo je jedan od najkorisnijih alata u cijelom pandas ekosustavu.

Više funkcija na jednom stupcu

# Više agregacija na stupcu "iznos"
rezultat = prodaja.groupby("regija")["iznos"].agg(["sum", "mean", "count", "std"])
print(rezultat)

Različite funkcije za različite stupce

# Rječnik — svaki stupac dobiva svoju funkciju
rezultat = prodaja.groupby("regija").agg({
    "iznos": ["sum", "mean"],
    "kolicina": "sum",
    "povrat": "sum"  # True se broji kao 1
})
print(rezultat)

Prilagođene funkcije u agg()

# Definiranje prilagođene funkcije
def raspon(x):
    """Razlika između maksimuma i minimuma."""
    return x.max() - x.min()

def koeficijent_varijacije(x):
    """Standardna devijacija podijeljena s prosjekom."""
    return x.std() / x.mean() * 100

rezultat = prodaja.groupby("regija")["iznos"].agg(
    ["mean", "std", raspon, koeficijent_varijacije]
)
print(rezultat)

Prilagođene funkcije su super kad ugrađene ne pokrivaju vaš specifični slučaj. Jedino imajte na umu da su sporije od ugrađenih — o tome više u dijelu o optimizaciji.

Imenovano agregiranje s NamedAgg

Evo nečega što mi je doslovno promijenilo način pisanja pandas koda — imenovano agregiranje (named aggregation) pomoću pd.NamedAgg. Umjesto da nakon grupiranja ručno preimenujete stupce (što je bilo prilično nezgrapno), odmah definirate željene nazive izlaznih stupaca:

# Imenovano agregiranje — čisto i čitljivo
rezultat = prodaja.groupby("regija").agg(
    ukupna_prodaja=pd.NamedAgg(column="iznos", aggfunc="sum"),
    prosjecna_prodaja=pd.NamedAgg(column="iznos", aggfunc="mean"),
    ukupna_kolicina=pd.NamedAgg(column="kolicina", aggfunc="sum"),
    broj_povrata=pd.NamedAgg(column="povrat", aggfunc="sum"),
    broj_transakcija=pd.NamedAgg(column="iznos", aggfunc="count")
)
print(rezultat)

Postoji i kraća sintaksa s torkama (tuples) ako preferirate sažetiji kod:

# Ekvivalentno, ali s torkama
rezultat = prodaja.groupby("regija").agg(
    ukupna_prodaja=("iznos", "sum"),
    prosjecna_prodaja=("iznos", "mean"),
    ukupna_kolicina=("kolicina", "sum"),
    broj_povrata=("povrat", "sum")
)
print(rezultat)

Zašto je ovo toliko bolje? Zato što vam eliminira potrebu za naknadnim .rename() pozivom ili ručnim petljanjem s MultiIndex stupcima. Kod je čitljiviji, rezultat odmah spreman za daljnju upotrebu. Jednom kad počnete koristiti NamedAgg, teško je vratiti se na stari način.

Razlika između agg(), transform() i apply()

Ovo je, po mom iskustvu, pitanje broj jedan koje zbunjuje ljude koji uče pandas. Sve tri metode rade nad grupama, ali razlikuju se u obliku izlaza i fleksibilnosti. Hajde da raščistimo jednom za svagda.

agg() — sažetak podataka

Vraća smanjeni rezultat — jednu vrijednost (ili više vrijednosti) po grupi. Koristite kad trebate sumarnu statistiku.

# agg() — jedan red po grupi
prosjek_po_regiji = prodaja.groupby("regija")["iznos"].agg("mean")
print(prosjek_po_regiji)
# regija
# Osijek     1850.5
# Rijeka     2100.3
# Split      2340.0
# Zagreb     2650.7

transform() — rezultat iste veličine

Vraća rezultat iste veličine kao ulazni DataFrame. Ovo je idealno kad želite dodati grupne statistike natrag u originalni DataFrame — na primjer, kad trebate izračunati koliko svaka transakcija odstupa od prosjeka svoje grupe.

# transform() — ista veličina kao original
prodaja["prosjek_regije"] = prodaja.groupby("regija")["iznos"].transform("mean")
prodaja["odstupanje"] = prodaja["iznos"] - prodaja["prosjek_regije"]

# Z-score normalizacija unutar grupe
prodaja["z_score"] = prodaja.groupby("regija")["iznos"].transform(
    lambda x: (x - x.mean()) / x.std()
)

print(prodaja[["regija", "iznos", "prosjek_regije", "odstupanje", "z_score"]].head())

Moram priznati, transform() mi je trebao neko vrijeme da "klikne" u glavi. Ali kad jednom shvatite ideju — da rezultat zadržava isti broj redova kao original — počnete ga viđati svugdje. Z-score normalizacija unutar grupa, postotni udio unutar kategorije, razlika od grupnog prosjeka... sve to postaje trivijalno.

apply() — potpuna fleksibilnost

Metoda apply() prima cijeli DataFrame grupe kao argument, pa imate pristup svim stupcima unutar grupe. Koristite je samo kad agg() ili transform() nisu dovoljni.

# apply() — pristup cijelom DataFrame-u grupe
def top_3_prodaje(grupa):
    """Vraća 3 najveće prodaje za svaku grupu."""
    return grupa.nlargest(3, "iznos")

top_po_regiji = prodaja.groupby("regija").apply(top_3_prodaje)
print(top_po_regiji[["regija", "prodavac", "iznos"]])

Usporedna tablica

Da bude potpuno jasno, evo pregleda ključnih razlika:

Značajkaagg()transform()apply()
Oblik izlazaSmanjen (1 red po grupi)Isti kao originalFleksibilan
Ulaz u funkcijuSeries (stupac grupe)Series (stupac grupe)DataFrame (cijela grupa)
Više stupacaDaJedan po jedanDa
Više funkcijaDaJedna po jednaDa
PerformanseBrzoBrzoSporije
Najbolje zaSumarnu statistikuDodavanje grupnih podataka redovimaSloženu prilagođenu logiku

Pravilo palca: Uvijek krenite s agg() ili transform(). Na apply() pribjegnite samo kad vam treba pristup cijelom DataFrame-u grupe ili kad rezultat ne odgovara ni agregaciji ni transformaciji. Vaša budućnost (i vaši kolege) će vam biti zahvalni na bržem kodu.

Filtriranje grupa

Ponekad ne trebate sve grupe — želite zadržati samo one koje zadovoljavaju neki uvjet. Za to postoji metoda filter():

# Zadrži samo regije s ukupnom prodajom većom od 30000
filtrirano = prodaja.groupby("regija").filter(lambda x: x["iznos"].sum() > 30000)
print(f"Originalni DataFrame: {len(prodaja)} redaka")
print(f"Filtrirani DataFrame: {len(filtrirano)} redaka")
print(f"Preostale regije: {filtrirano['regija'].unique()}")

# Zadrži samo grupe s više od 20 transakcija
vece_grupe = prodaja.groupby("regija").filter(lambda x: len(x) > 20)
print(vece_grupe["regija"].value_counts())

Ovo je iznimno korisno kad, recimo, želite izbaciti kategorije s premalo uzoraka prije statističke analize. Radije filtrirajte grupe ovako nego da ručno tražite koji redovi pripadaju kojoj grupi.

Grupiranje po vremenskim intervalima s Grouper

Ovo je nešto na što ćete često naletjeti u praksi — rad s vremenskim nizovima. pd.Grouper omogućuje grupiranje po vremenskim intervalima (tjedan, mjesec, kvartal...) bez da morate ručno izvlačiti mjesec ili godinu iz datuma:

# Osigurajmo da je stupac datum tipa datetime
prodaja["datum"] = pd.to_datetime(prodaja["datum"])

# Mjesečna prodaja
mjesecna = prodaja.groupby(pd.Grouper(key="datum", freq="ME")).agg(
    ukupno=("iznos", "sum"),
    prosjek=("iznos", "mean"),
    transakcija=("iznos", "count")
)
print(mjesecna)

# Tjedno po regiji
tjedno_regija = prodaja.groupby(
    ["regija", pd.Grouper(key="datum", freq="W")]
)["iznos"].sum()
print(tjedno_regija.head(20))

Mala napomena — u pandas 3.0 koristite "ME" za kraj mjeseca umjesto starog "M", i "YE" umjesto "Y". Ako naiđete na FutureWarning u starijem kodu, to je razlog.

Novosti u pandas 3.0 za GroupBy

Pandas 3.0 (siječanj 2026.) donio je nekoliko promjena koje direktno utječu na groupby() operacije. Evo najvažnijih:

  • Copy-on-Write je sada uključen prema zadanom — Rezultati grupiranja uvijek se ponašaju kao kopije. Nema više onih neugodnih situacija kad neočekivano mutirate originalni DataFrame kroz poglede (views). Iskreno, ovo je promjena koja je trebala doći davno.
  • Konzistentno rukovanje nepromatranim grupama — Kad koristite apply() ili agg() s višestrukim grupiranjima, nepromatrane kombinacije se sada pravilno prosljeđuju funkciji. Prije je ovo znalo rezultirati neočekivanim NaN vrijednostima.
  • Novi podrazumijevani StringDtype — Stupci s tekstualnim podatcima koriste optimizirani str tip (s PyArrow pozadinom). To utječe na agregacije nad tekstualnim stupcima — sum() na nepromatranim kategorijama sada vraća prazan string "" umjesto 0.
  • Popravak sortiranja u value_counts()groupby("a", sort=True).value_counts(sort=False) sada pravilno razdvaja sortiranje ključeva grupa od sortiranja vrijednosti. Sitnica, ali ako ste se ikad čudili zašto vam sortiranje ne radi kako očekujete — ovo je bio razlog.
# Primjer: Copy-on-Write u pandas 3.0
# Rezultat groupby operacije je uvijek neovisna kopija
rezultat = prodaja.groupby("regija")["iznos"].sum()
# Modificiranje rezultata NE utječe na originalni DataFrame
rezultat["Zagreb"] = 0
print(prodaja[prodaja["regija"] == "Zagreb"]["iznos"].sum())  # Nepromijenjeno

Optimizacija performansi za velike skupove podataka

Dok vam je DataFrame mali, performanse nisu problem. Ali kad počnete raditi s milijunima redova (a to se dogodi brže nego što mislite), svaka optimizacija postaje važna. Evo tehnika koje sam osobno koristio i koje stvarno čine razliku.

1. Koristite kategorički tip za stupce grupiranja

# Pretvaranje string stupaca u category
prodaja["regija"] = prodaja["regija"].astype("category")
prodaja["kategorija"] = prodaja["kategorija"].astype("category")

# Ovo može ubrzati groupby operacije i do 10x
# jer pandas ne mora svaki put hashirati stringove
%timeit prodaja.groupby("regija")["iznos"].sum()

Ova jednostavna promjena tipa može dramatično ubrzati stvari jer pandas interno koristi cjelobrojne kodove umjesto hashiranja stringova pri svakom grupiranju.

2. Koristite sort=False i observed=True

# sort=False preskaže sortiranje rezultata — brže
rezultat = prodaja.groupby("regija", sort=False)["iznos"].sum()

# observed=True ignorira nepromatrane kategorije
rezultat = prodaja.groupby("regija", observed=True)["iznos"].sum()

Parametar sort=False je posebno koristan kad vam redoslijed grupa nije bitan — a u mnogim slučajevima zaista nije.

3. Izbjegavajte apply() kad god je moguće

# SPORO — apply s lambda funkcijom
sporo = prodaja.groupby("regija")["iznos"].apply(lambda x: x.sum())

# BRZO — ugrađena sum() funkcija
brzo = prodaja.groupby("regija")["iznos"].sum()

# Ugrađene funkcije su implementirane u Cythonu
# i mogu biti i do 100x brže od apply() s Pythonom

Da, sto puta. Nije pretjerivanje. Razlika između Python petlje i Cython optimizacije je stvarno tolika na velikim skupovima podataka.

4. Optimizirajte tipove podataka

# Smanjite veličinu numeričkih stupaca
prodaja["kolicina"] = prodaja["kolicina"].astype("int16")
prodaja["iznos"] = prodaja["iznos"].astype("float32")

# Provjera uštede memorije
print(prodaja.memory_usage(deep=True))

Ovo je često zanemareno, ali ako radite s velikim podatcima, prebacivanje s float64 na float32 prepolovit će memorijsku potrošnju tog stupca. Zvuči trivijalno dok nemate DataFrame od nekoliko gigabajta.

Praktični primjer: Analiza prodajnih podataka

Hajde da sada spojimo sve naučeno u jednu cjelovitu analizu. Ovo je otprilike kako bih ja pristupio tipičnoj analizi prodajnih podataka:

import pandas as pd
import numpy as np

# Generiranje realističnijih podataka
np.random.seed(2026)
n = 1000

prodaja = pd.DataFrame({
    "datum": pd.date_range("2025-01-01", periods=n, freq="D"),
    "regija": np.random.choice(
        ["Zagreb", "Split", "Rijeka", "Osijek", "Dubrovnik"], n,
        p=[0.3, 0.25, 0.2, 0.15, 0.1]
    ),
    "kategorija": np.random.choice(
        ["Elektronika", "Odjeća", "Hrana", "Sport", "Knjige"], n
    ),
    "prodavac": np.random.choice(
        ["Ana", "Marko", "Ivan", "Petra", "Luka", "Sara", "Tomislav"], n
    ),
    "iznos": np.random.lognormal(mean=7, sigma=1, size=n).round(2),
    "kolicina": np.random.randint(1, 50, n)
})

# 1. Pregled prodaje po regiji
print("=== Prodaja po regiji ===")
po_regiji = prodaja.groupby("regija").agg(
    ukupan_prihod=("iznos", "sum"),
    prosjecna_transakcija=("iznos", "mean"),
    medijan_transakcije=("iznos", "median"),
    broj_transakcija=("iznos", "count"),
    ukupna_kolicina=("kolicina", "sum")
)
po_regiji["prosjecna_transakcija"] = po_regiji["prosjecna_transakcija"].round(2)
print(po_regiji)

# 2. Top 3 prodavača po regiji
print("\n=== Top 3 prodavača po regiji ===")
top_prodavaci = prodaja.groupby(["regija", "prodavac"]).agg(
    ukupno=("iznos", "sum")
).reset_index()

top_3 = top_prodavaci.groupby("regija").apply(
    lambda x: x.nlargest(3, "ukupno"), include_groups=False
).reset_index(level=0)
print(top_3)

# 3. Mjesečni trendovi s transform()
prodaja["mjesecni_prosjek"] = prodaja.groupby(
    [pd.Grouper(key="datum", freq="ME"), "regija"]
)["iznos"].transform("mean").round(2)

prodaja["rang_u_regiji"] = prodaja.groupby("regija")["iznos"].rank(
    ascending=False, method="dense"
)

print("\n=== Prvih 5 redova s dodanim stupcima ===")
print(prodaja[["datum", "regija", "iznos", "mjesecni_prosjek", "rang_u_regiji"]].head())

Često postavljana pitanja (FAQ)

Koja je razlika između groupby().agg() i groupby().apply() u pandasu?

Metoda agg() služi za agregaciju — prima Series (pojedinačni stupac) i vraća smanjen rezultat s jednim redom po grupi. Metoda apply() prima cijeli DataFrame grupe, što daje pristup svim stupcima, ali je sporija. Koristite agg() kad god je moguće jer su ugrađene funkcije optimizirane u Cythonu i mogu biti i do 100 puta brže.

Kako grupirati podatke po datumu ili vremenskim intervalima?

Koristite pd.Grouper(key="stupac_datuma", freq="ME") za mjesečno grupiranje, freq="W" za tjedno, freq="QE" za kvartalno ili freq="YE" za godišnje. Stupac mora biti tipa datetime64. Možete kombinirati Grouper s drugim stupcima, na primjer groupby(["regija", pd.Grouper(key="datum", freq="ME")]).

Što je imenovano agregiranje (NamedAgg) i zašto ga koristiti?

pd.NamedAgg omogućuje definiranje naziva izlaznih stupaca tijekom agregacije, bez naknadnog preimenovanja. Sintaksa je naziv_stupca=pd.NamedAgg(column="izvorni_stupac", aggfunc="funkcija") ili kraće naziv_stupca=("izvorni_stupac", "funkcija"). Rezultat je čitljiviji kod i manje petljanja s MultiIndex stupcima.

Kako ubrzati groupby operacije na velikim skupovima podataka?

Četiri ključne optimizacije: (1) pretvorite stupce grupiranja u category tip jer je hashiranje stringova skupo; (2) koristite sort=False ako ne trebate sortirani rezultat; (3) izbjegavajte apply() s lambda funkcijama — ugrađene funkcije su dramatično brže; (4) optimizirajte numeričke tipove (float32 umjesto float64, int16 umjesto int64) da smanjite memoriju.

Što je transform() i kada ga koristiti umjesto agg()?

transform() vraća rezultat iste veličine kao ulaz, dok agg() sažima podatke u jedan red po grupi. Koristite transform() kad želite grupne statistike natrag u originalne redove — npr. postotak od grupnog prosjeka ili z-score normalizaciju. Tipičan primjer: df["prosjek_grupe"] = df.groupby("stupac")["vrijednost"].transform("mean").

O Autoru Editorial Team

Our team of expert writers and editors.