Designing a Network with Minimum Cost

🕸️ A Python Guide to Minimum Spanning Trees

When you’re designing a network — whether it’s connecting computers, cities, or pipelines — you often want to connect all nodes with the lowest possible cost.
This is the heart of the Minimum Spanning Tree (MST) problem in graph theory.

Today, we’ll solve an MST problem using Python in a Google Colab environment, leveraging Kruskal’s Algorithm and visualizing the result with networkx and matplotlib.


🌐 The Scenario: Connecting Cities

Imagine you’re tasked with connecting 7 cities with network cables.
You’re given a list of possible cable routes and the cost of each connection.
Your goal is to connect all cities using the least total cable cost, ensuring that the network remains connected (no isolated cities) and no cycles (loops) are formed.

Here’s our list of cities and connection costs:

City A City B Cost
A B 7
A D 5
B C 8
B D 9
B E 7
C E 5
D E 15
D F 6
E F 8
E G 9
F G 11

🧮 Mathematical Definition

Given an undirected graph $G = (V, E)$, where:

  • $V$: set of vertices (cities)
  • $E$: set of edges with weights (costs)

Find a subset $T \subseteq E$ such that:

  • $T$ connects all vertices (spanning)
  • $T$ contains no cycles (tree)
  • The sum of edge weights in $T$ is minimized

Mathematically, minimize:

$$
\sum_{(u, v) \in T} w(u, v)
$$


🧑‍💻 Python Code: Kruskal’s Algorithm

Let’s implement Kruskal’s Algorithm to solve this MST problem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import networkx as nx
import matplotlib.pyplot as plt

# Define the graph edges and their weights
edges = [
("A", "B", 7),
("A", "D", 5),
("B", "C", 8),
("B", "D", 9),
("B", "E", 7),
("C", "E", 5),
("D", "E", 15),
("D", "F", 6),
("E", "F", 8),
("E", "G", 9),
("F", "G", 11)
]

# Create an undirected graph
G = nx.Graph()
G.add_weighted_edges_from(edges)

# Use Kruskal's algorithm to compute the Minimum Spanning Tree
mst = nx.minimum_spanning_tree(G, algorithm="kruskal")

# Display MST edges and total cost
print("Edges in MST:")
total_cost = 0
for u, v, data in mst.edges(data=True):
print(f"{u} - {v}: {data['weight']}")
total_cost += data['weight']
print(f"\nTotal Cost of MST: {total_cost}")

🧠 Code Explanation

  • networkx.Graph(): Initializes an undirected graph.
  • add_weighted_edges_from(edges): Adds edges with costs (weights).
  • minimum_spanning_tree(..., algorithm="kruskal"): Runs Kruskal’s algorithm.
  • The MST edges and their weights are printed along with the total cost.

📊 Visualizing the Network

Let’s visualize both the original graph and the resulting MST.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Position nodes using spring layout for clarity
pos = nx.spring_layout(G, seed=42)

# Draw original graph in light gray
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
nx.draw(G, pos, with_labels=True, node_color='lightblue', node_size=800, edge_color='gray')
nx.draw_networkx_edge_labels(G, pos, edge_labels={(u,v): d['weight'] for u,v,d in G.edges(data=True)})
plt.title("Original Graph")

# Draw MST in second subplot
plt.subplot(1, 2, 2)
nx.draw(G, pos, with_labels=True, node_color='lightblue', node_size=800, edge_color='lightgray')
nx.draw(mst, pos, with_labels=True, node_color='lightgreen', node_size=800, edge_color='green', width=2)
nx.draw_networkx_edge_labels(mst, pos, edge_labels={(u,v): d['weight'] for u,v,d in mst.edges(data=True)})
plt.title("Minimum Spanning Tree")

plt.tight_layout()
plt.show()

🔍 Visual Result Analysis

Edges in MST:
A - D: 5
A - B: 7
B - E: 7
D - F: 6
C - E: 5
E - G: 9

Total Cost of MST: 39

  • In the left graph, all possible connections and costs are visible.
  • In the right graph, only the optimal (cheapest) connections forming the MST are shown in green.
  • The MST does not contain any cycles and connects all 7 cities.
  • This is the most efficient way to lay cables with a total cost of 39.

✅ Conclusion

The Minimum Spanning Tree is a powerful tool in network design, ensuring all points are connected at the lowest possible cost without redundancies.
Using networkx, we efficiently solved and visualized the MST problem in Python.

Whether you’re building data centers, road networks, or even game maps — understanding MSTs can help you make smart, cost-effective decisions.

Optimizing Capital Investment Planning with Dynamic Programming

Have you ever wondered how companies decide when and where to invest their capital for maximum returns?
Today, we’ll dive into a fascinating application of dynamic programming to solve capital investment planning problems.
I’ll walk you through a concrete example, implement it in Python, and visualize the results with some elegant graphs.

The Capital Investment Planning Problem

Imagine you’re the CFO of a growing tech company with $10 million to invest over the next 5 years.
You have identified several potential projects, each requiring different levels of investment and offering different returns.
Your goal is to determine the optimal investment strategy that maximizes the total return.

This is a perfect application for dynamic programming, which helps us break down complex optimization problems into simpler subproblems and build up the solution systematically.

Mathematical Formulation

Let’s formulate this problem mathematically:

  • We have a total budget of $B$ to allocate
  • We need to decide how much to invest in each period $t = 1, 2, …, T$
  • Let $x_t$ be the investment amount in period $t$
  • Let $f_t(x_t)$ be the return function for period $t$
  • Our objective is to maximize $\sum_{t=1}^{T} f_t(x_t)$
  • Subject to $\sum_{t=1}^{T} x_t \leq B$ and $x_t \geq 0$ for all $t$

The Bellman equation for this problem is:

$$V_t(b) = \max_{0 \leq x_t \leq b} {f_t(x_t) + V_{t+1}(b - x_t)}$$

Where:

  • $V_t(b)$ is the maximum return from periods $t$ to $T$ with budget $b$
  • $V_{T+1}(b) = 0$ for all $b$ (boundary condition)

Python Implementation

Let’s implement this solution in Python.
We’ll create a realistic scenario and solve it step by step.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Set the style for our plots
plt.style.use('ggplot')
sns.set_palette("viridis")
sns.set_context("notebook", font_scale=1.2)

# Problem parameters
total_budget = 10 # Total budget in million dollars
periods = 5 # Number of investment periods (years)
discretization = 100 # How finely we discretize the budget
step_size = total_budget / discretization # The step size for discretization

# Generate return functions for each period
# For this example, we'll use quadratic functions with diminishing returns
# f_t(x) = a_t * sqrt(x) - b_t * x
def create_return_functions():
# Parameters for return functions (a_t, b_t)
# These create unique return profiles for each period
params = [
(4.0, 0.2), # Period 1: Higher initial returns but quicker diminishment
(3.5, 0.1), # Period 2: Moderate returns with slower diminishment
(5.0, 0.3), # Period 3: Highest initial returns but quick diminishment
(3.0, 0.05), # Period 4: Lower returns but very slow diminishment
(4.5, 0.15) # Period 5: Good returns with moderate diminishment
]

# Create return functions for each period
return_functions = []
for a, b in params:
# Define the return function for this period
def f(x, a=a, b=b):
if x == 0:
return 0
return a * np.sqrt(x) - b * x
return_functions.append(f)

return return_functions

# Create the return functions
return_functions = create_return_functions()

# Function to plot the return functions
def plot_return_functions(return_functions):
plt.figure(figsize=(10, 6))
x = np.linspace(0, total_budget, 100)

for t, f in enumerate(return_functions):
returns = [f(xi) for xi in x]
plt.plot(x, returns, label=f'Period {t+1}')

plt.title('Return Functions for Each Period')
plt.xlabel('Investment Amount ($ Million)')
plt.ylabel('Return ($ Million)')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

# Plot the return functions
plot_return_functions(return_functions)

# Dynamic Programming Solution
def solve_dp():
# Initialize value table and decision table
# Value table: V[t][b] = maximum return from periods t to T with budget b
# Decision table: D[t][b] = optimal investment amount in period t with budget b
budget_steps = int(total_budget / step_size) + 1
V = np.zeros((periods + 1, budget_steps))
D = np.zeros((periods, budget_steps))

# DP backward recursion
for t in range(periods - 1, -1, -1):
for b in range(budget_steps):
budget = b * step_size

# Try different investment amounts for period t
best_value = -float('inf')
best_decision = 0

for i in range(b + 1):
investment = i * step_size
remaining = budget - investment
remaining_idx = int(remaining / step_size)

current_return = return_functions[t](investment)
future_return = V[t + 1][remaining_idx]
total_return = current_return + future_return

if total_return > best_value:
best_value = total_return
best_decision = investment

V[t][b] = best_value
D[t][b] = best_decision

# Extract the optimal policy
optimal_policy = []
remaining_budget = total_budget

for t in range(periods):
b_idx = int(remaining_budget / step_size)
investment = D[t][b_idx]
optimal_policy.append(investment)
remaining_budget -= investment

return V, D, optimal_policy

# Solve the problem
V, D, optimal_policy = solve_dp()

# Print the results
print("Optimal Investment Policy:")
for t, investment in enumerate(optimal_policy):
print(f"Period {t+1}: ${investment:.2f} million")

print("\nTotal Expected Return: ${:.2f} million".format(V[0][int(total_budget / step_size)]))

# Function to plot the optimal investment policy
def plot_optimal_policy(optimal_policy):
plt.figure(figsize=(10, 6))
periods_list = list(range(1, periods + 1))

# Bar plot for investments
bars = plt.bar(periods_list, optimal_policy, color='skyblue', alpha=0.7)

# Calculate returns for each period
returns = [return_functions[t](optimal_policy[t]) for t in range(periods)]

# Line plot for returns
plt.plot(periods_list, returns, 'ro-', label='Return', linewidth=2)

# Adding value labels on top of the bars
for i, (bar, investment, ret) in enumerate(zip(bars, optimal_policy, returns)):
plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.1,
f"${investment:.2f}M", ha='center', va='bottom')
plt.text(i+1, ret + 0.1, f"${ret:.2f}M", ha='center', va='bottom', color='red')

plt.title('Optimal Investment Strategy and Returns by Period')
plt.xlabel('Period')
plt.ylabel('Amount ($ Million)')
plt.legend(['Return', 'Investment'])
plt.xticks(periods_list)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# Function to visualize the value function
def plot_value_function(V):
plt.figure(figsize=(10, 6))
budget_values = np.linspace(0, total_budget, V.shape[1])

for t in range(periods):
plt.plot(budget_values, V[t], label=f'Period {t+1}')

plt.title('Value Function by Period and Available Budget')
plt.xlabel('Available Budget ($ Million)')
plt.ylabel('Maximum Expected Return ($ Million)')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

# Function to create a heatmap of the decision table
def plot_decision_heatmap(D):
plt.figure(figsize=(12, 8))

# We'll use a subset of the decision table for better visualization
step = max(1, int(D.shape[1] / 20))
subset_D = D[:, ::step]
subset_budgets = np.linspace(0, total_budget, subset_D.shape[1])

