Hinweis zum Feature Engineering¶

Wichtiger Hinweis: Dieses Notebook demonstriert das Feature Engineering zu Lern- und Demonstrationszwecken durch manuelle Programmierung. In der Praxis könnten sämtliche Schritte der Datenaufbereitung auch mit etablierten Bibliotheken durchgeführt werden:

  • Datenaufbereitung: Hinzufügen von Spalten, Transformation der Daten
  • Skalierung: Normalisierung der Changes-Werte mit scikit-learn (z.B. MinMaxScaler, StandardScaler)
  • Train-Test-Split: Automatische Aufteilung mit train_test_split aus scikit-learn
  • Feature Engineering: Verwendung von Feature Engineering Pipelines

Die manuelle Implementierung hier erfolgt bewusst, um die einzelnen Schritte transparent und nachvollziehbar zu machen und ein tieferes Verständnis der Prozesse zu vermitteln.

Original Daten¶

In diesem Abschnitt laden wir die originalen Verkaufsdaten aus einer CSV-Datei. Die Daten enthalten wöchentliche Verkaufsmengen über mehrere Jahre.

Ziel: Erste Inspektion der Rohdaten, um Muster, Trends und potenzielle Ausreißer zu identifizieren.

Daten laden¶

Wir verwenden Pandas, um die CSV-Datei zu laden. Die Datei verwendet Semikolon als Trennzeichen.

In [ ]:
import pandas as pd
# original Daten
df_org = pd.read_csv("wochen_verkäufe.csv", header=0,delimiter=";")
In [2]:
von = 0
bis = len(df_org)
df_org[von:bis]
Out[2]:
jahr woche menge
0 2021 2 10
1 2021 3 47
2 2021 4 10
3 2021 5 11
4 2021 6 24
... ... ... ...
214 2025 14 100
215 2025 15 8
216 2025 16 13
217 2025 17 9
218 2025 18 12

219 rows × 3 columns

Visualisierung der Originaldaten¶

Ein Plot der Originalzeitreihe hilft uns, das Verkaufsverhalten zu verstehen:

  • Gibt es saisonale Muster?
  • Sind extreme Ausreißer vorhanden?
  • Wie ist die allgemeine Verteilung der Werte?
In [3]:
%matplotlib inline
import matplotlib.pyplot as plt
# original Daten
von = 0
bis = len(df_org)
plt.plot(df_org.index[von:bis], df_org["menge"][von:bis], label="Menge")
plt.grid(True)
plt.legend()

plt.show()
In [4]:
print(df_org["menge"].describe())
print(sorted(df_org["menge"].unique()))
print(df_org.loc[df_org["menge"] >= 30])
count    219.000000
mean      11.853881
std       12.901057
min        1.000000
25%        7.000000
50%       10.000000
75%       12.500000
max      100.000000
Name: menge, dtype: float64
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 24, 33, 47, 50, 66, 80, 85, 92, 100]
     jahr  woche  menge
1    2021      3     47
18   2021     20     66
25   2021     27     33
48   2021     50     50
49   2021     51     47
53   2022      3     92
145  2023     50     85
162  2024     15     80
214  2025     14    100

Aufbereitung Daten¶

Die Datenaufbereitung ist ein kritischer Schritt für das Training neuronaler Netze. Ausreißer können das Modell negativ beeinflussen.

Strategie zur Ausreißerbehandlung:

  • Berechne eine Grenze: Mittelwert × 3
  • Alle Werte über dieser Grenze werden auf das 75%-Quantil gesetzt
  • Dies reduziert extreme Ausreißer, ohne sie komplett zu entfernen

df.loc[df["menge"] > grenze, "menge"] = round(df["menge"].quantile(0.75))

Datenbereinigung und Inspektion¶

Laden der Daten erneut, Identifikation von Ausreißern und deren Behandlung. Der folgende Code:

  1. Zeigt statistische Kennzahlen (describe())
  2. Listet alle vorhandenen Werte auf
  3. Identifiziert Werte ≥ 40 als potenzielle Ausreißer
  4. Wendet die Ausreißerbehandlung an
  5. Zeigt die bereinigte Statistik
