Analiza Seriilor Temporale cu Pandas: Ghid Complet resample, rolling și DatetimeIndex

Ghid practic pentru analiza seriilor temporale în Python cu Pandas 3.0. Învață DatetimeIndex, resample(), rolling(), ewm(), shift() și decompoziție sezonieră cu exemple funcționale de cod.

Dacă lucrați cu date care au o componentă temporală — vânzări zilnice, temperaturi orare, trafic web pe minute sau prețuri de acțiuni — atunci aveți de-a face cu serii temporale (time series). Și sincer, în ecosistemul Python, Pandas rămâne instrumentul de referință pentru manipularea și analiza lor.

Am petrecut destul de mult timp lucrând cu serii temporale în diverse proiecte, de la analize financiare la monitorizarea senzorilor IoT. Ce am învățat e că, odată ce înțelegi câteva concepte de bază, totul devine mult mai intuitiv.

Hai să parcurgem împreună tot ce trebuie să știți despre lucrul cu serii temporale în Pandas 3.0: de la crearea și manipularea unui DatetimeIndex, la resample(), rolling(), shift(), ewm() și decompoziție sezonieră. Dacă ați citit deja ghidul despre GroupBy și tabele pivot sau cel despre combinarea DataFrame-urilor, acest articol e pasul următor firesc — odată ce aveți datele agregate, trebuie să le analizați în context temporal.

Ce Sunt Seriile Temporale și De Ce Contează

O serie temporală este, pe scurt, o secvență de observații ordonate cronologic. Ceea ce o diferențiază de un set de date obișnuit e că ordinea contează — nu puteți amesteca rândurile fără a pierde informație critică.

Fiecare punct de date are un timestamp asociat, iar relația dintre puncte consecutive e esențială pentru analiză.

Seriile temporale apar peste tot: în finanțe (prețuri de acțiuni, cursuri valutare), în meteorologie (temperaturi, precipitații), în e-commerce (vânzări, trafic), în IoT (date de la senzori) și în multe alte domenii. Înțelegerea lor vă permite să:

  • Identificați tendințe (trend) — direcția generală a datelor
  • Detectați sezonalitate — pattern-uri care se repetă la intervale regulate
  • Observați anomalii — puncte care deviază semnificativ de la pattern
  • Faceți previziuni (forecasting) — estimarea valorilor viitoare

Configurare: Pandas 3.0 și Setul de Date

Vom lucra cu Pandas 3.0, care aduce câteva modificări importante pentru seriile temporale. Cea mai relevantă: rezoluția implicită pentru datetime a trecut de la nanosecunde la microsecunde. Asta elimină erorile de overflow pentru date înainte de 1678 sau după 2262 (da, chiar era o problemă reală). De asemenea, Copy-on-Write e acum comportamentul implicit.

pip install pandas>=3.0 matplotlib
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

print(f"Pandas versiune: {pd.__version__}")

# Generăm un set de date simulat: vânzări zilnice pe 2 ani
np.random.seed(42)
date_range = pd.date_range("2024-01-01", "2025-12-31", freq="D")

# Trend crescător + sezonalitate + zgomot
n = len(date_range)
trend = np.linspace(100, 200, n)
sezonalitate = 30 * np.sin(2 * np.pi * np.arange(n) / 365.25)
zgomot = np.random.normal(0, 10, n)

df = pd.DataFrame({
    "Data": date_range,
    "Vanzari": trend + sezonalitate + zgomot,
    "Categorie": np.random.choice(["Electronice", "Imbracaminte", "Alimente"], n),
    "Regiune": np.random.choice(["Nord", "Sud", "Est", "Vest"], n)
})

# Adăugăm câteva valori lipsă pentru realism
df.loc[np.random.choice(n, 20, replace=False), "Vanzari"] = np.nan

print(df.head(10))
print(f"\nDimensiune: {df.shape}")
print(f"Valori lipsă: {df['Vanzari'].isna().sum()}")

DatetimeIndex: Fundația Analizei Temporale

Primul pas esențial e să transformați coloana de date într-un DatetimeIndex. Fără acest pas, funcțiile resample() și rolling() bazat pe timp pur și simplu nu funcționează. Am văzut mulți începători care săreau peste acest pas și apoi se întrebau de ce primesc erori.

