# Workshop 4 — kNN Hyperparametersuche Hyperparameter-Tuning für `KNeighborsClassifier` auf `bank_data_prep.csv` (Klassifikation: Hat der Kunde abgeschlossen, ja/nein?), inklusive Vergleich mit/ohne Standardisierung. - **Input:** `data/bank_data_prep.csv` (Output aus Workshop 3 / Bank-Implementation) - **Pipeline:** `src/hyperparametersearch.py` ## Aufgabenstellung Drei Teile gemäss Folie: 1. Features von Trainings- und Testdaten mit `StandardScaler` standardisieren. 2. Beste Parameter für `KNeighborsClassifier` finden: - `n_neighbors` ∈ {1..10} - `p` ∈ {1, 2, 3} 3. Ergebnisse mit vs. ohne Standardisieren vergleichen. ## Konzepte kurz erklärt ### Was ist `p`? (Minkowski-Distanz) `p` steuert die Distanzmetrik, mit der kNN „Nachbarschaft" misst: | `p` | Distanz | Formel | Verhalten | |----:|---------|--------|-----------| | 1 | Manhattan | Σ \|x_i − y_i\| | Strassen-Netz-Distanz, robuster gegen Ausreisser in Einzeldimensionen | | 2 | Euklidisch | √Σ(x_i − y_i)² | „Luftlinie", sklearn-Default | | 3+ | Höhere Minkowski | (Σ\|x_i − y_i\|^p)^(1/p) | Gewichtet grosse Einzelunterschiede zunehmend stärker | Bei diesem Datensatz gewinnt `p=1` (Manhattan) konsistent über alle Varianten — plausibel, weil viele Dummy-Features (0/1) drin sind und Manhattan diese gleichmässiger gewichtet als Euklidisch. ### Warum Standardisierung bei kNN nicht optional ist kNN ist **distanzbasiert**. Ohne Skalierung dominiert die Variable mit dem grössten Wertebereich die Distanzberechnung — Dummy-Features (0/1) werden faktisch ignoriert, weil ihr Beitrag zur Distanz verschwindet gegen z.B. `age` (17–98) oder `duration` (Sekunden). `StandardScaler` zentriert jede Spalte auf Mittelwert 0 / Standardabweichung 1 und stellt damit alle Features gleichwertig. ### Leakage vermeiden: `fit` nur auf Train ```python scaler.fit(X_train) # mean/std NUR aus Train lernen X_train = scaler.transform(X_train) X_test = scaler.transform(X_test) # mit Train-Statistiken transformieren ``` Würde der Scaler auf `X_test` (oder dem gesamten Datensatz) gefittet, flössen Test-Statistiken in die Vorverarbeitung ein → Leakage. Das Test-Set soll so behandelt werden, als sähe man es zum ersten Mal — denn so wird es in Produktion (neue Daten) auch sein. ### Hyperparametersuche: manuell vs. GridSearchCV Folien-Methode: Doppelschleife über `n_neighbors × p`, Score auf Test-Set genommen, beste Kombi gewählt. Funktioniert, hat aber zwei Schwächen: 1. **Optimistic Bias:** Tunt gegen das Test-Set. Die gewählte Kombi ist die, die zufällig auf *genau diesem einen* Split am besten lief. 2. **Einziger Split:** Keine Robustheit gegen ungünstige Train/Test-Aufteilungen. `GridSearchCV` löst beides: Cross-Validation auf den Trainingsdaten (k-Fold, hier 5), Test-Set wird **erst am Ende einmal** angefasst. ## Drei Varianten | Variante | Skalierung | Hyperparameter-Suche | Bestes k, p | Accuracy | |----------|:----------:|----------------------|:-----------:|:--------:| | A | nein | manuell (Doppelschleife) | k=9, p=1 | 76,6 % (Test) | | B | ja | manuell (Doppelschleife) | k=9, p=1 | 80,4 % (Test) | | C | ja | GridSearchCV (5-fold) | k=9, p=1 | 81,1 % (CV) / 80,4 % (Test) | **Lesart:** - A → B: +3,7 pp durch Skalierung. Belegt Aufgabenteil 3. - B → C: Accuracy bleibt gleich, aber der **CV-Score** ist der ehrliche Schätzer für die Generalisierung. Der Test-Score von B war leicht optimistisch verzerrt (gegen Test getunt); C bestätigt ihn unabhängig. ## Klassifikationsqualität (Variante C) ``` [[1461 274] [ 371 1181]] precision recall f1-score support no 0.80 0.84 0.82 1735 yes 0.81 0.76 0.79 1552 accuracy 0.80 3287 ``` Beide Klassen werden ausgewogen vorhergesagt (F1 ≈ 0,80 für beide). Das Modell ist leicht konservativer mit `yes`-Vorhersagen (Recall 0,76 vs. 0,84 für `no`). ## Befund zum Datensatz `bank_data_prep.csv` ist mit 53/47 (`no`/`yes`) annähernd balanciert. Das Original-Bank-Set hat eine ~88/12-Verteilung — der gelieferte Datensatz wurde also vorab **resampled** (vermutlich SMOTE oder Random Over/Undersampling via `imbalanced-learn`). Konsequenz: Accuracy ist hier eine vertretbare Metrik; wäre der Datensatz im Original-Verhältnis, wären 80 % Accuracy *schlechter* als die triviale „immer no"-Baseline (88 %), und man müsste mit Precision/ Recall/F1 arbeiten. ## Projektstruktur ``` workshop4 ├── data/ │ └── bank_data_prep.csv ├── src/ │ └── hyperparametersearch.py ├── devenv.nix └── README.md ``` ## Ausführen ```sh python src/hyperparametersearch.py ``` ## Implementierungs-Notizen Bewusste Entscheidungen, die über die Folien-Vorlage hinausgehen: - **`fit` nur auf `X_train`** beim `StandardScaler` (Folie macht es so, viele Anleitungen nicht — daher hier explizit dokumentiert). - **`GridSearchCV` zusätzlich zur Folien-Doppelschleife**, um Optimistic Bias sichtbar zu machen und einen leakage-freien CV-Score zu erhalten. - **`classification_report` + Confusion Matrix** über die Folien-Anforderungen hinaus, weil reine Accuracy bei Klassifikation nie das ganze Bild zeigt. ## Offene Punkte für ein echtes Projekt - **`Pipeline` für Scaler + kNN.** Ein `Pipeline([('scaler', ...), ('knn', ...)])` macht aus den zwei Schritten ein Objekt — `predict()` skaliert dann automatisch mit dem trainierten Scaler. Verhindert strukturell die „Modell trainiert auf skaliert, Vorhersage auf unskaliert"-Klasse von Bugs. Auch GridSearchCV erhält dann die Pipeline statt nur den Classifier, was die Skalierung in *jedem* CV-Fold korrekt nur auf dem Fold-Train fittet. - **Resampling-Schritt im Datenpfad sichtbar machen.** Der Resampling-Eingriff passierte ausserhalb dieser Pipeline; in einem echten Projekt gehört er dokumentiert oder selbst (re-)produziert, sonst sind Ergebnisse nicht reproduzierbar. - **Mehr Metriken bei der CV.** `GridSearchCV(scoring=...)` kann auch auf F1, ROC-AUC u.a. optimieren — sinnvoller als Accuracy, sobald Klassen unbalanciert sind.