Skip to content

Configuração de model definition

Objetivo

Esta página documenta a forma útil do ModelDefinition para operação diária: quais campos importam, quais combinações são aceitas e como configurar estratégias Nixtla sem depender de leitura direta do código.

Papel no fluxo operacional

Um ModelDefinition é a política de treino, promoção e forecast para uma população de séries derivada de um snapshot canônico. Ele não é o artefato treinado. O artefato treinado é um ModelInstance, criado por série depois que o workflow roda.

Na prática, a organização configura os ModelDefinitions que quer operar. Cada definição responde a perguntas como:

  • qual tipo de série será treinado;
  • em qual granularidade temporal;
  • quais regras validam o input e a projeção;
  • quais features e estratégias participam do treino;
  • qual métrica decide promoção;
  • quando uma campeã ativa pode ser reaproveitada.

O campo que define a população de séries é series_projection_recipe. Por exemplo, uma definição com target_series_kind=sku_store e target_grain=daily governa a visão diária por SKU e loja produzida a partir do snapshot. Quando o workflow é disparado para essa definição, ele projeta todas as séries sku_store diárias deriváveis do snapshot, treina candidatas por SeriesKey e promove uma campeã por série.

Hoje não existe uma tabela ou API de atribuição granular dizendo que uma série individual usa uma definição e outra série usa outra. A seleção operacional é feita pelo model_definition_id escolhido no trigger:

organization_id + model_definition_id + snapshot_id
-> resolve a configuração ativa desse ModelDefinition
-> aplica a series_projection_recipe
-> processa todas as séries projetadas para esse snapshot

Se duas definições da mesma organização apontarem para o mesmo target_series_kind e target_grain, o sistema permite rodar ambas. Elas produzem conjuntos independentes de ModelInstances sob model_definition_ids diferentes. Isso só deve ser usado quando houver dois produtos ou políticas de forecast realmente distintos para a mesma população de séries.

Configurações por série existem apenas como overrides de configuração (series_overrides) depois que a série já pertence à população projetada pela definição. Elas não são um mecanismo de roteamento granular para escolher quais séries entram ou saem do workflow.

Estrutura principal

Os campos centrais de configuração são:

CampoPapel
namenome legível da definição
horizon_spechorizonte previsto
evaluation_specpolítica temporal usada para promoção
validation_policiespolíticas de validação do frame canônico agrupadas por fase
series_projection_recipereceita que deriva a série alvo treinável
feature_recipepreparo padrão de feature engineering, com override por estratégia/série quando configurado
champion_reuse_policyquando reaproveitar a campeã ativa
evaluator_specsmétricas suportadas para promoção e tracking
primary_metricmétrica usada para comparação principal
promotion_thresholdslimites mínimos para promoção
strategy_specscatálogo habilitado de candidatas

O endpoint PUT /v1/model-definitions/{id}/config aplica atualização parcial: campos omitidos ou enviados como null preservam o valor atual. Campos enviados com valor substituem aquele trecho da configuração; por exemplo, "feature_recipe": {"steps": []} limpa a receita, enquanto omitir feature_recipe mantém a receita existente.

Para configuração por cliente, use manifestos JSON versionados e aplique no ClickHouse:

Terminal window
just config-validate configs/clients/client-a.json
just config-apply configs/clients/client-a.json

O manifesto de cliente continua sendo o caminho operacional para ativar revisões. O treino não lê esse registro diretamente do adapter de ClickHouse: control/modeling/config expõe o boundary ActiveConfigurationResolverPort. No runtime de jobs, infra/jobs/bootstrap.py injeta um resolver backed por configuration_revisions.

O manifesto também é o ponto onde a organização declara quais definições quer manter ativas para operação. Cada entrada de model_definitions referencia uma definição persistida e atualiza sua configuração efetiva. Depois do apply, os triggers de treino e predição continuam escolhendo uma única definição por vez via model_definition_id.

