Glättung

Zeitreihen
Prognose
Interpretation
R
Python
Gleitender Durchschnitt und exponentielle Glättung als erste Modelle.

Kurzsteckbrief

Fragestellung Wie glättet man Rauschen heraus, und wie prognostiziert man mit einfachen Mitteln?
Zielvariable eine Zeitreihe
Prädiktoren keine, nur die eigene Vergangenheit
Was die Methode liefert geglättete Reihe, Prognose mit Prognoseintervall, Fehlermasse
R-Funktion ma(), ses(), holt(), hw() beziehungsweise ets()
Python-Funktion rolling().mean(), SimpleExpSmoothing(), ExponentialSmoothing()
Verwandte Methoden ARIMA und SARIMA, Zeitreihen: Grundlagen

Wann diese Methode, wann nicht

Typische Fragestellungen: Rauschen aus einer Reihe entfernen, um den Verlauf zu sehen; eine schnelle Prognose für die nächsten Monate; eine Vergleichsgrundlage, gegen die sich ein aufwendigeres Modell beweisen muss.

Glättungsverfahren sind einfach, robust und oft überraschend gut. Ihr Kern ist ein gewichteter Mittelwert der Vergangenheit: Der gleitende Durchschnitt gewichtet alle Werte im Fenster gleich, die exponentielle Glättung gewichtet neuere Werte stärker.

Situation Stattdessen
Die Abhängigkeitsstruktur soll modelliert werden ARIMA und SARIMA
Externe Einflussgrössen sollen einfliessen Regression mit Zeitreihenfehlern
Nur die Struktur soll sichtbar werden, ohne Prognose Zerlegung, siehe Grundlagen
Reihe mit vielen Nullen oder sehr sporadisch eigene Verfahren für spärliche Nachfrage
Umbrüche im Niveau, etwa nach einer Regeländerung Modell mit Strukturbruch, oder Reihe teilen

Grundidee und Modell

Gleitender Durchschnitt. Der Wert an jeder Stelle wird durch das Mittel eines Fensters ersetzt. Bei ungerader Fensterbreite \(m\):

\[\hat{y}_t = \frac{1}{m} \sum_{j=-(m-1)/2}^{(m-1)/2} y_{t+j}\]

Das ist beschreibend, nicht prognostisch: Es braucht Werte aus der Zukunft und verkürzt die Reihe an beiden Enden. Für Saisonbereinigung ist die Fensterbreite die Periode, also 12 bei Monatsdaten.

Exponentielle Glättung. Hier wird nur die Vergangenheit verwendet, und ältere Werte zählen weniger:

\[\hat{y}_{t+1} = \alpha y_t + (1-\alpha)\hat{y}_t\]

Bestandteil Bedeutung
\(\alpha\) Glättungsparameter zwischen 0 und 1
\(\alpha\) nahe 1 reagiert schnell, folgt dem Rauschen
\(\alpha\) nahe 0 reagiert träge, glättet stark
\(\hat{y}_t\) bisherige Schätzung des Niveaus

Die einfache Form kennt nur ein Niveau und prognostiziert deshalb eine waagrechte Linie. Zwei Erweiterungen füllen das auf:

Verfahren Modelliert Prognose
Einfach Niveau konstant
Holt Niveau und Trend Gerade
Holt-Winters Niveau, Trend und Saison Gerade mit Saisonfigur

Alle drei sind Spezialfälle der ETS-Modellfamilie, die in R über ets() automatisch die passende Variante wählt.

Voraussetzungen und ihre Prüfung

Voraussetzung Was sie bedeutet Prüfung Folge bei Verletzung Vorgehen
Genügend Perioden für die Saison mindestens drei, besser vier vollständige Länge prüfen Saisonfigur nicht schätzbar Saison weglassen oder Daten sammeln
Kein Strukturbruch Das Niveau springt nicht dauerhaft Grafik Modell mittelt über zwei Regime Reihe teilen
Passende Zerlegungsart additiv oder multiplikativ richtig gewählt Grafik: wachsen die Zacken Prognoseintervalle falsch logarithmieren oder multiplikative Variante
Residuen ohne Struktur Das Modell hat alles Systematische erfasst ACF der Residuen, Ljung-Box-Test Prognose systematisch daneben Modell erweitern, ARIMA prüfen
Prognose nur über kurze Horizonte Die Unsicherheit wächst schnell Prognoseintervall ansehen Scheingenauigkeit Horizont begrenzen

Output lesen

R, hw() und ets():

Grösse Was sie sagt Faustregel oder Falle
ETS(A,A,A) gewählte Struktur: Fehler, Trend, Saison, je additiv oder multiplikativ N heisst nicht vorhanden
alpha Glättung des Niveaus nahe 1: folgt dem Rauschen; nahe 0: träge
beta Glättung des Trends nahe 0 heisst nahezu konstanter Trend
gamma Glättung der Saison nahe 0: starre Saisonfigur
sigma Streuung der Residuen Grundlage der Prognoseintervalle
AIC, BIC Kennzahlen für Modellvergleiche nur zwischen Modellen auf derselben Reihe vergleichbar
accuracy() Fehlermasse auf Trainings- und Testteil die Zeile Test set zählt
RMSE Wurzel des mittleren quadratischen Fehlers in Einheiten der Reihe, empfindlich für grosse Fehler
MAE mittlerer absoluter Fehler robuster, ebenfalls in Einheiten der Reihe
MAPE mittlerer absoluter prozentualer Fehler ungeeignet bei Werten nahe null

