Non-stationarity and unit root processes

import warnings
warnings.simplefilter("ignore", FutureWarning)

from statsmodels.tsa.deterministic import DeterministicProcess
import pandas as pd
import numpy as np

from numpy.random import default_rng
from statsmodels.tsa.arima_process import arma_acovf, ArmaProcess
from statsmodels.tsa.arima.model import ARIMA
import matplotlib.pyplot as plt
import statsmodels.api as sm
gen = default_rng(1)
import seaborn as sns
sns.set_theme()
sns.set_context("notebook")

Why need stationarity?

  • stationarity is a form of constancy of the properties of the process (mean, variance, autocovariances)

  • needed in order to be able to learn something about those properties

Many macroeconomic time series are non-stationary

#df = pd.read_excel('nama_10_gdp__custom_2373104_page_spreadsheet.xlsx',
#                   sheet_name='Sheet 1',
#                  skiprows=range(9), usecols=[0,1], names=['year', 'GDP'], index_col=0, parse_dates=True,
#                  ).loc['1995':'2021']
#df = df.astype('float')
#data = np.log(df['GDP'])
#title='Real GDP, Euro Area'
#ylabel='natural log of millions of euro'
#fig = data.plot(figsize=(10,3), title=title,ylabel=ylabel)
#plt.savefig('images/rGDPea.png')

Erg1

ARMA(p, q)

zt=α1zt1+α2zt1++αpztp+εt+β1εt1++βqεtq

in lag operator notation

α(L)zt=β(L)εt

ARMA(p, q) is stationary if the AR§ part is stationary, i.e. if all roots of

α(x)=1α1xα2x2αpxp=0

are outside the unit circle (|x|>1)

  • a non-stationary process with one root x=1 and the other roots |x|>1 is called a unit root process

  • if zt is an unit root process, Δzt=ztzt1=(1L)zt is stationary

α(x)=α(x)(1x)

and if all roots of

α(x)=1α1xα2x2αp1xp1=0

are outside the unit circle (|x|>1)

the AR part

α(L)zt=α(L)(1L)zt=α(L)(ztzt1)=α(L)Δzt
  • a non-stationary process whose first difference is stationary is called integrated of order one (I(1))

why “integrated”:

Δzt=ztzt1=ut,utstationary
zt=zt1+ut=zt2+ut1+ut=j=1tuj+z0
  • zt is the sum (integral) of a process (is an “integrated” process) starting from some initialization z0

  • j=1tuj refered to as a stochastic trend

  • Δzt is I(0) (stationary, no need to difference)

  • a non-stationary process which requres double difference to be stationary is called integrated of order two (I(2))

    • if zt is I(2) then Δ2zt=(1L)2zt is I(0)

  • etc for I(d).

    • if zt is I(d) then Δd is I(0)

    • d is the order of integration

  • if we difference a stationary process, we get another stationary process. However, no differencing was required to achieve stationarity

  • stationary process whose cumulative sum is also stationary, are called overdifferenced, and denoted with I(1)

example:

zt=εtεt1,εtiid(0,σ2)
  • zt is stationary but so is εt

  • zt is I(1)

  • non-invertible MA part

another example:

zt=δ0+δ1t+νtwith stationary νt
Δzt=δ1tδ1(t1)+Δνt=δ1+Δνt

is I(1)

  • linear time trend (deterministic trend)

  • zt is called trend-stationary process

in the sense that it is non-random

more generally:

zt=xtδ+νtwith stationary νt,Eνt=0

where xt are known constants

ztxtδ=νt

is stationary

Gaussian MLE

and, if νt is stationary Gaussian time series process

νN(0,Σ)

we have

zN(μ,Σ)

where

μ=Xδ

for example, if zt=δ0+δ1t+νt, the t-th row of X is [1,t]

Models with time trend in Python

Generate X for

zt=δ0+δ1t+νt,t=1,2,,T
z=Xδ+ν

T×2 matrix with t-th row given by [1,t]

from statsmodels.tsa.deterministic import DeterministicProcess

