Pandas vremenske serije: Vodič za rad s vremenskim podacima u Pythonu

Naučite raditi s vremenskim serijama u pandasu — od DatetimeIndex-a i resample operacija, preko rolling i expanding prozora, do detekcije anomalija. Vodič ažuriran za pandas 3.0 s primjerima koda.

Pandas Vremenske Serije 2026: Praktični Vodič

Ako ste pratili našu seriju o pandas biblioteci — od čišćenja podataka, preko GroupBy operacija, spajanja DataFrame-ova, preoblikovanja s pivot_table i melt, do primjene funkcija s apply i lambda — onda već imate solidan arsenal alata za podatke. Ali postoji jedna velika kategorija koju još nismo dotaknuli, a na koju ćete naletjeti gotovo svaki dan: vremenski podaci.

Cijene dionica, temperatura zraka, promet na webu, dnevna prodaja — svi ti podatci dijele jednu stvar: vezani su uz vremensku os. I baš za takve podatke pandas ima nevjerojatno moćan set alata. U ovom vodiču proći ćemo sve što vam treba za učinkovit rad s vremenskim serijama, uključujući promjene u pandas 3.0.

Hajdemo redom.

Što su vremenske serije i zašto su važne?

Vremenska serija (engl. time series) je niz podataka prikupljenih u pravilnim ili nepravilnim vremenskim intervalima. Za razliku od „običnih" tablica, ovdje redoslijed redaka zaista ima značenje — svaki red je mjerenje u nekom konkretnom trenutku.

Rad s vremenskim serijama omogućuje vam:

  • Otkrivanje trendova — rastu li ili padaju vrijednosti kroz vrijeme?
  • Identificiranje sezonskih obrazaca — postoje li ciklički obrasci koji se ponavljaju?
  • Izračun pomičnih prosjeka — kakav je glatki trend bez šuma?
  • Prognozu budućih vrijednosti na temelju prošlih podataka
  • Usporedbu podataka na različitim vremenskim razinama (dnevno, tjedno, mjesečno)

Iskreno, kad jednom savladate ove koncepte, pogledat ćete na podatke potpuno drugačije.

Priprema okruženja i primjer podataka

Za ovaj vodič koristit ćemo pandas 3.0 (najnovija stabilna verzija, objavljena u siječnju 2026.) i NumPy za generiranje primjera. Ako još niste nadogradili:

pip install pandas>=3.0 numpy

Kreirajmo simulirani dataset dnevnih prodaja za cijelu 2025. godinu:

import pandas as pd
import numpy as np

np.random.seed(42)
datumi = pd.date_range(start="2025-01-01", end="2025-12-31", freq="D")
prodaja = 200 + np.cumsum(np.random.randn(len(datumi)) * 10)

df = pd.DataFrame({
    "datum": datumi,
    "prodaja": np.round(prodaja, 2)
})

print(df.head())
#        datum  prodaja
# 0 2025-01-01   204.97
# 1 2025-01-01   203.58
# 2 2025-01-01   210.05
# 3 2025-01-01   225.23
# 4 2025-01-01   222.89

Ništa spektakularno — ali savršeno za demonstraciju svih tehnika koje slijede.

Timestamp i DatetimeIndex — temelji vremenskih serija

U pandasu, svaki pojedinačni trenutak u vremenu predstavljen je objektom Timestamp. Kad imate stupac ili indeks sastavljen od takvih objekata, pandas automatski stvara DatetimeIndex — a to je, da tako kažem, ključ koji otvara vrata svim naprednim operacijama.

Pretvaranje stringova u datume s to_datetime()

Podatci iz CSV-ova i baza često dolaze kao obični stringovi. Funkcija pd.to_datetime() pretvara ih u pandas Timestamp objekte:

# Automatsko prepoznavanje formata
df["datum"] = pd.to_datetime(df["datum"])

# Eksplicitno definiran format (brže za velike skupove)
df["datum"] = pd.to_datetime(df["datum"], format="%Y-%m-%d")

# Rukovanje neispravnim datumima
df["datum"] = pd.to_datetime(df["datum"], errors="coerce")  # NaT za neispravne