Python, ExponentialSmoothing:

Zugriff Was er liefert Unterschied zu R
params smoothing_level, smoothing_trend, smoothing_seasonal in R alpha, beta, gamma
forecast(h) Punktprognose in R Teil des Ergebnisobjekts
fittedvalues angepasste Werte wie fitted()
sse, aic, bic Anpassungskennzahlen wie in R
keine Prognoseintervalle über ETSModel oder simulate() zu erzeugen in R direkt in hw() enthalten
keine Fehlermasse RMSE und MAE selbst rechnen in R accuracy()

Drei Unterschiede, über die man stolpert. R wählt mit ets() die Modellstruktur automatisch, in Python werden trend und seasonal von Hand gesetzt. Und initialization_method="estimated" gehört gesetzt, sonst verwendet statsmodels ein einfacheres Startverfahren mit anderen Ergebnissen. Und forecast() liefert nur die Punktprognose: Prognoseintervalle gibt es erst über ETSModel aus statsmodels.tsa.exponential_smoothing.ets oder über wiederholte simulate().

Beispiele

Alle Beispiele arbeiten mit derselben konstruierten Absatzreihe: 72 Monate ab Januar 2020 mit linearem Trend, Jahressaison und festem Rauschen. Die letzten zwölf Monate bleiben ungesehen; geschätzt wird auf den ersten 60. Das ist die einzige zulässige Aufteilung bei Zeitreihen: Getestet wird auf der Zukunft, nie auf herausgelosten Zeitpunkten.

Frage und Datenlage

Der gleitende Durchschnitt hat genau eine Einstellung: die Fensterbreite. Sie ist keine technische Nebensache, sondern die Entscheidung darüber, was man sehen will.

Rechnung

absatz_ma3 <- ma(absatz, order = 3)
absatz_ma12 <- ma(absatz, order = 12)

par(mar = c(4, 4, 2, 1))
plot(absatz, col = "grey70", ylab = "Absatz", xlab = "Jahr")
lines(absatz_ma3, col = "#2166ac", lwd = 2)
lines(absatz_ma12, col = "#b2182b", lwd = 2)
legend("topleft", legend = c("Reihe", "MA(3)", "MA(12)"),
       col = c("grey70", "#2166ac", "#b2182b"), lwd = c(1, 2, 2), bty = "n")

round(rbind(
  streuung = c(reihe = sd(absatz), ma3 = sd(absatz_ma3, na.rm = TRUE),
               ma12 = sd(absatz_ma12, na.rm = TRUE)),
  fehlende_werte = c(0, sum(is.na(absatz_ma3)), sum(is.na(absatz_ma12)))
), 1)
               reihe ma3 ma12
streuung        94.5  90 71.3
fehlende_werte   0.0   2 12.0
Abbildung 1: Beispiel 1: die Reihe mit gleitenden Durchschnitten über 3 und 12 Monate.
ma3 = absatz.rolling(3, center=True).mean()
ma12 = absatz.rolling(12, center=True).mean()

fig, achse = plt.subplots()
achse.plot(absatz, color="0.7", label="Reihe")
achse.plot(ma3, color="#2166ac", lw=2, label="MA(3)")
achse.plot(ma12, color="#b2182b", lw=2, label="MA(12)")
achse.set_ylabel("Absatz")
achse.legend(frameon=False)
plt.tight_layout()
plt.show()
Abbildung 2: Beispiel 1: dieselbe Gegenüberstellung in Python.
print(pd.DataFrame({
    "streuung": [absatz.std(ddof=1), ma3.std(ddof=1), ma12.std(ddof=1)],
    "fehlende_werte": [0, int(ma3.isna().sum()), int(ma12.isna().sum())],
}, index=["reihe", "ma3", "ma12"]).round(1))
       streuung  fehlende_werte
reihe      94.5               0
ma3        90.0               2
ma12       72.5              11

Output Zeile für Zeile

Reihe MA(3) MA(12)
Streuung (Python / R) 94.5 90.0 72.5 / 71.3
fehlende Werte (Python / R) 0 2 11 / 12
Fenster Was es glättet Was es stehen lässt
MA(3) das monatliche Rauschen Trend und Saison. Die Streuung sinkt nur von 94.5 auf 90.0, weil die Saison den grössten Teil der Schwankung ausmacht und unberührt bleibt.
MA(12) Rauschen und Saison vollständig nur den Trend. Die Streuung fällt auf rund 72, was übrig bleibt, ist der Anstieg über sechs Jahre.
Ausgabe Wie sie zu lesen ist
Fensterbreite = Periode Der entscheidende Punkt: Ein Fenster von genau einer Periode mittelt jeden Monat des Jahres genau einmal und löscht die Saison damit exakt aus. Jede andere Breite lässt einen Rest stehen.
fehlende Werte: 2 und 11 Ein zentriertes Fenster braucht Nachbarn auf beiden Seiten. Bei MA(3) fehlt je einer am Rand, bei MA(12) je rund ein halbes Jahr.
Die fehlenden Werte am rechten Rand sind der eigentliche Haken: Für die aktuellsten Monate gibt es keinen geglätteten Wert. Ein gleitender Durchschnitt prognostiziert nicht, er beschreibt.

