feature: move code from python to jupyter notebook, add plots
@@ -1,66 +1,112 @@
|
||||
# K-Means: Elbow-Methode & Gütemasse
|
||||
# CAS PML – Unsupervised Learning: Clustering-Aufgaben
|
||||
|
||||
> Thema: Optimale Clusterzahl $k$ bestimmen und Clusterqualität bewerten
|
||||
> Skript: Unsupervised Learning (CAS PML)
|
||||
|
||||
## 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.
|
||||
> Modul: CAS Practical Machine Learning – Unsupervised Learning (Dehmer)
|
||||
> Inhalt: zwei Clustering-Übungen rund um K-Means und die Wahl der Clusterzahl $k$
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
.
|
||||
├── data/ # leer: Daten werden synthetisch generiert
|
||||
├── data/
|
||||
│ └── data_delivery_fleet.tsv # Übung 1 (tab-separiert)
|
||||
├── plots/ # generierte PNGs (headless)
|
||||
├── src/
|
||||
│ └── k_means2_elbow_measures.py
|
||||
│ ├── uebung1_kmeans_fleet.py # Übung 1: k-Means auf Fleet-Daten
|
||||
│ └── k_means2_elbow_measures.py # Übung 2: Elbow & Gütemasse (synthetisch)
|
||||
├── devenv.nix
|
||||
└── README.md
|
||||
```
|
||||
|
||||
Ausführen (in der devenv-Shell):
|
||||
> 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/k_means2_elbow_measures.py
|
||||
```
|
||||
|
||||
## Ablauf des Skripts
|
||||
Beide Skripte laufen **headless**: Plots werden als PNG nach `plots/` geschrieben (kein `plt.show()`, da in der Shell kein Display vorhanden ist – Agg-Backend).
|
||||
|
||||
1. Daten generieren: je 100 Punkte um die Zentren $(0,0)$, $(6,6)$, $(0,6)$ via `np.random.randn` (Seed fix → reproduzierbar)
|
||||
2. Für $k = 1 \dots 8$ K-Means fitten und drei Masse sammeln:
|
||||
- Inertia (`kmeans.inertia_`) — immer
|
||||
- Silhouette-Score und Davies-Bouldin-Index — erst ab $k \geq 2$ (für $k=1$ nicht definiert → `np.nan`)
|
||||
3. Bestes $k$ wählen (vereinfacht: maximaler Silhouette-Score via `np.nanargmax`)
|
||||
4. Finales Modell mit `best_k` fitten → Labels + Zentren
|
||||
5. Visualisierung in vier Panels: Rohdaten | Elbow (Inertia) | Silhouette | finales Clustering
|
||||
## Gütemasse zur Wahl von $k$ (gemeinsame Referenz)
|
||||
|
||||
## Gütemasse (Kurzreferenz)
|
||||
|
||||
- Inertia / WCSS — Summe der quadrierten Abstände zum jeweiligen Zentroid; sinkt monoton mit $k$, hat also kein Optimum, nur einen "Knick" (Elbow)
|
||||
- Inertia / WCSS – Summe der quadrierten Abstände zum Zentroid; sinkt monoton mit $k$, daher nur "Knick" (Elbow), kein echtes Optimum
|
||||
|
||||
$$ \text{WCSS} = \sum_{i=1}^{k} \sum_{x \in P_i} \lVert x - \mu_i \rVert^2 $$
|
||||
|
||||
- Silhouette-Score — $a_i$ = mittlere Intra-Cluster-Distanz, $b_i$ = mittlere Distanz zum nächsten Cluster; Bereich $[-1, 1]$, höher = besser
|
||||
- Erklärte Varianz – Anteil der Between-Cluster-Varianz an der Gesamtvarianz; nutzt $\text{TSS} = \text{WCSS} + \text{BSS}$, höher = besser (ideal 100 %)
|
||||
|
||||
$$ \text{Variance Explained} = \frac{\text{BSS}}{\text{TSS}} \cdot 100\,\% $$
|
||||
|
||||
- Silhouette-Score – $a_i$ = mittlere Intra-, $b_i$ = mittlere nächste Inter-Cluster-Distanz; Bereich $[-1, 1]$, höher = besser
|
||||
|
||||
$$ s_i = \frac{b_i - a_i}{\max(a_i, b_i)} $$
|
||||
|
||||
- Davies-Bouldin-Index — mittleres "Worst-Case"-Verhältnis von Cluster-Streuung zu Cluster-Abstand; niedriger = besser (Achtung: umgekehrt zur Silhouette!)
|
||||
- Davies-Bouldin (nur Übung 2) – mittleres Verhältnis Streuung/Abstand; 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)} $$
|
||||
|
||||
- mit $\sigma_i$ = mittlere Distanz der Punkte in Cluster $i$ zu ihrem Zentroid $c_i$
|
||||
---
|
||||
|
||||
## Verständnisfragen (im Code eingebettet)
|
||||
## Übung 1 — k-Means: Delivery Fleet
|
||||
|
||||
Aufgabenstellung (Skript): Fleet-Daten von Lieferfahrer:innen clustern.
|
||||
|
||||
1. Datensatz aus tab-separiertem File laden (Pandas)
|
||||
2. Daten visualisieren und eine sinnvolle Clusterzahl visuell *und* systematisch finden
|
||||
3. Daten clustern
|
||||
|
||||
Datensatz `data_delivery_fleet.tsv` (3 Spalten):
|
||||
|
||||
- `Driver_ID` – ID der Fahrer:in
|
||||
- `Distance_Feature` – mittlere gefahrene Meilen pro Tag
|
||||
- `Speeding_Feature` – % der Zeit über dem Tempolimit (>5 mph)
|
||||
|
||||
Geclustert wird auf `Distance_Feature` × `Speeding_Feature`.
|
||||
|
||||
Ablauf von `uebung1_kmeans_fleet.py` (erzeugt 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`)
|
||||
5. finales Modell mit bestem $k$ (`07_final.png`)
|
||||
|
||||
Verständnisfragen:
|
||||
|
||||
- [ ] 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`).
|
||||
|
||||
---
|
||||
|
||||
## Übung 2 — Elbow-Methode & Gütemasse (synthetisch)
|
||||
|
||||
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.
|
||||
|
||||
Ablauf von `k_means2_elbow_measures.py` (erzeugt einen 4-Panel-Plot):
|
||||
|
||||
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
|
||||
|
||||
Verständnisfragen:
|
||||
|
||||
- [ ] 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"?
|
||||
|
||||
## Experimente (zum Ausprobieren)
|
||||
Experimente (zum Ausprobieren):
|
||||
|
||||
- [ ] Zentren näher zusammenlegen (Daten stärker überlappen) → wie verändern sich Elbow und Scores?
|
||||
- [ ] Davies-Bouldin gegen die Silhouette als $k$-Kriterium tauschen → wählt es dasselbe $k$?
|
||||
- [ ] Davies-Bouldin statt Silhouette als $k$-Kriterium (dann `argmin` statt `argmax`!) → dasselbe $k$?
|
||||
- [ ] `n_init` reduzieren / Seed variieren → wie stabil ist das Ergebnis?
|
||||
|
||||
> Zu allen Verständnisfragen stehen im Skript kurze Musterantworten als Kommentar.
|
||||
> Zu den Verständnisfragen stehen im Skript kurze Musterantworten als Kommentar.
|
||||
|
||||
@@ -17,22 +17,6 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1767039857,
|
||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"inputs": {
|
||||
"nixpkgs-src": "nixpkgs-src"
|
||||
@@ -52,27 +36,6 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-python": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1781070343,
|
||||
"narHash": "sha256-wXAybU+2LlbXm9cfRDHRPASakq60rLvZBXVT2Ahkj1U=",
|
||||
"owner": "cachix",
|
||||
"repo": "nixpkgs-python",
|
||||
"rev": "23629493653be6df0472a46b1be1d65cbd6df55b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "nixpkgs-python",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
@@ -93,8 +56,7 @@
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-python": "nixpkgs-python"
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
{ pkgs, lib, config, ... }:
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
|
||||
packages = [
|
||||
pkgs.zlib
|
||||
pkgs.stdenv.cc.cc.lib
|
||||
];
|
||||
|
||||
languages.python = {
|
||||
enable = true;
|
||||
version = "3.14.4";
|
||||
venv.enable = true;
|
||||
venv.requirements = ''
|
||||
ipython
|
||||
jupyter
|
||||
jupyterlab
|
||||
matplotlib
|
||||
numpy
|
||||
pandas
|
||||
scikit-learn
|
||||
seaborn
|
||||
'';
|
||||
};
|
||||
|
||||
packages = [
|
||||
pkgs.graphviz
|
||||
pkgs.zsh
|
||||
env.LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
|
||||
pkgs.zlib
|
||||
pkgs.stdenv.cc.cc.lib
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
inputs:
|
||||
nixpkgs-python:
|
||||
url: github:cachix/nixpkgs-python
|
||||
inputs:
|
||||
nixpkgs:
|
||||
follows: nixpkgs
|
||||
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 623 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 163 KiB |
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
Übung 1 — k-Means: Delivery Fleet
|
||||
CAS PML, Unsupervised Learning
|
||||
|
||||
Aufgabe (Skript):
|
||||
1. Datensatz aus tab-separiertem File laden
|
||||
2. Daten visualisieren, sinnvolle Clusterzahl k finden (visuell + systematisch)
|
||||
3. Daten clustern
|
||||
|
||||
Umbau aus dem Lösungs-Notebook (lektion_1.ipynb):
|
||||
- .as_matrix() -> .to_numpy() (in aktuellem Pandas entfernt)
|
||||
- plt.show() -> savefig nach plots/ (headless, kein Display in der devenv-Shell)
|
||||
- n_init=10, random_state=0 ergänzt (Reproduzierbarkeit, keine FutureWarnings)
|
||||
"""
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg") # headless: nur in Dateien rendern
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
from sklearn.cluster import KMeans
|
||||
from sklearn.metrics import silhouette_score
|
||||
from scipy.spatial.distance import cdist, pdist
|
||||
|
||||
# =========================================================
|
||||
# EINSTELLUNGEN / PFADE
|
||||
# =========================================================
|
||||
|
||||
BASE = Path(__file__).resolve().parent.parent # .../aufgaben
|
||||
DATA = BASE / "data" / "data_delivery_fleet.tsv"
|
||||
PLOTS = BASE / "plots"
|
||||
PLOTS.mkdir(exist_ok=True)
|
||||
|
||||
RANDOM_SEED = 0
|
||||
RUN_HARTIGAN = False # Legacy-Mass (gilt als veraltet), siehe unten
|
||||
|
||||
|
||||
def _scatter_clusters(ax, data, model):
|
||||
"""Punkte nach Cluster-Label einfärben, Zentren als schwarzes X."""
|
||||
ax.scatter(data[:, 0], data[:, 1], c=model.labels_, cmap="tab10", s=10)
|
||||
centers = model.cluster_centers_
|
||||
ax.scatter(centers[:, 0], centers[:, 1], c="k", marker="X", s=150)
|
||||
ax.set_xlabel("distance [mi]")
|
||||
ax.set_ylabel("speeding [%]")
|
||||
|
||||
|
||||
def save_kmeans(data, model, outfile, title=None):
|
||||
fig, ax = plt.subplots()
|
||||
_scatter_clusters(ax, data, model)
|
||||
ax.set_title(title or f"k-Means (k={len(model.cluster_centers_)})")
|
||||
fig.savefig(PLOTS / outfile, dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
# =========================================================
|
||||
# 1. DATEN LADEN & VORBEREITEN
|
||||
# =========================================================
|
||||
|
||||
if not DATA.exists():
|
||||
raise SystemExit(
|
||||
f"Daten nicht gefunden: {DATA}\n"
|
||||
"Lege 'data_delivery_fleet.tsv' in den data/-Ordner."
|
||||
)
|
||||
|
||||
df = pd.read_csv(DATA, sep="\t", header=0)
|
||||
print(df.head())
|
||||
|
||||
# Spalten:
|
||||
# Driver_ID - ID der Fahrer:in
|
||||
# Distance_Feature - mittlere gefahrene Meilen pro Tag
|
||||
# Speeding_Feature - % der Zeit > Tempolimit (>5 mph)
|
||||
data = df[["Distance_Feature", "Speeding_Feature"]].to_numpy()
|
||||
print("shape:", data.shape)
|
||||
|
||||
# Rohdaten
|
||||
fig, ax = plt.subplots()
|
||||
ax.scatter(df.Distance_Feature, df.Speeding_Feature, marker=".")
|
||||
ax.set_xlabel("distance [mi]")
|
||||
ax.set_ylabel("speeding fraction [%]")
|
||||
ax.grid(True)
|
||||
fig.savefig(PLOTS / "01_rohdaten.png", dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
# =========================================================
|
||||
# 2. ERSTES CLUSTERING (frei gewähltes k)
|
||||
# =========================================================
|
||||
|
||||
k_means = KMeans(n_clusters=4, n_init=10, random_state=RANDOM_SEED).fit(data)
|
||||
save_kmeans(data, k_means, "02_kmeans_k4.png")
|
||||
|
||||
# Cluster-Zuordnung für k = 1 ... 8 als Übersichts-Grid (statt 8 Einzelplots)
|
||||
fig, axes = plt.subplots(2, 4, figsize=(20, 8))
|
||||
for k, ax in zip(range(1, 9), axes.ravel()):
|
||||
model = KMeans(n_clusters=k, n_init=10, random_state=RANDOM_SEED).fit(data)
|
||||
_scatter_clusters(ax, data, model)
|
||||
ax.set_title(f"k={k}")
|
||||
fig.tight_layout()
|
||||
fig.savefig(PLOTS / "03_kmeans_sweep_k1-8.png", dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
# =========================================================
|
||||
# 3. OPTIMALE CLUSTERZAHL k
|
||||
# =========================================================
|
||||
|
||||
# ---- Within-Cluster Sum of Squares (Inertia) ----
|
||||
Ks = range(1, 10)
|
||||
inertia = [KMeans(n_clusters=k, n_init=10, random_state=RANDOM_SEED).fit(data).inertia_
|
||||
for k in Ks]
|
||||
|
||||
fig, ax = plt.subplots()
|
||||
ax.bar(Ks, inertia)
|
||||
ax.set_title("Within-Cluster Sum of Squares")
|
||||
ax.set_xlabel("Cluster Count k")
|
||||
ax.set_ylabel("Error")
|
||||
ax.grid(True)
|
||||
fig.savefig(PLOTS / "04_inertia.png", dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
# ---- Percentage of Variance Explained ----
|
||||
# TSS = WCSS + BSS -> Variance Explained = BSS / TSS
|
||||
ks = range(1, 10)
|
||||
models = [KMeans(n_clusters=k, n_init=10, random_state=RANDOM_SEED).fit(data) for k in ks]
|
||||
centroids = [m.cluster_centers_ for m in models]
|
||||
|
||||
# Abstand jedes Punkts zum nächsten Zentrum -> WCSS pro k
|
||||
dist = [np.min(cdist(data, cent, "euclidean"), axis=1) for cent in centroids]
|
||||
wcss = np.array([np.sum(d ** 2) for d in dist])
|
||||
tss = np.sum(pdist(data) ** 2) / data.shape[0]
|
||||
bss = tss - wcss
|
||||
|
||||
fig, ax = plt.subplots()
|
||||
ax.plot(list(ks), bss / tss * 100, "b*-")
|
||||
ax.set_ylim((0, 100))
|
||||
ax.set_xlabel("Cluster Count k")
|
||||
ax.set_ylabel("Percentage of variance explained")
|
||||
ax.set_title("Variance Explained vs. k")
|
||||
ax.grid(True)
|
||||
fig.savefig(PLOTS / "05_variance_explained.png", dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
# ---- Silhouette Score (erst ab k >= 2 definiert) ----
|
||||
ks_sil = range(2, 10)
|
||||
silhouette = [silhouette_score(
|
||||
data,
|
||||
KMeans(n_clusters=k, n_init=10, random_state=RANDOM_SEED).fit(data).labels_,
|
||||
metric="euclidean")
|
||||
for k in ks_sil]
|
||||
|
||||
fig, ax = plt.subplots()
|
||||
ax.plot(list(ks_sil), silhouette, marker="o")
|
||||
ax.set_xlabel("Cluster Count k")
|
||||
ax.set_ylabel("Silhouette Score")
|
||||
ax.set_title("Silhouette for k-Means")
|
||||
ax.grid(True)
|
||||
fig.savefig(PLOTS / "06_silhouette.png", dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
best_k = list(ks_sil)[int(np.argmax(silhouette))]
|
||||
print(f"Bestes k nach Silhouette: {best_k}")
|
||||
|
||||
# =========================================================
|
||||
# 4. FINALES MODELL
|
||||
# =========================================================
|
||||
|
||||
k_means_final = KMeans(n_clusters=best_k, n_init=10, random_state=RANDOM_SEED).fit(data)
|
||||
save_kmeans(data, k_means_final, "07_final.png", title=f"Finales Clustering (k={best_k})")
|
||||
|
||||
print(f"Plots gespeichert in: {PLOTS}")
|
||||
|
||||
|
||||
# =========================================================
|
||||
# 5. HARTIGAN-REGEL (optional, Legacy)
|
||||
# =========================================================
|
||||
# Im Lösungs-Notebook enthalten, gilt aber als veraltet (vgl. Chiang & Mirkin).
|
||||
# Silhouette/Elbow werden bevorzugt -> standardmässig deaktiviert (RUN_HARTIGAN).
|
||||
|
||||
def hartigan_k(data, threshold=12):
|
||||
"""'Korrekte' Clusterzahl nach Hartigan: erstes k mit H <= threshold."""
|
||||
inertia_list = np.zeros(len(data) + 1)
|
||||
num = 0
|
||||
h_rule = threshold + 1
|
||||
while num < len(data) and h_rule > threshold:
|
||||
kmn = KMeans(n_clusters=num + 1, n_init=10, random_state=RANDOM_SEED).fit(data)
|
||||
inertia_list[num + 1] = kmn.inertia_
|
||||
if num > 0:
|
||||
h_rule = ((float(inertia_list[num]) / inertia_list[num + 1]) - 1) * (len(data) - num - 1)
|
||||
print(f"k={num}, H={h_rule:.2f}")
|
||||
num += 1
|
||||
if h_rule > threshold:
|
||||
num += 1
|
||||
return num - 1
|
||||
|
||||
|
||||
if RUN_HARTIGAN:
|
||||
k_h = hartigan_k(data, threshold=12)
|
||||
print(f"Hartigan k: {k_h}")
|
||||
model_h = KMeans(n_clusters=k_h, n_init=10, random_state=RANDOM_SEED).fit(data)
|
||||
save_kmeans(data, model_h, "08_hartigan.png", title=f"Hartigan (k={k_h})")
|
||||
@@ -106,10 +106,11 @@ ax[3].set_title(f"K-Means Ergebnis (k={best_k})")
|
||||
# Frage:
|
||||
# Warum sind die Cluster farbig getrennt?
|
||||
# Lösung:
|
||||
# ? Hard Clustering: jeder Punkt genau ein Label
|
||||
# - Hard Clustering: jeder Punkt genau ein Label
|
||||
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
plt.savefig("plots/elbow_measures.png", dpi=150, bbox_inches="tight")
|
||||
print("Plot gespeichert: plots/elbow_measures.png")
|
||||
|
||||
# =========================================================
|
||||
# ? WEITERE FRAGEN
|
||||