avatarOscar Arzamendia

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

5257

Abstract

pueden ocurrir varias ideas para crear nuevos atributos a partir de los que ya existen. Por simplicidad, calculemos solamente las siguientes columnas.

  • holiday_weekday: si el feriado cayó entre semana
  • holiday_weekend: si el feriado cayó un fin de semana
  • isweekday: si la fecha es un día entre semana (Lunes a Viernes)
  • isweekend: si la fecha es fin de semana (Sábado o Domingo)
  • inbetween25and5: los sueldos generalmente son pagados durante esos días</p><div id="200f"><pre>data_df[<span class="hljs-string">'isweekday'</span>] = [<span class="hljs-number">1</span> if d >= <span class="hljs-number">0</span> and d <= <span class="hljs-number">4</span> else <span class="hljs-number">0</span> for d in data_df.index.dayofweek] data_df[<span class="hljs-string">'isweekend'</span>] = [<span class="hljs-number">0</span> if d >= <span class="hljs-number">0</span> and d <= <span class="hljs-number">4</span> else <span class="hljs-number">1</span> for d in data_df.index.dayofweek] data_df[<span class="hljs-string">'inbetween25and5'</span>] = [<span class="hljs-number">1</span> if d >= <span class="hljs-number">25</span> or d <= <span class="hljs-number">5</span> else <span class="hljs-number">0</span> for d in data_df.index.day] data_df[<span class="hljs-string">'holiday_weekend'</span>] = [<span class="hljs-number">1</span> if (we == <span class="hljs-number">1</span> and h not in [np.nan]) else <span class="hljs-number">0</span> for we,h in data_df[[<span class="hljs-string">'isweekend'</span>,<span class="hljs-string">'Holiday'</span>]].values] data_df[<span class="hljs-string">'holiday_weekday'</span>] = [<span class="hljs-number">1</span> if (wd == <span class="hljs-number">1</span> and h not in [np.nan]) else <span class="hljs-number">0</span> for wd,h in data_df[[<span class="hljs-string">'isweekday'</span>,<span class="hljs-string">'Holiday'</span>]].values]</pre></div><p id="16fd">Apliquemos también la codificación one-hot en la columna ‘Holiday’.</p><div id="b8e6"><pre><span class="hljs-attr">data_df</span> = pd.get_dummies(data_df, columns=[<span class="hljs-string">'Holiday'</span>], prefix=[<span class="hljs-string">'holiday'</span>], dummy_na=<span class="hljs-literal">True</span>)</pre></div><figure id="81b5"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*iMx4JIgv6xQWtq0j11CItg.png"><figcaption>feature-engineered data_df</figcaption></figure><h2 id="86c9">¡¿Podemos predecir ya por favor?!</h2><figure id="31ed"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*o8enGAyaVOaSK-qxh8ps4Q.jpeg"><figcaption></figcaption></figure><p id="d46f">Ya casi, ya casi… Primero tenemos que dividir nuestros datos en datos entrenamiento y datos de prueba. Ya saben, para seguir buenas prácticas y evitar <i>overfiting</i> ;)</p><p id="0d90">No podemos simplemente aplicar <i>k-folding</i> para dividir nuestro conjunto de datos entre datos de entrenamiento y datos de prueba ya que para la ST necesitamos tener en cuenta el factor de tiempo. Existen ciertas técnicas que podemos aplicar, entre ellas:</p><ol><li><b>División de Entrenamiento-Prueba</b>, que respete el orden temporal de las observaciones.</li><li><b>Múltiples divisiones de Entrenamiento-Prueba</b>, que respeten el orden temporal de las observaciones.</li><li><b>Validación Walk-Forward</b>, donde el modelo pueda actualizarse cada instante de tiempo que reciba datos nuevos.</li></ol><p id="63f9">En este caso, utilizaremos la número uno. Los puntos de datos desde el comienzo de la serie hasta febrero de 2019 servirán como datos de entrenamiento. El resto de los puntos de datos, serán utilizados para las pruebas.</p><h1 id="1c95">Generando y Visualizando Predicciones</h1><div id="a4be"><pre><span class="hljs-attribute">result_daily</span> = my_train_sarimax(data_df[:'<span class="hljs-number">2019</span>-<span class="hljs-number">02</span>-<span class="hljs-number">28</span>'], i_order=(<span class="hljs-number">2</span>,<span class="hljs-number">1</span>,<span class="hljs-number">2</span>), i_freq='D', i_seasonorder=(<span class="hljs-number">2</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">12</span>))</pre></div><p id="dcac">En el bloque de arriba, los datos de entrenamiento ‘data_df[:’2019–02–28’] se pasan a la función.</p><p id="4e03">Vale la pena destacar que la primera columna del <i>dataframe </i>debe contener los valores a predecir. El resto de las columnas son variables exógenas (es decir, feriados y demás atributos agregados).</p><p id="4b50">La frecuencia del <i>dataframe </i>es dada en el argumento ‘i_freq’. Los argumentos ‘i_order’ y ’i_seasonorder’ especifican los parámetros requeridos para entrenar el modelo. Verificá la documentación de SARIMAX para saber más sobre estos parámetros.</p><p id="a45c">La definición de la función my_train_sarimax() es cuanto sigue.</p><figure id="aa6a"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*v1WY1npnAvHN9wsAb-kfMQ.png"><figcaption></figcaption></figure><p id="7f5c">Ahora es el momento de validar nuestras predicciones. Para hacer eso vamos a usar una función para recuperar los valores predichos y luego los compararemos con los valores reale

Options

s de nuestros datos de prueba.</p><div id="94b9"><pre><span class="hljs-attribute">ypred</span>, ytruth = compare_pred_vs_real(result_daily, data_df, ‘<span class="hljs-number">2019</span><span class="hljs-number">03</span><span class="hljs-number">01</span>’, exog_validation=data_df[‘<span class="hljs-number">2019</span><span class="hljs-number">03</span><span class="hljs-number">01</span>’:].iloc[:,<span class="hljs-number">1</span>:])</pre></div><p id="67e9">Cabe mencionar que se debe proporcionar al modelo las variables exógenas para el periodo de tiempo a predecir. Recordá que estas son variables externas al modelo y éste las necesita para hacer las predicciones.</p><p id="7099">Si miramos a la definición de ‘compare_pred_vs_real()’, podemos ver que las predicciones son realizadas con la función ‘get_prediction()’. Los valores pueden ser extraídos utilizando el método ‘predicted_mean’.</p><figure id="f467"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*mQEai1xPe_ovv3PdFz4ylg.png"><figcaption>Desempeño del modelo</figcaption></figure><p id="0139">Podemos decir que nuestro modelo tiene un desempeño decente en cuanto a el ECM (Error Cuadrático Medio) y el RECM (Raíz del Error Cuadrático Medio). Veamos qué tan alejadas están nuestras predicciones en comparación a la <i>cantidad real de artículos vendidos</i>.</p><div id="00f9"><pre><span class="hljs-attribute">ypred - ytruth</span></pre></div><figure id="3fbd"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*UvSRTGvne6ZY58yc432oIQ.png"><figcaption>predicciones menos valores reales</figcaption></figure><p id="3198">Pero esperá… ¿Por qué vemos valores decimales? La cantidad de artículos vendidos debería ser siempre un número entero.</p><h1 id="0ffe">(Des-)transformando predicciones</h1><p id="535c">Acordate de que hicimos una transformación logarítmica y luego aplicamos una diferenciación a nuestro conjunto de datos. Para poder ver los números reales que nuestro modelo estima que se venderán, debemos revertir esas transformaciones.</p><p id="d1c0">La fecha original se perdió a causa de la diferenciación. Necesitamos completar este valor faltante a partir de ‘datos_df’. Después tenemos que anexar ‘y_pred’ a todas las fechas <i>anteriores a la predicción</i>. Estas fechas también vienen de ‘data_df’. Una vez que terminamos con todo eso podemos revertir la diferenciación con cumsum() y luego aplicamos exp() para revertir la transformación logarítmica.</p><div id="c904"><pre><span class="hljs-comment">#Crear serie con fechas que fueron eliminadas en la diferenciación</span> <span class="hljs-attribute">restore_first_values</span> = pd.Series([<span class="hljs-number">6</span>.<span class="hljs-number">008813</span>], index=[pd.to_datetime(‘<span class="hljs-number">2014</span><span class="hljs-number">01</span><span class="hljs-number">01</span>’)])</pre></div><div id="d258"><pre>#Obtener valores <span class="hljs-keyword">que</span> <span class="hljs-keyword">la</span> predicció<span class="hljs-keyword">n</span> <span class="hljs-keyword">no</span> tiene missing_part = data_df[‘cantidad’][:’2019–02–28'] rebuilt = restore_first_values.<span class="hljs-keyword">append</span>(missing_part).<span class="hljs-keyword">append</span>(ypred)</pre></div><div id="6eca"><pre><span class="hljs-comment">#Revertir diferenciación:</span> <span class="hljs-attr">rebuilt</span> = rebuilt.cumsum()</pre></div><div id="f663"><pre>#Revertir la transformación logarítmica: rebuilt = <span class="hljs-built_in">np</span>.<span class="hljs-built_in">exp</span>(rebuilt).<span class="hljs-built_in">round</span>() # <span class="hljs-built_in">apply</span> <span class="hljs-built_in">round</span>() to have integers</pre></div><p id="83fa">¡Vamoos! ¡Por fin podemos ver nuestros valores predichos y comprarlos con los valores reales!</p><div id="148f"><pre><span class="hljs-comment"># Verificar cuán lejos están las predicciones de los valores reales</span> <span class="hljs-attribute">rebuilt</span>['<span class="hljs-number">2019</span>-<span class="hljs-number">03</span>-<span class="hljs-number">01</span>':] - ventas_df['cantidad']['<span class="hljs-number">2019</span>-<span class="hljs-number">03</span>-<span class="hljs-number">01</span>':]</pre></div><figure id="7a50"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*NQ-_qFAH5l5-NP6X2LbJFw.png"><figcaption>parece ser que acertamos la mayoría de las veces :)</figcaption></figure><h1 id="1707">Comentarios finales</h1><p id="8c2f">Por favor tené en cuenta que se pueden utilizar varios métodos para lograr la estacionaridad en una ST y que SARIMAX no es el único modelo existente que realiza predicciones en series de tiempo. Además, un mejor ajuste de parámetros puede mejorar la precisión del modelo.</p><p id="e261">No dudes en revisar el código completo para esta guía en mi <a href="https://github.com/osbarge/salesforecaster">repo</a> de github.</p><p id="5ffa">Gracias por leer.</p><p id="67f1"><i>Esta es una traducción de mi artículo original en inglés: <a href="https://towardsdatascience.com/time-series-forecasting-a-getting-started-guide-c435f9fa2216">Time Series Forecasting — A Getting Started Guide</a>.</i></p></article></body>

Predicción con Series de Tiempo - Una guía inicial

Introducción

Cuando empecé a escribir esta publicación tuve la intención de solamente explicar cómo hacer predicciones con una serie de tiempo (ST) “sencilla”, es decir, una serie de tiempo univariante. Sin embargo, en el proyecto que me tocó trabajar, la parte complicada era que la predicción debía hacerse en conjunto con múltiples variables. Por esta razón, decidí acercar esta guía un poco más a la realidad y usar una serie de tiempo multivariante.

Primero aclaremos algunos conceptos…

Una Serie de Tiempo Multivariante es una serie de tiempo con más de una variable que depende del tiempo. Cada variable depende de sus valores pasados, pero a la vez también tiene cierto grado de dependencia con otras variables y esta dependencia es tenida en cuenta al predecir los valores. Dichas variables pueden ser endógenas o exógenas. Aquí me estaré enfocando en las variables exógenas.

Una variable exógena es aquella cuyo valor es determinado fuera del modelo e impuesta en el modelo. Dicho de otra forma, son variables que afectan el modelo sin ser afectadas por él. Más info sobre variables exógenas aquí.

Se pueden utilizar varios modelos para resolver una tarea como esta, pero SARIMAX es el que emplearemos. SARIMAX se refiere al Promedio Móvil Integrado Autorregresivo Estacional con Regresores Exógenos.

Bueno, ahora vamos a ver los pasos a seguir para construir un predictor de ventas.

Lidiar con una serie de tiempo implica ciertos desafíos, tales como volverla estacionaria. Si querés saber por qué hice algunas transformaciones al dataframe, podés chequear mi publicación anterior. El enfoque de este artículo es el método para la predicción.

En esta oportunidad tenemos dos archivos: uno con los datos de las ventas anteriores y el otro con información de los feriados nacionales. Como podrás imaginarte, la tarea será tratar de predecir la cantidad de ventas combinando estos dos conjuntos de datos. Luego de cargar los archivos, el dataframe terminará viéndose más o menos así:

feriados_df — dataframe de feriados
ventas_df — dataframe de histórico de ventas

La granularidad de ambos conjuntos de datos está al nivel de Día, es decir, las dos columnas “Date” y “Fecha” son índices con una frecuencia diaria. Si queremos ajustar la frecuencia del conjunto de datos, podemos utilizar la siguiente línea:

ventas_df = ventas_df.resample(‘D’).mean() # 'D' por frecuencia diaria

Tenemos que unir ambos conjuntos de datos para alimentar nuestro modelo con todos los datos que tenemos. ‘ventas_df’ tiene la variable que queremos predecir. ‘feriados_df’ contiene nuestras variables exógenas.

Para facilitarnos la vida, es mejor “estacionarizar” ventas_df (volverla una ST estacionaria), antes de unirla con feriados_df. El método que utilicé para hacer la serie más estacionaria consiste en aplicar una transformación logarítmica y una diferenciación. La serie estacionaria la guardé en el dataframe ‘ts_log_diff’.

test_stationarity(ts_log_diff)

Ahora podemos juntar (JOIN) feriados_df y ts_log_diff, este último es nuestro ventas_df transformado.

data_df = ts_log_diff.join(feriados_df, how='left')
data_df.head()
data_df — joined dataframe

Algunas veces después de realizar algunas operaciones con Pandas, nuestro dataframe resultante, pierde su frecuencia. Para arreglar eso, podemos hacer lo siguiente:

data_df = data_df.asfreq('D')

Ahora un poco de Ingeniería de Atributos

A uno se le pueden ocurrir varias ideas para crear nuevos atributos a partir de los que ya existen. Por simplicidad, calculemos solamente las siguientes columnas. - holiday_weekday: si el feriado cayó entre semana - holiday_weekend: si el feriado cayó un fin de semana - isweekday: si la fecha es un día entre semana (Lunes a Viernes) - isweekend: si la fecha es fin de semana (Sábado o Domingo) - inbetween25and5: los sueldos generalmente son pagados durante esos días

data_df['isweekday'] = [1 if d >= 0 and d <= 4 else 0 for d in data_df.index.dayofweek]
data_df['isweekend'] = [0 if d >= 0 and d <= 4 else 1 for d in data_df.index.dayofweek]
data_df['inbetween25and5'] = [1 if d >= 25 or d <= 5 else 0 for d in data_df.index.day]
data_df['holiday_weekend'] = [1 if (we == 1 and h not in [np.nan]) else 0 for we,h in data_df[['isweekend','Holiday']].values]
data_df['holiday_weekday'] = [1 if (wd == 1 and h not in [np.nan]) else 0 for wd,h in data_df[['isweekday','Holiday']].values]

Apliquemos también la codificación one-hot en la columna ‘Holiday’.

data_df = pd.get_dummies(data_df, columns=['Holiday'], prefix=['holiday'], dummy_na=True)
feature-engineered data_df

¡¿Podemos predecir ya por favor?!

Ya casi, ya casi… Primero tenemos que dividir nuestros datos en datos entrenamiento y datos de prueba. Ya saben, para seguir buenas prácticas y evitar overfiting ;)

No podemos simplemente aplicar k-folding para dividir nuestro conjunto de datos entre datos de entrenamiento y datos de prueba ya que para la ST necesitamos tener en cuenta el factor de tiempo. Existen ciertas técnicas que podemos aplicar, entre ellas:

  1. División de Entrenamiento-Prueba, que respete el orden temporal de las observaciones.
  2. Múltiples divisiones de Entrenamiento-Prueba, que respeten el orden temporal de las observaciones.
  3. Validación Walk-Forward, donde el modelo pueda actualizarse cada instante de tiempo que reciba datos nuevos.

En este caso, utilizaremos la número uno. Los puntos de datos desde el comienzo de la serie hasta febrero de 2019 servirán como datos de entrenamiento. El resto de los puntos de datos, serán utilizados para las pruebas.

Generando y Visualizando Predicciones

result_daily = my_train_sarimax(data_df[:'2019-02-28'], i_order=(2,1,2), i_freq='D', i_seasonorder=(2, 1, 1, 12))

En el bloque de arriba, los datos de entrenamiento ‘data_df[:’2019–02–28’] se pasan a la función.

Vale la pena destacar que la primera columna del dataframe debe contener los valores a predecir. El resto de las columnas son variables exógenas (es decir, feriados y demás atributos agregados).

La frecuencia del dataframe es dada en el argumento ‘i_freq’. Los argumentos ‘i_order’ y ’i_seasonorder’ especifican los parámetros requeridos para entrenar el modelo. Verificá la documentación de SARIMAX para saber más sobre estos parámetros.

La definición de la función my_train_sarimax() es cuanto sigue.

Ahora es el momento de validar nuestras predicciones. Para hacer eso vamos a usar una función para recuperar los valores predichos y luego los compararemos con los valores reales de nuestros datos de prueba.

ypred, ytruth = compare_pred_vs_real(result_daily, data_df, ‘20190301’, exog_validation=data_df[‘20190301’:].iloc[:,1:])

Cabe mencionar que se debe proporcionar al modelo las variables exógenas para el periodo de tiempo a predecir. Recordá que estas son variables externas al modelo y éste las necesita para hacer las predicciones.

Si miramos a la definición de ‘compare_pred_vs_real()’, podemos ver que las predicciones son realizadas con la función ‘get_prediction()’. Los valores pueden ser extraídos utilizando el método ‘predicted_mean’.

Desempeño del modelo

Podemos decir que nuestro modelo tiene un desempeño decente en cuanto a el ECM (Error Cuadrático Medio) y el RECM (Raíz del Error Cuadrático Medio). Veamos qué tan alejadas están nuestras predicciones en comparación a la cantidad real de artículos vendidos.

ypred - ytruth
predicciones menos valores reales

Pero esperá… ¿Por qué vemos valores decimales? La cantidad de artículos vendidos debería ser siempre un número entero.

(Des-)transformando predicciones

Acordate de que hicimos una transformación logarítmica y luego aplicamos una diferenciación a nuestro conjunto de datos. Para poder ver los números reales que nuestro modelo estima que se venderán, debemos revertir esas transformaciones.

La fecha original se perdió a causa de la diferenciación. Necesitamos completar este valor faltante a partir de ‘datos_df’. Después tenemos que anexar ‘y_pred’ a todas las fechas anteriores a la predicción. Estas fechas también vienen de ‘data_df’. Una vez que terminamos con todo eso podemos revertir la diferenciación con cumsum() y luego aplicamos exp() para revertir la transformación logarítmica.

#Crear serie con fechas que fueron eliminadas en la diferenciación
restore_first_values = pd.Series([6.008813], index=[pd.to_datetime(‘20140101’)])
#Obtener valores que la predicción no tiene
missing_part = data_df[‘cantidad’][:’2019–02–28']
rebuilt = restore_first_values.append(missing_part).append(ypred)
#Revertir diferenciación:
rebuilt = rebuilt.cumsum()
#Revertir la transformación logarítmica:
rebuilt = np.exp(rebuilt).round() # apply round() to have integers

¡Vamoos! ¡Por fin podemos ver nuestros valores predichos y comprarlos con los valores reales!

# Verificar cuán lejos están las predicciones de los valores reales
rebuilt['2019-03-01':] - ventas_df['cantidad']['2019-03-01':]
parece ser que acertamos la mayoría de las veces :)

Comentarios finales

Por favor tené en cuenta que se pueden utilizar varios métodos para lograr la estacionaridad en una ST y que SARIMAX no es el único modelo existente que realiza predicciones en series de tiempo. Además, un mejor ajuste de parámetros puede mejorar la precisión del modelo.

No dudes en revisar el código completo para esta guía en mi repo de github.

Gracias por leer.

Esta es una traducción de mi artículo original en inglés: Time Series Forecasting — A Getting Started Guide.

Series De Tiempo
Sarimax
Análisis De Datos
Python
Ciencia Y Datos
Recommended from ReadMedium