Cómo se usan los modelos de series de tiempo para proyectar las ventas en una empresa#

Отныне время будет течь по прямой; Шаг вверх, шаг вбок – их мир за спиной. Я сжег их жизнь, как ворох газет – Остался только грязный асфальт.

  • Рок-н-ролл мертв Аквариум

Elaborado en el ejercicio del año sabático autorizado en la UJED

Introducción#

Si lo que buscas es una herramienta para predecir el comportamiento de una variable en el tiempo, esto es lo más cercano que encontrarás.

La frase “series de tiempo” significa dos cosas:

  1. Modelos que analizan la naturaleza de fluctuaciones temporales de una variable (o varias).

  2. Registros regulares de datos de una variable en el tiempo (p. ej. registros mensuales del Producto Interno Bruto).

El supuesto clave de las series de tiempo es que podemos extraer información sobre el comportamiento de nuestros datos sólo con el registro de su comportamiento en el tiempo. Si lo piensas, es un supuesto fuerte. Pero pronto verás que aplicando algunos trucos, este supuesto puede llegar a ser muy razonable.

Para qué se usan las series de tiempo#

Los modelos de series de tiempo son muy populares en todas las ciencias.

Hay aplicaciones en sismología, medicina, epidemiología, e incluso para predecir los rendimientos agrícolas. Pero uno de los usos más populares de las series de tiempo son los negocios.

Los usos en negocios de las series de tiempo incluyen:

  • Proyección de ventas.

  • Predicción de demanda.

  • Finanzas.

  • Energía.

  • Mercados financieros.

  • Optimización de inventarios.

Lo que tienen en común estos usos es que conocer los valores en el futuro es crítico.

En este capítulo veremos los modelos más relevantes para el análisis de series de tiempos. Veremos también cómo comprobar su capacidad de predecir causalidad. Finalmente, veremos aplicaciones específicas de los negocios.

Propiedades de las Series de Tiempo#

Definimos una serie de tiempo como una secuencia \(\{X_t\}\) de observaciones de una variable aleatoria en el tiempo.

Las observaciones \(X_t\) tienen a \(t\) como subíndice, que indica el momento en el tiempo de la observación. Esto nos permite crear modelos sobre el comportamiento de la variable en el tiempo. Un ejemplo es una caminata aleatoria:

\[ X_t = X_{t-1} + \varepsilon, \]

donde \(\varepsilon\) es un ruido blanco.

import numpy as np
import matplotlib.pyplot as plt

# Fijar la semilla del generador de números aleatorios para reproducibilidad
np.random.seed(42)

# Parámetros
N = 100  # Número de pasos en el tiempo
sigma = 1  # Desviación estándar del ruido
X_0 = 0  # Valor inicial

# Inicializar el arreglo para X_t
X_t = np.zeros(N)
X_t[0] = X_0

# Generar el proceso
for t in range(1, N):
    epsilon = np.random.normal(0, sigma)  # Generar el ruido
    X_t[t] = X_t[t-1] + epsilon  # Actualizar el valor de X_t

# Graficar el proceso
plt.figure(figsize=(10, 6))
plt.plot(X_t, label='$X_t$')
plt.xlabel('Paso del tiempo')
plt.ylabel('Valor')
plt.title('Simulación de $X_t = X_{t-1} + \\varepsilon$')
plt.legend()
plt.grid(True)
plt.show()
../../_images/d577345754c19816dff95b696745b74ffa11877a7770a2a1efc659baa4d04788.png

Cualquier parecido con una serie de tiempo real, es mera coincidencia.

Estacionariedad#

En un sentido intuitivo, una serie de tiempo es estacionaria cuando las propiedades estadísticas del proceso que genera la serie de tiempo no cambian en el tiempo.

Hay mucha filosofía detrás de esa definición. Para empezar, tenemos el supuesto de que existe un proceso que genera el comportamiento de la serie de tiempo. Luego hay que notar que esta definición no implica que no existan cambios de la serie en el tiempo, sólo que la forma en que los datos cambian permanecen constantes.

Por eso es un concepto clave: sin estacionariedad, muchos modelos no funcionan.

