Hvorfor er tidsserieanalyse så vigtigt?
Tidsseriedata er overalt – og det mener jeg helt bogstaveligt. Aktiekurser, temperaturer, webtrafik, salgstal, sensordata fra IoT-enheder... listen stopper bare aldrig. Hvis du vil forstå tendenser, forudsige fremtidige værdier eller bare skabe et ordentligt overblik over historisk data, så er tidsserieanalyse noget, du bliver nødt til at have styr på.
Med pandas 3.0 (udgivet januar 2026) er arbejdet med tidsserier faktisk blevet markant bedre. Den nye standardopløsning for datetime-data er nu mikrosekunder i stedet for nanosekunder, og det betyder, at du kan arbejde med datoer helt tilbage til år 1 og langt ud i fremtiden – uden at løbe ind i de irriterende out-of-bounds fejl, som mange af os har kæmpet med før. Det er ærligt talt en kæmpe forbedring.
Så lad os dykke ned i de tre vigtigste pandas-funktioner til tidsserieanalyse: resample(), rolling() og shift(). Vi starter fra bunden med konkrete kodeeksempler, du kan kopiere direkte ind i din Jupyter Notebook.
Opsætning: Opret et DatetimeIndex
Før vi går i gang med de sjove ting, skal dine data have et korrekt datetime-indeks. Det er fundamentet for alt det, vi gennemgår her – og det er heldigvis ret nemt at sætte op.
Konverter en kolonne til datetime
Har du en CSV-fil med en datokolonne som tekst? Sådan konverterer du den:
import pandas as pd
# Indlæs data
df = pd.read_csv("salgsdata.csv")
# Konverter datokolonnen
df["dato"] = pd.to_datetime(df["dato"])
# Sæt den som indeks
df = df.set_index("dato")
# Sortér efter tid (vigtigt!)
df = df.sort_index()
print(df.head())
Vigtigt: Pandas' tidsserieværktøjer kræver, at dine data er sorteret kronologisk. Glem ikke sort_index() – jeg har selv brugt timer på at debugge mærkelige resultater, der viste sig at skyldes usorteret data.
Opret testdata fra bunden
Lad os oprette et realistisk datasæt, vi kan lege med gennem hele guiden:
import pandas as pd
import numpy as np
# Opret en tidsserie med daglige salgstal over et år
np.random.seed(42)
datoer = pd.date_range(start="2025-01-01", end="2025-12-31", freq="D")
salg = 1000 + np.cumsum(np.random.randn(len(datoer)) * 50)
temperatur = 10 + 15 * np.sin(np.arange(len(datoer)) * 2 * np.pi / 365) + np.random.randn(len(datoer)) * 3
df = pd.DataFrame({
"salg": salg,
"temperatur": temperatur
}, index=datoer)
df.index.name = "dato"
print(df.head(10))
Nu har vi et DataFrame med daglige salgstal og temperaturer for hele 2025. Perfekt til at demonstrere alle tre funktioner.
resample(): Skift frekvensen på dine data
resample() lader dig ændre tidsfrekvensen på dine data. Du kan enten downsample (fx fra daglige til ugentlige data) eller upsample (fx fra månedlige til daglige data). Tænk på det som en slags "group by" for tid.
Downsampling: Fra daglige til ugentlige og månedlige data
# Ugentligt gennemsnit
ugentlig = df["salg"].resample("W").mean()
print("Ugentlige salgsgennemsnit:")
print(ugentlig.head())
# Månedlig sum
maanedlig = df["salg"].resample("ME").sum()
print("\nMånedlige samlede salg:")
print(maanedlig.head())
# Kvartalsvis med flere aggregeringer
kvartal = df["salg"].resample("QE").agg(["mean", "min", "max", "std"])
print("\nKvartalsvis statistik:")
print(kvartal)
Bemærk frekvensforkortelserne: "W" for uge, "ME" for månedsslut, "QE" for kvartalsslut. En vigtig detalje: i pandas 3.0 bruges "ME" i stedet for det gamle "M", og "QE" i stedet for "Q". Bruger du de gamle forkortelser, får du en FutureWarning (eller en direkte fejl).
Upsampling: Fra daglige til timevise data
# Upsample til timer og fyld med interpolering
timedvist = df["temperatur"].resample("h").interpolate(method="linear")
print("Timer (interpoleret):")
print(timedvist.head(25))
Når du upsampler, opstår der selvfølgelig huller i dine data. Du kan fylde dem med:
ffill()– brug den seneste kendte værdi (forward fill)bfill()– brug den næste kendte værdi (backward fill)interpolate()– beregn værdier mellem kendte punkter (dette er oftest det bedste valg til numeriske data)
Praktisk eksempel: Månedlig salgsrapport
# Opret en komplet månedlig rapport
rapport = df.resample("ME").agg(
gennemsnit_salg=("salg", "mean"),
max_salg=("salg", "max"),
min_temperatur=("temperatur", "min"),
max_temperatur=("temperatur", "max")
)
print(rapport)
Denne form for aggregering er noget, jeg bruger konstant i mine egne projekter. Den giver dig hurtigt et overblik, som er nemt at præsentere for stakeholders, der ikke nødvendigvis forstår de rå daglige data.
rolling(): Glidende vinduesberegninger
Hvor resample() ændrer frekvensen, beregner rolling() statistikker over et glidende vindue af fast størrelse. Det er perfekt til at udglatte støjende data og identificere tendenser – og det er nok den funktion, jeg personligt bruger mest i tidsserieanalyse.
Glidende gennemsnit
# 7-dages glidende gennemsnit
df["salg_7d_gennemsnit"] = df["salg"].rolling(window=7).mean()
# 30-dages glidende gennemsnit
df["salg_30d_gennemsnit"] = df["salg"].rolling(window=30).mean()
print(df[["salg", "salg_7d_gennemsnit", "salg_30d_gennemsnit"]].head(35))
De første rækker vil have NaN-værdier, fordi der ikke er nok data til at fylde vinduet endnu. Det kan være lidt irriterende, men du kan styre det med parameteren min_periods:
# Tillad beregning selvom vinduet ikke er fuldt
df["salg_fleksibel"] = df["salg"].rolling(window=7, min_periods=1).mean()
Glidende standardafvigelse
Standardafvigelse er rigtig nyttigt til at måle volatilitet – altså hvor meget dine data svinger over tid.
# 14-dages glidende standardafvigelse
df["salg_volatilitet"] = df["salg"].rolling(window=14).std()
print(df[["salg", "salg_volatilitet"]].dropna().head(10))
Centreret glidende vindue
# Centreret vindue: kigger både fremad og bagud
df["temperatur_centreret"] = df["temperatur"].rolling(window=7, center=True).mean()
print(df[["temperatur", "temperatur_centreret"]].head(10))
Med center=True placeres vinduet symmetrisk omkring hvert datapunkt, i stedet for kun at kigge bagud. Det giver et mere retvisende billede af den underliggende tendens – men husk, at det kræver "fremtidige" datapunkter, så de sidste rækker får også NaN-værdier.
Eksponentielt vægtet glidende gennemsnit (EWMA)
# EWMA vægter nyere observationer højere
df["salg_ewma"] = df["salg"].ewm(span=7).mean()
print(df[["salg", "salg_7d_gennemsnit", "salg_ewma"]].head(15))
Den store forskel her er, at ewm() giver nyere datapunkter mere vægt. Det gør den mere responsiv over for aktuelle ændringer, hvilket kan være en fordel – men også en ulempe, hvis du vil se den langsigtede tendens. Det kommer an på, hvad du har brug for.
shift(): Forskyd dine data i tid
shift() er deceptivt simpel: den flytter bare dine datapunkter fremad eller bagud i tid, mens indekset forbliver det samme. Men den er helt uundværlig til at beregne ændringer over tid og oprette lag-features til machine learning.
Beregn daglige ændringer
# Forskyd data én dag bagud
df["salg_igaar"] = df["salg"].shift(1)
# Beregn daglig ændring
df["daglig_aendring"] = df["salg"] - df["salg"].shift(1)
# Procentvis ændring
df["pct_aendring"] = df["salg"].pct_change() * 100
print(df[["salg", "salg_igaar", "daglig_aendring", "pct_aendring"]].head(10))
Kig fremad med negativ shift
# Se morgendagens salg (negativt shift = fremadrettet)
df["salg_imorgen"] = df["salg"].shift(-1)
# Beregn forventet ændring
df["forventet_aendring"] = df["salg"].shift(-1) - df["salg"]
print(df[["salg", "salg_imorgen", "forventet_aendring"]].tail(5))
Bemærk at den sidste række får NaN, fordi der simpelthen ikke er en næste dag at kigge frem til. Det er logisk nok, men det er værd at have i baghovedet, når du bygger dine pipelines.
Opret lag-features til machine learning
# Opret flere lag-features
for i in range(1, 8):
df[f"salg_lag_{i}"] = df["salg"].shift(i)
# Se resultatet
lag_kolonner = ["salg"] + [f"salg_lag_{i}" for i in range(1, 8)]
print(df[lag_kolonner].dropna().head())
Lag-features er ærligt talt en af de mest effektive teknikker i tidsseriemodellering. Ved at give din model adgang til historiske værdier kan den lære mønstre og sæsonudsving, som den ellers ville gå glip af. Jeg har oplevet, at bare 3-4 lag-features kan forbedre en models nøjagtighed markant.
Kombinér resample(), rolling() og shift()
Den virkelige magi opstår, når du kombinerer alle tre funktioner. Her er et komplet eksempel, der viser det hele i praksis:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Opret data
np.random.seed(42)
datoer = pd.date_range("2025-01-01", "2025-12-31", freq="D")
salg = 1000 + np.cumsum(np.random.randn(len(datoer)) * 50)
df = pd.DataFrame({"salg": salg}, index=datoer)
# 1. Resample til ugentlige gennemsnit
ugentlig = df["salg"].resample("W").mean()
# 2. Beregn 4-ugers glidende gennemsnit
ugentlig_rolling = ugentlig.rolling(window=4).mean()
# 3. Beregn uge-over-uge ændring
ugentlig_aendring = ugentlig - ugentlig.shift(1)
# Visualiser det hele
fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
axes[0].plot(df.index, df["salg"], alpha=0.3, label="Daglige salg")
axes[0].plot(ugentlig.index, ugentlig, label="Ugentligt gennemsnit")
axes[0].plot(ugentlig_rolling.index, ugentlig_rolling, label="4-ugers glidende gns.", linewidth=2)
axes[0].set_title("Salgsudvikling 2025")
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[1].bar(ugentlig_aendring.index, ugentlig_aendring, width=5, color=ugentlig_aendring.apply(lambda x: "green" if x > 0 else "red"))
axes[1].set_title("Ugentlig ændring i salg")
axes[1].axhline(y=0, color="black", linewidth=0.5)
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("tidsserieanalyse.png", dpi=150)
plt.show()
Denne kode giver dig et professionelt dashboard med to paneler: øverst ser du de daglige salg, ugentlige gennemsnit og et glidende gennemsnit. Nederst vises den ugentlige ændring som et søjlediagram – grøn for stigninger og rød for fald. Det er den slags visualisering, der gør indtryk til et møde.
Nyheder i pandas 3.0 for tidsserieanalyse
Pandas 3.0 (januar 2026) bragte flere vigtige ændringer, som er værd at kende til:
- Mikrosekund som standard – Datetime-data bruger nu mikrosekunder i stedet for nanosekunder. Det betyder, at du kan arbejde med datoer langt uden for det gamle interval (1678-2262), som var en reel begrænsning i mange historiske analyser.
- Opdaterede frekvensforkortelser – Brug
"ME"i stedet for"M","QE"i stedet for"Q", og"YE"i stedet for"Y"for måned/kvartal/årsslut. De gamle forkortelser giver nu fejl. - Copy-on-Write er standard – Alle operationer følger nu Copy-on-Write semantik. Kort sagt: du behøver ikke længere bekymre dig om, hvorvidt du arbejder med en kopi eller en view. Det "bare virker" nu.
- Bedre fejlhåndtering – Tidszoneoperationer, der tidligere kastede kryptiske pytz-fejl, rejser nu standard
ValueErrormed mere forståelige fejlmeddelelser.
Tips til effektiv tidsserieanalyse
Her er de vigtigste ting, jeg har lært af at arbejde med tidsserier i pandas:
- Sortér altid dine data – Kald
df.sort_index()før du brugerresample()ellerrolling(). Seriøst, det sparer dig for så mange hovedpiner. - Kend forskellen:
resample()er til frekvensændringer,rolling()er til glidende statistikker. De kan godt ligne hinanden, men de løser fundamentalt forskellige problemer. - Vælg den rette fyldmetode –
ffill()til den seneste kendte værdi,interpolate()til jævne overgange. Brug ikke bare altid den ene. - Test med
min_periods– Undgå at miste for mange rækker i starten af din tidsserie. Det er ofte bedre at have en omtrentlig beregning end slet ingen. - Brug
pct_change()som genvej i stedet for manuelt at beregne procentvise ændringer medshift(). Det er renere og mindre fejlbehæftet.
Ofte stillede spørgsmål
Hvad er forskellen mellem resample() og rolling() i pandas?
resample() ændrer frekvensen af dine data (fx fra daglige til månedlige), mens rolling() beregner statistikker over et glidende vindue inden for den eksisterende frekvens. Brug resample(), når du vil konvertere mellem tidsfrekvenser, og rolling(), når du vil udglatte data eller beregne løbende statistikker.
Hvordan håndterer jeg manglende datoer i en tidsserie?
Brug resample() til at oprette et komplet datoindeks og derefter ffill(), bfill() eller interpolate() til at fylde hullerne. Du kan også bruge asfreq() til at sikre en fast frekvens og derefter fillna() til at håndtere de manglende værdier.
Kan jeg bruge rolling() med et tidsbaseret vindue?
Ja, helt sikkert. I stedet for at angive window som et heltal kan du bruge en tidsstreng som "7D" for 7 dage eller "2h" for 2 timer. Det kræver bare, at dit indeks er et DatetimeIndex. Eksempel: df["salg"].rolling("7D").mean().
Hvad er de vigtigste ændringer i pandas 3.0 for tidsseriedata?
Den største ændring er, at datetime-data nu bruger mikrosekunder som standardopløsning i stedet for nanosekunder. Derudover er frekvensforkortelserne opdateret (brug "ME" i stedet for "M"), og Copy-on-Write er nu standard, hvilket gør det nemmere at arbejde med DataFrames uden uventede sideeffekter.
Hvordan opretter jeg lag-features til machine learning med pandas?
Brug shift() til at oprette forskudte versioner af dine data. For eksempel opretter df["salg_lag_1"] = df["salg"].shift(1) en kolonne med gårsdagens salg. Du kan oprette flere lags i en løkke og bruge dem som features i din model. Kombiner gerne med rolling() for at tilføje glidende gennemsnit som ekstra features.