Está en la página 1de 36

10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Blog de Tomás Acerca de Archivo RSS Etiquetas

Pandas modernos (Parte 7): Serie


temporal
13 de mayo de 2016

Esta es la parte 7 de mi serie sobre cómo escribir pandas idiomáticos modernos.


Pandas modernos
Encadenamiento de métodos
Índices
Pandas rápidos
Datos ordenados
Visualización
Series de tiempo
Escalada

Series de tiempo
Pandas comenzó en el mundo financiero, por lo que, naturalmente, tiene un fuerte
soporte de series temporales.
La primera mitad de esta publicación analizará las capacidades de los pandas para
manipular datos de series temporales. La segunda mitad discutirá el modelado de
datos de series de tiempo con statsmodels.
%matplotlib inline

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 1/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

import os
import numpy as np
import pandas as pd
import pandas_datareader.data as web
import seaborn as sns
import matplotlib.pyplot as plt
sns.set(style='ticks', context='talk')

if int(os.environ.get("MODERN_PANDAS_EPUB", 0)):
import prep # noqa

Tomemos algunos datos bursátiles de Goldman Sachs usando el pandas-


paquete, que surgió de pandas:
datareader

gs = web.DataReader("GS", data_source='yahoo', start='2006-01-01',


end='2010-01-01')
gs.head().round(2)

Abierto Alto Bajo Cerca cerrar Volumen


Fecha
2006-01-03 126.70 129.44 124.23 128.87 112.34 6188700
2006-01-04 127.35 128.91 126.38 127.09 110.79 4861600
2006-01-05 126.00 127.32 125.61 127.04 110.74 3717400
2006-01-06 127.29 129.25 127.29 128.84 112.31 4319600
2006-01-09 128.50 130.62 128.00 130.39 113.66 4723500

No hay un contenedor de datos especial solo para series temporales en pandas, son
solo o
Series s con una extensión
DataFrame . DatetimeIndex

Rebanado especial
https://tomaugspurger.github.io/posts/modern-7-timeseries/ 2/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Mirando los elementos de gs.index , vemos que DatetimeIndex es se compone de


s:
pandas.Timestamp

Mirando los elementos de gs.index , vemos que DatetimeIndex es se compone de


s:
pandas.Timestamp

gs.index[0]

Timestamp('2006-01-03 00:00:00')

A es mayormente compatible con la


Timestamp clase, pero se datetime.datetime

presta mucho al almacenamiento en arreglos.


Trabajar con s puede ser incómodo, por lo que Series y DataFrames
Timestamp

DatetimeIndexestienen algunas reglas de división especiales. El primer caso especial


es la indexación de cadenas parciales . Digamos que queríamos seleccionar todos los
días en 2006. Incluso con los convenientes constructores de , es un pai
Timestamp

gs.loc[pd.Timestamp('2006-01-01'):pd.Timestamp('2006-12-31')].head()

Abierto Alto Bajo Cerca cerrar Volu


Fecha
2006- 126.699997 129.440002 124.230003 128.869995 112.337547 618
01-03
2006- 127.349998 128.910004 126.379997 127.089996 110.785889 486
01-04
2006- 126.000000 127.320000 125.610001 127.040001 110.742340 3717
01-05
2006- 127.290001 129.250000 127.290001 128.839996 112.311401 431
01-06

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 3/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Abierto Alto Bajo Cerca cerrar Volu


Fecha
2006- 128.500000 130.619995 128.000000 130.389999 113.662605 472
01-09
Gracias a la indexación de cadenas parciales, es tan simple como
gs.loc['2006'].head()

Abierto Alto Bajo Cerca cerrar Volu


Fecha
2006- 126.699997 129.440002 124.230003 128.869995 112.337547 618
01-03
2006- 127.349998 128.910004 126.379997 127.089996 110.785889 486
01-04
2006- 126.000000 127.320000 125.610001 127.040001 110.742340 3717
01-05
2006- 127.290001 129.250000 127.290001 128.839996 112.311401 431
01-06
2006- 128.500000 130.619995 128.000000 130.389999 113.662605 472
01-09

Dado que la división de etiquetas es inclusiva, esta división selecciona cualquier


observación cuyo año sea 2006.
La segunda "conveniencia" es la indexación alternativa (entre
__getitem__

corchetes). Solo lo mencionaré aquí, con la advertencia de que nunca debe usarlo.
DataFrame generalmente busca en la columna:
__getitem__ buscaría , gs['2006']

no lo encontraría y generaría un . Pero DataFrames con una captura de eso e intenta


dividir el índice. Si logra dividir el índice, se devuelve el resultado. Si falla, se vuelve a
https://tomaugspurger.github.io/posts/modern-7-timeseries/ 4/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

subir. Esto es confuso porque en casi todos los demás casos funciona en columnas, y
es frágil porque si tuviera una columna , obtendría solo esa columna y no se produciría
una indexación alternativa. Solo utilícelo cuando corte los índices de
DataFrame. gs.columns '2006' KeyError DatetimeIndex KeyError gs.loc['2006'

] KeyError DataFrame.__getitem__ '2006' gs.loc['2006']

