1 Einleitung

Der Lebensmitteleinzelhandel gehört zu den zentralen Versorgungsstrukturen in Deutschland und prägt maßgeblich das Alltagsleben der Bevölkerung. Besonders große Handelsketten, wie Rewe oder die Discounter Netto und Lidl, verfügen über ein dichtes Filialnetz und unterscheiden sich hinsichtlich Preisstrategie, Sortiment und Standortwahl. Diese Unterschiede beeinflussen nicht nur das Einkaufsverhalten, sondern auch die räumliche Versorgung in Städten und Regionen. Eine erste Beobachtung zeigt, dass Supermärkte wie Rewe in Frankfurt verstärkt in innenstadtnahen und einkommensstarken Stadtteilen vertreten sind, während Discounter wie Netto häufiger in peripheren und weniger nachgefragten Stadtvierteln vorkommen. So finden sich Netto-Filialen eher in Stadtteilen wie Harheim, Nieder-Eschbach oder Höchst, während die Rewe-Filialdichte in Richtung Innenstadt zunimmt. Diese Muster werfen Fragen nach den zugrunde liegenden Standortstrategien auf: Welche Rolle spielen Faktoren wie Zielgruppen oder Einkommen für die räumliche Verteilung? Ziel dieser Untersuchung ist es, die Unterschiede zwischen Supermärkten und Discountern in Deutschland systematisch zu analysieren. Dabei soll geklärt werden, inwiefern sich ihre Filialnetze nach Einkommensniveau und Urbanität unterscheiden und welche Bevölkerungsschichten dadurch möglicherweise benachteiligt werden. Um diese Forschungsfrage zu beantworten, werden im Folgenden zunächst der aktuelle Forschungsstand vorgestellt und die Forschungsfrage präzisiert (Kapitel 2). Daraufhin werden die verwendeten Daten und das methodische Vorgehen erläutert (Kapitel 3), bevor schließlich die Ergebnisse sowohl schriftlich (Kapitel 4), als auch visuell dargestellt werden (Kapitel 5). Anschließend werden die Ergebnisse interpretiert und diskutiert (Kapitel 6), und das eigene Vorgehen in der Projektarbeit kritisch reflektiert (Kapitel 7). Den Schluss bildet das Fazit (Kapitel 8).

2 Forschungsstand und Forschungsfrage

2.1 Forschungsstand

Supermärkte und Discounter sind zwei zentrale Formate des deutschen Lebensmitteleinzelhandels und unterscheiden sich sowohl in ihrem Konzept als auch in ihrer Zielgruppenansprache (HDE 2024: 3). Supermärkte verfügen über eine Verkaufsfläche von mindestens 400 bis zu 5000 Quadratmetern und bieten ein breites Sortiment an Lebensmitteln und Haushaltswaren an, das sowohl Grundbedürfnisse als auch spezielle Produktsegmente abdeckt (HDE 2024: 3). Bekannte Vertreter in Deutschland sind beispielsweise Rewe und Edeka (Ahlert et al. 2018: 128). Discounter hingegen arbeiten mit einer deutlich reduzierten Sortimentsbreite, einfacher Ladenausstattung und einer Verkaufsfläche von in der Regel unter 1000 Quadratmetern (HDE 2024: 3). Sie konzentrieren sich auf preisgünstige Produkte, setzen auf Effizienz in der Logistik und bieten zusätzlich wöchentlich wechselnde Aktionswaren an (Ahlert et al. 2018: 385). Diese konzeptionellen Unterschiede spiegeln sich auch in den Standortentscheidungen wider. Während Supermärkte stärker auf Kaufkraft und Sortimentstiefe setzen, verfolgen Discounter eine Preispolitik, die preissensible Kundengruppen anspricht. Standortentscheidungen erfolgen daher nicht zufällig, sondern sind das Ergebnis strategischer Überlegungen. Studien zeigen, dass sowohl das Einzugsgebiet als auch die sozioökonomische Struktur der Bevölkerung wesentliche Faktoren darstellen. (Ahlert et al. 2018: 199) Kaufkraft spielt hier eine zentrale Rolle, da Kunden einerseits gut erreichbare Einkaufsmöglichkeiten erwarten, andererseits aber ihre Zahlungsbereitschaft und Präferenzen entscheidend für die Marktansiedlung sind. Preis bleibt dabei eines der wichtigsten Marketinginstrumente im deutschen Lebensmittelhandel. Laut einer Erhebung des Handelsverbandes Deutschland orientieren sich 52 Prozent der Konsumenten beim Lebensmitteleinkauf primär am Preis. (HDE 2024: 20) Für Discounter ergibt sich daraus ein klarer Wettbewerbsvorteil, da sie mit niedrigen Preisen auf genau diese Nachfrage reagieren. Supermärkte setzen dagegen stärker auf Qualität, Sortiment und zusätzliche Serviceleistungen, was sich in höheren Preisen widerspiegelt (HDE 2024: 3). Aus diesen strategischen Unterschieden ergibt sich die Erwartung, dass sich die räumliche Verteilung der beiden Handelsformen Supermarkt und Discounter entlang sozioökonomischer Faktoren wie Einkommen und Kaufkraft unterscheidet. Regionen mit niedrigem Einkommen dürften eine höhere Dichte an Discountern aufweisen, während Supermärkte eher in wohlhabenderen Gegenden etabliert sind.

2.2 Forschungsfrage

Die bisherigen Beobachtungen legen nahe, dass Discounter und Supermärkte unterschiedlichen Mustern in der Standortwahl folgen. Besonders auffällig ist, dass Discounter wie Netto häufig in einkommensschwächeren Stadtteilen oder Randlagen vertreten sind, während Supermärkte wie Rewe häufiger in zentraleren oder kaufkräftigeren Gegenden auftreten. Deshalb wird in dieser Arbeit untersucht, wie sich verschiedene Lebensmittelketten in Deutschland verteilen und ob es räumliche Muster gibt. Eine zentrale Frage ist außerdem, ob ein Zusammenhang zwischen der Filialdichte von Supermärkten und Discountern und dem Medianeinkommen in den deutschen Bundesländern besteht. Es stellt sich zuerst die Frage, ob urbane Strukturen eine Rolle bei der Verteilung der verschiedenen Lebensmittelketten spielen. Denkbar ist, dass Großstädte mit höherer Kaufkraft mehr Supermärkte pro Kopf aufweisen, während ländlichere Regionen stärker von Discountern geprägt sind. Es wird auch die Frage gestellt, ob sich Unterschiede zwischen Ost- und Westdeutschland erkennen lassen. Aufgrund historischer und wirtschaftlicher Entwicklungen könnte vermutet werden, dass Discounter im Osten stärker verbreitet sind, während im Westen eine größere Supermarktdichte besteht. Die Analyse zielt außerdem darauf ab zu prüfen, ob sich die Hypothese bestätigen lässt, dass Supermärkte mit ihrem breiteren Sortiment und höheren Preisen in wohlhabenderen Bundesländern stärker vertreten sind, während Discounter mit ihrer Preisstrategie eher in einkommensschwächeren Bundesländern präsent sind. Die Untersuchung zeigen, ob sozioökonomische Faktoren wie Einkommen tatsächlich eine signifikante Rolle in der Verteilung von Filialstandorten spielen und ob sich daraus regionale Unterschiede in der Lebensmittelversorgung ableiten lassen.

3 Datenbeschaffung

3.1 Webscraping

3.1.1 Datengewinnung (Webscraping der Filialen)

Im Rahmen dieses Projekts wurden die Adressdaten der Supermarktketten Lidl, Rewe, Netto, Edeka, Kaufland, Alnatura, Mix Markt – automatisiert von der Plattform meinprospekt.de extrahiert. Da alle Filialübersichten auf dieser Website nach dem gleichen HTML-Aufbau strukturiert sind, konnten wir einen generischen Scraping-Ansatz in R entwickeln. Die entsprechenden Unterseiten jeder Supermarktkette wurden dabei in einer Schleife aufgerufen und die Filialadressen systematisch extrahiert und aufbereitet. Im Folgenden wird exemplarisch das Vorgehen anhand der Kette Lidl ausführlich dargestellt.

3.1.2 Laden der Pakete

Bevor mit dem Webscraping begonnen werden kann, müssen zunächst die benötigten R-Pakete geladen werden. Das Paket rvest wird verwendet, um HTML-Seiten zu laden und gezielt Informationen aus dem Quelltext auszulesen. dplyr und stringr stammen aus dem tidyverse und dienen der effizienten Datenmanipulation, insbesondere beim Filtern, Umformen und Bearbeiten von Zeichenketten. Zusätzlich kommt purrr zum Einsatz, um in der Schleife Listenobjekte systematisch zu verarbeiten.

install.packages("tidyverse")    
install.packages("rvest")

3.1.3 Webscraping Lidl-Filialen

Die Plattform meinprospekt.de bietet eine Übersicht über Filialen verschiedener Supermarktketten in Deutschland. Da das Layout bei allen Seiten einheitlich ist, konnte ein wiederverwendbares Skript erstellt werden. Im folgenden Beispiel wird der Ablauf für Lidl-Filialen dargestellt. Die Filialen sind auf mehrere Unterseiten verteilt. In einer for-Schleife werden daher die Seiten 0 bis 9 durchlaufen und jeweils die HTML-Inhalte geladen. Anschließend werden alle Textelemente mit der CSS-Klasse .text-sm extrahiert. Diese enthalten die Adressen in zwei Zeilen: eine für Straße und eine für Postleitzahl + Ort. Um nur die relevanten Adressdaten zu behalten, werden Einträge mit Entfernungsangaben (z. B. „1,3 km“) herausgefiltert. Danach wird jeder zweiter Eintrag der Liste jeweils einer Kategorie zugewiesen: Straße bzw. Postleitzahl (PLZ) und Ort. Die PLZ wird per regulärem Ausdruck extrahiert, der Ortsname wird bereinigt, indem die PLZ entfernt wird. Alle Teilergebnisse werden seitenweise in einer Liste gespeichert und später zu einem großen Datensatz zusammengeführt.

lidl_alle_adressen <- list()

