6  Standortplanung II – Diskrete Netzwerkplanungsmodelle

Lernziele

Nach diesem Kapitel können Sie …

  • diskrete Standort- und Netzwerkplanungsprobleme formulieren,
  • Transport-, Umschlags- und Standortentscheidungen in einem Netzwerk modellieren,
  • zentrale Entscheidungsvariablen, Zielfunktionen und Nebenbedingungen interpretieren,
  • Ergebnisse diskreter Netzwerkmodelle für strategische Standortentscheidungen bewerten,
  • ein WLP mit dem R-Paket ompr exakt lösen und das Ergebnis ökonomisch einordnen,
  • die Grundideen konstruktiver Add- und Drop-Heuristiken nachvollziehen, in R anwenden und ihre Lösungsgüte beurteilen,
  • Listen, purrr::map() und Matrizen als R-Werkzeuge zur Strukturierung und Auswertung von Netzwerkplanungsdaten einsetzen.

Literatur zur Vertiefung

  • Transport-, Umschlags- und Netzwerkmodelle: Günther und Tempelmeier (2012), Kapitel 4.
  • Diskrete Standortmodelle, Algorithmen und Anwendungen: Daskin (2013), Kapitel 1–4.
  • Theorie und Anwendungen der Standortplanung: Drezner und Hamacher (2002).

6.1 Von der kontinuierlichen zur diskreten Standortplanung

Im vorangegangenen Kapitel haben Sie gelernt, wie man mit dem Center-of-Gravity-Ansatz und dem Weiszfeld-Algorithmus einen beliebigen Punkt in der Ebene bestimmt, der die gewichteten Entfernungen zu einer Menge von Nachfragepunkten minimiert. Am Beispiel der SushiFresh GmbH – einem regionalen Hersteller frischer Sushi-Boxen im Rhein-Main-Gebiet, der täglich sechs Supermarkt-Filialen beliefert – lag der so berechnete Optimalpunkt zwischen Raunheim und Frankfurt, deutlich zur nachfragestärksten Filiale Frankfurt hin verschoben. Dieser kontinuierliche Blick auf die Standortwahl ist analytisch elegant, in der unternehmerischen Praxis aber nur bedingt hilfreich: SushiFresh kann eine Produktionsstätte nicht exakt an den berechneten Koordinaten errichten, sondern muss unter den tatsächlich verfügbaren Gewerbegrundstücken, Hallen oder Bestandsimmobilien auswählen. Genehmigungsrecht, Grundstückspreise, Verkehrsanbindung und Lebensmittelhygiene-Auflagen schränken die Menge realistisch nutzbarer Standorte auf eine endliche, vorab bekannte Menge von Alternativen ein. Genau dies ist der Ausgangspunkt der diskreten Standortplanung: Anstelle eines Optimierungsproblems über die gesamte Ebene \(\mathbb{R}^2\) steht eine Auswahlentscheidung aus einer festen Liste potenzieller Standorte im Mittelpunkt.

In Kapitel 5 wurde die Standortfrage für SushiFresh bereits zweimal aufgegriffen: Zunächst wurde mittels AHP (Analytic Hierarchy Process) eine qualitative Bewertung dreier Kandidatenstandorte vorgenommen – mit dem Ergebnis, dass Mainz-Hechtsheim knapp vor Raunheim lag, wobei die Kriteriengewichte auf nicht ganz konsistenten Urteilen beruhten. Anschließend lieferte der Center-of-Gravity-/Weiszfeld-Ansatz einen kontinuierlichen Optimalpunkt in der Mitte des Absatzgebiets. Dieses Kapitel stellt die Standortfrage ein drittes Mal – nun auf Basis harter Kostenzahlen: fixe Kosten für den Betrieb eines Standorts und variable Transportkosten zu den belieferten Filialen. Wie sich zeigen wird, kommt eine rein kostenbasierte, diskrete Analyse zu einem anderen Ergebnis als die qualitative Vorauswahl – und genau diese Spannung zwischen beiden Verfahren wird am Ende des Kapitels explizit diskutiert.

Definition: Diskrete Standortplanung

Bei der diskreten Standortplanung wird aus einer endlichen Menge vorab definierter, potenzieller Standorte diejenige Teilmenge ausgewählt, die eine gegebene Zielgröße (in der Regel die Summe aus fixen Standort- und variablen Transportkosten) minimiert. Im Gegensatz zur kontinuierlichen Standortplanung ist der Lösungsraum nicht mehr stetig, sondern kombinatorisch: Jede mögliche Teilmenge von Standorten stellt eine zulässige Lösung dar.

Diskrete Standortentscheidungen sind stets eingebettet in ein größeres Supply-Chain-Netzwerk, das sich grob in die Stufen Beschaffung, Produktion, Distribution und Absatz gliedern lässt. Auf jeder dieser Stufen können Knoten (Lieferantenwerke, Produktionsstätten, Lagerhäuser, Cross-Docking-Punkte) geöffnet, geschlossen oder miteinander verbunden werden; man spricht in diesem Zusammenhang auch von Netzwerkdesign. Die Vorlesung betont dabei, dass in der Praxis meist nur ein Ausschnitt des Gesamtnetzes betrachtet wird: Man wählt konkrete Standorte auf einer oder zwei Stufen aus und hält die übrigen Stufen fest. Dieses Kapitel konzentriert sich entsprechend auf die Frage, welche Produktions- und Distributionsstandorte SushiFresh betreiben und welche Filiale von welchem Standort beliefert werden soll. Die vorgestellten Modelle lassen sich jedoch unmittelbar auf andere Netzwerkstufen übertragen.

Wir nähern uns der diskreten Standortplanung in zwei Schritten. Zunächst betrachten wir kurz den Spezialfall, dass die Standorte bereits feststehen und nur noch die Liefermengen optimal auf die Filialen verteilt werden müssen – das klassische Transportproblem, dessen Lösungsverfahren Kapitel 7 vertieft. Anschließend erweitern wir das Modell um die eigentliche Standortentscheidung: das Warehouse Location Problem (WLP), bei dem sowohl die Frage „wo?” als auch die Frage „wie viel wohin?” simultan zu beantworten ist.

6.2 Fallbeispiel: SushiFresh GmbH

Fallstudie: Entscheidungssituation

SushiFresh muss entscheiden, welche Kandidatenstandorte für Produktion und Distribution ausgebaut werden und welche Filiale von welchem Standort versorgt werden soll. Das Unternehmen möchte die Entscheidung so treffen, dass die jährlichen Gesamtkosten aus Fixkosten und Transport minimiert werden. Gegenüber Kapitel 5 wurde die Liste der Kandidaten um zwei weitere Gewerbeflächen erweitert: Frankfurt-Höchst und Neu-Isenburg, beide näher an den nachfragestarken Filialen im Osten des Absatzgebiets gelegen.

Die sechs Filialen (Nachfrageknoten \(j\), in der Vorlesung als C1–C6 bezeichnet) mit ihrer mittleren täglichen Nachfrage \(\mu_j\) und der Nachfragestreuung \(\sigma_j\), die uns erst in Kapitel 8 wieder begegnet:

Code
filialen_fall <- tibble(
  ID    = paste0("C", 1:6),
  Filiale = c("Wiesbaden", "Mainz", "Frankfurt", "Ruesselsheim", "Darmstadt", "Offenbach"),
  lat   = c(50.0782, 50.0000, 50.1109, 49.9904, 49.8728, 50.0956),
  lon   = c(8.2398, 8.2711, 8.6821, 8.4116, 8.6512, 8.7761),
  mu    = c(120, 150, 280, 90, 110, 95),
  sigma = c(25, 30, 55, 20, 24, 22)
)

filialen_fall |>
  kbl(col.names = c("ID", "Filiale", "Breitengrad", "Längengrad", "μ (Boxen/Tag)", "σ")) |>
  kable_styling(bootstrap_options = c("striped", "condensed"), full_width = FALSE)
ID Filiale Breitengrad Längengrad μ (Boxen/Tag) σ
C1 Wiesbaden 50.0782 8.2398 120 25
C2 Mainz 50.0000 8.2711 150 30
C3 Frankfurt 50.1109 8.6821 280 55
C4 Ruesselsheim 49.9904 8.4116 90 20
C5 Darmstadt 49.8728 8.6512 110 24
C6 Offenbach 50.0956 8.7761 95 22

Die fünf Kandidatenstandorte (Angebotsknoten \(i\), S1–S5) mit ihren Fixkosten: Die in Kapitel 5 genannten Beträge von 180.000 € bis 260.000 € sind einmalige Investitions- und Erschließungskosten. Für ein Jahresmodell werden sie – vereinfacht, ohne Verzinsung – linear auf eine Nutzungsdauer von fünf Jahren verteilt. So ergeben sich die jährlichen Fixkosten \(f_i\):

Code
standorte_fall <- tibble(
  ID          = paste0("S", 1:5),
  Standort    = c("Wiesbaden-Erbenheim", "Mainz-Hechtsheim", "Raunheim",
                  "Frankfurt-Hoechst", "Neu-Isenburg"),
  lat         = c(50.0632, 49.9698, 50.0000, 50.11146, 50.0543),
  lon         = c(8.2999, 8.2831, 8.4650, 8.54218, 8.7072),
  Investition = c(180000, 210000, 260000, 240000, 200000)
) |>
  mutate(Fixkosten = Investition / 5)   # lineare Verteilung auf 5 Jahre

standorte_fall |>
  kbl(col.names = c("ID", "Standort", "Breitengrad", "Längengrad",
                    "Investition (€)", "Fixkosten f_i (€/Jahr)"),
      format.args = list(big.mark = ".", decimal.mark = ",")) |>
  kable_styling(bootstrap_options = c("striped", "condensed"), full_width = FALSE)
ID Standort Breitengrad Längengrad Investition (€) Fixkosten f_i (€/Jahr)
S1 Wiesbaden-Erbenheim 50,06320 8,29990 180.000 36.000
S2 Mainz-Hechtsheim 49,96980 8,28310 210.000 42.000
S3 Raunheim 50,00000 8,46500 260.000 52.000
S4 Frankfurt-Hoechst 50,11146 8,54218 240.000 48.000
S5 Neu-Isenburg 50,05430 8,70720 200.000 40.000

Für die Transportkosten wird angenommen, dass jede Box auf jedem Kilometer Straßenweg 0,02 € kostet (anteilige Fahrzeug-, Personal- und Kühlkosten). Aus Straßenentfernungen und diesem Kostensatz ergibt sich die Transportkostenmatrix \(c_{ij}\) in € je Box. Für das Fallbeispiel ist sie – wie in der Vorlesung – vorgegeben und didaktisch vereinfacht; sie ist daher nicht in jedem Eintrag mit den tatsächlichen Entfernungen im Rhein-Main-Gebiet identisch. Wie man reale Straßendistanzen selbst berechnet, zeigt der (nicht ausgeführte) Code im Anschluss.

Code
transportkostensatz <- 0.02   # EUR je Box und Kilometer

# Transportkosten c_ij in EUR je Box (Zeilen: Standorte S1-S5, Spalten: Filialen C1-C6)
kosten_mat <- matrix(
  c(0.12348, 0.41478, 0.47474, 0.55152, 0.85996, 0.18220,
    0.09206, 0.51490, 0.59168, 0.88578, 0.73856, 0.78004,
    0.48168, 0.27202, 0.17036, 0.54650, 0.44772, 0.12592,
    0.61176, 0.66708, 0.89392, 0.33502, 0.68526, 0.74242,
    0.50226, 0.87512, 0.90576, 0.60740, 0.32042, 0.11400),
  nrow = 5, byrow = TRUE,
  dimnames = list(standorte_fall$ID, filialen_fall$ID)
)

# Implizite Strassendistanzen in km (Kosten / Kostensatz)
round(kosten_mat / transportkostensatz, 1)
     C1   C2   C3   C4   C5   C6
S1  6.2 20.7 23.7 27.6 43.0  9.1
S2  4.6 25.7 29.6 44.3 36.9 39.0
S3 24.1 13.6  8.5 27.3 22.4  6.3
S4 30.6 33.4 44.7 16.8 34.3 37.1
S5 25.1 43.8 45.3 30.4 16.0  5.7
Code
# Nicht ausgefuehrt (benoetigt Internetzugang): So lassen sich die
# Strassendistanzen mit dem OSRM-Routingdienst selbst berechnen.
library(osrm)
tab <- osrmTable(
  src = standorte_fall[, c("lon", "lat")],
  dst = filialen_fall[, c("lon", "lat")],
  measure = "distance"
)
dist_km <- tab$distances / 1000
HinweisHinweis zur Lesart der Matrix

In realen Anwendungen ist eine Transportkostenmatrix weder symmetrisch noch an der Luftlinie orientiert: Straßenwege, Autobahnauffahrten und Rheinbrücken bestimmen die tatsächliche Entfernung, und Fahrzeiten können je nach Tageszeit stark schwanken. Solche Effekte sind der Grund, warum diskrete Modelle in der Praxis mit Straßendistanzen oder -fahrzeiten statt mit euklidischen Distanzen arbeiten – und warum die kontinuierlichen Verfahren aus Kapitel 5 nur eine erste Orientierung liefern.

6.3 Ein Spezialfall: das Transportproblem

Bevor wir die Standortentscheidung modellieren, lohnt ein Blick auf den Spezialfall, in dem die Standorte bereits feststehen. Dann bleibt nur die Frage, welche Filiale von welchem Standort mit welcher Menge beliefert wird. Das ist das klassische Transportproblem: \(m\) Quellen mit Angebotsmengen \(s_i\), \(n\) Senken mit Nachfragemengen \(a_j\) und Transportkosten \(c_{ij}\) je Einheit. Mit den Transportmengen \(x_{ij} \geq 0\) lautet das Modell

\[ \min \sum_{i} \sum_{j} c_{ij} \cdot x_{ij} \quad \text{u.d.N.} \quad \sum_{j} x_{ij} \leq s_i \;\; \forall i, \qquad \sum_{i} x_{ij} = a_j \;\; \forall j, \qquad x_{ij} \geq 0. \]

Das Warehouse Location Problem erweitert genau dieses Modell um die Entscheidung, welche Quellen überhaupt geöffnet werden. Die Transportmengen \(x_{ij}\) und die Kostenmatrix \(c_{ij}\) tauchen deshalb im WLP unverändert wieder auf. Lösungsverfahren für das Transportproblem, also die Nordwestecken-Regel, die Vogel’sche Approximationsmethode und die exakte Lösung als LP, behandelt Kapitel 7. Es greift die Standortentscheidung dieses Kapitels auf und fragt, was passiert, wenn die Kapazität der gewählten Zentralküche in der Adventszeit nicht mehr ausreicht.

