Modelling Data – Case Study: Importance of domain knowledge

What´s the relation between earnings and happiness? I saw this chart and was strongly irritated – why is there a linear regression, it´s clearly a logarithmic relationship.
Linear relationship between GDP and happiness.

So I got angry and wanted to know, which model is the better fit. I started to work immediatly, because it´s a huge difference for man kind. Think about it: you give a poor person money and he gets as happy as a rich person with the same amount added – that´s against common sense and propaganda to get rich. Like an cultural desease.

So I gathered the data and did a first comparation, and this logarithmic model was the better fit:
Logarithmic relationship between GDP and happiness.

I was right and seriously willing to clear the mess up – so posted the “correct” model on facebook, to explain things to my friends.

Once I came down…

I asked myself: “What´s the model that fits the data best – that would be more correct?”

So I started to write an algorithm to check polynominal regression levels for fit using a random train and test data split. Finally, I got to this result and was amazed:
Best polynominal relationship between GDP and happiness.

This seriously hit me: “What the f***! There seems to be maximum happiness reachable with a certain amount of income / GDP.” Can you understand, what this result would mean for our world and economy? Think about all economies growing continiously, but well happiest was there or will come there. What would you do? Send income to less developed countries, because you don´t need it? Stop invention and progress, because it´s of no use? Seriously, I felt like a socialist: Stop progress at this point and share.

So I thought a while and concluded: “F***ing statistics, we need a profound econometric model.”

I started modelling: Well, the first amount of money in a market based on money leverages a huge amount of happiness, because you can participate and feed yourself. We can approximate that by infinit marginal utility. Then the more you have, the less utility should be provided by the additional same amount added. Finally, more income is more options, so more should be always better. I concluded, that this is catched by a Cobb Douglas production function. Here´s the graph:
Cobb Douglas relationship between GDP and happiness.

That´s it, that´s the final model. Here I feel home, this looks like a normal world – for an economist.

The Relevance of Domain Knowledge

As this short case study shows, we get completly wrong information and conclusions, if we don´t do it right. If you were the most important decision making algorithm in global economic politics, imagine what desasterous outcomes it would have produced to automatically find an optimum of income.

This is a serious border of AI. If you want to analyse Big Data with algorithms, you may produce seriously wrong information and conclusions. Statistical analysis is allways about using the right model. And modelling is about the assumptions of the model. As long as you can not create the right assumtions for the statistical model automatically, Big Data analysis is near to crazy. So out of this point of view, Big Data analysis is either about very simplistic tendencies (like linear trends) or it´s bound to Data Scientists with domain knowledge checking each model – that´s slow.

Discussion

I´m quite new to the field of Data Science, but this case study shows very though limitations, clearly. It´s not about flexible fitting of data, it´s about right models. And right models don´t scale into the Big Data domain. What do you think is the solution for this issue?

Countries of Happiness – the Full Article

If you are interested in my final article on my personal blog, explaining the final results: Please feel welcome to read the article here. There is a translation widget in the menu, to read in your favorite language. The original article is german.

ID3-Algorithmus: Ein Rechenbeispiel

Dieser Artikel ist Teil 3 von 4 der Artikelserie Maschinelles Lernen mit Entscheidungsbaumverfahren und nun wollen wir einen Entscheidungsbaum aus Daten herleiten, jedoch ohne Programmierung, sondern direkt auf Papier (bzw. HTML :-).

Folgender Datensatz sei gegeben:

Zeile Kundenart Zahlungsgeschwindigkeit Kauffrequenz Herkunft Zahlungsmittel: Rechnung?
 1  Neukunde  niedrig  niedrig  Inland  false
 2  Neukunde  niedrig  niedrig  Ausland  false
 3  Stammkunde  niedrig  niedrig  Inland  true
 4  Normalkunde  mittel  niedrig  Inland  true
 5  Normalkunde  hoch  hoch  Inland  true
 6  Normalkunde  hoch  hoch  Ausland  false
 7  Stammkunde  hoch  hoch  Ausland  true
 8  Neukunde  mittel  niedrig  Inland  false
 9  Neukunde  hoch  hoch  Inland  true
 10  Normalkunde  mittel  hoch  Inland  true
 11  Neukunde  mittel  hoch  Ausland  true
 12  Stammkunde  mittel  niedrig  Ausland  true
 13  Stammkunde  niedrig  hoch  Inland  true
 14  Normalkunde  mittel  niedrig  Ausland  false

Gleich vorweg ein Disclaimer: Der Datensatz ist natürlich überaus klein, ja gerade zu winzig. Dafür würden wir in der Praxis niemals einen Machine Learning Algorithmus einsetzen. Dennoch bleiben wir besser übersichtlich und nachvollziehbar mit diesen 14 Zeilen. Das Lernziel dieser Übung ist es, ein Gefühl für die Erstellung von Entscheidungsbäumen zu erhalten.
Zu beachten ist ferner, dass dieser Datensatz bereits aggregiert ist, denn eigentlich nummerisch abbildbare Daten wurden in Klassen zusammengefasst.

Das Ziel:

Der Datensatz spielt wieder, welchem Kunden (ID) bisher die Zahlung per Rechnung erlaubt und nicht widerrufen wurde. Das Ziel soll sein, eine Vorhersage darüber zu machen zu können, wann ein Kunde per Rechnung zahlen darf und wann nicht (dann per Vorkasse).

Der Algorithmus:

Wir verwenden den ID3-Algorithmus in seiner Reinform. Der ID3-Algorithmus ist der gängigste Algorithmus zum Aufbau datengetriebener Entscheidungsbäume und es gibt mehrere Abwandlungen. Die Vorgehensweise des Algorithmus wird in dem Teil 2 der Artikelserie Entscheidungsbaum-Algorithmus ID3 erläutert.

1. Schritt: Auswählen des Attributes mit dem höchsten Informationsgewinn

Der Informationsgewinn eines Attributes (A) im Sinne des ID3-Algorithmus ist die Differenz aus der Entropie (E(S)) (siehe Teil 1 der Artikelserie Entropie, ein Maß für die Unreinheit in Daten) des gesamten Datensatzes (S) und der Summe aus den gewichteten Entropien des Attributes für jeden einzelnen Wert (Value i), der im Attribut vorkommt:
IG(S, A) = H(S) - \sum_{i=1}^n \frac{\bigl|S_i\bigl|}{\bigl|S\bigl|} \cdot H(S_i)

1.1 Gesamt-Entropie des Datensatzes berechnen

Erstmal schauen wir uns die Entropie des gesamten Datensatzes an. Die Entropie bezieht sich dabei auf das gewünschte Klassifikationsergebnis, also ist die Zahlung via Rechnung erlaubt oder nicht? Diese Frage wird entweder mit true oder false beantwortet.

H(S) = - \frac{9}{14} \cdot \log_2(\frac{9}{14}) - \frac{5}{14} \cdot \log_2(\frac{5}{14})  = 0.94

1.2 Berechnung der Informationsgewinne aller Attribute

Berechnen wir nun also die Informationsgewinne über alle Spalten.

Attribut Subset Count(true) Count(false)
Kundenart “Neukunde” 2 3
“Stammkunde” 4 0
“Normalkunde” 3 2

Wir zerlegen den gesamten Datensatz gedanklich in drei Kategorien der Kundenart und berechnen die Entropie bezogen auf das Klassifikationsziel:

H(S_{Neukunde}) = - \frac{2}{5} \cdot \log_2(\frac{2}{5}) - \frac{3}{5} \cdot \log_2(\frac{3}{5})  = 0.97

H(S_{Stammkunde}) = - \frac{4}{4} \cdot \log_2(\frac{4}{4}) - \frac{0}{4} \cdot \log_2(\frac{0}{4})  = 0.00

H(S_{Normalkunde}) = - \frac{3}{5} \cdot \log_2(\frac{3}{5}) - \frac{2}{5} \cdot \log_2(\frac{2}{5})  = 0.97

Zur Erinnerung, der Informationsgewinn (Information Gain) wird wie folgt berechnet:

    \[ IG(S, A_{Kundenart}) =  - \sum_{i=1}^n \frac{\bigl|S_i\bigl|}{\bigl|S\bigl|} \cdot H(S_i) \]

Angewendet auf das Attribut “Kundenart”…

    \[ IG(S, A_{Kundenart}) =  H(S) - \frac{\bigl|S_{Neukunde}\bigl|}{\bigl|S\bigl|} \cdot H(S_{Neukunde}) - \frac{\bigl|S_{Stammkunde}\bigl|}{\bigl|S\bigl|} \cdot H(S_{Stammkunde}) - \frac{\bigl|S_{Normalkunde}\bigl|}{\bigl|S\bigl|} \cdot H(S_{Normalkunde}) \]

… erhalten wir der Formal nach folgenden Informationsgewinn:

    \[ IG(S, A_{Kundenart}) =  0.94 - \frac{5}{14} \cdot 0.97 - \frac{4}{14} \cdot 0.00 - \frac{5}{14} \cdot 0.97 = 0.247 \]

Nun für die weiteren Spalten:

Attribut Subset Count(true) Count(false)
Zahlungsgeschwindigkeit “niedrig” 2 2
“mittel” 4 2
“schnell” 3 1

Entropien für die “Zahlungsgeschwindigkeit”:

H(S_{niedrig}) = - \frac{2}{4} \cdot \log_2(\frac{2}{4}) - \frac{2}{4} \cdot \log_2(\frac{2}{4})  = 1.00

H(S_{mittel}) = - \frac{4}{6} \cdot \log_2(\frac{4}{6}) - \frac{2}{6} \cdot \log_2(\frac{2}{6})  = 0.92

H(S_{schnell}) = - \frac{3}{4} \cdot \log_2(\frac{3}{4}) - \frac{1}{4} \cdot \log_2(\frac{1}{4})  = 0.81

So berechnen wir wieder den Informationsgewinn:

    \[ IG(S, A_{Zahlungsgeschwindigkeit}) =  H(S) - \frac{\bigl|S_{niedrig}\bigl|}{\bigl|S\bigl|} \cdot H(S_{niedrig}) - \frac{\bigl|S_{mittel}\bigl|}{\bigl|S\bigl|} \cdot H(S_{mittel}) - \frac{\bigl|S_{schnell}\bigl|}{\bigl|S\bigl|} \cdot H(S_{schnell}) \]

Einsatzen und ausrechnen:

    \[ IG(S, A_{Zahlungsgeschwindigkeit}) =  0.94 - \frac{4}{14} \cdot 1.00 - \frac{6}{14} \cdot 0.92 - \frac{4}{14} \cdot 0.81 = 0.029 \]

Und nun für die Spalte “Kauffrequenz”:

Attribut Subset Count(true) Count(false)
Kauffrequenz “niedrig” 3 4
“hoch” 6 1

Entropien:

H(S_{niedrig}) = - \frac{3}{7} \cdot \log_2(\frac{3}{7}) - \frac{4}{7} \cdot \log_2(\frac{4}{7})  = 0.99

H(S_{hoch}) = - \frac{6}{7} \cdot \log_2(\frac{6}{7}) - \frac{1}{7} \cdot \log_2(\frac{1}{7})  = 0.59

Informationsgewinn:

    \[ IG(S, A_{Kauffrequenz}) =  H(S) - \frac{\bigl|S_{niedrig}\bigl|}{\bigl|S\bigl|} \cdot H(S_{niedrig}) - \frac{\bigl|S_{hoch}\bigl|}{\bigl|S\bigl|} \cdot H(S_{hoch}) \]

Einsetzen und Ausrechnen:

    \[ IG(S, A_{Kauffrequenz}) =  0.94 - \frac{7}{14} \cdot 1.00 - \frac{7}{14} \cdot 0.59 = 0.150 \]

