- Teil 1: Gemischtdatenladen (dieser Artikel)
- Teil 2: Marktanalyse
Mit Beginn der Pandemie haben viele ihr Einkaufsverhalten umgestellt: Statt wie früher mit Einkaufszettel und Anonymität gewappnet den Supermarkt zu betreten, nutzt man heutzutage den bequemen Lieferdienst. Der erlaubt alle Produkte in heimischer Ruhe auszusuchen und bei Folgebestellungen ganz einfach frühere Produkte erneut in den Warenkorb zu legen. Die dabei anfallenden Daten – samt Zahlungsmethoden – bewahrt der Supermarkt fein säuberlich auf.
Dabei kommt schnell einiges zusammen, denn anders als in großen Onlineshops wie Amazon oder Zalando kauft man bei Lebensmittelhändlern Produkte oft wiederholt und mit hoher Frequenz. Nicht selten wird zwei- bis dreimal pro Monat oder öfter ein Warenkorb geliefert, der zu beachtlichen Teilen aus den immer gleichen Produkten besteht.
Manche Onlinehändler bieten löblicherweise einen Datenexport an. Zu dieser Gruppe gehört die Supermarktkette Rewe, deren Export-Daten wir als Beispiel für diesen Artikel nutzen. Rewe-Kunden bekommen in den Einstellungen des Onlineshops mit wenigen Klicks ausgeliefert, was der Händler über sie weiß – aber was fängt man mit den paar Hundert Kilobyte Einkaufsdaten im JSON-Format an? Außerdem weist Rewe darauf hin, dass die Bestelldaten nicht in einem interoperablen Format zur Verfügung stehen, weil es dafür kein solches Format gebe. Also kann man nicht zu standardisieren Tools greifen, sondern muss bei der Datenanalyse selbst Hand anlegen.
Im Folgenden erklären wir, wie man so einen Datenexport analysiert und die Datenqualität verbessert. Die konkreten Daten sind spezifisch für die Einkäufe eines einzelnen Kunden, ihre Zeitpunkte und den Händler, aber die allgemeine Vorgehensweise kann man breit anwenden – sofern man an die Daten kommt.
Entheddern
Als meistverwendete Programmiersprache für alle möglichen Datenanalysen hat sich Python etabliert. Gepaart mit Jupyter Notebooks, dem Schweizer Taschenmesser für flinke Entwicklung von Prototypen, kommt man damit schnell zu ansehnlichen Ergebnissen. Doch vor dem Anlegen eines frischen Jupyter-Notebooks ist es sinnvoll, sich die Struktur der Export-Datei anzusehen. Zum komfortablen Lesen von größeren JSON-Dateien bietet sich der Browser Firefox oder das Kommandozeilentool jless
an. Der Export der Daten von Rewe sieht stark gekürzt folgendermaßen aus:
Wie gesagt, der Aufbau der Daten ist nicht standardisiert. In der Zukunft, bei anderen Händlern oder gänzlich anderen JSON-Exporten ist das Bild ein anderes – aber die Inspektion per Browser oder jless
dient ja genau dazu, sich einen Überblick zu verschaffen. Wenn Sie einfach nur die Datenanalyse nachvollziehen wollen, können Sie einen anonymisierten und gekürzten Datensatz herunterladen.
Neben den reinen Bestelldaten listet die Datei noch zahlreiche andere personenbezogene Informationen auf. Die eher sinnlose Verschachtelung eines orders
-Arrays in einem orders
-Objekt ist übrigens kein Druckfehler, sondern tatsächlich so in der Datei enthalten. Derartiges kommt in der Praxis gerne dann vor, wenn keine genauen Schnittstellendefinitionen vorliegen.
Jeder Eintrag in dem Array mit den Bestellinformationen enthält mehrere Unterobjekte, unter anderem für Zahlungs- und Adressinformationen – diese sogar als GPS-Koordinaten. Überlegen Sie sich also gut, ob Sie für die weitere Verarbeitung Ihrer personenbezogenen Daten eine Cloud-Software oder lieber lokale Programme nutzen. Das interessanteste Unterobjekt heißt subOrders
:
Neben der Auslieferung durch eigene Fahrer ("deliveryType": "DELIVERY"
) bietet der Lebensmittelhändler auch den Paketversand von haltbaren Produkten sowie Abholung im Laden an. Diese verschiedenen Bestelltypen kann man in einer Bestellung kombinieren. Unser Beispielkunde hat dergleichen nicht genutzt, weshalb es zu jeder Order nur eine Sub-Order gibt.
Der Schlüssel subOrderValue
enthält den gesamten Bestellwert, hier glatte 93 Euro. Die Zahl unter creationDate
gibt den Zeitpunkt der Bestellung an, allerdings nicht als Unix-Zeitstempel, sondern als String im Format Jahr, Monat, Tag, Stunde, Minute (ohne Trennzeichen). Das Objekt unter timeSlot
gibt das Lieferzeitfenster an sowie eventuelle Extrakosten.
Bei der Auswertung chronologischer Daten ist es in der Regel sinnvoll, sie nach ihrem Zeitpunkt zu sortieren, zu gruppieren und so weiter. Für eine konsistente Analyse muss man sich bei diesem Datensatz also entscheiden, welchen der beiden Zeitpunkte man nutzen will; wir wählen das Bestelldatum und merken uns daher den Key creationDate
.
Jetzt geht es endlich ans Eingemachte, die eigentlichen Bestellpositionen im lineItems
-Array. Die Einträge stellen eine Art digitalen Kassenzettel mit Produkttitel, Einzelpreis, Anzahl und Gesamtpreis dar:
Auch hier sind die Beträge in Eurocent beziffert. Wer weiter durch die Daten scrollt, entdeckt, dass bei Mehrweggebinden auch das Pfand als „Produkt“ im Array auftaucht. Das sollte man – genau wie den mit null Euro bepreisten Eintrag „Getränke-Sperrgutaufschlag“ – vor der Weiterverarbeitung herausfiltern.
Schlangen und Bären
Nach dieser ersten Inspektion gibt es diverse Optionen für die weitere Analyse. Von der händischen Auswertung zur weitgehend automatischen Analyse über (Python-)Skripte ist vieles denkbar. Eine besonders vielseitige und schnell umsetzbare Auswertung baut man sich mit „Pandas“, einer mächtigen Python-Bibliothek zur Datenanalyse. Die Installation von Jupyter Notebooks, Pandas und einem Hilfsmodul für grafische Ausgaben gelingt sehr einfach über Pythons Paketmanager pip
. Der zweite Befehl startet direkt eine Jupyter-Notebook-Instanz, die sich im Standardbrowser öffnet:
Den kompletten Code des im Folgenden beschriebenen Notebooks können Sie ebenfalls herunterladen. Navigieren Sie die Notebook-Ansicht zur Datei und klicken Sie darauf, um sie zu öffnen. Alternativ können Sie über den Button „New“ ein neues Notebook anlegen, um den Code selbst abzufassen.
Um die Daten mit Pandas zu analysieren, muss man sie aus dem hierarchischen JSON-Format in ein DataFrame
-Objekt von Pandas konvertieren. So ein DataFrame ist im Grunde eine Liste von Datensätzen. Jeder Datensatz muss dabei die gleichen Schlüssel enthalten, damit Pandas aus der Liste eine Tabelle mit einheitlichen Spalten konstruieren kann. Dafür öffnet das Skript zunächst die Datenexport-Datei mittels des json
-Moduls aus Pythons Standardbibliothek:
Anschließend klopft der Code die Hierarchie der JSON-Daten flach, indem er über jede Bestellposition in jeder Sub-Order in jeder Order wandert und die Bestelldaten samt der zugehörigen Order- und Sub-Order-Information speichert. Dadurch entsteht zwar Redundanz, aber häufig sind solche tabellarischen Daten einfacher als Hierarchien zu handhaben. Pythons „dictionary unpacking“-Syntax mit dem Doppelstern, kombiniert mit drei Generator-Ausdrücken, die über alle drei Hierarchieebenen iterieren, erledigen die Aufgabe in vier Zeilen:
Im nächsten Schritt kommt Pandas ins Spiel, denn das flache Python-Dictionary wird in einen DataFrame
importiert. Außerdem ergänzen wir die Spalte date
, die das Bestelldatum – konvertiert in ein Python-datetime
-Objekt – enthält. Pandas’ DataFrame
implementiert den []
-Operator, was das Hinzufügen von Spalten sehr einfach macht:
Die alte Spalte creationDate
braucht man jetzt nicht mehr, genau wie viele andere der insgesamt 26 Spalten. Der nächste Codeschnipsel filtert daher die sieben interessanten Spalten heraus, was ebenfalls mit dem []
-Operator leicht gelingt:
Außerdem ist jetzt ein guter Zeitpunkt, um irrelevante Zeilen wie die erwähnten Pfandbeträge zu filtern. Hier hilft Pandas’ „boolean indexing“, die Tilde (~
) dient als NOT-Operator:
Zahlreiche weitere solcher Selektionsmethoden findet man in Pandas’ Dokumentation. Zum Schluss legt set_index()
das Datum als Index fest, um chronologische Auswertungen zu vereinfachen:
Erste Analysen
Diesen Code auszuführen dauert weniger als eine Sekunde. Im geöffneten Notebook drücken Sie dafür einfach wiederholt auf den Play-Button, um Schritt für Schritt die Codeschnipsel auszuführen. Den Pfad zur Datei mit dem JSON-Export müssen Sie eventuell anpassen. Wenn man sich den produzierten DataFrame
ausgeben lässt, indem man in einer Zelle des Notebooks nur die Variable df
notiert, dann zeigt Jupyter automatisch eine hübsche Tabelle an. In unserem Fall besteht der Datensatz aus insgesamt 1049 Bestellpositionen.
Jetzt sind erste statistische Auswertungen möglich. Beispielsweise ist interessant, wie viel Geld man ungefähr monatlich für Lebensmittel ausgibt. In Pandas formuliert sieht das so aus:
Die Codezeile gruppiert den DataFrame
nach Kalendermonaten (resample('M')
) und berechnet die monatlichen Summen aller numerischen Spalten (sum()
). Vom Ergebnis wird die Spalte mit den Preisen ausgewählt (['totalPrice']
). Außerdem kommt gleich noch die Bibliothek Matplotlib zum Zug (plot(kind='bar')
) und gibt das Resultat als Balkendiagramm aus. Um den import
von Matplotlib kümmert sich Pandas automatisch. Ohne den Aufruf von plot()
werden die Daten als tabellarischer Text ausgegeben.
Im Diagramm sieht man deutlich, dass unser Beispielkunde im August 2020 das meiste Geld ausgegeben hat, aber im August 2021 gar keins. Das deutet auf einen sommerlichen Urlaubsaufenthalt hin. Beim Einkaufsverhalten handelt es sich also um wirklich sensible Daten, die allerhand Schlüsse zulassen und die man sorgsam behandeln muss.
Fluktuationen
Mit ähnlichen Abfragen kann man jetzt viele weitere Analysen anstellen. Beispielsweise, welches Produkt man am häufigsten gekauft hat oder für welches das meiste Geld ausgegeben wurde. Die Pandas-Dokumentation bietet einige Tutorials, wie man solche Fragen beantwortet.
Uns hat zunächst interessiert, bei welchem Produkt der Preis am meisten geschwankt hat. Diese Fluktuation kann man grob abschätzen, indem man den Mindest- und Höchstpreis eines Produktes über den ganzen Lieferzeitraum ins Verhältnis setzt, zum Beispiel über die Formel (Max − Min) ÷ (Max + Min). Der passende Pandas-Code, sieht wie folgt aus:
Zunächst gruppiert man die Tabelle per groupby()
nach identischen Produkttiteln. (Das weiter oben genutzte resample()
dient speziell zur zeitlichen Gruppierung.) Der Einfachheit halber verwenden wir die Variable df
wieder. Wer auch im folgenden Code weiter Zugriff auf die ungruppierten Daten haben will, muss stattdessen eine neue Variable nutzen. Bei der Gruppierung wird der Produkttitel automatisch auch als Index für das Ergebnis genutzt. Die nächsten beiden Zeilen selektieren Minimum beziehungsweise Maximum vom Preis jedes Produktes. Dabei entstehen Objekte, die Pandas als „Series“ bezeichnet. Konzeptuell handelt es sich um eindimensionale Arrays, allerdings inklusive Metadaten wie etwa einem Index (wofür in diesem Fall weiter der Produkttitel genutzt wird). Auf diesen Arrays kann man arithmetische Operationen und Sortierungen durchführen, was wir für die Fluktuationsberechnung nutzen; das Ergebnis ist wieder eine Series.
Zum Schluss wird die Series sortiert und zurück in einen DataFrame konvertiert, damit Jupyter eine schöne Tabelle anzeigt. Das Ergebnis zeigt, dass bei den Einkäufen des Beispielkunden insbesondere Gemüse eine hohe Preisspanne aufweist, angeführt von der Bio-Paprika mit knapp 50 Prozent Fluktuation.
Einheiten
Wie man in den gezeigten Tabellen sieht, tragen einige Produkte eine Maßeinheit im Namen, was Vergleiche zwischen Produkten schwierig macht. Abhilfe schafft eine schnell geschriebene Klasse (Unit
) mit einer statischen parse()
-Methode. Die sucht nach Gewichts- und Volumenangaben in Produktnamen und bildet sie auf Werte ab, mit denen man Berechnungen und Vergleiche anstellen kann:
Der Parser nutzt einen recht simplen regulären Ausdruck, andere Eingangsdaten können durchaus Anpassungen erfordern. Der Code soll nicht „production ready“ sein, sondern möglichst schnell und einfach Analysen ermöglichen.
In Pandas kann man die gruppierten Daten nun um eine neue Spalte mit verarbeiteten Maßeinheiten ergänzen:
Die erste Zeile summiert zunächst alle numerischen Spalten der nach Titel gruppierten Produkte. Andere Spalten, für deren Werte Summen keinen Sinn ergeben, verwirft Pandas. Uns interessiert ohnehin nur die Spalte mit der Anzahl; der Aufruf [['quantity']]
filtert danach. Der Produkttitel ist der Index dieses Data-Frames und bleibt deshalb ebenfalls erhalten. Die zweite Zeile nutzt genau diesen Index, um Werte für die neue Spalte unit
zu berechnen. Eingetragen wird jeweils, was die parse()
-Methode im Produkttitel findet. Produkte, bei denen parse()
nichts findet – und daher None
zurückgibt –, filtert die dritte Zeile mittels der Funktion notna()
heraus. (Mit „NA“ bezeichnet Pandas fehlende Werte, notna()
filtert also nicht-fehlende, soll heißen, vorhandene Werte.) Die vierte Zeile nutzt den auf der Unit
-Klasse implementierten Multiplikationsoperator, um die gesamte Bestellmenge zu berechnen.
Nach all diesen Vorarbeiten kann man sich zum Beispiel die – nach Masse – meistgekauften Produkte anzeigen lassen. Dabei hilft wieder das „boolean indexing“, um mit einer anonymen Hilfsfunktion (lambda …
) all die Produkte auszuwählen, die eine Gewichtsangabe tragen. Anschließend wird die Liste nach der Gesamtbestellmenge sortiert:
Angeführt wird die Liste unseres Beispielkunden von Milchprodukten; abgeschlagen auf den letzten Plätzen liegen Kräuter. Für Produkte mit Volumenangaben kann man analog nach UnitType.VOLUME
filtern. Kurios ist übrigens, dass stückige Tomaten eine Volumenangabe haben, passierte Tomaten aber eine Gewichtsangabe.
Bei Kollegen und Freunden, die Einblick in ihre Datensätze gaben, konnten wir mit solchen Methoden Interessantes herausfinden. Beispielsweise hat ein Bekannter über mehrere Jahre hinweg mehr als drei Hektoliter verschiedener Mategetränke umgesetzt – beim Filtern muss man beachten, dass der Produktname „Mate“, aber nicht „Tomate“ enthalten soll. Bei einer maisbegeisterten Freundin liegt Dosenmais mit insgesamt knapp 100 Litern auf Platz eins der Liste – aber erst, nachdem die „Müllbeutel mit Zugband 60l“ aussortiert wurden.