Skip to content

Modelagem de domínio

Por que a modelagem é crítica aqui

Na forecast-engine, modelagem não é apenas desenho de classes: é a camada que impede estados inválidos de chegarem à produção. Cada invariante reduz risco operacional da plataforma.

A camada domain está organizada em três contextos:

  • demand: histórico observado por série (SKU + loja + granularidade);
  • modeling: definição/versionamento de modelos e instâncias;
  • forecasting: run de inferência, pontos previstos e metadados de fallback.

Para visão aprofundada de regras e objetos, consulte a seção Domínio detalhado.

1. Construir uma chave de série (SeriesKey)

Toda execução de treino/inferência depende de uma identidade canônica de série.

from uuid import UUID
from domain.demand.model import SeriesKey
from domain.shared.value_objects import ExternalKey, OrganizationId, TimeGrain
series_key = SeriesKey(
organization_id=OrganizationId(UUID("0194fd5b-5ac2-7e56-b8f0-97d276b57b01")),
sku_id=UUID("0194fd5b-5ac2-7e56-b8f0-97d276b57b02"),
store_id=UUID("0194fd5b-5ac2-7e56-b8f0-97d276b57b03"),
sku_key=ExternalKey(system="erp", value="sku-1"),
store_key=ExternalKey(system="erp", value="store-1"),
grain=TimeGrain.DAILY,
)

Regras importantes:

  • organization_id, sku_id, store_id não podem ser UUID nulo;
  • ExternalKey.system é normalizado para minúsculo;
  • ExternalKey.value é normalizado para maiúsculo.

2. Registrar observações de demanda

from datetime import date
from decimal import Decimal
from domain.demand.model import DemandObservation, DemandSeries
from domain.shared.value_objects import DemandQuantity, UomCode
series = DemandSeries(key=series_key)
series.record_observation(
DemandObservation(
period_start=date(2026, 1, 1),
quantity=DemandQuantity(value=Decimal("10"), uom=UomCode("EA")),
source="erp_sync",
)
)

Invariantes de DemandSeries:

  • não aceita período duplicado (DuplicatePeriodError);
  • mantém ordenação temporal consistente;
  • exige UOM homogênea na série;
  • valida compatibilidade entre period_start e grain.

3. Definir um modelo (ModelDefinition)

from domain.modeling.model import (
EvaluatorSpec,
HorizonSpec,
LagSpec,
MetricDirection,
MetricSpec,
ModelDefinition,
ModelName,
StrategySpec,
WindowSpec,
)
from domain.shared.value_objects import TimeGrain
model = ModelDefinition(
organization_id=series_key.organization_id,
name=ModelName("baseline-xgboost"),
target_selector="sku:*|store:*",
horizon_spec=HorizonSpec(periods=7, grain=TimeGrain.DAILY, step=1),
window_spec=WindowSpec(lookback_periods=56, min_history_periods=28),
lag_spec=LagSpec(lags=(1, 7, 14)),
evaluator_specs=(EvaluatorSpec(registry_key="mae"),),
strategy_specs=(
StrategySpec(
strategy_key="xgboost",
library_family="nixtla-statsforecast",
params_schema={
"model": {
"name": "auto-arima",
"hyperparams": {"season_length": 7},
},
"features": {
"date_features": ["dayofweek"],
"static_features": ["sku_key", "store_key"],
"known_future_features": [],
},
"intervals": {"levels": [80, 95]},
},
),
),
primary_metric=MetricSpec(name="mae", direction=MetricDirection.MIN),
promotion_thresholds={"mae": 2.0},
)

Invariantes de ModelDefinition:

  • ao menos um avaliador e uma estratégia;
  • primary_metric deve existir entre os avaliadores;
  • ao menos uma estratégia habilitada (enabled=True);
  • horizon_spec.grain permitido em allowed_target_grains;
  • chaves de estratégia normalizadas em slug.

Convenção recomendada para library_family e params_schema

library_family agora deve ser tratado como seletor de família de engine, não como rótulo solto. As convenções atuais para estratégias Nixtla são:

  • nixtla-statsforecast
  • nixtla-mlforecast

params_schema aceita árvores JSON-like aninhadas (dict + list + escalares). A convenção padrão para novas estratégias é:

{
"model": {
"name": "auto-arima",
"hyperparams": {
"season_length": 7
}
},
"features": {
"date_features": ["dayofweek"],
"static_features": ["sku_key", "store_key"],
"known_future_features": []
},
"intervals": {
"levels": [80, 95]
},
"execution": {
"training_mode": "series"
}
}

Notas operacionais:

  • model.name é obrigatório para adapters que seguem essa convenção;
  • model.hyperparams, features.* e intervals.levels têm defaults vazios;
  • execution.training_mode é opcional e suporta series (default) e batch;
  • known_future_features já pode ser declarado, mas os adapters concretos ainda podem não consumi-lo em runtime.

4. Evoluir versões e instâncias

from domain.modeling.model import ModelInstanceStatus
model_version = model.ensure_current_version()
instance = model.add_instance(
series_key=series_key,
model_version=model_version.version,
strategy_key="xgboost",
status=ModelInstanceStatus.DRAFT,
artifact_uri="s3://bucket/model-v1.bin",
metric_snapshot={"mae": 0.8},
)
model.promote_instance(series_key=series_key, version=instance.version)

Regra crítica para produção: só pode existir uma instância ativa por série.

Observação de lineage:

  • ModelVersion representa o snapshot congelado da definição do modelo;
  • ModelInstance.version continua sendo a versão do artefato treinado por série;
  • ModelInstance.model_version liga o artefato ao snapshot de configuração que abasteceu o treino.

5. Criar um ForecastRun

from domain.forecasting.model import ForecastRun
from domain.modeling.model import HorizonSpec
from domain.shared.value_objects import UomCode
run = ForecastRun(
organization_id=series_key.organization_id,
model_definition_id=model.id,
model_version=1,
series_key=series_key,
horizon_spec=HorizonSpec(periods=7, grain=TimeGrain.DAILY, step=1),
expected_uom=UomCode("EA"),
)

Invariantes de previsão:

  • horizon_spec.grain deve coincidir com series_key.grain;
  • run completo deve ter exatamente horizon_spec.periods pontos;
  • sequência temporal deve ser monotônica e alinhada ao step;
  • metadados de fallback (degraded, fallback_from_version, executed_model_version) devem ser consistentes.