Novost u pandas 3.0: Zadana rezolucija za datetime podatke više nije nanosekunda, već mikrosekunda (ili rezolucija ulaznih podataka). U praksi to znači da sad možete raditi s datumima prije 1678. i nakon 2262. godine bez grešaka — što je super za povijesne i astronomske podatke, ali i za svakodnevne scenarije eliminira jednu dosadnu klasu bugova.

Postavljanje DatetimeIndex-a

Da biste iskoristili sve mogućnosti vremenskih serija, postavite stupac s datumima kao indeks:

df = df.set_index("datum")
print(type(df.index))
# <class 'pandas.core.indexes.datetimes.DatetimeIndex'>

print(df.index.dtype)
# datetime64[us]  # mikrosekunde u pandas 3.0

Ovaj korak je jednostavan, ali ključan. Bez njega vam resample i slične metode neće raditi.

Indeksiranje i filtriranje po vremenu

Kad jednom imate DatetimeIndex, pandas omogućuje nevjerojatno intuitivno filtriranje pomoću stringova:

# Svi podatci za ožujak 2025.
ozujak = df.loc["2025-03"]

# Podatci za prvi kvartal
q1 = df.loc["2025-01":"2025-03"]

# Specifičan datum
dan = df.loc["2025-06-15"]

# Podatci od lipnja nadalje
od_lipnja = df.loc["2025-06":]

Ovo radi jer pandas automatski parsira stringove i uspoređuje ih s DatetimeIndex-om. Ne trebate ručno stvarati Timestamp objekte za filtriranje — pandas to odradi umjesto vas.

Korisna .dt svojstva

Ako datum nije indeks nego obični stupac, koristite .dt accessor za izvlačenje komponenti:

df_reset = df.reset_index()

df_reset["godina"] = df_reset["datum"].dt.year
df_reset["mjesec"] = df_reset["datum"].dt.month
df_reset["dan_u_tjednu"] = df_reset["datum"].dt.day_name()
df_reset["kvartal"] = df_reset["datum"].dt.quarter
df_reset["je_vikend"] = df_reset["datum"].dt.dayofweek >= 5

Meni osobno .dt accessor je jedan od onih pandas feature-ova koje koristim konstantno, a često ih se podcjenjuje.

Resample — promjena frekvencije vremenskih podataka

E sad dolazimo do pravog mesa. Metoda resample() jedno je od najmoćnijih oružja za vremenske serije. Najlakše je zamisliti kao GroupBy za vremenske podatke — grupira podatke u vremenske „kante" (dnevne, tjedne, mjesečne...) i primjenjuje agregacijsku funkciju.

Downsampling — smanjenje frekvencije

Recimo da imate dnevne podatke, ali trebate mjesečni pregled:

# Mjesečna ukupna prodaja
mjesecna_prodaja = df["prodaja"].resample("MS").sum()
print(mjesecna_prodaja.head())

# Tjedni prosjek prodaje
tjedni_prosjek = df["prodaja"].resample("W").mean()

# Kvartalna maksimalna prodaja
kvartalni_max = df["prodaja"].resample("QS").max()

Najčešće korišteni frekvencijski stringovi:

  • "D" — dnevno
  • "W" — tjedno (završava nedjeljom)
  • "MS" — mjesečno (početak mjeseca)
  • "ME" — mjesečno (kraj mjeseca)
  • "QS" — kvartalno (početak kvartala)
  • "YS" — godišnje (početak godine)
  • "h" — po satu

Važna napomena: U pandas 3.0, zastarjele kratice poput "M" (za kraj mjeseca) zamijenjene su s "ME", a "Y" s "YE". Ako koristite stare kratice, dobit ćete grešku, ne samo upozorenje kao u pandasu 2.x.

Upsampling — povećanje frekvencije

Ponekad trebate podatke na višoj frekvenciji. Primjerice, imate mjesečne podatke, a trebaju vam dnevni:

mjesecni = df["prodaja"].resample("MS").mean()

# Upsampling na dnevnu frekvenciju — popunjavanje prema naprijed
dnevni_ffill = mjesecni.resample("D").ffill()

# Linearna interpolacija
dnevni_interp = mjesecni.resample("D").interpolate(method="linear")

# Bez popunjavanja — NaN gdje nema podataka
dnevni_nan = mjesecni.resample("D").asfreq()

