Proyecto de redes neuronales¶

Predicción del precio de una acción utilizando RNN y LSTM¶

Autor: Luis Fernando Apáez Álvarez


In [1]:
# Importaciones necesarias
import yfinance as yf
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import MinMaxScaler
from keras.models import Sequential
from keras.layers import Dense, LSTM, Dropout
from keras.callbacks import ModelCheckpoint
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

# Guardar/cargar modelos
import joblib 

# Estilo de graficacion
# from jupyterthemes import jtplot
# jtplot.style()
plt.style.use('seaborn')

Realizaremos un ajuste de la tendencia de los precios de apertura (Open) de las acciones de Alphabet (google) para agosto del 2022 hasta el 31 de octubre del 2022, para lo cual utilizaremos 4 características:

  • Open. Daremos los mismos precios con los cuales estaremos haciendo las predicciones, debido a que utilizamos una red neuronal recurrente.
  • High: Precio más alto alcanzado ese día.
  • Low: Precio más bajo alcanzado ese día.
  • Close: Precio de cierre.

Definimos los conjuntos de entrenamiento:

In [2]:
# Datos
goog = yf.Ticker('aapl')
data = goog.history(interval='1d', start='2016-01-01', end='2021-05-10').reset_index()
# Instanciamos y configuramos el escalador. Escalaremos los datos
# al intervalo [0,1]
sc = MinMaxScaler(feature_range = (0,1))
# Preconjunto de entrenamiento: Precios del 2016-01-01 al 2021-05-10'
training_set = data.loc[:, ['Open', 'High', 'Low', 'Close']].values
# Realizamos el escalado de los datos
training_set_escaled = sc.fit_transform(training_set)
# Definicion del conjunto de entrenamiento
x_train = []
y_train = []
for i in range(60, data.shape[0]):
    x_train.append(training_set_escaled[i-60:i, 0:4])
    y_train.append(training_set_escaled[i,0])
# Convertimos las listas anteriores en arrays
x_train, y_train = np.array(x_train), np.array(y_train)
In [3]:
print('Número total de registros: ', data.shape[0])
print('Forma del array x_train: ', x_train.shape)
Número total de registros:  1346
Forma del array x_train:  (1286, 60, 4)

donde:

exp1.PNG

x_train es una lista de arrays. Cada array contempla un día fijo y considera los 60 días anteriores a ese día fijo, lo anterior debido a que la red estará "viendo" la información de 60 días atrás para cada día. Luego, y_train el valor del día siguiente dependiendo del array en x_train; por ejemplo, el último array en x_train tiene enla última fila los precios para el 10 de mayo del 2021, entonces la entrada correspondiente a ese array en y_train tiene el precio de apertura para la fecha del 11 de mayo del 2021, y así sucesivamente.

Después, definimos la arquitectura de la red neuronal y la entrenamos con los datos anteriores:

In [7]:
# Arquitectura de la red:
# Guardaremos el mejor modelo
path = 'best_model.hdf5'
c = ModelCheckpoint(filepath = path, monitor = "loss", save_best_only = True, save_freq="epoch")
callbacks = [c]        

model = Sequential()
model.add(LSTM(units=60, return_sequences=True, input_shape=(x_train.shape[1], x_train.shape[2])))
model.add(Dropout(rate=0.2))
model.add(LSTM(units=60, return_sequences=True))
model.add(Dropout(rate=0.2))
model.add(LSTM(units=60, return_sequences=True))
model.add(Dropout(rate=0.2))
model.add(LSTM(units=60, return_sequences=False))
model.add(Dropout(rate=0.2))
model.add(Dense(units=1))

# Compilacion del modelo
model.compile(optimizer='adam', loss='mean_squared_error')

# Entrenamiento
model.fit(x_train, y_train, epochs=140, batch_size=32, callbacks=callbacks, verbose=False)
Out[7]:
<keras.callbacks.History at 0x18fe2407760>
In [8]:
# Valor de la funcion de perdida sobre el conjunto de entrenamiento:
model.evaluate(x_train, y_train)
41/41 [==============================] - 3s 28ms/step - loss: 1.6686e-04
Out[8]:
0.00016686141316313297

In [3]:
# Con el siguiente codigo podemos cargar el mejor modelo
# obtenido en el entrenamiento
model = Sequential()
model.add(LSTM(units=60, return_sequences=True, input_shape=(x_train.shape[1], x_train.shape[2])))
model.add(Dropout(rate=0.2))
model.add(LSTM(units=60, return_sequences=True))
model.add(Dropout(rate=0.2))
model.add(LSTM(units=60, return_sequences=True))
model.add(Dropout(rate=0.2))
model.add(LSTM(units=60, return_sequences=False))
model.add(Dropout(rate=0.2))
model.add(Dense(units=1))
model.load_weights('best_model.hdf5')
model.compile(optimizer='adam', loss='mean_squared_error')
model.evaluate(x_train, y_train)
41/41 [==============================] - 4s 32ms/step - loss: 2.3006e-04
Out[3]:
0.00023006116680335253