R-Exkurs: Matrizen als Kosten- und Transporttableaus

Bislang haben Sie tabellarische Daten vor allem als data.frame/tibble kennengelernt, bei denen jede Spalte einen benannten, inhaltlich unterschiedlichen Merkmalsvektor darstellt (z. B. Ort, Längengrad, Bedarf). Ein Kosten- oder Transporttableau ist dagegen eine homogene, rein numerische Tabelle, bei der es nicht auf Spaltennamen im inhaltlichen Sinn ankommt, sondern auf die Position (Zeile \(i\), Spalte \(j\)). Dafür ist der R-Datentyp Matrix (matrix()) die natürliche Wahl – wie oben bei kosten_mat. Zeilen und Spalten lassen sich über dimnames benennen und dann gezielt ansprechen:

Code
kosten_mat["S3", ]          # alle Kostensaetze von Raunheim
     C1      C2      C3      C4      C5      C6 
0.48168 0.27202 0.17036 0.54650 0.44772 0.12592 
Code
kosten_mat[, "C3"]          # Kosten, Frankfurt von jedem Standort zu beliefern
     S1      S2      S3      S4      S5 
0.47474 0.59168 0.17036 0.89392 0.90576 
Code
apply(kosten_mat, 2, which.min)   # je Filiale: Zeilenindex des guenstigsten Standorts
C1 C2 C3 C4 C5 C6 
 2  3  3  4  5  5 

Mit rowSums() und colSums() lässt sich später sehr einfach prüfen, ob eine berechnete Zuordnung zulässig ist: Die Spaltensummen einer Liefermatrix müssen der Nachfrage der Filialen entsprechen.

6.4 Das Warehouse Location Problem

In der Praxis ist meist nicht nur zu klären, wie Filialen beliefert werden, sondern auch, von welchen Standorten aus überhaupt geliefert werden soll. Für SushiFresh ist das die eigentliche strategische Frage: Lohnt es sich, mehrere Standorte gleichzeitig zu betreiben, oder ist eine Konsolidierung auf einen oder zwei Standorte günstiger? Jede Standorteröffnung ist mit einer fixen jährlichen Kostenbelastung verbunden, unabhängig davon, wie viel später tatsächlich über diesen Standort geliefert wird. Dieses Entscheidungsproblem heißt Warehouse Location Problem (WLP) oder auch Uncapacitated Facility Location Problem, wenn – wie hier – keine Kapazitätsobergrenzen je Standort unterstellt werden.

Definition: Warehouse Location Problem

Gegeben seien eine Menge potenzieller Standorte \(i \in I\) mit jährlichen Eröffnungskosten \(f_i\) sowie eine Menge von Kundenstandorten \(j \in J\) mit Nachfragemengen \(a_j\). Für jede mögliche Transportverbindung \((i,j) \in E \subseteq I \times J\) entstehen Transportkosten \(c_{ij}\) pro belieferter Einheit. Gesucht ist die Teilmenge der zu eröffnenden Standorte sowie die zugehörige Zuordnung von Kunden zu Standorten, welche die Summe aus Eröffnungs- und Transportkosten minimiert.

Im Unterschied zum Transportproblem enthält das WLP nun zwei Arten von Entscheidungsvariablen: eine binäre Standortentscheidung \(y_i \in \{0,1\}\) (Standort \(i\) wird eröffnet oder nicht) und weiterhin die kontinuierlichen Transportmengen \(x_{ij} \geq 0\). Das vollständige Modell lautet:

\[ \min \rightarrow \sum_{i \in I} y_i \cdot f_i + \sum_{(i,j) \in E} x_{ij} \cdot c_{ij} \]

unter den Nebenbedingungen

\[ \begin{align} \sum_{(i,j) \in E} x_{ij} &= a_j & \forall j \in J &\quad\rightarrow \text{Nachfrage vollständig befriedigen} \\ x_{ij} &\leq y_i \cdot a_j & \forall (i,j) \in E &\quad\rightarrow \text{nur Transporte von eröffneten Standorten} \\ x_{ij} &\geq 0 & \forall (i,j) \in E &\quad\rightarrow \text{Transportmengen nicht negativ} \\ y_i &\in \{0,1\} & \forall i \in I &\quad\rightarrow \text{Standort wird eröffnet oder nicht} \end{align} \]

Die zweite Nebenbedingung ist der entscheidende Unterschied zum reinen Transportproblem: Solange \(y_i = 0\) ist (Standort \(i\) nicht eröffnet), zwingt sie alle zugehörigen Transportmengen \(x_{ij}\) auf null – ein geschlossener Standort kann keine Filiale beliefern. Erst wenn \(y_i = 1\) gesetzt wird, ist \(x_{ij}\) bis maximal zur vollen Nachfrage \(a_j\) zulässig. Weil sowohl kontinuierliche als auch binäre Variablen im selben Modell auftreten, handelt es sich um ein gemischt-ganzzahliges lineares Optimierungsmodell (Mixed-Integer Linear Program, MILP) – eine deutlich schwierigere Problemklasse als das reine LP des Transportproblems.

Theorie-Vertiefung: Struktur der Optimallösung und “starke” Formulierungen

Weil das WLP keine Kapazitätsgrenzen kennt, hat seine Optimallösung eine einfache Struktur: Sobald feststeht, welche Standorte geöffnet sind, wird jede Filiale vollständig vom günstigsten offenen Standort beliefert. Eine Aufteilung einer Filiale auf mehrere Standorte kann nie besser sein, da jede Box auf der billigsten verfügbaren Verbindung transportiert werden sollte. Die eigentliche Schwierigkeit des Problems liegt deshalb allein in der kombinatorischen Auswahl der Standortmenge – bei \(|I|\) Kandidaten gibt es \(2^{|I|}-1\) nichtleere Teilmengen. Genau diese Beobachtung nutzen die Add- und Drop-Heuristiken sowie die Hilfsfunktion .wlp_cost().

Für die exakte Lösung mit einem MILP-Solver ist die Formulierung der Kopplungsbedingung bedeutsam. Statt der hier verwendeten disaggregierten Form \(x_{ij} \leq y_i \cdot a_j\) (eine Bedingung je Verbindung) ließe sich auch eine aggregierte Form \(\sum_j x_{ij} \leq y_i \cdot \sum_j a_j\) (eine Bedingung je Standort) aufschreiben. Beide Formulierungen haben dieselben ganzzahligen Lösungen. Solver arbeiten jedoch mit der LP-Relaxation, in der \(y_i\) Werte zwischen 0 und 1 annehmen darf. In der aggregierten Form genügt dann ein sehr kleines \(y_i\), um einen Standort “ein bisschen” zu öffnen und alle Filialen darüber zu beliefern – die Relaxation liefert eine schwache untere Schranke, und das Branch-and-Bound-Verfahren muss viele Verzweigungen durchsuchen. Die disaggregierte Form verlangt dagegen für jede einzelne Verbindung \(y_i \geq x_{ij}/a_j\) und liegt damit viel näher an der ganzzahligen Lösung; man spricht von einer starken Formulierung. Für das unkapazitierte WLP ist die LP-Relaxation der disaggregierten Form häufig bereits ganzzahlig (Daskin 2013).