ax = sns.heatmap(subset_D, cmap='viridis',
xticklabels=[f'{x:.1f}' for x in subset_budgets],
yticklabels=[f'Period {t+1}' for t in range(periods)])

plt.title('Optimal Investment Decision by Period and Available Budget')
plt.xlabel('Available Budget ($ Million)')
plt.ylabel('Period')
cbar = ax.collections[0].colorbar
cbar.set_label('Investment Amount ($ Million)')
plt.tight_layout()
plt.show()

# Plot the optimal policy
plot_optimal_policy(optimal_policy)

# Plot the value function
plot_value_function(V)

# Plot the decision heatmap
plot_decision_heatmap(D)

# Sensitivity Analysis: Effect of total budget
def budget_sensitivity_analysis():
budget_range = np.linspace(2, 20, 10) # Test budgets from $2M to $20M
total_returns = []

for budget in budget_range:
# Reconfigure the problem with a new budget
discretization = 100
step_size = budget / discretization

# Solve the DP problem
budget_steps = int(budget / step_size) + 1
V = np.zeros((periods + 1, budget_steps))
D = np.zeros((periods, budget_steps))

# DP backward recursion
for t in range(periods - 1, -1, -1):
for b in range(budget_steps):
b_amount = b * step_size

best_value = -float('inf')
best_decision = 0

for i in range(b + 1):
investment = i * step_size
remaining = b_amount - investment
remaining_idx = int(remaining / step_size)

current_return = return_functions[t](investment)
future_return = V[t + 1][remaining_idx]
total_return = current_return + future_return

if total_return > best_value:
best_value = total_return
best_decision = investment

V[t][b] = best_value
D[t][b] = best_decision

total_returns.append(V[0][budget_steps - 1])

# Plot the results
plt.figure(figsize=(10, 6))
plt.plot(budget_range, total_returns, 'o-', linewidth=2)
plt.title('Sensitivity Analysis: Effect of Total Budget on Maximum Return')
plt.xlabel('Total Budget ($ Million)')
plt.ylabel('Maximum Expected Return ($ Million)')
plt.grid(True)
plt.tight_layout()
plt.show()

# Calculate and display ROI for each budget level
roi = [ret/budget for ret, budget in zip(total_returns, budget_range)]

plt.figure(figsize=(10, 6))
plt.plot(budget_range, roi, 'o-', linewidth=2, color='green')
plt.title('Return on Investment (ROI) by Total Budget')
plt.xlabel('Total Budget ($ Million)')
plt.ylabel('ROI (Return/Budget)')
plt.grid(True)
plt.tight_layout()
plt.show()

# Perform sensitivity analysis
budget_sensitivity_analysis()

Understanding the Code

Let’s break down the key components of the Python implementation:

1. Problem Setup and Return Functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Problem parameters
total_budget = 10 # Total budget in million dollars
periods = 5 # Number of investment periods (years)
discretization = 100 # How finely we discretize the budget
step_size = total_budget / discretization

# Generate return functions for each period
def create_return_functions():
# Parameters for return functions (a_t, b_t)
params = [
(4.0, 0.2), # Period 1: Higher initial returns but quicker diminishment
(3.5, 0.1), # Period 2: Moderate returns with slower diminishment
(5.0, 0.3), # Period 3: Highest initial returns but quick diminishment
(3.0, 0.05), # Period 4: Lower returns but very slow diminishment
(4.5, 0.15) # Period 5: Good returns with moderate diminishment
]

# Create return functions for each period
return_functions = []
for a, b in params:
def f(x, a=a, b=b):
if x == 0:
return 0
return a * np.sqrt(x) - b * x
return_functions.append(f)

return return_functions

First, we define the problem parameters:

  • A total budget of $10 million
  • 5 investment periods (years)
  • A discretization of 100 steps for our budget (for numerical approximation)

For each period, we create a unique return function with the form $f_t(x) = a_t \sqrt{x} - b_t x$.
This represents diminishing returns on investment - as you invest more, the marginal return decreases.
Each period has different parameters to reflect varying investment opportunities over time.

2. Dynamic Programming Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def solve_dp():
# Initialize value table and decision table
budget_steps = int(total_budget / step_size) + 1
V = np.zeros((periods + 1, budget_steps))
D = np.zeros((periods, budget_steps))

# DP backward recursion
for t in range(periods - 1, -1, -1):
for b in range(budget_steps):
budget = b * step_size

# Try different investment amounts for period t
best_value = -float('inf')
best_decision = 0

for i in range(b + 1):
investment = i * step_size
remaining = budget - investment
remaining_idx = int(remaining / step_size)

current_return = return_functions[t](investment)
future_return = V[t + 1][remaining_idx]
total_return = current_return + future_return

if total_return > best_value:
best_value = total_return
best_decision = investment

V[t][b] = best_value
D[t][b] = best_decision

# Extract the optimal policy
optimal_policy = []
remaining_budget = total_budget

for t in range(periods):
b_idx = int(remaining_budget / step_size)
investment = D[t][b_idx]
optimal_policy.append(investment)
remaining_budget -= investment

return V, D, optimal_policy

This is the heart of our implementation using dynamic programming:

  1. We create two tables:

    • V[t][b]: The maximum return achievable from periods t to T with budget b
    • D[t][b]: The optimal investment amount in period t with budget b
  2. We use backward recursion (starting from the last period):

    • For each period t and available budget b
    • Try all possible investment amounts for period t
    • Calculate the return from the current investment plus the optimal return from future periods
    • Choose the investment amount that maximizes the total return
  3. Finally, we extract the optimal policy by following the decisions from period 1 to period 5, adjusting the remaining budget along the way.

3. Visualization and Analysis

The code includes several visualization functions to help understand the solution:

  • plot_return_functions(): Visualizes the return functions for each period
  • plot_optimal_policy(): Shows the optimal investment amounts and returns by period
  • plot_value_function(): Displays how the maximum achievable return varies with available budget
  • plot_decision_heatmap(): Shows the optimal investment decision for each period and budget level
  • budget_sensitivity_analysis(): Analyzes how changing the total budget affects returns and ROI

Results and Analysis

Let’s examine the results of our optimization:

Optimal Investment Strategy

The optimal investment policy suggests how to distribute our $10 million budget across the five periods to maximize returns.
The exact allocation depends on the specific return functions we defined, but typically it allocates more to periods with higher returns and less to periods with lower returns.

Return Functions Comparison

Optimal Investment Policy:
Period 1: $2.00 million
Period 2: $1.60 million
Period 3: $2.60 million
Period 4: $1.30 million
Period 5: $2.50 million

Total Expected Return: $26.90 million

The first graph shows the return functions for each period. We can observe:

  • Period 3 has the highest potential returns for higher investment amounts
  • Period 4 has the lowest initial returns but maintains steady growth
  • All functions exhibit diminishing returns as investment increases

Value Function Visualization

The value function graph shows how the maximum expected return changes with available budget:

  • Earlier periods generally have higher value functions as they incorporate returns from more future periods
  • The slope of each curve decreases as budget increases, reflecting diminishing returns

Decision Heatmap

The heatmap provides a comprehensive view of how optimal investment decisions change based on the available budget and current period:

  • For each period and available budget level, it shows the optimal amount to invest
  • This can help visualize how the strategy adapts to different budget constraints

Sensitivity Analysis

The sensitivity analysis shows how changing the total budget affects:

  1. The maximum achievable return
  2. The return on investment (ROI)

This helps determine the optimal overall budget level - the point where adding more budget no longer significantly improves ROI.

Conclusion

Dynamic programming provides a powerful framework for solving capital investment planning problems.
By breaking down the complex problem into simpler subproblems and solving them systematically, we can find the optimal investment strategy that maximizes returns.

The approach we’ve implemented is flexible and can be adapted to various investment scenarios by modifying the return functions or adding additional constraints.

While our example uses specific return functions, the same methodology can be applied to real-world data where return functions might be estimated from historical performance or market forecasts.

For businesses facing capital allocation decisions, this type of analysis can provide valuable insights and help maximize the impact of limited investment resources.

Optimizing Supply Chain Design with Sustainability Metrics

A Python-Powered Approach

In today’s world, supply chain design isn’t just about minimizing costs—it’s about balancing economic efficiency with environmental and social responsibility.
Companies are increasingly integrating sustainability metrics, such as carbon emissions and resource usage, into their decision-making processes.

In this blog post, we’ll dive into a concrete example of a supply chain design problem that incorporates sustainability metrics, solve it using Python in Google Colaboratory, and visualize the results with insightful graphs.

Let’s get started!


Problem Statement: Sustainable Supply Chain Design

Imagine a company that needs to design a supply chain to distribute goods from suppliers to distribution centers (DCs) and finally to customers.
The goal is to minimize total costs, which include transportation costs and carbon emissions (a proxy for environmental impact), while meeting customer demand and respecting capacity constraints at DCs.

Mathematical Formulation

Let’s formalize the problem. We have:

  • Suppliers: $( S = {s_1, s_2, \dots, s_m} )$
  • Distribution Centers: $( D = {d_1, d_2, \dots, d_n} )$
  • Customers: $( C = {c_1, c_2, \dots, c_p} )$

Parameters:

  • $( c_{sd} )$: Transportation cost from supplier $( s )$ to DC $( d )$ per unit.
  • $( c_{dc} )$: Transportation cost from DC $( d )$ to customer $( c )$ per unit.
  • $( e_{sd} )$: Carbon emissions (kg CO₂/unit) from supplier $( s )$ to DC $( d )$.
  • $( e_{dc} )$: Carbon emissions (kg CO₂/unit) from DC $( d )$ to customer $( c )$.
  • $( q_c )$: Demand of customer $( c )$.
  • $( k_d )$: Capacity of DC $( d )$.
  • $( w )$: Weight for carbon emissions in the objective function (to balance cost and emissions).

Decision Variables:

  • $( x_{sd} )$: Quantity shipped from supplier $( s )$ to DC $( d )$.
  • $( y_{dc} )$: Quantity shipped from DC $( d )$ to customer $( c )$.

Objective Function:
Minimize the total cost, which combines transportation costs and a monetized penalty for emissions:
$$
\text{Minimize} \quad Z = \sum_{s \in S} \sum_{d \in D} c_{sd} x_{sd} + \sum_{d \in D} \sum_{c \in C} c_{dc} y_{dc} + w \left( \sum_{s \in S} \sum_{d \in D} e_{sd} x_{sd} + \sum_{d \in D} \sum_{c \in C} e_{dc} y_{dc} \right)
$$

Constraints:

  1. Demand Satisfaction:
    $$
    \sum_{d \in D} y_{dc} = q_c \quad \forall c \in C
    $$
  2. Flow Balance at DCs:
    $$
    \sum_{s \in S} x_{sd} = \sum_{c \in C} y_{dc} \quad \forall d \in D
    $$
  3. DC Capacity:
    $$
    \sum_{s \in S} x_{sd} \leq k_d \quad \forall d \in D
    $$
  4. Non-negativity:
    $$
    x_{sd} \geq 0, \quad y_{dc} \geq 0 \quad \forall s \in S, d \in D, c \in C
    $$

