Vorhersagetechniken

Nutzung fortge­schrit­tener Vorher­sa­ge­tech­niken mit Stats­Fo­re­cast: Eine Fallstudie

Share post:

EINLEI­TUNG

Für einen Kunden haben wir kürzlich eine inter­es­sante Frage­stel­lung gelöst: Seltene Ereig­nisse (der Ausfall bestimmter Bauteile) sollten anhand von unregel­mä­ßigen histo­ri­schen Daten vorher­ge­sagt werden. Die Vorher­sage sollte gruppiert werden nach Kunde und Bauteil. Als ich mich mit der Aufgabe beschäf­tigte, habe ich das von Nixtla entwi­ckelte Stats­Fo­re­cast-Paket entdeckt und unter­sucht. Es bietet eine Reihe von robusten Vorher­sa­ge­mo­dellen, die speziell für die Verar­bei­tung von unregel­mä­ßigen Daten entwi­ckelt wurden. Was das Stats­Fo­re­cast-Paket kann und welche Bewer­tungs­me­thoden eigent­lich für unregel­mä­ßige Daten genutzt werden sollten, erfahrt ihr in diesem Blogpost!

Die Heraus­for­de­rung

Problem­stel­lung

Unser Kunde stellte uns einen Daten­satz zur Verfü­gung, der sich über mehrere Jahre erstreckte und vergan­gene Ereig­nisse für verschie­dene Kunden und Bauteile enthielt. Die Ereig­nisse traten selten auf, sodass der Daten­satz sehr unregel­mäßig war. Unser Ziel war es, voraus­zu­sagen, wann diese Ereig­nisse in der Zukunft auftreten werden, um eine bessere Ressour­cen­zu­wei­sung und -planung zu ermög­li­chen. Obwohl die Zukunft mathe­ma­tisch unabhängig von der Vergan­gen­heit ist, konnten wir mit den Vorher­sagen aus diesem Daten­satz eine vernünf­tige Vorher­sage erzeugen.

Beschrei­bung der Daten

Unregel­mä­ßiger Charakter: Die Ereig­nisse traten sehr selten auf, was zu spora­di­schen, unregel­mä­ßigen Daten­punkten führte. Mehrere Kunden und Bauteile: Die Vorher­sagen sollten für verschie­dene Kunden und Teile gemacht werden, was einen segmen­tierten Ansatz erfor­derte.

Beispiel:

Auf der rechten Seite sieht man ein Beispiel dafür, wie die Daten aussehen, wenn sie verar­beitet und für die Prognose vorbe­reitet sind.

‚ds‘ ist ein wöchent­li­cher Zeitstempel (wir wollen wöchent­liche Prognosen), ‚unique_id‘ steht für die Gruppe (verkettet aus Bauteil und Kunde) und ‚y‘ bezeichnet die Anzahl der Ereig­nisse in dieser bestimmten Woche.

| ds         | unique_id           | y   |
| ---        | ---                 | --- |
| 2023-07-24 | part_a, customer_a  | 1   |
| 2022-02-28 | part_b, customer_b  | 2   |
| 2024-04-22 | part_b, customer_c  | 1   |
| 2024-03-18 | part_b, customer_d  | 1   |
| 2024-03-25 | part_b, customer_d  | 0   |
| …          | …                   | …   |
| 2017-04-24 | part_y, customer_a  | 0   |
| 2017-05-01 | part_y, customer_a  | 1   |
| 2017-05-08 | part_y, customer_a  | 0   |
| 2017-05-15 | part_y, customer_a  | 1   |
| 2021-04-26 | part_b, customer_z  | 2   |

Lösungs­an­satz

Um diese Heraus­for­de­rung zu bewäl­tigen, habe ich mehrere Ansätze unter­sucht, bevor ich mich für das Stats­Fo­re­cast-Paket entschied. Im Folgenden sind einige der von mir unter­suchten Techniken aufge­führt:

1. Klassi­sche Dekom­po­si­tion
Bei dieser Technik werden Zeitrei­hen­daten in Trend-, Saison- und Restkom­po­nenten zerlegt. Sie war zwar nützlich für das Verständnis von Mustern, konnte aber nicht gut mit Unter­bre­chungen umgehen.
 
2. Optimiertes Modell von Croston
Das optimierte Modell von Croston ist eine fortschritt­liche Progno­se­me­thode für unregel­mä­ßige Nachfra­ge­daten, die exponen­ti­elle Glättung zur Erfas­sung von Trends und saiso­nalen Schwan­kungen mit separaten Schät­zungen für das Auftreten und die Größe von Nicht-Null-Nachfrage kombi­niert. Dieser Ansatz hilft, Über- und Unter­pro­gnosen auszu­glei­chen und liefert genauere Vorher­sagen für spora­di­sche Nachfra­ge­muster.
 
