The Problem
In inventory management, the Integer Order Quantity Problem asks: how many units of each part should we order to minimize total cost, given that order quantities must be whole numbers?
This is fundamentally different from the continuous EOQ (Economic Order Quantity) model β real-world orders come in boxes, pallets, or minimum lot sizes. Fractional orders donβt exist.
Problem Setup
Suppose a factory orders 3 types of components (A, B, C) from a supplier. Each part has:
- An annual demand $D_i$
- A unit cost $c_i$
- An ordering cost $K_i$ (fixed cost per order)
- A holding rate $h$ (fraction of unit cost per year)
- A minimum order quantity (MOQ) $q_i^{\min}$ and lot size $l_i$
The Total Annual Cost for part $i$ is:
$$TC_i(Q_i) = \frac{D_i}{Q_i} \cdot K_i + \frac{Q_i}{2} \cdot c_i \cdot h$$
Where:
- $\frac{D_i}{Q_i} \cdot K_i$ β annual ordering cost
- $\frac{Q_i}{2} \cdot c_i \cdot h$ β annual holding cost
The continuous EOQ is:
$$Q_i^* = \sqrt{\frac{2 D_i K_i}{c_i \cdot h}}$$
But since $Q_i$ must satisfy:
$$Q_i \in {q_i^{\min},\ q_i^{\min} + l_i,\ q_i^{\min} + 2l_i,\ \ldots}$$
We solve this as a Mixed-Integer Optimization problem.
Part Data
| Part | Annual Demand | Unit Cost | Order Cost | MOQ | Lot Size |
|---|---|---|---|---|---|
| A | 1200 units | Β₯500 | Β₯3,000 | 50 | 10 |
| B | 3600 units | Β₯200 | Β₯1,500 | 100 | 20 |
| C | 800 units | Β₯1,200 | Β₯5,000 | 20 | 5 |
Holding rate: $h = 0.20$ (20% per year)
Python Source Code
1 | # ============================================================ |
Code Walkthrough
1. total_cost(Q, D, c, K, h)
This implements the classic EOQ cost formula:
$$TC(Q) = \frac{D}{Q} \cdot K + \frac{Q}{2} \cdot c \cdot h$$
Both terms trade off against each other β ordering cost decreases with larger Q, while holding cost increases.
2. eoq_continuous(D, c, K, h)
The analytical minimum of $TC(Q)$ via calculus:
$$\frac{dTC}{dQ} = 0 \implies Q^* = \sqrt{\frac{2DK}{ch}}$$
This gives us a benchmark β the ideal quantity ignoring integer/lot constraints.
3. feasible_quantities(moq, lot, max_Q)
Generates the set of valid order quantities:
$$\mathcal{Q}_i = {q_i^{\min},\ q_i^{\min} + l_i,\ q_i^{\min} + 2l_i,\ \ldots}$$
In NumPy this is simply np.arange(moq, max_Q, lot).
4. integer_eoq(...) β The Core Optimizer
The key insight: the integer optimum lies near the continuous EOQ. We:
- Locate the continuous EOQ $Q^*$
- Find the nearest feasible step below it
- Evaluate $TC(Q)$ for
search_rangesteps in both directions - Return the $Q$ with minimum cost
This runs in $O(2 \times \text{search_range})$ β extremely fast compared to exhaustive search over thousands of candidates.
Visualization Explained
Top Row β TC Curves (one per part)
Each chart shows $TC(Q)$ as a smooth curve over all feasible $Q$. The vertical gray lines mark every feasible lot-size-aligned quantity. The white dot marks the integer optimum. You can visually confirm it sits at the bottom of the curve.
Notice that the curve is asymmetric: it rises steeply for small $Q$ (many orders, high ordering cost) but more gradually for large $Q$ (excess inventory, linear holding cost increase).
Bottom Left β 3D Surface Plot
This is the joint cost surface $TC(Q_A, Q_B)$ with Part C fixed at its optimum. The landscape forms a bowl shape β the minimum is clearly visible as the white dot at the bottom. The integer lattice structure means the true optimum snaps to the nearest feasible grid point, not the smooth bowlβs analytic minimum.
This 3D view makes it viscerally clear why integer constraints matter: the continuous optimum floats freely in the bowl, while the integer optimum must land on a discrete grid.
Bottom Right β Cost Breakdown Bar Chart
This separates ordering cost (darker bar) from holding cost (lighter bar) for each part. A perfectly balanced EOQ would have these equal β deviations reveal where the integer rounding pushes cost balance off.
Part C is notable: its high unit cost (Β₯1,200) means holding cost dominates, pushing the optimal quantity down and lot-size constraints become more binding.
Expected Output
1 | ================================================================= |
================================================================= Part EOQ(cont) EOQ(int) TC(cont) TC(int) Gap% ----------------------------------------------------------------- A 268.3 270 26,833 26,833 0.00% B 519.6 520 20,785 20,785 0.00% C 182.6 185 43,818 43,822 0.01% ================================================================= Total Annual Cost (integer): Β₯91,440 Total Annual Cost (continuous): Β₯91,435

[Figure saved as integer_eoq.png]
Key Takeaways
1. The gap is tiny but real. Integer constraints add only 0.00β0.03% cost in this example β but in high-volume manufacturing across hundreds of parts, this compounds.
2. Lot size matters more than MOQ. Coarser lot sizes mean fewer feasible candidates near the continuous EOQ, increasing the potential rounding gap.
3. The 3D surface reveals interaction effects. Even though parts are ordered independently here, the visualization shows how a joint ordering policy (e.g., combined shipments) could be analyzed as a 3D optimization over the bowl.
4. Integer programming scales to real problems. For large part catalogs with shared supplier constraints, budget limits, or volume discounts, this same approach extends naturally to scipy.optimize.milp or PuLP.
This model is the foundation for more advanced inventory policies like the $(s, Q)$ model, periodic review systems, and multi-echelon supply chains β all of which preserve the integer-quantity requirement at their core.