Example Scenario

Let’s consider a small supply chain with:

  • 2 suppliers ($( S_1, S_2 )$)
  • 2 DCs ($( D_1, D_2 )$)
  • 3 customers ($( C_1, C_2, C_3 )$)

Parameters:

  • Customer demands: $( q_{C_1} = 100 )$, $( q_{C_2} = 150 )$, $( q_{C_3} = 200 )$
  • DC capacities: $( k_{D_1} = 250 )$, $( k_{D_2} = 300 )$
  • Transportation costs ($( c_{sd} )$, $( c_{dc} )$) and emissions ($( e_{sd} )$, $( e_{dc} )$) are given in tables (defined in the code).
  • Weight for emissions: $( w = 0.5 )$ (to balance cost and environmental impact).

We’ll use the PuLP library in Python to model and solve this linear programming problem.


Python Code to Solve the Problem

Below is the complete Python code to model the supply chain problem, solve it, and visualize the results.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import pulp
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Problem data
suppliers = ['S1', 'S2']
dcs = ['D1', 'D2']
customers = ['C1', 'C2', 'C3']
demand = {'C1': 100, 'C2': 150, 'C3': 200}
capacity = {'D1': 250, 'D2': 300}
w = 0.5 # Weight for emissions

# Transportation costs
cost_sd = {
('S1', 'D1'): 10, ('S1', 'D2'): 15,
('S2', 'D1'): 12, ('S2', 'D2'): 8
}
cost_dc = {
('D1', 'C1'): 5, ('D1', 'C2'): 7, ('D1', 'C3'): 9,
('D2', 'C1'): 6, ('D2', 'C2'): 4, ('D2', 'C3'): 8
}

# Emissions (kg CO2/unit)
emission_sd = {
('S1', 'D1'): 0.2, ('S1', 'D2'): 0.3,
('S2', 'D1'): 0.25, ('S2', 'D2'): 0.15
}
emission_dc = {
('D1', 'C1'): 0.1, ('D1', 'C2'): 0.12, ('D1', 'C3'): 0.14,
('D2', 'C1'): 0.11, ('D2', 'C2'): 0.09, ('D2', 'C3'): 0.13
}

# Initialize the model
model = pulp.LpProblem("Supply_Chain_Sustainability", pulp.LpMinimize)

# Decision variables
x = pulp.LpVariable.dicts("x", [(s, d) for s in suppliers for d in dcs], lowBound=0, cat='Continuous')
y = pulp.LpVariable.dicts("y", [(d, c) for d in dcs for c in customers], lowBound=0, cat='Continuous')

# Objective function
model += (
pulp.lpSum(cost_sd[s, d] * x[s, d] for s in suppliers for d in dcs) +
pulp.lpSum(cost_dc[d, c] * y[d, c] for d in dcs for c in customers) +
w * (
pulp.lpSum(emission_sd[s, d] * x[s, d] for s in suppliers for d in dcs) +
pulp.lpSum(emission_dc[d, c] * y[d, c] for d in dcs for c in customers)
)
)

# Constraints
# Demand satisfaction
for c in customers:
model += pulp.lpSum(y[d, c] for d in dcs) == demand[c]

# Flow balance at DCs
for d in dcs:
model += pulp.lpSum(x[s, d] for s in suppliers) == pulp.lpSum(y[d, c] for c in customers)

# DC capacity
for d in dcs:
model += pulp.lpSum(x[s, d] for s in suppliers) <= capacity[d]

# Solve the model
model.solve()

# Extract results
status = pulp.LpStatus[model.status]
total_cost = pulp.value(model.objective)
x_values = {(s, d): x[s, d].varValue for s in suppliers for d in dcs}
y_values = {(d, c): y[d, c].varValue for d in dcs for c in customers}

# Print results
print(f"Status: {status}")
print(f"Total Cost (including emissions penalty): {total_cost:.2f}")
print("\nShipments from Suppliers to DCs:")
for s in suppliers:
for d in dcs:
if x_values[s, d] > 0:
print(f"{s} -> {d}: {x_values[s, d]:.2f} units")
print("\nShipments from DCs to Customers:")
for d in dcs:
for c in customers:
if y_values[d, c] > 0:
print(f"{d} -> {c}: {y_values[d, c]:.2f} units")

# Visualization
# Prepare data for heatmaps
x_matrix = np.array([[x_values[s, d] for d in dcs] for s in suppliers])
y_matrix = np.array([[y_values[d, c] for c in customers] for d in dcs])

# Plot heatmap for supplier to DC shipments
plt.figure(figsize=(10, 4))
sns.heatmap(x_matrix, annot=True, fmt=".1f", xticklabels=dcs, yticklabels=suppliers, cmap="YlGnBu")
plt.title("Shipments from Suppliers to DCs (Units)")
plt.xlabel("Distribution Centers")
plt.ylabel("Suppliers")
plt.savefig("supplier_to_dc.png")
plt.show()

# Plot heatmap for DC to customer shipments
plt.figure(figsize=(10, 4))
sns.heatmap(y_matrix, annot=True, fmt=".1f", xticklabels=customers, yticklabels=dcs, cmap="YlOrRd")
plt.title("Shipments from DCs to Customers (Units)")
plt.xlabel("Customers")
plt.ylabel("Distribution Centers")
plt.savefig("dc_to_customer.png")
plt.show()

Code Explanation

Let’s break down the code step-by-step to understand how it works.

  1. Imports and Setup:

    • We import pulp for linear programming, numpy for array handling, matplotlib.pyplot for plotting, and seaborn for creating heatmaps.
    • The data is defined: lists of suppliers, DCs, and customers; dictionaries for demands, capacities, costs, and emissions; and the emissions weight $( w = 0.5 )$.
  2. Model Initialization:

    • A PuLP model is created with pulp.LpProblem("Supply_Chain_Sustainability", pulp.LpMinimize) to minimize the objective function.
  3. Decision Variables:

    • x[s, d]: Continuous variables for shipments from supplier $( s )$ to DC $( d )$.
    • y[d, c]: Continuous variables for shipments from DC $( d )$ to customer $( c )$.
    • Both are created using LpVariable.dicts with a lower bound of 0.
  4. Objective Function:

    • The objective combines transportation costs and emissions:
      $$
      \sum c_{sd} x_{sd} + \sum c_{dc} y_{dc} + w \left( \sum e_{sd} x_{sd} + \sum e_{dc} y_{dc} \right)
      $$
    • This is implemented using pulp.lpSum to sum over all supplier-DC and DC-customer pairs.
  5. Constraints:

    • Demand: Ensures each customer’s demand is met ($( \sum_d y_{dc} = q_c )$).
    • Flow Balance: Ensures inflow to each DC equals outflow ($( \sum_s x_{sd} = \sum_c y_{dc} )$).
    • Capacity: Ensures DC capacity is not exceeded ($( \sum_s x_{sd} \leq k_d )$).
    • These are added using model += with pulp.lpSum.
  6. Solving the Model:

    • model.solve() invokes the solver to find the optimal solution.
    • The status and objective value are extracted, and shipment quantities are stored in dictionaries x_values and y_values.
  7. Visualization:

    • Two heatmaps are created using seaborn.heatmap:
      • One for shipments from suppliers to DCs (x_matrix).
      • One for shipments from DCs to customers (y_matrix).
    • The heatmaps use YlGnBu and YlOrRd colormaps for visual clarity, with annotations showing exact quantities.

Results and Visualization

When you run the code in Google Colaboratory, you’ll see:

  • Status: “Optimal” (indicating the solver found a solution).
  • Total Cost: A value like 6712.50 (includes transportation costs and emissions penalty).
  • Shipments: Lists of non-zero shipments, e.g.:
    • $( S_1 \to D_1 )$: 150 units
    • $( S_2 \to D_2 )$: 300 units
    • $( D_1 \to C_1 )$: 100 units, $( D_1 \to C_3 )$: 50 units
    • $( D_2 \to C_2 )$: 150 units, $( D_2 \to C_3 )$: 150 units
Status: Optimal
Total Cost (including emissions penalty): 6712.50

Shipments from Suppliers to DCs:
S1 -> D1: 150.00 units
S2 -> D2: 300.00 units

Shipments from DCs to Customers:
D1 -> C1: 100.00 units
D1 -> C3: 50.00 units
D2 -> C2: 150.00 units
D2 -> C3: 150.00 units


Heatmap Analysis

  1. Supplier to DC Shipments:

    • The heatmap shows quantities shipped from suppliers to DCs.
    • For example, if $( S_1 \to D_1 = 150 )$ and $( S_2 \to D_2 = 300 )$, the heatmap highlights these flows with darker shades.
    • This visualization helps identify which supplier-DC routes are heavily utilized, reflecting cost and emissions optimization.
  2. DC to Customer Shipments:

    • The heatmap shows how DCs serve customers.
    • For example, $( D_2 )$ might supply all of $( C_2 )$ (150 units) and $( C_3 )$ (150 units), indicating $( D_2 )$ is a critical hub.
    • The color intensity (from light to dark) reflects shipment volumes, making it easy to spot key distribution patterns.

Why Heatmaps?

Heatmaps are intuitive for visualizing flow matrices.
The annotations provide exact values, while the color gradients highlight the magnitude of shipments, making it easy to interpret the supply chain’s structure at a glance.


Summary and Insights

This example demonstrates how to incorporate sustainability metrics (carbon emissions) into supply chain design using linear programming.
The Python code, powered by PuLP, efficiently solves the optimization problem, and the heatmaps provide a clear visual summary of the results.
By adjusting the emissions weight $( w )$, you can explore trade-offs between cost and environmental impact—set $( w = 0 )$ for a cost-only focus or increase $( w )$ to prioritize sustainability.

To extend this model, consider:

  • Adding fixed costs for opening DCs.
  • Incorporating multiple time periods.
  • Including social metrics, like labor conditions, in the objective function.

Run the code in Google Colaboratory to experiment with different parameters and visualize how the supply chain adapts.

Sustainability in supply chains isn’t just a buzzword—it’s a practical goal we can achieve with the right tools and mindset.

Demand Forecasting Meets Price Optimization

📊 A Data-Driven Python Approach

In today’s fast-paced e-commerce and retail environment, finding the optimal price that balances demand and revenue is a cornerstone of profitability.
But this is no longer a guessing game.
Thanks to machine learning and optimization, we can forecast demand based on historical data and optimize pricing accordingly.

In this post, we’ll walk through a concrete example of this integration using Python, forecasting demand with linear regression and then using that to maximize revenue through optimization.


🔍 Problem Overview

We have historical data containing:

  • Prices at which a product was sold
  • Corresponding units sold

We want to:

  1. Forecast demand based on price using linear regression.
  2. Optimize price to maximize expected revenue.

The Revenue Function

Let:

  • $p$ be the price
  • $D(p)$ be the demand at price $p$

Then:

$$
\text{Revenue}(p) = p \cdot D(p)
$$

We’ll forecast $D(p)$ with a simple linear regression:

$$
D(p) = a - b \cdot p
$$

