Skip to content

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

\[ \mathit{IP} = \text{on\_hand} + \sum_k \text{in\_transit}[k] - \text{backorders}. \]

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.

counts["date"] = opening_date
state = InventoryStateDataFrame.from_observed(counts)

A new product, or a what-if that starts from nothing:

empty = InventoryStateDataFrame(["tea_250g"]).initialize_zero(opening_date=opening_date)

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"]]
    unique_id  on_hand   in_transit  backorders  inventory_position
0    tea_250g     30.0  [0.0, 12.0]         0.0                42.0
1  coffee_1kg      0.0  [10.0, 0.0]         4.0                 6.0

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:

state.get_dataframe()["in_transit"].iloc[0]
array([20., 30., 10.])

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