Bei gerader Fensterbreite gibt es ein Zentrierungsproblem: Ein Fenster über 12 Monate hat keinen mittleren Monat. R löst es in ma(order = 12) über ein doppeltes Mittel mit halbem Gewicht an den Enden und verliert dabei je sechs Werte an beiden Rändern, also zwölf. rolling(12, center=True) in Python legt das Fenster dagegen leicht asymmetrisch und verliert elf. Für die Betrachtung ändert das wenig, für exakte Vergleiche zwischen den Sprachen viel.

Interpretation

Wer den Trend sehen will, nimmt die Periodenlänge. Wer den Verlauf inklusive Saison glätten will, ein kurzes Fenster. Beides ist beschreibend; für Prognosen braucht es die exponentielle Glättung.

Frage und Datenlage

Ein Modell mit Niveau, Trend und Saison wird auf 60 Monaten geschätzt und prognostiziert die folgenden zwölf. Die tatsächlichen Werte sind bekannt, aber das Modell hat sie nicht gesehen.

Rechnung

modell_hw <- hw(training, h = 12, seasonal = "additive")
modell_hw$model
Holt-Winters' additive method 

Call:
hw(y = training, h = 12, seasonal = "additive")

  Smoothing parameters:
    alpha = 1e-04 
    beta  = 1e-04 
    gamma = 1e-04 

  Initial states:
    l = 494.6758 
    b = 4.1177 
    s = 11.4019 0.34 -39.623 -54.4855 -58.6967 -70.2048
           -18.1762 16.0993 30.7372 68.748 60.1054 53.7545

  sigma:  23.4454

     AIC     AICc      BIC 
639.6124 654.1839 675.2163 
par(mar = c(4, 4, 2, 1))
plot(modell_hw, ylab = "Absatz", xlab = "Jahr", main = "Holt-Winters")
lines(test, col = "#b2182b", lwd = 2)
legend("topleft", legend = c("Prognose", "tatsächlich"),
       col = c("#2166ac", "#b2182b"), lwd = 2, bty = "n")

round(cbind(prognose = as.numeric(modell_hw$mean),
            unten = as.numeric(modell_hw$lower[, "95%"]),
            oben = as.numeric(modell_hw$upper[, "95%"]),
            tatsaechlich = as.numeric(test),
            fehler = as.numeric(test) - as.numeric(modell_hw$mean)), 1)
      prognose unten  oben tatsaechlich fehler
 [1,]    799.7 753.7 845.6          803    3.3
 [2,]    810.1 764.2 856.1          806   -4.1
 [3,]    822.9 776.9 868.8          796  -26.9
 [4,]    789.0 743.0 834.9          780   -9.0
 [5,]    778.5 732.5 824.4          754  -24.5
 [6,]    748.3 702.4 794.3          741   -7.3
 [7,]    700.4 654.4 746.4          738   37.6
 [8,]    716.0 670.1 762.0          689  -27.0
 [9,]    724.4 678.4 770.3          694  -30.4
[10,]    743.3 697.4 789.3          747    3.7
[11,]    787.4 741.5 833.4          829   41.6
[12,]    802.6 756.6 848.6          788  -14.6
round(accuracy(modell_hw, test)[, c("RMSE", "MAE", "MAPE")], 2)
              RMSE   MAE MAPE
Training set 20.08 15.75 2.57
Test set     23.28 19.16 2.54
Abbildung 3: Beispiel 2: Holt-Winters mit Prognoseintervall, dazu die tatsächlichen Werte.
modell_hw = ExponentialSmoothing(training, trend="add", seasonal="add",
                                 seasonal_periods=12,
                                 initialization_method="estimated").fit()
prognose = modell_hw.forecast(12)

print({k: round(float(v), 4) for k, v in modell_hw.params.items()
       if k.startswith("smoothing")})
{'smoothing_level': 0.0, 'smoothing_trend': 0.0, 'smoothing_seasonal': 0.0}
print("Residuen, Standardabweichung:",
      round(float(modell_hw.resid.std(ddof=1)), 2))
Residuen, Standardabweichung: 19.89
fig, achse = plt.subplots()
achse.plot(training, color="0.5", label="Training")
achse.plot(test, color="#b2182b", lw=2, label="tatsächlich")
achse.plot(prognose, color="#2166ac", lw=2, label="Prognose")
# Die Holt-Winters-Umsetzung in statsmodels liefert keine analytischen
# Prognoseintervalle. Das Band hier ist eine Naeherung aus der Streuung der
# Residuen; exakte Intervalle liefert ETSModel oder simulate().
band = 1.96 * modell_hw.resid.std(ddof=1)
achse.fill_between(test.index, prognose - band, prognose + band,
                   color="#2166ac", alpha=0.15)