Na hora de treinar, avaliar e promover, o resolver monta uma configuração congelada por SeriesKey e estratégia: começa pelo snapshot de produto da ModelVersion, aplica revisões ativas de organização, depois de ModelDefinition, depois de série, e produz fingerprints estáveis para política de validação, receita de projeção, receita de features, parâmetros da estratégia, política de seleção e configuração resolvida. Esses hashes e o payload resolvido são gravados nas referências de treino e auditoria de promoção.

Na execução de forecast, a campeã ativa não reconsulta as revisões ativas para decidir horizonte, features, parâmetros de estratégia, receita de projeção ou família da biblioteca. O job seleciona as instâncias ativas que serão forecastadas, reconstrói a configuração a partir de TrainingReference.resolved_config_snapshot, valida o hash congelado antes de chamar a estratégia e falha se uma instância ativa antiga não tiver esse snapshot. Por padrão, as campeãs ativas podem pertencer a versões diferentes do modelo; model_version continua existindo apenas como filtro explícito do comando. A projeção canônica para série alvo é executada uma vez quando todas as instâncias selecionadas compartilham a mesma receita congelada. Receitas de projeção incompatíveis falham de forma clara por enquanto. Assim, uma mudança de configuração depois do treino afeta novos treinos, não a execução de um artefato já treinado. Cada ForecastRun completado também grava a referência de execução usada: versão da instância ativa, estratégia, URI do artefato e os hashes da configuração congelada. Se uma série falhar depois dessa referência ser resolvida, o job persiste o ForecastRun como FAILED, sem pontos e com failure_reason, salva os demais resultados da seleção e ainda falha a tarefa para o orquestrador.

O config-apply ativa revisões separadas por config_kind: validation_policy, series_projection_recipe, feature_recipe, modeling_policy e champion_reuse_policy.

No workflow, series_projection_recipe é consumida antes do treino pela task project_training_series. Essa task atualiza o DemandSnapshotManifest com os artefatos projetados. train_model_candidates passa a carregar as partições por series_id e aplicar feature_recipe por candidata.

O trigger não recebe lista de séries. O conjunto de séries vem do índice projetado por project_training_series, que é consequência da receita resolvida para aquele ModelDefinition e do conteúdo do snapshot.

O manifesto JSON também aceita series_overrides dentro de cada entrada de model_definitions. Cada override informa o series_key compacto e um bloco config; o apply grava essas mudanças como revisões ConfigScopeType.SERIES na mesma tabela configuration_revisions. Na resolução efetiva, as revisões são aplicadas em ordem determinística: organização, definição de modelo e série.

{
"$schema": "../../../schemas/config-manifest.schema.json",
"organization_id": "01956c73-9c1f-7fb7-94b1-a2c5bc65d900",
"created_by": "ops",
"model_definitions": [
{
"model_definition_id": "01956c73-9c1f-7fb7-94b1-a2c5bc65d910",
"config": {
"name": "retail weekly",
"horizon_spec": { "periods": 8, "grain": "weekly", "step": 1 },
"validation_policies": {
"ingestion": {
"rule_set_key": "atreides-demand-canonical-v1",
"schema_version": "1",
"rules": [
{ "rule_key": "required_columns" },
{ "rule_key": "organization_scope" },
{ "rule_key": "quantity_validity" },
{ "rule_key": "temporal_validity" },
{ "rule_key": "series_identity_validity" },
{ "rule_key": "source_validity" },
{ "rule_key": "chronological_uniqueness" },
{ "rule_key": "uom_consistency" }
]
},
"snapshot_materialization": {
"rule_set_key": "atreides-demand-canonical-v1",
"schema_version": "1",
"rules": [
{ "rule_key": "schema_version" },
{ "rule_key": "manifest_row_count" },
{ "rule_key": "manifest_series_count" }
]
},
"training_preparation": {
"rule_set_key": "atreides-demand-canonical-v1",
"schema_version": "1",
"rules": [
{ "rule_key": "required_projection_dimensions" },
{ "rule_key": "chronological_uniqueness" },
{ "rule_key": "uom_consistency" }
]
}
},
"series_projection_recipe": {
"recipe_key": "category-store-weekly-sum",
"target_series_kind": "category_store",
"target_grain": "weekly",
"aggregation": "sum"
},
"feature_recipe": {
"steps": [
{ "step_key": "target.lags.v1", "params": { "lags": [1, 4] } }
]
}
}
}
]
}