Und last but not least die Spalte “Herkunft”:

Attribut Subset Count(true) Count(false)
Herkunft “Inland” 6 2
“Ausland” 3 3

Entropien:

H(S_{Inland}) = - \frac{6}{8} \cdot \log_2(\frac{6}{8}) - \frac{2}{8} \cdot \log_2(\frac{2}{8})  = 0.81

H(S_{Ausland}) = - \frac{3}{6} \cdot \log_2(\frac{3}{6}) - \frac{3}{6} \cdot \log_2(\frac{3}{6})  = 1.00

Informationsgewinn:

    \[ IG(S, A_{Herkunft}) =  H(S) - \frac{\bigl|S_{Inland}\bigl|}{\bigl|S\bigl|} \cdot H(S_{Inland}) - \frac{\bigl|S_{Ausland}\bigl|}{\bigl|S\bigl|} \cdot H(S_{Ausland}) \]

Einsetzen und Ausrechnen:

    \[ IG(S, A_{Herkunft}) =  0.94 - \frac{8}{14} \cdot 0.81 - \frac{6}{14} \cdot 1.00 = 0.05 \]

2. Schritt: Anlegen des Wurzel-Knotens

Der Informationsgewinn ist für das Attribut “Kundenart” am größten, daher entscheiden wir uns im Sinne des ID3-Algorithmus für dieses Attribut als Wurzel-Knoten.

3. Schritt: Rekursive Wiederholung (!!!)

Nun stellt sich natürlich die Frage: Wie geht es weiter?

Der Algorithmus kann eigentlich nur eines: Einen Wurzelknoten finden. Diesen Vorgang müssen wir nun nur noch rekursiv wiederholen, und das tun wir wie folgt.

Der Datensatz wurde bereits aufgeteilt in die drei Kundenarten. Für jede Kundenart ergibt sich jeweils ein Subset mit den verbleibenden Attributen. Für alle drei Subsets erstellen wir dann wieder einen Wurzelknoten, so dass ein neuer Ast entsteht.

3.1 Erster Rekursionsschritt

Machen wir also weiter und bestimmen wir das nächste Attribut nach der Kundenart, für die Fälle Kundenart = “Neukunde”:

Zeile Kundenart Zahlungsgeschwindigkeit Kauffrequenz Herkunft Zahlungsmittel: Rechnung?
 1  Neukunde  niedrig  niedrig  Inland  false
 2  Neukunde  niedrig  niedrig  Ausland  false
 8  Neukunde  mittel  niedrig  Inland  false
 9  Neukunde  hoch  hoch  Inland  true
 11  Neukunde  mittel  hoch  Ausland  true

Die Entropie des Gesamtdatensatzes (ja, es ist für diesen Schritt betrachtet der gesamte Datensatz!) ist wie folgt:

H(S_{Neukunde}) = - \frac{2}{5} \cdot \log_2(\frac{2}{5}) - \frac{3}{5} \cdot \log_2(\frac{3}{5})  = 0.97

Die Entropie ist weit weg von einer bestimmten Wahrscheinlichkeit (nahe der Gleichverteilung). Daher müssen wir hier nochmal ansetzen und losrechnen:

Entropien für “Zahlungsgeschwindigkeit” bei Neukunden:

H(S_{niedrig}) = 0.00

H(S_{mittel}) = 1.00

H(S_{hoch}) = 0.00

Informationsgewinn des Attributes “Zahlungsgeschwindigkeit” bei Neukunden:

    \[ IG(S_{Neukunde},A_{Zahlungsgeschwindigkeit}) = 0.97 - \frac{3}{5} \cdot 0.00 - \frac{2}{5} \cdot 1.00 -  \frac{1}{5} \cdot 0.00 = 0.57 \]

Betrachtung der Spalte “Kauffrequenz” bei Neukunden:

Entropien für “Kauffrequenz” bei Neukunden:

H(S_{niedrig}) = 0.00

H(S_{hoch}) = 0.00

Informationsgewinn des Attributes “Kauffrequenz” bei Neukunden:

    \[ IG(S_{Neukunde},A_{Kauffrequenz}) = 0.97 - \frac{3}{5} \cdot 0.00 - \frac{2}{5} \cdot 0.00 = 0.97 \]

Betrachtung der Spalte “Herkunft” bei Neukunden:

Entropien für “Herkunft” bei Neukunden:

H(S_{Inland}) = 0.92

H(S_{hoch}) = 1.00

Informationsgewinn des Attributes “Herkunft” bei Neukunden:

    \[ IG(S_{Neukunde},A_{Herkunft}) = 0.97 - \frac{3}{5} \cdot 0.92 - \frac{2}{5} \cdot 1.00 = 0.018 \]

Wir entscheiden uns also für das Attribut “Kauffrequenz” als Ast nach der Entscheidung “Neukunde”, denn dieses Attribut bring uns den größten Informationsgewinn und trennt uns die Unterscheidung für oder gegen das Zahlungsmittel “Rechnung” eindeutig auf.

3.1 Zweiter Rekursionsschritt

Was passiert mit der Kundenart “Stammkunde”?

Zeile Kundenart Zahlungsgeschwindigkeit Kauffrequenz Herkunft Zahlungsmittel: Rechnung?
 3  Stammkunde  niedrig  niedrig  Inland  true
 7  Stammkunde  hoch  hoch  Ausland  true
 12  Stammkunde  mittel  niedrig  Ausland  true
 13  Stammkunde  niedrig  hoch  Inland  true

Die Antwort ist einfach: Nichts!
Wer ein Stammkunde ist, dem wurde stets die Zahlung per Rechnung erlaubt.

H(S_{Stammkunde}) = 0.0

3.1 Dritter Rekursionsschritt

Fehlt nun nur noch die Frage nach der Unterscheidung von Normalkunden.

Zeile Kundenart Zahlungsgeschwindigkeit Kauffrequenz Herkunft Zahlungsmittel: Rechnung?
 4  Normalkunde  mittel  niedrig  Inland  true
 5  Normalkunde  hoch  hoch  Inland  true
 6  Normalkunde  hoch  hoch  Ausland  false
 14  Normalkunde  mittel  niedrig  Ausland  false

Zwar ist die Entropie des Subsets der Normalkunden…

H(S_{Normalkunde}) = 1.0

… denkbar schlecht, da maximal. Aber wir können genauso vorgehen, wie wir es bei dem Subset der Neukunden getan haben. Ich nehme es nun aber vorweg: Wenn wir uns den Datensatz näher ansehen, erkennen wir, dass wir diese Gesamtentropie von 1.0 für das Subset “Normalkunde” nicht mit den Attributen “Kauffrequenz” oder “Zahlungsgeschwindigkeit” reduzieren können, da dieses auch für sich betrachtet in Entropien der Größe 1.0 erhalten werden. Das Attribut “Herkunft” hingegen teilt den Datensatz sauber in true und false auf:

Somit ist der Informationsgewinn für das Attribut “Herkunft” am größten und wir haben unseren Baum komplett und – glücklicherweise – eindeutig bestimmen können!

Ergebnis: Der Entscheidungsbaum

Somit haben wir den Entscheidungsbaum über den ID3-Algorithmus erstellt, der eine Auskunft darüber macht, ob einem Kunden die Zahlung über Rechnung (statt Vorkasse) erlaubt wird:

true = Rechnung als Zahlungsmittel erlaubt
false = Rechnung als Zahlungsmittel nicht erlaubt

Lineare Regression in Python mit Scitkit-Learn

Die lineare Regressionsanalyse ist ein häufiger Einstieg ins maschinelle Lernen um stetige Werte vorherzusagen (Prediction bzw. Prädiktion). Hinter der Regression steht oftmals die Methode der kleinsten Fehlerquadrate und die hat mehr als eine mathematische Methode zur Lösungsfindung (Gradientenverfahren und Normalengleichung). Alternativ kann auch die Maximum Likelihood-Methode zur Regression verwendet werden. Wir wollen uns in diesem Artikel nicht auf die Mathematik konzentrieren, sondern uns direkt an die Anwendung mit Python Scikit-Learn machen:

Haupt-Lernziele:

  • Einführung in Machine Learning mit Scikit-Learn
  • Lineare Regression mit Scikit-Learn

Neben-Lernziele:

  • Datenvorbereitung (Data Preparation) mit Pandas und Scikit-Learn
  • Datenvisualisierung mit der Matplotlib direkt und indirekt (über Pandas)

Was wir inhaltlich tun:

Der Versuch einer Vorhersage eines Fahrzeugpreises auf Basis einer quantitativ-messbaren Eigenschaft eines Fahrzeuges.


Die Daten als Download

Für dieses Beispiel verwende ich die Datei “Automobil_data.txt” von Kaggle.com. Die Daten lassen sich über folgenden Link downloaden, nur leider wird ein (kostenloser) Account benötigt:
https://www.kaggle.com/toramky/automobile-dataset/downloads/automobile-dataset.zip
Sollte der Download-Link unerwartet mal nicht mehr funktionieren, freue ich mich über einen Hinweis als Kommentar 🙂

Die Entwicklungsumgebung

Ich verwende hier die Python-Distribution Anaconda 3 und als Entwicklungs-Umgebung Spyder (in Anaconda enthalten). Genauso gut funktionieren jedoch auch Jupyter Notebook, Eclipse mit PyDev oder direkt die IPython QT-Console.


Zuerst einmal müssen wir die Daten in unsere Python-Session laden und werden einige Transformationen durchführen müssen. Wir starten zunächst mit dem Importieren von drei Bibliotheken NumPy und Pandas, deren Bedeutung ich nicht weiter erläutern werde, somit voraussetze.

import matplotlib.pyplot as plt  # Die Nr.1 der Bibliotheken zur Datenvisualisierung
import numpy as np               # Bibliothek "Nummerisches Python"
import pandas as pd              # Bibliothek "Panel Data"

Wir nutzen die Pandas-Bibliothek, um die “Automobile_data.txt” in ein pd.DataFrame zu laden.

dataSet = pd.read_csv("Automobile_data.txt",  # Hier liegt die Datei im selben Verzeichnis wie das Python-Skript!
                      delimiter = ',',
                      thousands = None,
                      decimal = '.')

Schauen wir uns dann die ersten fünf Zeilen in IPython via dataSet.head().

In : dataSet.head()
Out: 
   symboling normalized-losses         make fuel-type aspiration num-of-doors  \
0          3                 ?  alfa-romero       gas        std          two   
1          3                 ?  alfa-romero       gas        std          two   
2          1                 ?  alfa-romero       gas        std          two   
3          2               164         audi       gas        std         four   
4          2               164         audi       gas        std         four   

    body-style drive-wheels engine-location  wheel-base  ...    engine-size  \
0  convertible          rwd           front        88.6  ...            130   
1  convertible          rwd           front        88.6  ...            130   
2    hatchback          rwd           front        94.5  ...            152   
3        sedan          fwd           front        99.8  ...            109   
4        sedan          4wd           front        99.4  ...            136   

   fuel-system  bore  stroke compression-ratio horsepower  peak-rpm city-mpg  \
0         mpfi  3.47    2.68               9.0        111      5000       21   
1         mpfi  3.47    2.68               9.0        111      5000       21   
2         mpfi  2.68    3.47               9.0        154      5000       19   
3         mpfi  3.19     3.4              10.0        102      5500       24   
4         mpfi  3.19     3.4               8.0        115      5500       18   

  highway-mpg  price  
0          27  13495  
1          27  16500  
2          26  16500  
3          30  13950  
4          22  17450  