for (i in 0:9) {
  lidl_url <- paste0("https://www.meinprospekt.de/filialen/lidl/", i)
  page <- read_html(lidl_url)

  texte <- page %>%
    html_element("#StoreLogoLinkbox") %>%
    html_elements(".text-sm") %>%
    html_text2() %>%
    str_trim()

  texte_clean <- texte[!str_detect(texte, " km")]

  if (length(texte_clean) %% 2 != 0) {
    texte_clean <- texte_clean[-length(texte_clean)]
  }

  straße <- texte_clean[seq(1, length(texte_clean), by = 2)]
  plz_ort <- texte_clean[seq(2, length(texte_clean), by = 2)]

  plz <- str_extract(plz_ort, "^\\d{5}")
  ort <- str_remove(plz_ort, "^\\d{5}\\s*")

  df <- data.frame(
    strasse = straße,
    plz = as.character(plz),
    ort = as.character(ort),
    stringsAsFactors = FALSE
  )

  lidl_alle_adressen[[i + 1]] <- df
}

lidl_daten <- bind_rows(lidl_alle_adressen)

Nach Ausführung dieses Codes enthält der Data Frame lidl_daten die Rohdaten aller Lidl-Filialen auf den Seiten 0-9. Noch sind darin aber potenzielle Fehler enthalten, zum Beispiel unvollständige oder fehlerhafte Postleitzahlen.

3.2 Datenbereinigung: Korrektur fehlerhafter Postleitzahlen

Bei einigen Adressen – insbesondere im Osten Deutschlands – fehlt die führende Null in der Postleitzahl (z. B. „1234 Leipzig“ statt „01234 Leipzig“). Diese fehlerhaften Postleitzahlen (PLZ) lassen sich dadurch identifizieren, dass sie nur vierstellig (statt fünfstellig) sind oder gar nicht erkannt wurden. In einem ersten Schritt werden solche fehlerhaften Einträge aus dem ursprünglichen Datensatz herausgefiltert. Danach wird versucht, eine vierstellige Zahl am Anfang des Ortsnamens zu extrahieren, diese mit einer führenden Null zu ergänzen und anschließend den Ortsnamen entsprechend zu bereinigen. Die korrigierten Adressen werden schließlich mit den ursprünglich gültigen zusammengeführt, sodass ein konsistenter, bereinigter Datensatz entsteht.

# Fehlerhafte PLZs herausfiltern
plz_fehler <- lidl_daten %>%
  filter(!str_detect(plz, "^\\d{5}$"))

# Korrekturversuch für 4-stellige PLZs
plz_korrektur <- plz_fehler %>%
  mutate(
    neue_plz = str_extract(ort, "^\\d{4}"),
    neue_plz = ifelse(!is.na(neue_plz), paste0("0", neue_plz), NA),
    neuer_ort = ifelse(!is.na(neue_plz), str_remove(ort, "^\\d{4}\\s*"), ort)
  ) %>%
  select(strasse, plz = neue_plz, ort = neuer_ort)

# Gültige PLZs behalten
lidl_ok <- lidl_daten %>%
  filter(str_detect(plz, "^\\d{5}$"))

plz_korrektur <- plz_korrektur %>% 
  mutate( 
    plz = as.character(plz), 
          ort = as.character(ort) 
    ) 

lidl_ok <- lidl_ok %>% 
  mutate( 
    plz = as.character(plz), 
          ort = as.character(ort) 
    )

# Zusammenführen
lidl_final <- bind_rows(lidl_ok, plz_korrektur)

Der finale Datensatz lidl_final enthält nun ausschließlich vollständige und bereinigte Adressen. Jede Zeile besteht aus drei Spalten: Straße, Postleitzahl und Ort. Auf dieser Datenbasis konnten im Anschluss die weiterführenden Analysen durchgeführt werden.

Um spätere Analysen zu erleichtern wurden die Bundesländer und die Postleitzahlen, die in diesen vorkommen webscraped. Es wurde in den Tabellen zu jeder Filiale auch das Bundesland zugeordnet, in dem sich die Filiale befindet.

library(tidyr)

url <- "https://cebus.net/de/plz-bundesland.htm"


page <- read_html(url)

# Alle Texte aus <p> extrahieren
texte <- page %>%
  html_elements("p") %>%
  html_text2() %>%
  str_split("\\n") %>%        # einzelne Zeilen trennen
  unlist() %>%
  str_trim()

# Nur Zeilen mit PLZ-Bereichen behalten
plz_raw <- texte[str_detect(texte, "^\\d{5}-\\d{5}")]

# In Dataframe aufsplitten
plz_tabelle <- tibble(raw = plz_raw) %>%
  mutate(
    start_plz = str_extract(raw, "^\\d{5}"),
    end_plz   = str_extract(raw, "(?<=-)\\d{5}"),
    Bundesland = str_remove(raw, "^\\d{5}-\\d{5}\\s+")
  ) %>%
  select(start_plz, end_plz, Bundesland)

# PLZ expandieren und als character behalten (Nullen bleiben erhalten)
plz_bundesland_expanded <- plz_tabelle %>%
  rowwise() %>%
  mutate(
    plz = list(str_pad(seq(as.integer(start_plz), as.integer(end_plz)), width = 5, pad = "0"))
  ) %>%
  unnest(cols = c(plz)) %>%
  ungroup() %>%
  select(plz, Bundesland)

View(plz_bundesland_expanded)

Diese Information wird mit folgendem Code generiert:

lidl_final <- lidl_final %>%
  left_join(plz_bundesland_expanded, by = "plz")

3.2.1 Ergebnis

Am Ende liegt ein strukturierter Datensatz mit vollständigen Adressinformationen aller Lidl-Filialen in Deutschland vor:

View(lidl_final)
Lidl-Filialen – erste 10 Zeilen
Straße Postleitzahl Ort Bundesland
Wiener Platz 4 01069 Dresden Sachsen
Budapester Straße 26 a 01069 Dresden Sachsen
Schlesischer Platz 1 01097 Dresden Sachsen
Hansastraße 50 01097 Dresden Sachsen
Königsbrücker Landstraße 303 01108 Dresden Sachsen
Ludwig-Kossuth-Straße 49 01109 Dresden Sachsen
Karl-Marx-Straße 20 01109 Dresden Sachsen
Leipziger Straße 76 01127 Dresden Sachsen
Großenhainer Straße 156 01129 Dresden Sachsen
Cossebauder Straße 6 01157 Dresden Sachsen

Jede Zeile des Data Frames enthält: Strasse und Hausnummer, Postleitzahl, Ort, Bundesland.

Diese Daten bildeten anschließend die Grundlage für weiterführende Analysen.

Codes der weiteren Supermarktketten:

#Alnatura

alnatura_alle_adressen <- list()

for (i in 0:9) {
  alnatura_url <- paste0("https://www.meinprospekt.de/filialen/alnatura-de/", i)
 
  page <- read_html(alnatura_url)
  
  texte <- page %>%
    html_element("#StoreLogoLinkbox") %>%       
    html_elements(".text-sm") %>%               
    html_text2() %>%
    str_trim()
  
  texte_clean <- texte[!str_detect(texte, " km")]
  
  if (length(texte_clean) %% 2 != 0) {
    texte_clean <- texte_clean[-length(texte_clean)]
  }
  
  straße <- texte_clean[seq(1, length(texte_clean), by = 2)]
  plz_ort <- texte_clean[seq(2, length(texte_clean), by = 2)]
  
  plz <- str_extract(plz_ort, "^\\d{5}")
  ort <- str_remove(plz_ort, "^\\d{5}\\s*")
  
  df <- data.frame(
    strasse = straße,
    plz = as.character(plz),
    ort = as.character(ort),
    stringsAsFactors = FALSE
  )
  
  alnatura_alle_adressen[[i + 1]] <- df
}

alnatura_daten <- bind_rows(alnatura_alle_adressen)

plz_fehler <- alnatura_daten %>%
  filter(!str_detect(plz, "^\\d{5}$"))

plz_korrektur <- plz_fehler %>%
  mutate(
    neue_plz = str_extract(ort, "^\\d{4}"),
    neue_plz = ifelse(!is.na(neue_plz), paste0("0", neue_plz), NA),
    neuer_ort = ifelse(!is.na(neue_plz), str_remove(ort, "^\\d{4}\\s*"), ort)
  ) %>%
  select(strasse, plz = neue_plz, ort = neuer_ort)

alnatura_ok <- alnatura_daten %>%
  filter(str_detect(plz, "^\\d{5}$"))

plz_korrektur <- plz_korrektur %>%
  mutate(
    plz = as.character(plz),
    ort = as.character(ort)
  )

alnatura_ok <- alnatura_ok %>%
  mutate(
    plz = as.character(plz),
    ort = as.character(ort)
  )

alnatura_final <- bind_rows(alnatura_ok, plz_korrektur)

alnatura_final <- alnatura_final %>%
  left_join(plz_bundesland_expanded, by = "plz")



#Mixmarkt

mixmarkt_alle_adressen <- list()

for (i in 0:9) {
  mixmarkt_url <- paste0("https://www.meinprospekt.de/filialen/mixmarkt-de/", i)
  page <- read_html(mixmarkt_url)
  
  texte <- page %>%
    html_element("#StoreLogoLinkbox") %>%       
    html_elements(".text-sm") %>%               
    html_text2() %>%
    str_trim()
  
  texte_clean <- texte[!str_detect(texte, " km")]
  
  if (length(texte_clean) %% 2 != 0) {
    texte_clean <- texte_clean[-length(texte_clean)]
  }
  
  straße <- texte_clean[seq(1, length(texte_clean), by = 2)]
  plz_ort <- texte_clean[seq(2, length(texte_clean), by = 2)]
  
  plz <- str_extract(plz_ort, "^\\d{5}")
  ort <- str_remove(plz_ort, "^\\d{5}\\s*")
  
  df <- data.frame(
    strasse = straße,
    plz = as.character(plz),
    ort = as.character(ort),
    stringsAsFactors = FALSE
  )
  
  mixmarkt_alle_adressen[[i + 1]] <- df
}

mixmarkt_daten <- bind_rows(mixmarkt_alle_adressen)

plz_fehler <- mixmarkt_daten %>%
  filter(!str_detect(plz, "^\\d{5}$"))

plz_korrektur <- plz_fehler %>%
  mutate(
    neue_plz = str_extract(ort, "^\\d{4}"),
    neue_plz = ifelse(!is.na(neue_plz), paste0("0", neue_plz), NA),
    neuer_ort = ifelse(!is.na(neue_plz), str_remove(ort, "^\\d{4}\\s*"), ort)
  ) %>%
  select(strasse, plz = neue_plz, ort = neuer_ort)