In [ ]:
import pandas as pd
df = pd.read_csv("wochen_verkäufe.csv", header=0,delimiter=";")
In [6]:
print(df.describe())
print("\n")
print(df.info())
print("\n")
print(sorted(df["menge"].unique()))
print(df.loc[df["menge"] >= 40])

grenze = df["menge"].mean() * 3
# new_value = df["menge"].mean() + df["menge"].std()
df.loc[df["menge"] > grenze, "menge"] = round(df["menge"].quantile(0.75))

print(df.describe())
print(sorted(df["menge"].unique()))
print(df.loc[df["menge"] >= 40])
              jahr       woche       menge
count   219.000000  219.000000  219.000000
mean   2022.698630   25.328767   11.853881
std       1.292373   15.029990   12.901057
min    2021.000000    1.000000    1.000000
25%    2022.000000   12.000000    7.000000
50%    2023.000000   24.000000   10.000000
75%    2024.000000   38.000000   12.500000
max    2025.000000   53.000000  100.000000


<class 'pandas.core.frame.DataFrame'>
RangeIndex: 219 entries, 0 to 218
Data columns (total 3 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   jahr    219 non-null    int64
 1   woche   219 non-null    int64
 2   menge   219 non-null    int64
dtypes: int64(3)
memory usage: 5.3 KB
None


[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 24, 33, 47, 50, 66, 80, 85, 92, 100]
     jahr  woche  menge
1    2021      3     47
18   2021     20     66
48   2021     50     50
49   2021     51     47
53   2022      3     92
145  2023     50     85
162  2024     15     80
214  2025     14    100
              jahr       woche       menge
count   219.000000  219.000000  219.000000
mean   2022.698630   25.328767    9.703196
std       1.292373   15.029990    4.402724
min    2021.000000    1.000000    1.000000
25%    2022.000000   12.000000    7.000000
50%    2023.000000   24.000000   10.000000
75%    2024.000000   38.000000   12.000000
max    2025.000000   53.000000   33.000000
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 24, 33]
Empty DataFrame
Columns: [jahr, woche, menge]
Index: []
In [7]:
%matplotlib inline
import matplotlib.pyplot as plt

von = 0
bis = len(df)
plt.plot(df.index[von:bis], df["menge"][von:bis], label="Menge")
plt.grid(True)
plt.legend()

plt.show()

Datenreihenfolge für LSTM/GRU¶

Für die Zeitreihenvorhersage mit rekurrenten Netzen kehren wir die Reihenfolge der Daten um:

  • Die neuesten Daten kommen zuerst
  • Dadurch wird menge_before zur Vorwoche
  • Dies vereinfacht die Sliding Window Erstellung
In [8]:
print("Reihenfolge aus Datei")
print(df.head())
df = df[::-1] # für LSTM
df.reset_index(drop=True,inplace=True)
df = df.dropna()
print("Reihenfolge gedreht für LSTM")
print(df.head())
Reihenfolge aus Datei
   jahr  woche  menge
0  2021      2     10
1  2021      3     12
2  2021      4     10
3  2021      5     11
4  2021      6     24
Reihenfolge gedreht für LSTM
   jahr  woche  menge
0  2025     18     12
1  2025     17      9
2  2025     16     13
3  2025     15      8
4  2025     14     12
In [9]:
df["menge_before"] = df["menge"].shift(-1)
In [10]:
df.head(10)
Out[10]:
jahr woche menge menge_before
0 2025 18 12 9.0
1 2025 17 9 13.0
2 2025 16 13 8.0
3 2025 15 8 12.0
4 2025 14 12 13.0
5 2025 13 13 16.0
6 2025 12 16 13.0
7 2025 11 13 9.0
8 2025 10 9 13.0
9 2025 9 13 8.0

Berechnung des Changes-Feature¶

Das changes-Feature repräsentiert die relative Veränderung zur Vorwoche:

  • Formel: (menge / menge_before) / 10
  • Verhältnis (vor Division durch 10):
    • 1.0 = keine Veränderung (gleiche Menge)
    • 2.0 = doppelt so viel
    • 0.5 = halb so viel
  • Nach Normalisierung (÷ 10): Die Werte werden durch 10 geteilt, um sie in einen günstigen Bereich für das neuronale Netz zu bringen (typischerweise 0.05 bis 0.3)