3. IMAPA (Inter­mit­tent Multiple Aggre­ga­tion Predic­tion Algorithm)
Der Inter­mit­tent Multiple Aggre­ga­tion Predic­tion Algorithm (IMAPA) ist ein Algorithmus, der zukünf­tige Werte unregel­mä­ßiger Zeitreihen vorher­sagt, indem er die Zeitrei­hen­werte in regel­mä­ßigen Abständen aggre­giert und dann ein belie­biges Progno­se­mo­dell, wie z. B. eine optimierte einfache exponen­ti­elle Glättung (SES), verwendet, um diese aggre­gierten Werte vorher­zu­sagen. IMAPA ist robust gegen­über fehlenden Daten, rechne­risch effizient und einfach zu imple­men­tieren, so dass es für verschie­dene Aufgaben im Bereich der spora­di­schen Zeitrei­hen­pro­gnose geeignet ist.
 
4. ADIDA (Aggre­gate-Disag­gre­gate Inter­mit­tent Demand Approach)
Das ADIDA-Modell verwendet Simple Exponen­tial Smoot­hing (SES) auf zeitlich aggre­gierten Daten, um die unregel­mä­ßige Nachfrage zu prognos­ti­zieren, wobei die Nachfrage in Bereiche mit einer mittleren Inter­vall­größe aggre­giert wird. Die Vorher­sagen werden dann auf die ursprüng­li­chen Zeiträume disagg­re­giert.
 
5. TSB (Teunter-Syntetos-Babai)
Das TSB-Modell ist eine fortschritt­liche Methode, die in der Bestands­ver­wal­tung und der Nachfra­ge­pro­gnose für Produkte mit inter­mit­tie­render Nachfrage einge­setzt wird und als Erwei­te­rung des Modells von Croston vorge­schlagen wurde. Es aktua­li­siert die Nachfra­ge­wahr­schein­lich­keit in jeder Periode auch dann, wenn keine Nachfrage auftritt, wodurch es sich besser für das Manage­ment des Veral­te­rungs­ri­sikos bei Daten mit vielen Nullen eignet.

DAS STATS­FO­RE­CAST PAKET

Nachdem ich diese Modelle unter­sucht hatte, stieß ich auf das Stats­Fo­re­cast Paket, das Imple­men­tie­rungen aller oben genannten Methoden und weiteren in nur wenigen Zeilen Code bietet. Hier ist ein sehr einfa­ches Beispiel, wie man es imple­men­tieren könnte:
import polars as pl
from statsforecast import StatsForecast
from statsforecast.models import ADIDA, CrostonClassic, CrostonOptimized, CrostonSBA, IMAPA, TSB

# Create the models (can be extended with hyperparameter tuning)
models = [
    ADIDA(),
    CrostonClassic(),
    CrostonOptimized(),
    CrostonSBA(),
    IMAPA(),
    TSB(alpha_d=0.2, alpha_p=0.2)
]

# Store the names of the models to use them later in polars colum selection
model_names = [m.alias for m in models]

# We want weekly forecasts
sf = StatsForecast(models=models, freq='1w', n_jobs=-1, verbose=True)

sf.fit(train)
FORECAST_HORIZON = 52   # weeks

forecasts_df = sf.predict(h=FORECAST_HORIZON)

# join the forecasts with the target values ('y') from the test data for evaluation
forecasts_df = (
    forecasts_df
    .join(test, on=["unique_id", "ds"], how="left")
    .fill_null(0)
)

Dieser kurze Codeschnipsel war die Grund­lage dafür, dass ich schnell mehrere Modelle für den Daten­satz erstellen und ausführen konnte. Tatsäch­lich haben wir anschlie­ßend noch die Hyper­pa­ra­meter jedes Modells mit bewährten Methoden angepasst.

Handha­bung gleich­mä­ßiger Vertei­lungen

Die von diesen Modellen erstellten Prognosen waren ursprüng­lich diskrete Gleich­ver­tei­lungen über den Progno­se­ho­ri­zont. Um die Anfor­de­rung zu erfüllen, bestimmte geschätzte Zeitpunkte für vorher­ge­sagte Ereig­nisse zu haben, habe ich folgenden Ansatz gewählt:

Kumulierte Wahrschein­lich­keiten Modulo 1

