20.1 Időtengely, resampling és állomásossági kérdés

20.1.1 Cél és konkrét helyzet

A Northstar-lab orders.csv táblája rendelésenként egy sort tartalmaz: a megfigyelési egység itt egy rendelés, az időbélyeg pedig az order_ts oszlop UTC-értéke. Ha a kérdés az, hogyan alakul a rendelési volumen napról napra, az elemzési egységet megváltoztatjuk: az új megfigyelési egység egy naptári nap, a célváltozó pedig az aznapi rendelések darabszáma. A feladat nem előrejelzés, hanem az idősor helyes időtengelyének és hiányosságának előállítása, mert a későbbi modellezés Időhelyes feature és gördülő visszateszt ettől függ.

A bemenet 2400 rendelés, a rendelések 2025 első 180 napjára esnek. Első becslésként napi 2400/180 ≈ 13,3 rendelést várunk, de a hét napjai között lehet eltérés. Az elvárt ellenőrzés:

20.1.2 Fogalmi alapozás: gyakoriság, hézag, trend, szezonalitás

Gyakoriság (frequency). Egy idősor gyakorisága az egymást követő megfigyelések közötti névleges időtávolság. A nyers orders.csv szabálytalan gyakoriságú: két rendelés között lehet 1 másodperc vagy 3 óra, nincs előre rögzített rács. Ha a 2025-01-01, 2025-01-02, 2025-01-04 napokon vannak megfigyelések, akkor az első két lépés 1 nap, a harmadik 2 nap; ez nem nevezhető napi frekvenciájú sorozatnak abban az értelemben, hogy a naptár minden napjához tartozik érték.

A gyakorlatban a groupby("order_day") vagy a resample("D") művelet napi frekvenciájúvá (D) teszi a sorozatot: a naptár az a rács, amelyen az értékeket értelmezzük. Az ennél durvább W heti, a finomabb h órai rács szintén lehetséges, és a választás mindig a döntési helyzettől függ.

Hézag (gap). A hézag a teljes időrács azon eleme, amelyhez nincs megfigyelt érték. Ha TgridT_{\text{grid}} az elvárt napi rács, TobservedT_{\text{observed}} a megfigyelt napok halmaza, akkor

G=Tgrid\Tobserved, G = T_{\text{grid}} \setminus T_{\text{observed}},

ahol GG a hiányzó napok halmaza. Egy TgridT_{\text{grid}} elemről tehát nem eldönthető pusztán a hiánya, hogy mit jelent: lehet, hogy a folyamat aktív volt, de nulla esemény történt, és lehet, hogy a megfigyelés szakadt meg. Ez a két eset statisztikailag nem azonos. Az elsőnél a nulla valódi adat, a másodiknál ismeretlen állapot.

A különbség a gyakorlatban így jelenik meg: a daily_counts sorozat indexe csak a megfigyelt napokat tartalmazza, a daily_dense sorozat a teljes naptári rácsot, ahol a hiányzó elemekre fill_value=0 került. A daily_counts hossza legfeljebb 180, a daily_dense hossza pontosan 180; a kettő közötti napok halmaza GG. A 0-val kitöltés csak akkor megalapozott, ha az adatszerződés kimondja: a megfigyelő rendszer minden nap aktív volt, és a napon nulla rendelés érkezett. Ez a Northstar-lab generátoránál a rendelési eseménysorozatra teljesül, de a mesterségesen kihagyott nap már nem ilyen.

Trend. A trend a sorozat szintjének lassú, irányított változása. Ha a napi rendelések átlagos száma folyamatosan emelkedik az idővel, akkor az első 30 nap átlaga kisebb, mint az utolsó 30 napé. A trendet legkényelmesebben mozgóátlaggal közelítjük: például a 7 napos mozgóátlag

$$ \hat{T}t = \frac{1}{7}\left(Y{t-6} + Y_{t-5} + \cdots + Y_t\right),