mixmarkt_ok <- mixmarkt_daten %>%
  filter(str_detect(plz, "^\\d{5}$"))

plz_korrektur <- plz_korrektur %>%
  mutate(
    plz = as.character(plz),
    ort = as.character(ort)
  )

mixmarkt_ok <- mixmarkt_ok %>%
  mutate(
    plz = as.character(plz),
    ort = as.character(ort)
  )

mixmarkt_final <- bind_rows(mixmarkt_ok, plz_korrektur)

mixmarkt_final <- mixmarkt_final %>%
  left_join(plz_bundesland_expanded, by = "plz")



#Netto

netto_alle_adressen <- list()

for (i in 0:9) {
  netto_url <- paste0("https://www.meinprospekt.de/filialen/netto-marken-discount-de/", i)
  page <- read_html(netto_url)
  
  texte <- page %>%
    html_element("#StoreLogoLinkbox") %>%       
    html_elements(".text-sm") %>%               
    html_text2() %>%
    str_trim()
  
  texte_clean <- texte[!str_detect(texte, " km")]
  
  if (length(texte_clean) %% 2 != 0) {
    texte_clean <- texte_clean[-length(texte_clean)]
  }
  
  straße <- texte_clean[seq(1, length(texte_clean), by = 2)]
  plz_ort <- texte_clean[seq(2, length(texte_clean), by = 2)]
  
  plz <- str_extract(plz_ort, "^\\d{5}")
  ort <- str_remove(plz_ort, "^\\d{5}\\s*")
  
  df <- data.frame(
    strasse = straße,
    plz = as.character(plz),
    ort = as.character(ort),
    stringsAsFactors = FALSE
  )
  
  netto_alle_adressen[[i + 1]] <- df
}

netto_daten <- bind_rows(netto_alle_adressen)

plz_fehler <- netto_daten %>%
  filter(!str_detect(plz, "^\\d{5}$"))

plz_korrektur <- plz_fehler %>%
  mutate(
    neue_plz = str_extract(ort, "^\\d{4}"),
    neue_plz = ifelse(!is.na(neue_plz), paste0("0", neue_plz), NA),
    neuer_ort = ifelse(!is.na(neue_plz), str_remove(ort, "^\\d{4}\\s*"), ort)
  ) %>%
  select(strasse, plz = neue_plz, ort = neuer_ort)

netto_ok <- netto_daten %>%
  filter(str_detect(plz, "^\\d{5}$"))

plz_korrektur <- plz_korrektur %>%
  mutate(
    plz = as.character(plz),
    ort = as.character(ort)
  )

netto_ok <- netto_ok %>%
  mutate(
    plz = as.character(plz),
    ort = as.character(ort)
  )

netto_final <- bind_rows(netto_ok, plz_korrektur)

netto_final <- netto_final %>%
  left_join(plz_bundesland_expanded, by = "plz")



#Rewe

rewe_alle_adressen <- list()

for (i in 0:9) {
  rewe_url <- paste0("https://www.meinprospekt.de/filialen/rewe-de/", i)
  page <- read_html(rewe_url)
  
  texte <- page %>%
    html_element("#StoreLogoLinkbox") %>%       
    html_elements(".text-sm") %>%               
    html_text2() %>%
    str_trim()
  
  texte_clean <- texte[!str_detect(texte, " km")]
  
  if (length(texte_clean) %% 2 != 0) {
    texte_clean <- texte_clean[-length(texte_clean)]
  }
  
  straße <- texte_clean[seq(1, length(texte_clean), by = 2)]
  plz_ort <- texte_clean[seq(2, length(texte_clean), by = 2)]
  
  plz <- str_extract(plz_ort, "^\\d{5}")
  ort <- str_remove(plz_ort, "^\\d{5}\\s*")
  
  df <- data.frame(
    strasse = straße,
    plz = as.character(plz),
    ort = as.character(ort),
    stringsAsFactors = FALSE
  )
  
  rewe_alle_adressen[[i + 1]] <- df
}

rewe_daten <- bind_rows(rewe_alle_adressen)

plz_fehler <- rewe_daten %>%
  filter(!str_detect(plz, "^\\d{5}$"))

plz_korrektur <- plz_fehler %>%
  mutate(
    neue_plz = str_extract(ort, "^\\d{4}"),
    neue_plz = ifelse(!is.na(neue_plz), paste0("0", neue_plz), NA),
    neuer_ort = ifelse(!is.na(neue_plz), str_remove(ort, "^\\d{4}\\s*"), ort)
  ) %>%
  select(strasse, plz = neue_plz, ort = neuer_ort)

rewe_ok <- rewe_daten %>%
  filter(str_detect(plz, "^\\d{5}$"))

plz_korrektur <- plz_korrektur %>%
  mutate(
    plz = as.character(plz),
    ort = as.character(ort)
  )

rewe_ok <- rewe_ok %>%
  mutate(
    plz = as.character(plz),
    ort = as.character(ort)
  )

rewe_final <- bind_rows(rewe_ok, plz_korrektur)

rewe_final <- rewe_final %>%
  left_join(plz_bundesland_expanded, by = "plz")

#Edeka

edeka_alle_adressen <- list()

for (i in 0:9) {
  edeka_url <- paste0("https://www.meinprospekt.de/filialen/edeka/", i)
  page <- read_html(edeka_url)
  
  texte <- page %>%
    html_element("#StoreLogoLinkbox") %>%       
    html_elements(".text-sm") %>%               
    html_text2() %>%
    str_trim()
  
  texte_clean <- texte[!str_detect(texte, " km")]
  
  if (length(texte_clean) %% 2 != 0) {
    texte_clean <- texte_clean[-length(texte_clean)]
  }
  
  straße <- texte_clean[seq(1, length(texte_clean), by = 2)]
  plz_ort <- texte_clean[seq(2, length(texte_clean), by = 2)]
  
  plz <- str_extract(plz_ort, "^\\d{5}")
  ort <- str_remove(plz_ort, "^\\d{5}\\s*")
  
  df <- data.frame(
    strasse = straße,
    plz = as.character(plz),
    ort = as.character(ort),
    stringsAsFactors = FALSE
  )
  
  edeka_alle_adressen[[i + 1]] <- df
}

edeka_daten <- bind_rows(edeka_alle_adressen)

plz_fehler <- edeka_daten %>%
  filter(!str_detect(plz, "^\\d{5}$"))

plz_korrektur <- plz_fehler %>%
  mutate(
    neue_plz = str_extract(ort, "^\\d{4}"),
    neue_plz = ifelse(!is.na(neue_plz), paste0("0", neue_plz), NA),
    neuer_ort = ifelse(!is.na(neue_plz), str_remove(ort, "^\\d{4}\\s*"), ort)
  ) %>%
  select(strasse, plz = neue_plz, ort = neuer_ort)

edeka_ok <- edeka_daten %>%
  filter(str_detect(plz, "^\\d{5}$"))

plz_korrektur <- plz_korrektur %>%
  mutate(
    plz = as.character(plz),
    ort = as.character(ort)
  )

edeka_ok <- edeka_ok %>%
  mutate(
    plz = as.character(plz),
    ort = as.character(ort)
  )

edeka_final <- bind_rows(edeka_ok, plz_korrektur)

edeka_final <- edeka_final %>%
  left_join(plz_bundesland_expanded, by = "plz")


#Kaufland

kaufland_alle_adressen <- list()

for (i in 0:9) {
  kaufland_url <- paste0("https://www.meinprospekt.de/filialen/kaufland/", i)
  page <- read_html(kaufland_url)
  
  texte <- page %>%
    html_element("#StoreLogoLinkbox") %>%       
    html_elements(".text-sm") %>%               
    html_text2() %>%
    str_trim()
  
  texte_clean <- texte[!str_detect(texte, " km")]
  
  if (length(texte_clean) %% 2 != 0) {
    texte_clean <- texte_clean[-length(texte_clean)]
  }
  
  straße <- texte_clean[seq(1, length(texte_clean), by = 2)]
  plz_ort <- texte_clean[seq(2, length(texte_clean), by = 2)]
  
  plz <- str_extract(plz_ort, "^\\d{5}")
  ort <- str_remove(plz_ort, "^\\d{5}\\s*")
  
  df <- data.frame(
    strasse = straße,
    plz = as.character(plz),
    ort = as.character(ort),
    stringsAsFactors = FALSE
  )
  
  kaufland_alle_adressen[[i + 1]] <- df
}

kaufland_daten <- bind_rows(kaufland_alle_adressen)

plz_fehler <- kaufland_daten %>%
  filter(!str_detect(plz, "^\\d{5}$"))

plz_korrektur <- plz_fehler %>%
  mutate(
    neue_plz = str_extract(ort, "^\\d{4}"),
    neue_plz = ifelse(!is.na(neue_plz), paste0("0", neue_plz), NA),
    neuer_ort = ifelse(!is.na(neue_plz), str_remove(ort, "^\\d{4}\\s*"), ort)
  ) %>%
  select(strasse, plz = neue_plz, ort = neuer_ort)

kaufland_ok <- kaufland_daten %>%
  filter(str_detect(plz, "^\\d{5}$"))

plz_korrektur <- plz_korrektur %>%
  mutate(
    plz = as.character(plz),
    ort = as.character(ort)
  )

kaufland_ok <- kaufland_ok %>%
  mutate(
    plz = as.character(plz),
    ort = as.character(ort)
  )

kaufland_final <- bind_rows(kaufland_ok, plz_korrektur)

kaufland_final <- kaufland_final %>%
  left_join(plz_bundesland_expanded, by = "plz")

3.2.2 Städtedaten