achse.set_ylabel("Absatz")
achse.legend(frameon=False)
plt.tight_layout()
plt.show()
Abbildung 4: Beispiel 2: dieselbe Prognose in Python.
def masse(wahr, geschaetzt):
    fehler = wahr - geschaetzt
    return {"RMSE": round(float(np.sqrt((fehler ** 2).mean())), 2),
            "MAE": round(float(np.abs(fehler).mean()), 2),
            "MAPE": round(float(np.abs(fehler / wahr).mean() * 100), 2)}


print(pd.DataFrame({"prognose": prognose.round(1).values,
                    "tatsaechlich": test.values,
                    "fehler": (test.values - prognose.values).round(1)},
                   index=test.index.strftime("%Y-%m")))
         prognose  tatsaechlich  fehler
2025-01     802.5         803.0     0.5
2025-02     813.3         806.0    -7.3
2025-03     812.5         796.0   -16.5
2025-04     789.7         780.0    -9.7
2025-05     779.3         754.0   -25.3
2025-06     749.1         741.0    -8.1
2025-07     701.5         738.0    36.5
2025-08     712.3         689.0   -23.3
2025-09     721.3         694.0   -27.3
2025-10     743.5         747.0     3.5
2025-11     789.7         829.0    39.3
2025-12     805.1         788.0   -17.1
print(masse(test.values, prognose.values))
{'RMSE': 21.6, 'MAE': 17.87, 'MAPE': 2.37}

Output Zeile für Zeile

Die Prognose gegen die tatsächlichen Werte (Python-Ausgabe):

Monat Prognose tatsächlich Fehler
2025-01 802.5 803 +0.5
2025-02 813.3 806 -7.3
2025-03 812.5 796 -16.5
2025-04 789.7 780 -9.7
2025-05 779.3 754 -25.3
2025-06 749.1 741 -8.1
2025-07 701.5 738 +36.5
2025-08 712.3 689 -23.3
2025-09 721.3 694 -27.3
2025-10 743.5 747 +3.5
2025-11 789.7 829 +39.3
2025-12 805.1 788 -17.1

(R schätzt die Glättungsparameter und die Startwerte leicht anders und kommt auf Prognosen von 799.7 bis 802.6 statt 802.5 bis 805.1. Siehe den Kasten unten.)

Kennzahl Wert (Python / R) Bedeutung
RMSE 21.60 / 23.28 Wurzel des mittleren quadratischen Fehlers, in Einheiten der Reihe. Weil quadriert wird, schlagen die beiden Ausreisser im Juli und November überproportional durch.
MAE 17.87 / 19.16 Mittlerer absoluter Fehler, ebenfalls in Einheiten der Reihe. Immer kleiner oder gleich dem RMSE; der Abstand zwischen beiden zeigt, wie ungleich die Fehler verteilt sind.
MAPE 2.37 / 2.54 % Mittlerer absoluter prozentualer Fehler. Nur brauchbar, weil die Reihe weit von null entfernt liegt.
Residuenstreuung im Training 19.89 Fast identisch mit dem RMSE auf dem Testteil. Das ist das erwünschte Bild: Das Modell prognostiziert die ungesehenen Monate ungefähr so gut, wie es die gesehenen anpasst.
Fehler mit beiden Vorzeichen +0.5 bis -27.3 Kein systematischer Versatz nach oben oder unten. Läge die Prognose durchgehend zu hoch, wäre der Trend falsch geschätzt.

Die Glättungsparameter fallen in Python auf null. Das Verfahren entscheidet also, dass Niveau, Trend und Saisonfigur gar nicht nachgeführt werden müssen und das ist richtig, denn die Reihe wurde mit einem festen Trend und einer festen Saisonfigur konstruiert. Aus Holt-Winters wird damit faktisch eine Regression mit Gerade und Saisondummies.

R lässt exakte Nullen nicht zu (die Optimierung läuft in den Grenzen 0.0001 bis 0.9999) und schätzt deshalb alle drei Parameter zu 0.0001. Zusätzlich bestimmt R die Startwerte anders, sichtbar an Initial states mit einem Niveau von 494.68 und einem Trend von 4.12, die den eingebauten 500 und 4 sehr nahe kommen. Daraus ergeben sich leicht andere Prognosen und ein etwas höherer Testfehler (RMSE 23.3 statt 21.6).

In echten Daten sind Werte nahe null ein Hinweis, dass ein einfacheres Modell genügt und Werte nahe eins einer, dass das Modell dem Rauschen folgt.

Interpretation und Ergebnissatz

