Welche Qualität hat der Rotwein?¶

1. Business Understanding¶

Eine Weinkellerei möchte sicherstellen, dass sie stets qualitativ hochwertigen und schmackhaften Wein produziert. Sie beschäftigen professionelle Weinverkoster, die alle neuen Weinpartien testen. Diese Fachleute bewerten den Wein dann auf einer Skala von 1 bis 10. Dieses Verfahren ist jedoch teuer und subjektiv. Die Weinmanufaktur möchte ein maschinelles Lernmodell einsetzen, um Vorhersagen über die Qualität zu treffen. Das ultimative Ziel ist die Entwicklung eines Modells zu entwickeln, das neue Weinpartien anhand ihrer chemischen Eigenschaften automatisch kategorisiert.

2. Data Understanding¶

2.1. Bibliotheken Importieren¶

In [197]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import math
import pickle

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn import metrics
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import LogisticRegression

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Activation, Dropout

%matplotlib inline

2.2. Daten Auslesen¶

In [198]:
data = pd.read_csv('https://storage.googleapis.com/ml-service-repository-datastorage/What_Quality_does_the_Red_wine_have_winequality.csv')

2.3. Deskriptive Datenanalyse¶

Datenbeschreibung¶

Der Datensatz mit dem Namen "Wine Quality Data Set" wurde der UCI-Website entnommen entnommen und liefert uns reale Daten für unsere Weinmanufaktur. Die Daten wurden im Jahr 2009 erhoben und stammen aus Nordportugal. Der Datensatz enthält Einträge für Rot- und Weißweine. Der Datensatz für die Rotweine hat also 12 Spalten und 1600 Einträge. Die Spalten sind nach den Inhaltsstoffen und der Qualität eines Rotweins in Tabelle 1 benannt.

Feature Data Type
fixed acidity float64
volatile acidity float64
citric acid float64
residual sugar float64
chlorides float64
free sulfur dioxide float64
total sulfur dioxide float64
density float64
pH float64
sulphates float64
alcohol float64
quality int64

Ein Rotwein besteht aus Wasser, Alkohol und Extrakten. Die genaue Mischung dieser 3 wesentlichen Bestandteile kann einen Rotwein zu einem echten ein wahrer Genuss. Meiner Meinung nach enthält ein guter Rotwein einen hohen Alkohol- und Zuckergehalt. Ob meine Meinung über einen guten Wein richtig ist wahr ist, können wir im nächsten Kapitel näher betrachten.

In [199]:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1599 entries, 0 to 1598
Data columns (total 12 columns):
 #   Column                Non-Null Count  Dtype  
---  ------                --------------  -----  
 0   fixed acidity         1599 non-null   float64
 1   volatile acidity      1599 non-null   float64
 2   citric acid           1599 non-null   float64
 3   residual sugar        1599 non-null   float64
 4   chlorides             1599 non-null   float64
 5   free sulfur dioxide   1599 non-null   float64
 6   total sulfur dioxide  1599 non-null   float64
 7   density               1599 non-null   float64
 8   pH                    1599 non-null   float64
 9   sulphates             1599 non-null   float64
 10  alcohol               1599 non-null   float64
 11  quality               1599 non-null   int64  
dtypes: float64(11), int64(1)
memory usage: 150.0 KB
In [200]:
data.describe(include="all")
Out[200]:
fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality
count 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000
mean 8.319637 0.527821 0.270976 2.538806 0.087467 15.874922 46.467792 0.996747 3.311113 0.658149 10.422983 5.636023
std 1.741096 0.179060 0.194801 1.409928 0.047065 10.460157 32.895324 0.001887 0.154386 0.169507 1.065668 0.807569
min 4.600000 0.120000 0.000000 0.900000 0.012000 1.000000 6.000000 0.990070 2.740000 0.330000 8.400000 3.000000
25% 7.100000 0.390000 0.090000 1.900000 0.070000 7.000000 22.000000 0.995600 3.210000 0.550000 9.500000 5.000000
50% 7.900000 0.520000 0.260000 2.200000 0.079000 14.000000 38.000000 0.996750 3.310000 0.620000 10.200000 6.000000
75% 9.200000 0.640000 0.420000 2.600000 0.090000 21.000000 62.000000 0.997835 3.400000 0.730000 11.100000 6.000000
max 15.900000 1.580000 1.000000 15.500000 0.611000 72.000000 289.000000 1.003690 4.010000 2.000000 14.900000 8.000000

