Predictor de precios de mercado - General Motors - Parte I

David Cerezal Landa 19 minutos de lectura

Desde hace tiempo los economistas han estudiado e intentado comprender los movimientos de los precios en la bolsa de valores, debido a que las inversiones en bolsa están sujetas a riesgos, los rendimientos son variables y su existencia es incierta. La predicción de la bolsa de valores es un tema de gran interés, en particular para quienes invierten en ella. Sería muy provechoso poder predecir la tendencia y, si fuera posible, el precio de las acciones, ya que con esta información los inversionistas podrían realizar movimientos apropiados y así ganar dinero.

En este ejercicio hemos querido realizar el estudio de diferentes modelos para intentar realizar la predicción del precio de valor para la compañía General Motor y describir cada uno de los pasos llevados a cabo, desde el análisis de los datos hasta el proceso de evaluación y comparación de las diferentes aproximaciones.

En este trabajo no se pretende llevar a cabo un predictor efectivo, sino más bien el explorar y aprender diferentes técnicas que pueden ser aplicadas para la resolución de problemas de esta índole, somos conscientes de la complejidad de este tipo de predictores de precios, puesto que los precios de valores de mercado pueden estar sujetos a altas variaciones que pueden depender de diferentes indicadores sociales, económicos y políticos, que no se incluyen en las variables del modelo..

En este set de diferentes notebooks trataremos de hacer un análisis de la serie temporal de los precios de cierre de mercado de la compañía General Motors en la bolsa de NY. Como objetivo se fijará crear un modelo de referencia y tratar de mejorarlo usando redes secuenciales.

Los datos serán obtenidos del proveedor de datos https://www.alphavantage.co/ por medio del uso de una librería.

En el primer post, Análisis de la serie temporal, haremos una limpieza inicial, un estudio de las tendencias y por último un estudio sobre la estacionaridad y transformación de esta a una serie estacionaria. El cual será clave para establecer un modelo de referencia correcto y ajustar después la red secuencial.

En un segundo post, Modelos de referencia, estudiaremos principalmente un modelo autorregresivo integrado de promedio móvil o ARIMA, dado que será el que mejor se ajuste a nuestra serie en función del pre-análisis del punto uno. También, se probarán otros métodos como Naïve method y random-forest para ver si nuestras premisas eran correctas.

En un último post, Modelos secuenciales intentaremos crear modelos de redes secuenciales que mejoren estos modelos de referencia.

En el post de hoy trataremos:

1.- Carga de Datos

2.- Análisis de los datos

3.- Estacionariedad

4.- Transformaciones

Carga de Datos

Obtenemos el "symbol" (NYSE:GM) valor del mercado sobre el que haremos el estudio. Hemos hecho el estudio de datos de diferentes valores decidiendonos finalmente por este. "General Motors"

In [0]:
from google.colab import drive
drive.mount('/content/drive')
Go to this URL in a browser: https://accounts.google.com/o/oauth2/auth?client_id=947318989803-6bn6qk8qdgf4n4g3pfee6491hc0brc4i.apps.googleusercontent.com&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=email%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdocs.test%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive.photos.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fpeopleapi.readonly&response_type=code

Enter your authorization code:
··········
Mounted at /content/drive
In [0]:
cd /content/drive/My Drive/RedesSecuenciales
/content/drive/My Drive/RedesSecuenciales
In [0]:
import requests
import json
import matplotlib.pyplot as plt
import seaborn as sns

sns.set()
%matplotlib inline
plt.rcParams['figure.figsize'] = (12, 5)
plt.rcParams['figure.dpi'] = 100

import numpy as np
import pandas as pd
pd.options.mode.chained_assignment = None

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error as sk_mse, mean_absolute_error as sk_mae, mean_squared_log_error as sk_msle

from keras.models import Model, model_from_json
from keras.layers import (Embedding, Conv1D, MaxPooling1D, LSTM, GRU, Dense, Flatten, Input, Dropout,
                          BatchNormalization, TimeDistributed,Reshape, Multiply)