Anders als im Transportproblem des vorigen Abschnitts rechnen wir das WLP auf Jahresbasis: Die Fixkosten \(f_i\) sind Jahreswerte, und auch die Nachfrage muss von der Tages- auf die Jahresebene hochgerechnet werden. Das Zahlenbeispiel der Vorlesung verwendet dafür vereinfachend den Faktor 1.000 (\(a_j = 1.000 \cdot \mu_j\)). Wir übernehmen diese Annahme zunächst und prüfen am Ende des Kapitels, wie empfindlich das Ergebnis auf eine realistischere Hochrechnung mit 365 Verkaufstagen reagiert. Bevor wir die Daten zusammenstellen, lohnt sich ein Blick auf ein weiteres zentrales R-Konzept: die Liste.

R-Exkurs: Listen als flexible Datenstruktur

Ein Vektor in R kann nur Elemente eines Datentyps aufnehmen, und ein data.frame/tibble verlangt, dass alle Spalten dieselbe Länge haben. Häufig möchte man aber unterschiedlich strukturierte, unterschiedlich lange oder unterschiedlich typisierte Objekte gemeinsam verwalten – etwa den Namen eines Standorts (Text), seine Koordinaten (ein Vektor der Länge 2), seine Fixkosten (eine Zahl) und einen Status (logisch). Genau dafür ist die Liste (list()) gedacht: ein Container, dessen Elemente beliebige – auch verschiedenartige – R-Objekte sein dürfen.

Der Zugriff auf ein benanntes Element erfolgt entweder mit dem Dollarzeichen $name oder mit doppelten eckigen Klammern [["name"]] bzw. [[position]]. Der Unterschied zu einfachen eckigen Klammern [ ] ist wichtig: liste[1] liefert eine Teilliste mit einem Element, liste[[1]] liefert den Inhalt dieses Elements selbst.

Code
# Eine einzelne Standortinformation als Liste
standort_info <- list(
  name        = "Wiesbaden-Erbenheim",
  koordinaten = c(lon = 8.2999, lat = 50.0632),  # Vektor als Element
  fixkosten   = 36000,
  aktiv       = TRUE
)

standort_info$name                     # Zugriff via $
[1] "Wiesbaden-Erbenheim"
Code
standort_info[["fixkosten"]]           # Zugriff via [[ ]] mit Namen
[1] 36000
Code
standort_info[["koordinaten"]]["lat"]  # verschachtelter Zugriff
    lat 
50.0632 

Listen sind nicht nur zur Eingabe nützlich – auch die Rückgabewerte vieler R-Funktionen sind Listen, sobald mehr als ein Ergebnis zurückgegeben werden soll. So liefern etwa add_heuristic_wlp() und drop_heuristic_wlp(), die wir weiter unten einsetzen, ihr Ergebnis als Liste mit den Feldern open_warehouses, fixed_cost, transport_cost, total_cost und assignment.

Für die eigentliche Rechnung benötigen wir die Fixkosten und die Jahresnachfrage als benannte Vektoren sowie die Kostenmatrix kosten_mat:

Code
fixkosten_vec  <- setNames(standorte_fall$Fixkosten, standorte_fall$ID)   # EUR/Jahr
nachfrage_jahr <- setNames(filialen_fall$mu * 1000, filialen_fall$ID)     # Boxen/Jahr (Faktor 1.000)

fixkosten_vec
   S1    S2    S3    S4    S5 
36000 42000 52000 48000 40000 
Code
nachfrage_jahr
    C1     C2     C3     C4     C5     C6 
120000 150000 280000  90000 110000  95000 

Fünf Standorte bedeuten \(2^5 - 1 = 31\) mögliche nichtleere Standortkonfigurationen – klein genug, um sie im nächsten R-Exkurs vollständig durchzurechnen.

R-Exkurs: purrr::map() – eine Funktion auf jedes Element anwenden

Häufig möchte man dieselbe Berechnung auf jedes Element einer Liste oder eines Vektors anwenden – zum Beispiel: “berechne für jede mögliche Standortkombination die Gesamtkosten”. Statt dafür eine for-Schleife (Kapitel 5) zu schreiben, bietet das tidyverse-Paket purrr dafür die Funktionenfamilie map(). map(x, f) wendet die Funktion f auf jedes Element von x an und gibt eine Liste zurück; map_dbl(x, f) tut dasselbe, verlangt aber, dass f je Element eine einzelne Zahl (double) liefert, und vereinfacht das Ergebnis dann automatisch zu einem numerischen Vektor. Das ist der direkte Verwandte von sapply() aus Kapitel 5, mit dem Vorteil, dass der Rückgabetyp klar feststeht und Fehler dadurch früher auffallen.

Wir erzeugen zunächst alle nichtleeren Teilmengen der fünf Standorte mit der Basis-R-Funktion combn() und werten sie anschließend mit map_dbl() und der Hilfsfunktion .wlp_cost() aus scm_functions.R aus, die für eine gegebene Menge offener Standorte die optimale Filialzuordnung (jede Filiale zum günstigsten offenen Standort) sowie die resultierenden Gesamtkosten berechnet:

Code
n_I <- length(fixkosten_vec)

# Alle nichtleeren Teilmengen von {1, ..., 5}: combn(5, k) liefert alle
# Kombinationen der Groesse k, unlist(..., recursive = FALSE) fasst sie zu
# einer einzigen Liste zusammen
alle_teilmengen <- unlist(
  lapply(1:n_I, function(k) combn(n_I, k, simplify = FALSE)),
  recursive = FALSE
)
length(alle_teilmengen)  # 2^5 - 1 = 31
[1] 31
Code
# Fuer jede Teilmenge die Gesamtkosten berechnen
kosten_je_teilmenge <- map_dbl(alle_teilmengen, function(S) {
  .wlp_cost(S, fixkosten_vec, kosten_mat, nachfrage_jahr)$total
})

beste_teilmenge <- alle_teilmengen[[which.min(kosten_je_teilmenge)]]
standorte_fall$Standort[beste_teilmenge]
[1] "Wiesbaden-Erbenheim" "Raunheim"           
Code
round(min(kosten_je_teilmenge))
[1] 301718

Die vollständige Enumeration zeigt: Am günstigsten ist es, zwei Standorte zu betreiben – Wiesbaden-Erbenheim und Raunheim – mit Gesamtkosten von rund 301.718 €/Jahr. Die zehn besten Konfigurationen im Überblick:

Code
brute_force_df <- map_dfr(alle_teilmengen, function(S) {
  res <- .wlp_cost(S, fixkosten_vec, kosten_mat, nachfrage_jahr)
  tibble(
    Standorte       = paste(standorte_fall$ID[S], collapse = " + "),
    Anzahl          = length(S),
    Fixkosten       = res$fixed,
    Transportkosten = round(res$transport),
    Gesamtkosten    = round(res$total)
  )
}) |>
  arrange(Gesamtkosten)

