Quicopt
Live demo

Inventory optimization, to the point

Planning with the average can get expensive.

It’s the classic optimization problem: the order has to go out today, and the sales figures of the last few years give a clear signal on average. But real demand is not an average — it fluctuates from day to day and therefore follows a distribution.

Let’s say a unit in stock costs you €1 per day. Use the slider to set what a missing unit costs you by comparison.

You describe your situation

How much does a missing unit cost you?

€4

That’s all you need to provide. Quicopt finds the optimal order quantity.

Distribution across 500 days

demand coveredshortfall

✗ planned with the average✓ Quicopt’s optimum

40120200280360ACTUAL DEMANDQ* = 243

That works out to a 80% service level. Covering the rest completely would cost more in stock than it saves in shortages.

Which order quantity really pays off across all possible scenarios?

If you plan with the average

€104

Optimal order quantity Q* = 243

€75
28% cheaper
Cost per missing unit€4
€37€175€313€451€58960120180240300360ORDER QUANTITYexpected cost across 500 daysQ* = 243

How it works

You could simply take average demand and plan with it. But that misleads you, because the costs don’t rise evenly: a shortfall weighs heavier than surplus stock. Anyone working only with the average overlooks these expensive outliers. Quicopt does the opposite: it evaluates every possible day individually with your actual costs, and averages only afterwards. That way the bad days are counted correctly, and you get the order quantity that is cheapest across all scenarios. In short: don’t average the demand — choose the order quantity whose expected cost across all possible days is lowest.

In real systems

This demo deliberately shows the simplest example, with a single product, so the principle is immediately clear. In operation, Quicopt optimizes hundreds of items at once, with shared budget, limited warehouse space and supply constraints.

One step further

Eight products, one shelf

For a single product there is still a formula for the best order quantity. The moment several products share the same space, that ends: what product A gets, product B goes without. Here there are eight products, each with its own demand and its own shortage cost, sharing 1020 units of shelf space — less than their average demand adds up to.

Shelf split by average demand
€1,381.6
per day
Optimized by Quicopt
€1,041.9
per day · 24% cheaper

Where the space goes

ProductAvg demandShortage costsby averageoptimized
A200€4170194
B150€4128131
C90€1276112
D300€225580
E120€9102142
F60€155183
G180€3153168
H100€785110

Product D is the striking one: it has by far the highest average demand and still gets the least — because running out of it only costs €2. Products C and F are stocked the other way, above their own average. No rule built on averages arrives at this split.

The complete model

That is all of it. The code runs against Quicopt’s API endpoint — no solver runs on your machine.

inventory_multi.jl
using QuicoptClient, JuMP

products = ["A", "B", "C", "D", "E", "F", "G", "H"]
mu       = [200.0, 150.0,  90.0, 300.0, 120.0,  60.0, 180.0, 100.0]  # average daily demand
sigma    = [ 20.0,  55.0,  30.0,  30.0,  45.0,  25.0,  18.0,  38.0]  # how much it varies
short    = [  4.0,   4.0,  12.0,   2.0,   9.0,  15.0,   3.0,   7.0]  # cost of one missing unit
hold     = 1.0                                                       # cost of one unit in stock
capacity = 1020.0                                                    # shared shelf space

m = Model()
set_scenarios(m, 500; seed = 7)                  # 500 possible days, drawn once

@variable(m, Q[1:8] >= 0, Int)                   # what we order, in whole units
@variable(m, d[1:8])                             # demand: one random variable per product
for i in 1:8
    set_distribution(m, d[i], :normal, mu[i], sigma[i]; name = Symbol("demand_", products[i]))
end

@constraint(m, sum(Q) <= capacity)               # everything has to fit on the shelf

cost = sum(short[i] * max(d[i] - Q[i], 0.0) + hold * max(Q[i] - d[i], 0.0) for i in 1:8)
@objective(m, Min, expectation(cost))            # expected cost over all 500 days

result = solve(m)

Output

▄▀▀▀▄        ▄                     █
█   █ █   █ ▄▄  ▄▀▀▀▄ ▄▀▀▀▄ █▀▀▀▄ ▀█▀▀
█ ▀▄▀ █   █  █  █   ▄ █   █ █▄▄▄▀  █  ▄
 ▀▀ ▀  ▀▀▀▀ ▀▀▀  ▀▀▀   ▀▀▀  █       ▀▀
├── status:     heuristic
├── feasible:   true
├── objective:  1041.8957479341643
├── x:          x1=194, x2=131, x3=112, x4=80, x5=142, x6=83, …  (8 variables)
└── solve_time: 1.0182 s
planned with the average : 1381.6 EUR/day
optimized                : 1041.9 EUR/day

product   avg demand   by average   optimized
   A          200          170         194
   B          150          128         131
   C           90           76         112
   D          300          255          80
   E          120          102         142
   F           60           51          83
   G          180          153         168
   H          100           85         110

And now it gets interesting

Quicopt works purely primally: it evaluates your cost function scenario by scenario without presupposing any structure. So you can formulate costs and conditions that have no “smooth” mathematical formulation. The two examples below each add exactly one line to the model above, and nothing else changes:

A flat cost falls due the moment a product cannot be served

@objective(m, Min, expectation(cost) + 50.0 * sum(prob(d[i] - Q[i], ≥, 0.0) for i in 1:8))

A step function jumps at the first missing unit and then stays flat. Precisely the kind of term that defeats methods relying on derivatives. Against the simpler model above, this comes out more expensive: → €1,215.7 per day

A delivery guarantee as a hard requirement

@constraint(m, prob(sum(max(d[i] - Q[i], 0.0) for i in 1:8), ≤, 350.0) >= 0.90)

This constraint reads as “on 90% of days, total shortfall stays under 350 units.” So you see straight away what such a guarantee would cost you — about 5% against the roughly €1042 per day above: → €1,095.7 per day

For context: the solver status reads “heuristic”: for this class of objective there is no proof of optimality, and the solver returns the best solution found. Several allocations come within less than a per mille of each other, so a later version of the solver may return a slightly different one.

Would you like to see this with your own numbers?

Get in touch

A demo by Quicopt · quicopt.com