Métodos especiales
remuestreo
El remuestreo es similar a un : divide la serie temporal en grupos (cubos de 5
groupby

días a continuación), aplica una función a cada grupo ( ) y combina el resultado mean

(una fila por grupo).


gs.resample("5d").mean().head()

Abierto Alto Bajo Cerca cerrar Volu


Fecha
2006- 126.834999 128.730002 125.877501 127.959997 111.544294 4.77
01-03
2006- 130.349998 132.645000 130.205002 131.660000 114.769649 4.66
01-08
2006- 131.510002 133.395005 131.244995 132.924995 115.872357 3.25
01-13
2006- 132.210002 133.853333 131.656667 132.543335 115.611125 4.99
01-18
2006- 133.771997 136.083997 133.310001 135.153998 118.035918 3.96
01-23

gs.resample("W").agg(['mean', 'sum']).head()

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 5/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Abierto Alto Bajo


significar suma significar suma significar sum
Fecha
2006- 126.834999 507.339996 128.730002 514.920006 125.877501 50
01-08
2006- 130.684000 653.419998 132.848001 664.240006 130.544000 65
01-15
2006- 131.907501 527.630005 133.672501 534.690003 131.389999 52
01-22
2006- 133.771997 668.859986 136.083997 680.419983 133.310001 66
01-29
2006- 140.900000 704.500000 142.467999 712.339996 139.937998 69
02-05
Puede aumentar la muestra para convertir a una frecuencia más alta. Los nuevos
puntos se rellenan con NaN.
gs.resample("6H").mean().head()

Abierto Alto Bajo Cerca cerrar Volu


Fecha
2006-
01-03 126.699997 129.440002 124.230003 128.869995 112.337547 618
00:00:00
2006-
01-03 Yaya Yaya Yaya Yaya Yaya Yaya
06:00:00
2006- Yaya Yaya Yaya Yaya Yaya Yaya
01-03
https://tomaugspurger.github.io/posts/modern-7-timeseries/ 6/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Abierto Alto Bajo Cerca cerrar Volu


Fecha
12:00:00
2006-
01-03 Yaya Yaya Yaya Yaya Yaya Yaya
18:00:00
2006-
01-04 127.349998 128.910004 126.379997 127.089996 110.785889 486
00:00:00

Laminado / Expansión / EW
Estos métodos no son exclusivos de DatetimeIndex es, pero a menudo tienen sentido
con series de tiempo, así que los mostraré aquí.
gs.Close.plot(label='Raw')
gs.Close.rolling(28).mean().plot(label='28D MA')
gs.Close.expanding().mean().plot(label='Expanding Average')
gs.Close.ewm(alpha=0.03).mean().plot(label='EWMA($\\alpha=.03$)')

plt.legend(bbox_to_anchor=(1.25, .5))
plt.tight_layout()
plt.ylabel("Close ($)")
sns.despine()

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 7/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Cada uno de .rolling , .expanding y .ewm devuelve un objeto diferido, similar a


GroupBy.
roll = gs.Close.rolling(30, center=True)
roll

Rolling [window=30,center=True,axis=0]

m = roll.agg(['mean', 'std'])
ax = m['mean'].plot()
ax.fill_between(m.index, m['mean'] - m['std'], m['mean'] + m['std'],
alpha=.25)
plt.tight_layout()
plt.ylabel("Close ($)")
sns.despine()

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 8/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Bolsa de sorpresas
Compensaciones
Son similares a dateutil.relativedelta , pero funcionan con arreglos.
gs.index + pd.DateOffset(months=3, days=-2)

DatetimeIndex(['2006-04-01', '2006-04-02', '2006-04-03', '2006-04-04',


'2006-04-07', '2006-04-08', '2006-04-09', '2006-04-10',
'2006-04-11', '2006-04-15',
...
'2010-03-15', '2010-03-16', '2010-03-19', '2010-03-20',
'2010-03-21', '2010-03-22', '2010-03-26', '2010-03-27',
'2010-03-28', '2010-03-29'],
dtype='datetime64[ns]', name='Date', length=1007, freq=None)

Calendarios de días festivos


https://tomaugspurger.github.io/posts/modern-7-timeseries/ 9/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Hay un montón de calendarios especiales, probablemente útiles para los


comerciantes.
from pandas.tseries.holiday import USColumbusDay

USColumbusDay.dates('2015-01-01', '2020-01-01')

DatetimeIndex(['2015-10-12', '2016-10-10', '2017-10-09', '2018-10-08',


'2019-10-14'],
dtype='datetime64[ns]', freq='WOM-2MON')

Zonas horarias
Pandas funciona con agradables fechas y horas conscientes de la zona horaria.
pytz

El flujo de trabajo típico es


1. localizar las marcas de tiempo ingenuas de la zona horaria en alguna zona horaria
2. convertir a la zona horaria deseada
Si ya tiene marcas de tiempo con reconocimiento de zona horaria, no es necesario
realizar el primer paso.
# tz naiive -> tz aware..... to desired UTC
gs.tz_localize('US/Eastern').tz_convert('UTC').head()