brute_force_df |>
  head(10) |>
  kbl(col.names = c("Offene Standorte", "Anzahl", "Fixkosten (€)",
                    "Transportkosten (€)", "Gesamtkosten (€/Jahr)"),
      format.args = list(big.mark = ".", decimal.mark = ",")) |>
  kable_styling(bootstrap_options = c("striped", "condensed"), full_width = FALSE) |>
  row_spec(1, bold = TRUE, background = "#E8F4F1")
Offene Standorte Anzahl Fixkosten (€) Transportkosten (€) Gesamtkosten (€/Jahr)
S1 + S3 2 88.000 213.718 301.718
S2 + S3 2 94.000 209.948 303.948
S3 1 52.000 256.702 308.702
S1 + S3 + S5 3 128.000 198.583 326.583
S2 + S3 + S5 3 134.000 194.812 328.812
S1 + S3 + S4 3 136.000 194.685 330.685
S2 + S3 + S4 3 142.000 190.914 332.914
S3 + S5 2 92.000 241.567 333.567
S3 + S4 2 100.000 237.669 337.669
S1 + S2 + S3 3 130.000 209.948 339.948

Die Rangliste ist aufschlussreich. Erstens liegen die besten Lösungen sehr dicht beieinander: Die Kombination Mainz-Hechtsheim + Raunheim und der Einzelstandort Raunheim sind nur wenige tausend Euro pro Jahr teurer als das Optimum. Zweitens kommt Raunheim in allen vorderen Lösungen vor – der Standort mit den höchsten Investitionskosten ist das Rückgrat jedes guten Netzes, weil er laut Kostenmatrix die nachfragestarke Filiale Frankfurt und die meisten übrigen Filialen am günstigsten erreicht. Drittens lohnt sich ein zweiter Standort im Westen (Wiesbaden-Erbenheim) nur knapp: Er spart Transportkosten zur Filiale Wiesbaden, die die zusätzlichen Fixkosten gerade eben übersteigen.

Diese Brute-Force-Suche funktioniert nur, weil \(|I|=5\) sehr klein ist. Bei \(|I|=20\) Standorten gäbe es bereits über eine Million, bei \(|I|=50\) mehr als \(10^{15}\) Teilmengen – eine vollständige Suche ist dann selbst auf leistungsfähigen Rechnern aussichtslos. Für realistische Instanzen braucht es daher entweder effiziente exakte Verfahren oder Heuristiken.

6.5 Exakte Lösung mit ompr

Das WLP lässt sich als MILP mit spezialisierten Solvern exakt lösen, ohne alle Teilmengen einzeln auszuwerten. In R übernimmt das Paket ompr (Optimization Modeling Package for R) die algebraische Formulierung des Modells; die eigentliche numerische Lösung wird über ompr.roi an einen Solver – hier glpk, das freie GNU Linear Programming Kit – delegiert. Der Charme von ompr liegt darin, dass sich Zielfunktion und Nebenbedingungen fast wörtlich aus der mathematischen Formulierung in R-Code übersetzen lassen.

R-Exkurs: Algebraische Optimierungsmodellierung mit ompr

Bislang haben wir Optimierungsprobleme (Center of Gravity, Weiszfeld) über maßgeschneiderte Algorithmen gelöst. ompr verfolgt einen generischeren Ansatz: Man beschreibt das Modell deklarativ (Variablen, Zielfunktion, Nebenbedingungen) und überlässt die Lösungssuche einem universellen Solver. Der Workflow gliedert sich in sechs Schritte:

  1. MIPModel() – ein leeres Modellobjekt anlegen,
  2. add_variable() – Entscheidungsvariablen mit Typ (binary, continuous, integer) und Indexmenge definieren,
  3. set_objective() – die Zielfunktion formulieren (sum_expr() bildet dabei Summen über Indexmengen, analog zum \(\sum\) in der mathematischen Notation),
  4. add_constraint() – Nebenbedingungen formulieren, ebenfalls indiziert,
  5. solve_model() – das Modell an einen konkreten Solver übergeben,
  6. get_solution() – die optimalen Werte der Entscheidungsvariablen aus dem Ergebnisobjekt extrahieren.

Im folgenden Code entspricht y[i] der binären Standortvariable \(y_i\) und x[i,j] der Transportmengenvariable \(x_{ij}\). sum_expr(f[i] * y[i], i = 1:n_I) bildet exakt den Ausdruck \(\sum_{i\in I} f_i \cdot y_i\); die beiden add_constraint()-Aufrufe setzen die Nachfrage- bzw. Kopplungsnebenbedingung aus dem WLP-Modell um.

Code
library(ompr)
library(ompr.roi)
library(ROI.plugin.glpk)

n_J  <- length(nachfrage_jahr)
f    <- fixkosten_vec     # Eroeffnungskosten f_i (EUR/Jahr)
a    <- nachfrage_jahr    # Nachfrage a_j (Boxen/Jahr)
c_ij <- kosten_mat        # Transportkosten (EUR/Box)

modell <- MIPModel() |>
  # Entscheidungsvariable y_i: Standort i eroeffnen (1) oder nicht (0)
  add_variable(y[i], i = 1:n_I, type = "binary") |>
  # Entscheidungsvariable x_ij: Transportmenge von Standort i zu Filiale j
  add_variable(x[i, j], i = 1:n_I, j = 1:n_J, type = "continuous", lb = 0) |>
  # Zielfunktion: Eroeffnungskosten + Transportkosten minimieren
  set_objective(
    sum_expr(f[i] * y[i], i = 1:n_I) +
      sum_expr(c_ij[i, j] * x[i, j], i = 1:n_I, j = 1:n_J),
    sense = "min") |>
  # NB1: Nachfrage jeder Filiale j vollstaendig befriedigen
  add_constraint(sum_expr(x[i, j], i = 1:n_I) == a[j], j = 1:n_J) |>
  # NB2: Transport nur von eroeffneten Standorten (x_ij <= y_i * a_j)
  add_constraint(x[i, j] <= y[i] * a[j], i = 1:n_I, j = 1:n_J)

ergebnis_milp <- solve_model(modell, with_ROI(solver = "glpk", verbose = FALSE))

Nach dem Lösen extrahieren wir mit get_solution() die optimalen Werte der Variablen \(y_i\) und \(x_{ij}\) sowie mit objective_value() den optimalen Zielfunktionswert \(Z^*\):

Code
y_sol <- get_solution(ergebnis_milp, y[i])
x_sol <- get_solution(ergebnis_milp, x[i, j])

offene_idx       <- y_sol$i[y_sol$value > 0.5]
offene_standorte <- standorte_fall$Standort[offene_idx]
offene_standorte
[1] "Wiesbaden-Erbenheim" "Raunheim"           
Code
round(objective_value(ergebnis_milp))
[1] 301718
Code
x_sol |>
  filter(value > 0.001) |>
  mutate(Standort = standorte_fall$Standort[i],
         Filiale  = filialen_fall$Filiale[j],
         Menge    = value,
         Transportkosten = round(c_ij[cbind(i, j)] * value)) |>
  select(Standort, Filiale, Menge, Transportkosten) |>
  kbl(col.names = c("Standort", "Filiale", "Menge x_ij (Boxen/Jahr)", "Transportkosten (€/Jahr)"),
      format.args = list(big.mark = ".", decimal.mark = ",")) |>
  kable_styling(bootstrap_options = c("striped", "condensed"), full_width = FALSE)
