Series temporales: (4) series temporales como features

Serie temporal como feature

¿Qué es dependencia serial?

Ya conocemos las propiedades de las series temporales que se modelan más fácilmente como propiedades dependientes del tiempo, es decir, con features que podíamos derivar directamente del índice del tiempo. Sin embargo, algunas propiedades de las series temporales solo se pueden modelar como propiedades dependientes serialmente, es decir, usando como features valores pasados de la serie objetivo. La estructura de estas series temporales puede no ser evidente en un gráfico de tiempo; sin embargo, dibujada contra valores pasados, la estructura se vuelve clara, como vemos la figura siguiente.

serial_dependence

Estas dos series tienen dependencia serial, pero no dependencia de tiempo. Los puntos de la parte derecha tiene coordenadas (valor en el tiempo t-1, valor en el tiempo t).

Con la tendencia y la estacionalidad, entrenamos modelos para ajustar curvas a gráficos como estos de la izquiera de la figura anterior, donde los modelos aprendían la dependencia de tiempo. El objetivo en este caso es entrenar modelos para ajustar curvas a gráficos como los de la derecha, donde lo que queremos es aprender dependencias seriales.

Ciclos

Una forma especialmente común en la que se manifiesta la dependencia serial es en los ciclos. Los ciclos son patrones de crecimiento y decrecimiento en una serie temporal asociados a cómo los valores de una serie en un momento dependen de los valores del momento anterior, pero no necesariamente del paso de tiempo en sí. El comporamiento cíclico es característico de los sistemas que pueden afectarse a sí mismos o cuyas reacciones persisten en el tiempo. Las economías, las epidemias, las poblaciones animales, las erupciones volcánicas y otros fenómenos naturales similares suelen mostrar comportamientos cíclicos. En la figura siguiente se pueden observar cuatro series temporales con comportamiento cíclico.

comportamiento_ciclico

Lo que distingue el comportamiento cíclico de la estacionalidad es que los ciclos no son necesariamente dependientes del tiempo, como lo es la estacionalidad. Lo que sucede en un ciclo tiene menos que ver con la fecha en particular de ocurrencia y más con lo que sucedió en el pasado reciente. La, al menos relativa, independencia del tiempo significa que el comportamiento cíclico puede ser mucho más irregular que la estacionalidad.

Series demoradas y gráficos de lag

Para investivar la posible dependencia serial (como ciclos) en una serie temporal, necesitamos crear copias “demoradas” (lagged) de la serie. Demorar o lagging una serie temporal significa deslizar/desplazar sus valores hacia adelante uno o más pasos de tiempo, o de forma equivalente, desplazar los tiempos de su índice hacia atrás uno o más pasos de tiempo. En cualquier caso, el efecto es que las observaciones en las serie lagged parecen haber sucedido más tarde en el tiempo.

La siguiente tabla muestra la tasa de desempleo mensual en EEUU (y) junto con sus primera y segunda serie lagged (y_lag_1) y (y_lag_2), respectivamente. Observemos que los valores de las series lagged se desplazan hacia adelante en el tiempo.

import pandas as pd

# dataset de la reserva federal: https://www.kaggle.com/federalreserve/interest-rates
reserve = pd.read_csv(
    "../data/reserve.csv",
    parse_dates={"Date": ['Year', 'Month', 'Day']},
    index_col="Date",
)

y = reserve.loc[:, "Unemployment Rate"].dropna().to_period('M')
df = pd.DataFrame({
    "y": y,
    "y_lag_1": y.shift(1),
    "y_lag_2": y.shift(2),
})

df.head()

y y_lag_1 y_lag_2
Date
1954-07 5.8 NaN NaN
1954-08 6.0 5.8 NaN
1954-09 6.1 6.0 5.8
1954-10 5.7 6.1 6.0
1954-11 5.3 5.7 6.1

