Hoppa till innehållet
AI-grafen
E· Universitetklassisk-ml· ca 60 min· utvecklande· verifierad 2026-09-20

Tidsserier

Kunna dela upp tidsserier korrekt, bygga lag-features och undvika framtidsläckage.

Förkunskaper

Intuition

Tidsserier bryter mot antagandet all annan maskininlärning vilar på: att observationerna är oberoende och utbytbara. Det får tre konsekvenser.

1. Slumpmässig uppdelning är fel. Tränar du på slumpvis valda dagar och testar på resten har modellen sett framtiden. Dela kronologiskt:

|--------- träning ---------|--- val ---|--- test ---|
         2024                  2025        2026

2. Baslinjen är svårare än man tror. «I morgon blir som i dag» (naiv prognos) och «samma veckodag förra veckan» (säsongsnaiv) är förvånansvärt starka. Slår din modell inte dem har den inget värde.

3. Framtidsläckage är lätt att råka ut för. Ett rullande medelvärde som råkar inkludera nuvarande eller kommande värden ger fantastiska resultat i utvärderingen och värdelösa i drift.

Formellt

Features som inte läcker:

FeatureKonstruktionFälla
Lagy.shift(k) med k ≥ prognoshorisontshift(1) när du prognosticerar 7 dagar fram är läckage
Rullande medely.shift(1).rolling(w).mean()rolling(w) utan shift inkluderar nuvarande värde
Differensy.shift(1) - y.shift(2)
Kalenderveckodag, månad, helgdaginget läckage — känt i förväg
Externaväder, kampanjmåste vara känt vid prognostillfället

Regeln: varje feature måste gå att beräkna med enbart information som fanns vid prognostillfället. Skriv ner tidpunkten och fråga för varje kolumn: visste vi detta då?

Korsvalidering görs med expanderande eller glidande fönster, aldrig med KFold:

veck 1: [träna....] [test]
veck 2: [träna.......] [test]
veck 3: [träna..........] [test]

sklearn.model_selection.TimeSeriesSplit gör detta. Lägg dessutom in ett gap mellan träning och test om målet beror på flera dagar bakåt, annars läcker det över gränsen.

Mått, och när de vilseleder:

MåttBra påFälla
MAEtolkbart i enheten
RMSEstraffar stora felkänsligt för utstickare
MAPEprocentuellt, jämförbartexploderar när det sanna värdet är nära 0
sMAPEsymmetrisktfortfarande instabilt vid små värden
MASEskalat mot naiv prognosdet ärligaste — 1,0 betyder «lika bra som naiv»

MASE är att föredra just för att den har en självklar referenspunkt: över 1,0 är modellen sämre än att gissa gårdagens värde.

Metodval: börja med de klassiska. ARIMA, exponentiell utjämning och särskilt gradient boosting på lag-features är svårslagna på de flesta verkliga serier. Djupa sekvensmodeller vinner först vid många parallella serier och mycket data — och i M-tävlingarna har enkla statistiska metoder länge hållit sig i toppen.

Kod

import numpy as np, pandas as pd
from sklearn.model_selection import TimeSeriesSplit
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.metrics import mean_absolute_error

rng = np.random.default_rng(0)
n = 730
idx = pd.date_range("2024-01-01", periods=n, freq="D")
trend = np.linspace(100, 140, n)
veckosasong = 12 * np.sin(2 * np.pi * np.arange(n) / 7)
y = trend + veckosasong + rng.normal(0, 5, n)
df = pd.DataFrame({"y": y}, index=idx)

HORISONT = 7            # vi prognosticerar 7 dagar fram

def bygg_features(df, horisont=HORISONT):
    d = df.copy()
    # Alla lag måste vara minst `horisont` steg bakåt — annars läckage
    for lag in (horisont, horisont + 1, horisont + 7, horisont + 14):
        d[f"lag_{lag}"] = d["y"].shift(lag)
    for w in (7, 28):
        d[f"medel_{w}"] = d["y"].shift(horisont).rolling(w).mean()
        d[f"std_{w}"] = d["y"].shift(horisont).rolling(w).std()
    d["veckodag"] = d.index.dayofweek
    d["manad"] = d.index.month
    return d.dropna()

d = bygg_features(df)
X, mal = d.drop(columns="y"), d["y"]

# Baslinjer först — de måste slås
naiv = d["lag_7"]                                   # "samma dag förra veckan"
print(f"säsongsnaiv MAE: {mean_absolute_error(mal, naiv):.2f}")

# Kronologisk korsvalidering med gap
tscv = TimeSeriesSplit(n_splits=5, gap=HORISONT)
fel = []
for tr, te in tscv.split(X):
    m = HistGradientBoostingRegressor(random_state=0).fit(X.iloc[tr], mal.iloc[tr])
    fel.append(mean_absolute_error(mal.iloc[te], m.predict(X.iloc[te])))
print(f"modell MAE: {np.mean(fel):.2f} ± {np.std(fel):.2f}")

# MASE: skalat mot den naiva prognosen — 1,0 betyder "lika bra som naiv"
def mase(sant, pred, naiv_pred):
    return mean_absolute_error(sant, pred) / mean_absolute_error(sant, naiv_pred)

# SÅ HÄR SER LÄCKAGE UT — och hur bra det ser ut i utvärderingen
lackt = df.copy()
lackt["medel_7_LACKER"] = lackt["y"].rolling(7).mean()      # ingen shift!
print("korrelation mellan läckande feature och målet:",
      round(float(lackt[["y", "medel_7_LACKER"]].dropna().corr().iloc[0, 1]), 4))
# ~0.95 — modellen får i praktiken se svaret, och MAE blir orealistiskt låg

Sista blocket är värt att köra. En rullande medelvärdesfeature utan shift korrelerar nära perfekt med målet, ger ett fantastiskt valideringsresultat, och fungerar inte alls i drift — eftersom värdet den bygger på inte finns när prognosen ska göras.

Behärskning innebär

  • Delar upp tidsserier kronologiskt
  • Bygger lag- och rullande features utan läckage
  • Väljer baslinje och mått medvetet

Logga in för att göra övningarna och bygga upp din behärskning.

Källor

Alla källor och licenser