Um dieses Problem zu lösen, habe ich alle Wahrschein­lich­keiten über die Zeit kumuliert und den kumula­tiven Wert modulo 1 berechnet. So konnte ich die ursprüng­li­chen Wahrschein­lich­keits­werte mit diesen neuen Werten verglei­chen. Wenn die ursprüng­liche Wahrschein­lich­keit größer oder gleich dem Modulo-Wert ist, deutet dies auf ein Ereignis zu diesem Zeitpunkt hin.

Imple­men­tie­rung-Schritte

1. Wahrschein­lich­keiten kumulieren: Addition der Wahrschein­lich­keiten für jeden Progno­se­zeit­raum.
2. Modulo 1 berechnen: Berech­nung des kumulierten Wert modulo 1.
3. Ereig­nisse vorher­sagen: Wenn die ursprüng­liche Wahrschein­lich­keit größer oder gleich dem Modulo-Wert ist, sagt dies ein Ereignis zu diesem Zeitpunkt voraus.

Codebei­spiel

Das folgende Python-Codefrag­ment veran­schau­licht diesen Vorgang:
forecasts_df = (
    forecasts_df
    # Calculate the value with modulo 1, to obtain some kind of 'virtual' probability
    .with_columns(pl.col(model_names).cum_sum().mod(1).over("unique_id").name.suffix("_cum"))
    # If the modulo values are smaller than the original, this indicates 
    # an event with the demand from the original column rounded to the next integer.
    # Otherwise we do not have an event and set the value to None.
    .with_columns([
        pl.when(pl.col(m) >= pl.col(f"{m}_cum"))
        .then(pl.col(m).ceil())
        .otherwise(None)
        for m in model_names]
    )
    # Here we do not need the cumulated values anymore
    .drop(pl.col("^*_cum$"))
)

Gültig­keit des Ansatzes

Dieser Ansatz wandelt eine Gleich­ver­tei­lung effektiv in diskrete Ereig­nis­pro­gnosen auf der Grund­lage kumula­tiver Wahrschein­lich­keiten um. Durch den Vergleich jeder prognos­ti­zierten Wahrschein­lich­keit mit dem entspre­chenden kumula­tiven Wert modulo 1 können wir bestimmte Zeitpunkte identi­fi­zieren, an denen Ereig­nisse wahrschein­lich eintreten werden.

Bewer­tung der Leistung

Zur Bewer­tung der Vorher­sa­ge­er­geb­nisse befasse ich mich mit zwei wesent­li­chen Metriken für unregel­mä­ßige, spora­di­sche Vorher­sagen: dem Kumulierten Vorher­sa­ge­fehler (CFE) und den Stock-keeping-oriented Predic­tion Error Costs (SPEC). Standard­me­triken wie MAPE oder RMSE sind für diese Prognosen nicht wirklich geeignet, da sie zeitliche Verschie­bungen oder kosten­be­zo­gene Aspekte nicht ausrei­chend berück­sich­tigen.
 

Kumulierter Vorher­sa­ge­fehler (CFE)

Der kumulierte Vorher­sa­ge­fehler (CFE) misst die kumula­tive Summe der Diffe­renz zwischen tatsäch­li­chen Werten (`y`) und prognos­ti­zierten Werten (`<Modell>`). Er gibt Aufschluss darüber, wie gut die Prognosen eines Modells mit den tatsäch­li­chen Ergeb­nissen im Laufe der Zeit überein­stimmen.
 
Imple­men­tie­rung
def cfe(df: pl.DataFrame, model_names: list[str]) -> pl.DataFrame:
    """Calculate the Cumulative Forecast Error (CFE) for multiple models in a Polars DataFrame.
    
    This function calculates the Cumulative Forecast Error (CFE) for each forecast model in a DataFrame. CFE is defined as the cumulative sum of the difference between actual values (`y`) and forecast values (``). The result includes the minimum, maximum, and last CFE value for each unique identifier.

    The function takes a Polars DataFrame with the following structure:
    - `unique_id`: A unique identifier for each data point
    - `y`: The actual target value
    - `model_names` (list of strings): Column names representing different model predictions

    Parameters:
        df (pl.DataFrame): Input DataFrame containing the data. It must include a column named `y` representing the actual values, and columns named `` for each model in the `model_names` list.
        
        model_names (list[str]): A list of strings representing the names of the forecast models.

    Returns:
        pl.DataFrame: Output DataFrame with CFE results. It contains columns for `unique_id`, `model`, and statistics such as `cfe_min` (minimum CFE), `cfe_max` (maximum CFE), and `cfe_last` (last  CFE) for each unique identifier and model.
    """
    df = (
        df
        # Calculate Cumulative Forecast Error for each model per unique_id
        .with_columns((pl.col(model_names) - pl.col("y")).cum_sum().over("unique_id"))
        # Unpivot the DataFrame to have a single column for CFE values and corresponding model names
        .unpivot(model_names, index="unique_id", variable_name="model", value_name="cfe")
        # Group by unique_id and model, then aggregate to get min, max, and last CFE value
        .group_by("unique_id", "model")
        .agg(
            pl.min("cfe").alias("cfe_min"),
            pl.max("cfe").alias("cfe_max"),
            pl.last("cfe").abs().alias("cfe_last")
        )
    )
    return df