Oprez: Upsampling s krivom metodom interpolacije može unijeti pristranost u podatke. Uvijek se zapitajte ima li popunjavanje smisla za vaš konkretan slučaj — nekad je bolje ostaviti NaN nego izmišljati vrijednosti.

Više agregacija odjednom

mjesecni_pregled = df["prodaja"].resample("MS").agg(["mean", "std", "min", "max", "count"])
print(mjesecni_pregled.head())

Kratko i čisto. Jedna linija koda, a dobijete kompletnu mjesečnu statistiku.

Rolling — pomični prozor za izglađivanje podataka

Dok resample() mijenja frekvenciju podataka, rolling() klizi fiksni „prozor" kroz njih i računa statistiku unutar tog prozora. Ovo je ključna tehnika za uklanjanje šuma i otkrivanje trendova.

Osnovna uporaba

# Pomični prosjek za 7 dana
df["ma_7"] = df["prodaja"].rolling(window=7).mean()

# Pomični prosjek za 30 dana
df["ma_30"] = df["prodaja"].rolling(window=30).mean()

# Pomična standardna devijacija (mjera volatilnosti)
df["std_7"] = df["prodaja"].rolling(window=7).std()

print(df.head(10))

Zašto su prve vrijednosti NaN? Jer prozor od 7 elemenata ne može izračunati prosjek dok nema barem 7 podataka. To je potpuno normalno ponašanje, ne greška — ne trebate se brinuti.

Ključni parametri rolling()

# min_periods — minimalan broj podataka za izračun
df["ma_7_flex"] = df["prodaja"].rolling(window=7, min_periods=3).mean()

# center — centriraj prozor oko trenutne točke
df["ma_7_cent"] = df["prodaja"].rolling(window=7, center=True).mean()

# Vremenski definirani prozor (umjesto fiksnog broja redaka)
df["ma_7d"] = df["prodaja"].rolling(window="7D").mean()

Parametar min_periods je posebno koristan kad imate praznine u podacima. Stavite ga na manji broj od veličine prozora i dobit ćete rezultate čak i kad prozor nije potpuno popunjen. U praksi ga koristim gotovo uvijek.

Prilagođene funkcije s rolling().apply()

Kad vam ugrađene metode nisu dovoljne, tu je apply() s vlastitom funkcijom:

# Koeficijent varijacije u pomičnom prozoru
def koef_varijacije(prozor):
    return prozor.std() / prozor.mean() * 100

df["cv_14"] = df["prodaja"].rolling(window=14).apply(koef_varijacije, raw=True)

# Raspon (max - min) u prozoru
df["raspon_7"] = df["prodaja"].rolling(window=7).apply(
    lambda x: x.max() - x.min(), raw=True
)

Savjet: parametar raw=True prosljeđuje NumPy niz umjesto pandas Series, što dosta ubrzava izvršavanje. Za velike datasetove razlika može biti značajna.

Expanding — kumulativni prozor

Za razliku od rolling() koji koristi fiksni prozor, expanding() koristi sve podatke od početka do trenutne točke. Jednostavno rečeno — prozor koji neprestano raste.

# Kumulativni prosjek
df["kum_prosjek"] = df["prodaja"].expanding().mean()

# Kumulativni maksimum
df["kum_max"] = df["prodaja"].expanding().max()

# Kumulativna standardna devijacija
df["kum_std"] = df["prodaja"].expanding().std()

Ovo je korisno kad vas zanima kako se neka metrika razvija kroz vrijeme od samog početka praćenja.

Shift i diff — pomak i razlike

Dvije jednostavne, ali izuzetno korisne metode za analizu vremenskih serija. Koriste se stalno, a često ih se zanemari u tutorijalima.

shift() — pomak podataka

# Prodaja prethodnog dana
df["prodaja_jucer"] = df["prodaja"].shift(1)

# Prodaja istog dana prošlog tjedna
df["prodaja_prosli_tjedan"] = df["prodaja"].shift(7)

# Postotna promjena u odnosu na prethodni dan
df["dnevna_promjena_pct"] = (df["prodaja"] / df["prodaja"].shift(1) - 1) * 100

diff() — razlika između uzastopnih vrijednosti

# Apsolutna dnevna promjena
df["dnevna_promjena"] = df["prodaja"].diff()