Where $a, b$ are learned from data.


📁 Step 1: Python Code – Forecasting + Optimization

Let’s start with the full code, then break it down step-by-step.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from scipy.optimize import minimize_scalar

# Step 1: Simulated historical data (price vs demand)
np.random.seed(0)
prices = np.linspace(5, 20, 30)
true_a = 100
true_b = 3
demand = true_a - true_b * prices + np.random.normal(0, 5, size=prices.shape)

# Step 2: Fit linear regression: D(p) = a - b * p
X = prices.reshape(-1, 1)
y = demand
model = LinearRegression()
model.fit(X, y)
a = model.intercept_
b = -model.coef_[0] # reverse sign to match D(p) = a - b*p

# Step 3: Revenue function R(p) = p * D(p) = p * (a - b * p)
def revenue(p):
return -(p * (a - b * p)) # Negative for minimization

# Step 4: Find optimal price
result = minimize_scalar(revenue, bounds=(5, 20), method='bounded')
optimal_price = result.x
optimal_revenue = -result.fun
optimal_demand = a - b * optimal_price

# Step 5: Plot results
p_range = np.linspace(5, 20, 100)
predicted_demand = a - b * p_range
revenue_curve = p_range * predicted_demand

plt.figure(figsize=(12, 5))

# Demand curve
plt.subplot(1, 2, 1)
plt.scatter(prices, demand, label='Observed Demand')
plt.plot(p_range, predicted_demand, color='green', label='Predicted Demand')
plt.axvline(optimal_price, color='red', linestyle='--', label=f'Optimal Price = ${optimal_price:.2f}')
plt.xlabel('Price')
plt.ylabel('Demand')
plt.title('Demand vs. Price')
plt.legend()

# Revenue curve
plt.subplot(1, 2, 2)
plt.plot(p_range, revenue_curve, color='blue', label='Revenue Curve')
plt.axvline(optimal_price, color='red', linestyle='--', label=f'Optimal Revenue = ${optimal_revenue:.2f}')
plt.xlabel('Price')
plt.ylabel('Revenue')
plt.title('Revenue vs. Price')
plt.legend()

plt.tight_layout()
plt.show()

🧠 Code Breakdown

🏗️ Simulating Historical Data

1
2
3
4
prices = np.linspace(5, 20, 30)
true_a = 100
true_b = 3
demand = true_a - true_b * prices + np.random.normal(0, 5, size=prices.shape)

We simulate demand with noise, assuming a negative linear relationship between price and demand.


📈 Linear Regression for Demand Estimation

1
2
3
X = prices.reshape(-1, 1)
model = LinearRegression()
model.fit(X, y)

We fit a linear model: $\hat{D}(p) = a - b \cdot p$

Note: b is extracted as -model.coef_[0] since sklearn learns $y = a + b \cdot p$.


🧮 Revenue Function and Optimization

1
2
def revenue(p):
return -(p * (a - b * p)) # for minimization

We define the negative revenue function because minimize_scalar finds the minimum, and we want the maximum revenue.


🔎 Finding the Optimal Price

1
result = minimize_scalar(revenue, bounds=(5, 20), method='bounded')

We restrict the price to the range $5–$20 and find the price that yields the highest revenue.


📊 Visualization & Explanation

The first plot shows:

  • Observed demand (scatter)
  • Predicted demand curve (green line)
  • Optimal price (red dashed line)

The second plot:

  • Revenue curve over price
  • Optimal price with corresponding maximum revenue

This helps managers visualize:

  • How demand drops as price increases
  • Where revenue peaks — the sweet spot

🎯 Conclusion

With just a few lines of Python, we’ve:

  • Learned demand behavior from data
  • Built a revenue model
  • Optimized pricing to maximize profit

This is a basic framework, but in production systems, you could extend this with:

  • Time series forecasting (seasonality)
  • Price elasticity models
  • Multi-product optimization
  • Bayesian or probabilistic models

Let data guide your pricing decisions — because guessing is expensive.

Optimizing Call Center Staffing Using Queueing Theory and Python

Call centers are at the heart of customer service for many businesses.
One of the key challenges is ensuring that there are enough staff members to handle incoming calls efficiently—too few, and customers wait too long; too many, and resources are wasted.

Queueing theory offers a mathematical framework to analyze and solve this problem.
In this post, we’ll walk through a concrete example using Python, calculate the optimal number of staff, and visualize the results.


📊 Problem Setting

Suppose we run a call center that receives an average of λ = 20 calls per hour, and each staff member can handle μ = 5 calls per hour on average (i.e., the average handling time is 12 minutes).
Our goal is to determine the minimum number of staff required so that the average waiting time in queue is less than 1 minute.

We’ll model this using the M/M/c queue, where:

  • Arrivals follow a Poisson process (rate $\lambda$)
  • Service times are exponentially distributed (rate $\mu$)
  • There are $c$ parallel servers (staff members)

🧮 Mathematical Background

For an M/M/c queue, we calculate:

  1. Traffic intensity per server:

    $$
    \rho = \frac{\lambda}{c \mu}
    $$

  2. Probability of zero customers in system $P_0$:

    $$
    P_0 = \left[ \sum_{n=0}^{c-1} \frac{(\lambda/\mu)^n}{n!} + \frac{(\lambda/\mu)^c}{c!} \cdot \frac{1}{1 - \rho} \right]^{-1}
    $$

  3. Average number in queue $L_q$:

    $$
    L_q = \frac{P_0 \cdot (\lambda/\mu)^c \cdot \rho}{c! (1 - \rho)^2}
    $$

  4. Average waiting time in queue $W_q$:

    $$
    W_q = \frac{L_q}{\lambda}
    $$

We’ll write code to calculate these for different values of $c$ and find the smallest $c$ such that $W_q < 1/60$ hour (i.e., under 1 minute).


🧑‍💻 Python Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import numpy as np
import matplotlib.pyplot as plt
from math import factorial

# Parameters
λ = 20 # calls per hour
μ = 5 # service rate per agent (calls per hour)
target_wait = 1 / 60 # target waiting time in hours (1 minute)

def P0(λ, μ, c):
rho = λ / (c * μ)
sum1 = sum((λ/μ)**n / factorial(n) for n in range(c))
sum2 = ((λ/μ)**c / factorial(c)) * (1 / (1 - rho)) if rho < 1 else float('inf')
return 1 / (sum1 + sum2)

def Lq(λ, μ, c):
rho = λ / (c * μ)
if rho >= 1:
return float('inf')
p0 = P0(λ, μ, c)
return (p0 * (λ/μ)**c * rho) / (factorial(c) * (1 - rho)**2)

def Wq(λ, μ, c):
return Lq(λ, μ, c) / λ

# Try various numbers of staff
staff_range = range(1, 21)
waiting_times = [Wq(λ, μ, c) for c in staff_range]

# Find minimum staff for target waiting time
for c, wq in zip(staff_range, waiting_times):
if wq < target_wait:
optimal_staff = c
break

print(f"✅ Optimal number of staff: {optimal_staff}")
✅ Optimal number of staff: 7

📈 Visualizing the Result

1
2
3
4
5
6
7
8
9
10
11
plt.figure(figsize=(10, 6))
plt.plot(staff_range, [w * 60 for w in waiting_times], marker='o') # convert to minutes
plt.axhline(1, color='red', linestyle='--', label='Target wait time (1 minute)')
plt.axvline(optimal_staff, color='green', linestyle='--', label=f'Optimal staff = {optimal_staff}')
plt.xlabel("Number of Staff")
plt.ylabel("Average Waiting Time in Queue (minutes)")
plt.title("Waiting Time vs. Number of Staff")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()


🧠 Interpretation

The graph shows how the average waiting time drops as the number of staff increases.
Initially, adding more staff dramatically reduces waiting time.
But after a certain point, the improvement becomes marginal—this is where queueing theory shines: helping us find the sweet spot between customer satisfaction and staffing cost.

In our case, with $\lambda = 20$ and $\mu = 5$, the optimal number of staff needed to ensure the average wait is below 1 minute is:

Optimal number of staff: X
(where X is computed by the code)

This approach helps managers allocate just enough staff to meet service level targets—saving money while maintaining quality.

Solving the Knapsack Problem in Python:Maximizing Utility Within Budget

We’ll dive into a classic optimization problem — the Knapsack Problem — with a practical example and a Python implementation you can run on Google Colaboratory.
We’ll model a realistic scenario where we have a limited budget and must choose a subset of items to maximize our total benefit.
The logic is driven by the fundamental constraint:

$$
\sum_{i=1}^{n} c_i x_i \leq B, \quad \text{maximize} \quad \sum_{i=1}^{n} v_i x_i
$$

Where:

  • $c_i$: cost of item $i$
  • $v_i$: value (utility) of item $i$
  • $x_i \in {0, 1}$: whether item $i$ is selected
  • $B$: total budget

🎯 Scenario: Shopping on a Budget

Imagine you’re shopping for gear for a hiking trip.
You have a $15 budget and a list of items, each with a cost and a utility score representing how useful it is on the trip.

Item Cost ($) Utility
Water Filter 4 10
First Aid Kit 3 7
Tent 7 12
Map 2 4
Compass 2 5
Flashlight 3 6

🧠 Let’s Code It!

We’ll use Dynamic Programming (DP) to solve the 0-1 Knapsack Problem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import numpy as np
import matplotlib.pyplot as plt

# Define the items
items = [
{"name": "Water Filter", "cost": 4, "value": 10},
{"name": "First Aid Kit", "cost": 3, "value": 7},
{"name": "Tent", "cost": 7, "value": 12},
{"name": "Map", "cost": 2, "value": 4},
{"name": "Compass", "cost": 2, "value": 5},
{"name": "Flashlight", "cost": 3, "value": 6}
]

# Budget
budget = 15
n = len(items)

# Initialize DP table
dp = np.zeros((n + 1, budget + 1), dtype=int)

# Fill DP table
for i in range(1, n + 1):
for w in range(budget + 1):
if items[i-1]["cost"] <= w:
dp[i][w] = max(dp[i-1][w], dp[i-1][w - items[i-1]["cost"]] + items[i-1]["value"])
else:
dp[i][w] = dp[i-1][w]

# Traceback to find selected items
w = budget
selected = []
for i in range(n, 0, -1):
if dp[i][w] != dp[i-1][w]:
selected.append(items[i-1])
w -= items[i-1]["cost"]

# Show result
print("Selected items:")
for item in selected:
print(f"- {item['name']} (Cost: ${item['cost']}, Utility: {item['value']})")

print(f"Total cost: ${sum(i['cost'] for i in selected)}")
print(f"Total utility: {sum(i['value'] for i in selected)}")

🔍 Code Breakdown

  • We define our items with associated costs and utility scores.
  • The dp table is used to store the maximum utility achievable for each subproblem (i.e., combinations of items with a given budget).
  • The nested loop fills the DP table by choosing whether to include an item or not.
  • We then trace back from dp[n][budget] to identify which items were selected to reach the optimal utility.

📈 Visualizing the DP Table

Let’s plot the maximum utility achievable for each budget level.

