Quant500 Academy

7. Evaluar modelos: persistencia y residuos

Nivel 8: Series Temporales - Estacionariedad

El objetivo de la predicción de series temporales es hacer predicciones precisas sobre el futuro. Los métodos rápidos y potentes en los que confiamos habitualmente en machine learning, como las particiones aleatorias de entrenamiento y prueba o la validación cruzada k-fold, no funcionan con datos temporales, porque ignoran el componente temporal inherente al problema. Entrenar con datos de 2022 para predecir 2018 es indefendible.

En predicción de series temporales, esta evaluación de modelos sobre datos históricos se llama backtesting. Hay tres métodos principales: una partición entrenamiento-prueba que respete el orden temporal; múltiples particiones sucesivas que respeten el orden temporal; y validación walk-forward, en la que el modelo puede actualizarse cada vez que llega una observación nueva.

X = Re
train_size = int(len(X) * 0.66)
train, test = X[0:train_size], X[train_size:len(X)]

print('Observations: %d' % (len(X)))
print('Training Observations: %d' % (len(train)))
print('Testing Observations: %d' % (len(test)))

Modelo de persistencia. Establecer una referencia es esencial en cualquier problema de predicción de series temporales, porque te dice cómo de bien van a comportarse realmente todos los demás modelos. Aquí desarrollamos una predicción de persistencia que usaremos como nivel de referencia. Los pasos son: transformar el conjunto univariante en un problema supervisado, establecer los conjuntos de entrenamiento y prueba, definir el modelo de persistencia, hacer la predicción y establecer el rendimiento de referencia, y revisar el ejemplo completo con su gráfico.

# conjunto con retardo
values = pd.DataFrame(Re.values)
Re2 = pd.concat([values.shift(1), values], axis=1)
Re2.columns = ['t', 't+1']
Re2 = Re2[1:]

X = Re2.values
train_size = int(len(X) * 0.66)
train, test = X[1:train_size], X[train_size:]
train_X, train_y = train[:, 0], train[:, 1]
test_X, test_y = test[:, 0], test[:, 1]

def model_persistence(x):
    return x

predictions = []
for x in test_X:
    yhat = model_persistence(x)
    predictions.append(yhat)

rmse = np.sqrt(mean_squared_error(test_y, predictions))
print('Test RMSE: %.3f' % rmse)

El modelo devuelve literalmente lo que recibe: la predicción para mañana es el valor de hoy. Cualquier modelo sofisticado que no bata esta referencia no vale nada.

Errores residuales de predicción. Los errores de predicción en series temporales se llaman errores residuales o residuos. Un residuo se calcula como el resultado esperado menos la predicción. Explorar con cuidado los residuos puede decirte mucho sobre tu modelo e incluso sugerir mejoras. El primer gráfico consiste en mirar los residuos a lo largo del tiempo como un gráfico de línea: esperaríamos que fuese aleatorio alrededor del valor 0, sin tendencia ni estructura cíclica.

residuals = [test_y[i] - predictions[i] for i in range(len(predictions))]
residuals = pd.DataFrame(residuals)

residuals.plot()
residuals.describe()
residuals.hist()
residuals.plot(kind='kde')
qqplot(residuals, line='r'); plt.show()

Podemos calcular estadísticos descriptivos sobre los residuos. Interesa sobre todo su media: un valor cercano a cero sugiere que las predicciones no tienen sesgo, mientras que valores positivos o negativos indican sesgo en una u otra dirección. Conocer ese sesgo es útil porque puede corregirse directamente antes de usar o evaluar las predicciones.

Autocorrelación de los residuos. No esperaríamos que hubiese correlación entre los residuos, lo que se manifestaría en autocorrelaciones por debajo del umbral de significación (las líneas horizontales discontinuas del gráfico). Una autocorrelación significativa en el gráfico de residuos sugiere que el modelo podría hacer un trabajo mejor incorporando la relación entre las observaciones y sus observaciones retardadas, es decir, autorregresión.

autocorrelation_plot(residuals)
plot_acf(residuals, lags=20); plt.show()

Paseo aleatorio. Un paseo aleatorio es un modelo en el que el valor de la variable sube o baja con igual probabilidad en cada instante: el valor esperado de la observación actual es igual a la observación anterior. Dicho en código, el valor de hoy es el de ayer más un término de ruido blanco. Si la serie que modelamos es un paseo aleatorio, es impredecible, porque literalmente es un paseo al azar. Su importancia es que no es estacionaria: aunque la media del paseo aleatorio siga siendo cero, la covarianza sí depende del tiempo. En concreto, la covarianza es igual a la varianza multiplicada por el tiempo, de modo que a medida que el tiempo avanza también lo hace la varianza.

np.random.seed(1)
n_samples = 1000
x = w = np.random.normal(size=n_samples)
for t in range(n_samples):
    x[t] = x[t-1] + w[t]

_ = tsplot(x, lags=30)          # claramente no estacionaria
_ = tsplot(np.diff(x), lags=30) # la primera diferencia debe ser ruido blanco

Si el modelo de paseo aleatorio es un buen ajuste, la resta del valor de hoy menos el de ayer debe ser exactamente ruido blanco: las primeras diferencias de un paseo aleatorio son un proceso de ruido blanco. Aplicándolo a datos financieros reales:

_ = tsplot(np.diff(df), lags=30)   # primera diferencia del S&P 500

Se parece bastante a ruido blanco, pero aparece cierta correlación serial significativa en los gráficos ACF y PACF. Además, la forma de los gráficos QQ y de probabilidad indica que el proceso está cerca de la normalidad pero con colas pesadas. Eso significa que deberían existir modelos mejores para explicar el precio del S&P 500.

El resto de Quant500

Lo que estás aprendiendo, aplicado al índice todos los días:

La cartera del modelo, cada semana en tu correo

Cada sábado, su semana en un minuto: cómo fue, lo mejor y lo peor, y qué directivos compraron con su dinero. Y el día que el modelo se rehace, qué entra y qué sale. Es una simulación, no un consejo. Como mucho un correo a la semana, sin publicidad y sin cuenta; te das de baja con un clic. Lo hace una persona: Pol Gracia.

Cartera modelo con resultados simulados, no una recomendación personalizada. Tu correo se usa solo para esto: privacidad.

Detrás de la Academia y del resto de Quant500 hay una sola persona: Pol Gracia Serrahima.