Previsão de demanda de Vinhos usando Séries Temporais

Gabriel Ribeiro Ferreira Lopes
16 min readDec 15, 2023

--

Photo by Kym Ellis on Unsplash

Recentemente, fiz um projeto de ciência de dados voltado para a análise de séries temporais. Meu objetivo era o de construir um modelo de Machine Learning que pudesse prever vendas futuras com um bom nível de acurácia. Para isso, estudei mais a fundo sobre as séries temporais, os testes estatísticos e os algoritmos que possibilitam a previsão, como Prophet, ARIMA, SARIMAX, entre outros. Neste artigo, quero compartilhar o que eu aprendi, descrevendo o projeto em detalhes.

Veja o projeto completo aqui.

O que é uma série temporal?

O primeiro passo para começar o projeto, foi buscar uma definição de o que é uma série temporal.

Trata-se de um conjunto de dados ordenados cronologicamente, de modo que torna possível realizar uma análise de como determinada variável mudou ao longo de um período. Para isso, buscamos por padrões nessa dimensão temporal que indique alguma periodicidade ou tendência.

Tendência, sazonalidade e estacionariedade

Dentre os padrões que buscamos numa série temporal estão a tendência e a sazonalidade. A tendência diz respeito à direção que os dados estão tomando. Pode haver uma propensão para o crescimento em algum período, ou à queda em outros.

Já a sazonalidade diz respeito a padrões que se repetem durante um período específico do tempo. A partir dessa análise, definimos se uma série é estacionária ou não, de acordo com a maneira como os dados variam ao longo do tempo.

Uma série não-estacionária é aquela em que a escolha da origem do tempo faz a diferença, pois, a partir daquele ponto, os parâmetros estatísticos dos dados variam de uma forma significativa.

Já a série temporal estacionária é caracterizada pelo fato de que não faz diferença a escolha da origem temporal. Por causa da estacionariedade, efeitos como tendência e sazonalidade são removidos (ou ao menos abrandados), de forma que não há variação significativa de parâmetros estatísticos como média, variância e covariância.

Com essas definições preliminares, podemos seguir para o projeto.

Previsão da Demanda de Vendas de Vinhos

Primeiro, vamos contextualizar o problema de negócios e entender nossos objetivos com o projeto.

Entendimento do Negócio

O vinho é uma bebida viva. Cada tipo de uva produz uma bebida com uma personalidade própria, que é ressaltada também pelo processo de produção. O resultado é uma bebida complexa, detalhada e cheia de nuances tanto no sabor quanto no aroma.

Por trazer tanta riqueza dentro da garrafa, carregando o trabalho e o cuidado de tantas pessoas desde o plantio da uva até a venda, é preciso pensar em como armazenar e transportar apropriadamente esse tipo de bebida. Nesse sentido, o lucro da empresa está diretamente associado à capacidade logística no estoque, na guarda e no transporte, para que não se perca o vinho e o consumidor final receba um produto de qualidade.

A previsão de demanda diz respeito à capacidade de saber de antemão quais produtos os consumidores estão mais propensos a comprar e qual a estimativa do volume de vendas futuras, de modo que possamos organizar o estoque para gerar maior fluxo desses produtos e, consequentemente, mais lucro.

Para isso, avaliamos os dados históricos de venda e, com base neles, projetamos as vendas e prevemos a demanda para determinado período no futuro.

Objetivos

Dentro do contexto de negócios que estamos tratando, é importante prestar atenção em alguns pontos, como a competição, a geografia e o tipo de produto. Portanto, alguns pontos em que o projeto foi focado são:

  • Quais itens são mais vendidos;
  • Quais itens são menos vendidos;
  • Quais são os produtos mais lucrativos;
  • Quais marcas convertem em maior receita;
  • Quais estilos de vinho e de quais regiões os clientes preferem;
  • Qual a demanda diária de produtos nas lojas;
  • Qual a receita diária das lojas;
  • Como a demanda atual se compara com períodos anteriores;
  • Qual a projeção das vendas futuras.

