Warum dieser Schritt jetzt kommt

Nach der Theorie braucht ihr eine vernuenftige Projekt-Routine. Ein Modell ist erst dann hilfreich, wenn es auf neuen Daten stabil ist, transparent bewertet wird und ihr die Grenzen benennen koennt.

Codepfad
Dauer: 60-120 Minuten
Python: 3.11.3
Libraries: pandas, scikit-learn, matplotlib
Ziel: robuste Vorhersage statt Demo-Effekt

1) Train/Test-Split: warum wir fast nie "alle Wahrheit" sehen

In der Praxis arbeiten wir mit Stichproben. Darum sind Metriken immer Schaetzungen. Mit Train/Test trennt ihr Lernen und Pruefen sauber.

Code
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

def make_demo_df(seed=42, n_samples=320):
    rng = np.random.default_rng(seed)
    df = pd.DataFrame({
        "marketing_spend": rng.normal(120, 25, size=n_samples).clip(40, 220),
        "price_index": rng.normal(1.0, 0.12, size=n_samples).clip(0.7, 1.3),
        "season_index": rng.integers(0, 4, size=n_samples),
        "web_traffic": rng.normal(3200, 700, size=n_samples).clip(1200, 6000)
    })
    noise = rng.normal(0, 9, size=n_samples)
    df["target_demand"] = (
        55
        + 0.22 * df["marketing_spend"]
        - 28 * df["price_index"]
        + 5.5 * df["season_index"]
        + 0.012 * df["web_traffic"]
        + noise
    )
    return df

# Standalone-Block
df = make_demo_df()
X = df.drop(columns=["target_demand"])
y = df["target_demand"]

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42,
    shuffle=True
)

print("Train shape:", X_train.shape)
print("Test shape:", X_test.shape)
Kernbotschaft:
Der Trainingsfehler allein ist keine Aussage ueber echte Prognosefaehigkeit. Wir brauchen den Fehler auf bisher ungesehenen Daten.
Mini-Check: Warum reicht ein einziger Split manchmal nicht?

Ein einzelner Split kann zufaellig guenstig oder unguenstig sein. Fuer stabilere Aussagen nutzt man oft Cross-Validation oder mehrere Splits.

2) Baseline und Metriken: MSE vor R² fuer Modellvergleich

Beim Vergleich von Vorhersagemodellen ist MSE/MAE oft robuster interpretierbar. R² kann negativ werden und in der Kommunikation missverstanden werden.

Code
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

def make_demo_df(seed=42, n_samples=320):
    rng = np.random.default_rng(seed)
    df = pd.DataFrame({
        "marketing_spend": rng.normal(120, 25, size=n_samples).clip(40, 220),
        "price_index": rng.normal(1.0, 0.12, size=n_samples).clip(0.7, 1.3),
        "season_index": rng.integers(0, 4, size=n_samples),
        "web_traffic": rng.normal(3200, 700, size=n_samples).clip(1200, 6000)
    })
    noise = rng.normal(0, 9, size=n_samples)
    df["target_demand"] = (
        55
        + 0.22 * df["marketing_spend"]
        - 28 * df["price_index"]
        + 5.5 * df["season_index"]
        + 0.012 * df["web_traffic"]
        + noise
    )
    return df

# Standalone-Block
df = make_demo_df()
X = df.drop(columns=["target_demand"])
y = df["target_demand"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, shuffle=True
)

baseline = LinearRegression()
baseline.fit(X_train, y_train)

pred_train = baseline.predict(X_train)
pred_test = baseline.predict(X_test)

print("Train MSE:", round(mean_squared_error(y_train, pred_train), 3))
print("Test  MSE:", round(mean_squared_error(y_test, pred_test), 3))
print("Test  MAE:", round(mean_absolute_error(y_test, pred_test), 3))
print("Test  R2 :", round(r2_score(y_test, pred_test), 3))
Hinweis: Ein negatives R² bedeutet, dass das Modell schlechter als eine sehr einfache Mittelwert-Baseline ist.
Mini-Check: Warum nicht nur den kleinsten Trainingsfehler nehmen?

Weil ein Modell Trainingsdaten auswendig lernen kann. Entscheidend ist der Testfehler als Naeherung fuer Generalisierung.

3) L1/L2 in Praxis: Lasso vs Ridge mit Parameter-Tuning

Jetzt wird die Modellkomplexitaet bewusst gesteuert: Ridge (L2) schrumpft Gewichte weich, Lasso (L1) kann Features auf 0 setzen.

Code
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge, Lasso
from sklearn.metrics import mean_squared_error