Conversia și Setarea Indexului

# Metoda 1: Setăm coloana existentă ca index
df = df.set_index("Data")
print(type(df.index))  # DatetimeIndex

# Metoda 2: La citirea din CSV (varianta recomandată)
# df = pd.read_csv("vanzari.csv", parse_dates=["Data"], index_col="Data")

# Verificăm tipul indexului
print(f"Tip index: {type(df.index)}")
print(f"Frecvență: {df.index.freq}")
print(f"Prima dată: {df.index.min()}")
print(f"Ultima dată: {df.index.max()}")

Accesarea Componentelor Temporale cu .dt și Indexare Directă

Odată ce aveți un DatetimeIndex, puteți accesa componentele individuale direct din index. Și partea cea mai plăcută — puteți filtra datele extrem de intuitiv:

# Accesare componente temporale
print(df.index.year.unique())    # Anii prezenți
print(df.index.month.unique())   # Lunile
print(df.index.dayofweek)        # Ziua săptămânii (0=Luni)
print(df.index.quarter)          # Trimestrul

# Filtrare intuitivă cu string-uri
vanzari_2025 = df.loc["2025"]                     # Tot anul 2025
vanzari_q1 = df.loc["2025-01":"2025-03"]          # T1 2025
vanzari_jan = df.loc["2025-01"]                    # Doar ianuarie 2025
o_singura_zi = df.loc["2025-06-15"]               # O singură zi

print(f"Vânzări 2025: {len(vanzari_2025)} rânduri")
print(f"Vânzări T1 2025: {len(vanzari_q1)} rânduri")

Această indexare cu string-uri (numită partial string indexing) e una dintre cele mai conveniente funcționalități ale Pandas pentru serii temporale. Nu trebuie să scrieți condiții complicate — Pandas interpretează automat string-ul ca interval temporal. Sincer, e genul de feature care te face să apreciezi cu adevărat librăria.

Generarea de Intervale cu date_range() și Frecvențe

Funcția pd.date_range() e esențială pentru a crea intervale regulate de date:

# Frecvențe comune
zilnic = pd.date_range("2025-01-01", periods=30, freq="D")
saptamanal = pd.date_range("2025-01-01", periods=12, freq="W")
lunar_inceput = pd.date_range("2025-01-01", periods=12, freq="MS")  # Month Start
lunar_sfarsit = pd.date_range("2025-01-01", periods=12, freq="ME")  # Month End
trimestrial = pd.date_range("2025-01-01", periods=4, freq="QS")     # Quarter Start
ore_lucru = pd.date_range("2025-01-01", periods=48, freq="bh")      # Business Hours

print("Lunar (început lună):", lunar_inceput[:5])
print("Trimestrial:", trimestrial)

Iată un tabel cu cele mai folosite coduri de frecvență în Pandas 3.0:

  • D — Zilnic (Calendar day)
  • B — Zile lucrătoare (Business day)
  • W — Săptămânal
  • MS / ME — Lunar (început / sfârșit lună)
  • QS / QE — Trimestrial (început / sfârșit trimestru)
  • YS / YE — Anual (început / sfârșit an)
  • h — Orar
  • min — Minutar
  • s — Secundar

Resample: Schimbarea Frecvenței Datelor

Metoda resample() e echivalentul groupby() pentru dimensiunea temporală. Gândiți-vă la ea ca la un mecanism care creează „găleți" (buckets) de timp și aplică funcții de agregare pe fiecare. Dacă ați folosit deja groupby() (din ghidul anterior), veți simți imediat familiaritatea.

Downsampling: De la Frecvență Înaltă la Joasă

Downsampling-ul reduce frecvența datelor — de exemplu, din date zilnice în date lunare. Fiecare „găleată" lunară conține toate zilele acelei luni, iar funcția de agregare le rezumă:

# Din date zilnice în medii lunare
lunar = df["Vanzari"].resample("MS").mean()
print(lunar.head(12))

# Mai multe agregări simultan
lunar_detaliat = df["Vanzari"].resample("MS").agg(["mean", "sum", "std", "count"])
print(lunar_detaliat.head(6))