Holt-Winters mit additivem Trend und additiver Saison (geschätzt auf 60 Monaten) prognostiziert die folgenden zwölf Monate mit einem RMSE von 21.6 und einem MAPE von 2.4 Prozent. Alle tatsächlichen Werte liegen im 95-Prozent-Prognoseintervall; die grössten Abweichungen betreffen Juli (+36.5) und November (+39.3).

Frage und Datenlage

Eine Prognose ist nur so gut wie ihr Vergleich. Vier Kandidaten auf denselben Daten: die naive saisonale Prognose (der Wert von vor zwölf Monaten), die einfache exponentielle Glättung, Holt (Niveau und Trend) und Holt-Winters (zusätzlich Saison).

Rechnung

naiv <- snaive(training, h = 12)
modell_ses <- ses(training, h = 12)
modell_holt <- holt(training, h = 12)

par(mar = c(4, 4, 2, 1))
plot(test, col = "black", lwd = 2, ylim = c(600, 900),
     ylab = "Absatz", xlab = "Jahr", main = "Testjahr 2025")
lines(naiv$mean, col = "grey50", lwd = 2)
lines(modell_ses$mean, col = "#1b7837", lwd = 2)
lines(modell_holt$mean, col = "#b2182b", lwd = 2)
lines(modell_hw$mean, col = "#2166ac", lwd = 2)
legend("bottomleft",
       legend = c("tatsächlich", "naiv", "einfach", "Holt", "Holt-Winters"),
       col = c("black", "grey50", "#1b7837", "#b2182b", "#2166ac"),
       lwd = 2, bty = "n", cex = 0.8)

spalten <- c("RMSE", "MAE", "MAPE")
round(rbind(
  naiv_saisonal = accuracy(naiv, test)["Test set", spalten],
  einfach = accuracy(modell_ses, test)["Test set", spalten],
  holt = accuracy(modell_holt, test)["Test set", spalten],
  holt_winters = accuracy(modell_hw, test)["Test set", spalten]
), 2)
               RMSE   MAE MAPE
naiv_saisonal 50.65 42.92 5.58
einfach       43.54 36.58 4.92
holt          45.42 37.26 5.03
holt_winters  23.28 19.16 2.54
Abbildung 5: Beispiel 3: die vier Prognosen nebeneinander.
naiv = pd.Series(training.iloc[-12:].values, index=test.index)
modell_ses = SimpleExpSmoothing(training,
                                initialization_method="estimated").fit()
modell_holt = ExponentialSmoothing(training, trend="add",
                                   initialization_method="estimated").fit()

kandidaten = {
    "naiv_saisonal": naiv.values,
    "einfach": modell_ses.forecast(12).values,
    "holt": modell_holt.forecast(12).values,
    "holt_winters": prognose.values,
}

print(pd.DataFrame({name: masse(test.values, werte)
                    for name, werte in kandidaten.items()}).T)
                RMSE    MAE  MAPE
naiv_saisonal  50.65  42.92  5.58
einfach        43.54  36.58  4.92
holt           56.94  45.74  6.25
holt_winters   21.60  17.87  2.37
print("alpha der einfachen Glättung:",
      round(float(modell_ses.params["smoothing_level"]), 3))
alpha der einfachen Glättung: 1.0

Output Zeile für Zeile

Modell RMSE (Python / R) MAPE (Python / R) Was es kann
naiv saisonal 50.65 / 50.65 5.58 / 5.58 % Saison ja, Trend nein
einfache Glättung 43.54 / 43.54 4.92 / 4.92 % nur ein Niveau
Holt 56.94 / 45.42 6.25 / 5.03 % Niveau und Trend, keine Saison
Holt-Winters 21.60 / 23.28 2.37 / 2.54 % Niveau, Trend und Saison

Die beiden Spalten unterscheiden sich genau dort, wo Parameter geschätzt werden müssen: Bei der naiven Prognose und der einfachen Glättung stimmen die Zahlen exakt überein, bei Holt und Holt-Winters nicht. Die Rangfolge ist in beiden Sprachen dieselbe.

Beobachtung Erklärung
Holt-Winters ist mehr als doppelt so gut wie alles andere Die Reihe hat Trend und Saison; nur ein Modell, das beides kennt, kann sie prognostizieren.
Holt ist schlechter als die einfache Glättung In beiden Sprachen. Überraschend, aber erklärbar: Holt schreibt den Trend als Gerade fort und ignoriert die Saison, die Prognose läuft also mitten durch die Saisonzacken hindurch und verfehlt Hoch- wie Tiefpunkte. Die einfache Glättung liegt mit ihrer waagrechten Linie zufällig näher. Ein zusätzlicher Modellbestandteil verbessert nicht automatisch.
Die einfache Glättung schätzt \(\alpha = 1\) Damit ist sie die naive Prognose “der letzte Wert”, die Vorhersage für alle zwölf Monate ist der Dezemberwert von 2024.
Die naive saisonale Prognose ist der eigentliche Massstab: Sie kostet nichts und erreicht 5.58 Prozent MAPE. Ein Modell, das sie nicht deutlich schlägt, ist den Aufwand nicht wert.