from keras import regularizers
from keras.callbacks import EarlyStopping, ModelCheckpoint
from keras.losses import mean_squared_error
from keras.utils.vis_utils import plot_model
import keras.backend as K
Using TensorFlow backend.
In [0]:
def save_keras_model(export_path, model, model_name, create_folder=False, save_weights=False):
    try:
        if create_folder:
            aux = model_name.split('_')[-1]
            assert aux not in os.listdir(export_path), 'Directory "{0}" already created.'.format(aux)
            os.mkdir(export_path + "/" + aux)
            model_path = export_path + "/" + aux
        
        else:
            model_path = export_path
            
        model_json = model.to_json()
        
        with open(model_path + "/" + model_name + ".json", "w") as json_file:
            json_file.write(model_json)
        
        if save_weights:
            model.save_weights(model_path + "/" + model_name + ".h5")
        
        print("Saved model to disk as {0}/{1}".format(model_path, model_name))
        return True, model_path
    
    except Exception as err:
        print('Save error: {0}'.format(err))
        return False, err


def load_keras_model(import_path):
    try:
        path = import_path + ".json" if not import_path.endswith(".json") else import_path
        print(path)
        
        with open(path, "r") as json_file:
            loaded_model_json = json_file.read()
            loaded_model = model_from_json(loaded_model_json)
            loaded_model.load_weights(path.replace('.json', '.h5'))
        
        print("Loaded model from disk")
        return loaded_model
    
    except Exception as err:
        print("Loading error: {0}".format(err))
        return False

Para la obtencion de los datos es necesaria la instalacion de la libreria alpha_vanatage

In [0]:
#Instalacion de alpha_vantage para poder bajarnos los precios
!pip install alpha_vantage
Collecting alpha_vantage
  Downloading https://files.pythonhosted.org/packages/99/68/f8632f0ded3f8d43a2e607062a628e2e9bff2837d3f19acdf95d960e09b8/alpha_vantage-2.1.1-py3-none-any.whl
Requirement already satisfied: requests in /usr/local/lib/python3.6/dist-packages (from alpha_vantage) (2.21.0)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.6/dist-packages (from requests->alpha_vantage) (2019.6.16)
Requirement already satisfied: chardet<3.1.0,>=3.0.2 in /usr/local/lib/python3.6/dist-packages (from requests->alpha_vantage) (3.0.4)
Requirement already satisfied: urllib3<1.25,>=1.21.1 in /usr/local/lib/python3.6/dist-packages (from requests->alpha_vantage) (1.24.3)
Requirement already satisfied: idna<2.9,>=2.5 in /usr/local/lib/python3.6/dist-packages (from requests->alpha_vantage) (2.8)
Installing collected packages: alpha-vantage
Successfully installed alpha-vantage-2.1.1
In [0]:
#Para la obtencion de las TimeSeries necesitamos una clave que se nos proporciona de forma diaria para
# bajarnos los datos.
from alpha_vantage.timeseries import TimeSeries
API_KEY =  'JBMAO3WLU9Y4JWER'
#Le indicamos que queremos el formato en pandas
ts = TimeSeries(key=API_KEY, output_format='pandas')
In [0]:
data, metadata = ts.get_daily(symbol='NYSE:GM',outputsize='full') #Probamos con General Motors.
print("Datos precios General Motors")
display(data.head(10))

#Salvamos los datos a disco para que podamos trabajar con ellos despues.
Datos precios General Motors
1. open 2. high 3. low 4. close 5. volume
date
2010-11-18 35.0000 35.99 33.8900 34.19 457044300.0
2010-11-19 34.1500 34.50 33.1100 34.26 107842000.0
2010-11-22 34.2000 34.48 33.8100 34.08 36650600.0
2010-11-23 33.9500 33.99 33.1900 33.25 31170200.0
2010-11-24 33.7300 33.80 33.2186 33.48 26138000.0
2010-11-26 33.4100 33.81 33.2100 33.80 12301200.0
2010-11-29 33.8000 33.81 33.0700 33.80 27776900.0
2010-11-30 33.5300 34.25 33.3600 34.20 57476900.0
2010-12-01 34.6524 34.95 34.4200 34.78 34633200.0
2010-12-02 34.9200 34.98 34.5100 34.68 23196100.0
In [0]:
file_data_GM = "daily_GM.csv"
In [0]:
data.to_csv(file_data_GM)
In [0]:
data = pd.read_csv(file_data_GM)
print(data.head(10))
         date  1. open  2. high   3. low  4. close    5. volume