# Tjedna promjena
df["tjedna_promjena"] = df["prodaja"].diff(7)

# Isto kao: df["prodaja"] - df["prodaja"].shift(7)

Metoda diff() je važnija nego što se čini na prvi pogled. Pomaže ukloniti trend i učiniti podatke stacionarnima — a to je preduvjet za mnoge statističke modele (ARIMA, na primjer).

EWM — eksponencijalno ponderirani pomični prosjek

Obični rolling prosjek daje svim vrijednostima u prozoru jednaku težinu. ewm() je pametniji — daje veću težinu novijim vrijednostima, pa je osjetljiviji na nedavne promjene.

# EWM s span parametrom (ekvivalent rolling prozoru)
df["ema_12"] = df["prodaja"].ewm(span=12).mean()
df["ema_26"] = df["prodaja"].ewm(span=26).mean()

# Usporedba s običnim rolling prosjekom
df["sma_12"] = df["prodaja"].rolling(window=12).mean()

Ako ste ikad radili s financijskim podacima, EWM vam je vjerojatno poznat. MACD indikator, recimo, koristi upravo razliku dvaju eksponencijalno ponderiranih prosjeka. Ali i izvan financija, EWM je odličan izbor kad želite brže reagirati na promjene u podacima.

Praktični primjer: Analiza prodaje od početka do kraja

Dosta teorije — spojimo sve naučeno u jedan realistični primjer. Ovo je otprilike kako bi izgledao tipični workflow analize vremenskih serija:

import pandas as pd
import numpy as np

# 1. Učitavanje i priprema podataka
np.random.seed(42)
datumi = pd.date_range("2024-01-01", "2025-12-31", freq="D")
sezonska = 50 * np.sin(2 * np.pi * np.arange(len(datumi)) / 365)
trend = np.linspace(100, 300, len(datumi))
sum_noise = np.random.randn(len(datumi)) * 20
prodaja = trend + sezonska + sum_noise

df = pd.DataFrame({"prodaja": prodaja}, index=datumi)

# 2. Mjesečna agregacija
mjesecno = df.resample("MS").agg(
    prosjecna_prodaja=("prodaja", "mean"),
    ukupna_prodaja=("prodaja", "sum"),
    volatilnost=("prodaja", "std")
)

# 3. Pomični prosjeci za otkrivanje trenda
df["trend_30d"] = df["prodaja"].rolling(30).mean()
df["trend_90d"] = df["prodaja"].rolling(90).mean()

# 4. Detekcija anomalija pomoću rolling statistika
rolling_mean = df["prodaja"].rolling(30).mean()
rolling_std = df["prodaja"].rolling(30).std()
df["gornja_granica"] = rolling_mean + 2 * rolling_std
df["donja_granica"] = rolling_mean - 2 * rolling_std
df["anomalija"] = (
    (df["prodaja"] > df["gornja_granica"]) |
    (df["prodaja"] < df["donja_granica"])
)

# 5. Usporedba s prošlom godinom (YoY)
df["prodaja_prosle_god"] = df["prodaja"].shift(365)
df["yoy_promjena_pct"] = (
    (df["prodaja"] - df["prodaja_prosle_god"]) /
    df["prodaja_prosle_god"] * 100
)

print(f"Broj otkrivenih anomalija: {df['anomalija'].sum()}")
print(f"\nMjesečni pregled:\n{mjesecno.head()}")
print(f"\nProsječna YoY promjena: {df['yoy_promjena_pct'].mean():.1f}%")

Ovaj primjer pokriva stvarno velik dio onoga što ćete raditi u praksi: agregaciju, trendove, detekciju anomalija i year-over-year usporedbe. Naravno, u stvarnom projektu biste dodali vizualizacije (matplotlib ili plotly), ali logika obrade podataka ostaje ista.

Česte pogreške i kako ih izbjeći

Evo najčešćih zamki na koje sam i sam naletio (i vidio druge kako se u njih hvataju):

1. Zaboravljeno postavljanje DatetimeIndex-a

# KRIVO — resample ne radi bez DatetimeIndex-a
df = pd.read_csv("podaci.csv")
df["prodaja"].resample("MS").mean()  # TypeError!

# ISPRAVNO
df = pd.read_csv("podaci.csv", parse_dates=["datum"], index_col="datum")
df["prodaja"].resample("MS").mean()  # Radi!

