Dynamic linear models — user manual

This package implements the Bayesian dynamic linear model (DLM, Harrison and West, 1999) for time series analysis. The DLM is built upon two layers. The first layer is the fitting algorithm. DLM adopts a modified Kalman filter with a unique discounting technique from Harrison and West (1999). Like the usual Kalman filter, it accepts a transition matrix, a measurement matrix, an observation, a latent state, an innovation and an error covariance matrix and return the updated state and error covariance. These quantities will all be supplied internally – users are free from any annoying calculations. Different from the usual Kalman filter, the modified Kalman filter does not require the tuning of the two parameters: the error covariance matrix and the observational variance, so the model fitting is extremely efficient (could be up to 1000 times faster than the EM algorithm), more details will be provided in the section of the discounting technique.

The second layer of DLM is itsmodeling feature. Nicely summarized in Harrison and West (1999), most common models can be expressed in one unified form – canonical form, which is closely related to the Jordan decomposition. Thanks to this keen observation, the DLM can easily incorporate most modeling components and turn them into the corresponding transition matrices and other quantities to be supplied to the Kalman filter. Examples are trend, seasonality, holidays, control variables and auto-regressive, which could appear simultaneously in one model. Due to this nice property, users of this package can construct models simply by “adding” some component into the model as:

myDLM = dlm(data) + trend(1)

The modeling process is simple.

The purpose of the modeling is to better understand the time series data and for to forecast into the future. So the key output from the model are the filtered time series, smoothed time series and one-step ahead prediction. We will cover this topic later in this section.

The advantage of pydlm:

  • flexibility in constructing complicated models
  • Extremely efficient model fitting with the discounting technique
  • user-specific adjustment on the adaptive property of the model

The disadvantage of pydlm:

  • only for Gaussian noise

Miscellaneous

  • PyDLM index starts at 0 instead of 1, i.e., for any prediction or modification that involves date argument, it corresponds to the actual index in the array. For instance, for a time series with length 10, the date of the last day is 9.

Modeling

As discussed in the beginning, the modeling process is very simple with pydlm, most modeling functions are integrated in the class dlm. Following is an example for constructing a dlm with linear trend, 7-day seasonality and control variables:

from pydlm import dlm, trend, seasonality, dynamic, autoReg, longSeason
data = [0] * 100 + [3] * 100
SP500Index = [[2000] for i in range(100)] + [[2010] for i in range(100)]
page = [[i, i + 1, i + 2, i + 3] for i in range(200)]
myDLM = dlm(data)
myDLM = myDLM + trend(degree=1, discount=0.95, name='trend1')
myDLM = myDLM + seasonality(period=7, discount=0.99, name='week')
myDLM = myDLM + dynamic(features=SP500Index, discount=1, name='SP500')
myDLM = myDLM + dynamic(features=page, discount=1, name='page')

User can also use dlm.add() method to add new component:

myDLM.add(trend(degree=0, discount=0.99, name='trend2'))

The imput data must be an 1d array or a list, since the current dlm only supports one dimensional time series. Supporting for multivariate time series will be built upon this one dimensional class and added in the future.

Now the variable myDLM contains the data and the modeling information. It will construct the corresponding transition, measurement, innovation, latent states and error covariance matrix once model fitting is called. Modify an existing model is also simple. User can brows the existing components of the model by:

myDLM.ls()

It will show all the existing components and their corresponding names. Name can be specified when the component is added to the dlm, for example:

myDLM = myDLM + seasonality(4, name = 'day4')
myDLM.ls()

We can also easily delete the unwanted component by using delete:

myDLM.delete('trend2')
myDLM.delete('day4')

After the building steps, we can specify some parameters for the model fitting, the most common one is the prior guess on the observational noise. The default value is 1.0. To change that to 10 you can do:

myDLM.noisePrior(10.0)

Such change usually has small impact on the model and is almost ignorable.

Model components

There are four model components provided with this package: trend, seasonality, dynamic and the auto-regression.

Trend

trend class is a model component for trend behavior. The data might be const or increasing linearly or quadraticly, which can all be captured by trend. The degree argument specifics the shape of the trend. degree=0 indicates this is a const, degree=1 indicates a line and degree=2 stands for a quadratic curve and so on so forth. w sets the prior covariance for the trend component (same for all the other components). The discounting factor will be explained later in next section:

linearTrend = trend(degree=1, discount=0.99, name='trend1', w=1e7)

Seasonality