Warum die naive Prognose in jeden Bericht gehört, und zwar als Zeile in derselben Tabelle: Ein RMSE von 21.6 sagt für sich genommen nichts. Erst der Vergleich mit den 50.7 der naiven saisonalen Prognose macht daraus die Aussage “das Modell halbiert den Fehler”.

Formalisiert wird das im MASE (Mean Absolute Scaled Error), dem Verhältnis des MAE zum MAE der naiven Prognose: Werte unter eins heissen besser als naiv. R gibt ihn in accuracy() mit aus.

Interpretation und Ergebnissatz

Holt-Winters mit additivem Trend und additiver Saison erreicht auf den letzten zwölf Monaten einen RMSE von 21.6 (MAPE 2.4 Prozent) gegenüber 50.7 (5.6 Prozent) bei der naiven saisonalen Prognose. Modelle ohne Saisonkomponente bleiben deutlich zurück (Holt: RMSE 56.9).

Frage und Datenlage

Der Glättungsparameter \(\alpha\) steuert, wie stark das Modell auf den jüngsten Wert reagiert. Was passiert, wenn man ihn selbst wählt statt ihn schätzen zu lassen?

Rechnung

alphas <- c(0.05, 0.5, 0.95)
farben <- c("#1b7837", "#b2182b", "#2166ac")

par(mar = c(4, 4, 2, 1))
plot(training, col = "grey70", ylab = "Absatz", xlab = "Jahr",
     main = "Geglättetes Niveau")
for (i in seq_along(alphas)) {
  m <- ses(training, h = 12, alpha = alphas[i], initial = "simple")
  lines(fitted(m), col = farben[i], lwd = 2)
}
legend("topleft", legend = paste("alpha =", alphas), col = farben,
       lwd = 2, bty = "n")

spalten <- c("RMSE", "MAE")
ergebnisse <- t(sapply(alphas, function(a) {
  m <- ses(training, h = 12, alpha = a, initial = "simple")
  accuracy(m, test)["Test set", spalten]
}))
rownames(ergebnisse) <- paste("alpha", alphas)
round(rbind(ergebnisse,
            geschaetzt = accuracy(modell_ses, test)["Test set", spalten]), 2)
             RMSE   MAE
alpha 0.05 105.68 96.84
alpha 0.5   45.10 37.56
alpha 0.95  43.31 36.58
geschaetzt  43.54 36.58
Abbildung 6: Beispiel 4: dieselbe Glättung mit alpha = 0.05, 0.5 und 0.95.
for a in [0.05, 0.5, 0.95]:
    m = SimpleExpSmoothing(training, initialization_method="known",
                           initial_level=float(training.iloc[0])).fit(
                               smoothing_level=a, optimized=False)
    print(f"alpha = {a:<5}", masse(test.values, m.forecast(12).values))
alpha = 0.05  {'RMSE': 105.68, 'MAE': 96.84, 'MAPE': 12.4}
alpha = 0.5   {'RMSE': 45.1, 'MAE': 37.56, 'MAPE': 4.88}
alpha = 0.95  {'RMSE': 43.31, 'MAE': 36.58, 'MAPE': 4.91}
print("geschätzt   ", masse(test.values, modell_ses.forecast(12).values))
geschätzt    {'RMSE': 43.54, 'MAE': 36.58, 'MAPE': 4.92}

Output Zeile für Zeile

\(\alpha\) RMSE MAE Verhalten
0.05 105.68 96.84 Das Niveau folgt dem Trend viel zu träge und bleibt am Ende weit unter der Reihe. Der Fehler ist fünfmal so gross wie nötig.
0.5 45.10 37.56 Schon nahe am Optimum, die Kurve ist flach.
0.95 43.31 36.58 Minimal besser als der geschätzte Wert.
geschätzt (\(\alpha = 1\)) 43.54 36.58

R und Python liefern hier identische Zahlen, weil \(\alpha\) vorgegeben ist und der Startwert in beiden Fällen auf den ersten Beobachtungswert gesetzt wurde.

Beobachtung Erklärung
Ein kleines \(\alpha\) ist hier klar schlecht Bei einer Reihe mit Trend muss das Niveau ständig nachgeführt werden. Mit \(\alpha = 0.05\) zählt der jüngste Wert nur zu fünf Prozent, und das geglättete Niveau hinkt Jahre hinterher.
Ein grosses \(\alpha\) ist hier fast optimal Weil die Reihe stark steigt, ist “der letzte Wert” die beste Schätzung, die ein Modell ohne Trendkomponente abgeben kann.
Der geschätzte Wert ist \(\alpha = 1\) Die Optimierung landet am Rand des zulässigen Bereichs. Das ist selbst ein Befund: Das Modell passt nicht. Eine einfache Glättung, die \(\alpha = 1\) wählt, sagt “ich kann nichts glätten”, die Reihe braucht eine Trendkomponente.