En el apéndice de este capítulo te dejo las definiciones formales de estacionariedad y sus ideas claves. En resumen:

  • Podemos detectar si un modelo es estacionario usando la prueba Dickey-Fuller: si el p-value de nuestra prueba es inferior a 0.05, entonces nuestra serie es estacionaria.

  • Si nuestra serie no es estacionaria, podemos diferenciarla. La serie se diferencia restando a cada elemento su rezago \(X_t - X_{t-1}\).

El siguiente código hace una simulación de la serie de tiempo \(y_t = \beta_0 + \beta_1 t + \phi y_{t-1} + \varepsilon_t\), así como su diferencia. Se muestran los gráficos correspondientes y los resultados de la prueba Dickey-Fuller, hecha con la paquetería statsmodels.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.stattools import adfuller

# Parámetros del modelo
np.random.seed(42) # Para reproducibilidad
n = 100 # Número de observaciones
beta0 = 0.5 # Intercepto
beta1 = 0.01 # Tendencia
phi = 0.8 # Coeficiente autoregresivo
epsilon = np.random.normal(0, 1, n) # Términos de error

# Generar la serie de tiempo
t = np.arange(n) # Tiempo
y = np.empty(n)
y[0] = beta0 + beta1 + epsilon[0] # Inicializar la primera observación

for i in range(1, n):
    y[i] = beta0 + beta1 * t[i] + phi * y[i-1] + epsilon[i]

# Diferenciar la serie para conseguir estacionariedad
y_diff = np.diff(y)

# Prueba de Dickey-Fuller para la serie original y diferenciada
adf_result_original = adfuller(y)
adf_result_diff = adfuller(y_diff)

# Crear los gráficos
fig, axs = plt.subplots(1, 2, figsize=(14, 6))

# Serie original
axs[0].plot(t, y, label='Serie Original')
axs[0].set_title('Serie de Tiempo No Estacionaria', fontsize=14)
axs[0].set_xlabel('Tiempo', fontsize=12)
axs[0].set_ylabel('Valor', fontsize=12)
axs[0].legend()

# Serie diferenciada
axs[1].plot(t[1:], y_diff, label='Serie Diferenciada', color='orange')
axs[1].set_title('Serie de Tiempo Diferenciada', fontsize=14)
axs[1].set_xlabel('Tiempo', fontsize=12)
axs[1].set_ylabel('Valor', fontsize=12)
axs[1].legend()

plt.tight_layout()
plt.show()

(adf_result_original[0], adf_result_original[1], adf_result_diff[0], adf_result_diff[1])
../../_images/4405571c6b6adf28a1af233cf85d8cb82ea896655eb546b08396aa37038b4e29.png
(-2.297082991922703,
 0.1729104907141269,
 -6.61625846070549,
 6.19646154282778e-09)

Algunos puntos relevantes.

  • Normalmente cuando observas una serie con una tendencia como la que se muestra en el primer modelo, podemos esperar que la serie no sea estacionaria.

  • Por lo general una diferencia debería ser suficiente para lograr estacionariedad. Es posible hacer dos o más diferencias al modelo, pero hay que ser cautelosos, pues podrías hacer la serie extremadamente volátil e introducir patrones artificiales en los datos.

  • Si con una o dos diferencias no logras generar estacionariedad en la serie de tiempo, vale la pena revisar si no existen otros aspectos como cambios estructurales o estacionalidad en la serie. Estos aspectos se ven más adelante con modelos específicos.

El modelo ARIMA es un ejemplo de un modelo que funciona cuando la serie es estacionaria. Es también un modelo popular por su flexibilidad de uso y lo poderoso de sus resultados. Veamos más a fondo.

El modelo ARIMA#

El modelo ARIMA es como tener una bola de cristal que proyecta el futuro con base en el comportamiento en el pasado y nada más.

Una idea muy intuitiva cuando vemos un gráfico de líneas es que podemos simplemente seguir dibujando. Después de todo, tiene mas sentido pensar que la línea seguirá una misma tendencia a que el siguiente número será totalmente aleatorio. Esa es la idea básica detrás del modelo ARIMA.

El modelo ARIMA asume una relación lineal entre las variables y sus rezagos.

Observa el siguiente modelo:

\[ y_t = \phi_0 + \phi_1 y_{t-1} + \varepsilon \]

Nota que es básicamente un modelo de regresión lineal, con la única diferencia de que la variable independiente y la dependiente son la misma. La historia que este modelo cuenta es que el valor de \(X\) en el periodo \(t\) depende de su valor en el periodo \(t-1\).

Este es un modelo AR(1).

Modelos AR(p)#

El predictor más lógico del valor de una variable en el tiempo son sus rezagos.

Dicho de otra manera: la mejor manera de saber cuáles son las ventas del próximo año es observando las ventas del próximo año. Es un modelo sencillo, pero muy poderoso. El modelo AR(\(p\)) asume que el valor de \(y_t\) depende linealmente de los primeros \(p\) rezagos de la serie.

Observa la siguiente simulación de un modelo AR(3).

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# Establecer la semilla para reproducibilidad
np.random.seed(42)

# Número de observaciones
n = 1000

# Coeficientes del proceso AR(3)
phi = np.array([0.5, -0.2, 0.1])

# Término de ruido
sigma = 1
epsilon = np.random.normal(loc=0, scale=sigma, size=n)

# Inicializando la serie temporal
y = np.zeros(n)

# Generando el proceso AR(3)
for t in range(3, n):
    y[t] = phi[0] * y[t-1] + phi[1] * y[t-2] + phi[2] * y[t-3] + epsilon[t]

# Creando un índice de series temporales
dates = pd.date_range(start='2024-01-01', periods=n)

# Convirtiendo a una serie de pandas para graficar
y_series = pd.Series(y, index=dates)

# Graficando
plt.figure(figsize=(14, 6))
plt.plot(y_series)
plt.title('Proceso Simulado AR(3)')
plt.xlabel('Tiempo')
plt.ylabel('Valor')
plt.show()
../../_images/19dbacdbd6654ec6852cabd450dabf4dca2abd17370316b93f7c3d37fc12a59f.png

Estamos haciendo trampa.

Estoy creando una simulación de un modelo donde

\[ y_t = \phi_1 y_{t-1} + \phi_2 y_{t-2} + \phi_3 y_{t-3} + \varepsilon_t \]

La ventaja es que sabemos que \(\phi_1 = 0.5\), \(\phi_2 = -0.2\) y \(\phi_3 = 0.1\), y podemos hacer pruebas con python para aprender a hacer modelos AR.

Estos son los pasos:

Paso #1: Verifica si tu serie de tiempo es estacionaria.#

La estacionariedad es lo que permite que los modelos sean consistentes. Este es el código:

from statsmodels.tsa.stattools import adfuller

# Realizando el test ADF
adf_result = adfuller(y_series)

# Extrayendo el valor p y los valores críticos
p_value = adf_result[1]
critical_values = adf_result[4]

adf_result, p_value, critical_values
((-15.31091470485936,
  4.187673159303107e-28,
  2,
  997,
  {'1%': -3.4369259442540416,
   '5%': -2.8644432969122833,
   '10%': -2.5683158550174094},
  2739.6857321374964),
 4.187673159303107e-28,
 {'1%': -3.4369259442540416,
  '5%': -2.8644432969122833,
  '10%': -2.5683158550174094})