Neste artigo, vamos dar mais enfoque nos itens em negrito. Mas, caso queira ver a análise completa, basta acessar o projeto clicando aqui.

Para alcançar esses objetivos, foi feita a análise exploratória dos dados de 219 produtos e das respectivas vendas num período de 3 anos, utilizando para isso as bibliotecas de análise de dados do Python, além do algoritmo de previsão do Meta, o Prophet.

Resultados da Análise Exploratória

Neste projeto, analisamos os dados sobre os produtos e vendas de 3 lojas de vinhos. Buscamos entender mais a fundo o contexto em que se está inserido o negócio de vendas de vinhos, estudando os desafios de manutenção, conservação, logística e satisfação do cliente.

A análise exploratória nos deu um panorama de como se relacionam os vinhos com as vendas e a receita bruta gerada por eles. Junto disso, visualizamos o impacto das marcas e regiões de produção nas vendas dos produtos, desenhando assim uma possível estratégia para ter um estoque com os produtos que mais tem fluxo de venda e retornam maior receita.

A Figura 1.1 abaixo mostra uma das análises feitas sobre a distribuição das vendas e da receita bruta por marca de vinho. A partir disto, pudemos concluir quais marcas convertem em maior receita, quais geram mais receita apesar das vendas mais baixas, e quais são de preferência do consumidor.

,,Figura 1.1 — Marcas de vinho com maior volume de vendas e maior geração de receita bruta.

Note que algumas das marcas que mais vendem não são as que geram maior receita. O maior volume de vendas, porém, mostra qual estilo de bebida os consumidores preferem, e como poderíamos melhorar a variedade do estoque segundo essa informação. A marca Domaine Ponsot é a que mais gera receita, sendo também a maior em vendas. Seus vinhos são da região da Borgonha, na França, e são na sua maioria tintos e brancos.

Vamos ver, na Figura 1.2, como se distribuem os preços dos vinhos segundo o tipo.

Figura 1.2 — Distribuição dos preços dos vinhos segundo o tipo.

Note que os vinhos tintos estão entre os mais caros no conjunto de dados. Inclusive, o vinho mais caro é um tinto. De fato, neste conjunto de dados estudado, a maioria dos exemplos são desse tipo, seguido pelos vinhos brancos. Isso explica porque marcas que oferecem mais opções em vinhos tintos podem ser aquelas que também geram a maior receita, como é o caso da Domaine Ponsot.

Porém, de modo geral, poucos vinhos apresentam um preço muito elevado. Isso é claro ao se observar os outliers na Figura 1.2, que ocorrem principalmente para o vinho tinto. Para ter uma melhor noção de como se distribuem os preços dos vinhos, fizemos uma análise com o gráfico de violino, que pode ser visto na Figura 1.3.

Figura 1.3 — Distribuição dos preços do vinhos.

A maioria dos vinhos estão abaixo de US$ 100, e menos de 10% dos vinhos apresentam preço maior do que US$ 500. O vinho mais caro é o francês Domaine Ponsot Clos de La Roche Grand Cru Cuvée, que custa US$ 1901,73, enquanto o mais barato é o espanhol Cava Juvé & Camps Cinta Purpura, um espumante que custa US$ 9,13.

Com esse panorama sobre volume de vendas, receita, tipos de vinho e marcas, a análise exploratória proporcionou a capacidade de entender melhor o contexto de negócios e, por si só, já ajudaria na construção de estratégias para melhorar as vendas e aumentar o lucro. Porém, vamos seguir com a construção dos modelos de previsão.

Construção do Modelo de Previsão

Na parte de construção do modelo de previsão de demanda, utilizamos os dados de venda para criar uma série temporal. Nosso objetivo era o de ajustar o modelo do Prophet aos dados históricos, buscando minimizar os erros e maximizar a aderência da previsão aos dados, de modo que pudéssemos utilizar o modelo final para prever a demanda futura.

Análise da Série Temporal Não-Estacionária

