Module 3: Stationary Data

Are The Time Series Stationary?

library(fpp3)
Warning: package 'fpp3' was built under R version 4.5.2
Registered S3 method overwritten by 'tsibble':
  method               from 
  as_tibble.grouped_df dplyr
── Attaching packages ──────────────────────────────────────────── fpp3 1.0.2 ──
✔ tibble      3.3.0     ✔ tsibble     1.1.6
✔ dplyr       1.1.4     ✔ tsibbledata 0.4.1
✔ tidyr       1.3.1     ✔ feasts      0.4.2
✔ lubridate   1.9.4     ✔ fable       0.5.0
✔ ggplot2     3.5.2     
Warning: package 'tsibble' was built under R version 4.5.2
Warning: package 'tsibbledata' was built under R version 4.5.2
Warning: package 'feasts' was built under R version 4.5.2
Warning: package 'fabletools' was built under R version 4.5.2
Warning: package 'fable' was built under R version 4.5.2
── Conflicts ───────────────────────────────────────────────── fpp3_conflicts ──
✖ lubridate::date()    masks base::date()
✖ dplyr::filter()      masks stats::filter()
✖ tsibble::intersect() masks base::intersect()
✖ tsibble::interval()  masks lubridate::interval()
✖ dplyr::lag()         masks stats::lag()
✖ tsibble::setdiff()   masks base::setdiff()
✖ tsibble::union()     masks base::union()
library(fredr)
Warning: package 'fredr' was built under R version 4.5.2
library(tidyverse)
Warning: package 'stringr' was built under R version 4.5.2
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ forcats 1.0.1     ✔ readr   2.1.5
✔ purrr   1.1.0     ✔ stringr 1.6.0
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter()     masks stats::filter()
✖ tsibble::interval() masks lubridate::interval()
✖ dplyr::lag()        masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(patchwork)
library(feasts)
library(fabletools)
library(quantmod)
Loading required package: xts
Loading required package: zoo

Attaching package: 'zoo'

The following object is masked from 'package:tsibble':

    index

The following objects are masked from 'package:base':

    as.Date, as.Date.numeric


######################### Warning from 'xts' package ##########################
#                                                                             #
# The dplyr lag() function breaks how base R's lag() function is supposed to  #
# work, which breaks lag(my_xts). Calls to lag(my_xts) that you type or       #
# source() into this session won't work correctly.                            #
#                                                                             #
# Use stats::lag() to make sure you're not using dplyr::lag(), or you can add #
# conflictRules('dplyr', exclude = 'lag') to your .Rprofile to stop           #
# dplyr from breaking base R's lag() function.                                #
#                                                                             #
# Code in packages is not affected. It's protected by R's namespace mechanism #
# Set `options(xts.warn_dplyr_breaks_lag = FALSE)` to suppress this warning.  #
#                                                                             #
###############################################################################

Attaching package: 'xts'

The following objects are masked from 'package:dplyr':

    first, last

Loading required package: TTR
Registered S3 method overwritten by 'quantmod':
  method            from
  as.zoo.data.frame zoo 
library(tseries)
remove(list=ls())

fredr_set_key("18f0dfa970a5782481171e8fa3980e41")

cpi <- fredr(series_id = "CPIAUCSL",
              observation_start = as.Date("2004-01-01"),
              observation_end   = as.Date("2024-12-01")
              ) |>
  transmute(Month = yearmonth(date), value) |>
  as_tsibble(index = Month)

gdp<- myts <- fredr(series_id = "GDPC1",
              observation_start = as.Date("2004-01-01"),
              observation_end   = as.Date("2024-12-01")
              ) |>
  transmute(Month = yearmonth(date), value) |>
  as_tsibble(index = Month)
unemp <- fredr(series_id = "UNRATE",
              observation_start = as.Date("2004-01-01"),
              observation_end   = as.Date("2024-12-01")
              ) |>
  transmute(Month = yearmonth(date), value) |>
  as_tsibble(index = Month)

Visual Stationary Test

p1<- autoplot(cpi, main="CPI (Level)") 
Plot variable not specified, automatically selected `.vars = value`
Warning in geom_line(...): Ignoring unknown parameters: `main`
p2<- autoplot(gdp, main="Real GDP (Level)") 
Plot variable not specified, automatically selected `.vars = value`
Warning in geom_line(...): Ignoring unknown parameters: `main`
p3<- autoplot(unemp, main="Unemployment Rate (Level)")
Plot variable not specified, automatically selected `.vars = value`
Warning in geom_line(...): Ignoring unknown parameters: `main`
p1/p2/p3

Looking at CPI, we can straight away see that there is a trend, violating the constant mean assumption.

Looking at Real GDP, we see the same thing: a trend over time, violating the constant mean assumption.

Looking at the unemployment rate, it satisfies the constant mean trend. However, it looks like it might violate the constant variance and covariance assumptions.

ADF

adf.test(cpi$value)
Warning in adf.test(cpi$value): p-value greater than printed p-value

    Augmented Dickey-Fuller Test

data:  cpi$value
Dickey-Fuller = -0.028283, Lag order = 6, p-value = 0.99
alternative hypothesis: stationary
adf.test(gdp$value)

    Augmented Dickey-Fuller Test