Um die Supermarktstandorte später nach Stadtgrößenklassen (urban, mittelstädtisch, ländlich) einordnen zu können, wurden aktuelle Einwohnerzahlen deutscher Städte benötigt. Diese Daten wurden automatisiert von Wikipedia erhoben. Grundlage war die Seite „Liste der Groß- und Mittelstädte in Deutschland“, die eine tabellarische Übersicht aller Städte mit mindestens 20.000 Einwohnern (Stand 2024) enthält. Mithilfe des Pakets rvest wurde die Webseite eingelesen und alle HTML-Tabellen extrahiert. Die für unser Projekt relevante Tabelle war die zweite, die neben den Städtenamen auch die Einwohnerzahlen für verschiedene Jahre sowie das jeweilige Bundesland enthält. Da die Tabelle Zusatzinformationen wie Fußnoten, Sternchen oder Klammerzusätze (z. B. „Halle (Saale)“, „Rheinstetten 33“) enthielt, wurden diese mithilfe regulärer Ausdrücke (str_remove_all) bereinigt. Auch die Einwohnerzahlen, die ursprünglich mit Tausenderpunkten dargestellt waren, wurden von Zeichen befreit und in Integer-Werte umgewandelt. Zusätzlich wurde die letzte Tabellenzeile entfernt, da sie lediglich Überschriften und keine echten Daten enthielt. Das Ergebnis ist ein konsistenter Datensatz mit den Variablen: Ort (bereinigter Stadtname ohne Fußnoten oder Zusätze), Einwohner (Bevölkerungszahl im Jahr 2024), und Bundesland.

library(rvest)
library(dplyr)
library(stringr)

url <- "https://de.wikipedia.org/wiki/Liste_der_Gro%C3%9F-_und_Mittelst%C3%A4dte_in_Deutschland"

tabellen <- url %>%
  read_html() %>%
  html_table(fill = TRUE)

staedte_roh <- tabellen[[2]]

staedte_roh <- staedte_roh[-nrow(staedte_roh), ]

staedte <- staedte_roh %>%
  select(
    Ort = 2,          # Stadtname
    Einwohner = 9,    # Einwohner 2024
    Bundesland = 10   # Bundesland
  ) %>%
  mutate(
        Ort = Ort %>%
      str_remove_all("\\[.*?\\]") %>%
      str_remove_all("\\*+") %>%
      str_remove_all("\\(.*?\\)") %>%
      str_remove_all("\\d+$") %>%
      str_squish(),
    
      Einwohner = Einwohner %>%
      str_remove_all("\\.") %>%
      str_remove_all("[^0-9]") %>%
      as.integer(),
    
      Bundesland = Bundesland %>%
      str_remove_all("\\[.*?\\]") %>%
      str_trim()
  ) %>%
  filter(!is.na(Einwohner))  # nur gültige Zeilen behalten

View(staedte)

3.2.3 Medianeinkommen der Bundesländer

Für die spätere Analyse des Zusammenhangs von Verteilung der Supermärkte und Medianeinkommen wurden die Medianeinkommen der Bundesländer über Webscraping von der Seite finanz.de erhoben. Da die Werte dort als Liste im HTML-Code vorlagen, mussten sie zunächst aus den <li>-Elementen extrahiert, von störenden Zusätzen wie „Euro“ bereinigt und anschließend in numerische Werte überführt werden.

Der Code für das Scraping und die Aufbereitung sieht folgendermaßen aus:

library(rvest)
library(dplyr)
library(stringr)
library(tidyr)
library(tibble)

# Seite einlesen
url  <- "https://www.finanz.de/gehalt/#medianeinkommen_nach_bundesl%C3%A4ndern"
page <- read_html(url)

# Alle LI-Texte holen
li_text <- page %>%
  html_elements("li") %>%
  html_text2() %>%
  str_squish()

# Referenzliste der 16 Bundesländer
bl_vec <- c(
  "Baden-Württemberg","Bayern","Berlin","Brandenburg","Bremen","Hamburg",
  "Hessen","Mecklenburg-Vorpommern","Niedersachsen","Nordrhein-Westfalen",
  "Rheinland-Pfalz","Saarland","Sachsen","Sachsen-Anhalt","Schleswig-Holstein","Thüringen"
)

# Regex für BL-Namen (große Alternative)
bl_regex <- paste0("(", paste(bl_vec, collapse = "|"), ")")

# Einkommen erkennen (mit/ohne Tausenderpunkt/Leerzeichen) vor "€" oder "Euro"
income_regex <- "(?<!\\d)(\\d{1,3}(?:[\\.\\s]\\d{3})+|\\d{4,6})(?=\\s*(?:€|Euro))"

medianeinkommen <- tibble(text = li_text) %>%
  mutate(
    Bundesland = str_extract(text, bl_regex),
    income_raw = str_extract(text, income_regex)
  ) %>%
  mutate(
    Einkommen = income_raw %>% str_remove_all("\\D") %>% as.integer()
  ) %>%
  filter(!is.na(Bundesland), !is.na(Einkommen)) %>%
  distinct(Bundesland, .keep_all = TRUE) %>%
  select(Bundesland, Einkommen) %>%
  arrange(Bundesland)

3.3 Herausforderungen und Probleme

Bei der Erstellung der Filialtabellen traten verschiedene Schwierigkeiten auf. So führte das Entfernen unerwünschter Supermarktkettennamen aus den Daten zunächst zum Erfolg, erzeugte jedoch neue Fehler. Beispielsweise wurde die Zeichenfolge „Norma“ auch in Straßennamen wie „Normannenstraße“ gelöscht, wodurch die ursprüngliche Spaltenstruktur verrutschte und Postleitzahlen oder Ortsangaben in falschen Spalten landeten. Weitere Probleme ergaben sich bei der Extraktion von Postleitzahlen, insbesondere bei Rewe-Filialen: Hier wurden führende Nullen nicht übernommen, sodass vierstellige PLZ-Werte entstanden und teilweise in die Spalte „Ort“ verschoben wurden. Auch die Validität der verwendeten Quelle meinprospekt.de ist kritisch zu hinterfragen, da die dort angegebene Anzahl von Filialen (z. B. 136 bei Alnatura) von den Angaben auf den offiziellen Webseiten der Anbieter (über 150 Filialen) abweicht. Mögliche Ursachen sind veraltete Daten, fehlende Aktualisierung nach Schließungen oder Umbauten sowie eine unvollständige Abbildung des tatsächlichen Filialnetzes. Diese Herausforderungen verdeutlichen die Grenzen von Webscraping und die Notwendigkeit einer kritischen Reflexion der verwendeten Datenquellen.

3.4 Geocoding der Adressdaten

Um die gewonnenen Adressdaten der Supermark- bzw. Discounterfilialen auch geografisch analysieren zu können, mussten diese in Koordinaten (Breiten- und Längengrad) überführt werden. Dieser Prozess wird als Geocoding bezeichnet. Er ermöglicht es, die Standorte anschließend auf Karten darzustellen oder räumliche Analysen durchzuführen. Für das Geocoding wurde das R-Paket tidygeocoder verwendet, das auf frei zugängliche Geodatenquellen (z. B. OpenStreetMap) zugreift. Zunächst wurde aus den vorhandenen Attributen Straße, Postleitzahl und Ort eine vollständige Adressspalte erzeugt. Diese dient als Eingabe für die Geocoding-Funktion. Der folgende Code zeigt exemplarisch den Ablauf am Beispiel der Lidl-Filialen:

install.packages("tidygeocoder")
library(tidygeocoder)
library(dplyr)

# Erstelle eine neue Spalte mit vollständiger Adresse
lidl_final <- lidl_final %>%
  mutate(adresse = paste(strasse, plz, ort, sep = ", "))

# Geocode: Adresse -> Längen- und Breitengrad
lidl_final_geo <- lidl_final %>%
  geocode(address = adresse, method = "osm", lat = latitude, long = longitude)

head(lidl_final_geo)

View(lidl_final_geo)

Die Funktion geocode() nimmt dabei die Spalte “adresse” als Eingabe und erzeugt zwei neue Variablen: latitude (geografische Breite) und longitude (geografische Länge). Im Ergebnis liegt nun für jede Filiale nicht nur die postalische Adresse, sondern auch die exakte geografische Position vor. Diese bildet die Grundlage für alle weiterführenden räumlichen Auswertungen wie Kartenvisualisierungen, Clusteranalysen oder die Berechnung von regionalen Verteilungen.

Da die Geokodierung rechenintensiv ist und bei Supermarktketten mit mehr als 1.000 Filialen rund eine Stunde in Anspruch nimmt, wurden die Ergebnisse lokal gespeichert. Dadurch werden erneute Ausführungen desselben Rechenvorgangs – etwa nach dem Leeren des Environment – vermieden.

Der verwendete Code zum Abspeichern der Ergebnisse wird exemplarisch am Beispiel von Lidl gezeigt:

saveRDS(lidl_final_geo, "Dateipfad/lidl_final_geo.rds")

Zum Laden der Datei benutzen wir:

lidl_final_geo <- readRDS("Dateipfad/lidl_final_geo.rds") 

4 Analyse und Ergebnisse

4.1 Ruralitätsindex

Um die Verteilung der Supermarktstandorte besser analysieren zu können, wurde ein Ruralitätsindex definiert. Dieser ordnet Städte anhand ihrer Einwohnerzahl drei Größenklassen zu: Städte mit über 100.000 Einwohnern gelten als urban und erhalten den Indexwert 0, Städte mit 20.000 bis 99.999 Einwohnern gelten als mittelstädtisch und werden mit dem Wert 0,5 versehen, und Orte mit weniger als 20.000 Einwohnern gelten als ländlich mit dem Wert 1. Die Einwohnerdaten, die zuvor durch Webscraping von Wikipedia gewonnen wurden (vgl. Kapitel Datenbeschaffung), bilden die Grundlage für diese Klassifikation. Auf Basis der Zuordnung wurde jedem Ort sowohl eine Kategorie (stadt_typ) als auch ein numerischer Indexwert (rural_index) zugewiesen. Der Ruralitätsindex erlaubt damit eine quantitative Auswertung: je niedriger der Wert, desto urbaner die Lage des Supermarkts, während hohe Werte auf ländliche Standorte hinweisen. Die Umsetzung in R erfolgte durch eine Ergänzung der Städtedatenbank um diese Klassifikation:

library(dplyr)

staedte <- staedte %>%
  mutate(
    stadt_typ = case_when(
      Einwohner >= 100000 ~ "urban",
      Einwohner >= 20000  ~ "mittel",
      TRUE                ~ "laendlich"
    ),
    rural_index = case_when(
      stadt_typ == "urban"     ~ 0,
      stadt_typ == "mittel"    ~ 0.5,
      stadt_typ == "laendlich" ~ 1
    )
  )