The seasonality class models the periodic behavior of the data. Compared to the sine or cosine periodic curves, seasonality in this packages is more flexible, since it can turn into any shapes, much broader than the triangular families:

weekPeriod = seasonality(period=7, discount=0.99, name='week', w=1e7)

In the package, we implements the seasonality component in a form-free way (Harrison and West, 1999) to avoid the identifiability issue. The states of one seasonality component are always summed up to zero, so that it will not tangle with the trend component.

Dynamic

The dynamic class offers the modeling ability to add any additional observed time series as a controlled variable to the current one. For example, when studying the stock price, the ‘SP500’ index could be a good indicator for the modeling stock. A dynamic component need the user to supply the necessary information of the control variable over time:

SP500 = dynamic(features=SP500Index, discount=0.99, name='SP500', w=1e7)

The input features for dynamic should be a list of lists, since multi-dimension features are allowed. Following is one simple example:

Features = [[2000], [2010], [2020], [2030]]
Features = [[1.0, 2.0], [1.0, 3.0], [3.0, 3.0]]

Auto-regression

The autoReg class constructs the auto-regressive component on the model, i.e., the direct linear or non-linear dependency between the current observation and the previous days. User needs to specify the number of days of the dependency:

AR3 = autoReg(degree=3, discount=0.99, name='ar3', w=1e7)

There is once a data argument needed for constructing autoregression features but is now deprecated. autoReg can now fetch the data directly from the main dlm class and no need to provide during instantiation.

In this example, the latent stats for Auto-regression are aligned in a way of [today - 3, today - 2, today - 1]. So when fetching the coefficients from the latent states, this will be the correct order to read the coefficients.

Long-seasonality

The longSeason class is a complement class for seasonality. It allows constructing seasonality component that does not change every step. For example, the time unit is day, but user wants to add a monthly seasonality, then longSeason is the correct choice:

monthly = longSeason(period=12, stay=30, data=data, name='monthly', w=1e7)

These five classes of model components offer abundant modeling possiblities of the Bayesian dynamic linear model. Users can construct very complicated models using these components, such as hourly, weekly or monthly periodicy and holiday indicator and many other features.

Model fitting

Entailed before, the fitting of the dlm is fulfilled by a modified Kalman filter. Once the user finished constructing the model by adding different components. the dlm will compute all the necessary quantities internally for using Kalman filter. So users can simply call dlm.fitForwardFilter(), dlm.fitBackwardSmoother() or even simply dlm.fit() to fit both forward filter and backward smoother:

myDLM.fitForwardFilter()
myDLM.fitBackwardSmoother()
myDLM.fit()

The dlm.fitForwardFilter() is implemented in an online manner. It keeps an internal count on the filtered dates and once new data comes in, it only filters the new data without touching the existing results. In addition, this function also allows a rolling window fitting on the data, i.e., there will be a moving window and for each date, the kalman filter will only use the data within the window to filter the observation. This is equivalent to that the model only remembers a fixed length of dates:

myDLM.fitForwardFilter(useRollingWindow=True, windowLength=30)
myDLM.fitBackwardSmoother()

For dlm.backwardSmoother(), it has to use the whole time series to smooth the latent states once new data comes in. The smoothing provides a good retrospective analysis on our past decision of the data. For example, we might initially believe the time series is stable, while that could be a random behavior within a volatile time series, and the user learn this from the smoother.

Once the model fitting is completed, users can fetch the filtered or smoothed results from dlm:

myDLM.getMean(filterType='forwardFilter')
myDLM.getMean(filterType='backwardSmoother')
myDLM.getMean(filterType='predict')

myDLM.getVar(filterType='forwardFilter')
myDLM.getVar(filterType='backwardSmoother')
myDLM.getVar(filterType='predict')

The dlm recomputes a wide variety of model quantities that can be extracted by the user. For example, user can get the filtered states and covariance by typing:

myDLM.getLatentState(filterType='forwardFilter')
myDLM.getLatentState(filterType='backwardSmoother')

myDLM.getLatentCov(filterType='forwardFilter')
myDLM.getLatentCov(filterType='backwardSmoother')

This can be specified into individual component. For example, assume the model contains a trend component with a name of trend1, we can extract the corresponding latent state only for trend1 as:

myDLM.getLatentState(filterType='forwardFilter', name='trend1')
myDLM.getLatentState(filterType='backwardSmoother', name='trend1')

