Ako ste pratili našu seriju o pandas biblioteci — od čišćenja podataka, preko GroupBy operacija, spajanja DataFrame-ova, do preoblikovanja s pivot_table i melt — onda već imate solidne temelje za rad s podacima. No što kad vam ugrađene pandas metode jednostavno nisu dovoljne? Što napraviti kada trebate primijeniti vlastitu logiku, neki prilagođeni izračun ili složenu transformaciju na svaki red, stupac ili element?
E tu na scenu stupaju tri ključna alata: apply(), map() i lambda funkcije. Iskreno, ovo su metode koje ćete koristiti gotovo svakodnevno — bilo da kategorizirate podatke prema prilagođenim pravilima, transformirate tekstualne vrijednosti ili gradite potpuno nove značajke iz postojećih stupaca. U ovom vodiču pokrit ćemo sve što trebate znati, uključujući novosti iz pandas 3.x verzije.
Što su apply(), map() i lambda u pandas-u?
Prije nego zaronimo u primjere, razjasnimo osnove. Svaka od ovih metoda ima drugačiju namjenu i opseg djelovanja:
apply()— najfleksibilnija metoda. Radi na Series-u i DataFrame-u. Može primjenjivati funkcije po redovima, stupcima ili elementima.map()— radi element po element. U pandas 3.x funkcionira i na Series-u i na DataFrame-u (zamijenila je zastarjelu metoduapplymap()).- Lambda funkcije — anonimne jednoline funkcije koje se najčešće koriste unutar
apply()ilimap()za brze transformacije bez potrebe za zasebnom definicijom.
Zvuči jednostavno, zar ne? Hajde da vidimo kako to izgleda u praksi.
Metoda apply(): Primjena funkcija po redovima i stupcima
apply() je najsvestraniji alat u vašem arsenalu. Funkcionira i na Series objektima i na cijelim DataFrame-ovima, a ključna razlika leži u parametru axis.
apply() na Series-u — element po element
Kad pozovete apply() na jednom stupcu (Series), funkcija se primjenjuje na svaki element zasebno. Evo klasičnog primjera s kategorizacijom:
import pandas as pd
import numpy as np
df = pd.DataFrame({
"ime": ["Ana", "Marko", "Petra", "Ivan", "Lucija"],
"dob": [28, 35, 42, 23, 31],
"placa": [5500, 7200, 8100, 4300, 6800]
})
# Kategorizacija dobi pomoću vlastite funkcije
def dobna_skupina(dob):
if dob < 25:
return "mladi"
elif dob < 35:
return "srednja dob"
else:
return "stariji"
df["skupina"] = df["dob"].apply(dobna_skupina)
print(df)
Rezultat:
ime dob placa skupina
0 Ana 28 5500 srednja dob
1 Marko 35 7200 stariji
2 Petra 42 8100 stariji
3 Ivan 23 4300 mladi
4 Lucija 31 6800 srednja dob
Prilično čitljivo, zar ne? Definirali smo funkciju, predali je u apply(), i gotovo.
apply() na DataFrame-u — po redovima ili stupcima
Kada koristite apply() na DataFrame-u, parametar axis određuje smjer primjene:
axis=0(zadano) — funkcija se primjenjuje na svaki stupacaxis=1— funkcija se primjenjuje na svaki red
Ovo je nešto što zna zbuniti početnike (pa i iskusnije korisnike, moram priznati), pa evo konkretnog primjera za obje varijante:
# Primjena po stupcima (axis=0) — prosjek numeričkih stupaca
prosjeci = df[["dob", "placa"]].apply(np.mean, axis=0)
print(prosjeci)
# dob 31.8
# placa 6380.0
# Primjena po redovima (axis=1) — kombinirani opis svakog zaposlenika
df["opis"] = df.apply(
lambda red: f"{red['ime']} ({red['dob']} god.) - plaća {red['placa']} EUR",
axis=1
)
print(df["opis"])
# 0 Ana (28 god.) - plaća 5500 EUR
# 1 Marko (35 god.) - plaća 7200 EUR
# ...
apply() s dodatnim argumentima
Funkciji unutar apply() možete proslijediti dodatne argumente pomoću parametara args i **kwargs. Ovo je korisno kad želite parametrizirati transformaciju bez pisanja nove funkcije svaki put:
# Funkcija s dodatnim parametrom — postotak povišice
def izracunaj_novu_placu(placa, postotak):
return round(placa * (1 + postotak / 100), 2)
df["nova_placa"] = df["placa"].apply(izracunaj_novu_placu, args=(10,))
print(df[["ime", "placa", "nova_placa"]])
# ime placa nova_placa
# 0 Ana 5500 6050.0
# 1 Marko 7200 7920.0
# 2 Petra 8100 8910.0
# 3 Ivan 4300 4730.0
# 4 Lucija 6800 7480.0
Metoda map(): Mapiranje i transformacija element po element
map() je dizajniran za transformacije element po element i ima jednu veliku prednost nad apply() — osim funkcija, prima i rječnike te Series objekte kao argument. Po mom iskustvu, upravo ta mogućnost mapiranja rječnikom je nešto što ćete koristiti iznimno često.
map() s rječnikom — zamjena vrijednosti
# Mapiranje kategorija pomoću rječnika
gradovi = {
"Ana": "Zagreb",
"Marko": "Split",
"Petra": "Rijeka",
"Ivan": "Osijek",
"Lucija": "Zagreb"
}
df["grad"] = df["ime"].map(gradovi)
print(df[["ime", "grad"]])
# ime grad
# 0 Ana Zagreb
# 1 Marko Split
# 2 Petra Rijeka
# 3 Ivan Osijek
# 4 Lucija Zagreb
Ovako jednostavno. Nema petlji, nema if-else lanca — samo rječnik i map().
map() s funkcijom
# Formatiranje plaće s valutom
df["placa_fmt"] = df["placa"].map(lambda x: f"{x:,.0f} EUR")
print(df[["ime", "placa_fmt"]])
# ime placa_fmt
# 0 Ana 5,500 EUR
# 1 Marko 7,200 EUR
# 2 Petra 8,100 EUR
# 3 Ivan 4,300 EUR
# 4 Lucija 6,800 EUR
map() vs apply() na Series-u — kada koristiti što?
Za jednostavne transformacije na jednom stupcu, map() je obično brži od apply(). Na testovima s milijun redaka, razlika može biti i do 10 puta. To je značajno.
Evo kratkih smjernica:
- Koristite
map()za: zamjenu vrijednosti rječnikom, jednostavne transformacije jednog stupca, mapiranje kategorija - Koristite
apply()za: složenu logiku koja zahtijeva pristup cijelom redu (axis=1), funkcije s dodatnim argumentima, operacije po stupcima na DataFrame-u
DataFrame.map() — nasljednica zastarjele applymap()
Ovo je bitna promjena koju mnogi propuštaju. U pandas verziji 2.1.0, metoda applymap() je označena kao zastarjela (deprecated) i zamijenjena metodom DataFrame.map(). U pandas 3.x (aktualna verzija 2026. godine) trebali biste koristiti isključivo DataFrame.map():
# Primjena funkcije na svaki element DataFrame-a
numericke_kolone = df[["dob", "placa"]]
# STARI način (zastarjelo, izbjegavajte):
# numericke_kolone.applymap(lambda x: x * 1.1)
# NOVI način (pandas 3.x):
rezultat = numericke_kolone.map(lambda x: round(x * 1.1, 2))
print(rezultat)
# dob placa
# 0 30.8 6050.0
# 1 38.5 7920.0
# 2 46.2 8910.0
# 3 25.3 4730.0
# 4 34.1 7480.0
Napomena: Ako radite na starijem projektu koji još koristi applymap(), pandas 3.x će podići grešku — ne upozorenje, nego grešku. Jednostavno zamijenite sve pozive applymap() s map() i sve će raditi jer je ponašanje identično.
Lambda funkcije — brze anonimne transformacije
Lambda funkcije su, jednostavno rečeno, bezimene funkcije koje definirate u jednoj liniji koda. U kombinaciji s apply() i map(), omogućuju vam pisanje transformacija bez prethodnog definiranja zasebne funkcije. Idealne su za one "brze" operacije koje ne zaslužuju cijelu def definiciju.
Osnovna sintaksa lambda funkcija
# Sintaksa: lambda argumenti: izraz
kvadrat = lambda x: x ** 2
print(kvadrat(5)) # 25
# Ekvivalentna obična funkcija:
def kvadrat_func(x):
return x ** 2
Lambda s uvjetnom logikom
# Kategorizacija plaća pomoću lambda izraza
df["razina_place"] = df["placa"].apply(
lambda x: "visoka" if x >= 7000 else ("srednja" if x >= 5000 else "niska")
)
print(df[["ime", "placa", "razina_place"]])
# ime placa razina_place
# 0 Ana 5500 srednja
# 1 Marko 7200 visoka
# 2 Petra 8100 visoka
# 3 Ivan 4300 niska
# 4 Lucija 6800 srednja
Kada NE koristiti lambda?
Ovo je važno. Lambda izrazi su praktični za kratku logiku, ali postaju nečitljivi čim ih previše zakomplicirate. Moje pravilo palca: ako vaš lambda ima više od jednog uvjeta ili prelazi 80 znakova, definirajte običnu funkciju. Čitljivost koda je važnija nego ušteda na jednoj liniji.
# ❌ Nečitljiv lambda — izbjegavajte
df["kategorija"] = df.apply(
lambda r: "senior visoka" if r["dob"] > 40 and r["placa"] > 7000 else ("junior niska" if r["dob"] < 25 and r["placa"] < 5000 else "ostali"),
axis=1
)
# ✅ Čitljiva imenovana funkcija — preporučeno
def kategoriziraj_zaposlenika(red):
if red["dob"] > 40 and red["placa"] > 7000:
return "senior visoka"
elif red["dob"] < 25 and red["placa"] < 5000:
return "junior niska"
else:
return "ostali"
df["kategorija"] = df.apply(kategoriziraj_zaposlenika, axis=1)
Razlika u čitljivosti je ogromna, a debugiranje imenovane funkcije je neusporedivo lakše.
Metoda pipe(): Čisto ulančavanje transformacija
Sad dolazimo do nečeg što dosta ljudi ne koristi dovoljno. Metoda pipe() omogućuje vam ulančavanje prilagođenih funkcija u čist, čitljiv pipeline. Umjesto ugniježđenih poziva funkcija (koji brzo postanu nečitljivi), svaki korak teče od vrha prema dnu:
# Definicija koraka pipeline-a
def ukloni_duplikate(df):
return df.drop_duplicates()
def dodaj_bonus(df, postotak=5):
df = df.copy()
df["bonus"] = df["placa"] * postotak / 100
return df
def oznaci_visoke_place(df, prag=7000):
df = df.copy()
df["visoka_placa"] = df["placa"] >= prag
return df
# Ulančavanje s pipe() — čisto i čitljivo
rezultat = (
df.pipe(ukloni_duplikate)
.pipe(dodaj_bonus, postotak=10)
.pipe(oznaci_visoke_place, prag=6500)
)
print(rezultat[["ime", "placa", "bonus", "visoka_placa"]])
# ime placa bonus visoka_placa
# 0 Ana 5500 550.0 False
# 1 Marko 7200 720.0 True
# 2 Petra 8100 810.0 True
# 3 Ivan 4300 430.0 False
# 4 Lucija 6800 680.0 True
Prednost pipe() posebno dolazi do izražaja u produkcijskim pipeline-ovima gdje imate 5, 10 ili više koraka obrade. Svaki korak je zasebna funkcija koju možete testirati neovisno — a to je ogromna prednost kad nešto pođe po krivu.
Performanse: Kada izbjegavati apply()
Sad ću biti iskren — ovo je dio koji mnogi vodiči preskaču, a po meni je jedan od najvažnijih. Metoda apply() je spora. Interno prolazi kroz Python petlju red po red, što znači da za velike skupove podataka (stotine tisuća ili milijuni redaka) razlika u brzini može biti ogromna. Govorimo o 10 do 100 puta sporijoj izvedbi u usporedbi s vektoriziranim operacijama.
Hijerarhija brzine
Evo smjernice od najbržeg do najsporijeg pristupa:
- Vektorizirane NumPy/pandas operacije — uvijek prvi izbor
np.where()/np.select()— za uvjetnu logiku.strpristupnik — za operacije na stringovimamap()— brže od apply za elementwise operacijeapply()— zadnji izbor, samo kad nema alternative
Praktični primjeri zamjene apply()-a
Pogledajmo konkretne primjere kako zamijeniti spori apply() bržim alternativama:
import numpy as np
# Generiramo veći DataFrame za testiranje
veliki_df = pd.DataFrame({
"vrijednost": np.random.randint(0, 100, size=100_000),
"kategorija": np.random.choice(["A", "B", "C"], size=100_000)
})
# ❌ SPORO — apply s lambda
veliki_df["duplo"] = veliki_df["vrijednost"].apply(lambda x: x * 2)
# ✅ BRZO — vektorizirana operacija
veliki_df["duplo"] = veliki_df["vrijednost"] * 2
# ❌ SPORO — uvjetna logika s apply
veliki_df["oznaka"] = veliki_df["vrijednost"].apply(
lambda x: "visoko" if x > 75 else ("srednje" if x > 25 else "nisko")
)
# ✅ BRZO — np.select za uvjetnu logiku
uvjeti = [
veliki_df["vrijednost"] > 75,
veliki_df["vrijednost"] > 25
]
izbori = ["visoko", "srednje"]
veliki_df["oznaka"] = np.select(uvjeti, izbori, default="nisko")
Mjerenje performansi
Ako me ne vjerujete na riječ (i ne biste trebali — uvijek mjerite!), evo kako sami možete provjeriti:
import time
# Benchmark: apply vs vektorizacija
start = time.time()
veliki_df["rezultat_apply"] = veliki_df["vrijednost"].apply(lambda x: x ** 2 + x * 3)
trajanje_apply = time.time() - start
start = time.time()
veliki_df["rezultat_vektor"] = veliki_df["vrijednost"] ** 2 + veliki_df["vrijednost"] * 3
trajanje_vektor = time.time() - start
print(f"apply(): {trajanje_apply:.4f} sekundi")
print(f"vektorizirano: {trajanje_vektor:.4f} sekundi")
print(f"Ubrzanje: {trajanje_apply / trajanje_vektor:.1f}x")
# Tipičan rezultat: vektorizacija je 20-50x brža
Razlika je drastična. Na 100.000 redaka to je par sekundi, ali na 10 milijuna redaka — to je razlika između "gotovo odmah" i "idemo na kavu dok se ovo izvrti".
Praktični primjer: Kompletna obrada podataka o zaposlenicima
Hajde da sad spojimo sve naučeno u jednom realističnom primjeru. Cilj je pokazati cijeli tijek obrade podataka i kako za svaki korak odabrati pravi alat:
import pandas as pd
import numpy as np
# Sirovi podaci o zaposlenicima
podaci = pd.DataFrame({
"ime": [" Ana Horvat ", "marko KOVAČEVIĆ", "PETRA novak", "ivan BABIĆ", "lucija Jurić"],
"odjel": ["IT", "Marketing", "IT", "Financije", "Marketing"],
"placa_bruto": [8500, 7200, 9100, 6300, 7800],
"datum_zaposlenja": ["2019-03-15", "2021-07-01", "2018-11-20", "2023-01-10", "2020-06-05"],
"ocjena_rada": [4.2, 3.8, 4.7, 3.5, 4.0]
})
# Korak 1: Čišćenje imena — .str pristupnik (vektorizirano)
podaci["ime"] = podaci["ime"].str.strip().str.title()
# Korak 2: Konverzija datuma
podaci["datum_zaposlenja"] = pd.to_datetime(podaci["datum_zaposlenja"])
# Korak 3: Izračun staža u godinama (vektorizirano)
podaci["staz_godine"] = ((pd.Timestamp.now() - podaci["datum_zaposlenja"]).dt.days / 365.25).round(1)
# Korak 4: Neto plaća — vektorizirana operacija s np.where
porezna_stopa = np.where(podaci["placa_bruto"] > 8000, 0.35, 0.30)
podaci["placa_neto"] = (podaci["placa_bruto"] * (1 - porezna_stopa)).round(2)
# Korak 5: Kategorija učinkovitosti — np.select
uvjeti = [
podaci["ocjena_rada"] >= 4.5,
podaci["ocjena_rada"] >= 3.5
]
kategorije = ["izvrsna", "dobra"]
podaci["ucinkovitost"] = np.select(uvjeti, kategorije, default="potrebno poboljšanje")
# Korak 6: Složeni izračun bonusa — apply jer ovisi o više stupaca
# i ima složenu poslovnu logiku
def izracunaj_bonus(red):
bazni_bonus = red["placa_bruto"] * 0.10
if red["ocjena_rada"] >= 4.5:
bazni_bonus *= 1.5
if red["staz_godine"] >= 5:
bazni_bonus *= 1.2
return round(bazni_bonus, 2)
podaci["bonus"] = podaci.apply(izracunaj_bonus, axis=1)
# Korak 7: Mapiranje odjela na kratice
kratice_odjela = {"IT": "IT", "Marketing": "MKT", "Financije": "FIN"}
podaci["odjel_kratica"] = podaci["odjel"].map(kratice_odjela)
print(podaci[["ime", "odjel_kratica", "placa_neto", "staz_godine", "ucinkovitost", "bonus"]])
# ime odjel_kratica placa_neto staz_godine ucinkovitost bonus
# 0 Ana Horvat IT 5525.00 7.0 dobra 1020.00
# 1 Marko Kovačević MKT 5040.00 4.7 dobra 720.00
# 2 Petra Novak IT 5915.00 7.3 izvrsna 1638.00
# 3 Ivan Babić FIN 4410.00 3.2 dobra 630.00
# 4 Lucija Jurić MKT 5460.00 5.8 dobra 936.00
Primijetite obrazac: za svaki korak odabrali smo optimalan pristup. Vektorizirane operacije gdje god je moguće, map() za jednostavno mapiranje, a apply() samo za onu složenu poslovnu logiku bonusa koja ovisi o više stupaca i sadrži ugniježđene uvjete. U praksi, upravo ovakav pristup daje najbolji balans između čitljivosti i performansi.
Brza referenca: Tablica usporedbe metoda
| Metoda | Radi na | Prima | Najbolja za | Brzina |
|---|---|---|---|---|
Series.apply() | Series | Funkciju | Složenu logiku na jednom stupcu | Spora |
DataFrame.apply(axis=0) | DataFrame | Funkciju | Operacije po stupcima | Spora |
DataFrame.apply(axis=1) | DataFrame | Funkciju | Operacije po redovima (više stupaca) | Najsporija |
Series.map() | Series | Funkciju, rječnik, Series | Zamjena/transformacija vrijednosti | Brza |
DataFrame.map() | DataFrame | Funkciju | Elementwise transformacije | Srednja |
pipe() | DataFrame/Series | Funkciju | Ulančavanje koraka pipeline-a | Ovisi o funkciji |
Često postavljana pitanja (FAQ)
Koja je razlika između apply() i map() u pandas-u?
apply() je svestranija — radi i na Series-u i na DataFrame-u, te može primjenjivati funkcije po redovima (axis=1) ili stupcima (axis=0). S druge strane, map() na Series-u je specijalizirana za transformacije element po element i može primati rječnike i Series objekte kao argument, ne samo funkcije. Za jednostavne transformacije jednog stupca, map() je brži izbor.
Je li applymap() zastarjela i što koristiti umjesto nje?
Da, definitivno. applymap() je označena kao zastarjela od pandas verzije 2.1.0 i potpuno uklonjena u pandas 3.0. Umjesto nje koristite DataFrame.map() koja ima identično ponašanje — primjenjuje funkciju na svaki element DataFrame-a.
Zašto je pandas apply() spor i kako ga ubrzati?
apply() je spor jer interno koristi Python petlju umjesto optimiziranih C operacija koje pokreću vektorizirane pandas/NumPy funkcije. Za ubrzavanje: zamijenite aritmetičke operacije vektoriziranim izrazima (npr. df["a"] * 2), uvjetnu logiku s np.where() ili np.select(), a string operacije s .str pristupnikom.
Kada je bolje koristiti apply() umjesto vektoriziranih operacija?
Koristite apply() kada logika zahtijeva pristup više stupaca istovremeno (axis=1), kada pozivate vanjske biblioteke ili API-je, ili kada je poslovna logika presložena za izražavanje vektoriziranim operacijama. Za male skupove podataka (manje od 10.000 redaka), razlika u performansama je zanemariva, pa čitljivost koda može imati prednost.
Kako koristiti pipe() za čišći pandas kod?
pipe() omogućuje ulančavanje prilagođenih funkcija u čitljiv pipeline. Definirajte svaki korak obrade kao zasebnu funkciju koja prima DataFrame i vraća DataFrame, a zatim ih povežite s .pipe(). Svaka funkcija može imati vlastite parametre, lako se testira zasebno, i čitav pipeline teče od vrha prema dnu.