Aprendamos a interpretar esta prueba:

  • La prueba se llama Dickey-Fuller Aumentada (ADF). Se usa para identificar la presencia de raíces unitarias. Identificar estacionariedad directamente requeriría que supiéramos el proceso que genera la serie de tiempo. En nuestro caso lo sabemos porque nosotros lo generamos, pero en la vida real eso es justo lo que queremos estimar.

  • Una serie de tiempo tiene raíces unitarias si se puede representar por el proceso donde las raíces de la ecuación característica son iguales a uno (o están en el círculo unitario de un espacio complejo.

  • Especificación del modelo. La prueba ADF se basa en la estimación del modelo siguiente:

\[ ΔYt=α+βt+γY_{t−1}+δ_1ΔY_{t−1}+δ_2ΔY_{t−2}+⋯+δ_{p−1}ΔY_t−p+1+\varepsilon_t\]
  • La hipótesis nula de la prueba ADF es que la serie de tiempo tiene raíz unitaria (\(\gamma = 0\)), lo que significa que no es estacionaria. La hipótesis alternativa es que la serie de tiempo no es estacionaria (\(\gamma < 0\)).

En otras palabras, como el p-value es muy pequeño (\(p<0.01\)), podemos interpretar que nuestra serie no es estacionaria.

Paso #2: Si la serie no es estacionaria, considera transformarla para hacerla estacionaria.#

Hay transformaciones que resultan naturales a una serie de tiempo. Por ejemplo, el indicador general de la actividad Económica es evidentemente No-estacionario.

# Importar las librerías necesarias
import pandas as pd
import matplotlib.pyplot as plt

# URL de los datos IGAE de INEGI
url = 'https://www.inegi.org.mx/contenidos/programas/igae/2018/tabulados/ori/IGAE_2.xlsx'

# Cargar los datos en un DataFrame de pandas
igae_data = pd.read_excel(url, skiprows=5)

# Mostrar las primeras filas del DataFrame para verificar los datos
print(igae_data.head())

# Paso 1: Eliminar la primera fila que no contiene datos, si aún no se ha hecho
igae_data_cleaned = igae_data.drop(igae_data.index[0])

# Restablecer el índice después de eliminar la fila
igae_data_cleaned.reset_index(drop=True, inplace=True)

# Asumiendo que la fila "Total" es la primera fila después de limpiar
total_series = igae_data_cleaned.iloc[0, 1:].transpose()

# Excluir las columnas que no sean de meses y calcular el periodo
monthly_columns = [col for col in igae_data_cleaned.columns if col not in ['Unnamed: 0', 'Anual.30']]  # Ajustar las columnas excluidas según sea necesario
periods = len(monthly_columns)

# Crear un rango de fechas para el índice
start_year = 1993  # Año de inicio, ajustar según los datos
start_month = 1  # Enero como mes de inicio

dates = pd.date_range(start=pd.Timestamp(year=start_year, month=start_month, day=1),
                      periods=len(total_series),
                      freq='M')

# Crear un DataFrame a partir de la serie para usar el índice de fecha
df = pd.DataFrame(total_series.values, index=dates, columns=['Valor IGAE'])

# Graficar los datos
plt.figure(figsize=(10, 6))
plt.plot(df.index, df['Valor IGAE'])
plt.title('Serie de Tiempo IGAE')
plt.xlabel('Fecha')
plt.ylabel('Valor')
plt.xticks(rotation=45)  # Rotar las fechas para una mejor legibilidad
plt.show()
                                          Unnamed: 0      Enero    Febrero  \
0             Índice de volumen físico base 2018=100        NaN        NaN   
1                                              Total  55.434736  56.456971   
2                              Actividades primarias  58.796198  60.763816   
3                                  111 - Agricultura  54.888804  56.855371   
4                 112 - Cría y explotación de ani...  67.248422  68.977815   

       Marzo      Abril       Mayo      Junio      Julio     Agosto  \
0        NaN        NaN        NaN        NaN        NaN        NaN   
1  58.900549  57.135844  57.891853  57.475466  57.902380  57.123934   
2  55.964953  58.989164  61.762078  63.641132  72.628654  57.488221   
3  50.409267  52.261002  55.582213  57.282877  68.319936  42.898071   
4  67.346044  73.409111  75.161246  77.687091  81.891772  88.914213   

   Septiembre  ...      AbrilR       MayoR      JunioR      JulioR  \
0         NaN  ...         NaN         NaN         NaN         NaN   
1   58.482049  ...  105.871192  107.888729  103.860038  106.992011   
2   54.105257  ...   94.941702  115.586103  107.228853  114.379706   
3   38.338437  ...   88.083718  117.429436  104.395314  113.263263   
4   88.045743  ...  109.899862  112.091931  113.823867  117.068706   

      AgostoR  SeptiembreP  Octubre.31  Noviembre.31  Diciembre.31  Anual.31  
0         NaN          NaN         NaN           NaN           NaN       NaN  
1  106.051382   103.216695         NaN           NaN           NaN       NaN  
2   84.758369    83.453805         NaN           NaN           NaN       NaN  
3   69.893293    68.579728         NaN           NaN           NaN       NaN  
4  117.180183   115.975894         NaN           NaN           NaN       NaN  

[5 rows x 417 columns]
../../_images/5c6ac7d06da3ff1c3ee37e9e4627b706ef18e135541bec74440bea43f47fdb65.png

Con algo de experiencia vas a aprender a notar cuando una serie no es estacionaria sólo al ver el gráfico, pero siempre debes de corroborar tus sospechas haciendo una prueba estadística.

Paso #3: Identifica el orden del rezago \(p\). La siguiente pregunta que tenemos es ¿cuántos rezagos debo usar en mi modelo?#

Para resolver este problema usamos la función de autocorrelación (AFC) y la función de autocorrelación parcial (PAFC). Este es el gráfico que generan.

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

# Creando figuras para los gráficos
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))