myDLM.getLatentCov(filterType='forwardFilter', name='trend1')
myDLM.getLatentCov(filterType='backwardSmoother', name='trend1')

as well as the mean of trend1 (evaluation * latent states):

myDLM.getMean(filterType='forwardFilter', name='trend1')
myDLM.getVar(filterType='forwardFilter', name='trend1')

One can also get the confidence interval on the filtered time series:

myDLM.geInterval(filterType='forwardFilter', p = 0.99)

There are also corresponding methods for smoothed and predicted results. For more detail, please refer to the dlm class documentation.

Model prediction

dlm provides three predict functions: dlm.predict() and dlm.continuePredict() and dlm.predictN(). The last one is wrapper of the former two and is recommended to use. (The former two will be gradually deprecated).

The dlm.predict() is a one-day ahead prediction function based on a user given date and feature set:

# predict next date after the time series
featureDict = {'SP500':[2090], 'page':[1, 2, 3, 4]}
(predictMean, predictVar) = myDLM.predict(date=myDLM.n - 1, featureDict=featureDict)

The function returns a tuple of predicted mean and predicted variance. The featureDict argument is a dictionary contains the feature information for dynamic component. Suppose the model contains a one-dimensional dynamic component named SP500 and another four-dimensional dynamic component page, then the featureDict takes the following Form:

featureDict = {'SP500':[2090], 'page':[1, 2, 3, 4]}

If the featureDict is not supplied but the date is not the last day, then the algorithm will automatically fetch from the old data about the feature value of all the dynamic component:

# predict a day in the middle
(predictMean, predictVar) = myDLM.predict(date=myDLM.n - 10)

The algorithm will use the feature on the date of myDLM.n - 9 in featureDict. If date is the last day but the featureDict is not provided, then an error will be raised.

If the user is interested beyond one-day ahead prediction, they can use dlm.continuePredict() for multiple-day ahead prediction, after using dlm.predict():

feature1 = {'SP500':[2090], 'page':[10, 20, 30, 40]}
feature2 = {'SP500':[2010], 'page':[11, 21, 31, 41]}
feature3 = {'SP500':[1990], 'page':[12, 22, 32, 42]}

# one-day ahead prediction after the last day
(predictMean, predictVar) = myDLM.predict(date=myDLM.n - 1, featureDict=feature1)
# we continue to two-day ahead prediction after the last day
(predictMean, predictVar) = myDLM.continuePredict(featureDict=feature2)
# we continue to three-day ahead prediction after the last day
(predictMean, predictVar) = myDLM.continuePredict(featureDict=feature3)

dlm.continuePredict() can only be used after dlm.predict() for multiple-day prediction. The featureDict can also be ignored if the prediction is requested on dates before the last day and the features on the predict day can be found from the old data.

dlm.predictN() (recommended) predicts over multiple days and is a wrapper of the two functions above. Using the same example, the results can be obtained by just called:

features = {'SP500':[[2090], [2010], [1990]], 'page':[[10, 20, 30,
40], [11, 21, 31, 41], [12, 22, 32, 42]]}
(predictMean, predictVar) = myDLM.predictN(N=3, date=myDLM.n-1,
featureDict=features)

The predictMean and predictVar will be two lists of three elements containing the predicted mean and variance.

Model amending

The user can still add, delete, modify data even when the model has been constructed.

Adding new data

For adding more data, user can opt to dlm.append():

newData = [0, 1, 2]
myDLM.append(newData, component = 'main')

If the model contains dynamic component, the corresponding features need to be updated as well:

newSP500 = [[2000], [2100], [2200]]
myDLM.append(data = newSP500, component = 'SP500')

Then the user can rerun the forward filter:

myDLM.fitForwardFilter()

The package will continue running the forward filter on the three new data ponts.

Deleting existing data

To delete any existing data, user can simply use the dlm.popout() function from dlm on a specific date, for example:

myDLM.popout(0)

Different from dlm.append(), dlm.popout() will be executed automatically for all components, so the user does not need to conduct the deletion mannually for each component. After the deletion, the forward filter needs to be rerun following the deleted date:

myDLM.fitForwardFilter()

Again, the package will automatically recognize the date and fit only the necessary period of time.

Ignoring a date

Ignoring is very similar to deleting. The only difference is the time counts. Because deleting will delete the data entirely, the time counts will therefore reduce by 1. By contrast, ignoring will treat the specific date as missing data, so the time count will not change. This difference is important when preriodicy is concerned. Changing of time counts will have high impacts on seasonality components.