[5 rows x 26 columns]

Hinweis: Der Datensatz hat viele Spalten, so dass diese in der Darstellung mit einem Backslash \ umgebrochen werden.

Gleich noch eine weitere Ausgabe dataSet.info(), die uns etwas über die Beschaffenheit der importierten Daten verrät:

In : dataSet.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 205 entries, 0 to 204
Data columns (total 26 columns):
symboling            205 non-null int64
normalized-losses    205 non-null object
make                 205 non-null object
fuel-type            205 non-null object
aspiration           205 non-null object
num-of-doors         205 non-null object
body-style           205 non-null object
drive-wheels         205 non-null object
engine-location      205 non-null object
wheel-base           205 non-null float64
length               205 non-null float64
width                205 non-null float64
height               205 non-null float64
curb-weight          205 non-null int64
engine-type          205 non-null object
num-of-cylinders     205 non-null object
engine-size          205 non-null int64
fuel-system          205 non-null object
bore                 205 non-null object
stroke               205 non-null object
compression-ratio    205 non-null float64
horsepower           205 non-null object
peak-rpm             205 non-null object
city-mpg             205 non-null int64
highway-mpg          205 non-null int64
price                205 non-null object
dtypes: float64(5), int64(5), object(16)
memory usage: 41.7+ KB

Einige Spalten entsprechen hinsichtlich des Datentypes nicht der Erwartung. Für die Spalten ‘horsepower’ und ‘peak-rpm’ würde ich eine Ganzzahl (Integer) erwarten, für ‘price’ hingegen eine Fließkommazahl (Float), allerdings sind die drei Spalten als Object deklariert. Mit Trick 17 im Data Science, der Anzeige der Minimum- und Maximum-Werte einer zu untersuchenden Datenreihe, kommen wir dem Übeltäter schnell auf die Schliche:

dataSet['horsepower'].min()
Out: '100'

dataSet['horsepower'].max()
Out: '?'

Datenbereinigung

Für eine Regressionsanalyse benötigen wir nummerische Werte (intervall- oder ratioskaliert), diese möchten wir auch durch richtige Datentypen-Deklaration herstellen. Nun wird eine Konvertierung in den gewünschten Datentyp jedoch an den (mit ‘?’ aufgefüllten) Datenlücken scheitern.

Schauen wir uns doch einmal die Datenreihen an, in denen in der Spalte ‘peak-rpm’ Fragezeichen stehen:

dataSet[dataSet['peak-rpm'] == '?'][['engine-type', 'num-of-cylinders']]
Out: 
    engine-type num-of-cylinders
130         ohc             four
131         ohc             four

Zwei Datenreihen sind vorhanden, bei denen ‘peak-rpm’ mit einem ‘?’ aufgefüllt wurde. Nun könnten wir diese Datenreihen einfach rauslöschen. Oder mit sinnvollen (im Sinne von wahrscheinlichen) Werten auffüllen. Vermutlichen haben beide Einträge – beide sind OHC-Motoren mit 4 Zylindern – eine ähnliche Drehzahl-Angabe wie vergleichbare Motoren. Mit folgendem Quellcode, gruppieren wir die Spalten ‘engine-type’ und ‘num-of-cylinders’ und bilden für diese Klassen den arithmetischen Mittelwert (.mean()) für die ‘peak-rpm’.

dataSet_rpm = dataSet[dataSet['peak-rpm'] != '?'][['engine-type', 'num-of-cylinders','peak-rpm']]
dataSet_rpm['peak-rpm'] = dataSet_rpm['peak-rpm'].astype(float)
dataSet_rpm_grouped = dataSet_rpm.groupby(['engine-type', 'num-of-cylinders'])
dataSet_rpm_grouped['peak-rpm'].mean()

Und schauen wir uns das Ergebnis an:

dataSet_rpm_grouped['peak-rpm'].mean()
Out: 
engine-type  num-of-cylinders
dohc         four                5700.000000 -- 
             six                 5050.000000
dohcv        eight               5750.000000
l            four                4668.181818
             three               5100.000000
ohc          five                5081.818182
             four                5155.468750
             six                 4821.428571
ohcf         four                4775.000000
             six                 5900.000000
ohcv         eight               4625.000000
             six                 5212.500000
             twelve              5000.000000
rotor        two                 6000.000000
Name: peak-rpm, dtype: float64

Ein Vier-Zylinder-OHC-Motor hat demnach durchschnittlich einen Drehzahl-Peak von 5155 Umdrehungen pro Minute. Ohne nun (fahrlässigerweise) auf die Verteilung in dieser Klasse zu achten, nehmen wir einfach diesen Schätzwert, um die zwei fehlende Datenpunkte zu ersetzen.

Wir möchten jedoch die Original-Daten erhalten und legen ein neues DataSet (dataSet_c) an, in welches wir die Korrekturen vornehmen:

dataSet_c = dataSet.copy()   # das "c"-Anhängsel steht für "corrected"

Nun können wir die fehlenden Peak-RPM-Einträge mit unserem Schätzwert ersetzen:

dataSet_c.loc[dataSet_c['peak-rpm'] == '?', 'peak-rpm'] = 5155

Was bei einer Drehzahl-Angabe noch funktionieren mag, ist für anderen Spalten bereits etwas schwieriger: Die beiden Spalten ‘price’ und ‘horsepower’ sind ebenfalls vom Typ Object, da sie ‘?’ enthalten. Verzichten wir einfach auf die betroffenen Zeilen:

dataSet_c = dataSet_c[dataSet_c['price'] != '?']                    # entsprechende Zeilen herausfiltern
dataSet_c['price'] = dataSet_c['price'].astype(float)               # Typ-Konvertierung zu Float

dataSet_c = dataSet_c[dataSet_c.horsepower != '?']                  # entsprechende Zeilen herausfiltern
dataSet_c['horsepower'] = dataSet_c['horsepower'].astype(float)     # Typ-Konvertierung in Int

Datenvisualisierung mit Pandas

Wir wollen uns nicht lange vom eigentlichen Ziel ablenken, dennoch nutzen wir die Visualisierungsfähigkeiten der Pandas-Library (welche die Matplotlib inkludiert), um uns dann die Anzahlen an Einträgen nach Hersteller der Fahrzeuge (Spalte ‘make’) anzeigen zu lassen:

dataSet_grouped_make = dataSet_c.groupby('make')
dataSet_grouped_make['make'].count().plot(kind = 'bar', figsize = (10, 10))
plt.show()    # Besser jedes Plot abschließen! Auch wenn es in Pandas entstanden ist.

Oder die durchschnittliche PS-Zahl nach Hersteller:

(dataSet_c.groupby('make'))['horsepower'].mean().plot(kind = 'barh',
                                                      title = 'Mean Horsepower',
                                                      figsize = (10, 10))
plt.show()

Vorbereitung der Regressionsanalyse

Nun kommen wir endlich zur Regressionsanalyse, die wir mit Scikit-Learn umsetzen möchten. Die Regressionsanalyse können wir nur mit intervall- oder ratioskalierten Datenspalten betreiben, daher beschränken wir uns auf diese. Die “price”-Spalte nehmen wir jedoch heraus und setzen sie als unsere Zielgröße fest.

""" ----- Vorbereitung für die Regressionsanalyse ----- """
cols_ratio = ['horsepower', 'wheel-base', 'length', 'width', 'height', 'curb-weight', 'engine-size', 'compression-ratio', 'city-mpg', 'highway-mpg']
cols_target = ['price']

dataSet_ratio = dataSet_c.loc[:, cols_ratio]
dataSet_target = dataSet_c[cols_target]

Interessant ist zudem die Betrachtung vorab, wie die einzelnen nummerischen Attribute untereinander korrelieren. Dafür nehmen wir auch die ‘price’-Spalte wieder in die Betrachtung hinein und hinterlegen auch eine Farbskala mit dem Preis (höhere Preise, hellere Farben).

grr = pd.plotting.scatter_matrix(dataSet_c[cols_target + cols_ratio]
                                 ,c = dataSet_target
                                 ,figsize=(15, 15)
                                 ,marker = 'o'
                                 ,hist_kwds={'bins' : 20}
                                 ,s = 60
                                 ,alpha = 0.8)
plt.show()

Die lineare Korrelation ist hier sehr interessant, da wir auch nur eine lineare Regression beabsichtigen.

Wie man in dieser Scatter-Matrix recht gut erkennen kann, scheinen einige Größen-Paare nahezu perfekt zu korrelieren, andere nicht.

Korrelation…

  • …nahezu perfekt linear: highway-mpg vs city-mpg (mpg = Miles per Gallon)
  • … eher nicht gegeben: highway-mpg vs height
  • … nicht linear, dafür aber nicht-linear: highway-mpg vs price

Nun, wir wollen den Preis eines Fahrzeuges vorhersagen, wenn wir eine andere quantitative Größe gegeben haben. Auf den Preis bezogen, erscheint mir die Motorleistung (Horsepower) einigermaßen linear zu korrelieren. Versuchen wir hier die lineare Regression und setzen somit die Spalte ‘horsepower’ als X und ‘price’ als y fest.

X = dataSet_ratio[['horsepower']] # doppelte [], da eine Liste von Spalten zu übergeben ist
y = dataSet_c[cols_target]

Die gängige Konvention ist übrigens, X groß zu schreiben, weil hier auch mehrere x-Dimensionen enthalten sein dürfen (multivariate Regression). y hingegen, ist stets nur eine Zielgröße (eine Dimension).

Die lineare Regression ist ein überwachtes Verfahren des maschinellen Lernens, somit müssen wir unsere Prädiktionsergebnisse mit Test-Daten testen, die nicht für das Training verwendet werden dürfen. Scitkit-Learn (oder kurz: sklearn) bietet hierfür eine Funktion an, die uns das Aufteilen der Daten abnimmt:

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y,
                                                    test_size = 0.3,     # 70% der Daten für das Training
                                                    random_state = None) # bei Bedarf kann hier "dem Zufall auf die Sprünge geholfen" werden

Zu beachten ist dabei, dass die Daten vor dem Aufteilen in Trainings- und Testdaten gut zu durchmischen sind. Auch dies übernimmt die train_test_split-Funktion für uns, nur sollte man im Hinterkopf behalten, dass die Ergebnisse (auf Grund der Zufallsauswahl) nach jedem Durchlauf immer wieder etwas anders aussehen.

Lineare Regression mit Scikit-Learn

Nun kommen wir zur Durchführung der linearen Regression mit Scitkit-Learn, die sich in drei Zeilen trainieren lässt:

""" ----- Lineare Regressionsanalyse ------- """

from sklearn.linear_model import LinearRegression   # importieren der Klasse

lr = LinearRegression()                             # instanziieren der Klasse

lr.fit(X_train, y_train)                            # trainieren

Aber Vorsicht! Bevor wir eine Prädiktion durchführen, wollen wir festlegen, wie wir die Güte der Prädiktion bewerten wollen. Die gängigsten Messungen für eine lineare Regression sind der MSE und R².

MSE = \frac{\sum_{i=1}^n (y_i - \hat{y_i})^2}{n}

Ein großer MSE ist schlecht, ein kleiner gut.

R^2 = 1 - \frac{MSE}{Var(y)}= \frac{\frac{1}{n} \cdot \sum_{i=1}^n (y_i - \hat{y_i})^2}{\frac{1}{n} \cdot \sum_{i=1}^n (y_i - \hat{\mu_y})^2}

Ein kleines R² ist schlecht, ein großes R² gut. Ein R² = 1.0 wäre theoretisch perfekt (da der Fehler = 0.00 wäre), jedoch in der Praxis unmöglich, da dieser nur bei absolut perfekter Korrelation auftreten würde. Die Klasse LinearRegression hat eine R²-Messmethode implementiert (score(x, y)).

