m <- seas(AirPassengers)
summary(m)
#>
#> Call:
#> seas(x = AirPassengers)
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> Weekday -0.0029497 0.0005232 -5.638 1.72e-08 ***
#> Easter[1] 0.0177674 0.0071580 2.482 0.0131 *
#> AO1951.May 0.1001558 0.0204387 4.900 9.57e-07 ***
#> MA-Nonseasonal-01 0.1156204 0.0858588 1.347 0.1781
#> MA-Seasonal-12 0.4973600 0.0774677 6.420 1.36e-10 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> SEATS adj. ARIMA: (0 1 1)(0 1 1) Obs.: 144 Transform: log
#> AICc: 947.3, BIC: 963.9 QS (no seasonality in final): 0
#> Box-Ljung (no autocorr.): 26.65 Shapiro (normality): 0.9908
9 Trading days
Similarly to irregular holidays, the weekly pattern differs from year to year. So the number of weekdays in a given month or quarter may be different from year to year. In the example below, January 2022 has 21 weekdays, while January 2023 has 22 weekdays. Some time series, such as manufacturing, may be affected by this pattern.
Similar to the holiday adjustments, X-13 offers various tools to deal with differences in the number of days or weekdays. This chapter discusses the automated trading day adjustments in X-13, and explores various ways of user customization. Note that the term trading day adjustment is X-13 slang for weekday adjustment and will be used interchangeably.
Trading day effects constitute a predictable movement in a time series, and therefore should not be part of the seasonally adjusted series. Trading day effects are usually removed by the regression spec, although some adjustments can also be done by the transform spec. Contrary to the regression spec, the transform spec performs a 1:1 adjustment, while the regression spec estimates the size of the effect from the data.
As with most adjustments, X-13 offers a built-in automated adjustment that works well in most circumstances. We start the chapter by looking at the automated procedures. Sometimes, you want to rely on a user-defined specification of trading days, a topic covered in Section 9.1. Finally, in Section 9.3, we link the two sections by replicating the built-in regressors in R.
9.1 Built-in trading day adjustment
In a default run of seas()
, X-13 uses a familiar AICc test to decide between a number of potential trading day adjustments. From the various models, it uses the one with the lowest AICc as the best model. By default, the following models are evaluated:
td1coef
: A single coefficient trading day adjustmenttd
: A six-day coefficient trading day adjustment
The first model distinguishes between weekdays and weekends, while the second model treats every day as its own. The first model is appropriate for many economic time series, where variables behave differently during the week than during the weekend. In some series, there may be large differences between weekdays, or even between Saturday and Sunday. Retail sales, for example, usually peak towards the end of the week and are weak on Sunday.
Using the built-in trading day adjustment in X-13 is straightforward:
The procedure has opted for a one coefficient model. During weekdays, the number of air passengers was lower during the period of the example series, by about 0.3% (note we are looking at a logarithmic, multiplicative model).
We can manipulate the automated model. To enforce a six coefficient model, regression.variables
can be specified as "td"
(as opposed to "td1coef"
):
m_td <- seas(AirPassengers, regression.variables = "td")
summary(m_td)
#>
#> Call:
#> seas(x = AirPassengers, regression.variables = "td")
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> Mon -0.001527 0.003458 -0.442 0.6588
#> Tue -0.007677 0.003607 -2.129 0.0333 *
#> Wed -0.001125 0.003465 -0.325 0.7453
#> Thu -0.005350 0.003425 -1.562 0.1183
#> Fri 0.004676 0.003447 1.357 0.1749
#> Sat 0.003025 0.003568 0.848 0.3965
#> Easter[1] 0.017999 0.007246 2.484 0.0130 *
#> AO1951.May 0.109256 0.019651 5.560 2.70e-08 ***
#> MA-Seasonal-12 0.500775 0.077252 6.482 9.03e-11 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> SEATS adj. ARIMA: (0 1 0)(0 1 1) Obs.: 144 Transform: log
#> AICc: 949.5, BIC: 976.4 QS (no seasonality in final): 0
#> Box-Ljung (no autocorr.): 28.05 Shapiro (normality): 0.9902
Note that the resulting AICc is higher (949.5) than for the one coefficient model (947.3). That is why the automated procedure has opted for the simpler model.
9.2 Case Study: Hobby Toy Game
m <- seas(hobby_toy_game)
summary(m)
#>
#> Call:
#> seas(x = hobby_toy_game)
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> Mon -0.009625 0.003358 -2.866 0.004157 **
#> Tue 0.014245 0.003276 4.349 1.37e-05 ***
#> Wed -0.009167 0.003289 -2.787 0.005326 **
#> Thu -0.001631 0.003258 -0.501 0.616624
#> Fri 0.010977 0.003332 3.295 0.000984 ***
#> Sat 0.011069 0.003316 3.338 0.000843 ***
#> Easter[8] 0.061462 0.006426 9.564 < 2e-16 ***
#> AO1996.May -0.217402 0.025347 -8.577 < 2e-16 ***
#> AO1996.Dec 0.145309 0.025339 5.735 9.77e-09 ***
#> MA-Nonseasonal-01 0.377066 0.056405 6.685 2.31e-11 ***
#> MA-Nonseasonal-02 0.360585 0.056510 6.381 1.76e-10 ***
#> MA-Seasonal-12 0.326875 0.055259 5.915 3.31e-09 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> SEATS adj. ARIMA: (0 1 2)(0 1 1) Obs.: 282 Transform: log
#> AICc: 2826, BIC: 2871 QS (no seasonality in final): 0
#> Box-Ljung (no autocorr.): 27.25 Shapiro (normality): 0.9887 *
static(m)
#> seas(
#> x = hobby_toy_game,
#> regression.variables = c("td", "easter[8]", "ao1996.May", "ao1996.Dec"),
#> arima.model = "(0 1 2)(0 1 1)",
#> regression.aictest = NULL,
#> outlier = NULL,
#> transform.function = "log"
#> )
m0 <- seas(
x = hobby_toy_game,
regression.variables = c(
"td",
"easter[8]",
"ao1996.May",
"ao1996.Dec"
),
arima.model = "(0 1 2)(0 1 1)",
regression.aictest = NULL,
outlier = NULL,
transform.function = "log"
)
summary(m0)
#>
#> Call:
#> seas(x = hobby_toy_game, transform.function = "log", regression.aictest = NULL,
#> outlier = NULL, regression.variables = c("td", "easter[8]",
#> "ao1996.May", "ao1996.Dec"), arima.model = "(0 1 2)(0 1 1)")
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> Mon -0.009625 0.003358 -2.866 0.004157 **
#> Tue 0.014245 0.003276 4.349 1.37e-05 ***
#> Wed -0.009167 0.003290 -2.787 0.005327 **
#> Thu -0.001631 0.003258 -0.501 0.616637
#> Fri 0.010977 0.003332 3.295 0.000985 ***
#> Sat 0.011069 0.003316 3.338 0.000843 ***
#> Easter[8] 0.061462 0.006426 9.564 < 2e-16 ***
#> AO1996.May -0.217403 0.025348 -8.577 < 2e-16 ***
#> AO1996.Dec 0.145309 0.025339 5.735 9.78e-09 ***
#> MA-Nonseasonal-01 0.377086 0.056405 6.685 2.30e-11 ***
#> MA-Nonseasonal-02 0.360597 0.056509 6.381 1.76e-10 ***
#> MA-Seasonal-12 0.326884 0.055258 5.916 3.31e-09 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> SEATS adj. ARIMA: (0 1 2)(0 1 1) Obs.: 282 Transform: log
#> AICc: 2826, BIC: 2871 QS (no seasonality in final): 0
#> Box-Ljung (no autocorr.): 27.25 Shapiro (normality): 0.9887 *
m1 <- seas(
x = hobby_toy_game,
regression.variables = c(
"td1coef",
"easter[8]",
"ao1996.May",
"ao1996.Dec"
),
arima.model = "(0 1 2)(0 1 1)",
regression.aictest = NULL,
outlier = NULL,
transform.function = "log"
)
summary(m1)
#>
#> Call:
#> seas(x = hobby_toy_game, transform.function = "log", regression.aictest = NULL,
#> outlier = NULL, regression.variables = c("td1coef", "easter[8]",
#> "ao1996.May", "ao1996.Dec"), arima.model = "(0 1 2)(0 1 1)")
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> Weekday 0.0005289 0.0005944 0.890 0.374
#> Easter[8] 0.0542854 0.0074088 7.327 2.35e-13 ***
#> AO1996.May -0.2300586 0.0290640 -7.916 2.46e-15 ***
#> AO1996.Dec 0.1231689 0.0289604 4.253 2.11e-05 ***
#> MA-Nonseasonal-01 0.4421089 0.0569371 7.765 8.17e-15 ***
#> MA-Nonseasonal-02 0.3495768 0.0569694 6.136 8.45e-10 ***
#> MA-Seasonal-12 0.3571851 0.0551869 6.472 9.65e-11 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> SEATS adj. ARIMA: (0 1 2)(0 1 1) Obs.: 282 Transform: log
#> AICc: 2875, BIC: 2904 QS (no seasonality in final): 0
#> Box-Ljung (no autocorr.): 40.88 * Shapiro (normality): 0.9943
library(tsbox)
ts_dygraphs(ts_c(
final(m0),
final(m1)
))
9.3 Case Study: Replicate X-13 trading days adjustment
‘Trading day adjustment’ removes the effect of the weekdays, but does not include holidays, such as Christmas or Easter. These are handled separately (Easter) (Chapter 8) or dealt with standard seasonal adjustment (Christmas).
Sometimes, users may want to specify user-defined trading day regressors, to incorporate country specific trading day patterns. In Section 9.3, we replicate the built-in regressors. These regressors simply use the number of weekdays, and do not pay any reference to a country specific calendar. If you want to deviate from them, a good way is to start with the replicated values, and adjust from there.
9.3.1 Constructing weekday regressors
To construct weekday regressors in R, we use the following code:
library(dplyr)
dates <- seq(as.Date("1931-01-01"), as.Date("2030-12-31"), by = "day")
first_of_month <- function(x) {
as.Date(paste(
data.table::year(dates),
data.table::month(dates),
1,
sep = "-"
))
}
td_m_tbl <-
tibble(dates, wd = as.POSIXlt(dates)$wday, name = weekdays(dates)) |>
group_by(time = first_of_month(dates)) |>
summarize(
mon = sum(wd == 1) - sum(wd == 0),
tue = sum(wd == 2) - sum(wd == 0),
wed = sum(wd == 3) - sum(wd == 0),
thu = sum(wd == 4) - sum(wd == 0),
fri = sum(wd == 5) - sum(wd == 0),
sat = sum(wd == 6) - sum(wd == 0),
td1 = sum(wd %in% 1:5) - 5 / 2 * sum(wd %in% c(6, 0))
)
time | mon | tue | wed | thu | fri | sat | td1 |
---|---|---|---|---|---|---|---|
1931-01-01 | 0 | 0 | 0 | 1 | 1 | 1 | -0.5 |
1931-02-01 | 0 | 0 | 0 | 0 | 0 | 0 | 0.0 |
1931-03-01 | 0 | 0 | -1 | -1 | -1 | -1 | -0.5 |
1931-04-01 | 0 | 0 | 1 | 1 | 0 | 0 | 2.0 |
1931-05-01 | -1 | -1 | -1 | -1 | 0 | 0 | -4.0 |
1931-06-01 | 1 | 1 | 0 | 0 | 0 | 0 | 2.0 |
For each month, td_m_tbl
computes the number of specific weekdays, and compares them to the number of Sundays (wd == -
) (columns mon
to sat
). For example, in May 1931, the number of Mondays was 4 while the number of Sundays was 5. The column td1
compares the total number of weekdays with the total number of weekends. The normalization formula is the one used by X-13 and described in the X-13 manual.
To extract the regressors as ts
time series, we use the following code:
9.3.2 Single coefficient regressor
It is straightforward to replicate these values to the automated procedure in X-13:
m_td1coef <- seas(
AirPassengers,
xreg = td1nolpyear,
regression.aictest = NULL,
outlier = NULL,
regression.usertype = "td"
)
summary(m_td1coef)
#>
#> Call:
#> seas(x = AirPassengers, xreg = td1nolpyear, regression.aictest = NULL,
#> outlier = NULL, regression.usertype = "td")
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> xreg -0.0025474 0.0006732 -3.784 0.000154 ***
#> MA-Nonseasonal-01 0.3292278 0.0813633 4.046 5.20e-05 ***
#> MA-Seasonal-12 0.5695911 0.0739360 7.704 1.32e-14 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> SEATS adj. ARIMA: (0 1 1)(0 1 1) Obs.: 144 Transform: log
#> AICc: 976.6, BIC: 987.7 QS (no seasonality in final): 0
#> Box-Ljung (no autocorr.): 24.86 Shapiro (normality): 0.9805 *
m_td1coef_built_in <- seas(
AirPassengers,
regression.aictest = NULL,
outlier = NULL,
regression.variables = c("td1nolpyear", outlier = NULL)
)
summary(m_td1coef_built_in)
#>
#> Call:
#> seas(x = AirPassengers, regression.aictest = NULL, outlier = NULL,
#> regression.variables = c("td1nolpyear", outlier = NULL))
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> Weekday -0.0025474 0.0006732 -3.784 0.000154 ***
#> MA-Nonseasonal-01 0.3292278 0.0813633 4.046 5.20e-05 ***
#> MA-Seasonal-12 0.5695911 0.0739360 7.704 1.32e-14 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> SEATS adj. ARIMA: (0 1 1)(0 1 1) Obs.: 144 Transform: log
#> AICc: 976.6, BIC: 987.7 QS (no seasonality in final): 0
#> Box-Ljung (no autocorr.): 24.86 Shapiro (normality): 0.9805 *
9.3.3 One coefficient per day
Note that the baseline is Sunday.
m_td <- seas(
AirPassengers,
xreg = tdnolpyear,
regression.aictest = NULL,
outlier = NULL,
regression.usertype = "td"
)
summary(m_td)
#>
#> Call:
#> seas(x = AirPassengers, xreg = tdnolpyear, regression.aictest = NULL,
#> outlier = NULL, regression.usertype = "td")
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> xreg1 -0.004982 0.004731 -1.053 0.292359
#> xreg2 -0.004589 0.004762 -0.964 0.335172
#> xreg3 -0.001612 0.004745 -0.340 0.734094
#> xreg4 -0.003817 0.004680 -0.816 0.414652
#> xreg5 0.003958 0.004706 0.841 0.400272
#> xreg6 0.003164 0.004829 0.655 0.512342
#> MA-Nonseasonal-01 0.298942 0.082193 3.637 0.000276 ***
#> MA-Seasonal-12 0.579965 0.073855 7.853 4.07e-15 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> SEATS adj. ARIMA: (0 1 1)(0 1 1) Obs.: 144 Transform: log
#> AICc: 983.9, BIC: 1008 QS (no seasonality in final): 0
#> Box-Ljung (no autocorr.): 29.43 Shapiro (normality): 0.9781 *
m_td_built_in <- seas(
AirPassengers,
regression.aictest = NULL,
outlier = NULL,
regression.variables = c("tdnolpyear", outlier = NULL)
)
summary(m_td_built_in)
#>
#> Call:
#> seas(x = AirPassengers, regression.aictest = NULL, outlier = NULL,
#> regression.variables = c("tdnolpyear", outlier = NULL))
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> Mon -0.004982 0.004731 -1.053 0.292359
#> Tue -0.004589 0.004762 -0.964 0.335172
#> Wed -0.001612 0.004745 -0.340 0.734094
#> Thu -0.003817 0.004680 -0.816 0.414652
#> Fri 0.003958 0.004706 0.841 0.400272
#> Sat 0.003164 0.004829 0.655 0.512342
#> MA-Nonseasonal-01 0.298942 0.082193 3.637 0.000276 ***
#> MA-Seasonal-12 0.579965 0.073855 7.853 4.07e-15 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> SEATS adj. ARIMA: (0 1 1)(0 1 1) Obs.: 144 Transform: log
#> AICc: 983.9, BIC: 1008 QS (no seasonality in final): 0
#> Box-Ljung (no autocorr.): 29.43 Shapiro (normality): 0.9781 *
9.3.4 Is it worth building country specific regressors?
The built-in trading day adjustment in X-13 is exclusively based on the calendar to construct regressor variables, meaning it does not consider country-specific holidays like Thanksgiving in the US. These holidays are addressed separately, as discussed in the Chapter 8 section.
As previously noted, regular holidays, such as Christmas, typically don’t need special handling. When dealing with irregular holidays, such as Chinese New Year, employing holiday regressors is the preferred approach. Consequently, creating country-specific regressors is generally unnecessary.
9.4 Case Study: Azerbaijani Consumer price index for construction
td1coef
is significant. But does the series have a seasonal pattern?
library(cbar.sa)
#>
#> Attaching package: 'cbar.sa'
#> The following object is masked _by_ '.GlobalEnv':
#>
#> m1
#> The following object is masked from 'package:seasonal':
#>
#> cpi
m0 <- seas(
x = cpi_const,
regression.variables = c("const", "ls2009.1"),
arima.model = "(1 1 0)",
regression.aictest = NULL,
outlier = NULL,
transform.function = "none"
)
#> Model used in SEATS is different: (0 1 1)
summary(m0)
#>
#> Call:
#> seas(x = cpi_const, transform.function = "none", regression.aictest = NULL,
#> outlier = NULL, regression.variables = c("const", "ls2009.1"),
#> arima.model = "(1 1 0)")
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> Constant 0.7068 0.1674 4.224 2.40e-05 ***
#> LS2009.1 -3.5364 0.8613 -4.106 4.02e-05 ***
#> AR-Nonseasonal-01 0.3144 0.1229 2.558 0.0105 *
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> SEATS adj. ARIMA: (1 1 0) Obs.: 62 Transform: none
#> AICc: 169, BIC: 176.8 QS (no seasonality in final):0.7392
#> Box-Ljung (no autocorr.): 11.86 Shapiro (normality): 0.9754
#> Messages generated by X-13:
#> Notes:
#> - Model used for SEATS decomposition is different from the
#> model estimated in the regARIMA modeling module of
#> X-13ARIMA-SEATS.
m1 <- seas(
x = cpi_const,
regression.variables = c("const", "td1coef", "ls2009.1"),
arima.model = "(1 1 0)",
regression.aictest = NULL,
outlier = NULL,
transform.function = "none"
)
#> Model used in SEATS is different: (0 1 1)
summary(m1)
#>
#> Call:
#> seas(x = cpi_const, transform.function = "none", regression.aictest = NULL,
#> outlier = NULL, regression.variables = c("const", "td1coef",
#> "ls2009.1"), arima.model = "(1 1 0)")
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> Constant 0.7050 0.1736 4.062 4.86e-05 ***
#> Weekday 0.1262 0.0436 2.896 0.00379 **
#> Leap Year -0.5241 0.2943 -1.781 0.07491 .
#> LS2009.1 -3.3119 0.7863 -4.212 2.53e-05 ***
#> AR-Nonseasonal-01 0.3927 0.1210 3.245 0.00117 **
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> SEATS adj. ARIMA: (1 1 0) Obs.: 62 Transform: none
#> AICc: 164, BIC: 175.1 QS (no seasonality in final):0.423
#> Box-Ljung (no autocorr.): 12.95 Shapiro (normality): 0.9701
#> Messages generated by X-13:
#> Notes:
#> - Model used for SEATS decomposition is different from the
#> model estimated in the regARIMA modeling module of
#> X-13ARIMA-SEATS.
9.5 Exercises
- What is the difference between
td1coef
andtd
regressor? When would fit better than the other? - Run
seas()
to on AirPassengers usingtd1coef
andtd
regressors and compare the results. - Consult X-13ARIMA-SEATS Reference Manual. What other options are available as built-in regressors to model trading days?
- Would it make sense to model both the holiday and trading days effect at the same time?
- Explain the difference among the following arguments:
xreg
,regression.variables
, andregression.usertype
. Are these equivalent? - Use the
trade_tur
series. Try modeling the holiday and trading days effect simultaneously and compare the results with the individual solutions. Do you get better results?