Guide¶
The Guide explains each part of Stockcast in depth: what it does, the ideas behind it, and every option it takes. New to Stockcast? The Walkthrough introduces the same parts in nine short steps.
- The core of a run: the parts every simulation uses, in the order a run uses them: inventory state, demand, forecast targets, policies and their schedules, the engine and its timing, the event table, metrics, and plots.
- Real-world operations: optional rules, each added with one argument: ordering constraints, callbacks, suppliers, unreliable deliveries, and shelf life.
- Recipes: short pages for specific tasks, with runnable code.
I want to…¶
Find the building block for your job, the page that explains it, and a notebook that uses it.
| I want to… | Use | Read | See it in |
|---|---|---|---|
| start from my current stock and open orders | InventoryStateDataFrame, with_open_orders |
Inventory state | 01, 05d |
| feed in sales history or simulated demand | demand table, DemandGenerator |
Demand and calendars | 02, 02c |
| forecast from sales that ran out of stock | demand vs fulfilled_units in the event table, get_history() in predict |
Sales and demand, When the forecast learns from the run | 04a |
| turn my forecast into a stock target | target table + fit |
Forecast targets, Connect any forecasting model | 04c, 04e |
| order up to a level at every review | OrderUpToPolicy |
Order-up-to | 04, 06 |
| order only when stock falls low | ReorderPointPolicy |
Reorder point | 05b, 05g, 08 |
| buy once for a season | SingleOrderPolicy, newsvendor_critical_fractile |
Single order | 04b |
| use my own ordering rule | subclass BasePolicy |
Write your own policy | 05 |
| order on specific days only | PeriodicSchedule, ExplicitSchedule, DecisionSchedule |
Decision schedules | 02b, 04f |
| simulate a policy over time | SimulationEngine.run |
The simulation engine | 02 |
| use a lead time from a paper or another tool | lead_time, PeriodicSchedule(start=...) |
Work with any timing convention | – |
| read what happened in a run | to_event_frame, validate_event_frame |
The event table | 02, 08 |
| measure service, stock, and cost | InventoryEvaluator, metrics |
Evaluation and metrics, Put costs on a run | 07, 09 |
| plot a run or a comparison | plot_simulation_dashboard, plot_comparison_dashboard |
Plots | 02, 06 |
| respect minimums, case sizes, or capacity | OrderingConstraints and friends |
Ordering constraints | 05c, 09 |
| add holidays, promotions, or stock corrections | SimulationCallback, scheduled callbacks |
Callbacks | 08, 05c |
| use several suppliers or random lead times | SupplyModel, Supplier, SupplierAllocation |
Suppliers and open orders | 05d |
| model late or short deliveries | DeliveryOutcome |
Unreliable deliveries | 05f |
| model products that expire, inspections, returns | ShelfLife, InventoryProcess |
Shelf life and processes | 05e, 07 |
| refresh targets as new forecasts arrive | policy_schedule |
Refresh targets as forecasts roll | 04a, 04d, 09 |
| compare forecasts or policies fairly | run_comparison |
Compare forecasts and policies | 06, 09 |
| compute real orders every period | advance_period → predict → constraints → fulfill_demand |
Use Stockcast in production, Walkthrough step 9 | 10 |
All together¶
Every optional part is one argument of run. This run adds two ordering
constraints, a holiday callback, a supplier whose deliveries sometimes take a
day longer, and shelf life to the tea shop. Remove any of them and the rest
still works.
Setup: the tea shop from the Walkthrough
import numpy as np
import pandas as pd
from stockcast.core import InventoryStateDataFrame, SimulationEngine
from stockcast.policies import OrderUpToPolicy
from stockcast.utils import DemandGenerator
sku = "tea_250g"
opening_date = pd.Timestamp("2026-01-05")
lead_time, review_period = 2, 4
horizon = lead_time + review_period
demand = DemandGenerator(
[sku], first_date=opening_date + pd.Timedelta(days=1), freq="D",
random_seed=3, negative_demand_handling="clip_zero",
).seasonal(n_periods=56, base=6.0, amplitude=2.0, season_length=7, std=2.0)
demand["y"] = demand["y"].round()
inventory = InventoryStateDataFrame.from_observed(
pd.DataFrame({"unique_id": [sku], "date": [opening_date], "on_hand": [30.0]}),
allow_backorders=False, # some pages step this state by hand
)
paths = np.random.default_rng(42).poisson(6.0, size=(10_000, horizon))
def tea_policy(probability):
"""Order-up-to policy whose target is the given quantile of 6-day demand."""
target = pd.DataFrame({
"unique_id": [sku],
"target": [np.quantile(paths.sum(axis=1), probability)],
"date": [opening_date + pd.Timedelta(days=horizon)], # the last day it covers
})
return OrderUpToPolicy(
lead_time=lead_time, review_period=review_period, freq="D",
service_level=probability, allow_backorders=False,
).fit(target, target_column="target")
policy = tea_policy(0.95)
run_settings = dict(random_seed=3)
result = SimulationEngine().run(
policy=policy, demand_source=demand, inventory=inventory, **run_settings,
)
from stockcast.core import (
InventoryStateDataFrame, MinimumOrderQuantity,
OrderMultiple, ScheduledOrderHold, ShelfLife, SimulationEngine, Supplier,
SupplyModel,
)
# Orders of at least 12 packs, in cases of 6.
constraints = [
MinimumOrderQuantity(12, mode="adjust"),
OrderMultiple(6, mode="adjust"),
]
# The supplier is closed on 18 January: skip that review.
holiday = ScheduledOrderHold(pd.DataFrame({
"unique_id": [sku],
"date": [pd.Timestamp("2026-01-18")],
"reason": ["supplier closed"],
"source": ["supplier calendar"],
}))
# Deliveries take 2 days, sometimes 3.
supply = SupplyModel(
[Supplier("tea_wholesaler", lead_time={2: 0.8, 3: 0.2})],
random_seed=11,
)
# Tea keeps for 21 days; the opening 30 packs arrived three days ago.
shelf_life = ShelfLife(
shelf_life_days=21,
opening_lots=pd.DataFrame({
"unique_id": [sku],
"received_date": [opening_date - pd.Timedelta(days=3)],
"quantity": [30.0],
}),
)
inventory_3 = InventoryStateDataFrame.from_observed(
pd.DataFrame({"unique_id": [sku], "on_hand": [30.0]}), opening_date=opening_date,
)
result = SimulationEngine().run(
policy=policy,
demand_source=demand,
inventory=inventory_3,
order_constraints=constraints,
callbacks=[holiday],
supply=supply,
processes=[shelf_life],
**run_settings,
)
events = result.to_event_frame()
events.loc[events["requested_order_quantity"] > 0,
["date", "requested_order_quantity", "callback_adjusted_order_quantity",
"order_quantity", "binding_constraints"]].head(6)
date requested_order_quantity callback_adjusted_order_quantity order_quantity binding_constraints
0 2026-01-06 16.0 16.0 18.0 order_multiple
4 2026-01-10 25.0 25.0 30.0 order_multiple
8 2026-01-14 9.0 9.0 12.0 minimum_order_quantity
12 2026-01-18 29.0 0.0 0.0
16 2026-01-22 46.0 46.0 48.0 order_multiple
20 2026-01-26 9.0 9.0 12.0 minimum_order_quantity
Read across a row and you see each component at work: the policy asked for 16
packs, the case-pack rule rounded it to 18; on 18 January the holiday callback
set the order to zero. The order frame (result.to_order_frame()) shows which
deliveries took three days, and the callback audit
(result.to_callback_audit_frame()) records the holiday with its reason.
Lead-time warning
This run prints a UserWarning: the policy's target was sized for a
2-day lead time, and some deliveries take 3. Stockcast simulates what
you declared and warns that the two differ; that mismatch is often what
you want to study. See
Suppliers: the lead-time assumption.
See also: Notebook 05c: extension points · Notebook 09: a full experiment · Philosophy: why Stockcast is built this way