.

.

.

.

.

HealthyNumerics

HealthPoliticsEconomics | Quant Analytics | Numerics

Basic Stats 32: Multiple linear Regression, ANOVA


Das Beispiel stammt aus:

Reinhold Hatzinger, Kurt Hornik, Herbert Nagel (2011): R - Einführung durch angewandte Statistik, Pearson Studium, 465pp, ISBN978-3-8632-6599-1 , siehe auch hier

Dort könnten auch unter Extras/CWS die Input-Daten gefunden werden. Das nachfolgende Beispiel ist aus Kapitel 9.4 (Mehrere metrische Variablen), die verwendete Datendatei: gebrauchtwagen.csv.

Im Beispiel liegen Daten zu Preis, Meilen, Servicehäufigkeit, Garagenparkierung und Farbe von Fahrzeugen vor. Es soll ein Prädiktor für die Preisbildung erstellt werden.

import numpy  as np
import pandas as pd
import statsmodels.api as sm
import statsmodels.formula.api as smf
import seaborn as sns
import matplotlib.pyplot as plt
sns.set(style="ticks")
%matplotlib inline

Daten

Preis;Meilen;Service;Garage;Farbe
5318;37388;2;0;1
5061;44758;2;0;1
5008;45833;2;0;3
5795;30862;4;1;3
5784;31705;4;1;2
5359;34010;2;0;2
5235;45854;3;1;1

Die Daten liegen in einer CSV-Datei vor. Oben sind nur die ersten paar Zeilen davon angezeigt. Sie können mit pd.read_csv direkt in ein Pandas DataFrame eingelesen werden. Dabei muss der Lese-Funktion mitgeteilt werden, dass ";" als Trennzeichen auftritt. Alle Daten liegen nun als integer-Werte vor, auch die kategorialen Daten wie zur Farbe oder zur Garagierung.

f10Dir = "C:\gcg\\7_Wissen_T\eBücher\R-HatzingerHornikNagel\R-Begleitmaterial\Daten\\"
f10Name= "gebrauchtwagen.csv"
dg = pd.read_csv(open(f10Dir+f10Name,
                      newline=''),delimiter=';') # set ; as delimiter and import the data as pd.DataFrame
dg.head(7)                                       # display the first few lines of the DataFrame
Preis Meilen Service Garage Farbe
0 5318 37388 2 0 1
1 5061 44758 2 0 1
2 5008 45833 2 0 3
3 5795 30862 4 1 3
4 5784 31705 4 1 2
5 5359 34010 2 0 2
6 5235 45854 3 1 1

Darstellung der Daten

Mit Seaborn können die Daten dargestellt werden. Die Regressionsgerade sowie die Häufigkeitsverteilungen werden automatisch berechnet und geplottet.

sns.lmplot(x="Meilen", y="Preis", row="Garage", col="Farbe", data=dg,
             ci=None, palette="muted", size=4,
             scatter_kws={"s": 50, "alpha": 1});

png

Analyse mit den metrischen Daten

Wir berechnen nun die Regression mit StatsModels, vorerst mit den metrischen Daten. Die gewünschte Regressionsbeziehung kann mit

$$ Preis \sim Meilen + Service $$

in einem Format angegeben werden, wie es bei R üblich ist: y=Preis soll durch x1=Meilen und x2=Service ausgedrückt werden.

$$y = p_0 + p_1x_1 + p_2x_2$$

Wenn die formula-Methode zur Eingabe des Regressionsmodelles verwendet wird, so wird ein allfälliges intercept (\(p_0\)) von StatsModels automatisch beigefügt. Im Eingabeformat mit Numpy-Arrays müsste der Benutzer das intercept (\(p_0\)) explizit einfügen oder weglassen.

Die Ergebnisparameter liegen als pd.DataSeries vor, also ein 1-dimensionales DataFrame.