1
2
3
4
5
6
7
plt.figure(figsize=(10, 6))
plt.plot(range(budget + 1), dp[n], marker='o', color='green')
plt.title("Maximum Utility vs. Budget")
plt.xlabel("Budget ($)")
plt.ylabel("Maximum Utility")
plt.grid(True)
plt.show()

📉 Result

Selected items:
- Flashlight (Cost: $3, Utility: 6)
- Compass (Cost: $2, Utility: 5)
- Map (Cost: $2, Utility: 4)
- First Aid Kit (Cost: $3, Utility: 7)
- Water Filter (Cost: $4, Utility: 10)
Total cost: $14
Total utility: 32

📊 Interpretation of the Graph

This graph illustrates how the maximum achievable utility increases as the budget increases.
The slope flattens as we reach the optimal set of items — showing diminishing returns.
This helps visually confirm that beyond a certain budget, adding more money doesn’t improve utility (unless new items are introduced).


✅ Conclusion

We’ve successfully solved a real-world version of the Knapsack Problem using Python.
With just a few lines of code, we were able to determine the best combination of items to purchase under a fixed budget, and visualize the trade-offs between cost and benefit.

This same logic can be extended to portfolio optimization, resource allocation in manufacturing, and even project selection under capital constraints.

Understanding Network Importance with PageRank

A Python Implementation

Have you ever wondered how Google decides which websites are more important than others?
Today, we’re diving into one of the most revolutionary algorithms in search engine history: PageRank.
Created by Larry Page and Sergey Brin, the founders of Google, this algorithm fundamentally changed how we navigate the internet.

What is PageRank?

PageRank is an algorithm that measures the importance of nodes in a network based on the structure of incoming links.
In simple terms, a page is important if other important pages link to it.
This creates a recursive definition that can be solved mathematically.

The core equation behind PageRank can be expressed as:

$$PR(A) = (1-d) + d \sum_{i=1}^n \frac{PR(T_i)}{C(T_i)}$$

Where:

  • $PR(A)$ is the PageRank of page A
  • $d$ is a damping factor (typically 0.85)
  • $T_i$ are pages linking to page A
  • $C(T_i)$ is the number of outbound links from page $T_i$

Let’s implement this algorithm in Python to analyze a simple network!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.colors import LinearSegmentedColormap

# Set the style for our plots
plt.style.use('fivethirtyeight')
sns.set_palette("viridis")

def create_sample_network():
"""
Create a sample directed network representing a citation network
between academic papers or web pages.
"""
# Create a directed graph
G = nx.DiGraph()

# Add nodes (representing web pages or academic papers)
nodes = ["A", "B", "C", "D", "E", "F", "G", "H"]
G.add_nodes_from(nodes)

# Add edges (representing links between pages)
edges = [
("A", "B"), ("A", "C"), ("A", "D"),
("B", "C"), ("B", "E"),
("C", "A"), ("C", "F"),
("D", "B"), ("D", "G"),
("E", "D"), ("E", "H"),
("F", "G"), ("F", "H"),
("G", "E"),
("H", "G")
]
G.add_edges_from(edges)

return G

def calculate_pagerank_matrix(G, alpha=0.85, max_iter=100, tol=1e-6):
"""
Calculate PageRank using the power iteration method.

Parameters:
- G: NetworkX DiGraph
- alpha: damping factor (default 0.85)
- max_iter: maximum number of iterations (default 100)
- tol: convergence tolerance (default 1e-6)

Returns:
- PageRank values dictionary
- Number of iterations performed
- Convergence history
"""
n = len(G)

# Create adjacency matrix
A = nx.to_numpy_array(G, nodelist=list(G.nodes())).T

# Create out-degree matrix
out_degrees = np.array([max(G.out_degree(node), 1) for node in G.nodes()])
D_inv = np.diag(1.0 / out_degrees)

# Create transition probability matrix
M = A @ D_inv

# Initialize PageRank vector
pr = np.ones(n) / n

# For tracking convergence
convergence_history = [np.copy(pr)]

# Power iteration method
for iteration in range(max_iter):
prev_pr = np.copy(pr)

# PageRank formula: (1-alpha)/n + alpha * M * pr
pr = (1 - alpha) / n + alpha * M @ pr

# Normalize to ensure sum equals 1
pr = pr / np.sum(pr)

# Store for convergence history
convergence_history.append(np.copy(pr))

# Check for convergence
if np.linalg.norm(pr - prev_pr, 1) < tol:
break

# Create a dictionary with node names and their PageRank values
pagerank = {node: rank for node, rank in zip(G.nodes(), pr)}

return pagerank, iteration + 1, convergence_history

def visualize_network_with_pagerank(G, pagerank):
"""
Visualize the network with node sizes proportional to PageRank values.
"""
plt.figure(figsize=(12, 10))

# Create a custom colormap from light to dark blue
colors = ["#9ecae1", "#6baed6", "#4292c6", "#2171b5", "#084594"]
custom_cmap = LinearSegmentedColormap.from_list("custom_blues", colors)

# Node positions using spring layout
pos = nx.spring_layout(G, seed=42)

# Scale PageRank values for visualization
node_sizes = [pagerank[node] * 10000 for node in G.nodes()]
node_colors = [pagerank[node] for node in G.nodes()]

# Draw nodes
nodes = nx.draw_networkx_nodes(
G, pos,
node_size=node_sizes,
node_color=node_colors,
cmap=custom_cmap,
alpha=0.9
)

# Draw edges with arrows
nx.draw_networkx_edges(
G, pos,
width=1.5,
alpha=0.7,
edge_color='gray',
arrowsize=15,
connectionstyle='arc3,rad=0.1'
)

# Draw labels
nx.draw_networkx_labels(
G, pos,
font_size=12,
font_weight='bold',
font_family='sans-serif'
)

# Add a colorbar
plt.colorbar(nodes, label='PageRank Score', shrink=0.8)

plt.title("Network Visualization with PageRank Importance", fontsize=18)
plt.axis('off')
plt.tight_layout()
plt.savefig('network_pagerank.png', dpi=300, bbox_inches='tight')
plt.show()

def visualize_pagerank_distribution(pagerank):
"""
Create a bar chart showing the PageRank distribution.
"""
plt.figure(figsize=(12, 6))

# Sort PageRank values
sorted_pagerank = {k: v for k, v in sorted(pagerank.items(), key=lambda item: item[1], reverse=True)}

# Plot bar chart
bars = plt.bar(sorted_pagerank.keys(), sorted_pagerank.values(), color=sns.color_palette("viridis", len(pagerank)))

# Add value labels on top of bars
for bar in bars:
height = bar.get_height()
plt.text(
bar.get_x() + bar.get_width()/2.,
height * 1.01,
f'{height:.4f}',
ha='center',
va='bottom',
fontsize=10,
rotation=0
)

plt.title('PageRank Score Distribution', fontsize=18)
plt.xlabel('Node', fontsize=14)
plt.ylabel('PageRank Score', fontsize=14)
plt.grid(axis='y', linestyle='--', alpha=0.7)
plt.tight_layout()
plt.savefig('pagerank_distribution.png', dpi=300, bbox_inches='tight')
plt.show()

def plot_convergence(convergence_history, node_names):
"""
Plot the convergence of PageRank values over iterations.
"""
plt.figure(figsize=(12, 6))

# Convert list of arrays to 2D array for easier indexing
convergence_data = np.array(convergence_history)

# Plot each node's PageRank convergence
for i, node in enumerate(node_names):
plt.plot(convergence_data[:, i], marker='o', markersize=4, label=f'Node {node}')

plt.title('PageRank Convergence Over Iterations', fontsize=18)
plt.xlabel('Iteration', fontsize=14)
plt.ylabel('PageRank Value', fontsize=14)
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(loc='center right')
plt.tight_layout()
plt.savefig('pagerank_convergence.png', dpi=300, bbox_inches='tight')
plt.show()

def comparative_analysis(G):
"""
Compare our PageRank implementation with NetworkX's built-in PageRank.
"""
# Calculate PageRank using our implementation
our_pagerank, iterations, _ = calculate_pagerank_matrix(G)

# Calculate PageRank using NetworkX's implementation
nx_pagerank = nx.pagerank(G, alpha=0.85)

# Create a DataFrame for comparison
comparison_data = {
'Node': list(our_pagerank.keys()),
'Our Implementation': list(our_pagerank.values()),
'NetworkX Implementation': [nx_pagerank[node] for node in our_pagerank.keys()]
}

# Plot comparison
plt.figure(figsize=(12, 6))
bar_width = 0.35
x = np.arange(len(comparison_data['Node']))

# Plot bars
plt.bar(x - bar_width/2, comparison_data['Our Implementation'], bar_width, label='Our Implementation')
plt.bar(x + bar_width/2, comparison_data['NetworkX Implementation'], bar_width, label='NetworkX Implementation')

# Add details
plt.title('PageRank Implementation Comparison', fontsize=18)
plt.xlabel('Node', fontsize=14)
plt.ylabel('PageRank Score', fontsize=14)
plt.xticks(x, comparison_data['Node'])
plt.legend()
plt.grid(axis='y', linestyle='--', alpha=0.7)
plt.tight_layout()
plt.savefig('pagerank_comparison.png', dpi=300, bbox_inches='tight')
plt.show()

# Calculate absolute difference
diff = np.abs(np.array(list(our_pagerank.values())) - np.array(list(nx_pagerank.values())))
max_diff = np.max(diff)

print(f"Maximum absolute difference between implementations: {max_diff:.10f}")
print(f"Number of iterations until convergence: {iterations}")

return our_pagerank, nx_pagerank

# Main execution
def main():
# Create our sample network
G = create_sample_network()

# Calculate PageRank
pagerank, iterations, convergence_history = calculate_pagerank_matrix(G)

# Print results
print("PageRank Results:")
for node, rank in sorted(pagerank.items(), key=lambda x: x[1], reverse=True):
print(f"Node {node}: {rank:.6f}")

print(f"\nNumber of iterations until convergence: {iterations}")

# Visualize the network with PageRank values
visualize_network_with_pagerank(G, pagerank)

# Visualize PageRank distribution
visualize_pagerank_distribution(pagerank)

# Plot convergence
plot_convergence(convergence_history, list(G.nodes()))

# Compare with NetworkX implementation
our_pagerank, nx_pagerank = comparative_analysis(G)

if __name__ == "__main__":
main()```


## Code Explanation

Let's break down the key components of the code:

### Network Creation

We start by creating a sample directed network with 8 nodes (A through H), where each node could represent a web page or an academic paper.
The edges represent links or citations between these nodes.

```python
def create_sample_network():
G = nx.DiGraph()

# Add nodes (representing web pages or academic papers)
nodes = ["A", "B", "C", "D", "E", "F", "G", "H"]
G.add_nodes_from(nodes)

# Add edges (representing links between pages)
edges = [
("A", "B"), ("A", "C"), ("A", "D"),
("B", "C"), ("B", "E"),
# ... other connections
]
G.add_edges_from(edges)

return G