$$

ahol YtY_t a tt nap rendelési száma, és az ablakot gyakran a jelenlegi napig görgetjük. A mozgóátlag elsimítja a napon belüli véletlen ingadozást, és láthatóvá teszi a lassabb szintváltozást.

Szezonalitás (seasonality). A szezonalitás rögzített periódusú, ismétlődő minta. A napi rendelési adatban a legtermészetesebb periódus a hét napja: a hétvégi napokon eltérő lehet a rendelési volumen. A hét napja szerinti szezonális átlag

$$ \bar{Y}j = \frac{1}{N_j}\sum{t: \mathrm{dow}(t)=j} Y_t,

$$

ahol j∈{0,…,6}j \in \{0,\dots,6\} a hét napjának kódja a DatetimeIndex.dayofweek szerint (0 = hétfő, 6 = vasárnap), NjN_j pedig az adott nap előfordulásainak száma. A szezonális hatás nem oksági magyarázat: csak annyit mond, hogy a hét egyes napjain szisztematikusan más a szint.

Aggregálás (aggregation). Az idősor előállítása aggregálás: a rendelési szintű táblából csoportosítjuk a sorokat a nap szerint, majd minden csoportban összegzünk, átlagolunk vagy valamilyen más összefoglaló értéket számolunk. A Northstar-példában a size() az adott napi rendelések számát adja. Az aggregálás előtti shape (2400, n_columns), utána (180,), ha a teljes rácsot tekintjük; az általános szabály, hogy a csoportosítás mindig csökkenti a megfigyelési egység számát.

Stacionaritási intuíció (stationarity intuition). Egy idősor akkor tekinthető gyenge értelemben stacionáriusnak, ha a statisztikai tulajdonságai nem függenek attól, hogy a sorozat melyik időablakát nézzük. A leggyakrabban használt közelítő feltételek:

E[Yt]=μ,Var(Yt)=σ2,Cov(Yt,Yt−k)=γk, E[Y_t] = \mu,\qquad \mathrm{Var}(Y_t) = \sigma^2,\qquad \mathrm{Cov}(Y_t, Y_{t-k}) = \gamma_k,

ahol YtY_t az idősor értéke a tt időpontban, μ\mu és σ2\sigma^2 konstans, γk\gamma_k pedig csak a kk késleltetéstől függő autokovariancia. A feltétel azt fejezi ki, hogy a sorozat átlaga, varianciája és késleltetett kapcsolata azonos, ha az ablakot későbbre toljuk.

A napi rendelési sorozatunk csak akkor stacionárius, ha nincs szisztematikus trend, és a variancia sem változik az időben. A trend kimozdítja az átlagot az állandóságból; a hét napja szerinti szezonalitás azt jelenti, hogy E[Yt]E[Y_t] a nap kódjától periodikusan függ, tehát nem állandó a teljes sorozaton. A gyakorlati ellenőrzés egyszerű formája: a mozgóátlag és a mozgóvariancia ne mutasson erős, irányított elmozdulást. Ez még nem formális teszt, de az intuícióhoz elegendő; a formális becslési és tesztelési kérdéseket később a megfelelő modellekkel kezeljük.

20.1.3 Teljes minimális munkapélda

A példát a tanulólabor könyvtárából futtatjuk: lab/ munkakönyvtár, Linux/macOS bash vagy Windows PowerShell. A környezet a lab pyproject.toml alapján uv sync-kel telepíthető; a futtatáshoz uv run python szükséges.

import pandas as pd
from statsmodels.tsa.seasonal import seasonal_decompose

# Munkakönyvtár: lab/
orders = pd.read_csv("orders.csv")

# Az UTC-bélyegből naptári nap: az explicit +00:00 offsetet kezeljük,
# majd a napra normalizáljuk időzóna nélkül.
orders["order_day"] = (
    pd.to_datetime(orders["order_ts"], utc=True)
    .dt.tz_convert(None)
    .dt.normalize()
)

# Napi aggregálás: ez a hiányos indexű sorozat.
daily_counts = orders.groupby("order_day").size()

# Teljes naptári rács 2025 első 180 napjára.
start_day = pd.Timestamp("2025-01-01")
end_day = pd.Timestamp("2025-06-29")
full_day_index = pd.date_range(start_day, end_day, freq="D")

# Explicit nullás sorozat: a rács minden napjához tartozik érték.
daily_dense = daily_counts.reindex(full_day_index, fill_value=0)

# Ellenőrző invariánsok.
assert len(full_day_index) == 180
assert daily_dense.notna().all()
assert (daily_dense.index == full_day_index).all()
print("dtype:", daily_dense.dtype, "| index dtype:", daily_dense.index.dtype)
print("sűrű sorozat hossza:", len(daily_dense))

# Mesterséges hiány: tegyük fel, hogy 2025-02-02-t a rendszer nem rögzítette.
daily_with_gap = daily_dense.drop(pd.Timestamp("2025-02-02"))
actual_gaps = full_day_index.difference(daily_with_gap.index)
print("hiányzó napok:", actual_gaps)

# Trend és szezonalitás szemléltető vizsgálata.
trend_7d = daily_dense.rolling(7).mean()
weekday_means = daily_dense.groupby(daily_dense.index.dayofweek).mean()
vol_28d = daily_dense.rolling(28).var()

print("7 napos mozgóátlag eleje:")
print(trend_7d.dropna().head(3))
print("héten belüli napátlagok (0=hétfő):")
print(weekday_means)
print("28 napos mozgóvariancia eleje és vége:")
print(vol_28d.dropna().head(1), vol_28d.dropna().tail(1))

decomp = seasonal_decompose(daily_dense, model="additive", period=7)
print("dekompozíciós trend vége:")
print(decomp.trend.dropna().tail(3))

A seasonal_decompose az additív felbontást Yt=Tt+St+RtY_t = T_t + S_t + R_t alakban becsli, ahol TtT_t a trend, StS_t a heti szezonális komponens, RtR_t a maradék; a period=7 a heti ciklust jelöli. A dekompozíció itt leíró eszköz, nem előrejelzési modell.

A futtatási parancs Linux/macOS alatt:

cd lab
uv run python time_grid_example.py

Windows PowerShellben ugyanez a cd lab; uv run python time_grid_example.py paranccsal adható meg. A kód a lab definiált adatfájljait használja, külső hálózatot nem igényel.

20.1.4 Eredmény értelmezése

A daily_counts a rendelés sorok csoportosításából keletkezik: az index csak azokat a napokat tartalmazza, amelyekhez legalább egy rendelés tartozik. Ha minden nap érkezett rendelés, a hossza 180; ha valamelyik nap nem, akkor rövidebb. A daily_dense hossza minden esetben pontosan 180, mert a reindex a teljes full_day_index-et használja. A daily_dense.dtype int64, mert a fill_value=0 egész szám; a daily_dense.index.dtype datetime64[ns].

A mesterségesen kihagyott nap mutatja a két fogalom közötti különbséget. A daily_dense 2025-02-02-ére 0 áll, mert az előbbi feltételezés szerint a napon nulla rendelés volt. A daily_with_gap indexéből ugyanez a nap hiányzik; a full_day_index.difference(...) halmaz pontosan {2025-02-02}. A hiányos nap tehát nem nulla érték, hanem hiányzó megfigyelés: az elem hiányzik az indexből, ezért a sorozat hossza csak 179.

A trend és a szezonalitás vizsgálata a gyakorlatban ilyen ellenőrzéseket ad:

A fenti kód kimenete a helyi futtatástól függ; az itt közölt sorok szemléltető, nem mért kimenetek. A hallgató a saját futás során ellenőrzi a három invariant: a sűrű sorozat hossza 180, nincs NaN, és a hiányzó napok halmaza a szándékozott napot tartalmazza.

Időindex-definíció: a hiányzó nap és a valóban nulla rendelést tartalmazó nap más állítás. Zérussal kitöltés csak akkor helyes, ha bizonyított a teljes adatgyűjtés; időzónaváltásnál és napra aggregálásnál az üzleti naptárnapot rögzíteni kell.

20.1.5 Feladatok

  1. Értelmezés. Adott három nap: 2025-01-01 (4 rendelés), 2025-01-02 (7 rendelés), 2025-01-04 (5 rendelés). Milyen két különböző állítást tehetünk a 2025-01-03 napra, ha az adatszerződés szerint (a) a rendszer a napon aktív volt, de nem volt rendelés; (b) a rendszer aznap nem rögzített adatot? Melyik esetben szabad 0-val kitölteni a hiányt?

  2. Megvalósítás. A orders.csv beolvasása után készíts óránkénti sorozatot: az új megfigyelési egység az óra, az érték az adott órában leadott rendelések száma. Használd a teljes rácsot 2025-01-01 00:00-tól 2025-06-29 23:00-ig. Add meg az explicit nullás sorozat hosszát, dtype-ját és a hiányzó órák számát. Várt invariáns: a teljes órai rács hossza 180⋅24=4320180 \cdot 24 = 4320, a hiányzó órák száma legalább 4320−2400=19204320 - 2400 = 1920, mert legfeljebb 2400 különböző órában lehet rendelés.

  3. Hibakeresés. A következő kódrészlet heti átlagot számol:

daily = orders.groupby("order_day").size()
daily_full = daily.reindex(full_day_index)  # fill_value nélkül
weekly_avg = daily_full.resample("W").mean()

Mi a hiba, és hogyan függ a javítás attól, hogy a hiányzó nap tényleges nulla vagy mérési hiány? Adj mindkét esetre javított kódot.

  1. Megváltozott feltétel. A Northstar külön kísérleti projektjében az outcomes.csv ügyfélenként egyetlen conversion_7d bináris értéket és revenue_7d_minor összeget tartalmaz a 2025-07-01-től 2025-07-08-ig tartó teljes 7 napos ablakra. Helyes-e ebből napi konverziós arány idősorozatot készíteni úgy, hogy a conversion_7d értékét 7-tel osztjuk vagy naponként ismételjük? Indokold, melyik megfigyelési szerződés sérül.

20.1.6 Megoldások és indoklás

  1. Megoldás. Az (a) esetben a 2025-01-03-hoz tartozó érték tényleges nulla: a rendszer figyelte a napot, és nem érkezett rendelés. Ezt 0-val ki lehet tölteni, mert a 0 valódi adat. A (b) esetben a 2025-01-03 megfigyelés hiányzik: nem tudjuk, hány rendelés érkezett. A 0-val kitöltés hamis adatot gyárt, mert a napot nem figyelték. Ilyenkor a helyes kezelés vagy a nap elhagyása, vagy egy explicit hiány-modell, például imputáció; a konkrét Northstar-laborban a rendelési folyamat minden nap aktív, ezért ott a hiány nulla rendelést jelent, azonban ezt az adatszerződésből lehet tudni, nem a CSV-ből.

  2. Megoldás. A kód:

orders["order_hour"] = (
    pd.to_datetime(orders["order_ts"], utc=True)
    .dt.tz_convert(None)
    .dt.floor("h")
)
hourly_counts = orders.groupby("order_hour").size()
full_hours = pd.date_range(
    "2025-01-01", "2025-06-29 23:00", freq="h"
)
hourly_dense = hourly_counts.reindex(full_hours, fill_value=0)
missing_hours = full_hours.difference(hourly_counts.index)

assert len(full_hours) == 4320
assert hourly_dense.notna().all()
assert len(missing_hours) == len(full_hours) - len(hourly_counts)
print(len(hourly_dense), hourly_dense.dtype, len(missing_hours))

