Policies¶
Ordering rules and their target providers. Guide: Policies.
BasePolicy
¶
BasePolicy(
lead_time: int,
review_period: Optional[int] = None,
service_level: Optional[float] = None,
allow_backorders: bool = None,
*,
schedule: Optional[DecisionSchedule] = None,
)
Base class for every ordering policy, in the spirit of PyTorch's nn.Module.
A policy is configured with the operation (lead time, decision schedule,
shortage rule), fitted on forecast information with fit, and asked for
orders with predict. It reads a copy of the state and returns an
OrderDecision; it never changes stock. Subclasses implement fit (set
self.fitted_ = True and return self) and predict.
Example
class DaysOfCover(BasePolicy):
def __init__(self, days, **kwargs):
super().__init__(**kwargs)
self.days = days
def fit(self, forecast_df, **kwargs):
self.daily_mean_ = forecast_df.set_index("unique_id")["daily_mean"]
self.fitted_ = True
return self
def predict(self, inventory_state_df, *, current_period, **kwargs):
state = inventory_state_df.inventory_position()
level = state["unique_id"].map(self.daily_mean_) * self.days
orders = pd.DataFrame({
"unique_id": state["unique_id"],
"order_quantity": (level - state["inventory_position"]).clip(lower=0),
"order_period": current_period,
"expected_delivery_period": current_period + self.lead_time,
})
return OrderDecision(orders, lead_time=self.lead_time)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lead_time
|
int
|
Periods from order to delivery, an integer >= 0. An order
placed in period |
required |
review_period
|
Optional[int]
|
Periods between ordering opportunities, an integer >= 1.
Shorthand for |
None
|
service_level
|
Optional[float]
|
Probability that the policy's targets represent, in
(0, 1), or |
None
|
allow_backorders
|
bool
|
|
None
|
schedule
|
Optional[DecisionSchedule]
|
A |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If a value is out of range or the timing arguments disagree. |
fit
¶
fit(forecast_df: DataFrame, **kwargs: object) -> BasePolicy
Bind forecast information to the policy. Subclasses must implement it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forecast_df
|
DataFrame
|
Targets or forecasts, one or more rows per SKU. |
required |
**kwargs
|
object
|
Policy-specific options. |
{}
|
Returns:
| Type | Description |
|---|---|
BasePolicy
|
The fitted policy ( |
predict
¶
predict(
inventory_state_df: Union[
DataFrame, InventoryStateDataFrame
],
**kwargs: object,
) -> Union[OrderDecision, DataFrame]
Propose orders for the given state. Subclasses must implement it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inventory_state_df
|
Union[DataFrame, InventoryStateDataFrame]
|
A copy of the state before demand. |
required |
**kwargs
|
object
|
The engine passes |
{}
|
Returns:
| Type | Description |
|---|---|
Union[OrderDecision, DataFrame]
|
An |
OrderUpToPolicy
¶
OrderUpToPolicy(
lead_time: int,
review_period: Optional[int] = None,
*,
freq: Optional[str] = None,
service_level: Optional[float] = None,
allow_backorders: bool,
schedule: Optional[DecisionSchedule] = None,
sku_column: str = "unique_id",
date_column: str = "date",
)
Bases: BasePolicy
Order-up-to (R, S) policy: restore the inventory position to S.
At every decision period the policy orders max(0, S - IP), where IP
is on hand + on order - backorders. S is fitted per SKU as a target for
the protection window H = L + R: typically the service_level
quantile of total demand over the window, computed outside Stockcast, or
from per-period means and standard deviations under an independent-normal
assumption.
Example
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lead_time
|
int
|
Lead time in periods (L) |
required |
review_period
|
Optional[int]
|
Review period in periods (R) |
None
|
freq
|
Optional[str]
|
Length of one period, a pandas frequency such as |
None
|
service_level
|
Optional[float]
|
Target probability, or None for external planner targets |
None
|
allow_backorders
|
bool
|
Explicitly choose backorders or lost sales |
required |
schedule
|
Optional[DecisionSchedule]
|
Optional DecisionSchedule; replaces periodic shorthand. |
None
|
sku_column
|
str
|
SKU column of the tables given to |
'unique_id'
|
date_column
|
str
|
Date column of the table given to |
'date'
|
fit
¶
fit(
forecast_df: DataFrame,
*,
forecast_origin: Optional[Timestamp] = None,
target_column: Optional[str] = None,
target_probability: Optional[float] = None,
protection_horizon: Optional[int] = None,
mean_column: Optional[str] = None,
std_column: Optional[str] = None,
) -> OrderUpToPolicy
Calculate target inventory levels from explicit scientific inputs.
Exactly one input mode must be selected:
- Supply one externally calculated protection-period target per SKU
with
target_column. - Supply consecutive marginal means and standard deviations with
mean_columnandstd_column. They are combined under an independence assumption across periods:S = sum(means) + z * sqrt(sum(stds**2)).
Marginal quantiles are not accepted because their sum is not generally the quantile of cumulative protection-period demand.
The target window runs from forecast_origin for
protection_horizon periods, so its end date is
forecast_origin + protection_horizon periods. Give the origin, a
date column, or both (they must then agree): the origin follows from
each row's date exactly. The table's column names are the policy's
sku_column and date_column.
The period length is the policy's freq. Without it, mean/std mode
reads it from the forecast dates when every SKU has at least three
evenly spaced dates on a standard calendar (daily, weekly, monthly,
quarterly or yearly, with any step such as "2D"; day steps need a
Saturday or Sunday among the dates when weekdays alone would fit a
business-day step too); a direct target has one date per SKU, so it
needs freq.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forecast_df
|
DataFrame
|
Target or forecast DataFrame. |
required |
forecast_origin
|
Optional[Timestamp]
|
Information-set date at which the forecast or direct target was created. Optional when the date column dates the rows. |
None
|
target_column
|
Optional[str]
|
Direct protection-period target column. The frame must contain exactly one row per SKU in this mode. |
None
|
target_probability
|
Optional[float]
|
Probability represented by the target. Defaults
to the policy's |
None
|
protection_horizon
|
Optional[int]
|
Number of periods represented by the target.
Defaults to |
None
|
mean_column
|
Optional[str]
|
Marginal forecast mean column for mean/std mode. |
None
|
std_column
|
Optional[str]
|
Marginal forecast standard deviation column for
mean/std mode. It is required; Stockcast never invents it.
Each mean/std row has a step |
None
|
Returns:
| Type | Description |
|---|---|
OrderUpToPolicy
|
self (for method chaining) |
validate_decision_window
¶
Check, before a run, that the target covers the decision's window.
Called by the engine for every scheduled decision. For irregular schedules the
window is (next decision - period) + lead_time; the forecast origin and
end date must match the decision's information date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
period
|
Zero-based demand period of the decision. |
required | |
information_date
|
Date of the last demand known at the decision. |
required | |
offset
|
The simulation's period frequency. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the target's window or dates do not match. |
predict
¶
predict(
inventory_state_df: Union[
DataFrame, InventoryStateDataFrame
],
sku_column: Optional[str] = None,
*,
current_period: int,
) -> OrderDecision
Calculate order quantities for each SKU.
Order quantity = max(0, S - IP) where S = target level (from fit) IP = inventory position (from inventory_state_df)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inventory_state_df
|
Union[DataFrame, InventoryStateDataFrame]
|
InventoryStateDataFrame object or DataFrame with inventory data Must have 'inventory_position' or ['on_hand', 'on_order', 'backorders'] |
required |
sku_column
|
Optional[str]
|
SKU column of the state; defaults to the policy's |
None
|
current_period
|
int
|
Explicit decision period used for order and delivery timing. |
required |
Returns:
| Type | Description |
|---|---|
OrderDecision
|
OrderDecision object ( |
get_target_levels
¶
Return the fitted order-up-to level S per SKU.
Returns:
| Type | Description |
|---|---|
DataFrame
|
A DataFrame with the SKU column and |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the policy is not fitted. |
get_target_metadata
¶
Return how the target was made.
Returns:
| Type | Description |
|---|---|
dict
|
A dict with |
dict
|
|
dict
|
|
dict
|
targets, |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the policy is not fitted. |
ReorderPointPolicy
¶
ReorderPointPolicy(
lead_time: int,
review_period: Optional[int] = None,
*,
freq: Optional[str] = None,
policy_type: Optional[Literal["sQ", "sS"]] = None,
service_level: Optional[float] = None,
order_quantity: Optional[float] = None,
allow_backorders: bool,
schedule: Optional[DecisionSchedule] = None,
sku_column: str = "unique_id",
date_column: str = "date",
)
Bases: BasePolicy
Order when inventory position is at or below a reorder point s.
(s,Q): order a fixed, explicit quantityQ.(s,S): order up toS.
policy_type follows from the inputs: giving order_quantity makes an
(s,Q) policy, otherwise (s,S). Passing it states the choice and is
checked.
Review timing comes from review_period or an explicit schedule; use
review_period=1 for every-period review.
fit takes s (and S) from one of three sources:
- a forecast table (
reorder_point_column):sis dated and covers the schedule's protection window, checked at every decision (see the module docstring). Withservice_levelset,sis the demand quantile at that probability; withservice_level=Noneit is a planner value. - fixed values (
reorder_point=): one number for every SKU, or one per SKU, for example jointly optimized(s,S)pairs. - your own rule (
target_provider=), aReorderPointTargetProvider.
S is never interpreted as a quantile: in the literature s and S
are generally determined jointly. Stockcast only requires S >= s.
freq is the length of one period (a pandas frequency such as "D");
lead_time and review_period count periods of this length. Forecast
targets need it; fixed values and target providers may omit it.
sku_column (default "unique_id") names the SKU column of the tables
given to fit and predict; date_column (default "date") the
date column of a forecast table: the last period each row's s covers.
The default date column is read only if it exists; a column named
explicitly must exist.
fit
¶
fit(
target_df: Optional[DataFrame] = None,
*,
reorder_point_column: Optional[str] = None,
forecast_origin: Optional[Timestamp] = None,
reorder_horizon: Optional[int] = None,
target_probability: Optional[float] = None,
order_up_to_column: Optional[str] = None,
reorder_point=None,
order_up_to_level=None,
target_provider: Optional[
ReorderPointTargetProvider
] = None,
) -> ReorderPointPolicy
Set the reorder point s (and S for (s,S)) per SKU.
Choose exactly one source:
reorder_point_column(andorder_up_to_column): targets intarget_df, typically from a forecast. They are dated: giveforecast_origin, the policy's date column, or both; the window ofsendsreorder_horizonperiods after the origin and is checked at every decision.reorder_point(andorder_up_to_level): fixed planning values, one number for every SKU, a{sku: value}dict, or a Series indexed by SKU.target_provider: your ownReorderPointTargetProvider, called withtarget_df.
Sources 2 and 3 are planning levels: they need service_level=None
and claim no window. Their dates are optional: forecast_origin
needs the policy's freq, and the run then checks it as for source
1; freq alone records the period length without an origin.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target_df
|
Optional[DataFrame]
|
One row per SKU (sources 1 and 3; optional for 2). |
None
|
reorder_point_column
|
Optional[str]
|
Column holding |
None
|
forecast_origin
|
Optional[Timestamp]
|
Last observed demand date used to build |
None
|
reorder_horizon
|
Optional[int]
|
Protection window of |
None
|
target_probability
|
Optional[float]
|
Quantile mode only. Defaults to
|
None
|
order_up_to_column
|
Optional[str]
|
Column holding |
None
|
reorder_point
|
Fixed |
None
|
|
order_up_to_level
|
Fixed |
None
|
|
target_provider
|
Optional[ReorderPointTargetProvider]
|
A |
None
|
Returns:
| Type | Description |
|---|---|
ReorderPointPolicy
|
The fitted policy ( |
validate_decision_window
¶
Check, before a run, that s covers the decision's window.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
period
|
Zero-based demand period of the decision. |
required | |
information_date
|
Date of the last demand known at the decision. |
required | |
offset
|
The simulation's period frequency. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the reorder horizon or dates do not match. |
predict
¶
predict(
inventory_state_df: Union[
DataFrame, InventoryStateDataFrame
],
sku_column: Optional[str] = None,
*,
current_period: int,
) -> OrderDecision
Order when inventory_position <= s.
(s,Q) orders Q; (s,S) orders max(0, S - inventory_position).
get_reorder_points
¶
Return the fitted reorder point s per SKU.
Returns:
| Type | Description |
|---|---|
DataFrame
|
A DataFrame with the SKU column and |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the policy is not fitted. |
get_parameters
¶
Return s with Q (for (s,Q)) or S (for (s,S)) per SKU.
get_target_metadata
¶
Return a copy of target, window, and quantity provenance.
SingleOrderPolicy
¶
SingleOrderPolicy(
lead_time: int,
*,
freq=None,
selling_horizon: int,
decision_period: int = 0,
service_level=None,
allow_backorders: bool,
sku_column: str = "unique_id",
date_column: str = "date",
)
Bases: OrderUpToPolicy
Buy once for a selling season (newsvendor).
The policy decides once, at decision_period. The order arrives after
lead_time periods, which is when the season starts; the season lasts
selling_horizon periods. The target is a quantile of total season demand
(for example at newsvendor_critical_fractile), and the order is the
target minus the stock already available.
Demand outside the season must be zero, the run must cover the whole season, and any opening pipeline must arrive by the season's start.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lead_time
|
int
|
Periods from the order to the start of the season, >= 0. |
required |
freq
|
Length of one period, a pandas frequency such as |
None
|
|
selling_horizon
|
int
|
Length of the season in periods, >= 1. |
required |
decision_period
|
int
|
Demand period of the single decision (default 0). |
0
|
service_level
|
Probability the target represents, or |
None
|
|
allow_backorders
|
bool
|
|
required |
sku_column
|
str
|
SKU column of the tables given to |
'unique_id'
|
date_column
|
str
|
Date column of the target table: the season's last
period (default |
'date'
|
fit
¶
Bind the season target.
Give forecast_origin, the policy's date column, or both: the
season ends lead_time + selling_horizon periods after the origin,
on the date column's date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target_df
|
One row per SKU. |
required | |
target_column
|
Column with the season target. |
required | |
forecast_origin
|
Date of the last information used, normally the date
before the decision. Optional when the date column dates the
rows (the season's last period, |
None
|
|
target_probability
|
Defaults to |
None
|
Returns:
| Type | Description |
|---|---|
|
The fitted policy ( |
validate_decision_window
¶
Check, before a run, that the target covers the selling season.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
period
|
Zero-based demand period of the decision. |
required | |
information_date
|
Date of the last demand known at the decision. |
required | |
offset
|
The simulation's period frequency. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the forecast origin is not the decision's information date, or the target does not end at the season's last date. |
validate_demand_window
¶
Check the demand table: zero outside the season, and the whole season simulated.
predict
¶
Order the season target minus available stock.
Raises:
| Type | Description |
|---|---|
ValueError
|
If opening pipeline would arrive after the season starts. |
newsvendor_critical_fractile
¶
The newsvendor critical fractile (p - c) / (p - v).
For one purchase with selling price p, purchase cost c and salvage
value v, the profit-maximising quantity is the demand quantile at this
probability. It assumes linear costs, lost sales, and no second purchase.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
selling_price
|
|
required | |
purchase_cost
|
|
required | |
salvage_value
|
|
required |
Returns:
| Type | Description |
|---|---|
|
The probability, between 0 and 1. |
Raises:
| Type | Description |
|---|---|
ValueError
|
Unless |
rolling_schedule
¶
rolling_schedule(
policy: BasePolicy,
targets: DataFrame,
inventory: InventoryStateDataFrame,
*,
origin_column: str = "cutoff",
missing_targets: str = "raise",
**fit_kwargs,
) -> Dict[int, BasePolicy]
Fit one policy per forecast origin of a table of targets.
policy gives the configuration: a copy of it is fitted at every origin
with that origin's rows and forecast_origin=origin, so every check of
fit applies to every snapshot. Origin o belongs to decision period
t when o == opening date + t periods of the policy's freq;
t must be a decision period of the policy's schedule.
Example
schedule = rolling_schedule(
OrderUpToPolicy(lead_time=2, review_period=1, freq="W-MON",
service_level=0.9, allow_backorders=False),
targets, # unique_id, cutoff, date, q_90
inventory,
target_column="q_90",
)
first = min(schedule)
result = SimulationEngine().run(
policy=schedule[first], policy_schedule=schedule,
demand_source=demand, inventory=inventory, allow_target_reuse=False,
)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
policy
|
BasePolicy
|
An |
required |
targets
|
DataFrame
|
One row per SKU and origin (per SKU, origin and step |
required |
inventory
|
InventoryStateDataFrame
|
The opening |
required |
origin_column
|
str
|
Column holding each row's forecast origin, the last
date its forecast saw (default |
'cutoff'
|
missing_targets
|
str
|
What a row whose target columns are NaN means: the
forecast could not produce that target. |
'raise'
|
**fit_kwargs
|
Arguments of the policy's |
{}
|
Returns:
| Type | Description |
|---|---|
Dict[int, BasePolicy]
|
|
Dict[int, BasePolicy]
|
one as |
Raises:
| Type | Description |
|---|---|
ValueError
|
If an origin is off the calendar, before the opening date or
not a decision period, a decision between the first and the last
origin has no origin, an inventory SKU has no row at an origin, a
target is missing under |
TypeError
|
If the policy or an input has the wrong type. |
ReorderPointTargetProvider
¶
Bases: ABC
Base class for your own rule that sets s (and S) per SKU.
Pass an instance to ReorderPointPolicy.fit(data, target_provider=...).
Implement provide. Set the class attribute target_source to
"external_direct" when the provider only passes along values computed
elsewhere (default "custom_provider"), and extend to_manifest with
your settings.
provide
abstractmethod
¶
provide(
target_data: DataFrame, *, sku_column: str
) -> ReorderPointTargets
Return one reorder point (and order-up-to level) per SKU.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target_data
|
DataFrame
|
The table passed to |
required |
sku_column
|
str
|
SKU column name. |
required |
Returns:
| Type | Description |
|---|---|
ReorderPointTargets
|
A |
ReorderPointTargets
|
finite, |
ReorderPointTargets
dataclass
¶
The result of a target provider.
Attributes:
| Name | Type | Description |
|---|---|---|
frame |
DataFrame
|
One row per SKU with the SKU column and |
metadata |
dict
|
JSON-serialisable description, stored in the run manifest. |