O schema para IDEs fica em schemas/config-manifest.schema.json. Manifests de configuração são JSON.

Regras que valem sempre

  • primary_metric precisa existir em evaluator_specs;
  • promotion_thresholds só podem referenciar métricas declaradas;
  • precisa existir ao menos uma estratégia habilitada;
  • strategy_key e library_family são normalizados para slug;
  • params_schema.execution.max_workers, quando informado, precisa ser inteiro positivo;
  • evaluation_spec.strategy aceita holdout_tail;
  • validation_policies aceita ingestion, snapshot_materialization, training_preparation e prediction_preparation; o validador rejeita regras em fases que não têm o contexto runtime necessário;
  • ingestão valida o payload carregado antes da canonicalização; as demais fases validam frames canônicos Atreides; regras não renomeiam, juntam, completam nem normalizam dados de cliente;
  • somente as regras presentes na política resolvida para a fase são executadas;
  • regras de manifesto e projeção, quando configuradas, recebem params tipados no runtime: schema_version, manifest_row_count e manifest_series_count vêm do DemandSnapshotManifest; required_projection_dimensions vem da series_projection_recipe;
  • series_projection_recipe.target_grain precisa bater com horizon_spec.grain;
  • feature_recipe.steps representa preparo reutilizável padrão; uma estratégia pode declarar strategy_specs[].feature_recipe quando precisar de um frame diferente;
  • cada feature_recipe.steps[] é produzido pela definição canônica do step, que carrega o schema de params e retorna uma chamada configurada e tipada;
  • revisões ativas podem sobrescrever partes da configuração de treino, mas o objeto resolvido e congelado é o que segue para feature execution, avaliação, promoção, artefatos e forecast;
  • com champion_reuse_policy.require_current_config=true, campeãs antigas só são refrescadas quando o hash da configuração resolvida e os hashes de componentes gravados no treino ainda batem com a configuração atual da série e estratégia;
  • cada chamada ao DemandValidationCatalog recebe um contexto tipado de fase: ingestão usa frame e organization_id; materialização também carrega DemandSnapshotManifest; treino e predição carregam manifesto e series_projection_recipe. A strategy da fase resolve os params finais da regra a partir da configuração declarada ou do contexto runtime tipado;
  • após a projeção, o dataset de treino precisa conter apenas séries da mesma organização, mesmo target_series_kind e mesmo target_grain da receita de projeção;
  • quando evaluation_spec.holdout_periods=null, a engine usa horizon_spec.periods;
  • a família e a estratégia escolhidas precisam estar registradas no catálogo de estratégias.

Famílias suportadas

library_familyContrato
nixtla-statsforecastuma chamada de treino por série
nixtla-mlforecastuma chamada de treino por série

Contrato de evaluation_spec

{
"strategy": "holdout_tail",
"holdout_periods": null
}

Regras:

  • holdout_periods precisa ser > 0 quando informado;
  • a promoção usa as métricas retornadas pela estratégia no treino do candidato final;
  • o holdout temporal continua servindo como caminho de validação train/predict;
  • se uma série não satisfizer o caminho atual ou falhar durante avaliação/treino, a engine falha o run inteiro, não promove campeões parciais e deixa o erro explícito para retry do job.

Contrato de série, features e campeã

series_projection_recipe define qual recorte de série este ModelDefinition treina:

{
"recipe_key": "sku-store-daily-sum",
"target_series_kind": "sku_store",
"target_grain": "daily",
"aggregation": "sum"
}

Hoje os valores de target_series_kind são padronizados no domínio e a agregação suportada é sum. As dimensões esperadas são derivadas desse valor pelo SeriesKind. Exemplos válidos incluem sku_store, sku_region, sku_channel, category_store, category_region, channel_store, channel_region, category, region e channel.