Con el lagging de una serie temporal podemos hacer que sus valores pasados parezcan contemporáneos con los valores que intentamos predecir (en la misma fila, en otras palabras). Esto hace a las series lagged útiles como features para modelar la dependencia serial. Para pronosticar la serie de tasa de desempleo en EEUU, podríamos usar y_lag_1 e y_lag_2 como features para predecir el objetivo y. Esto pronosticaría la futura tasa de desempleo en función de la tasa de desempleo de los dos meses anteriores.

Gráficos de lag

Un gráfico de lag de una serie temporal muestra sus valores dibujados contra sus lag’s. La dependencia serial en una serie temporal a menudo se hace evidente mirando un gráfico de lag. Podemos ver en el gráfico de lag de este dataset que hay una relación fuerte y aparentemente lineal entre la tasa de desempleo actual y las tasas pasadas.

lag_plot

La medida más comúnmente usada de dependencia serial es la autocorrelación, que es simplemente la correlación que una serie temporal tiene con uno de sus lag’s. En este dataset, la tasa de desempleo de EEUU tiene una autocorrelación de 0.99 en el lag 1, 0.98 en el lag 2, y así sucesivamente.

Elegir el lag

Generalmente, cuando elegimos lag’s para usarlos como features, no será útil incluir cada lag con una gran autocorrelación. En cuanto al desempleo de EEUU, por ejemplo, la autocorrelación en el lag 2 podría resultar completamente de información “descompuesta” del lag 1, solo correlación que se transfiere del paso anterior. Si el lag 2 no contiene nada nuevo, no habría razón para incluirlo si ya disponemos del lag 1.

La autocorrelación parcial nos dice la correlación de un lag teniendo en cuenta todos los lag’s previos, por así decirlo la cantidad de correlación “nueva” que aporta el lag. Dibujar la autocorrelación parcial puede ayudar a elegir qué feature de lag usar. En la siguiente figura, el lag 1 hasta el lag 6 caen fuera del intervalo de “no correlación” (en azul), por lo que podríamos esos 6 lag’s como features para el dataset de desempleo de EEUU. (El lag 11 es probablemente un falso positivo).

partial_correlation

La figura muestra las autocorrelaciones parciales de desempleo EEUU hasta el lag 12 con intervarlos de confianza del 95% de no correlación.

Un gráfico como el anterior se denomina correlograma. El correlograma es a las features lag lo que el periodograma es a las features Fourier.

Finalmente, debemos tener en mente que la autocorrelación y la autocorrelación parcial son medidas de la dependencia lineal. Debido a que las series temporales del mundo real suelen tener dependencias no lineales sustancialmente, es mejor mirar un gráfico de lag (o usar alguna medida general de dependiencia, como información mutua) al elegir features lag. La serie Sunspots tiene lag’s con dependencia no lineal que podríamos ignorar con la autocorrelación.

lag_plot_sunspot

Las relaciones no lineales como esta pueden ser transformadas para ser lineales o aprenderse con un algoritmo apropiado.

Ejemplo - Tendencias gripe

El dataset de Tendencias gripe contiene registros de consultas médicas por la gripe durante semanas de 2009 a 2016. Nuestro objetivo es predecir el número de casos de gripe para las próximas semanas.