Ao observar a série temporal original, vemos que existem padrões nos dados característicos de sazonalidade e tendência. Visualmente, como podemos ver na Figura 1.4, a série realmente não é estacionária.

Figura 1.4 — Vendas diárias com a média mensal móvel de 30 dias.

Note como os dados mostram claramente um ciclo de vendas, que cresce no primeiro semestre e cai no segundo, mantendo, contudo, uma tendência anual de crescimento. De fato, se decompormos essa série temporal em suas componentes de tendência e sazonalidade, vemos que elas são bem definidas, como mostra a Figura 1.5.

decomposition = seasonal_decompose(x = df_ts['sales'], 
model = 'multiplicative',
period = 52)
Figura 1.5 — Série temporal não-estacionária decomposta em suas componentes de tendência e sazonalidade.

Note os seguintes pontos a partir dos gráficos acima:

  • A tendência é de aumento gradual, com um crescimento durante o primeiro semestre seguido de uma queda no segundo;
  • O padrão da sazonalidade ficou bem explícito com a decomposição;
  • Os resíduos são os dados que restam ao retirar as componentes de tendência e sazonalidade. Quanto mais aspecto de ruído branco tiver, melhor, pois indica a aleatoriedade dos dados, e que o algoritmo capturou bem estas componentes. Neste caso, os resíduos estão muito bons, aleatórios e com alguns pontos extremos que podem ser considerados outliers.

Testes de Estacionariedade

Num primeiro momento, testamos a estacionariedade da série temporal através dos testes ADF e KPSS. Os testes apresentam abordagens distintas para testar se a série é estacionária ou não, e segundo os resultados de cada um, podemos diagnosticar se há estacionariedade ou não.

No teste ADF (Augmented Dickey-Fuller), a hipótese nula (H0) é a de que a série não é estacionária e possui uma raíz única, ou seja, apresenta alguma estrutura de dependência temporal. Essa hipótese é rejeitada se conseguirmos constatar a estacionariedade da série com níveis de confiança maiores do que 90%, 95% ou 99%, correspondendo a p-valores de 0.1, 0.05 e 0.01, respectivamente.

A hipótese alternativa (H1) constata a estacionariedade da série. Se o p-valor não tiver um nível de signficância menor do que 10%, então não haverá evidências para rejeitar a hipótese nula, de modo que os dados precisarão passar por alguns procedimentos (e.g, diferenciação) para tornar a série estacionária.

Para realizar o teste, criamos a seguinte função:

def adf_test(time_series):

print('Resultados do teste ADF\n')

result = adfuller(time_series)

df_output = pd.Series(
result[:2],
index = ['Estatística do Teste:', 'p-valor:']
)

for key, value in result[4].items():
df_output[f'Valor crítico {key}'] = value

print(df_output)

Os resultados foram os seguintes:

A estatística ADF é o valor obtido no teste — quanto mais negativo for, maiores as chances de se rejeitar a H0. Se olharmos os valores críticos, eles dizem respeito aos valores da estatística ADF que corresponderia aos níveis de confiança de 90%, 95% e 99%. Neste caso, obtivemos -2,36, enquanto que, para ultrapassar o limiar de 10% para o p-valor, precisaríamos de -2,56.

De fato, o p-valor é de 0,15, significando que não obtivemos evidências para rejeitar a hipótese nula de que nossa série é não-estacionária.

Resultado Teste ADF: Não-estacionária

Com os resultados do teste ADF, vamos realizar o teste KPSS para tornar nossa conclusão sobre a estacionariedade da série temporal mais robusta.

É sempre bom aplicar dois testes estatísticos para saber se a série é realmente estacionária. Podemos ter alguns cenários:

  1. Os dois testes concordam na estacionariedade → A série é estacionária.
  2. Os dois testes concordam na não-estacionariedade → A série não é estacionária.
  3. O teste ADF indica estacionariedade e o KPSS não → A série é estacionária por diferenciação. Ou seja, a série temporal diferenciada deve ser testada por sua estacionariedade.
  4. O teste KPSS indica estacionariedade e o ADF não → A série é estacionária por tendência. A tendência precisa ser removida para tornar a série estritamente estacionária.