estimP1 = smf.ols(formula='Preis ~ Meilen + Service ', data=dg).fit()
p       = estimP1.params   # hier sind die Parameter enthalten
print(p)
estimP1.summary()         # Asudruck der vollständigen Zusammenfassung
Intercept    6206.128356
Meilen         -0.031463
Service       135.837493
dtype: float64
OLS Regression Results
Dep. Variable: Preis R-squared: 0.974
Model: OLS Adj. R-squared: 0.974
Method: Least Squares F-statistic: 1822.
Date: Tue, 12 Sep 2017 Prob (F-statistic): 1.19e-77
Time: 16:55:26 Log-Likelihood: -512.89
No. Observations: 100 AIC: 1032.
Df Residuals: 97 BIC: 1040.
Df Model: 2
Covariance Type: nonrobust
coef std err t P>|t| [0.025 0.975]
Intercept 6206.1284 24.966 248.581 0.000 6156.577 6255.679
Meilen -0.0315 0.001 -49.788 0.000 -0.033 -0.030
Service 135.8375 3.903 34.807 0.000 128.092 143.583
Omnibus: 3.778 Durbin-Watson: 2.280
Prob(Omnibus): 0.151 Jarque-Bera (JB): 2.103
Skew: -0.039 Prob(JB): 0.349
Kurtosis: 2.294 Cond. No. 2.21e+05

Weiterverwendung der Ergebnisse

Mit den Regressionsparametern drücken wir die Regressionsebene aus. Dies ist mit 3-dimensionaler Grafik möglich, solang nur 2 Eingangsgrössen x1 und x2 vorliegen. Für die x1- und x2-Werte bilden wir je einen NumPy-array, welche wir zu einem meshgrid erweitern. Damit können wir die y-Werte der Regressionsebene berechnen. Mit Matplotlib plotten wir die Daten und die Ebene.

Mmin = min(dg["Meilen"]);  Mmax = max(dg["Meilen"]);
Smin = min(dg["Service"]); Smax = max(dg["Service"]);
Pmin = min(dg["Preis"]);   Pmax = max(dg["Preis"]);

print("Meilen : min, max = ", Mmin,",", Mmax )
print("Service: min, max = ", Smin,",",  Smax)
print("Preis  : min, max = ", Pmin,",",  Pmax)

x1 = np.linspace(Mmin, Mmax, 6)                # 1-dim array für x1 = Meilen
x2 = np.linspace(Smin, Smax, 6)                # 1-dim array für x2 = Service
x1,x2 = np.meshgrid(x1, x2)                    # 2-dim array für x1 und x2
y = p[0]*np.ones_like(x1) + p[1]*x1 + p[2]*x2  # Ebenen-Gleichung
Meilen : min, max =  19057 , 49223
Service: min, max =  0 , 5
Preis  : min, max =  4787 , 5911
from mpl_toolkits.mplot3d import Axes3D      # 3-dim Grafik erstellen
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.gca(projection='3d')
surf = ax.plot_surface(x1, x2, y, rstride=1, cstride=1, cmap=cm.coolwarm,
                       linewidth=0, antialiased=True)     # Regressionsebene
ax.scatter(dg["Meilen"], dg["Service"], dg["Preis"]+440)   # Daten
ax.set_zlim(2000, 6400)
ax.zaxis.set_major_locator(LinearLocator(10))
ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))
fig.colorbar(surf, shrink=0.5, aspect=5)
plt.show()

png

Analyse mit den kategorialen Daten

Wir schliessen nun auch die kategorialen Daten wie die Farbe (Werte: 1, 2, 3) oder die Garagierung der Fahrzeuge (0 = nein, 1 = ja) in die Analyse ein. Würden die Farben als Begriffe wie "blau", "rot" oder "schwarz" vorliegen, dann wäre StatsModels in der Lage, diese automatisch als kategoriale Daten zu erkennen und zu behandeln. Da sie aber als integer-Werte vorliegen, müssen wir StatsModels explizit mitteilen, dass sie nicht metrisch sondern kategorial behandelt werden sollen. Dies erfolgt recht einfach, indem die Werte mit C() ins Regressionsmodel eingegeben werden. In R spricht man dann auch davon, "die Daten als Faktor beifügen"

