Introducción a los modelos de previsión de ventas con python y tensorflow

Category : Noticias

Introducción

En Jortilles Llevamos algún tiempo trabajando con modelos predictivos y librerías de Machine Learning. Concretamente con TensorFlow . Hoy queremos hacer un ejercicio de predicción de ventas. Para ello necesitaremos un poco más de potencia que en la entrada anterior. Por eso lo haremos con Python + TensoFlow.

En ésta entrada presentamos una introducción a algunos de los modelos que podemos usar para realizar previsiones de ventas. La previsión de ventas, o en general previsión de series temporales, es un campo ámpliamente estudiado en el campo de la estadística. Éste trabajo pretende ser un resumen de algunas de las técnicas más comunes que podemos usar para realizar previsiones a futuro.

Presentaremos dos modelos: ARIMA (Autoregressive integrated moving average) y un modelo basado en redes neuronales. En cada caso trabajaremos con dos sets de datos, uno real y uno prefabricado que nos servirá para evaluar la calidad del modelo.

Configuración del entorno

from __future__ import absolute_import, division, print_function, unicode_literals
import tensorflow as tf
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import os
import pandas as pd
from numpy import array

#newral networks
import tensorflow.keras as keras
from keras.models import Sequential
from keras.layers import LSTM
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import Bidirectional
from keras.layers import RepeatVector
from keras.layers import TimeDistributed
from keras.layers import Conv1D
from keras.layers import MaxPooling1D
from keras.layers import Flatten


#Arima/autoarima
import statsmodels
from statsmodels.tsa.arima_model import ARIMA
from statsmodels.graphics.tsaplots import plot_pacf
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.tsa.seasonal import seasonal_decompose
from pmdarima.arima import auto_arima

#Config
plt.rcParams["figure.figsize"] = (20,3)

import random
random.seed(1)

ARIMA model

En primer lugar presentamos el modelo ARIMA. Es un modelo clásico que deberia de servir como punto de partida para evaluar otros modelos mas sofisticados.

Los pasos a seguir són: preparar los datos – definir el modelo – entrenarlo – evaluarlo.

Nuestro set de datos real es una serie de ventas bastante caóticas, para tener con qué comparar definiremos sets de datos a medida para los que sabemos que hay modelos capaces de predecirlos. Así, si obtenemos malas predicciones con los datos reales sabremos que no es por el modelo en si, sino porqué hemos configurado mal los parámetros. O simplemente porque estamos usando un modelo inapropiado.

Fabricamos nuestro set de datos

Para el modelo ARIMA fabricaremos un modelo de datos muy básico, sin periodos y en el que los valores se incrementan de forma constante más un ruido aleatorio.

# build testDataset
def buildDataset(size):
    data = []
    noise = np.random.normal(0,1,size)
    for i in range(2, size):
        data.append((i + 3*noise[i]))
    return data
buildedData = pd.DataFrame(data=buildDataset(144))
buildedData.plot(title='Artificial Data')

Importamos los datos reales

Éstos datos són datos reales de ventas agregadas por mes.

rawDF = pd.read_csv("/Files/aggregatedDataByMonth.csv")
# build dataset
df = rawDF['amount']
df.index = rawDF['index']
realData = df
# visualize data
df.plot(title='Sales Data')
print(df.shape)

Tratamiento de los datos

En éste paso separamos los datos entre test y train para poder entrenar y evaluar el modelo con datos distintos.

#real data
data = realData
SIZE = data.shape[0]
TRAIN_SPLIT = round(SIZE * 0.8)

#standarize
#data = data.values
MEAN = data[:TRAIN_SPLIT].mean()
STD = data[:TRAIN_SPLIT].std()
nData = (data-MEAN)/STD

PERIODS_TO_PREDICT =SIZE - TRAIN_SPLIT
mTrain = data[0:TRAIN_SPLIT]
mTest = data[TRAIN_SPLIT:]
#Artificial data
bdata = buildedData
bSIZE = bdata.shape[0]
bTRAIN_SPLIT = round(bSIZE * 0.7)

#standarize
#data = data.values
bMEAN = bdata[:bTRAIN_SPLIT].mean()
bSTD = bdata[:bTRAIN_SPLIT].std()
bDataNorm = (bdata-bMEAN)/bSTD

bPERIODS_TO_PREDICT =bSIZE - bTRAIN_SPLIT
bmTrain = bdata[0:bTRAIN_SPLIT]
bmTest = bdata[bTRAIN_SPLIT:]
print('SIZE -- TRAIN_SPLIT -- TEST_SPLIT -- PERIODS_TO_PREDICT' )
print(bSIZE,bmTrain.shape[0], bmTest.shape[0], bPERIODS_TO_PREDICT)

Autocorrelación y autocorrelación parcial

La configuración del modelo ARIMA, depende del tipo de serie temporal que queramos predecir. Algunas de las características que hay que tener en cuenta són la autocorrelación i la autocorrelación parcial de los datos. Podemos encontrar más información aquí:
https://machinelearningmastery.com/gentle-introduction-autocorrelation-partial-autocorrelation/
A partir de éstas gráficas podremos ajustar mejor los parámetros del modelo.

#Correlation
acf = plot_acf(data)
plt.title('Autocorrelation - real data')
plt.show()
pacf = plot_pacf(data)
plt.title('Partial autocorrelation - real data')
plt.show()
#Correlation
acf = plot_acf(bdata)
plt.title('Autocorrelation - builded data')
plt.show()
pacf = plot_pacf(bdata)
plt.title('Partial autocorrelation - builded data')
plt.show()

Seasonal decompose

Otra de las herramientas que tenemos a nuestra disposición para analizar el tipo de serie temporal que queremos predecir es el seasonal decompose, que descompone una série temporal en tres series: tendencia, temporada y residual. Analizar éstas gráficas nos puede dar información esencial para decidir qué modelos aplicar o con qué parámetros.

En éste caso aplicamos la función sólo a los datos reales. Mediante estas series podemos extraer información muy útil que nos ayudará a entender qué problemas tenemos que resolver y cómo podemos hacerlo. Por ejemplo si nuestros datos son estacionarios o si hay temporalidad.

#Seasonal decompose
plt.rcParams["figure.figsize"] = (15,10)
data = data;
result = seasonal_decompose(data, model='multiplicative', period=12)
fig = result.plot()
plt.rcParams["figure.figsize"] = (20,6)

Auto-ARIMA

La función auto_arima realiza una búsqueda de la mejor configuración para el modelo ARIMA segun los datos de que disponemos. Eso no evita tener que pasarle una configuración inicial,que dependerá de la evaluaciónque hemos hecho con las herramientas anteriores.
Aquí podemos encontrar algo mas de información acerca de cómo configurar los parámetros de auto_arima:

https://alkaline-ml.com/pmdarima/tips_and_tricks.html

Modelo para los datos reales


stepwise_model = auto_arima(mTrain,
                            start_p=5, d=0, start_q=2,
                            max_p=6, max_d=0, max_q=2,
                            start_P=0, D=0, start_Q=0,
                            max_P=2, max_D=2, max_Q=2,
                            max_order=2, m=12,
                            seasonal=True, stationary=False,
                            information_criterion='aic',
                            alpha=0.02,
                            trace=False,
                            error_action='ignore',
                            suppress_warnings=True,
                            stepwise=True,
                            n_jobs=-1,
                            maxiter=10, verbose=0)
print(stepwise_model)

Modelo para los datos prefabricados