Die übliche Lehrbuchwarnung “\(\alpha\) nahe eins heisst, das Modell folgt dem Rauschen” gilt hier nicht und das zeigt, warum Faustregeln über Parameterwerte mit Vorsicht zu geniessen sind. Ein \(\alpha\) nahe eins bedeutet nur: Der jüngste Wert ist die beste verfügbare Information. Bei einer rauschenden Reihe ohne Trend ist das ein Warnzeichen, bei einer stark steigenden Reihe die richtige Antwort.

Deshalb wird \(\alpha\) geschätzt und nicht gesetzt: Die Programme minimieren den Fehler auf den Trainingsdaten. Und deshalb wird das Ergebnis gedeutet statt gegen eine Schwelle geprüft.

Interpretation

Ein von Hand gesetzter Parameter liegt fast immer daneben. Interessanter als sein Wert ist, was er über das Modell verrät, hier, dass die einfache Glättung für diese Reihe die falsche Wahl ist.

Frage und Datenlage

Der letzte Schritt jeder Zeitreihenmodellierung: Steckt in den Residuen noch Struktur? Wenn ja, hat das Modell etwas übersehen, und die Prognoseintervalle sind zu schmal.

Rechnung

checkresiduals(modell_hw$model)

    Ljung-Box test

data:  Residuals from Holt-Winters' additive method
Q* = 19.09, df = 12, p-value = 0.08637

Model df: 0.   Total lags used: 12
Abbildung 7: Beispiel 5: Residuen des Holt-Winters-Modells, Verlauf, ACF und Verteilung.
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.stats.diagnostic import acorr_ljungbox

residuen = modell_hw.resid

fig, achsen = plt.subplots(1, 3, figsize=(7.5, 3))
achsen[0].plot(residuen, color="#2166ac")
achsen[0].axhline(0, ls="--", color="black", lw=0.8)
achsen[0].set_title("Verlauf")
plot_acf(residuen, ax=achsen[1], lags=20, title="ACF")
achsen[2].hist(residuen, bins=15, color="#2166ac", edgecolor="white")
achsen[2].set_title("Verteilung")
plt.tight_layout()
plt.show()
Abbildung 8: Beispiel 5: dieselbe Prüfung in Python.
print(acorr_ljungbox(residuen, lags=[6, 12, 18], return_df=True).round(4))
    lb_stat  lb_pvalue
6   12.3529     0.0545
12  21.4998     0.0435
18  27.1134     0.0769
print("Mittelwert der Residuen:", round(float(residuen.mean()), 3))
Mittelwert der Residuen: 0.0
print("Standardabweichung:    ", round(float(residuen.std(ddof=1)), 3))
Standardabweichung:     19.891

Output Zeile für Zeile

Der Ljung-Box-Test über zwölf Abstände:

Umgebung Teststatistik Freiheitsgrade p
Python, acorr_ljungbox 21.50 12 0.044
R, checkresiduals 19.09 12 0.086

Zusätzlich über andere Abstandszahlen (Python): p = 0.055 bei 6 Abständen und p = 0.077 bei 18.

Prüfpunkt Befund Wie er zu lesen ist
Ljung-Box 0.044 in Python, 0.086 in R Der Test prüft, ob irgendeine der ersten zwölf Autokorrelationen von null verschieden ist. Die Nullhypothese lautet “keine Autokorrelation”; ein kleiner p-Wert heisst also, dass Struktur übrig ist. Hier: ein Grenzfall.
ACF der Residuen ein bis zwei Balken knapp ausserhalb Bei 20 angezeigten Abständen ist ein Ausreisser zufällig zu erwarten. Entscheidend ist, ob er an einer deutbaren Stelle liegt, etwa bei 12, was auf eine unerfasste Saison hinweisen würde.
Verlauf schwankt um null, keine Trichterform Keine wachsende Streuung, kein Muster.
Verteilung ungefähr symmetrisch Grundlage der Prognoseintervalle; starke Schiefe würde sie unbrauchbar machen.
Mittelwert der Residuen nahe null Ein deutlich von null verschiedener Mittelwert wäre ein systematischer Versatz.

Ein Grenzfall wie p = 0.044 trägt kein Urteil. Drei Gründe:

  1. Der Test wird mit wachsender Reihenlänge empfindlich und findet dann auch praktisch bedeutungslose Autokorrelationen.
  2. Die Anzahl geprüfter Abstände ist eine Wahl, hier ergeben 6, 12 und 18 Abstände p-Werte von 0.055, 0.044 und 0.077.
  3. R und Python kommen auf verschiedene Teststatistiken (19.09 gegen 21.50) und damit auf 0.086 gegen 0.044, einmal über und einmal unter der Schwelle. Der Grund liegt in den leicht verschiedenen Modellanpassungen. Ein Befund, der an der Wahl der Umgebung kippt, ist kein Befund.

Was zählt, ist die Grafik: Liegt der auffällige Balken bei Abstand 12, fehlt Saison. Liegt er irgendwo, ist es Zufall. Und wenn wirklich Struktur übrig ist, ist ARIMA der nächste Schritt.

Interpretation und Ergebnissatz