print('------ Lineare Regression -----')
print('Funktion via sklearn: y = %.3f * x + %.3f' % (lr.coef_[0], lr.intercept_))
print("Alpha: {}".format(lr.intercept_))
print("Beta: {}".format(lr.coef_[0]))
print("Training Set R² Score: {:.2f}".format(lr.score(X_train, y_train)))
print("Test Set R² Score: {:.2f}".format(lr.score(X_test, y_test)))
print("\n")

Die Ausgabe (ein Beispiel!):

------ Lineare Regression -----
Funktion via sklearn: y = 170.919 * x + -4254.701     # Die Funktion ist als y = 171 * x - 4254.7
Alpha: [-4254.70114803]                               # y-Achsenschnitt bei x = 0
Beta: [ 170.91919086]                                 # Steigung der Gerade
Training Set R² Score: 0.62                           
Test Set R² Score: 0.73

Nach jedem Durchlauf ändert sich mit der Datenaufteilung (train_test_split()) das Modell etwas und auch R² schwankt um eine gewisse Bandbreite. Berauschend sind die Ergebnisse dabei nicht, und wenn wir uns die Regressionsgerade einmal ansehen, wird auch klar, warum:

plt.figure(figsize=(10,10))
plt.scatter(X_train, y_train, color = 'blue')                 # Blaue Punkte sind Trainingsdaten
plt.scatter(X_test, y_test, color = 'green')                  # Grüne Punkte sind Testdaten
plt.plot(X_train, lr.predict(X_train), color = 'red')         # Hier ensteht die Gerade (x, y) = (x, lr.predict(x)
plt.xlabel(X_train.columns[0])
plt.ylabel(cols_target[0])
plt.show()

Bei kleineren Leistungsbereichen, etwa bis 100 PS, ist die Preis-Varianz noch annehmbar gering, doch bei höheren Leistungsbereichen ist die Spannweite deutlich größer. (Nachträgliche Anmerkung vom 06.05.2018: relativ betrachtet, bleibt der Fehler über alle Wertebereiche ungefähr gleich [relativer Fehler]. Die absoluten Fehlerwerte haben jedoch bei größeren x-Werten so eine Varianz der möglichen y-Werte, dass keine befriedigenden Prädiktionen zu erwarten sind.)

Egal wie wir eine Gerade in diese Punktwolke legen, wir werden keine befriedigende Fehlergröße erhalten.

Nehmen wir einmal eine andere Spalte für X, bei der wir vor allem eine nicht-lineare Korrelation erkannt haben: “highway-mpg”

X = dataSet_ratio[['highway-mpg']]
y = dataSet_c[cols_target]

Wenn wir dann das Training wiederholen:

------ Lineare Regression -----
Funktion via sklearn: y = -868.787 * x + 40575.036
Alpha: [ 40575.03556055]
Beta: [-868.7869183]
Training Set R² Score: 0.49
Test Set R² Score: 0.40

Die R²-Werte sind nicht gerade berauschend, und das erklärt sich auch leicht, wenn wir die Trainings- und Testdaten sowie die gelernte Funktionsgerade visualisieren:

Die Gerade lässt sich nicht wirklich gut durch diese Punktwolke legen, da letztere eher eine Kurve als eine Gerade bildet. Im Grunde könnte eine Gerade noch einigermaßen gut in den Bereich von 22 bis 43 mpg passen und vermutlich annehmbare Ergebnisse liefern. Die Wertebereiche darunter und darüber jedoch verzerren zu sehr und sorgen zudem dafür, dass die Gerade auch innerhalb des mittleren Bereiches zu weit nach oben verschoben ist (ggf. könnte hier eine Ridge-/Lasso-Regression helfen).

Richtig gute Vorhersagen über nicht-lineare Verhältnisse können jedoch nur mit einer nicht-linearen Regression erreicht werden.

Nicht-lineare Regression mit Scikit-Learn

Nicht-lineare Regressionsanalysen erlauben es uns, nicht-lineare korrelierende Werte-Paare als Funktion zu erlernen. Im folgenden Scatter-Plot sehen wir zum einen die gewohnte lineare Regressionsgerade (y = a * x + b) in rot, eine polinominale Regressionskurve dritten Grades (y = a * x³ + b * x² + c * x + d) in violet sowie einen Entscheidungsweg einer Entscheidungsbaum-Regression in gelb.

Nicht-lineare Regressionsanalysen passen sich dem Verlauf der Punktwolke sehr viel besser an und können somit in der Regel auch sehr gute Vorhersageergebnisse liefern. Ich ziehe hier nun jedoch einen Gedankenstrich, liefere aber den Quellcode für die lineare Regression als auch für die beiden nicht-linearen Regressionen mit:

Python Script Regression via Scikit-Learn

Weitere Anmerkungen

  • Bibliotheken wie Scitkit-Learn erlauben es, machinelle Lernverfahren schnell und unkompliziert anwenden zu können. Allerdings sollte man auch verstehen, wei diese Verfahren im Hintergrund mathematisch arbeiten. Diese Bibliotheken befreien uns also nicht gänzlich von der grauen Theorie.
  • Statt der “reinen” lineare Regression (LinearRegression()) können auch eine Ridge-Regression (Ridge()), Lasso-Regression (Lasso()) oder eine Kombination aus beiden als sogenannte ElasticNet-Regression (ElasticNet()). Bei diesen kann über Parametern gesteuert werden, wie stark Ausreißer in den Daten berücksichtigt werden sollen.
  • Vor einer Regression sollten die Werte skaliert werden, idealerweise durch Standardisierung der Werte (sklearn.preprocessing.StandardScaler()) oder durch Normierung (sklearn.preprocessing.Normalizer()).
  • Wir haben hier nur zwei-dimensional betrachtet. In der Praxis ist das jedoch selten ausreichend, auch der Fahrzeug-Preis ist weder von der Motor-Leistung, noch von dem Kraftstoffverbrauch alleine abhängig – Es nehmen viele Größen auf den Preis Einfluss, somit benötigen wir multivariate Regressionsanalysen.

Unsupervised Learning in R: K-Means Clustering

Die Clusteranalyse ist ein gruppenbildendes Verfahren, mit dem Objekte Gruppen – sogenannten Clustern zuordnet werden. Die dem Cluster zugeordneten Objekte sollen möglichst homogen sein, wohingegen die Objekte, die unterschiedlichen Clustern zugeordnet werden möglichst heterogen sein sollen. Dieses Verfahren wird z.B. im Marketing bei der Zielgruppensegmentierung, um Angebote entsprechend anzupassen oder im User Experience Bereich zur Identifikation sog. Personas.

Es gibt in der Praxis eine Vielzahl von Cluster-Verfahren, eine der bekanntesten und gebräuchlichsten Verfahren ist das K-Means Clustering, ein sog. Partitionierendes Clusterverfahren. Das Ziel dabei ist es, den Datensatz in K Cluster zu unterteilen. Dabei werden zunächst K beliebige Punkte als Anfangszentren (sog. Zentroiden) ausgewählt und jedem dieser Punkte der Punkt zugeordnet, zu dessen Zentrum er die geringste Distanz hat. K-Means ist ein „harter“ Clusteralgorithmus, d.h. jede Beobachtung wird genau einem Cluster zugeordnet. Zur Berechnung existieren verschiedene Distanzmaße. Das gebräuchlichste Distanzmaß ist die quadrierte euklidische Distanz:

D^2 = \sum_{i=1}^{v}(x_i - y_i)^2

Nachdem jede Beobachtung einem Cluster zugeordnet wurde, wird das Clusterzentrum neu berechnet und die Punkte werden den neuen Clusterzentren erneut zugeordnet. Dieser Vorgang wird so lange durchgeführt bis die Clusterzentren stabil sind oder eine vorher bestimmte Anzahl an Iterationen durchlaufen sind.
Das komplette Vorgehen wird im Folgenden anhand eines künstlich erzeugten Testdatensatzes erläutert.

set.seed(123)
Alter <- c(24, 22, 28, 25, 41, 39, 35, 40, 62, 57, 60, 55)
Einkommen <- c(20000, 22000, 25000, 24000, 55000, 65000, 75000, 60000, 30000, 34000, 30000, 34000)
Daten <- as.data.frame(cbind(Alter, Einkommen))

Zunächst wird ein Testdatensatz mit den Variablen „Alter“ und „Einkommen“ erzeugt, der 12 Fälle enthält. Als Schritt des „Data preprocessing“ müssen zunächst beide Variablen standardisiert werden, da ansonsten die Variable „Alter“ die Clusterbildung zu stark beeinflusst.

DatenAlter <- scale(DatenAlter)
DatenEinkommen <- scale(DatenEinkommen)

Das Ganze geplottet:

plot(DatenAlter, DatenEinkommen, col = "blue", pch = 19,
     xlab = "Alter (scaled)",
     ylab = "Einkommen (scaled)",
     main = "Alter vs. Einkommen (scaled)")

Wie bereits eingangs erwähnt müssen Cluster innerhalb möglichst homogen und zu Objekten anderer Cluster möglichst heterogen sein. Ein Maß für die Homogenität die „Within Cluster Sums of Squares“ (WSS), ein Maß für die Heterogenität „Between Cluster Sums of Squares“ (BSS).

Diese sind beispielsweise für eine 3-Cluster-Lösung wie folgt:

KmeansObj <- kmeans(Daten, 3, nstart = 20)
KmeansObjwithinss # Within Cluster Sums of Squares (WSS) KmeansObjtotss # Between Cluster Sums of Suqares (BSS)

> KmeansObjwithinss # Within Cluster Sums of Squares (WSS) [1] 0.7056937 0.1281607 0.1792432 > KmeansObjtotss # Between Cluster Sums of Suqares (BSS)
[1] 22

Sollte man die Anzahl der Cluster nicht bereits kennen oder sind diese extern nicht vorgegeben, dann bietet es sich an, anhand des Verhältnisses von WSS und BSS die „optimale“ Clusteranzahl zu berechnen. Dafür wird zunächst ein leerer Vektor initialisiert, dessen Werte nachfolgend über die Schleife mit dem Verhältnis von WSS und WSS gefüllt werden. Dies lässt sich anschließend per „Screeplot“ visualisieren.

ratio <- vector()
for (k in 1:6) {
    KMeansObj <- kmeans(Daten, k, nstart = 20)
    ratio[k] <- KMeansObjtot.withinss / KMeansObjtotss
}
plot(ratio, type = "b",
     xlab = "Anzahl der Cluster",
     ylab = "Ratio WSS/BSS",
     main = "Screeplot für verschiedene Clusterlösungen",
col = "blue",  pch = 19)

Die „optimale“ Anzahl der Cluster zählt sich am Knick der Linie ablesen (auch Ellbow-Kriterium genannt). Alternativ kann man sich an dem Richtwert von 0.2 orientieren. Unterschreitet das Verhältnis von WSS und BSS diesen Wert, so hat man die beste Lösung gefunden. In diesem Beispiel ist sehr deutlich, dass eine 3-Cluster-Lösung am besten ist.

KmeansObj <- kmeans(Daten, centers = 3, nstart = 20)
plot(DatenAlter, DatenEinkommen, col = KmeansObjcluster, pch = 19, cex = 4,      xlab = "Alter (scaled)",      ylab = "Einkommen (scaled)",      main = "3-Cluster-Lösung") points(KmeansObjcenters, col = 1:3, pch = 3, cex = 5, lwd = 5)