0  2010-11-18  35.0000    35.99  33.8900     34.19  457044300.0
1  2010-11-19  34.1500    34.50  33.1100     34.26  107842000.0
2  2010-11-22  34.2000    34.48  33.8100     34.08   36650600.0
3  2010-11-23  33.9500    33.99  33.1900     33.25   31170200.0
4  2010-11-24  33.7300    33.80  33.2186     33.48   26138000.0
5  2010-11-26  33.4100    33.81  33.2100     33.80   12301200.0
6  2010-11-29  33.8000    33.81  33.0700     33.80   27776900.0
7  2010-11-30  33.5300    34.25  33.3600     34.20   57476900.0
8  2010-12-01  34.6524    34.95  34.4200     34.78   34633200.0
9  2010-12-02  34.9200    34.98  34.5100     34.68   23196100.0

Analisis de los datos

En este punto realizaremos un estudio desde el punto de vista de los datos puramente numerico. Valores fuera de banda o outliners, o datos incompletos o indeterminados. También realizaremos un estudio de las tendencias y por último un estudio sobre la estacionaridad y transformación de esta a una serie estacionaria.

Limpieza y preánalisis de los datos de precio

In [0]:
#Comprobamos que el fichero este completo, y no tengamos valores 0 o indeterminados.
missing_values_count = data.isnull().sum()
print(missing_values_count)
#Comprobamos si existen valores 0 en alguna de las columnas. Si es asi deberiamos rellenarlos con valores cercanos, media.
colMinValue = [(col, data[col].min()) for col in data.columns]
print(colMinValue)
date         0
1. open      0
2. high      0
3. low       0
4. close     0
5. volume    0
dtype: int64
[('date', '2010-11-18'), ('1. open', 19.02), ('2. high', 19.13), ('3. low', 18.72), ('4. close', 18.8), ('5. volume', 2300985.0)]

Para este caso concreto de los datos, no tenemos valores desconocidos, que o bien sean 0 o nan y tampoco tenemos valores con unos maximos descontrolados.

In [0]:
#Sacamos un describe de los datos para ver como se distribuyen y si tenemos algunos datos raros
data.describe()
Out[0]:
1. open 2. high 3. low 4. close 5. volume
count 2229.000000 2229.000000 2229.000000 2229.000000 2.229000e+03
mean 33.108944 33.448940 32.724754 33.088887 1.383685e+07
std 5.604785 5.608339 5.589150 5.594305 1.225582e+07
min 19.020000 19.130000 18.720000 18.800000 2.300985e+06
25% 30.200000 30.530000 29.840000 30.180000 8.919200e+06
50% 34.190000 34.520000 33.830000 34.190000 1.169220e+07
75% 36.850000 37.180000 36.470000 36.840000 1.574260e+07
max 45.910000 46.760000 45.720000 46.480000 4.570443e+08

La variables de cierre tiene una distribucion normal, no tendremos que aplicar transformadores de Box Cox La asimetria tiene que estar [-1, 1] y el coeficiente de variabilidad menor 1

In [0]:
from scipy.stats import skew
import scipy.stats as ss
asimetria = skew(data['4. close'].values)
coef_var = ss.variation(data['4. close'])
print("Asimetria=",asimetria)
print("coef_variabilidad=", coef_var)
Asimetria= -0.5255504175239769
coef_variabilidad= 0.16903105412981095

Pintamos la curvas del valor de cierre y vemos si nos aparece a simple vista algo que nos ayude a describir y comprender nuestros datos.

In [0]:
import seaborn as sns

sns.set()
%matplotlib inline

plt.rcParams['figure.figsize'] = (12, 5)
plt.rcParams['figure.dpi'] = 80
df = data['4. close']
df.index = data['date']
df.plot()
plt.title('Daily TimeSeries General Motors')
plt.show()
In [0]:
print('El valor maximo obtenido', df.idxmax(), "=", df.loc[df.idxmax()])
print('El valor minimo obtenido', df.idxmin(), "=", df.loc[df.idxmin()])
El valor maximo obtenido 2017-10-24 = 46.48
El valor minimo obtenido 2012-07-25 = 18.8

A simple vista vemos que desde finales de Julio del 2012 donde la empresa obtuvo su valor minimo del periodo, la empresa ha mantenido una tendencia creciente, obteniendo sus maximos en Octubre del 2017, pero actualemente el crecimiento se va manteniendo

Remuestreo y convirtiendo frecuencias