# Graficando la Función de Autocorrelación (ACF)
plot_acf(y_series, ax=ax1, lags=40)  # y_series es la serie temporal
ax1.set_title('Función de Autocorrelación (ACF)')

# Graficando la Función de Autocorrelación Parcial (PACF)
plot_pacf(y_series, ax=ax2, lags=40)  # y_series es la serie temporal
ax2.set_title('Función de Autocorrelación Parcial (PACF)')

plt.tight_layout()  # Ajusta automáticamente los subgráficos para que encajen en el área de la figura
plt.show()
/usr/local/lib/python3.9/site-packages/statsmodels/graphics/tsaplots.py:348: FutureWarning: The default method 'yw' can produce PACF values outside of the [-1,1] interval. After 0.13, the default will change tounadjusted Yule-Walker ('ywm'). You can use this method now by setting method='ywm'.
  warnings.warn(
../../_images/2d406a70430938ffe838af2ae7502cefbd76be9543d76c221d51a42216ebe473.png

El truco para identificar el orden del rezago en un modelo AR(\(p\)) es mirar el PAFC. El punto en el que la autocorrelación cae por debajo del nivel de significancia (no está cubierto por el color en el gráfico), es el nivel de rezago que debemos usar.

En este caso el gráfico nos muestra una caída después del tercer rezago, que es lo que esperamos con el modelo que hemos diseñado.

Paso #4: Ejecutar el modelo y mostrar los resultados#

Finalmente estamos listos para ejecutar nuestro modelo AR(3).

from statsmodels.tsa.ar_model import AutoReg

# Ajustando un modelo AR(3)
model = AutoReg(y_series, lags=3)
model_fitted = model.fit()

# Mostrando los resultados
model_results = model_fitted.summary()

model_results
AutoReg Model Results
Dep. Variable: y No. Observations: 1000
Model: AutoReg(3) Log Likelihood -1394.228
Method: Conditional MLE S.D. of innovations 0.980
Date: Wed, 27 Nov 2024 AIC 2798.455
Time: 11:52:47 BIC 2822.979
Sample: 01-04-2024 HQIC 2807.777
- 09-26-2026
coef std err z P>|z| [0.025 0.975]
const 0.0187 0.031 0.601 0.548 -0.042 0.080
y.L1 0.4882 0.032 15.468 0.000 0.426 0.550
y.L2 -0.1837 0.035 -5.298 0.000 -0.252 -0.116
y.L3 0.0851 0.032 2.693 0.007 0.023 0.147
Roots
Real Imaginary Modulus Frequency
AR.1 2.0962 -0.0000j 2.0962 -0.0000
AR.2 0.0312 -2.3674j 2.3676 -0.2479
AR.3 0.0312 +2.3674j 2.3676 0.2479

Lo que podemos observar en los resultados de este modelo:

  • Los coeficientes son efectivamente cercanos a los que hicimos en la simulación, con un valor significativo (p < 0.05)

  • La sección donde dice Real e Imaginario nos ayudan a confirmar que nuestro modelo es estable (i.e. todas las unidades de la ecuación característica están fuera del círculo unitario).

Paso #5: Comparar AIC y BIC para encontrar el mejor modelo#

El criterio de Información de Akaike (AIC) y el Criterio de información Bayesiano son medidas que nos ayudan a identificar el “mejor modelo” en términos de la información que podemos obtener.

La idea es esta: cuando incluimos más variables a nuestro modelo, podemos tener mayor precisión, pero nos arriesgamos a un sobre-ajuste. Veamos que sucede si comparamos AR(3) con los modelos AR(2) y AR(4).

La regla de oro es el que el modelo con AIC y BIC más bajo, gana.

# Ajustando modelos AR(2) y AR(4) y comparando AIC y BIC
model_ar2 = AutoReg(y_series, lags=2).fit()
model_ar4 = AutoReg(y_series, lags=4).fit()

# Extrayendo AIC y BIC para cada modelo
aic_bic_comparison = pd.DataFrame({
    'Model': ['AR(2)', 'AR(3)', 'AR(4)'],
    'AIC': [model_ar2.aic, model_fitted.aic, model_ar4.aic],
    'BIC': [model_ar2.bic, model_fitted.bic, model_ar4.bic]
})

aic_bic_comparison
Model AIC BIC
0 AR(2) 2805.485454 2825.108467
1 AR(3) 2798.455225 2822.978979
2 AR(4) 2794.998432 2824.420916

Interesante.

El modelo AR(4) tiene un nivel menor de AIC, con un BIC ligeramente mayor. Eso quiere decir que, si no supiéramos el modelo real, bien podríamos considerar el modelo AR(4) como viable.

Generalmente, el AIC se enfoca más en la calidad del ajuste, mientras que el BIC añade una penalización más fuerte por la cantidad de parámetros, favoreciendo modelos más simples.

Paso 6: Generar proyecciones#

Finalmente, podemos crear proyecciones a partir de nuestros modelos. En el siguiente código, dividimos la base de datos en datos de entrenamiento (train) y de prueba (test). La idea es comprobar que nuestras proyecciones ayudan a predecir el valor que estamos buscando.

from sklearn.metrics import mean_absolute_error

# Dividiendo los datos en entrenamiento y prueba
train_data = y_series[:int(0.9 * len(y_series))]
test_data = y_series[int(0.9 * len(y_series)):]

# Ajustando el modelo AR(3) al conjunto de entrenamiento
model_train = AutoReg(train_data, lags=3).fit()

# Haciendo predicciones en el conjunto de prueba
predictions = model_train.predict(start=len(train_data), end=len(train_data) + len(test_data) - 1, dynamic=False)

# Calculando el Error Absoluto Medio (MAE)
mae = mean_absolute_error(test_data, predictions)

mae
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
<ipython-input-9-18ecff461b8f> in <module>
----> 1 from sklearn.metrics import mean_absolute_error
      2 
      3 # Dividiendo los datos en entrenamiento y prueba
      4 train_data = y_series[:int(0.9 * len(y_series))]
      5 test_data = y_series[int(0.9 * len(y_series)):]

ModuleNotFoundError: No module named 'sklearn'

Un gráfico nos puede ayudar a tener mayor claridad. Nota que la proyección es sólo una línea horizontal en el valor medio de los datos. Si lo pensamos bien, considerando el comportamiento de los datos, es una predicción razonable!

# Creando un gráfico para comparar los valores reales y las predicciones
plt.figure(figsize=(14, 7))
plt.plot(train_data, label='Training Data')
plt.plot(test_data, label='Actual Value')
plt.plot(predictions, label='Forecast', linestyle='--')
plt.title('AR(3) Forecast vs Actuals')
plt.xlabel('Date')
plt.ylabel('Value')
plt.legend()
plt.show()

Medias Móviles: La MA de ARIMA#

El procedimiento que vimos antes se puede aplicar igual con un modelo más complejo.

En ocasiones, nos enfrentamos a dos problemas:

  • Los rezagos de la variable no son suficientes para explicar su comportamiento.

  • Los rezagos de los errores muestran una tendencia.

Lo curioso es que ambos problemas los podemos solucionar con la inclusión de los rezagos al modelo. Considera el siguiente modelos ARMA(\(p\), \(q\)), que contiene un rezago (\(p = 1\)) y tres rezagos del error (\(q = 3\)).

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.arima.model import ARIMA

# Estableciendo la semilla para reproducibilidad
np.random.seed(42)

# Número de observaciones
n = 1000

# Generando un término de ruido
epsilon = np.random.normal(loc=0, scale=1, size=n)

# Inicializando la serie temporal
y = np.zeros(n)

# Parámetros para el proceso ARMA(1,3)
phi = 0.5  # Coeficiente AR
theta = [0.1, -0.2, 0.3]  # Coeficientes MA

# Generando el proceso ARMA(1,3)
for t in range(4, n):
    y[t] = phi * y[t-1] + epsilon[t] + theta[0] * epsilon[t-1] + theta[1] * epsilon[t-2] + theta[2] * epsilon[t-3]

# Creando un índice de series temporales
dates = pd.date_range(start='2024-01-01', periods=n)

# Convirtiendo a una serie de pandas para graficar y modelar
y_series_arma = pd.Series(y, index=dates)

# Ajustando un modelo ARMA(1,3)
arma_model = ARIMA(y_series_arma, order=(1, 0, 3))
arma_result = arma_model.fit()

# Creando predicciones con el modelo ARMA(1,3) para los últimos 100 puntos de datos
arma_predictions = arma_result.predict(start=n-100, end=n-1)

# Creando un gráfico para comparar los valores reales y las predicciones de ARMA(1,3)
plt.figure(figsize=(14, 7))
plt.plot(y_series_arma[n-100:], label='Actual Values')
plt.plot(arma_predictions, label='ARMA(1,3) Predictions', linestyle='--')
plt.title('ARMA(1,3) Simulation')
plt.xlabel('Date')
plt.ylabel('Value')
plt.legend()
plt.show()

arma_result.summary()

Este modelo se ve más interesante.

Los pasos que seguimos en la sección anterior aplican también aquí. De igual manera, si nos enfrentamos a un modelo ARMA, debemos verificar que sea estacionario, identificar el orden del proceso y hacer nuestras proyecciones.

Una diferencia clave son las funciones ACF y PACF. Para detectar el orden MA(\(q\)) revisamos donde la función ACF corta de manera abrupta.

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

# Plotting ACF and PACF for the ARMA(1,3) model
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))

