Intro
Es ist Mai 2021, ein Tag in der Berufsschule zieht sich, in den Supermärkten ist das Klopapier alle. Weil sowieso nichts besseres zu tun ist begann die suche nach vernünftigen Datenquellen, um die Mangellage besser beurteilen zu können. Nachdem zum Beispiel EDEKA einfach gar keinen Online-Shop anbietet ist bei Rewe quasi das Gegenteil vorzufinden: Ein schöner Webshop mit guter REST-API dahinter. Daraus entstand die erste Version eines Scrapers für Rewe-Daten. Schon beim betreten des Shops wird man nach der Postleitzahl gefragt - anhand dieser kann dann die nächstgelegene Filiale ausgewählt werden. Entweder fällt die Wahl auf den Liefer- oder Abholservice einer spezifischen Filiale - oder den “Versand ab Logistikzentrum” per Post. Ursprünglich wollte ich eigentlich alle Filialen deren Daten verfügbar sind abgrasen. Allerdings war es mir auch wichtig, die Preisdaten alle mehr oder weniger zum selben Zeitpunkt abzurufen. Das ließ sich leider ob der Masse nicht realisieren, weshalb die Wahl auf ~13 Filialen über Deutschland verteilt fiel.
Filial-Standort | ⌀ Sortiments-Größe | Datenaufzeichnung seit |
---|---|---|
Frankfurt | 9423 |
2021-05-13 |
Flensburg | 9395 |
2021-05-13 |
Bad Wurzach | 9400 |
2021-05-13 |
Leutkirch | 9397 |
2021-05-13 |
Ulm Trollingerweg | 7992 |
2021-05-13 |
Dresden | 6737 |
2021-05-13 |
Gelsenkirchen | 9399 |
2021-05-13 |
Ulm Schulzentrum Kuhberg | 9409 |
2021-05-21 |
Jessen/Brandenburg | 8306 |
2021-06-11 |
Darmstadt-groß | 8377 |
2022-02-16 |
Harsewinkel | 8959 |
2022-02-16 |
Görlitz | 8904 |
2022-02-16 |
Gütersloh | 9041 |
2022-02-16 |
Leipzig | 5846 |
2022-12-12 |
Technik-Doku: Erzeugung der Filial-Übersicht
SELECT m.market_id, m.market, (COUNT(s.snap_id) / COUNT(DISTINCT s.date)) as "Größe Sortiment", MIN(s.date) as "Start der Aufzeichnungen" FROM market m JOIN snapshot s ON m.market_id = s.market_id GROUP BY s.market_id ORDER BY MIN(s.date)
Jede Woche am Donnerstag startete also mein Server die Abfragen an die Programmschnittstelle der REWE-Server und legte die Rohdaten in kaum modifizierter Form auf der Festplatte ab. Nach ein paar Wochen konnte ich schon die ersten Analysen aus diesen Daten “ziehen”. Die ersten, einfacheren Analysen führte ich mithilfe eines Python-Skriptes direkt auf den Rohdaten aus. Mit der steigenden Datenmenge erwies sich das jedoch zunehmend unperformanter, weshalb mit der Zeit eine “Pipeline” für die Verarbeitung entstand. Diese stellt die Daten in einem relationalen Datenbankschema dar. Über diverse JOIN
s lassen sich so die nötigen Daten ohne “Dubletten” generieren. Das reduzierte die Dateigröße im Vergleich zum JSON-Rohformat auf 1:12.
Abb. 1: Daten-Verarbeitung und Größen-Reduktion
Öl-Preise
Pünktlich zum Russischen Überfall auf die Ukraine 2022 destabilisierten sich nunmehr auch die Öl-Preise. Während die Filialen in Brandenburg und Dresden auf die erhöhte Nachfrage in einer kurzen Phase mit Rabatten reagierten stiegen zum Beispiel die Preise der Leutkircher Filiale auf den doppelten Literpreis an. Fast gleichzeitig senkten allerdings alle Filialen die maximal bestellbare Menge von den sonst üblichen 100 auf ein bis zwei Flaschen pro Einkauf. Diese Beschränkung blieb erstaunlicherweise bis zum Herbst 2022 bestehen. In einem ähnlichen Zeitrahmen bewegen sich auch die Preisschwankungen. Etwa seit November 2022 gibt es weniger Ausreißer, die Preise haben sich allerdings Ungefähr 2€/Liter über Vorkriegsniveau stabilisiert.
Abb. 2: Preisentwicklung von Rapsöl in verschiedenen Filialen.
Technik-Doku: Query für den durchschnittlichen Rapsölpreis
Die Visualisierung erfolgt über Python und Pandas’ Pivot-Funktion.SELECT s.date, m.market, ROUND(AVG(s.basePrice))/100 as "preis/liter", ROUND(AVG(s.orderLimitation)) as "max bestellmenge" FROM snapshot s JOIN couple_art_cat c ON c.gtin = s.gtin JOIN market m ON s.market_id = m.market_id WHERE c.cat_id = 644 GROUP BY s.market_id, s.date ORDER BY date, market
avg_price = df.pivot(index=["date"], columns=["market"], values=["preis/liter"]) avg_price.plot(legend=False, figsize=(15, 4))
Shrinkflation
Bei den - wie ich durch die Presseberichterstattung lernte - Eckpreisartikeln können (oder mehr wollen) die Unternehmen die Preise nicht anheben, weshalb dann einfach die Packungsgröße reduziert wird. Das ist an den folgenden zwei Beispielen von Haribo und Chipsfrisch gut zu erkennen. Ein durchgezogener Balken bedeutet eine Verfügbarkeit in irgendeiner der überwachten Filialen. Bei Haribo hielten die verkleinerten Tüten ab August 2022 Einzug in die Regale, währenddessen verschwanden die letzten 200g-Tüten bis Ende November 2022. Gut zu erkennen sind auch zwei zeitliche Sonderangebote - die Kindheits-Knaller von Februar bis April sowie die ÜBÄRraschung im Spätsommer.
Abb. 3: Verfügbarkeit der Haribo-Goldbären
Bei den Chips ist die Übergangszeit sogar deutlich kürzer - hier wurden die 50 bzw. 175g-Tüten innerhalb von zwei Wochen durch 40 bzw. 150g-Tüten ersetzt. Die deutlich kürzere Übergangszeit begründet sich meiner Vermutung nach aus der geringeren Lagerhaltung in den Filialen selbst. Ein großer Karton Gummibärchen mit 100 Tüten nimmt lange nicht so viel Platz weg wie eine Palette Chips mit 100 Tüten.
Abb. 4: Verfügbarkeit von Funny-frisch Chipsfrisch-Sorten
Semesterticket
Die Preiserhöungen machen auch vor dem Semesterticket nicht halt. Auf einer seit Jahren ungepflegten Website der Uni-StuVe entdeckte ich eine Tabelle mit alten Semesterticketpreisen. Ergänzt um die Messpunkte meiner selbst in den letzten Jahren erstandenen Tickets ergibt auch eine Preisverdopplung in ca. 20 Jahren. Immerhin stiegen die Preise mehr oder weniger linear.
Abb. 5: Preissteigerung Semesterticket
Fernab von Inflation und co…
…lassen sich noch ein paar andere Metriken aus den Rewe-Daten generieren!
Zum Beispiel der Anteil der vegetarischen/veganen Artikel in einem Markt. Spannend ist hierbei der Dip auf nahezu Null im Sommer 2021. Die starke Steigerung im darauffolgenden Herbst lässt mich auf eine “Neu-Erfassung” der Daten schließen (ist also nicht unbedingt aussagekräftig) - während ich die Steigerungen ab 2022 als durchaus “natürlich” interpretiere. Für eine detailliertere Analyse rücke ich gerne die Rohdaten raus. Abb. 6: Anteil der vegetarischen bzw. veganen Artikel in einem Markt.
Technik-Doku: Query für den Vergleich von Artikelzahlentwicklung und Anteil der Veganen Produkte daraus
SELECT s.date, m.market, COUNT(DISTINCT CASE WHEN at.attribute LIKE 'veg%' THEN gtin END) AS veg_count, COUNT(DISTINCT gtin) AS all_count FROM snapshot s JOIN couple_attr c ON s.snap_id = c.snap_id JOIN attribute at ON c.attr_id = at.attr_id JOIN market m ON m.market_id = s.market_id GROUP BY s.date, m.market ORDER BY m.market, s.date
Auch sehr spannend ist der Vergleich des generellen Preisniveaus zwischen den verschiedenen untersuchten Märkten. Für die Analyse wurden - um den Vergleich fair zu halten - nur die Artikel herangezogen, die auch in allen Filialen geführt werden. So wird z.B. verhindert, dass ein Markt mit einer ausführlichen Weinauswahl (zu gehobenen Preisen) in der Statistik benachteiligt wird. Wie in Tabelle 1 zu sehen ist die Rewe-Filiale in Dresden die günstigste in der untersuchten Flotte, während die in Leipzig knapp 12% teurer als die in Dresden ist.
Filiale | relatives Preisniveau |
---|---|
Dresden | 100% |
Gelsenkirchen | 101.56% |
Leutkirch | 101.79% |
Frankfurt | 103.09% |
Ulm Schulzentrum Kuhberg | 103.28% |
Flensburg | 103.59% |
Jessen/Brandenburg | 103.66% |
Bad Wurzach | 103.71% |
Darmstadt-groß | 105.00% |
Ulm Trollingerweg | 105.12% |
Görlitz | 105.54% |
Harsewinkel | 105.69% |
Gütersloh | 108.73% |
Leipzig | 111.74% |
Tab. 1: Preisniveau der Filialen
Abb. 8: Preisniveau allgemein (oben) und relativ dazu nach Kategorie (unten)
Wie in Abbildungen 8 zu sehen, war die “REWE-Inflationsrate”" bis Oktober 2021 sogar im Deflationsbereich - seitdem sind die Preise aber um 16% gestiegen. Bei der Betrachtung der Preisänderungen nach Kategorien fällt auf, dass dabei die Teuerungsrate der Babyprodukte deutlich größer war als bei Drogerieartikeln, Obst und Gemüse - bei letzteren sind die Preise sogar im Schnitt “weniger schnell” teurer geworden. Molkereiprodukte blieben interessanterweise bis Oktober 2022 relativ im “Inflationsschnitt” bis sie plötzlich einen Preissprung um ~15% hinlegten. Gründe hierfür erschließen sich mir vorerst leider keine.
Technik-Doku Allgemeine Preisentwicklung pro Kategorie
SELECT s.date, AVG(price) as "allgemein", AVG(DISTINCT CASE WHEN ct.cat_id = 1 THEN price END) as "obst+gemüse", AVG(DISTINCT CASE WHEN ct.cat_id = 3680 THEN price END) as "fleisch+fisch", AVG(DISTINCT CASE WHEN ct.cat_id = 3684 THEN price END) as "käse+ei+milch", AVG(DISTINCT CASE WHEN ct.cat_id = 273 THEN price END) as "tk", AVG(DISTINCT CASE WHEN ct.cat_id = 3738 THEN price END) as "fertigessen+konserven", AVG(DISTINCT CASE WHEN ct.cat_id = 820 THEN price END) as "süß+salzig", AVG(DISTINCT CASE WHEN ct.cat_id = 1066 THEN price END) as "drogerie", AVG(DISTINCT CASE WHEN ct.cat_id = 1136 THEN price END) as "baby" FROM snapshot s JOIN market m ON m.market_id = s.market_id JOIN couple_art_cat ct ON ct.gtin = s.gtin WHERE s.gtin IN ( SELECT q.gtin FROM snapshot q GROUP BY q.gtin HAVING COUNT(DISTINCT market_id) > 12 ) GROUP BY s.date ORDER BY s.date
DIY!
Als proof-of-concept entstand außerdem eine kleine Web-Anwendung, die über sql.js die SQLite-Datenbank (allerdings mit deutlich reduzierter “Datenauflösung”) direkt nach dem Download im Browser lokal weiterverarbeiten kann.