Der Markt regelt das schon…

2023-02-18 • 7 minutes to read

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 JOINs lassen sich so die nötigen Daten ohne “Dubletten” generieren. Das reduzierte die Dateigröße im Vergleich zum JSON-Rohformat auf 1:12.

Daten-Pipeline für die Rewe-Datenbank 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.

Preisentwicklung Rapsöl

Abgabemenge Rapsöl Abb. 2: Preisentwicklung von Rapsöl in verschiedenen Filialen.

Technik-Doku: Query für den durchschnittlichen Rapsölpreis

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
Die Visualisierung erfolgt über Python und Pandas’ Pivot-Funktion.
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.

Shrinkflation bei Gummibärchen 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.

Shrinkflation bei Gummibärchen 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.

Preissteigerung beim Semesterticket

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. Vegan/Vegetarisch-Anteil 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

Preisniveau Allgemein Preisniveau Kategorie 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.

(Achtung, es werden im Hintergrund ca. 200MB geladen!)
bastelkramstatistikdaten

DIY-Leistungsmesser

Vektorkarte der (deutschen) Bahntrassen nach Breite und Elektrifizierung