Abierto Alto Bajo Cerca cerrar


Fecha
2006-01-03 126.699997 129.440002 124.230003 128.869995 112.337547
05:00:00+00:00
2006-01-04 127.349998 128.910004 126.379997 127.089996 110.785889
05:00:00+00:00
2006-01-05 126.000000 127.320000 125.610001 127.040001 110.742340
05:00:00+00:00
https://tomaugspurger.github.io/posts/modern-7-timeseries/ 10/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Abierto Alto Bajo Cerca cerrar


Fecha
2006-01-06 127.290001 129.250000 127.290001 128.839996 112.311401
05:00:00+00:00
2006-01-09 128.500000 130.619995 128.000000 130.389999 113.662605
05:00:00+00:00

Modelado de series de tiempo


El resto de esta publicación se centrará en series de tiempo en el sentido
econométrico. Mi lector sangrado para esta sección no es tan claro, así que pido
disculpas por adelantado por cualquier cambio repentino en la complejidad. Estoy
apuntando aproximadamente al material que podría presentarse en un curso de
estadística aplicada del primer o segundo semestre. Lo que sigue ciertamente no es
un reemplazo para eso. Cualquier formalidad quedará restringida a notas a pie de
página para curiosos. He puesto un montón de recursos al final para las personas que
desean aprender más.
Nos centraremos en modelar los vuelos mensuales promedio. Descarguemos los
datos. Si has estado siguiendo la serie, ya has visto la mayor parte de este código
antes, así que siéntete libre de omitirlo.
import os
import io
import glob
import zipfile
from utils import download_timeseries

import statsmodels.api as sm

def download_many(start, end):


months = pd.period_range(start, end=end, freq='M')
# We could easily parallelize this loop.
for i, month in enumerate(months):

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 11/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

download_timeseries(month)

def time_to_datetime(df, columns):


'''
Combine all time items into datetimes.

2014-01-01,1149.0 -> 2014-01-01T11:49:00


'''
def converter(col):
timepart = (col.astype(str)
.str.replace('\.0$', '') # NaNs force float dtype
.str.pad(4, fillchar='0'))
return pd.to_datetime(df['fl_date'] + ' ' +
timepart.str.slice(0, 2) + ':' +
timepart.str.slice(2, 4),
errors='coerce')
return datetime_part
df[columns] = df[columns].apply(converter)
return df

def read_one(fp):
df = (pd.read_csv(fp, encoding='latin1')
.rename(columns=str.lower)
.drop('unnamed: 6', axis=1)
.pipe(time_to_datetime, ['dep_time', 'arr_time', 'crs_arr_time',
'crs_dep_time'])
.assign(fl_date=lambda x: pd.to_datetime(x['fl_date'])))
return df

/Users/taugspurger/miniconda3/envs/modern-pandas/lib/python3.6/site-packages/stats
from pandas.core import datetools

store = 'data/ts.hdf5'

if not os.path.exists(store):
download_many('2000-01-01', '2016-01-01')

zips = glob.glob(os.path.join('data', 'timeseries', '*.zip'))


csvs = [unzip_one(fp) for fp in zips]
dfs = [read_one(fp) for fp in csvs]
df = pd.concat(dfs, ignore_index=True)

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 12/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

df['origin'] = df['origin'].astype('category')
df.to_hdf(store, 'ts', format='table')
else:
df = pd.read_hdf(store, 'ts')

with pd.option_context('display.max_rows', 100):


print(df.dtypes)

fl_date datetime64[ns]
origin category
crs_dep_time datetime64[ns]
dep_time datetime64[ns]
crs_arr_time datetime64[ns]
arr_time datetime64[ns]
dtype: object

Podemos calcular los valores históricos con una nueva muestra.


daily = df.fl_date.value_counts().sort_index()
y = daily.resample('MS').mean()
y.head()

2000-01-01 15176.677419
2000-02-01 15327.551724
2000-03-01 15578.838710
2000-04-01 15442.100000
2000-05-01 15448.677419
Freq: MS, Name: fl_date, dtype: float64

Tenga en cuenta que utilizo el código de frecuencia allí. Pandas por defecto a
"MS"

fin de mes (o fin de año). Agregue un para comenzar. 'S'

ax = y.plot()
ax.set(ylabel='Average Monthly Flights')
sns.despine()

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 13/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

import statsmodels.formula.api as smf


import statsmodels.tsa.api as smt
import statsmodels.api as sm

Una nota de advertencia: estoy usando la versión de desarrollo de statsmodels


(confirmar para ser precisos). No todos los elementos que he mostrado aquí
de15ec8

están disponibles en la versión actual.


Piense en un problema de regresión típico, ignorando todo lo que tenga que ver con
series de tiempo por ahora. La tarea habitual es predecir algún valor $y$ usando
alguna combinación lineal de características en $X$.
$$y = \beta_0 + \beta_1 X_1 + \ldots + \beta_p X_p + \epsilon$$
Cuando se trabaja con series de tiempo, algunas de las características más
importantes (ya veces las únicas ) son los valores anteriores o rezagados de $y$.
Comencemos por intentarlo "manualmente": ejecutar una regresión de los valores y