# Agregări săptămânale
saptamanal = df["Vanzari"].resample("W").agg(
    Media="mean",
    Total="sum",
    Minim="min",
    Maxim="max"
)
print(saptamanal.head(8))

Upsampling: De la Frecvență Joasă la Înaltă

Upsampling-ul crește frecvența — de exemplu, din date lunare în date zilnice. Problema evidentă: cum completăm zilele intermediare? Din fericire, Pandas oferă mai multe strategii:

# Creăm date lunare
lunar_simplu = df["Vanzari"].resample("MS").mean()

# Upsampling la frecvență zilnică
zilnic_ffill = lunar_simplu.resample("D").ffill()    # Forward fill
zilnic_bfill = lunar_simplu.resample("D").bfill()    # Backward fill
zilnic_interp = lunar_simplu.resample("D").interpolate("linear")  # Interpolare liniară

# Comparare vizuală
fig, ax = plt.subplots(figsize=(12, 5))
lunar_simplu.plot(ax=ax, marker="o", label="Lunar (original)", linewidth=2)
zilnic_interp.plot(ax=ax, alpha=0.7, label="Zilnic (interpolare)")
zilnic_ffill.plot(ax=ax, alpha=0.5, label="Zilnic (forward fill)")
ax.set_title("Upsampling: Lunar la Zilnic")
ax.legend()
plt.tight_layout()
plt.show()

Un sfat din experiență: upsampling-ul prin interpolare poate introduce un bias dacă datele nu sunt liniare între puncte. Folosiți-l cu precauție și verificați dacă metoda de interpolare reflectă realitatea datelor voastre. Am făcut greșeala asta într-un proiect cu date de senzori și m-a costat câteva ore de debugging.

Resample cu Grupare: resample() + groupby()

Puteți combina resample() cu groupby() pentru a analiza serii temporale pe mai multe dimensiuni simultan. E extrem de util în practică:

# Medii lunare pe categorie
lunar_categorie = df.groupby("Categorie")["Vanzari"].resample("MS").mean()
print(lunar_categorie.head(12))

# Alternativa cu pd.Grouper (mai flexibilă)
rezultat = df.groupby([pd.Grouper(freq="MS"), "Regiune"])["Vanzari"].sum()
rezultat = rezultat.unstack("Regiune").fillna(0)
print(rezultat.head(6))

# Vizualizare
rezultat.plot(figsize=(12, 5), title="Vânzări Lunare pe Regiune")
plt.ylabel("Total Vânzări")
plt.tight_layout()
plt.show()

Rolling Windows: Media Mobilă și Statistici Glisante

Dacă resample() schimbă frecvența datelor, rolling() calculează statistici pe o fereastră glisantă de dimensiune fixă, păstrând frecvența originală. E tehnica fundamentală pentru netezirea (smoothing) seriilor temporale și identificarea tendințelor.

Rolling cu Fereastră Fixă (Număr de Observații)

# Media mobilă pe 7 zile
df["MA_7"] = df["Vanzari"].rolling(window=7).mean()

# Media mobilă pe 30 zile
df["MA_30"] = df["Vanzari"].rolling(window=30).mean()

# Media mobilă pe 90 zile (trimestrială)
df["MA_90"] = df["Vanzari"].rolling(window=90).mean()

# Vizualizare
fig, ax = plt.subplots(figsize=(14, 6))
df["Vanzari"].plot(ax=ax, alpha=0.3, label="Zilnic (brut)")
df["MA_7"].plot(ax=ax, label="MA 7 zile")
df["MA_30"].plot(ax=ax, label="MA 30 zile", linewidth=2)
df["MA_90"].plot(ax=ax, label="MA 90 zile", linewidth=2)
ax.set_title("Medii Mobile: Netezirea Seriei Temporale")
ax.set_ylabel("Vânzări")
ax.legend()
plt.tight_layout()
plt.show()

Observați cum o fereastră mai mare produce o curbă mai netedă. MA pe 7 zile păstrează variațiile săptămânale, MA pe 30 le elimină dar păstrează tendințele lunare, iar MA pe 90 de zile arată doar trendul general. E un compromis clasic între detaliu și claritate.

Rolling cu Fereastră Temporală (Offset)

