Introduction and Preprocessing¶

To run these notebooks you will need the sktime library. You can install it with pip or conda

pip install sktime

or

conda install conda-forge::sktime
In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sktime.datasets import load_airline, load_uschange, load_basic_motions
from sktime.utils.plotting import plot_series

Intro¶

Time Series Data¶

In [2]:
univariate_ts = pd.read_csv("data/airline.csv", index_col=0)
univariate_ts.plot()
Out[2]:
<Axes: xlabel='Period'>
No description has been provided for this image
In [3]:
_, multivariate_ts = load_uschange()
sns.lineplot(data=multivariate_ts.iloc[:, :2], color="C0")
Out[3]:
<Axes: >
No description has been provided for this image
In [4]:
multivariate_ts_panel, labels = load_basic_motions()
colors = ["C0", "C1"]
linestyles = ["-", "--", ":", "-."]
for i in range(2):
    for j in range(2):
        multivariate_ts_panel.iloc[j, i].plot(color=colors[i], linestyle=linestyles[i + j], label=f"channel {j+1} of ts {i+1}")
plt.legend()
plt.show()
No description has been provided for this image

Load the data¶

In [5]:
# from csv
ts = pd.read_csv("data/airline.csv", index_col=0)
ts.head()
Out[5]:
Number of airline passengers
Period
1949-01 112.0
1949-02 118.0
1949-03 132.0
1949-04 129.0
1949-05 121.0
In [6]:
# directly from sktime
ts = load_airline()
ts.head()
Out[6]:
Period
1949-01    112.0
1949-02    118.0
1949-03    132.0
1949-04    129.0
1949-05    121.0
Freq: M, Name: Number of airline passengers, dtype: float64

Some basic statistics¶

In [7]:
ts.shape
Out[7]:
(144,)
In [8]:
len(ts)
Out[8]:
144
In [9]:
ts.index
Out[9]:
PeriodIndex(['1949-01', '1949-02', '1949-03', '1949-04', '1949-05', '1949-06',
             '1949-07', '1949-08', '1949-09', '1949-10',
             ...
             '1960-03', '1960-04', '1960-05', '1960-06', '1960-07', '1960-08',
             '1960-09', '1960-10', '1960-11', '1960-12'],
            dtype='period[M]', name='Period', length=144)
In [10]:
# what is the frequency of the time series?
ts.index.freqstr
Out[10]:
'M'
In [11]:
ts.index.year
Out[11]:
Index([1949, 1949, 1949, 1949, 1949, 1949, 1949, 1949, 1949, 1949,
       ...
       1960, 1960, 1960, 1960, 1960, 1960, 1960, 1960, 1960, 1960],
      dtype='int64', name='Period', length=144)
In [12]:
# how many values from each year?
ts.index.year.value_counts()
Out[12]:
Period
1949    12
1950    12
1951    12
1952    12
1953    12
1954    12
1955    12
1956    12
1957    12
1958    12
1959    12
1960    12
Name: count, dtype: int64
In [13]:
ts.array
Out[13]:
<NumpyExtensionArray>
[112.0, 118.0, 132.0, 129.0, 121.0, 135.0, 148.0, 148.0, 136.0, 119.0,
 ...
 419.0, 461.0, 472.0, 535.0, 622.0, 606.0, 508.0, 461.0, 390.0, 432.0]
Length: 144, dtype: float64
In [14]:
ts.to_numpy()
Out[14]:
array([112., 118., 132., 129., 121., 135., 148., 148., 136., 119., 104.,
       118., 115., 126., 141., 135., 125., 149., 170., 170., 158., 133.,
       114., 140., 145., 150., 178., 163., 172., 178., 199., 199., 184.,
       162., 146., 166., 171., 180., 193., 181., 183., 218., 230., 242.,
       209., 191., 172., 194., 196., 196., 236., 235., 229., 243., 264.,
       272., 237., 211., 180., 201., 204., 188., 235., 227., 234., 264.,
       302., 293., 259., 229., 203., 229., 242., 233., 267., 269., 270.,
       315., 364., 347., 312., 274., 237., 278., 284., 277., 317., 313.,
       318., 374., 413., 405., 355., 306., 271., 306., 315., 301., 356.,
       348., 355., 422., 465., 467., 404., 347., 305., 336., 340., 318.,
       362., 348., 363., 435., 491., 505., 404., 359., 310., 337., 360.,
       342., 406., 396., 420., 472., 548., 559., 463., 407., 362., 405.,
       417., 391., 419., 461., 472., 535., 622., 606., 508., 461., 390.,
       432.])