def make_demo_df(seed=42, n_samples=320):
    rng = np.random.default_rng(seed)
    df = pd.DataFrame({
        "marketing_spend": rng.normal(120, 25, size=n_samples).clip(40, 220),
        "price_index": rng.normal(1.0, 0.12, size=n_samples).clip(0.7, 1.3),
        "season_index": rng.integers(0, 4, size=n_samples),
        "web_traffic": rng.normal(3200, 700, size=n_samples).clip(1200, 6000)
    })
    noise = rng.normal(0, 9, size=n_samples)
    df["target_demand"] = (
        55
        + 0.22 * df["marketing_spend"]
        - 28 * df["price_index"]
        + 5.5 * df["season_index"]
        + 0.012 * df["web_traffic"]
        + noise
    )
    return df

# Standalone-Block
df = make_demo_df()
X = df.drop(columns=["target_demand"])
y = df["target_demand"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, shuffle=True
)

ridge_pipe = Pipeline([
    ("scale", StandardScaler()),
    ("model", Ridge())
])

lasso_pipe = Pipeline([
    ("scale", StandardScaler()),
    ("model", Lasso(max_iter=10000))
])

param_grid = {"model__alpha": [0.01, 0.1, 1.0, 3.0, 10.0]}

ridge_cv = GridSearchCV(ridge_pipe, param_grid=param_grid, scoring="neg_mean_squared_error", cv=5)
lasso_cv = GridSearchCV(lasso_pipe, param_grid=param_grid, scoring="neg_mean_squared_error", cv=5)

ridge_cv.fit(X_train, y_train)
lasso_cv.fit(X_train, y_train)

print("Best Ridge alpha:", ridge_cv.best_params_["model__alpha"])
print("Best Lasso alpha:", lasso_cv.best_params_["model__alpha"])
print("Ridge Test MSE:", round(mean_squared_error(y_test, ridge_cv.predict(X_test)), 3))
print("Lasso Test MSE:", round(mean_squared_error(y_test, lasso_cv.predict(X_test)), 3))
Wann eher was?
Ridge, wenn viele schwache Signale gemeinsam nuetzlich sind. Lasso, wenn ihr Feature-Selektion und ein kompakteres Modell wollt.
Mini-Check: Warum braucht Lasso oft Standardisierung?

L1-Strafe haengt von der Koeffizientengroesse ab. Ohne vergleichbare Feature-Skalen wird die Strafe ungleich verteilt.

4) Residuen-Check: Muster erkennen statt nur Score lesen

Ein guter MSE ist wichtig, aber nicht genug. Residuen zeigen euch, ob systematische Fehler uebrig bleiben (z. B. gekruemmte Muster, heterogene Varianz).

Code
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge

def make_demo_df(seed=42, n_samples=320):
    rng = np.random.default_rng(seed)
    df = pd.DataFrame({
        "marketing_spend": rng.normal(120, 25, size=n_samples).clip(40, 220),
        "price_index": rng.normal(1.0, 0.12, size=n_samples).clip(0.7, 1.3),
        "season_index": rng.integers(0, 4, size=n_samples),
        "web_traffic": rng.normal(3200, 700, size=n_samples).clip(1200, 6000)
    })
    noise = rng.normal(0, 9, size=n_samples)
    df["target_demand"] = (
        55
        + 0.22 * df["marketing_spend"]
        - 28 * df["price_index"]
        + 5.5 * df["season_index"]
        + 0.012 * df["web_traffic"]
        + noise
    )
    return df

# Standalone-Block
df = make_demo_df()
X = df.drop(columns=["target_demand"])
y = df["target_demand"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, shuffle=True
)

ridge_pipe = Pipeline([
    ("scale", StandardScaler()),
    ("model", Ridge())
])
param_grid = {"model__alpha": [0.01, 0.1, 1.0, 3.0, 10.0]}
ridge_cv = GridSearchCV(ridge_pipe, param_grid=param_grid, scoring="neg_mean_squared_error", cv=5)
ridge_cv.fit(X_train, y_train)

best_model = ridge_cv.best_estimator_
y_pred = best_model.predict(X_test)
residuals = y_test - y_pred

plt.figure(figsize=(7, 4))
plt.axhline(0, color="black", linewidth=1)
plt.scatter(y_pred, residuals, alpha=0.7)
plt.xlabel("Vorhersage y-hat")
plt.ylabel("Residuum (y - y-hat)")
plt.title("Residualplot")
plt.show()

print("Residual-Mittelwert:", round(float(np.mean(residuals)), 4))
Interpretation: Ein zufaelliges Wolkenbild um 0 ist besser als ein klares Muster. Muster deuten oft auf fehlende Features oder falsche Modellform.
Mini-Check: Was bedeutet ein U-foermiges Residuenmuster?