In [18]:
goog.history(interval='1d', start='2021-05-11', end='2022-11-02').shape
Out[18]:
(374, 7)

Procedemos a definir una función mediante la cual ajustaremos la tendencia y haremos la predicción del precio de la acción para el día siguiente

In [20]:
def index_predict(clave, fecha_inicio, fecha_fin, fecha_pred, c1, c2, c3):
    """Función que calcula las predicciones de los precios de apertura
    para la compañía de clave @clave, de la fecha @fecha_inicio a la fecha
    @fecha_fin. Con esas predicciones se realiza el ajuste de la tendencia
    de la serie de tiempo de los precios de apertura.
    El parámetro @fecha_pred debe ser la fecha inmediata siguiente (en días hábiles)
    de la fecha @fecha_fin, con lo cual compararemos el precio de predicción calculado
    por la red y el precio real de ese día. Lo anterior lo haremos mediante un dataframe
    el cual albergará los precios reales y los precios predichos. Realmente, para el
    el fin de la predicción, sólo nos interesará el último registro de dicho dataframe, 
    en el cual tenemos el precio real para la fecha @fecha_pred y el precio predicho
    para ese día.
    Para los demás parámetros:
    * c1: color del histograma.
    * c2: color del gráfico de líneas para las diferencias (df['diff']); además
          color del gráfico de la serie de tiempo.
    * c3: color de la línea punteada para el gráfico de líneas de las diferencias;
          además, color del ajuste de la tendencia hecha por la red."""
    # Obtencion de la informacion
    goog = yf.Ticker(clave)
    # Instanciamos y configuramos
    sc = MinMaxScaler(feature_range = (0,1))
    # Conjunto de prueba:
    # Preeliminares
    dataset_test = goog.history(interval='1d', start=fecha_inicio, end=fecha_fin)
    inputs = dataset_test.loc[:, ['Open', 'High', 'Low', 'Close']].values
    # Escalado de los datos
    # inputs = inputs.reshape(-1, 1)
    inputs = sc.fit_transform(inputs)
    # Definicion del conjunto de prueba. La explicacion es totalmente analoga
    # a la dada para x_train
    x_test = []
    for i in range(60, dataset_test.shape[0]+1):
        x_test.append(inputs[i-60:i, 0:4])
    x_test = np.array(x_test)
    # Reshape necesario para poder ingresar x_test a la red
    x_test = np.reshape(x_test, (x_test.shape[0], x_test.shape[1], x_test.shape[2]))
    # Valores reales del @fecha_inicio hasta @fecha_pred
    df_real = goog.history(interval='1d', start=fecha_inicio, end=fecha_pred)
    # Escogemos los registros a partir de la fila 60. La red predecira los precios
    # a partir de la fila 60 y hasta la fecha en @fecha_pred. Asi, df_pruebas
    # y precio_pred tienen el mismo numero de filas
    df_pruebas = pd.DataFrame(df_real.values[60:, 0:4]).rename(columns={0: 'Open'})
    # Valores predichos:
    # EL ultimo array de x_test tiene como ultimo registro el precio para la fecha
    # @fecha_fin, de modo que se predecira el precio del dia inmediato siguiente, el
    # cual corresponde a la fecha @fecha_pred
    precio_pred = model.predict(x_test)
    # auxiliar para regresar la transformacion del escalado. Lo cual es necesario pues
    # el esacalado se ajusto sobre los datos de las 4 columnas. Como precio_pred
    # solo tiene una columna, lo que haremos a continuacion sera agregar 3 columnas
    # de ceros
    extends = np.zeros((len(precio_pred), 4))
    extends[:,2] = precio_pred[:,0]
    # Regresamos a la escala original y solo consideramos la columna para
    # precio_pred
    precio_pred = sc.inverse_transform(extends)[:,2]
    precio_pred = precio_pred.reshape(-1,1)
    # Creamos un dataframe:
    # compararemos los precios reales con los predichos
    df = pd.DataFrame(df_pruebas['Open'].values).rename(columns={0: 'real'})
    df['pred'] = precio_pred
    # Calculamos las diferencias entre los precios reales y las predicciones
    df['diff'] = df['real'] - df['pred']
    # Calculamos el porcentaje de error
    df['error'] = (abs(df['diff']) * 100) / df['real']
    # Mensaje de salida:
    df_s = df.describe()
    print()
    print('Información de los errores:')
    print(f'Valor real: {df.iloc[-1][0]}\nValor predicho: {df.iloc[-1][1]}')
    print(f'Error promedio: {df_s.iloc[1][3]}')
    print(f'Error máximo: {df_s.iloc[-1][3]}\nError mínimo: {df_s.iloc[3][3]}')
    print(f'Cuartil 25%: {df_s.iloc[4][3]}\nCuartil 50%: {df_s.iloc[5][3]}')
    print(f'Cuartil 75%: {df_s.iloc[6][3]}')
    print()
    # values
    values = [df.iloc[-1][0], df.iloc[-1][1], df.iloc[1][3], df.iloc[-1][3],
             df.iloc[3][3], df.iloc[4][3], df.iloc[5][3], df.iloc[6][3]]
    # Graficos:
    # * Histograma de la frecuencia del porcentaje de los errores
    plt.figure(figsize=(14,8))
    plt.subplot(2,2,1)
    plt.hist(data=df, x='error', color=c1, rwidth=0.85)
    plt.title('Histograma del porcentaje de los errores', size=18)
    plt.ylabel('Frecuencia')
    # * Grafico de lineas de las diferencias
    plt.subplot(2,2,2)
    plt.plot(df.values[-18:,2], color=c1)
    plt.plot([0,17.5], [0,0], "x--", color=c3)
    plt.title('Diferencias entre precios reales y predichos', size=18)
    # * Grafico de la serie de tiempo con los valores reales y el ajuste
    # de la tendencia calculado mediante las predicciones hechas por la red
    plt.subplot(2,1,2)
    plt.plot(df['real'], color=c2, label='Valores reales')
    plt.plot(df['pred'], color=c3, label='Valores predichos')
    plt.title('Ajuste de la tendencia en 60 días hasta el ' + fecha_fin, size=18)
    plt.xlabel('Número-día')
    plt.ylabel('Open ($)')
    plt.legend()
    plt.show()
    # Regresamos el dataframe del contraste entre precios reales y predicciones.
    return df