In [15]:
ts.describe()
Out[15]:
count    144.000000
mean     280.298611
std      119.966317
min      104.000000
25%      180.000000
50%      265.500000
75%      360.500000
max      622.000000
Name: Number of airline passengers, dtype: float64
In [16]:
ts.info()
<class 'pandas.core.series.Series'>
PeriodIndex: 144 entries, 1949-01 to 1960-12
Freq: M
Series name: Number of airline passengers
Non-Null Count  Dtype  
--------------  -----  
144 non-null    float64
dtypes: float64(1)
memory usage: 2.2 KB

Plotting¶

In [17]:
# pandas
ts.plot()
plt.show()
No description has been provided for this image
In [18]:
# matplotlib
plt.plot(ts.index.astype("str"), ts.to_numpy())
plt.xticks(ts.index.astype("str")[::20], rotation=45)
plt.show()
No description has been provided for this image
In [19]:
# seaborn
sns.lineplot(x=ts.index.astype("str"), y=ts)
plt.xticks(ts.index.astype("str")[::20], rotation=45)
plt.show()
No description has been provided for this image
In [20]:
# sktime
plot_series(ts)
plt.show()
/Users/francesco/miniforge3/envs/timeseries_dl/lib/python3.12/site-packages/gluonts/json.py:102: UserWarning: Using `json`-module for json-handling. Consider installing one of `orjson`, `ujson` to speed up serialization and deserialization.
  warnings.warn(
No description has been provided for this image
In [21]:
ts.plot(kind="box")
Out[21]:
<Axes: >
No description has been provided for this image
In [22]:
ts.plot(kind="hist")
Out[22]:
<Axes: ylabel='Frequency'>
No description has been provided for this image
In [23]:
# how many values from each year?
ts.index.strftime("%Y").value_counts().sort_index().plot(kind="bar")
Out[23]:
<Axes: >
No description has been provided for this image

Missing Values¶

In [24]:
ts_missing = ts.copy().to_numpy()

# add missing values for demonstration
ts_missing[::10] = np.nan
ts_missing[::11] = np.nan
In [25]:
plt.plot(ts_missing)
plt.show()
No description has been provided for this image

Removing missing values¶

In [26]:
ts_cleaned = pd.Series(ts_missing).dropna()
In [27]:
ts_cleaned.plot()
Out[27]:
<Axes: >
No description has been provided for this image

Imputing Missing Values¶

In [28]:
from sktime.transformations.series.impute import Imputer
In [29]:
imputer = Imputer(method="mean")
In [30]:
ts_imputed = imputer.fit_transform(ts_missing)
In [31]:
plt.plot(ts_imputed, color="C1", label="Imputed", linestyle="--")
plt.plot(ts_missing, color="C0", label="Missing")
plt.legend()
plt.show()
No description has been provided for this image
In [32]:
imputer = Imputer(method="ffill")
ts_imputed = imputer.fit_transform(ts_missing)
plt.plot(ts_imputed, color="C1", label="Imputed", linestyle="--")
plt.plot(ts_missing, color="C0", label="Missing")
plt.legend()
plt.show()
No description has been provided for this image
In [129]:
imputer = Imputer(method="bfill")
ts_imputed = imputer.fit_transform(ts_missing)
plt.plot(ts_imputed, color="C1", label="Imputed", linestyle="--")
plt.plot(ts_missing, color="C0", label="Missing")
plt.legend()
plt.show()
No description has been provided for this image
In [34]:
imputer = Imputer(method="linear")
ts_imputed = imputer.fit_transform(ts_missing)
plt.plot(ts_imputed, color="C1", label="Imputed", linestyle="--")
plt.plot(ts_missing, color="C0", label="Missing")
plt.legend()
plt.show()
No description has been provided for this image

Anomalies¶

In [35]:
from sktime.transformations.series.func_transform import FunctionTransformer

Hampel Filter¶

In [130]:
from sktime.transformations.series.outlier_detection import HampelFilter
In [135]:
# if we lower the threshold we would detect more outliers (default n_sigma is 3)
annotator = HampelFilter(window_length=12, n_sigma=3, return_bool=True)
In [136]:
labels = annotator.fit_transform(ts).ravel()
/var/folders/kj/v66zvn217x31k6lx63lt02q40000gn/T/ipykernel_42309/3923777818.py:1: FutureWarning: Series.ravel is deprecated. The underlying array is already 1D, so ravel is not necessary.  Use `to_numpy()` for conversion to a numpy array instead.
  labels = annotator.fit_transform(ts).ravel()
In [137]:
plt.plot(ts.ravel())
plt.scatter(np.arange(len(labels)), ts, alpha=labels, color="red", marker="x")
plt.show()
/var/folders/kj/v66zvn217x31k6lx63lt02q40000gn/T/ipykernel_42309/4027205443.py:1: FutureWarning: Series.ravel is deprecated. The underlying array is already 1D, so ravel is not necessary.  Use `to_numpy()` for conversion to a numpy array instead.
  plt.plot(ts.ravel())
No description has been provided for this image

Grubbs Test¶

In [138]:
from scipy import stats

Estimation¶

Each point is judged using statistics computed from the full series

In [139]:
def grubbs_test(ts, alpha=0.05):
    z_scores = np.abs(stats.zscore(ts))
    threshold = stats.t.ppf((1 + (1-alpha))/2, len(ts) - 2)
    return (z_scores > threshold).astype(int)
In [140]:
# you can use the function directly on the data
labels = grubbs_test(ts)
In [141]:
# or you can use the FunctionTransformer (the advantage is that this is compatible with all sktime features)
transformer = FunctionTransformer(func=grubbs_test, X_type="pd.Series")
labels = transformer.fit_transform(ts)
In [142]:
plt.plot(ts.to_numpy())
plt.scatter(np.arange(len(labels)), ts.to_numpy(), alpha=labels.to_numpy(), color="red", marker="x")
plt.show()
No description has been provided for this image

Prediction¶

Processes the time series causally, using only past data available up to each time point

In [151]:
def grubbs_test_prediction(ts, alpha=0.05, starting_win=12):
    initial_labels = list(grubbs_test(ts[:starting_win], alpha))
    for i in range(starting_win, len(ts)):
        initial_labels.append(grubbs_test(np.array(ts)[:i+1], alpha)[-1])  # -1 to only get the label of the last point
    return np.array(initial_labels)
In [152]:
# you can use the function directly on the data
labels = grubbs_test_prediction(ts)
In [153]:
# or you can use the FunctionTransformer (the advantage is that this is compatible with all sktime features)
transformer = FunctionTransformer(func=grubbs_test_prediction, X_type="pd.Series")
labels = transformer.fit_transform(ts)
In [150]:
plt.plot(ts.to_numpy())
plt.scatter(np.arange(len(labels)), ts.to_numpy(), alpha=labels.to_numpy(), color="red", marker="x")
plt.show()
No description has been provided for this image

IQR-based¶

Estimation¶

In [154]:
def iqr_outliers(x, k=1.5):
    q1 = np.quantile(x, 0.25)
    q3 = np.quantile(x, 0.75)
    iqr = q3 - q1
    outliers = list()
    for v in x:
        if v < q1 - k * iqr or v > q3 + k * iqr:
            outliers.append(True)
        else:
            outliers.append(False)
    return np.array(outliers).astype(int)
In [155]:
# again, we decrease the threshold to detect more outliers (default is 1.5)
transformer = FunctionTransformer(func=iqr_outliers, kw_args=dict(k=1), X_type="pd.Series")
In [156]:
labels = transformer.fit_transform(ts)
labels
Out[156]:
0      0
1      0
2      0
3      0
4      0
      ..
139    1
140    0
141    0
142    0
143    0
Length: 144, dtype: int64
In [157]:
plt.plot(ts.to_numpy())
plt.scatter(np.arange(len(labels)), ts.to_numpy(), alpha=labels, color="red", marker="x")
plt.show()
No description has been provided for this image

Prediction¶

In [158]:
def iqr_outliers_prediction(x, k=1.5, starting_win=12):
    initial_labels = list(iqr_outliers(x[:starting_win], k))
    for i in range(starting_win, len(x)):
        initial_labels.append(iqr_outliers(np.array(x)[:i+1], k)[-1])
    return np.array(initial_labels)
In [159]:
# you can use the function directly on the data
labels = iqr_outliers_prediction(ts)
In [160]:
# or you can use the FunctionTransformer (the advantage is that this is compatible with all sktime features)
transformer = FunctionTransformer(func=iqr_outliers_prediction, kw_args=dict(k=1.5), X_type="pd.Series")
labels = transformer.fit_transform(ts)
In [161]:
plt.plot(ts.to_numpy())
plt.scatter(np.arange(len(labels)), ts.to_numpy(), alpha=labels, color="red", marker="x")
plt.show()
No description has been provided for this image

Replacing Anomalies¶

In [162]:
transformer = FunctionTransformer(func=iqr_outliers, kw_args=dict(k=1), X_type="pd.Series")
labels = transformer.fit_transform(ts)
In [163]:
plt.plot(ts.to_numpy())
plt.scatter(np.arange(len(labels)), ts.to_numpy(), alpha=labels, color="red", marker="x")
plt.show()
No description has been provided for this image
In [164]:
ts_replaced = ts.copy().to_numpy()
ts_replaced[labels.astype(bool)] = np.nan
In [165]:
plt.plot(ts_replaced)
plt.show()
No description has been provided for this image
In [166]:
imputer = Imputer(method="nearest")
In [167]:
ts_imputed = imputer.fit_transform(ts_replaced)
In [168]:
plt.plot(ts.to_numpy(), label="Original")
plt.plot(ts_imputed, label="Imputed")
plt.legend()
plt.show()
No description has been provided for this image

Manipulating Time Series¶

In [64]:
# let's split the time series into two
ts1 = ts[0: len(ts)//2].to_numpy()
ts2 = ts[len(ts)//2:].to_numpy()
In [65]:
plt.plot(ts1)
plt.plot(ts2)
plt.show()
No description has been provided for this image
In [66]:
plt.plot(ts1)
plt.plot(ts2)
plt.yscale("log")
plt.show()
No description has been provided for this image

Normalizations¶

In [67]:
from sktime.transformations.series.adapt import TabularToSeriesAdaptor

Min-Max normalization¶

In [68]:
from sklearn.preprocessing import MinMaxScaler
In [69]:
scaler = TabularToSeriesAdaptor(MinMaxScaler(), fit_in_transform=True)
In [70]:
ts1_minmax = scaler.fit_transform(ts1.reshape(-1, 1))
ts2_minmax = scaler.fit_transform(ts2.reshape(-1, 1))
In [71]:
plt.plot(ts1_minmax)
plt.plot(ts2_minmax)
plt.axhline(0, color="black", linestyle="--")
plt.axhline(1, color="black", linestyle="--")
plt.show()
No description has been provided for this image

Offset Transformation¶

In [72]:
from sklearn.preprocessing import StandardScaler
In [73]:
scaler = TabularToSeriesAdaptor(StandardScaler(with_std=False), fit_in_transform=True)
In [74]:
ts1_offset = scaler.fit_transform(ts1.reshape(-1, 1))
ts2_offset = scaler.fit_transform(ts2.reshape(-1, 1))
In [75]:
ts1_offset.mean(), ts1_offset.std()
Out[75]:
(6.315935428978668e-15, 47.371803533977136)
In [76]:
ts2_offset.mean(), ts2_offset.std()
Out[76]:
(-1.2631870857957336e-14, 85.8368347002499)
In [77]:
plt.plot(ts1_offset)
plt.plot(ts2_offset)
plt.show()
No description has been provided for this image

Z-score normalization (Amplitude Scaling)¶

In [78]:
scaler = TabularToSeriesAdaptor(StandardScaler(), fit_in_transform=True)
In [79]:
ts1_zscored = scaler.fit_transform(ts1.reshape(-1, 1))
ts2_zscored = scaler.fit_transform(ts2.reshape(-1, 1))
In [80]:
ts1_zscored.mean(), ts1_zscored.std()
Out[80]:
(1.4494578377050655e-16, 0.9999999999999999)
In [81]:
ts2_zscored.mean(), ts2_zscored.std()
Out[81]:
(-1.603655480014115e-16, 1.0)
In [82]:
plt.plot(ts1_zscored)
plt.plot(ts2_zscored)
plt.show()
No description has been provided for this image

Smoothing¶

In [83]:
def moving_average(x, w):
    return x.rolling(window=w).mean()
In [84]:
transformer = FunctionTransformer(func=moving_average, kw_args=dict(w=4), X_type="pd.Series")
In [85]:
ts1_smoothed = transformer.fit_transform(pd.Series(ts1_zscored.ravel()))
ts2_smoothed = transformer.fit_transform(pd.Series(ts2_zscored.ravel()))
In [86]:
plt.plot(ts1_zscored.ravel())
plt.plot(ts1_smoothed)
Out[86]:
[<matplotlib.lines.Line2D at 0x179fdb560>]
No description has been provided for this image
In [87]:
plt.plot(ts2_zscored.ravel())
plt.plot(ts2_smoothed)
Out[87]:
[<matplotlib.lines.Line2D at 0x179ddce90>]
No description has been provided for this image
In [88]:
plt.plot(ts1_smoothed)
plt.plot(ts2_smoothed)
plt.show()
No description has been provided for this image

Log transformation¶

In [89]:
from sktime.transformations.series.boxcox import LogTransformer
In [90]:
log_transformer = LogTransformer()
In [91]:
ts1_log = log_transformer.fit_transform(ts1)
ts2_log = log_transformer.fit_transform(ts2)
In [92]:
plt.plot(ts1_log)
plt.plot(ts2_log)
Out[92]:
[<matplotlib.lines.Line2D at 0x17a023a40>]
No description has been provided for this image

Differencing¶

In [93]:
from sktime.transformations.series.difference import Differencer
In [94]:
differencer =  Differencer(1)
In [95]:
ts1_diff = differencer.fit_transform(ts1)
ts2_diff = differencer.fit_transform(ts2)
In [96]:
plt.plot(ts1_diff)
plt.plot(ts2_diff)
plt.show()
No description has been provided for this image

Components¶

In [97]:
from sktime.forecasting.trend import STLForecaster
In [98]:
forecaster = STLForecaster(sp=12)
In [99]:
forecaster.fit(ts1)
ts1_trend = forecaster.trend_
ts1_seasonal = forecaster.seasonal_
ts1_resid = forecaster.resid_

forecaster.fit(ts2)
ts2_trend = forecaster.trend_
ts2_seasonal = forecaster.seasonal_
ts2_resid = forecaster.resid_
In [100]:
fig, axs = plt.subplots(4, 1, sharex=True)
axs[0].plot(ts1, color="C0")
axs[0].set_ylabel("Original")
axs[1].plot(ts1_trend, color="C1")
axs[1].set_ylabel("Trend")
axs[2].plot(ts1_seasonal, color="C2")
axs[2].set_ylabel("Seasonal")
axs[3].plot(ts1_resid, color="C3")
axs[3].set_ylabel("Residual")
plt.show()
No description has been provided for this image
In [101]:
fig, axs = plt.subplots(4, 1,  sharex=True)
axs[0].plot(ts2, color="C0")
axs[0].set_ylabel("Original")
axs[1].plot(ts2_trend, color="C1")
axs[1].set_ylabel("Trend")
axs[2].plot(ts2_seasonal, color="C2")
axs[2].set_ylabel("Seasonal")
axs[3].plot(ts2_resid, color="C3")
axs[3].set_ylabel("Residual")
plt.show()
No description has been provided for this image

Stationarity¶

In [102]:
from sktime.param_est.stationarity import StationarityADF
from statsmodels.tsa.stattools import adfuller
In [103]:
adfuller(ts)
Out[103]:
(0.8153688792060371,
 0.9918802434376408,
 13,
 130,
 {'1%': -3.4816817173418295,
  '5%': -2.8840418343195267,
  '10%': -2.578770059171598},
 996.692930839019)
In [104]:
sty_est = StationarityADF()  
sty_est.fit(ts)  
sty_est.stationary_
Out[104]:
False

Detrend¶

In [105]:
from sktime.transformations.series.detrend import Detrender
In [106]:
# linear detrending
detrender = Detrender()
In [107]:
ts_detrended = detrender.fit_transform(ts)
In [108]:
plt.plot(ts_detrended.to_numpy())
plt.show()
No description has been provided for this image

Deseasonalize¶

In [109]:
from sktime.transformations.series.detrend import Deseasonalizer
from sktime.utils.plotting import plot_correlations
In [110]:
plot_correlations(ts)
plt.show()
No description has been provided for this image
In [111]:
deseasonalizer = Deseasonalizer(sp=12, model="multiplicative")  # yearly seasonality
In [112]:
ts_deseasonalized = deseasonalizer.fit_transform(ts)
In [113]:
plt.plot(ts_deseasonalized.to_numpy())
Out[113]:
[<matplotlib.lines.Line2D at 0x16c016c00>]
No description has been provided for this image

Detrend and Deseasonalize¶

In [114]:
detr_dese = Detrender(model="multiplicative") * Deseasonalizer(sp=12, model="multiplicative")  # this is a pipeline
detr_dese
Out[114]:
TransformerPipeline(steps=[Detrender(model='multiplicative'),
                           Deseasonalizer(model='multiplicative', sp=12)])
Please rerun this cell to show the HTML repr or trust the notebook.
TransformerPipeline(steps=[Detrender(model='multiplicative'),
                           Deseasonalizer(model='multiplicative', sp=12)])
In [115]:
ts_detr_dese = detr_dese.fit_transform(ts)
In [116]:
sty_est = StationarityADF()  
sty_est.fit(ts_detr_dese)  
sty_est.stationary_
Out[116]:
True
In [117]:
plt.plot(ts_detr_dese.to_numpy())
Out[117]:
[<matplotlib.lines.Line2D at 0x1797e5b80>]
No description has been provided for this image

Pipeline¶

In [118]:
# without pipeline
ts1_log = LogTransformer().fit_transform(ts1)
ts1_det = Detrender().fit_transform(ts1_log)
ts1_scaled = StandardScaler().fit_transform(ts1_det)
In [119]:
from sktime.pipeline import make_pipeline
In [120]:
pipe = make_pipeline(LogTransformer(), Detrender(), TabularToSeriesAdaptor(StandardScaler(), fit_in_transform=True))
In [121]:
ts1_pipe = pipe.fit_transform(ts1.reshape(-1, 1))
ts2_pipe = pipe.fit_transform(ts2.reshape(-1, 1))
In [122]:
plt.plot(ts1_pipe)
plt.plot(ts2_pipe)
plt.show()
No description has been provided for this image

Multivariate Time Series¶

In [123]:
from sktime.datasets import load_uschange
In [124]:
_, X = load_uschange()
In [125]:
sns.lineplot(X[["Savings", "Income"]])
plt.show()
No description has been provided for this image
In [126]:
pipe = make_pipeline(TabularToSeriesAdaptor(MinMaxScaler(), fit_in_transform=True), FunctionTransformer(func=np.log1p), Differencer(1))
# this is equivalent to the following
# pipe = TabularToSeriesAdaptor(MinMaxScaler(), fit_in_transform=True) * FunctionTransformer(func=np.log1p) * Differencer(1)
pipe
Out[126]:
TransformerPipeline(steps=[TabularToSeriesAdaptor(fit_in_transform=True,
                                                  transformer=MinMaxScaler()),
                           FunctionTransformer(func=<ufunc 'log1p'>),
                           Differencer()])
Please rerun this cell to show the HTML repr or trust the notebook.
TransformerPipeline(steps=[TabularToSeriesAdaptor(fit_in_transform=True,
                                                  transformer=MinMaxScaler()),
                           FunctionTransformer(func=<ufunc 'log1p'>),
                           Differencer()])
In [127]:
X_transformed = pipe.fit_transform(X)
In [128]:
sns.lineplot(X_transformed[["Savings", "Income"]])
Out[128]:
<Axes: >
No description has been provided for this image