Trend Inflation

This example is a replication of the univariate state space model suggested by (Stock & Watson, 2016) using GeneralisedFilters to define a heirarchical model for use in Rao- Blackwellised particle filtering.

using GeneralisedFilters
using SSMProblems
using Distributions
using Random
using StatsBase
using LinearAlgebra

const GF = GeneralisedFilters

Model Definition

We begin by defining the local level trend model, a linear Gaussian model with a weakly stationary random walk component. The dynamics of which are as follows:

\[\begin{aligned} y_{t} &= x_{t} + \eta_{t} \\ x_{t+1} &= x_{t} + \varepsilon_{t} \end{aligned}\]

However, this model is not enough to capture trend dynamics when faced with structural breaks. (Stock & Watson, 2007) suggest adding a stochastic volatiltiy component, defined like so:

\[\begin{aligned} \log \sigma_{\eta, t+1} = \log \sigma_{\eta, t} + \nu_{\eta, t} \\ \log \sigma_{\varepsilon, t+1} = \log \sigma_{\varepsilon, t} + \nu_{\varepsilon, t} \end{aligned}\]

where $\nu_{z,t} \sim N(0, \gamma)$ for $z \in \{ \varepsilon, \eta \}$.

Using GeneralisedFilters, we can construct a heirarchical version of this model such that the local level trend component is conditionally linear Gaussian on the volatility draws.

Stochastic Volatility Process

We begin by defining the non-linear dynamics, which aren't conditioned contemporaneous states. Since these processes are traditionally non-linear/non-Gaussian we use the SSMProblems interface to define the stochastic volatility components.

struct StochasticVolatilityPrior{T<:Real} <: StatePrior end
function SSMProblems.distribution(prior::StochasticVolatilityPrior{T}; kwargs...) where {T}
    return product_distribution(Normal(zero(T), T(1)), Normal(zero(T), T(1)))
end

For the dynamics, instead of using the SSMProblems.distribution utility, we only define the simulate method, which is sufficient for the RBPF.

struct StochasticVolatility{ΓT<:AbstractVector} <: LatentDynamics
    γ::ΓT
end
function SSMProblems.simulate(
    rng::AbstractRNG,
    proc::StochasticVolatility,
    step::Integer,
    state::AbstractVector{T};
    kwargs...,
) where {T<:Real}
    new_state = deepcopy(state)
    new_state[1:2] += proc.γ .* randn(rng, T, 2)
    return new_state
end

Local Level Trend Process

For the conditionally linear and Gaussian components, we subtype the model and provide a keyword argument as the conditional element. In this case $A$ and $b$ remain constant, but $Q$ is conditional on the log variance, stored in new_outer (the nomenclature chosen for heirarchical modeling).

struct LocalLevelTrend <: LinearGaussianLatentDynamics end
GF.calc_A(::LocalLevelTrend, ::Integer; kwargs...) = [1;;]
GF.calc_b(::LocalLevelTrend, ::Integer; kwargs...) = [0;]
function GF.calc_Q(::LocalLevelTrend, ::Integer; new_outer, kwargs...)
    return [exp(new_outer[1]);;]
end

Similarly, we define the observation process conditional on a separate log variance.

struct SimpleObservation <: LinearGaussianObservationProcess end
GF.calc_H(::SimpleObservation, ::Integer; kwargs...) = [1;;]
GF.calc_c(::SimpleObservation, ::Integer; kwargs...) = [0;]
function GF.calc_R(::SimpleObservation, ::Integer; new_outer, kwargs...)
    return [exp(new_outer[2]);;]
end

Unobserved Components with Stochastic Volatility

The state space model suggested by (Stock & Watson, 2007) can be constructed with the following method:

function UCSV(γ::T) where {T<:Real}
    stoch_vol_prior = StochasticVolatilityPrior{T}()
    stoch_vol_process = StochasticVolatility(fill(γ, 2))

    local_level_model = StateSpaceModel(
        GF.HomogeneousGaussianPrior(zeros(T, 1), Matrix(100.0I(1))),
        LocalLevelTrend(),
        SimpleObservation(),
    )

    return HierarchicalSSM(stoch_vol_prior, stoch_vol_process, local_level_model)
end;

For plotting, we can extract the ancestry of the Rao Blackwellised particles using the callback system. For our inflation data, this reduces to the following:

rng = MersenneTwister(1234);
sparse_ancestry = GF.AncestorCallback(nothing);
states, ll = GF.filter(
    rng,
    UCSV(0.2),
    RBPF(KalmanFilter(), 2^12; threshold=1.0),
    [[pce] for pce in fred_data.value];
    callback=sparse_ancestry,
);

The sparse_ancestry object stores a sparse ancestry tree which we can use to approximate the smoothed series without an additional backwards pass. We can convert this data structure to a human readable array by using GeneralisedFilters.get_ancestry and then take the mean path by passing a custom function.

trends, volatilities = mean_path(GF.get_ancestry(sparse_ancestry.tree), states);
plot_ucsv(trends[1, :], eachrow(volatilities), fred_data)

Outlier Adjustments

For additional robustness, (Stock & Watson, 2016) account for one-time measurement shocks and suggest an alteration in the observation equation, where

\[\eta_{t} \sim N(0, s_{t} \cdot \sigma_{\eta, t}^2) \quad \quad s_{t} \sim \begin{cases} U(0,2) & \text{ with probability } p \\ \delta(1) & \text{ with probability } 1 - p \end{cases}\]

The prior is the same as before, but with additional state which we can assume will always be 1; using the Distributions interface this is just Dirac(1)

struct OutlierAdjustedVolatilityPrior{T<:Real} <: StatePrior end
function SSMProblems.distribution(
    prior::OutlierAdjustedVolatilityPrior{T}; kwargs...
) where {T}
    return product_distribution(Normal(zero(T), T(1)), Normal(zero(T), T(1)), Dirac(one(T)))
end

In terms of the model definition, we can construct a separate LatentDynamics which contains the same volatility process as before, but with the respective draw in the third component.

struct OutlierAdjustedVolatility{ΓT} <: LatentDynamics
    volatility::StochasticVolatility{ΓT}
    switch_dist::Bernoulli
    outlier_dist::Uniform
end

The simulation then calls the volatility process, and computes the outlier term in the third state

function SSMProblems.simulate(
    rng::AbstractRNG,
    proc::OutlierAdjustedVolatility,
    step::Integer,
    state::AbstractVector{T};
    kwargs...,
) where {T<:Real}
    new_state = SSMProblems.simulate(rng, proc.volatility, step, state; kwargs...)
    new_state[3] = rand(rng, proc.switch_dist) ? rand(rng, proc.outlier_dist) : one(T)
    return new_state
end

For the observation process, we define a new object where $R$ is dependent on both the measurement volatility as well as this outlier adjustment coefficient.

struct OutlierAdjustedObservation <: LinearGaussianObservationProcess end
GF.calc_H(::OutlierAdjustedObservation, ::Integer; kwargs...) = [1;;]
GF.calc_c(::OutlierAdjustedObservation, ::Integer; kwargs...) = [0;]
function GF.calc_R(::OutlierAdjustedObservation, ::Integer; new_outer, kwargs...)
    return [new_outer[3] * exp(new_outer[2]);;]
end

Outlier Adjusted UCSV

The state space model suggested by (Stock & Watson, 2007) can be constructed with the following method:

function UCSVO(γ::T, prob::T) where {T<:Real}
    stoch_vol_prior = OutlierAdjustedVolatilityPrior{T}()
    stoch_vol_process = OutlierAdjustedVolatility(
        StochasticVolatility(fill(γ, 2)), Bernoulli(prob), Uniform{T}(2, 10)
    )

    local_level_model = StateSpaceModel(
        GF.HomogeneousGaussianPrior(zeros(T, 1), Matrix(100.0I(1))),
        LocalLevelTrend(),
        OutlierAdjustedObservation(),
    )

    return HierarchicalSSM(stoch_vol_prior, stoch_vol_process, local_level_model)
end;

We then repeat the same experiment, this time with an outlier probability of $p = 0.05$

rng = MersenneTwister(1234);
sparse_ancestry = GF.AncestorCallback(nothing)
states, ll = GF.filter(
    rng,
    UCSVO(0.2, 0.05),
    RBPF(KalmanFilter(), 2^12; threshold=1.0),
    [[pce] for pce in fred_data.value];
    callback=sparse_ancestry,
);

this process is identical to the last, except with an additional volatilities state which captures the outlier distance. We omit this feature in the plots, but the impact is clear when comparing the maximum transitory noise around the GFC.

trends, volatilities = mean_path(GF.get_ancestry(sparse_ancestry.tree), states);
plot_ucsv(trends[1, :], eachrow(volatilities), fred_data)

This page was generated using Literate.jl.