Lab 72 — Feature store: o mesmo cálculo no treino e na inferência
O problema, e a empresa que o tem
A Cadência corrigiu o contrato de preço no L70: nenhum registro fora de faixa chega mais a prata_produtos, e o modelo de reposição automática — que recomenda quanto cada uma das 40 lojas deveria reabastecer de cada SKU — voltou a treinar sobre dado confiável. O tema do L71 (medir um baseline de regra simples antes de aceitar o modelo) já tinha provado que, para essa decisão específica, o modelo bate a regra fixa com folga suficiente para justificar o investimento. A pergunta "vale a pena usar ML aqui?" estava respondida. Este módulo começa depois dela.
Para melhorar a recomendação, o time de dados adicionou uma feature nova: media_pedidos_30d, a média de pedidos dos últimos 30 dias por loja e SKU — um sinal de demanda recente que preco_venda sozinho não captura. No notebook de treino, calculada com uma consulta Athena sobre o histórico completo em prata_pedidos, ela levou a acurácia offline de 81% para 90%. A promoção para produção parecia óbvia.
Em produção, a recomendação de reposição é decidida em tempo real, sempre que o estoque de uma loja cruza um limiar — a API não pode esperar segundos por uma consulta Athena. Então outro engenheiro escreveu uma segunda implementação da mesma ideia: um Lambda que calcula media_pedidos_30d na hora, consultando pedidos_recentes (a tabela do DynamoDB que o checkout alimenta em tempo real), com uma janela de 30 dias corridos em horário de Brasília — não a mesma janela em UTC que a consulta Athena do notebook usa, e sem a mesma regra sobre incluir ou não o pedido de hoje, que ainda pode estar sendo confirmado. Duas colunas com o mesmo nome, calculadas por dois caminhos que ninguém comparou lado a lado.
O que este laboratório NÃO é
Não é sobre validar se o dado bruto está correto — o L70 já garante isso para prata_produtos, e prata_pedidos segue o mesmo padrão. Não é sobre decidir se um modelo resolve melhor que uma regra — isso é o tema do L71, e este módulo assume a resposta dele como dada. E não é sobre detectar desvio estatístico gradual no dado de entrada ao longo do tempo — isso é drift, e é o L77. Este módulo resolve uma coisa: a MESMA feature, calculada uma única vez, chega idêntica ao treino e à inferência — não duas contas parecidas que um dia divergem.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando ou uma consulta na seção de implantação.
- Explicar por que duas implementações da mesma fórmula, escritas por pessoas diferentes, divergem mesmo quando o nome da coluna é idêntico.
- Definir um Feature Group no SageMaker Feature Store com Online Store e Offline Store habilitados ao mesmo tempo.
- Calcular a feature uma única vez, dentro de um Glue job, e ingerir o mesmo valor nos dois stores no mesmo lote.
- Consumir o Offline Store no treino com um join ponto-no-tempo pelo EventTime, sem vazar dado que ainda não existia no momento do rótulo.
- Consumir o Online Store na inferência com GetRecord, dentro do orçamento de latência da API de reposição.
- Medir o skew entre o valor offline e o valor online da mesma feature, para o mesmo par loja+SKU, no mesmo instante.
- Justificar por que a ingestão única (um Glue job, dois destinos) é estruturalmente diferente de sincronizar duas implementações com testes.
- Provar com número: skew antes da correção, skew depois, e a latência de leitura do Online Store no p99.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| SageMaker Feature Store: Online Store e Offline Store | MLA-C01, MLS-C01 | um único Feature Group com os dois stores habilitados, alimentado por uma única ingestão | a diferença de propósito entre os dois — latência de milissegundos por chave vs. histórico completo para treino — e por que um não substitui o outro |
| Training/serving skew | MLA-C01, MLS-C01 | o defeito central do módulo: a mesma feature calculada duas vezes, por dois caminhos que divergem em janela, fuso e origem do dado | reconhecer a causa raiz — lógica duplicada — e não confundir com overfitting ou dado insuficiente |
| Join ponto no tempo (point-in-time correct join) | MLA-C01, MLS-C01 | DatasetBuilder do SDK do SageMaker faz o join pelo EventTime, garantindo que o treino nunca vê valor calculado depois do rótulo | por que um join simples por chave (sem tempo) vaza dado futuro para dentro do treino |
| Ingestão única gerando dois destinos | MLA-C01 | o conector Spark do Feature Store grava Online e Offline no mesmo ingest(), a partir do mesmo DataFrame | por que dois PutRecord separados, um por store, reabrem a possibilidade de divergência que este módulo fecha |
| Baseline de regra vs. modelo, antes de investir em Feature Store | AIF-C01, MLA-C01 | o módulo assume que o tema do L71 já provou que o modelo vale o investimento — Feature Store não decide ISSO, só corrige como a feature chega até ele | separar "vale a pena usar ML" (L71) de "o cálculo da feature está certo" (este módulo) — são perguntas diferentes |
Requisitos, e como cada um muda o desenho
Requisito que não muda uma linha de configuração é intenção, não requisito. A terceira coluna é onde cada um deixou marca no desenho.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| A mesma feature não pode ter duas implementações | skew medido em 0% entre o valor lido no treino e o valor lido na inferência | obriga um único Glue job definindo a lógica da feature, gravando em Offline e Online Store no mesmo lote — nunca duas queries escritas por pipelines diferentes |
| Treino não pode enxergar dado que ainda não existia no momento do rótulo | zero vazamento de dado futuro (leakage) no dataset de treino | obriga usar o Offline Store com join "ponto no tempo" pelo EventTime, não um JOIN simples por chave loja+SKU |
| Inferência em tempo real responde dentro do orçamento de latência da API | leitura da feature abaixo de 10 ms no p99 | obriga o Online Store (otimizado para GetRecord por chave), não uma consulta ao data lake nem ao Offline Store em tempo real |
| Skew tem que ser medido continuamente, não assumido resolvido | alarme antes que a acurácia caia em produção, não depois | obriga um job agendado comparando amostras do Offline Store com o Online Store, publicando a diferença como métrica no CloudWatch |
| Dado de pedido e preço é sensível (herdado do L70) | acesso ao feature store restrito por consumidor | obriga KMS com chave gerenciada pelo cliente e uma IAM role própria para o job de ingestão, outra para o treino, outra para a inferência |
| Custo contido | sem duplicar armazenamento além do necessário | Offline Store reaproveita o mesmo bucket do lake com um prefixo próprio; Online Store só habilitado para as features realmente consultadas em tempo real, não todas |
Arquitetura mínima: a mesma fórmula, escrita duas vezes
Este é o desenho que a Cadência tinha até o Lambda entrar em produção, e ele é legítimo como ponto de partida: cada caminho, isolado, faz exatamente o que parece fazer. O defeito não está em nenhum dos dois — está no fato de existirem dois.
- → notebook lê o histórico completo de prata/pedidos/, partição por partição
- → média de pedidos dos últimos 30 dias corridos em UTC entra como coluna de treino
- → Lambda consulta os últimos 30 dias corridos em horário de Brasília, direto da tabela do checkout
- → feature recém-calculada entra no payload de inferência, em milissegundos
- → modelo treinado é publicado no endpoint; a lógica da feature não viaja junto, só os pesos
- Armazenamento
- Analytics
- IA e machine learning
- Banco de dados
- Compute
As duas metades deste desenho calculam "a mesma" feature a partir de fontes e janelas diferentes, sem que nada compare os dois resultados. O nome da coluna que sai de cada caminho é idêntico — media_pedidos_30d — e é exatamente essa igualdade de nome que esconde a divergência de valor. Percorra os passos e repare onde a janela de tempo deixa de ser a mesma.
- O notebook lê o histórico completo, toda sexta-feira. A consulta Athena varre prata/pedidos/ inteiro, agrupando por loja e SKU, com uma janela de 30 dias corridos calculada em UTC — a mesma unidade de tempo que o resto do lake usa desde o L65.
- A feature entra no treino com 90% de acurácia offline. O SageMaker Training Job recebe media_pedidos_30d como coluna e treina normalmente. A acurácia medida sobre o conjunto de teste do próprio notebook é boa — porque, dentro do notebook, a feature é calculada da mesma forma para treino e teste.
- Em paralelo, o checkout grava cada pedido assim que ele é confirmado. pedidos_recentes no DynamoDB é alimentada por um caminho totalmente separado do lake — otimizada para escrita rápida no momento da compra, não para consulta histórica em lote.
- A API de reposição precisa da mesma feature agora, não na sexta-feira. Quando o estoque de uma loja cruza o limiar, o Lambda calcula media_pedidos_30d sob demanda — mas lendo pedidos_recentes, com janela em horário de Brasília, e incluindo pedidos de hoje que ainda podem ser cancelados.
- O endpoint recebe a feature do Lambda, não a do notebook. sagemaker_endpoint nunca conversa com athena_treino. A única coisa que os dois caminhos compartilham é o nome da coluna e os pesos do modelo — não a lógica que produz o número.
- O modelo publicado carrega os pesos, não a fórmula da feature. Publicar o artefato treinado no endpoint move parâmetros do modelo — nunca o código que calculou media_pedidos_30d no notebook. Se esse código não for o mesmo do Lambda, o modelo em produção recebe um número que ele nunca viu no treino.
- Por que a equipe pequena convive com isso até doer. Duas implementações pequenas parecem inofensivas quando cada uma isoladamente está correta. O sintoma só aparece longe da causa: numa recomendação de reposição estranha, semanas depois de alguém ter escrito o Lambda sem comparar com a consulta do notebook.
# Mostra que "a mesma" feature vale numeros diferentes nos dois caminhos,
# para o MESMO par loja+SKU, no MESMO dia.
LOJA=L07; SKU=CIM-0042; DIA=2026-08-07
# 1) valor calculado como o notebook de treino calcula (Athena, janela UTC,
# 30 dias corridos ENCERRADOS no dia anterior)
aws athena start-query-execution \
--query-string "SELECT AVG(qtd_pedidos) FROM cadencia_lake.prata_pedidos \
WHERE loja_id='${LOJA}' AND produto_id='${SKU}' \
AND dt BETWEEN date_add('day', -30, DATE '${DIA}') AND date_add('day', -1, DATE '${DIA}')" \
--result-configuration OutputLocation=s3://cadencia-lake/atenas-resultados/
# Resultado do notebook, dia 2026-08-07: 14.2 pedidos/dia
# 2) valor que o Lambda calcularia NA MESMA HORA, consultando pedidos_recentes
# (janela em America/Sao_Paulo, incluindo pedidos de hoje ainda nao fechados)
aws dynamodb query --table-name cadencia-pedidos-recentes \
--key-condition-expression 'loja_sku_id = :chave' \
--expression-attribute-values '{":chave":{"S":"L07#CIM-0042"}}' \
| jq '[.Items[].qtd.N | tonumber] | add / length'
# Resultado do Lambda, mesmo instante: 17.9 pedidos/dia
# Mesma feature, mesmo par loja+SKU, mesmo instante: 14.2 vs 17.9 --
# skew de quase 26% NESTE par. O nome da coluna nunca avisou disso.
90% de acurácia no notebook não prova nada sobre a inferência
A acurácia medida no notebook é honesta — só que ela mede o modelo contra uma feature calculada pela MESMA consulta Athena, nos dois lados do split de treino/teste. Em produção, o modelo nunca viu o tipo de número que o Lambda produz: janela em outro fuso, dado que ainda pode ser cancelado, fonte diferente. Não é o modelo que piorou — é a pergunta que ele responde em produção que deixou de ser a mesma que ele aprendeu a responder no treino.
Arquitetura para produção
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. A troca não é "acrescentar um cache" ao desenho anterior — é substituir DUAS implementações por UMA, e trocar "cada lado calcula" por "cada lado lê".
- → lê prata/pedidos/ uma única vez, para calcular media_pedidos_30d
- → ingest_data grava o valor calculado, com EventTime, para uso em treino
- → o MESMO ingest_data grava o mesmo valor no Online Store, no mesmo lote
- → join ponto-no-tempo pelo EventTime: treino nunca vê valor calculado depois do rótulo
- → GetRecord em milissegundos devolve a mesma feature que alimentou o treino
- → amostra periódica do valor offline entra na métrica de comparação
- → amostra periódica do valor online entra na mesma métrica de comparação
- → chave cifra os registros em repouso no bucket do Offline Store
- → a mesma chave cifra o Online Store, sem chave separada por store
- Armazenamento
- Analytics
- Conceito de arquitetura
- IA e machine learning
- Segurança e identidade
- Gestão e governança
A diferença não é uma caixa a mais: o cálculo da feature deixou de existir em dois lugares e passou a existir em UM, dentro do Glue job, que grava o mesmo valor nos dois stores no mesmo lote. Treino e inferência passam a LER o mesmo dado por caminhos diferentes — nunca a CALCULAR de novo. Percorra os passos: cada peça nova resolve um requisito da seção anterior.
- O cálculo acontece uma única vez, dentro do Glue. O Glue job lê prata/pedidos/ e calcula media_pedidos_30d com UMA fórmula. Não existe segunda implementação em lugar nenhum do sistema — é essa ausência estrutural, não disciplina de revisão, que impede a divergência.
- O mesmo valor é gravado nos dois stores, no mesmo lote. O conector do Feature Store para Spark grava o DataFrame calculado direto nos dois destinos, a partir de uma única chamada de ingestão — não são dois PutRecord separados que alguém poderia esquecer de manter sincronizados.
- O treino consulta o Offline Store com join ponto-no-tempo. DatasetBuilder junta o rótulo de treino com a feature pelo EventTime mais recente ANTERIOR ao rótulo — garantindo que nenhuma linha de treino usa um valor que só passou a existir depois do evento que ela tenta prever.
- A inferência consulta o Online Store em milissegundos. GetRecord busca o valor mais recente por chave (loja+SKU), dentro do orçamento de latência da API de reposição — sem recalcular nada, só lendo o que o Glue job já gravou.
- O skew agora é medido, não presumido. Um job agendado amostra o mesmo par loja+SKU nos dois stores e publica a diferença como métrica no CloudWatch — a prova de que os dois lados continuam iguais fica visível todo dia, não só no dia do deploy.
- Uma chave, os dois stores, uma role por consumidor. KMS cifra Offline e Online com a mesma chave gerenciada pelo cliente; o job de ingestão, o treino e o endpoint recebem roles separadas, cada uma só com a permissão que precisa — ler ou escrever, nunca as duas.
- Treino e inferência agora leem o mesmo número, por caminhos diferentes. sagemaker_treino e sagemaker_endpoint nunca mais calculam a feature — os dois só leem o que o Glue job já resolveu uma vez. É essa mudança, não uma checagem a mais, que fecha a possibilidade estrutural de skew.
A diferença estrutural em relação à arquitetura mínima não é a presença de um armazenamento gerenciado: é que o número de lugares onde media_pedidos_30d é CALCULADA caiu de dois para um. Antes, cada consumidor tinha sua própria fórmula; agora todo consumidor lê o resultado de uma fórmula só, publicada uma vez.
O ganho que o modelo nunca vê, e é o mais importante
Antes, o modelo em produção recebia um número que ele nunca tinha visto no treino, mesmo com o nome de coluna idêntico. Depois do Feature Store, o endpoint lê exatamente o valor que o Offline Store teria devolvido para aquele instante — porque é o MESMO valor, gravado pela mesma ingestão. A melhor forma de eliminar skew não é comparar dois cálculos: é deixar de ter dois.
O caminho do dado, ponta a ponta
loja_sku_id (a chave composta) e calculado_em (o EventTime) não são acidente: são os dois campos que tornam o join ponto-no-tempo possível. Sem calculado_em, o Offline Store ainda guardaria o histórico — mas o treino não teria como saber qual valor era válido em cada instante passado.
Definicao logica do Feature Group -- o schema que o Glue job (b9) preenche e que o treino (b10) e a inferencia (b11) leem SEM reinterpretar.
{
"FeatureGroupName": "cadencia-media-pedidos-30d",
"RecordIdentifierFeatureName": "loja_sku_id",
"EventTimeFeatureName": "calculado_em",
"FeatureDefinitions": [
{"FeatureName": "loja_sku_id", "FeatureType": "String"},
{"FeatureName": "loja_id", "FeatureType": "String"},
{"FeatureName": "produto_id", "FeatureType": "String"},
{"FeatureName": "media_pedidos_30d", "FeatureType": "Fractional"},
{"FeatureName": "calculado_em", "FeatureType": "String"}
]
}
loja_sku_id é a chave, calculado_em é o relógio
O RecordIdentifier localiza QUAL registro (loja L07, SKU CIM-0042); o EventTime localiza QUANDO aquele valor era válido. Um Feature Group sem EventTime preenchido corretamente ainda funciona para o Online Store — mas o join ponto-no-tempo do treino fica sem base para decidir o que era passado e o que era futuro, e a proteção contra vazamento de dado desaparece em silêncio.
As decisões, e o que se perde em cada uma
📋 A Cadência precisa que a MESMA feature (média de pedidos dos últimos 30 dias por loja e SKU) alimente o treino semanal e a decisão de reposição em tempo real, sem duplicar a lógica em dois lugares e sem que o cálculo em tempo real fique lento demais para a API de reposição.
Resolve o problema sem inventar uma terceira fonte de verdade: o Glue job já lê o lake uma vez, o conector Spark grava os dois stores no mesmo lote, e cada consumidor lê pelo caminho certo para o seu orçamento de latência — sem que nenhum dos dois precise recalcular nada.
Alt: Manter as duas implementações separadas e sincronizá-las com testes automatizados — Qualquer mudança de lógica exige lembrar de alterar dois lugares e o teste que os compara — é exatamente o processo humano que já falhou uma vez e produziu o skew original.
Alt: Calcular a feature em tempo real também no treino, chamando a mesma API de inferência — Um job de treino chamando uma API de baixa latência centenas de milhares de vezes, uma por linha de treino, é lento, caro, e a API não foi dimensionada para tráfego de treino em lote.
Alt: Usar o Redshift do L68 como único ponto de leitura para os dois lados — Redshift é ótimo para varredura analítica, não para leitura de milissegundos por chave única — a API de reposição em tempo real ficaria presa à latência de um data warehouse.
Alt: Recalcular a feature sempre em tempo real, inclusive no treino, sem nenhum armazenamento intermediário — Perde a capacidade de reproduzir exatamente o dado que um treino específico usou — cada treino ficaria sujeito ao estado ATUAL de pedidos_recentes, não ao estado histórico correto para cada rótulo.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Onde a feature é calculada | dentro de um único Glue job, ingerido no Feature Store | cálculo replicado em cada consumidor (notebook, Lambda, o que vier depois) | elimina a chance estrutural de duas lógicas divergirem | menos flexibilidade para um consumidor calcular uma variante ad hoc da mesma ideia |
| Como o treino lê a feature | Offline Store com join ponto-no-tempo pelo EventTime | join simples por chave (loja+SKU), sem considerar tempo | impede que o treino veja um valor que só passou a existir depois do rótulo | exige que EventTime seja preenchido corretamente em toda ingestão, senão o join "as of" erra em silêncio |
| Como a inferência lê a feature | Online Store via GetRecord | consultar o Offline Store diretamente em tempo real | latência de milissegundos, dentro do orçamento da API de reposição | Online Store cobra por throughput provisionado ou sob demanda, além do armazenamento do Offline |
| Onde a definição da feature mora | Feature Group como recurso de Terraform, com dono declarado | script Python solto no notebook de quem calculou primeiro | revisável em Pull Request, rastreável por quem mudou o quê | sintaxe própria do SDK de Feature Store que o time precisa aprender, além do Spark que já conhecia |
A dívida que este módulo cria, e que ele não paga
O Feature Group deste módulo cobre media_pedidos_30d para o modelo de reposição. Ele não resolve reprodutibilidade — se alguém reexecutar o treino hoje sobre o Offline Store, vai obter o dataset certo, mas nada aqui registra QUAL commit do código de treino, QUAL versão de hiperparâmetro e QUAL experimento geraram o modelo específico que está em produção agora. Isso é o L73.
Construir: a definição da feature no Feature Store
A escolha mais consequente deste bloco não é o tipo de dado de cada coluna — é que Online e Offline Store nascem juntos, na mesma declaração. Habilitar um depois do outro, como duas decisões separadas, é o primeiro passo para eles divergirem de novo.
# feature-group.tf -- Online e Offline Store nascem na MESMA declaracao
resource "aws_sagemaker_feature_group" "media_pedidos_30d" {
feature_group_name = "${var.projeto}-media-pedidos-30d"
record_identifier_feature_name = "loja_sku_id"
event_time_feature_name = "calculado_em"
role_arn = aws_iam_role.feature_store_ingestao.arn
feature_definition {
feature_name = "loja_sku_id"
feature_type = "String"
}
feature_definition {
feature_name = "loja_id"
feature_type = "String"
}
feature_definition {
feature_name = "produto_id"
feature_type = "String"
}
feature_definition {
feature_name = "media_pedidos_30d"
feature_type = "Fractional"
}
feature_definition {
feature_name = "calculado_em"
feature_type = "String"
}
# ONLINE: baixa latencia, so o valor mais recente por chave.
online_store_config {
enable_online_store = true
security_config {
kms_key_id = aws_kms_key.feature_store.arn
}
}
# OFFLINE: historico completo, particionado, para o join ponto-no-tempo.
# Confira os nomes exatos dos blocos aninhados na versao do provider AWS
# que voce usa -- este recurso evoluiu bastante entre versoes.
offline_store_config {
s3_storage_config {
s3_uri = "s3://${var.bucket_lake}/feature-store/media-pedidos-30d/"
kms_key_id = aws_kms_key.feature_store.arn
}
disable_glue_table_creation = false
}
tags = {
Projeto = var.projeto
Time = "dados"
Consome = "modelo-reposicao"
}
}
resource "aws_kms_key" "feature_store" {
description = "Chave unica para Online e Offline Store de features da Cadencia"
deletion_window_in_days = 30
enable_key_rotation = true
}
Habilitar os dois stores na mesma declaração não é estética
Se online_store_config e offline_store_config vivessem em dois recursos separados, criados em momentos diferentes por pessoas diferentes, seria fácil um deles ficar com uma definição de feature ligeiramente diferente da do outro — o mesmo erro estrutural que este módulo inteiro existe para fechar, só que dentro da própria declaração do Feature Group em vez de fora dela.
Construir: o job que calcula a feature uma única vez
A mudança que importa aqui não é a fórmula de média móvel — é o destino da ingestão. Um único DataFrame, um único ingest_data(), gravando nos dois stores no mesmo lote. Não existe um segundo caminho de código que alguém precisa lembrar de manter igual a este.
# calcular_feature_pedidos.py -- roda uma vez por dia; UNICA fonte de
# media_pedidos_30d para treino e inferencia.
import sys
from datetime import datetime, timezone
from awsglue.context import GlueContext
from awsglue.job import Job
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from pyspark.sql import functions as F, Window
from sagemaker_feature_store_pyspark.FeatureStoreManager import FeatureStoreManager
DATABASE = "cadencia_lake"
TABELA_PEDIDOS = "prata_pedidos"
FEATURE_GROUP_ARN = "arn:aws:sagemaker:sa-east-1:111122223333:feature-group/cadencia-media-pedidos-30d"
args = getResolvedOptions(sys.argv, ["JOB_NAME"])
sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args["JOB_NAME"], args)
# 1. Le TODO o historico de pedidos pelo catalogo -- a janela de 30 dias e
# calculada DENTRO do Spark, nao filtrada na leitura, para que a mesma
# consulta sirva qualquer dia de referencia sem reescrever SQL.
pedidos = glueContext.create_dynamic_frame.from_catalog(
database=DATABASE, table_name=TABELA_PEDIDOS,
transformation_ctx="fonte_prata_pedidos",
).toDF()
# 2. Janela de 30 dias CORRIDOS em UTC -- a mesma unidade de tempo que o
# resto do lake usa desde o L65. Esta e a UNICA definicao de janela que
# existe no sistema para esta feature.
janela_30d = Window.partitionBy("loja_id", "produto_id") \
.orderBy(F.col("dt").cast("timestamp").cast("long")) \
.rangeBetween(-30 * 86400, -86400) # ontem, olhando 30 dias pra tras
com_media = pedidos.withColumn(
"media_pedidos_30d", F.avg("qtd_pedidos").over(janela_30d)
)
agora = datetime.now(timezone.utc).isoformat()
registros = com_media.select(
F.concat_ws("#", "loja_id", "produto_id").alias("loja_sku_id"),
"loja_id", "produto_id", "media_pedidos_30d",
F.lit(agora).alias("calculado_em"),
).filter(F.col("media_pedidos_30d").isNotNull())
# 3. UMA UNICA ingestao grava Offline E Online no mesmo lote. Nao ha um
# segundo put_record para o Online Store em lugar nenhum deste job --
# e essa ausencia, nao um teste de comparacao, que fecha o skew.
gerenciador = FeatureStoreManager()
gerenciador.ingest_data(
input_data_frame=registros,
feature_group_arn=FEATURE_GROUP_ARN,
target_stores=["OnlineStore", "OfflineStore"],
)
print(f"ingeridos {registros.count()} pares loja+SKU em {agora}")
job.commit()
Escrever um segundo caminho de ingestão reabre exatamente este defeito
Se algum consumidor futuro decidir que precisa da feature mais rápido e escrever seu próprio put_record direto no Online Store, contornando este job, a garantia deste módulo inteiro desaparece — porque volta a existir mais de um lugar calculando ou escrevendo o mesmo valor. A regra não é "os dois stores estão sincronizados"; é "só existe UM caminho de escrita".
Construir: treino com junção ponto-no-tempo
O notebook de treino não escreve mais nenhuma consulta Athena para media_pedidos_30d. Ele pede ao SDK do Feature Store um dataset já reconciliado pelo tempo — e é essa reconciliação, não uma query manual, que impede o vazamento de dado futuro.
# montar_dataset_treino.py -- roda toda sexta-feira antes do SageMaker
# Training Job. Confira a versao do SDK antes de copiar: a API do
# DatasetBuilder mudou de assinatura entre versoes do sagemaker-python-sdk.
import sagemaker
from sagemaker.feature_store.feature_group import FeatureGroup
from sagemaker.feature_store.feature_store import FeatureStore
sessao = sagemaker.Session()
feature_store = FeatureStore(sagemaker_session=sessao)
grupo_pedidos = FeatureGroup(
name="cadencia-media-pedidos-30d", sagemaker_session=sessao,
)
# base_de_rotulos ja tem uma coluna 'evento_em' com o instante em que cada
# recomendacao de reposicao foi decidida -- e o ANCORAGEM temporal do join.
base_de_rotulos = sessao.default_bucket() + "/rotulos-reposicao/dt=2026-08-07/"
construtor = feature_store.create_dataset(
base=base_de_rotulos,
event_time_identifier_feature_name="evento_em",
record_identifier_feature_name="loja_sku_id",
output_path=f"s3://{sessao.default_bucket()}/datasets-treino/media-pedidos-30d/",
)
# .with_feature_group() adiciona a feature ao join; o metodo do proprio
# construtor faz o join PONTO NO TEMPO -- para cada rotulo, pega o valor de
# media_pedidos_30d com calculado_em mais recente ANTES de evento_em. Nunca
# um calculado_em posterior ao rotulo entra no dataset.
dataset, consulta_gerada = (
construtor
.with_feature_group(grupo_pedidos, target_feature_name_in_dataset="media_pedidos_30d")
.point_in_time_accurate_join()
.to_dataframe()
)
print(f"dataset de treino: {dataset.count()} linhas, sem vazamento de dado futuro")
print(f"consulta Athena gerada pelo builder:\n{consulta_gerada}")
O join ponto-no-tempo é sobre QUANDO, não sobre QUAL
Um join comum por loja_sku_id devolveria o valor de media_pedidos_30d MAIS RECENTE que existe hoje — inclusive se esse valor só passou a existir depois do rótulo de treino que ele está sendo colado. point_in_time_accurate_join() resolve uma pergunta diferente: qual era o valor válido NAQUELE instante do passado, e é essa pergunta que evita treinar com informação que o modelo nunca teria em mãos no momento real da decisão.
Construir: inferência lendo o Online Store em milissegundos
A API de reposição em .NET 8 troca o Lambda ad hoc por duas chamadas: GetRecord no Online Store e InvokeEndpoint no modelo — as duas com retry e timeout, porque nenhuma decisão de estoque pode travar a requisição do lojista esperando a AWS responder.
// RecomendarReposicaoHandler.cs -- le a MESMA feature que o treino usou,
// via Online Store, e nunca recalcula media_pedidos_30d na mao.
using Amazon.SageMakerFeatureStoreRuntime;
using Amazon.SageMakerFeatureStoreRuntime.Model;
using Amazon.SageMakerRuntime;
using Amazon.SageMakerRuntime.Model;
using Microsoft.Extensions.Http.Resilience;
using Polly;
namespace Cadencia.Reposicao;
public record PedidoDeReposicao(string LojaId, string ProdutoId, decimal PrecoVenda);
public class RecomendarReposicaoHandler
{
private readonly AmazonSageMakerFeatureStoreRuntimeClient _featureStore;
private readonly AmazonSageMakerRuntimeClient _endpoint;
private readonly ResiliencePipeline _resiliencia;
private const string FeatureGroup = "cadencia-media-pedidos-30d";
private const string EndpointModelo = "cadencia-modelo-reposicao";
public RecomendarReposicaoHandler(
AmazonSageMakerFeatureStoreRuntimeClient featureStore,
AmazonSageMakerRuntimeClient endpoint)
{
_featureStore = featureStore;
_endpoint = endpoint;
// Retry com backoff E jitter -- retry sem jitter transforma
// instabilidade em apagao sincronizado (assunto do L36).
_resiliencia = new ResiliencePipelineBuilder()
.AddRetry(new Polly.Retry.RetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
Delay = TimeSpan.FromMilliseconds(50),
})
.AddTimeout(TimeSpan.FromMilliseconds(200))
.Build();
}
public async Task<double> RecomendarAsync(PedidoDeReposicao pedido)
{
var lojaSkuId = $"{pedido.LojaId}#{pedido.ProdutoId}";
// 1. LE a feature -- nao recalcula. Se o registro nao existir ainda
// (SKU novo, sem 30 dias de historico), o fallback e decisao de
// produto, nao deste modulo: normalmente comeca em zero.
var registro = await _resiliencia.ExecuteAsync(async ct =>
await _featureStore.GetRecordAsync(new GetRecordRequest
{
FeatureGroupName = FeatureGroup,
RecordIdentifierValueAsString = lojaSkuId,
}, ct));
var mediaPedidos30d = registro.Record?
.FirstOrDefault(f => f.FeatureName == "media_pedidos_30d")
?.ValueAsString is { } valor ? double.Parse(valor) : 0.0;
// 2. Invoca o endpoint com a MESMA feature que o treino usaria para
// esta chave, nao uma reimplementacao da formula.
var payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(new
{
preco_venda = pedido.PrecoVenda,
media_pedidos_30d = mediaPedidos30d,
});
var resposta = await _resiliencia.ExecuteAsync(async ct =>
await _endpoint.InvokeEndpointAsync(new InvokeEndpointRequest
{
EndpointName = EndpointModelo,
ContentType = "application/json",
Body = new MemoryStream(payload),
}, ct));
using var leitor = new StreamReader(resposta.Body);
return double.Parse(await leitor.ReadToEndAsync());
}
// /health nunca toca a AWS -- so confirma que o processo esta de pe.
// /ready confirma Feature Store e endpoint alcancaveis; sao perguntas
// diferentes, e confundi-las derruba a instancia sadia quando a AWS oscila.
public bool Health() => true;
public async Task<bool> ReadyAsync()
{
try
{
await _featureStore.GetRecordAsync(new GetRecordRequest
{
FeatureGroupName = FeatureGroup,
RecordIdentifierValueAsString = "sonda#sonda",
});
return true;
}
catch (ResourceNotFoundException)
{
// registro de sonda nao existe -- e RESPOSTA valida, o servico
// alcancou o Feature Store com sucesso.
return true;
}
catch
{
return false;
}
}
}
Nenhuma fórmula de janela de 30 dias existe neste arquivo
É a ausência que prova a arquitetura: RecomendarReposicaoHandler não sabe o que é uma janela de 30 dias, UTC ou horário de Brasília. Ele só sabe pedir um registro por chave. Toda a lógica de cálculo mora num único lugar — o Glue job da seção anterior — e é impossível a API reimplementá-la torto, porque ela nunca implementa nada.
Implantar, e provar que o skew caiu a zero
Quatro provas. A terceira é a que fecha o argumento inteiro: medir o mesmo par loja+SKU nos dois stores, no mesmo instante, e mostrar que a diferença é zero — não "pequena", zero.
# provas.sh -- quatro medicoes; nenhuma conclusao vem de "o job rodou"
PROJETO=cadencia; GRUPO=cadencia-media-pedidos-30d; LOJA=L07; SKU=CIM-0042
# --- Prova 1: skew ANTES da correcao (arquitetura minima), medido de proposito
# Reexecuta as duas consultas divergentes do reproduzir-o-defeito.sh (secao 4)
# para o mesmo par, e calcula a diferenca percentual.
# Resultado medido em 2026-08-07: offline=14.2, online=17.9 -> skew de 25.8%
# nesse par; a media amostral sobre 50 pares aleatorios foi de 4.2%, com pico
# de 31% em SKUs de baixo volume (poucos pedidos tornam a media sensivel a 1
# pedido de diferenca entre as janelas UTC e America/Sao_Paulo).
# --- Prova 2: skew DEPOIS da correcao, mesmo par, mesmo instante ------------
aws sagemaker-featurestore-runtime get-record \
--feature-group-name ${GRUPO} \
--record-identifier-value-as-string "${LOJA}#${SKU}" \
--query "Record[?FeatureName=='media_pedidos_30d'].ValueAsString" --output text
aws athena start-query-execution \
--query-string "SELECT media_pedidos_30d FROM cadencia_lake.\"cadencia-media-pedidos-30d-offline\" \
WHERE loja_sku_id='${LOJA}#${SKU}' ORDER BY calculado_em DESC LIMIT 1" \
--result-configuration OutputLocation=s3://cadencia-lake/atenas-resultados/
# Esperado: os dois comandos devolvem o MESMO numero, porque vieram da MESMA
# ingestao. Skew = 0% -- nao ha mais dois calculos para divergirem.
# --- Prova 3: latencia de leitura do Online Store, no p99 -------------------
for i in $(seq 1 100); do
/usr/bin/time -f "%e" aws sagemaker-featurestore-runtime get-record \
--feature-group-name ${GRUPO} \
--record-identifier-value-as-string "${LOJA}#${SKU}" \
--output text > /dev/null
done 2>&1 | sort -n | tail -1
# Esperado: abaixo de 10 ms no p99 -- o limiar que a secao de requisitos exige
# para nao atrasar a resposta da API de reposicao.
# --- Prova 4: contagem de registros ingeridos bate nos dois stores ----------
aws athena start-query-execution \
--query-string "SELECT COUNT(*) FROM cadencia_lake.\"cadencia-media-pedidos-30d-offline\" \
WHERE date(calculado_em) = current_date" \
--result-configuration OutputLocation=s3://cadencia-lake/atenas-resultados/
# Compare com o log do Glue job: "ingeridos N pares loja+SKU". Os dois numeros
# tem de bater -- diferenca aqui indica falha PARCIAL na ingestao, nao skew.
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · Skew medido antes da correção | reexecutar as duas consultas divergentes do b4 | skew médio de 4,2% (pico de 31% em SKU de baixo volume) — o número que justificou este módulo | skew menor que isso indica que o cenário de reprodução não capturou a divergência real de janela/fuso |
| 2 · Skew depois da correção | get-record no Online Store vs. consulta no Offline Store | os dois valores idênticos, skew de 0% | qualquer diferença aqui significa que existe um SEGUNDO caminho de escrita — volte à seção 9 e procure um put_record fora do Glue job |
| 3 · Latência de leitura no p99 | 100 chamadas de GetRecord cronometradas | abaixo de 10 ms no p99 | latência maior sugere Online Store sem capacidade suficiente, ou uma chamada de rede desnecessária entre a API e o Feature Store |
| 4 · Contagem de registros ingeridos | COUNT no Offline Store vs. log do Glue job | os dois números batem | número menor no Offline Store indica falha parcial de ingestão — parte do lote não chegou a um dos dois stores |
Quebrar de propósito: três falhas e o diagnóstico
As três se parecem no sintoma superficial — "a recomendação de reposição está estranha". O que separa uma da outra é ONDE o defeito mora: na ingestão, no join de treino, ou numa suposição sobre o que o Feature Store garante sozinho.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Glue job falha na ingestão do Online Store, mas não no Offline | revogue a permissão de escrita no Online Store do role do job e rode uma ingestão | o Offline Store recebe o lote normalmente, mas GetRecord no Online Store devolve o valor do dia anterior | log do Glue job (a chamada ingest_data levanta exceção parcial); CloudWatch Logs do job | corrigir a policy IAM do role de ingestão e reprocessar o dia — a ingestão é idempotente por design do conector |
| Alguém cria um segundo caminho de escrita direto no Online Store | publique um put_record manual para um par loja+SKU, com um valor diferente do que o Glue job calcularia | o skew volta a existir, mesmo com o Feature Group configurado corretamente | CloudTrail em PutRecord no Feature Store, filtrando por origem que não é o role do Glue job | remover o caminho de escrita paralelo; a regra deste módulo não é "os dois stores concordam", é "só existe um escritor" |
| Treino usa join simples por chave em vez de ponto-no-tempo | troque point_in_time_accurate_join() por um merge comum por loja_sku_id no notebook | acurácia offline sobe de forma suspeita, porque o treino passa a ver o valor MAIS RECENTE da feature, inclusive para rótulos antigos | revisar o código do DatasetBuilder no notebook; comparar a acurácia com a de um treino que usa o join correto | restaurar o join ponto-no-tempo — acurácia mais alta obtida assim é vazamento de dado futuro, não modelo melhor |
A pergunta que resolve metade destes casos
Antes de mexer em qualquer parâmetro, pergunte: o valor está errado porque a INGESTÃO falhou, ou porque alguém está LENDO de um jeito que o Feature Store não garante sozinho (join sem tempo, escrita fora do Glue job)? São duas investigações diferentes — uma termina no CloudWatch Logs do job, a outra numa revisão de código de quem consome a feature.
O modelo de reposição da Cadência tem 90% de acurácia no notebook de treino, usando media_pedidos_30d calculada por uma consulta Athena. Em produção, a mesma coluna é calculada por um Lambda que consulta pedidos_recentes no DynamoDB, com janela em horário de Brasília. Por que a recomendação de reposição erra tanto em produção, mesmo com o modelo sendo o mesmo?
Segurança: um store a mais é uma superfície a mais
O Online Store existe justamente para ser lido rápido por qualquer coisa que souber a chave — o que também é o seu maior risco: sem role dedicada por consumidor, qualquer serviço com uma credencial genérica de SageMaker consegue ler media_pedidos_30d de qualquer loja.
| Risco | Probabilidade | Impacto | Controle preventivo | Detecção | Resposta |
|---|---|---|---|---|---|
| Role genérica de SageMaker usada para ler o Online Store de qualquer feature group | média | médio | role dedicada por consumidor (ingestão, treino, inferência), cada uma restrita ao ARN do Feature Group específico | CloudTrail em GetRecord filtrando por identidade que não é a role esperada | revisar e restringir a policy; reemitir credenciais do consumidor certo |
| Segundo escritor no Online Store, contornando o Glue job | baixa | alto | só o role de ingestão do Glue job tem permissão de PutRecord no Feature Group; qualquer outro escritor é bloqueado por IAM | CloudTrail em PutRecord com origem diferente do role de ingestão | revogar a credencial usada, reingestão completa do par afetado a partir do lake |
| Offline Store e Online Store com chaves KMS diferentes, dificultando auditoria | baixa | médio | uma única CMK para os dois stores, declarada no mesmo Feature Group | revisão do Terraform state; chave divergente aparece no plano antes do apply | recriar o Feature Group com a chave correta — trocar a chave de um store existente exige reingestão |
| Feature Group revisado sem quem entende o modelo consumidor | média | alto | exigir aprovação do time de dados de ML como reviewer obrigatório do PR que toca feature-group.tf | diff de PR sem o reviewer certo, via regra de proteção de branch | reverter o PR e reabrir com o reviewer correto antes de reaplicar |
| Preço e volume de pedido por loja expostos a quem não deveria ver o Feature Group | baixa hoje, cresce com mais consumidores | médio | este módulo não resolve — é o L69, com Lake Formation, aplicado também ao Feature Store | consulta de auditoria do Lake Formation, quando existir para este recurso | aplicar permissão fina antes de liberar novo consumidor ao Feature Group |
media_pedidos_30d revela volume de venda por loja — trate como dado de negócio
Assim como preco_venda no L70, a média de pedidos por loja e SKU é informação comercial sensível: um concorrente com acesso ao Online Store saberia exatamente quanto cada loja parceira vende de cada produto. A role de leitura do endpoint de inferência tem permissão só sobre este Feature Group específico — nunca uma policy ampla de "leitura de qualquer feature da conta", que é o atalho que parece inofensivo até o segundo modelo chegar.
Observabilidade: as perguntas que o painel tem de responder
Um painel deste Feature Group tem função estreita: dizer se o valor que a inferência está lendo agora é o mesmo que o treino leria para a mesma chave. Métrica que não ajuda essa pergunta pertence a outro painel.
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| O skew entre Offline e Online está em zero? | job agendado compara amostra de pares loja+SKU nos dois stores, publica diferença percentual no CloudWatch | skew acima de zero indica um segundo caminho de escrita ou uma ingestão parcial | acima de 0% em qualquer amostra — aqui a régua não é "pequeno é aceitável" |
| A ingestão diária está completa? | COUNT no Offline Store do dia vs. contagem logada pelo Glue job | divergência indica falha parcial de escrita em um dos dois stores | qualquer diferença maior que zero |
| A latência de leitura do Online Store está dentro do orçamento? | p50/p95/p99 de GetRecord, medido pelo cliente .NET | aumento de latência pressiona o tempo de resposta da API de reposição inteira | p99 acima de 10 ms |
| Há pares loja+SKU sem feature calculada? | COUNT de SKUs ativos sem registro correspondente no Online Store | SKU novo, sem 30 dias de histórico, é esperado; SKU antigo sem registro é falha de ingestão | qualquer SKU com mais de 30 dias de operação sem registro |
| O treino mais recente usou join ponto-no-tempo? | revisão do log de execução do DatasetBuilder no experimento de treino | ausência de point_in_time_accurate_join() no log indica vazamento de dado futuro potencial | qualquer execução sem essa chamada registrada |
A métrica que engana neste pipeline
Ingestão bem-sucedida (job termina SUCCEEDED) não significa skew zero — significa que o lote foi ESCRITO nos dois stores. Se existir um segundo caminho de escrita em algum lugar do sistema, ele pode sobrescrever o Online Store depois da ingestão do dia, e o painel de sucesso do job mostraria tudo verde enquanto o valor lido pela API já diverge de novo.
Escala: 310 SKUs, 3.100 SKUs, e o que passa a doer
| Volume | O que acontece | O que passa a doer | O que fazer |
|---|---|---|---|
| 40 lojas, 310 SKUs, 8.200 pedidos/dia (Cadência hoje) | ingestão diária e leitura em tempo real correm sem esforço | nada | nada |
| 400 lojas (10×) | mais pares loja+SKU no Offline Store; mesma lógica de cálculo | throughput do Online Store pode saturar em picos de checkout, se ficou em modo sob demanda | avaliar throughput provisionado para o Online Store, dimensionado pelo pico, não pela média |
| Uma feature nova por mês (ex.: tempo médio de entrega por loja) | novo Feature Group ou nova coluna no existente, cada um com sua própria ingestão | governança de quais features existem, quem é dono, e se duas features não estão medindo quase a mesma coisa | catálogo de features com dono declarado, revisado trimestralmente — o mesmo problema de governança do ruleset do L70 |
| Mais de um modelo consumindo a mesma feature (ex.: previsão de cancelamento de pedido) | o mesmo Feature Group serve dois modelos diferentes | mudar a definição da feature para um modelo pode quebrar silenciosamente o outro | versionar a definição da feature; comunicar mudança a todos os donos de modelo antes de alterar a fórmula |
| Falha de AZ | SageMaker Feature Store é regional e gerenciado; o Online Store é replicado pela AWS sem operação manual | nada específico deste módulo — a garantia de disponibilidade é do serviço gerenciado, não de uma configuração deste laboratório | confirmar no SLA do serviço antes de assumir Multi-AZ implícito |
| Latência do Online Store degradando sob carga | GetRecord começa a ultrapassar o p99 de 10 ms nos horários de pico | a API de reposição pode atrasar a resposta ao lojista | medir throughput real antes de assumir que o modo sob demanda escala para 10× o volume atual |
O gargalo que só aparece com muitos modelos, não com muitos pedidos
O volume de pedidos escala razoavelmente bem — é a governança de MUITAS features compartilhadas por MUITOS modelos que fica difícil primeiro. Sem dono declarado por feature, o Feature Store vira um catálogo que ninguém mais entende de cabeça, exatamente como o ruleset do L70 sem revisão do time de negócio.
Custo: o que este laboratório acrescenta à fatura
O Offline Store em si é barato — é basicamente S3. O que pesa é o throughput do Online Store, que antes deste módulo não existia.
| Cenário | Volume | O que acrescenta | Tendência | Otimização |
|---|---|---|---|---|
| Piloto | 1 Feature Group, poucas centenas de registros | throughput mínimo do Online Store, Offline Store desprezível | desprezível | nenhuma |
| Produção pequena | Cadência hoje: 40 lojas × 310 SKUs de pares ativos | throughput sob demanda do Online Store cobrindo leituras da API de reposição | baixa e previsível — a leitura é por chave única, barata por natureza | sob demanda em vez de provisionado, enquanto o tráfego não tem pico regular |
| Alta escala | 400 lojas, milhares de SKUs, múltiplos modelos consumindo | throughput provisionado dimensionado pelo pico de checkout, mais Offline Store crescendo com o histórico | cresce com o número de MODELOS consumidores, não só com o volume de pedidos | revisar throughput provisionado trimestralmente contra o tráfego real medido |
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| Online Store | throughput de leitura/escrita (provisionado ou sob demanda) | é a variável que mais pesa; meça o padrão real de tráfego antes de provisionar acima do necessário |
| Offline Store | armazenamento S3 e requisições, como qualquer prefixo do lake | cresce indefinidamente com o histórico; considere ciclo de vida para dados de treino muito antigos |
| Glue job de ingestão | DPU-hora, proporcional a workers × duração | roda uma vez por dia sobre um volume pequeno — desprezível frente ao restante da fatura |
| SageMaker Training Job semanal | instância de treino por hora | inalterado por este módulo — a mudança é na feature, não no tamanho do treino |
O custo que este módulo evita, e que não aparece em nenhuma fatura da AWS
Recomendações de reposição erradas por semanas, alimentadas por uma feature que nunca foi a mesma nos dois lados, custaram à Cadência decisões de estoque sobre um sinal que parecia bom e não era — capital parado ou faltando no lugar errado, de novo. Esse custo nunca teve linha na fatura da AWS, e é maior que qualquer throughput de Online Store.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | feature calculada em um único lugar, ingerida automaticamente todo dia nos dois stores | segundo caminho de escrita criado sem revisão reabre o skew silenciosamente | regra de CloudTrail alertando sobre PutRecord de origem diferente do role de ingestão | alta |
| Segurança | role dedicada por consumidor, chave KMS única cifrando os dois stores | Feature Group revela volume de venda por loja a qualquer consumidor com a permissão certa | aplicar Lake Formation sobre o Feature Store quando houver mais de um time consumidor (tema do L69) | média |
| Confiabilidade | ingestão idempotente; skew medido continuamente, não presumido | ingestão parcial (um store recebe, o outro não) pode passar despercebida sem a prova 4 rodando com frequência | alarme automático sobre a métrica de contagem divergente entre Offline e Online | alta |
| Eficiência de performance | Online Store dedicado à leitura de milissegundos; Offline dedicado a histórico completo | throughput sob demanda pode não escalar sem aviso em pico inesperado de checkout | medir throughput real antes de assumir que sob demanda cobre 10× o volume atual | média |
| Otimização de custos | Offline Store reaproveita o mesmo bucket do lake; sem cluster novo além do Glue existente | throughput provisionado dimensionado por adivinhação, não por medição, desperdiça capacidade | revisar throughput provisionado contra métrica real de GetRecord, trimestralmente | média |
| Sustentabilidade | um único cálculo por dia, reaproveitado por todos os consumidores em vez de recalculado por cada um | Feature Groups órfãos, de modelos descontinuados, continuam ingerindo sem necessidade | revisão periódica de Feature Groups sem modelo consumidor ativo | baixa |
Evolução em níveis: o que muda, e o que passa a doer
A terceira arquitetura não é um desenho: é a resposta a QUANDO trocar de desenho. Cada nível resolve um risco deste módulo e compra outro — até o ponto em que o próprio DADO que produziu um modelo específico passa a ser rastreável.
Notebook com consulta Athena e Lambda ad hoc calculam "a mesma" feature por caminhos diferentes — é onde a Cadência estava até este módulo entrar no ar.Um Glue job calcula a feature uma vez, ingere Offline e Online Store no mesmo lote; treino lê com join ponto-no-tempo, inferência lê por GetRecord.Quando um segundo modelo (por exemplo, previsão de cancelamento de pedido) também precisa de "pedidos dos últimos 30 dias", ele lê o mesmo Feature Group em vez de recalcular.O Glue job de cálculo passa a rodar dentro de um pipeline disparado por dado novo, com cache de passo e linhagem — não mais um agendamento independente que alguém pode esquecer de monitorar.Antes de promover uma nova versão do modelo de reposição — treinada sobre a mesma feature — para produção, há aprovação e caminho de rollback declarados.O modelo em produção passa a ser reproduzível a partir do commit do código de treino e da versão exata do dataset que o Feature Store gerou — não só a feature está certa, a ORIGEM dela é auditável (L73).A ordem não é negociável, e o motivo é concreto
Rastrear experimento e dado (nível 6) sobre uma feature que ainda diverge entre treino e inferência (nível 1) reproduziria fielmente um modelo construído sobre um defeito — o commit certo, o dataset certo, mas a fórmula errada em produção continuaria errada. O cálculo único deste módulo é o que torna a reprodutibilidade do L73 uma garantia sobre um sistema correto, não uma fotografia detalhada de um sistema quebrado.
Onde IA entra nesta arquitetura, e onde não entra
A decisão "vale a pena usar um modelo para recomendar reposição" já foi tomada — é o tema do L71, e este módulo não a reabre. O problema que ESTE módulo resolve — "os dois caminhos leem o mesmo número?" — tem resposta de engenharia determinística: um único cálculo, servido dos dois lados. Não é uma pergunta que um modelo responde melhor que uma arquitetura correta.
Há um lugar próximo onde um sinal estatístico agrega de verdade, mas não é aqui — é o L77, com SageMaker Model Monitor. Lá a pergunta é diferente: mesmo com a feature calculada de forma idêntica nos dois lados (o que este módulo garante), a DISTRIBUIÇÃO de media_pedidos_30d pode se deslocar ao longo do tempo — mais pedidos numa Black Friday, menos numa baixa temporada — e isso é desvio estatístico legítimo, não bug de engenharia. Detectar isso é um problema diferente do que este módulo resolve.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria aqui, além do modelo que o L71 já validou? | nenhum — "os dois caminhos leem o mesmo valor" é uma pergunta de engenharia de dados, respondida por arquitetura (cálculo único), não por mais um modelo |
| Por que não usar um modelo para detectar quando os dois caminhos divergem? | porque comparar dois números e calcular a diferença percentual é aritmética simples, mais rápida, mais barata e mais auditável do que treinar algo para aprender a mesma comparação |
| Onde um sinal estatístico entraria de fato neste tema? | no L77: detectar desvio gradual e legítimo na distribuição da feature ao longo do tempo — problema diferente de skew estrutural entre dois cálculos |
| Qual o risco de tratar divergência de engenharia como se fosse drift estatístico? | mascarar um bug real (segundo caminho de escrita, ingestão parcial) como se fosse variação natural do negócio — e nunca corrigir a causa |
O uso de IA que parece atraente e é armadilha aqui
Treinar um classificador para "prever" quando a feature de treino e a de inferência provavelmente divergiram, em vez de eliminar a possibilidade estrutural de divergência, trocaria uma garantia arquitetural (um único cálculo) por uma estimativa probabilística sobre o próprio defeito que deveria ter sido corrigido na origem. O Feature Store já resolve isso de forma verificável — IA aqui esconderia o problema, não o resolveria.
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Por que é problema | Sintoma em produção | Forma correta | Quando é aceitável |
|---|---|---|---|---|---|
| Calcular a feature duas vezes — uma no notebook, outra no serviço de inferência | parece mais rápido no começo: cada time escreve sua própria consulta sem esperar o outro nem negociar um contrato de feature | as duas implementações divergem em janela, fuso ou fonte de dado sem que ninguém compare os resultados — é exatamente o defeito que abre este módulo | acurácia boa no notebook, recomendação ruim em produção, com o nome da feature idêntico nos dois lados | um único cálculo, ingerido nos dois stores de um Feature Group pelo mesmo job | protótipo de um dia, descartado antes de qualquer decisão real depender dele |
| Usar só o Online Store, inclusive para montar o dataset de treino | parece mais simples manter um único ponto de leitura para tudo | Online Store guarda só o valor MAIS RECENTE por chave — um treino que lê dali para rótulos antigos usa o valor de hoje para prever o passado, vazamento de dado futuro garantido | acurácia offline artificialmente alta que não se sustenta em produção | Offline Store com join ponto-no-tempo para tudo que é treino; Online Store só para leitura em tempo real | nunca para treino — é a distinção central que a certificação cobra sobre os dois stores |
| Ingerir só no Offline Store e simular baixa latência com um cache manual por cima | parece evitar o custo do Online Store | o cache manual é uma TERCEIRA implementação da mesma feature, com sua própria lógica de expiração — reabre a possibilidade de divergência que este módulo existe para fechar | cache servindo valor desatualizado enquanto o Offline Store já tem um mais recente | Online Store gerenciado, que já resolve baixa latência sem lógica de cache própria para manter | protótipo sem tráfego real, onde a diferença de latência nunca é sentida por ninguém |
| Recalcular a feature em tempo real a cada request, achando que Lambda resolve | parece eliminar a necessidade de um store, calculando tudo na hora | além de lento (consulta ao histórico completo a cada request), reintroduz exatamente a chance de essa lógica divergir da usada no treino, meses depois, quando outra pessoa mexer no código | latência alta na API de reposição, e risco de skew voltando silenciosamente com qualquer mudança futura no Lambda | cálculo em lote, uma vez, ingerido no Feature Store — a inferência só lê | feature que muda a cada segundo e não tolera nenhum atraso de ingestão — caso raro, e mesmo assim vale avaliar streaming ingestion antes de recalcular na inferência |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Recomendação de reposição errada mesmo depois do Feature Store implantado | modelo em produção foi treinado ANTES da correção, sobre a feature antiga divergente | comparar a data do último treino com a data em que o Feature Group entrou no ar | metadado de treino do SageMaker (experiment tracking) | forçar retrain sobre o dataset montado com point_in_time_accurate_join() |
| GetRecord devolve None para um par loja+SKU que deveria existir | SKU sem 30 dias de histórico ainda (esperado) ou falha de ingestão (não esperado) | verificar há quantos dias o SKU está ativo; se mais de 30, checar log do Glue job daquele dia | CloudWatch Logs do Glue job, filtrando pela data em questão | se for falha de ingestão, reprocessar o dia; se for SKU novo, o comportamento é esperado até completar a janela |
| Skew volta a aparecer depois de meses em zero | novo caminho de escrita foi introduzido, contornando o Glue job de ingestão | CloudTrail em PutRecord no Feature Group, filtrando por identidade diferente do role de ingestão | IAM policy do Feature Group; histórico de PRs recentes em código que toca o Feature Store | remover o segundo escritor; restringir a permissão de PutRecord a uma única role |
| Treino demora muito mais do que antes para montar o dataset | join ponto-no-tempo sobre um Offline Store que cresceu sem particionamento revisado | medir o tempo da consulta Athena gerada pelo DatasetBuilder isoladamente | plano de execução da consulta no console do Athena | revisar particionamento do Offline Store; considerar reduzir a janela de histórico consultada se o treino não precisa de tudo |
| Latência de GetRecord acima do esperado só em determinados horários | throughput sob demanda saturando no pico de checkout | correlacionar o horário da latência alta com o volume de pedidos por hora | métrica de throughput do Online Store no CloudWatch | mudar para throughput provisionado, dimensionado pelo pico medido |
A pergunta que resolve metade destes casos
Antes de mexer em qualquer parâmetro, pergunte: o modelo em produção foi treinado ANTES ou DEPOIS deste Feature Group existir? Um modelo antigo continua carregando o skew antigo até ser retreinado — implantar a arquitetura corrige o CAMINHO, não retroage sobre um modelo já publicado.
Limpeza: o que o destroy não leva
Este laboratório cria um recurso que sobrevive por conta própria mesmo depois do Terraform destruir o Feature Group: os arquivos que o Offline Store já gravou no S3.
#!/usr/bin/env bash
# limpar.sh -- o que o destroy nao leva, e o que continua cobrando
set -euo pipefail
PROJETO="${PROJETO:?defina PROJETO}"
# 1. Derrube o que o Terraform administra (Feature Group, chave KMS, roles).
terraform destroy -auto-approve
# 2. O Feature Group some do SageMaker com o destroy; confirme.
aws sagemaker describe-feature-group --feature-group-name "${PROJETO}-media-pedidos-30d" 2>&1 \
| grep -q "ResourceNotFound" && echo "feature group removido, ok"
# 3. O ARQUIVO PARQUET do Offline Store NAO e removido pelo destroy do
# Feature Group -- ele mora no bucket do lake, fora do controle do recurso.
aws s3 ls "s3://cadencia-lake/feature-store/media-pedidos-30d/" --recursive --summarize \
| tail -2
# Decida: manter para auditoria de qual dado treinou modelos passados, ou:
aws s3 rm "s3://cadencia-lake/feature-store/media-pedidos-30d/" --recursive
# 4. A TABELA DO GLUE CATALOG criada automaticamente para o Offline Store
# tambem sobrevive, se disable_glue_table_creation nao foi usado.
aws glue delete-table --database-name cadencia_lake \
--name "${PROJETO}-media-pedidos-30d-offline" || true
# 5. Prova final: nada com o nome do projeto de pe fora do esperado.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Projeto,Values="${PROJETO}" \
--query "ResourceTagMappingList[].ResourceARN" --output table
| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Feature Group (definição, Online Store) | sim | não | recurso gerenciado; sem definição, não há o que cobrar |
| Chave KMS | sim, com janela de exclusão de 30 dias | sim, até a janela terminar | AWS exige período de retenção antes de apagar chave definitivamente |
| Arquivos Parquet do Offline Store no S3 | não — não pertence ao recurso do Feature Group | sim, GB-mês | decisão editorial: manter para auditoria de treinos passados, ou apagar — este módulo não decide por você |
| Tabela do Glue Catalog do Offline Store | não, a menos que apagada explicitamente | não diretamente, mas confunde quem consulta depois | criada automaticamente pelo Feature Group; sobrevive à exclusão dele se não for removida à parte |
| Roles IAM adicionais (ingestão, treino, inferência) | sim, se em Terraform | não | IAM não cobra por si só, mas role órfã sobrevivendo é risco de auditoria, não de fatura |
Apagar o Feature Group não apaga o que ele já ensinou aos modelos treinados
Remover aws_sagemaker_feature_group do Terraform tira a definição do ar, mas modelos já publicados continuam com os pesos que aprenderam sobre o dado que passou por ali — apagar a infraestrutura sem registrar qual modelo dependeu de qual versão da feature é perder rastreabilidade, não só economizar armazenamento.
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Duas implementações da mesma feature divergem sem que ninguém compare | cálculo único, dentro de um Glue job | elimina a possibilidade ESTRUTURAL de divergência — não depende de disciplina de sincronização |
| Treino não pode ver dado que ainda não existia no momento do rótulo | Offline Store com join ponto-no-tempo pelo EventTime | a lacuna que um join simples por chave deixa aberta, e que causa vazamento de dado futuro |
| Inferência em tempo real não pode recalcular a feature do zero a cada request | Online Store com leitura por GetRecord | latência de milissegundos, sem lógica de cálculo duplicada na API |
| O skew precisa ser medido, não presumido | job agendado comparando amostra Offline vs. Online, publicado no CloudWatch | prova contínua de que os dois lados continuam iguais, não só no dia do deploy |
| Dado de pedido e preço por loja é sensível (herdado do L70) | chave KMS única, role dedicada por consumidor | acesso restrito por quem realmente precisa ler cada Feature Group |
| Modelo treinado sobre feature divergente aprende um sinal que não existe em produção | modelo lê a mesma feature nos dois lados, garantidamente | a melhor forma de eliminar skew é impedir que ele possa existir, não detectá-lo depois |
| Reprodutibilidade do modelo — qual dado e qual commit geraram a versão em produção | não resolvido neste módulo | risco residual aceito e documentado; é o assunto do L73 |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| Feature calculada duas vezes, com valores diferentes | cálculo único ingerido nos dois stores pelo mesmo job | segundo caminho de escrita criado por engano ou pressa, contornando o Feature Group |
| Vazamento de dado futuro no treino | join ponto-no-tempo pelo EventTime no Offline Store | EventTime preenchido errado na ingestão — o join confia no valor que recebeu |
| Latência alta na inferência | Online Store dedicado a leitura por chave em milissegundos | throughput sub-dimensionado para um pico de tráfego real, não medido antes |
| Acesso indevido a dado de negócio sensível | role dedicada por consumidor, chave KMS única | é o L69 inteiro, com Lake Formation, para governança fina por coluna |
| Modelo em produção impossível de reproduzir | nada neste módulo | é o L73 inteiro, com experimento e dataset versionados |
- Checkout confirma o pedido e grava em prata/pedidos/dt= (resolvido pelo L70).
- Uma vez por dia, o Glue job lê o histórico e calcula media_pedidos_30d com uma única fórmula.
- O conector Spark do Feature Store ingere o resultado no Offline Store e no Online Store, no mesmo lote.
- Toda sexta-feira, o treino monta o dataset com join ponto-no-tempo pelo EventTime.
- A cada verificação de estoque, a API de reposição lê o Online Store por GetRecord.
- A mesma feature entra no payload de InvokeEndpoint que o treino usaria para aquela chave.
- Um job agendado compara amostras Offline vs. Online e publica o skew no CloudWatch.
- O skew medido caiu de uma média de 4,2% (pico de 31%) para 0%.
Perguntas frequentes
❓ Por que o modelo tinha 90% de acurácia no notebook e errou tanto em produção?
❓ SageMaker Feature Store substitui a decisão de usar ML tomada no L71?
❓ Por que não bastaria escrever um teste comparando as duas implementações antigas?
❓ Por que o treino usa join ponto no tempo em vez de um join simples por chave?
❓ O Online Store substitui o DynamoDB que o checkout já usa para outras coisas?
❓ Preciso do SageMaker Pipelines para este Feature Group funcionar?
❓ Quanto tempo leva para uma mudança na feature aparecer no Online Store?
Fixando
Na arquitetura de produção, um único Glue job calcula media_pedidos_30d e chama ingest_data() com target_stores=["OnlineStore", "OfflineStore"]. O que essa chamada única garante que duas chamadas separadas de put_record — uma para cada store — não garantiriam da mesma forma?
O notebook de treino usa point_in_time_accurate_join() do DatasetBuilder para juntar os rótulos de reposição com media_pedidos_30d do Offline Store. Se essa chamada fosse trocada por um merge comum por loja_sku_id, o que aconteceria com um rótulo de treino de três meses atrás?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L70 (Glue Data Quality, contrato e quarentena) — prata_pedidos e prata_produtos já chegam validados; tema do L71 (SageMaker AI, baseline de regra vs. modelo) — a decisão de usar ML já foi tomada |
| Conhecimentos adquiridos | diferença entre Online Store e Offline Store; join ponto-no-tempo pelo EventTime; ingestão única servindo dois destinos; skew de treino/inferência como defeito estrutural, não estatístico |
| Limitação que fica | o modelo em produção não é reproduzível a partir do commit e do dataset exato que o gerou — é o L73 quem trata disso |
| Próximo exemplo recomendado | L73 — Treinar no SageMaker AI com experimento rastreável. Reutiliza o dataset montado com join ponto-no-tempo deste módulo, agora com o experimento, o hiperparâmetro e o dado versionados até o commit |
| Também habilitado por este módulo | L77 (drift: descobrir antes do negócio reclamar) passa a medir desvio estatístico LEGÍTIMO na distribuição da feature — só é um sinal limpo porque este módulo já eliminou a divergência de engenharia que confundiria a leitura |
| Data da última validação técnica | 8 de agosto de 2026 |
Documentação oficial consultada: Amazon SageMaker Feature Store — Online Store e Offline Store, e o conector Spark de ingestão (sagemaker-feature-store-pyspark); SageMaker Python SDK — DatasetBuilder e point_in_time_accurate_join(); AWS provider Terraform — o resource aws_sagemaker_feature_group; AWS SDK for .NET — Amazon.SageMakerFeatureStoreRuntime e Amazon.SageMakerRuntime. Nenhum valor de preço aparece neste módulo por decisão: use o AWS Pricing Calculator, porque preço varia por região e envelhece mais rápido que o conteúdo.
O que não foi verificado, e você deve conferir na sua conta
A assinatura exata de create_dataset() e point_in_time_accurate_join() no SageMaker Python SDK evoluiu entre versões — confira a referência atual antes de copiar o código de montar_dataset_treino.py linha a linha, como o comentário no código já avisa. Os blocos aninhados de aws_sagemaker_feature_group também variam por versão do provider Terraform da AWS. Os volumes da Cadência — 40 lojas, 310 SKUs, 8.200 pedidos/dia, skew de 4,2% — são os do cenário de exemplo; meça os seus antes de copiar um limiar de alarme.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…