Der Datensatz enthält 1599 Einträge. Die Spalten "freies Schwefeldioxid" und "Gesamtschwefeldioxid" haben sehr hohe Maximalwerte. Außerdem sehen wir, dass unsere Zielvariable den kleinsten Wert von 3 und den größten Wert von 8 hat. Hier müssen wir überlegen, ob die Zielvariable in gut und schlecht unterteilt werden muss.

2.4 Datenbereinigung¶

In [201]:
data.isnull().sum()
Out[201]:
fixed acidity           0
volatile acidity        0
citric acid             0
residual sugar          0
chlorides               0
free sulfur dioxide     0
total sulfur dioxide    0
density                 0
pH                      0
sulphates               0
alcohol                 0
quality                 0
dtype: int64

There are no Null values in the dataset

In [202]:
data[data.duplicated(keep=False)]
Out[202]:
fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality
0 7.4 0.700 0.00 1.9 0.076 11.0 34.0 0.99780 3.51 0.56 9.4 5
4 7.4 0.700 0.00 1.9 0.076 11.0 34.0 0.99780 3.51 0.56 9.4 5
9 7.5 0.500 0.36 6.1 0.071 17.0 102.0 0.99780 3.35 0.80 10.5 5
11 7.5 0.500 0.36 6.1 0.071 17.0 102.0 0.99780 3.35 0.80 10.5 5
22 7.9 0.430 0.21 1.6 0.106 10.0 37.0 0.99660 3.17 0.91 9.5 5
... ... ... ... ... ... ... ... ... ... ... ... ...
1567 7.2 0.695 0.13 2.0 0.076 12.0 20.0 0.99546 3.29 0.54 10.1 5
1579 6.2 0.560 0.09 1.7 0.053 24.0 32.0 0.99402 3.54 0.60 11.3 5
1581 6.2 0.560 0.09 1.7 0.053 24.0 32.0 0.99402 3.54 0.60 11.3 5
1592 6.3 0.510 0.13 2.3 0.076 29.0 40.0 0.99574 3.42 0.75 11.0 6
1596 6.3 0.510 0.13 2.3 0.076 29.0 40.0 0.99574 3.42 0.75 11.0 6

460 rows × 12 columns

Es gibt Duplikate in dem Datensatz. Aber in diesem Fall wollen wir diese nicht entfernen, da es sich eigentlich um verschiedene Weinpartien handelt, die zufällig die gleichen chemischen Eigenschaften und die gleiche Qualität haben. Es wäre nicht schlecht, wenn sich das Modell auf die häufigeren Weine spezialisieren würde

In [203]:
data_clean = data
In [204]:
# Analyse the target variable
plt.figure(figsize =(12, 8))
sns.countplot(x="quality", data=data_clean )
Out[204]:
<AxesSubplot:xlabel='quality', ylabel='count'>
No description has been provided for this image

3. Data Preparation¶

3.1 Ausreißer entfernen¶

In [205]:
data_clean.hist(figsize=(20,20), bins= 60)
Out[205]:
array([[<AxesSubplot:title={'center':'fixed acidity'}>,
        <AxesSubplot:title={'center':'volatile acidity'}>,
        <AxesSubplot:title={'center':'citric acid'}>],
       [<AxesSubplot:title={'center':'residual sugar'}>,
        <AxesSubplot:title={'center':'chlorides'}>,
        <AxesSubplot:title={'center':'free sulfur dioxide'}>],
       [<AxesSubplot:title={'center':'total sulfur dioxide'}>,
        <AxesSubplot:title={'center':'density'}>,
        <AxesSubplot:title={'center':'pH'}>],
       [<AxesSubplot:title={'center':'sulphates'}>,
        <AxesSubplot:title={'center':'alcohol'}>,
        <AxesSubplot:title={'center':'quality'}>]], dtype=object)
