The marketing model answers how to split a weekly budget across channels.
An operator then asks the next question: how much should we spend over the next four weeks, given the orders already waiting in the warehouse?
This extension answers it by combining the calibrated Kettle & Pine posterior with a simulation of the running warehouse.
The code is in examples/kettle_and_pine/warehouse.clj (the warehouse model) and study/warehouse_study.clj (the decision); the scenario is examples/kettle_and_pine/data/warehouse.json.
decision/plan-responses gives the weekly conditional-mean revenue over the four campaign weeks and five carryover weeks.decision/allocate, with channel bounds scaled in proportion to the budget.flowchart TB
C["C ~ Normal(650, 120)<br/>reduced capacity"]
subgraph week ["for each of the 3 disrupted weeks w"]
cw["c_w ~ Normal(C, 30)<br/>that week's capacity"]
arr[/"arrivals a_w (observed)"/]
ship["ship-week!: shipped_w, backlog_w"]
rep[/"reported shipments r_w (observed)"/]
end
b0[/"backlog when the disruption began: 300"/]
C --> cw
cw --> ship
arr --> ship
b0 --> ship
ship -- "backlog carried to the next week" --> ship
ship --> rep
The current reduced capacity $C$ is unknown.
Each disrupted week's actual capacity varies around it, and the application's own ship-week! function moves the warehouse forward one week:
$$c_w \sim \text{Normal}(C,\ 30), \qquad \text{shipped}w = \min(\text{backlog}{w-1} + a_w,\ c_w)$$
$$\text{backlog}w = (1 - 0.15),(\text{backlog}{w-1} + a_w - \text{shipped}_w), \qquad r_w \sim \text{Normal}(\text{shipped}_w,\ 5)$$
The minimum is where the information comes from. In a week with spare capacity, shipments equal arrivals and say nothing about $C$; in a backlogged week they measure it. The same reports would mean something different if the backlog were ignored, which is why the inference starts from the warehouse's actual state.
The model is written as ordinary Clojure: a Spindel spin that calls sample, observe and ship-week!, which updates a Spindel atom holding the warehouse.
Foerster runs SMC over it with :world-policy :fork, so each of the 4,000 particles executes ship-week! in its own fork of the live warehouse, and the caller's atom is never changed.
SMC needs no gradients, so the same approach works for queues, discrete decisions and business rules that NUTS cannot handle.
This problem is small enough to check by grid quadrature, which study/warehouse_check.py does; the point of running it through Foerster is that the business logic does not have to be rewritten as a model graph.
For each weekly budget $b \in \lbrace 0, 1{,}250, \dots, 7{,}500 \rbrace$:
decision/allocate chooses the channel mix $p_b$ that maximises expected revenue at that budget, with channel bounds scaled in proportion to $b$ (the allocation of the marketing model).decision/plan-responses gives the weekly expected revenue $\mu_t(p_b; \theta)$ over the four campaign weeks and five carryover weeks. Orders arrive at $\mu_t / 45$ a week.ship-week! runs the nine weeks forward in a fork of the current state, and the run is scored:$$\text{profit}(b) = 0.40 \cdot 45 \sum_t \text{shipped}_t ;-; 4 \sum_t \text{backlog}_t ;-; 4,b$$
The study reports the mean and 90% interval of profit for each budget and of the paired difference against keeping EUR 5,000 a week. It repeats the calculation from an empty warehouse at normal capacity to show how the recommendation changes.
flowchart LR
subgraph both ["Foerster Marketing and PyMC-Marketing"]
mmm["media mix model<br/>NUTS → posterior over θ"]
alloc["allocate: best mix<br/>for a given budget"]
end
subgraph foerster ["Foerster only"]
cap["warehouse model<br/>SMC → posterior over C"]
sim["forward simulation<br/>demand meets capacity<br/>from today's backlog"]
best["profit by budget<br/>→ budget level"]
end
mmm --> alloc --> sim
mmm -- "θ draws" --> sim
cap -- "C particles" --> sim
sim --> best
Two simplifications keep this tractable, and both are worth stating when the result is used:
study/warehouse_check.py) within three Monte Carlo standard errors, for three seeds.All checks pass. The capacity posterior means for seeds 17, 31 and 73 (551.1, 555.0 and 550.5 orders a week) lie 0.6, 1.8 and 0.7 Monte Carlo standard errors from the quadrature reference of 552.1 (SD 17.4); the hidden truth is 560. The NumPy reimplementation reproduces every plan's mean profit exactly.
Mean campaign profit relative to keeping EUR 5,000 a week, in EUR:
| Budget (EUR/week) | Empty warehouse | With backlog |
|---|---|---|
| 0 | −17,157 | −3,392 |
| 1,250 | −5,490 | +3,539 |
| 2,500 | −1,146 | +4,318 (90%: 3,049 to 5,403) |
| 3,750 | +242 (90%: −349 to +815) | +2,687 |
| 5,000 | 0 | 0 |
| 6,250 | −1,263 | −3,169 |
| 7,500 | −3,220 | −6,772 |
With an empty warehouse, EUR 3,750 a week is best and practically level with EUR 5,000.
With the backlog, EUR 2,500 is best.
Under the hidden truth, EUR 2,500 is also best with the backlog (EUR 4,393 above EUR 5,000), and EUR 3,750 without it.
The full table is in report.json.
The random draws of the SMC inference are seeded from the seeds above and from the source location of each draw in warehouse.clj, so editing that file above the draws changes the exact numbers, though not the conclusions.
The warehouse, its costs and the disruption are synthetic. Capacity and demand uncertainty are treated as independent. The decision covers the budget level only, not its timing; delaying the campaign would be a natural next option. The marketing model is fitted in batch with NUTS; updating it with SCMC as weekly data arrive is not part of this study.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |