Inventory state¶
InventoryStateDataFrame is the snapshot of an inventory system at one moment:
for every SKU, what is on the shelf, what is on its way, and what is owed. It
is the starting point of every simulation and the object policies look at when
they decide.
What the state holds¶
One row per SKU. The columns that matter most:
| Column | Meaning |
|---|---|
unique_id |
The SKU (the name is configurable with sku_column). |
on_hand |
Units on the shelf. |
in_transit |
A NumPy array of length max_lead_time. Slot \(k\) holds the units arriving \(k + 1\) periods from now. |
backorders |
Demand owed to customers. |
period, date |
The state's position in time. All SKUs share one period and one date. |
latest_* |
Flows of the most recent period (latest_received, latest_fulfilled, latest_shortage, …), reset at the start of each period. |
target_level |
Informational field written by policies. |
From these, inventory_position() adds total_in_transit and
Opening state¶
The common case: you counted the shelf; nothing is on order or owed.
import numpy as np
import pandas as pd
from stockcast.core import InventoryStateDataFrame
opening_date = pd.Timestamp("2026-01-05")
counts = pd.DataFrame({"unique_id": ["tea_250g", "coffee_1kg"],
"on_hand": [30.0, 12.0]})
state = InventoryStateDataFrame.from_observed(counts, opening_date=opening_date)
The SKUs are the rows of counts. The stock column defaults to
"on_hand" (pass on_hand_column= for another name) and the SKU column
to "unique_id" (pass sku_column=).
A stock table can also carry its own date, just like a
forecast table: with a
date column you can leave out opening_date. Every row holds the same
date, the day the shelf was counted; give both and Stockcast checks they
agree.
A new product, or a what-if that starts from nothing:
When you already know the pipeline and the backorders, pass a complete table:
full = InventoryStateDataFrame(pd.DataFrame({
"unique_id": ["tea_250g", "coffee_1kg"],
"on_hand": [30.0, 0.0],
"backorders": [0.0, 4.0],
"in_transit": [np.array([0.0, 12.0]), np.array([10.0, 0.0])],
"period": [0, 0],
"date": [opening_date, opening_date],
}), max_lead_time=2, allow_backorders=True)
full.inventory_position()[["unique_id", "on_hand", "in_transit",
"backorders", "inventory_position"]]
You always supply the opening state. The state rejects missing or contradictory values (a SKU with both stock and backorders, backorders in a lost-sales state, negative stock, a pipeline of the wrong length) as soon as it is used.
Constructor arguments¶
| Argument | Meaning |
|---|---|
data |
A list of SKUs, or a DataFrame with a SKU column |
max_lead_time |
Length of the pipeline. Leave it out and it is sized when needed: a run makes room for the policy's lead time and the suppliers' longest delivery, and placing or declaring orders makes room for them. The new slots are empty. Give a number to fix it, for example to leave room for delayed deliveries |
sku_column |
Name of the SKU column, default "unique_id" |
allow_backorders |
True (backorders) or False (lost sales). Must match the policy's setting; leave it unset to take the policy's. Reading positions works without it; set it when you serve demand or place orders yourself (below) |
opening_date |
Opening date, if not supplied later by an initializer |
Open orders¶
The in_transit array says how much arrives when. Often you also know
which orders make it up: supplier, order number, partial deliveries. Declare
them with with_open_orders, one row per scheduled delivery:
state = InventoryStateDataFrame(
["tea_250g"], max_lead_time=3, allow_backorders=False,
).initialize_zero(opening_date=opening_date).with_open_orders(pd.DataFrame({
"unique_id": ["tea_250g", "tea_250g", "tea_250g"],
"supplier_id": ["roaster", "importer", "importer"],
"order_id": ["PO-17", "PO-12", "PO-12"], # PO-12 arrives in two parts
"order_period": [-1, -2, -2],
"due_period": [1, 2, 3],
"quantity": [20.0, 30.0, 10.0],
}))
state.open_orders()
order_id unique_id supplier_id source order_period ordered_quantity remaining_quantity due_period final_due_period
0 0 tea_250g roaster opening -1.0 20.0 20.0 1 1
1 1 tea_250g importer opening -2.0 40.0 40.0 2 3
due_period counts state periods: the opening state is period 0, so
due_period=1 arrives before the first demand. scheduled_receipts() lists
the individual deliveries, and the pipeline is filled in from them:
Required columns are the SKU, due_period, and quantity; supplier_id,
order_period, order_id, and ordered_quantity are optional. If the state
already has a pipeline, the orders must add up to it exactly: they then
explain the existing quantities rather than adding new ones.
Suppliers and open orders shows how open orders flow through a
simulation.
Manual stepping¶
The engine moves the state forward for you. For a caller-owned loop, the same steps are public:
| Method | Does |
|---|---|
advance_period(freq=..., is_review_period=...) |
Move to the next period, receive due deliveries, serve old backorders |
stockcast.utils.update_inventory_with_orders(state, decision) |
Place an OrderDecision (zero lead time: received now) |
stockcast.utils.place_order_lines(state, lines) |
Place order lines with explicit due periods |
fulfill_demand(demand_df) |
Serve the current period's demand |
See Run your own simulation loop.
Reading the state¶
| Method | Returns |
|---|---|
get_dataframe() |
A copy of the current state table |
inventory_position() |
The state plus total_in_transit and inventory_position |
open_orders() |
One row per open order line |
scheduled_receipts() |
One row per open delivery |
get_history() |
Snapshots of past periods recorded on this object |
See also: Walkthrough step 1 · API: state and orders · Notebook 01