În loc de un număr fix de observații, puteți specifica o durată temporală. Asta e util mai ales când datele nu sunt echidistante:

# Fereastră de 14 zile (nu 14 observații!)
ma_14d = df["Vanzari"].rolling(window="14D").mean()

# Diferența: dacă lipsesc zile, fereastra pe offset
# include mai puține puncte, nu se extinde
print(ma_14d.head(20))

Parametri Importanți pentru rolling()

# min_periods: câte observații minime trebuie să existe în fereastră
# (implicit = window size pentru ferestre fixe, 1 pentru offset)
df["MA_7_min3"] = df["Vanzari"].rolling(window=7, min_periods=3).mean()

# center: eticheta la centrul ferestrei, nu la margine
df["MA_7_centrat"] = df["Vanzari"].rolling(window=7, center=True).mean()

# Comparare: centrat vs. implicit (margine dreaptă)
fig, ax = plt.subplots(figsize=(12, 4))
df["Vanzari"].iloc[:60].plot(ax=ax, alpha=0.4, label="Brut")
df["MA_7"].iloc[:60].plot(ax=ax, label="MA 7 (margine)")
df["MA_7_centrat"].iloc[:60].plot(ax=ax, label="MA 7 (centrat)")
ax.legend()
ax.set_title("Rolling centrat vs. la margine")
plt.tight_layout()
plt.show()

Când center=True, media mobilă e aliniată la centrul ferestrei, nu la marginea dreaptă. Folosiți varianta centrată pentru analiză exploratorie (arată mai bine pe grafice), dar varianta implicită (margine dreaptă) pentru previziuni — altfel introduceți informație din viitor, ceea ce nu e deloc ideal.

Alte Statistici Rolling

# Deviație standard mobilă (măsoară volatilitatea)
df["STD_30"] = df["Vanzari"].rolling(window=30).std()

# Minimul și maximul pe fereastră
df["MIN_30"] = df["Vanzari"].rolling(window=30).min()
df["MAX_30"] = df["Vanzari"].rolling(window=30).max()

# Banda de încredere (medie +/- 2 * std)
df["Banda_Sup"] = df["MA_30"] + 2 * df["STD_30"]
df["Banda_Inf"] = df["MA_30"] - 2 * df["STD_30"]

# Vizualizare cu benzi de încredere
fig, ax = plt.subplots(figsize=(14, 6))
df["Vanzari"].plot(ax=ax, alpha=0.2, label="Brut")
df["MA_30"].plot(ax=ax, label="MA 30 zile", color="blue")
ax.fill_between(df.index, df["Banda_Inf"], df["Banda_Sup"],
                alpha=0.15, color="blue", label="banda 2 sigma")
ax.set_title("Banda de Incredere pe Fereastra Mobila de 30 Zile")
ax.legend()
plt.tight_layout()
plt.show()

Expanding Windows: Statistici Cumulative

Spre deosebire de rolling() care are o fereastră fixă, expanding() ia în considerare toate datele de la începutul seriei până la punctul curent. E ca și cum fereastra crește continuu. Util pentru a calcula statistici cumulative:

# Media cumulativă (running average)
df["Media_Cumulativa"] = df["Vanzari"].expanding().mean()

# Maximul cumulativ (running max)
df["Max_Cumulativ"] = df["Vanzari"].expanding().max()

# Sumă cumulativă
df["Suma_Cumulativa"] = df["Vanzari"].expanding().sum()

# Vizualizare
fig, axes = plt.subplots(2, 1, figsize=(14, 8), sharex=True)

df["Vanzari"].plot(ax=axes[0], alpha=0.3, label="Zilnic")
df["Media_Cumulativa"].plot(ax=axes[0], label="Medie Cumulativa", linewidth=2)
axes[0].set_title("Medie Cumulativa vs. Date Brute")
axes[0].legend()

df["Max_Cumulativ"].plot(ax=axes[1], label="Maxim Cumulativ", color="red")
axes[1].set_title("Maximul Cumulativ in Timp")
axes[1].legend()

plt.tight_layout()
plt.show()

EWM: Media Mobilă Exponențială

