Datenanalyse mit R

von Tobias Kraft

Ziel dieses Blogposts ist es, an einem Beispiel zu zeigen, dass es möglich ist, aus der Programmiersprache R auf die in Elasticsearch gespeicherten Daten zuzugreifen, diese zu verarbeiten und die Ergebnisse für eine Java-Anwendung zugreifbar zu machen. Hierfür wird auf die durch Sensoren erfassten Wetterdaten zugegriffen, die bereits in diesem Blogvorgestellt.

Was ist R?

R beschreibt sich selbst als "a free software environment for statistical computing and graphics" [1]. Es bietet die Möglichkeit, Probleme aus Bereichen wie Statistik und Data Mining schnell zu lösen oder Prototypen zu bauen. R ist jedoch nicht für jedes Problem in diesen Bereichen eine gute Wahl [2]. Unsere Erfahrung mit den Wetterdaten bestätigt diese Einschätzung. Als interpretierte Sprache ist R auch nicht besonders schnell - abgesehen von den eingebauten Vektor/Matrixoperationen(hinter denen BLAS/LAPACK o.ä. steht). Um dieses Problem zu umgehen, enthalten viele R-Pakete C, C++ oder Fortran-Code, in den die rechenintensiven Operationen ausgelagert sind. Beispielsweise hat R Probleme, wenn nicht alle Daten gleichzeitig in den RAM geladen werden können. 
Für die prototypische Anwendung soll der Temperaturverlauf in einem Raum für den nächsten Tag vorhergesagt werden. Es wurde hierfür der Serverraum ausgewählt.

Zugriff auf Elasticsearch

Der Zugriff auf Elasticsearch erfolgt über die REST-Schnittstelle. Um aus R HTTP-Requests senden zu können, wird das Paket RCurl verwendet. RCurl ist ein Wrapper um libcurl. Um den Response, der als JSON vorliegt, in R-Objekte zu parsen, wird das Paket RJSONIO verwendet. Das Paket jsonlite, das den selben Zweck erfüllt und besser aufgebaute R-Objekte liefert, wird nicht verwendet, da es sich als wesentlich langsamer erwiesen hat (selbst wenn man die Ausgaben von RJSONIO nachträglich umformatiert) und daher für die vorliegende Datenmenge nicht geeignet ist. 
Will man R professionell an Elasticsearch anbinden, könnte es sich lohnen, aus libcurl und einem geeigneten JSON-Parser einen Client zu bauen, um das Parsen von JSON und Abbilden in R-Objekte gezielt auf Elasticsearch zu optimieren. Anhand der Abfrage weiß man im Wesentlichen, wie das zurückgegebene JSON aufgebaut ist. Da dies mit größerem Aufwand verbunden ist, wurde für die hier beschriebenen Experimente darauf verzichtet. 
Nachfolgend ist die an Elasticsearch gesendete Aggregations-Query und ihre Verarbeitung dargestellt.

device <- "ex-raspberrypi-01"
type <- "indoor"
request <- paste('{
                 "query": {
                  "bool": {
                   "must": [
                    {
                     "term": { 
                      "device": "', device, '"
                     }
                    }, 
                    {"term": {"type": "', type, '"} },
                    { 
                     "range": {
                      "created" : {
                       "gte": "2014-01-01T11:00:00",
                       "lte": "2015-01-01T12:00:00"
                      }
                     }
                    },
                    { 
                     "range": {
                      "temperature": {
                       "gte": 10.0,
                       "lte": 35.0
                      }
                     }
                    }
                   ]
                  }
                 },
                 "aggs": {
                  "device_count": {
                   "terms": {
                    "field":"device"
                   },
                   "aggs": {
                    "splitminute": {
                     "date_histogram":{
                      "field":"created",
                      "interval":"hour"
                     },
                     "aggs": {
                      "avg_degrees": {
                       "avg":{
                        "field":"temperature"
                       }
                      }
                     }
                    }
                   }       
                  }
                 },
                 "from" : 0,
                 "size":1 
                }', sep='')
headers <- list('Accept' = 'application/json', 'Content-Type' = 'application/json')

h <- basicTextGatherer()

response <- curlPerform(url="http://exensiosearch02:9201/sensorraspberry/_search?pretty", postfields=request, 
                        writefunction = h$update,httpheader=headers, curl = curlhandle)
x <- h$value()

# x ist jetzt eine Zeichenkette mit der Antwort (json) - muss geparst werden.
y <- fromJSON(x)

# y besser struktieren / als data.frame zurückgeben.
data <- y$aggregations$device_count[[1]]
data <- data[[1]]$splitminute$buckets

temp <- numeric(length(data))
time <- numeric(length(data))
for(i in 1:length(data)) {
  temp[i] <- data[[i]]$avg_degrees
  time[i] <- data[[i]]$key
}
rawInput <- data.frame(created=time,  temperature=temp)

Zur Vorhersage wird ein zweischichtiges künstliches neuronales Netz verwendet. Eingaben sind die mittlere Temperatur jeder der letzten 10 Stunden und die Tageszeit/Stunde, für die vorhergesagt werden soll. Ausgeben wird die prognostizierte Temperatur für die nächste Stunde. Um beispielsweise den Temperaturverlauf über einen gesamten Tag vorzusagen, wird die Vorhersage selbst als Input verwendet, damit die Temperatur in der nachfolgenden Stunde vorhergesagt wird. Zur Erzielung besserer Ergebnisse werden die Eingabedaten geeignet skaliert. 
Zum Training des neuronalen Netzes wird das Paket nnet und die Daten bis zum 1. September 2014 verwendet. Die Daten seit dem 1. September dienen zur Evaluation. Die untenstehende Abbildung zeigt die tatsächliche (rot) und die vorhergesagte Temperatur (blau) . Die Vorhersage wurde jeweils um Mitternacht auf Basis der am vergangenen Tag beobachteten Daten für den gesamten Tag erstellt. Es zeigt sich, dass die Vorhersage im letzen Drittel des Oktobers abrupt wesentlich schlechter wird. Dies liegt vermutlich an der Änderung des Standorts des Sensors im Serverraum. Vor der Umpositionierung stand der Sensor in der Nähe eines verfälschenden Abluftstrom eines Gerätes. Für diese neue Situation wurde das neuronale Netz nicht trainiert.