T = 500
index = pd.RangeIndex(0, T)
det_proc = DeterministicProcess(index, constant=True, order=1) # constant and a linear (1 order) trend
X = det_proc.in_sample()
X.head()
const trend
0 1.0 1.0
1 1.0 2.0
2 1.0 3.0
3 1.0 4.0
4 1.0 5.0
z=Xδ+ν
# constant and time trend part
delta = np.array([3, .1])
exog = X.values@delta 

νt as ARMA(1,1) process

alpha = np.array([.8])
beta = np.array([0.1])
ar = np.r_[1, -alpha] # coefficient on z(t) and z(t-1)
ma = np.r_[1, beta]  # coefficients on e(t) and e(t-1)
arma11_process = ArmaProcess(ar, ma)

nu =  arma11_process.generate_sample(T, distrvs=gen.normal)

z = exog + nu

Option 1 Estimate indicating constant and time trend

arma_model = ARIMA(z, order=(1, 0, 1), trend="ct")
arma_results = arma_model.fit()
print(arma_results.summary())
                               SARIMAX Results                                
==============================================================================
Dep. Variable:                      y   No. Observations:                  500
Model:                 ARIMA(1, 0, 1)   Log Likelihood                -733.673
Date:                Mon, 28 Mar 2022   AIC                           1477.345
Time:                        10:48:25   BIC                           1498.418
Sample:                             0   HQIC                          1485.614
                                - 500                                         
Covariance Type:                  opg                                         
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
const          3.0500      0.512      5.954      0.000       2.046       4.054
x1             0.0992      0.002     57.573      0.000       0.096       0.103
ar.L1          0.7971      0.035     22.981      0.000       0.729       0.865
ma.L1          0.0650      0.057      1.142      0.253      -0.047       0.176
sigma2         1.1054      0.073     15.235      0.000       0.963       1.248
===================================================================================
Ljung-Box (L1) (Q):                   0.00   Jarque-Bera (JB):                 0.28
Prob(Q):                              1.00   Prob(JB):                         0.87
Heteroskedasticity (H):               1.20   Skew:                            -0.01
Prob(H) (two-sided):                  0.24   Kurtosis:                         2.89
===================================================================================

Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).

Option 2 Estimate providing exogenous regressors

z=Xδ+ν
arma_model2 = ARIMA(z, exog=X, order=(1, 0, 1), trend="n")
arma_results2 = arma_model2.fit()
print(arma_results2.summary()) 
                               SARIMAX Results                                
==============================================================================
Dep. Variable:                      y   No. Observations:                  500
Model:                 ARIMA(1, 0, 1)   Log Likelihood                -733.673
Date:                Mon, 28 Mar 2022   AIC                           1477.345
Time:                        10:48:29   BIC                           1498.418
Sample:                             0   HQIC                          1485.614
                                - 500                                         
Covariance Type:                  opg                                         
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
const          3.0500      0.512      5.954      0.000       2.046       4.054
trend          0.0992      0.002     57.573      0.000       0.096       0.103
ar.L1          0.7971      0.035     22.981      0.000       0.729       0.865
ma.L1          0.0650      0.057      1.142      0.253      -0.047       0.176
sigma2         1.1054      0.073     15.235      0.000       0.963       1.248
===================================================================================
Ljung-Box (L1) (Q):                   0.00   Jarque-Bera (JB):                 0.28
Prob(Q):                              1.00   Prob(JB):                         0.87
Heteroskedasticity (H):               1.20   Skew:                            -0.01
Prob(H) (two-sided):                  0.24   Kurtosis:                         2.89
===================================================================================

Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).

Difference-stationary vs trend-stationary

zt=δ0+δ1t+νt
  • trend-stationary if νt is stationary

  • difference-stationary if νt is a unit root process

νt is a unit root process if in

α(L)νt=β(L)εt

one of the roots of

α(x)=1α1xα2x2αp+1xp+1=0

is x=1, and the other roots are |x|>1

α(L)=(1L)α(L)

all p roots of

α(x)=1α1xα2x2αpxp=0

are |x|>1

Δνt=(1L)νt is a stationary ARMA(p, q) process

α(L)νt=β(L)εtα(L)(1L)νt=β(L)εtα(L)Δνt=β(L)εt

Difference-stationary process