Veamos el comportamiento anual de la serie. Para ello, remuestreamos los datos al final del año comercial convirtiendo a frecuencias anuales. En cada punto, resample informa de la media del año anterior, mientras que asfreq informa el valor al final del año.

In [0]:
stock = data['4. close']
stock.index = pd.to_datetime(stock.index)
stock.index.name = 'date'
In [0]:
stock.plot(alpha=0.5, style='-')
stock.resample('BA').mean().plot(style=':')
stock.asfreq('BA').plot(style='--');
plt.legend(['input', 'resample', 'asfreq'],
           loc='upper left');
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-1-f40a9c94d82f> in <module>()
----> 1 stock.plot(alpha=0.5, style='-')
      2 stock.resample('BA').mean().plot(style=':')
      3 stock.asfreq('BA').plot(style='--');
      4 plt.legend(['input', 'resample', 'asfreq'],
      5            loc='upper left');

NameError: name 'stock' is not defined
In [0]:
import numpy as np
serie = data.copy(True)
serie.date = pd.to_datetime(data.date)
serie.index.name = 'date'
serie['year'] = [dat.year for dat in serie.date]
serie['month'] = [str(dat.strftime('%m')) for dat in serie.date]
years = serie['year'].unique()
In [0]:
import matplotlib as matplot
import datetime

np.random.seed(100)
colors = np.random.choice(list(matplot.colors.XKCD_COLORS.keys())[::-1], len(years), replace=False)
In [0]:
plt.clf()

plt.figure(figsize=(20, 8), dpi=80)
symbol = "GM"
for i, y in enumerate(years):
    if i > 0:
        year = serie[serie['year']==y].groupby('month').mean().reset_index(drop=False)
        year['month'] = year['month'].map(lambda x: datetime.datetime.strptime(x, '%m').strftime('%b'))
        plt.plot('month', '4. close', data=year, color=colors[i], label=y)
        plt.text(year.shape[0] - 0.9, year['4. close'][-1:].values[0] - 0.25, y, fontsize=12, color=colors[i])

#plt.gca().set(xlim=(-0.3, 11), ylim=(2, 30), ylabel='Price', xlabel='Mes')
plt.yticks(fontsize=12, alpha=.7)
plt.title('Gráfica estacional anual del stock de '+symbol, fontsize=20)
plt.show()
<Figure size 960x400 with 0 Axes>

En esta gráfica podemos ver que año tras año la evolución ha sido relativamente constante. Se aprecina una caída en el histórico a finales de 2011 y que perdura durante la primera mitad del año 2012, donde comienza a remontar nuevamente.

Media Movil

Uno de los indicadores tecnicos que podemos encontrar el AlphaVantage es el indicador MSA , correspondiente a la media movil. La media movil, nos permitira estudiar como evolucionan los precios es media para un determinada ventana de tiempo. Dentro de los indicadores podemos encontrar otro indicador similar que es el ema correspondiente a la media movil exponencial.

In [0]:
#Media movil
from alpha_vantage.techindicators import TechIndicators
symbol='NYSE:GM'
ti = TechIndicators(key=API_KEY,output_format='pandas')
#Obtenemos la media movil mensual.
data_msa, meta_msa = ti.get_sma(symbol=symbol, interval='daily',time_period=30, series_type='close')
#display(data_msa.head(10))
#Sacamos la exponencia 
data_ema, meta_ema = ti.get_ema(symbol=symbol, interval='daily',time_period=30, series_type='close')
In [0]:
#Lo ploteamos para ver si vemos la tendecnia en los datos
plt.figure(figsize=(20,10)) 
data_msa.plot()
plt.title('Daily TimeSeries General Motors - MSE')
data_ema.plot()
plt.title('Daily TimeSeries General Motors - EMA')
plt.show()
<Figure size 1600x800 with 0 Axes>

Podemos también calcularla nosotros con respecto a una ventana de tiempo y la desviacion de los datos con respecto a la media:

In [0]:
def media_movil(serie, window_size, plotea=True):
    roll_std = serie.rolling(window=window, center=False).std()
    roll_mean = serie.rolling(window=window, center=False).mean()
    if plotea:
        date = data.date.astype('O')
        plt.figure(figsize=(15,5)) 
        plt.plot(serie, color='blue',label='Original')
        plt.plot(roll_mean, color='red', label='Media')
        plt.plot(roll_std, color='black', label = 'Std')
        plt.legend(loc='best')
        plt.xlabel('Index number')
        plt.ylabel('Precio de cierre')
        plt.title('Media móvil & Desviación típica')
        plt.show()
    return roll_std, roll_mean