Ein Blick auf die Daten in Boxplot-Form zeigt, dass garagierte Fahrzeuge höhere Preise erzielen (vgl. graue Boxen), und es macht den Eindruck, dass innerhalb der Farben die Farbe Nr. 2 höhere Preise zu erzielen vermag als die andern(vgl.rote Boxen).

sns.boxplot(x="Garage", y="Preis",              data=dg, palette="Greys");
sns.boxplot(x="Garage", y="Preis", hue="Farbe", data=dg, palette="Reds" );

png

Wir berechnen zuerst ein Modell für

$$ Preis \sim Meilen + Service + C(Garage) $$

und dann für

$$ Preis \sim Meilen + Service + C(Garage)+ C(Farbe) $$

Die Ergebnistabellen zeigen anhand der t- und p-Werte, dass die Koeffizienten für die Farbe nicht signifikant sind, d.h. die Farben unterscheiden sich in ihrer Wirkung auf den Preis nicht signifikant voneinander.

estimP2 = smf.ols(formula='Preis ~ Meilen + Service + C(Garage)', data=dg).fit()
q = estimP2.params      # hier sind die Parameter enthalten
print(type(q))
print(q)
#estimP2.summary()      # Asudruck der vollständigen Zusammenfassung
<class 'pandas.core.series.Series'>
Intercept         6187.365916
C(Garage)[T.1]      19.007410
Meilen              -0.031137
Service            134.541218
dtype: float64
estimP3 = smf.ols(formula='Preis ~ Meilen + Service + C(Garage)+ C(Farbe)', data=dg).fit()
r = estimP3.params      # hier sind die Parameter enthalten
print(type(r))
print(r)
estimP3.summary()      # Asudruck der vollständigen Zusammenfassung
<class 'pandas.core.series.Series'>
Intercept         6197.324575
C(Garage)[T.1]      20.454997
C(Farbe)[T.2]      -10.438274
C(Farbe)[T.3]       -6.703134
Meilen              -0.031331
Service            135.185787
dtype: float64
OLS Regression Results
Dep. Variable: Preis R-squared: 0.976
Model: OLS Adj. R-squared: 0.974
Method: Least Squares F-statistic: 751.9
Date: Tue, 12 Sep 2017 Prob (F-statistic): 3.88e-74
Time: 16:33:32 Log-Likelihood: -509.83
No. Observations: 100 AIC: 1032.
Df Residuals: 94 BIC: 1047.
Df Model: 5
Covariance Type: nonrobust
coef std err t P>|t| [0.025 0.975]
Intercept 6197.3246 28.027 221.119 0.000 6141.676 6252.973
C(Garage)[T.1] 20.4550 8.639 2.368 0.020 3.303 37.607
C(Farbe)[T.2] -10.4383 11.498 -0.908 0.366 -33.267 12.391
C(Farbe)[T.3] -6.7031 9.924 -0.675 0.501 -26.408 13.002
Meilen -0.0313 0.001 -44.874 0.000 -0.033 -0.030
Service 135.1858 4.197 32.210 0.000 126.852 143.519
Omnibus: 0.814 Durbin-Watson: 2.267
Prob(Omnibus): 0.666 Jarque-Bera (JB): 0.930
Skew: -0.157 Prob(JB): 0.628
Kurtosis: 2.647 Cond. No. 2.56e+05

ANOVA (Analysis of Variances)

In der Varianzanalyse werden mit einem F-Test die Anteile der Variablen auf das Gesamtergebnis bewertet. In diesem Beispiel ist nur die Farbe nicht signifikant.

table = sm.stats.anova_lm(estimP3, typ=2)
print(table)
                 sum_sq    df            F        PR(>F)
C(Garage)  9.362784e+03   1.0     5.606652  1.994089e-02
C(Farbe)   1.581404e+03   2.0     0.473491  6.243016e-01
Meilen     3.362746e+06   1.0  2013.690498  2.766973e-65
Service    1.732516e+06   1.0  1037.470903  1.407394e-52
Residual   1.569745e+05  94.0          NaN           NaN