zt=δ0+δ1t+νt
  • νt is an ARIMA(p, 1, q) process

  • zt is an ARIMAX(p, 1, q) process (ARIMA with exogenous part - constant and time trend)

  • Δzt is an ARMAX(p, q) process (ARMA with exogenous part - constant)

Δzt=δ1+Δvt

model z as regression model with ARIMA errors

Difference between trend- and difference-stationary models

example

zt=δ0+δ1t+νtνt=ανt1+εt|α|1

at t+h

zt+h=δ0+δ1(t+h)+νt+hνt+h=ανt+h1+εt+h

forecast at t:

Etzt+h=δ0+δ1(t+h)+Etνt+hEtνt+h=αhνtEtzt+h=δ0+δ1(t+h)+αhνt
  • if |α|<1, as h, αh0

Etzt+h=δ0+δ1(t+h)+αhνtδ0+δ1(t+h)=Ezt+h
Ezt+h=δ0+δ1(t+h)+Eνt+hEνt+h=αEνt+h1+Eεt+h=0

For trend-stationary processes:

  • the long-run forecast is the unconditional mean of zt (mean reversion)

  • the long-run forecast is independent of zt

  • shocks to zt have temporary impact (transitory)

  • if α=1,

Etzt+h=δ0+δ1(t+h)+αhνtEtzt+h=δ0+δ1(t+h)+νtEtzt+h=δ0+δ1(t+h)+ztδ0δ1tνtEtzt+h=δ1h+zt

For difference-stationary processes:

  • the value of zt has a permanent effect on all future forecasts

    • shocks to zt have permanent effects

  • zt is expected to grow by δ1 every period

Martingale process

if for {zt}

E(zt+1|zt,zt1,)=zt

zt is a martingale process, and Δzt is a martingale difference process

unlike a random walk, martingale process allows for heteroskedasticity or conditional heteroskedasticity

Beveridge-Nelson decomposition

  • Every difference-stationary process can be written as a sum of a random walk and a stationary component

  • the effect of innovations (shocks) can be decomposed into permanent and transitory effects

  • ARIMA(p,1,q)

α(L)Δzt=δ0+β(L)εt
  • α(L) is invertible. Why?

  • equivalently (by invertability of α(L))

Δzt=δ1+ϕ(L)εt
δ1=δ0α(1),ϕ(L)=β(L)α(L)α(1)=1α1αp

(a bit of magic here…)

Since 1 is a root of the polinomial

ϕ(L)ϕ(1)

it can be witten as

ϕ(L)ϕ(1)=ϕ(L)(1L)

and

ϕ(L)=ϕ(L)(1L)+ϕ(1)

where

ϕ(L)=ϕ(L)ϕ(1)(1L)ϕj=i=j+1ϕi
Δzt=δ1+ϕ(L)εt(1L)zt=δ1+ϕ(1)εt+ϕ(L)(1L)εtzt=(1L)1δ1+(1L)1ϕ(1)εt+ϕ(L)εt

denote ztp=(1L)1δ1+(1L)1ϕ(1)εt

then,

(1L)ztp=δ1+ϕ(1)εtztpzt1p=δ1+ϕ(1)εtztp=δ1+zt1p+ϕ(1)εt

Therefore

zt=ztp+ztswhereztp=δ1+zt1p+ϕ(1)εtzts=ϕ(L)εt
  • ztp - permanent component (trend)

  • zts - transitory component (cycle)

  • εt has a permanent impact on zt through ztp

  • εt has a transitory impact on zt through zts

  • relative importance of permanent/transitory impact depends on the value of ϕ(1)

example ARIMA(0,1,1)

Δzt=εt+βεt1
zt=zt1+εt+βεt1=zt2+(εt1+βεt2)+εt+βεt1(assuming z0=ε0=0)=j=1tεj+βj=1t1εj(add and subtract βεt)=(1+β)j=1tεjztp+(βεt)zts
ztp=(1+β)j=1tεj=(1+β)j=1t1εj+(1+β)εt=zt1p+(1+β)εt

Random walk with a drift model:

zt=δ+zt1+εt
zt=δt+εt+εt1+εt2++ε1+z0Ezt=δt+Ez0var(zt)=σ2t+var(z0)

Beveridge-Nelson decomposition

