Do ponto de venda ao centro de distribuição — um pipeline de dados para decidir quanto repor, onde e quando, na linha high-end OPPO da rede Claro.
Toda semana, alguém precisa responder essa pergunta — loja a loja, modelo a modelo.
Ruptura. Loja sem produto, venda perdida, cliente migra para o concorrente.
Capital parado. Estoque de giro lento imobilizado na loja errada.
Hoje essa decisão depende de planilhas manuais — difíceis de auditar e de repetir toda semana com os mesmos critérios.
| Tier | Perfil | Cobertura alvo (DOS) |
|---|---|---|
| S | Loja de maior potencial | 30 dias |
| A | Loja de potencial alto | 30 dias |
| B | Loja de potencial padrão | 21 dias |
A dimensão cluster OPPO define quais modelos cada loja pode vender (flags OPPO 1…OPPO 4, NOT CLUSTER no cadastro de produtos). Uma loja do cluster 4 não é candidata a receber um modelo reservado ao cluster 2.
DOS (Days of Supply) — quantos dias o estoque atual sustenta o ritmo de vendas da loja.
Simples na teoria — a dificuldade real está em medir velocidade de vendas de forma confiável.
Loja fica 3 dos últimos 7 dias sem estoque, e ainda assim vende 4 unidades:
O pipeline sempre calcula velocidade por dia com produto disponível na prateleira. Loja sem estoque não é loja sem demanda.
Loja nova, modelo recém-lançado, ou poucos dias com estoque na janela — a amostra não é confiável.
Evita que uma loja com 1 dia de estoque e 1 venda vire “vende 1 unidade por dia” — erro clássico de extrapolação com amostra pequena.
O time ajusta tiers, cobertura alvo ou pesos sem tocar em lógica Python.
tiers_doi:
S: 30
A: 30
B: 21
peso_vel_a: 0.6 # peso da velocidade de 7 dias (curto prazo, reativo)
peso_vel_b: 0.4 # peso da velocidade de 30 dias (estável, menos ruído)
min_dias_disponiveis_a: 5 # mínimo de dias com estoque p/ confiar na janela de 7d
min_dias_disponiveis_b: 21 # idem, janela de 30d
parameters_data_processing.ymlpipeline.pyCamadas de dado seguem a convenção do Kedro: 01_raw → 02_intermediate → 03_primary → 04_feature → 05_model_input → 07_model_output.
Cada dataset é declarado — formato, caminho, e como ler os arquivos brutos.
estoques_claro_folder:
type: partitions.PartitionedDataset
path: data/01_raw/claro/estoques
dataset:
type: pandas.CSVDataset
load_args:
sep: ";"
encoding: "latin-1"
A Claro entrega estoque e vendas como um CSV por dia. O PartitionedDataset lê a pasta inteira automaticamente — nenhum código muda quando um novo dia de arquivo chega.
Cruza cada loja com os produtos do seu cluster.
cluster_col = cluster_map.get(cluster, None) # "OPPO 2" -> "cluster_2"
modelos = produtos[produtos[cluster_col] == 'Y']['modelo']
A combinação loja × modelo só existe se aquele modelo é vendido naquele cluster — evita recomendar um Find X9 Pro para uma loja que nunca deveria vendê-lo. criar_base_cds faz o mesmo para os CDs, com produto cartesiano completo.
Grade loja × modelo × dia dos últimos 35 dias, cruzada com vendas e estoque reportados.
disponibilidade = 1 se estoques > 0 — a coluna que sustenta toda a correção de rupturaloja_confiavel = velocidade['dias_disponiveis'] >= min_dias_disponiveis
# Nivel 1: pooled por cluster_oppo + store_tier + modelo
peer = velocidade.groupby(['cluster_oppo','store_tier','modelo']).agg(...)
# Nivel 2 (ultimo recurso): pooled por modelo em toda a base
modelo_pool = velocidade.groupby('modelo').agg(...)
velocidade = where(loja_confiavel, vel_propria, vel_peer).fillna(vel_modelo)
Rodado duas vezes — janela de 7 dias (reativo) e 30 dias (estável) — e combinado com peso 0,6 / 0,4.
velocidade_vendas = vel_7d * 0.6 + vel_30d * 0.4
estoque_proj = estoques + transito
dos_ideal = tiers_doi[store_tier] # 30 / 30 / 21 dias
estoque_ideal = ceil(dos_ideal * velocidade_vendas) # ou 2, se velocidade e totalmente desconhecida
necessidade = max(0, estoque_ideal - estoque_proj)
O piso de 2 unidades só se aplica quando nem a loja, nem o grupo de pares, nem o modelo têm dado suficiente. Um assert final garante que nenhuma loja fica sem número.
A soma da necessidade de todas as lojas de um CD pode exceder o estoque real do CD.
proporcao = necessidade_loja / necessidade_total_cd
abastecimento = (
floor(estoque_cd * proporcao) # CD insuficiente -> rateia proporcionalmente
if necessidade_total_cd > estoque_cd
else necessidade_loja # CD cobre tudo -> atende 100%
)
floor garante que a soma rateada nunca ultrapasse o estoque físico do CD.
Um Excel (base_necessidade_abastecimento.xlsx) com três abas, pronto para o time de trade marketing:
Mesmo resultado também em Parquet/CSV (data/05_model_input/) para análise programática.
NaN em vez de inf, tratável no fallback seguinteassert de sanidade antes de publicar o resultado finalpipeline.pyPor design, este é um motor de decisão baseado em regras — não um modelo preditivo de caixa-preta. Escolha deliberada para manter o número auditável e defensável perante a operação.
O pipeline resolve o cálculo — os próximos passos são sobre alcance e usabilidade.
Hoje o resultado é um Excel gerado via pipeline. Uma interface reduziria a barreira de uso para o time de trade marketing e permitiria ajustar parâmetros (tiers, pesos, cobertura) sem tocar em YAML.
Reaproveitar a arquitetura — catálogo, cascata de fallback, fórmula de necessidade — para outros clientes/varejistas além da Claro, isolando apenas o que é específico de cada integração (formato de dado, cadastro de produto, regras de cluster).
A separação entre regra de negócio (parâmetros) e lógica (nós Kedro) já foi pensada para isso: generalizar o pipeline é, em grande parte, um trabalho de configuração — não de reescrita.