Tomaremos dos enfoques. En el primero predeciremos las consultas médicas usando features lag. El segundo enfoque será predecir las consultas médicas usando lag’s de otro conjunto de series temporales: términos de búsqueda relacionados con la gripe capturados por Google Trends.

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from scipy.signal import periodogram
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from statsmodels.graphics.tsaplots import plot_pacf
# Set Matplotlib defaults
plt.style.use("seaborn-whitegrid")
plt.rc("figure", autolayout=True, figsize=(11, 4))
plt.rc(
    "axes",
    labelweight="bold",
    labelsize="large",
    titleweight="bold",
    titlesize=16,
    titlepad=10,
)
plot_params = dict(
    color="0.75",
    style=".-",
    markeredgecolor="0.25",
    markerfacecolor="0.25",
)
%config InlineBackend.figure_format = 'retina'
def lagplot(x, y=None, lag=1, standardize=False, ax=None, **kwargs):
    from matplotlib.offsetbox import AnchoredText
    x_ = x.shift(lag)
    if standardize:
        x_ = (x_ - x_.mean()) / x_.std()
    if y is not None:
        y_ = (y - y.mean()) / y.std() if standardize else y
    else:
        y_ = x
    corr = y_.corr(x_)
    if ax is None:
        fig, ax = plt.subplots()
    scatter_kws = dict(
        alpha=0.75,
        s=3,
    )
    line_kws = dict(color='C3', )
    ax = sns.regplot(x=x_,
                     y=y_,
                     scatter_kws=scatter_kws,
                     line_kws=line_kws,
                     lowess=True,
                     ax=ax,
                     **kwargs)
    at = AnchoredText(
        f"{corr:.2f}",
        prop=dict(size="large"),
        frameon=True,
        loc="upper left",
    )
    at.patch.set_boxstyle("square, pad=0.0")
    ax.add_artist(at)
    ax.set(title=f"Lag {lag}", xlabel=x_.name, ylabel=y_.name)
    return ax
def plot_lags(x, y=None, lags=6, nrows=1, lagplot_kwargs={}, **kwargs):
    import math
    kwargs.setdefault('nrows', nrows)
    kwargs.setdefault('ncols', math.ceil(lags / nrows))
    kwargs.setdefault('figsize', (kwargs['ncols'] * 2, nrows * 2 + 0.5))
    fig, axs = plt.subplots(sharex=True, sharey=True, squeeze=False, **kwargs)
    for ax, k in zip(fig.get_axes(), range(kwargs['nrows'] * kwargs['ncols'])):
        if k + 1 <= lags:
            ax = lagplot(x, y, lag=k + 1, ax=ax, **lagplot_kwargs)
            ax.set_title(f"Lag {k + 1}", fontdict=dict(fontsize=14))
            ax.set(xlabel="", ylabel="")
        else:
            ax.axis('off')
    plt.setp(axs[-1, :], xlabel=x.name)
    plt.setp(axs[:, 0], ylabel=y.name if y is not None else x.name)
    fig.tight_layout(w_pad=0.1, h_pad=0.1)
    return fig
flu_trends = pd.read_csv("../data/flu-trends.csv")
flu_trends.set_index(
    pd.PeriodIndex(flu_trends.Week, freq="W"),
    inplace=True,
)
flu_trends.drop("Week", axis=1, inplace=True)

flu_trends.head()

AInfluenza AcuteBronchitis BodyTemperature BraunThermoscan BreakAFever Bronchitis ChestCold ColdAndFlu ColdOrFlu ColdVersusFlu ... TreatingTheFlu TreatmentForFlu TreatmentForTheFlu Tussin Tussionex TypeAInfluenza UpperRespiratory WalkingPneumonia WhatToDoIfYouHaveTheFlu FluVisits
Week
2009-06-29/2009-07-05 36 20 43 27 11 22 16 7 3 8 ... 16 6 13 25 34 30 25 24 15 180
2009-07-06/2009-07-12 25 19 40 31 10 21 11 6 8 8 ... 16 7 8 27 32 27 28 29 9 115
2009-07-13/2009-07-19 24 30 45 20 12 20 20 6 6 8 ... 16 6 9 24 28 25 25 25 9 132
2009-07-20/2009-07-26 23 19 40 15 10 19 12 7 10 15 ... 8 5 12 21 26 26 29 24 13 109
2009-07-27/2009-08-02 27 21 44 20 11 19 17 8 10 15 ... 8 8 12 33 29 21 27 30 9 120

5 rows × 130 columns

ax = flu_trends.FluVisits.plot(title='Tendencia Gripe', **plot_params)
_ = ax.set(ylabel="Visitas médicas")

png

