feature: add exercise 2 solution
This commit is contained in:
+3
-14
@@ -12,9 +12,9 @@
|
||||
│ └── data_stocks.tsv # Übung 2 (Dateiname ggf. anpassen)
|
||||
├── plots/ # generierte PNGs (headless)
|
||||
├── src/
|
||||
│ ├── 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)
|
||||
│ ├── uebung1_kmeans_fleet.py # Übung 1: k-Means
|
||||
│ ├── uebung2_kprototypes_stocks.py # Übung 2: k-Prototypes
|
||||
│ └── k_means2_elbow_measures.py # Zusatz: Elbow & Gütemasse
|
||||
├── devenv.nix
|
||||
└── README.md
|
||||
```
|
||||
@@ -29,13 +29,6 @@ python src/uebung2_kprototypes_stocks.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).
|
||||
|
||||
## 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`)
|
||||
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`).
|
||||
|
||||
---
|
||||
@@ -121,8 +112,6 @@ Gerüst-Abschnitte in `uebung2_kprototypes_stocks.py` (mit TODO selbst füllen):
|
||||
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)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
venv.enable = true;
|
||||
venv.requirements = ''
|
||||
jupyterlab
|
||||
kmodes
|
||||
matplotlib
|
||||
numpy
|
||||
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-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
|
||||
|
||||
@@ -24,40 +20,42 @@ 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")
|
||||
from kmodes.kprototypes import KPrototypes
|
||||
from sklearn.preprocessing import RobustScaler
|
||||
|
||||
# =========================================================
|
||||
# EINSTELLUNGEN / PFADE
|
||||
# =========================================================
|
||||
|
||||
matplotlib.use("Agg") # headless: Plots nur in Dateien
|
||||
BASE = Path(__file__).resolve().parent.parent # .../aufgaben
|
||||
DATA = BASE / "data" / "data_stocks.tsv" # TODO: Dateinamen anpassen
|
||||
DATA = BASE / "data" / "stocks2.csv"
|
||||
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?
|
||||
|
||||
# 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
|
||||
@@ -65,56 +63,134 @@ RANDOM_SEED = 0
|
||||
# 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?
|
||||
|
||||
# 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_)
|
||||
# =========================================================
|
||||
# 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)
|
||||
|
||||
# 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_)
|
||||
# ... 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)?
|
||||
# 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
|
||||
# =========================================================
|
||||
# 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
|
||||
# =========================================================
|
||||
# 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
|
||||
|
||||
df["cluster"] = clusters
|
||||
|
||||
# 5a) Numerische Zentren zurück in Originaleinheiten
|
||||
centroids = kproto.cluster_centroids_
|
||||
num_centers_scaled = centroids[:, :len(num_cols)].astype(float) # erste 3 Spalten
|
||||
num_centers = scaler.inverse_transform(num_centers_scaled) # zurückrechnen
|
||||
sector_modes = centroids[:, len(num_cols)] # 4. Spalte = Sector-Modus
|
||||
|
||||
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