b_stepwise_model = auto_arima(bmTrain,
                            start_p=0, d=1, start_q=0,
                            max_p=4, max_d=1, max_q=0,
                            start_P=0, D=0, start_Q=0,
                            max_P=0, max_D=1, max_Q=0,
                            max_order=2, m=1,
                            seasonal=False, stationary=False,
                            information_criterion='aic',
                            alpha=0.02,
                            trace=False,
                            error_action='ignore',
                            suppress_warnings=True,
                            stepwise=True,
                            n_jobs=-1,
                            maxiter=10)
print(stepwise_model)

Entrenamiento de los modelos

stepwise_model.fit(mTrain)
future_forecast = stepwise_model.predict(n_periods=PERIODS_TO_PREDICT)

b_stepwise_model.fit(bmTrain)
b_future_forecast = b_stepwise_model.predict(n_periods=bPERIODS_TO_PREDICT)

Representación de los resultados

Datos reales

future_forecast = pd.DataFrame(future_forecast, index = mTest.index,columns=['Prediction'])
df = pd.concat([mTest,future_forecast],axis=1)
plt.plot(df['amount'], label='amount')
plt.plot(df['Prediction'], label='Prediction')
plt.legend()
plt.title('ARIMA predictions')
plt.show()

Datos prefabricados

b_future_forecast = pd.DataFrame(b_future_forecast, index = bmTest.index,columns=['Prediction'])
b_df = pd.concat([bmTest,b_future_forecast],axis=1)
plt.plot(bmTest, label='amount')
plt.plot(b_future_forecast, label='Prediction')
plt.legend()
plt.title('ARIMA predictions (builded Data)')
plt.show()
df = pd.concat([data,future_forecast],axis=1)
bdf = pd.concat([bdata,b_future_forecast],axis=1)

fig = plt.figure()
ax1 = fig.add_subplot(2, 1, 1)
ax2 = fig.add_subplot(2, 1, 2)
ax1.plot(bdf)
ax2.plot(df)
ax1.set_title('Builded data training evaluation')
ax2.set_xlabel('Month')
ax2.set_title('Real data training evaluation')
plt.show()

Conclusión

En las gráficas podemos ver cómo el modelo para los datos prefabricados fuciona relativamente bien, teniendo en cuenta que los datos siguen una tendencia ascendente más un ruido aleatorio (por tal como los hemos definido). El modelo es capaz de predecir la tendencia a la perfección, ignorando el ruido. En éste caso la predicción equivale a la recta de regresión por el tipo de datos que le hemos pasado.
El modelo generado a partir de los datos reales es capaz de predecir los primeros 6 meses de datos con cierta precisión, pero mas allá de esos seis meses pierde consistencia.

Hay que señalar que mostramos el proceso de forma muy superficial, tanto la configuración como la evaluación de los modelos requieren un proceso riguroso.

Redes neuronales

Una vez presentado el modelo ARIMA desarrolaremos un modelo basado en redes neuronales.


Para ello lo primero que haremos es definir una función que nos servirá para preparar los datos en la forma correcta. En éste caso nuestro modelo espera que cada observación se componga de una serie de datos a pasado que serán las variables de la observación: si t es el momento en el que queremos empezar a hacer predicciones debemos pasarle al modelo un vector que contenga (t-1, t-2, …, t-n), siendo n el número de valores a pasado que estimemos que pueden aportar la información necesaria para las predicciones. El valor de n hay que estimarlo en base al análisis de los datos que hayamos hecho previamente.


Como valores a predecir el modelo espera un determinado número de valores a futuro, que definiremos mas adelante, en la forma (t, t+1, t+2, …).

Así, dada una entrada (t-1, t-2, …, t-n) el model predice (t, t+1, t+2, …, t+m).

# restructure data to forecasting window 
def univariate_data(dataset, start_index, end_index, history_size, target_size):
  data = []
  labels = []

  start_index = start_index + history_size
  if end_index is None:
    end_index = len(dataset) - target_size

  for i in range(start_index, end_index):
    indices = range(i-history_size, i)
    # Reshape data from (history_size,) to (history_size, 1)
    data.append(np.reshape(dataset[indices], (history_size, 1)))
    labels.append(dataset[i:i+target_size].flatten())
  return np.array(data), np.array(labels)