In [11]:
# Faktor für Änderung zur Vorwoche 
#df["changes"] = ((df["menge"] / df["menge_before"]) -1 ) #/ 10
df["changes"] = ((df["menge"] / df["menge_before"]) / 10 )
In [12]:
df = df.dropna()
df.head(10)
Out[12]:
jahr woche menge menge_before changes
0 2025 18 12 9.0 0.133333
1 2025 17 9 13.0 0.069231
2 2025 16 13 8.0 0.162500
3 2025 15 8 12.0 0.066667
4 2025 14 12 13.0 0.092308
5 2025 13 13 16.0 0.081250
6 2025 12 16 13.0 0.123077
7 2025 11 13 9.0 0.144444
8 2025 10 9 13.0 0.069231
9 2025 9 13 8.0 0.162500
In [13]:
#print(df["menge_before"][4] * (df["changes"][4] * 10 +1))
In [14]:
changes = df["changes"]
print(len(changes))
218

Sliding Window für Sequenzen¶

Die Sliding Window Technik erstellt Trainingssequenzen aus den Zeitreihendaten:

  • block_size = 3: Wir verwenden 3 aufeinanderfolgende Werte als Input
  • train_offset = 30: Die ersten 30 Datenpunkte werden als Testset reserviert
  • X: Input-Sequenzen (3 aufeinanderfolgende Werte)
  • y: Target-Werte (nächster Wert nach der Sequenz)

Zeitliche Aufteilung (wichtig bei Zeitreihen!):

  • Train: Datenpunkte 30 bis Ende
  • Test: Datenpunkte 0 bis 30

Die umgekehrte Reihenfolge ([::-1]) stellt sicher, dass die neuesten Werte zuerst kommen.

In [15]:
import numpy as np

X = []
y = []
X_train = []
y_train = []
X_test = []
y_test = []
train_offset = 30
block_size = 3
for i in range(0, len(changes) - block_size):
    y.append(changes[i])
    X.append(np.array(changes[i+1:i+(block_size+1)][::-1]))

for i in range(train_offset, len(changes) - block_size):
    y_train.append(changes[i])
    X_train.append(np.array(changes[i+1:i+(block_size+1)][::-1]))

for i in range(0, train_offset - block_size):
    y_test.append(changes[i])
    X_test.append(np.array(changes[i+1:i+(block_size+1)][::-1]))
    
X = np.array(X).reshape(-1, block_size, 1)
y = np.array(y)

X_train = np.array(X_train).reshape(-1, block_size, 1)
y_train = np.array(y_train)

X_test = np.array(X_test).reshape(-1, block_size, 1)
y_test = np.array(y_test)

print(X[0])
print(y[0])

print(X_train[0])
print(y_train[0])

print(X_test[0])
print(y_test[0])
[[0.06666667]
 [0.1625    ]
 [0.06923077]]
0.13333333333333333
[[0.10909091]
 [0.09166667]
 [0.08181818]]
0.13333333333333333
[[0.06666667]
 [0.1625    ]
 [0.06923077]]
0.13333333333333333
In [ ]:
 
In [16]:
X.shape # type: ignore
#from sklearn.model_selection import train_test_split
#X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.33,shuffle=True)
Out[16]:
(215, 3, 1)

GRU-Modell (Gated Recurrent Unit)¶

GRU ist eine vereinfachte Variante von LSTM, die oft ähnlich gute Ergebnisse bei weniger Parametern liefert.

Modellarchitektur¶

  1. GRU-Schicht: Verarbeitet die sequenziellen Eingaben (3 Zeitschritte)
  2. Dense-Schichten: Mehrere vollvernetzte Schichten mit abnehmender Größe (128 → 64 → 32 → 16 → 8 → 4)
  3. LeakyReLU: Aktivierungsfunktion, die auch negative Gradienten zulässt
  4. Dropout (0.1): Regularisierung zur Vermeidung von Overfitting
  5. Output-Schicht: Ein Neuron für die Vorhersage

Training-Konfiguration¶

  • Optimizer: Adam (adaptive learning rate)
  • Loss: MSE (Mean Squared Error)
  • Early Stopping: Stoppt Training nach 10 Epochen ohne Verbesserung
  • Batch Size: 8
  • Epochs: Max 100 (mit Early Stopping)

Hinweis: is_production = False bedeutet, wir trainieren nur auf dem Trainingsset, nicht auf allen Daten.