cfe_df = forecasts_df.pipe(cfe, model_names=model_names)

Stock-keeping -oriented Predic­tion Error Costs(SPEC)

Die Stock-keeping-oriented Predic­tion Error Costs (SPEC) misst die Vorher­sa­ge­ge­nau­ig­keit durch den Vergleich von tatsäch­li­chen Ereig­nissen und Vorher­sage in Form von virtuell entstan­denen Kosten über den Vorher­sa­ge­ho­ri­zont. Wenn die Prognosen Ereig­nisse vor dem Eintreten des Zielereig­nisses vorher­sagen, entstehen Bestands­er­hal­tungs­kosten. Im umgekehrten Fall erhalten wir Oppor­tu­ni­täts­kosten. Die Bezie­hung zwischen beiden Fehlern wird mit $\alpha \in [0, 1]$ gewichtet. Höhere Alphas gewichten die Oppor­tu­ni­täts­kosten stärker, während niedri­gere Alphas den Lager­hal­tungs­kosten mehr Gewicht verleihen. In diesem Szenario sind beide von Bedeu­tung, daher setzen wir $\alpha = 0,5$.

Darüber hinaus haben wir die Imple­men­tie­rung der Autoren leicht verän­dert und eine eigene Metrik verwendet, die viel schneller arbeitet und uns nicht nur die Summe der Lager­hal­tungs­kosten und der Oppor­tu­ni­täts­kosten, sondern auch die beiden Einzel­kosten liefert:

Imple­men­tie­rung
def spec(df: pl.DataFrame, model_names: list[str], alpha: float = 0.5):
    """Stock-keeping-oriented Prediction Error Costs (SPEC)
    Read more in the :ref:`https://arxiv.org/abs/2004.10537`.

    The function takes a Polars DataFrame with the following structure:
    - `unique_id`: A unique identifier for each data point
    - `y`: The actual target value
    - `model_names` (list of strings): Column names representing different model predictions

    Parameters:
        df (pl.DataFrame): Input DataFrame containing the data. It must include a column named `y` representing the actual values, and columns named `` for each model in the `model_names` list.
        model_names (list[str]): A list of strings representing the names of the forecast models.
        alpha (float): Provides the weight of the opportunity costs. The weight of stock-keeping costs is taken as (1 - alpha), hence alpha should be in the interval [0, 1].

    Returns
    -------
    pl.DataFrame: Output DataFrame with SPEC results. It contains columns for `unique_id`, `model`, and the SPEC error for each unique identifier and model. The SPEC error is also provided as opportunity costs, stock-keeping costs, and the sum of both.

    """
    df = (
        df
        .sort("unique_id", "ds")
        # Calculate cum sum for every prediction and the target
        .with_columns(pl.col(["y"] + model_names).cum_sum().over("unique_id").name.suffix("_cum_sum"))
        # Compute differences in both directions from the predictions cumsum and the target cumsum
        .with_columns(
            *[(-pl.col(f"{m}_cum_sum") + pl.col("y_cum_sum")).alias(f"{m}_diff_y_f") for m in model_names],
            *[(pl.col(f"{m}_cum_sum") - pl.col("y_cum_sum")).alias(f"{m}_diff_f_y") for m in model_names]
        )
        # Multiply the first difference with alpha (opportunity costs)
        # and the second difference with (1 - alpha) (stock-keeping costs)
        .with_columns(
            *[(pl.col(f"{m}_diff_y_f") * alpha).clip(lower_bound=0).alias(f"{m}_o") for m in model_names],
            *[(pl.col(f"{m}_diff_f_y") * (1 - alpha)).clip(lower_bound=0).alias(f"{m}_s") for m in model_names]
        )
        # Add the opportunity and stock-keeping costs
        .with_columns([(pl.sum_horizontal(f"{m}_o", f"{m}_s")).alias(f"{m}_total") for m in model_names])
        # Group by unique_id and model, then aggregate to get average SPEC values
        .group_by("unique_id")
        .agg(*[pl.col(f"{m}_total", f"{m}_o", f"{m}_s").mean() for m in model_names])
        # Unpivot the DataFrame to have separate columns for SPEC values and corresponding model names
        .unpivot([x for xs in [[f"{m}_total", f"{m}_o", f"{m}_s"] for m in model_names] for x in xs], index="unique_id", variable_name="model", value_name="spec")
        # extract the different spec value indicators (total, o, s) to a separate column
        .with_columns(pl.col("model").str.split("_").list.to_struct(fields=["model", "error"]))
        .unnest("model")
        # pivot by the different splits (total, o, s) in the error column
        .pivot("error", index=["unique_id", "model"], values="spec")
        # rename accordingly
        .rename({"total": "spec_total", "o": "spec_o", "s": "spec_s"})
    )
    return df