Media mobilă simplă (SMA) dă aceeași greutate tuturor punctelor din fereastră. Media mobilă exponențială (EMA / EWMA) dă mai multă greutate observațiilor recente, reacționând mai rapid la schimbări. E tehnica preferată în finanțe și analiza senzorilor — și, din experiența mea, face o diferență vizibilă în calitatea analizei.

# EWM cu span (echivalentul unei ferestre de N zile)
df["EMA_7"] = df["Vanzari"].ewm(span=7).mean()
df["EMA_30"] = df["Vanzari"].ewm(span=30).mean()

# Comparare SMA vs EMA
fig, ax = plt.subplots(figsize=(14, 6))
df["Vanzari"].plot(ax=ax, alpha=0.2, label="Brut")
df["MA_30"].plot(ax=ax, label="SMA 30 zile", linewidth=1.5)
df["EMA_30"].plot(ax=ax, label="EMA span=30", linewidth=1.5)
ax.set_title("SMA vs. EMA — Observati Diferenta de Reactivitate")
ax.legend()
plt.tight_layout()
plt.show()

Parametrii EWM: span, alpha, halflife

Există trei moduri echivalente de a specifica rata de declin (decay rate). Alegerea depinde mai mult de preferința personală:

# span: N-day moving average (cel mai intuitiv)
ema_span = df["Vanzari"].ewm(span=10).mean()

# alpha: factorul de netezire direct (0 < alpha <= 1)
# alpha = 2 / (span + 1), deci span=10 produce alpha = 0.182
ema_alpha = df["Vanzari"].ewm(alpha=0.182).mean()

# halflife: timpul necesar ca greutatea să scadă la jumătate
ema_halflife = df["Vanzari"].ewm(halflife=5).mean()

# Toate trei produc rezultate similare (span=10 este aproape de halflife=5)
print("Diferenta maxima span vs alpha:", (ema_span - ema_alpha).abs().max())

Când să folosiți EWM în loc de SMA:

  • Când datele recente sunt mai relevante decât cele vechi
  • Când vreți detecție rapidă a schimbărilor de trend
  • În aplicații financiare (EMA e standard pentru indicatori tehnici)
  • Când SMA produce un lag prea mare pentru nevoile voastre

shift() și diff(): Comparații Temporale

Funcțiile shift() și diff() sunt instrumente esențiale pentru a compara valori la momente diferite de timp. Par simple la prima vedere, dar sunt un pattern fundamental în analiza seriilor temporale.

shift(): Deplasarea Datelor

# shift(1) — valoarea de ieri
df["Ieri"] = df["Vanzari"].shift(1)

# shift(-1) — valoarea de mâine
df["Maine"] = df["Vanzari"].shift(-1)

# Variația zilnică: azi vs. ieri
df["Variatie_Zilnica"] = df["Vanzari"] - df["Vanzari"].shift(1)

# Variația procentuală zilnică
df["Variatie_Pct"] = df["Vanzari"].pct_change() * 100

# Variația față de aceeași zi din săptămâna trecută
df["Variatie_Sapt"] = df["Vanzari"] - df["Vanzari"].shift(7)

print(df[["Vanzari", "Ieri", "Variatie_Zilnica", "Variatie_Pct"]].head(10))

diff(): Diferențierea Seriei

Diferențierea e crucială pentru a face o serie staționară — o cerință pentru multe modele de previziune (ARIMA, de exemplu). Dacă nu ați auzit de staționalitate, pe scurt: înseamnă că proprietățile statistice ale seriei nu se schimbă în timp.

# Diferența de ordinul 1 (echivalent cu shift + scădere)
df["Diff_1"] = df["Vanzari"].diff(1)

# Diferența de ordinul 2 (rata de schimbare a ratei de schimbare)
df["Diff_2"] = df["Vanzari"].diff(2)

# Diferența sezonieră (față de aceeași perioadă, un an în urmă)
# Util pentru a elimina sezonalitatea
df["Diff_365"] = df["Vanzari"].diff(365)

# Vizualizare: seria originală vs. diferențiată
fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True)

df["Vanzari"].plot(ax=axes[0], title="Seria Originala")
df["Diff_1"].plot(ax=axes[1], title="Diferentiata (ordinul 1)")
df["Diff_365"].dropna().plot(ax=axes[2], title="Diferentiata Sezonier (365 zile)")