In [17]:
import keras.backend
keras.backend.clear_session()
from keras.models import Sequential
from keras.layers import GRU
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import Input
from keras.layers import LeakyReLU
from keras.callbacks import EarlyStopping

early_stopping = EarlyStopping(monitor='loss', patience=10, restore_best_weights=True)

is_production = False

model = Sequential()
model.add(Input(shape=(block_size, 1)))
model.add(GRU(block_size))
model.add(Dense(128, name="hidden001"))
model.add(LeakyReLU(negative_slope=0.1))
model.add(Dropout(0.1))
model.add(Dense(64, name="hidden01"))
model.add(LeakyReLU(negative_slope=0.1))
model.add(Dropout(0.1))
model.add(Dense(32, name="hidden1"))
model.add(LeakyReLU(negative_slope=0.1))
model.add(Dropout(0.1))
model.add(Dense(16, name="hidden2"))
model.add(LeakyReLU(negative_slope=0.1))
model.add(Dropout(0.1))
model.add(Dense(8, name="hidden3"))
model.add(LeakyReLU(negative_slope=0.1))
model.add(Dense(4, name="hidden4"))
model.add(LeakyReLU(negative_slope=0.1))
model.add(Dense(1, name="out"))

model.compile(optimizer="adam", loss="mse") #, metrics=['mae','Accuracy'])

model.summary()
if (is_production):
    model.fit(X, y, batch_size=8, epochs=55)
else:
    model.fit(X_train, y_train, batch_size=8, epochs=100,callbacks=[early_stopping]) # epochs=55 ergab sich aus early-stopping
    
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                    ┃ Output Shape           ┃       Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ gru (GRU)                       │ (None, 3)              │            54 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ hidden001 (Dense)               │ (None, 128)            │           512 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ leaky_re_lu (LeakyReLU)         │ (None, 128)            │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout (Dropout)               │ (None, 128)            │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ hidden01 (Dense)                │ (None, 64)             │         8,256 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ leaky_re_lu_1 (LeakyReLU)       │ (None, 64)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_1 (Dropout)             │ (None, 64)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ hidden1 (Dense)                 │ (None, 32)             │         2,080 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ leaky_re_lu_2 (LeakyReLU)       │ (None, 32)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_2 (Dropout)             │ (None, 32)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ hidden2 (Dense)                 │ (None, 16)             │           528 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ leaky_re_lu_3 (LeakyReLU)       │ (None, 16)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_3 (Dropout)             │ (None, 16)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ hidden3 (Dense)                 │ (None, 8)              │           136 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ leaky_re_lu_4 (LeakyReLU)       │ (None, 8)              │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ hidden4 (Dense)                 │ (None, 4)              │            36 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ leaky_re_lu_5 (LeakyReLU)       │ (None, 4)              │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ out (Dense)                     │ (None, 1)              │             5 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 11,607 (45.34 KB)
 Trainable params: 11,607 (45.34 KB)
 Non-trainable params: 0 (0.00 B)
