Supply Chain Optimization with AI
Reading time: ~45 min · Interview relevance: High · Target roles: ML Engineer, Operations Research Engineer, Supply Chain Data Scientist
The $4.4 Trillion Problem
In March 2021, a single ship - the Ever Given - ran aground in the Suez Canal and blocked it for six days. This 400-meter vessel carrying 9 billion in lost trade per day, with reverberations lasting six months as the downstream inventory replenishment wave propagated through global supply chains.
The companies that navigated the disruption with the least damage had something in common: they had been tracking the Ever Given's route, they had supplier-level inventory visibility, and they had AI systems that, when the ship ran aground, immediately computed the exposure: which components would run out first, from which suppliers, affecting which production lines. Within hours they were rerouting orders to backup suppliers, air-freighting critical components, and re-sequencing production to prioritize models with the most available inventory. The disruption still hurt. It hurt far less.
Supply chain management is fundamentally an optimization problem under uncertainty. You are trying to have the right inventory, at the right locations, at the right time, at minimum cost - while the demand signal is noisy, supplier lead times vary, transportation is disrupted, and geopolitical events periodically scramble the entire system. The scale is staggering: a mid-size automotive OEM manages 30,000 components sourced from 1,200 tier-1 suppliers across 40 countries. Optimizing inventory decisions for this network by hand, updated weekly, is mathematically intractable.
AI changes the equation. Probabilistic demand forecasts replace point forecasts, properly quantifying uncertainty for inventory decisions. Supplier risk models watch thousands of signals - financial health, news sentiment, logistics reliability - and flag deteriorating suppliers before they fail. Multi-echelon inventory optimization algorithms compute optimal safety stock across the entire supply network simultaneously. Route optimization runs in seconds for delivery networks that would take a human planner days. This lesson covers the ML systems behind each of these capabilities.
Why This Exists
Why Traditional Supply Chain Planning Falls Short
The standard tool for supply chain planning is ERP software (SAP, Oracle) combined with demand planning systems (SAP IBP, Kinaxis) that use statistical time-series forecasting - typically ARIMA, Holt-Winters, or exponential smoothing. These tools have served manufacturing for 30 years. They also have well-known failure modes.
Point forecasts without uncertainty quantification lead to inventory decisions made without knowledge of downside risk. If the forecast says "demand = 10,000 units," the planner sets safety stock based on that number. But if the true demand distribution is bimodal - either 8,000 or 14,000 with equal probability - the point forecast of 11,000 is the worst possible basis for inventory decisions. Probabilistic forecasting surfaces this uncertainty and enables proper safety stock calculation.
Classical time-series methods struggle with the complexity of modern demand signals. Demand is driven by promotions, weather, macroeconomic conditions, competitor actions, social media, and dozens of other factors that ARIMA cannot incorporate. ML-based demand models - gradient boosting, neural networks - naturally incorporate all available features.
Most critically: traditional planning systems treat supply chain decisions independently. The inventory level at the distribution center is optimized separately from the factory buffer stock, which is optimized separately from the supplier's delivery schedule. In reality these are coupled - a stockout at the DC triggers replenishment that exhausts factory buffer, which triggers supplier expediting. Multi-echelon optimization treats the supply network as a system.
Historical Context
Supply chain optimization as a formal discipline traces to World War II operations research - specifically the military logistics problems that motivated the simplex method (Dantzig, 1947) and early inventory models. Wilson's EOQ (Economic Order Quantity) formula dates to 1913 and remains in use.
The modern era begins with Walmart's legendary 1990s work on supply chain data sharing. Walmart connected its point-of-sale data directly to suppliers' manufacturing systems - what came to be called CPFR (Collaborative Planning, Forecasting, and Replenishment). This was genuinely revolutionary: demand signals propagated upstream at machine speed instead of through weekly purchase orders. The "bullwhip effect" - the amplification of demand variability as you move upstream in the supply chain - was tamed.
Amazon's supply chain innovations are the defining influence of the 2000s-2010s. Their probabilistic inventory positioning (keeping some SKUs at fulfillment centers closest to high-demand regions, others only at central facilities) is explicitly ML-driven. Amazon's 2018 blog post describing their demand forecasting stack - an ensemble of models including ARIMA, ETS, NPTS, MQCNN, Temporal Fusion Transformer - revealed the scale and sophistication of the deployed system.
The COVID-19 pandemic of 2020-2022 was the stress test that exposed the fragility of just-in-time supply chains and accelerated investment in supply chain AI. McKinsey estimated that companies advanced their supply chain AI adoption by 3-5 years between 2020 and 2022.
Core Concepts
Supply Chain Structure
A supply chain is a multi-tier network. For an automotive OEM:
- Tier 1 suppliers: direct suppliers to the OEM - seat manufacturers, brake system vendors, electronics suppliers. ~500-2,000 for a major OEM.
- Tier 2 suppliers: suppliers to tier-1 suppliers - steel mills, chemical companies, electronics component makers. Often unknown to the OEM.
- Tier 3 suppliers: suppliers to tier-2. Rare earth material miners, specialty chemical producers. Highly concentrated geographically.
- Distribution network: warehouses, distribution centers, dealerships.
Each tier represents a delay and a potential disruption point. The OEM only has visibility into tier-1 by default. When a tier-3 supplier in Japan has a factory fire, the OEM learns about it weeks later when tier-1 runs out of components. Supply chain AI extends visibility further up the chain through news monitoring, financial tracking, and logistics data.
Probabilistic Demand Forecasting
A point forecast answers: "What will demand be?" A probabilistic forecast answers: "What is the full distribution of demand?" The difference matters enormously for inventory decisions.
Safety stock is the buffer inventory held above average demand to cover demand variability during lead time. The formula for a given service level is:
where is the z-score for service level (1.65 for 95%, 2.33 for 99%) and is the standard deviation of demand over the lead time. With a probabilistic forecast, you have the full demand distribution, not just the mean and variance - you can compute the exact safety stock needed for any service level, accounting for skewness, heavy tails, and correlation across SKUs.
Quantile regression is the standard approach for probabilistic forecasting in industry. Instead of minimizing squared error (which gives the mean), you minimize the pinball loss for target quantiles:
Training with quantile losses for produces a model that can output the 5th, 25th, 50th, 75th, and 95th percentile of demand - a full picture of the demand distribution.
Supplier Risk Scoring
Supplier risk has multiple dimensions:
- Financial health: Altman Z-score, bond ratings, revenue trends, supplier's customer concentration
- Operational reliability: on-time delivery rate, defect rates, lead time variability
- Geopolitical risk: country risk index, regulatory stability, tariff exposure
- Concentration risk: sole-source components, unique capabilities
- Natural disaster exposure: geographic concentration, proximity to flood zones, earthquake regions
- Cyber risk: security posture, known vulnerabilities
An ML-based supplier risk score aggregates these signals into a single 0-100 score, updated continuously. The score feeds into supplier selection, order allocation (split orders across multiple suppliers), and procurement strategy.
Vehicle Routing Problem (VRP)
The Vehicle Routing Problem is the generalization of the Travelling Salesman Problem to multiple vehicles and capacity constraints. Given a fleet of vehicles at a depot and a set of delivery locations with time windows and demands, find routes for each vehicle that minimize total distance while respecting vehicle capacity and time window constraints.
VRP is NP-hard, so exact solutions are only practical for small instances. Production systems use heuristics and metaheuristics: Google OR-Tools (open source) implements Lin-Kernighan-Helsgott moves, simulated annealing, and guided local search. For dynamic VRP (orders arrive throughout the day), reinforcement learning and attention-based models (Pointer Networks, POMO) have shown competitive performance.
Code Examples
1. Probabilistic Demand Forecast with Quantile Regression
"""
Probabilistic demand forecasting using gradient boosting with quantile regression.
Features:
- Historical demand (lag features)
- Calendar features (day of week, month, holidays)
- Promotional indicators
- Price elasticity features
- External signals (weather, economic indices)
Output: quantile forecasts for demand over planning horizon
"""
import numpy as np
import pandas as pd
from lightgbm import LGBMRegressor
from sklearn.preprocessing import LabelEncoder
from typing import Dict, List, Tuple
import warnings
warnings.filterwarnings("ignore")
def create_demand_features(
df: pd.DataFrame,
date_col: str = "date",
demand_col: str = "demand",
sku_col: str = "sku_id"
) -> pd.DataFrame:
"""
Feature engineering for demand forecasting.
Creates lag features, rolling statistics, and calendar features.
These are the most impactful features for demand forecasting.
"""
df = df.copy().sort_values([sku_col, date_col])
# Lag features (previous demand values)
for lag in [1, 2, 3, 4, 7, 14, 28]:
df[f"lag_{lag}"] = df.groupby(sku_col)[demand_col].shift(lag)
# Rolling statistics
for window in [4, 8, 13, 26]: # Weeks
rolling = df.groupby(sku_col)[demand_col].transform(
lambda x: x.shift(1).rolling(window, min_periods=1).mean()
)
df[f"rolling_mean_{window}w"] = rolling
rolling_std = df.groupby(sku_col)[demand_col].transform(
lambda x: x.shift(1).rolling(window, min_periods=1).std()
)
df[f"rolling_std_{window}w"] = rolling_std
# Coefficient of variation (demand volatility)
df["cv_13w"] = df["rolling_std_13w"] / (df["rolling_mean_13w"] + 1e-10)
# Calendar features
df["week_of_year"] = pd.to_datetime(df[date_col]).dt.isocalendar().week.astype(int)
df["month"] = pd.to_datetime(df[date_col]).dt.month
df["quarter"] = pd.to_datetime(df[date_col]).dt.quarter
df["year"] = pd.to_datetime(df[date_col]).dt.year
# Trend features
df["time_index"] = (
pd.to_datetime(df[date_col]) - pd.to_datetime(df[date_col]).min()
).dt.days
return df
class QuantileDemandForecaster:
"""
Multi-quantile demand forecaster using LightGBM.
Trains separate models for each quantile - this is simpler and
often works better than joint quantile training for tabular data.
"""
def __init__(
self,
quantiles: List[float] = [0.1, 0.25, 0.5, 0.75, 0.9],
forecast_horizon: int = 12, # weeks ahead
feature_cols: Optional[List[str]] = None
):
self.quantiles = quantiles
self.forecast_horizon = forecast_horizon
self.feature_cols = feature_cols
self.models: Dict[float, LGBMRegressor] = {}
def fit(
self,
X_train: pd.DataFrame,
y_train: pd.Series
) -> "QuantileDemandForecaster":
"""Train a LightGBM model for each quantile."""
for q in self.quantiles:
model = LGBMRegressor(
objective="quantile",
alpha=q,
n_estimators=500,
learning_rate=0.05,
num_leaves=63,
min_child_samples=20,
subsample=0.8,
colsample_bytree=0.8,
reg_alpha=0.1,
reg_lambda=0.1,
random_state=42,
n_jobs=-1,
verbose=-1
)
model.fit(
X_train, y_train,
eval_set=[(X_train, y_train)],
callbacks=[
# lgb.early_stopping(50, verbose=False)
]
)
self.models[q] = model
print(f"Quantile {q:.2f} model trained")
return self
def predict(self, X: pd.DataFrame) -> pd.DataFrame:
"""
Predict all quantiles for input features.
Returns DataFrame with columns named by quantile (q10, q50, etc.)
"""
preds = {}
for q, model in self.models.items():
raw_preds = model.predict(X)
# Ensure non-negative demand predictions
preds[f"q{int(q*100):02d}"] = np.maximum(raw_preds, 0)
result = pd.DataFrame(preds, index=X.index)
# Enforce quantile monotonicity (q10 < q25 < q50 etc.)
# Violations can occur with independently trained models
result = self._enforce_monotonicity(result)
return result
def _enforce_monotonicity(self, quantile_df: pd.DataFrame) -> pd.DataFrame:
"""
Ensure q10 < q25 < q50 < q75 < q90 for each row.
Use isotonic regression on each row.
"""
from sklearn.isotonic import IsotonicRegression
ir = IsotonicRegression()
cols = sorted(quantile_df.columns)
quantile_values = [int(c[1:]) / 100 for c in cols]
result = quantile_df.copy()
for idx in range(len(quantile_df)):
row = quantile_df.iloc[idx].values
row_sorted = ir.fit_transform(quantile_values, row)
result.iloc[idx] = row_sorted
return result
def compute_safety_stock(
quantile_forecasts: pd.DataFrame,
lead_time_weeks: int,
service_level: float = 0.95
) -> float:
"""
Compute safety stock from probabilistic forecasts.
Uses simulation approach: aggregate lead-time demand distributions
by summing weekly quantile forecasts, then compute the (1-service_level)
quantile as the safety stock buffer.
lead_time_weeks: procurement lead time
service_level: target probability of no stockout during lead time
"""
# Use Monte Carlo to aggregate weekly forecast distributions
n_simulations = 10000
cumulative_demands = np.zeros(n_simulations)
q_cols = [c for c in quantile_df.columns if c.startswith("q")]
quantile_values = np.array([int(c[1:]) / 100 for c in q_cols])
for week_idx in range(min(lead_time_weeks, len(quantile_forecasts))):
row = quantile_forecasts.iloc[week_idx][q_cols].values
# Sample from piecewise linear interpolation of quantile function
uniform_samples = np.random.uniform(0, 1, n_simulations)
demand_samples = np.interp(uniform_samples, quantile_values, row)
cumulative_demands += demand_samples
mean_lead_time_demand = np.mean(cumulative_demands)
safety_stock_quantile = np.percentile(
cumulative_demands, service_level * 100
)
safety_stock = safety_stock_quantile - mean_lead_time_demand
return max(0.0, safety_stock)
2. Supplier Risk Scoring Model
"""
Supplier risk scoring using a gradient boosting ensemble.
Features drawn from multiple data sources:
- Financial data: D&B ratings, financial ratios
- Operational data: ERP/procurement system
- External data: news sentiment, logistics provider APIs
- Geographic data: country risk indices
"""
import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score
import shap
from typing import Dict, List
class SupplierRiskScorer:
"""
Multi-factor supplier risk scoring model.
Target: probability that supplier fails to deliver on time
within the next 90 days.
Risk dimensions:
1. Financial health (30% weight)
2. Operational reliability (35% weight)
3. Geopolitical exposure (20% weight)
4. Concentration risk (15% weight)
"""
FINANCIAL_FEATURES = [
"altman_z_score",
"current_ratio",
"debt_to_equity",
"revenue_growth_yoy",
"days_payable_outstanding",
"gross_margin",
]
OPERATIONAL_FEATURES = [
"otd_rate_90d", # On-time delivery rate, last 90 days
"otd_rate_365d", # On-time delivery rate, last 365 days
"defect_rate_ppm", # Parts per million defects
"lead_time_variability", # Std dev of lead time in days
"capacity_utilization", # Known production capacity utilization
"quality_incidents_12m", # Count of quality events in 12 months
]
GEOPOLITICAL_FEATURES = [
"country_risk_score", # From providers like Control Risks, Marsh
"political_stability",
"regulatory_risk",
"tariff_exposure", # Current tariff rate as fraction of part cost
"logistics_reliability_index",
]
CONCENTRATION_FEATURES = [
"sole_source_flag", # 1 if this is the only approved supplier
"n_qualified_alternates",
"geographic_concentration_score", # 1 if all production in one site
"customer_concentration", # Fraction of supplier's revenue from this buyer
]
ALL_FEATURES = (
FINANCIAL_FEATURES + OPERATIONAL_FEATURES +
GEOPOLITICAL_FEATURES + CONCENTRATION_FEATURES
)
def __init__(self):
self.pipeline = Pipeline([
("scaler", StandardScaler()),
("model", GradientBoostingClassifier(
n_estimators=300,
learning_rate=0.05,
max_depth=4,
subsample=0.8,
min_samples_leaf=10,
random_state=42
))
])
self.explainer = None
self.fitted = False
def fit(
self,
X_train: pd.DataFrame,
y_train: pd.Series
) -> "SupplierRiskScorer":
"""
Train on historical supplier performance data.
y_train: binary - 1 if supplier had a delivery failure in next 90 days
"""
features = [f for f in self.ALL_FEATURES if f in X_train.columns]
X = X_train[features].fillna(X_train[features].median())
# Cross-validate before final fit
cv_scores = cross_val_score(
self.pipeline, X, y_train, cv=5, scoring="roc_auc"
)
print(f"Cross-validated ROC-AUC: {cv_scores.mean():.3f} +/- {cv_scores.std():.3f}")
self.pipeline.fit(X, y_train)
self.fitted_features = features
# Build SHAP explainer for feature importance and individual explanations
model = self.pipeline.named_steps["model"]
self.explainer = shap.TreeExplainer(model)
self.fitted = True
return self
def score(
self,
X: pd.DataFrame,
return_components: bool = False
) -> pd.DataFrame:
"""
Score suppliers. Returns risk probability (0-1) plus component breakdown.
risk_score: probability of delivery failure in 90 days
risk_category: LOW (<0.2), MEDIUM (0.2-0.5), HIGH (>0.5)
"""
features = [f for f in self.fitted_features if f in X.columns]
X_feat = X[features].fillna(X[features].median())
scaler = self.pipeline.named_steps["scaler"]
X_scaled = scaler.transform(X_feat)
risk_proba = self.pipeline.named_steps["model"].predict_proba(X_feat)[:, 1]
result = pd.DataFrame({
"supplier_id": X.index,
"risk_score": risk_proba,
"risk_category": pd.cut(
risk_proba,
bins=[0, 0.2, 0.5, 1.0],
labels=["LOW", "MEDIUM", "HIGH"]
)
})
if return_components:
# SHAP values explain each feature's contribution to the risk score
shap_values = self.explainer.shap_values(X_scaled)
shap_df = pd.DataFrame(
shap_values, columns=features, index=X.index
)
# Aggregate SHAP by risk dimension
result["financial_risk"] = shap_df[self.FINANCIAL_FEATURES].sum(axis=1)
result["operational_risk"] = shap_df[self.OPERATIONAL_FEATURES].sum(axis=1)
result["geopolitical_risk"] = shap_df[self.GEOPOLITICAL_FEATURES].sum(axis=1)
result["concentration_risk"] = shap_df[self.CONCENTRATION_FEATURES].sum(axis=1)
return result
class SupplyChainRiskMonitor:
"""
Continuous supply chain risk monitoring system.
Combines supplier risk scores with inventory exposure analysis.
"""
def __init__(self, risk_scorer: SupplierRiskScorer):
self.risk_scorer = risk_scorer
def compute_exposure(
self,
supplier_scores: pd.DataFrame,
inventory_data: pd.DataFrame,
bom_data: pd.DataFrame # Bill of materials: part -> supplier mapping
) -> pd.DataFrame:
"""
Compute production exposure for each production line.
exposure_days: estimated production days before stockout
if flagged supplier fails to deliver.
"""
high_risk_suppliers = supplier_scores[
supplier_scores["risk_category"] == "HIGH"
]["supplier_id"].tolist()
exposures = []
for supplier_id in high_risk_suppliers:
# Find all parts sourced from this supplier
affected_parts = bom_data[
bom_data["supplier_id"] == supplier_id
]["part_number"].tolist()
for part in affected_parts:
inv_row = inventory_data[
inventory_data["part_number"] == part
]
if len(inv_row) == 0:
continue
daily_usage = float(inv_row["daily_usage"].iloc[0])
current_stock = float(inv_row["current_stock"].iloc[0])
in_transit = float(inv_row["in_transit_qty"].iloc[0])
exposure_days = (current_stock + in_transit) / (daily_usage + 1e-10)
exposures.append({
"supplier_id": supplier_id,
"part_number": part,
"supplier_risk_score": supplier_scores[
supplier_scores["supplier_id"] == supplier_id
]["risk_score"].iloc[0],
"exposure_days": round(exposure_days, 1),
"action_priority": "URGENT" if exposure_days < 30 else "WATCH"
})
return pd.DataFrame(exposures).sort_values("exposure_days")
3. Multi-Echelon Inventory Optimizer
"""
Multi-echelon inventory optimization.
Optimizes safety stock levels across the supply chain network
(factory, distribution centers, regional warehouses)
to achieve target service levels at minimum inventory cost.
"""
import numpy as np
from scipy.optimize import minimize
from scipy.stats import norm
from typing import List, Dict, Tuple
class EchelonNode:
"""Represents one node in the supply chain network."""
def __init__(
self,
name: str,
mean_demand_per_day: float,
std_demand_per_day: float,
lead_time_days: float,
lead_time_std_days: float,
holding_cost_per_unit_day: float,
target_service_level: float = 0.95
):
self.name = name
self.mu_d = mean_demand_per_day
self.sigma_d = std_demand_per_day
self.mu_lt = lead_time_days
self.sigma_lt = lead_time_std_days
self.h = holding_cost_per_unit_day
self.sl = target_service_level
@property
def demand_during_lead_time(self) -> Tuple[float, float]:
"""
Mean and standard deviation of demand during lead time.
Lead time demand variance = sigma_d^2 * mu_lt + mu_d^2 * sigma_lt^2
"""
mu_dlt = self.mu_d * self.mu_lt
sigma_dlt = np.sqrt(
self.sigma_d**2 * self.mu_lt + self.mu_d**2 * self.sigma_lt**2
)
return mu_dlt, sigma_dlt
def safety_stock_for_service_level(self, sl: float) -> float:
"""Compute required safety stock for given service level."""
z = norm.ppf(sl)
_, sigma_dlt = self.demand_during_lead_time
return z * sigma_dlt
def holding_cost(self, safety_stock: float) -> float:
"""Annual holding cost for given safety stock level."""
return safety_stock * self.h * 365
class MultiEchelonInventoryOptimizer:
"""
Optimize safety stock levels across a multi-tier supply network.
Objective: minimize total network holding cost
Constraint: each node meets its minimum service level
Uses the Graves-Willems algorithm as inspiration:
service times propagate upstream, safety stocks protect downstream.
"""
def __init__(self, nodes: List[EchelonNode], network_topology: Dict):
"""
nodes: list of EchelonNode objects
network_topology: dict mapping downstream -> upstream node names
Example: {"DC_East": "Factory", "DC_West": "Factory"}
"""
self.nodes = {n.name: n for n in nodes}
self.topology = network_topology
def optimize(
self,
verbose: bool = True
) -> Dict[str, Dict]:
"""
Find safety stock levels that minimize total holding cost
while meeting service level constraints at every node.
Returns dict of node_name -> {safety_stock, reorder_point, service_level}
"""
node_names = list(self.nodes.keys())
n = len(node_names)
# Decision variables: safety stock multiplier (z-score) for each node
# Constraints: z >= z_min for each node's service level requirement
x0 = np.array([norm.ppf(self.nodes[name].sl) for name in node_names])
bounds = [
(norm.ppf(0.70), norm.ppf(0.999)) # z from 70% to 99.9% service level
for _ in range(n)
]
def total_cost(z_values: np.ndarray) -> float:
cost = 0.0
for i, name in enumerate(node_names):
node = self.nodes[name]
_, sigma_dlt = node.demand_during_lead_time
ss = z_values[i] * sigma_dlt
cost += node.holding_cost(ss)
return cost
constraints = []
for i, name in enumerate(node_names):
min_z = norm.ppf(self.nodes[name].sl)
constraints.append({
"type": "ineq",
"fun": lambda z, i=i, min_z=min_z: z[i] - min_z
})
result = minimize(
total_cost,
x0,
method="SLSQP",
bounds=bounds,
constraints=constraints,
options={"ftol": 1e-6, "maxiter": 1000}
)
recommendations = {}
for i, name in enumerate(node_names):
node = self.nodes[name]
mu_dlt, sigma_dlt = node.demand_during_lead_time
z_star = result.x[i]
ss = z_star * sigma_dlt
rop = mu_dlt + ss # Reorder point
recommendations[name] = {
"safety_stock_units": round(ss, 1),
"reorder_point": round(rop, 1),
"achieved_service_level": norm.cdf(z_star),
"annual_holding_cost": round(node.holding_cost(ss), 2),
"z_score": round(z_star, 3)
}
if verbose:
print(f"\n{name}:")
print(f" Safety stock: {ss:.1f} units ({norm.cdf(z_star)*100:.1f}% SL)")
print(f" Reorder point: {rop:.1f} units")
print(f" Annual holding cost: ${node.holding_cost(ss):,.0f}")
total = sum(v["annual_holding_cost"] for v in recommendations.values())
if verbose:
print(f"\nTotal network holding cost: ${total:,.0f}/year")
return recommendations
4. VRP Solver with OR-Tools
"""
Vehicle Routing Problem solver using Google OR-Tools.
Minimizes total distance for delivery routes from a depot to customers.
"""
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
import numpy as np
from typing import List, Tuple, Dict
def solve_vrp(
distance_matrix: np.ndarray,
demands: List[int],
vehicle_capacity: int,
num_vehicles: int,
depot: int = 0,
time_limit_seconds: int = 30
) -> Dict:
"""
Solve Capacitated VRP (CVRP) with OR-Tools.
Args:
distance_matrix: N x N matrix of distances between locations
demands: list of demand at each location (depot has demand 0)
vehicle_capacity: max load per vehicle
num_vehicles: number of available vehicles
depot: index of depot location
Returns dict with routes, total distance, and per-vehicle loads
"""
n_locations = len(demands)
# Create routing index manager
manager = pywrapcp.RoutingIndexManager(n_locations, num_vehicles, depot)
routing = pywrapcp.RoutingModel(manager)
# Distance callback
def distance_callback(from_index, to_index):
from_node = manager.IndexToNode(from_index)
to_node = manager.IndexToNode(to_index)
return int(distance_matrix[from_node][to_node])
transit_callback_index = routing.RegisterTransitCallback(distance_callback)
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
# Capacity constraint
def demand_callback(from_index):
from_node = manager.IndexToNode(from_index)
return demands[from_node]
demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback)
routing.AddDimensionWithVehicleCapacity(
demand_callback_index,
0, # No slack
[vehicle_capacity] * num_vehicles,
True, # Start cumul at zero
"Capacity"
)
# Search parameters
search_params = pywrapcp.DefaultRoutingSearchParameters()
search_params.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
)
search_params.local_search_metaheuristic = (
routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH
)
search_params.time_limit.FromSeconds(time_limit_seconds)
# Solve
solution = routing.SolveWithParameters(search_params)
if not solution:
return {"status": "no_solution", "routes": []}
routes = []
total_distance = 0
for vehicle_id in range(num_vehicles):
index = routing.Start(vehicle_id)
route = []
route_distance = 0
route_load = 0
while not routing.IsEnd(index):
node = manager.IndexToNode(index)
route.append(node)
route_load += demands[node]
previous_index = index
index = solution.Value(routing.NextVar(index))
route_distance += routing.GetArcCostForVehicle(
previous_index, index, vehicle_id
)
route.append(depot) # Return to depot
if len(route) > 2: # Exclude empty routes
routes.append({
"vehicle": vehicle_id,
"stops": route,
"distance": route_distance,
"load": route_load
})
total_distance += route_distance
return {
"status": "optimal" if solution.ObjectiveValue() == routing.CostVar().Min() else "feasible",
"total_distance": total_distance,
"routes": routes,
"n_vehicles_used": len(routes)
}
5. Disruption News Classifier with BERT
"""
Supply chain disruption detection from news and social media.
Fine-tunes a BERT model to classify news articles as
disruption-relevant or not, and extract affected suppliers/regions.
"""
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from transformers import TrainingArguments, Trainer
from torch.utils.data import Dataset
import numpy as np
from typing import List, Tuple
class SupplyChainDisruptionDataset(Dataset):
"""
Dataset for fine-tuning BERT on supply chain disruption classification.
Labels:
0: Not a supply chain disruption
1: Potential disruption (worth monitoring)
2: Confirmed disruption requiring action
"""
def __init__(self, texts: List[str], labels: List[int], tokenizer, max_length: int = 256):
self.encodings = tokenizer(
texts,
truncation=True,
max_length=max_length,
padding="max_length",
return_tensors="pt"
)
self.labels = labels
def __len__(self):
return len(self.labels)
def __getitem__(self, idx):
return {
"input_ids": self.encodings["input_ids"][idx],
"attention_mask": self.encodings["attention_mask"][idx],
"labels": torch.tensor(self.labels[idx], dtype=torch.long)
}
class DisruptionDetector:
"""
BERT-based supply chain disruption classifier.
Screens incoming news articles, social media posts, and logistics
data for signals of supply chain disruption.
Disruption types detected:
- Natural disasters near supplier facilities
- Port congestion and shipping delays
- Labor disputes at key suppliers or ports
- Regulatory/trade policy changes
- Cyber attacks on logistics providers
"""
def __init__(self, model_name: str = "bert-base-uncased", num_labels: int = 3):
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = AutoModelForSequenceClassification.from_pretrained(
model_name, num_labels=num_labels
)
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.model.to(self.device)
self.id2label = {0: "no_disruption", 1: "potential", 2: "confirmed"}
def predict(self, texts: List[str], batch_size: int = 32) -> List[dict]:
"""
Classify news articles for supply chain disruption signals.
Returns list of dicts with label, confidence, and severity.
"""
self.model.eval()
all_results = []
for i in range(0, len(texts), batch_size):
batch_texts = texts[i:i+batch_size]
encodings = self.tokenizer(
batch_texts,
truncation=True,
max_length=256,
padding=True,
return_tensors="pt"
).to(self.device)
with torch.no_grad():
outputs = self.model(**encodings)
probs = torch.softmax(outputs.logits, dim=-1).cpu().numpy()
for j, text in enumerate(batch_texts):
pred_label = int(np.argmax(probs[j]))
confidence = float(probs[j][pred_label])
all_results.append({
"text_preview": text[:100] + "..." if len(text) > 100 else text,
"label": self.id2label[pred_label],
"confidence": round(confidence, 3),
"disruption_probability": round(float(probs[j][1] + probs[j][2]), 3),
"severity": "HIGH" if probs[j][2] > 0.5 else
("MEDIUM" if probs[j][1] > 0.3 else "LOW")
})
return all_results
System Architecture
Production Engineering Notes
The Bullwhip Effect and How AI Reduces It
The bullwhip effect: small variations in retail demand amplify as they propagate upstream through the supply chain. A 5% demand fluctuation at retail becomes a 10% fluctuation at the distributor, a 20% fluctuation at the manufacturer, and a 40% fluctuation at the raw material supplier. This happens because each tier adds safety stock on top of the uncertainty it perceives.
AI reduces the bullwhip effect by two mechanisms. First, sharing the demand signal directly: if the raw material supplier can see the real-time retail POS data, they do not need to infer demand from orders - they see the same signal you see, with minimal distortion. This requires supply chain data sharing agreements, but the economic incentive is strong. Second, probabilistic forecasting with explicit uncertainty quantification gives each tier an accurate picture of demand variability. They set safety stock based on actual demand variance, not an inflated estimate of what the tier below them might order.
Forecast Value Added
Before deploying an ML forecasting system, measure the current forecast accuracy: MAPE (Mean Absolute Percentage Error), RMSE, and bias. Then measure Forecast Value Added (FVA): the improvement in forecast accuracy versus the naive baseline (forecast = last period's actuals). If your ML model improves on the naive baseline by 5%, it has positive FVA. If it does not beat the naive baseline, it is not adding value regardless of how sophisticated it is.
A well-built ML forecaster should beat the naive baseline by 15-30% for most SKUs. For very intermittent demand (SKUs with many zero-demand periods), the Croston method or zero-inflated models often outperform standard ML approaches. Always segment your SKU portfolio by demand pattern (continuous high-volume, intermittent low-volume, seasonal) and use the best model for each segment.
Model Refresh Frequency
Demand patterns shift over time. Product lifecycle changes, seasonality evolves, new competitors enter. Your forecasting model must be retrained regularly. The typical cadence: daily model updates with new data incorporated, weekly full retraining with hyperparameter check, monthly business review of forecast accuracy by product family.
Supplier risk models need continuous data updates (financial data refreshed weekly, news data daily, operational metrics updated from ERP on every procurement cycle) but full model retraining monthly - supplier landscapes change slowly.
:::warning Safety Stock Underestimation from Point Forecasts If you compute safety stock using only the mean absolute error (MAE) of your forecast - a common practice - you are underestimating required safety stock for all SKUs with skewed or heavy-tailed demand distributions. A single large demand event (a promotion, a competitor stockout, a bulk order) can exceed many months of average demand. Always use the full demand distribution to set safety stock, not just forecast error statistics. :::
:::danger Single-Source Dependency If your supply chain has any component that comes from a single approved supplier with no qualified alternative, that component is a hidden catastrophic risk. The risk is invisible during normal operations - the single supplier delivers reliably for years. Then they have a fire, a strike, or a regulatory shutdown, and you have zero alternatives. Supplier risk AI will not save you here - the solution is procurement policy: require qualification of at least two sources for every critical component. Use the risk scoring to identify and prioritize which single-source components to de-risk first. :::
Interview Questions and Answers
Q1: What is the bullwhip effect and how does probabilistic forecasting address it?
The bullwhip effect is the amplification of demand variability as you move upstream in a supply chain. A 5% swing in consumer demand might become a 40% swing in orders to the raw material supplier because each tier adds safety stock on top of perceived variability. It is caused by order batching, price fluctuation, shortage gaming, and most importantly, the loss of demand signal fidelity as each tier infers demand from the tier below it rather than from the true source. Probabilistic forecasting addresses two root causes: it provides each tier with an accurate uncertainty estimate (so they do not over-buffer), and when the probabilistic forecast includes confidence intervals based on actual retail demand signals rather than downstream order patterns, each tier effectively sees the true demand signal. The combination of accurate uncertainty quantification and demand signal sharing throughout the supply chain is the most effective reduction of bullwhip effect.
Q2: How do you handle cold-start in supply chain demand forecasting - a brand new product with no sales history?
Several approaches, each appropriate for different contexts. First, similarity-based forecasting: find the most similar historical product in your catalog (by category, price point, target market, competitive position) and use its demand ramp-up as the forecast for the new product, adjusting for any known differences. This works well when you have a rich product catalog with similar prior introductions. Second, causal modeling: use market research data (survey intent, sample testing response rates) as leading indicators, scaled by historical conversion factors. This is common in consumer goods. Third, hierarchical forecasting: even if the new SKU has no history, the product category and brand do - use top-down allocation from the category forecast. Fourth, for truly novel products with no analogues, Bayesian forecasting with diffuse priors is the honest answer - you have wide uncertainty and your safety stock should reflect this. Communicating this uncertainty to planners and setting inventory policy appropriately (accept higher stockout risk early while waiting for real demand signal) is a key skill.
Q3: Walk through how you would compute optimal safety stock for a component with probabilistic lead times.
This is a simulation problem when lead times are stochastic. The standard formula assumes demand variance and lead time variance are both Gaussian and independent. For the combined variance: where is mean lead time, is demand standard deviation per period, is mean demand, and is lead time standard deviation. But in practice, lead time distributions are often heavily right-skewed - a supplier usually delivers in 14 days but occasionally takes 60 - and demand during a 60-day lead time is very different from demand during 14 days. The accurate approach is Monte Carlo simulation: sample lead time from the empirical distribution, then sample demand for that lead time duration, repeat 10,000 times, and take the (1-service_level) percentile of the resulting cumulative demand distribution. This gives accurate safety stock without Gaussian assumptions.
Q4: How do you use graph neural networks for supply chain risk modeling?
The supply chain is naturally represented as a graph: nodes are companies (suppliers, manufacturers, distributors), edges represent supply relationships. A tier-2 disruption propagates through edges to affect tier-1 suppliers and then to the OEM - the propagation follows the graph structure. GNNs exploit this structure. You build a heterogeneous graph where each node has features (financial health, location, capacity, lead times) and edges have features (volume, criticality, lead time). The GNN learns representations for each node by aggregating information from its neighbors, capturing how a disruption at one node affects connected nodes. This enables the model to predict which upstream disruptions will materialize as downstream delivery problems - something a node-level risk score cannot do because it ignores network structure. The practical challenges are graph construction (data quality and completeness for tier-2 and tier-3 is poor) and graph dynamics (the supply network changes over time as contracts are won and lost).
Q5: A demand forecast is showing large errors for a subset of SKUs. How do you diagnose and fix this?
Start by segmenting the error by demand pattern. Separate SKUs into: high-volume continuous demand (where standard ML should work well), intermittent low-volume demand (where Croston or ADI/CV2 classification is needed), seasonal demand (where seasonality modeling is critical), and event-driven demand (promotions, new product launches). If the problematic SKUs are in the intermittent category, your standard model is probably optimizing for average error but failing on the zero-demand periods. Switch to a count model (Poisson regression, negative binomial) or Croston's method for these. If the problem is in seasonal SKUs, check whether your features include Fourier terms or seasonal lags at the right frequency. If the issue is across-the-board on a specific time period, check for data quality issues - a system migration, a change in how promotions are recorded, or a change in the demand capture process. Always look at the residuals over time; systematic bias (model consistently over- or under-predicts for a period) indicates a missing feature or a structural break in the data generating process.
Key Takeaways
Supply chain AI operates across four domains: demand forecasting (probabilistic, not point estimates), supplier risk (multi-dimensional scoring updated continuously), inventory optimization (multi-echelon, treating the network as a system rather than independent nodes), and disruption response (real-time news classification and exposure analysis). The critical insight throughout: uncertainty quantification is not optional - safety stock, supplier diversification, and disruption response all require knowing not just the best-case outcome but the distribution of outcomes. The companies that navigated COVID-19 and the Suez Canal disruption best were the ones that had invested in supply chain visibility and probabilistic risk modeling before the crisis hit.