Da mesma forma como no ADF, fizemos uma função para o teste KPSS:

def kpss_test(time_series):

print('Resultados do teste KPSS\n')

result = kpss(time_series)

df_output = pd.Series(
result[:2],
index = ['Estatística do Teste:', 'p-valor:']
)

for key, value in result[3].items():
df_output[f'Valor crítico {key}'] = value

print(df_output)

Que obteve os seguintes resultados:

Note que a estatística de teste deu um valor tão alto que está fora da tabela de p-valores. Assim, segundo o teste KPSS, estamos lidando com uma série estacionária com um nível de confiança que tende a 100%.

Resultado Teste KPSS: Estacionária

Ou seja, temos o cenário 4, em que o teste ADF diz que a série é não-estacionária, enquanto que o KPSS aponta para sua estacionariedade. Assim, tiramos a seguinte conclusão:

Conclusão: Série Não-Estacionária por Tendência

Modelo de Baseline na Série Não-Estacionária

Ainda assim, fizemos um primeiro ajuste do modelo sobre a série não-estacionária, de modo a termos um baseline para comparações. Os dados históricos foram muito bem ajustados pelo modelo, porém o erro absoluto médio (MAE = 1015,2) ficou muito alto. Para termos boas previsões futuras, é preciso diminuir esse erro através do ajuste na série estacionária.

A Figura 1.6 mostra como a previsão se ajustou aos dados históricos, juntamente com os pontos de mudança. Note que, por causa da série não ser estacionária, houveram muitos pontos de mudança, que são locais onde o algoritmo percebe uma variação significativa dos parâmetros estatísticos, como a média ou variância.

Figura 1.6 — Ajuste do modelo sobre a série não-estacionária com pontos de mudança.

Convertendo a Série Temporal

Como a série é não-estacionária por tendência, segundo nossos testes estatísticos, é preciso aplicar uma transformação nos dados para que eles fiquem numa escala onde as variações dos parâmetros estatísticos se tornem desprezíveis.

Para remover a tendência da série temporal, vamos aplicar o logaritmo aos dados de vendas e à média móvel. Em seguida, iremos subtrair essas duas componentes calculadas, que se encontram na mesma escala, de modo que teremos como resultado uma série aproximadamente estacionária.

# Aplicando o logaritmo sobre a série 
# temporal não-estacionária e a média móvel
df_log = np.log(df_not_st)
ma_log = np.log(df_not_st.rolling(30).mean())

# Construindo um novo conjunto de dados baseado
# na diferença dos dados logarítmicos
df_detrend = (df_log - ma_log).dropna()
# Média móvel do conjunto normalizado
ma_detrend = df_detrend.rolling(30).mean()
# Desvio-padrão móvel do conjunto normalizado
std_detrend = df_detrend.rolling(30).std()

A Figura 1.7 mostra como ficaram os dados da série temporal após remover a tendência e diminuir os efeitos de sazonalidade ao mudar a escala dos dados para o logarítmo.

Figura 1.7 — Resultado da subtração da série temporal em escala logarítmica com a média móvel, também em escala logarítmica. Essa visualização mostra como os dados nessa escala se comportam como uma série estacionária, sem variações grandes na média ou no desvio-padrão.

Visualmente, a série já parece ser estacionária. Note que agora a média móvel oscila, mas numa escala muito menor. Além disso, o desvio-padrão (raiz quadrada da variância), também não varia muito. Aplicando os testes estatísticos, obtemos os seguintes resultados:

Com esse teste, obtivemos uma estatística de -4,82 (comparado com -2,36 do primeiro). O p-valor, que representa nosso nível de significância para aceitar a hipótese alternativa, está muito abaixo do limiar de 1%, o que significa que podemos rejeitar a hipótese nula com quase 100% de confiança. Assim, a série é estacionária pelo teste ADF.