Az ellenőrző invariant: a sűrű sorozat hossza 4320, dtype-ja int64; a hiányzó órák száma legalább 1920, mert egyedi óra legfeljebb 2400 van. A tényleges szám valamivel nagyobb lehet, ha több rendelés esik ugyanabba az órába.

  1. Megoldás. A hiba az, hogy a reindex fill_value=0 nélkül NaN értéket ad a hiányzó napokra. A resample("W").mean() ezt a NaN-t csendben figyelmen kívül hagyja; a heti átlag ezért nem a teljes hétre vonatkozó összeg, hanem csak a megfigyelt napokra. A javítás attól függ, mit jelent a hiány:
daily_full = daily.reindex(full_day_index, fill_value=0)
weekly_avg = daily_full.resample("W").mean()
daily_full = daily.reindex(full_day_index)
weekly_avg = daily_full.resample("W").mean()  # NaN-t explicit ignorál

A választott megoldás csak akkor elfogadható, ha a döntést az adatszerződésben rögzítjük.

  1. Megoldás. Nem helyes. Az outcomes.csv megfigyelési egysége egy ügyfél végső 7 napos kimenete, nem pedig egy ügyfél-nap pár. A conversion_7d azt jelzi, hogy az ügyfél az egész ablak alatt konvertált-e; nem tartalmazza a konverzió pontos napját. A 7-tel osztás vagy naponkénti ismétlés új adatot gyárt, amely sem nem konverziós esemény, sem nem per-nap ráta. A napi konverziós idősorhoz külön naponkénti eseményadat kellene, például conversion_event_at; ilyen a jelenlegi kísérleti adatban nincs. Az ITT-elemzés számára a helyes megfigyelés továbbra is a teljes randomizált 300 ügyfél és a 7 napos kimenet.

20.1.7 Gyakori tévedés és önellenőrzés

A leggyakoribb tévedés az, hogy a hiányzó megfigyelést automatikusan nullának tekintjük. A fillna(0) mesterégesen csökkenti a szintet: egy kételemes példában, ahol az első nap 10 rendelést látott, a második nap pedig ismeretlen, a [10, 0] sorozat átlaga 5, holott a második nap akár 10 rendelés is lehetett volna. A dropna ezzel szemben azt állítja, hogy a második napról nincs megfigyelés; az átlag akkor 10. Mindkét döntés állítás, nem technikai művelet. Ez a Hiány és anomália mint vizsgálati kérdés hiányzási szerződés időbeli változata: a mechanizmus feltevése nem a táblából következik, hanem a mérési folyamatból. A Nullmodell és p-érték nullmodell-logikája is figyelmeztet: a szélsőségesség csak az előre rögzített modell szerint értelmezhető, így az idősor hiányosságát is a modellben kell rögzítenünk.

Önellenőrzés: mi a különbség a távollévő index és a nulla érték között? Miért nem stacioner a növekvő trendes sorozat? Mit jelent egy napi sorozat D frekvenciája? Ha a csúszó ablakos variancia határozottan nő, mire következtetünk?

20.1.8 Rövid összefoglaló

A nyers rendelés-időbélyeg szabálytalan gyakoriságú; a napi rácsra aggregálással szabályos frekvenciájú sorozatot kapunk. A hézag a teljes rács és a megfigyelt napok halmazkülönbsége; a hiányzó napokat csak akkor szabad nullával kitölteni, ha a mérési folyamat aktív volt és tényleg nulla esemény történt. A trend lassú irányított szintváltozás, a szezonalitás rögzített periódusú ismétlődés; a mozgóátlag és mozgóvariancia erős elmozdulása a stacionaritás sérülésére utal. A következő alkalmazásban erre az időtengelyre építünk cutoff előtti feature-képzést és gördülő visszatesztet, ahol a predikciós pillanat és a címke elérhetősége tovább szűkíti a megfigyelt ablakot.