Standort Filiale Menge x_ij (Boxen/Jahr) Transportkosten (€/Jahr)
Wiesbaden-Erbenheim Wiesbaden 120.000 14.818
Raunheim Mainz 150.000 40.803
Raunheim Frankfurt 280.000 47.701
Raunheim Ruesselsheim 90.000 49.185
Raunheim Darmstadt 110.000 49.249
Raunheim Offenbach 95.000 11.962

Der Solver bestätigt das Ergebnis der vollständigen Enumeration: Es werden Wiesbaden-Erbenheim und Raunheim eröffnet. Wiesbaden-Erbenheim beliefert dabei nur die Filiale Wiesbaden, alle übrigen fünf Filialen werden aus Raunheim versorgt – jede Filiale vollständig von genau einem Standort, wie es die Theorie für das unkapazitierte WLP vorhersagt. Der entscheidende Unterschied zur Enumeration ist der Rechenweg: ompr/glpk musste nicht alle 31 Teilmengen einzeln auswerten, sondern nutzt intern Branch-and-Bound- und Schnittebenenverfahren, mit denen auch WLP-Instanzen mit hunderten Standorten und Kunden in vertretbarer Zeit exakt gelöst werden können.

6.6 Heuristische Lösungsverfahren: Add- und Drop-Heuristik

Trotz der Leistungsfähigkeit moderner MILP-Solver bleibt das WLP ein NP-schweres Problem: Im ungünstigsten Fall wächst die zur exakten Lösung benötigte Rechenzeit exponentiell mit der Anzahl der Standorte und Kunden. Für sehr große Praxisinstanzen – ein deutschlandweites Filialnetz mit hunderten Kandidatenstandorten und tausenden Filialen, gegebenenfalls mit weiteren Nebenbedingungen (Kapazitäten, mehrstufige Netzwerke) – kann selbst ein guter Solver an Zeit- oder Speichergrenzen stoßen. In solchen Fällen sind konstruktive Heuristiken häufig der praktikable Weg zu einer brauchbaren, wenn auch nicht notwendigerweise optimalen Lösung.

Die Grundidee beider im Folgenden vorgestellten Heuristiken ist greedy (gierig): Ausgehend von einer zulässigen Startkonfiguration wird in jedem Schritt genau diejenige Änderung vorgenommen, die die Gesamtkosten am stärksten reduziert; das Verfahren stoppt, sobald keine weitere Verbesserung mehr möglich ist. Filialen werden dabei stets dem günstigsten offenen Standort zugeordnet (\(\sigma(j) = \arg\min_{i \in S} c_{ij}\)), sodass sich die Gesamtkosten einer Standortmenge \(S\) eindeutig berechnen lassen als

\[Z(S) = \underbrace{\sum_{i \in S} f_i}_{\text{Eröffnung}} + \underbrace{\sum_{j \in J} \min_{i \in S} c_{ij} \cdot a_j}_{\text{Transport}}.\]

Die Add-Heuristik startet mit der leeren Menge \(S = \emptyset\) und fügt in jedem Schritt denjenigen Standort \(i^*\) hinzu, dessen Öffnung die Gesamtkosten am stärksten senkt: \(\Delta_i = Z(S) - Z(S \cup \{i\})\), \(i^* = \arg\max_{i \notin S} \Delta_i\). Ist \(\Delta_{i^*} > 0\), wird \(i^*\) geöffnet, andernfalls bricht das Verfahren ab. Ökonomisch lohnt sich das Hinzufügen eines Standorts genau dann, wenn die dadurch erzielbare Transportkosteneinsparung größer ist als seine Eröffnungskosten. Die Transportkosten sinken dabei von Schritt zu Schritt, die Fixkosten steigen.

Die Drop-Heuristik geht den umgekehrten Weg: Sie startet mit allen Standorten geöffnet (\(S = I\)) und schließt in jedem Schritt denjenigen Standort, dessen Schließung die Gesamtkosten am stärksten senkt (\(\Delta_i = Z(S) - Z(S \setminus \{i\})\)), bis auch hier keine weitere Verbesserung mehr möglich ist. Ein Standort wird geschlossen, wenn seine Fixkosten höher sind als die Transportkosten, die entstehen, weil seine Filialen auf den nächstbesten Standort umgehängt werden müssen.

Merkmal Add-Heuristik Drop-Heuristik
Start \(S = \emptyset\) (Kosten \(\infty\)) \(S = I\) (alle Fixkosten fallen an)
Richtung aufbauend abbauend
Aufwand je Schritt \(\mathcal{O}(\lvert I \rvert \cdot \lvert J \rvert)\) \(\mathcal{O}(\lvert I \rvert \cdot \lvert J \rvert)\)
Tendenz eher wenige Standorte eher mehr Standorte

Beide Varianten sind in R/scm_functions.R als add_heuristic_wlp() bzw. drop_heuristic_wlp() implementiert (inklusive der internen Kostenberechnung über .wlp_cost()). Mit verbose = TRUE protokollieren sie jeden Schritt:

Code
names(fixkosten_vec) <- standorte_fall$Standort   # Namen fuer lesbares Protokoll

add_ergebnis <- add_heuristic_wlp(
  fixed_costs        = fixkosten_vec,
  transport_cost_mat = kosten_mat,
  demand             = nachfrage_jahr,
  verbose            = TRUE
)
[Add iter 1] Open Raunheim | Total cost: 308702
[Add iter 2] Open Wiesbaden-Erbenheim | Total cost: 301718
Code
round(add_ergebnis$total_cost)
[1] 301718

Die Add-Heuristik öffnet im ersten Schritt Raunheim – den Standort, der für sich allein die geringsten Gesamtkosten aller Einzelstandorte verursacht. Im zweiten Schritt wird Wiesbaden-Erbenheim hinzugefügt, weil die Transportkosteneinsparung bei der Filiale Wiesbaden die zusätzlichen Fixkosten übersteigt. Danach kann kein weiterer Standort die Kosten senken.

Code
drop_ergebnis <- drop_heuristic_wlp(
  fixed_costs        = fixkosten_vec,
  transport_cost_mat = kosten_mat,
  demand             = nachfrage_jahr,
  verbose            = TRUE
)
[Drop iter 1] Close Mainz-Hechtsheim | New total cost: 355549
[Drop iter 2] Close Frankfurt-Hoechst | New total cost: 326583
[Drop iter 3] Close Neu-Isenburg | New total cost: 301718
Code
round(drop_ergebnis$total_cost)
[1] 301718

Die Drop-Heuristik startet mit allen fünf offenen Standorten und schließt nacheinander Mainz-Hechtsheim, Frankfurt-Höchst und Neu-Isenburg – jeweils den Standort, dessen Fixkosten den geringsten “Transportnutzen” stiften. Übrig bleiben Wiesbaden-Erbenheim und Raunheim: Add- und Drop-Heuristik konvergieren hier unabhängig voneinander auf dieselbe, nachweislich optimale Lösung. Das ist keineswegs selbstverständlich – bei anderen Kostenstrukturen können beide Verfahren in unterschiedlichen, suboptimalen Lösungen enden –, zeigt hier aber, wie deutlich die Kostenstruktur des Fallbeispiels auf diese Konfiguration hinweist.