Every difference-stationary processes can be written as a sum of a random walk and a stationary component

  • many macro variables are well represented by ARIMA(p,1,q) models

  • every ARIMA(p,1,q) model has a random walk stochastic trend

  • the growth in macro variables can be characterized by stochastic trends

Forecasting integrated variables

  • if zt is I(1), we estimate and forecast using Δzt

  • from the forecasts of Δzt+i, i=1,2,,h

zt+hf=zt+i=1hΔzt+if

Augmented Dickey–Fuller test

Consider AR(2) process:

zt=α1zt1+α2zt2+εt

re-write as

zt=α1zt1+(α2zt1α2zt1)+α2zt2+εt=(α1+α2)zt1α2(zt1zt2)+εt=(α1+α2)zt1α2Δzt1+εt

and

ztzt1=(α1+α21)zt1α2Δzt1+εtΔzt=(α1+α21)zt1α2Δzt1+εt

For AR(2) process, unit root means that x=1 is a solution to

α(x)=1α1xα2x2=0

i.e

α(1)=1α1α2=0

Therefore, testing for unit root is equivalent to testing that ρ=0 in

Δzt=(α1+α21)zt1α2Δzt1+εt=ρzt1+α1Δzt1+εt

In general AR§ model for zt

Δzt=(α1+α2++αp1)zt1+α1Δzt1+αp1Δztp+1+εtΔzt=ρzt1+i=1p1αiΔzti+εt
  • testing for unit root is equivalent to testing that ρ=0

(5)Δzt=ρzt1+i=1p1αiΔzti+εt
  • in (5) the H1 is that zt is a stationary mean 0 AR§ process

  • to test for unit root agains H1 that zt is a stationary around a constant (0) mean, estimate

(7)Δzt=δ0+ρzt1+ip1αiΔzti+εt
  • to test for unit root agains H1 that zt is a stationary around a deterministic linear time trend (trend-stationary)

(7)Δzt=δ0+δ1t+ρzt1+ip1αiΔzti+εt
  • appropriate for series that exhibit growth over the long run

The test for unit root is a test of ρ=0 (unit root) against the alternative of ρ<0 (stationary)

  • The asymptotic distribution of the test statistic for ρ is non-standard.

  • The critical values of the test statistics are obtained by Monte Carlo simulations, and depend of whether constant and time trend are included

  • The null hypothesis of a unit root is rejected when value of the test statistics is below the critical value.

  • If the null hypothesis cannot be rejected, zt should be differenced prior to estimation

ADF test in Python

df = pd.read_stata(r"C:\Users\eeu227\Documents\PROJECTS\econ108-repos\econ108-practice\EconometricsData\FRED-QD\FRED-QD.dta")
rgdp = df[['time', 'gdpc1']].set_index('time')['1961q1':].rename(columns={'gdpc1': 'rGDP'})
rgdp = np.log(rgdp)
df = pd.read_stata(r"C:\Users\eeu227\Documents\PROJECTS\econ108-repos\econ108-practice\EconometricsData\FRED-MD\FRED-MD.dta")
unemp = df[['time', 'unrate']].set_index('time')['1961-01':]
df = pd.read_stata(r"C:\Users\eeu227\Documents\PROJECTS\econ108-repos\econ108-practice\EconometricsData\FRED-MD\FRED-MD.dta")
cpi = np.log(df[['time', 'cpiaucsl']].set_index('time')['1961-01':])

dcpi = (cpi.diff(12)*100).dropna()

Real GDP

#fig = rgdp.plot(figsize=(12,4), title='log Real GDP, US', legend=False)
#plt.savefig('images/rGDPus.png')

Erg1

from statsmodels.tsa.stattools import adfuller
def get_adfresults(dftest):
    dfoutput = pd.Series(
            dftest[0:4],
            index=[
                "Test Statistic",
                "p-value",
                "#Lags Used",
                "Number of Observations Used",
            ],
        )
    for key, value in dftest[4].items():
        dfoutput[f"Critical Value ({key})"] = value
    return dfoutput

ADF with constant and trend, 4 lags

dftest = adfuller(rgdp, maxlag=4, autolag=None, regression='ct')
dftest
(-2.0141544954585036,
 0.5937024444875985,
 4,
 223,
 {'1%': -3.9999506167815206,
  '5%': -3.4303636888103926,
  '10%': -3.138725564735756})
