Parameter System¶
The ParameterResolver (files/utils/parameter_resolver.py) implements a three-level hierarchy that lets you configure forecasting behaviour globally, per-segment, and per-SKU — all without code changes.
Figure: parameter scope taxonomy — global default, segment assignment, per-SKU deep-merged override, across the business types.
mindmap
root((Parameter scope))
global
default-set is_default
applied-to-all-unassigned
segment
series_parameter_assignment
lowest-sort_order-wins
per-SKU
series_hyperparameters_overrides
deep-merge-partial
business-types
forecasting
outlier_detection
characterization
evaluation
best_method
valuation
order_frequency
The Three-Level Hierarchy¶
Figure: parameter resolution flow — default set, segment assignment (lowest sort_order wins), then per-SKU overrides deep-merged on top. The only legitimate pipeline_id=0 is global rows in interact_series_param_overrides (parameter_resolver).
flowchart TD
A["resolve(item_id, site_id, business_type)"] --> B{"series_parameter_assignment<br/>has non-NULL param_id?"}
B -->|"yes"| C["Segment-assigned set<br/>(lowest sort_order wins)"]
B -->|"no, NULL"| D["Default set (is_default = TRUE)"]
C --> E{"series_hyperparameters_overrides<br/>for this method?"}
D --> E
E -->|"yes"| F["deep_merge(base, overrides)<br/>override keys win recursively"]
E -->|"no"| G["base unchanged"]
F --> H["Merged config"]
G --> H
N["interact_series_param_overrides<br/>pipeline_id = 0 = global rows<br/>(only legitimate use of 0)"]
N -.-> D
Level 3 (highest): series_hyperparameters_overrides — per-SKU, partial merge
Level 2: series_parameter_assignment — segment-based assignment
Level 1 (lowest): parameters (is_default=TRUE) — global fallback
Resolution always starts at the bottom and each higher level wins over lower ones for the keys it defines. A level that defines no keys for a given parameter simply passes through the level below.
Level 1 — Default Parameter Sets¶
Every parameter_type must have exactly one row with is_default = TRUE. This row's parameters_set JSONB column is the global configuration applied to all series that have no segment assignment and no per-SKU override.
Example: the default forecasting parameter set might look like:
{
"horizon": 78,
"min_obs": 12,
"frequency": "W",
"models": {
"AutoARIMA": { "enabled": true, "season_length": 52 },
"AutoETS": { "enabled": true, "season_length": 52 },
"MSTL": { "enabled": true, "season_length": [4, 52] },
"CrostonOptimized": { "enabled": true }
},
"dask": {
"n_workers": 4,
"chunk_size": 50
}
}
Level 2 — Segment-Based Parameter Assignment¶
The series_parameter_assignment table maps every (item_id, site_id) to a specific parameter set ID for each business type:
CREATE TABLE scenario.series_parameter_assignment (
item_id BIGINT,
site_id BIGINT,
PRIMARY KEY (item_id, site_id),
forecasting_parameter_id INTEGER REFERENCES parameters(id),
outlier_detection_parameter_id INTEGER REFERENCES parameters(id),
characterization_parameter_id INTEGER REFERENCES parameters(id),
backtesting_parameter_id INTEGER REFERENCES parameters(id),
valuation_parameter_id INTEGER REFERENCES parameters(id)
);
A NULL in any column means "use the default". When a non-NULL ID is present, the resolver substitutes the corresponding parameters_set for that business type, completely replacing the default for that series.
Assignments are populated by the Segmentation Engine (POST /api/parameters/resolve) which reads the parameter_segment join table:
CREATE TABLE scenario.parameter_segment (
parameter_id INTEGER REFERENCES parameters(id),
segment_id INTEGER REFERENCES segments(id),
PRIMARY KEY (parameter_id, segment_id)
);
Every series that belongs to a segment linked to a parameter set receives that assignment. When a series belongs to multiple segments with conflicting assignments, the parameter set with the lowest sort_order wins (the "most specific" assignment).
Level 3 — Per-SKU Hyperparameter Overrides¶
Figure: override precedence — higher specificity (per-SKU) carries higher precedence; lower-right quadrant wins.
quadrantChart
title Parameter override precedence
x-axis "Low specificity" --> "High specificity"
y-axis "Low precedence" --> "High precedence"
quadrant-1 "Per-SKU wins"
quadrant-2 "Global default"
quadrant-3 "No assignment"
quadrant-4 "Segment scoped"
"Default set (is_default)": [0.1, 0.15]
"Segment assignment": [0.5, 0.55]
"Per-SKU override": [0.9, 0.92]
The series_hyperparameters_overrides table holds partial overrides for individual series:
CREATE TABLE scenario.series_hyperparameters_overrides (
item_id BIGINT NOT NULL,
site_id BIGINT NOT NULL,
method TEXT NOT NULL, -- business type or model name
overrides JSONB NOT NULL,
PRIMARY KEY (item_id, site_id, method)
);
Overrides are deep-merged onto the resolved level-2 config using ParameterResolver.deep_merge(). This means you only need to specify the keys you want to change — all other keys from the segment or default set are preserved.
Example: override only the horizon for a specific series:
This leaves all other forecasting parameters (models, dask settings, etc.) untouched.
The ParameterResolver Class¶
Initialisation¶
from utils.parameter_resolver import ParameterResolver
resolver = ParameterResolver(config_path="/path/to/config.yaml")
On construction, _bulk_load() opens a single DB connection and reads all three tables into memory:
_assignment_map:{(item_id, site_id): {btype: param_id}}_param_by_id:{param_id: parameters_set_dict}_defaults:{btype: parameters_set_dict}(rows whereis_default=TRUE)_overrides_by_uid:{(item_id, site_id): {method: overrides_dict}}
resolve(item_id, site_id, business_type) → dict¶
Returns the fully merged configuration for a single series and business type:
Resolution logic:
def resolve(self, item_id, site_id, business_type):
# 1. Start from segment assignment or global default
assignment = self._assignment_map.get((item_id, site_id), {})
param_id = assignment.get(business_type)
if param_id is not None and param_id in self._param_by_id:
base = dict(self._param_by_id[param_id]) # segment-assigned set
else:
base = dict(self._defaults.get(business_type, {})) # global default
# 2. Deep-merge per-SKU override on top
uid_overrides = self._overrides_by_uid.get((item_id, site_id), {})
sku_override = uid_overrides.get(business_type, {})
if sku_override:
base = self.deep_merge(base, sku_override)
return base
group_series_by_param_set(item_ids, site_ids, business_type) → dict¶
For batch pipeline execution, group series so all series with the same parameter set run together (allows re-using the same instantiated forecaster object):
groups = resolver.group_series_by_param_set(all_uids, "forecasting")
# Returns {param_id: [uid, ...]} where param_id=None means "use default"
for param_id, uids in groups.items():
override = resolver.build_group_config_override(param_id, "forecasting")
forecaster = StatisticalForecaster(config_path, config_override=override)
forecaster.forecast_multiple_series(df[df.unique_id.isin(uids)])
build_group_config_override(param_id, business_type) → dict | None¶
Converts a parameter set ID into the config override dict expected by pipeline components:
override = resolver.build_group_config_override(42, "forecasting")
# Returns {"forecasting": {...parameters_set...}}
# or None if param_id is None (use defaults)
build_config_override(item_id, site_id, business_type) → dict | None¶
Per-series variant — includes both segment-level and per-SKU overrides:
override = resolver.build_config_override("12_301", "forecasting")
# Returns {"forecasting": {<merged config>}}
deep_merge(base, override) → dict¶
Recursive merge utility. Override keys win; nested dicts are merged recursively rather than replaced wholesale:
base = {"models": {"AutoARIMA": {"enabled": True}, "MSTL": {"enabled": True}}, "horizon": 78}
override = {"models": {"AutoARIMA": {"enabled": False}}, "horizon": 12}
result = ParameterResolver.deep_merge(base, override)
# {"models": {"AutoARIMA": {"enabled": False}, "MSTL": {"enabled": True}}, "horizon": 12}
Business Type → Config Section Mapping¶
The resolver maps each business type to the corresponding top-level key in the config dict:
| Business Type | Config Section | DB Column |
|---|---|---|
forecasting |
forecasting |
forecast_parameter_set_id |
outlier_detection |
outlier_detection |
outlier_parameter_id |
characterization |
characterization |
characterization_parameter_id |
backtesting |
backtesting |
backtesting_parameter_id |
valuation |
valuation |
valuation_parameter_id |
meio_target |
meio |
meio_target_parameter_id |
order_frequency |
order_frequency |
order_frequency_parameter_id |
supply |
supply |
supply_parameter_id |
Note:
evaluationandbest_methodparameter types were removed/renamed (best_method→backtesting).nettingandmethod_selectionare not segment-parameter-set types — netting config lives inconfig_forecast_parameter_set.params['netting'](pipeline-level, per-segment netting is intentionally unsupported) and method_selection lives in the scenario JSON (settings_param_sets) + thebacktestingparameter set.
Parameter Categories and Keys¶
Forecasting Parameters¶
Stored under parameter_type = 'forecasting'.
| Key | Type | Default | Description |
|---|---|---|---|
horizon |
int | 24 | Number of periods to forecast ahead |
min_obs |
int | 12 | Minimum observations required to run any model |
frequency |
str | W |
Time series frequency: D, W, M, Q, Y |
models.AutoARIMA.enabled |
bool | true | Enable AutoARIMA |
models.AutoARIMA.season_length |
int | 52 | Seasonal period for ARIMA |
models.AutoETS.enabled |
bool | true | Enable AutoETS |
models.AutoETS.season_length |
int | 52 | Seasonal period for ETS |
models.MSTL.enabled |
bool | true | Enable MSTL |
models.MSTL.season_length |
list | [4, 52] | Multiple seasonal periods |
models.CrostonOptimized.enabled |
bool | true | Enable Croston for intermittent demand |
models.NHITS.enabled |
bool | false | Enable N-HiTS neural model |
models.NBEATS.enabled |
bool | false | Enable N-BEATS neural model |
models.PatchTST.enabled |
bool | false | Enable PatchTST transformer |
models.TFT.enabled |
bool | false | Enable Temporal Fusion Transformer |
models.DeepAR.enabled |
bool | false | Enable DeepAR |
models.TimesFM.enabled |
bool | false | Enable Google TimesFM foundation model |
dask.n_workers |
int | 4 | Dask worker count for parallel forecast |
dask.chunk_size |
int | 50 | Series per Dask batch |
method_selection_strategy |
str | auto |
auto or fixed_set |
best_fit_methods |
list | [] | Methods to always run regardless of characteristics |
method_overrides |
dict | {} | Force specific methods for series matching a condition |
Characterization Parameters¶
Stored under parameter_type = 'characterization'.
| Key | Type | Default | Description |
|---|---|---|---|
intermittency.adi_threshold |
float | 1.3 | ADI above this → intermittent |
intermittency.cv2_threshold |
float | 0.49 | CV² above this → lumpy demand |
seasonality.acf_threshold |
float | 0.3 | ACF peak threshold for seasonality detection |
seasonality.min_periods |
int | 2 | Minimum seasonal periods required |
trend.significance_level |
float | 0.05 | Mann-Kendall test p-value threshold |
stationarity.significance_level |
float | 0.05 | ADF test p-value threshold |
data_sufficiency.min_for_ml |
int | 52 | Minimum observations for ML models |
data_sufficiency.min_for_dl |
int | 104 | Minimum observations for deep learning |
complexity.weights.seasonality |
float | 0.3 | Weight of seasonality in complexity score |
complexity.weights.trend |
float | 0.2 | Weight of trend |
complexity.weights.intermittency |
float | 0.3 | Weight of intermittency |
complexity.weights.variance |
float | 0.2 | Weight of variance coefficient |
Outlier Detection Parameters¶
Stored under parameter_type = 'outlier_detection'.
| Key | Type | Default | Description |
|---|---|---|---|
iqr_multiplier |
float | 1.5 | IQR fence multiplier (lower = stricter) |
zscore_threshold |
float | 3.0 | Z-score threshold |
stl_period |
int | 52 | STL decomposition seasonal period |
stl_threshold |
float | 3.0 | STL residual z-score threshold |
min_obs_for_stl |
int | 24 | Minimum observations to use STL method |
Evaluation / Backtesting Parameters¶
Stored under parameter_type = 'evaluation' but resolved from the forecasting config section.
| Key | Type | Default | Description |
|---|---|---|---|
backtesting.n_windows |
int | 3 | Number of rolling backtest windows |
backtesting.window_size |
int | 12 | Length of each validation window |
backtesting.step_size |
int | 1 | Steps between window origins |
backtesting.metrics |
list | [mae, rmse, mase, bias, coverage_90] | Metrics to compute |
Best Method Selection Weights¶
Stored under parameter_type = 'best_method'.
| Key | Type | Default | Description |
|---|---|---|---|
weights.mae |
float | 0.40 | Weight for Mean Absolute Error |
weights.rmse |
float | 0.20 | Weight for Root Mean Square Error |
weights.bias |
float | 0.15 | Weight for forecast bias |
weights.coverage_90 |
float | 0.15 | Weight for 90% interval coverage |
weights.mase |
float | 0.10 | Weight for Mean Absolute Scaled Error |
All weights must sum to 1.0. Lower composite score = better method.
Alert / NL Query Parameters¶
Stored under parameter_type = 'alerts'.
Controls the natural-language alert query parser — specifically the LLM (Ollama) tier. See Alerts — Natural Language Parser for full details.
| Key | Type | Default | Description |
|---|---|---|---|
llm_parser_enabled |
bool | true |
Set false to always use TF-IDF, skipping Ollama entirely |
ollama_url |
str | http://localhost:31434 |
Ollama server base URL |
ollama_model |
str | mistral |
Model name served by Ollama (llama3, phi3, etc.) |
llm_confidence_threshold |
float | 0.55 |
Minimum LLM confidence score; below this, falls back to TF-IDF |
Order Frequency Parameters¶
Stored under parameter_type = 'order_frequency'.
Controls which replenishment frequencies are allowed per item group. When
configured with non-empty allowed_orders_per_year, the K-Curve optimizer
snaps each item's EOQ to the nearest allowed frequency that minimises total
annual cost. See K-Curve — Order-frequency constraints
for the full algorithm and EOQ — Order-Frequency Constrained
for the per-item formula.
Opt-in: auto-seeded default is empty
The default order_frequency row is seeded with allowed_orders_per_year: []
(empty). Constraints only activate when a user creates or edits a parameter
set with non-empty frequencies. Pipeline runs also require constrained: true
in the scenario parameters.
| Key | Type | Default | Description |
|---|---|---|---|
allowed_orders_per_year |
list | [] |
Allowed replenishment frequencies. Accepts named strings ("weekly", "monthly", "quarterly", "annually", etc.) or numeric values (orders/year). |
Supported frequency names: daily (365), weekly (52), bi-weekly (26),
monthly (12), bi-monthly (6), quarterly (4), semi-annually (2),
annually (1).
Managing Parameters via the UI¶
Navigate to Settings (/settings) to view, create, edit and reorder parameter sets.
Creating a segment-specific parameter set¶
- Go to Settings → Parameters.
- Choose the parameter type (e.g.
forecasting). - Click Add version.
- Edit the
parameters_setJSON — only include keys you want to differ from the default. - In the Segments column, assign one or more segments.
- Click Resolve assignments (calls
POST /api/parameters/resolve) to propagate assignments toseries_parameter_assignment.
Reordering¶
When multiple parameter sets are assigned to the same series (via overlapping segments), the one with the lowest sort_order wins. Drag to reorder in the UI (calls PUT /api/parameters/reorder). The default set must always be last.
API Reference for Parameters¶
| Method | Path | Description |
|---|---|---|
| GET | /api/parameters |
List all parameter sets (optionally filter by ?parameter_type=) |
| GET | /api/parameters/{id} |
Get one parameter set |
| POST | /api/parameters |
Create a new parameter set |
| PUT | /api/parameters/{id} |
Update a parameter set |
| DELETE | /api/parameters/{id} |
Delete a parameter set |
| PUT | /api/parameters/reorder |
Reorder within a type |
| POST | /api/parameters/resolve |
Trigger assignment resolution for all series |
| GET | /api/parameters/{id}/segments |
List segments linked to a parameter set |
| PUT | /api/parameters/{id}/segments |
Update segment links |