Previsão de demanda de loja de vinhos — Parte 3/3 — Facebook Prophet
Nessa parte final do projeto, iremos desenvolver nosso algoritmo para prever as demandas das lojas de vinhos e a projeção de vendas e analisar os resultados.
Caso você esteja aqui, mas ainda não tenha visto a primeira e segunda parte do nosso projeto, é melhor retornar para entender todo o processo que teve de ser realizado, além do estudo de caso.
Facebook Prophet
Desenvolvido pelo Facebook, o Prophet é um software de código aberto criado para prever dados de uma série temporal, que são observações realizadas em um período de tempo, no nosso caso as vendas de algumas lojas de vinho.
Além do seu desenvolvimento, o Facebook também utiliza o Prophet em vários aplicativos para fazer previsões confiáveis que serão utilizadas no planejamento e definição de metas.
Dados ausentes e grandes mudanças não afetam o seu desempenho, pois leva em conta além da sazonalidade anual, mensal e diária, efeitos de feriados já disponíveis no seu código e datas representativas para o problema que podem ser definidas manualmente.
Para mais informações sobre o Prophet, esse link contém todo o material disponibilizado pelo próprio Facebook.
Testes estatísticos e ARIMA
Para podermos fazer previsões baseadas em séries temporais, precisamos que ela seja estacionária. Ou seja, suas variáveis precisam ser constantes.
Repare que existe uma tendência de alta na imagem B, ou seja, sua média está aumentando. A imagem C, apesar de possuir apenas uma tendência, sua amplitude está sendo influenciada devido à variância não constante. Já na última imagem, C, a covariância está variando conforme o tempo.
Uma série temporal estacionária exibe um comportamento como o da imagem A, com média, variância e covariância constantes.
Para verificarmos se uma série é estacionária, podemos fazer algumas validações, como:
- Verificar visualmente o comportamento do plot.
- Realização de testes estatísticos.
Um dos testes estatísticos mais usados para isso, é o Augmented Dickey Fuller (ADF), que utilizaremos mais a frente.
Preparação dos dados
Ao utilizarmos o Prophet, não podemos usar qualquer formatação, mas sim uma formatação padrão:
- Uma coluna “ds”, com a data da observação, nesse caso a data da venda.
- Uma coluna “y”, que irá conter a quantidade de observações, no caso todas as vendas da data presente na coluna “ds”.
Nesse nosso projeto será feito uma análise considerando o total de vendas, mas nada impede que façamos uma previsão de vendas de apenas um tipo de vinho, de um produtor ou de qualquer uma das informações disponíveis. Caso fosse outro objetivo, na coluna “y” deixaríamos o total de observações com relação a ele.
Para isso, primeiro vamos ajustar o nosso dataset:
df_ts=df_clean.groupby('Date',as_index=False)['sales'].sum()df_ts['Date'] = pd.to_datetime(df_ts['Date'], format="%Y-%m-%d")df_ts.index = pd.to_datetime(df_ts['Date'], format="%Y-%m-%d")df_ts.drop('Date', axis=1, inplace=True)
E agora vamos renomear as colunas, conforme mostrado anteriormente, e exibir as 5 primeiras linhas, com o comando head():
df_ts = df_ts.reset_index()df_ts.columns = ['ds', 'y']df_ts.head()
Com o nosso dataset no formato correto, podemos importar a biblioteca Prophet:
from fbprophet import Prophetimport logginglogging.getLogger().setLevel(logging.ERROR)
Teste Augmented Dickey Fuller (ADF)
Conforme citamos anteriormente o teste ADF é realizado para verificar se uma série temporal é estacionária ou não. Lembrando que para fazermos as previsões, ela precisa ser estacionária.
Com esse teste, obteremos o p-value, que nos mostrará o nível de confiança que rejeitaremos a hipótese nula. Essa hipótese nula, considera que a série não é estacionária.
Esse teste será realizado com o dataset que contém apenas as colunas “ds” e “y”, com o pacote “adfuller” que será importado agora.
from statsmodels.tsa.stattools import adfullerX = df_ts.yresult = adfuller(X)print('Augmented Dickey–Fuller')print('Statistical Test: {:.4f}'.format(result[0]))print('P Value: {:.10f}'.format(result[1]))print('Critical Values:')for key, value in result[4].items():print('\t{}: {:.4f}'.format(key, value))
O resultado do p-value foi de aproximadamente 0.154. Com isso podemos rejeitar a hipótese nula, que a série temporal não é estacionária, com um nível de confiança de 85%. Devido a esse nível de confiança, podemos dar prosseguimento no projeto ou já transformarmos a série em estacionária, para não termos retrabalho caso os resultados não sejam satisfatórios, que é o que faremos.
Conversão para Série Estacionária
Para realizarmos essa transformação, precisamos remover a tendência e sazonalidade que existe nos dados originais. Para isso aplicaremos o log para reduzirmos a magnitude dos valores, só não podemos esquecer de converter o resultado final para a escala correta.
Para mais detalhes sobre essa conversão ou séries temporais, recomendo essa publicação.
Usaremos como base o dataset no formato que usamos no Prophet, que criamos anteriormente:
Ao aplicarmos o log na coluna “y”, a plotagem deixa de ser assim:
Para ficar assim:
Note que o gráfico está idêntico, porém com outra escala, o que pode ser identificado no eixo à esquerda. Agora sim podemos dar seguimento na nossa análise.
Período das previsões
Na imagem acima, temos as previsões de uma série temporal. Repare que na região cinza estão as previsões , como no seu início, no lado esquerdo, ela inicia pequena e vai aumentando conforme o passar do tempo. Isso ocorre pois quanto maior o período que estamos tentando prever, menor o grau de confiança.
Devido a isso, e também ao fato do nosso histórico de vendas conter informações de três anos, faremos uma previsão de apenas 30 dias. Lembrando que conforme as lojas realizarem vendas, as informações podem ser incluídas e o algoritmo atualizado.
Então primeiro vamos definir a quantidade de dias que serão feitas as previsões, sempre utilizando nosso dataset que foi aplicado o log, chamado de “df_log”:
prediction_size = 30train_df = df_log[:-prediction_size]
Estanciar e treinar o modelo:
b = Prophet()b.fit(train_df)
E então fazer as previsões:
future = b.make_future_dataframe(periods=prediction_size)forecast = b.predict(future)forecast.head()
O Prophet trás diversas informações, mas as que iremos utilizar são essas acima. O yhat contém as previsões da quantidade de vendas, além dos limites inferiores e superiores, yhat_lower e yhat_upper, respectivamente.
Para visualizar essas previsões, podemos plotá-las em um gráfico rapidamente, sendo “y” a quantidade de vendas para determinada data.
b.plot(forecast).savefig('forecast_non_stationary.png')
Componentes da previsão
Também podemos utilizar o Prophet para verificar tendências em determinados períodos de tempo com apenas um linha de código:
b.plot_components(forecast).savefig('non_st_components.png')
Acima temos as tendências de venda geral, semanal e anual, respectivamente.
Veja no primeiro gráfico como existe uma tendência de alta na venda de vinhos, por dois períodos se manteve constante, mas desde início de 2020 vem em uma alta constante.
Analisando o gráfico semanal, podemos identificar a segunda-feira como o dia histórico com menos vendas, aumentando constantemente até atingir o seu pico no sábado.
No gráfico anual identificamos que o mês com mais vendas tende a ser o mês de julho, com janeiro sendo o mês com menos vendas.
Verificação dos resultados
Nessa etapa, verificaremos se os resultados estão bons ou ruins. Para isso usaremos duas métricas:
- Mean Absolute Percentage Error — Mostra quanto as nossas previsões diferem do valor real em porcentagem.
- Mean Absolute Error — Valor absoluto do nosso erro na previsão em relação à série real.
Agora vamos calcular o MAPE e MAE da nossa série:
# definindo a funçãodef make_comparison_dataframe(historical, forecast):return forecast.set_index('ds')[['yhat', 'yhat_lower','yhat_upper']].join(historical.set_index('ds'))cmp_df = make_comparison_dataframe(df_ts, forecast)def calculate_forecast_errors(df_ts, prediction_size):df = df_ts.copy()df['e'] = df['y'] - df['yhat']df['p'] = 100 * df['e'] / df['y']predicted_part = df[-prediction_size:]error_mean = lambda error_name: np.mean(np.abs(predicted_part[error_name]))return{'MAPE': error_mean('p'), 'MAE': error_mean('e')}# imprimindo o MAPE e MAEfor err_name, err_value in calculate_forecast_errors(cmp_df, prediction_size).items():print(err_name, err_value)
Com a conversão para a escala logarítmica, chegamos a um erro absoluto de 0,02. Lembrando que quanto menores esses números, melhor o resultado, assim temos números aceitáveis nessa situação.
Resultado final
Agora, na última parte do nosso estudo, precisamos converter as previsões para a escala real, para podermos apresentar os resultados.
Para isso, vamos usar a função inversa da logarítmica, que é a exponencial. Também vamos apresentar o resultado em um novo conjunto de dados, chamado de “df_final”.
Então, vamos criar o dataset e incluir tanto o resultado do Prophet, quanto criar uma coluna com as previsões de venda na escala correta e exibir os 10 primeiros resultados:
df_final = forecast[['ds', 'yhat']]
df_final['AmountSpentLog'] = df_log['y']
df_final['AmountSpentPred'] = np.exp(df_final['yhat'].values)df_final.head(10)
Agora, a terceira coluna apresenta os resultados das nossas previsões, que poderão ser usadas no planejamento de estoque e para apresentar ao entrevistador da vaga descrita na primeira parte do projeto, que pedia exatamente isso, um algoritmo de previsão de demanda.
Aqui encerramos esse projeto, caso tenha alguma dúvida ou queira discutir sobre esse projeto, pode me chamar no LinkedIn. Ah, e não esqueça de “aplaudir” a história, para que esse projeto chegue ao máximo de pessoas possível. Muito obrigado e até a próxima!