window=30
roll_std, roll_mean = media_movil(data['4. close'], window, True)

Como podemos observar los primeros valores de la serie no aparecen puesto que se necesita acumular los winow_size primeros elementos antes de dar el primer valor.

Que nos aportan las medias moviles?. Las medias moviles se van recalculando a medida que vamos avanzando en el tiempo, el calculo de la media movil nos ayuda entre otras cosas a suavizar los picos o fluctuaciones en nuestros datos, se suelen usar para poder indentificar tendencias.

In [0]:
window = 130
roll_std_360, roll_mean_360 = media_movil(data['4. close'], window, True)

Si aumentamos la ventana suavizamos aun mas y vemos la tendencia en un intervalo de tiempo mayor.

Diagrama de caja de la distribución mensual (estacional) y anual (tendencia)

In [0]:
plt.clf()

fig, axes = plt.subplots(1, 2, figsize=(20,7), dpi= 80)

sns.boxplot(x='year', y='4. close', data=serie, ax=axes[0])
sns.boxplot(x='month', y='4. close', data=serie[~serie.year.isin([1991, 2008])])

axes[0].set_title('Distribución anual (tendencia)', fontsize=18); 
axes[1].set_title('Distribución mensual (estacionalidad)', fontsize=18)
plt.show()
<Figure size 960x400 with 0 Axes>

Vemos nuevamente el año 2011 como año con mayor variación negativa de los precios. En el gráfico de la derecha puede verse que mensualmente se mantiene más bien constante y se aprecian mayores flutuaciones de los precios a mitad y a final de año.

Preanálisis de las variables exógenas

Para aquellas variables que en un futuro puede ser interesante añadir de las que nos presenta el proveedor, es importante valorar cuáles pueden ser interesantes y cuáles no. Para ello, se hará uso de un analisis de sus correlaciones.

A parte de la media móvil y la media móvil exponencial, vamos a evaluar las que la propia página referencia como de alto uso, y que tengan sentido en nuestro análisis intra-día que serían:

  • MACD, Media Móvil de Convergencia/Divergencia. Muy usado en el estado del arte y relevante, ya que es la diferencia entre medias móviles a corto y medio plazo, dando una alta sensibilidad del cambio.
  • STOCH, Oscilador Estocástico. Está limitado por el rango entre 0 y 100. Esto lo convierte en un indicador útil de las condiciones de sobrecompra y sobreventa. Tradicionalmente, las lecturas de más de 80 se consideran en el rango de sobrecompra, y las lecturas de menos de 20 se consideran sobrevendidas.
  • RSI, Índice de Fuerza Relativa. Fácil de interpretar, supone que cuando hay un valor alto de este indicador, los precios han seguido una tendencia que en el extremo se revertirá.
  • ADX, Índice Direccional Medio. Evalua la fuerza de la tendencia actual y saber si estamos realmente en una tendencia (subida, bajada, estancamiento) ó rango (rebote del precio entre dos valores).
  • CCI, Índice del Canal de Comodidad. Detección de tendencias y concretamente de la volatilidad. A partir de 10 indica tendencias alzistas y por debajo de -100 tendencias bajistas.
  • AROON, identifica tendencias y su fortaleza en valor porcentual.
  • BBANDS, Bandas de Bollinger. Compara la volatilidad de niveles de precio a lo largo de un periodo de tiempo.
  • AD, Acumulación/Distribución. Si el precio o cotización del mercado se encuentra en una tendencia alcista, el indicador estará ascendente reflejando la presión compradora, en cambio, en una tendencia bajista, el indicador estará bajando y reflejando la presión vendedora. Así, puede servir de aviso a giros en el mercado.
  • OBV, Balance de Volúmenes. Se basa en que el volumen precede al movimiento del precio, por lo tanto si un activo presenta un incremento en el OBV es una señal de que el volumen de trading se está elevando durante los periodos en que el precio sube, y viceversa.