Nuestros datos de Tendencias Gripe muestran ciclos irregulares en lugar de una estacionalidad regular: el pico tiende a ocurrir alrededor del año nuevo, pero algunas veces antes o después, a veces más grande o más pequeño. Modelar estos ciclos con features lag permitirá a nuestras predicciones reaccionar dinámicamente a condiciones cambiantes en lugar de estar restringidas a fechas y horas exactas como las features estacionales.

En primer lugar, echemos un vistazo a los gráficos de lag y autocorrelación:

_ = plot_lags(flu_trends.FluVisits, lags=12, nrows=2)
_ = plot_pacf(flu_trends.FluVisits, lags=12)
C:\Program Files\Python310\lib\site-packages\statsmodels\graphics\tsaplots.py:348: FutureWarning: The default method 'yw' can produce PACF values outside of the [-1,1] interval. After 0.13, the default will change tounadjusted Yule-Walker ('ywm'). You can use this method now by setting method='ywm'.
  warnings.warn(

png

png

El gráfico de lag indica que la relación de FluVisits con sus lag’s es mayormente lineal, mientras que el gráfico de autocorrelación sugiere que la dependencia puede ser capturada usando lag 1, 2, 3 y 4. En Pandas podemos crear features lag con el método shift. Para este problema, rellenaremos los datos faltantes que genera el lagging con 0.0.

def make_lags(ts, lags):
    return pd.concat(
        {
            f'y_lag_{i}': ts.shift(i)
            for i in range(1, lags + 1)
        },
        axis=1)
X = make_lags(flu_trends.FluVisits, lags=4)
X = X.fillna(0.0)

X.head()

y_lag_1 y_lag_2 y_lag_3 y_lag_4
Week
2009-06-29/2009-07-05 0.0 0.0 0.0 0.0
2009-07-06/2009-07-12 180.0 0.0 0.0 0.0
2009-07-13/2009-07-19 115.0 180.0 0.0 0.0
2009-07-20/2009-07-26 132.0 115.0 180.0 0.0
2009-07-27/2009-08-02 109.0 132.0 115.0 180.0

Cuando usamos features lag estamos limitados a pronosticar pasos de tiempo cuyos valores lagged estén disponibles. Usando una feature lag 1 para lunes, no podemos predecir el miércoles porque el valor necesario de lag 1 es el martes, que aún no ha sucedido. Ya veremos posteriormente estrategias para manejar este problema. Ahora solo usaremos valroes de un conjunto de prueba.

# Crea la serie objetivo y divide los datos
y = flu_trends.FluVisits.copy()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=60, shuffle=False)

# Entrena y predice
model = LinearRegression()  # `fit_intercept=True` dado que no usamos DeterministicProcess
model.fit(X_train, y_train)
y_pred = pd.Series(model.predict(X_train), index=y_train.index)
y_fore = pd.Series(model.predict(X_test), index=y_test.index)
ax = y_train.plot(**plot_params)
ax = y_test.plot(**plot_params)
ax = y_pred.plot(ax=ax)
_ = y_fore.plot(ax=ax, color='C3')

png

Mirando solo los valores de predicción, podemos ver cómo nuestro modelo necesita un paso de tiempo para reaccionar a cambios repentinos en la serie objetivo. Esta es una limitación común de los modelos que usan solo lag’s de la serie objetivo como features.

ax = y_test.plot(**plot_params)
_ = y_fore.plot(ax=ax, color='C3')

png

Para mejorar la predicción, podríamos intentar encontrar indicadores adelantados (leading indicators), series temporales que pudieran proporcionar una “alerta temprana” para cambios en los casos de gripe. Para nuestro segundo enfoque añadiremos a nuestros datos de entrenamiento datos de la popularidad de algunos términos de búsqueda relacionados con la gripe según lo medido por Google Trends.