Ovo je daleko najčešća greška kod početnika. Poruka greške nije uvijek intuitivna, pa vrijedi zapamtiti: resample() zahtijeva DatetimeIndex.

2. Zaboravljena agregacijska funkcija nakon resample()

# KRIVO — vraća Resampler objekt, ne podatke
rezultat = df["prodaja"].resample("MS")
print(rezultat)  # DatetimeIndexResampler...

# ISPRAVNO
rezultat = df["prodaja"].resample("MS").mean()
print(rezultat)  # Stvarni agregirani podaci

Lako za previdjeti. Resample bez agregacije ne daje grešku — samo vam vrati objekt koji nije ono što ste očekivali.

3. Zastarjele frekvencijske kratice u pandas 3.0

# ZASTARJELO (FutureWarning u pandas 2.x, greška u 3.0)
df.resample("M").mean()   # "M" je sada "ME"
df.resample("Y").mean()   # "Y" je sada "YE"
df.resample("H").mean()   # "H" je sada "h"

# ISPRAVNO za pandas 3.0
df.resample("ME").mean()
df.resample("YE").mean()
df.resample("h").mean()

Ako nadograđujete s pandasom 2.x, ovo će vas vjerojatno ugristi. Napravite brzi find-and-replace po projektu i uštedite si glavobolje.

4. Miješanje .loc i .iloc za vremenske indekse

# .loc koristi labele (datume)
df.loc["2025-03"]  # Svi podaci za ožujak

# .iloc koristi pozicije (brojeve)
df.iloc[0:30]  # Prvih 30 redaka

Pravilo je zapravo jednostavno: .loc za datume, .iloc za brojeve. Ali u žurbi se lako zabunite.

Često postavljana pitanja

Koja je razlika između resample() i rolling() u pandasu?

resample() mijenja frekvenciju podataka — pretvara, recimo, dnevne podatke u mjesečne grupirajući ih u vremenske intervale. rolling() ne mijenja frekvenciju, već klizi fiksni prozor kroz podatke i računa statistiku unutar njega. Ukratko: resample() kad trebate drugu granularnost, rolling() za izglađivanje i trendove.

Zašto rolling() vraća NaN za prve vrijednosti?

To je normalno ponašanje. Ako imate pomični prozor veličine 7, prvih 6 vrijednosti jednostavno nema dovoljno prethodnih podataka za izračun. Rješenje: koristite parametar min_periods — npr. rolling(window=7, min_periods=1).mean() će izračunati prosjek čim postoji barem jedan podatak.

Kako pretvoriti string stupac u datetime u pandasu 3.0?

Koristite pd.to_datetime(). U pandas 3.0 zadana rezolucija je mikrosekunda umjesto nanosekunde, što proširuje raspon podržanih datuma. Za neispravne vrijednosti dodajte errors="coerce" — problematični zapisi postaju NaT (Not a Time) umjesto da bacaju grešku. A za bolje performanse na velikim datasetovima, eksplicitno navedite format s format parametrom.

Što je Copy-on-Write u pandas 3.0 i kako utječe na vremenske serije?

Copy-on-Write (CoW) novo je zadano ponašanje u pandas 3.0 koje osigurava da svaka operacija indeksiranja vraća kopiju, ne pogled na originalne podatke. Za vremenske serije to je zapravo odlična vijest — eliminira čitavu klasu bugova vezanih uz neočekivane mutacije, posebno kod obrazaca poput df.loc['2025-03']['prodaja'] = 0 koji su prije davali nepredvidive rezultate. Više se ne trebate pitati „jesam li upravo promijenio originalni DataFrame ili ne?".

Kako odabrati pravu veličinu rolling prozora?

Ovisi o podacima i cilju. Za dnevne podatke, 7 dana hvata tjedne cikluse, 30 dana mjesečne trendove, a 90 ili 365 dana dugoročne. Manji prozor je osjetljiviji na promjene, veći daje glađi trend. Moj savjet: isprobajte nekoliko veličina i usporedite vizualno. To je gotovo uvijek bolji pristup od pokušaja da matematički izračunate „savršenu" veličinu.

O Autoru Editorial Team

Our team of expert writers and editors.