spec_df = forecasts_df.pipe(spec, model_names=model_names)

CFE und SPEC zusammen

Die Verwen­dung von CFE und SPEC ermög­licht eine umfas­sende Bewer­tung von Progno­se­mo­dellen. Hier sind die Gründe dafür:
1. CFE (kumulierter Vorher­sa­ge­fehler):
  • Trend­ana­lyse: CFE hilft bei der Identi­fi­zie­rung von Trends in den Fehlern im Laufe der Zeit, so dass Sie erkennen können, ob das Modell konstant zu niedrige oder zu hohe Prognosen abgibt.
  • Magni­tude und Richtung: Die Minimal- und Maximal­werte von CFE können Aufschluss über die Gesamt­leis­tung und die Richtung der Fehler geben.
2. SPEC (Stock-keeping-oriented Predic­tion Error Costs):
  • Zeitba­sierte Abwei­chung: SPEC berechnet Vorher­sa­ge­fehler auf der Grund­lage ihrer Auswir­kungen auf künftige Lager­be­stände und bestraft Fehler, die zu hohen Lager­hal­tungs­kosten oder Fehlbe­ständen führen könnten.
  • Gewich­tete Kosten: Es ermutigt Unter­nehmen, sich auf die Minimie­rung von Über- und Unter­be­ständen zu konzen­trieren, was zu einer verbes­serten betrieb­li­chen Effizienz führt.
Durch die Kombi­na­tion beider Messgrößen könnt ihr eine fundier­tere Entschei­dung darüber treffen, welches Modell ihr für eure spezi­fi­schen Anfor­de­rungen verwenden solltet. Wenn das Verständnis von Trends im Zeitver­lauf entschei­dend ist, kann CFE zusätz­liche Erkennt­nisse liefern. Wenn die Unter­schiede zwischen Vorher­sage- und Zielzeiten von Inter­esse sind, könnte SPEC von größerem Inter­esse sein.

FAZIT

Unser Ansatz unter Verwen­dung des Stats­Fo­re­cast-Pakets und unsere Handha­bung von Gleich­ver­tei­lungen hat eine robuste Lösung für die Vorher­sage seltener Ereig­nisse gelie­fert. Das Stats­fo­re­cast-Paket hat wesent­lich dazu beigetragen, dass wir verschie­dene Vorher­sa­ge­mo­delle einfach und effizient nutzen konnten. Die Bewer­tungs­me­thoden für die Vorher­sage zeigen, dass man genau wissen sollte, wann welche Methoden sinnvoll einzu­setzen sind und nicht blind eine wählen darf. Falls ihr Fragen habt zu Vorher­sagen oder auch zu anderen mathe­ma­ti­schen Frage­stel­lungen, kontak­tiert uns gerne!

ANMER­KUNG

Wie ihr sehen könnt, verwende ich Polars anstelle von Pandas, Numpy, etc. für alle Berech­nungen. Bei m2hycon verwenden wir Polars seit Anfang 2023, wo wir das Poten­zial und die Auswir­kungen gegen­über anderen Biblio­theken wie Pandas, Dask, Ray und sogar Apache Spark gesehen haben. Ein Blog-Beitrag darüber, wie wir den Wechsel vollzogen haben und wie er sich auf unsere Produk­ti­vität und Ergeb­nisse auswirkt, folgt in Kürze!

Picture of Torben Windler

Torben Windler

Lead AI Engineer

Projektanfrage

Vielen Dank für Ihr Interesse an den Leistungen von m²hycon. Wir freuen uns sehr, von Ihrem Projekt zu erfahren und legen großen Wert darauf, Sie ausführlich zu beraten.

Von Ihnen im Formular eingegebene Daten speichern und verwenden wir ausschließlich zur Bearbeitung Ihrer Anfrage. Ihre Daten werden verschlüsselt übermittelt. Wir verarbeiten Ihre personenbezogenen Daten im Einklang mit unserer Datenschutzerklärung.