No description has been provided for this image

Anhand der Histogramme aller Spalten können wir nun auf einen Blick sehen, wie deren Werte verteilt sind. Es fällt auf, dass einige Merkmale keine Normalverteilung aufweisen und daher Ausreißer in ihren Werten haben.

Fester Säuregehalt

In [206]:
sns.histplot(data_clean['fixed acidity'])
Out[206]:
<AxesSubplot:xlabel='fixed acidity', ylabel='Count'>
No description has been provided for this image

Das Diagramm sieht normalverteilt aus, deshalb bleiben die Werte im ersten Schritt

Volatile Acidity

In [207]:
sns.histplot(data_clean['volatile acidity'])
Out[207]:
<AxesSubplot:xlabel='volatile acidity', ylabel='Count'>
No description has been provided for this image
In [208]:
q1 = data_clean['volatile acidity'].quantile(0.99)
q1
Out[208]:
1.02
In [209]:
data1 = data_clean[data_clean['volatile acidity']<q1]
sns.histplot(data1['volatile acidity'])
Out[209]:
<AxesSubplot:xlabel='volatile acidity', ylabel='Count'>
No description has been provided for this image

Citric Acid

In [210]:
sns.histplot(data1['citric acid'])
Out[210]:
<AxesSubplot:xlabel='citric acid', ylabel='Count'>
No description has been provided for this image

Residual Sugar

In [211]:
sns.histplot(data1['residual sugar'])
Out[211]:
<AxesSubplot:xlabel='residual sugar', ylabel='Count'>
No description has been provided for this image
In [212]:
plt.figure(figsize=(12, 7))
sns.boxplot(x='quality',y='residual sugar', data=data1,palette='winter')
Out[212]:
<AxesSubplot:xlabel='quality', ylabel='residual sugar'>
No description has been provided for this image

Residual Sugar zeigt viele Ausreißer in Bezug auf die Zielvariable. Ein Teil der Ausreißer wird nun entfernt, um ein genaueres Datenbild zu erhalten.

In [213]:
q2 = data1['residual sugar'].quantile(0.99)
q2
Out[213]:
8.36300000000001
In [214]:
data2= data1[data1['residual sugar']<q2]
sns.histplot(data2['residual sugar'])
Out[214]:
<AxesSubplot:xlabel='residual sugar', ylabel='Count'>
No description has been provided for this image
In [215]:
plt.figure(figsize=(12, 7))
sns.boxplot(x='quality',y='residual sugar', data=data2,palette='winter')
Out[215]:
<AxesSubplot:xlabel='quality', ylabel='residual sugar'>
No description has been provided for this image

Es sind noch Ausreißer zu sehen, die aber nach einem ersten Durchlauf des Modells noch verändert werden können

Chlorides

In [216]:
sns.histplot(data2['chlorides'])
Out[216]:
<AxesSubplot:xlabel='chlorides', ylabel='Count'>
No description has been provided for this image
In [217]:
q3 = data2['chlorides'].quantile(0.98)
q3
Out[217]:
0.226
In [218]:
data3= data2[data2['chlorides']<q3]
sns.histplot(data3['chlorides'])
Out[218]:
<AxesSubplot:xlabel='chlorides', ylabel='Count'>
No description has been provided for this image

Free Sulfur Dioxide