plt.tight_layout()
plt.show()

Tratarea Valorilor Lipsă în Serii Temporale

Valorile lipsă în serii temporale necesită o abordare specială. Spre deosebire de datele tabulare obișnuite, nu e recomandat să înlocuiți cu media generală — asta ignoră complet tendința și sezonalitatea.

Iată metode mai potrivite:

# Forward fill: repetă ultima valoare cunoscută
df["Vanzari_ffill"] = df["Vanzari"].ffill()

# Backward fill: folosește următoarea valoare cunoscută
df["Vanzari_bfill"] = df["Vanzari"].bfill()

# Interpolare liniară (de obicei cea mai bună opțiune simplă)
df["Vanzari_interp"] = df["Vanzari"].interpolate(method="linear")

# Interpolare cu metoda time (ține cont de distanța temporală)
df["Vanzari_time"] = df["Vanzari"].interpolate(method="time")

# Interpolare spline (pentru date non-liniare)
df["Vanzari_spline"] = df["Vanzari"].interpolate(method="spline", order=3)

# Completare cu media mobilă a ultimelor N zile
df["Vanzari_rolling"] = df["Vanzari"].fillna(
    df["Vanzari"].rolling(window=7, min_periods=1).mean()
)

# Verificare
print(f"NaN originale: {df['Vanzari'].isna().sum()}")
print(f"NaN dupa interpolare: {df['Vanzari_interp'].isna().sum()}")

O regulă de aur: pentru serii temporale staționare (fără trend), forward fill sau interpolare liniară funcționează bine. Pentru serii cu trend puternic, interpolarea bazată pe timp sau spline e preferabilă. Alegeți metoda în funcție de natura datelor, nu pe bază de „ce e mai simplu".

Decompoziția Seriilor Temporale

Orice serie temporală poate fi descompusă în trei componente: trend, sezonalitate și reziduu (zgomot). Decompoziția vă ajută să înțelegeți ce „forțe" acționează asupra datelor — și sincer, e unul dintre cele mai satisfăcătoare momente în analiză, când vezi clar fiecare componentă separată.

from statsmodels.tsa.seasonal import seasonal_decompose

# Completăm valorile lipsă înainte de decompoziție
serie_completa = df["Vanzari"].interpolate(method="linear")

# Decompoziție aditivă (componentele se adună)
decomp = seasonal_decompose(serie_completa, model="additive", period=365)

# Vizualizare
fig, axes = plt.subplots(4, 1, figsize=(14, 12), sharex=True)

decomp.observed.plot(ax=axes[0], title="Observat")
decomp.trend.plot(ax=axes[1], title="Trend")
decomp.seasonal.plot(ax=axes[2], title="Sezonalitate")
decomp.resid.plot(ax=axes[3], title="Reziduu (Zgomot)")

plt.tight_layout()
plt.show()

Modelul aditiv presupune că seria = trend + sezonalitate + reziduu. Folosiți modelul multiplicativ (model="multiplicative") când amplitudinea sezonalității crește proporțional cu nivelul seriei.

STL: Decompoziție Robustă

Decompoziția STL (Season-Trend using Loess) e mai robustă și gestionează mai bine sezonalitatea care se schimbă în timp. Personal, o prefer față de decompoziția clasică în majoritatea cazurilor:

from statsmodels.tsa.seasonal import STL

stl = STL(serie_completa, period=365, seasonal=13, robust=True)
rezultat_stl = stl.fit()

fig = rezultat_stl.plot()
fig.set_size_inches(14, 10)
plt.tight_layout()
plt.show()

Autocorelare: Măsurarea Dependenței Temporale

Autocorelarea măsoară cât de mult seamănă o serie cu versiunea ei deplasată în timp. E un instrument esențial pentru a identifica pattern-uri periodice și pentru a selecta parametrii modelelor de previziune.

# Autocorelarea cu Pandas
autocorr_1 = serie_completa.autocorr(lag=1)    # Lag de 1 zi
autocorr_7 = serie_completa.autocorr(lag=7)    # Lag de 7 zile
autocorr_365 = serie_completa.autocorr(lag=365)  # Lag de 1 an

