Customer Journey Analytics am Beispiel von Suchaktivitäten
Seite 3: Praxis
Die Datenverarbeitung – Data Processing
Für das Fallbeispiel sei nun folgendes Szenario angenommen: Um einen konkreten Rahmen abzustecken, wird nur ein Touchpoint berücksichtigt, nämlich die Shop-interne Suchfunktion. Die hier analysierten Daten stammen aus einem Datengenerator, entworfen und zur Verfügung gestellt von Emil Siemes für ein Webinar zum Thema. In den Daten enthalten sind die Suchbegriffe, ein Timestamp mit der Uhrzeit der Suchanfrage sowie die Anzahl der Treffer, die diese Anfrage zum Ergebnis hatte. Damit das Beispiel auch auf Hardware nachvollziehbar ist, die nicht auf ein Big-Data-Projekt ausgelegt ist, wurde der fiktive Zeitraum auf zwei Tage eingeschränkt. Ein
Beispieldatensatz sieht folgendermaßen aus:
2015-04-01T0:1:00Z cable 120
Es handelt sich um Tab-separierte Datensätze, die aus einem Datum beziehungsweise einem Timestamp, der eingegebenen Suche und der Trefferanzahl, die die Suchmaschine lieferte. Um nicht gänzlich auf die Interaktion mit Hadoop verzichten zu müssen, werden die Daten zunächst ins HDFS importiert:
hadoop fs -put search_log_data.txt /input
Der Befehl lädt die Datei search_log_data.txt in das Verzeichnis /input des HDFS. Dadurch ist der Zugriff auf die Datei vom gesamten Hadoop-Cluster gewährleistet, ohne dass genaue Kenntnis über den exakten Ort herrscht. Standardmäßig ist auf http://localhost:10008 die Startseite zu sehen.
Notebooks
Notebooks sind der Ort, an dem die Programmierung und Visualisierung innerhalb von Zeppelin stattfindet. Da sie über den Browser erreichbar sind, lässt sich ohne Installation von Software von jedem Rechner aus arbeiten, der Internetzugriff und einen Browser hat. Eine Zusammenarbeit mit mehreren Personen ist möglich, man kann also von einer Art Google Docs fürs Programmieren sprechen. Alle relevanten Funktionen werden in den Nootebooks zur Verfügung gestellt: Datenimport, Datenanalyse, Visualisierung und Collaboration.
Zunächst wird ein neues Notebook erstellt und als "Customer Journey" benannt. Das Fallbeispiel nutzt Scala als Programmiersprache. Python wäre die Alternative bei der Arbeit mit Zeppelin. Zunächst bedarf es eines einfachen Imports, dann geht es direkt ans Einlesen der Daten im HDFS. Um zu prüfen, ob der Import geklappt hat, werden die ersten zehn Einträge als Datensatz ausgegeben. Die folgenden drei Zeilen lassen sich direkt
ausführen, und als Ausgabe erhält man unter anderem die ersten zehn Datensätze:
import sys.process._
val data = sc.textFile("hdfs:///input/Search_Log_data.txt")
data.take(10).mkString("\n")
Das Importieren muss nur einmal geschehen, die gesamte Datei ist im Moment in einem sogenannten RDD (Resilient Distributed Dataset) geladen. RDDs sind so etwas wie Objekte, mit denen man mit Spark arbeiten kann. Auf ihnen lassen sich entweder Transformationen ausführen, was wiederum ein neues RDD zur Folge hat, oder Aktionen, was ein Ausführen aller Transformationen nach sich zieht. Beispiele für Transformationen sind filter() oder map(). Das Importieren der Daten bewirkte ein zeilenweises Einlesen. Das heißt, im RDD ist nun für jede Zeile der Datei genau ein Wert enthalten. Das ist die Grundlage, auf deren Basis sich weiter verarbeiten lässt und in Richtung Analyse der Daten gegangen wird. Dafür definiert man eine Klasse, die als Schema für die semistrukturierten Daten dient:
case class LogLine(date: String, searchString: String, numHits: Int)
Datum (date) und Suchstring (searchString) werden als String-Datentyp definiert, die Anzahl der Treffer (numHits) als Integer. Als Nächstes importiert man die eingelesenen Daten in dieses Format mit der eben definierten Klasse:
val logLines = data.map(_.split("\t")).map(l => LogLine(l(0), l(1), l(2).
trim.toInt)).toDF()
Die Zeile sieht durchaus kompliziert aus, bei genauerer Betrachtung lässt sich die Komplexität aber einfach aufschlüsseln. Zunächst wird mit einem map-Befehl jeder einzelne Datensatz bei einem Tab gesplittet (data.map(_.split("\t"))) und somit pro Zeile der Eingabedaten ein Array aus drei Elementen erstellt. Im Anschluss überführt man das Ergebnis des split()- mit einem weiteren map-Befehl in das erstellte Schema, wobei darauf zu achten ist, dass es sich beim letzten Element des Arrays immer um einen Integer-Wert handelt (map(l => LogLine(l(0), l(1), l(2).trim.toInt)).
Zu guter Letzt wird die Funktion toDF() aufgerufen, die das Ergebnis der Berechnung in ein sogenanntes DataFrame überführt und in der Variable logLines speichert. Kurz zusammengefasst, wurden folgende Arbeitsschritte bis hierher durchgeführt:
- Einlesen der Daten in ein RDD,
- Erstellen eines Schemas und
- Überführen des RDD in ein DataFrame durch Anwendung des Schemas.
Die Überführung in ein DataFrame hat den Vorteil, dass es sich bei der Datenstruktur um eine verteilte Sammlung von Daten handelt, die in Spalten organisiert sind. Man kann also von einer konzeptionell ähnlichen Struktur wie die einer relationalen Datenbank sprechen. Von hier an lässt sich entweder mit den Transformationen und Aktionen weiterarbeiten, die auch auf RDDs anwendbar sind, oder das erstellte Schema nutzen, das über die Daten gelegt wurde. Bei Transformationen kann man die filter()- und groupBy()-Funktion verwenden, um festzustellen, was die häufigsten Suchanfragen mit null Treffern sind:
val zeroLogs = logLines.filter(logLines("numHits") < 1).
groupBy(logLines("searchString")).
count()
zeroLogs.sort($"count" desc).show(8)
Der erste Befehl filtert die Suchanfragen heraus, die mindestens einen Treffer zurückliefern (logLines.filter(logLines("numHits") <1)). Das Ergebnis wird anhand der Suchterme gruppiert
(groupBy(logLines("searchString"))) und anschließend pro Suchanfrage gezählt (count()), um letztlich in der Variable zeroLogs gespeichert zu werden. Im Anschluss sortiert man nach der berechneten Anzahl in absteigender Reihenfolge, wobei nur die häufigsten acht ausgegeben werden.
Schon sind die häufigsten Suchanfragen zu sehen, die zu keinem Treffer über die Shop-Suche führen – eingeschränkt auf den Zeitraum, den die Daten berücksichtigen. Bevor es an die Analyse der Ergebnisse geht, wird noch eine alternative Herangehensweise bei der Verarbeitung der Daten aufgezeigt, die stärker in Richtung SQL-Abfragen abzielt und die Ergebnisse grafisch besser darstellt als die eben beschriebene Herangehensweise. Denn die Möglichkeit, SQL- oder zumindest SQL-ähnliche Abfragen an die Daten zu richten, bedeutet für alle, die mit solchen Aufgaben aus der Welt der relationalen Datenbanken vertraut sind, eine niedrige Hürde für die Einarbeitung in die neue Technik. Die Daten sind als Tabelle zu registrieren, um Abfragen auf die Tabelle zu erlauben. Das ist einfach mit folgendem Befehl erreicht:
logLines.registerTempTable("logs")
Ab dem Zeitpunkt sind SQL-Abfragen an die Tabelle möglich, die genau das Ergebnis liefern soll wie die Transformationen aus oben beschriebenem Beispiel:
%sql
SELECT searchString, count(1) AS occurrences
FROM logs
WHERE numHits < 1
GROUP BY searchString
ORDER BY occurrences DESC LIMIT 8
Die Zeile vor der Abfrage ist ein Hinweis an Zeppelin, dass nun der SQL-Interpreter benötigt wird. Dann folgt die Abfrage, die jeder versteht, der schon mit relationalen Datenbanken gearbeitet hat. Jetzt kommt eine große Stärke von Zeppelin ins Spiel: die visualisierte Aufbereitung der Daten. Ist die Standard-Tabellenansicht bereits ein Fortschritt zum obigen Output, lässt sich wahlweise noch auf ein Balkendiagramm oder eine Pie Chart umstellen. Das kann auch bei Management-tauglichen Reports helfen.