Es können auch ganze Modelle miteinander verglichen werden. Das einfachere Model estimP2 erscheint in der Tabelle mit dem Index 0 auf der ersten Zeile. Seine Residualquadratsumme ist kaum grösser als diejenige des umfangreicheren Modelles estimP3. Es ist damit vertretbar, das einfachere Modell zu verwenden.

table = sm.stats.anova_lm(estimP2,estimP3, typ=1)
print(table)
   df_resid            ssr  df_diff      ss_diff         F    Pr(>F)
0      96.0  158555.953796      0.0          NaN       NaN       NaN
1      94.0  156974.549379      2.0  1581.404417  0.473491  0.624302

Normalverteilung der Residuen

Für die graphische Überprüfung, ob die Residuen normalverteilt sind, steht der QQ-Plot zur Verfügung. Mit den gesetzten Befehlsparameter werden die Parameter für die t-Verteilung mit bestimmt.

resP3 = estimP3.resid          # residuals
fitP3 = estimP3.fittedvalues   # fitted values
import scipy.stats as stats
fig = sm.qqplot(resP3, stats.t, fit=True, line='45')
plt.show()

png

Lillifors test for normality, Kolmogorov Smirnov test with estimated mean and variance

Der Kolmogorov Smirnov Test ist ein testtheoretisches Verfahren, das die Residuen auf Normalverteilung prüft.

kstest = sm.stats.diagnostic.kstest_normal(resP3, pvalmethod='approx')
print("Kolmogorov Smirnov Test     :", kstest)
print("Residuen Mittelwert         :", np.mean(estimP3.resid))
print("Residuen Standardabweichung :", np.std(estimP3.resid))
Kolmogorov Smirnov Test     : (0.047867552271722208, 0.81426179797264298)
Residuen Mittelwert         : 6.168340405565686e-08
Residuen Standardabweichung : 39.62001380346251

Plot: fitted Values vs Residuum

Im Residuen-Plot, mit den vorhergesagten Werten auf der x-Achse und den Residuen auf der y-Achse, sind keine Muster erkennbar. Die Voraussetzungen für die Regression wurden vermutlich erfüllt.

plt.plot(fitP3,resP3,'o')
plt.show()

png

Weitere Ergebnisse

estimP3 ist als der Output der ols-Funktion ein (programmiertechnisches) Objekt, dem ziemliche viele Attribute mitgegeben werden. Diese können abgerufen werden mit estimP3.attribut . Eine Liste der verfügbaren Attribute erhält man mit dem Befehl dir(estimP3)

dir(estimP3)
['HC0_se',
 'HC1_se',
 'HC2_se',
 'HC3_se',
 '_HCCM',
 '__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 '_cache',
 '_data_attr',
 '_get_robustcov_results',
 '_is_nested',
 '_wexog_singular_values',
 'aic',
 'bic',
 'bse',
 'centered_tss',
 'compare_f_test',
 'compare_lm_test',
 'compare_lr_test',
 'condition_number',
 'conf_int',
 'conf_int_el',
 'cov_HC0',
 'cov_HC1',
 'cov_HC2',
 'cov_HC3',
 'cov_kwds',
 'cov_params',
 'cov_type',
 'df_model',
 'df_resid',
 'diagn',
 'eigenvals',
 'el_test',
 'ess',
 'f_pvalue',
 'f_test',
 'fittedvalues',
 'fvalue',
 'get_influence',
 'get_prediction',
 'get_robustcov_results',
 'initialize',
 'k_constant',
 'llf',
 'load',
 'model',
 'mse_model',
 'mse_resid',
 'mse_total',
 'nobs',
 'normalized_cov_params',
 'outlier_test',
 'params',
 'predict',
 'pvalues',
 'remove_data',
 'resid',
 'resid_pearson',
 'rsquared',
 'rsquared_adj',
 'save',
 'scale',
 'ssr',
 'summary',
 'summary2',
 't_test',
 'tvalues',
 'uncentered_tss',
 'use_t',
 'wald_test',
 'wald_test_terms',
 'wresid']