rezagados de sí mismo. Veremos que esta regresión adolece de algunos problemas:


multicolinealidad, autocorrelación, no estacionariedad y estacionalidad. Explicaré qué
https://tomaugspurger.github.io/posts/modern-7-timeseries/ 14/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

es cada uno de ellos y por qué son problemas. Posteriormente, utilizaremos un


segundo modelo, ARIMA estacional, que soluciona esos problemas por nosotros.
Primero, creemos un marco de datos con nuestros valores rezagados de usar el y
.shiftmétodo, que cambia los períodos del índice, para que se alinee con esa
i

observación.
X = (pd.concat([y.shift(i) for i in range(6)], axis=1,
keys=['y'] + ['L%s' % i for i in range(1, 6)])
.dropna())
X.head()

y L1 L2 L3
2000- 15703.333333 15448.677419 15442.100000 15578.838710 15327.5
06-01
2000- 15591.677419 15703.333333 15448.677419 15442.100000 15578.8
07-01
2000- 15850.516129 15591.677419 15703.333333 15448.677419 15442.1
08-01
2000- 15436.566667 15850.516129 15591.677419 15703.333333 15448.6
09-01
2000- 15669.709677 15436.566667 15850.516129 15591.677419 15703.3
10-01

Podemos ajustar el modelo rezagado usando statsmodels (que usa patsy para traducir
la cadena de fórmula a una matriz de diseño).
mod_lagged = smf.ols('y ~ trend + L1 + L2 + L3 + L4 + L5',
data=X.assign(trend=np.arange(len(X))))
res_lagged = mod_lagged.fit()
res_lagged.summary()

Resultados de la regresión OLS


https://tomaugspurger.github.io/posts/modern-7-timeseries/ 15/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

dep. Variable: y R-cuadrado: 0.896


Modelo: MCO adj. R-cuadrado: 0.893
Método: mínimos cuadrados Estadística F: 261.1
Fecha: dom, 03 sep 2017 Prob (estadística F): 2.61e-86
Tiempo: 11:21:46 Logaritmo de probabilidad: -1461.2
Nº Observaciones: 188 AIC: 2936.
Residuos Df: 181 BIC: 2959.
Modelo Df: 6
Tipo de covarianza: no robusto

coef error estándar t P>|t| [0.025 0.975]


Interceptar 1055.4443 459.096 2.299 0.023 149.575 1961.314
tendencia -1.0395 0.795 -1.307 0.193 -2.609 0.530
L1 1.0143 0.075 13.543 0.000 0.867 1.162
L2 -0.0769 0.106 -0.725 0.470 -0.286 0.133
L3 -0.0666 0.106 -0.627 0.531 -0.276 0.143
L4 0.1311 0.106 1.235 0.219 -0.078 0.341
L5 -0.0567 0.075 -0.758 0.449 -0.204 0.091
https://tomaugspurger.github.io/posts/modern-7-timeseries/ 16/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

General: 74.709 Durbin-Watson: 1.979


Prob(ómnibus): 0.000 Jarque Bera (JB): 851.300
Sesgar: 1.114 Prob(JB): 1.39e-185
Curtosis: 13.184 cond. No. 4.24e+05

Sin embargo, hay algunos problemas con este enfoque. Dado que nuestros valores
rezagados están altamente correlacionados entre sí, nuestra regresión sufre de
multicolinealidad . Eso arruina nuestras estimaciones de las pendientes.
sns.heatmap(X.corr());

En segundo lugar, esperaríamos intuitivamente que los $\beta_i$s disminuyan


gradualmente hasta cero. El período inmediatamente anterior debería ser el más
importante ($\beta_1$ es el coeficiente más grande en valor absoluto), seguido de
https://tomaugspurger.github.io/posts/modern-7-timeseries/ 17/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

$\beta_2$ y $\beta_3$... Mirando el resumen de regresión y el gráfico de barras a


continuación, esto no es el caso (la causa está relacionada con la multicolinealidad).
ax = res_lagged.params.drop(['Intercept', 'trend']).plot.bar(rot=0)
plt.ylabel('Coefficeint')
sns.despine()

Finalmente, nuestros grados de libertad caen ya que perdemos dos por cada variable
(uno por estimar el coeficiente, uno por la observación perdida como resultado de )
shift. Al menos en (macro) econometría, cada observación es valiosa y no nos
gusta tirarla, aunque a veces eso es inevitable.
Autocorrelación
Otro problema que sufrió nuestro modelo rezagado es la autocorrelación (también
conocida como correlación serial). En términos generales, la autocorrelación es
cuando hay un patrón claro en los residuos de su regresión (lo observado menos lo
predicho). Ajustemos un modelo simple de $y = \beta_0 + \beta_1 T + \epsilon$, donde
está la tendencia temporal (
T ). np.arange(len(y))

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 18/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

# `Results.resid` is a Series of residuals: y - ŷ


mod_trend = sm.OLS.from_formula(
'y ~ trend', data=y.to_frame(name='y')
.assign(trend=np.arange(len(y))))
res_trend = mod_trend.fit()