Die Residuen des Modells schwanken ohne erkennbares Muster um null (Standardabweichung 19.9); der Ljung-Box-Test über zwölf Abstände liegt mit p = 0.04 bis 0.09 je nach Umgebung im Grenzbereich, ohne dass die ACF eine deutbare Struktur zeigt. Die Prognoseintervalle sind damit vertretbar.

Interpretationsfallen

  • Fehlermasse auf den Trainingsdaten sagen nichts. Das Modell hat diese Werte gesehen; bewertet wird auf dem Testteil.
  • Zufällige Aufteilung ist bei Zeitreihen falsch. Getestet wird auf der Zukunft, sonst nutzt das Modell Information, die es nicht haben dürfte.
  • Eine Prognose ohne Intervall ist unvollständig. Und das Intervall wächst mit dem Horizont, meist schneller als man erwartet.
  • Ein gleitender Durchschnitt prognostiziert nicht. Er ist beschreibend und braucht Werte aus der Zukunft.
  • MAPE ist tückisch. Bei Werten nahe null explodiert er, und er bestraft Überschätzung anders als Unterschätzung.
  • Alpha nahe eins ist keine gute Anpassung, sondern Folgen des Rauschens.
  • Glättung erklärt nichts. Sie beschreibt die Reihe aus sich selbst; für Ursachen braucht es Einflussgrössen.

Ergebnis berichten

Vorlage für den Ergebnissatz:

Die Reihe wurde mit [Verfahren] modelliert (geschätzt auf [Zeitraum], Test auf den folgenden [h] Perioden). Die Prognose erreicht einen RMSE von [Wert] und einen MAPE von [Wert] Prozent, gegenüber [Wert] beziehungsweise [Wert] bei der naiven saisonalen Prognose. Die Residuen zeigen im Ljung-Box-Test p = [p].

Genannt werden: Verfahren und Modellstruktur, Trainings- und Testzeitraum, Fehlermasse auf dem Testteil, der Vergleich mit einer naiven Prognose, das Ergebnis der Residuenprüfung und der Prognosehorizont mit Intervall.

Beispiel für eine Ergebnistabelle:

Modell RMSE MAE MAPE
naiv saisonal
einfache Glättung
Holt-Winters

Abgrenzung zu verwandten Methoden

Methode Wann diese statt der Glättung
ARIMA und SARIMA die Abhängigkeitsstruktur selbst interessiert, Modellordnung soll begründet werden
Regression mit Zeitreihenfehlern externe Einflussgrössen sollen einfliessen
Zerlegung nur Beschreibung, keine Prognose
Verfahren für spärliche Nachfrage viele Nullen in der Reihe
Modelle mit Strukturbruch dauerhafter Niveausprung in der Reihe

Verständnisfragen

Ein Modell erreicht auf den Trainingsdaten einen RMSE von 8 und auf dem Testteil von 45. Was folgt?

Das Modell ist an die Trainingsdaten angepasst und prognostiziert schlecht
Richtig. Der Testteil ist der einzige aussagekräftige Wert. Ein grosser Unterschied deutet auf ein zu reaktives Modell, etwa ein Alpha nahe eins.
Der Testteil ist zu kurz
Möglich, aber der Unterschied ist zu gross, um allein daran zu liegen.
Beide Werte gemeinsam beurteilen
Der Trainingswert trägt zur Beurteilung der Prognose nichts bei.

Warum darf man Zeitreihen nicht zufällig in Trainings- und Testdaten aufteilen?

Weil das Modell dann Werte aus der Zukunft kennt
Richtig. Liegen Testzeitpunkte zwischen Trainingszeitpunkten, nutzt das Modell Information, die zum Prognosezeitpunkt nicht vorlag. Der Fehler wird dadurch zu klein.
Weil die Reihenfolge für die Rechnung nötig ist
Auch das, aber der entscheidende Punkt ist die Information.
Weil die Stichprobe sonst zu klein wird
Die Grösse ändert sich nicht.

Ein gleitender Durchschnitt über zwölf Monate wird für eine Prognose verwendet. Was ist das Problem?

Er ist beschreibend und braucht Werte aus der Zukunft
Richtig. Ein zentrierter gleitender Durchschnitt mittelt um jeden Zeitpunkt herum, also auch über spätere Werte, und endet deshalb sechs Monate vor dem Reihenende.
Zwölf Monate sind ein zu langes Fenster
Die Fensterbreite ist hier nicht das Problem.
Er glättet die Saison weg
Das tut er, und für die Trendbetrachtung ist es gewollt.

Der Ljung-Box-Test auf den Residuen ergibt p = 0.003. Was heisst das?

In den Residuen steckt noch Abhängigkeit, das Modell hat etwas übersehen
Richtig. Die Folge sind zu schmale Prognoseintervalle. Zu prüfen ist, ob Saison, Trend oder eine Abhängigkeitsstruktur fehlt, die ein ARIMA-Modell erfassen würde.
Das Modell passt gut, der p-Wert ist klein
Die Richtung ist vertauscht: Die Nullhypothese lautet keine Autokorrelation.
Die Residuen sind nicht normalverteilt
Das prüft ein anderer Test.

Verlinkte Ressourcen