Dibujar la frase de búsqueda "FluCough" contra el objetivo "FluVisits" sugiere que tales términos podrían ser útiles como indicadores adelantados: las búsquedas relacionadas con la gripe tienden a ser más populares en las semanas anteriores a las visitas médicas.

ax = flu_trends.plot(
    y=["FluCough", "FluVisits"],
    secondary_y="FluCough",
)

png

El dataset contiene 219 de tales términos, pero solo usaremos unos pocos.

search_terms = ["FluContagious", "FluCough", "FluFever", "InfluenzaA", "TreatFlu", "IHaveTheFlu", "OverTheCounterFlu", "HowLongFlu"]

# Crea tres lag's para cada término de búsqueda
X0 = make_lags(flu_trends[search_terms], lags=3)

# Crea cuatro lag's para el objetivo, como antes
X1 = make_lags(flu_trends['FluVisits'], lags=4)

# Combina para crar los datos de entrenamiento
X = pd.concat([X0, X1], axis=1).fillna(0.0)

X.head()

(y_lag_1, FluContagious) (y_lag_1, FluCough) (y_lag_1, FluFever) (y_lag_1, InfluenzaA) (y_lag_1, TreatFlu) (y_lag_1, IHaveTheFlu) (y_lag_1, OverTheCounterFlu) (y_lag_1, HowLongFlu) (y_lag_2, FluContagious) (y_lag_2, FluCough) ... (y_lag_3, FluFever) (y_lag_3, InfluenzaA) (y_lag_3, TreatFlu) (y_lag_3, IHaveTheFlu) (y_lag_3, OverTheCounterFlu) (y_lag_3, HowLongFlu) y_lag_1 y_lag_2 y_lag_3 y_lag_4
Week
2009-06-29/2009-07-05 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2009-07-06/2009-07-12 5.0 16.0 15.0 36.0 6.0 5.0 11.0 6.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 180.0 0.0 0.0 0.0
2009-07-13/2009-07-19 6.0 14.0 15.0 25.0 12.0 7.0 7.0 7.0 5.0 16.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 115.0 180.0 0.0 0.0
2009-07-20/2009-07-26 6.0 13.0 13.0 24.0 8.0 8.0 7.0 7.0 6.0 14.0 ... 15.0 36.0 6.0 5.0 11.0 6.0 132.0 115.0 180.0 0.0
2009-07-27/2009-08-02 5.0 13.0 12.0 23.0 5.0 4.0 6.0 6.0 6.0 13.0 ... 15.0 25.0 12.0 7.0 7.0 7.0 109.0 132.0 115.0 180.0

5 rows × 28 columns

Nuestras predicciones son un poco más aproximadas, pero nuestro modelo parece ser más capaz de anticipar incrementos repentinos en las consultas médicas, lo que sugiere que las diversas series temporales de búsqueda de popularidad han sido, de hecho, efectivas como indicadores adelantados.

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=60, shuffle=False)

model = LinearRegression()
model.fit(X_train, y_train)
y_pred = pd.Series(model.predict(X_train), index=y_train.index)
y_fore = pd.Series(model.predict(X_test), index=y_test.index)