Fazit: Mit K-Means Clustering lassen sich schnell und einfach Muster in Datensätzen erkennen, die, gerade wenn mehr als zwei Variablen geclustert werden, sonst verborgen blieben. K-Means ist allerdings anfällig gegenüber Ausreißern, da Ausreißer gerne als separate Cluster betrachtet werden. Ebenfalls problematisch sind Cluster, deren Struktur nicht kugelförmig ist. Dies ist vor der Durchführung der Clusteranalyse mittels explorativer Datenanalyse zu überprüfen.

R als Tool im Process Mining

Die Open Source Sprache R ermöglicht eine Vielzahl von Analysemöglichkeiten, die von einer einfachen beschreibenden Darstellung eines Prozesses bis zur umfassenden statistischen Analyse reicht. Dabei können Daten aus einem Manufacturing Execution System, kurz MES, als Basis der Prozessanalyse herangezogen werden. R ist ein Open Source Programm, welches sich für die Lösung von statischen Aufgaben im Bereich der Prozessoptimierung sehr gut eignet, erfordert jedoch auf Grund des Bedienungskonzepts als Scriptsprache, grundlegende Kenntnisse der Programmierung. Aber auch eine interaktive Bedienung lässt sich mit einer Einbindung der Statistikfunktionen in ein Dashboard erreichen. Damit können entsprechend den Anforderungen, automatisierte Analysen ohne Programmierkenntnisse realisiert werden.

Der Prozess als Spagetti Diagramm

Um einen Überblick zu erhalten, wird der Prozess in einem „process value flowchart“, ähnlich einem Spagetti‐ Diagramm dargestellt und je nach Anforderung mit Angaben zu den Key Performance Indicators ergänzt. Im konkreten Fall werden die absolute Anzahl und der relative Anteil der bearbeiteten Teile angegeben. Werden Teile wie nachfolgend dargestellt, aufgrund von festgestellten Mängel bei der Qualitätskontrolle automatisiert ausgeschleust, können darüber Kennzahlen für den Ausschuss ermittelt werden.

Der Prozess in Tabellen und Diagrammen

Im folgenden Chart sind grundlegende Angaben zu den ausgeführten Prozessschritten, sowie deren Varianten dargestellt. Die Statistikansicht bietet eine Übersicht zu den Fällen, den sogenannte „Cases“, sowie zur Dauer und Taktzeit der einzelnen Aktivitäten. Dabei handelt es sich um eine Fertigungsline mit hohem Automatisierungsgrad, bei der jeder Fertigungsschritt im MES dokumentiert wird. Die Tabelle enthält statistische Angaben zur Zykluszeit, sowie der Prozessdauer zu den einzelnen Aktivitäten. In diesem Fall waren keine Timestamps für das Ende der Aktivität vorhanden, somit konnte die Prozessdauer nicht berechnet werden.

Die Anwendung von Six Sigma Tools

R verfügt über eine umfangreiche Sammlung von Bibliotheken zur Datendarstellung, sowie der Prozessanalyse. Darin sind auch Tools aus Six Sigma enthalten, die für die weitere Analyse der Prozesse eingesetzt werden können. In den folgenden Darstellungen wird die Möglichkeit aufgezeigt, zwei Produktionszeiträume, welche über eine einfache Datumseingabe im Dashboard abgegrenzt werden, gegenüber zu stellen. Dabei handelt es sich um die Ausbringung der Fertigung in Stundenwerten, die für jeden Prozessschritt errechnet wird. Das xbar und r Chart findet im Bereich der Qualitätssicherung häufig Anwendung zur ersten Beurteilung des Prozessoutputs.

Zwei weitere Six Sigma typische Kennzahlen zur Beurteilung der Prozessfähigkeit sind der Cp und Cpk Wert und deren Ermittlung ein Bestandteil der R Bibliotheken ist. Bei der Berechnung wird von einer Normalverteilung der Daten ausgegangen, wobei das Ergebnis aus der Überprüfung dieser Annahme im Chart durch Zahlen, als auch grafisch dargestellt wird.

Von Interesse ist auch die Antwort auf die Frage, welchem Trend folgt der Prozess? Bereits aus der Darstellung der beiden Produktionszeiträume im Box‐Whiskers‐Plot könnte man anhand der Mediane auf einen Trend zu einer Verschlechterung der Ausbringung schließen, den der Interquartilsabstand nicht widerspiegelt. Eine weitere Absicherung einer Aussage über den Trend, kann über einen statistischen Vergleichs der Mittelwerte erfolgen.

Der Modellvergleich

Besteht die Anforderung einer direkten Gegenüberstellung des geplanten, mit dem vorgefundenen, sogenannten „Discovered Model“, ist aufgrund der Komplexität beim Modellvergleich, dieser in R mit hohem Programmieraufwand verbunden. Besser geeignet sind dafür spezielle Process Miningtools. Diese ermöglichen den direkten Vergleich und unterstützen bei der Analyse der Ursachen zu den dargestellten Abweichungen. Bei Produktionsprozessen handelt es sich meist um sogenannte „Milestone Events“, die bei jedem Fertigungsschritt durch das MES dokumentiert werden und eine einfache Modellierung des Target Process ermöglichen. Weiterführende Analysen der Prozessdaten in R sind durch einen direkten Zugriff über ein API realisierbar oder es wurde vollständig integriert. Damit eröffnen sich wiederum die umfangreichen Möglichkeiten bei der statistischen Prozessanalyse, sowie der Einsatz von Six Sigma Tools aus dem Qualitätsmanagement. Die Analyse kann durch eine, den Kundenanforderungen entsprechende Darstellung in einem Dashboard vereinfacht werden, ermöglicht somit eine zeitnahe, weitgehend automatisierte Prozessanalyse auf Basis der Produktionsdaten.

Resümee

Process Mining in R ermöglicht zeitnahe Ergebnisse, die bis zur automatisierten Analyse in Echtzeit reicht. Der Einsatz beschleunigt erheblich das Process Controlling und hilft den Ressourceneinsatz bei der Datenerhebung, sowie deren Analyse zu reduzieren. Es kann als stand‐alone Lösung zur Untersuchung des „Discovered Process“ oder als Erweiterung für nachfolgende statistische Analysen eingesetzt werden. Als stand‐alone Lösung eignet es sich für Prozesse mit geringer Komplexität, wie in der automatisierten Fertigung. Besteht eine hohe Diversifikation oder sollen standortübergreifende Prozessanalysen durchgeführt werden, übersteigt der Ressourcenaufwand rasch die Kosten für den Einsatz einer Enterprise Software, von denen mittlerweile einige angeboten werden.

 

Einstieg in das Maschinelle Lernen mit Python(x,y)

Python(x,y) ist eine Python-Distribution, die speziell für wissenschaftliche Arbeiten entwickelt wurde. Es umfasst neben der Programmiersprache auch die Entwicklungsumgebung Spyder und eine Reihe integrierter Python-Bibliotheken. Mithilfe von Python(x,y) kann eine Vielzahl von Interessensbereichen bearbeitet werden. Dazu zählen unter anderem Bildverarbeitung oder auch das maschinelle Lernen. Das All-in-One-Setup für Python(x,y) ist für alle gängigen Betriebssysteme online erhältlich. Read more

R Data Frames meistern mit dplyr – Teil 2

Dieser Artikel ist Teil 2 von 2 aus der Artikelserie R Data Frames meistern mit dplyr.

Noch mehr Datenbank-Features

Im ersten Teil dieser Artikel-Serie habe ich die Parallelen zwischen Data Frames in R und Relationen in SQL herausgearbeitet und gezeigt, wie das Paket dplyr eine Reihe von SQL-analogen Operationen auf Data Frames standardisiert und optimiert. In diesem Teil möchte ich nun drei weitere Analogien aufzeigen. Es handelt sich um die

  • Window Functions in dplyr als Entsprechung zu analytischen Funktionen in SQL,
  • Joins zwischen Data Frames als Pendant zu Tabellen-Joins
  • Delegation von Data Frame-Operationen zu einer bestehenden SQL-Datenbank

Window Functions

Im letzten Teil habe ich gezeigt, wie durch die Kombination von group_by() und summarise() im Handumdrehen Aggregate entstehen. Das Verb group_by() schafft dabei, wie der Name schon sagt, eine Gruppierung der Zeilen des Data Frame anhand benannter Schlüssel, die oft ordinaler oder kategorialer Natur sind (z.B. Datum, Produkt oder Mitarbeiter).

Ersetzt man die Aggregation mit summarise() durch die Funktion mutate(), um neue Spalten zu bilden, so ist der Effekt des group_by() weiterhin nutzbar, erzeugt aber „Windows“, also Gruppen von Datensätzen des Data Frames mit gleichen Werten der Gruppierungskriterien. Auf diesen Gruppen können nun mittels mutate() beliebige R-Funktionen angewendet werden. Das Ergebnis ist im Gegensatz zu summarise() keine Verdichtung auf einen Datensatz pro Gruppe, sondern eine Erweiterung jeder einzelnen Zeile um neue Werte. Das soll folgendes Beispiel verdeutlichen:

library(dplyr)
set.seed(42)	

df <- data.frame(id = 1:20, 
                 a=sample(c("Hund","Katze","Maus","Tiger"),20,replace=T),
                 b=sample(1:10,20, replace = T))
df
   id     a  b
1   1  Maus  7
2   2  Hund  3
3   3 Katze  3
4   4  Maus  4
5   5 Tiger 10
6   6  Maus 10
7   7  Hund  8
8   8  Hund  8
9   9  Hund  6
10 10 Katze  1
11 11  Maus  7
12 12  Hund  9
13 13  Hund  8
14 14 Tiger  5
15 15 Tiger  6
16 16  Maus  6
17 17 Katze  1
18 18  Maus  4
19 19  Maus  7
20 20  Maus  9
df %>%
  group_by(a) %>%
  mutate(r = row_number(),        # aus dplyr 
         n_memb = n(),            # aus dplyr
         n_dist = n_distinct(b),  # aus dplyr
         ra=rank(desc(b)),        # aus base und dplyr
         last_b = lag(b),         # aus dplyr
         next_b = lead(b),        # aus dplyr
         mb = mean(b),            # aus base
         cs = cumsum(b)  )        # aus base
Source: local data frame [20 x 11]
Groups: a [4]

     id      a     b     r n_memb n_dist    ra last_b next_b       mb     cs
                    
1      1   Maus     7     1      8      5   4.0     NA      4 6.750000     7
2      2   Hund     3     1      6      4   6.0     NA      8 7.000000     3
3      3  Katze     3     1      3      2   1.0     NA      1 1.666667     3
4      4   Maus     4     2      8      5   7.5      7     10 6.750000    11
5      5  Tiger    10     1      3      3   1.0     NA      5 7.000000    10
6      6   Maus    10     3      8      5   1.0      4      7 6.750000    21
7      7   Hund     8     2      6      4   3.0      3      8 7.000000    11
8      8   Hund     8     3      6      4   3.0      8      6 7.000000    19
9      9   Hund     6     4      6      4   5.0      8      9 7.000000    25
10    10  Katze     1     2      3      2   2.5      3      1 1.666667     4
11    11   Maus     7     4      8      5   4.0     10      6 6.750000    28
12    12   Hund     9     5      6      4   1.0      6      8 7.000000    34
13    13   Hund     8     6      6      4   3.0      9     NA 7.000000    42
14    14  Tiger     5     2      3      3   3.0     10      6 7.000000    15
15    15  Tiger     6     3      3      3   2.0      5     NA 7.000000    21
16    16   Maus     6     5      8      5   6.0      7      4 6.750000    34
17    17  Katze     1     3      3      2   2.5      1     NA 1.666667     5
18    18   Maus     4     6      8      5   7.5      6      7 6.750000    38
19    19   Maus     7     7      8      5   4.0      4      9 6.750000    45
20    20   Maus     9     8      8      5   2.0      7     NA 6.750000    54

