feature: add exercise 2 solution
This commit is contained in:
+3
-14
@@ -12,9 +12,9 @@
|
|||||||
│ └── data_stocks.tsv # Übung 2 (Dateiname ggf. anpassen)
|
│ └── data_stocks.tsv # Übung 2 (Dateiname ggf. anpassen)
|
||||||
├── plots/ # generierte PNGs (headless)
|
├── plots/ # generierte PNGs (headless)
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── uebung1_kmeans_fleet.py # Übung 1: k-Means (Lösung, lauffähig)
|
│ ├── uebung1_kmeans_fleet.py # Übung 1: k-Means
|
||||||
│ ├── uebung2_kprototypes_stocks.py # Übung 2: k-Prototypes (Codegerüst, TODOs)
|
│ ├── uebung2_kprototypes_stocks.py # Übung 2: k-Prototypes
|
||||||
│ └── k_means2_elbow_measures.py # Zusatz: Elbow & Gütemasse (synthetisch)
|
│ └── k_means2_elbow_measures.py # Zusatz: Elbow & Gütemasse
|
||||||
├── devenv.nix
|
├── devenv.nix
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
@@ -29,13 +29,6 @@ python src/uebung2_kprototypes_stocks.py
|
|||||||
python src/k_means2_elbow_measures.py
|
python src/k_means2_elbow_measures.py
|
||||||
```
|
```
|
||||||
|
|
||||||
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).
|
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)
|
## Gütemasse zur Wahl von $k$ (gemeinsame Referenz)
|
||||||
@@ -82,8 +75,6 @@ Ablauf von `uebung1_kmeans_fleet.py` (PNGs in `plots/`):
|
|||||||
4. $k$ über Inertia/Elbow (`04`), erklärte Varianz (`05`), Silhouette (`06`)
|
4. $k$ über Inertia/Elbow (`04`), erklärte Varianz (`05`), Silhouette (`06`)
|
||||||
5. finales Modell mit bestem $k$ (`07_final.png`)
|
5. finales Modell mit bestem $k$ (`07_final.png`)
|
||||||
|
|
||||||
Status: **fertige Lösung** (aus dem Notebook umgebaut, lauffähig).
|
|
||||||
|
|
||||||
> Anpassungen ggü. Notebook: `.as_matrix()` → `.to_numpy()`; `plt.show()` → `savefig`; Hartigan als optionaler Legacy-Block (`RUN_HARTIGAN = False`).
|
> Anpassungen ggü. Notebook: `.as_matrix()` → `.to_numpy()`; `plt.show()` → `savefig`; Hartigan als optionaler Legacy-Block (`RUN_HARTIGAN = False`).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -121,8 +112,6 @@ Gerüst-Abschnitte in `uebung2_kprototypes_stocks.py` (mit TODO selbst füllen):
|
|||||||
4. finales Clustering fitten
|
4. finales Clustering fitten
|
||||||
5. Interpretation: Cluster je Feature beschreiben, semantisch deuten
|
5. Interpretation: Cluster je Feature beschreiben, semantisch deuten
|
||||||
|
|
||||||
Status: **Codegerüst** – Boilerplate (Imports/Pfade/Plots) gesetzt, Implementierung offen.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Zusatz — Elbow-Methode & Gütemasse (synthetisch)
|
## Zusatz — Elbow-Methode & Gütemasse (synthetisch)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
venv.enable = true;
|
venv.enable = true;
|
||||||
venv.requirements = ''
|
venv.requirements = ''
|
||||||
jupyterlab
|
jupyterlab
|
||||||
|
kmodes
|
||||||
matplotlib
|
matplotlib
|
||||||
numpy
|
numpy
|
||||||
pandas
|
pandas
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 214 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
@@ -10,13 +10,9 @@ Warum k-Prototypes? Die Stock-Daten sind gemischt (numerisch + kategorial).
|
|||||||
- k-Means -> nur numerisch
|
- k-Means -> nur numerisch
|
||||||
- k-Modes -> nur kategorial (Modus statt Mittel, Mismatch-Distanz)
|
- k-Modes -> nur kategorial (Modus statt Mittel, Mismatch-Distanz)
|
||||||
- k-Prototypes = k-Means + k-Modes -> gemischte Attribute
|
- 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
|
import matplotlib
|
||||||
matplotlib.use("Agg") # headless: Plots nur in Dateien
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -24,40 +20,42 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
# k-Prototypes ist NICHT in scikit-learn -> Paket 'kmodes'
|
from kmodes.kprototypes import KPrototypes
|
||||||
# In devenv.nix zu venv.requirements hinzufügen: kmodes
|
from sklearn.preprocessing import RobustScaler
|
||||||
try:
|
|
||||||
from kmodes.kprototypes import KPrototypes
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
raise SystemExit("Paket 'kmodes' fehlt. In devenv.nix zu venv.requirements: kmodes")
|
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# EINSTELLUNGEN / PFADE
|
# EINSTELLUNGEN / PFADE
|
||||||
# =========================================================
|
# =========================================================
|
||||||
|
|
||||||
BASE = Path(__file__).resolve().parent.parent # .../aufgaben
|
matplotlib.use("Agg") # headless: Plots nur in Dateien
|
||||||
DATA = BASE / "data" / "data_stocks.tsv" # TODO: Dateinamen anpassen
|
BASE = Path(__file__).resolve().parent.parent # .../aufgaben
|
||||||
|
DATA = BASE / "data" / "stocks2.csv"
|
||||||
PLOTS = BASE / "plots"
|
PLOTS = BASE / "plots"
|
||||||
PLOTS.mkdir(exist_ok=True)
|
PLOTS.mkdir(exist_ok=True)
|
||||||
|
|
||||||
RANDOM_SEED = 0
|
RANDOM_SEED = 0
|
||||||
|
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# 1. DATEN LADEN & VERSTEHEN
|
# 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?
|
|
||||||
|
|
||||||
|
# Daten aus dem Datensatz laden
|
||||||
|
df = pd.read_csv(DATA)
|
||||||
|
|
||||||
|
# Überblich verschaffen, vor dem Clustering
|
||||||
|
print(df.head())
|
||||||
|
print(df.shape)
|
||||||
|
print(df.dtypes)
|
||||||
|
print(df.nunique())
|
||||||
|
print(df.describe(include="all"))
|
||||||
|
|
||||||
|
# Frage: Welche Spalten sind numerisch, welche kategorial?
|
||||||
|
# - Numerisch: Dividend, PE, CAP
|
||||||
|
# - Kategorisch: Sector
|
||||||
|
# Frage: Gibt es eine ID-/Name-Spalte, die NICHT ins Clustering gehört?
|
||||||
|
# - ID: Symbol, Stock -> Reine Identifikation
|
||||||
|
# Entscheidung:
|
||||||
|
# - Wir verwenden also Dividend, PE, CAP und Sector fürs Clustering, der Rest fliegt raus!
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# 2. FEATURES VORBEREITEN
|
# 2. FEATURES VORBEREITEN
|
||||||
@@ -65,56 +63,134 @@ RANDOM_SEED = 0
|
|||||||
# k-Prototypes braucht:
|
# k-Prototypes braucht:
|
||||||
# - ein gemischtes Array X (numerische Spalten als Zahl, kategoriale als String)
|
# - ein gemischtes Array X (numerische Spalten als Zahl, kategoriale als String)
|
||||||
# - die INDIZES (0-basiert, bezogen auf X) der kategorialen Spalten
|
# - 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?
|
|
||||||
|
|
||||||
|
# features auswählen, welche wir oben selektiert haben
|
||||||
|
feature_cols = ["Dividend", "PE", "Cap", "Sector"]
|
||||||
|
# und nach numpy laden
|
||||||
|
X = df[feature_cols].to_numpy()
|
||||||
|
# Sector ist die 4. Spalte in feature_cols -> Index 3 (0-basiert)
|
||||||
|
categorical_idx = [3]
|
||||||
|
# Ausgeben ob wir NaN im Datensatz haben, die wir erst bereinigen müssten
|
||||||
|
print(df[feature_cols].isna().sum())
|
||||||
|
# Überblick über die Daten verschaffen
|
||||||
|
print(df[["Dividend", "PE", "Cap"]].describe())
|
||||||
|
|
||||||
|
# Das Describe oben liefert zutage: CAP hat Werte von 13.9 bis 592771
|
||||||
|
# Und ist damit viel grösser als DIV und PE. Das bedeutet wir müssen CAP entsprechend
|
||||||
|
# skalieren, damit wir nicht nur auf die Marktkapitalisierung clustern.
|
||||||
|
# - Entscheidung: Deshalb: numerische Spalten skalieren, damit jede ungefähr gleich viel beiträgt.
|
||||||
|
|
||||||
|
num_cols = ["Dividend", "PE", "Cap"]
|
||||||
|
cat_cols = ["Sector"]
|
||||||
|
|
||||||
|
# RobustScaler -> gegen die PE/Cap-Ausreisser, spreizt die Hauptmasse besser
|
||||||
|
scaler = RobustScaler()
|
||||||
|
num_scaled = scaler.fit_transform(df[num_cols]) # -> numerisch, standardisiert
|
||||||
|
|
||||||
|
# X zusammenbauen: skalierte numerische Spalten + kategoriale Spalte
|
||||||
|
X = np.column_stack([num_scaled, df[cat_cols].to_numpy()])
|
||||||
|
|
||||||
|
# Reihenfolge in X: [Dividend, PE, Cap, Sector] -> Sector ist Index 3
|
||||||
|
categorical_idx = [3]
|
||||||
|
|
||||||
|
# Ausgeben was der Scaler genau macht
|
||||||
|
print("=== VOR Skalierung ===")
|
||||||
|
print(df[num_cols].describe().loc[["mean", "std", "min", "max"]])
|
||||||
|
print("\n=== NACH Skalierung ===")
|
||||||
|
num_scaled_df = pd.DataFrame(num_scaled, columns=num_cols)
|
||||||
|
print(num_scaled_df.describe().loc[["mean", "std", "min", "max"]])
|
||||||
|
|
||||||
|
# Plotten was der Scaler genau macht
|
||||||
|
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
|
||||||
|
df[num_cols].boxplot(ax=axes[0])
|
||||||
|
axes[0].set_title("Vor Skalierung")
|
||||||
|
axes[0].set_yscale("log") # sonst erdrückt Cap alles
|
||||||
|
num_scaled_df.boxplot(ax=axes[1])
|
||||||
|
axes[1].set_title("Nach StandardScaler")
|
||||||
|
fig.tight_layout()
|
||||||
|
fig.savefig(PLOTS / "u2_skalierung_vergleich.png", dpi=150, bbox_inches="tight")
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# 3. PASSENDES k FINDEN (Elbow über cost_)
|
# 3. PASSENDES k FINDEN (Elbow über cost_)
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# KPrototypes hat nach dem Fit das Attribut .cost_ (analog zur Inertia bei k-Means).
|
# 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)?
|
|
||||||
|
|
||||||
|
# Elbow über cost_
|
||||||
|
k_values = range(2, 10)
|
||||||
|
costs = []
|
||||||
|
|
||||||
|
GAMMA = 0.5 # fixes Gewicht kategorial vs. numerisch -> cost_ über k vergleichbar
|
||||||
|
|
||||||
|
#for k in k_values:
|
||||||
|
# kp = KPrototypes(
|
||||||
|
# n_clusters=k, init="Cao", random_state=RANDOM_SEED, n_init=5, gamma=GAMMA
|
||||||
|
# )
|
||||||
|
# kp.fit_predict(X, categorical=categorical_idx)
|
||||||
|
# costs.append(kp.cost_)
|
||||||
|
# print(f"k={k}: cost={kp.cost_:.1f}")
|
||||||
|
|
||||||
|
#fig, ax = plt.subplots()
|
||||||
|
#ax.plot(list(k_values), costs, marker="o")
|
||||||
|
#ax.set_xlabel("Cluster Count k")
|
||||||
|
#ax.set_ylabel("cost_ (Unähnlichkeit)")
|
||||||
|
#ax.set_title("Elbow – k-Prototypes")
|
||||||
|
#ax.grid(True)
|
||||||
|
#fig.savefig(PLOTS / "u2_elbow_kprototypes.png", dpi=150, bbox_inches="tight")
|
||||||
|
#plt.close(fig)
|
||||||
|
#print("Plot gespeichert: plots/u2_elbow_kprototypes.png")
|
||||||
|
|
||||||
|
# Ergebnis: Elbow: stärkster Steigungsabfall nach k=4
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# 4. FINALES CLUSTERING
|
# 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)
|
|
||||||
|
|
||||||
|
best_k = 4 # Elbow: stärkster Steigungsabfall nach k=4
|
||||||
|
|
||||||
|
kproto = KPrototypes(
|
||||||
|
n_clusters=best_k, init="Cao", random_state=RANDOM_SEED, n_init=5, gamma=GAMMA
|
||||||
|
)
|
||||||
|
clusters = kproto.fit_predict(X, categorical=categorical_idx)
|
||||||
|
|
||||||
|
print("Cluster-Grössen:", np.bincount(clusters))
|
||||||
|
print("\nZentren (Dividend, PE, Cap skaliert | Sector):")
|
||||||
|
print(kproto.cluster_centroids_)
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# 5. INTERPRETATION
|
# 5. INTERPRETATION
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# TODO: Labels an df hängen und Cluster beschreiben
|
|
||||||
# df["cluster"] = clusters
|
df["cluster"] = clusters
|
||||||
# - numerisch: df.groupby("cluster").mean(numeric_only=True)
|
|
||||||
# - kategorial: häufigste Kategorie je Cluster (z.B. mode() oder value_counts())
|
# 5a) Numerische Zentren zurück in Originaleinheiten
|
||||||
#
|
centroids = kproto.cluster_centroids_
|
||||||
# Frage: Kannst du den Clustern eine semantische Bedeutung geben?
|
num_centers_scaled = centroids[:, :len(num_cols)].astype(float) # erste 3 Spalten
|
||||||
# (Skript-Beispiel: "Männer 30–40, Fussball")
|
num_centers = scaler.inverse_transform(num_centers_scaled) # zurückrechnen
|
||||||
#
|
sector_modes = centroids[:, len(num_cols)] # 4. Spalte = Sector-Modus
|
||||||
# TODO (optional): zwei numerische Features streuen, nach Cluster einfärben,
|
|
||||||
# nach PLOTS / "cluster_scatter.png" speichern
|
profile = pd.DataFrame(num_centers, columns=num_cols)
|
||||||
|
profile["Sector (Modus)"] = sector_modes
|
||||||
|
profile["Grösse"] = np.bincount(clusters)
|
||||||
|
print("=== Cluster-Profile (Originaleinheiten) ===")
|
||||||
|
print(profile.round(2))
|
||||||
|
|
||||||
|
# 5b) Plausibilitätscheck: direkt aus den Daten je Cluster mitteln
|
||||||
|
print("\n=== Mittelwerte je Cluster (direkt aus df) ===")
|
||||||
|
print(df.groupby("cluster")[num_cols].mean().round(2))
|
||||||
|
|
||||||
|
# 5c) Cluster visualisieren (Cap vs PE, log-log) logarithmisch, da viele Ausreisser
|
||||||
|
fig, ax = plt.subplots(figsize=(8, 6))
|
||||||
|
ax.scatter(df["Cap"], df["PE"], c=df["cluster"], cmap="tab10", s=12, alpha=0.6)
|
||||||
|
ax.scatter(num_centers[:, num_cols.index("Cap")],
|
||||||
|
num_centers[:, num_cols.index("PE")],
|
||||||
|
c="k", marker="X", s=200, label="Zentren")
|
||||||
|
ax.set_xscale("log")
|
||||||
|
ax.set_yscale("log")
|
||||||
|
ax.set_xlabel("Cap [log]")
|
||||||
|
ax.set_ylabel("PE [log]")
|
||||||
|
ax.set_title(f"k-Prototypes Cluster (k={best_k})")
|
||||||
|
ax.legend()
|
||||||
|
fig.savefig(PLOTS / "u2_cluster_scatter.png", dpi=150, bbox_inches="tight")
|
||||||
|
plt.close(fig)
|
||||||
|
print("Plot gespeichert: plots/u2_cluster_scatter.png")
|
||||||
|
|||||||
Reference in New Issue
Block a user