O teste KPSS também concorda que a série é estacionária, retornando uma estatística de 0,54 que corresponde a um nível de confiança de 97%.

Conclusão: Série Estacionária

Ajuste do Modelo sobre a Série Estacionária

Como vocês puderam ver, temos agora uma série temporal estacionária na escala logarítmica. Retiramos a tendência dos dados para que pudéssemos agora ajustar o modelo sobre eles, de modo que tenhamos um erro reduzido e uma boa previsão da demanda futura.

Os mesmos procedimentos de ajuste aplicados sobre a série não-estacionária foram feitos sobre esta estacionária. Foi reservado 60% dos dados para treino e 40% para a validação. O modelo foi instanciado com o argumento n_changepoints para identificar até 20 pontos de mudança. Diferentemente do primeiro modelo de base, este não encontrou nenhum, já que a série é estacionária.


# Resetando o índice
df_log.reset_index(inplace = True)

# Dividindo os conjunto de treino e validação
train_df_st = df_log.sample(frac = .6, random_state = 30)
val_df_st = df_log.drop(train_df_st.index)

# Resetando os índices
train_df_st = train_df_st.reset_index(drop = True)
val_df_st = val_df_st.reset_index(drop = True)

# Instanciando o modelo
model = Prophet(n_changepoints = 20)

# Ajustando o modelo
model.fit(train_df_st)

# Previsões no conjunto de validação
pred_st = model.predict(pd.DataFrame({'ds': val_df_st['ds']}))

# Valores reais
y_true = val_df_st['y']

# Valores previstos
y_pred = pred_st['yhat']

# Convertendo os valores de previsão para inteiros
y_pred = y_pred.astype('int64')

Como resultado, obtivemos um erro absoluto médio (MAE) baixíssimo, de 0,74, enquanto que o MAPE, a média do erro percentual absoluto, ficou em 7%. Isto confirma a ótima acurácia do modelo, já que conseguimos reduzir o MAE de 1015,1 para 0,7.

A Figura 1.8 mostra o quão boa foi a aderência da previsão aos dados históricos.

Figura 1.8 — Previsão sobre a série estacionária.

Note como o modelo foi capaz de capturar as nuances e oscilações das vendas diárias, acompanhando a tendência geral dos dados ao longo dos 3 anos.

Isto mostra que podemos agora seguir para a previsão de demanda futura.

Previsões Futuras e Limites do Modelo

Com os erros devidamente minimizados e a acurácia do modelo treinado constatada, iremos agora terminar nosso projeto prevendo dados futuros. Como o modelo foi capaz de capturar com perfeição a tendência e a sazonalidade dos dados, e como temos um histórico de 3 anos de venda, podemos testá-lo prevendo vendas de 1 ano no futuro.

Para isso, construímos um novo conjunto de dados com datas que partem de Janeiro de 2021 até Janeiro de 2022. Utilizamos para isso o seguinte código:

from datetime import date, timedelta

# Criando uma lista vazia para receber as datas
future = []

# Datas inicial e final
start_date = date(2021, 1, 1)
end_date = date(2021, 12, 31)

# Intervalo entre as entradas geradas (7 dias)
delta = timedelta(days = 7)

# Loop para gerar as datas e colocar na lista
while start_date <= end_date:
future.append(start_date.isoformat())
start_date += delta

# Criando o df das datas futuras
future = pd.DataFrame(future)
future.columns = ['ds']
future['ds'] = pd.to_datetime(future['ds'])

future.head()

Então, ajustamos o modelo sobre esses novos dados, testando sua capacidade de generalização. O resultado pode ser visto na Figura 1.9.

Figura 1.9 — Previsão de 1 ano de vendas no futuro.

Note que o modelo capturou perfeitamente o ciclo de crescimento seguido de queda nas vendas, observado durante os outros anos.

