Wird dieser Post performen? Eine kompakte scikit-learn-Pipeline, die aus Post-Metadaten (Tageszeit, Hashtags, Länge, Medientyp) Engagement vorhersagt – mit Logistischer Regression und Gradient Boosting als faire Baselines, evaluiert über ROC-AUC und PR-AUC.
1. Das Problem als Klassifikation formulieren
„Wird dieser Post performen?" ist die häufigste Frage im Content-Marketing. Bevor ein Modell trainiert wird, muss sie in ein wohldefiniertes Lernproblem übersetzt werden:
| Element | Konkretisierung |
|---|---|
| Eingabe | Post-Metadaten zum Zeitpunkt der Veröffentlichung |
| Ausgabe | high_engagement ∈ {0,1} (oberste 20 % der Engagement-Verteilung) |
| Schätzer | Kalibrierte Wahrscheinlichkeit P(high_engagement = 1 | features) |
| Erfolgsmaß | ROC-AUC + PR-AUC + Calibration |
Eine binäre Schwelle (Top-20 %) ist robuster als eine Regression auf rohe Like-Zahlen: Letztere wird von viralen Ausreißern dominiert.
2. Feature Engineering aus Post-Metadaten
Vier Feature-Familien decken den Großteil des erklärbaren Signals ab:
- Zeitmerkmale: Stunde, Wochentag, Feiertag-Flag, „Prime-Time"-Indikator.
- Inhaltsmerkmale: Textlänge, Anzahl Hashtags, Anzahl Mentions, Emoji-Anteil, Sprache.
- Medienmerkmale: hat_bild, hat_video, hat_link, Bildanzahl.
- Historische Merkmale: Rolling-Mean Engagement des Accounts (letzte 30 Tage).
Quick-Check: Welches Feature ist am ehesten ein 'Data-Leak' beim Engagement-Modell?
❌ Imperativ: Verstreute Feature-Erzeugung
# Anti-Pattern: Features werden ad-hoc in Notebooks erzeugt,
# Train- und Test-Logik divergieren.
df["hour"] = df["timestamp"].dt.hour
df["prime_time"] = ((df["hour"] >= 18) & (df["hour"] <= 22)).astype(int)
df_train["text_len"] = df_train["text"].str.len()
df_test["text_len"] = df_test["text"].str.len()
# ... 50 weitere Zeilen, jede mit potentieller Inkonsistenz
✅ Funktional: Pipeline als komponierte Transformationen
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler
# Eine einzige, wiederverwendbare Definition – kein Drift zwischen Train und Test.
pipeline = Pipeline([
("preprocess", ColumnTransformer([
("num", StandardScaler(), ["hour", "text_len", "n_hashtags"]),
# Kategoriale Features würden hier OneHot/Target-Encoding bekommen.
])),
("clf", LogisticRegression(max_iter=1000)),
])
3. End-to-End-Pipeline: Daten → Modell → Evaluation
Der folgende lauffähige Block trainiert beide Modelle (LogReg, GradientBoosting) auf einem synthetischen Datensatz und evaluiert sie mit beiden AUC-Varianten plus Calibration.
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import roc_auc_score, average_precision_score, brier_score_loss
rng = np.random.default_rng(42)
n = 3000
# Synthetischer Post-Datensatz mit realistischen Treibern.
hour = rng.integers(0, 24, n)
has_image = rng.integers(0, 2, n)
has_video = rng.integers(0, 2, n)
text_len = rng.integers(20, 280, n)
n_hashtags = rng.integers(0, 8, n)
account_avg = rng.normal(50, 20, n).clip(5, 200)
prime_time = ((hour >= 18) & (hour <= 22)).astype(int)
logit = (
-1.5
+ 0.9 * has_image
+ 0.6 * has_video
+ 0.7 * prime_time
+ 0.01 * (account_avg - 50)
- 0.002 * (text_len - 150)
+ 0.05 * n_hashtags
+ rng.normal(0, 0.5, n)
)
p = 1 / (1 + np.exp(-logit))
y = (rng.random(n) < p).astype(int)
X = pd.DataFrame({
"hour": hour, "has_image": has_image, "has_video": has_video,
"text_len": text_len, "n_hashtags": n_hashtags, "account_avg": account_avg,
"prime_time": prime_time,
})
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
logreg = Pipeline([("sc", StandardScaler()), ("clf", LogisticRegression(max_iter=1000))])
gbm = GradientBoostingClassifier(random_state=0, n_estimators=100, max_depth=3)
def evaluate(name, model):
model.fit(X_tr, y_tr)
p_hat = model.predict_proba(X_te)[:, 1]
print(f"{name:8s} ROC-AUC={roc_auc_score(y_te, p_hat):.3f} "
f"PR-AUC={average_precision_score(y_te, p_hat):.3f} "
f"Brier={brier_score_loss(y_te, p_hat):.3f}")
print(f"Positiv-Rate im Testset: {y_te.mean():.3f}")
evaluate("LogReg", logreg)
evaluate("GBM", gbm)
Drei Metriken erzählen drei verschiedene Geschichten:
- ROC-AUC misst die globale Ranking-Qualität (höher = besser, 0.5 = Zufall).
- PR-AUC ist sensibler gegenüber der Positiv-Klasse – relevanter, wenn die Klasse selten ist.
- Brier-Score misst die Kalibrierung (niedriger = besser): Sind die Wahrscheinlichkeiten realistisch?
Quick-Check: Was misst der AUC-Wert (ROC-AUC)?
4. Feature-Importance und Erklärbarkeit
LogReg liefert direkt interpretierbare Koeffizienten (nach Standardisierung). GBM benötigt feature_importances_ oder besser SHAP-Werte. Das folgende Snippet zeigt beide Wege nebeneinander:
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
rng = np.random.default_rng(42)
n = 3000
X = pd.DataFrame({
"hour": rng.integers(0, 24, n),
"has_image": rng.integers(0, 2, n),
"has_video": rng.integers(0, 2, n),
"text_len": rng.integers(20, 280, n),
"n_hashtags": rng.integers(0, 8, n),
})
y = ((0.9 * X["has_image"] + 0.6 * X["has_video"] + rng.normal(0, 0.5, n)) > 0.3).astype(int)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0)
logreg = Pipeline([("sc", StandardScaler()), ("clf", LogisticRegression(max_iter=1000))]).fit(X_tr, y_tr)
gbm = GradientBoostingClassifier(random_state=0).fit(X_tr, y_tr)
print("LogReg-Koeffizienten (standardisiert):")
for name, coef in zip(X.columns, logreg.named_steps["clf"].coef_[0]):
print(f" {name:12s} {coef:+.3f}")
print("\nGBM Feature-Importances:")
for name, imp in zip(X.columns, gbm.feature_importances_):
print(f" {name:12s} {imp:.3f}")
5. Diskussion: Trade-offs zwischen den Modellklassen
| Aspekt | Logistische Regression | Gradient Boosting |
|---|---|---|
| Performance auf typischen Marketing-Daten | Solide Baseline | Oft 1–3 Prozentpunkte AUC besser |
| Trainingszeit | Sekunden | Minuten bis Stunden |
| Erklärbarkeit | Direkte Koeffizienten | SHAP, Permutation Importance |
| Robustheit gegen Drift | Hoch | Mittel |
| Out-of-the-box Kalibrierung | Gut | Eher schlecht → CalibratedClassifierCV |
In der Praxis ist die ehrliche Frage selten „Welches Modell ist besser?", sondern „Lohnt sich der Performance-Zugewinn die zusätzliche Komplexität?". Bei einem Engagement-Prediction-Use-Case mit 50.000 Posts pro Tag und einer Geschäfts-Hebelwirkung im einstelligen Prozentbereich ist die Antwort fast immer: Eine kalibrierte LogReg gewinnt, wenn man Wartung mit einrechnet.
6. Abschluss-Quiz
Welche Aussage zum Vergleich LogReg vs. Gradient Boosting ist im Marketing-Kontext am sinnvollsten?
Warum ist eine `Pipeline` (sklearn) besser als manuelles Feature Engineering im Notebook?
In welchem Szenario ist PR-AUC aussagekräftiger als ROC-AUC?
7. Weiterführend
- Nächster Artikel: Click-Through-Rate (CTR) Prediction (Artikel 03 dieser Serie).
- Knowledgebase:
machinelearningwithpytorchandscikit-learn.md– Kapitel zu Pipelines und Modell-Selektion. - Querverweis: recommendation-systems.
Quellen
- Machine Learning with PyTorch and Scikit-Learn (Sebastian Raschka et al.) – Kapitel 6 + 7.
- Hands-On Machine Learning (Aurélien Géron) – Kapitel zu Ensemble-Methoden.