Code
vergleich <- tibble(
  Verfahren = c("Optimum (MILP)", "Add-Heuristik", "Drop-Heuristik"),
  `Offene Standorte` = c(paste(offene_standorte, collapse = ", "),
                         paste(add_ergebnis$open_names, collapse = ", "),
                         paste(drop_ergebnis$open_names, collapse = ", ")),
  Fixkosten = c(sum(f[offene_idx]), add_ergebnis$fixed_cost, drop_ergebnis$fixed_cost),
  Gesamtkosten = round(c(objective_value(ergebnis_milp),
                         add_ergebnis$total_cost, drop_ergebnis$total_cost))
) |>
  mutate(Transportkosten = Gesamtkosten - Fixkosten,
         `Gap zum Optimum (%)` = round((Gesamtkosten / Gesamtkosten[1] - 1) * 100, 2))

vergleich |>
  kbl(format.args = list(big.mark = ".", decimal.mark = ",")) |>
  kable_styling(bootstrap_options = c("striped", "condensed"), full_width = FALSE)
Verfahren Offene Standorte Fixkosten Gesamtkosten Transportkosten Gap zum Optimum (%)
Optimum (MILP) Wiesbaden-Erbenheim, Raunheim 88.000 301.718 213.718 0
Add-Heuristik Raunheim, Wiesbaden-Erbenheim 88.000 301.718 213.718 0
Drop-Heuristik Wiesbaden-Erbenheim, Raunheim 88.000 301.718 213.718 0
Code
bestes_je_anzahl <- brute_force_df |>
  group_by(Anzahl) |>
  slice_min(Gesamtkosten, n = 1) |>
  ungroup()

ggplot(bestes_je_anzahl, aes(Anzahl, Gesamtkosten / 1000)) +
  geom_line(colour = "#2F3E46") +
  geom_point(size = 3, colour = "#2F3E46") +
  geom_text(aes(label = Standorte), vjust = -1, size = 3) +
  labs(x = "Anzahl offener Standorte", y = "Gesamtkosten (T€/Jahr)",
       subtitle = "Bestes Netz je Standortanzahl (vollständige Enumeration)") +
  scale_y_continuous(limits = c(250, 420)) +
  theme_minimal(base_size = 12)

Gesamtkosten in Abhängigkeit von der Anzahl offener Standorte: bestes Netz je Standortanzahl sowie die Pfade von Add- und Drop-Heuristik

Die Abbildung zeigt die typische U-Form der Gesamtkosten über der Anzahl der Standorte – dieselbe Abwägungslogik, die in Kapitel 3 als “Entscheidung über die Anzahl der Lager” eingeführt wurde: Mit jedem zusätzlichen Standort sinken die Transportkosten, während die Fixkosten steigen. Das Minimum liegt bei zwei Standorten, die Kurve ist in der Umgebung des Minimums aber sehr flach.

Für die Praxis gilt: Add- und Drop-Heuristik sollten – wenn möglich – parallel gerechnet und mit dem Ergebnis eines exakten Verfahrens (oder zumindest einer bekannten unteren Schranke) verglichen werden, bevor eine Standortentscheidung mit langjähriger Bindungswirkung getroffen wird. Gerade bei Distributionsstandorten, die nur mit hohem Aufwand revidierbar sind, ist ein belastbares Gütemaß (der Gap zum Optimum) unverzichtbar (Chopra und Meindl 2025).

6.7 Wie robust ist die Lösung? Eine Sensitivitätsanalyse

Die flache Kostenkurve legt nahe, dass die Optimallösung empfindlich auf Annahmen reagieren könnte. Die wichtigste Annahme des Zahlenbeispiels ist die Hochrechnung der Tages- auf die Jahresnachfrage mit dem Faktor 1.000. Verkauft SushiFresh an 365 Tagen im Jahr, wäre die tatsächliche Jahresmenge – und damit das Gewicht der Transportkosten gegenüber den Fixkosten – deutlich kleiner. Mit map_dfr() lässt sich das Optimum für verschiedene Hochrechnungsfaktoren bestimmen:

Code
bestes_netz <- function(faktor) {
  a_tmp  <- filialen_fall$mu * faktor
  kosten <- map_dbl(alle_teilmengen, ~ .wlp_cost(.x, fixkosten_vec, kosten_mat, a_tmp)$total)
  S      <- alle_teilmengen[[which.min(kosten)]]
  tibble(Tage_pro_Jahr = faktor,
         Optimales_Netz = paste(standorte_fall$Standort[S], collapse = " + "),
         Gesamtkosten = round(min(kosten)))
}

map_dfr(c(250, 300, 365, 500, 750, 1000), bestes_netz) |>
  kbl(format.args = list(big.mark = ".", decimal.mark = ",")) |>
  kable_styling(bootstrap_options = c("striped", "condensed"), full_width = FALSE)
Tage_pro_Jahr Optimales_Netz Gesamtkosten
250 Raunheim 116.176
300 Raunheim 129.011
365 Raunheim 145.696
500 Raunheim 180.351
750 Raunheim 244.526
1.000 Wiesbaden-Erbenheim + Raunheim 301.718

Das Ergebnis ist lehrreich: Bei einer realistischen Hochrechnung mit 365 Verkaufstagen ist nicht mehr die Zweistandortlösung, sondern der Einzelstandort Raunheim optimal. Je geringer das Transportvolumen, desto weniger lohnt sich die Fixkostenbelastung eines zweiten Standorts; erst bei deutlich höheren Jahresmengen rechnet sich die zusätzliche Nähe zur Filiale Wiesbaden. Die Sensitivitätsanalyse zeigt damit, dass die Frage “ein oder zwei Standorte?” im Fallbeispiel auf der Kippe steht – während die Rolle von Raunheim als zentralem Standort in allen Szenarien stabil bleibt.

6.8 Qualitative vs. quantitative Standortbewertung

An dieser Stelle lohnt sich ein Blick zurück auf Kapitel 5: Dort hatte die AHP-Analyse unter den drei ursprünglichen Kandidaten knapp Mainz-Hechtsheim favorisiert – vor allem wegen der dort sehr gut eingeschätzten Fachkräfteverfügbarkeit. Die in diesem Kapitel durchgeführte quantitative Kostenoptimierung kommt zu einem deutlich anderen Schluss: Mainz-Hechtsheim taucht in keiner der besten Lösungen auf; kostenoptimal sind Raunheim (allein oder zusammen mit Wiesbaden-Erbenheim).

Code
map_dfr(1:3, function(i) {
  res <- .wlp_cost(i, fixkosten_vec, kosten_mat, nachfrage_jahr)
  tibble(Standort = standorte_fall$Standort[i], Fixkosten = res$fixed,
         Transportkosten = round(res$transport), Gesamtkosten = round(res$total))
}) |>
  kbl(caption = "Die drei ursprünglichen Kandidaten als Einzelstandort (Faktor 1.000)",
      format.args = list(big.mark = ".", decimal.mark = ",")) |>
  kable_styling(bootstrap_options = c("striped", "condensed"), full_width = FALSE)
Die drei ursprünglichen Kandidaten als Einzelstandort (Faktor 1.000)
Standort Fixkosten Transportkosten Gesamtkosten
Wiesbaden-Erbenheim 36.000 371.503 407.503
Mainz-Hechtsheim 42.000 489.018 531.018
Raunheim 52.000 256.702 308.702

