feature: add exercise 2

This commit is contained in:
2026-06-18 18:57:43 +02:00
parent 3423172072
commit 7642f2698a
2 changed files with 188 additions and 36 deletions
+68 -36
View File
@@ -1,42 +1,50 @@
# CAS PML Unsupervised Learning: Clustering-Aufgaben
> Modul: CAS Practical Machine Learning Unsupervised Learning (Dehmer)
> Inhalt: zwei Clustering-Übungen rund um K-Means und die Wahl der Clusterzahl $k$
> Inhalt: die zwei Clustering-Übungen aus dem Skript, plus ein Elbow-Demo
## Projektstruktur
```
.
├── data/
── data_delivery_fleet.tsv # Übung 1 (tab-separiert)
── data_delivery_fleet.tsv # Übung 1
│ └── data_stocks.tsv # Übung 2 (Dateiname ggf. anpassen)
├── plots/ # generierte PNGs (headless)
├── src/
│ ├── uebung1_kmeans_fleet.py # Übung 1: k-Means auf Fleet-Daten
── k_means2_elbow_measures.py # Übung 2: Elbow & Gütemasse (synthetisch)
│ ├── uebung1_kmeans_fleet.py # Übung 1: k-Means (Lösung, lauffähig)
── uebung2_kprototypes_stocks.py # Übung 2: k-Prototypes (Codegerüst, TODOs)
│ └── k_means2_elbow_measures.py # Zusatz: Elbow & Gütemasse (synthetisch)
├── devenv.nix
└── README.md
```
> Tipp für konsistente Namen: `k_means2_elbow_measures.py` ggf. zu `uebung2_elbow_measures.py` umbenennen.
## Setup & Ausführen
In der devenv-Shell:
```
python src/uebung1_kmeans_fleet.py
python src/uebung2_kprototypes_stocks.py
python src/k_means2_elbow_measures.py
```
Beide Skripte laufen **headless**: Plots werden als PNG nach `plots/` geschrieben (kein `plt.show()`, da in der Shell kein Display vorhanden ist Agg-Backend).
Für Übung 2 wird das Paket **`kmodes`** benötigt (k-Prototypes ist nicht in scikit-learn).
In `devenv.nix` zu den `venv.requirements` hinzufügen:
```
kmodes
```
Alle Skripte laufen **headless**: Plots werden als PNG nach `plots/` geschrieben (kein `plt.show()`, da in der Shell kein Display Agg-Backend).
## Gütemasse zur Wahl von $k$ (gemeinsame Referenz)
- Inertia / WCSS Summe der quadrierten Abstände zum Zentroid; sinkt monoton mit $k$, daher nur "Knick" (Elbow), kein echtes Optimum
- Inertia / WCSS (k-Means) bzw. `cost_` (k-Prototypes) Summe der Abstände/Unähnlichkeiten zum Zentrum; sinkt monoton mit $k$, daher nur "Knick" (Elbow)
$$ \text{WCSS} = \sum_{i=1}^{k} \sum_{x \in P_i} \lVert x - \mu_i \rVert^2 $$
- Erklärte Varianz Anteil der Between-Cluster-Varianz an der Gesamtvarianz; nutzt $\text{TSS} = \text{WCSS} + \text{BSS}$, höher = besser (ideal 100 %)
- Erklärte Varianz Anteil der Between-Cluster-Varianz an der Gesamtvarianz, höher = besser (ideal 100 %)
$$ \text{Variance Explained} = \frac{\text{BSS}}{\text{TSS}} \cdot 100\,\% $$
@@ -44,7 +52,7 @@ $$ \text{Variance Explained} = \frac{\text{BSS}}{\text{TSS}} \cdot 100\,\% $$
$$ s_i = \frac{b_i - a_i}{\max(a_i, b_i)} $$
- Davies-Bouldin (nur Übung 2) mittleres Verhältnis Streuung/Abstand; niedriger = besser (Achtung: umgekehrt zur Silhouette)
- Davies-Bouldin (nur Zusatz-Demo) niedriger = besser (Achtung: umgekehrt zur Silhouette)
$$ DB = \frac{1}{k} \sum_{i=1}^{k} \max_{j \neq i} \frac{\sigma_i + \sigma_j}{d(c_i, c_j)} $$
@@ -66,47 +74,71 @@ Datensatz `data_delivery_fleet.tsv` (3 Spalten):
Geclustert wird auf `Distance_Feature` × `Speeding_Feature`.
Ablauf von `uebung1_kmeans_fleet.py` (erzeugt PNGs in `plots/`):
Ablauf von `uebung1_kmeans_fleet.py` (PNGs in `plots/`):
1. Rohdaten-Scatter (`01_rohdaten.png`)
2. K-Means mit frei gewähltem $k=4$ (`02_kmeans_k4.png`)
3. Sweep $k = 1 \dots 8$ als Übersichts-Grid (`03_kmeans_sweep_k1-8.png`)
4. $k$ bestimmen über Inertia/Elbow (`04`), erklärte Varianz (`05`) und Silhouette (`06`)
4. $k$ über Inertia/Elbow (`04`), erklärte Varianz (`05`), Silhouette (`06`)
5. finales Modell mit bestem $k$ (`07_final.png`)
Verständnisfragen:
Status: **fertige Lösung** (aus dem Notebook umgebaut, lauffähig).
- [ ] Legen Elbow, erklärte Varianz und Silhouette dasselbe $k$ nahe?
- [ ] Sollte man die zwei Features vor dem Clustern skalieren (Meilen vs. %, sehr unterschiedliche Spannweiten)?
- [ ] Die Hartigan-Regel ist im Lösungs-Notebook enthalten, gilt aber als veraltet warum bevorzugt man Silhouette/Elbow?
> Anpassungen ggü. dem Notebook: `.as_matrix()` → `.to_numpy()` (in aktuellem Pandas entfernt); `plt.show()` → `savefig` (headless); die Hartigan-Funktion ist als optionaler Legacy-Block enthalten (`RUN_HARTIGAN = False`).
> Anpassungen ggü. Notebook: `.as_matrix()` → `.to_numpy()`; `plt.show()` → `savefig`; Hartigan als optionaler Legacy-Block (`RUN_HARTIGAN = False`).
---
## Übung 2 — Elbow-Methode & Gütemasse (synthetisch)
## Übung 2 — k-Prototypes: Stock Data
Ziel: auf synthetischen 2D-Daten (drei Gauss-Blobs) K-Means für verschiedene $k$ laufen lassen und die "richtige" Clusterzahl systematisch über Gütemasse bestimmen statt nur visuell zu schätzen.
Aufgabenstellung (Skript): einen grösseren Stock-Datensatz clustern.
Ablauf von `k_means2_elbow_measures.py` (erzeugt einen 4-Panel-Plot):
1. Datensatz (tab-separiert) laden
2. Daten clustern
1. Daten generieren: je 100 Punkte um $(0,0)$, $(6,6)$, $(0,6)$ (Seed fix → reproduzierbar)
2. Für $k = 1 \dots 8$ K-Means fitten und sammeln: Inertia, Silhouette, Davies-Bouldin (Letztere ab $k \geq 2$)
3. Bestes $k$ wählen (vereinfacht: maximaler Silhouette-Score)
4. Finales Modell fitten → Labels + Zentren
5. Visualisierung: Rohdaten | Elbow (Inertia) | Silhouette | finales Clustering
Warum k-Prototypes? Die Daten sind **gemischt** (numerisch + kategorial):
Verständnisfragen:
- k-Means → nur numerisch
- k-Modes → nur kategorial (Modus statt Mittel, Mismatch-Distanz)
- k-Prototypes = beides kombiniert, gewichtet über $\gamma$
- [ ] Warum nimmt man zur $k$-Wahl die Silhouette und nicht die Inertia?
- [ ] Warum sind die Cluster im Ergebnis-Plot farblich klar getrennt?
- [ ] Warum können verschiedene Runs unterschiedliche Ergebnisse liefern?
- [ ] Ist das gefundene $k$ immer "korrekt"?
$$ D(x, c) = \underbrace{\sum_{\text{num}} (x_j - c_j)^2}_{\text{numerisch (L2)}} + \gamma \underbrace{\sum_{\text{cat}} \delta(x_j, c_j)}_{\text{kategorial (Mismatches)}} $$
Experimente (zum Ausprobieren):
API-Kern (`kmodes`):
- [ ] Zentren näher zusammenlegen (Daten stärker überlappen) → wie verändern sich Elbow und Scores?
- [ ] Davies-Bouldin statt Silhouette als $k$-Kriterium (dann `argmin` statt `argmax`!) → dasselbe $k$?
- [ ] `n_init` reduzieren / Seed variieren → wie stabil ist das Ergebnis?
```python
from kmodes.kprototypes import KPrototypes
kp = KPrototypes(n_clusters=k, init="Cao", random_state=0, n_init=5)
labels = kp.fit_predict(X, categorical=[<indizes>])
kp.cost_ # Elbow-Analogon zur Inertia
kp.cluster_centroids_ # numerisch = Mittel, kategorial = Modus
```
> Zu den Verständnisfragen stehen im Skript kurze Musterantworten als Kommentar.
Gerüst-Abschnitte in `uebung2_kprototypes_stocks.py` (mit TODO selbst füllen):
1. Daten laden & verstehen (dtypes → numerisch vs. kategorial, ID-Spalte raus?)
2. Features vorbereiten: $X$ bauen + `categorical_idx` setzen
3. $k$ finden: Elbow über `cost_`
4. finales Clustering fitten
5. Interpretation: Cluster je Feature beschreiben, semantisch deuten
Status: **Codegerüst** Boilerplate (Imports/Pfade/Plots) gesetzt, Implementierung offen.
---
## Zusatz — Elbow-Methode & Gütemasse (synthetisch)
Kein Skript-Auftrag, sondern ein Demo zur $k$-Wahl auf synthetischen Daten
(drei Gauss-Blobs). Vergleicht Inertia (Elbow), Silhouette und Davies-Bouldin.
Ablauf von `k_means2_elbow_measures.py` (4-Panel-Plot):
1. Daten generieren: je 100 Punkte um $(0,0)$, $(6,6)$, $(0,6)$ (Seed fix)
2. Für $k = 1 \dots 8$ K-Means fitten und Inertia, Silhouette, Davies-Bouldin sammeln
3. Bestes $k$ wählen (vereinfacht: max. Silhouette)
4. Finales Modell + Visualisierung (Rohdaten | Elbow | Silhouette | Ergebnis)
Verständnisfragen / Experimente:
- [ ] Warum zur $k$-Wahl Silhouette statt Inertia?
- [ ] Daten stärker überlappen lassen → wie verändern sich die Scores?
- [ ] Davies-Bouldin statt Silhouette als Kriterium (dann `argmin`!) → dasselbe $k$?
@@ -0,0 +1,120 @@
"""
Übung 2 — k-Prototypes: Stock Data
CAS PML, Unsupervised Learning (Exercise 2 aus dem Skript)
Aufgabe (Skript):
1. Datensatz (tab-separiert) laden
2. Daten clustern
Warum k-Prototypes? Die Stock-Daten sind gemischt (numerisch + kategorial).
- k-Means -> nur numerisch
- k-Modes -> nur kategorial (Modus statt Mittel, Mismatch-Distanz)
- k-Prototypes = k-Means + k-Modes -> gemischte Attribute
CODEGERÜST: Die mit TODO markierten Abschnitte selbst ausfüllen.
Die Boilerplate (Imports, Pfade, Plot-Verzeichnis) ist bereits gesetzt.
"""
import matplotlib
matplotlib.use("Agg") # headless: Plots nur in Dateien
from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# k-Prototypes ist NICHT in scikit-learn -> Paket 'kmodes'
# In devenv.nix zu venv.requirements hinzufügen: kmodes
try:
from kmodes.kprototypes import KPrototypes
except ModuleNotFoundError:
raise SystemExit("Paket 'kmodes' fehlt. In devenv.nix zu venv.requirements: kmodes")
# =========================================================
# EINSTELLUNGEN / PFADE
# =========================================================
BASE = Path(__file__).resolve().parent.parent # .../aufgaben
DATA = BASE / "data" / "data_stocks.tsv" # TODO: Dateinamen anpassen
PLOTS = BASE / "plots"
PLOTS.mkdir(exist_ok=True)
RANDOM_SEED = 0
# =========================================================
# 1. DATEN LADEN & VERSTEHEN
# =========================================================
# TODO: Datensatz tab-separiert laden (pd.read_csv, sep="\t", header=0)
# df = ...
#
# Verschaffe dir einen Überblick, bevor du clusterst:
# df.head() -> erste Zeilen
# df.shape -> Grösse
# df.dtypes -> welche Spalten sind Zahl, welche object/Text?
# df.describe(include="all")
#
# Frage: Welche Spalten sind numerisch, welche kategorial?
# Frage: Gibt es eine ID-/Name-Spalte, die NICHT ins Clustering gehört?
# =========================================================
# 2. FEATURES VORBEREITEN
# =========================================================
# k-Prototypes braucht:
# - ein gemischtes Array X (numerische Spalten als Zahl, kategoriale als String)
# - die INDIZES (0-basiert, bezogen auf X) der kategorialen Spalten
#
# TODO: ID-/irrelevante Spalten entfernen
# TODO: X = df[<feature-spalten>].to_numpy()
# TODO: categorical_idx = [...] # Spaltenindizes der kategorialen Features in X
#
# Frage: Sollten die numerischen Features skaliert werden? Was macht sonst der
# gamma-Parameter (Gewicht numerisch vs. kategorial) implizit mit ungleichen Skalen?
# =========================================================
# 3. PASSENDES k FINDEN (Elbow über cost_)
# =========================================================
# KPrototypes hat nach dem Fit das Attribut .cost_ (analog zur Inertia bei k-Means).
#
# TODO: für mehrere k fitten und cost_ sammeln, dann als Elbow plotten + speichern
# k_values = range(2, 8)
# costs = []
# for k in k_values:
# kp = KPrototypes(n_clusters=k, init="Cao", random_state=RANDOM_SEED, n_init=5)
# kp.fit_predict(X, categorical=categorical_idx)
# costs.append(kp.cost_)
# ... plt-Plot nach PLOTS / "elbow_kprototypes.png"
#
# Frage: Ist ein Elbow erkennbar? Warum ist die k-Wahl bei gemischten Daten oft
# unschärfer als bei rein numerischen (Silhouette braucht ein Distanzmass
# für gemischte Typen z.B. Gower und ist daher nicht out-of-the-box nutzbar)?
# =========================================================
# 4. FINALES CLUSTERING
# =========================================================
# TODO: bestes k wählen und finales Modell fitten
# best_k = ...
# kproto = KPrototypes(n_clusters=best_k, init="Cao", random_state=RANDOM_SEED, n_init=5)
# clusters = kproto.fit_predict(X, categorical=categorical_idx)
#
# kproto.cluster_centroids_ -> Cluster-Zentren
# numerische Spalten: Mittelwert | kategoriale Spalten: Modus (häufigster Wert)
# =========================================================
# 5. INTERPRETATION
# =========================================================
# TODO: Labels an df hängen und Cluster beschreiben
# df["cluster"] = clusters
# - numerisch: df.groupby("cluster").mean(numeric_only=True)
# - kategorial: häufigste Kategorie je Cluster (z.B. mode() oder value_counts())
#
# Frage: Kannst du den Clustern eine semantische Bedeutung geben?
# (Skript-Beispiel: "Männer 3040, Fussball")
#
# TODO (optional): zwei numerische Features streuen, nach Cluster einfärben,
# nach PLOTS / "cluster_scatter.png" speichern