Damit war die Städtedatenbank so vorbereitet, dass sie mit den Supermarktdaten verknüpft werden konnte. Alle Filialdaten der Supermarktketten Rewe, Lidl, Netto und Mixmarkt wurden in einem gemeinsamen Datensatz zusammengeführt und mit den Städtedaten verbunden. Für Orte, die nicht in der Städtedatenbank enthalten waren, erfolgte standardmäßig die Einstufung als ländlich mit einem Ruralitätsindex von 1.

rewe_final   <- rewe_final   %>% mutate(supermarkt = "Rewe")
lidl_final   <- lidl_final   %>% mutate(supermarkt = "Lidl")
netto_final  <- netto_final  %>% mutate(supermarkt = "Netto")
mixmarkt_final <- mixmarkt_final %>% mutate(supermarkt = "Mixmarkt")
kaufland_final <- kaufland_final %>% mutate(supermarkt = "Kaufland")
alnatura_final <- alnatura_final %>% mutate(supermarkt = "Alnatura")
edeka_final <- edeka_final %>% mutate(supermarkt = "Edeka")

filialdaten <- bind_rows(rewe_final, lidl_final, netto_final, mixmarkt_final, kaufland_final, alnatura_final, edeka_final)

filialdaten_joined <- filialdaten %>%
  left_join(staedte %>% select(Ort, rural_index, stadt_typ),
            by = c("ort" = "Ort")) %>%
  mutate(
    stadt_typ   = ifelse(is.na(stadt_typ), "laendlich", stadt_typ),
    rural_index = ifelse(is.na(rural_index), 1, rural_index)
  )

filialdaten_clean <- filialdaten_joined %>%
  select(supermarkt, ort, strasse, plz, Bundesland, stadt_typ, rural_index)

Auf dieser Grundlage konnten schließlich die Anteile der Filialen in urbanen, mittelstädtischen und ländlichen Regionen sowie der durchschnittliche Ruralitätsindex pro Supermarktkette berechnet werden.

library(tidyr)

anteile <- filialdaten_clean %>%
  group_by(supermarkt, stadt_typ) %>%
  summarise(anzahl = n(), .groups = "drop") %>%
  group_by(supermarkt) %>%
  mutate(anteil = anzahl / sum(anzahl)) %>%
  select(supermarkt, stadt_typ, anteil)

anteile_breit <- anteile %>%
  pivot_wider(
    names_from = stadt_typ,
    values_from = anteil,
    values_fill = 0,
    names_prefix = "anteil_"
  )

durchschnitt_index <- filialdaten_clean %>%
  group_by(supermarkt) %>%
  summarise(durchschnitt_ruralitaetsindex = mean(rural_index), .groups = "drop")

supermarkt_ruralitaet <- anteile_breit %>%
  left_join(durchschnitt_index, by = "supermarkt")

Die resultierende Tabelle liefert für jede Supermarktkette sowohl die Anteile ihrer Filialen in den drei Größenklassen als auch den durchschnittlichen Ruralitätsindex. Damit wird sichtbar, welche Ketten stärker im urbanen Raum konzentriert sind und welche tendenziell häufiger in ländlichen Regionen vertreten sind. Je näher der Ruralitätsindex ist, desto stärker ist die Präsenz der Kette in ländlichen Regionen. Die Ergebnisse sind in der folgenden Tabelle dargestellt. Sie zeigt für jede Supermarktkette den Anteil der Filialen in ländlichen, mittleren und urbanen Räumen sowie den jeweils berechneten durchschnittlichen Ruralitätsindex:

library(dplyr)
library(knitr)

tab_rur <- supermarkt_ruralitaet %>%
  rename(
    Supermarkt = supermarkt,
    `Anteil ländlich` = anteil_laendlich,
    `Anteil mittel`   = anteil_mittel,
    `Anteil urban`    = anteil_urban,
    `Durchschnittlicher Ruralitätsindex` = durchschnitt_ruralitaetsindex
  )

knitr::kable(
  tab_rur,
  digits = 3,
  row.names = FALSE,
  caption = "Ruralität der Supermärkte"
)   
Ruralität der Supermärkte
Supermarkt Anteil ländlich Anteil mittel Anteil urban Durchschnittlicher Ruralitätsindex
Alnatura 0.213 0.125 0.662 0.276
Edeka 0.507 0.192 0.301 0.603
Kaufland 0.328 0.409 0.262 0.533
Lidl 0.477 0.257 0.266 0.606
Mixmarkt 0.202 0.421 0.376 0.413
Netto 0.535 0.217 0.249 0.643
Rewe 0.433 0.230 0.337 0.548

Die Auswertung des Ruralitätsindex zeigt deutliche Unterschiede zwischen den untersuchten Ketten. Alnatura weist mit einem Anteil von rund zwei Dritteln die stärkste Konzentration auf urbane Standorte auf und erzielt entsprechend den niedrigsten Ruralitätsindex. Mixmarkt liegt ebenfalls stärker im urbanen Bereich, allerdings mit einem höheren Anteil an Filialen in mittleren Regionen. Demgegenüber haben Netto, Lidl und Edeka den höchsten ländlichen Anteil: Bei diesen liegt ungefähr die Hälfte ihrer Filialen in ländlichen Gebieten, was sich auch in den höchsten Ruralitätswerten widerspiegelt. Kaufland und Rewe bewegen sich mit ihren Verteilungen im Mittelfeld, wobei Rewe in ländlichen als auch urbanen Räumen eine nennenswerte Präsenz hat. Kaufland zeigt eine relativ ausgeglichene Verteilung zwischen den drei Raumtypen, mit einem besonders hohen Anteil in mittleren Regionen. Damit liefert die Analyse einen ersten Überblick über die strukturellen Unterschiede der Standortwahl, die im nächsten Schritt im Hinblick auf die Forschungsfrage weiter eingeordnet werden können.

4.2 Verteilung der Filialen auf Ost und Westdeutschland

Um die regionale Verteilung der Supermarkt- und Discounterfilialen im Hinblick auf Ost- und Westdeutschland zu untersuchen, wurde ein Ost-West-Index berechnet. Anders als bei der Berechnung über Postleitzahlen, die geografisch zwar eine gute Annäherung liefern, aber nicht immer eindeutig zugeordnet werden können, erfolgte die Einteilung hier direkt auf Basis der Bundesländer. Diese Information war in den bereinigten Filialtabellen bereits enthalten und konnte daher unmittelbar genutzt werden. Dazu wurde zunächst eine Liste jener Bundesländer erstellt, die traditionell zu Ostdeutschland gezählt werden: Brandenburg, Mecklenburg-Vorpommern, Sachsen, Sachsen-Anhalt, Thüringen sowie Berlin. Alle übrigen Bundesländer wurden der Kategorie Westdeutschland zugeordnet. Mit Hilfe dieser Zuordnung wurde in den Daten eine neue Variable ost_west angelegt, die jede Filiale einem der beiden Bereiche zuweist. Zusätzlich wurde eine numerische Variable ost_west_index eingeführt, die für Filialen in Ostdeutschland den Wert 1 und für Filialen in Westdeutschland den Wert 0 erhält. Damit konnte später ein durchschnittlicher Indexwert je Supermarktkette berechnet werden.

ost_bundeslaender <- c(
  "Brandenburg", "Mecklenburg-Vorpommern", "Sachsen",
  "Sachsen-Anhalt", "Thüringen", "Berlin"
)

filialdaten_clean <- filialdaten_clean %>%
  mutate(
    ost_west = ifelse(Bundesland %in% ost_bundeslaender, "Ost", "West"),
    ost_west_index = ifelse(ost_west == "Ost", 1, 0)
  )

Im nächsten Schritt wurden die relativen Anteile der Filialen nach Supermarktketten getrennt für Ost- und Westdeutschland berechnet. Hierfür wurde zunächst die Anzahl der Filialen je Kette und Region aggregiert. Anschließend wurde der Anteil ermittelt, den Ost- oder Westdeutschland an der Gesamtzahl der Filialen einer Kette ausmacht.

anteile_ost_west <- filialdaten_clean %>%
  group_by(supermarkt, ost_west) %>%
  summarise(anzahl = n(), .groups = "drop") %>%
  group_by(supermarkt) %>%
  mutate(anteil = anzahl / sum(anzahl))

Zur besseren Übersicht wurden diese Ergebnisse in eine breite Tabelle überführt, in der die Spalten anteil_Ost und anteil_West die prozentualen Verteilungen enthalten. Ergänzend dazu wurde der Durchschnittswert des ost_west_index für jede Supermarktkette berechnet. Dieser Wert liegt zwischen 0 (ausschließlich Westdeutschland) und 1 (ausschließlich Ostdeutschland).

anteile_ost_west_breit <- anteile_ost_west %>%
  pivot_wider(
    names_from = ost_west,
    values_from = anteil,
    values_fill = 0,
    names_prefix = "anteil_"
  )

durchschnitt_ost_west_index <- filialdaten_clean %>%
  group_by(supermarkt) %>%
  summarise(durchschnitt_ost_west_index = mean(ost_west_index), .groups = "drop")

Schließlich wurden beide Tabellen zusammengeführt, sodass eine Gesamtübersicht pro Supermarkt vorliegt. Diese zeigt sowohl die Anteile der Filialen in Ost- und Westdeutschland als auch den durchschnittlichen Ost-West-Index.

supermarkt_ost_west <- anteile_ost_west_breit %>%
  left_join(durchschnitt_ost_west_index, by = "supermarkt")

Damit liegt eine strukturierte Grundlage vor, um die Verteilung einzelner Ketten in Ost- und Westdeutschland zu vergleichen und mögliche regionale Unterschiede sichtbar zu machen.

library(dplyr)
library(tidyr)
library(knitr)

# Robust aus der *langen* Tabelle bauen -> garantiert eine Zeile pro Supermarkt
tab_ost_west <- anteile_ost_west %>%
  group_by(supermarkt, ost_west) %>%
  summarise(anzahl = sum(anzahl), .groups = "drop_last") %>%
  mutate(anteil = anzahl / sum(anzahl)) %>%
  ungroup() %>%
  pivot_wider(
    names_from = ost_west,
    values_from = anteil,
    values_fill = 0
  ) %>%
  # Durchschnittlicher Index anhängen
  left_join(durchschnitt_ost_west_index, by = "supermarkt") %>%
  # Spalten sauber benennen/formatieren und nur gewünschte Spalten behalten
  transmute(
    Supermarkt = supermarkt,
    `Anteil Ost`  = sprintf("%.1f%%", Ost  * 100),
    `Anteil West` = sprintf("%.1f%%", West * 100),
    `Durchschnittlicher Ost–West-Index` = round(durchschnitt_ost_west_index, 3)
  ) %>%
  arrange(Supermarkt)