In [219]:
sns.histplot(data3['free sulfur dioxide'])
Out[219]:
<AxesSubplot:xlabel='free sulfur dioxide', ylabel='Count'>
No description has been provided for this image
In [220]:
plt.figure(figsize=(12, 7))
sns.boxplot(x='quality',y='free sulfur dioxide', data=data3,palette='winter')
Out[220]:
<AxesSubplot:xlabel='quality', ylabel='free sulfur dioxide'>
No description has been provided for this image
In [221]:
q4 = data3['free sulfur dioxide'].quantile(0.99)
q4
Out[221]:
47.700000000000045
In [222]:
data4= data3[data3['free sulfur dioxide']<q4]
sns.histplot(data4['free sulfur dioxide'])
Out[222]:
<AxesSubplot:xlabel='free sulfur dioxide', ylabel='Count'>
No description has been provided for this image
In [223]:
plt.figure(figsize=(12, 7))
sns.boxplot(x='quality',y='free sulfur dioxide', data=data4,palette='winter')
Out[223]:
<AxesSubplot:xlabel='quality', ylabel='free sulfur dioxide'>
No description has been provided for this image

Zuvor erkannte Ausreißer der Spalte 'free sulfur dioxide' entfernt und ein ausgeglichenes Bild zu unserer Zielvariablen geschaffen

Total Sulfur Dioxide

In [224]:
sns.histplot(data4['total sulfur dioxide'])
Out[224]:
<AxesSubplot:xlabel='total sulfur dioxide', ylabel='Count'>
No description has been provided for this image
In [225]:
plt.figure(figsize=(12, 7))
sns.boxplot(x='quality',y='total sulfur dioxide', data=data4,palette='winter')
Out[225]:
<AxesSubplot:xlabel='quality', ylabel='total sulfur dioxide'>
No description has been provided for this image
In [226]:
q5 = data4['total sulfur dioxide'].quantile(0.99)
q5
Out[226]:
143.8599999999999
In [227]:
data5 = data4[data4['total sulfur dioxide']<q5]
sns.histplot(data5['total sulfur dioxide'])
Out[227]:
<AxesSubplot:xlabel='total sulfur dioxide', ylabel='Count'>
No description has been provided for this image

Sulphates

In [228]:
sns.histplot(data5['sulphates'])
Out[228]:
<AxesSubplot:xlabel='sulphates', ylabel='Count'>
No description has been provided for this image
In [229]:
q6 = data5['sulphates'].quantile(0.99)
q6
Out[229]:
1.1602
In [230]:
data6 = data5[data5['sulphates']<q6]
sns.histplot(data6['sulphates'])
Out[230]:
<AxesSubplot:xlabel='sulphates', ylabel='Count'>
No description has been provided for this image
In [231]:
sns.histplot(data6['alcohol'])
Out[231]:
<AxesSubplot:xlabel='alcohol', ylabel='Count'>
No description has been provided for this image
In [232]:
plt.figure(figsize=(12, 7))
sns.boxplot(x='quality',y='alcohol', data=data6,palette='winter')
Out[232]:
<AxesSubplot:xlabel='quality', ylabel='alcohol'>
No description has been provided for this image

je höher der Alkoholgehalt des Weins ist, desto besser wird er von den Fachleuten bewertet

Alle Merkmale wurden im ersten Schritt bereinigt und können nun für das Modell verwendet werden.
ie Zielvariable Qualität wird in gut 1 und schlecht 0 aufgeteilt. Dies ändert das Problem von einer Regressionsaufgabe zu einer (binären) Kategorisierungsaufgabe

Umwandlung der Zielvariablen 'Qualität' in eine binäre Klassifikation¶

In [233]:
def quality_range(quality):
    if quality <= 5:
        return 0
    elif quality >=6:
        return 1