Das lineare Modell erfasst den Zusammenhang nicht vollstaendig. Es fehlt oft eine nichtlineare Komponente oder ein wichtiges Feature.

5) Ausreisser mit z-Score erkennen (erste einfache Stufe)

Outlier-Kontrolle gehoert in jeden Predictive-Workflow. Als Einstieg hilft der z-Score pro numerischem Feature.

Code
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

def make_demo_df(seed=42, n_samples=320):
    rng = np.random.default_rng(seed)
    df = pd.DataFrame({
        "marketing_spend": rng.normal(120, 25, size=n_samples).clip(40, 220),
        "price_index": rng.normal(1.0, 0.12, size=n_samples).clip(0.7, 1.3),
        "season_index": rng.integers(0, 4, size=n_samples),
        "web_traffic": rng.normal(3200, 700, size=n_samples).clip(1200, 6000)
    })
    noise = rng.normal(0, 9, size=n_samples)
    df["target_demand"] = (
        55
        + 0.22 * df["marketing_spend"]
        - 28 * df["price_index"]
        + 5.5 * df["season_index"]
        + 0.012 * df["web_traffic"]
        + noise
    )
    return df

# Standalone-Block
df = make_demo_df()
X = df.drop(columns=["target_demand"])
y = df["target_demand"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, shuffle=True
)

numeric_cols = X_train.select_dtypes(include=["number"]).columns
z = (X_train[numeric_cols] - X_train[numeric_cols].mean()) / X_train[numeric_cols].std(ddof=0)

# einfache Regel: |z| > 3 als potenzieller Ausreisser
outlier_mask = (np.abs(z) > 3).any(axis=1)
outlier_count = int(outlier_mask.sum())

print("Potenziell auffaellige Zeilen im Train-Set:", outlier_count)
z-Score ist nur ein Einstieg. Kontext und Domainenwissen entscheiden, ob ein Punkt Fehler, Sonderfall oder wertvoller Extremfall ist.
Mini-Check: Welche Outlier-Typen solltet ihr unterscheiden?

Messfehler, seltene aber reale Ereignisse und Datenpunkte ausserhalb des gueltigen Einsatzbereichs. Die Behandlung ist je Typ unterschiedlich.

6) `fit`, `transform`, `fit_transform` richtig einsetzen

Viele Fehler in Einsteigerprojekten entstehen beim Preprocessing. Die goldene Regel: auf Train fitten, auf Test nur transformieren.

Code
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

def make_demo_df(seed=42, n_samples=320):
    rng = np.random.default_rng(seed)
    df = pd.DataFrame({
        "marketing_spend": rng.normal(120, 25, size=n_samples).clip(40, 220),
        "price_index": rng.normal(1.0, 0.12, size=n_samples).clip(0.7, 1.3),
        "season_index": rng.integers(0, 4, size=n_samples),
        "web_traffic": rng.normal(3200, 700, size=n_samples).clip(1200, 6000)
    })
    noise = rng.normal(0, 9, size=n_samples)
    df["target_demand"] = (
        55
        + 0.22 * df["marketing_spend"]
        - 28 * df["price_index"]
        + 5.5 * df["season_index"]
        + 0.012 * df["web_traffic"]
        + noise
    )
    return df

# Standalone-Block
df = make_demo_df()
X = df.drop(columns=["target_demand"])
y = df["target_demand"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, shuffle=True
)

scaler = StandardScaler()

# richtig:
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

print("Scaled train shape:", X_train_scaled.shape)
print("Scaled test shape:", X_test_scaled.shape)

# fit: lernt Parameter (z. B. Mittelwert/Std)
# transform: wendet gelernte Parameter an
# fit_transform: beides in einem Schritt
Warum so streng?
Wenn ihr auf Testdaten fitten wuerdet, fliesst Testinformation ins Training (Data Leakage). Dann wirken Metriken besser als sie real sind.
Mini-Check: Warum helfen Pipelines?

Sie erzwingen die richtige Reihenfolge von Preprocessing und Modell und reduzieren Leakage-Risiko in Training und Cross-Validation.

Nächste Schritte fuer den Kurs

  1. Vergleiche Baseline, Ridge und Lasso auf derselben Testmenge mit MSE und MAE.
  2. Erstelle einen Residualplot und schreibe 3 fachliche Beobachtungen dazu.
  3. Dokumentiere fuer ein Mini-Reporting: Split, bestes alpha, Metriken, Grenzen und naechste Iteration.