data:  gdp$value
Dickey-Fuller = -1.0493, Lag order = 4, p-value = 0.9251
alternative hypothesis: stationary
adf.test(unemp$value)

    Augmented Dickey-Fuller Test

data:  unemp$value
Dickey-Fuller = -2.2659, Lag order = 6, p-value = 0.4638
alternative hypothesis: stationary

In an ADF test, the null hypothesis is non-stationary, and the alternative hypothesis is that the series is stationary. We reject the null (series is stationary) when p-value < 0.05. For cpi, gdp, and unemp, we fail to reject the null hypothesis, meaning they are non-stationary.

Difference

cpi_diff <-cpi %>% mutate(diff = difference(value))
gdp_diff <-gdp %>% mutate(diff = difference(value))
unemp_diff <-unemp %>% mutate(diff = difference(value))

Visualize To Confirm

p4<- autoplot(cpi_diff,diff)+ggtitle("CPI 1 Difference")
p5<- autoplot(gdp_diff,diff)+ggtitle("GDP 1 Difference")
p6<- autoplot(unemp_diff, diff)+ggtitle("Unemployment 1 Difference")

p4/p5/p6
Warning: Removed 1 row containing missing values or values outside the scale range
(`geom_line()`).
Removed 1 row containing missing values or values outside the scale range
(`geom_line()`).
Removed 1 row containing missing values or values outside the scale range
(`geom_line()`).

ADF To Confirm

adf.test(na.omit(cpi_diff$diff))
Warning in adf.test(na.omit(cpi_diff$diff)): p-value smaller than printed
p-value

    Augmented Dickey-Fuller Test

data:  na.omit(cpi_diff$diff)
Dickey-Fuller = -4.1292, Lag order = 6, p-value = 0.01
alternative hypothesis: stationary
adf.test(na.omit(gdp_diff$diff))
Warning in adf.test(na.omit(gdp_diff$diff)): p-value smaller than printed
p-value

    Augmented Dickey-Fuller Test

data:  na.omit(gdp_diff$diff)
Dickey-Fuller = -4.8227, Lag order = 4, p-value = 0.01
alternative hypothesis: stationary
adf.test(na.omit(unemp_diff$diff))
Warning in adf.test(na.omit(unemp_diff$diff)): p-value smaller than printed
p-value

    Augmented Dickey-Fuller Test

data:  na.omit(unemp_diff$diff)
Dickey-Fuller = -7.0663, Lag order = 6, p-value = 0.01
alternative hypothesis: stationary

Visual and ADF tests prove that the data is stationary because p<.05 for all 3 differenced sets.

ACF and PCF Charts

CPI

cpi_acf<-cpi_diff%>%
  ACF(diff)%>%
  autoplot()+ggtitle("CPI ACF")
cpi_pacf<-cpi_diff%>%
  PACF(diff)%>%
  autoplot()+ggtitle("CPI PACF")

cpi_acf/cpi_pacf

For CPI, the ARMA process shows us that we have AR(1) and MA(1). This tells us that today’s CPI depends on the CPI from the previous period and that it took 1 period to adjust from an unexpected shock.

GDP

gdp_acf<-gdp_diff%>%
  ACF(diff)%>%
  autoplot()+ggtitle("GDP ACF")
gdp_pacf<-gdp_diff%>%
  PACF(diff)%>%
  autoplot()+ggtitle("GDP PACF")

gdp_acf/gdp_pacf

From the charts, it appears that there is no autoregressive or moving average component to GDP since there is no lag above the cutoff.

Unemployment Rate

unemp_acf<-unemp_diff%>%
  ACF(diff)%>%
  autoplot()+ggtitle("Unemployment ACF")
unemp_pacf<-cpi_diff%>%
  PACF(diff)%>%
  autoplot()+ggtitle("Unemployment PACF")

unemp_acf/unemp_pacf

For unemployment, we get ARMA(1,0), meaning that there is only an autoregressive component. This means that today’s GDP depends on the GDP from the previous period

Decompositions

cpi_decomp<-cpi%>%
  model(STL(value))%>%
  components
autoplot(cpi_decomp) + ggtitle("CPI STL Decomposition")

Trend:

We can determine from the trend that there is constant, long-term growth in CPI. This is consistent with typical inflation.

Seasonality:

There is obvious seasonality in CPI, but it has dampened over time.

gdp_decomp<-gdp%>%
  model(STL(value))%>%
  components
autoplot(gdp_decomp) + ggtitle("GDP STL Decomposition")

Trend:

We can determine from the trend that there is constant, long-term growth in GDP. This is consistent with a typical developed country, as there is little wiggle room in the efficient frontier, leading to consistent “low” growth.

Seasonality:

There is obvious seasonality in CPI, but it has grown over time. Seasonality was very week and constantly got stronger. This is consistent with recent consumer trends as the winter season is constantly showing increased spending.

unemp_decomp<-unemp%>%
  model(STL(value))%>%
  components
autoplot(unemp_decomp) + ggtitle("Unemployment STL Decomposition")

Trend:

We can determine from the trend that there is no structural change to the unemployment rate, but it can experience upticks and downticks, as we can see from the waves in the data.

Seasonality:

The same goes for unemployment as GDP; there was little seasonality at first, but recently, it has magnified and strengthened.