Skip to content

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:

{
  "unique_id": "12_301",
  "method": "forecasting",
  "overrides": { "horizon": 12 }
}

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 where is_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:

config = resolver.resolve("12_301", "forecasting")
# Returns the merged forecasting config dict

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: evaluation and best_method parameter types were removed/renamed (best_methodbacktesting). netting and method_selection are not segment-parameter-set types — netting config lives in config_forecast_parameter_set.params['netting'] (pipeline-level, per-segment netting is intentionally unsupported) and method_selection lives in the scenario JSON (settings_param_sets) + the backtesting parameter 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

  1. Go to Settings → Parameters.
  2. Choose the parameter type (e.g. forecasting).
  3. Click Add version.
  4. Edit the parameters_set JSON — only include keys you want to differ from the default.
  5. In the Segments column, assign one or more segments.
  6. Click Resolve assignments (calls POST /api/parameters/resolve) to propagate assignments to series_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