Epoch 1/100
2025-11-02 18:41:55.493754: I tensorflow/core/grappler/optimizers/custom_graph_optimizer_registry.cc:117] Plugin optimizer for device_type GPU is enabled.
24/24 ━━━━━━━━━━━━━━━━━━━━ 6s 135ms/step - loss: 0.0195
Epoch 2/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 103ms/step - loss: 0.0160
Epoch 3/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 105ms/step - loss: 0.0161
Epoch 4/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 103ms/step - loss: 0.0148
Epoch 5/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 104ms/step - loss: 0.0147
Epoch 6/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 94ms/step - loss: 0.0151
Epoch 7/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 109ms/step - loss: 0.0155
Epoch 8/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 111ms/step - loss: 0.0145
Epoch 9/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 107ms/step - loss: 0.0141
Epoch 10/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 105ms/step - loss: 0.0142
Epoch 11/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 122ms/step - loss: 0.0138
Epoch 12/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 114ms/step - loss: 0.0133
Epoch 13/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 106ms/step - loss: 0.0123
Epoch 14/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 114ms/step - loss: 0.0121
Epoch 15/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 107ms/step - loss: 0.0106
Epoch 16/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 108ms/step - loss: 0.0154
Epoch 17/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 103ms/step - loss: 0.0107
Epoch 18/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 98ms/step - loss: 0.0123
Epoch 19/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 94ms/step - loss: 0.0123
Epoch 20/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 126ms/step - loss: 0.0122
Epoch 21/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 114ms/step - loss: 0.0134
Epoch 22/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 102ms/step - loss: 0.0100
Epoch 23/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 98ms/step - loss: 0.0085
Epoch 24/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 126ms/step - loss: 0.0096
Epoch 25/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 104ms/step - loss: 0.0105
Epoch 26/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 104ms/step - loss: 0.0107
Epoch 27/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 102ms/step - loss: 0.0098
Epoch 28/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 106ms/step - loss: 0.0075
Epoch 29/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 96ms/step - loss: 0.0164
Epoch 30/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 109ms/step - loss: 0.0134
Epoch 31/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 131ms/step - loss: 0.0061
Epoch 32/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 116ms/step - loss: 0.0077
Epoch 33/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 102ms/step - loss: 0.0067
Epoch 34/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 105ms/step - loss: 0.0108
Epoch 35/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 104ms/step - loss: 0.0064
Epoch 36/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 3s 104ms/step - loss: 0.0097
Epoch 37/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 93ms/step - loss: 0.0139
Epoch 38/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 97ms/step - loss: 0.0126
Epoch 39/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 95ms/step - loss: 0.0064
Epoch 40/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 95ms/step - loss: 0.0085
Epoch 41/100
24/24 ━━━━━━━━━━━━━━━━━━━━ 2s 94ms/step - loss: 0.0076
In [18]:
print(model.evaluate(X_test, y_test))
print(model.evaluate(X_train, y_train))
print(model.evaluate(X, y))
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 188ms/step - loss: 0.0023
0.0022551948204636574
6/6 ━━━━━━━━━━━━━━━━━━━━ 0s 20ms/step - loss: 0.0065
0.006532527506351471
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 15ms/step - loss: 0.0059
0.005915033631026745

Vorhersage mit dem trainierten Modell¶

Jetzt verwenden wir das trainierte GRU-Modell, um Vorhersagen auf allen Daten zu machen.

  • model.predict(X) gibt die vorhergesagten changes-Werte zurück
  • Diese müssen später zurück in absolute Verkaufsmengen umgerechnet werden