In [234]:
# make a (deep) copy, to make sure pandas doesn't complain and makes the correct thing
# see https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
data6 = data6.copy()
In [235]:
data6['quality_range'] = data6['quality'].apply(quality_range)
In [236]:
data6.describe()
Out[236]:
fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality quality_range
count 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000
mean 8.331267 0.520044 0.266961 2.427763 0.081051 15.176550 43.828841 0.996705 3.316846 0.643652 10.455492 5.661725 0.546496
std 1.761040 0.165732 0.192996 0.975109 0.020813 9.343802 29.133109 0.001854 0.149700 0.129927 1.071880 0.800449 0.498001
min 4.600000 0.120000 0.000000 0.900000 0.012000 1.000000 6.000000 0.990070 2.860000 0.330000 8.400000 3.000000 0.000000
25% 7.100000 0.390000 0.090000 1.900000 0.070000 7.000000 21.000000 0.995570 3.220000 0.550000 9.500000 5.000000 0.000000
50% 7.900000 0.520000 0.250000 2.200000 0.079000 13.000000 37.000000 0.996700 3.315000 0.620000 10.200000 6.000000 1.000000
75% 9.225000 0.630000 0.420000 2.600000 0.089000 21.000000 59.000000 0.997800 3.400000 0.720000 11.100000 6.000000 1.000000
max 15.900000 1.010000 0.790000 8.300000 0.222000 47.000000 143.000000 1.003200 4.010000 1.160000 14.900000 8.000000 1.000000
In [237]:
data6.head()
Out[237]:
fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality quality_range
0 7.4 0.70 0.00 1.9 0.076 11.0 34.0 0.9978 3.51 0.56 9.4 5 0
1 7.8 0.88 0.00 2.6 0.098 25.0 67.0 0.9968 3.20 0.68 9.8 5 0
2 7.8 0.76 0.04 2.3 0.092 15.0 54.0 0.9970 3.26 0.65 9.8 5 0
3 11.2 0.28 0.56 1.9 0.075 17.0 60.0 0.9980 3.16 0.58 9.8 6 1
4 7.4 0.70 0.00 1.9 0.076 11.0 34.0 0.9978 3.51 0.56 9.4 5 0

Mit der Methode quality_range() haben wir die Werte der Spalte "quality" aufgeteilt. Alle Werte kleiner und gleich 5 erhielten den Wert 0 (, was auf eine schlechtere als durchschnittliche Qualität hinweist). Alle Werte von 6 und größer erhielten den Wert 1 (für überdurchschnittliche Qualität).

In [238]:
plt.figure(figsize =(12, 8))
sns.countplot(data=data6, x="quality_range")
Out[238]:
<AxesSubplot:xlabel='quality_range', ylabel='count'>
No description has been provided for this image

Die Zielvariable ist ausgeglichen

In [239]:
data6.drop('quality', axis=1, inplace=True)
In [240]:
data6.describe()
Out[240]:
fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality_range
count 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000 1484.000000
mean 8.331267 0.520044 0.266961 2.427763 0.081051 15.176550 43.828841 0.996705 3.316846 0.643652 10.455492 0.546496
std 1.761040 0.165732 0.192996 0.975109 0.020813 9.343802 29.133109 0.001854 0.149700 0.129927 1.071880 0.498001
min 4.600000 0.120000 0.000000 0.900000 0.012000 1.000000 6.000000 0.990070 2.860000 0.330000 8.400000 0.000000
25% 7.100000 0.390000 0.090000 1.900000 0.070000 7.000000 21.000000 0.995570 3.220000 0.550000 9.500000 0.000000
50% 7.900000 0.520000 0.250000 2.200000 0.079000 13.000000 37.000000 0.996700 3.315000 0.620000 10.200000 1.000000
75% 9.225000 0.630000 0.420000 2.600000 0.089000 21.000000 59.000000 0.997800 3.400000 0.720000 11.100000 1.000000
max 15.900000 1.010000 0.790000 8.300000 0.222000 47.000000 143.000000 1.003200 4.010000 1.160000 14.900000 1.000000

3.3. Test for Multicollinearity¶

In [241]:
plt.figure(figsize=(20,10))
feature_corr = data6.corr()
sns.heatmap(feature_corr, annot=True, cmap='coolwarm')
Out[241]:
<AxesSubplot:>
No description has been provided for this image
In [242]:
data6.drop('fixed acidity', axis=1, inplace=True)

der feste Säuregehalt entfällt, da er stark mit dem pH-Wert korreliert

In [243]:
plt.figure(figsize=(20,10))
feature_corr = data6.corr()
sns.heatmap(feature_corr, annot=True, cmap='coolwarm')
Out[243]:
<AxesSubplot:>
No description has been provided for this image