knitr::kable(
  tab_ost_west,
  row.names = FALSE,
  caption = "Ost–West-Verteilung der Filialen nach Supermarktketten"
)
Ost–West-Verteilung der Filialen nach Supermarktketten
Supermarkt Anteil Ost Anteil West Durchschnittlicher Ost–West-Index
Alnatura 17.6% 0.0% 0.176
Alnatura 0.0% 82.4% 0.176
Edeka 18.0% 0.0% 0.180
Edeka 0.0% 82.0% 0.180
Kaufland 33.3% 0.0% 0.333
Kaufland 0.0% 66.7% 0.333
Lidl 18.7% 0.0% 0.187
Lidl 0.0% 81.3% 0.187
Mixmarkt 3.4% 0.0% 0.034
Mixmarkt 0.0% 96.6% 0.034
Netto 27.6% 0.0% 0.276
Netto 0.0% 72.4% 0.276
Rewe 16.1% 0.0% 0.161
Rewe 0.0% 83.9% 0.161

Die Ergebnisse zeigen deutliche Unterschiede zwischen den untersuchten Supermarktketten. Alnatura weist mit einem Ost-Anteil von etwa 18 % eine klare Konzentration im Westen auf, ähnlich wie Edeka (18 %) und Rewe (16 %). Lidl liegt mit knapp 19 % Ost-Anteil in einem vergleichbaren Bereich. Netto ist stärker im Osten vertreten: Mit einem Ost-Anteil von rund 28 % erreicht die Kette den zweithöchsten Wert im Vergleich. Noch deutlicher fällt die Ost-Verteilung bei Kaufland aus, das mit einem Anteil von etwa 34 % die höchste Präsenz in Ostdeutschland zeigt. Mixmarkt bildet das andere Extrem. Mit nur knapp 4 % der Filialen im Osten und einem durchschnittlichen Ost-West-Index von 0,039 liegt die Kette fast ausschließlich im Westen. Insgesamt liegen die berechneten Ost-West-Indizes für alle Ketten unter 0,5, was bedeutet, dass die Filialverteilung insgesamt westlastig ist, auch wenn Kaufland und Netto im Vergleich stärker im Osten vertreten sind.

4.3 Filialen pro 100.000 Einwohnern

Um die Verfügbarkeit von Supermärkten und Discountern in den einzelnen Bundesländern vergleichbar zu machen, wurde zunächst eine Kennzahl „Filialen pro 100.000 Einwohner“ berechnet. Ausgangspunkt waren die bereits bereinigten Filialdaten von Rewe, Netto und weiteren Ketten, die durch die Zuordnung der Postleitzahlen zu den Bundesländern ergänzt wurden. Parallel dazu wurde die aktuelle Bevölkerungszahl für jedes Bundesland per Webscraping von Wikipedia gewonnen. Die relevanten Tabellen mussten dabei aus der HTML-Struktur extrahiert, anschließend bereinigt und auf die Spalte mit den Werten für 2023 reduziert werden. Der entsprechende Code sah folgendermaßen aus:

url <- "https://de.wikipedia.org/wiki/Liste_der_deutschen_Bundesl%C3%A4nder_nach_Bev%C3%B6lkerungsentwicklung"

# Tabellen extrahieren
tabellen <- url %>%
  read_html() %>%
  html_table(fill = TRUE)

bundeslaender_roh <- tabellen[[1]]

bundeslaender <- bundeslaender_roh %>%
  select(
    Bundesland = 1,
    Bevoelkerung_2023 = 4
  ) %>%
  mutate(
    # Fußnoten entfernen
    Bundesland = str_remove_all(Bundesland, "\\[.*?\\]"),
    Bundesland = str_squish(Bundesland),
    # Doppelte vollständige Bundeslandnamen entfernen (inkl. Bindestriche & Leerzeichen)
    Bundesland = str_replace(Bundesland, "^([A-Za-zÄÖÜäöüß\\- ]+)\\s+\\1$", "\\1"),
    Bundesland = str_squish(Bundesland),
    # Bevölkerung bereinigen
    Bevoelkerung_2023 = str_remove_all(Bevoelkerung_2023, "\\.") %>%
      str_remove_all("[^0-9]") %>%
      as.integer()
  ) %>%
  filter(
    !is.na(Bevoelkerung_2023),
    !str_detect(Bundesland, "Land / Jahr"),
    !str_detect(Bundesland, "Deutschland")
  ) %>%
  distinct(Bundesland, .keep_all = TRUE)

Die gewonnenen Bevölkerungszahlen wurden anschließend mit den Filialdaten gematcht, sodass die absoluten Filialzahlen auf die Einwohnerzahlen der Bundesländer bezogen werden konnten. Dadurch entsteht eine Kennzahl, die angibt, wie viele Filialen pro 100.000 Einwohner in einem Bundesland existieren. Diese Maßzahl erlaubt eine faire Bewertung, da große Bundesländer wie Nordrhein-Westfalen oder Bayern naturgemäß mehr Filialen haben, dies aber nicht zwangsläufig eine höhere Versorgungsdichte bedeutet. Im ersten Schritt wurden die einzelnen Filialdaten der verschiedenen Handelsketten vereinheitlicht. Dabei wurden die Bundesländer bereinigt, um ein konsistentes Matching mit den Bevölkerungszahlen sicherzustellen. Danach erfolgte ein Join mit den aus Wikipedia gescrapten Bevölkerungsdaten. Anschließend wurde die Dichte berechnet, indem die Anzahl der Filialen je Bundesland durch die Einwohnerzahl dividiert und mit 100.000 multipliziert wurde.

library(dplyr)
library(stringr)

# -----------------------
# 1. Discounter (Netto + Lidl)
# -----------------------
discounter <- bind_rows(
  lidl_final %>% mutate(supermarkt = "Lidl"),
  netto_final %>% mutate(supermarkt = "Netto")
)

# Bundesland-Spalte bereinigen
discounter <- discounter %>%
  mutate(Bundesland = str_squish(Bundesland))

# Bevölkerung anhängen
bundeslaender <- bundeslaender %>%
  mutate(Bundesland = str_squish(Bundesland))

discounter <- discounter %>%
  left_join(bundeslaender %>% select(Bundesland, Bevoelkerung_2023),
            by = "Bundesland")

# Filialen pro 100.000 Einwohner berechnen
discounter_pro_kopf <- discounter %>%
  group_by(Bundesland) %>%
  summarise(
    anzahl_filialen = n(),
    bevoelkerung = first(Bevoelkerung_2023),
    filialen_pro_100000 = anzahl_filialen / bevoelkerung * 100000
  ) %>%
  arrange(desc(filialen_pro_100000))

View(discounter_pro_kopf)


# -----------------------
# 2. Supermärkte (Rewe + Edeka)
# -----------------------
supermaerkte <- bind_rows(
  rewe_final %>% mutate(supermarkt = "Rewe"),
  edeka_final %>% mutate(supermarkt = "Edeka")
)

# Bundesland-Spalte bereinigen
supermaerkte <- supermaerkte %>%
  mutate(Bundesland = str_squish(Bundesland))

# Bevölkerung anhängen
supermaerkte <- supermaerkte %>%
  left_join(bundeslaender %>% select(Bundesland, Bevoelkerung_2023),
            by = "Bundesland")

# Filialen pro 100.000 Einwohner berechnen
supermaerkte_pro_kopf <- supermaerkte %>%
  group_by(Bundesland) %>%
  summarise(
    anzahl_filialen = n(),
    bevoelkerung = first(Bevoelkerung_2023),
    filialen_pro_100000 = anzahl_filialen / bevoelkerung * 100000
  ) %>%
  arrange(desc(filialen_pro_100000))

View(supermaerkte_pro_kopf)

4.4 Zusammenhang zwischen Filialen pro 100.000 Einwohnern und Medianeinkommen in den Bundesländern

Nachdem zunächst die Filialdichte pro 100.000 Einwohner für die Supermärkte und Discounter berechnet wurde, war es möglich, die Werte mit sozioökonomischen Indikatoren in Beziehung zu setzen. Daher wurde zunächst die Korrelation zwischen der Filialdichte pro 100.000 Einwohner und dem Medianeinkommen der Bundesländer getrennt für Supermärkte und Discounter berechnet. Dafür wurden zunächst die zuvor erstellten Tabellen mit den berechneten Filialen pro Kopf für beide Gruppen mit den gescrapten Daten zum Medianeinkommen nach Bundesland zusammengeführt.

Zuerst wurden die Tabellen zusammengeführt:

library(dplyr); library(stringr); library(tidyr)

# 1) Referenzliste der 16 Länder
bundeslaender_vec <- c(
  "Baden-Württemberg","Bayern","Berlin","Brandenburg","Bremen","Hamburg",
  "Hessen","Mecklenburg-Vorpommern","Niedersachsen","Nordrhein-Westfalen",
  "Rheinland-Pfalz","Saarland","Sachsen","Sachsen-Anhalt","Schleswig-Holstein","Thüringen"
)

# 2) Hilfsfunktion: BL-Namen robust normalisieren
norm_bl <- function(x){
  x %>%
    str_replace_all("[\u2010-\u2015\u2212\u2043\uFE58\uFE63\uFF0D]", "-") %>%  # exot. Bindestriche
    str_replace_all("Baden\\s*W[uü]rttemberg|Baden\\s*Wuerttemberg", "Baden-Württemberg") %>%
    str_replace_all("Mecklenburg\\s*-?\\s*Vorpommern", "Mecklenburg-Vorpommern") %>%
    str_replace_all("Rheinland\\s*-?\\s*Pfalz", "Rheinland-Pfalz") %>%
    str_replace_all("Sachsen\\s*-?\\s*Anhalt", "Sachsen-Anhalt") %>%
    str_replace_all("Schleswig\\s*-?\\s*Holstein", "Schleswig-Holstein") %>%
    str_replace_all("Thueringen|Thuringen", "Thüringen") %>%
    str_squish()
}