Ergebnisse

Es hat sich gezeigt, dass der Parameter maxit, der die Anzahl der Iterationen während des Trainings begrenzt, große Bedeutung hat: Wird maxit zu groß gewählt, neigt das Netz zu starkem overfitting, d.h. der Fehler auf den Trainingsdaten ist zwar sehr klein, aber die Vorhersagen auf dem Evaluationsdatensatz sind schlecht. Der mittlere Fehler auf den Evaluationsdaten (d.h. ab dem 1. September) beträgt 0.81 Grad Celsius. 

Vorhersage aus Java abrufen

Eine Möglichkeit R von Java aus aufzurufen ist rServe. Dieses Paket ermöglicht es, R als Server zu betreiben und mittels eines Java-Clients anzusprechen. Will man R nicht als Server betreiben, sondern direkt einbinden, bietet sich alternativ RNI an. Mit rServe sieht der Java-Code wie folgt aus:

RConnection c = new RConnection();
// R-Skript einlesen und ausführen:
// - Trainingsdaten aus elasticsearch abfragen und in Variable "rawInput" speichern.
// - diverse Hilfsfunktionen zum Trainieren des neuronalen Netzes ("trainNet"), 
//    Erstellen der Prognose ("getPrognose") etc. definieren.
c.voidEval("source('.../setupPrediction.R')");
// Neuronales Netz trainieren und der Variable "knn" zuweisen.
c.voidEval("knn <- trainNet(rawInput=rawInput)");
//Das sind die mittleren Temperaturen der letzten 10 Stunden
// werden als Input für Prognose benötigt.
double[] lastTemperatures = new double[]{26.63763, 26.60047, 26.62897, 26.63891, 26.58628, 
                                         26.55079, 26.44443, 26.52929, 26.48949, 26.37};
// Werte aus Java nach R kopieren und in Variable "lastvalues" speichern.
c.assign("lastvalues", lastTemperatures);
// Temperaturprognose berechnen
// Da KNN auf skalierten Werten arbeitet, muss man Input skalieren ("scaleServerTemp") 
//  und Ergebnis zurückskalieren ("rescaleServerTemp")
REXP x = c.eval("rescaleServerTemp(getPrognose(knn, scaleServerTemp(rev(lastvalues)), 0))");
// Werte aus R wieder in Java kopieren und ausgeben.
double[] result = x.asDoubles(); 
for(int hour = 0; hour < 24; hour++) {
  System.out.println("" + hour + ":00 - " + (hour+1) + ":00 : Vorhersage: " + result[hour]);
}

Die Ausgabe sieht dann folgendermaßen aus:

0:00 - 1:00 : Vorhersage: 26.313316185020042
1:00 - 2:00 : Vorhersage: 26.275947152882623
2:00 - 3:00 : Vorhersage: 26.251989278387292
3:00 - 4:00 : Vorhersage: 26.246811159392138
4:00 - 5:00 : Vorhersage: 26.25657048628711
5:00 - 6:00 : Vorhersage: 26.279495173271123
6:00 - 7:00 : Vorhersage: 26.297577826042687
7:00 - 8:00 : Vorhersage: 26.326254511775634
8:00 - 9:00 : Vorhersage: 26.364349490351472
9:00 - 10:00 : Vorhersage: 26.39934045980503
10:00 - 11:00 : Vorhersage: 26.431707850819965
11:00 - 12:00 : Vorhersage: 26.461165964549796
12:00 - 13:00 : Vorhersage: 26.48547429218186
13:00 - 14:00 : Vorhersage: 26.503398807877936
14:00 - 15:00 : Vorhersage: 26.51429011947722
15:00 - 16:00 : Vorhersage: 26.518425475695256
16:00 - 17:00 : Vorhersage: 26.51439402371549
17:00 - 18:00 : Vorhersage: 26.501447900578995
18:00 - 19:00 : Vorhersage: 26.479813409022864
19:00 - 20:00 : Vorhersage: 26.44911167010077
20:00 - 21:00 : Vorhersage: 26.40898653790765
21:00 - 22:00 : Vorhersage: 26.35936238134047
22:00 - 23:00 : Vorhersage: 26.300194063174178
23:00 - 24:00 : Vorhersage: 26.231449480426715

Fazit

Die entwickelte Prognosefunktionalität ist sicher noch verbesserungsfähig, vor allem was die Eingaben in das neuronale Netz angeht. Es ist etwa anzunehmen, dass die Außentemperatur zumindest im Sommer einen Einfluss auf die Innentemperatur hat. Dies war jedoch nicht das Hauptziel: Es konnte gezeigt werden, dass R, Elasticsearch und Java-Anwendungen kombiniert werden können und wie man dabei vorgehen kann.

Links

[1] R-Project 
[2] Joseph Adler: R In a Nutshell. Second Edition. O'Reilly. 2012

Kategorien: Elasticsearch

Zurück