In [19]:
predictions = model.predict(X)
predictions.shape
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 19ms/step
Out[19]:
(215, 1)
In [20]:
print(predictions)
[[0.09813829]
 [0.08790225]
 [0.13468355]
 [0.09812401]
 [0.09625456]
 [0.08980297]
 [0.0886924 ]
 [0.09766522]
 [0.08615153]
 [0.09731602]
 [0.09098737]
 [0.09328337]
 [0.09612396]
 [0.09070449]
 [0.09431577]
 [0.08779415]
 [0.0839823 ]
 [0.1501984 ]
 [0.09753084]
 [0.08095044]
 [0.10007307]
 [0.08282965]
 [0.08382986]
 [0.0890993 ]
 [0.07591484]
 [0.46153292]
 [0.09334129]
 [0.08876219]
 [0.11034367]
 [0.091677  ]
 [0.10526877]
 [0.09826311]
 [0.09724215]
 [0.10623837]
 [0.08502066]
 [0.07648113]
 [0.09334676]
 [0.1392675 ]
 [0.09137205]
 [0.09011222]
 [0.09026998]
 [0.13817564]
 [0.09323836]
 [0.09789144]
 [0.11692637]
 [0.08035758]
 [0.07578386]
 [0.07262777]
 [0.15037346]
 [0.08316055]
 [0.11562733]
 [0.08483283]
 [0.07637224]
 [0.18801594]
 [0.09272344]
 [0.0878893 ]
 [0.0979758 ]
 [0.09604811]
 [0.12101276]
 [0.09664869]
 [0.09123904]
 [0.09205766]
 [0.09437017]
 [0.09241752]
 [0.08487571]
 [0.09112975]
 [0.07946242]
 [0.09677161]
 [0.07618962]
 [0.09806639]
 [0.07716269]
 [0.07498149]
 [0.06987651]
 [0.8557852 ]
 [0.15330812]
 [0.10865845]
 [0.09778069]
 [0.10838339]
 [0.09568754]
 [0.09285083]
 [0.09314213]
 [0.09393417]
 [0.08752847]
 [0.11662638]
 [0.09123904]
 [0.09319563]
 [0.09433337]
 [0.09385673]
 [0.07646156]
 [0.10185117]
 [0.08577968]
 [0.07648061]
 [0.14224404]
 [0.09538481]
 [0.09143222]
 [0.10919128]
 [0.07654379]
 [0.0939876 ]
 [0.08594742]
 [0.1249598 ]
 [0.08567232]
 [0.08311823]
 [0.14170094]
 [0.09345919]
 [0.08288924]
 [0.08881554]
 [0.07756311]
 [0.13947727]
 [0.08797656]
 [0.08377407]
 [0.08773283]
 [0.07502352]
 [0.07452963]
 [0.09481268]
 [0.09405357]
 [0.07601722]
 [0.09832951]
 [0.11285783]
 [0.15455991]
 [0.08784886]
 [0.09042952]
 [0.14701891]
 [0.12919366]
 [0.12418304]
 [0.12070633]
 [0.12529948]
 [0.09726508]
 [0.10106568]
 [0.12437459]
 [0.09100367]
 [0.08728938]
 [0.09541513]
 [0.09848803]
 [0.09436902]
 [0.08849856]
 [0.09803865]
 [0.09152956]
 [0.09116653]
 [0.09616092]
 [0.15361932]
 [0.09138961]
 [0.08754236]
 [0.07578711]
 [0.15509121]
 [0.08967691]
 [0.08663513]
 [0.09698086]
 [0.09545711]
 [0.09623728]
 [0.08219852]
 [0.13689105]
 [0.09784909]
 [0.09603292]
 [0.08698736]
 [0.07907201]
 [0.07599338]
 [0.23256454]
 [0.12502968]
 [0.08849856]
 [0.0972493 ]
 [0.09149289]
 [0.09042952]
 [0.13837439]
 [0.08818066]
 [0.0761607 ]
 [0.07432958]
 [0.63080615]
 [0.09441724]
 [0.09038205]
 [0.08193275]
 [0.12672128]
 [0.08956105]
 [0.09118912]
 [0.08790939]
 [0.13273695]
 [0.0877209 ]
 [0.11734685]
 [0.09005768]
 [0.14043845]
 [0.09635933]
 [0.08526149]
 [0.07573691]
 [0.07392338]
 [0.69957477]
 [0.13333629]
 [0.14131862]
 [0.09829878]
 [0.09256272]
 [0.09242696]
 [0.09764808]
 [0.09397329]
 [0.09265485]
 [0.07608499]
 [0.11428607]
 [0.09373078]
 [0.09409864]
 [0.08674458]
 [0.10033745]
 [0.09740625]
 [0.0961329 ]
 [0.08749868]
 [0.09728593]
 [0.092214  ]
 [0.09664247]
 [0.08053426]
 [0.09607401]
 [0.09826829]
 [0.08538634]
 [0.16894531]
 [0.09714226]
 [0.09269595]
 [0.08034411]
 [0.09909739]
 [0.07654618]
 [0.09465578]]
In [21]:
predictions = predictions.reshape(-1)
print(predictions.shape)
(215,)
In [22]:
predictions = np.append(predictions, np.zeros(len(df) - predictions.shape[0]))
In [23]:
predictions.shape
Out[23]:
(218,)
In [24]:
df["predictions"] = predictions

Rekonstruktion der absoluten Verkaufsmengen¶

Das Modell sagt die normierten changes-Werte vorher. Um zurück zu den tatsächlichen Verkaufsmengen zu kommen:

  1. Füge die Vorhersagen zum DataFrame hinzu
  2. Berechne menge_predicted = menge_before × (10 × predictions)

Formel erklärt:

  • predictions ist der vorhergesagte normierte changes-Wert
  • Multiplikation mit 10 macht die Normalisierung rückgängig
  • Multiplikation mit menge_before berechnet die absolute Menge
In [25]:
df["menge_predicted"] = df["menge_before"] * (10 * df["predictions"])
In [26]:
%matplotlib inline
import matplotlib.pyplot as plt


uebereinander = True
scatter = False

df_plot = df[::-1]
df_plot.reset_index(drop=True,inplace=True)

von = 100
bis = len(df_plot)