In [0]:
data_indexed, metadata_indexed = ts.get_daily(symbol='NYSE:GM',outputsize='full')
data_msa, meta_msa = ti.get_sma(symbol=symbol, interval='daily',time_period=30, series_type='close')
data_ema, meta_ema = ti.get_ema(symbol=symbol, interval='daily',time_period=30, series_type='close')
In [0]:
data_macd, meta_macd = ti.get_macd(symbol=symbol, interval='daily',series_type='close')
data_stoch, meta_stoch = ti.get_stoch(symbol=symbol, interval='daily')
data_rsi, meta_rsi = ti.get_rsi(symbol=symbol, interval='daily',time_period=30, series_type='close')
data_adx, meta_adx = ti.get_adx(symbol=symbol, interval='daily',time_period=30)
data_cci, meta_cci = ti.get_cci(symbol=symbol, interval='daily',time_period=30)
In [0]:
data_aroon, meta_aroon = ti.get_aroon(symbol=symbol, interval='daily',time_period=30)
data_bbands, meta_bbands = ti.get_bbands(symbol=symbol, interval='daily',time_period=30, series_type='close')
data_ad, meta_ad = ti.get_ad(symbol=symbol, interval='daily')
data_obv, meta_obv = ti.get_obv(symbol=symbol, interval='daily')

Al tener cada dato una escala, lo más correcto es utilizar la correlación de Pearson, que independiza este factor:

In [0]:
from functools import reduce

dfs = [data_indexed,
      data_msa, data_ema, data_macd, data_stoch,
      data_rsi, data_adx, data_cci, data_aroon,
      data_bbands, data_ad, data_obv]

df_corr = reduce(lambda left,right: pd.merge(left,right,on='date'), dfs)
In [0]:
import seaborn as sns 

corrmat = df_corr.corr() 

f, ax = plt.subplots(figsize =(9, 8)) 
sns.heatmap(corrmat, ax = ax, cmap ="YlGnBu", linewidths = 0.1)
Out[0]:
<matplotlib.axes._subplots.AxesSubplot at 0x7fe8ca99c4a8>

La media móvil de convergenica/divergencia (MACD) tiene una covarianza suficientemente alta para tomarla como variable exógena de referencia. Se probará con la RNN simple más adelante y analizaremos su comportamiento y aportación.

Estacionaridad

A continuacion vamos a identificar si nuestra serie es estacionaria y en caso de no serlo convertirla en estacionaria. Para poder aplicar el metodo ARIMA debemos asegurarnos de que nuestra serie lo es.

Se dice que una serie de tiempo es estacionaria cuando su distribución y sus parámetros no varían con el tiempo. En términos más concretos, la media y la varianza de una serie estacionaria no cambian con el tiempo, y tampoco siguen una tendencia.

Una serie estacionaria es mucho más fácil de predecir. Si se comportaba de una manera en el pasado ( digamos con una determinada media y varianza) , podremos suponer que se seguirá comportando de la misma forma en el futuro. Bueno, o que tiene una gran probabilidad de continuar comportándose de la misma forma.

Prueba de Dicky Fuller

Esta prueba nos permite a priory identificar si una serie es estacionaria o no lo es.

El test de la raiz unitaria nos indicara como de marcada tiene una serie una determinada tendencia.

La hipotesis nula lo que nos indica es que si nuestra serie puede ser representada por una raiz unitaria es que NO es estacionaria y existe alguna dependencia temporal dentro de la serie. Esto lo comprobamos con el valor del p_value del test.

Si Valor p > 0.05: No se puede rechazar la hipótesis nula (H0), los datos tienen una raíz unitaria y NO son estacionarios.

Si Valor p <= 0.05: Rechaza la hipótesis nula (H0), los datos no tienen una raíz unitaria y son ESTACIONARIOS.

Pasamos el test inicialmente a nuestra serie sin procesar.

In [0]:
from statsmodels.tsa.stattools import adfuller
In [0]:
def test_fuller(df):
    dftest = adfuller(df)
    #Nos quedamos con los 4 primeros elementos de la tabla del test
    dfoutput = pd.Series(dftest[0:4], index=['ADF Test Statistic','p-value','#Lags usados','Numero de observaciones'])
    for key,value in dftest[4].items():
        dfoutput['Valor crítico del Test Statistic (%s)'%key] = value
    return dfoutput