A projeção acontece sobre o contrato canônico de demanda, não sobre dados brutos de cliente. Para treinar category_store, por exemplo, o frame canônico precisa trazer category_id, category_system e category_key; a engine agrega as observações por categoria, loja, período e UOM antes de criar o SeriesTrainingContext.

feature_recipe é a área para passos reutilizáveis de preparo. Ela pode ficar no nível do modelo como padrão ou em strategy_specs[].feature_recipe quando uma estratégia precisar de outro frame para a mesma série:

{
"steps": [
{
"step_key": "calendar.date_parts.v1",
"params": {
"columns": ["dayofweek"]
}
}
]
}

step_key identifica uma definição canônica de feature engineering. Essa definição carrega o schema de params; ao receber o payload, ela retorna uma chamada configurada e tipada para o executor. Esses passos são executados pelo boundary de data-preparation antes de cada estratégia receber o frame por série. Hoje existem implementações Spark e Polars para os mesmos step_key suportados; um step_key desconhecido ou params malformados falham cedo.

Passos canônicos disponíveis:

  • target.lags.v1: cria defasagens da variável alvo, como quantity_lag_1;
  • target.rolling.v1: cria médias móveis por janela configurada e a média expansiva defasada (quantity_expanding_mean_lag_1);
  • target.ewm.v1: cria média móvel exponencial defasada;
  • calendar.date_parts.v1: cria partes de data como period_week, period_month, period_quarter e, quando solicitado, period_dayofweek;
  • calendar.fourier.v1: cria termos sazonais seno/cosseno;
  • calendar.br_holidays.v1: cria features de feriado brasileiro.

champion_reuse_policy controla refresh de campeãs:

{
"refresh_active_champion": true,
"require_current_config": true
}

Com a política padrão, uma campeã ativa só é retreinada diretamente quando o resolved_config_hash gravado no treino continua compatível com a configuração atual da mesma série e estratégia. Se qualquer revisão efetiva mudar esse hash, se a estratégia campeã é desabilitada, ou se refresh_active_champion=false, a série volta para busca completa entre as estratégias habilitadas.

Contrato de strategy_specs

Cada item de strategy_specs tem esta forma:

{
"strategy_key": "seasonal-naive",
"library_family": "nixtla-statsforecast",
"enabled": true,
"params_schema": {
"model": {
"name": "seasonal-naive",
"hyperparams": {}
},
"intervals": {
"levels": []
},
"execution": {
"max_workers": 1
}
}
}

Campos de params_schema

CampoTipoUso
model.namestringnome concreto do modelo dentro do adapter
model.hyperparamsobjectmapa livre de hiperparâmetros
intervals.levelsinteger[]níveis de intervalo, como [80, 95]
execution.max_workersintegerparalelismo máximo entre séries independentes

Exemplo mínimo para nixtla-statsforecast

{
"name": "mvp-seasonal-naive",
"horizon_spec": {
"periods": 7,
"grain": "daily",
"step": 1
},
"evaluation_spec": {
"strategy": "holdout_tail",
"holdout_periods": null
},
"validation_policies": {
"ingestion": {
"rule_set_key": "atreides-demand-canonical-v1",
"schema_version": "1",
"rules": [
{ "rule_key": "required_columns" },
{ "rule_key": "organization_scope" },
{ "rule_key": "quantity_validity" },
{ "rule_key": "temporal_validity" },
{ "rule_key": "series_identity_validity" },
{ "rule_key": "source_validity" },
{ "rule_key": "chronological_uniqueness" },
{ "rule_key": "uom_consistency" }
]
},
"snapshot_materialization": {
"rule_set_key": "atreides-demand-canonical-v1",
"schema_version": "1",
"rules": [
{ "rule_key": "schema_version" },
{ "rule_key": "manifest_row_count" },
{ "rule_key": "manifest_series_count" }
]
},
"training_preparation": {
"rule_set_key": "atreides-demand-canonical-v1",
"schema_version": "1",
"rules": [
{ "rule_key": "chronological_uniqueness" },
{ "rule_key": "uom_consistency" },
{ "rule_key": "required_projection_dimensions" }
]
}
},
"series_projection_recipe": {
"recipe_key": "sku-store-daily-sum",
"target_series_kind": "sku_store",
"target_grain": "daily",
"aggregation": "sum"
},
"feature_recipe": {
"steps": []
},
"champion_reuse_policy": {
"refresh_active_champion": true,
"require_current_config": true
},
"evaluator_specs": [
{ "registry_key": "mae", "params": {} }
],
"primary_metric": {
"name": "mae",
"direction": "min"
},
"promotion_thresholds": {
"mae": 1000.0
},
"strategy_specs": [
{
"strategy_key": "seasonal-naive",
"library_family": "nixtla-statsforecast",
"enabled": true,
"params_schema": {
"model": {
"name": "seasonal-naive",
"hyperparams": {}
},
"intervals": {
"levels": []
},
"execution": {
"max_workers": 1
}
}
}
]
}