Se supone que los residuos (lo observado menos lo esperado, o $\hat{e_t} = y_t -
\hat{y_t}$) son ruido blanco . Esa es una de las suposiciones sobre las que se basan
muchas de las propiedades de la regresión lineal. En este caso, existe una correlación
entre un residuo y el siguiente: si el residuo en el momento $t$ estuvo por encima de
las expectativas, entonces es mucho más probable que el residuo en el momento $t +
1$ también esté por encima del promedio ($e_t > 0 \implica E_t[e_{t+1}] > 0$).
Definiremos una función auxiliar para trazar la serie temporal de residuos y algunos
diagnósticos sobre ellos.
def tsplot(y, lags=None, figsize=(10, 8)):
fig = plt.figure(figsize=figsize)
layout = (2, 2)
ts_ax = plt.subplot2grid(layout, (0, 0), colspan=2)
acf_ax = plt.subplot2grid(layout, (1, 0))
pacf_ax = plt.subplot2grid(layout, (1, 1))

y.plot(ax=ts_ax)
smt.graphics.plot_acf(y, lags=lags, ax=acf_ax)
smt.graphics.plot_pacf(y, lags=lags, ax=pacf_ax)
[ax.set_xlim(1.5) for ax in [acf_ax, pacf_ax]]
sns.despine()
plt.tight_layout()
return ts_ax, acf_ax, pacf_ax

Llamándolo a los residuos de la tendencia lineal:


tsplot(res_trend.resid, lags=36);

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 19/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

La subparcela superior muestra la serie temporal de nuestros residuales $e_t$, que


debería ser ruido blanco (pero no lo es). La parte inferior muestra la autocorrelación de
los residuos como un correlograma. Mide la correlación entre un valor y su propio
retraso, por ejemplo, $corr(e_t, e_{t-1}), corr(e_t, e_{t-2}), \ldots$. La gráfica de
autocorrelación parcial en la parte inferior derecha muestra un concepto similar. Es
parcial en el sentido de que el valor de $corr(e_t, e_{tk})$ es la correlación entre esos
dos períodos, después de controlar los valores en todos los retrasos más cortos.
La autocorrelación es un problema en las regresiones regulares como la anterior, pero
la usaremos a nuestro favor cuando configuremos un modelo ARIMA a continuación.
La idea básica es bastante sensata: si sus residuos de regresión tienen un patrón
claro, entonces claramente hay alguna estructura en los datos que no está
aprovechando. Si un residual positivo hoy significa que probablemente tendrá un
https://tomaugspurger.github.io/posts/modern-7-timeseries/ 20/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

residual positivo mañana, ¿por qué no incorporar esa información en su pronóstico y


reducir su valor pronosticado para mañana? Eso es más o menos lo que hace ARIMA.
Es importante que su conjunto de datos sea estacionario, de lo contrario corre el
riesgo de encontrar correlaciones falsas . Un ejemplo común es la relación entre el
número de televisores por persona y la esperanza de vida. No es probable que haya
una relación causal real allí. Más bien, podría haber una tercera variable que esté
impulsando ambas (la riqueza, por ejemplo). Granger y Newbold (1974) tuvieron
algunas palabras severas para la literatura econométrica sobre esto.
Nos resulta muy curioso que, mientras que prácticamente todos los libros de texto
sobre metodología econométrica contienen advertencias explícitas sobre los
peligros de los errores autocorrelacionados, este fenómeno surge con tanta
frecuencia en trabajos aplicados muy respetados.
(:fuego:), pero de esa manera académica pasivo-agresiva.
La forma típica de manejar la no estacionariedad es diferenciar la variable no
estacionaria hasta que sea estacionaria.
y.to_frame(name='y').assign(Δy=lambda x: x.y.diff()).plot(subplots=True)
sns.despine()

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 21/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Nuestra serie original en realidad no se ve tan mal. No se parece al PIB nominal,


digamos, donde hay una tendencia claramente alcista. Pero tenemos métodos más
rigurosos para detectar si una serie no es estacionaria que simplemente graficarla y
entrecerrar los ojos. Un método popular es la prueba Dickey-Fuller aumentada. Es una
prueba de hipótesis estadística que dice más o menos:
$H_0$ (hipótesis nula): $y$ no es estacionario, debe diferenciarse
$H_A$ (hipótesis alternativa): $y$ es estacionario, no necesita diferenciarse
No quiero entrar en detalles sobre cuál es exactamente la estadística de prueba y
cómo se ve la distribución. Esto se implementa en statsmodels como . El smt.adfuller

tipo de retorno está un poco ocupado para mí, así que lo envolveremos en un
namedtuple .
from collections import namedtuple

ADF = namedtuple("ADF", "adf pvalue usedlag nobs critical icbest")

ADF(*smt.adfuller(y))._asdict()

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 22/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

OrderedDict([('adf', -1.3206520699512339),
('pvalue', 0.61967180643147923),
('usedlag', 15),
('nobs', 177),
('critical',
{'1%': -3.4678453197999071,
'10%': -2.575551186759871,
'5%': -2.8780117454974392}),
('icbest', 2710.6120408261486)])