Das group_by() unterteilt den Data Frame nach den 4 gleichen Werten von a. Innerhalb dieser Gruppen berechnen die beispielsweise eingesetzten Funktionen

  • row_number(): Die laufende Nummer in dieser Gruppe
  • n(): Die Gesamtgröße dieser Gruppe
  • n_distinct(b): Die Anzahl verschiedener Werte von b innerhalb der Gruppe
  • rank(desc(b)): Den Rang innerhalb der selben Gruppe, absteigend nach b geordnet
  • lag(b): Den Wert von b der vorherigen Zeile innerhalb derselben Gruppe
  • lead(b): Analog den Wert von b der folgenden Zeile innerhalb derselben Gruppe
  • mean(b): Den Mittelwert von b innerhalb der Gruppe
  • cumsum(b): Die kumulierte Summe der b-Werte innerhalb der Gruppe.

Wichtig ist hierbei, dass die Anwendung dieser Funktionen nicht dazu führt, dass die ursprüngliche Reihenfolge der Datensätze im Data Frame geändert wird. Hier erweist sich ein wesentlicher Unterschied zwischen Data Frames und Datenbank-Relationen von Vorteil: Die Reihenfolge von Datensätzen in Data Frames ist stabil und definiert. Sie resultiert aus der Abfolge der Elemente auf den Vektoren, die die Data Frames bilden. Im Gegensatz dazu haben Tabellen und Views keine Reihenfolge, auf die man sich beim SELECT verlassen kann. Nur mit der ORDER BY-Klausel über eindeutige Schlüsselwerte erreicht man eine definierte, stabile Reihenfolge der resultierenden Datensätze.

Die Wirkungsweise von Window Functions wird noch besser verständlich, wenn in obiger Abfrage das group_by(a) entfernt wird. Dann wirken alle genannten Funktionen auf der einzigen Gruppe, die existiert, nämlich dem gesamten Data Frame:

df %>%
  mutate(r = row_number(),        # aus dplyr 
         n_memb = n(),            # aus dplyr
         n_dist = n_distinct(b),  # aus dplyr
         ra=rank(desc(b)),        # aus base und dplyr
         last_b = lag(b),         # aus dplyr
         next_b = lead(b),        # aus dplyr
         mb = mean(b),            # aus base
         cs = cumsum(b)  )        # aus base


   id     a  b  r n_memb n_dist   ra last_b next_b  mb  cs
1   1  Maus  7  1     20      9  9.0     NA      3 6.1   7
2   2  Hund  3  2     20      9 17.5      7      3 6.1  10
3   3 Katze  3  3     20      9 17.5      3      4 6.1  13
4   4  Maus  4  4     20      9 15.5      3     10 6.1  17
5   5 Tiger 10  5     20      9  1.5      4     10 6.1  27
6   6  Maus 10  6     20      9  1.5     10      8 6.1  37
7   7  Hund  8  7     20      9  6.0     10      8 6.1  45
8   8  Hund  8  8     20      9  6.0      8      6 6.1  53
9   9  Hund  6  9     20      9 12.0      8      1 6.1  59
10 10 Katze  1 10     20      9 19.5      6      7 6.1  60
11 11  Maus  7 11     20      9  9.0      1      9 6.1  67
12 12  Hund  9 12     20      9  3.5      7      8 6.1  76
13 13  Hund  8 13     20      9  6.0      9      5 6.1  84
14 14 Tiger  5 14     20      9 14.0      8      6 6.1  89
15 15 Tiger  6 15     20      9 12.0      5      6 6.1  95
16 16  Maus  6 16     20      9 12.0      6      1 6.1 101
17 17 Katze  1 17     20      9 19.5      6      4 6.1 102
18 18  Maus  4 18     20      9 15.5      1      7 6.1 106
19 19  Maus  7 19     20      9  9.0      4      9 6.1 113
20 20  Maus  9 20     20      9  3.5      7     NA 6.1 122

Anwendbar sind hierbei sämtliche Funktionen, die auf Vektoren wirken. Diese müssen also wie in unserem Beispiel nicht unbedingt aus dplyr stammen. Allerdings komplettiert das Package die Menge der sinnvoll anwendbaren Funktionen um einige wichtige Elemente wie cumany() oder n_distinct().

Data Frames Hand in Hand…

In relationalen Datenbanken wird häufig angestrebt, das Datenmodell zu normalisieren. Dadurch bekommt man die negativen Folgen von Datenredundanz, wie Inkonsistenzen bei Datenmanipulationen und unnötig große Datenvolumina, in den Griff. Dies geschieht unter anderem dadurch, dass tabellarische Datenbestände aufgetrennt werden Stammdaten- und Faktentabellen. Letztere beziehen sich über Fremdschlüsselspalten auf die Primärschlüssel der Stammdatentabellen. Durch Joins, also Abfragen über mehrere Tabellen und Ausnutzen der Fremdschlüsselbeziehungen, werden die normalisierten Tabellen wieder zu einem fachlich kompletten Resultat denormalisiert.

In den Data Frames von R trifft man dieses Modellierungsmuster aus verschiedenen Gründen weit seltener an als in RDBMS. Dennoch gibt es neben der Normalisierung/Denormalisierung andere Fragestellungen, die sich gut durch Joins beantworten lassen. Neben der Zusammenführung von Beobachtungen unterschiedlicher Quellen anhand charakteristischer Schlüssel sind dies bestimmte Mengenoperationen wie Schnitt- und Differenzmengenbildung.

Die traditionelle R-Funktion für den Join zweier Data Frames lautet merge(). dplyr erweitert den Funktionsumfang dieser Funktion und sorgt für sprechendere Funktionsnamen und Konsistenz mit den anderen Operationen.

Hier ein synthetisches Beispiel:

products <- data.frame(
  id = 1:5, 
  name = c("Desktop", "Laptop", "Maus", "Tablet", "Smartphone"),
  preis = c(500, 700, 10, 300, 500)  
)

set.seed(1)

(salesfacts <- data.frame(
  prod_id = sample(1:5,size = 8,replace = T),
  date = as.Date('2017-01-01') + sample(1:5,size = 8,replace = T)
)  )  

 prod_id       date
1      2 2017-01-05
2      2 2017-01-02
3      3 2017-01-03
4      5 2017-01-02
5      2 2017-01-05
6      5 2017-01-03
7      5 2017-01-05
8      4 2017-01-04

Nun gilt es, die Verkäufe aus dem Data Frame sales mit den Produkten in products zusammenzuführen und auf Basis von Produkten Bilanzen zu erstellen. Diese Denormalisierung geschieht durch das Verb inner_join() auf zweierlei Art und Weise:

salesfacts %>% 
  inner_join(products, by = c("prod_id" = "id"))

  prod_id       date       name preis
1       2 2017-01-05     Laptop   700
2       2 2017-01-02     Laptop   700
3       3 2017-01-03       Maus    10
4       5 2017-01-02 Smartphone   500
5       2 2017-01-05     Laptop   700
6       5 2017-01-03 Smartphone   500
7       5 2017-01-05 Smartphone   500
8       4 2017-01-04     Tablet   300

products %>% 
  inner_join(salesfacts, by = c("id" = "prod_id")) 

  id       name preis       date
1  2     Laptop   700 2017-01-05
2  2     Laptop   700 2017-01-02
3  2     Laptop   700 2017-01-05
4  3       Maus    10 2017-01-03
5  4     Tablet   300 2017-01-04
6  5 Smartphone   500 2017-01-02
7  5 Smartphone   500 2017-01-03
8  5 Smartphone   500 2017-01-05

Die Ergebnisse sind bis auf die Reihenfolge der Spalten und der Zeilen identisch. Außerdem ist im einen Fall der gemeinsame Schlüssel der Produkt-Id als prod_id, im anderen Fall als id enthalten. dplyr entfernt also die Spalten-Duplikate der Join-Bedingungen. Letzere wird bei Bedarf im by-Argument der Join-Funktion angegeben. R-Experten erkennen hier einen „Named Vector“, also einen Vektor, bei dem jedes Element einen Namen hat. Diese Syntax verwendet dplyr, um elegant die äquivalenten Spalten zu kennzeichnen. Wird das Argument by weggelassen, so verwendet dplyr im Sinne eines „Natural Join“ automatisch alle Spalten, deren Namen in beiden Data Frames vorkommen.

Natürlich können wir dieses Beispiel mit den anderen Verben erweitern, um z.B. eine Umsatzbilanz pro Produkt zu erreichen:

salesfacts %>% 
  inner_join(products, by = c("prod_id" = "id")) %>% 
  group_by(prod_id) %>% 
  summarise(n_verk = n(), sum_preis = sum(preis), letzt_dat = max(date))

# A tibble: 4 × 4
  prod_id n_verk sum_preis  letzt_dat
                
1       2      3      2100 2017-01-05
2       3      1        10 2017-01-03
3       4      1       300 2017-01-04
4       5      3      1500 2017-01-05

dplyr bringt insgesamt 6 verschiedene Join-Funktionen mit: Neben dem bereits verwendeten Inner Join gibt es die linksseitigen und rechtsseitigen Outer Joins und den Full Join. Diese entsprechen genau der Funktionalität von SQL-Datenbanken. Daneben gibt es die Funktion semi_join(), die in SQL etwa folgendermaßen ausgedrückt würde:

SELECT ...
FROM a
WHERE EXISTS (SELECT * FROM b WHERE b.a_id = a.id)

Das Gegenteil, also ein NOT EXISTS, realisiert die sechste Join-Funktion: anti_join(). Im folgenden Beispiel sollen alle Produkte ausgegeben werden, die noch nie verkauft wurden:

products %>% anti_join(salesfacts,c("id" = "prod_id"))

  id    name preis
1  1 Desktop   500

… und in der Datenbank

Wir schon mehrfach betont, hat dplyr eine Reihe von Analogien zu SQL-Operationen auf relationalen Datenbanken. R Data Frames entsprechen Tabellen und Views und die dplyr-Operationen den Bausteinen von SELECT-Statements. Daraus ergibt sich die Möglichkeit, dplyr-Funktionen ohne viel Zutun auf eine bestehende Datenbank und deren Relationen zu deligieren.

Mir fallen folgende Szenarien ein, wo dies sinnvoll erscheint:

  • Die zu verarbeitende Datenmenge ist zu groß für das Memory des Rechners, auf dem R läuft.
  • Die interessierenden Daten liegen bereits als Tabellen und Views auf einer Datenbank vor.
  • Die Datenbank hat Features, wie z.B. Parallelverarbeitung oder Bitmap Indexe, die R nicht hat.

In der aktuellen Version 0.5.0 kann dplyr nativ vier Datenbank-Backends ansprechen: SQLite, MySQL, PostgreSQL und Google BigQuery. Ich vermute, unter der Leserschaft des Data Science Blogs dürfte MySQL (oder der Fork MariaDB) die weiteste Verbreitung haben, weshalb ich die folgenden Beispiele darauf zeige. Allerdings muss man beachten, dass MySQL keine Window Funktionen kennt, was sich 1:1 auf die Funktionalität von dplyr auswirkt.

Im folgenden möchte ich zeigen, wie dplyr sich gegen eine bestehende MySQL-Datenbank verbindet und danach einen bestehenden R Data Frame in eine neue Datenbanktabelle wegspeichert:

mysql_db <- src_mysql(host = "localhost", user = "testuser",
                   password = "********", dbname = "test")