Esse é o shape usado pelo seed local do repositório.

Exemplo com paralelismo para nixtla-statsforecast

{
"strategy_key": "auto-arima",
"library_family": "nixtla-statsforecast",
"enabled": true,
"params_schema": {
"model": {
"name": "auto-arima",
"hyperparams": {}
},
"intervals": {
"levels": [80]
},
"execution": {
"max_workers": 4
}
}
}

Use max_workers quando quiser permitir que a engine treine séries independentes em paralelo. A unidade semântica continua sendo uma chamada de train() por SeriesKey.

Importante:

  • paralelismo é otimização operacional;
  • avaliação, promoção e ativação continuam independentes por series_key;
  • não existe “campeão do painel” nesse contrato.

Exemplo para nixtla-mlforecast

{
"strategy_key": "random-forest",
"library_family": "nixtla-mlforecast",
"enabled": true,
"params_schema": {
"model": {
"name": "random-forest",
"hyperparams": {
"n_estimators": 200,
"max_depth": 8
}
},
"intervals": {
"levels": []
},
"execution": {
"max_workers": 1
}
}
}

Regras extras dessa família:

  • lags, rolling windows e calendário devem ser declarados em feature_recipe;
  • hiperparâmetros precisam ser escalares JSON, não listas nem objetos aninhados.

O que congela por versão

No treino, o domínio congela o snapshot base em ModelVersion e congela a configuração efetivamente resolvida em cada TrainingReference. Isso significa que:

  • trocar strategy_specs depois não reescreve versões antigas;
  • trocar validation_policy, series_projection_recipe, feature_recipe ou champion_reuse_policy cria uma nova configuração efetiva;
  • cada ModelInstance aponta para a versão usada no treino, e seu TrainingReference guarda o resolved_config_snapshot mais um config_fingerprint com resolved_config_hash, validation_policy_hash, projection_recipe_hash, feature_recipe_hash, strategy_params_hash e selection_policy_hash;
  • uma campeã antiga com configuração diferente deixa de ser refrescada por padrão e a série volta para busca completa;
  • horizonte, avaliação, feature recipe e parâmetros usados pela estratégia vêm da configuração resolvida, não dos campos vivos do ModelDefinition;
  • a promoção escolhe campeãs com a política congelada na candidata treinada, mesmo que thresholds ou métrica primária mudem antes do retry de promoção;
  • artefato, métricas e promoção continuam auditáveis.

Erros de configuração mais comuns

CenárioResultado esperado
primary_metric fora de evaluator_specsrequisição rejeitada
execution.max_workers <= 0treino falha cedo
model.name incompatível com o adaptertreino/predição falham com erro explícito
intervals.levels em modelo point-onlyadapter rejeita a estratégia

Quando usar esta página

Use esta página quando estiver:

  • criando um ModelDefinition novo;
  • ajustando estratégias via HTTP;
  • revisando se um payload é compatível com a família escolhida.

Para o comportamento interno dos adapters, veja Nixtla.