Así que no pudimos rechazar la hipótesis nula de que la serie original no era
estacionaria. Vamos a diferenciarlo.
ADF(*smt.adfuller(y.diff().dropna()))._asdict()

OrderedDict([('adf', -3.6412428797327996),
('pvalue', 0.0050197770854934548),
('usedlag', 14),
('nobs', 177),
('critical',
{'1%': -3.4678453197999071,
'10%': -2.575551186759871,
'5%': -2.8780117454974392}),
('icbest', 2696.3891181091631)])

Esto se ve mejor. No es estadísticamente significativo al nivel del 5%, pero a quién le


importa lo que digan las estadísticas de todos modos.
Ajustaremos otro modelo OLS de $\Delta y = \beta_0 + \beta_1 L \Delta y_{t-1} + e_t$
data = (y.to_frame(name='y')
.assign(Δy=lambda df: df.y.diff())
.assign(LΔy=lambda df: df.Δy.shift()))
mod_stationary = smf.ols('Δy ~ LΔy', data=data.dropna())
res_stationary = mod_stationary.fit()

tsplot(res_stationary.resid, lags=24);

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 23/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Así que nos hemos ocupado de la multicolinealidad, la autocorrelación y la


estacionariedad, pero aún no hemos terminado.
estacionalidad
Tenemos fuerte estacionalidad mensual:
smt.seasonal_decompose(y).plot();

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 24/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Hay algunas maneras de manejar la estacionalidad. Solo confiaremos en el


método para que lo haga por nosotros. Por ahora, reconoce que es un
SARIMAX

problema a resolver.

ARIMA
Entonces, hemos esbozado los problemas con la regresión antigua regular:
multicolinealidad, autocorrelación, no estacionariedad y estacionalidad. Nuestra
herramienta de elección, que significa Seasonal ARIMA with eXogenous
smt.SARIMAX
Regressors, puede manejar todo esto. Recorreremos los componentes por partes.
ARIMA significa Media Móvil Integrada AutoRegresiva. Es una forma relativamente
simple pero flexible de modelar series temporales univariadas. Se compone de tres
componentes y normalmente se escribe como $\mathrm{ARIMA}(p, d, q)$.
ARIMA significa AutoRegressive Integrated Moving Average, y es una forma
relativamente simple de modelar series temporales univariadas. Se compone de tres
https://tomaugspurger.github.io/posts/modern-7-timeseries/ 25/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

componentes y normalmente se escribe como $\mathrm{ARIMA}(p, d, q)$.


Autoregresivo
La idea es predecir una variable mediante una combinación lineal de sus valores
rezagados ( auto -regresivo como en la regresión de un valor en su propio pasado ).
Un AR(p), donde $p$ representa el número de valores rezagados utilizados, se
escribe como
$$y_t = c + \phi_1 y_{t-1} + \phi_2 y_{t-2} + \ldots + \phi_p y_{tp} + e_t$$
$c$ es una constante y $e_t$ es ruido blanco. Esto se parece mucho a un modelo de
regresión lineal con múltiples predictores, pero los predictores resultan ser valores
rezagados de $y$ (aunque se estiman de manera diferente).
Integrado
Integrado es como lo contrario de diferenciar, y es la parte que se ocupa de la
estacionariedad. Si tiene que diferenciar su conjunto de datos 1 vez para que sea
estacionario, entonces $d=1$. Introduciremos un poco de notación para diferenciar:
$\Delta y_t = y_t - y_{t-1}$ for $d=1$.
Media móvil
Los modelos MA se parecen un poco al componente AR, pero se trata de valores
diferentes.
$$y_t = c + e_t + \theta_1 e_{t-1} + \theta_2 e_{t-2} + \ldots + \theta_q e_{tq}$$
$c$ nuevamente es una constante y $e_t$ nuevamente es ruido blanco. Pero ahora
los coeficientes son los residuos de predicciones anteriores.
Combinatorio
Juntando eso, un proceso ARIMA(1, 1, 1) se escribe como
$$\Delta y_t = c + \phi_1 \Delta y_{t-1} + \theta_t e_{t-1} + e_t$$
https://tomaugspurger.github.io/posts/modern-7-timeseries/ 26/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Usando la notación de retraso , donde $L y_t = y_{t-1}$, es decir, en y.shift()

pandas, podemos reescribir eso como


$$(1 - \phi_1 L) (1 - L)y_t = c + (1 + \theta L)e_t$$
Eso fue para nuestro modelo $\mathrm{ARIMA}(1, 1, 1)$ específico. Para el
$\mathrm{ARIMA}(p, d, q)$ general, eso se convierte en
$$(1 - \phi_1 L - \ldots - \phi_p L^p) (1 - L)^d y_t = c + (1 + \theta L + \ldots + \theta_q
L^q)e_t$$
Pasamos por eso extremadamente rápido, así que no te sientas mal si las cosas no
están claras. Afortunadamente, el modelo es bastante fácil de usar con statsmodels
(usarlo correctamente , en un sentido estadístico, es otra cuestión).
mod = smt.SARIMAX(y, trend='c', order=(1, 1, 1))
res = mod.fit()
tsplot(res.resid[2:], lags=24);

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 27/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

