Retail logistics · Optimization · Feasibility studyCJ AI Center, with CJ FreshwayWrite-up October 2026 · 7 min read

Placing stock across a food-distribution network

Conservative inventory keeps shelves full and centres overflowing. The question was whether one model could decide, SKU by SKU, where stock should live, which centre should serve each demand point, and how many pallets to hold — and how much capacity and transport cost that would free.

Built withPythonNumPypandasPySCIPOpt · SCIPMIQCPJupyter
Hubs fill with pallets; depots only consume. Every arrow is a single-sourcing decision, and every pallet is safety stock that pooling might remove.

The cost of playing it safe

A food distributor's inventory decisions ripple into centre capacity, transport cost, stock-outs and overstock all at once. A conservative policy — keep plenty of every SKU close to where it sells — protects service but fills storage, multiplies safety stock, and moves goods between centres that did not need to hold them.

After interviews with the business on ordering logic, inventory policy, capacity and transport conditions, the objective was set as improving capacity utilization and reducing transport cost while keeping supply stable. That made it an optimization problem rather than a forecasting one: given what each centre is likely to sell, where should stock live?

Three decisions, one model

For every SKU the model decides together:

The objective is transport cost over four weeks plus a weighted holding cost per pallet. The interesting part is the safety buffer. When a hub serves several demand points, their uncertainties pool: the buffer grows with the square root of the summed variances, not with their sum. Consolidating a SKU into fewer hubs therefore saves pallets even when it adds transport — and the model has to weigh exactly that trade, SKU by SKU, under shared capacity.

IngredientHow it enters
DemandPer SKU and centre, a weighted mean and spread from the last 90 days — 80 / 15 / 5 % by month — measured from the first day the item sold there.
Safety stocklead time × mean + 3 × √(lead time × Σ variance) over the demand points a hub serves.
PalletsRounded up to whole pallets, at least the minimum order quantity, zero if the centre does not hold the item.
FeasibilityChecked per SKU before the model: an item whose demand cannot be reached from any allowed centre is set aside instead of making the whole model infeasible.

Live model, solved on a Python server for a generated network of eight centres and 24 SKUs. Left: every SKU stocked where it sells. Right: the optimized storage, sourcing and pallet counts. Change the holding-cost weight to watch the model consolidate harder as storage gets more expensive; "New network" solves a fresh one. One SKU is planted to be unreachable and is caught by the pre-check. Open the live model on its own page ↗

How it's built

Architecture, stack and core formulation

A notebook-level feasibility study: recency-weighted demand statistics, a per-SKU feasibility check and one mixed-integer model with a quadratic safety-stock constraint, solved with SCIP.

1 · Demand

Recency-weighted statistics

Mean and standard deviation of daily demand per item and location, with recent months weighted more.

NumPypandas
2 · Pre-check

Can it be served at all?

For each SKU, check that one to three centres can cover its demand points within capacity — a single unreachable SKU would make the whole model infeasible.

itertoolspandas
3 · Model

One MIQCP

Storage, single sourcing, pooled safety stock and whole pallets in one nonconvex mixed-integer quadratic model.

PySCIPOptSCIP
4 · Report

Tables for the business

Placement, sourcing, pallets and capacity use per centre, with cost and pallet totals.

pandasJupyter
Stack
LayerTechnologyWhat it does here
DataPython, pandas, NumPyDaily demand per item × location, lanes and costs, lead times, MOQ and case packs
StatisticsRecency-weighted mean and varianceLead-time demand and safety-stock inputs
FeasibilityCombinatorial pre-checkFilters SKUs that no centre combination can serve
SolverSCIP through PySCIPOptNonconvex MIQCP with a time limit and an optimality-gap target
FormJupyter notebookProof of concept; no service layer
Core formulation
y_icj ∈ {0,1}   centre c supplies demand point j with item i      (single source)
z_ic  ∈ {0,1}   item i stocked at centre c          p_ic ∈ ℤ₊   pallets

min   Σ cost_cj · w_i · μ_ij · H · y_icj   +   w_s · Σ p_ic
s.t.  Σ_c y_icj = 1,       y_icj ≤ z_ic,       Σ_c z_ic ≤ K
      S_ic² = LT_i · Σ_j σ_ij² · y_icj                         pooled safety stock
      R_ic ≥ LT_i · Σ_j μ_ij · y_icj + 3·S_ic,      R_ic ≥ MOQ_i · b_i · z_ic
      b_i·p_ic ≥ R_ic,     b_i·p_ic ≤ R_ic + b_i − 1           p = ⌈R / b⌉
      Σ_i p_ic ≤ cap_c
  • Why it is nonconvex. The equality S² = LT·Σσ²y is quadratic; the second-order-cone form S ≥ ‖√LT·σ∘y‖₂ would make the model convex — noted as the next step.
  • Pooling drives the trade. Safety stock grows with the square root of pooled variance, so fewer hubs mean fewer pallets but longer lanes.
  • Feasibility first. Filtering unreachable SKUs before the solve keeps a full-scale model from being infeasible.
In production vs in the live model
ComponentIn productionIn the live model above
SolverOne MIQCP in SCIP over thousands of SKUs × tens of centresPer-SKU plan enumeration + an integer program (SciPy / HiGHS) choosing one plan per SKU
Safety stockQuadratic constraint inside the modelComputed exactly for each enumerated plan
ScaleThousands of SKUs × tens of centres24 SKUs × 8 centres generated from a seed

Inside the model

A square root inside an integer program

Written directly, the pooled buffer is a quadratic equality — the squared buffer equals the lead-time-weighted sum of variances of the points assigned to the hub — which makes the model a non-convex mixed-integer quadratically constrained program. It was solved with SCIP, which branches on the non-convexity; the same constraint can also be written as a second-order cone, which keeps it convex and is the direction for scaling up. Linking constraints tie assignment to storage, force the rounded-up pallet count, and enforce capacity and self-supply.

Methods compared

The study did not assume the MIP was the answer. Mathematical programming, search-based optimization and reinforcement learning were reviewed against each other, and a simulation structure was built to evaluate how a change in policy moves capacity, cost and service level — so the business could test alternatives rather than accept a single number.

How the demo solves it

The live model keeps the same ingredients but solves a small network a different way: for each SKU it enumerates every feasible set of storage centres, routes each demand point to its cheapest open hub, and sizes the pooled pallets; a small integer program then picks one plan per SKU subject to centre capacity. For a network this size that gives the same optimum in milliseconds, and it makes the trade-off visible.

What it showed

Capacitypotential to free logistics-centre space, quantified
Transportcost-reduction potential, quantified
Policyshift proposed: conservative → demand-based inventory

As a feasibility study, the result was a case rather than a deployment: the optimization confirmed room to secure centre capacity and cut transport cost, and it proposed moving from conservative stocking to demand-based inventory operation. Just as important, it left a decision environment — data marts joining inventory, demand and capacity, plus the model and simulation — in which inventory policy alternatives can be tested before they are tried.

Limitations

About the demo and confidentiality

The centres, SKUs, demand histories, lanes and costs in the embedded model are generated from a seed. No network layout, item master, cost table or result from the real study appears here.

Taehee Lee · Data Scientist / Applied AI Scientist, CJ AI CenterStakeholder interviews, objective definition, data marts, model and method comparison, simulation design. Demo re-implemented on synthetic data for this site.