Scripting Tutorial
==================

Start by importing the relevant objects:

>>> from orangecontrib.timeseries import *

Let's load new :class:`Timeseries`, for example:

>>> data = Timeseries('airpassengers')
>>> np.set_printoptions(precision=1)

:class:`Timeseries` object is just an :class:`Orange.data.Table` object with some
extensions.

Find more info and function docstrings in the :doc:`reference <reference>`.


Periodicity
-----------
You can compute periodogram values using :func:`periodogram` or
:func:`periodogram_nonequispaced` (Lomb-Scargle) for non-uniformly spaced time series.

With our air passengers example, calculate the periodogram on the only
data-bearing column, which also happens to be a class variable:

>>> periods, pgram_values = periodogram(data.Y, detrend='diff')
>>> periods
array([  2.4,   3. ,   4. ,   6. ,  11.9])
>>> pgram_values
array([0.1, 0.2, 0.2, 1. , 0.9])

Obviously, 6 and 12 are important periods for this data set.


Autocorrelation
---------------
Compute autocorrelation or partial autocorrelation coefficients using
:func:`autocorrelation` or :func:`partial_autocorrelation` functions.
For example:

>>> acf = autocorrelation(data.Y)
>>> acf[:4]
array([[12. ,  0.8],
       [24. ,  0.6],
       [36. ,  0.4],
       [48. ,  0.2]])
>>> pacf = partial_autocorrelation(data.Y)
>>> pacf[:4]
array([[ 9. ,  0.2],
       [13. , -0.5],
       [25. , -0.2],
       [40. , -0.1]])


Interpolation
-------------
Let's say your data is missing some values:

>>> data.Y[7:11]
array([148., 136., 119., 104.])
>>> data.Y[7:11] = np.nan

You can interpolate those values with one of supported interpolation methods
using :func:`interpolate_timeseries` function:

>>> interpolated = interpolate_timeseries(data, method='cubic')
>>> interpolated[7:11].Y
array([151.2, 146.8, 137.8, 127.2])
>>> data = interpolated


Seasonal decomposition
----------------------
To decompose the time series into trend, seasonal and residual components,
use :func:`seasonal_decompose` function:

>>> from Orange.data import Domain
>>> passengers = Timeseries(Domain(['Air passengers'], source=data.domain), data)
>>> decomposed = seasonal_decompose(passengers, model='multiplicative', period=12)
>>> decomposed.domain
[Air passengers (season. adj.), Air passengers (seasonal), Air passengers (trend), Air passengers (residual)]

To use this decomposed time series effectively, we just have to add back the
time variable that was stripped in the first step above:

>>> ts = Timeseries(Timeseries.concatenate((data, decomposed)))
>>> ts.time_variable = data.time_variable

Just kidding. Use :func:`statsmodels.seasonal.seasonal_decompose` instead.


Moving transform
----------------
It's easy enough to apply moving windows transforms over any raw data in Python.
In Orange3-Timeseries, you can use :func:`moving_transform` function. It accepts
a time series object and a transform specification (list of tuples
``(Variable, window length, aggregation function)``).
For example:

>>> spec = [(data.domain['Air passengers'], 10, np.nanmean), ]  # Just 10-year SMA
>>> transformed = moving_transform(data, spec)
>>> transformed.domain
[Month, Air passengers (10; nanmean) | Air passengers]
>>> transformed
[[1949-01-01, 112.000 | 112],
 [1949-02-01, 115.000 | 118],
 [1949-03-01, 120.667 | 132],
 [1949-04-01, 122.750 | 129],
 [1949-05-01, 122.400 | 121],
 ...
]

There are a couple of nan-safe aggregation functions available in
:mod:`orangecontrib.timeseries.agg_funcs` module.


Time series modelling and forecast
----------------------------------
There are, as of yet, two models available: ARIMA and VAR. Both models have a
common interface, so the usage of one is similar to the other. Let's look at an
example. The data we model must have defined a class variable:

>>> data = Timeseries('airpassengers')
>>> data.domain
[Month | Air passengers]
>>> data.domain.class_var
ContinuousVariable(name='Air passengers', number_of_decimals=0)

We define the model with its parameters (see the reference for what arguments
each model accepts):

>>> model = ARIMA((2, 1, 1))

Now we fit the data:

>>> model.fit(data)
<...ARIMA object at 0x...>

After fitting, we can get the forecast along with desired confidence intervals:

>>> forecast, ci95_low, ci95_high = model.predict(steps=10, alpha=.05)

We can also output the prediction as a :class:`Timeseries` object:

>>> forecast = model.predict(10, as_table=True)
>>> forecast.domain
[Air passengers (forecast), Air passengers (95%CI low), Air passengers (95%CI high)]
>>> forecast.X
array([[470.5, 417.8, 523.1],
       [492.6, 414.1, 571.1],
       [498.5, 411.5, 585.4],
       ...
       [492.7, 403. , 582.4],
       [497.1, 407.3, 586.8]])

We can examine model's fitted values and residuals with appropriately-named
methods:

>>> model.fittedvalues(as_table=False)
array([114.7, 121.7, ..., 440.4, 386.8])
>>> model.residuals(as_table=False)
array([ 3.3,  10.3, ..., -50.4,  45.2])

We can evaluate the model on in-sample, fitted values:

>>> for measure, error in sorted(model.errors().items()):
...     print('{:7s} {:>6.2f}'.format(measure.upper(), error))
MAE      19.66
MAPE      0.08
POCID    58.45
R2        0.95
RMSE     27.06

Finally, one should more robustly evaluate their models using cross validation.
An example, edited for some clarity:

>>> models = [ARIMA((1, 1, 0)), ARIMA((2, 1, 2)), VAR(1), VAR(3)]
>>> model_evaluation(data, models, n_folds=10, forecast_steps=3)  # doctest: +SKIP
[['Model',                    'RMSE', 'MAE', 'MAPE', 'POCID', 'R²', 'AIC', 'BIC'],
 ['ARIMA(1,1,0)',             47.318, 36.803, 0.093, 68.965, 0.625, 1059.3, 1067.4],
 ['ARIMA(1,1,0) (in-sample)', 32.040, 20.340, 0.089, 58.450, 0.927, 1403.4, 1412.3],
 ['ARIMA(2,1,2)',             44.659, 28.332, 0.075, 72.413, 0.666, 1032.8, 1049.2],
 ['ARIMA(2,1,2) (in-sample)', 25.057, 16.159, 0.070, 59.859, 0.955, 1344.0, 1361.8],
 ['VAR(1)',                   63.185, 45.553, 0.118, 68.965, 0.332, 28.704, 28.849],
 ['VAR(1) (in-sample)',       31.316, 19.001, 0.084, 54.929, 0.930, 29.131, 29.255],
 ['VAR(3)',                   46.210, 28.526, 0.085, 82.758, 0.643, 28.140, 28.482],
 ['VAR(3) (in-sample)',       25.642, 18.010, 0.072, 61.428, 0.953, 28.406, 28.698]]


Granger Causality
-----------------
Use :func:`granger_causality` to estimate causality between series. A synthetic
example:

>>> series = np.arange(100)
>>> X = np.column_stack((series, np.roll(series, 1), np.roll(series, 3)))
>>> threecol = Timeseries(Domain.from_numpy(X), X)
>>> for lag, ante, cons in granger_causality(threecol, 10):
...     if lag > 1:
...         print('Series {cons} lags by {ante} by {lag} lags.'.format(**locals()))
...
Series Feature 1 lags by Feature 2 by 3 lags.
Series Feature 2 lags by Feature 3 by 4 lags.

Use this knowledge wisely.