dlm.ignore() simply set the data of a specific date to be None:

myDLM.ignore(2)

modify data

The dlm also provides user the ability to modify the data on a specific date and a specific component. This function enables possible future extension to interactive anomaly detection and data debugging:

myDLM.alter(date = 2, data = 0, component = 'main')

Model plotting

This package offers rich ploting options for illustrating the results. User can simply call dlm.plot() to directly plot the results once the models are fitted:

myDLM.plot()

# plot the mean of a given component
myDLM.plot(name=the_component_name)

# plot the latent state of a given component
myDLM.plotCoef(name=the_component_name)

User can choose which results to plot via dlm.turnOn() and dlm.turnOff():

myDLM.turnOn('filtered plot')
myDLM.turnOff('predict plot')
myDLM.turnOff('smoothed plot')

User can also choose whether to plot the confidence interval and whether plot the results in one figure or separate figures. The default is to plot the confidence interval and in separate plots. To change that:

myDLM.turnOff('confidence')
myDLM.turnOff('multiple plots')

The quantile of the confidence interval can be set via dlm.setConfidence():

myDLM.setConfidence(p = 0.95)

Currently there are two types of confidence interval realization. The default is ‘ribbon’ and the alternative is ‘line’. Users can change the confidence interval plots by:

myDLM.setIntervalType('ribbon')
myDLM.setIntervalType('line')

The default colors for the plots are:

  • original data: ‘black’
  • filtered results: ‘blue’
  • one-step ahead prediction: ‘green’
  • smoothed results: ‘red’

User can change the color setting via dlm.setColor(). The color space is the same as the matplotlib:

myDLM.setColor('filtered plot', 'yellow')
myDLM.setColor('data', 'blue')

If user decide to go back to the original setting, they can use dlm.resetPlotOptions() to reset all the plot option:

myDLM.resetPlotOptions()

Model Tuning and evaluation

The discounting factor of DLM determines how fast the model adapts to the new data. It could cause troublesome when users actually want the model itself to figure that out. The modelTuner provides users automatic tool for tuning the discounting factors:

from pydlm import modelTuner
myTuner = modelTuner(method='gradient_descent', loss='mse')
tunedDLM = myTuner.tune(myDLM, maxit=100)

The tuned discounting factor will be set inside the tunedDLM. Users can examine the tuned value from myTuner:

tuned_discounts = myTuner.getDiscounts()

After tuning, myDLM will remain unchanged and tunedDLM will contain the tuned discounting factos and will be in uninitialized status. Users need to run:

tunedDLM.fit()

before any other analysis on tunedDLM. The dlm also provides a simpler way to tune the discounting factor if the user would like myDLM to be altered directly:

myDLM.tune()

The tuner makes use of the MSE (one-day ahead prediction loss) and the gradient descent algorithm to tune the discounting factor to achieve the minimum loss. The discounting factors are assumed to be different across components but the same within a component. For now, only the MSE loss and the gradient descent algorithm is supported.

To ease the evaluation of the performance of the model fitting, the model also provides the residual time series and the one-day a head prediction error via:

mse = myDLM.getMSE()
residual = myDLM.getResidual(filterType='backwardSmoother')

Users can use these values to evaluate and choose the optimal model.

Advanced Settings

This part of settings closely relate to the algorithm behavior and offers some advanced features, some of which are still under development. Currently implemented is the dlm.stableMode() function and the dlm.evolveMode(). The dlm.stableMode() is turned on by default, and you can turn it off by:

myDLM.stableModel(False)

This mode helps increasing the numerical stability of the dlm when small discounting factor is used. Details about discounting factor will be covered in next section. The dlm.evolveMode() is used to control how different components evolve over time. See Harrison and West (1999, Page 202). They could evolve independently, which is equivalent to assume the innovation matrix is a block-diagonal matrix. The default assumption is ‘independent’ and to change to ‘dependent’, we can simply type:

myDLM.evolveMode('dependent')

The difference between ‘independent’ and ‘dependent’ is best explained when there are multiple components with different discounting factor and one of them is One. In the ‘dependent’ mode, the smoothed latent states of the component with discount factor 1 will be a value fluctuating around a constant, while in the ‘independent’ mode, it will be an exact constant. User can choose which to use depending on their own use case.

In the future, following functionalities are planned to be added: feature selection among dynamic components, factor models for high dimensional latent states.