7 Gaussian HMM
Univariate Gaussian HMMs with TMB
are very similar to the previous Poisson HMM.
The main changes are the distribution parameters being changed from \({\boldsymbol\lambda}\) to \({\boldsymbol\mu}\) and \({\boldsymbol\sigma}\).
In turn, these cause the density function to change from dpois
to dnorm
(and from rpois
to rnorm
for simulations).
Many functions available in functions/utils.R have been adapted for the Gaussian case in functions/norm_utils.R.
We detail below an example of a Gaussian HMM with TMB.
7.1 Dataset
The dataset we show in this example contains log-returns of the SP500 dataset pulled from Yahoo finance. These log-returns are generated with the following code.
library(tidyverse)
# Loading the sp500 dataset
<- read_csv("data/S&P500.csv") %>%
SP500 mutate(lag = lag(`Adj Close`),
lr = log(`Adj Close`/lag)) %>%
select(lr) %>%
drop_na()
<- 100 * SP500$lr
SP500 save(SP500, file = "data/SP500.RData")
7.2 Likelihood function
The Gaussian negative log-likelihood in TMB can be coded as
#include <TMB.hpp>
#include "../functions/norm_utils.cpp"
// Likelihood for a normal hidden markov model.
template<class Type>
<Type>::operator() ()
Type objective_function{
// Data
(x); // timeseries vector
DATA_VECTOR(m); // Number of states m
DATA_INTEGER
// Parameters
(tmu); // conditional means
PARAMETER_VECTOR(tsigma); // conditional log_sd's
PARAMETER_VECTOR(tgamma); // m(m-1) working parameters of TPM
PARAMETER_VECTOR// PARAMETER_VECTOR(tdelta); // m-1 working parameters of initial distribution
// Transform working parameters to natural parameters:
<Type> mu = tmu;
vector<Type> sigma = tsigma.exp();
vector<Type> gamma = gamma_w2n(m, tgamma);
matrix// vector<Type> delta = delta_w2n(m, tdelta);
// Construct stationary distribution
<Type> delta = stat_dist(m, gamma);
vector
// Get number of timesteps (n)
int n = x.size();
// Evaluate conditional distribution: Put conditional probabilities
// of observed x in n times m matrix (one column for each state)
<Type> emission_probs(n, m);
matrix<Type> row1vec(1, m);
matrix.setOnes();
row1vecfor (int i = 0; i < n; i++) {
if (x[i] != x[i]) { // f != f returns true if and only if f is NaN.
// Replace missing values (NA in R, NaN in C++) with 1
.row(i) = row1vec;
emission_probs}
else {
.row(i) = dnorm(x(i), mu, sigma, false);
emission_probs}
}
// Corresponds to (Zucchini et al., 2016, p 333)
<Type> foo, P;
matrix, sumfoo, lscale;
Type mllk
= (delta * vector<Type>(emission_probs.row(0))).matrix();
foo = foo.sum();
sumfoo = log(sumfoo);
lscale .transposeInPlace();
foo/= sumfoo;
foo for (int i = 2; i <= n; i++) {
= emission_probs.row(i - 1);
P = ((foo * gamma).array() * P.array()).matrix();
foo = foo.sum();
sumfoo += log(sumfoo);
lscale /= sumfoo;
foo }
= -lscale;
mllk
// Use adreport on variables for which we want standard errors
(mu);
ADREPORT(sigma);
ADREPORT(gamma);
ADREPORT(delta);
ADREPORT
// Variables we need for local decoding and in a convenient format
(mu);
REPORT(sigma);
REPORT(gamma);
REPORT(delta);
REPORT(n);
REPORT// REPORT(emission_probs);
(mllk);
REPORT
return mllk;
}
and requires the following utility functions.
// Function transforming working parameters in initial distribution
// to natural parameters
template<class Type>
<Type> delta_w2n(int m, vector<Type> tdelta) {
vector
<Type> delta(m);
vector<Type> foo(m);
vector
if (m == 1)
return Type(1);
// set first element to one.
// Fill in the last m - 1 elements with working parameters
// and take exponential
<< Type(1), tdelta.exp();
foo
// normalize
= foo / foo.sum();
delta
return delta;
}
// Function transforming the working parameters in TPM to
// natural parameters (w2n)
template<class Type>
<Type> gamma_w2n(int m, vector<Type> tgamma) {
matrix
// Construct m x m identity matrix
<Type> gamma(m, m);
matrix.setIdentity();
gamma
if (m == 1)
return gamma;
// Fill offdiagonal elements with working parameters column-wise:
int idx = 0;
for (int i = 0; i < m; i++){
for (int j = 0; j < m; j++){
if (j != i){
// Fill gamma according to mapping and take exponential
(j, i) = tgamma.exp()(idx);
gamma++;
idx}
}
}
// Normalize each row:
<Type> cs = gamma.rowwise().sum();
vectorfor (int i = 0; i < m; i++) gamma.row(i) /= cs[i];
return gamma;
}
// Function computing the stationary distribution of a Markov chain
template<class Type>
<Type> stat_dist(int m, matrix<Type> gamma) {
vector
// Construct stationary distribution
<Type> I(m, m);
matrix<Type> U(m, m);
matrix<Type> row1vec(1, m);
matrix= U.setOnes();
U = I.setIdentity();
I .setOnes();
row1vec<Type> A = I - gamma + U;
matrix<Type> Ainv = A.inverse();
matrix<Type> deltamat = row1vec * Ainv;
matrix<Type> delta = deltamat.row(0);
vector
return delta;
}
7.3 Estimation
The following R code illustrates how to compute the estimates and standard errors, using the objective function written above.
# Load packages and utility functions
source("code/packages.R")
source("functions/norm_utils.R")
# Compile and load the objective function for TMB
::compile("code/norm_hmm.cpp")
TMB## [1] 0
dyn.load(dynlib("code/norm_hmm"))
# Parameters and covariates
load("data/SP500.RData")
<- 2
m # Means
<- c(-2 * mean(SP500), 2 * mean(SP500))
mu # Standard deviations
<- c(0.5 * sd(SP500), 2 * sd(SP500))
sigma # TPM
<- matrix(c(0.9, 0.1,
gamma 0.1, 0.9), nrow = 2, byrow = TRUE)
# Parameters & covariates for TMB
<- norm.HMM.pn2pw(m = m, mu = mu, sigma = sigma, gamma = gamma)
working_params <- list(x = SP500, m = m)
TMB_data <- MakeADFun(TMB_data, working_params, DLL = "norm_hmm", silent = TRUE)
obj
# Estimation
<- MakeADFun(data = TMB_data,
obj_tmb parameters = working_params,
DLL = "norm_hmm",
silent = TRUE)
<- nlminb(start = obj_tmb$par,
mod_tmb objective = obj_tmb$fn,
gradient = obj_tmb$gr,
hessian = obj_tmb$he)
Note that the script above makes a simple educated guess on the true parameters in order to choose initial parameters.
7.4 Results
Estimates can be shown here along with their standard errors, as was done previously in the Poisson case. As before, the matrix results are displayed in a column-wise format. This can be seen by comparing estimates with a more readable view.
summary(sdreport(obj_tmb), "report")
## Estimate Std. Error
## mu 0.06116848 0.005401313
## mu -0.12288814 0.032944675
## sigma 0.69860750 0.005201810
## sigma 2.24762988 0.029916757
## gamma 0.98850975 0.001067611
## gamma 0.04374985 0.004126772
## gamma 0.01149025 0.001067611
## gamma 0.95625015 0.004126772
## delta 0.79199446 0.016439163
## delta 0.20800554 0.016439163
# Readable estimates
$report()
obj_tmb## $mu
## [1] 0.06116848 -0.12288814
##
## $sigma
## [1] 0.6986075 2.2476299
##
## $delta
## [1] 0.7921573 0.2078427
##
## $n
## [1] 23349
##
## $gamma
## [,1] [,2]
## [1,] 0.98852111 0.01147889
## [2,] 0.04374985 0.95625015
##
## $mllk
## [1] 31516.82
An intuitive interpretation of these results is that on average, market prices either grow slowly (positive low mean) but surely (low variance) as people invest carefully, or the market prices decrease (negative larger mean) as people sell in panic for fear of losing too much (higher variance). For more details on this, see e.g. and get some information on bull and bear markets.
Note that the TPM is displayed column-wise.