This creates a directed graph where, for example, node A points to nodes B, C, and D.
This could represent a website A linking to websites B, C, and D.

PageRank Implementation

The core of our implementation is the calculate_pagerank_matrix function:

1
def calculate_pagerank_matrix(G, alpha=0.85, max_iter=100, tol=1e-6):

Here’s how it works:

  1. We create an adjacency matrix A from the graph
  2. We calculate the out-degree matrix D_inv, which contains the reciprocal of the number of outgoing links from each node
  3. We compute the transition probability matrix M = A @ D_inv
  4. We initialize the PageRank vector with equal probabilities
  5. We iterate until convergence using the power iteration method

The main PageRank update equation in code:

1
2
# PageRank formula: (1-alpha)/n + alpha * M * pr
pr = (1 - alpha) / n + alpha * M @ pr

This corresponds directly to the mathematical formula:

$$PR(A) = (1-d) + d \sum_{i=1}^n \frac{PR(T_i)}{C(T_i)}$$

The damping factor alpha (typically 0.85) represents the probability that a random surfer will follow a link rather than jump to a random page.

Visualization Functions

We’ve included several visualization functions to help understand the results:

  1. visualize_network_with_pagerank: Displays the network with node sizes proportional to their PageRank scores
  2. visualize_pagerank_distribution: Creates a bar chart showing the distribution of PageRank values
  3. plot_convergence: Shows how PageRank values converge over iterations
  4. comparative_analysis: Compares our implementation with NetworkX’s built-in PageRank function

Running the Algorithm

Let’s execute the code and analyze the results:

PageRank Results:
Node E: 0.243118
Node G: 0.218767
Node H: 0.142874
Node D: 0.135941
Node B: 0.090391
Node C: 0.071032
Node A: 0.048939
Node F: 0.048939

Number of iterations until convergence: 63

Maximum absolute difference between implementations: 0.0000020290
Number of iterations until convergence: 63

Analyzing the Results

Let’s analyze the output and visualize what our PageRank algorithm has discovered about this network.

Network Visualization

In this visualization, the size and color intensity of each node corresponds to its PageRank value.
Nodes E and G have the highest PageRank values, indicating they are the most important nodes in our network.
This makes sense because:

PageRank Distribution

The bar chart clearly shows the distribution of PageRank values across all nodes.
We can see that:

  • Node E has the highest PageRank value (0.2431)
  • Node G follows closely (0.2188)
  • Node F has the lowest PageRank (0.0489)

This distribution reflects the structure of our network and how information or importance flows through it.

Convergence Analysis

The convergence plot shows how the PageRank values stabilize over iterations.
Most nodes reach a stable value after about 15-20 iterations.

This demonstrates the iterative nature of the PageRank algorithm and how it gradually refines its estimates of node importance.

Implementation Comparison

Our implementation produces identical results to NetworkX’s built-in PageRank function, with a maximum absolute difference of practically zero.
This validates our implementation and confirms that it correctly applies the PageRank algorithm principles.

Why Does This Matter?

PageRank has applications far beyond web search:

  1. Academic Citation Networks: Identifying influential papers
  2. Social Network Analysis: Finding key influencers
  3. Biological Networks: Understanding protein-protein interactions
  4. Infrastructure Analysis: Identifying critical nodes in transportation networks
  5. Recommendation Systems: Ranking items based on user preferences

Conclusion

The PageRank algorithm is a powerful tool for analyzing network structures and determining node importance.
Our implementation demonstrates how relatively simple matrix operations can reveal complex network dynamics.

In our example network, nodes E and G emerged as the most important, while A and F were less significant.
These insights could help prioritize resources, identify influential actors, or understand information flow within the network.

The next time you search for something on Google, remember that a variation of this algorithm is working behind the scenes to rank the pages you see!

Minimizing Cost While Ensuring Service Level

📦 Optimal Inventory Management with Python

Inventory management is a delicate balancing act.
Too much stock ties up capital and raises storage costs; too little risks lost sales and dissatisfied customers.
In this post, we’ll walk through a real-world example using Python to find the optimal inventory level that minimizes cost while maintaining a specific service level.

We’ll use a classical inventory model that considers:

  • Holding cost
  • Stockout cost (penalty)
  • Service level target (e.g. 95%)

Let’s dive in.


📘 Problem Setup

Imagine you’re managing a warehouse for a retail company.
A particular product has:

  • Daily demand: Normally distributed with a mean of 100 units and standard deviation of 20.
  • Lead time: 5 days
  • Holding cost: $2 per unit per day
  • Stockout cost: $50 per unit short
  • Target service level: 95%

We want to find the optimal reorder point $R$ and order quantity $Q$ such that we:

  • Minimize the total expected cost
  • Maintain a 95% service level (i.e., only 5% of demand during lead time results in stockouts)

🔢 Mathematical Model

Let’s define:

  • $\mu$: Average daily demand
  • $\sigma$: Standard deviation of daily demand
  • $L$: Lead time (days)
  • $h$: Holding cost per unit per day
  • $p$: Stockout penalty per unit
  • $SL$: Desired service level

Lead time demand is normally distributed:

  • Mean: $\mu_L = \mu \cdot L$
  • Std Dev: $\sigma_L = \sigma \cdot \sqrt{L}$

To achieve the desired service level $SL$, the reorder point $R$ is calculated as:

$$
R = \mu_L + z \cdot \sigma_L
$$

Where $z$ is the z-score corresponding to $SL$.


🧪 Python Implementation

Let’s translate all this into Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt

# Parameters
mu = 100 # average daily demand
sigma = 20 # standard deviation of daily demand
L = 5 # lead time in days
h = 2 # holding cost per unit per day
p = 50 # stockout penalty per unit
service_level = 0.95

# Lead time demand distribution
mu_L = mu * L
sigma_L = sigma * np.sqrt(L)

# Find z-score for target service level
z = stats.norm.ppf(service_level)

# Reorder point R
R = mu_L + z * sigma_L

# Safety stock
safety_stock = z * sigma_L

# Expected shortage (standard normal loss function)
L_z = stats.norm.pdf(z) - z * (1 - service_level)

# Expected units short per cycle
E_short = sigma_L * L_z

# Total expected cost function over order quantity Q
def total_cost(Q):
cycle_stock = Q / 2
holding_cost = h * (cycle_stock + safety_stock)
stockout_cost = p * E_short * (mu / Q)
return holding_cost + stockout_cost

# Search for optimal order quantity Q
Q_values = np.arange(50, 1000, 10)
costs = [total_cost(Q) for Q in Q_values]
optimal_index = np.argmin(costs)
optimal_Q = Q_values[optimal_index]
min_cost = costs[optimal_index]

print(f"Reorder Point (R): {R:.2f}")
print(f"Safety Stock: {safety_stock:.2f}")
print(f"Optimal Order Quantity (Q): {optimal_Q}")
print(f"Minimum Total Cost: ${min_cost:.2f}")

# Plotting
plt.figure(figsize=(10, 6))
plt.plot(Q_values, costs, label="Total Cost", color='blue')
plt.axvline(optimal_Q, color='red', linestyle='--', label=f"Optimal Q = {optimal_Q}")
plt.title("Total Expected Cost vs. Order Quantity")
plt.xlabel("Order Quantity (Q)")
plt.ylabel("Total Expected Cost ($)")
plt.legend()
plt.grid(True)
plt.show()

🧠 Code Breakdown

  • Input parameters define the demand distribution, lead time, and costs.

  • We calculate the lead time demand and z-score for the desired service level.

  • The reorder point $R$ ensures we cover average demand plus safety stock.

  • L_z computes the expected shortage per cycle using the standard normal loss function.

  • The total_cost() function combines:

    • Cycle stock holding cost: $\frac{Q}{2} \cdot h$
    • Safety stock holding cost: $SS \cdot h$
    • Expected stockout cost per cycle
  • Finally, we search for the order quantity $Q$ that minimizes total cost using a simple grid search.


📊 Results Visualization

Reorder Point (R): 573.56
Safety Stock: 73.56
Optimal Order Quantity (Q): 70
Minimum Total Cost: $283.86

The graph above shows the relationship between order quantity $Q$ and total cost.
The red dashed line indicates the optimal order quantity, where the cost is minimized.

This visualization helps decision-makers see the cost sensitivity around the optimal point.


✅ Conclusion

With just a few lines of Python, we’ve built an inventory optimization model that:

  • Satisfies a 95% service level
  • Calculates the reorder point
  • Finds the order quantity that minimizes total cost

This is a powerful way to bring data-driven decisions to supply chain management!

Solving the Assignment Problem in Python

A Hands-On Example

The assignment problem is a classic optimization problem.
Suppose you have n workers and n jobs, and each worker has a different cost (or time, effort, etc.) associated with doing each job.
The goal is to assign each worker to exactly one job in a way that minimizes the total cost.

Mathematically, this is framed as minimizing the total cost:

$$
\min_{\sigma \in S_n} \sum_{i=1}^{n} C_{i, \sigma(i)}
$$

Where:

  • $C$ is the cost matrix.
  • $\sigma$ is a permutation of jobs assigned to workers.

Let’s walk through a concrete example and solve it using Python.


🧪 Example Scenario

We have 4 staff members and 4 jobs.
The cost matrix is as follows (rows = staff, columns = jobs):

Job 1 Job 2 Job 3 Job 4
A 90 76 75 80
B 35 85 55 65
C 125 95 90 105
D 45 110 95 115

Our task is to assign each staff member to a job such that the total cost is minimized.


🔍 Step-by-Step: Python Code

We’ll use scipy.optimize.linear_sum_assignment, which implements the Hungarian Algorithm.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.optimize import linear_sum_assignment

# Step 1: Define the cost matrix
cost_matrix = np.array([
[90, 76, 75, 80], # A
[35, 85, 55, 65], # B
[125, 95, 90, 105], # C
[45, 110, 95, 115] # D
])

# Step 2: Solve the assignment problem
row_ind, col_ind = linear_sum_assignment(cost_matrix)
total_cost = cost_matrix[row_ind, col_ind].sum()

# St 3: Output the result
staff = ['A', 'B', 'C', 'D']
jobs = ['Job 1', 'Job 2', 'Job 3', 'Job 4']

print("Optimal Assignment:")
for i in range(len(row_ind)):
print(f" Staff {staff[row_ind[i]]}{jobs[col_ind[i]]} (Cost: {cost_matrix[row_ind[i], col_ind[i]]})")

print(f"\nTotal Minimum Cost: {total_cost}")

💡 Code Explanation

  • Step 1: We define the cost_matrix, where each cell $(i, j)$ indicates the cost of assigning staff $i$ to job $j$.

  • Step 2: We use linear_sum_assignment() from SciPy to compute the optimal assignment.

    • row_ind and col_ind give the optimal pairing.
  • Step 3: We print the assignments and compute the total cost.


📊 Visualization: Heatmap of Assignments

Let’s visualize the assignment on a heatmap where the assigned cells are highlighted.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Step 4: Visualize with heatmap
plt.figure(figsize=(8, 6))
ax = sns.heatmap(cost_matrix, annot=True, fmt="d", cmap="YlGnBu", cbar=True,
xticklabels=jobs, yticklabels=staff)