print(f"Autocorelare lag 1: {autocorr_1:.4f}")
print(f"Autocorelare lag 7: {autocorr_7:.4f}")
print(f"Autocorelare lag 365: {autocorr_365:.4f}")

# Graficul funcției de autocorelare (ACF)
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

fig, axes = plt.subplots(1, 2, figsize=(14, 5))
plot_acf(serie_completa.dropna(), lags=50, ax=axes[0])
axes[0].set_title("Functia de Autocorelare (ACF)")
plot_pacf(serie_completa.dropna(), lags=50, ax=axes[1])
axes[1].set_title("Functia de Autocorelare Partiala (PACF)")
plt.tight_layout()
plt.show()

Un vârf semnificativ la lag 7 indică sezonalitate săptămânală, la lag 30 sezonalitate lunară, iar la lag 365 sezonalitate anuală. Aceste grafice vă ghidează direct în alegerea parametrilor pentru modelele ARIMA sau SARIMA.

Exemplu Complet: Analiză End-to-End

Bun, hai să punem totul laolaltă într-o analiză completă. De la date brute la insight-uri acționabile — exact cum arată un workflow real:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# 1. Încărcare și pregătire date
np.random.seed(42)
date_range = pd.date_range("2024-01-01", "2025-12-31", freq="D")
n = len(date_range)
trend = np.linspace(100, 200, n)
sezonalitate = 30 * np.sin(2 * np.pi * np.arange(n) / 365.25)
zgomot = np.random.normal(0, 10, n)

df = pd.DataFrame({
    "Vanzari": trend + sezonalitate + zgomot
}, index=date_range)
df.index.name = "Data"

# 2. Analiza pe rezoluții multiple
lunar = df["Vanzari"].resample("MS").agg(["mean", "sum", "std"])
lunar.columns = ["Medie", "Total", "DevStd"]

# 3. Medii mobile și EMA
df["SMA_30"] = df["Vanzari"].rolling(30).mean()
df["EMA_30"] = df["Vanzari"].ewm(span=30).mean()

# 4. Variația lunară (Month-over-Month)
lunar["MoM_Pct"] = lunar["Total"].pct_change() * 100

# 5. Raport rezumativ
print("=" * 60)
print("RAPORT ANALIZA SERII TEMPORALE")
print("=" * 60)
print(f"Perioada: {df.index.min().date()} — {df.index.max().date()}")
print(f"Total zile: {len(df)}")
print(f"Media zilnica: {df['Vanzari'].mean():.2f}")

sma_first = df["SMA_30"].iloc[30]
sma_last = df["SMA_30"].iloc[-1]
directie = "Crescator" if sma_last > sma_first else "Descrescator"
print(f"Trend general: {directie}")

print(f"\nTop 3 luni ca vanzari totale:")
print(lunar.nlargest(3, "Total")[["Total"]])
print(f"\nCea mai volatila luna (DevStd max):")
print(lunar.nlargest(1, "DevStd")[["DevStd"]])

# 6. Vizualizare finală
fig, axes = plt.subplots(3, 1, figsize=(14, 12))

# Panel 1: Date brute + medii mobile
df["Vanzari"].plot(ax=axes[0], alpha=0.3, label="Zilnic")
df["SMA_30"].plot(ax=axes[0], label="SMA 30", linewidth=2)
df["EMA_30"].plot(ax=axes[0], label="EMA 30", linewidth=2)
axes[0].set_title("Vanzari Zilnice cu Medii Mobile")
axes[0].legend()

# Panel 2: Totaluri lunare
lunar["Total"].plot(ax=axes[1], kind="bar", color="steelblue", alpha=0.8)
axes[1].set_title("Totaluri Lunare")
axes[1].tick_params(axis="x", rotation=45)

# Panel 3: Variația procentuală MoM
culori = ["green" if x >= 0 else "red" for x in lunar["MoM_Pct"]]
lunar["MoM_Pct"].plot(ax=axes[2], kind="bar", color=culori)
axes[2].set_title("Variatia Lunara (%)")
axes[2].axhline(y=0, color="black", linewidth=0.5)
axes[2].tick_params(axis="x", rotation=45)

plt.tight_layout()
plt.show()

Performanță: Sfaturi pentru Serii Temporale Mari