Tenemos entonces que toda la información de los precios hasta cierto día (digamos $k$) nos darán las predicciones para realizar el ajuste de la tendencia. Luego, el día $k$ nos dará la predicción para el precio de apertura del día $k+1$, de modo que el último registro del dataframe df nos dará el contraste entre el precio real para el día $k+1$ con el precio predicho por la red.

Aplicación del modelo¶

Para google¶

In [21]:
df_google = index_predict('goog', '2021-05-11', '2022-11-01', '2022-11-02',
                         c1='#FF3206', c2='#FE5F00', c3='#FE8800')
10/10 [==============================] - 0s 33ms/step

Información de los errores:
Valor real: 95.58999633789062
Valor predicho: 94.21456609220274
Error promedio: 2.047309869861606
Error máximo: 12.139117906609947
Error mínimo: 0.0377950619278667
Cuartil 25%: 0.9805901546957925
Cuartil 50%: 1.7514528562013338
Cuartil 75%: 2.781113640880524

Apple¶

In [15]:
df_apple = index_predict('aapl', '2021-05-11', '2022-11-01', '2022-11-02',
                         c1='#0075FA', c2='#6606FF', c3='#00EDFA')
10/10 [==============================] - 0s 33ms/step

Información de los errores:
Valor real: 152.90634972825453
Valor predicho: 148.379840424398
Error promedio: 1.6883013265567774
Error máximo: 5.260354565219102
Error mínimo: 0.008761468537259438
Cuartil 25%: 0.9297047741548818
Cuartil 50%: 1.5841167190701282
Cuartil 75%: 2.371592810598173

Microsoft¶

In [16]:
df_microsoft = index_predict('msft', '2021-05-11', '2022-11-01', '2022-11-02',
                         c1='#C95CFF', c2='#FF5CE4', c3='#F9006A')
10/10 [==============================] - 0s 32ms/step

Información de los errores:
Valor real: 233.10307007565976
Valor predicho: 227.2700350437432
Error promedio: 1.705739315295022
Error máximo: 6.450203363443842
Error mínimo: 9.751466482708785e-05
Cuartil 25%: 0.9700206389952886
Cuartil 50%: 1.5266364994855026
Cuartil 75%: 2.308444211844514

Meta¶

In [19]:
df_meta = index_predict('meta', '2021-05-11', '2022-11-01', '2022-11-02',
                         c1='#064AFF', c2='#FF06EC', c3='#06D3FF')
10/10 [==============================] - 0s 34ms/step

Información de los errores:
Valor real: 98.22000122070312
Valor predicho: 105.6927645139308
Error promedio: 2.693205884777734
Error máximo: 26.034242222404306
Error mínimo: 0.013240309456469498
Cuartil 25%: 1.1775827982261753
Cuartil 50%: 2.2205602394150237
Cuartil 75%: 3.5304846161199888