Saber o "futuro" é algo que eu acredito que todas as organizações buscam, desde a quantidade de vendas realizadas na próxima estação, a quantidade de não-conformidades detectadas em um produto, a previsão de demandas em vários setores.
Conhecido como Time Series
ou Séries Temporais em estatística, é uma coleção de observações feitas sequencialmente ao longo do tempo. Um ponto importante que deve ser considerado é que a ordem dos eventos é fundamental e uma característica deste tipo de dados é que as observações vizinhas são dependentes e o interesse é analisar e modelar essa dependência.
Facebook Prophet
é uma ferramenta Open Source criado pelo Facebook em 2017, com o objetivo de forecasting
ou previsão em séries temporais, tanto para uso em Python quanto em R.
As caracaterísticas do Prophet são:
Com o Prophet
você não precisa ficar preso aos resultados de um procedimento automático, caso a previsão não for satisfatória, pois mesmo não sendo experiente em métodos de Séries Temporais, é possível ajustar as previsões para melhorias nesse processo usando uma variedade de parâmetros de fácil interpretação.
Vamos demonstrar um exemplo de como funciona esse framework.
Principal: Demonstrar o uso, de uma forma simples, da biblioteca Prophet
.
Etapas do processo:
Vamos usar um conjunto de dados do Kaggle, denominado como Avocado Prices, que nos dá o histórico de vendas de abacates de 2015 à 2018, somente aos domingos, dos Estados Unidos para demonstrarmos como funciona o Prophet.
Antes de iniciar, vamos dar uma olhada no significado de cada coluna do conjunto de dados.
Observações:
Usaremos as variável AveragePrice
ou Preços médios
para previsão.
As bibliotecas a serem usadas são:
Pandas
e Numpy
para manipulação dos dados e estatísticas.Matplotlib
e Seaborn
para visualizações.Prophet
para séries temporais.# importando o conjunto de dados
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from fbprophet import Prophet
O nome do arquivo é avocados e está no formato csv.
# importando o dataset
df = pd.read_csv('avocado.csv')
Uma coisa importante é saber a estrutura dos dados, então vamos visualizar as primeiras 5 linhas para isso, utilizando o método head()
.
# visualizando as primeiras linhas
df.head()
Podemos ver que a coluna 0 é Unnamed:0 e está como se fosse um índice, podemos remover, mas não vamos fazer isso agora.
Assim como visualizar as primeiras linhas, também podemos ver as últimas linhas e definir quanto queremos ver, vamos por exemplo, visualizar as 10 últimas, passando como parâmetro do método tail()
.
# visualizando as últimas linhas
df.tail(10)
Vamos checar as dimensões do data frame com o método shape
.
# verificando as dimensões
df.shape
Podemos ver que o data frame contém 18249 linhas e 14 colunas.
Análise estatística é fundamental, e só para termos ums visão geral das principais, de uma forma rápida, podemos utilizar o método describe()
.
# analisando as principais estatísticas
df.describe()
Assim, por exemplo, observamos que a coluna AveragePrice tem uma média de 1.4 com um desvio padrão de 0.4, nos preços médios.
Está um pouco ruim de visualizar as outras estatísticas em notação científica, mas podemos resolver isso fazendo uma transposição nos dados, simplesmente utilizando .T
após o describe()
.
# fazendo uma transposição das principais estatísticas
df.describe().T
Com o método dtypes
podemos ver o tipo das entradas de cada coluna.
E por que isso é importante? Porque obervamos que todas as colunas são do tipo numeric, porém quando importamos pra cá, pode ser que tenha vindo como tipo string ou a coluna de formato data também como string.
# olhando os tipos dos dados
df.dtypes
Podemos confirmar que a coluna Date está no formato incorreto, por exemplo.
Vamos fazer a transformação passando o parâmetro datetime64
no método astype
.
df['Date'] = df.Date.astype('datetime64')
Vamos checar novamente.
df.info()
Outro check importante é ver se há dados faltantes, pois isso influencia nas nossa análises. Podemos ver isso de várias formas, mas vamos dar uma olhada na porcentagem de dados.
# verificando dados faltantes
df.isnull().sum() / df.shape[0]
Não temos dados faltantes nesse caso, mas se houvesse poderíamos definir uma linha de corte e remover a coluna abaixo desse limite, por exemplo, eu removeria qualquer coluna com valores acima de 75% faltando. Claro que isso é variável e dependendo do problema, esse threshold poderia ser maior ou menor, visando sempre o problema a ser resolvido.
Como também poderíamos optar em preencher os dados com a média, mediana, 0, -1... ou qualquer outro valor.
Agora vamos começar a parte de exploração. Com base no nosso objetivo, vamos focar somente na exploração dos dados em função do tempo e na nossa variável target, a coluna AveragePrice
.
Vamos deixar nosso data frame somente com as colunas que vamos utilizar, mantendo as colunas: Date, AveragePrice, type, year e region, mas antes, como uma boa prática, vamos criar uma cópia do dataframe original, caso algo dê errado não será necessário voltar tudo.
Importante ressaltar que a nossa variável target, que vamos querer prever são as médias dos preços, ou seja, a coluna AveragePrice.
# criando uma cópia do dataframe
df1 = df.copy()
Primeiro vamos ver como a nossa variável target está distribuída, plotando um histograma
da coluna AveragePrice, utilizando a função distplot
da biblioteca Seaborn
.
# definindo o tamanho do plot
plt.figure(figsize = (10, 6))
# plotando o gráfico
sns.distplot(df1['AveragePrice'])
# definindo o título
plt.title("Média dos Preços")
Podemos observar uma distribuição aproximadamente normal, com uma calda um pouco maior do lado direito, provavelmente nesses dados, há a presença de outliers. Além disso, tem dois picos e isso provavelmente pode indicar amostras de populações diferentes.
Vamos ver essa distribuição de outra forma, e dessa vez vamos separar os tipos, conventional e organic, e um ótima ferramenta para conferir se realmente há outliers é o boxplot
.
# definindo o tamanho do plot
plt.figure(figsize = (10, 6))
# plotando o gráfico
sns.boxplot(x = 'type', y = 'AveragePrice', data = df1)
# definindo o título
plt.title("Distribuição da média dos preços por tipos");
Dessa vez, está mais claro de enxergar! A distribuição dos dois tipos são um pouco diferentes (há técnicas estatísticas para saber se essa diferença pode influenciar ou não, mas não é foco deste trabalho). O tipo "orgânico" tem mais outliers do que o tipo convencional, assim como também observamos que há dispersão maior para este tipo.
Poderíamos remover os outliers e/ou também fazer uma normalização ou padronização, mas visando nosso objetivo e mencionado acima, uma das características do Prophet
é trabalhar com outliers.
Também já dito anteriormente, uma das premissas para se trabalhar com série temporal é que deve manter a ordem correta dos acontecimentos dos eventos. Portanto vamos ordenar a coluna Date.
# ordenando a coluna data
df1 = df1.sort_values('Date')
Então vamos criar uma visualização desses dados, para enxergar o comportamento ao longo do tempo.
# definindo o tamanho do plot
plt.figure(figsize = (16, 6))
# plotando o gráfico
plt.plot(df1['Date'], df1['AveragePrice'])
# definindo o título
plt.title("Média de preços ao longo do tempo")
Não ficou muito bom para visualizar os dados diários, mas vamos melhorar isso mais a frente.
Vamos dar uma olhada a quantidade por ano com um countplot
da biblioteca Seaborn
.
# definindo o tamanho do plot
plt.figure(figsize = (12, 8))
# plotando o gráfico
sns.countplot(x = 'year', data = df1)
# definindo o título
plt.title("Quantidade vendida ao longo dos anos");
Vamos fazer uma visualização da média dos preços por regiões por ano, para o tipo conventional, utilizando a função de catplot
do Seaborn
.
# plot preços vs regiões para avocados convencionais
conventional = sns.catplot('AveragePrice', 'region', data = df1[df1['type'] == 'conventional'], hue = 'year', height = 15)
Olhando para os pontos, parece que no ano de 2017 tivemos uma variação maior, em relação aos outros anos em praticamente todas as regiões.
Agora vamo dar uma olhada na média dos preços por regiões por ano, para o tipo organic.
# plot preços vs regiões para avocados organicos
organic = sns.catplot('AveragePrice', 'region', data = df1[df1['type'] == 'organic'], hue = 'year', height = 15)
Em proporção menor, mas parece que os comportamentos são semelhantes dos dois tipos.
Claro que estamos concluindo somente "batendo o olho", também poderiamos utilizar outras ferramentas para refinar a análise.
Nesta etapa vamos preparar nossos dados para aplicar o forecasting com o Facebook Prophet
.
Vamos iniciar, criando um outro dataframe somente com duas colunas, que serão as entradas para o framework, as colunas Date e AveragePrice.
# criando um data frame padrão
df_prophet = df1[['Date', 'AveragePrice']]
Vamos visualizar como ficou esse data frame.
# verificando as primeiras 5 linhas
df_prophet.head()
O Prophet
espera receber um dataframe com duas colunas, como já fizemos acima, porém as colunas terão de ser renomeadas.
As colunas devem estar renomeadas como ds (datestamp) para representar a coluna de data e y para representar a coluna que terão os dados e que servirão para as previsões. Vamos entender um pouco mais...
A coluna ds deverá estar no formato do Pandas
, como YYY-MM-DD para data ou YYYY-MM-DD HH:MM:SS para timestamp.
A coluna y deverá estar no formato numérico e representar as medições do que se deseja prever.
# renomeando as colunas
df_prophet = df_prophet.rename(columns={'Date': 'ds', 'AveragePrice': 'y'})
Olhando o resultado.
# verificando as primeiras 5 linhas
df_prophet.head()
O processo agora, segue o padrão do sklearn
, criamos uma instancia do modelo, fazemos o fit
(treinamento) e posteriormente o predict
(previsão).
# instanciando o modelo
m = Prophet()
# realizando o treinamento
m.fit(df_prophet)
Modelo treinado! Vamos fazer a previsão, mas antes temos que definir o período que queremos que o modelo faça a previsão. Neste exemplo vamos querer que faça uma previsão de 365 dias, ou 1 ano, à frente.
O Prophet
disponibiliza um dataframe adequado que se estenda para o futuro por um número especificado de dias (que escolhemos para prever) usando o método auxiliar `make_future_dataframe.
# método auxiliar para previsão
future = m.make_future_dataframe(periods = 365)
# previsão do modelo
forecast = m.predict(future)
Vamos olhar o resultado.
A coluna yhat representa as previsões, o yhat_lower e yhat_upper são as componentes do intervalo de incertezas. Quanto mais distante o tempo à frente, mais incerto fica a previsão.
# verificando as primeiras 5 linhas
forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].head()
Vamos visualizar essas previsões, com o método próprio do Prophet.
figure = m.plot(forecast, xlabel = 'Date', ylabel = 'Price')
Podemos visualizar também os seus componentes, trend (tendência) e yearly seasonality (sazonalidade anual).
figure2 = m.plot_components(forecast)
Agora que já vimos como o Prophet
funciona, vamos fazer da forma correta, treinamento, teste e métricas.
Vamos começar dividindo nossos dados em dados de treino e teste. Criaremos um objeto para definir a "data fim" e a partir disso serão criados mais dois dataframes, todos os dados anterior à data de corte serão os dados de treinamento e o outro serão para os dados de teste.
Esta data foi definida arbitrariamente, não levando em consideração nenhum tipo de análise.
# definindo uma data de corte
data_fim = '2017-06-25'
# definindo os dados de treino, antes da data de corte
train = df_prophet.loc[df_prophet['ds'] <= data_fim]
# definindo os dados de teste posterior a data de corte
test = df_prophet.loc[df_prophet['ds'] > data_fim]
Para automatizar o processo, vamos criar um objeto com a quantidade de dias a serem previstos, pra isso basta tirar os dias repetidos dos dados de teste e fazer uma contagem.
Posteriormente isso servirá para inserir a quantidade de dias à frente para previsão como parâmetro.
# contando a quantidade de dias à frente para checar a acurácia da previsão
dias_a_prever = len(test.ds.unique())
# checando a quantidade de dias
dias_a_prever
Agora vamos dar uma olhada na separação dos dados, podemos juntar os dois e colocar em cores diferentes.
# definindo o tamanho do plot
f, ax = plt.subplots(figsize=(14,5))
# plotando os dados de treino
train.plot(kind='line', x='ds', y='y', color='blue', label='Train', ax=ax)
# plotando os dados de teste
test.plot(kind='line', x='ds', y='y', color='red', label='Test', ax=ax)
# definindo o título
plt.title('Preço médio nos dados de treino e teste');
Precisamos definir uma métrica para checar.
Vamos utilizar a métrica MAPE
(Mean absolute percentage error). Essa métrica nos mostra o quanto do ajuste está errado percentualmente.
Vamos criar uma função para o MAPE.
# criando a função MAPE
def mean_absolute_percentage_error(y_true, y_pred):
y_true, y_pred = np.array(y_true), np.array(y_pred)
return np.mean(np.abs((y_true - y_pred) / y_true)) * 100
Agora vamos fazer uma previsão com os dados que separamos, seguindo o mesmo processo apresentado acima.
Primeiro, com os dados de treino vamos prever um período à frente.
# instanciando o modelo
m = Prophet()
# realizando o treinamento
m.fit(train)
# método auxiliar para previsão
future = m.make_future_dataframe(periods=dias_a_prever)
# previsão do modelo
forecast = m.predict(future)
# olhando os resultados das previsões com os dados de treino
forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail()
# Plotando os componentes
figure2 = m.plot_components(forecast)
Para melhorar a visualização, a partir do próximo plot, vou alterar algumas configurações.
# configurando a área de plotagem
fig, ax = plt.subplots(1)
# alterando a altura
fig.set_figheight(5)
# alterando a largura
fig.set_figwidth(15)
# plotando o gráfico
fig = m.plot(forecast, ax=ax)
# definindo o título
plt.title('Previsões nos dados de treino');
Agora vamos utilizar os dados de teste.
# fazendo previsões com os dados de teste
test_forecast = m.predict(test)
# olhando os resultados das previsões com os dados de teste
test_forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail(7)
Podemos checar os dados da previsão com o real, vamos comparar e depois checar o quanto o nosso modelo errou.
# configurando a área de plotagem
fig, ax = plt.subplots(1)
# alterando a altura
fig.set_figheight(5)
# alterando a largura
fig.set_figwidth(15)
# adicionando os dados de teste
ax.scatter(test.ds, test['y'], color='r')
# plotando o gráfico
fig = m.plot(test_forecast, ax=ax)
# definindo o título
plt.title('Previsões com os dados de teste');
Podemos ver que visualmente que o modelo não performou tão mal, porém temos que checar as métricas.
Antes, vamos olhar somente a parte acrescentada, comparando os dados de teste com as previsões.
# configurando a área de plotagem
f, ax = plt.subplots(figsize=(14,5))
# alterando a altura
f.set_figheight(5)
# alterando a largura
f.set_figwidth(15)
# plotando o gráfico com dados de teste
test.plot(kind='line',x='ds', y='y', color='red', label='Test', ax=ax)
# plotando o gráfico com os dados previstos
test_forecast.plot(kind='line',x='ds',y='yhat', color='green',label='Forecast', ax=ax)
# definindo o título
plt.title('Dados de teste vs Previsões');
Agora vamos usar nossa função de MAPE para saber o percentual de erro do modelo.
mape = mean_absolute_percentage_error(test['y'],test_forecast['yhat'])
print("MAPE",round(mape,4))
Nosso modelo errou 28%. Podemos melhorar? Claro que sim!
Uma das coisas que temos que considerar quando lidamos com séries temporais são feriados que ocorrem ao longo do ano, além disso a empresa pode fazer uma campanha ou lançamento de um produto e isso influencia nas previsões, pois são oscilações que ocorrem ao longo dos períodos de avaliação.
Podemos adicionar os feriados, existe uma biblioteca que se chama Holidays que poderá nos auxliar nisso, mas isso não é tudo, pois junto com os feriados podemos adicionar outros parâmetros e também treinar um grid search para escolha dos melhores, obtendo assim uma melhora significativa, mas isso ficará para um próximo trabalho.
Fizemos uma análise exploratória básica e uma aplicação simples do modelo afim de demonstração e podemos dizer que o processo de forecasting do Prophet é fácil, intuitivo e poderoso. Com ele podemos resolver muitos problemas de séries temporais e em larga escala.
Documentação oficial
https://facebook.github.io/prophet/docs/quick_start.html#python-api
Projeto guiado do Coursera
https://www.coursera.org/projects/prophet-timeseries-prediction
Portal SAS sobre Analytics
https://www.sas.com/pt_br/insights/analytics/analytics.html
Wikipedia sobre Séries Temporais
https://pt.wikipedia.org/wiki/S%C3%A9rie_temporal