Datos prefabricados

Preparamos los datos que nos servirán para testear nuestro modelo. Para éste modelo definiremos unos datos simulando un posible gráfico de ventas, con subidas y bajadas periódicas y algo de ruido aleatorio.

# build testDataset
def buildDataset(size):
    data = []
    noise = np.random.normal(0,1,size)
    for i in range(2, size):
        data.append(i/(i-1) *pow(-1, i%11) + noise[i]/2)
    return data
buildedData = pd.DataFrame(data=buildDataset(456))
buildedData.plot(title='Artificial Data')

#standarize artificial data
ASIZE = buildedData.shape[0]
ATRAIN_SPLIT = round(ASIZE * 0.7)
Auni_data = buildedData.values
Auni_train_mean = Auni_data[:ATRAIN_SPLIT].mean()
Auni_train_std = Auni_data[:ATRAIN_SPLIT].std()
Auni_data = (Auni_data-Auni_train_mean)/Auni_train_std

Los datos reales que usaremos són los mismos que para el apartado anterior.

df = pd.read_csv("/Files/aggregatedDataByWeek.csv")
df.head()
# build dataset
data = df['amount']
data.index = df['index']
print(data.shape)

SIZE = data.shape[0]
TRAIN_SPLIT = round(SIZE * 0.7)
#standarize
uni_data = data.values
uni_train_mean = uni_data[:TRAIN_SPLIT].mean()
uni_train_std = uni_data[:TRAIN_SPLIT].std()
uni_data = (uni_data-uni_train_mean)/uni_train_std

Preparación de los datos

Ahora hay que preparar los datos para nuestra red neronal. Para ello definiremos dos modelos. Después de alguna pruebas hemos llegado a la conslusión de que para los datos artificiales ‘mirar’ 12 meses atrás da muy buenos resultados, sin embargo para los datos reales obtenemos mejores resultados con sólo 3 meses.

Preparamos los datos según estas conclusiones

Datos prefabricados

# prepare forecast window multiple steps
bunivariate_past_history = 12
bunivariate_future_target = 12
features = 1;
Bx_train_uni, By_train_uni = univariate_data(Auni_data, 0, ATRAIN_SPLIT,
                                           bunivariate_past_history,
                                           bunivariate_future_target)
Bx_val_uni, By_val_uni = univariate_data(Auni_data, ATRAIN_SPLIT, None,
                                       bunivariate_past_history,
                                       bunivariate_future_target)
print(Bx_train_uni.shape, By_train_uni.shape)
print(Bx_val_uni.shape, By_val_uni.shape)

Datos reales

# prepare forecast window multiple steps
univariate_past_history = 3
univariate_future_target = 12
features = 1;
x_train_uni, y_train_uni = univariate_data(uni_data, 0, TRAIN_SPLIT,
                                           univariate_past_history,
                                           univariate_future_target)
x_val_uni, y_val_uni = univariate_data(uni_data, TRAIN_SPLIT, None,
                                       univariate_past_history,
                                       univariate_future_target)
print(x_train_uni.shape, y_train_uni.shape)
print(x_val_uni.shape, y_val_uni.shape)

Nuestro modelo – Multistep LSTM

Hay multitud de modelos de red neuronal que podemos aplicar a éste problema. Para éste ejemplo usaremos el modelo simple de LSTM (Long-short term memory).

#modelo datos artificiales
model = Sequential()
model.add(LSTM(200, activation='relu', return_sequences=True, input_shape=(bunivariate_past_history, features)))
model.add(LSTM(200, activation='relu'))
model.add(Dropout(rate=0.5))
model.add(Dense(bunivariate_future_target))
model.compile(optimizer='adam', loss='mse')