# ACF
plot_acf(y_series_arma, ax=ax1, lags=40)
ax1.set_title('Autocorrelation Function (ACF) for ARMA(1,3)')

# PACF
plot_pacf(y_series_arma, ax=ax2, lags=40)
ax2.set_title('Partial Autocorrelation Function (PACF) for ARMA(1,3)')

plt.tight_layout()
plt.show()

Lo que podemos observar:

En el gráfico ACF parece haber un rechazo significativo en el primer retardo y luego una disminución exponencial o una disminución sinusoidal amortiguada en los retardos subsiguientes. Esto es típico de un proceso AR(1) o un proceso ARMA con un componente AR(1). El hecho de que el gráfico ACF no se corte después de un número determinado de retardos sugiere la presencia de una componente AR.

El gráfico PACF muestra un rechazo significativo en el primer retardo y rechazos significativos en los retardos tercero y cuarto, lo cual es consistente con un proceso MA(3), ya que el PACF de un proceso MA(q) generalmente muestra rechazos significativos para los primeros \(q\) retardos y luego se corta.

En la práctica la interpretación del ACF y PACF puede ser más arte que ciencia. Se basa en la identificación de “cortes” y rechazos significativos, por lo que una interpretación plausible de estos gráficos sería que estamos mirando un proceso con una componente autoregresiva de orden 1 (AR(1)) y una componente de media móvil de orden 3 (MA(3)).