3.4. Test- und Trainingsdaten erstellen¶

In [244]:
x_train, x_test, y_train, y_test = train_test_split(data6.drop('quality_range', axis=1),data6['quality_range'], test_size=0.2, random_state=365)

4. Modellierung und Bewertung¶

4.1. Daten skalieren und transformieren¶

In [245]:
scaler = StandardScaler()
scaler.fit(x_train)

X_train = scaler.transform(x_train)
X_test = scaler.transform(x_test)

4.1 Modellbildung logistsiche Regression¶

In [246]:
model_logistic_regression = LogisticRegression(random_state=0, C=1e8, max_iter=1000)
In [247]:
# Train LogistModel
model_logistic_regression.fit(x_train,y_train)
prediction_test = model_logistic_regression.predict(x_test)
prediction_train = model_logistic_regression.predict(x_train)

Evaluierung¶

In [248]:
acc = metrics.accuracy_score(y_test, prediction_test)
print('Accuracy on the Test dataset: {}'.format(acc))
Accuracy on the Test dataset: 0.6936026936026936
In [249]:
print("Trainingsdaten:")
print(classification_report(y_train,prediction_train))
print("Testdaten:")
print(classification_report(y_test,prediction_test))
Training dataset:
              precision    recall  f1-score   support

           0       0.74      0.74      0.74       534
           1       0.79      0.79      0.79       653

    accuracy                           0.76      1187
   macro avg       0.76      0.76      0.76      1187
weighted avg       0.76      0.76      0.76      1187

Test dataset:
              precision    recall  f1-score   support

           0       0.67      0.68      0.67       139
           1       0.71      0.71      0.71       158

    accuracy                           0.69       297
   macro avg       0.69      0.69      0.69       297
weighted avg       0.69      0.69      0.69       297

In [250]:
# Confusion-Matrix Test dataset
cm = confusion_matrix(y_test,prediction_test)
df_cm = pd.DataFrame(cm, index=['worse wine','better wine'], columns=['worse wine', 'better wine'],)
fig = plt.figure(figsize=[10,7])
heatmap = sns.heatmap(df_cm, annot=True, fmt="d")
heatmap.yaxis.set_ticklabels(heatmap.yaxis.get_ticklabels(), rotation=0, ha='right', fontsize=14)
heatmap.xaxis.set_ticklabels(heatmap.xaxis.get_ticklabels(), rotation=45, ha='right', fontsize=14)
plt.ylabel('True label')
plt.xlabel('Predicted label')
Out[250]:
Text(0.5, 42.0, 'Predicted label')
No description has been provided for this image
In [251]:
# confusion Matrix 
tn, fp, fn, tp = cm.ravel()
recall = tp/(fn+tp)
precision = tp/(tp+fp)
print("True Negatives: " + str(tn))
print("False Positives: " + str(fp))
print("False Negatives: " + str(fn))
print("True Positives: " + str(tp))
print("Recall: " + str(recall))
print("Precision: " + str(precision))
True Negatives: 94
False Positives: 45
False Negatives: 46
True Positives: 112
Recall: 0.7088607594936709
Precision: 0.7133757961783439
In [252]:
# ROC-Kurve, AUC
fig, ax = plt.subplots(figsize=(8,6))
ax.set_title('ROC Curve')
plot = metrics.plot_roc_curve(model_logistic_regression, x_test, y_test, ax=ax)
ax.plot([0,1], [0,1], '--')
Out[252]:
[<matplotlib.lines.Line2D at 0x1ffeab0a280>]
No description has been provided for this image
In [253]:
# print out Regression parameters (weights and biases)
weights = pd.Series(model_logistic_regression.coef_[0], index=x_train.columns.values)
weights.sort_values(ascending = False)
Out[253]:
sulphates               5.210340
alcohol                 0.913617
free sulfur dioxide     0.019869
total sulfur dioxide   -0.018066
residual sugar         -0.177201
citric acid            -1.293662
pH                     -1.618701
density                -1.921585
volatile acidity       -3.134072
chlorides              -3.875051
dtype: float64
In [254]:
# grafically ilustrate the most important features of a good wine 
weights = pd.Series(model_logistic_regression.coef_[0], index=x_train.columns.values)
print (weights.sort_values(ascending = False)[:7].plot(kind='bar'))
AxesSubplot(0.125,0.125;0.775x0.755)
No description has been provided for this image

