Magíster en Data Science - Universidad del Desarrollo
Overview
Resultado de aprendizaje esperado:
Identificar datos de series temporales, sus particularidades y riesgos, en el contexto de posibles aplicaciones profesionales.
Bibliografía recomendada:
Stock & Watson, C.14 link ; Wooldridge, c.12 link, Gujarati, c.12 link
Tipos de datos
Recordemos
Los datos que analizamos con modelos se pueden categorizar en tres grandes tipos:
corte transversal
series de tiempo (o longitudinal)
panel
Corte transversal
Cuando se empieza a estudiar modelamiento de datos, la aplicación natural de estos es en lo que llamamos datos de corte transversal. En estos, la unidad de observación son individuos separados unos de otros.
Usualmente, cada fila reprsenta un individuo (personas, familias, empresas) en un momento del tiempo específico (mismo año, mismo mes, etc). Es análogo a una fotografía de un grupo.
Esta es una encuesta de caracter nacional realizada a hogares, en la cual se busca levantar información para conocer la situación de los hogares y de la población, especialmente de aquella en situación de pobreza y de aquellos grupos definidos como prioritarios y permite evaluar impacto de política social. Es representativa a nivel regional.
Es realizada por el Ministerio de Desarrollo Social desde el año 1990 con una periodicidad; bianual o trianual. Hasta ahora, las encuestas aplicadas corresponden a los años 1990, 1992, 1994, 1996, 1998, 2000, 2003, 2006, 2009, 2011, 2013, 2105 y 2017.
# Paquetes y settingsfrom dateutil.parser import parse import matplotlib as mplimport matplotlib.pyplot as pltimport seaborn as snsimport numpy as npimport pandas as pd# setting de graficosplt.figure(figsize=(5,3), dpi=200, facecolor='w', edgecolor='k')
import seaborn as snsimport matplotlib.pyplot as plt# Supongamos que ya tienes un DataFrame df_casen2020 y quieres hacer un scatterplot# Define el tamaño del gráficoplt.figure(figsize=(10, 6)) # Ajusta el tamaño a tus preferencias (ancho, alto)# Crea el scatterplotsns.scatterplot(data=df_casen2020, x="esc", y="yaut", alpha=0.5)# Personaliza el gráfico si es necesario (títulos, etiquetas, etc.)# Muestra el gráficoplt.show()
Series de tiempo
En cambio, un tipo diferente de datos son las Series Temporales o tambien longitudinales.
En este tipo de información, se tiene diferentes momentos del tiempo (día, semana, mes, año, etc.) para una misma unidad de análisis (individuo, país, empresa, etc)
Ejemplos típicos de series de tiempo son: - Datos macroeconómicos (PIB. Inglación, empleo, etc.) - Financieros (precios de acciones) - Empresariales (Ventas, costos, etc…)
#Ejemplo datos de acciones#Usemos la api de Yahoo finance: instalar: pip install yahoo_fin y pip install requests_htmlfrom yahoo_fin.stock_info import get_dataamazon_weekly= get_data("amzn", start_date="12/04/2009", end_date="25/09/2024", index_as_date =False, interval="1wk")amazon_weekly.head()
/Users/melita/anaconda3/lib/python3.10/site-packages/yahoo_fin/stock_info.py:29: UserWarning:
Parsing dates in DD/MM/YYYY format when dayfirst=False (the default) was specified. This may lead to inconsistently parsed dates! Specify a format to ensure consistent parsing.
Trabajar con series de tiempo no es para nada sencillo: necesitaremos técnicas nuevas.
¿Por qué?
Necesitaremos modelos sofisticados de errores y técnicas para estimarlos, por ciertas dieferencias en la naturaleza del proceso generador de datos
Panel
El último tipo de datos, es lo que se llama un panel. Este es la combinacion de los dos tipos anteriores, donde para un conjunto de individuos tenemos varias observaciones en el tiempo.
Suele ser el tipo de datos más completo, pero tabien más dificil de conseguir. Además, enfrenta los problemas típicos de ambos tipos anteriores, dependiendo si es un panel corto o largo.
Una alternativa a etsos son los llamados pooled cross section.
El análisis de este tipo de datos escapa a los objetivos del taller, pero una introducción pueden revisarla en Stock y Watson Cap. 10.
Cortes transversal vs series de tiempo
Existe una diferencia clave, que se desprende de trabajar con corte tranvseral vs series de tiempo y es el concepto de la muestra aleatoria
Muestras en corte transversal
En corte transversal solemos trabajar con MUESTRAS
Corte transversal
Estimamos modelos de la forma:
Estos modelos representan una correlación marginal en las observaciones entre y y x’s (escalada por la varianza de x) y para que podamos interpretarlas causalmente tenemos varias condiciones o supuestos que de se deben cumplir.
Uno de estos, es el supuesto de exogeneidad:
Sin embargo, esta forma de ver este supuesto es una simplificación ya que, como estamos en una uestra aleatoria, no tenemos que verificar que los efectos cruzados tambien sean exogenos:
Esto se daba por cumplido, como consecuencia de que era una muestra aletaoria.
Lo cual, generalente, ocurre en corte transversal por lo cual cada observación es i.i.d.
La imposibilidad de la muestra aleatoria en series de tiempo
En serie de tiempo, nuestro universo es un proceso estocástico:
Corte transversal
Estimamos modelos de la forma:
Estamos en presencia de un proceso estocástico
En cada momento se observa un posible resultado (o realización) del proceso estocásticos.
Esto tiene vaias implicancias:
NO SON INDEPENDIENTES ya que por construcción, viene del mismo proceso.
Por lo tanto, el supuesto de exogeneidad $E[u_{i}|X_{i}]=0 $ NO ES SUFICIENTE
Requerimos su versión más exigente: (Exogeneidad estricta)
Este supuesto, generalmente NO SE CUMPLE.
Si se cumpliese, seguiruíamos operando como siempre con modelos de regresión multiple estándar.
¿Qué hacer entonces?
Reconocer los datos como procesos estocásticos e incluir sus particularidades en la modelación. De eso se tratarán los dos siguientes sesiones - Sesion 5: Peculiaridades de los procesos estocasticos y su exploración en la data - Sesion 6: Modelamiento
Conceptos propios de las series de tiempo
Dado que una serie de tiempo tiene un orden específico, que en si mismo es importante. En base a este orden se suelen crear nuevos indicadores o variables. Revisemos los más comunes:
Notación y transformaciones:
Notación
Variables de series de tiempo se denominan con sub-índice “t” para indicar el perído en el tiempo:
El total de periodos se suele referir como T.
Rezagos
Un rezago es el valor de la variable en períodos anteriores:
Primer rezago: es el valor 1 período anterior
Segundo rezago: es el valor 2 períodos atrás
j-ésimo rezago: es el valor j períodos atrás
Calculemos el primer y segundo rezago en los datos de amazon:
# Calcular el primer rezago (Lag 1) para la columna 'open'amazon_weekly['open_lag1'] = amazon_weekly['open'].shift(1)# Calcular el segundo rezago (Lag 2) para la columna 'open'amazon_weekly['open_lag2'] = amazon_weekly['open'].shift(2)# Ver los primeros registros del DataFrame con las columnas de rezagoprint(amazon_weekly[['date', 'open', 'open_lag1', 'open_lag2']].head())
date open open_lag1 open_lag2
0 2009-11-30 7.1810 NaN NaN
1 2009-12-07 6.9000 7.181 NaN
2 2009-12-14 6.6250 6.900 7.181
3 2009-12-21 6.5240 6.625 6.900
4 2009-12-28 6.9875 6.524 6.625
Diferencias
Una diferencia corresponde al cambio en una variable entre dos periodos específicos y se usa la notación
Primera diferencia:
Calculemos la primera y segunda diferencia en los datos de Amazon:
# Calcular la primera diferencia para la columna 'open'amazon_weekly['open_diff1'] = amazon_weekly['open'] - amazon_weekly['open'].shift(1)# Calcular la segunda diferencia para la columna 'open'amazon_weekly['open_diff2'] = amazon_weekly['open_diff1'] - amazon_weekly['open_diff1'].shift(1)# Ver los primeros registros del DataFrame con las columnas de diferenciaprint(amazon_weekly[['date', 'open', 'open_diff1', 'open_diff2']].head())
date open open_diff1 open_diff2
0 2009-11-30 7.1810 NaN NaN
1 2009-12-07 6.9000 -0.2810 NaN
2 2009-12-14 6.6250 -0.2750 0.0060
3 2009-12-21 6.5240 -0.1010 0.1740
4 2009-12-28 6.9875 0.4635 0.5645
Tasas de crecimiento
Si calculamos la primera diferencia el logaritmo natural, podemos obtener incorporar la tasa de crecimiento en una regresión:
Primera diferencia en logs:
Cambio porcentual de entre y
Calculemos la primera diferencia en logaritmo natural y la taza de crecimiento para la serie de Amazon, en su precio de apertura:
import numpy as np# Calcular el logaritmo natural de la columna 'open' con NumPyamazon_weekly['open_ln'] = np.log(amazon_weekly['open'])# Calcular la primera diferencia en los logaritmos naturales ('open_ln_diff1')amazon_weekly['open_ln_diff1'] = amazon_weekly['open_ln'] - amazon_weekly['open_ln'].shift(1)# Calcular el cambio porcentual entre t-1 y t para 'open' ('open_percent_change')amazon_weekly['open_percent_change'] = (amazon_weekly['open'] - amazon_weekly['open'].shift(1)) / amazon_weekly['open'].shift(1) *100# Ver los primeros registros del DataFrame con las columnas calculadasprint(amazon_weekly[['date', 'open', 'open_ln', 'open_ln_diff1', 'open_percent_change']].head())
date open open_ln open_ln_diff1 open_percent_change
0 2009-11-30 7.1810 1.971439 NaN NaN
1 2009-12-07 6.9000 1.931521 -0.039917 -3.913106
2 2009-12-14 6.6250 1.890850 -0.040671 -3.985509
3 2009-12-21 6.5240 1.875488 -0.015363 -1.524526
4 2009-12-28 6.9875 1.944123 0.068635 7.104537
Tipos de modelos
Modelo estático
Se modela la relación contemporánea entre dos variables, i.e., relación en el mismo momento en el tiempo
Ejemplos: * Modelo Simple:
Modelo múltiple:
Modelo dinámico
Se incluyen efectos temporales, que se piensan que tienen que ver con tendencias, inercia o estacionalidades
Pandas y series de tiempo
Pandas fue desarrollado originalmente para trabajar con datos financieros.
Como las series temporales son un tipo de datos comunes encontrados en aplicaiones de finanzas, naturalmente pandas tiene muy buen soporte para la mayoria de operaciones comunes.
pd.to_datetime() : conviere una serie o valor en un timestamp
Este formato permite un mejor manejo de las series.
#pandas remote data access support for calls to the World Bank Indicators APIfrom pandas_datareader import data, wb #instalar conda install pandas-datareader o pip installpandas-datareader
#Revisemos que indicadores hay disponibles. En este caso revisare de PIB (GDP en ingés), pero se pueden explorar muchas más opciones.wb.search('gdp.*capita.*const')
id
name
unit
source
sourceNote
sourceOrganization
topics
691
6.0.GDPpc_constant
GDP per capita, PPP (constant 2011 internation...
LAC Equity Lab
GDP per capita based on purchasing power parit...
b'World Development Indicators (World Bank)'
Economy & Growth
10978
NY.GDP.PCAP.KD
GDP per capita (constant 2015 US$)
World Development Indicators
GDP per capita is gross domestic product divid...
b'World Bank national accounts data, and OECD ...
Economy & Growth
10980
NY.GDP.PCAP.KN
GDP per capita (constant LCU)
World Development Indicators
GDP per capita is gross domestic product divid...
b'World Bank national accounts data, and OECD ...
Economy & Growth
10982
NY.GDP.PCAP.PP.KD
GDP per capita, PPP (constant 2017 internation...
World Development Indicators
GDP per capita based on purchasing power parit...
b'International Comparison Program, World Bank...
Economy & Growth
10983
NY.GDP.PCAP.PP.KD.87
GDP per capita, PPP (constant 1987 internation...
WDI Database Archives
b''
# Obtengamos la lista de paises disponiblescountries=wb.get_countries()#Preview primeras filas lista de paisescountries[:5]
iso3c
iso2c
name
region
adminregion
incomeLevel
lendingType
capitalCity
longitude
latitude
0
ABW
AW
Aruba
Latin America & Caribbean
High income
Not classified
Oranjestad
-70.0167
12.5167
1
AFE
ZH
Africa Eastern and Southern
Aggregates
Aggregates
Aggregates
NaN
NaN
2
AFG
AF
Afghanistan
South Asia
South Asia
Low income
IDA
Kabul
69.1761
34.5228
3
AFR
A9
Africa
Aggregates
Aggregates
Aggregates
NaN
NaN
4
AFW
ZI
Africa Western and Central
Aggregates
Aggregates
Aggregates
NaN
NaN
#sabemos que queremos Chile, asi que busquemos su infocountries[ countries['name'] =='Chile' ]
iso3c
iso2c
name
region
adminregion
incomeLevel
lendingType
capitalCity
longitude
latitude
49
CHL
CL
Chile
Latin America & Caribbean
High income
IBRD
Santiago
-70.6475
-33.475
# Descarguemos la data desde la API del banco muncial a un dataframedf_GPDpc_Chile = wb.download(#Use the indicator attribute to identify which indicator or indicators to download indicator='NY.GDP.PCAP.KD',#Use the country attribute to identify the countries you want data for country=['CL'],#Identify the first year for which you want the data, as an integer or a string start='1980',#Identify the last year for which you want the data, as an integer or a string end=2020, )#Veamos el data framedf_GPDpc_Chile.head(10)
ILI.plot(x='date', y=['Percent of Deaths Due to Pneumonia and Influenza', 'Expected', 'Threshold'])ax = plt.gca()ax.legend(['Mortality', 'Expected', 'Threshold'])ax.set_xlabel('Date')ax.set_ylabel('% Mortality')
Text(0, 0.5, '% Mortality')
El comportamiento es muy diferente, podemos ver una estacionalidad.
Es decir hay un cambio periodico asociado al tiempo de la variable.
Transformaciones
En base a estos conceptos, entonces, es muy comun realizar transformaciones a las series de tiempo ya sea para obtener rezagos, diferencias, tasas de crecimiento, etc.
Ilustraremos varios de estos, con el dataset del DOw-Jonses indsutrial Average.
Una manera de remover tendencias de un dataset es diferenciandolo. Como nuestros datasets son discretos, usaremos diferencias finitas.
def differentiate(values, d=1):# First value is required so that we can recover the original values with np.cumsum x = np.concatenate([[values[0]], values[1:]-values[:-1]])if d ==1:return xelse: return difference(x, d -1)
Para recuperar la data original, basta con integrar los puntos diferenciados.
def integrate(values, d=1): x = np.cumsum(values)if d ==1:return xelse:return integrate(x, d-1)
rebuilt = integrate(differences)
Y con un chequeo rapido, podemos verificar que son los mismos valores.
np.mean(rebuilt-values)
0.0
Windowing
We also often want to calculate running values of some quantity. This requires the use of windowing functions that return the proper element at each step. def rolling(x, order): npoints = x.shape[0] running = []
for i in range(npoints-order+1):
running.append(x[i:i+order])
return np.array(running)
And a simple example values = np.arange(11) values rolling(values, 6) Since we return a numpy array with all the individual windows, this also provides us with a simple way to take running averages by chaining methods rolling(values, 2) rolling(values, 2).mean(axis=1) Or the running maximum, etc. rolling(values, 2).max(axis=1)
Exponential Smoothing
Another form of smoothing a noise time series is called exponential smoothing. This is equivalent to an exponentially weighted running average where past values get exponentially reduced. def ES(values, alpha= 0.05): N = len(values) S = [values[0]*alpha]
for i in range(1, N):
S.append(alpha*values[i]+(1-alpha)*S[-1])
return np.array(S)
As we can see with a few quick examples, the smaller the value of alpha the smoother (less noisy) the result smooth = [] smooth.append(ES(differences[1:], 0.01)) smooth.append(ES(differences[1:], 0.1)) smooth.append(ES(differences[1:], 0.5)) plt.plot(DJIA[‘DATE’].iloc[1:100], differences[1:100], label=‘Differences’) plt.plot(DJIA[‘DATE’].iloc[1:100], smooth[2][:99], label=r’‘) plt.plot(DJIA[’DATE’].iloc[1:100], smooth[1][:99], label=r’‘) plt.plot(DJIA[’DATE’].iloc[1:100], smooth[0][:99], label=r’‘) plt.xlabel(’Date’) plt.ylabel(‘Differences’) plt.legend()
Missing Data
Desafortunadamente, los datos no siempre están limpios o completos, lo que nos obliga a lidiar datos faltantes. Aquí ilustramos varios enfoques para introducir valores perdidos. Comenzamos generando un conjunto de datos con valores perdidos.
x = np.linspace(-np.pi, np.pi, 100)y = np.cos(x)y_missing = y.copy()y_missing[40:55] = np.nan#This is simply a cosine function with a few missing values at the peak.plt.plot(x, y, '*')plt.plot(x, y_missing)
Quizás la estrategia más común es simplemente mantener el último valor ‘bueno’ conocido y usarlo para completar los puntos de datos faltantes. Este enfoque no puede lidiar con los valores faltantes al comienzo del conjunto de datos.
def ffill(y): y0 = y.copy() N =len(y0) current =Nonefor i inrange(1, N):if np.isnan(y0[i]): y0[i] = currentelse: current = y0[i]return y0
Naturalmente, el enfoque opuesto también es común cuando usamos el siguiente valor bueno. De esta manera podemos manejar fácilmente los valores perdidos iniciales, pero no podemos hacer nada con los valores perdidos al final de la serie de tiempo.
def bfill(y): y0 = y.copy() N =len(y0) current =Nonefor i inrange(N-1, 0, -1):if np.isnan(y0[i]): y0[i] = currentelse: current = y0[i]return y0
Back-fill y Forward-fill son enfoques simples pero poderosos para lidiar con los datos faltantes. Sin embargo, a menudo queremos tener más cuidado con el valor que atribuimos. Un enfoque común es interpolar entre el valor anterior y el siguiente y conectarlos con una línea recta.
La imputación de datos (el cálculo de los valores perdidos esperados) es un gran subcampo de estadísticas con una amplia gama de técnicas y enfoques.
y_bfill = bfill(y_missing)y_ffill = ffill(y_missing)y_inter = interpolate(y_missing)# And a quick plot to visualize the differencesplt.plot(x, y_bfill, label='back fill')plt.plot(x, y_ffill, label='forward fill')plt.plot(x, y_inter, label='interpolate')plt.plot(x, y_missing, label='Data')plt.legend()
Resampling
En muchos casos, también necesitamos cambiar la frecuencia a la que estamos operando. Por ejemplo, nuestro conjunto de datos DJIA tiene valores al final del día, pero podríamos estar interesados en puntos de datos semanales o mensuales. El remuestreo es una serie de técnicas diseñadas para lidiar con esta situación y es similar en espíritu a las técnicas de ventanas que vimos anteriormente. La principal diferencia es que en lugar de simplemente mover la ventana en un paso fijo, cada ventana corresponde a nuestro período de interés.
Naturalmente, esta función groupBy es útil no solo para remuestrear sino también para una amplia gama de análisis estadísticos. Además de un mapeo, también debemos especificar qué función de agregación queremos usar. ¿Nos interesa el valor medio? ¿el maximo? ¿Desviación Estándar?
Como nuestra función groupBy también devuelve las posiciones de índice de la última vez que se vio cada bin, podemos comparar fácilmente los datos originales con los muestreados nuevamente.
Finalmente, en muchos casos queremos estimar cantidades estadísticas de series de tiempo. Un ejemplo obvio podría ser estimar la media móvil de una serie sin tendencia para verificar si de hecho está lo suficientemente cerca de cero para ser considerada estacionaria.
El estimador JackKnife nos permite obtener no solo el valor esperado en cuestión, sino también una medida de su varianza. Esto se logra mediante el uso de un enfoque de omisión para calcular N estimaciones de nuestra métrica. A partir de esta población de estimación, podemos obtener los promedios como la mejor estimación posible y la desviación estándar como una medida de las barras de error involucradas.
def jackknife(x, func, variance =False): N =len(x) pos = np.arange(N) values = [func(x[pos != i]) for i in pos] jack = np.sum(values)/Nif variance: values = [np.power(func(x[pos != i]) - jack, 2.0) for i in pos] var = (N-1)/N * np.sum(values)return jack, varelse:return jack
x = np.random.normal(0, 2, 100)print(x.std())jackknife(x, np.std, True)
1.8992867419484192
(1.8991395820804744, 0.01890134168632936)
Con jackknife obtenemos no solo una estimación del valor sino también una medida del error
Bootstrapping
Otra técnica común para estimar propiedades estadísticas se conoce como bootstrapping y está estrechamente relacionada con Jackknife. En este enfoque, simplemente tomamos muestras (con reemplazo) de la población original para obtener una medida de cuánta variabilidad se puede esperar.
def bootstrapping(x, n_samples, func=np.mean): y = x.copy() N =len(y) population = []for i inrange(n_samples): population.append(func(np.random.choice(y, N, replace=True)))return np.array(population)
Podemos generar fácilmente un histograma de las muestras bostrappeadas
Siga este ejemplo practico de importar datos desde la API del Banco Mundial y preparar la base para su análisis de series de tiempo.
Importe usted la serie de GDP total Y Percapita para otro país serie desde la API del Banco mundial, muestre sus principales características y realice un grafico.
Obtenga las primeras diferencias y compare, ¿pareciera haber tendencias?
Desarrollo
#pandas remote data access support for calls to the World Bank Indicators APIfrom pandas_datareader import data, wb #instalar conda install pandas-datareader o pip installpandas-datareader#Revisemos que indicadores hay disponibles. En este caso revisare de PIB (GDP en ingés), pero se pueden explorar muchas más opciones.wb.search('gdp.*capita.*const')
id
name
unit
source
sourceNote
sourceOrganization
topics
691
6.0.GDPpc_constant
GDP per capita, PPP (constant 2011 internation...
LAC Equity Lab
GDP per capita based on purchasing power parit...
b'World Development Indicators (World Bank)'
Economy & Growth
10978
NY.GDP.PCAP.KD
GDP per capita (constant 2015 US$)
World Development Indicators
GDP per capita is gross domestic product divid...
b'World Bank national accounts data, and OECD ...
Economy & Growth
10980
NY.GDP.PCAP.KN
GDP per capita (constant LCU)
World Development Indicators
GDP per capita is gross domestic product divid...
b'World Bank national accounts data, and OECD ...
Economy & Growth
10982
NY.GDP.PCAP.PP.KD
GDP per capita, PPP (constant 2017 internation...
World Development Indicators
GDP per capita based on purchasing power parit...
b'International Comparison Program, World Bank...
Economy & Growth
10983
NY.GDP.PCAP.PP.KD.87
GDP per capita, PPP (constant 1987 internation...
WDI Database Archives
b''
# Obtengamos la lista de paises disponiblescountries=wb.get_countries()#Preview primeras filas lista de paisescountries[:5]
iso3c
iso2c
name
region
adminregion
incomeLevel
lendingType
capitalCity
longitude
latitude
0
ABW
AW
Aruba
Latin America & Caribbean
High income
Not classified
Oranjestad
-70.0167
12.5167
1
AFE
ZH
Africa Eastern and Southern
Aggregates
Aggregates
Aggregates
NaN
NaN
2
AFG
AF
Afghanistan
South Asia
South Asia
Low income
IDA
Kabul
69.1761
34.5228
3
AFR
A9
Africa
Aggregates
Aggregates
Aggregates
NaN
NaN
4
AFW
ZI
Africa Western and Central
Aggregates
Aggregates
Aggregates
NaN
NaN
#sabemos que queremos Chile, asi que busquemos su infocountries[ countries['name'] =='Chile' ]
iso3c
iso2c
name
region
adminregion
incomeLevel
lendingType
capitalCity
longitude
latitude
49
CHL
CL
Chile
Latin America & Caribbean
High income
IBRD
Santiago
-70.6475
-33.475
# Descarguemos la data desde la API del banco muncial a un dataframedf_GPDpc_Chile = wb.download(#Use the indicator attribute to identify which indicator or indicators to download indicator='NY.GDP.PCAP.KD',#Use the country attribute to identify the countries you want data for country=['CL'],#Identify the first year for which you want the data, as an integer or a string start='1980',#Identify the last year for which you want the data, as an integer or a string end=2020 )#Veamos el data framedf_GPDpc_Chile.head(10)
Para poder interpretar causalmente un modelo de regresión lineal multiple en series de tiempo necesitamos que se cumplan los siguientes supuestos de Gauss Markov:
Modelo lineal en parámetros
Media condicionada nula o exogeneidad (que ahora es estricta)
No hay colinealidad perfecta.
Homoscedasticidad
5 No hay correlación serial.
Cada uno de estos supuestos tiene diferentes implicancias:
1-3 es que el estimador de MCO es insesgado. 4-5 que tiene minima varianza de los estimadores lineales (eficiente)
1-5 -> MELI
Vemos que hay dos elementos diferentes a corte transversal: cambia la exogeneidad a uno más estricto y aparece un nuevo supuesto, correlación serial.
Objetivos diferentes de los modelos
Los modelos de regresión se pueden usar con dos objetivos principales: predicción y explicación. En este bloque nos enfocaremos en la primera aplicación.
Un modelo de regresión puede ser útil para la predicción, aún cuando ninguno de sus coeficientes tenga interpretación causal.
Desde el punto de vista de la predicción, lo que es importante es que el modelo entregue una predicción lo más precisa posible.
La idea base es aprovechar las peculiaridades de las series temporales: la dependencia temporal y tendencias, para identificar patrones en la data que enriquezcan la predicción aun cuando no tengan valor explicativo. Estos elementos los incluiremos DENTRO de la regresión, para que represente de mejor manera el fenómeno a predecir.
Patrones de dependencia intertemporal
Primera aproximación a series de tiempo desde la intuiciónn - Con las series Tendremos dos casos: - La serie es estacionaria: bien comportada y el pasado describe bien el futuro. - Tenemos particularidades temporales actuando en el proceso estocástico:
* Efecto rezagados (variable independiente rezagada)
* Retroalimentación entre variables
* Inercia o auto-dependencia la variable depende de si misma en el pasado
Estacionareidad
Entenderemos estacionareidad como el futuro se parece al pasado, al menos en un sentido probabilistico.
En una serie estacionaria:
Nunca se aleja mucho de la media (media constante)
El efecto de los errores caen a lo largo del tiempo (varianza constante)
Lo que ocurre recientemente es relativamente más importante que lo que pasó en momentos más alejados (covarianza constante)
Si las series son estacionarias, no necesitamos muestreo aleatorio y podemos seguir usando OLS como siempre.
(En términos técnicos: Podemos reemplazar el supuesto de exogeneidad extricta por exogeneidad debil SI la serie es estacionaria.)
La media es constante a lo largo del tiempo, es decir, para todo .
La varianza es constante a lo largo del tiempo, es decir, para todo .
La covarianza no depende del tiempo, lo que implica que para cualquier par de instantes y , y para cualquier desfase (lag)
Ejemplo
Hay muchos ejemplos de tipos de no estacionareidad. Ilustremos los más tipicos con unos ejemplos ficticios.
x = np.linspace(0, np.pi*10, 360)y = np.sin(x)
Podemos generar los diferentes tipos con algunas manipulaciones algebráicas.
import matplotlib as pltimport matplotlib.pyplot as pltfig, axs = plt.subplots(2, 2, figsize=(18, 18))axs[0][0].plot(x, y)axs[0][0].set_title('Serie Estacionaria')axs[0][0].set_xlabel('tiempo')axs[0][0].set_ylabel('Amplitud')axs[0][1].plot(x, y+x/10)axs[0][1].set_title('Media cambiante')axs[0][1].set_xlabel('time')axs[0][1].set_ylabel('Amplitud')axs[1][0].plot(x, y*x/10)axs[1][0].set_title('Varianza cambiante')axs[1][0].set_xlabel('time')axs[1][0].set_ylabel('Amplitud')axs[1][1].plot(np.sin(x+x*x/30))axs[1][1].set_title('Co-variance cambiante')axs[1][1].set_xlabel('time')axs[1][1].set_ylabel('Amplitud')plt.tight_layout()
Ausencia de estacionareidad 1: Tendencias
Un claro ejemplo de series no estacionarias es cuando hay tendencias. Podemos identificar la tendencia a tarvés de una media movil Revisemoslo con los datos de pasajeros de aerolineas.
# Cargar datosairline = pd.read_csv('data_sesion5/international-airline-passengers.csv', sep=';')# Nota: si no les reconoce bien la dependencia de la carpeta, pueden usar también#airline= pd.read_csv("https://github.com/melanieoyarzun/taller_seriestiempo_IDS/blob/8c0b9774be8d4103da3801d3069d82b4fe006461/Data/international-airline-passengers.csv?raw=true", sep=';')airline['Month'] = pd.to_datetime(airline['Month']+'-01')airline.set_index('Month', inplace=True)
Con un grafico rápido, identificamos que está todo bien y que efectivamente se observa que la serie no es estacionaria.
Podemos identificar la tendencia en los datos, al calcular la media movil.
def running_average(x, order): current = x[:order].sum() running = []for i inrange(order, x.shape[0]): current += x[i] current -= x[i-order] running.append(current/order)return np.array(running)
Esta función es autoexplicativa. Simplemente corre en el dataset paso a paso y calcula la media en una ventana específica. Ahora podemos agregar esta línea de tendencia al gráfico anterior.
Ausencia de estacionareidad 2: Estacionalidad (no confundir con estacionareidad)
Es cuando hay un patron claro de ciclos que se repite en el tiempo. Generalmente los identificamos a priori con una inspección del grafico. Podemos usar una función para identificarlos.
def plot_seasons(detrended, order, plot_mean =True): colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] N =len(detrended) data = np.array([detrended[i::order] for i inrange(order)]) means = np.mean(data, axis=1) medians = np.median(data, axis=1) counts = [0] counts.extend([len(data[i]) for i inrange(order)]) counts = np.cumsum(counts) ticks = (counts[:-1]+counts[1]/2)for i inrange(order): values = data[i, :] npoints =len(values) plt.plot(range(counts[i], counts[i+1]), values, c=colors[0]) plt.plot(range(counts[i], counts[i+1]), np.ones(npoints)*means[i], c=colors[1]) plt.plot(range(counts[i], counts[i+1]), np.ones(npoints)*medians[i], c=colors[2]) plt.legend(['data', 'mean', 'median']) plt.xlabel('season') plt.ylabel('values') plt.xticks(ticks, np.arange(order));if plot_mean: plt.plot(ticks, means, c=colors[3])return means
Acá, simplemente vamos a graficar la curva para diferentes periodos en la temporada. Esto se hace yendo a traves del dataset con un stride igual al periodo estacional.
La figura tambien nos provee una forma arquetípica de comportamiento estacional. COn esto, podememos terminar la descomposición de la data en tres componentes.
Con esto , el patron estacional se remueve al repetir el patron medio estacional identificado anteriormente y dividiendo, desde la data sin tendencia. El resultado de esta disvision son simpemente los residuos. Lo que nos muestra que esta descomposición es demasiado sencilla aun.
Acá usamos una descomposición multiplicativa, pero este mismo proneso se podría haber hecho siguiendo una descomposición aditiva, con pequeños cambios al codigo.
Ausencia de estacionareidad 3: Quiebre estructural
Otro tipo de no estacionaeridad se presenta cuando la función de regresión poblacional cambia en el transcurso de la las observaciones. Esto puede ocurrir por varios motivos, por ejemplo cambios en una politica económica, cambios en la estructura de la economía, un nuevo invento o disrupción tecnológica, etc.
Si ocurren tales “cambios estructurales” o “rupturas”, entonces un modelo de regresión que no tenga en cuenta esos cambios puede proporcionar una base engañosa para la inferencia ya la predicción.
Las estrategias para identificar un cambio estructurale son varias revisaremos dos:
Contrastes de hipótesis comparando cambios en los coeficientes de regresión mediante estadístico F o Test de Chow.
La segunda es biuscar potenciales cambios estructurales desde la predicción: se simula que la mmuestra termina antes de lo que realmente lo hace y se comparan las predicciones. Los cambios estructurales se detectan cuando la capacidad de predicción es sustancialmente peor de lo esperado.
Ejemplo datos de amazon
Podemos utilizar datos de acciones y verificar si hay un quiebre estructural antes y después de la crisis financiera de 2008.
Para hacerlo, necesitaremos datos históricos de acciones y realizaremos análisis de series temporales.
# Importar las bibliotecas necesariasimport numpy as npimport pandas as pdimport statsmodels.api as smimport matplotlib.pyplot as plt# Obtener datos históricos de acciones de Amazonfrom yahoo_fin.stock_info import get_dataamazon_subprime= get_data("amzn", start_date="12/04/2000", end_date="25/09/2015", index_as_date =False, interval="1wk")amazon_subprime.head()
/Users/melita/anaconda3/lib/python3.10/site-packages/yahoo_fin/stock_info.py:29: UserWarning:
Parsing dates in DD/MM/YYYY format when dayfirst=False (the default) was specified. This may lead to inconsistently parsed dates! Specify a format to ensure consistent parsing.
date
open
high
low
close
adjclose
volume
ticker
0
2000-12-04
1.259375
1.381250
1.006250
1.171875
1.171875
1013370000
AMZN
1
2000-12-11
1.143750
1.375000
1.087500
1.143750
1.143750
804546000
AMZN
2
2000-12-18
1.037500
1.059375
0.743750
0.778125
0.778125
1390856000
AMZN
3
2000-12-25
0.815625
0.925000
0.750000
0.778125
0.778125
678338000
AMZN
4
2001-01-01
0.790625
0.893750
0.678125
0.728125
0.728125
866064000
AMZN
Graficamos los datos
import seaborn as sns# Crea el scatterplotsns.scatterplot(data=amazon_subprime, y="open", x="date")plt.xlabel("Fecha")plt.ylabel("Precio de Apertura")plt.title("Precios de Apertura de Acciones de Amazon (2000-2015)")plt.show()
Podriamos ajustar un modelo a todos los datos o separar anres y después de la crisis subprime.
# Dividir los datos en tres conjuntos: antes de 2008, después de 2008 y todos los datosdata_before_2008 = amazon_subprime[amazon_subprime['date'] <'2008-01-01']data_after_2008 = amazon_subprime[amazon_subprime['date'] >='2008-01-01']# Ajustar modelos de regresión lineal para cada conjunto de datosmodel_all_data = sm.OLS(amazon_subprime['open'], sm.add_constant(np.arange(len(amazon_subprime)))).fit()model_before_2008 = sm.OLS(data_before_2008['open'], sm.add_constant(np.arange(len(data_before_2008)))).fit()model_after_2008 = sm.OLS(data_after_2008['open'], sm.add_constant(np.arange(len(data_after_2008)))).fit()# Graficar los datos y las líneas de regresiónplt.figure(figsize=(12, 6))# Datos de todos los datos (en negro)plt.scatter(amazon_subprime['date'], amazon_subprime['open'], color='lightgray', label='Todos los datos')# Línea de regresión para todos los datos (en negro)plt.plot(amazon_subprime['date'], model_all_data.predict(sm.add_constant(np.arange(len(amazon_subprime)))), color='black', linewidth=2)# Datos hasta 2008 (en azul)plt.scatter(data_before_2008['date'], data_before_2008['open'], color='lightblue', label='Hasta 2008')# Línea de regresión hasta 2008 (en azul)plt.plot(data_before_2008['date'], model_before_2008.predict(sm.add_constant(np.arange(len(data_before_2008)))), color='blue', linewidth=2)# Datos después de 2008 (en rojo)plt.scatter(data_after_2008['date'], data_after_2008['open'], color='lightcoral', label='Después de 2008')# Línea de regresión después de 2008 (en rojo)plt.plot(data_after_2008['date'], model_after_2008.predict(sm.add_constant(np.arange(len(data_after_2008)))), color='red', linewidth=2)plt.xlabel("Fecha")plt.ylabel("Precio de Apertura")plt.title("Líneas de Regresión para Precios de Apertura de Acciones de Amazon")plt.legend()plt.show()
Claramente, los modelos por separados oarecen que explican mejor. Al parecer hay quiebre estructural. Revisemos usando el test de chow:
# Dividir los datos en dos conjuntos: antes de 2008 y después de 2008data_before_2008 = amazon_subprime[amazon_subprime['date'] <'2008-01-01']data_after_2008 = amazon_subprime[amazon_subprime['date'] >='2008-01-01']# Ajustar modelos de regresión lineal para cada conjunto de datosmodel_before_2008 = sm.OLS(data_before_2008['open'], sm.add_constant(np.arange(len(data_before_2008)))).fit()model_after_2008 = sm.OLS(data_after_2008['open'], sm.add_constant(np.arange(len(data_after_2008)))).fit()# Calcular SSR para cada modelossr_before_2008 = np.sum(model_before_2008.resid **2)ssr_after_2008 = np.sum(model_after_2008.resid **2)# Combinar todos los datos en un solo modeloall_data = amazon_subprime['open']model_all_data = sm.OLS(all_data, sm.add_constant(np.arange(len(amazon_subprime)))).fit()# Calcular SSR para el modelo completossr_all_data = np.sum(model_all_data.resid **2)# Calcular el estadístico F para el Test de Chowk =2# Número de coeficientes (incluyendo el intercepto)N =len(amazon_subprime)f_statistic = ((ssr_all_data - (ssr_before_2008 + ssr_after_2008)) / k) / ((ssr_before_2008 + ssr_after_2008) / (N -2* k))# Calcular el valor p asociado al estadístico Ffrom scipy.stats import fp_value =1- f.cdf(f_statistic, k, N -2* k)print("Valor p del Test de Chow:", p_value)