ax = y_test.plot(**plot_params)
_ = y_fore.plot(ax=ax, color='C3')
C:\Program Files\Python310\lib\site-packages\sklearn\utils\validation.py:1688: FutureWarning: Feature names only support names that are all strings. Got feature names with dtypes: ['str', 'tuple']. An error will be raised in 1.2.
  warnings.warn(
C:\Program Files\Python310\lib\site-packages\sklearn\utils\validation.py:1688: FutureWarning: Feature names only support names that are all strings. Got feature names with dtypes: ['str', 'tuple']. An error will be raised in 1.2.
  warnings.warn(
C:\Program Files\Python310\lib\site-packages\sklearn\utils\validation.py:1688: FutureWarning: Feature names only support names that are all strings. Got feature names with dtypes: ['str', 'tuple']. An error will be raised in 1.2.
  warnings.warn(

png

Las series temporales que hemos visto aquí son lo que se podrían llamar “puramente cíclicas”: no tienen una tendencia o estacionalidad obvias. Sin embargo, no es raro que las series temporales posean tendencias, estacionalidad y ciclos, los tres componentes a la vez. Podríamos modelar tales series con regresión lineal simplemente añadiendo las features apropiadas para cada componente. Incluso podemos combinar modelos entrenados para aprender los componentes por separado, que ya veremos con las predicciones híbridas.

Ejercicio

Vamos a realizar un ejercicio para ampliar lo que acabamos de ver.

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_log_error
from statsmodels.graphics.tsaplots import plot_pacf
from statsmodels.tsa.deterministic import CalendarFourier, DeterministicProcess
from helpers.style import * # importa estilos de gráficos
from helpers.utils import plot_lags, make_lags, make_leads
store_sales = pd.read_csv(
    "../data/store_sales/train.csv",
    usecols=['store_nbr', 'family', 'date', 'sales', 'onpromotion'],
    dtype={
        'store_nbr': 'category',
        'family': 'category',
        'sales': 'float32',
        'onpromotion': 'uint32',
    },
    parse_dates=['date'],
    infer_datetime_format=True,
)
store_sales['date'] = store_sales.date.dt.to_period('D')
store_sales = store_sales.set_index(['store_nbr', 'family', 'date']).sort_index()

store_sales.head()

sales onpromotion
store_nbr family date
1 AUTOMOTIVE 2013-01-01 0.0 0
2013-01-02 2.0 0
2013-01-03 3.0 0
2013-01-04 3.0 0
2013-01-05 5.0 0
family_sales = (
    store_sales
    .groupby(['family', 'date'])
    .mean()
    .unstack('family')
    .loc['2017', ['sales', 'onpromotion']]
)

family_sales.head()

sales ... onpromotion
family AUTOMOTIVE BABY CARE BEAUTY BEVERAGES BOOKS BREAD/BAKERY CELEBRATION CLEANING DAIRY DELI ... MAGAZINES MEATS PERSONAL CARE PET SUPPLIES PLAYERS AND ELECTRONICS POULTRY PREPARED FOODS PRODUCE SCHOOL AND OFFICE SUPPLIES SEAFOOD
date
2017-01-01 0.092593 0.037037 0.055556 74.222221 0.000000 9.084685 0.129630 7.500000 11.518518 3.629167 ... 0.0 0.018519 0.111111 0.018519 0.0 0.000000 0.037037 0.129630 0.0 0.000000
2017-01-02 11.481482 0.259259 11.648149 6208.055664 0.481481 844.836304 14.203704 2233.648193 1545.000000 539.114807 ... 0.0 0.462963 10.592593 0.537037 0.0 0.259259 1.166667 5.629630 0.0 0.407407
2017-01-03 8.296296 0.296296 7.185185 4507.814941 0.814815 665.124084 10.629630 1711.907349 1204.203735 404.300079 ... 0.0 0.481481 9.722222 0.444444 0.0 0.388889 1.351852 56.296296 0.0 0.407407
2017-01-04 6.833333 0.333333 6.888889 3911.833252 0.759259 594.160583 11.185185 1508.036987 1107.796265 309.397675 ... 0.0 0.370370 12.037037 0.444444 0.0 0.296296 5.444444 101.277778 0.0 0.333333
2017-01-05 6.333333 0.351852 5.925926 3258.796387 0.407407 495.511597 12.444445 1241.833374 829.277771 260.776489 ... 0.0 8.981481 5.666667 0.000000 0.0 0.296296 0.907407 5.018519 0.0 0.444444

5 rows × 66 columns

No todas las familias de productos tiene ventas que muestren comportamientos cíclicos y tampoco la serie de ventas promedio. Sin embargo, las ventas de material escolar y de oficina muestran patrones de crecimiento y decrecimiento que no están bien caracterizados por tendencias o estaciones. Vamos a modelar ciclos en las ventas de este tipo de material usando features lag.

Tanto la tendencia como la estacionalidad crearán una dependencia serial que se mostrará en los gráficos de correlograma y de lag. Para aislar cualquier comportamiento puramente cíclico, empezaremos desestacionalizando la serie.

supply_sales = family_sales.loc(axis=1)[:, 'SCHOOL AND OFFICE SUPPLIES']
y = supply_sales.loc[:, 'sales'].squeeze()

fourier = CalendarFourier(freq='M', order=4)
dp = DeterministicProcess(
    constant=True,
    index=y.index,
    order=1,
    seasonal=True,
    drop=True,
    additional_terms=[fourier],
)
X_time = dp.in_sample()
X_time['NewYearsDay'] = (X_time.index.dayofyear == 1)

model = LinearRegression(fit_intercept=False)
model.fit(X_time, y)
y_deseason = y - model.predict(X_time)
y_deseason.name = 'sales_deseasoned'

ax = y_deseason.plot()
ax.set_title("Ventas de material escolar y de oficina (desestacionalizadas)");

png

¿Esta serie desestacionalizada muestra patrones cíclicos? Para confirmar nuestra intuición, podemos intentar aislar el comportamiento cíclico usando un gráfico de media móvil, como se hace con la tendencia. La idea es elegir una ventana lo suficientemente grande para suavizar la estacionalidad a corto plazo, pero lo suficientemente pequeña para preservar los ciclos.

Dibujando ciclos

Vamos a crear una media móvil de siete días de y, la serie de venta de materiales. Usaremos una ventana centrada, pero no estableceremos el argumento min_periods.

y_ma = y.rolling(
    window=7,
    center=True,
).mean()


# Plot
ax = y_ma.plot()
ax.set_title("Media móvil de siete días");

png

¿Vemos cómo la traza de la media móvil se parece al gráfico de la serie desestacionalizada? En ambos casos se puede observar el comportamiento cíclico indicado.

Examinando la dependencia serial

Vamos a examinar nuestra serie desestacionalizada para determinar la dependencia serial. Echaremos un vistazo al correlograma de autocorrelación parcial y al gráfico de lag.

plot_pacf(y_deseason, lags=8)
plot_lags(y_deseason, lags=8, nrows=2);
C:\Program Files\Python310\lib\site-packages\statsmodels\graphics\tsaplots.py:348: FutureWarning: The default method 'yw' can produce PACF values outside of the [-1,1] interval. After 0.13, the default will change tounadjusted Yule-Walker ('ywm'). You can use this method now by setting method='ywm'.
  warnings.warn(

png

png

Según el correlograma, ¿hay algún lag significativo? ¿El gráfico de lags sugiere alguna relación que no fuera evidente en el correlograma?

El correlograma indica que el primer lag sea probablemente significativo, así como posiblemente el octavo lag. El gráfico de lag sugiere que el efecto es principalmente lineal.

Examinando features de series temporales

Ya vimos que un indicador adelantado es una serie cuyos valores en un momento dado pueden usarse para predecir el objetivo en un momento futuro. Un indicador adelantado proporciona un aviso anticipado de los cambios en el objetivo.

Nuestro dataset incluye una serie temporal que podría usarse potencialmente como un indicador adelantado, la serie onpromotion, que contiene el número de artículos en promoción especial ese día. Dado que la compañía es quien decice cuándo hacer una promoción, no hay que preocuparse por “fuga adelantada” o “fuga de datos”. Podríamos usar el valor onpromotion del martes para pronosticar las ventas del lunes, por ejemplo.

Vamos a examinar los valores adelantados y lagging de ompromotion dibujados contra las ventas de material escolar y de oficina.

onpromotion = supply_sales.loc[:, 'onpromotion'].squeeze().rename('onpromotion')
# Elimina los días sin promociones
plot_lags(x=onpromotion.loc[onpromotion > 1], y=y_deseason.loc[onpromotion > 1], lags=3, leads=3, nrows=1);

png

A tenor del resultado de la gráfica, ¿Podrían ser útiles como features los valores adelantados y lag de onpromotion?

La gráfica indica que tanto los valores adelantados como los lag de onpromotion se correlacionan con las ventas de materiales. Esto sugiere que ambos tipo de valores podrían ser útiles como features. También puede haber algunos efecto no lineales.

Creación de features de series temporales

Vamos a crear las features indicadas en el apartado anterior.

# genera features a partir de `y_deseason`
X_lags = make_lags(y_deseason, lags=1)

# genera features a partir de `onpromotion`
X_promo = pd.concat([
    make_lags(onpromotion, lags=1),
    onpromotion,
    make_leads(onpromotion, leads=1),
], axis=1)

X = pd.concat([X_time, X_lags, X_promo], axis=1).dropna()
y, X = y.align(X, join='inner')

Vamos a ver las predicciones del modelo resultante.

from sklearn.model_selection import train_test_split

X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=30, shuffle=False)

model = LinearRegression(fit_intercept=False).fit(X_train, y_train)
y_fit = pd.Series(model.predict(X_train), index=X_train.index).clip(0.0)
y_pred = pd.Series(model.predict(X_valid), index=X_valid.index).clip(0.0)

rmsle_train = mean_squared_log_error(y_train, y_fit) ** 0.5
rmsle_valid = mean_squared_log_error(y_valid, y_pred) ** 0.5
print(f'Entrenamiento RMSLE: {rmsle_train:.5f}')
print(f'Validación RMSLE: {rmsle_valid:.5f}')

ax = y.plot(**plot_params, alpha=0.5, title="Promedio ventas", ylabel="artículos vendidos")
ax = y_fit.plot(ax=ax, label="Entrenado", color='C0')
ax = y_pred.plot(ax=ax, label="Predicción", color='C3')
ax.legend();
Entrenamiento RMSLE: 0.23893
Validación RMSLE: 0.34245

png

En los mejores concursos de predicción (Kaggle, etc.) se suelen incluir promedios móviles y otras estadísticas móviles en sus conjuntos de features. Tales variables parecen ser especialmente útiles cuando se usan algoritmos GBDT, como XGBoost.

En otros posts vimos cómo calcular medias móviles para estimar tendencias. Calcular estadísticas móviles para usarse como features es similar, excepto que necesitamos tener cuidado para evitar la fuga de datos. En primer lugar, el resultado debe establecerse en el extremo derecho de la ventana, en lugar de en el centro. Es decir, usaremos center=False (valor por defecto) en el método rolling. En segundo lugar, el objetivo debe estar lagged a un paso de tiempo.

Crear features estadísticas

Ahora vamos a crear las siguientes features:

  • mediana (median) móvil de 14 días de lag del objetivo
  • desviación típica (std) móvil de 7 días de lag del objetivo
  • suma (sum) de 7 días de artículos “en promoción”, con ventana centrada
y_lag = supply_sales.loc[:, 'sales'].shift(1)
onpromo = supply_sales.loc[:, 'onpromotion']

# media de 7 días de lagged del objetivo
mean_7 = y_lag.rolling(7).mean()
# mediana de 14 días de lagged del objetivo
median_14 = y_lag.rolling(14).median()
# desviación típica de 7 días de lagged del objetivo
std_7 = y_lag.rolling(7).std()
# suma de 7 días de promociones con ventana centrada
promo_7 = onpromo.rolling(
    window=7,
    center=True
).sum()

Para calcular más estadísticos podemos acudir a la documentación de Window de Pandas. También podemos probar las ventanas “ponderadas exponencialmente” usando ewm en lugar de rolling; el decaimiento exponencial suele ser una representación más realista de cómo se propagan los efectos en el tiempo.

Fuente: Kaggle

updatedupdated2022-05-242022-05-24