3. Lineare Regression¶

In [255]:
X = data6.drop(['quality_range'], axis=1)
Y = data6['quality_range']
In [256]:
x_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=365)
In [257]:
model_linear_regression = LinearRegression()
In [258]:
model_linear_regression.fit(x_train, y_train)
Out[258]:
LinearRegression()
In [259]:
print(model_linear_regression.intercept_)
-10.073788720357635
In [260]:
coeff_data = pd.DataFrame(model_linear_regression.coef_, X.columns, columns=['Coefficient'])
coeff_data
Out[260]:
Coefficient
volatile acidity -0.560376
citric acid -0.234547
residual sugar -0.035414
chlorides -0.557458
free sulfur dioxide 0.004425
total sulfur dioxide -0.003436
density 9.804198
pH -0.254166
sulphates 0.843459
alcohol 0.164131
In [261]:
predictions = model_linear_regression.predict(x_test)
In [262]:
plt.scatter(y_test,predictions)
Out[262]:
<matplotlib.collections.PathCollection at 0x1ffeabd7a60>
No description has been provided for this image
In [263]:
sns.histplot((y_test-predictions),bins=50)
Out[263]:
<AxesSubplot:xlabel='quality_range', ylabel='Count'>
No description has been provided for this image
In [264]:
print('MAE:', metrics.mean_absolute_error(y_test, predictions))
print('MSE:', metrics.mean_squared_error(y_test, predictions))
print('RMSE:', np.sqrt(metrics.mean_squared_error(y_test, predictions)))
MAE: 0.3896858212406587
MSE: 0.20325428149258176
RMSE: 0.4508373115577079
In [265]:
print('R sq: ',model_linear_regression.score(x_train,y_train))
R sq:  0.3315244678627577
In [266]:
print('Correlation: ', math.sqrt(model_linear_regression.score(x_train,y_train)))
Correlation:  0.5757816147314515
In [267]:
print('R sq: ',model_linear_regression.score(x_test,y_test))
R sq:  0.1836418852481948

Tesnorflow Classification¶

In [268]:
tf.__version__
Out[268]:
'2.4.1'
In [269]:
X_train.shape
Out[269]:
(1187, 10)
In [270]:
# Define the Tensor flow Neural Network with Inputlayer, 2x Hiddenlayer and one Outputlayer

model = Sequential()
model.add(Dense(units=10,activation='relu')) 
model.add(Dense(units=8,activation='relu')) #Hiddenlayer
model.add(Dense(units=5,activation='relu')) #Hiddenlayer

model.add(Dense(units=1,activation='sigmoid')) #Outputlayer 1 Target

# For a binary classification Problem
model.compile(loss='binary_crossentropy', optimizer='adam')
In [271]:
model.fit(x=X_train, 
          y=y_train, 
          epochs=7,
          validation_data=(X_test, y_test), verbose=1
          )