res.summary()

Resultados del modelo de espacio de estados


dep. Variable: fl_date Nº Observaciones: 193
Modelo: SARIMAX(1, 1, 1) Probabilidad de registro -1494.618
Fecha: dom, 03 sep 2017 AIC 2997.236
Tiempo: 11:21:50 BIC 3010.287
Muestra: 01-01-2000 HQIC 3002.521
- 01-01-2016
https://tomaugspurger.github.io/posts/modern-7-timeseries/ 28/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Tipo de covarianza: opg

coef error estándar z P>|z| [0.025 0.975]


interceptar -5.4306 66.818 -0.081 0.935 -136.391 125.529
ar.L1 -0.0327 2.689 -0.012 0.990 -5.303 5.237
ma.L1 0.0775 2.667 0.029 0.977 -5.149 5.305
sigma2 3.444e+05 1.69e+04 20.392 0.000 3.11e+05 3.77e+05

Caja Ljung (Q): 225.58 Jarque Bera (JB): 1211.00


Prob(Q): 0.00 Prob(JB): 0.00
Heterocedasticidad (H): 0,67 Sesgar: 1.20
Prob(H) (bilateral): 0.12 Curtosis: 15.07

Hay un montón de resultados allí con varias pruebas, parámetros estimados y criterios
de información. Digamos que las cosas se ven mejor, pero aún no hemos tenido en
cuenta la estacionalidad.
Un modelo ARIMA estacional se escribe como $\mathrm{ARIMA}(p,d,q)×(P,D,Q)_s$.
Las letras minúsculas son para el componente no estacional, al igual que antes. Las
letras mayúsculas son una especificación similar para el componente estacional,
donde $s$ es la periodicidad (4 para trimestral, 12 para mensual).
Es como si tuviéramos dos procesos, uno para el componente no estacional y otro
para los componentes estacionales, y los multiplicamos con reglas regulares de
álgebra.
https://tomaugspurger.github.io/posts/modern-7-timeseries/ 29/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

La forma general de eso parece (citando los documentos de statsmodels aquí)


$$\phi_p(L)\tilde{\phi}_P(L^S)\Delta^d\Delta_s^D y_t = A(t) +
\theta_q(L)\tilde{\theta}_Q(L^s )e_t$$
dónde
$\phi_p(L)$ es el polinomio de rezago autorregresivo no estacional
$\tilde{\phi}_P(L^S)$ es el polinomio de retraso autorregresivo estacional
$\Delta^d\Delta_s^D$ es la serie temporal, $d$ veces diferenciada y $D$
diferenciada estacionalmente.
$A(t)$ es el polinomio de tendencia (incluido el intercepto)
$\theta_q(L)$ es el polinomio de rezago promedio móvil no estacional
$\tilde{\theta}_Q(L^s)$ es el polinomio de retraso de la media móvil estacional
No creo que sea muy claro, pero tal vez un ejemplo ayude. Adaptaremos un
ARIMA$(1,1,2)×(0, 1, 2)_{12}$ estacional.
Entonces el componente no estacional es
$p=1$: periodo autorregresivo: use $y_{t-1}$
$d=1$: una primera diferenciación de los datos (un mes)
$q=2$: use los dos residuales no estacionales anteriores, $e_{t-1}$ y $e_{t-2}$,
para pronosticar
Y el componente estacional es
$P=0$: no utilice ningún valor estacional anterior
$D=1$: Diferencia la serie 12 periodos atrás: y.diff(12)

$Q=2$: Usa los dos residuales estacionales anteriores


mod_seasonal = smt.SARIMAX(y, trend='c',
order=(1, 1, 2), seasonal_order=(0, 1, 2, 12),
simple_differencing=False)
res_seasonal = mod_seasonal.fit()

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 30/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

res_seasonal.summary()

Resultados del modelo de espacio de estados


dep. Variable: fl_date Nº Observaciones: 193
Modelo: SARIMAX(1, 1, 2)x(0, 1, 2, 12) Probabilidad de registro -1357.847
Fecha: dom, 03 sep 2017 AIC 2729.694
Tiempo: 11:21:53 BIC 2752.533
Muestra: 01-01-2000 HQIC 2738.943
- 01-01-2016
Tipo de covarianza: opg

coef error estándar z P>|z| [0.025 0.975]


interceptar -17.5871 44.920 -0.392 0.695 -105.628 70.454
ar.L1 -0.9988 0.013 -74.479 0.000 -1.025 -0.973
ma.L1 0.9956 0.109 9.130 0.000 0.782 1.209
ma.L2 0.0042 0.110 0.038 0.969 -0.211 0.219
ma.S.L12 -0.7836 0.059 -13.286 0.000 -0.899 -0.668
ma.S.L24 0.2118 0.041 5.154 0.000 0.131 0.292
sigma2 1.842e+05 1.21e+04 15.240 0.000 1.61e+05 2.08e+05

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 31/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Caja Ljung (Q): 32.57 Jarque Bera (JB): 1298.39


