diff --git a/NSL/aufgaben/README.md b/NSL/aufgaben/README.md index 23936ae..6027a81 100644 --- a/NSL/aufgaben/README.md +++ b/NSL/aufgaben/README.md @@ -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=[]) +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$? diff --git a/NSL/aufgaben/src/uebung2_kprototypes_stocks.py b/NSL/aufgaben/src/uebung2_kprototypes_stocks.py new file mode 100644 index 0000000..8262c75 --- /dev/null +++ b/NSL/aufgaben/src/uebung2_kprototypes_stocks.py @@ -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[].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 30–40, Fussball") +# +# TODO (optional): zwei numerische Features streuen, nach Cluster einfärben, +# nach PLOTS / "cluster_scatter.png" speichern