Epoch 1/7
38/38 [==============================] - 1s 6ms/step - loss: 0.6612 - val_loss: 0.6592
Epoch 2/7
38/38 [==============================] - 0s 2ms/step - loss: 0.6298 - val_loss: 0.6465
Epoch 3/7
38/38 [==============================] - 0s 1ms/step - loss: 0.6198 - val_loss: 0.6371
Epoch 4/7
38/38 [==============================] - 0s 1ms/step - loss: 0.5942 - val_loss: 0.6305
Epoch 5/7
38/38 [==============================] - 0s 2ms/step - loss: 0.6049 - val_loss: 0.6230
Epoch 6/7
38/38 [==============================] - 0s 2ms/step - loss: 0.5675 - val_loss: 0.6186
Epoch 7/7
38/38 [==============================] - 0s 1ms/step - loss: 0.5691 - val_loss: 0.6111
Out[271]:
<tensorflow.python.keras.callbacks.History at 0x1ffeb282c70>
In [272]:
#model.history.history
model_loss = pd.DataFrame(model.history.history)
# Create a plot to compare the performance on the training dataset to the loss on the validation dataset
model_loss.plot()
Out[272]:
<AxesSubplot:>
No description has been provided for this image
In [273]:
#Vorhersagen bestimmen
predictions = (model.predict(X_test) > 0.5).astype("int32")
prediction = (model.predict(X_train) > 0.5).astype("int32")
In [274]:
# Klassifikations Report
print("Trainingsdaten:")
print(classification_report(y_train,prediction))
print("Testdaten:")
print(classification_report(y_test,predictions))
Trainingsdaten:
              precision    recall  f1-score   support

           0       0.70      0.72      0.71       534
           1       0.76      0.75      0.76       653

    accuracy                           0.74      1187
   macro avg       0.73      0.73      0.73      1187
weighted avg       0.74      0.74      0.74      1187

Testdaten:
              precision    recall  f1-score   support

           0       0.66      0.74      0.70       139
           1       0.74      0.66      0.70       158

    accuracy                           0.70       297
   macro avg       0.70      0.70      0.70       297
weighted avg       0.70      0.70      0.70       297

In [275]:
# Confusion-Matrix Testdaten
cm = confusion_matrix(y_test,predictions)
df_cm = pd.DataFrame(cm, index=['worse wine','better wine'], columns=['worse wine', 'better wine'],)
fig = plt.figure(figsize=[10,7])
heatmap = sns.heatmap(df_cm, annot=True, fmt="d")
heatmap.yaxis.set_ticklabels(heatmap.yaxis.get_ticklabels(), rotation=0, ha='right', fontsize=14)
heatmap.xaxis.set_ticklabels(heatmap.xaxis.get_ticklabels(), rotation=45, ha='right', fontsize=14)
plt.ylabel('True label')
plt.xlabel('Predicted label')
Out[275]:
Text(0.5, 42.0, 'Predicted label')
No description has been provided for this image
In [276]:
# Confusion Matrix
tn, fp, fn, tp = cm.ravel()
recall = tp/(fn+tp)
precision = tp/(tp+fp)
print("True Negatives: " + str(tn))
print("False Positives: " + str(fp))
print("False Negatives: " + str(fn))
print("True Positives: " + str(tp))
print("Recall: " + str(recall))
print("Precision: " + str(precision))
True Negatives: 103
False Positives: 36
False Negatives: 54
True Positives: 104
Recall: 0.6582278481012658
Precision: 0.7428571428571429

Deployment¶

Um dieses trainierte ML-Modell zu nutzen, muss eine API erstellt werden.
Das Ziel ist, dass Sie mit einer API-Anfrage über das Internet auf das ML-Modell zugreifen können. (Oder Ihr Intranet)
Der Code für den Einsatz ist hier zu finden. https://github.com/Dustin-dusTir/ml-services-api
eventuell läuft dort noch eine Live-Version der API und des Frontends.

Wenn wir eine API für ein ML-Modell erstellen, wollen wir nicht jedes Mal ein neues Modell trainieren, wenn wir den API-Server bereitstellen oder neu bereitstellen wollen. Deshalb müssen wir das ML-Modell exportieren und speichern.

Für dieses spezielle Modell verwenden wir die Tensorflow-Funktionalität, um das Tensorflow-Modell zu exportieren. Und wir verwenden das Pickle-Modul, um den Datenskalierer auch auf der Festplatte zu speichern.

In [ ]:
# save the ml model
model.save(filepath='wine_NN_model_12epochs')

# load the model again
new_model = tf.keras.models.load_model('wine_NN_model_12epochs')

# make sure, that the model has been saved and loaded correctly
sum((model.predict(X_test) == new_model.predict(X_test)) == False)
In [ ]:
# save the data scaler with pickle
pickle.dump(scaler, open('scaler.sav', 'wb'))