# Highlight the assigned cells
for i in range(len(row_ind)):
ax.add_patch(plt.Rectangle((col_ind[i], row_ind[i]), 1, 1, fill=False, edgecolor='red', lw=3))

plt.title(f"Optimal Assignment (Total Cost = {total_cost})")
plt.xlabel("Jobs")
plt.ylabel("Staff")
plt.tight_layout()
plt.show()

🧠 Result Interpretation

The red boxes in the heatmap show the optimal assignment.
Here’s what it might look like:

1
2
3
4
5
6
7
Optimal Assignment:
Staff A → Job 4 (Cost: 80)
Staff B → Job 3 (Cost: 55)
Staff C → Job 2 (Cost: 95)
Staff D → Job 1 (Cost: 45)

Total Minimum Cost: 275

This means if each staff member takes on the assigned job, the total cost will be minimized to 275.


📌 Conclusion

The assignment problem is a fundamental optimization challenge with applications in scheduling, resource allocation, and logistics.

Python’s scipy.optimize library makes it easy to solve real-world cases efficiently.

The heatmap visualization helps stakeholders intuitively understand the optimal decision.

Navigating Uncertainty:Solving the Stochastic Vehicle Routing Problem with Python

In today’s fast-paced logistics world, planning efficient delivery routes isn’t just about finding the shortest path between points.
Real-world delivery operations face constant uncertainty - customer demands fluctuate, traffic conditions change, and unexpected events disrupt even the best-laid plans.

This is where the Stochastic Vehicle Routing Problem (SVRP) comes into play.
Unlike its deterministic counterpart, SVRP acknowledges that customer demands aren’t fixed but follow probability distributions.
Today, I’ll walk through a practical example of SVRP and solve it using Python.

Understanding the Stochastic Vehicle Routing Problem

In the SVRP, we need to design optimal routes for a fleet of vehicles to serve customers whose demands are uncertain.
The objective is typically to minimize the expected total cost while ensuring that the probability of route failure (where demand exceeds vehicle capacity) stays below an acceptable threshold.

The mathematical formulation of SVRP can be expressed as:

$$\min E[f(x,\xi)]$$

where $x$ represents the routing decisions, $\xi$ represents the random demand, and $f(x,\xi)$ is the cost function.
The constraints include:

$$P(g_i(x,\xi) \leq 0) \geq 1-\alpha_i, \forall i$$

Here, $g_i$ represents constraints (like capacity constraints), and $\alpha_i$ is the acceptable risk level.

Building a Practical SVRP Example

Let’s tackle a concrete example: A company operates a distribution center that needs to deliver products to 10 customers.
The demands of these customers are not fixed but follow normal distributions.
The company wants to determine optimal routes that minimize the total distance traveled while keeping the risk of exceeding vehicle capacity below 10%.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from scipy.stats import norm
from scipy.spatial import distance_matrix
import random
from itertools import permutations
import networkx as nx
from matplotlib.patches import Patch

# Set random seed for reproducibility
np.random.seed(42)
random.seed(42)

# Problem parameters
num_customers = 10
num_vehicles = 3
vehicle_capacity = 30
depot_coords = (0, 0)
confidence_level = 0.9 # 90% confidence that routes won't fail due to capacity

# Generate customer locations
customer_x = np.random.uniform(-10, 10, num_customers)
customer_y = np.random.uniform(-10, 10, num_customers)
customer_coords = list(zip(customer_x, customer_y))

# Add depot as node 0
all_coords = [depot_coords] + customer_coords

# Generate stochastic demands (normal distribution)
demand_means = np.random.uniform(5, 15, num_customers)
demand_stds = demand_means * 0.3 # Standard deviation as 30% of mean

# Calculate distance matrix
dist_matrix = np.zeros((num_customers + 1, num_customers + 1))
for i in range(num_customers + 1):
for j in range(num_customers + 1):
if i != j:
dist_matrix[i, j] = np.sqrt((all_coords[i][0] - all_coords[j][0])**2 +
(all_coords[i][1] - all_coords[j][1])**2)

# Function to calculate route length
def calculate_route_length(route, dist_mat):
length = 0
prev_node = 0 # Start at depot
for node in route:
length += dist_mat[prev_node, node]
prev_node = node
length += dist_mat[prev_node, 0] # Return to depot
return length

# Function to check if a route satisfies capacity constraints with given confidence
def check_capacity_constraint(route, means, stds, capacity, confidence):
route_mean_demand = sum(means[i-1] for i in route)
route_var_demand = sum(stds[i-1]**2 for i in route)
route_std_demand = np.sqrt(route_var_demand)

# Calculate the critical value based on confidence level
z_value = norm.ppf(confidence)

# The route will satisfy capacity with the given confidence if:
# mean + z * std <= capacity
effective_demand = route_mean_demand + z_value * route_std_demand

return effective_demand <= capacity, effective_demand

# Savings algorithm for SVRP
def savings_algorithm_svrp(dist_mat, means, stds, capacity, confidence, num_nodes):
# Calculate savings for each pair of customers
savings = {}
for i in range(1, num_nodes):
for j in range(1, num_nodes):
if i != j:
# Savings formula: s(i,j) = d(0,i) + d(0,j) - d(i,j)
savings[(i, j)] = dist_mat[0, i] + dist_mat[0, j] - dist_mat[i, j]

# Sort savings in descending order
sorted_savings = sorted(savings.items(), key=lambda x: x[1], reverse=True)

# Initialize routes: one route per customer
routes = [[i] for i in range(1, num_nodes)]

# Try to merge routes based on savings
for (i, j), saving in sorted_savings:
# Find routes containing i and j
route_i = None
route_j = None
for r in routes:
if i in r and i == r[-1]:
route_i = r
if j in r and j == r[0]:
route_j = r

# If i and j are in different routes and at the right positions
if route_i and route_j and route_i != route_j:
# Check if merging would violate capacity constraints
merged_route = route_i + route_j
feasible, _ = check_capacity_constraint(merged_route, means, stds, capacity, confidence)

if feasible:
# Merge routes
routes.remove(route_i)
routes.remove(route_j)
routes.append(merged_route)

return routes

# Apply the savings algorithm
routes = savings_algorithm_svrp(dist_matrix, demand_means, demand_stds,
vehicle_capacity, confidence_level, num_customers + 1)

# Evaluate the solution
total_distance = 0
route_demands = []
route_risks = []

for route in routes:
route_length = calculate_route_length(route, dist_matrix)
total_distance += route_length

# Calculate route statistics
feasible, effective_demand = check_capacity_constraint(route, demand_means, demand_stds,
vehicle_capacity, confidence_level)
route_mean_demand = sum(demand_means[i-1] for i in route)
route_std_demand = np.sqrt(sum(demand_stds[i-1]**2 for i in route))

# Calculate risk of exceeding capacity
risk = 1 - norm.cdf((vehicle_capacity - route_mean_demand) / route_std_demand)

route_demands.append((route_mean_demand, route_std_demand, effective_demand))
route_risks.append(risk)

# Output results
print(f"Number of routes: {len(routes)}")
print(f"Total expected distance: {total_distance:.2f}")

# Create dataframe for route statistics
route_stats = []
for i, route in enumerate(routes):
mean_demand, std_demand, effective_demand = route_demands[i]
route_stats.append({
'Route': i+1,
'Customers': route,
'Mean Demand': mean_demand,
'Std Demand': std_demand,
'Effective Demand': effective_demand,
'Risk of Exceeding Capacity': route_risks[i] * 100, # Convert to percentage
'Distance': calculate_route_length(route, dist_matrix)
})

route_stats_df = pd.DataFrame(route_stats)
print("\nRoute Statistics:")
print(route_stats_df[['Route', 'Customers', 'Mean Demand', 'Effective Demand',
'Risk of Exceeding Capacity', 'Distance']])

# Visualize the solution
plt.figure(figsize=(12, 10))

# Plot customers and depot
plt.scatter(depot_coords[0], depot_coords[1], c='red', s=200, marker='*', label='Depot')
plt.scatter(customer_x, customer_y, c='blue', s=100, label='Customers')

# Add customer labels
for i, (x, y) in enumerate(customer_coords):
plt.annotate(f"{i+1}", (x, y), fontsize=12)

# Define colors for routes
colors = ['green', 'purple', 'orange', 'brown', 'pink', 'gray', 'olive', 'cyan']

# Plot routes
for i, route in enumerate(routes):
color = colors[i % len(colors)]

# Plot route from depot to first customer
plt.plot([depot_coords[0], customer_coords[route[0]-1][0]],
[depot_coords[1], customer_coords[route[0]-1][1]],
c=color, linewidth=2)

# Plot route between customers
for j in range(len(route)-1):
plt.plot([customer_coords[route[j]-1][0], customer_coords[route[j+1]-1][0]],
[customer_coords[route[j]-1][1], customer_coords[route[j+1]-1][1]],
c=color, linewidth=2)

# Plot route from last customer back to depot
plt.plot([customer_coords[route[-1]-1][0], depot_coords[0]],
[customer_coords[route[-1]-1][1], depot_coords[1]],
c=color, linewidth=2,
label=f"Route {i+1}: {route}")

plt.title('Stochastic Vehicle Routing Problem Solution', fontsize=16)
plt.xlabel('X Coordinate', fontsize=14)
plt.ylabel('Y Coordinate', fontsize=14)
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(loc='best', fontsize=12)
plt.axis('equal')

# Generate legend for routes
route_patches = []
for i, route in enumerate(routes):
color = colors[i % len(colors)]
route_patches.append(Patch(color=color, label=f"Route {i+1}: {route}"))

plt.legend(handles=[
plt.Line2D([0], [0], marker='*', color='w', markerfacecolor='red', markersize=15, label='Depot'),
plt.Line2D([0], [0], marker='o', color='w', markerfacecolor='blue', markersize=10, label='Customers')
] + route_patches, loc='best', fontsize=12)

plt.tight_layout()
plt.savefig('svrp_solution.png', dpi=300)
plt.show()

# Visualize demand uncertainty
plt.figure(figsize=(14, 8))

# Plot demand distributions for each customer
x = np.linspace(0, 25, 100)
for i in range(num_customers):
y = norm.pdf(x, demand_means[i], demand_stds[i])
plt.plot(x, y, label=f"Customer {i+1}")

plt.axvline(x=vehicle_capacity, color='r', linestyle='--', label='Vehicle Capacity')
plt.title('Customer Demand Distributions', fontsize=16)
plt.xlabel('Demand', fontsize=14)
plt.ylabel('Probability Density', fontsize=14)
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(fontsize=12)
plt.tight_layout()
plt.savefig('demand_distributions.png', dpi=300)
plt.show()

# Visualize route demand uncertainty
plt.figure(figsize=(12, 8))

# Plot cumulative route demand distributions
x = np.linspace(0, 60, 100)
for i, route in enumerate(routes):
mean_demand, std_demand, _ = route_demands[i]
y = norm.pdf(x, mean_demand, std_demand)
plt.plot(x, y, label=f"Route {i+1}: {route}")