library(ggplot2)

str(diamonds)

Classes ‘tbl_df’, ‘tbl’ and 'data.frame':       53940 obs. of  10 variables:
 $ carat  : num  0.23 0.21 0.23 0.29 0.31 0.24 0.24 0.26 0.22 0.23 ...
 $ cut    : chr  "Ideal" "Premium" "Good" "Premium" ...
 $ color  : chr  "E" "E" "E" "I" ...
 $ clarity: chr  "SI2" "SI1" "VS1" "VS2" ...
 $ depth  : num  61.5 59.8 56.9 62.4 63.3 62.8 62.3 61.9 65.1 59.4 ...
 $ table  : num  55 61 65 58 58 57 57 55 61 61 ...
 $ price  : int  326 326 327 334 335 336 336 337 337 338 ...
 $ x      : num  3.95 3.89 4.05 4.2 4.34 3.94 3.95 4.07 3.87 4 ...
 $ y      : num  3.98 3.84 4.07 4.23 4.35 3.96 3.98 4.11 3.78 4.05 ...
 $ z      : num  2.43 2.31 2.31 2.63 2.75 2.48 2.47 2.53 2.49 2.39 ...

diamonds %>% mutate(cut = as.character(cut), 
                    color = as.character(color),
                    clarity = as.character(clarity)) -> diamonds

diamonds_mysql <- copy_to(mysql_db, diamonds, name="diamonds",
                         temporary = FALSE, indexes = list(
                       c("cut", "color", "clarity"), "carat", "price"))

diamonds_mysql %>% summarise(count = n())

Source:   query [?? x 1]
Database: mysql 5.5.54-0ubuntu0.14.04.1 [testuser@localhost:/test]

  count
  <dbl>
1 53940

Die erste Anweisung verbindet R mit einer bestehenden MySQL-Datenbank. Danach lade ich den Data Frame diamonds aus dem Paket ggplot2. Mit str() wird deutlich, dass drei darin enthaltene Variablen vom Typ Factor sind. Damit dplyr damit arbeiten kann, werden sie mit mutate() in Character-Vektoren gewandelt. Dann erzeugt die Funktion copy_to() auf der MySQL-Datenbank eine leere Tabelle namens diamonds, in die die Datensätze kopiert werden. Danach erhält die Tabelle noch drei Indexe (von dem der erste aus drei Segmenten besteht), und zum Schluß führt dplyr noch ein ANALYSE der Tabelle durch, um die Werteverteilungen auf den Spalten für kostenbasierte Optimierung zu bestimmen.

Meistens aber wird bereits eine bestehende Datenbanktabelle die interessierenden Daten enthalten. In diesem Fall lautet die Funktion zum Erstellen des Delegats tbl():

diamonds_mysql2 <- tbl(mysql_db,"diamonds")

identical(diamonds_mysql,diamonds_mysql2)

[1] TRUE

Die Rückgabewerte von copy_to() und von tbl() sind natürlich keine reinrassigen Data Frames, sondern Objekte, auf die die Operationen von dplyr wirken können, indem sie auf die Datenbank deligiert werden. Im folgenden Beispiel sollen alle Diamanten, die ein Gewicht von mindestens 1 Karat haben, pro Cut, Color und Clarity nach Anzahl und mittlerem Preis bilanziert werden:

bilanz <- diamonds_mysql2 %>% 
  filter(carat >= 1) %>% 
  group_by(cut,color,clarity) %>% 
  summarise(count = n(), mean_price = mean(price))

bilanz

Source:   query [?? x 5]
Database: mysql 5.5.54-0ubuntu0.14.04.1 [testuser@localhost:/test]
Groups: cut, color

     cut color clarity count mean_price
   <chr> <chr>   <chr> <dbl>      <dbl>
1   Fair     D      I1     3   9013.667
2   Fair     D     SI1    26   6398.192
3   Fair     D     SI2    29   6138.552
4   Fair     D     VS1     1   7083.000
5   Fair     D     VS2     7   8553.429
6   Fair     D    VVS1     1  10752.000
7   Fair     D    VVS2     2   9639.000
8   Fair     E      I1     5   2469.800
9   Fair     E     SI1    28   6407.464
10  Fair     E     SI2    45   5627.489
# ... with more rows

explain(bilanz)

<SQL>
SELECT `cut`, `color`, `clarity`, count(*) AS `count`, AVG(`price`) AS `mean_price`
FROM (SELECT *
FROM `diamonds`
WHERE (`carat` >= 1.0)) `cttxnwlelz`
GROUP BY `cut`, `color`, `clarity`


<PLAN>
  id select_type      table type  possible_keys  key key_len  ref  rows
1  1     PRIMARY <derived2>  ALL           <NA> <NA>    <NA> <NA> 19060
2  2     DERIVED   diamonds  ALL diamonds_carat <NA>    <NA> <NA> 50681
                            Extra
1 Using temporary; Using filesort
2                     Using where

Die Definition der Variablen bilanz geschieht dabei komplett ohne Interaktion mit der Datenbank. Erst beim Anzeigen von Daten wird das notwendige SQL ermittelt und auf der DB ausgeführt. Die ersten 10 resultierenden Datensätze werden angezeigt. Mittels der mächtigen Funktion explain() erhalten wir das erzeugte SQL-Kommando und sogar den Ausführungsplan auf der Datenbank. SQL-Kundige werden erkennen, dass die verketteten dplyr-Operationen in verschachtelte SELECT-Statements umgesetzt werden.

Zu guter Letzt sollen aber meistens die Ergebnisse der dplyr-Operationen irgendwie gesichert werden. Hier hat der Benutzer die Wahl, ob die Daten auf der Datenbank in einer neuen Tabelle gespeichert werden sollen oder ob sie komplett nach R transferiert werden sollen. Dies erfolgt mit den Funktionen compute() bzw. collect():

compute(bilanz, name = "t_bilanz", temporary = F)

df <- collect(bilanz)

str(df)

Classes ‘grouped_df’, ‘tbl_df’, ‘tbl’ and 'data.frame': 265 obs. of  5 variables:
 $ cut       : chr  "Fair" "Fair" "Fair" "Fair" ...
 $ color     : chr  "D" "D" "D" "D" ...
 $ clarity   : chr  "I1" "SI1" "SI2" "VS1" ...
 $ count     : num  3 26 29 1 7 1 2 5 28 45 ...
 $ mean_price: num  9014 6398 6139 7083 8553 ...
...

Durch diese beiden Operationen wurde eine neue Datenbanktabelle „t_bilanz“ erzeugt und danach der Inhalt der Bilanz als Data Frame zurück in den R-Interpreter geholt. Damit schließt sich der Kreis.

Fazit

Mit dem Paket dplyr von Hadley Wickham wird die Arbeit mit R Data Frames auf eine neue Ebene gehoben. Die Operationen sind konsistent, vollständig und performant. Durch den Verkettungs-Operator %>% erhalten sie auch bei hoher Komplexität eine intuitive Syntax. Viele Aspekte der Funktionalität lehnen sich an Relationale Datenbanken an, sodass Analysten mit SQL-Kenntnissen rasch viele Operationen auf R Data Frames übertragen können.

Zurück zu R Data Frames meistern mit dplyr – Teil 1.

 

Numerical Python – Einführung in wissenschaftliches Rechnen mit NumPy

NumPy steht für Numerical Python und ist eines der bekanntesten Pakete für alle Python-Programmierer mit wissenschaftlichen Hintergrund. Von persönlichen Kontakten erfuhr ich, dass NumPy heute in der Astrophysik fast genauso verwendet wird wie auch von sogenannten Quants im Investment-Banking. Das NumPy-Paket ist sicherlich ein Grundstein des Erfolges für Python in der Wissenschaft und für den häufigen Einsatz für die Implementierung von Algorihtmen des maschinellen Lernens in Python.

Die zentrale Datenstruktur in NumPy ist das mehrdimensionale Array. Dieses n-dimensionale Array (ndarray) ist eine sehr mächtige Datenstruktur und verwende ich beispielsweise in meinem Artikel über den k-Nächste-Nachbarn-Algorithmus. Die Besonderheit des NumPy-Arrays ist, dass es ein mehrdimensionaler Container für homogene Daten ist. Ein Datentyp gilt also für das gesamte Array, nicht nur für bestimmte Zeilen oder Spalten!

import numpy as np

Read more

Wahrscheinlichkeitsverteilungen – Zentralen Grenzwertsatz verstehen mit Pyhton

Wahrscheinlichkeitsverteilung sind im Data Science ein wichtiges Handwerkszeug. Während in der Mathevorlesung die Dynamik dieser Verteilungen nur durch wildes Tafelgekritzel schwierig erlebbar zu machen ist, können wir mit Programmierkenntnissen (in diesem Fall wieder mit Python) eine kleine Testumgebung für solche Verteilungen erstellen, um ein Gefühl dafür zu entwickeln, wie unterschiedlich diese auf verschiedene Wahrscheinlichkeitswerte, Varianz und Mengen an Datenpunkten reagieren und wann sie untereinander annäherungsweise ersetzbar sind – der zentrale Grenzwertsatz. Den Schwerpunkt lege ich in diesem Artikel auf die Binominal- und Normalverteilung.

Für die folgenden Beispiele werden folgende Python-Bibliotheken benötigt:

import matplotlib.pyplot as pyplot
import random as random
import math as math

Read more

R Data Frames meistern mit dplyr – Teil 1

Dieser Artikel ist Teil 1 von 2 aus der Artikelserie R Data Frames meistern mit dplyr.

Data Frames sind das Arbeitspferd von R, wenn Daten in eine Struktur gepackt werden sollen, um sie einzulesen, zu säubern, zu transformieren, zu analysieren und zu visualisieren. Abstrakt gesprochen sind Data Frames nichts anderes als Relationen, also Mengen von Tupels, gebildet aus Elementen von geeigneten Mengen.

Dieses Konzept hat sich auch außerhalb des R-Universums bestens bewährt, umzusammengesetzte Daten, Beobachtungen oder Geschäftsobjekte zu repräsentieren. Der beste Beleg für diese Aussage sind die allgegenwärtigen Relationalen Datenbanksysteme (RDBMS). Dort werden Relationen als Tabellen (Tables) oder Sichten (Views) bezeichnet, und darauf wirkt eine mächtige, imperative Abfrage- und Manipulationssprache namens Structured Query Language, kurz:
SQL.

SQL ist in meiner Wahrnehmung die Lingua Franca der Datenverarbeitung, da sie im Kern über sehr viele Softwareprodukte gleich ist und nach erstaunlich geringem Lernaufwand mächtige Auswerte- und Manipulationsoperationen an den Daten ermöglicht. Hier eine SQL-Anweisung, um eine fiktive Tabelle aller Verkäufe (SALES) nach den Top-10-Kunden in diesem Jahr zu untersuchen:

SELECT customer_id, SUM(sales_amt) AS amount,
COUNT(*) AS n_sales, COUNT(DISTINCT product_id) AS n_products
FROM sales
WHERE sales_date LIKE ’2016-%’
GROUP BY customer_id
ORDER BY SUM(sales_amt) DESC
LIMIT 10

Dieser selbsterklärliche Code aus sieben Zeilen hat einen enormen Effekt: Er fast alle Verkäufe des Jahres 2016 auf Basis der Kundennummer zusammen, berechnet dabei die Summe aller Verkaufsbeträge, zählt die Anzahl der Transaktionen und der verschiedenen vom Kunden gekauften Produkte. Nach Sortierung gemäß absteigenden Umsatzes schneidet der Code nach dem 10. Kunden ab.