Prob(Q): 0.79 Prob(JB): 0.00
Heterocedasticidad (H): 0.17 Sesgar: -1.33
Prob(H) (bilateral): 0.00 Curtosis: 15.89

tsplot(res_seasonal.resid[12:], lags=24);

Las cosas se ven mucho mejor ahora.

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 32/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Una cosa de la que realmente no hablé es la selección de pedidos. Cómo elegir $p, d,
q, P, D$ y $Q$. El paquete de pronóstico de R tiene una función útil que auto.arima

hace esto por usted. Python / statsmodels no tiene eso en este momento. La
alternativa parece ser la experiencia (boo), la intuición (boo) y la vieja búsqueda en
cuadrícula. Puede ajustar un montón de modelos para un montón de combinaciones
de parámetros y usar el AIC o BIC para elegir el mejor. Aquí hay una referencia útil, y
esta respuesta de StackOverflow recomienda algunas opciones.
Pronóstico
Ahora que encajamos en ese modelo, pongámoslo en uso. Primero, haremos un
montón de pronósticos de un paso adelante. En cada punto (mes), tomamos el
historial hasta ese punto y hacemos un pronóstico para el próximo mes. Por lo tanto, el
pronóstico de enero de 2014 tiene disponibles todos los datos hasta diciembre de
2013.
pred = res_seasonal.get_prediction(start='2001-03-01')
pred_ci = pred.conf_int()

ax = y.plot(label='observed')
pred.predicted_mean.plot(ax=ax, label='Forecast', alpha=.7)
ax.fill_between(pred_ci.index,
pred_ci.iloc[:, 0],
pred_ci.iloc[:, 1], color='k', alpha=.2)
ax.set_ylabel("Monthly Flights")
plt.legend()
sns.despine()

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 33/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Hay algunos lugares donde la serie observada se sale del intervalo de confianza del
95%. La serie parece especialmente inestable antes de 2005.
Alternativamente, podemos hacer pronósticos dinámicos a partir de algún mes (enero
de 2013 en el ejemplo a continuación). Eso significa que el pronóstico a partir de ese
momento solo usa información disponible a partir de enero de 2013. Las predicciones
se generan de manera similar: un montón de pronósticos de un solo paso. Solo que en
lugar de ingresar los valores reales más allá de enero de 2013, ingresamos los valores
de pronóstico .
pred_dy = res_seasonal.get_prediction(start='2002-03-01', dynamic='2013-01-01')
pred_dy_ci = pred_dy.conf_int()

ax = y.plot(label='observed')
pred_dy.predicted_mean.plot(ax=ax, label='Forecast')
ax.fill_between(pred_dy_ci.index,
pred_dy_ci.iloc[:, 0],
pred_dy_ci.iloc[:, 1], color='k', alpha=.25)
ax.set_ylabel("Monthly Flights")

# Highlight the forecast area


https://tomaugspurger.github.io/posts/modern-7-timeseries/ 34/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

ax.fill_betweenx(ax.get_ylim(), pd.Timestamp('2013-01-01'), y.index[-1],


alpha=.1, zorder=-1)
ax.annotate('Dynamic $\\longrightarrow$', (pd.Timestamp('2013-02-01'), 550))

plt.legend()
sns.despine()

Recursos
Esta es una colección de enlaces para aquellos interesados.
Modelado de series de tiempo en Python
Cuadernos Statespace de Statsmodels
Tutorial VAR de Statsmodels
https://tomaugspurger.github.io/posts/modern-7-timeseries/ 35/36
10/3/23, 15:05 Pandas modernos (Parte 7): Series temporales | Blog de Tomás

Biblioteca ARCH de Kevin Sheppard


Libros de texto generales
Pronóstico: principios y práctica : una gran introducción
Stock y Watson : recurso de pregrado legible, tiene algunos capítulos sobre series
temporales
Análisis econométrico de Greene : mi libro de texto favorito de nivel de doctorado
Análisis de la serie temporal de Hamilton : un clásico
Nueva introducción de Lutkehpohl al análisis de series temporales múltiples :
Extremadamente seco, pero útil si está implementando estas cosas
Conclusión
Felicidades si llegaste hasta aquí, esta pieza siguió creciendo (y todavía tenía que
cortar cosas). Lo principal que se cortó fue hablar sobre cómo se SARIMAX

implementa además del uso del marco de espacio de estado de statsmodels. El marco
de espacio de estado, desarrollado principalmente por Chad Fulton durante los
últimos dos años, es realmente bueno. Puede ampliarlo con bastante facilidad con
modelos personalizados, pero aún así obtener todos los beneficios de las funciones
de estimación y resultados del marco. Recomiendo leer los cuadernos . Tampoco
pudimos hablar en absoluto sobre el trabajo de Skipper Seabold en VAR, pero tal vez
en otro momento.
Como siempre, los comentarios son bienvenidos .

pandas

© 2023 Blog de Tom Desarrollado por Hugo & PaperMod

https://tomaugspurger.github.io/posts/modern-7-timeseries/ 36/36

También podría gustarte