#modelo datos reales
model1 = Sequential()
model1.add(LSTM(200, activation='relu', return_sequences=True, input_shape=(univariate_past_history, features)))
model1.add(LSTM(200, activation='relu'))
model1.add(Dropout(rate=0.5))
model1.add(Dense(univariate_future_target))
model1.compile(optimizer='adam', loss='mse')

Entrenamiento de los modelos

# fit multivariate
Bhistory = model.fit(
    Bx_train_uni, By_train_uni, epochs=100, batch_size = 32 , validation_data=(Bx_val_uni, By_val_uni), verbose=0)

# fit multivariate
history = model.fit(
    x_train_uni, y_train_uni, epochs=50, batch_size = 32 , validation_data=(x_val_uni, y_val_uni), verbose=0)

Evaluación del proceso de entrenamiento

fig = plt.figure()
ax1 = fig.add_subplot(1, 2, 1)
ax2 = fig.add_subplot(1, 2, 2)
ax1.plot(Bhistory.history['loss'], label='loss')
ax1.plot(Bhistory.history['val_loss'], label='val_loss')
ax2.plot(history.history['loss'], label='loss')
ax2.plot(history.history['val_loss'], label='val_loss')
ax1.set_xlabel('Week')
ax1.set_title('Builded data training evaluation')
ax1.legend()
ax2.set_xlabel('Week')
ax2.set_title('Real data training evaluation')
ax2.legend()
plt.show()

Las gráficas de la evolución del aprendizaje son positivas. La variable loss mide el error con los datos de entrenamiento, mientras que val_loss mide el error con los datos de test. Cuando entrena el modelo sólo ‘ve’ los datos de entrenamiento y se autoprueba con los de test. Un modelo ideal es aquel en el que lo aprendido con unos datos da buenos resultados con otros, es decir, en el que las dos gráficas són descendentes.

Predicciones

Una vez hemos entrenado los modelos podemos realizar predicciones. En nuestro caso las realiamos sobre los datos de test, asi podemos ver hasta que punto los datos reales se parecen a los que el modelo predice.

# prediCt values
predicted = model1.predict(x_val_uni[0].reshape(1, univariate_past_history, 1))
predicted = predicted * uni_train_mean + uni_train_std
real = y_val_uni[0] * uni_train_mean + uni_train_std

# prediCt values
Bpredicted = model.predict(Bx_val_uni[0].reshape(1, bunivariate_past_history, 1))
Bpredicted = Bpredicted * Auni_train_mean + Auni_train_std
Breal = By_val_uni[0] * Auni_train_mean + Auni_train_std

Representación de los resultados

fig = plt.figure()
ax1 = fig.add_subplot(2, 1, 1)
ax2 = fig.add_subplot(2, 1, 2)
ax1.plot(Breal.flatten(), marker='.', label='Real')
ax1.plot(Bpredicted.flatten(), marker='.', label='Predicted')
ax2.plot(real.flatten(), marker='.', label='Real')
ax2.plot(predicted.flatten(), marker='.', label='Predicted')
ax1.set_title('Builded data predictions')
ax1.legend()
ax2.set_xlabel('Month')
ax2.set_title('Real data predictions')
ax2.legend()
plt.show()

Conclusiones

Como en el caso anterior, el modelo enrenado con los datos prefabricados es capaz de generar predicciones muy aceptables. Sin embargo con los datos reales el modelo es por ahora algo mas impreciso.

Una vez llegados a éste punto hay que trabajar para mejorar los parámetros que damos al modelo para aprender de los datos pasados. Ésto incluye los períodos de tiempo pasados y futuros, el número de iteraciones o la cantidad de datos que puede ver el modelo para realizar las predicciones.

Hay mucha literatura acerca de como configurar tanto los parámetros de la red como la propia estructura de la red. No pretendemos condensar toda esa información en éste post, pero esperemos que sirva de introducción al apasionante mundo del machine learning y sus posiblidades.