SQL kann aber mit der gleichen Eleganz noch viel mehr: Beispielsweise verbinden Joins die Daten mehrerer Tabellen über Fremschlüsselbeziehungen oder analytische Funktionen bestimmen Rankings und laufende Summen. Wäre es nicht toll, wenn R ähnlich effektiv mit Data Frames analoger Struktur umgehen könnte? Natürlich! Aber schon der Versuch, obige SQL-Query auf einem R Data Frame mit den althergebrachten Bordmitteln umzusetzen (subset, aggregate, merge, …), führt zu einem unleserlichen, uneleganten Stück Code.

Genau in diese Bresche springt der von vielen anderen Bibliotheken bekannte Entwickler Hadley Wickham mit seiner Bibliothek dplyr: Sie standardisiert Operationen auf Data Frames analog zu SQL-Operationen und führt zu einer wirklich selbsterklärlichen Syntax, die noch dazu sehr performant abgearbeitet wird. Ganz analog zu ggplot2, das sich an der Grammar of Graphics orientiert, spricht Wickham bei dplyr von einer Grammar of Data Manipulation. Die Funktionen zur Manipulation nennt er folgerichtig Verben.

Dabei treten naturgemäß eine Reihe von Analogien zwischen den Teilen eines SELECT-Statements und dplyr-Funktionen auf:

SELECT-Operation dplyr-Funktion
Bildung der Spaltenliste select()
Bildung eines Ausdrucks mutate()
WHERE-Klausel filter()
GROUP BY Spaltenliste group_by()
Bildung von Aggregaten wie sum() etc. summarise()
HAVING-Klausel filter()
ORDER BY Spaltenliste arrange()
LIMIT-Klausel slice()

Die ersten Schritte

Ich möchte die Anwendung von dplyr mithilfe des Standard-Datensatzes Cars93
aus dem Paket MASS demonstrieren:

> install.packages("dplyr")
> library(dplyr)
> cars <- MASS::Cars93
> str(cars)
’data.frame’: 93 obs. of 27 variables:
$ Manufacturer : Factor w/ 32 levels "Acura","Audi",..: 1 1 2 2 3 ...
$ Model : Factor w/ 93 levels "100","190E","240",..: 49 56 ...

Die erste Aufgabe soll darin bestehen, aus dem Data Frame alle Autos zu selektieren, die vom Hersteller “Audi” stammen und nur Model und Anzahl Passagiere auszugeben. Hier die Lösung in Standard-R und mit dplyr:

> # Standard R: Zu allen Audis das Model, Anzahl Insassen
> subset(cars,Manufacturer=="Audi")[,c("Model","Passengers")]
Model Passengers
3 90 5
4 100 6
>
> # Das gleiche mit dplyr
> select(filter(cars,Manufacturer=="Audi"),Model,Passengers)
Model Passengers
1 90 5
2 100 6

Man sieht, dass die neue Funktion filter() der Zeilenselektion, also der Funktion subset() entspricht. Und die Auswahl der Ergebnisspalten, die in Standard-R durch Angabe einer Spaltenliste zwischen [ und ] erfolgt, hat in dplyr das Pendant in der Funktion select().

select() ist sehr mächtig in seinen Möglichkeiten, die Spaltenliste anzugeben. Beispielsweise funktioniert dies über Positionslisten, Namensmuster und ggf. das auch noch negiert:

select(cars, -starts_with("L")) 

Die obige Abfrage projiziert aus dem Data Frame sämtliche Spalten, die nicht mit “L” beginnen. Das scheint zunächst ein unscheinbares Feature zu sein, zahlt sich aber aus, wenn analytische Data Frames Dutzende oder Hunderte von Spalten haben, deren Bezeichnung sich nach einem logischen Namensschema richtet.
Soweit ist das noch nicht spektakulär. dplyr hilft uns in obigem Beispiel, als erstes bestimmte Datensätze zu selektieren und als zweites die interessierenden Spalten zu projizieren. dplyr ist aber bezüglich der Verarbeitung von Data Frames sehr intuitiv und funktional, sodass wir früher oder später viele Operationen auf unserem Data Frame verketten werden. So erreichen wir die Mächtigkeit von SQL und mehr. Die funktionale Syntax aus dem letzten Beispiel wird dann ganz schnell unleserlich, da die Verabeitungsreihenfolge (zuerst filter(), dann select()) nur durch Lesen des Codes von innen nach außen und von rechts nach links ersichtlich wird.

Daher geht dplyr einen Schritt weiter, indem es den eleganten Verkettungsoperator %>% aus dem magrittr-Paket importiert und zur Verfügung stellt. Dadurch werden die verschachtelten Ausdrücke in Sequenzen von Operationen gewandelt und somit sehr viel lesbarer und wartbarer:

select(filter(cars,Manufacturer=="Audi"),Model,Passengers)
Model Passengers
1 90 5
2 100 6

> cars %>%
+ filter(Manufacturer=="Audi") %>%
+ select(Model,Passengers)
Model Passengers
1 90 5
2 100 6

Diese in meinen Augen geniale Syntax durch den neuen Operator %>% erlaubt einen sequenziellen Aufbau der Operationen auf einem Data Frame. Benutzer der Unix-Kommandozeile werden hier leicht die Analogie zu Pipes erkennen. Ganz abstrakt kann man sagen, dass damit folgende Operationen äquivalent sind:

Traditioneller Funktionsaufruf Verkettung mit %>%
f(a,b) a %>% f(b)
f(a,b,c) a %>% f(b,c)
g(f(a,b),c) a %>% f(b) %>% g(c)

Weiteres erklärt die Dokumentation zum %>%-Operator im Paket magrittr mithilfe
des Befehls ?magrittr::‘%>%‘.

Neue Variablen

Durch die Funtionen select() und filter() können wir aus Data Frames Spalten projizieren und Zeilen selektieren. Ergebnisse neuer Ausdrücke entstehen hingegen mit dem Verb mutate():

> # Mit Umrechnung der Mileage in Verbrauch und des
> # Kaufpreises von USD in EUR
> cars2 <-
+ cars %>%
+ filter(Manufacturer=="Audi") %>%
+ mutate(l_100km = 235 / MPG.city,
+ eur = Min.Price * 1000 / 1.1) %>%
+ select(Model,Passengers,l_100km,eur)
> cars2
Model Passengers l_100km eur
1 90 5 11.75000 23545.45
2 100 6 12.36842 28000.00
> class(cars2)
[1] "data.frame"

Im obigen Beispiel wird zunächst auf den Hersteller Audi selektiert und danach auf einen Streich zwei neue Spalten eingeführt, l_100km und eur. Durch Zuweisen auf eine neue Variable wird das fertige Ergebnis dauerhaft gespeichert. Hierbei handelt es sich wieder um ein natives Data Frame-Objekt. Die Operation transmute() arbeitet analog zu mutate(), verwirft aber nach Bildung der Ausdrücke alle nicht genannten Spalten. Somit können wir obiges Beispiel auch wie folgt schreiben:

cars3 <-
+ cars %>%
+ filter(Manufacturer=="Audi") %>%
+ transmute(Model = Model, Passengers = Passengers,
+ l_100km = 235 / MPG.city,
+ eur = Min.Price * 1000 / 1.1)
cars3
Model Passengers l_100km eur
1 90 5 11.75000 23545.45
2 100 6 12.36842 28000.00

Aggregate

Neben der Selektion von Zeilen und Spalten sowie der Bildung abgeleiteter Ausdrücke ist bei Datenbanktabellen die Gruppierung und Aggregation mit GROUP BY eine sehr wichtige Operation. Dies gilt auch für Data Frames in R, wenngleich hier der Funktionsumfang über diverse Funktionen wie table() oder aggregate() verteilt ist und wenig intuitiv ist.

Hier bringt dplyr ebenfalls eine großartige Verbesserung mit. Das entsprechende Verb heißt group_by(). Diese Operation wird zusammen mit einer Spaltenliste auf ein Data Frame angewendet:

grp_cars <- cars %>% group_by(Manufacturer)
> class(grp_cars)
[1] "grouped_df" "tbl_df" "tbl" "data.frame"

Das Ergebnis von group_by() ist ein Objekt, das “mehr” ist als ein Data Frame, sondern auch noch einige spezifische Strukturinformationen von dplyr enthält. In unserem Beispiel sind dies Indizes von Zeilen, die zum gleichen Hersteller gehören. Das ursprüngliche Data Frame wird hierbei nicht kopiert, sondern nur eingebettet.

Nach Anwenden einer group_by()-Operation ist das Data Frame optimal vorbereitet für die eigentliche Aggregation mit summarise():

> sum_cars <- cars %>%
+ group_by(Manufacturer) %>%
+ summarise(nCars = n(), avg_price = mean(Min.Price))
> str(sum_cars)
Classes ‘tbl_df’, ‘tbl’ and ’data.frame’: 32 obs. of 3 variables:
$ Manufacturer: Factor w/ 32 levels "Acura","Audi",..: 1 2 3 4 5 6 7 8 9 10 ...
$ nCars : int 2 2 1 4 2 8 1 2 6 2 ...
$ avg_price : num 21.1 28.4 23.7 20.8 35.2 ...

Das Resultat von summarise() ist wieder ein Data Frame, das neben den ursprünglichen Gruppierungskriterien nur noch die Aggregate enthält.

Daten in Reih’ und Glied

Zwischen Relationalen Datenbanken und R-Data Frames besteht ein wesentlicher konzeptioneller Unterschied: Die Ergebnisse eines SELECT-Befehls haben keine definierte Reihenfolge, so lange die Zeilen nicht mit der Klausel ORDER BY festgelegt wird. Im Gegensatz dazu haben die Zeilen von Data Frames eine konstante Reihenfolge, die sich aus der Anordnung derWerte in den Spaltenvektoren ergibt.

Dennoch ist es manchmal wünschenswert, Data Frames umzusortieren, um eine fachliche Reihenfolge abzubilden. Hierzu dient in dplyr das Verb arrange(), das im Standard-R weitgehend der Indizierung eines Data Frames mit Ergebnissen der order()-Funktion entspricht, aber syntaktisch eleganter ist:

cars %>%
+ arrange(desc(Horsepower),Manufacturer) %>%
+ select(Manufacturer, Model, Horsepower,Min.Price) %>%
+ slice(1:5)
Manufacturer Model Horsepower Min.Price
1 Chevrolet Corvette 300 34.6
2 Dodge Stealth 300 18.5
3 Cadillac Seville 295 37.5
4 Infiniti Q45 278 45.4
5 Mazda RX-7 255 32.5

Dieses Beispiel hat zum Ziel, die fünf PS-stärksten Autos zu selektieren. Die arrange()-Funktion sortiert hier zunächst absteigend nach der PS-Stärke, dann aufsteigend nach Herstellername. Die Selektion der 5 ersten Zeilen erfolgt mit der hilfreichen Funktion slice(), die aus einem Data Frame Zeilen anhand ihrer Reihenfolge selektiert.

Fazit und Ausblick

Mit dplyr wird die Arbeit mit Data Frames stark verbessert: Im Vergleich zu “nacktem” R bringt das Paket eine klarere Syntax, abgerundete Funktionalität und bessere Performance. In der Kürze dieses Artikels konnte ich dies nur oberflächlich anreissen. Daher verweise ich auf die vielen Hilfe-Seiten, Vignetten und Internet-Videos zum Paket. Im zweiten Teil dieses Artikels werde ich auf einige fortgeschrittene Features von dplyr eingehen, z.B. die Verknüpfung von Data Frames mit Joins, die Window-Funktionen und die Verwendung von Datenbanken als Backend.

Weiter zu R Data Frames meistern mit dplyr – Teil 2.