plt.axvline(x=vehicle_capacity, color='r', linestyle='--', label='Vehicle Capacity')
plt.title('Route Demand Distributions', fontsize=16)
plt.xlabel('Total Route Demand', fontsize=14)
plt.ylabel('Probability Density', fontsize=14)
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(fontsize=12)
plt.tight_layout()
plt.savefig('route_demand_distributions.png', dpi=300)
plt.show()

# Risk analysis visualization
plt.figure(figsize=(10, 6))
risk_df = pd.DataFrame({
'Route': [f"Route {i+1}" for i in range(len(routes))],
'Risk (%)': [r * 100 for r in route_risks]
})
sns.barplot(x='Route', y='Risk (%)', data=risk_df)
plt.axhline(y=(1-confidence_level)*100, color='r', linestyle='--',
label=f'Acceptable Risk ({(1-confidence_level)*100}%)')
plt.title('Risk of Exceeding Vehicle Capacity by Route', fontsize=16)
plt.xlabel('Route', fontsize=14)
plt.ylabel('Risk (%)', fontsize=14)
plt.grid(True, axis='y', linestyle='--', alpha=0.7)
plt.legend(fontsize=12)
plt.tight_layout()
plt.savefig('route_risks.png', dpi=300)
plt.show()

# Monte Carlo simulation to validate solution
def simulate_demands(means, stds, num_simulations=1000):
return np.random.normal(
np.tile(means, (num_simulations, 1)),
np.tile(stds, (num_simulations, 1))
)

# Number of simulations
num_simulations = 10000

# Simulate demands for all customers
simulated_demands = simulate_demands(demand_means, demand_stds, num_simulations)

# Calculate route failures
route_failures = np.zeros(len(routes))
for sim in range(num_simulations):
for i, route in enumerate(routes):
# Sum the demands for this route in this simulation
route_demand = sum(max(0, simulated_demands[sim, j-1]) for j in route)
if route_demand > vehicle_capacity:
route_failures[i] += 1

# Calculate empirical failure probabilities
empirical_risks = route_failures / num_simulations * 100

# Compare theoretical and empirical risks
risk_comparison = pd.DataFrame({
'Route': [f"Route {i+1}" for i in range(len(routes))],
'Theoretical Risk (%)': [r * 100 for r in route_risks],
'Empirical Risk (%)': empirical_risks
})

plt.figure(figsize=(12, 6))
risk_comparison_melted = pd.melt(risk_comparison, id_vars=['Route'],
var_name='Risk Type', value_name='Risk (%)')
sns.barplot(x='Route', y='Risk (%)', hue='Risk Type', data=risk_comparison_melted)
plt.axhline(y=(1-confidence_level)*100, color='r', linestyle='--',
label=f'Acceptable Risk ({(1-confidence_level)*100}%)')
plt.title('Theoretical vs. Empirical Risk Comparison', fontsize=16)
plt.xlabel('Route', fontsize=14)
plt.ylabel('Risk (%)', fontsize=14)
plt.grid(True, axis='y', linestyle='--', alpha=0.7)
plt.legend(fontsize=12)
plt.tight_layout()
plt.savefig('risk_comparison.png', dpi=300)
plt.show()

print("\nRisk Comparison (Theoretical vs. Empirical):")
print(risk_comparison)

Code Implementation Explained

Let’s break down our implementation by key components:

1. Problem Setup

First, we set up our problem parameters and generate random customer locations and stochastic demands:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Problem parameters
num_customers = 10
num_vehicles = 3
vehicle_capacity = 30
depot_coords = (0, 0)
confidence_level = 0.9 # 90% confidence that routes won't fail due to capacity

# Generate customer locations
customer_x = np.random.uniform(-10, 10, num_customers)
customer_y = np.random.uniform(-10, 10, num_customers)
customer_coords = list(zip(customer_x, customer_y))

# Generate stochastic demands (normal distribution)
demand_means = np.random.uniform(5, 15, num_customers)
demand_stds = demand_means * 0.3 # Standard deviation as 30% of mean

Here, we’re creating 10 customers with randomly generated coordinates.
The demands follow normal distributions with means between 5 and 15 units, and standard deviations at 30% of the respective means.
This reflects real-world scenarios where customer demands fluctuate around expected values.

2. Handling Demand Uncertainty

The core of SVRP is managing uncertainty.
For this, we implement a function that checks if routes satisfy capacity constraints with a given confidence level:

1
2
3
4
5
6
7
8
9
10
11
12
13
def check_capacity_constraint(route, means, stds, capacity, confidence):
route_mean_demand = sum(means[i-1] for i in route)
route_var_demand = sum(stds[i-1]**2 for i in route)
route_std_demand = np.sqrt(route_var_demand)

# Calculate the critical value based on confidence level
z_value = norm.ppf(confidence)

# The route will satisfy capacity with the given confidence if:
# mean + z * std <= capacity
effective_demand = route_mean_demand + z_value * route_std_demand

return effective_demand <= capacity, effective_demand

This function applies probability theory to calculate an “effective demand” that represents the demand level we’re confident won’t be exceeded (in this case, with 90% confidence).
It uses the properties of normal distributions where:

$$P(X \leq \mu + z \cdot \sigma) = \Phi(z)$$

Where $\Phi$ is the cumulative distribution function of the standard normal distribution, and $z$ is the critical value for our desired confidence level.

3. Route Construction Algorithm

We implement a modified version of the Clarke-Wright savings algorithm adapted for stochastic demands:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def savings_algorithm_svrp(dist_mat, means, stds, capacity, confidence, num_nodes):
# Calculate savings for each pair of customers
savings = {}
for i in range(1, num_nodes):
for j in range(1, num_nodes):
if i != j:
# Savings formula: s(i,j) = d(0,i) + d(0,j) - d(i,j)
savings[(i, j)] = dist_mat[0, i] + dist_mat[0, j] - dist_mat[i, j]

# Sort savings in descending order
sorted_savings = sorted(savings.items(), key=lambda x: x[1], reverse=True)

# Initialize routes: one route per customer
routes = [[i] for i in range(1, num_nodes)]

# Try to merge routes based on savings
for (i, j), saving in sorted_savings:
# Find routes containing i and j
route_i = None
route_j = None
for r in routes:
if i in r and i == r[-1]:
route_i = r
if j in r and j == r[0]:
route_j = r

# If i and j are in different routes and at the right positions
if route_i and route_j and route_i != route_j:
# Check if merging would violate capacity constraints
merged_route = route_i + route_j
feasible, _ = check_capacity_constraint(merged_route, means, stds, capacity, confidence)

if feasible:
# Merge routes
routes.remove(route_i)
routes.remove(route_j)
routes.append(merged_route)

return routes

The key modification from the standard savings algorithm is that we check the stochastic capacity constraint when merging routes.
This ensures that our routes have a high probability of not exceeding the vehicle capacity.

4. Solution Validation with Monte Carlo Simulation

To validate our analytical solution, we use Monte Carlo simulation to empirically test how often our routes would exceed capacity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def simulate_demands(means, stds, num_simulations=1000):
return np.random.normal(
np.tile(means, (num_simulations, 1)),
np.tile(stds, (num_simulations, 1))
)

# Number of simulations
num_simulations = 10000

# Simulate demands for all customers
simulated_demands = simulate_demands(demand_means, demand_stds, num_simulations)

# Calculate route failures
route_failures = np.zeros(len(routes))
for sim in range(num_simulations):
for i, route in enumerate(routes):
# Sum the demands for this route in this simulation
route_demand = sum(max(0, simulated_demands[sim, j-1]) for j in route)
if route_demand > vehicle_capacity:
route_failures[i] += 1

# Calculate empirical failure probabilities
empirical_risks = route_failures / num_simulations * 100

This simulation helps us confirm that our theoretical risk calculations align with what would happen in practice.

Results and Visualization

Let’s examine the results of our implementation:

Route Planning Results

When we run the code, we get a solution with optimal routes that balance distance minimization with risk management.
The output shows each route’s customers, mean demand, effective demand (with safety buffer), risk of exceeding capacity, and total distance.

Visual Route Map

Let’s look at the generated route map:

Number of routes: 5
Total expected distance: 96.06

Route Statistics:
   Route  Customers  Mean Demand  Effective Demand  \
0      1        [9]    10.924146         15.124102   
1      2     [5, 6]    22.412459         28.570808   
2      3  [8, 2, 3]    24.458729         29.983696   
3      4     [1, 7]    18.115267         23.165920   
4      5    [4, 10]    14.128123         18.066203   

   Risk of Exceeding Capacity   Distance  
0                2.930074e-07   4.875363  
1                5.717190e+00  18.753356  
2                9.933792e+00  29.676845  
3                1.282233e-01  28.077425  
4                1.201471e-05  14.675028  

This visualization shows our depot (red star), customers (blue dots), and the optimized routes (colored lines).
Each route is assigned a different color, making it easy to see which customers are served by which vehicle.

Customer Demand Distributions

Understanding the stochastic nature of demands is crucial:

This graph shows the demand distribution for each customer.
Notice how they follow normal distributions with different means and standard deviations.
The red dashed line represents our vehicle capacity constraint.

Route Demand Distributions

When we combine customers into routes, we get aggregated demand distributions:

Each colored curve represents the total demand distribution for a route.
The red line again shows our vehicle capacity.
Routes that have curves extending far beyond the capacity line have higher risks of failure.

Risk Analysis

Let’s analyze the risk of exceeding capacity for each route:

This chart shows the probability that each route will exceed the vehicle capacity.
The red dashed line represents our maximum acceptable risk level (10%).

Theoretical vs. Empirical Risk Comparison

Our Monte Carlo simulation validates our theoretical calculations:

Risk Comparison (Theoretical vs. Empirical):
     Route  Theoretical Risk (%)  Empirical Risk (%)
0  Route 1          2.930074e-07                0.00
1  Route 2          5.717190e+00                5.96
2  Route 3          9.933792e+00                9.66
3  Route 4          1.282233e-01                0.08
4  Route 5          1.201471e-05                0.00

The close alignment between theoretical and empirical risks confirms that our probabilistic modeling approach is accurate.

Practical Implications

This implementation has several real-world applications:

  1. Risk Management: Companies can set their risk tolerance levels and design routes accordingly.
  2. Resource Planning: By understanding demand variability, logistics planners can allocate appropriate vehicles and resources.
  3. Service Level Guarantees: The ability to quantify and control route failure probabilities enables more reliable customer service.
  4. Cost Optimization: Balancing route efficiency with risk management leads to better long-term cost structures.

Conclusion

The Stochastic Vehicle Routing Problem represents a significant leap forward in realistic logistics planning.
By incorporating uncertainty into our models, we create more robust solutions that can withstand the variability inherent in real-world operations.

Our Python implementation demonstrates how probability theory, heuristic algorithms, and simulation techniques can be combined to solve complex logistics problems under uncertainty.
The visual analytics help decision-makers understand both the solutions and the risks involved.

Next time you’re planning deliveries with uncertain demands, consider applying these stochastic optimization techniques - your operations will be more resilient and your customers more satisfied!