Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Pandas Modernos (Parte 7) - Series Temporales - Blog de Tomás
Pandas Modernos (Parte 7) - Series Temporales - Blog de Tomás
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
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
gs.index[0]
Timestamp('2006-01-03 00:00:00')
gs.loc[pd.Timestamp('2006-01-01'):pd.Timestamp('2006-12-31')].head()
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
corchetes). Solo lo mencionaré aquí, con la advertencia de que nunca debe usarlo.
DataFrame generalmente busca en la columna:
__getitem__ buscaría , gs['2006']
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'
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
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
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
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)
USColumbusDay.dates('2015-01-01', '2020-01-01')
Zonas horarias
Pandas funciona con agradables fechas y horas conscientes de la zona horaria.
pytz
import statsmodels.api as sm
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 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')
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')
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
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"
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
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()
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());
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
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
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
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
tipo de retorno está un poco ocupado para mí, así que lo envolveremos en un
namedtuple .
from collections import namedtuple
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)])
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
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
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
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()
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
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()
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
tsplot(res_seasonal.resid[12:], lags=24);
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")
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
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
https://tomaugspurger.github.io/posts/modern-7-timeseries/ 36/36