if scatter:
    plt.scatter(df_plot.index[von:bis], df_plot["menge"][von:bis], label="Menge")
else:
    plt.plot(df_plot.index[von:bis], df_plot["menge"][von:bis], label="Menge")
    
if uebereinander:
    if scatter:
        plt.scatter(df_plot.index[von:bis], df_plot["menge_predicted"].shift(-1)[von:bis], label="Menge (predicted-scatter)")
    else:
        plt.plot(df_plot.index[von:bis], df_plot["menge_predicted"].shift(-1)[von:bis], label="Menge (predicted)")   
else:
    if scatter:
        plt.scatter(df_plot.index[von:bis], df_plot["menge_predicted"][von:bis], label="Menge (predicted-scatter)")
    else:
        plt.plot(df_plot.index[von:bis], df_plot["menge_predicted"][von:bis], label="Menge (predicted)")
    
plt.grid(True)
plt.legend()

plt.show()

Testdaten-Performance: Anzeige ab Datensatz 100 bis zum letzten vorhandenen.¶

Wichtige Beobachtung zur Modellqualität:

  • Die letzten 30 Wochen (entsprechend train_offset = 30) wurden nicht im Training verwendet
  • Diese Datenpunkte sind echte Testdaten, die das Modell während des Trainings nie gesehen hat
  • Dennoch zeigt das Modell beeindruckend gute Vorhersagen für diese ungesehenen Daten
  • Dies demonstriert die Generalisierungsfähigkeit des GRU-Modells und zeigt, dass es nicht einfach die Trainingsdaten auswendig gelernt hat, sondern tatsächlich die zugrundeliegenden Muster erkannt hat
In [27]:
print(df_plot)
     jahr  woche  menge  menge_before   changes  predictions  menge_predicted
0    2021      3     12          10.0  0.120000     0.000000         0.000000
1    2021      4     10          12.0  0.083333     0.000000         0.000000
2    2021      5     11          10.0  0.110000     0.000000         0.000000
3    2021      6     24          11.0  0.218182     0.094656        10.412136
4    2021      7      9          24.0  0.037500     0.076546        18.371084
..    ...    ...    ...           ...       ...          ...              ...
213  2025     14     12          13.0  0.092308     0.096255        12.513092
214  2025     15      8          12.0  0.066667     0.098124        11.774881
215  2025     16     13           8.0  0.162500     0.134684        10.774684
216  2025     17      9          13.0  0.069231     0.087902        11.427292
217  2025     18     12           9.0  0.133333     0.098138         8.832446

[218 rows x 7 columns]
In [28]:
df.head(6)
Out[28]:
jahr woche menge menge_before changes predictions menge_predicted
0 2025 18 12 9.0 0.133333 0.098138 8.832446
1 2025 17 9 13.0 0.069231 0.087902 11.427292
2 2025 16 13 8.0 0.162500 0.134684 10.774684
3 2025 15 8 12.0 0.066667 0.098124 11.774881
4 2025 14 12 13.0 0.092308 0.096255 12.513092
5 2025 13 13 16.0 0.081250 0.089803 14.368474
for i in range(0, len(changes) - block_size):
    Y.append(changes[i])
    X.append(np.array(changes[i+1:i+block_size][::-1]))
In [29]:
print("erster trainierter Block aus X_train:  " + str([ i[0] for i in X_train[0]]))
print("erstes y_train:                         " + str(y_train[0]) + "\n")
print("nicht beim Training gesehene Daten:\n")
print("erster Block aus X:                     " + str([ i[0] for i in X[0]]))
print("erstes y:                                " + str(y[0]) + "\n")
print("\033[1moberster Block für die Prediction:\033[92m      " + str([ i for i in df["changes"][0:block_size][::-1]]) + "\n")
df["changes"][0:10]
erster trainierter Block aus X_train:  [0.10909090909090909, 0.09166666666666666, 0.08181818181818182]
erstes y_train:                         0.13333333333333333

nicht beim Training gesehene Daten:

erster Block aus X:                     [0.06666666666666667, 0.1625, 0.06923076923076923]
erstes y:                                0.13333333333333333

oberster Block für die Prediction:      [0.1625, 0.06923076923076923, 0.13333333333333333]