Der Grund für die Diskrepanz liegt in den Transportkosten: Mainz-Hechtsheim liegt am westlichen Rand des Absatzgebiets, während der Großteil der Nachfrage (allen voran Frankfurt) weiter östlich anfällt. Die Transportkosten eines Einzelstandorts Mainz-Hechtsheim sind deshalb weit höher als die von Raunheim – ein Nachteil, den weder die niedrigeren Fixkosten noch die gute Fachkräfteverfügbarkeit aufwiegen können, sobald man Letztere nicht in Euro bewertet.

Diese Beobachtung ist kein Widerspruch, sondern verdeutlicht die unterschiedliche Funktion beider Verfahren im Entscheidungsprozess: Der AHP eignet sich für eine erste Vorauswahl unter schwer quantifizierbaren, teils weichen Kriterien und für die Einbindung unterschiedlicher Stakeholder-Perspektiven. Er ersetzt aber keine belastbare Kostenoptimierung, sobald harte Zahlen verfügbar sind. Umgekehrt greift eine reine Kostenoptimierung zu kurz, wenn schwer monetarisierbare Aspekte ignoriert werden: Wenn in Raunheim tatsächlich kaum geeignete Fachkräfte zu finden wären, würde sich das in höheren Personalkosten, Qualitätsproblemen oder Anlaufschwierigkeiten niederschlagen, die das WLP nicht kennt. In der Praxis ergänzen sich beide Verfahren: qualitative Verfahren zur Reduktion des Suchraums und zur Prüfung von K.-o.-Kriterien, quantitative Optimierungsmodelle zur finalen, kostenbasierten Entscheidung zwischen den verbleibenden Kandidaten.

Fallstudie: Zwischenergebnis für SushiFresh

Das WLP empfiehlt einen zentralen Standort in Raunheim, gegebenenfalls ergänzt um einen zweiten Standort in Wiesbaden-Erbenheim. Da die Zweistandortlösung im Zahlenbeispiel nur rund 6.984 €/Jahr günstiger ist als der Einzelstandort Raunheim und bei realistischer Hochrechnung sogar teurer, spricht vieles für eine Konsolidierung der Produktion in einer Zentralküche in Raunheim. Einen wichtigen Effekt blendet das WLP allerdings aus: die Bestände, die an jedem Standort zur Absicherung gegen Nachfrageschwankungen vorgehalten werden müssen. Genau diese Frage – wie viel Sicherheitsbestand spart eine zentrale gegenüber einer dezentralen Versorgung? – untersucht Kapitel 8. Sie liefert ein zusätzliches, gewichtiges Argument für die Konsolidierung. Zuvor fragt Kapitel 7, wie die Ware verteilt wird, wenn die Kapazität der Zentralküche in Spitzenzeiten nicht ausreicht.

6.9 Zusammenfassung

Dieses Kapitel hat den Übergang von der kontinuierlichen zur diskreten Standortplanung vollzogen: Statt eines beliebigen Punktes in der Ebene wird eine Teilmenge aus einer endlichen Liste vorab definierter Standortkandidaten gesucht. Als methodischen Ausgangspunkt haben wir das klassische Transportproblem eingeordnet, bei dem die belieferten Standorte bereits feststehen und nur die Transportflüsse kostenminimal zu bestimmen sind. Seine Lösungsverfahren folgen in Kapitel 7.

Das Warehouse Location Problem erweitert dieses Modell um die eigentliche Standortentscheidung: Eine binäre Variable \(y_i\) pro Standort entscheidet über Eröffnung oder Nichteröffnung, gekoppelt an die Transportmengen \(x_{ij}\) über die Nebenbedingung \(x_{ij} \leq y_i \cdot a_j\). Weil sowohl binäre als auch kontinuierliche Variablen auftreten, handelt es sich um ein MILP, das sich mit ompr in wenigen, an die mathematische Formulierung angelehnten Codezeilen exakt lösen lässt. Am Beispiel von SushiFresh zeigte sich, dass der zentral gelegene Standort Raunheim trotz der höchsten Investitionskosten das Rückgrat jedes guten Netzes bildet; ob ein zweiter Standort in Wiesbaden-Erbenheim hinzukommt, hängt – wie die Sensitivitätsanalyse zeigte – von der Hochrechnung der Nachfrage ab. Die qualitative AHP-Empfehlung (Mainz-Hechtsheim) wurde durch die Kostenoptimierung nicht bestätigt, was die Notwendigkeit beider Verfahren im Entscheidungsprozess unterstreicht. Da das WLP NP-schwer ist, sind für große Instanzen konstruktive Add- und Drop-Heuristiken ein praktikabler Ausweg – im Fallbeispiel fanden beide das globale Optimum, was allerdings keine allgemeingültige Eigenschaft dieser Verfahren ist.

Auf der R-Seite haben Sie mit diesem Kapitel vier neue Bausteine kennengelernt: Listen als flexibler Container für heterogene Daten und Funktionsergebnisse; purrr::map()/map_dbl()/map_dfr() zur kompakten Anwendung einer Funktion auf jedes Element einer Liste; Matrizen als natürliche Repräsentation positionsbasierter Tableaus mit rowSums()/colSums() zur Konsistenzprüfung; und die algebraische Optimierungsmodellierung mit ompr.

Kontrollfragen

  1. Worin unterscheidet sich die diskrete von der kontinuierlichen Standortplanung, und warum ist die diskrete Variante für die meisten praktischen Standortentscheidungen realistischer?
  2. Worin unterscheiden sich Transportproblem und Warehouse Location Problem? Welche Entscheidung trifft das WLP zusätzlich?
  3. Welche zusätzliche Entscheidungsvariable enthält das Warehouse Location Problem im Vergleich zum Transportproblem, und welche Rolle spielt die Nebenbedingung \(x_{ij} \leq y_i \cdot a_j\)? Warum ist sie der aggregierten Form \(\sum_j x_{ij} \leq y_i \sum_j a_j\) vorzuziehen?
  4. Beschreiben Sie Add- und Drop-Heuristik in eigenen Worten. Unter welcher Bedingung lohnt es sich in der Add-Heuristik, einen weiteren Standort zu öffnen?
  5. Was versteht man unter dem “Gap zum Optimum”, und warum ist er bei heuristischen Standortentscheidungen eine wichtige Kennzahl?
  6. Erklären Sie den Unterschied zwischen einer Liste (list()) und einem data.frame/tibble. Nennen Sie ein Beispiel aus diesem Kapitel, bei dem eine R-Funktion ihr Ergebnis sinnvollerweise als Liste zurückgibt.
  7. Warum ist im SushiFresh-Beispiel bei 365 Verkaufstagen ein Standort, bei 1.000 “Tagen” dagegen sind zwei Standorte optimal? Argumentieren Sie mit der Struktur der Zielfunktion.
  8. Die AHP-Analyse (Kapitel 5) favorisiert Mainz-Hechtsheim, die WLP-Analyse Raunheim. Erklären Sie die Diskrepanz und diskutieren Sie, wie beide Verfahren in der Praxis sinnvoll kombiniert werden können.
TippÜbungen und Fallstudie

Aufgaben zur Klausurvorbereitung mit ausklappbaren Musterlösungen (Teil 1) und eine vertiefende Gruppenfallstudie (Teil 2) zu diesem Kapitel finden Sie in Fallstudie 3: Diskrete Standortplanung und Transportplanung.