Apéndice#

Aquí van los temas que requieren una explicación adicional, pero que rompen el ritmo de la explicación principal si me extiendo en el texto.

Proceso Estocástico#

La palabra estocástico significa lo mismo que aleatorio.

La diferencia principal es que estocástico viene del griego, mientras que aleatorio viene del latín. Para definir correctamente lo que es un proceso estocástico necesitamos algunas definiciones adicionales que vienen de la teoría de la probabilidad:

Note

💡 Un espacio probabilístico es una tripla \((\Omega, \mathcal{F}, \mathbb{P})\) donde:

  • \(\Omega\) es un conjunto no vacío conocido como el espacio muestral.

  • \(\mathcal{F}\) es una \(\sigma\)-álgebra (se lee sigma-álgebra) de subconjuntos de \(\Omega\). Es decir, es una familia de subconjuntos cerrados con respecto a la unión contable y complementaria con respecto a \(\Omega\).

  • \(\mathbb{P}\) es una medida de probabilidad definida para todos lo miembros de \(\mathcal{F}\).

Ahora definamos las variables aleatorias

Note

💡 Una variable aletoria es una función \(x:\Omega\to\mathbb{R}\) tal que la imágen inversa de cualquier intervalo \((-\infty,a]\) pertenece a \(\mathcal{F}\), es decir, es una función medible.