testdffuller = test_fuller(df)
print(testdffuller)
ADF Test Statistic                          -2.330176
p-value                                      0.162420
#Lags usados                                 0.000000
Numero de observaciones                   2228.000000
Valor crítico del Test Statistic (1%)       -3.433288
Valor crítico del Test Statistic (5%)       -2.862838
Valor crítico del Test Statistic (10%)      -2.567461
dtype: float64

El p_value en este caso > 0.05 con lo cual no se puede rechazar la hipotesis nula . NO ES UNA SERIE ESTACIONARIA. Deberemos entonces aplicar diferentes metodos para intentar convertirla en estacionaria.

Transformaciones

Transformar una serie no estacionaria en estacionaria.

Logaritmo

Estudio del Logaritmo de la funcion. La transformacion logaritmica puede en algunos casos estabilizar la dispersion de nuestros datos y hay veces que es mucho mejor trabajar con el logaritmo que con los datos en bruto y puede hacer que la media se estabilice, pero en nuestro caso no conseguimos eliminar la tendencia.

In [0]:
data['4. close'].hist()
Out[0]:
<matplotlib.axes._subplots.AxesSubplot at 0x7fe67ca60a90>

Se parece a una distribucion gausiana pero esto ya nos esta indicando que nuestra serie no es estacionaria, podemos probar con el logaritmo y volver a realizar la distribucion y comprobar

In [0]:
import numpy as np
from matplotlib import pyplot
log_close = np.log(data['4. close'].values)
pyplot.hist(log_close)
pyplot.show()
In [0]:
window = 30
roll_std_log, roll_mean_log = media_movil(np.log(df), window, True)

si observamos la diferencia con la misma media movil, al mismo intervalo de tiempo, vemos que tanto la tendecia como la desviacion adoptan una forma mas uniforme.

Resta de Media Movil

Cuando restamos a nuestra serie inicial la media movil lo que conseguimos es eliminar su tendencia, en este caso, estamos restando la media movil referente con una ventana de tiempo de 30 dias.

In [0]:
 
In [0]:
plt.figure(figsize=(10,5)) 
resta_media_avg = data['4. close'].values - roll_mean
plt.plot(resta_media_avg, color='black', label = 'Rolling Std')
plt.show()

Diferenciacion

Eliminar tendencia y estacionalidad, se puede realizar con el proceso de diferenciacion. Este proceso de transformacion consiste en generar una nueva serie Snueva(t) = Sorig(t) - Sorig(t-1) A este proceso se le denomina diferenciacion de primer orden. Es posible que para convertir nuestra serie en estacionaria se necesesiten varios ordenes de diferenciacion.

In [0]:
diff_serie = data['4. close'].diff().dropna()
plt.figure(figsize=(10,5)) 
plt.plot(diff_serie)
plt.ylabel('Precios de Cierre')
plt.xlabel('Index number')
plt.show()

Aqui vemos claramente que nuestra serie no presenta tendencia, comprobamos con el test de Dicky Fuller que la serie diferenciada con diferencuacion de primer orden si lo es

In [0]:
test_diff = test_fuller(diff_serie)
print(test_diff)
ADF Test Statistic                         -46.157740
p-value                                      0.000000
#Lags usados                                 0.000000
Numero de observaciones                   2227.000000
Valor crítico del Test Statistic (1%)       -3.433290
Valor crítico del Test Statistic (5%)       -2.862839
Valor crítico del Test Statistic (10%)      -2.567461
dtype: float64

Al resultado de la primera diferenciacion, le hemos pasado el test y vemos que el p_value se hace muy pequeno.

Descomposicion

Toda serie temporal se puede descomponer en 3 componetes, su tendencia, componente estacional, que es un patron que se repite en ciertos periodos de tiempo y el residuo.

In [0]:
from statsmodels.tsa.seasonal import seasonal_decompose
#Hemos puesto frecuencia de 30 dias, lo mismmo resulta ser mucho.
descomposicion = seasonal_decompose(data['4. close'].values, freq=12)
fig, ax = plt.subplots(figsize=(10, 10))
plt.subplot(411)
plt.plot(data['4. close'].values, label='Original')
plt.legend(loc='best')
plt.subplot(412)
plt.plot(descomposicion.trend, label='Tendencia')
plt.legend(loc='best')
plt.subplot(413)
plt.plot(descomposicion.seasonal[2000:],label='Estacionalidad')
plt.legend(loc='best')
plt.subplot(414)
plt.plot(descomposicion.resid, label='Residuo')
plt.legend(loc='best')
plt.tight_layout()
In [0]:
#eliminamos posibles valores nulos del residuo
residual = descomposicion.resid
#Eliminamos los valores nulos.
residual = residual[~np.isnan(descomposicion.resid)]