# 3) Einkommen säubern & auf die 16 BL bringen
medianeinkommen_clean <- medianeinkommen %>%
  mutate(Bundesland = norm_bl(Bundesland)) %>%
  filter(Bundesland %in% bundeslaender_vec) %>%
  distinct(Bundesland, .keep_all = TRUE)

# 4) Pro-Kopf-Tabellen säubern und vervollständigen (fehlende Länder = 0)
supermaerkte_pro_kopf_clean <- supermaerkte_pro_kopf %>%
  mutate(Bundesland = norm_bl(Bundesland)) %>%
  complete(Bundesland = bundeslaender_vec,
           fill = list(anzahl_filialen = 0, filialen_pro_100000 = 0)) %>%
  arrange(Bundesland)

discounter_pro_kopf_clean <- discounter_pro_kopf %>%
  mutate(Bundesland = norm_bl(Bundesland)) %>%
  complete(Bundesland = bundeslaender_vec,
           fill = list(anzahl_filialen = 0, filialen_pro_100000 = 0)) %>%
  arrange(Bundesland)

# 5) Joins für die Korrelation
korrelation_sup_df <- supermaerkte_pro_kopf_clean %>%
  inner_join(medianeinkommen_clean, by = "Bundesland")

korrelation_dis_df <- discounter_pro_kopf_clean %>%
  inner_join(medianeinkommen_clean, by = "Bundesland")

# 6) Mini-Diagnose (optional)
fehl_eink <- setdiff(bundeslaender_vec, medianeinkommen_clean$Bundesland)
if (length(fehl_eink) > 0) message("In den Einkommensdaten fehlen: ", paste(fehl_eink, collapse = ", "))

Anschließend werden die Scatterplots mit Trendlinie gezeichnet:

library(ggplot2)

# Korrelationen (nur zur Anzeige im Untertitel)
cor_sup <- if (nrow(korrelation_sup_df) >= 2)
  cor(korrelation_sup_df$filialen_pro_100000, korrelation_sup_df$Einkommen) else NA
cor_dis <- if (nrow(korrelation_dis_df) >= 2)
  cor(korrelation_dis_df$filialen_pro_100000, korrelation_dis_df$Einkommen) else NA

# Plot: Supermärkte
ggplot(korrelation_sup_df, aes(x = Einkommen, y = filialen_pro_100000, label = Bundesland)) +
  geom_point(size = 3) +
  geom_smooth(method = "lm", se = FALSE) +
  geom_text(vjust = -1, size = 3) +
  labs(
    title = "Zusammenhang: Supermarktfilialdichte vs. Medianeinkommen",
    subtitle = ifelse(is.na(cor_sup), "r = n/v", paste0("r = ", round(cor_sup, 3))),
    x = "Medianeinkommen (EUR)",
    y = "Filialen je 100.000 Einwohner"
  ) +
  theme_minimal()

# Plot: Discounter
ggplot(korrelation_dis_df, aes(x = Einkommen, y = filialen_pro_100000, label = Bundesland)) +
  geom_point(size = 3) +
  geom_smooth(method = "lm", se = FALSE) +
  geom_text(vjust = -1, size = 3) +
  labs(
    title = "Zusammenhang: Discounterfilialdichte vs. Medianeinkommen",
    subtitle = ifelse(is.na(cor_dis), "r = n/v", paste0("r = ", round(cor_dis, 3))),
    x = "Medianeinkommen (EUR)",
    y = "Filialen je 100.000 Einwohner"
  ) +
  theme_minimal()

Zunächst wurde die Pearson-Korrelation berechnet:

# Korrelation – nur wenn ≥ 2 vollständige Paare vorhanden
if (nrow(korrelation_sup_df) >= 2) {
correlation_sup <- cor(korrelation_sup_df$filialen_pro_100000,
korrelation_sup_df$Einkommen,
method = "pearson")
print(paste("Korrelation zwischen Supermarktfilialen pro 100.000 Einwohner und Medianeinkommen:", round(correlation_sup, 3)))
} else {
message("Zu wenige vollständige Paare für Supermärkte (n=", nrow(korrelation_sup_df), "). Prüfe die Diagnoseausgaben oben.")
}
## [1] "Korrelation zwischen Supermarktfilialen pro 100.000 Einwohner und Medianeinkommen: 0.318"
if (nrow(korrelation_dis_df) >= 2) {
correlation_dis <- cor(korrelation_dis_df$filialen_pro_100000,
korrelation_dis_df$Einkommen,
method = "pearson")
print(paste("Korrelation zwischen Discounterfilialen pro 100.000 Einwohner und Medianeinkommen:", round(correlation_dis, 3)))
} else {
message("Zu wenige vollständige Paare für Discounter (n=", nrow(korrelation_dis_df), "). Prüfe die Diagnoseausgaben oben.")
}
## [1] "Korrelation zwischen Discounterfilialen pro 100.000 Einwohner und Medianeinkommen: -0.875"

Der Code arbeitet in zwei Schritten: Zunächst wird ein inner_join genutzt, um sicherzustellen, dass sowohl die Daten zur Filialendichte als auch die Einkommensdaten für jedes Bundesland vorhanden sind. Anschließend wird mit der Funktion cor() die Pearson-Korrelation berechnet. Dabei wird geprüft, ob es einen linearen Zusammenhang zwischen der Anzahl an Filialen pro 100.000 Einwohner und dem Einkommen gibt. Das Ergebnis zeigt zwei Werte: Einer beschreibt die Stärke und Richtung des Zusammenhangs zwischen Supermärkten und Einkommen, der andere zwischen Discountern und Einkommen. Werte nahe +1 weisen auf einen starken positiven Zusammenhang hin, Werte nahe –1 auf einen starken negativen Zusammenhang. Es konnte festgestellt werden, dass die Korrelation für Discounter mit negativ ist, während sie für klassische Supermärkte wie Rewe und Edeka positiv ausfällt. Die Korrelation der Discounter mit -0,875 sehr stark, während sie für die Supermärkte mit nur 0,318 weniger stark ist. Dies legt nahe, dass Discounter eher in einkommensschwächeren Regionen stärker vertreten sind, während Supermärkte in wohlhabenderen Regionen eine höhere Filialdichte aufweisen.

5 Visualisierung

5.1 Verteilung der Filialen für die einzelnen Ketten

## ### ALNATURA

## 
## 
## ### EDEKA

## 
## 
## ### KAUFLAND

## 
## 
## ### LIDL

## 
## 
## ### MIXMARKT

## 
## 
## ### NETTO

## 
## 
## ### REWE

## Übersicht der Gesamtverteilung aller Ketten:

5.2 Filialen pro 100.000 Einwohner pro Bundesland

Filialen je 100.000 Einwohner nach Bundesland
Bundesland Anzahl Filialen Bevölkerung (2023) Filialen je 100.000
Thüringen 385 2.114.870 18,20
Mecklenburg-Vorpommern 277 1.578.041 17,55
Brandenburg 428 2.554.464 16,75
Hessen 1041 6.267.546 16,61
Sachsen 657 4.054.689 16,20
Saarland 156 1.014.047 15,38
Bremen 108 702.655 15,37
Rheinland-Pfalz 623 4.125.163 15,10
Niedersachsen 1196 8.008.135 14,93
Sachsen-Anhalt 319 2.144.570 14,87
Nordrhein-Westfalen 2656 18.017.520 14,74
Bayern 1928 13.176.426 14,63
Schleswig-Holstein 412 2.953.202 13,95
Berlin 501 3.662.381 13,68
Baden-Württemberg 1461 11.230.740 13,01
Hamburg 211 1.851.596 11,40
NA 1 NA NA

5.3 Korrelation Medianeinkommen und Filialen pro 100.000 Einwohner:

6 Interpretation und Diskussion der Ergebnisse

Die Analyse mithilfe des Ruralitäts- sowie Ost-West-Index verdeutlicht, dass die Standortstrategien der untersuchten Supermarktketten nicht zufällig sind, sondern erkennbaren Mustern folgen, die sich mit theoretischen Annahmen aus dem Forschungsstand weitgehend decken. Die Ergebnisse zeigen, dass sich verschiedene Ketten in ihrer Standortwahl klar unterscheiden. Alnatura konzentriert sich fast ausschließlich auf urbane Räume, was mit der Zielgruppe einer kaufkräftigen und vielleicht ernährungsbewussten Stadtbevölkerung übereinstimmt. Mixmarkt, als Nischenanbieter für osteuropäische Produkte, weist ebenfalls eine starke Orientierung an Städten auf. Rewe und Lidl nehmen eine Mittelposition ein, indem sie sowohl urbane als auch ländliche Gebiete bedienen. Besonders auffällig ist jedoch die starke Präsenz von Netto und Edeka im ländlichen Raum: Über die Hälfte ihrer Filialen liegt in kleineren Orten mit unter 20.000 Einwohnern. Dieses Muster bestätigt die These also nicht eindeutig. Auch die Verteilung nach Ost- und Westdeutschland weist Unterschiede auf. Während Rewe, Alnatura, Lidl und Edeka stärker westlich geprägt sind, hat Netto mit einem Ost-Anteil von fast 30 % eine signifikant höhere Präsenz in den neuen Bundesländern. Dies könnte daran liegen, dass vielleicht im Osten eine traditionell stärkere Discount-Ausrichtung bestand, zum anderen könnte sich darin die im Durchschnitt geringere Kaufkraft widerspiegeln. Ebenfalls sticht Kaufland in der Ost-West-Verteilung hervor, da sie die einzige Lebensmittelkette in unserer Beobachtung sind, die zumindest ein Drittel, und somit den größten prozentualen Anteil aller beobachteten Lebensmittelanbietern, in den neuen Bundesländern stellt. Auffällig ist jedoch, dass der Unterschied zwischen den Ketten weniger stark ausfällt, als man annehmen könnte – selbst im Osten bleibt das Angebot divers, und auch Supermärkte wie Rewe oder Edeka sind dort vertreten, wenn auch in geringerem Umfang. Die unterschiedlichen Muster lassen sich inhaltlich mit der strategischen Ausrichtung der Ketten verbinden. Supermärkte wie Rewe oder Alnatura zielen auf Kundengruppen mit höherer Zahlungsbereitschaft und setzen daher auf Standorte in kaufkräftigen, urbanen Regionen. Discounter wie Netto oder Lidl bedienen stärker preissensible Kundengruppen und weiten ihr Netz gezielt in weniger wohlhabenden oder ländlichen Regionen aus. Das bestätigt die in der Forschungsfrage formulierte Hypothese eines Zusammenhangs zwischen Einkommen und Standortstruktur. Allerdings zeigen die Daten auch, dass es Überschneidungen gibt: Lidl beispielsweise verfügt sowohl in ländlichen als auch urbanen und mittleren Gebieten über eine hohe Präsenz, was auf eine Strategie der breiten Marktabdeckung schließen lässt. Die Berechnungen der Pearson-Korrelation zeigen zwei entgegengesetzte Muster für Supermärkte und Discounter. Für die Gruppe der Supermärkte (Rewe und Edeka) ergibt sich, dass in Regionen mit höherem Einkommen tendenziell mehr Supermarktfilialen pro Kopf vorhanden sind. Diese Tendenz deckt sich mit der Annahme, dass Supermärkte mit einem breiteren Sortiment, höheren Preisen und zusätzlichen Serviceleistungen stärker auf kaufkräftige Zielgruppen ausgerichtet sind. Für die Gruppe der Discounter (Netto und Lidl) zeigt sich hingegen eine negative Korrelation. Je niedriger das Medianeinkommen in einem Bundesland, desto höher ist die Dichte an Discounterfilialen. Dieses Ergebnis passt zu der strategischen Positionierung der Discounter, die durch günstige Preise und ein reduziertes Sortiment preissensible Kundengruppen ansprechen. Insbesondere in wirtschaftlich schwächeren Regionen erfüllen sie damit eine zentrale Rolle in der Grundversorgung. Diese Ergebnisse bestätigen die im Forschungsstand entwickelte Hypothese und verdeutlichen, dass sozioökonomische Strukturen maßgeblich zur Erklärung der Verteilung beitragen. Aus Sicht der Bevölkerung bedeutet diese Verteilung, dass die Grundversorgung mit Lebensmitteln auch in ländlichen Regionen durch Discounter wie Netto oder Edeka gewährleistet bleibt. Allerdings könnte dies zu einer gewissen Versorgungsungleichheit führen: Während Stadtbewohner Zugang zu einem diversifizierten Angebot einschließlich hochwertiger Supermärkte und Bioketten haben, sind ländliche Gebiete stärker auf ein reduziertes und preisorientiertes Sortiment angewiesen.

