feature(workshop): add workshop8 solutions
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
# Workshop 08 — Standardisierung & Lineare Regression
|
||||
|
||||
> CAS Practical Machine Learning · Supervised Learning · Lektion 5 (Foliensatz 13, Folie 68)
|
||||
> Zeit: 60'
|
||||
|
||||
## Aufgabenstellung
|
||||
|
||||
Untersuche den Einfluss des **Standardisierens der Features** auf folgende Ergebnisse
|
||||
der Linearen Regression:
|
||||
|
||||
- Modellkoeffizienten
|
||||
- Predictions
|
||||
- Score (R²)
|
||||
|
||||
**Optional / zu Hause:** Untersuche den Einfluss des **Logarithmierens des Targets**
|
||||
auf die Performance der Linearen Regression.
|
||||
|
||||
## Datensatz
|
||||
|
||||
Fortsetzung des Praxisteils → **Melbourne Housing Dataset**.
|
||||
|
||||
| Datei | Inhalt |
|
||||
| ---------------------- | --------------------------------------------------- |
|
||||
| `data/melb_data_prep.csv` | aufbereiteter Datensatz (Workshop 03), Target `Price` |
|
||||
| `src/bfh_cas_pml.py` | Kursmodul mit `prep_data()` / `prep_demo_data()` |
|
||||
|
||||
Beide Dateien stammen aus den Kursmaterialien (analog `bank_data_prep.csv` + `bfh_cas_pml.py`
|
||||
bei WS6). `prep_data()` erledigt Feature-Target-Split **und** Train-Test-Split:
|
||||
|
||||
```python
|
||||
from bfh_cas_pml import prep_data
|
||||
X_train, X_test, y_train, y_test = prep_data("data/melb_data_prep.csv", "Price", seed=1234)
|
||||
```
|
||||
|
||||
**Fallback** (self-contained, falls die Kursdateien fehlen): `sklearn.datasets.fetch_california_housing`
|
||||
— ebenfalls Immobilienpreise mit rechtsschiefem Target, gut für den Log-Teil.
|
||||
|
||||
## Ordnerstruktur
|
||||
|
||||
```
|
||||
workshop8
|
||||
├── data
|
||||
│ └── melb_data_prep.csv # aus Kursmaterial (oder Fallback via sklearn)
|
||||
├── devenv.lock
|
||||
├── devenv.nix
|
||||
├── README.md
|
||||
└── src
|
||||
├── bfh_cas_pml.py # aus Kursmaterial (nur für Melbourne-Variante)
|
||||
└── linearregression.py # Lösung
|
||||
```
|
||||
|
||||
## Vorgehen
|
||||
|
||||
1. Daten laden (Variante wählen: Melbourne oder California-Fallback).
|
||||
2. **Baseline** ohne Standardisierung: `LinearRegression` fitten → `coef_`, `intercept_`, Predictions, Score festhalten.
|
||||
3. **Mit Standardisierung**: `StandardScaler` *nur auf `X_train`* fitten, dann `X_train` + `X_test` transformieren → erneut fitten, dieselben Grössen festhalten.
|
||||
4. **Vergleichen**: Koeffizienten, Predictions, Score gegenüberstellen.
|
||||
5. *(optional)* Target logarithmieren (`log1p`/`expm1`), Performance auf Originalskala vergleichen.
|
||||
|
||||
## Erkenntnisse
|
||||
|
||||
### Standardisierung (Schritte 1–3)
|
||||
|
||||
**Kernaussage:** Bei der einfachen `LinearRegression` ändert Standardisieren der Features
|
||||
**nur** Koeffizienten und Intercept (→ Interpretation), **nicht** aber Predictions und Score.
|
||||
OLS ist invariant gegenüber linearer Umskalierung der Features.
|
||||
|
||||
- **Koeffizienten** — ändern sich. Zusammenhang: `coef_std ≈ coef_roh * X_train.std(axis=0)`
|
||||
(Populations-Std, `ddof=0`). Die skalierten Koeffizienten sind „pro Standardabweichung"-Gewichte
|
||||
→ das ist das **standardisierte Regressionsgewicht β** aus dem Theorieteil. Erst dadurch werden
|
||||
die Features untereinander vergleichbar (Roh-Koeffizienten hängen an der jeweiligen Feature-Skala).
|
||||
- **Intercept** — ändert sich: von ≈ −1.06e8 (Vorhersage am unsinnigen Punkt „alle Rohwerte = 0")
|
||||
auf ≈ +1.06e6. Nach dem Skalieren liegt „alle Features = 0" beim Mittelwert jedes Features →
|
||||
dort sagt OLS gerade `y_train.mean()` voraus, also `intercept_std ≈ y_train.mean()`.
|
||||
- **Predictions** — identisch (bis auf Fliesskomma-Rauschen). OLS „sieht" eine reine Umskalierung
|
||||
der Achsen nicht: die Geometrie der Punktwolke bleibt gleich, das Modell gleicht die Skalierung
|
||||
vollständig über die Koeffizienten aus.
|
||||
- **Score (R²)** — identisch: `0.5601419746121108` vs. `…148` (Unterschied erst an der 14. Stelle
|
||||
= numerisches Rauschen aus dem unterschiedlich skalierten Gleichungssystem, kein echter Effekt).
|
||||
|
||||
**Warum bei einfacher LR egal:** OLS wird analytisch über die Normalgleichungen gelöst, kein
|
||||
iterativer Solver → die Skalierung beeinflusst weder Lösung noch Konvergenz.
|
||||
|
||||
**Wann Standardisierung _doch_ zählt:**
|
||||
|
||||
- **Ridge / Lasso**: der Strafterm ($\lambda \sum \beta_j^2$ bzw. $\lambda \sum |b_j|$) bestraft die
|
||||
Koeffizienten*grösse* — die hängt bei Rohdaten an der Feature-Skala → ungleiche Bestrafung.
|
||||
Darum „Standardisierung erforderlich" (vgl. Theorienotizen, Ridge). Hier ändert Skalieren das
|
||||
Ergebnis tatsächlich.
|
||||
- **Interpretierbarkeit / Feature-Vergleich** über β (s.o.).
|
||||
- **iterative Solver** (Gradientenverfahren): Konvergenz — bei OLS irrelevant.
|
||||
|
||||
### Log-Target (Schritt 4, optional — noch offen)
|
||||
|
||||
Anders als Feature-Scaling verändert eine **Target**-Transformation das Modell _wirklich_:
|
||||
`log(y)` modelliert einen multiplikativen statt additiven Zusammenhang.
|
||||
|
||||
Vorgehen:
|
||||
|
||||
- fit auf `np.log1p(y_train)`
|
||||
- Predictions mit `np.expm1(...)` **zurücktransformieren**, *bevor* R² auf der Originalskala gerechnet wird
|
||||
- `log1p`/`expm1` statt `log`/`exp` wegen möglicher Nullwerte (`log(0)` undefiniert)
|
||||
|
||||
Erwartung (Hypothese, selbst verifizieren):
|
||||
|
||||
- Preise sind rechtsschief → Log-Transform macht die Verteilung symmetrischer, Residuen homoskedastischer
|
||||
- `expm1` ist immer > 0 → keine negativen Preis-Vorhersagen mehr (vgl. Folien-Fazit S. 64)
|
||||
- R² auf der Originalskala kann sich ändern — **Achtung:** nicht Log-Skala mit Original-Skala vergleichen
|
||||
|
||||
## Wichtig
|
||||
|
||||
- Scaler **nur auf Trainingsdaten** fitten → sonst Data Leakage.
|
||||
- Beim Log-Target Predictions **vor** der R²-Berechnung zurücktransformieren.
|
||||
|
||||
## Quellen
|
||||
|
||||
- Foliensatz 13 (Regressionsanalyse), V. Vogel, TI BFH — Praxisteil & Folie 68
|
||||
- Notizen: `../../L5_Notizen.md`
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"nodes": {
|
||||
"devenv": {
|
||||
"locked": {
|
||||
"dir": "src/modules",
|
||||
"lastModified": 1781147004,
|
||||
"narHash": "sha256-/s2Fk3BDmdIIwSWZc04fLrCK86chpxpeMRgHXGjzquk=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "15f44b869b9c99b0bb104b7d5a04d9faba540a5e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"dir": "src/modules",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"inputs": {
|
||||
"nixpkgs-src": "nixpkgs-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1778507786,
|
||||
"narHash": "sha256-HzSQCKMsMr8r55LwM1JuzIOB+8bzk0FEv6sItKvsfoY=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"rev": "8f24a228a782e24576b155d1e39f0d914b380691",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "rolling",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1778274207,
|
||||
"narHash": "sha256-I4puXmX1iovcCHZlRmztO3vW0mAbbRvq4F8wgIMQ1MM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b3da656039dc7a6240f27b2ef8cc6a3ef3bccae7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
# Native libs that the pip-wheel-installed numpy/scipy/matplotlib stack
|
||||
# dlopen()s at runtime. zlib war schon in W3/W4 nötig (libz.so.1),
|
||||
# stdenv.cc.cc.lib liefert libstdc++ für die scipy/sklearn-Wheels.
|
||||
packages = [
|
||||
pkgs.zlib
|
||||
pkgs.stdenv.cc.cc.lib
|
||||
];
|
||||
|
||||
languages.python = {
|
||||
enable = true;
|
||||
venv.enable = true;
|
||||
venv.requirements = ''
|
||||
pandas
|
||||
numpy
|
||||
scikit-learn
|
||||
matplotlib
|
||||
seaborn
|
||||
'';
|
||||
};
|
||||
|
||||
# Loader-Pfad für die obigen nativen Libs. Wenn beim Import trotzdem ein
|
||||
# "ImportError: libXYZ.so.N" auftaucht: das bereitstellende pkgs.<paket>
|
||||
# zu packages UND hier ergänzen — gleiches Muster wie der W3-Fix.
|
||||
env.LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
|
||||
pkgs.zlib
|
||||
pkgs.stdenv.cc.cc.lib
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
Useful functions for example notebooks and workshop solutions
|
||||
of course Practical Machine Learning - Supervised Learning
|
||||
Bern University of Applied Sciences (BFH)
|
||||
"""
|
||||
|
||||
|
||||
# ========== Packages ==========
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
|
||||
|
||||
# ========== Functions ==========
|
||||
|
||||
def prep_data(dataset, target, train_ratio = 2 / 3, seed = None, sep = ','):
|
||||
""" read and prepare real data from the current directory
|
||||
performs
|
||||
read data
|
||||
features - target - split
|
||||
train - test - split
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dataset: name of dataset in csv format
|
||||
target: name of target column
|
||||
train_ratio (2 / 3): (optional)
|
||||
seed (None): random seet for split (optional)
|
||||
sep (,): separator of csv file (optional)
|
||||
|
||||
Returns
|
||||
-------
|
||||
X_train: feature matrix of train set
|
||||
X_test: target vector of train set
|
||||
y_train: feature matrix of test set
|
||||
y_test: target vector of train set
|
||||
"""
|
||||
|
||||
## load data
|
||||
data = pd.read_csv(dataset, sep = sep)
|
||||
|
||||
## features - target - split
|
||||
X = data.drop(target, axis=1)
|
||||
y = data[target]
|
||||
|
||||
## train - test - split
|
||||
from sklearn.model_selection import train_test_split
|
||||
return train_test_split(
|
||||
X,
|
||||
y,
|
||||
train_size=train_ratio,
|
||||
random_state=seed)
|
||||
|
||||
|
||||
|
||||
def prep_demo_data(dataset, target):
|
||||
""" read demo data from the current directory
|
||||
performs
|
||||
read data
|
||||
features - target - split
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dataset: name of dataset in csv format, ',' separated
|
||||
target: name of target column
|
||||
|
||||
Returns
|
||||
-------
|
||||
X: feature matrix
|
||||
y: target vector
|
||||
"""
|
||||
|
||||
## load data
|
||||
data = pd.read_csv(dataset)
|
||||
|
||||
## features - target - split
|
||||
X = data.drop(target, axis=1)
|
||||
y = data[target]
|
||||
|
||||
return X, y
|
||||
|
||||
|
||||
|
||||
def inspect_decision_tree_model(model_def, features, target, figsize=(6, 6)):
|
||||
""" train a DecisionTreeClassifier and visualize the tree
|
||||
|
||||
prints some motel attributes from within the function
|
||||
|
||||
Parameters
|
||||
----------
|
||||
model_def: DecisionTreeClassifier object with set parameters
|
||||
features: feature matrix
|
||||
target: target vector
|
||||
figsize: size of image, optional, default = (6, 6)
|
||||
|
||||
Returns
|
||||
-------
|
||||
visualization of the trained tree
|
||||
prints model attributes
|
||||
"""
|
||||
|
||||
from sklearn.tree import plot_tree
|
||||
|
||||
model = model_def
|
||||
model.fit(features, target)
|
||||
|
||||
print('TREE DIAGNOSTICS:')
|
||||
print('depth :', model.get_depth())
|
||||
print('leaves :', model.get_n_leaves())
|
||||
print('score :', model.score(features, target))
|
||||
|
||||
plt.figure(figsize=figsize)
|
||||
plot_tree(model,
|
||||
feature_names=features.columns,
|
||||
class_names=model.classes_,
|
||||
filled=True);
|
||||
|
||||
|
||||
|
||||
def test_regression_model(model, X_train, y_train, X_test, y_test, show_plot=True):
|
||||
|
||||
""" shows behavoiur of univariate ML regression on synthetic dataset
|
||||
|
||||
performs
|
||||
- training on train data
|
||||
- prediction on test data
|
||||
- calculate performance measures
|
||||
|
||||
Parameters
|
||||
----------
|
||||
model: a parametrized regression model
|
||||
X_train, y_train: train data
|
||||
X_test, y_test: test data
|
||||
show_plot: show scatterplot ov pred vs true, optional, default=True
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
shows a scatterplot von X_test vs X_pred with a diagonal line, indicating identity
|
||||
prints r2_score and mean_squared_error
|
||||
|
||||
"""
|
||||
|
||||
from sklearn.metrics import r2_score
|
||||
from sklearn.metrics import mean_squared_error
|
||||
|
||||
model = model
|
||||
model.fit(X_train, y_train)
|
||||
y_pred = model.predict(X_test)
|
||||
print('R2 = %0.4f' %(r2_score(y_test, y_pred)))
|
||||
|
||||
if show_plot == True:
|
||||
plt.figure(figsize=(6,6))
|
||||
ax = sns.scatterplot(x=y_test, y=y_pred)
|
||||
ax.set(xlabel='y_test', ylabel='y_pred')
|
||||
ls = np.linspace(min(y_test), max(y_test), 100)
|
||||
plt.plot(ls, ls, color='black', linestyle='dashed')
|
||||
ax.set_title(model.__class__.__name__)
|
||||
plt.show()
|
||||
|
||||
return (model)
|
||||
|
||||
|
||||
|
||||
def show_pred_on_synth(model, X, y, X_synth, param_str):
|
||||
""" shows behavoiur of univariate ML regression on synthetic dataset
|
||||
|
||||
Parameters
|
||||
----------
|
||||
model: a parametrized regression model
|
||||
X, y: data for univariate regression
|
||||
X_synth: synthetic Feature
|
||||
param_str: parameter description for title
|
||||
seed (None): random seet for split
|
||||
|
||||
Returns
|
||||
-------
|
||||
a scatterplot von X, y, with the prediction values for X_synth
|
||||
|
||||
"""
|
||||
|
||||
model.fit(X.to_numpy(), y)
|
||||
y_pred = model.predict(X_synth)
|
||||
|
||||
ax = sns.scatterplot(x=X['X'], y=y)
|
||||
ax = sns.lineplot(x=X_synth[:,0], y=y_pred, color='orange')
|
||||
ax.set_title(model.__class__.__name__ + ' : ' + param_str)
|
||||
ax.set(xlabel='X', ylabel='y')
|
||||
plt.show()
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Workshop 08 — Einfluss von Standardisierung (und optional Log-Target)
|
||||
auf die Lineare Regression.
|
||||
|
||||
Aufgabe (Folie 68): untersuche den Einfluss des Standardisierens der Features auf
|
||||
- Modellkoeffizienten
|
||||
- Predictions
|
||||
- Score (R²)
|
||||
Optional: Einfluss des Logarithmierens des Targets auf die Performance.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from sklearn.linear_model import LinearRegression
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.metrics import r2_score
|
||||
from bfh_cas_pml import prep_data
|
||||
|
||||
# datensatz laden
|
||||
X_train, X_test, y_train, y_test = prep_data(
|
||||
"data/melb_data_prep.csv", "Price", seed=1234
|
||||
)
|
||||
|
||||
# --- 1) Baseline: ohne Standardisierung ----------------------------------
|
||||
|
||||
# LinearRegression auf X_train/y_train fitten
|
||||
model = LinearRegression()
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
# festhalten -> coef_, intercept_, y_pred = predict(X_test), score(X_test, y_test)
|
||||
print(f"---- Lineare Regression ohne Standardisierung:")
|
||||
print(f"params: {model.get_params()}")
|
||||
print(f"intercept: {model.intercept_}")
|
||||
print(f"coefficients: {model.coef_}")
|
||||
print(f"score: {model.score(X_test, y_test)}")
|
||||
|
||||
# --- 2) Mit Standardisierung der Features ---------------------------------
|
||||
|
||||
# StandardScaler() -> fit(X_train) -> transform(X_train), transform(X_test)
|
||||
scaler = StandardScaler()
|
||||
X_train_std = scaler.fit_transform(X_train) # lernt μ,σ auf train UND transformiert
|
||||
X_test_std = scaler.transform(X_test) # nutzt dieselben μ,σ kein erneutes fit!
|
||||
# neues LinearRegression auf den skalierten Trainingsdaten fitten
|
||||
model_std = LinearRegression()
|
||||
model_std.fit(X_train_std, y_train)
|
||||
|
||||
# festhalten -> coef_, intercept_, y_pred = predict(X_test), score(X_test, y_test)
|
||||
print(f"---- Lineare Regression mit Standardisierung:")
|
||||
print(f"params: {model_std.get_params()}")
|
||||
print(f"intercept: {model_std.intercept_}")
|
||||
print(f"coefficients: {model_std.coef_}")
|
||||
print(f"score: {model_std.score(X_test_std, y_test)}")
|
||||
|
||||
# --- 3) Vergleich ---------------------------------------------------------
|
||||
# Frage A (Koeffizienten):
|
||||
# Stell VOR dem Ausführen eine Hypothese auf, wie coef_neu mit coef_alt
|
||||
# zusammenhängt (Tipp: nur die Einheit jedes Features hat sich geändert).
|
||||
# Prüfe sie dann, z.B.:
|
||||
# np.round(coef_neu / (coef_alt * X_train.std(axis=0)), 6)
|
||||
# Was bedeutet das für die Vergleichbarkeit der Features untereinander?
|
||||
# (Bezug: standardisiertes Regressionsgewicht β aus dem Theorieteil)
|
||||
#
|
||||
# Frage B (Predictions):
|
||||
# Was "sieht" OLS von einer reinen linearen Umskalierung der Eingänge?
|
||||
# Erwartest du Unterschiede? Begründe, DANN prüfe:
|
||||
# np.allclose(y_pred_alt, y_pred_neu)
|
||||
# Sind sie exakt gleich oder nur sehr nahe? Warum?
|
||||
#
|
||||
# Frage C (Score):
|
||||
# Folgt direkt aus deiner Antwort zu B. Prüfe den R² beider Modelle.
|
||||
# TODO: Vergleich umsetzen (z.B. DataFrame für die Koeffizienten-Gegenüberstellung)
|
||||
|
||||
|
||||
# --- 4) Optional: Log-Target ---------------------------------------------
|
||||
# Achtung: das Target zu transformieren verändert das MODELL wirklich
|
||||
# (anders als Feature-Scaling in 1-3!).
|
||||
# - fit auf np.log1p(y_train)
|
||||
# - Vorhersagen mit np.expm1(...) ZURÜCKtransformieren, BEVOR du R²
|
||||
# auf der Originalskala rechnest
|
||||
# - Fragen:
|
||||
# * warum log1p/expm1 statt log/exp?
|
||||
# * warum kann das die negativen Preis-Vorhersagen aus dem Folien-Fazit verhindern?
|
||||
# * vergleichst du R² auf der Log- oder der Originalskala? (Vorsicht!)
|
||||
# TODO
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass # TODO: Ablauf aufrufen / Ergebnisse ausgeben
|
||||
Reference in New Issue
Block a user