Volvemos a aplicar el Text De DickyFuller sobre el residuo para comprobar si es estacionario, que lo deberia de ser.

In [0]:
dftest = adfuller(residual)
dfoutput = pd.Series(dftest[0:4], index=['ADF Test Statistic','p-value','#Lags usados','Numero de observaciones'])
for key,value in dftest[4].items():
    dfoutput['Valor crítico del Test Statistic (%s)'%key] = value
print(dfoutput)
ADF Test Statistic                       -1.575378e+01
p-value                                   1.214487e-28
#Lags usados                              2.400000e+01
Numero de observaciones                   2.190000e+03
Valor crítico del Test Statistic (1%)    -3.433339e+00
Valor crítico del Test Statistic (5%)    -2.862861e+00
Valor crítico del Test Statistic (10%)   -2.567473e+00
dtype: float64

En este caso vemos que p_value esta por debajo del 5% con lo cual podemos decir que el residuo lo es.

Autocorrelacion

La mayoría de los procesos físicos presentan una inercia y no cambian tan rápidamente. Esto, combinado con la frecuencia del muestreo, a menudo hace que las observaciones consecutivas estén correlacionadas. Esta correlación entre observaciones consecutivas se llama autocorrelación. Cuando los datos están autocorrelacionados, la mayoría de los métodos estadísticos estándares basados en la suposición de observaciones independientes pueden arrojar resultados engañosos o incluso ser inútiles.

In [0]:
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from pandas.plotting import autocorrelation_plot
autocorrelation_plot(data['4. close'].values)
Out[0]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f37bc5948d0>

Diagrama de autocorrelacion de la serie original. Observamos que la autocorrelacion en tiempo baja exponencialmente y lags altos de tiempo se hace casi 0. No parece que la serie presente autocorrelaciones visibles.

In [0]:
autocorrelation_plot(diff_serie)
pyplot.show()

Grafica de autocorrelaciones de la serie con el primer orden de diferenciacion. Como podemos observar las correlaciones en todos los retardos "lags" estan cercanos a cero. Estos resultados que estamos obteniendo nos hacen pensar que nuestra serie una vez diferenciada se parace a una Serie "Random Walk".

Una serie random Walk viene determinada, por la formula > Y(t) = Y(t-1) + Et siendo Et la componente aleatoria Las series random Walk, o de camino aleatorio, no son estacinarias, pero nosotros la hemos covertido en estacionaria con el proceso de diferenciacion , pero hemos obtenido un p_value bajisimo.

Si exploramos las graficas PACF ACF obtedriamos los siguiente (sobre la serie diferenciada)

In [0]:
plot_pacf(diff_serie, lags=60)
plt.show()
plot_acf(diff_serie, lags=60)
plt.show()

En ambas graficas vemos que solo tenemos correlacion en el primer retardo.

In [0]:
import seaborn as sns
sns.set_style('whitegrid')
sns.kdeplot(diff_serie, bw=0.5)
print(diff_serie.describe())
count    2226.000000
mean        0.001285
std         0.564064
min        -2.730000
25%        -0.300000
50%         0.020000
75%         0.320000
max         4.870000
Name: 4. close, dtype: float64

Las series que no presentan autocorrelacion se las denomina de ruido blanco, nuestra serie en

este caso se parece bastante al ruido blanco. https://otexts.com/fpp2/wn.html#fig:wnoise

El problema de las series radom walk o de camino aleatorio es que son mas impredecibles. Las mejores prediciones que podremos hacer seran las que tomen como partida el tiempo anterior y predigan el siguiente, a esto normalmente se le denomina pronostico ingenuo Una caminata aleatoria (random Walk) es aquella en la que los pasos o direcciones futuros no pueden predecirse sobre la base de la historia pasada. Cuando el término se aplica al mercado de valores, significa que los cambios a corto plazo en los precios de las acciones son impredecibles.

Por ahora, los resultados no son prometedores, esperemos al post II para ver que obtenemos.

Comentar