Out[29]:
0    0.133333
1    0.069231
2    0.162500
3    0.066667
4    0.092308
5    0.081250
6    0.123077
7    0.144444
8    0.069231
9    0.162500
Name: changes, dtype: float64
In [30]:
print("\033[92mLetzte bekannte VK-Menge: " + str(df["menge"][0]))
Letzte bekannte VK-Menge: 12
In [31]:
### df["menge_predicted"] = df["menge_before"] * (1 + df["predictions"])

pred_changes = df["changes"][0:block_size][::-1]
pred_X = np.array(pred_changes).reshape(1,block_size,1)
prediction_X = model.predict(pred_X)
print(prediction_X)
# Letze Menge
menge_last = df["menge"][0]
naechste_woche = df["woche"][0] + 1
#menge_p = menge_last * (1 + prediction_X)
menge_p = menge_last * (10 * prediction_X)
menge_p = round(menge_p.reshape(-1)[0])
print(menge_p)
print()
print("\033[1m Voraussage für Menge nächste Woche, Kalenderwoche(" + str(naechste_woche) + "):\033[1m \033[92m" +  str(menge_p))
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 46ms/step
[[0.09053151]]
11

 Voraussage für Menge nächste Woche, Kalenderwoche(19): 11
In [32]:
#pred_changes.shape
In [33]:
pred_changes = pred_changes[0:block_size-1]
#new_change = ((menge_p / menge_last) -1) #/ 10  # genau wie für Training: df["changes"] = ((df["menge"] / df["menge_before"]) -1 ) / 10
new_change = ((menge_p / menge_last) / 10) #/ 10  # genau wie für Training: df["changes"] = ((df["menge"] / df["menge_before"]) -1 ) / 10
pred_changes = pd.concat([pred_changes, pd.Series([new_change])], ignore_index=True)

pred_X = np.array(pred_changes).reshape(1,block_size,1)

prediction_X = model.predict(pred_X)
print(prediction_X)

menge_last = menge_p
menge_p = menge_last * (10 * prediction_X)
menge_p = round(menge_p.reshape(-1)[0])
print(menge_p)
print()
print("\033[1m Voraussage für Menge übernächste Woche, Kalenderwoche(" + str(naechste_woche +1) + "):\033[1m \033[92m" +  str(menge_p))
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 23ms/step
[[0.09792395]]
11

 Voraussage für Menge übernächste Woche, Kalenderwoche(20): 11
In [34]:
import pandas as pd

#df_bestand = pd.read_csv("pzn_bestand_name.csv", header=0,delimiter=";")
#bestand_akt = float(df_bestand[df_bestand['pzn'] == 8628264]['bestand'])

bestand_akt = 58 

bedarf_zwei_wochen = menge_last + menge_p

print("\033[1m Voraussichtlicher Bedarf für die nächsten zwei Wochen:\033[1m \033[92m" +  str(bedarf_zwei_wochen))
 Voraussichtlicher Bedarf für die nächsten zwei Wochen: 22
In [35]:
print("\033[1m Aktueller Bestand:\033[1m " +  "\033[92m" + str(bestand_akt))
print("\033[0m\033[1m Voraussichtlicher Bedarf für die nächsten zwei Wochen:\033[1m " +  "\033[92m" +str(bedarf_zwei_wochen))
if bestand_akt >= bedarf_zwei_wochen:
    if bestand_akt > (bedarf_zwei_wochen * 2):
        print("\033[0m\033[1m\033[4m Bestand ist mehr als doppelt so hoch wie der Bedarf!\033[1m \033[93m Bestellrythmus überprüfen! " ) 
    else:
        print("\033[92m\033[1m Kein Handlungsbedarf!\033[1m " )
else:
    print("\033[93m\033[1m Bestellung notwendig: \033[1m \033[92m" + str(bedarf_zwei_wochen - bestand_akt))
 Aktueller Bestand: 58
 Voraussichtlicher Bedarf für die nächsten zwei Wochen: 22
 Bestand ist mehr als doppelt so hoch wie der Bedarf!  Bestellrythmus überprüfen! 

Beispiel¶



Formatierung:¶

class bcolors: HEADER = '\033[95m' OKBLUE = '\033[94m' OKCYAN = '\033[96m' OKGREEN = '\033[92m' WARNING = '\033[93m' FAIL = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' UNDERLINE = '\033[4m'