A área sombreada em vermelho diz respeito ao intervalo de incerteza da previsão, o espaço em que provavelmente os dados de vendas diários oscilariam. Para um ano no futuro, a incerteza se mantém aceitável, dentro da variação esperada nas variações de vendas observadas a nível diário.

Como as datas futuras foram geradas de modo que cada entrada tenha 7 dias de diferença da última, a linha sólida vermelha representa como que a média móvel semanal prevista pelo modelo. Se buscamos mais resolução, por exemplo colocando um intervalo menor do que 7 dias, o modelo se mostra incapaz de prever os picos e quedas nessa escala, apenas reproduzindo padrões de sazonalidade que ele identificou. Esses padrões, contudo, não carregam informação nenhuma.

De qualquer forma, trata-se de um resultado fascinante e muito útil para a previsão de demanda das lojas.

Como um último experimento, vamos tentar prever para além de um ano e ver os limites do modelo.

Os limites do modelo

Seguimos o mesmo procedimento, porém agora criando um conjunto de dados com datas que vão desde Janeiro de 2021 até o começo de 2023, ou seja, se extendendo por dois anos.

Esperamos que à medida em que o modelo é forçado a prever mais no futuro, pior fica a sua estimativa da quantidade de vendas naquele período. Em consequência disso, o intervalo de incerteza tende a aumentar.

O resultado é mostrado na Figura 1.10 abaixo.

Ao testar os limites do modelo, prevendo dois anos de vendas no futuro, começamos a ver o intervalo de incerteza aumentar rapidamente, como esperado. Isto acontece porque começamos a realizar previsões numa escala de tempo comparável àquela dos dados históricos disponíveis (2 anos), de modo que passamos a ter uma composição de erros cada vez maior sobre as previsões.

Assim, precisaríamos de pelo menos 5 anos de dados históricos para conseguir fazer a previsão de demanda dois anos no futuro. Caso contrário, o modelo é incapaz de manter baixa variabilidade nos limites de previsão.

Note, contudo, que a média móvel das vendas nesse período segue uma tendência de crescimento coerente com o que foi observado antes. Assim, poderíamos nos guiar por ela para ter ao menos uma estimativa da demanda nesse período.

Em suma: O modelo apresentou uma capacidade preditiva alta da demanda de vendas de vinhos até um ano no futuro. Com um erro absoluto médio (MAE) baixíssimo, de 0,74, e um MAPE abaixo de 10%, trata-se de um modelo robusto, capaz de generalizar para novos dados no futuro e prever a demanda com precisão.

Conclusão

Manipular séries temporais para fazer uma análise preditiva não é tarefa fácil. Requer o conhecimento de diversas ferramentas estatísticas e testes para transformar os dados, deixando-os no formato mais adequado. Além disso, envolve também adaptar esse conhecimento ao contexto de negócios, para julgar qual variável é mais informativa para realizar a previsão.

Assim, não se trata de apenas organizar os dados e jogar no modelo, mas de compreender o motivo para cada etapa acontecer. Como em qualquer projeto de Machine Learning, o conhecimento do cientista de dados por trás deve transcender a mera técnica, integrando-se com um detalhamento do ponto de vista de negócios.

Este projeto mostrou como os dados são ativos valiosos para as empresas. Não somente foi possível identificar quais produtos são os melhores para se ter no estoque, através da análise exploratória, como também mostrou como a previsão de demandas futuras ajuda na organização desse estoque.

O vinho é uma bebida delicada e que requer muito cuidado, então é crucial conhecer quais tipos de produtos compensa manter em estoque, e de que maneira eles podem ser organizados, guardados e transportados para chegar com qualidade até o consumidor final.

Espero que este artigo tenha lhe dado informações valiosas sobre esse tipo de análise com séries temporais. Convido você que chegou até aqui a ver o projeto completo, com todos os códigos no link abaixo.

Clique aqui para ver o projeto completo.

Obrigado pela leitura!

Me acompanhe nas redes sociais e confira outros projetos no meu portfólio do GitHub, ou através dos artigos aqui no Medium.

--

--