7 Kritische Reflexion der Projektarbeit

Insgesamt können wir mit den Ergebnissen unserer Untersuchung zufrieden sein. Besonders im Bereich der Korrelation zwischen Filialdichte und Medianeinkommen wurden unsere Erwartungen bestätigt: Discounter sind in einkommensschwächeren Regionen deutlich stärker vertreten, während Supermärkte eher in wohlhabenderen Bundesländern eine höhere Dichte aufweisen. Diese Befunde stützen unsere Ausgangsthese und haben uns inhaltlich bestätigt. Positiv überrascht waren wir hingegen davon, dass die Unterschiede zwischen ländlichen und urbanen Räumen weniger klar ausfallen, als zunächst vermutet. Viele Ketten, wie etwa Lidl oder Rewe, verfolgen eine breite Standortstrategie und sind sowohl in Städten als auch im ländlichen Raum präsent. Auffällig war dagegen die Ost-West-Verteilung, bei der teilweise deutliche Ungleichgewichte sichtbar wurden – etwa eine stärkere Präsenz von Netto und Kaufland im Osten. Diese Ergebnisse sollten jedoch vorsichtig interpretiert werden, da unsere Analyse nicht alle relevanten Anbieter einschließt.

Methodisch war das Webscraping für uns ein zentraler Bestandteil des Projekts. Es hat uns ermöglicht, eigenständig Daten zu erheben und zu verarbeiten, brachte aber auch Schwierigkeiten mit sich. Fehlerhafte Postleitzahlen, verschobene Einträge oder abweichende Filialzahlen gegenüber den offiziellen Angaben zeigen die Grenzen dieser Methode. Zudem ist die Quelle meinprospekt.de nicht immer aktuell oder vollständig, was die Validität einschränkt. Wir haben versucht, diese Probleme durch Datenbereinigung, Plausibilitätsprüfungen und Geocoding abzufedern, mussten aber akzeptieren, dass gewisse Unsicherheiten bestehen bleiben.

Ein weiterer Punkt betrifft unsere Auswahl der betrachteten Ketten. Während wir fünf Supermärkte, aber nur zwei Discounter ausgewertet haben, entstand ein deutliches Ungleichgewicht. Für die Korrelationen haben wir dieses Problem dadurch entschärft, dass wir nur zwei Supermärkte und zwei Discounter direkt miteinander verglichen haben. Dennoch bleibt festzuhalten, dass eine umfassendere Analyse – insbesondere unter Einbeziehung von Aldi Nord und Süd, Penny oder Norma – notwendig wäre, um die Ergebnisse zu verallgemeinern. Auch unsere eigene Perspektive hat eine Rolle gespielt: Da wir beide in Frankfurt leben, war unsere Auswahl stark von den Ketten geprägt, die wir aus dem Alltag kennen.

Schließlich muss auch die räumliche Ebene kritisch betrachtet werden. Die Berechnungen auf Bundeslandebene liefern einen ersten Überblick, verbergen aber regionale Unterschiede innerhalb der Länder. Eine Analyse auf Kreis- oder Gemeindeebene wäre hier deutlich aussagekräftiger. Ebenso gilt es zu betonen, dass Korrelationen keine Kausalitäten erklären. Einkommen allein bestimmt nicht die Standortwahl, auch Faktoren wie Mieten, Kaufkraft, Verkehrsanbindung oder politische Rahmenbedingungen spielen eine Rolle.

8 Fazit

Zusammenfassend lässt sich sagen, dass unser Projekt wichtige Einblicke in die Standortstrategien von Supermärkten und Discountern bietet, auch wenn es methodische Grenzen und Datenunsicherheiten gibt. Unsere Analyse zeigt, dass die Standortstrategien von Supermärkten und Discountern stark mit sozioökonomischen und regionalen Faktoren verknüpft sind. Während Supermärkte tendenziell in einkommensstärkeren, urbanen und westdeutschen Regionen dominieren, sind Discounter besonders in einkommensschwächeren, ländlichen und ostdeutschen Gebieten stark vertreten. Trotz methodischer Grenzen liefert das Projekt wertvolle Einblicke in die Verteilung des Lebensmitteleinzelhandels in Deutschland und eröffnet Perspektiven für weiterführende Untersuchungen etwa zur Versorgungsgerechtigkeit oder zu regionalen Konsumstrukturen. Für eine zukünftige Erweiterung wäre es sinnvoll, zusätzliche Ketten zu berücksichtigen, offiziellere Datenquellen einzubeziehen und die Analysen auf einer feineren räumlichen Ebene durchzuführen.

Literaturverzeichnis

Ahlert, D., Kenning, P., & Brock, C. (2018). Handelsmarketing: Grundlagen der marktorientierten Führung von Handelsbetrieben. Springer Gabler. https://doi.org/10.1007/978-3-662-54802-0
CEBUS GmbH. (o. J.). PLZ nach Bundesland: Postleitzahlen-Bereiche der Bundesländer in Deutschland. Abgerufen 30. September 2025, von https://cebus.net/de/plz-bundesland.htm
finanz.de. (o. J.). Medianeinkommen nach Bundesländern. Abgerufen 30. September 2025, von https://www.finanz.de/gehalt/#medianeinkommen_nach_bundesl%C3%A4ndern
GADM. (o. J.). GADM 4.1: Administrative Grenzen Deutschlands (Level 1) — Shapefile. Abgerufen 30. September 2025, von https://geodata.ucdavis.edu/gadm/gadm4.1/shp/gadm41_DEU_shp.zip
Handelsverband Deutschland (HDE), & IFH Köln. (2024). Handelsreport Lebensmittel. Handelsverband Deutschland (HDE) & IFH Köln.
MeinProspekt GmbH. (o. J.-a). Alnatura – Filialübersicht (Seiten 0–9). Abgerufen 30. September 2025, von https://www.meinprospekt.de/filialen/alnatura-de/
MeinProspekt GmbH. (o. J.-b). EDEKA – Filialübersicht (Seiten 0–9). Abgerufen 30. September 2025, von https://www.meinprospekt.de/filialen/edeka/
MeinProspekt GmbH. (o. J.-c). Kaufland – Filialübersicht (Seiten 0–9). Abgerufen 30. September 2025, von https://www.meinprospekt.de/filialen/kaufland/
MeinProspekt GmbH. (o. J.-d). Lidl – Filialübersicht (Seiten 0–9). Abgerufen 30. September 2025, von https://www.meinprospekt.de/filialen/lidl/
MeinProspekt GmbH. (o. J.-e). Mix Markt – Filialübersicht (Seiten 0–9). Abgerufen 30. September 2025, von https://www.meinprospekt.de/filialen/mixmarkt-de/
MeinProspekt GmbH. (o. J.-f). Netto Marken-Discount – Filialübersicht (Seiten 0–9). Abgerufen 30. September 2025, von https://www.meinprospekt.de/filialen/netto-marken-discount-de/
MeinProspekt GmbH. (o. J.-g). REWE – Filialübersicht (Seiten 0–9). Abgerufen 30. September 2025, von https://www.meinprospekt.de/filialen/rewe-de/
OpenStreetMap contributors. (o. J.). Nominatim — OpenStreetMap Geocoding (via tidygeocoder). Abgerufen 30. September 2025, von https://nominatim.openstreetmap.org/
The R Foundation. (o. J.). CRAN Spiegel: cloud.r-project.org. Abgerufen 30. September 2025, von https://cloud.r-project.org
Wikipedia contributors. (o. J.-a). Liste der deutschen Bundesländer nach Bevölkerungsentwicklung. Abgerufen 30. September 2025, von https://de.wikipedia.org/wiki/Liste_der_deutschen_Bundesl%C3%A4nder_nach_Bev%C3%B6lkerungsentwicklung
Wikipedia contributors. (o. J.-b). Liste der Groß- und Mittelstädte in Deutschland. Abgerufen 30. September 2025, von https://de.wikipedia.org/wiki/Liste_der_Gro%C3%9F-_und_Mittelst%C3%A4dte_in_Deutschland