---
Die Klasse von Wein finden Sie im Notebook _"Klassifikation von Wein"_ heraus. Aber manchmal sind andere Eigenschaften des Weins von Interessse, die sich nicht in eine finite Anzahl von Klassen einteilen lassen. In diesem Notebook befassen wir uns daher mit der Vorhersage des Alkoholgehalts von Wein mittels Regressionsanalyse.

Dazu werden wir einige Regressionsmodelle und weitere Funktionen aus dem Python-Paket [```scikit-learn```](https://scikit-learn.org/stable/user_guide.html) (kurz: ```sklearn```) verwenden. ```Sklearn``` ist ein äußerst mächtiges Framework, das viele Methoden des Maschinenlernens mit einheitlichen Schnittstellen zur Verfügung stellt, so dass Sie schnell und einfach verschiedene Modelle ausprobieren können. Im Laufe dieses Notebooks werden Sie einige der Funktionenen und Objekte zur Regression kennenlernen. In dem Notebook _"Klassifikation von Wein"_ werden Sie ebenfalls mit ```sklearn``` arbeiten, allerdings mit dem Fokus auf die Methoden und Objekte zur Lösung von Klassifikationsproblemen.

Grundsätzlich gilt für beide Notebooks, dass Sie immer auch in der Dokumentation und dem User's Guide von ```sklearn``` sich weitere Informationen zu den verwendeten Objekten holen sollten, um ein tieferes Verständnis für die Zusammenhänge zu erwerben.

## Inhalt

          Daten importieren

    Daten analysieren

    Daten säubern

    Modellauswahl

    Modell speichern

**Hinweis:** In diesem Notebook sind einige Stellen enthalten, an denen Zufallszahlen zum Einsatz kommen (z.B. die Einteilung in Trainings- und Testset). Dadurch kann es passieren, dass manche Ergebnisse für Sie nicht exakt reproduzierbar sind und das relative Ranking der verschiedenen Regressionsmodelle oder die optimalen Werte mancher Hyperparameter bei Ihnen etwas anders aussehen. Das ändert allerdings nichts an der generellen Vorgehensweise oder den Programmiermustern.

---

1. Daten importieren

Die Daten der Weine wurden im Rahmen einer wissenschaftlichen Studie erhoben und die genaue Bedeutung der einzelnen Merkmale können Sie in dem entsprechenden Paper nachlesen. Die gelisteten Merkmale sind:

Fixed acidity

Nichtflüchtige Säure

Volatile acidity:

Flüchtige Säure

Citric acid:

Zitronensäure

Residual sugar:

Restzucker

Chlorides:

Chloride

Free sulfur dioxide:

Freies Schwefeldioxid

Total sulfur dioxide:

Schwefeldioxid gesamt

Density:

Dichte

pH:

pH-Wert

Sulphates

Sulfate

Alcohol:

Der zu bestimmende Alkoholgehalt

Quality:

Subjektive Qualtitätsbewertung

Quelle der Daten: P. Cortez, A. Cerdeira, F. Almeida, T. Matos and J. Reis.
  Modeling wine preferences by data mining from physicochemical properties.
  In Decision Support Systems, Elsevier, 47(4):547-553. ISSN: 0167-9236.

Uns liegen zwei Datensätze zu, von denen einer nur Rotweine und der andere nur Weißweine enthält. Für dieses Notebook wollen wir mal annehmen, dass die Farbe des Weins keinen Einfluss auf das Ergebnis hat.

Die ersten Schritt sind daher der Import und die Verbindung der beiden Datensätze. Die Daten befinden sich in ```Data/winequality-red.csv``` und ```Data/winequality-white.csv```

```python
import pandas as pd  # Siehe Preprocessing/Lego-Sets/Lego Sets Preprocessing.ipynb für eine Einführung in Pandas

# Datensätze importieren (pandas Funktion: pd.read_csv() Achtung: Trennzeichen ist hier das Semikolon ";"!)
df_red =
df_white =
# Verbinden Sie die beiden Datensätze mit pd.concat()
df =
```

2. Daten analysieren

Als nächstes wollen wir uns die Daten mal näher ansehen. Benötigen wir eine Konvertierung von Merkmalen? Müssen Werte aufgefüllt werden?

```python
# Beginnen wir mit ein bisschen deskriptiver Statistik (pandas-Funktion describe()):

```

Wir stellen drei wichtige Tatsachen fest:

1. Es gibt insgesamt 6497 Weine, für die uns Daten vorliegen (__```count```__), zumindest gibt es so viele Einträge in jeder Spalte. Wir müssen also erfreulicherweise keine Daten auffüllen!
2. Alle Merkmale sind rein numerisch. Wir müssen also auch keine Daten konvertieren!
3. Die Daten sind sehr unterschiedlich skaliert, da die Mittelwerte (__```mean```__) und Standardabweichungen (__```std```__) über mehrere Größenordnungen hinweg streuen.

Daher bietet es sich an, die Spalten zunächst einzeln zu standardisieren, so dass sie jeweils einen Mittelwert von 0 und eine Standardabweichung von 1 haben (auch $z$-Score Normalisierung genannt). Dadurch werden also alle Merkmale in die gleiche Größenordnung gebracht, ohne die relative Verteilung zu vereinfachen.

Unser Fahrplan für die Säuberung der Daten ist also klar:
- Kein Konvertieren oder Auffüllen nötig
- Standardisierung der Daten empfehlenswert
---

3. Daten säubern

Wie bereits oben festgestellt, müssen wir keine Konvertierungen oder Ersetzungen machen, sondern nur die Standardisierung vornehmen.

Bevor wir allerdings die Standardisierung vornehmen, sollten wir zunächst die Daten in Trainings- und Testset aufteilen, damit wir nicht schon bei der Standardisierung Daten verwenden, die eigentlich vom Modell "ungesehene" Testdaten sein sollen. Dazu teilen wir den Datensatz zunächst in die Merkmalvektorfolge $X$ und Klassenlabel $y$.

```python
## Zunächst trennen wir den gesamten Datensatz in die Merkmale und die vorherzusagende Größe
X = df.drop(columns=['alcohol']) # Alle Spalten von df außer 'alcohol'
y = df['alcohol'] # Nur die Spalte 'alcohol'
```

Nun kommt der eigentliche Split in Train- und Testset, den wir mit Hilfe der Funktion [```traint_test_split()```](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) aus ```scikit-learn``` vornehmen wollen.

```python
# Importieren Sie aus dem sklearn-Modul model_selection die Funktion train_test_split
from PAKET.MODUL import FUNKTION

# Verwenden Sie nun die Funktion train_test_split(), um ein zurückgehaltenes (Holdout) Test-Set von 20% der gesamten Daten zu erhalten
X_train, X_test, y_train, y_test =
```

Jetzt können wir auf dem Trainingsset die Parameter für die Standardisierung bestimmen und diese auf Trainings- und Testdaten ausführen. Wir könnten dazu natürlich von Hand in jeder Spalte den Mittelwert subtrahieren und sie durch ihre Standardabweichung teilen. Eleganter geht es jedoch mit Hilfe des Objekts [```StandardScaler```](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.StandardScaler.html):

```python
# Importieren Sie aus dem sklearn-Modul preprocessing das Objekt StandardScaler
from PAKET.MODUL import OBJEKT

# Instanziieren Sie ein Objekt StandardScaler()
stdScaler =

# Berechnen Sie die Standardisierungsparametr auf dem Trainingsset (!) X_train mit Hilfe der Funktion "fit()"

# Wenden Sie die **selbe** Standardisierung mit Hilfe der Methode transform() auf die Trainings- und Testdaten an
X_train =
X_test =
```

---

4. Modellauswahl

Nun können wir die aus der Vorlesung bekannten Regressionsansätze der Reihe nach ausprobieren, um das optimale Modell zu finden.

Der einfachste aus der Vorlesung bekannte Ansatz zur Regression des Alkoholgehalts ist die lineare Regression. Dazu existiert in ```sklearn``` das Objekt [LinearRegression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html), das eine sehr einfache Implementierung erlaubt:

```python
# Importieren Sie das Objekt LinearRegression aus dem Modul linear_model im Paket sklearn
from PAKET.MODUL import OBJEKT

# Instanziieren Sie ein Modell mit den Standardparametern
lr1_model = LinearRegression()
# Trainieren ("Fitten") Sie das Modell auf den Trainingsdaten X_train mit Hilfe der Methode fit()
lr1_model.fit(X_train, y_train)
```

Die Güte dieses Regressionsmodells (und auch jedes anderen Modells in ```sklearn```) auf einem beliebigen Datensatz können Sie mit der Methode [```score()```](https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#sklearn.neighbors.KNeighborsClassifier.score) bestimmen. Bei Regressionsmodellen ist die standardmäßig verwendete Metrik hier das Bestimmtheitsmaß $R^2$ (siehe Vorlesung).

```python
# R^2 im Training
train_score_lr1 = lr1_model.score(X_train, y_train)
# R^2 im Test
test_score_lr1 = lr1_model.score(X_test, y_test)
# Ausgabe der Performancemaße
print("Lineare Regression 1. Ordnung - Training: R² = {:.2f} Test: R² = {:.2f}".format(train_score_lr1, test_score_lr1))
```

In der Vorlesung haben Sie gelernt, dass man verschiedene Basisfunktionen $\varphi(x)$ bei der linearen Regression annehmen kann. Auch das Objekt ```LinearRegression``` können Sie mit beliebigen Funktionen $\varphi(x)$ verwenden. Dabei müssen Sie die durch $\varphi(x)$ vermittelte Transformation der Merkmale selbst vornehmen und dann die neuen, transformierten Merkmale (also die Matrix $\Phi$ in der Vorlesung) an die Fit-Methode von ```LinearRegression``` übergeben.

Wir probieren das hier mal am Beispiel von einer polynomialen Basisfunktion zweiten Grades aus:

\begin{align}
\varphi_1(x) &= x^1 \\
\varphi_2(x) &= x^2
\end{align}

Die Merkmale liegen in erster Ordnung ($x^1$) ja schon in den Trainingsdaten vor. Die Merkmale zweiter Ordnung ($x^2$) müssen wir nun durch spaltenweise Quadrierung der Merkmalmatrix manuell hinzufügen:

```python
import numpy as np # Für die Matrixoperationen

# Die Merkmalmatrix 1. Ordnung muss um die Merkmale 2. Ordnung ergänzt werden
X_2_train = np.concatenate([X_train, X_train ** 2], axis=1)

# Verfahren Sie analog für die Testdaten
X_2_test = np.concatenate([X_test, X_test ** 2], axis=1)
```

Die Merkmale 2. Ordnung sind nicht zwangsläufig mittelwertfrei und haben eine von 1 verschiedenen Varianz. Wir müssen sie also noch standardisieren:

```python
# Instanziieren Sie einen neuen StandardScaler und "fitten" Sie es in die Trainingsdaten inklusive 2. Ordnung
stdScaler_2 = StandardScaler().fit(X_2_train)

# Standardisieren Sie die Trainings- und Testdaten zweiter Ordnung
X_2_train = stdScaler_2.transform(X_2_train)
X_2_test = stdScaler_2.transform(X_2_test)

```

Nun können wir also ein Regressionsmodell für die Merkmale bis zur Ordnung 2 trainieren:

```python
# Instanziieren Sie ein Modell mit den Standardparametern
lr2_model =
# Trainieren ("Fitten") Sie das Modell auf den Trainingsdaten X_2_train mit Hilfe der Methode fit()

```

```python
# R^2 im Training
train_score_lr2 =
# R^2 im Test
test_score_lr2 =
# Ausgabe der Performancemaße
print("Lineare Regression 2. Ordnung - Training: R² = {:.2f} Test: R² = {:.2f}".format( XXX, YYY ))
```

Aha, die Vorhersage hat sich deutlich verbessert! Allerdings haben wir nun natürlich auch mehr Prädiktoren verwendet, was grundsätzlich die Gefahr von Überanpassung ("Overfitting") erhöht. Wir berechnen also zur Kontrolle das korrigierte Bestimmtheitsmaß $R^2_\mathrm{korr} = 1 - (1 - R^2)\frac{N-1}{N-M-1}$ (siehe Vorlesung):

```python
# Anzahl der Stichprobenelemente
N = X_2_train.shape[0]
# Anzahl der Prädiktoren
M = X_2_train.shape[1]
# Korrgiertes R^2 berechnen
R2_korr = 1 - (1-train_score_lr2) * (N-1) / (N-M-1)
print("Das korrigerte R² für die Trainingsdaten beträgt {:.2f}.".format(R2_korr))
```

Da $R^2 = R^2_\mathrm{korr}$, ist die Erhöhung der Anzahl der Prädiktoren aufgrund der großen Zahl von Beobachtungen also offenbar noch gerechtfertigt.

Würde die Hinzunahme von Merkmalen in der dritten Potenz vielleicht eine noch weitere Verbesserung liefern? Probieren Sie es aus!

```python
# Die Merkmalmatrizen 2. Ordnung müssen um die Merkmale 3. Ordnung ergänzt werden
X_3_train =
X_3_test =

# Instanziieren Sie einen neuen StandardScaler und "fitten" Sie es in die Trainingsdaten inklusive 3. Ordnung
stdScaler_3 =

# Standardisieren Sie die Trainings- und Testdaten dritter Ordnung
X_3_train =
X_3_test =

# Instanziieren Sie ein Modell mit den Standardparametern
lr3_model =
# Trainieren ("Fitten") Sie das Modell auf den Trainingsdaten X_3_train mit Hilfe der Methode fit()

# R^2 im Training
train_score_lr3 =
# R^2 im Test
test_score_lr3 =
# Ausgabe der Performancemaße
print("Lineare Regression 3. Ordnung - Training: R² = {:.2f} Test: R² = {:.2f}".format( XXX, YYY ))
```

**Hinweis:** Bei Regressionsmodellen höherer Ordnung verwendet man häufig nicht nur die Merkmale in der jeweiligen Potenz, sonder auch die sogenannten "Interaktionsterme", also die paarweisen Multiplikationen der Merkmale. Für nur zwei Merkmale $(x_1, x_2)$ würde das also das folgende Modell ergeben:

$$\hat{y} = w_0 + w_1 \cdot x_1 + w_2 \cdot x_1 \cdot x_2 + w_3 \cdot x_1^2 + w_4\cdot x_2 + w_5\cdot x_2^2$$

Bei einer größeren Anzahl Merkmale wird die Merkmaltransformation hierzu sehr aufwändig. Darum bietet ```sklearn``` das Objekt [```PolynomialFeatures```](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.PolynomialFeatures.html) an, mit dem diese Transformation erheblich einfacher implementiert werden kann. Schauen Sie sich doch die verlinkte Dokumentation mal an und versuchen Sie die Transformation für zweite und dritte Ordnung doch mal damit zu machen!

Wir fahren aber zunächst fort mit der $L^2$-regularisierten Regression, auch Ridge Regression genannt. Auch hier bietet ```sklearn``` ein Objekt [```Ridge```](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Ridge.html). Allerdings müssen Sie diesem Objekt den Wert für ```alpha``` (in der Vorlesung $\lambda$) explizit übergeben. Dieser Wert muss aber üblicherweise durch Kreuzvalidierung erst gefunden werden. Aus diesem Grund existiert zusätzlich das Objekt [```RidgeCV```](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.RidgeCV.html), mit dem Sie die Kreuzvalidierung (engl. crossvalidation, CV) zur Bestimmung von ```alpha``` direkt ausführen können:

```python
# Importieren Sie aus dem Modul linear_model im Paket sklearn das Objekt RidgeCV
from PAKET.MODUL import OBJEKT

# Wählen Sie die Werte für den Regularisierungsparameter alpha (in Vorlesung lambda genannt), die Sie ausprobieren möchten
alpha_candidates = np.logspace(-6, 10, 17) # üblicherweise verwendet man hier als Kandidaten eine logarithmische Spanne von Werten

# Instanziieren Sie ein RidgeCV-Objekt und übergeben Sie dabei die Kandidaten für alpha
ridge1_model = RidgeCV(alphas=alpha_candidates)

# Trainieren ("Fitten") Sie das Modell auf den Trainingsdaten
ridge1_model.fit(X_train, y_train)
```

Wie Sie sehen, wiederholt sich das Programmiermuster im Vergleich zu ```LinearRegression``` stark, obwohl Sie ein anderes Modell verwenden. Nach dem Training gibt Ihnen RidgeCV das optimal regularisierte Modell zurück. Der gefundene optimale Wert für ```alpha``` kann über den Parameter ```alpha_``` des Objekts ausgelesen werden:

```python
# Optimales alpha
alpha_opt = ridge1_model.alpha_
# R^2 im Training
train_score_ridge1 = ridge1_model.score(X_train, y_train)
# R^2 im Test
test_score_ridge1 = ridge1_model.score(X_test, y_test)
# Ausgabe der Performancemaße
print("Ridge Regression 1. Ordnung (alpha={:.2f}): Training R^2 = {:.2f} Test R^2 = {:.2f}".format(alpha_opt, train_score_ridge1, test_score_ridge1))
```

Verfahren Sie analog für die Merkmale bis zur Ordnung 2 und 3:

```python
# Instanziieren Sie ein RidgeCV-Objekt und übergeben Sie dabei die Kandidaten für alpha
ridge2_model =

# Trainieren ("Fitten") Sie das Modell auf den Trainingsdaten

# Optimales alpha
alpha_opt =
# R^2 im Training
train_score_ridge2 =
# R^2 im Test
test_score_ridge2 =
# Ausgabe der Performancemaße
print("Ridge Regression 2. Ordnung (alpha={:.2f}): Training R^2 = {:.2f} Test R^2 = {:.2f}".format(XXX, YYY, ZZZ))

# Instanziieren Sie ein RidgeCV-Objekt und übergeben Sie dabei die Kandidaten für alpha
ridge3_model =

# Trainieren ("Fitten") Sie das Modell auf den Trainingsdaten

# Optimales alpha
alpha_opt =
# R^2 im Training
train_score_ridge3 =
# R^2 im Test
test_score_ridge3 =
# Ausgabe der Performancemaße
print("Ridge Regression 3. Ordnung (alpha={:.2f}): Training R^2 = {:.2f} Test R^2 = {:.2f}".format(XXX, YYY, ZZZ))

```

Wir stellen fest, dass das optimale $\lambda$ für 1. und 2. Ordnung gleich 1 beträgt, was mathematisch identisch zur unregularisierten Regression ist (siehe Vorlesung). Für die 3. Ordnung wird eine sehr starke Regularisierung als optimal identifiziert. Allerdings ist die Performance auf dem Testset trotzdem noch schlechter, als bei 2. Ordnung. Offenbar kann mit dieser Menge von Beobachtungen eine Überanpassung auch mit $L2$-Regularisierung nicht vermieden werden.

In der Vorlesung wurde erwähnt, dass auch Support Vector Machines (SVM) für die Regression angewendet werden können. Auch wenn diese Methoden nicht Gegenstand der Vorlesung sind und ihr theoretischer Hintergrund an dieser Stelle den Rahmen sprengen würde, wollen wir sie doch ausprobieren, da sie ein sehr mächtiges Modell darstellen und durch ```sklearn```auch ohne tieferes Wissen implementierbar sind.

Die (sehr stark vereinfachte) Grundidee bei der Regression mit Support Vector Machines ist, dass sich ähnlich wie bei der Klassifikation die zu findende Funktion nicht auf alle Datenpunkte stützt, sondern nur auf einige wenige Supportvektoren, durch die dann die Regressionsfunktion gelegt wird. Ein Parameter $\epsilon$ steuert, wie viele Punkte noch berücksichtigt werden, wobei ein größeres $\epsilon$ eine geringere Empfindlichkeit auf Ausreißer bedeutet. Auch bei der Regression können Sie den Kernel-Trick (siehe Vorlesung) anwenden und durch den Parameter $C$ regularisieren.

Das Objekt zur Regression mit Support Vector Machines in ```sklearn``` heißt [```SVR```](https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVR.html?highlight=svr#sklearn.svm.SVR):

```python
# Importieren Sie aus dem Modul svm im Paket sklearn das Objekt SVR

# Instanziieren eines SVR-Objekts mit Standardeinstellungen
svr_model =
# Trainieren Sie das Modell

# R^2 im Training
train_score_svr =
# R^2 im Test
test_score_svr =

# Ausgabe der Performance
print("Support Vector Regression: Training R^2 = {:.2f} Test R^2 = {:.2f}".format(XXX, YYY))
```

Wie Sie sehen, erreicht die SVR "aus dem Stand" ein sehr gutes Testergebnis. Allerdings lohnt es sich meistens, die Hyperparameter $C$ und $\epsilon$ mit Hilfe von Kreuzvalidierung zu optimieren.

Zur Optimierung der Hyperparameter ("Tuning des Modells") basierend auf Kreuzvalidierung bietet uns ```sklearn``` im Modul ```model_selection```(neben einigen anderen Varianten) das Objekt [```GridSearchCV```](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.html) an. Dieses Objekt führt eine Gittersuche (engl. _grid search_) auf einem vorgebenen Parametergitter aus und überprüft an jedem Gitterknoten die Performance des so parametrisierten Modells mit Hilfe von Kreuzvalidierung (engl. _crossvalidation_, CV). Dazu benötigt es das zu tunende Regressionsmodell, das Parametergitter und weitere optionale Steuerparameter, wie die Anzahl der Folds in der Kreuzvalidierung.

Die Werte der Hyperparameter, die ausprobiert werden sollen, werden in Form einer Liste von [Dictionary](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) übergeben. Jedes Dictionary enthält ein Gitter, das abgesucht wird.

Am Ende wird das Modell mit dem als optimal identifizierten Hyperparametersatz auf allen Folds trainiert, so dass der Rückgabewert der Methode ```fit()``` ein auch im Sinne der Hyperparameter optimales Regressionsmodell darstellt.

**Es empfiehlt sich sehr, die Hyperparameteroptimierung auf einem parallen Rechner auszuführen!*** Da jede Hyperparameterkonfiguration und jede Kreuzvalidierung unabhängig voneinander berechnet werden können, ist hier ein nahezu perfekter paralleler [Speedup](https://de.wikipedia.org/wiki/Speedup) möglich. Ein einfaches Beispiel (gilt analog für Hyperparameter in der Regression):

Sie wollen prüfen, welche Zahl zwischen 1 und 10 die beste Zahl von Nachbarn in dem $k$ Nächste Nachbarn Klassifikator darstellt. Dazu müssen Sie also 10 Modelle mit 10 verschiedenen Werten für $k$ berechnen und diese miteinander vergleichen. Zum Vergleich wollen Sie jedes Modell mit 4 Folds validieren. Dazu müssen Sie also für jeden der 10 Werte von $k$ schon 4 Modell berechnen, also insgesamt 40 Trainingsvorgänge. Auf einem Single-CPU Rechner dauert dies dann zum Beispiel eine Minute. Da alle Berechnungen getrennt voneinander gemacht werden könne, kann das exakt gleiche Training auf einem Parallelrechner mit z.B. 20 CPUs in 1/20 der Zeit gerechnet werden, also in diesem Beispiel in nur 3 Sekunden. Wenn noch weitere Hyperparameter optimiert oder mehr Werte ausprobiert werden sollen, explodiert der Rechenaufwand entsprechend durch die kombinatorische Vielfalt des Gitters. Parallele Ausführung ist hier also enorm wichtig. ***Wir empfehlen daher nachdrücklich das Arbeiten auf dem HPC (siehe [Einführung ins Praktikum](https://bildungsportal.sachsen.de/opal/auth/RepositoryEntry/23165501449/CourseNode/101490883666782))!***

**Achtung:** Die Dauer des Trainings ist stark abhängig von der Größe des Parameters $C$. Für kleine Werte von $C$ kann das Training schon nach wenigen Sekunden abgeschlossen sein, während es bei großen Werten durchaus mehrere Minuten dauern kann. Wundern Sie sich also nicht, wenn die Kreuzvalidierung zwischendurch "eingefroren" erscheint.

```python
# Importieren Sie das Objekt GridSearchCV aus dem Modul model_selection im Paket sklearn
from PAKET.MODUL import OBJEKT

# Auswahl der zu testenden Hyperparameter (alle nicht spezifizierten bleiben auf dem Standardwert)
param_grid = [
    {'C': np.logspace(-2, 2, 5),        # Parameter C nach Vorlesung
     'epsilon': np.logspace(-3, 3, 7),  # Parameter epsilon zur Steuerung
     'kernel': ['linear', 'rbf']},      # Verwendeter Kernel
]
```

Falls Sie nicht auf dem HPC arbeiten und nicht so lange warten wollen, überspringen Sie die nächste Zelle und arbeiten Sie einfach mit den folgenden Werten für die Hyperparameter weiter:

$C = 10$, $\epsilon = 0.1$, Kernel: RBF

```python
# Instanziieren des Objekts "GridSearchCV"
svr_model = GridSearchCV(SVR(),            # Regressionsmodell, das benutzt werden soll
                         param_grid,       # Auszuprobierenden Hyperparameterwerte
                         scoring='r2',     # Zu verwendende Metrik
                         cv=5,             # Anzahl der Folds für die Kreuvalidierung
                         verbose=10,       # Menge des Output während der Suche (größere Zahl -> mehr Info)
                         n_jobs=-1)        # Anzahl der parallel genutzen CPUs; -1: alle verfügbaren CPUs nutzen

# Trainieren Sie das Modell
svr_model.fit(X_train, y_train)
```

Beachten Sie in der ersten Zeile der Ausgabe der Fit-Funktion die große Anzahl von zu trainierenden Modellen ("Fits"), die sich aus den relativ wenigen Hyperparametern ergeben haben.

Die beste gefundenen Hyperparameterkombination können Sie über die Eigenschaft ```best_params_``` der Instanz von ```GridSearchCV``` auslesen:

```python
print(svr_model.best_params_)
```

Ob die Performance besser geworden ist, sehen wir an einem Vergleich des $R^2$ auf dem Testset:

```python
# svr_model = SVR(C=10, epsilon=0.1, kernel='rbf').fit(X_train, y_train) # Kommentieren Sie diese Zeile ein, wenn Sie die Kreuzvalidierung übersprungen haben

# R^2 im Training
train_score_svr =
# R^2 im Test
test_score_svr =

# Ausgabe der Performance
print("Tuned Support Vector Regression: Training R^2 = {:.2f} Test R^2 = {:.2f}".format(XXX, YYY))
```

Durch das Hyperparametertuning hat sich die Performance also weiter verbessert! Damit ist die SVR das beste der betrachteten Modelle. Bevor wir es für die weitere Verwendung exportieren, lohnt sich noch ein kurzer Blick auf den sogenannten Target-Response-Plot. Dabei wird in einem Streudiagramm die Zielgröße (der tatsächliche Alkoholgehalt) als unabhängige Variable gegen die Response (der vorhergesagte Alkoholgehalt) aufgetragen. Eine perfekte Regression wäre in dieser Darstellung also eine Gerade mit der Steigung 1.

Zur grafischen Darstellung importieren wir zunächst ein Paket zur grafischen Darstellung von Daten. Es gibt eine Vielzahl von solchen Paketen in Python, aber wir werden hier [matplotlib](https://matplotlib.org/) verwenden.

```python
# Import des Moduls zum plotten aus Matplotlib
from matplotlib import pyplot as plt
# Dies ist ein sog. "Magic-Kommando", das die Darstellung der Plots direkt im Notebook ermöglicht.
# Wenn Sie dieses vergessen, tauchen die Plots nicht auf!
%matplotlib inline

# Vorhersagen eines Modells können Sie mit der Methode predict() generieren
y_predicted = svr_model.predict(X_test)

# Im Streudiagramm (Scatter-Plot) ide Größen gegeneinander auftragen
plt.scatter(y_test, y_predicted)
plt.xlabel("Target Alcohol")
plt.ylabel("Predicted Alcohol")
```

Im Target-Response-Plot ist zwar keine perfekte Einheitsgerade zu sehen, aber die Punktwolke ist doch relativ gleichförmig verteilt und es sind keine starken Ausreißer zu sehen. Damit deckt sich der Eindruck mit dem Wert von $R^2$ im Test und es ist folglich ein gutes Ergebnis erzielt worden.

---

5. Modell speichern

Die optimal getunte Support Vector Regression ist also das beste untersuchte Modell. Wir möchten es daher abspeichern, damit für zukünftige Anwendungen nicht immer erst das Modell wieder neu trainiert werden muss.

Hierzu importieren wir das Modul ```pickle```und schreiben das Modell und (ganz wichtig) auch das Objekt zur Standardisierung in eine Binärdatei. Wenn wir den Standardisierer nicht auch speicher würden, könnte eine Anwenderin oder ein Anwender unseres Modells neue Daten nicht standardisieren und so keine sinnvollen Vorhersagen machen!

```python
# Importieren Sie das Modul pickle
import pickle

with open('wine_alcohol_model.pickle', 'wb') as model_file:
    # Schreiben Sie die beiden Objekte in die Datei
    pickle.dump([svr_model, stdScaler], model_file)
```

Wenn Sie das Modell an anderer Stelle wieder benutzen möchten, können Sie es direkt aus der Binärdatei laden:

```python
with open('wine_alcohol_model.pickle', 'rb') as model_file:
    # Entpacken Sie die Objekte aus der Datei in zwei neue Variablen
    svr, scaler = pickle.load(model_file)
```

Das war's!

Source: xkcd

---

Wine data from UCI Machine Learning Repository

Icons made by Swifticons from www.flaticon.com

Notebook erstellt von Yifei Li und Simon Stone

tudais/notebooks/regression/Wine-Alcohol/Vorhersage des Alkoholgehalts von Wein.ipynb


# Structural Estimation

1. This notebook shows how to **estimate** the consumption model in **ConsumptionSaving.pdf** using **Simulated Minimum Distance (SMD)**
2. It also shows how to calculate **standard errors** and **sensitivity measures**

## Simulated Minimum Distance

**Data:** We assume that we have data available for $N$ households over $T$ periods, collected in $\{w_i\}_i^N$.

**Goal:** We wish to estimate the true, unknown, parameter vector $\theta_0$. We assume our model is correctly specified in the sense that the observed data stems from the model.

**Overview:**

1. We focus on matching certain (well-chosen) **empirical moments** in the data to **simulated moments** from the model.

2. We calculate a $J\times1$ vector of moments in the data, $\Lambda_{data} = \frac{1}{N}\sum_{i=1}^N m(\theta_0|w_i)$. This could e.g. be average consumption over the life-cycle, the income variance or regressions coefficients from some statistical model.

3. To estimate $\theta$ we chose $\theta$ as to **minimize the (squared) distance** between the moments in the data and the same moments calculated from simulated data. Let $\Lambda_{sim}(\theta) = \frac{1}{N_{sim}}\sum_{s=1}^{N_{sim}} m(\theta|w_s)$ be the same moments calculated on simulated data for $N_{sim}=S\times N$ observations for $T_{sim}$ periods from the model for a given value of $\theta$. As we change $\theta$, the simulated outomes will change and the moments will too.

The **Simulated Minimum Distance (SMD)** estimator then is

$$
\hat{\theta} = \arg\min_{\theta} g(\theta)'Wg(\theta)
$$

where $W$ is a $J\times J$ positive semidefinite **weighting matrix** and

$$
g(\theta)=\Lambda_{data}-\Lambda_{sim}(\theta)
$$

is the distance between $J\times1$ vectors of moments calculated in the data and the simulated data, respectively. Concretely,

$$
\Lambda_{data} = \frac{1}{N}\sum_{i=1}^N m(\theta_0|w_i) \\
\Lambda_{sim}(\theta) = \frac{1}{N_{sim}}\sum_{s=1}^{N_{sim}} m(\theta|w_s)
$$

are $J\times1$ vectors of moments calculated in the data and the simulated data, respectively.

**Settings:** In our baseline setup, we will have $N=5,000$ observations for $T=40$ periods, and simulate $N_{sim}=100,000$ synthetic consumers for $T_{sim} = 40$ periods when estimating the model.

**Solution of consumption-saving model:** This estimator requires the solution (and simulation) of the model each trial guess of $\theta$ as we search for the one that minimizes the objective function. Therefore, structural estimation can in general be quite time-consuming. We will use the EGM to solve the consumption model quite fast and thus be able to estimate parameters within a couple of minutes. Estimation of more complex models might take significantly longer.

> **Note I:** When regressions coefficients are used as moments, they are sometimes referred to as **auxiliary parameters** (APs) and the estimator using these APs as an **Indirect Inference (II)** estimator ([Gouriéroux, Monfort and Renault, 1993](https://doi.org/10.1002/jae.3950080507)).

> **Note II:** The estimator used is also called a **simulated method of momoments (SMM)** estimator. I.e. a simulated General Method of Moments (GMM) estimator.

# Setup

```python
%load_ext autoreload
%autoreload 2

import time
import numpy as np
import scipy.optimize as optimize

import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
prop_cycle = plt.rcParams['axes.prop_cycle']
colors = prop_cycle.by_key()['color']
```

```python
import figs
from ConsumptionSavingModel import ConsumptionSavingModelClass
from SimulatedMinimumDistance import SimulatedMinimumDistanceClass
```

    C:\Users\gmf123\Anaconda3\lib\site-packages\statsmodels\compat\pandas.py:65: FutureWarning: pandas.Int64Index is deprecated and will be removed from pandas in a future version. Use pandas.Index with the appropriate dtype instead.
      from pandas import Int64Index as NumericIndex

# Estimation choices

```python
# a. model settings
N = 5_000
N_sim = 100_000

par = {'simlifecycle':True,
       'sim_mini':1.0 ,
       'simT':40,
       'simN':N_sim,
       'Nxi':4,
       'Npsi':4,
       'Na':100}

par_true = par.copy()
par_true['simN'] = N

# b. parameters to estimate
est_par = {
    'rho': {'guess':2.0,'lower':0.5,'upper':5.0,},
    'beta': {'guess':0.97,'lower':0.90,'upper':0.999},
}

est_par_names = [key for key in est_par.keys()]

# c. moment function used in estimation.
def mom_func(data,ids=None):
    """ returns the age profile of wealth """

    if ids is None:
        mean_A = np.mean(data.A[:,1:],axis=0)
    else:
        mean_A = np.mean(data.A[ids,1:],axis=0)

    return mean_A

# d. choose weighting matrix
weighting_matrix = 0

# 0: identity (equal weight),
# 1: inverse of variance on the diagonal (removes scale),
# 2: inverse of covaraince matrix between estimation moments (optimal weighting matrix)
```

# Data and estimator

Construct **data**.

```python
# a. setup model to simulate data
true = ConsumptionSavingModelClass(name='true',par=par_true)
true.solve()
true.simulate(seed=2019) # this seed is different from the default

# b. data moments
datamoms = mom_func(true.sim)
moment_names = [i for i in range(true.par.age_min+1,true.par.age_min+true.par.simT)]
```

    model solved in 2.8 secs
    model simulated in 2.3 secs

**Bootstrap** variance of estimation moments used when later calculting standard errors below (and potentially for weighting matrix).

```python
num_boot = 200
num_moms = datamoms.size
smd = SimulatedMinimumDistanceClass(est_par,mom_func,datamoms=datamoms)
smd.Omega = smd.bootstrap_mom_var(true.sim,N,num_boot,num_moms)
```

**Setup estimator**.

```python
smd.plot({'data':moment_names},{'data':datamoms},xlabel='age',ylabel='wealth',hide_legend=True)
```

# Estimate the model

```python
model = ConsumptionSavingModelClass(name='estimated',par=par)
```

Choose **weighting matrix**:

```python
if weighting_matrix == 0:
    W = np.eye(smd.datamoms.size) # identity
elif weighting_matrix == 1:
    W = np.diag(1.0/np.diag(smd.Omega)) # inverse of variance on the diagonal
else:
    W = np.linalg.inv(smd.Omega) # optimal weighting matrix
```

## Estimation results

```python
# a. estimate the model (can take several minutes)
%time est = smd.estimate(model,W)

# b. print estimation results
print(f'\n       True  Est. ')
for key in est_par.keys():
    print(f'{key:5s} {getattr(true.par,key):2.3f} {est[key]:2.3f}')
```

    objective function at starting values: 5.9610038934631495
    Wall time: 59.4 s

           True  Est.
    rho   2.000 2.018
    beta  0.960 0.960

Show **model-fit**:

```python
plot_data_x = {'data':moment_names,'simulated':moment_names}
plot_data_y = {'data':datamoms,'simulated':mom_func(model.sim)}
smd.plot(plot_data_x,plot_data_y,xlabel='age',ylabel='wealth')
```

## Standard errors

The SMD estimator is **asymptotic Normal** and standard errors have the same form as standard GMM estimators scaled with the adjustment factor $(1+S^{-1})$ due to the fact that we use $S$ simulations of the model.

The **standard errors** are thus

$$
\begin{align}
\text{Var}(\hat{\theta})&=(1+S^{-1})\Gamma\Omega\Gamma'/N \\
\Gamma &= -(G'WG)^{-1}G'W \\
\Omega & = \text{Var}(m(\theta_0|w_i))
\end{align}
$$

where $G=\frac{\partial g(\theta)}{\partial \theta}$ is the $J\times K$ **Jacobian** with respect to $\theta$. $\Gamma$ is related to what is sometimes called the "influence function".

**Calculating $\Omega$**:

1. Can sometimes be done **analytically**
2. Can always be done using a **bootstrap** as done above

**Calculating the Jacobian, $G$:** This is done using numerical finite differences.

```python
# a. number of datasets simulated per individual in original data
S = model.par.simN/N

# b. find standard errors
Gamma, grad_theta = smd.calc_influence_function(est['theta'],model,W)
Var_theta = (1.0+1.0/S) * Gamma @ smd.Omega @ Gamma.T /N
se = np.sqrt(np.diag(Var_theta))

# b. print estimation results
print(f'       True  Est.  (se)')
for i,(key,val) in enumerate(est_par.items()):
    print(f'{key:5s} {getattr(true.par,key):2.3f} {est[key]:2.3f} ({se[i]:2.3f})')
```

           True  Est.  (se)
    rho   2.000 2.018 (0.043)
    beta  0.960 0.960 (0.001)

# Sensitivity Analysis

We now look into a **sensitivity analysis** of our estimation. Concretely, we implement the **informativeness measure** from [Honoré, Jørgensen and de Paula (2019)](https://doi.org/10.1002/jae.2779) and the **sensitivity to calibrated parameters** in [Jørgensen (2020)](https://www.ifs.org.uk/uploads/CWP1620-Sensitivity-to-Calibrated-Parameters.pdf). Further details can be found in these papers.

## The informativeness of estimation moments

The measures are motivated by those proposed in [Honoré, Jørgensen and de Paula (2019)](https://doi.org/10.1002/jae.2779). All the measures proposed in that paper is calculated, but we will focus on their measure 4 that asks **"what is the change in the asymptotic variance from completely excluding the k'th moment?"**. If the *k*th is very informative about a parameter, the asymptotic varaince of that parameter should increase significantly, if we leave out the *k*th moment.

```python
info = smd.informativeness_moments(grad_theta,smd.Omega,W)
smd.plot_heat(info['M4e'],est_par_names,moment_names,annot=False)
```

**Conclusion:** We can see that especially the wealth level for younger households are very informative regarding both $\rho$ and $\beta$. This is likely due to the fact that for low level of resources (which is the case at younger ages), the value of both these parameters affect consumption and saving decisions a lot. Thus, the level of saving especially in young ages are very informative and help to identify the two parameters.

## Sensitivity to calibrated parameters

The mesure is motivated by the one proposed in [Jørgensen (2020)](https://www.ifs.org.uk/uploads/CWP1620-Sensitivity-to-Calibrated-Parameters.pdf). Note that the estimation moments are all functions of the $L$ calibrated parameters, which we will denote $\gamma$, $g(\theta|\gamma)$.

The **sensitivity measure** is defined as

$$
\begin{align}
S &= \Gamma D
\end{align}
$$

where $D=\frac{\partial g(\theta|\gamma)}{\partial \gamma}$ is the $J\times L$ **Jacobian** with respect to $\gamma$.

*We only need to calculate $D$* since we have already calculated $\Gamma$ when we calculated standard errors above. We use numerical finite differences to calcualte this object.

**Chosen calibrated paramters:** $R$, $G$, $\sigma_{\psi}$, $\sigma_{\xi}$.

```python
cali_par_names = ('R','G','sigma_psi','sigma_xi')
cali_par = np.array([getattr(model.par,name) for name in cali_par_names])
```

**Calculate the sensitivty measure:**

```python
grad_gamma = smd.num_grad(cali_par,model,cali_par_names)
sens_cali = Gamma @ grad_gamma
```

**Plot sensitivity measure**

```python
smd.plot_heat(sens_cali,est_par_names,cali_par_names)
```

**Check:** We can compare this to a brute-force approach in which we re-estimate the model for marginal changes in the calibrated parameters. This takes considerable time, however. The results are almost identical.

```python
sens_cali_brute = smd.sens_cali_brute_force(model,est['theta'],W,cali_par_names)
smd.plot_heat(sens_cali_brute,est_par_names,cali_par_names)
```

**Arbitrary changes in $\gamma$**: We can also investigate larger simultaneous changes in $\gamma$.

```python
# a. set new calibrated parameters
cali_par_new = {'G':1.05}

# b. update calibrated parameters in new version of the model
model_new = model.copy()
for key,val in cali_par_new.items():
    setattr(model_new.par,key,val)

# c. calculate new objective function
obj_vec = smd.diff_vec_func(est['theta'],model,est_par_names)
obj_vec_new = smd.diff_vec_func(est['theta'],model_new,est_par_names)

# d. approximate change in theta
Gamma_new,_ = smd.calc_influence_function(est['theta'],model_new,W)
theta_delta =  Gamma_new @ obj_vec_new - Gamma @ obj_vec

# e. extrapolate the gradient
theta_delta_extrap = np.zeros(theta_delta.size)
for j,key in enumerate(cali_par_new):
    theta_delta_extrap += sens_cali[:,j]*(cali_par_new[key]-getattr(model.par,key))

print(theta_delta_extrap)
```

    [ 0.17300506 -0.02961878]

**Check:** Again, we can compare this approximation to a brute-force re-estimation of the model for the changed $\gamma$.

```python
est_new = smd.estimate(model_new,W)
theta_delta_brute = est_new['theta'] - est['theta']
print(theta_delta_brute)
```

    objective function at starting values: 5.46354064394105
    [ 0.11275468 -0.05969197]

00. DynamicProgramming/04. Structural Estimation.ipynb


```python
import numpy as np
import pandas as pd
import linearsolve as ls
import matplotlib.pyplot as plt
plt.style.use('classic')
%matplotlib inline
```

# Discussion: Week 7

## Exercise: The Baseline RBC Model without Labor

The equilibrium conditions for the RBC model without labor are:

\begin{align}
\frac{1}{C_t} & = \beta E_t \left[\frac{\alpha A_{t+1}K_{t+1}^{\alpha-1} +1-\delta }{C_{t+1}}\right]\\
K_{t+1} & = I_t + (1-\delta) K_t\\
Y_t & = A_t K_t^{\alpha}\\
Y_t & = C_t + I_t\\
\log A_{t+1} & = \rho \log A_t + \epsilon_{t+1}
\end{align}

where $\epsilon_{t+1} \sim \mathcal{N}(0,\sigma^2)$.

The objective is use `linearsolve` to create a stochastic simulation of the RBC model. USe the following parameter values for the simulation:

| $$\rho$$ | $$\sigma$$ | $$\beta$$ | $$\alpha$$ | $$\delta $$ |
|----------|------------|-----------|------------|-------------|
| 0.75     | 0.006      | 0.99      | 0.35       |  0.025      |

### Model Preparation

Before proceding, let's recast the model in the form required for `linearsolve`. Write the model with all variables moved to the lefthand side of the equations and dropping the expecations operator $E_t$ and the exogenous shock $\epsilon_{t+1}$:

\begin{align}
0 & = \beta\left[\frac{\alpha A_{t+1}K_{t+1}^{\alpha-1} +1-\delta }{C_{t+1}}\right] - \frac{1}{C_t}\\
0 & = A_t K_t^{\alpha} - Y_t\\
0 & = I_t + (1-\delta) K_t - K_{t+1}\\
0 & = C_t + I_t - Y_t\\
0 & = \rho \log A_t - \log A_{t+1}
\end{align}

Remember, capital and TFP are called *state variables* because they're $t+1$ values are predetermined. Output, consumption, and investment are called a *costate* or *control* variables. Note that the model as 5 equations in 5 endogenous variables.

### Initialization, Approximation, and Solution

The next several cells initialize the model in `linearsolve` and then approximate and solve it.

```python
# Create a variable called 'parameters' that stores the model parameter values in a Pandas Series. CELL PROVIDED
parameters = pd.Series(dtype=float)
parameters['rho'] = .75
parameters['sigma_squared'] = 0.006**2
parameters['beta'] = 0.99
parameters['alpha'] = 0.35
parameters['delta'] = 0.025

# Create variable called 'var_names' that stores the variable names in a list with state variables ordered first
var_names = ['a','k','y','c','i']

# Create variable called 'shock_names' that stores an exogenous shock name for each state variable.
shock_names = ['e_a','e_k']

# Define a function that evaluates the equilibrium conditions of the model solved for zero.
def equilibrium_equations(variables_forward,variables_current,parameters):

    # Parameters. PROVIDED
    p = parameters

    # Current variables. PROVIDED
    cur = variables_current

    # Forward variables. PROVIDED
    fwd = variables_forward

    # Euler equation
    euler_equation = p.beta*(p.alpha*fwd.a*fwd.k**(p.alpha-1)+1-p.delta)/fwd.c - 1/cur.c

    # Production function
    production_function = cur.a*cur.k**p.alpha - cur.y

    # Capital evolution
    capital_evolution = cur.i + (1 - p.delta)*cur.k - fwd.k

    # Market clearing
    market_clearing = cur.c+cur.i - cur.y

    # Exogenous tfp
    tfp_process = p.rho*np.log(cur.a) - np.log(fwd.a)

    # Stack equilibrium conditions into a numpy array
    return np.array([
        euler_equation,
        production_function,
        capital_evolution,
        market_clearing,
        tfp_process
        ])

# Initialize the model into a variable named 'rbc_model'
rbc_model = ls.model(equations = equilibrium_equations,
                 n_states=2,
                 var_names=var_names,
                 shock_names=shock_names,
                 parameters=parameters)

# Compute the steady state numerically using .compute_ss() method of rbc_model
guess = [1,4,1,1,1]
rbc_model.compute_ss(guess)

# Find the log-linear approximation around the non-stochastic steady state and solve using .approximate_and_solve() method of rbc_model
rbc_model.approximate_and_solve()
```

### Stochastic Simulation

Compute a 201 period stochastic simulation of the model. Set the seed for the simulation to 126.

Recall that `linearsolve` handles the model *as if* there is an exogenous shock for every state variable. In this case, that means two exogenous shocks $\epsilon_t^a$ and $\epsilon^k_t$. For the stochastic simulation, we need to specify the *covariance matrix* for the two shocks. The covariance matrix has the variance of each shock in the diagonal elements and covariances on the off-diagonal elements. Since there is no shock to capital in our model,

\begin{align}
\text{Covariance matrix} & = \left[\begin{array}{cc}\sigma^2 & 0\\ 0 & 0\end{array}\right]
\end{align}

```python
# Compute the stochastic simulation using the .stoch_sim() method of rbc_model
rbc_model.stoch_sim(T=201,seed=126,cov_mat = [[parameters['sigma_squared'],0],[0,0]])
```

```python
# Print the first 5 rows of rbc_model.simulated
print(rbc_model.simulated.head())
```

            e_a  e_k         a         k         y         c         i
    0  0.001484  0.0  0.003862  0.003977  0.005254  0.002906  0.012323
    1  0.001427  0.0  0.004323  0.004186  0.005788  0.003091  0.013910
    2  0.003155  0.0  0.006397  0.004429  0.007947  0.003499  0.021342
    3  0.011305  0.0  0.016103  0.004852  0.017801  0.004973  0.056433
    4 -0.001778  0.0  0.010299  0.006141  0.012449  0.005031  0.034786

```python
# Print the first 5 rows of simulated output
print(rbc_model.simulated['y'].head())
```

    0    0.005254
    1    0.005788
    2    0.007947
    3    0.017801
    4    0.012449
    Name: y, dtype: float64

## Exercise: Analyze Simulation Results

On a single axis, plot the simulated values for output, consumption and investment.

```python
# Plot simulated results
fig = plt.figure(figsize=(12,4))
ax = fig.add_subplot(1,1,1)
rbc_model.simulated[['y','c','i']].plot(ax=ax,lw=3,grid=True,alpha=0.75)
ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
```

**Questions**

1. What is the exogenous *cause* of the fluctuations in output, consumption and investment in your simulation?
2. Looking at the plot of your simulated data, which quantity — output, consumption or investment — has the smallest fluctuations from the steady state? What feature of the model leads to this?
3. Which quantity — output, consumption or investment — has the largest fluctuations from the steady state? What feature of the model leads to this?

**Answers**

1. Exogenous shocks to TFP. That is, exogenous shocks to the economy's production function.<!-- answer -->
2. Consumption. The household's Euler equation implies that the household wants to *smooth* consumption over time.<!-- answer -->
3. Investment. Again, the Euler equation. The household uses investment fluctuations to smooth consumption. When output is high, the household raises investment in order to accumulate capital and save some of the high production for later. Similarly, when output is low, the household reduces investment in order to deplete capital and therefore borrow against future output.<!-- answer -->