Când lucrați cu serii temporale de milioane de rânduri, performanța devine critică. Am învățat asta pe pielea mea cu un dataset de 50 de milioane de rânduri care abia se mișca. Iată câteva sfaturi practice:

  • Folosiți PyArrow backend: Pandas 3.0 suportă backend-ul PyArrow pentru string-uri implicit, dar îl puteți folosi și pentru coloane datetime pentru performanță îmbunătățită
  • Stocați în format Parquet: în loc de CSV, Parquet e de 5-10x mai rapid la citire și ocupă mult mai puțin spațiu
  • Evitați apply() cu funcții lambda: funcțiile vectorizate Pandas sunt de 10-100x mai rapide
  • Folosiți resample() în loc de groupby() manual: resample() e optimizat intern pentru operații temporale
  • Citiți doar coloanele necesare: pd.read_csv("fisier.csv", usecols=["Data", "Vanzari"])
  • Procesați în chunk-uri: pd.read_csv("mare.csv", chunksize=100000) pentru fișiere care nu încap în memorie
# Citire eficientă din Parquet
df = pd.read_parquet("vanzari.parquet", columns=["Data", "Vanzari"])

# Benchmark: resample vs groupby manual
import timeit

# Generăm date mari
big_df = pd.DataFrame({
    "Vanzari": np.random.randn(1_000_000)
}, index=pd.date_range("2020-01-01", periods=1_000_000, freq="min"))

# resample (optimizat)
t1 = timeit.timeit(lambda: big_df.resample("h").mean(), number=10)

# groupby manual (mai lent)
t2 = timeit.timeit(
    lambda: big_df.groupby(big_df.index.floor("h")).mean(), number=10
)

print(f"resample(): {t1:.3f}s")
print(f"groupby manual: {t2:.3f}s")
print(f"resample e {t2/t1:.1f}x mai rapid")

Întrebări Frecvente (FAQ)

Care e diferența dintre resample() și groupby() pentru serii temporale?

resample() e proiectat specific pentru date temporale și necesită un DatetimeIndex. E echivalentul temporal al lui groupby() — creează „găleți" de timp (ore, zile, săptămâni, luni) și aplică funcții de agregare. groupby() e mai general și funcționează pe orice coloană. Pentru operații pur temporale, resample() e mai rapid și mai expresiv, dar le puteți combina cu groupby().resample() pentru analiză multi-dimensională.

Cum aleg între media mobilă simplă (SMA) și exponențială (EMA)?

SMA (rolling().mean()) dă greutate egală tuturor punctelor din fereastră — e mai stabilă dar reacționează mai lent la schimbări. EMA (ewm().mean()) dă mai multă greutate datelor recente — reacționează rapid la schimbări dar poate fi mai zgomotoasă. Folosiți SMA pentru analiză exploratorie generală și EMA când datele recente sunt mai relevante (finanțe, monitorizare în timp real).

Ce rezoluție de datetime folosește Pandas 3.0 implicit?

Pandas 3.0 a schimbat rezoluția implicită de la nanosecunde la microsecunde. Asta elimină problemele de overflow pentru date înainte de 1678 sau după 2262, extinzând semnificativ intervalul de date suportat. Dacă aveți nevoie de rezoluție la nanosecunde, o puteți specifica explicit cu dtype="datetime64[ns]".

Cum tratez valorile lipsă în serii temporale fără a introduce bias?

Evitați înlocuirea cu media generală — aceasta ignoră trendul și sezonalitatea. Cele mai bune opțiuni sunt: interpolate(method="time") care ține cont de distanța temporală, ffill() pentru date care se schimbă rar, sau completarea cu media mobilă a ferestrei anterioare. Pentru lacune mari, considerați interpolarea spline (method="spline") sau modele de imputare specializate.

Pot folosi resample() fără DatetimeIndex?

Nu direct pe index, dar puteți specifica coloana temporală cu parametrul on: df.resample("MS", on="coloana_data").mean(). Alternativ, puteți folosi pd.Grouper(key="coloana_data", freq="MS") în interiorul unui groupby(). Ambele abordări funcționează, dar setarea unui DatetimeIndex rămâne varianta recomandată pentru analiză intensivă.

Despre Autor Editorial Team

Our team of expert writers and editors.