Con estas definiciones, podemos llegar a nuestra definición de proceso estocástico:

Note

💡 Un proceso estocástico real es una familia de variables aleatorias reales \(\mathbf{X} = \{x_t(\omega) | t \in T\}\) definidas en el mismo espacio probabilístico \((\Omega,\mathcal{F},\mathbb{P})\). El conjunto \(T\) se le llama el espacio índice de los procesos. Si \(T\subset \mathbb{Z}\), entonces es un proceso estocástico discreto. Si \(T\) es un intervalo de \(\mathbb{R}\) entonces es un proceso estocástico contínuo.

Ruido blanco#

Un ruido blanco es un proceso estocástico que no tiene correlación serial con media cero y varianza constante finita.

Note

De manera formal, un proceso \(\{w_t \}\) es un ruido blanco si:

  • Su primer momento es siempre cero, es decir \(E[w_t] = 0\).

  • Su segundo momento es finito. Es decir, \(E(w_t - \mu)^2]<\infty\).

  • El momento cruzado \(E[w_s w_t]\) es cero, para \(s\neq t\). Es decir, \(\text{cov}(w_s,w_t) = 0\).

Definición formal de estacionariedad#

Stationarity in time series analysis

Raíces unitarias#

Consideremos un proceso autorregresivo de orden \(p\):

\[ y_t = a_0 +a_1y_{t-1} + \cdots + a_py_{t-p} + \varepsilon_t \]

Donde \(\varepsilon_t\) representa un ruido blanco. Podemos reescribir el mismo proceso como

\[ (1-a_1L - \cdots - a_p L^p)y_t = a_0 + \varepsilon \]

donde \(L^i\) es el operador de rezago. La parte entre paréntesis de la izquierda se conoce como la ecuación característica de la serie de tiempo. Consideremos la raíz de esta ecuación:

\[ m^p - m^{p-1}a_1- \cdots - a_p = 0 \]

Si la raíz de la ecuación es \(m =1\), entonces el proceso estocástico tiene raíz unitaria. Esto se suele comprobar con la prueba Dickey-Fuller.

Referencias#

Abdul Jalil, N. H. R., & Özcan, B., & Öztürk, I. (Eds.). (2019). Time Series Analysis (Stationarity, Cointegration, and Causality). En Environmental Kuznets Curve (EKC) (pp. 85-99). Academic Press. https://doi.org/10.1016/B978-0-12-816797-7.00008-4

Como citar este libro#

Cita en APA (7a edición)

García Meza, M. A. (2024). *Inferencia causal para negocios: Una guía práctica con Python*. https://inferenciacausal.com

Cita en MLA (9a edición)

García Meza, Mario A. *Inferencia Causal para Negocios: Una Guía Práctica con Python*. Durango, México, 2024. https://inferenciacausal.com.

Cita en Chicago

García Meza, Mario A. I*nferencia Causal para Negocios: Una Guía Práctica con Python*. Durango, México, 2024. https://inferenciacausal.com.

Espero que este libro te resulte útil.

Si eres economista y deseas escribir tu primer paper de economía, hice este curso gratis por correo justo para tí.

En este curso aprenderás a:

  • Crear objetivos de investigación que tienen sentido, que ningún juez te podrá “tumbar”.

  • Usar causalidad en tus modelos y no sólo seguir una receta de cocina para trabajar con datos.

  • Apoyarte de otras personas y la tecnología para escribir al menos dos papers al año, todos los años, consistentemente y para siempre.

Elaborado en el ejercicio de año sabático UJED