get_adfresults(dftest)
Test Statistic                  -2.014154
p-value                          0.593702
#Lags Used                       4.000000
Number of Observations Used    223.000000
Critical Value (1%)             -3.999951
Critical Value (5%)             -3.430364
Critical Value (10%)            -3.138726
dtype: float64

Automatically determine optimal number of lags using AIC

dftest = adfuller(rgdp, autolag="AIC", regression='ct')
get_adfresults(dftest)
Test Statistic                  -2.157741
p-value                          0.513651
#Lags Used                       2.000000
Number of Observations Used    225.000000
Critical Value (1%)             -3.999579
Critical Value (5%)             -3.430185
Critical Value (10%)            -3.138621
dtype: float64

Unemployment Rate

#fig = unemp.plot(figsize=(12,4), title='Unemployment rate, US', legend=False)
#plt.savefig('images/UNus.png')

UNus

dftest = adfuller(unemp, autolag='AIC', regression='c')
get_adfresults(dftest)
Test Statistic                  -2.996063
p-value                          0.035264
#Lags Used                      12.000000
Number of Observations Used    671.000000
Critical Value (1%)             -3.440133
Critical Value (5%)             -2.865857
Critical Value (10%)            -2.569069
dtype: float64

Consumer Price Inflation

#fig = dcpi.plot(figsize=(12,4), title='Inflation rate, US', legend=False)
#plt.savefig('images/dCPIus.png')

dCPIus

dftest = adfuller(dcpi, autolag='AIC', regression='c')
get_adfresults(dftest)
Test Statistic                  -2.804916
p-value                          0.057575
#Lags Used                      15.000000
Number of Observations Used    656.000000
Critical Value (1%)             -3.440358
Critical Value (5%)             -2.865956
Critical Value (10%)            -2.569122
dtype: float64

ADF test using ARCH

from arch.unitroot import ADF
adf = ADF(rgdp, trend="ct")
adf.summary()
Augmented Dickey-Fuller Results
Test Statistic -2.158
P-value 0.514
Lags 2


Trend: Constant and Linear Time Trend
Critical Values: -4.00 (1%), -3.43 (5%), -3.14 (10%)
Null Hypothesis: The process contains a unit root.
Alternative Hypothesis: The process is weakly stationary.

Examine the regression results

reg_res = adf.regression
print(reg_res.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                      y   R-squared:                       0.176
Model:                            OLS   Adj. R-squared:                  0.161
Method:                 Least Squares   F-statistic:                     11.76
Date:                Mon, 28 Mar 2022   Prob (F-statistic):           1.13e-08
Time:                        16:43:14   Log-Likelihood:                 786.27
No. Observations:                 225   AIC:                            -1563.
Df Residuals:                     220   BIC:                            -1545.
Df Model:                           4                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
Level.L1      -0.0217      0.010     -2.158      0.032      -0.041      -0.002
Diff.L1        0.2365      0.065      3.611      0.000       0.107       0.366
Diff.L2        0.1917      0.066      2.923      0.004       0.062       0.321
const          0.1848      0.083      2.231      0.027       0.022       0.348
trend          0.0001    7.5e-05      1.935      0.054   -2.67e-06       0.000
==============================================================================
Omnibus:                       17.613   Durbin-Watson:                   1.994
Prob(Omnibus):                  0.000   Jarque-Bera (JB):               56.070
Skew:                          -0.116   Prob(JB):                     6.68e-13
Kurtosis:                       5.435   Cond. No.                     2.21e+04
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 2.21e+04. This might indicate that there are
strong multicollinearity or other numerical problems.

Examine the regression residuals

resids = pd.DataFrame(reg_res.resid)
resids.index = rgdp.index[3:]
resids.columns = ["resids"]
fig = resids.plot(figsize=(12,4))
../../_images/01-Unit-root_56_0.png
fig, ax = plt.subplots(figsize=(12,4))
fig = sm.graphics.tsa.plot_acf(reg_res.resid, lags=23, ax=ax)
ax.set_ylim([-0.3, 1.1])
(-0.3, 1.1)
../../_images/01-Unit-root_57_1.png