Co je Pandas GroupBy a proč ho budete používat pořád dokola
Pokud pracujete s daty v Pythonu, dřív nebo později narazíte na groupby() v Pandas. A upřímně? Je to jeden z těch nástrojů, bez kterého si analýzu dat nedokážu představit. Celý princip vychází ze strategie split-apply-combine (rozděl-aplikuj-zkombinuj) — data rozdělíte do skupin podle jednoho nebo více sloupců, na každou skupinu aplikujete výpočet a výsledky spojíte zpět dohromady.
Zní to jednoduše, a ono to jednoduché opravdu je. Ať už počítáte průměrné tržby podle regionů, analyzujete chování zákazníků podle segmentů, nebo připravujete agregované metriky pro strojové učení — groupby() bude váš denní chleba. Tak pojďme na to — od úplných základů až po pokročilé techniky včetně novinek v Pandas 3.0.
Základy GroupBy: Jak začít
Vytvoření ukázkového datasetu
Pro všechny příklady v článku budeme pracovat s tímhle realistickým datasetem prodejních dat. Nic složitého, ale dostatečně bohaté na to, abychom si ukázali vše podstatné:
import pandas as pd
import numpy as np
# Ukázkový dataset prodejních dat
data = {
"region": ["Praha", "Brno", "Praha", "Ostrava", "Brno", "Praha", "Ostrava", "Brno"],
"produkt": ["Notebook", "Notebook", "Telefon", "Notebook", "Telefon", "Tablet", "Telefon", "Tablet"],
"trzby": [45000, 38000, 12000, 41000, 11500, 8500, 12500, 9000],
"pocet_kusu": [3, 2, 5, 3, 4, 6, 5, 7],
"mesic": ["leden", "leden", "leden", "unor", "unor", "unor", "brezen", "brezen"]
}
df = pd.DataFrame(data)
print(df)
První GroupBy operace
Nejjednodušší použití groupby() je seskupení podle jednoho sloupce a aplikace agregační funkce. Takhle to vypadá v praxi:
# Celkové tržby podle regionu
trzby_region = df.groupby("region")["trzby"].sum()
print(trzby_region)
# region
# Brno 57500
# Ostrava 53500
# Praha 65500
Co se tu vlastně stalo? Pandas vzal DataFrame, rozdělil ho na skupiny podle sloupce region, na každou skupinu aplikoval sum() a výsledky spojil do nové Series. Jednoduché a elegantní.
GroupBy podle více sloupců
Seskupení podle více sloupců vám dá mnohem detailnější pohled na data. Stačí předat seznam sloupců:
# Tržby podle regionu a produktu
trzby_detail = df.groupby(["region", "produkt"])["trzby"].sum()
print(trzby_detail)
# region produkt
# Brno Notebook 38000
# Tablet 9000
# Telefon 11500
# Ostrava Notebook 41000
# Telefon 12500
# Praha Notebook 45000
# Tablet 8500
# Telefon 12000
Agregace s .agg(): Plná kontrola nad výpočty
Více funkcí najednou
Tady to začíná být zajímavé. Metoda .agg() vám umožní aplikovat více agregačních funkcí naráz:
# Více statistik pro tržby podle regionu
statistiky = df.groupby("region")["trzby"].agg(["sum", "mean", "std", "min", "max"])
print(statistiky)
Různé funkce pro různé sloupce
Slovníkový zápis je šikovný — můžete aplikovat úplně odlišné funkce na různé sloupce:
# Různé agregace pro různé sloupce
vysledek = df.groupby("region").agg({
"trzby": ["sum", "mean"],
"pocet_kusu": ["sum", "count"]
})
print(vysledek)
Výsledkem je DataFrame s víceúrovňovým (MultiIndex) záhlavím sloupců. A tady je háček — takové záhlaví může dost komplikovat další práci s daty. Naštěstí existuje lepší způsob.
Pojmenovaná agregace — moderní přístup v Pandas 3.0
Tohle je podle mě nejlepší způsob, jak psát agregace. Pojmenovaná agregace (named aggregation) je doporučený přístup od Pandas 2.0+ a v Pandas 3.0 je považována za zlatý standard. Proč? Výstupní sloupce mají hezké, čitelné názvy a žádný otravný MultiIndex.
# Pojmenovaná agregace — čistý a přehledný výstup
vysledek = df.groupby("region").agg(
celkove_trzby=("trzby", "sum"),
prumerne_trzby=("trzby", "mean"),
celkem_kusu=("pocet_kusu", "sum"),
pocet_transakci=("pocet_kusu", "count")
)
print(vysledek)
# celkove_trzby prumerne_trzby celkem_kusu pocet_transakci
# region
# Brno 57500 19166.666667 13 3
# Ostrava 53500 26750.000000 8 2
# Praha 65500 21833.333333 14 3
Jakmile si na tento zápis zvyknete, nebudete chtít používat nic jiného. Seriózně.
Transform: Skupinové výpočty se zachováním tvaru dat
Metoda .transform() je trochu jiná bestie. Potřebujete výsledek skupinového výpočtu přiřadit zpět ke každému řádku původního DataFrame? Přesně na to je .transform(). Na rozdíl od .agg(), která data redukuje, tady dostanete Series se stejným indexem jako vstup.
Typické použití: podíl na celku skupiny
# Přidáme sloupec s celkovými tržbami regionu
df["trzby_region"] = df.groupby("region")["trzby"].transform("sum")
# Procentuální podíl každé transakce na celkových tržbách regionu
df["podil_na_regionu"] = (df["trzby"] / df["trzby_region"] * 100).round(1)
print(df[["region", "produkt", "trzby", "trzby_region", "podil_na_regionu"]])
Normalizace (z-skóre) uvnitř skupin
Jedno z nejčastějších použití .transform() je normalizace hodnot uvnitř skupin. Pokud připravujete data pro strojové učení, tohle je něco, co budete dělat běžně:
# Z-skóre tržeb uvnitř každého regionu
df["trzby_zscore"] = df.groupby("region")["trzby"].transform(
lambda x: (x - x.mean()) / x.std()
)
print(df[["region", "produkt", "trzby", "trzby_zscore"]])
Kumulativní součty a pořadí
Další skvělé využití — kumulativní výpočty a řazení uvnitř skupin:
# Kumulativní součet tržeb uvnitř každého regionu
df["kumulativni_trzby"] = df.groupby("region")["trzby"].cumsum()
# Pořadí podle tržeb uvnitř regionu (sestupně)
df["poradi_v_regionu"] = df.groupby("region")["trzby"].rank(
ascending=False, method="dense"
).astype(int)
print(df[["region", "produkt", "trzby", "poradi_v_regionu"]])
Apply: Flexibilní skupinové operace
Metoda .apply() je ten švýcarský nůž skupinových operací. Je nejflexibilnější, ale (a to je důležité „ale") taky nejpomalejší. Použijte ji, když potřebujete pracovat s více sloupci najednou nebo provádět výpočty, které prostě nejdou vyjádřit přes .agg() nebo .transform().
Vážený průměr
# Vážený průměr ceny za kus (váženo počtem kusů)
def vazeny_prumer(skupina):
cena_za_kus = skupina["trzby"] / skupina["pocet_kusu"]
vahy = skupina["pocet_kusu"]
return (cena_za_kus * vahy).sum() / vahy.sum()
vysledek = df.groupby("region").apply(vazeny_prumer, include_groups=False)
print(vysledek)
Top N záznamů v každé skupině
Klasický use case — chcete najít top záznamy v každé skupině:
# Top 2 transakce s nejvyššími tržbami v každém regionu
top2 = df.groupby("region").apply(
lambda x: x.nlargest(2, "trzby"), include_groups=False
)
print(top2)
Apply vs Transform vs Agg — kdy co použít
Tohle je otázka, na kterou narazí snad každý. Správná volba metody výrazně ovlivňuje jak čitelnost, tak výkon kódu:
- .agg() — souhrnné statistiky (sum, mean, count). Redukuje data na jednu hodnotu za skupinu.
- .transform() — výsledek skupinového výpočtu zpět na úrovni jednotlivých řádků. Zachovává tvar dat.
- .apply() — poslední možnost pro složité operace vyžadující přístup k více sloupcům. Nejpomalejší, ale nejflexibilnější.
# Srovnání: tři způsoby, jak získat průměr za skupinu
# 1. agg — vrací zredukovaný DataFrame
prumer_agg = df.groupby("region")["trzby"].agg("mean")
# 2. transform — vrací Series se stejným indexem jako vstup
prumer_transform = df.groupby("region")["trzby"].transform("mean")
# 3. apply — flexibilní, ale pomalejší
prumer_apply = df.groupby("region")["trzby"].apply(lambda x: x.mean())
print(f"agg výsledek (3 řádky):\n{prumer_agg}\n")
print(f"transform výsledek (8 řádků):\n{prumer_transform}\n")
Filtrování skupin s .filter()
Občas nepotřebujete agregovat, ale chcete vyfiltrovat celé skupiny na základě nějaké podmínky. Na to je tu .filter(). Vrátí podmnožinu původního DataFrame — jen řádky ze skupin, které prošly podmínkou.
# Pouze regiony s celkovými tržbami nad 55 000
silne_regiony = df.groupby("region").filter(lambda x: x["trzby"].sum() > 55000)
print(silne_regiony)
# Vrátí pouze řádky pro Brno (57 500) a Praha (65 500)
Novinky GroupBy v Pandas 3.0
Pandas 3.0, vydaný v lednu 2026, přinesl několik změn, o kterých byste měli vědět. Některé z nich vám můžou rozbít existující kód, takže pozor.
observed=True je nyní výchozí
Tohle je asi nejdůležitější změna. Při seskupování podle kategorických sloupců se dříve ve výstupu objevovaly i kombinace kategorií, které v datech vůbec neexistovaly. Od Pandas 3.0 je parametr observed nastaven na True, takže uvidíte jen to, co v datech skutečně je:
# Pandas 3.0 — observed=True je výchozí
df["region"] = df["region"].astype("category")
# Zobrazí pouze regiony, které existují v datech
vysledek = df.groupby("region")["trzby"].sum()
print(vysledek)
# V Pandas 2.x by se zobrazily i prázdné kategorie
Copy-on-Write ovlivňuje výsledky GroupBy
Pandas 3.0 aktivoval Copy-on-Write jako výchozí chování. V praxi to znamená, že výsledky groupby() operací se vždy chovají jako kopie — úpravy výsledku neovlivní původní DataFrame:
# Pandas 3.0 — Copy-on-Write je výchozí
vysledek = df.groupby("region")["trzby"].sum()
# Modifikace vysledku neovlivní původní df
vysledek.iloc[0] = 0 # Bezpečné — pracujete s kopií
Nový string dtype
Pandas 3.0 taky zavádí nový nativní datový typ str místo dřívějšího object pro textové sloupce. Funkčně se nic nemění, ale pokud seskupujete podle textových sloupců, dtype výsledků může vypadat jinak než dřív. Nic dramatického, jen je dobré o tom vědět.
Optimalizace výkonu GroupBy
U malých datasetů si výkonem nemusíte lámat hlavu. Ale jakmile pracujete s miliony řádků (a věřte mi, v praxi to přijde dřív, než čekáte), groupby() se může stát úzkým hrdlem. Tady jsou osvědčené triky, jak ho zrychlit.
Používejte vestavěné funkce místo lambda
import timeit
# Pomalé — lambda funkce
%timeit df.groupby("region")["trzby"].agg(lambda x: x.sum())
# Rychlé — vestavěná funkce
%timeit df.groupby("region")["trzby"].sum()
# Vestavěné funkce jsou typicky 5-10× rychlejší
Proč takový rozdíl? Vestavěné agregační funkce jako sum(), mean() nebo count() jsou implementovány v C/Cythonu a obcházejí Python overhead. Lambda funkce musí projít Python interpretem pro každou skupinu zvlášť.
Převeďte klíče na kategorický typ
# Převod na kategorický typ před groupby
df["region"] = df["region"].astype("category")
df["produkt"] = df["produkt"].astype("category")
# GroupBy na kategorických sloupcích je rychlejší a paměťově efektivnější
vysledek = df.groupby(["region", "produkt"])["trzby"].sum()
Vypněte řazení s sort=False
Výchozí chování Pandas je řadit výsledné skupiny. Pokud vám na pořadí nezáleží, můžete si ušetřit čas:
# Výchozí: skupiny jsou seřazeny (pomalejší)
vysledek_sorted = df.groupby("region")["trzby"].sum()
# Bez řazení: rychlejší pro velké datasety
vysledek_unsorted = df.groupby("region", sort=False)["trzby"].sum()
Validujte vztahy s validate (při merge po GroupBy)
Když výsledky GroupBy spojujete zpět s původním DataFrame přes merge(), vždycky použijte parametr validate. Ušetří vám to spoustu debugování:
# Agregace + merge zpět do původního DataFrame
souhrn = df.groupby("region").agg(
celkove_trzby=("trzby", "sum")
).reset_index()
# Bezpečný merge s validací
df_enriched = df.merge(souhrn, on="region", validate="many_to_one")
print(df_enriched[["region", "produkt", "trzby", "celkove_trzby"]])
Praktický příklad: Měsíční obchodní report
Dost teorie — pojďme dát všechno dohromady a vytvořit něco, co byste mohli skutečně použít v praxi. Tenhle příklad simuluje realistický obchodní report:
import pandas as pd
import numpy as np
# Generování realistických dat
np.random.seed(42)
n = 1000
regiony = ["Praha", "Brno", "Ostrava", "Plzeň", "Liberec"]
produkty = ["Notebook", "Telefon", "Tablet", "Monitor", "Sluchátka"]
mesice = pd.date_range("2026-01-01", periods=3, freq="MS")
df_report = pd.DataFrame({
"datum": np.random.choice(mesice, n),
"region": np.random.choice(regiony, n),
"produkt": np.random.choice(produkty, n),
"trzby": np.random.randint(1000, 50000, n),
"pocet_kusu": np.random.randint(1, 20, n)
})
# 1. Měsíční souhrn podle regionů
mesicni_souhrn = df_report.groupby(
[df_report["datum"].dt.month_name(), "region"]
).agg(
celkove_trzby=("trzby", "sum"),
prumerna_objednavka=("trzby", "mean"),
pocet_objednavek=("trzby", "count"),
celkem_kusu=("pocet_kusu", "sum")
).round(0)
print("=== Měsíční souhrn ===")
print(mesicni_souhrn)
# 2. Top produkt v každém regionu
top_produkt = df_report.groupby(["region", "produkt"]).agg(
trzby=("trzby", "sum")
).reset_index()
top_produkt = top_produkt.loc[
top_produkt.groupby("region")["trzby"].idxmax()
]
print("\n=== Top produkt podle regionu ===")
print(top_produkt)
# 3. Procentuální podíl regionu na celkových tržbách
df_report["podil_regionu"] = (
df_report.groupby("region")["trzby"].transform("sum")
/ df_report["trzby"].sum() * 100
).round(1)
podily = df_report.groupby("region")["podil_regionu"].first()
print("\n=== Podíl regionů na celkových tržbách ===")
print(podily)
Často kladené otázky (FAQ)
Jaký je rozdíl mezi .agg(), .transform() a .apply() v Pandas GroupBy?
Ve zkratce: .agg() redukuje data na jednu hodnotu za skupinu (součet, průměr...) a vrací menší DataFrame. .transform() vrací výsledek se stejným počtem řádků jako vstup — skvělé pro přiřazení skupinových statistik zpět k řádkům. A .apply()? Ta je nejflexibilnější a umí pracovat s celou skupinou najednou, ale je taky nejpomalejší. Pravidlo palce: pokud to jde přes .agg() nebo .transform(), nepoužívejte .apply().
Jak resetovat index po GroupBy operaci?
Po groupby() se seskupovací sloupce stanou indexem. Chcete zpět normální číselný index? Použijte .reset_index(), nebo ještě lepší — nastavte rovnou as_index=False přímo v groupby: df.groupby("region", as_index=False)["trzby"].sum().
Jak GroupBy zachází s chybějícími hodnotami (NaN)?
Ve výchozím nastavení (parametr dropna=True) Pandas řádky s NaN ve seskupovacím sloupci prostě ignoruje. Pokud chcete NaN zachovat jako vlastní skupinu, nastavte dropna=False: df.groupby("region", dropna=False)["trzby"].sum(). Mimochodem, v Pandas 3.0.1 byla opravena nepříjemná regrese při kombinaci kategorických dat s NaN a observed=False.
Proč je GroupBy pomalý a jak ho zrychlit?
Nejčastější viník? Lambda funkce a .apply() místo vestavěných metod. Vestavěné funkce (sum, mean, count) jsou napsané v C a bývají 5–10× rychlejší. Další tipy: převeďte seskupovací sloupce na kategorický typ, vypněte řazení s sort=False a filtrujte data ještě před GroupBy operací.
Co se změnilo v GroupBy v Pandas 3.0?
Pandas 3.0 (leden 2026) přinesl tři hlavní změny. Parametr observed je teď výchozně True pro kategorická data, Copy-on-Write je aktivní (výsledky GroupBy se chovají jako kopie) a nový datový typ str nahrazuje object pro textové sloupce. Tip na hladký přechod: nejprve upgradujte na Pandas 2.3, ověřte, že kód nehlásí žádná varování, a teprve pak přejděte na 3.0.