---
title: "Replication Materials for hdMTD"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Replication Materials for hdMTD}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r setup, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>"
)
library("hdMTD")
library("dplyr")
library("ggplot2")
library("lubridate")
library("purrr")
library("tidyr")
replication_dir <- system.file(
"extdata",
"replication",
package = "hdMTD"
)
# Allows the vignette to be knitted directly from the package source.
if (!nzchar(replication_dir)) {
possible_dirs <- c(
file.path("inst", "extdata", "replication"),
file.path("..", "inst", "extdata", "replication")
)
replication_dir <- possible_dirs[dir.exists(possible_dirs)][1]
}
if (is.na(replication_dir) || !dir.exists(replication_dir)) {
stop("Pre-computed replication files were not found.")
}
precomputed <- readRDS(
file.path(replication_dir, "hdMTD_outputs.rds")
)
simulated_data <- readRDS(
file.path(replication_dir, "simulated_data.rds")
)
results_sequential <- readRDS(
file.path(replication_dir, "results_sequential_selection.rds")
)
```
This vignette provides replication materials for the examples and analyses presented in Section 5 of the article describing the `hdMTD` package, available as a preprint on arXiv at [https://arxiv.org/abs/2509.01808](https://arxiv.org/abs/2509.01808). Computations that take more than approximately two minutes are shown but not evaluated when the vignette is built. Their outputs are loaded from pre-computed files distributed with the package.
## Section 5: Using hdMTD
### 5.1 Data generation
1. Generate MTD model:
Parameters: $\Lambda = \{-30,-15,-1\}$, $\mathcal{A} = \{0,1\}$,
$\lambda_0= \{0.01\}$, $\lambda_{-1} = 0.39$, $\lambda_{-15} = \lambda_{-30} = 0.3$,
$p_0(0)=p_0(1)=0.5$, and transition matrices $p_j$, $j\in\Lambda$, sampled uniformly.
```{r binary-model}
set.seed(11)
Lambda <- c(1, 15, 30)
A <- c(0, 1)
lam0 <- 0.01
lamj <- c(0.39, 0.3, 0.3)
p0 <- c(0.5, 0.5)
MTD <- MTDmodel(Lambda = Lambda, A = A, lam0 = lam0, lamj = lamj, p0 = p0)
summary(MTD)
```
2. Sample from the invariant distribution
```{r binary-sample}
X <- perfectSample(MTD, N = 1000)
```
### 5.2 Estimation
3. Estimate relevant lags using FS method
```{r fs-specific}
hdMTD_FS(X, d = 40, l = 4)
```
```{r fs-general}
FS <- hdMTD(X, d = 40, method = "FS", l = 4)
S(FS); summary(FS)
```
4. Estimate relevant lags using BIC method
The unrestricted BIC computation below takes approximately 30 minutes and is therefore displayed without being evaluated. The stored result is printed immediately afterwards.
```{r bic-unrestricted, eval=FALSE}
BIC_d40_l4 <- hdMTD_BIC(X, d = 40, minl = 4, maxl = 4)
BIC_d40_l4
```
```{r bic-unrestricted-output, echo=FALSE}
BIC_d40_l4 <- precomputed$BIC_d40_l4
print(BIC_d40_l4)
```
Custom subset S
```{r bic-custom-set}
hdMTD_BIC(X, d = 40,
S = c(1, 5, 10, 15, 17, 20, 27, 30, 35, 40),
minl = 4, maxl = 4)
```
Varying number of lags to be selected
```{r bic-variable-size}
hdMTD_BIC(X, d = 40,
S = c(1, 5, 10, 15, 17, 20, 27, 30, 35, 40),
minl = 1, maxl = 4)
```
With BIC values by number of lags
```{r bic-by-size}
hdMTD_BIC(X, d = 40,
S = c(1, 5, 10, 15, 17, 20, 27, 30, 35, 40),
minl = 1, maxl = 4,
byl = TRUE, BICvalue = TRUE)
```
Setting $\xi=0.4$
```{r bic-xi}
hdMTD_BIC(X, d = 40,
S = c(1, 5, 10, 15, 17, 20, 27, 30, 35, 40),
minl = 1, maxl = 4,
byl = TRUE, BICvalue = TRUE,
xi = 0.4)
```
All matrices $p_j$ are equal and $\lambda_0=0$
```{r bic-single-matrix}
hdMTD_BIC(X, d = 40,
S = c(1, 5, 10, 15, 17, 20, 27, 30, 35, 40),
minl = 1, maxl = 4,
byl = TRUE, BICvalue = TRUE,
single_matrix = TRUE, indep_part = FALSE)
```
5. Estimate relevant lags using CUT method
The first two CUT computations below take more than two minutes and are therefore displayed without being evaluated. Their stored results are printed immediately afterwards.
```{r cut-full-candidate-set, eval=FALSE}
CUT_d40 <- hdMTD_CUT(
X,
d = 40,
S = c(1, 5, 10, 15, 17, 20, 27, 30, 35, 40)
)
CUT_d40
```
```{r cut-full-candidate-set-output, echo=FALSE}
CUT_d40 <- precomputed$CUT_d40
print(CUT_d40)
```
Setting $\alpha = 0.13$
```{r cut-alpha, eval=FALSE}
CUT_d40_alpha <- hdMTD_CUT(
X,
d = 40,
S = c(1, 5, 10, 15, 17, 20, 27, 30, 35, 40),
alpha = 0.13
)
CUT_d40_alpha
```
```{r cut-alpha-output, echo=FALSE}
CUT_d40_alpha <- precomputed$CUT_d40_alpha
print(CUT_d40_alpha)
```
Custom subset S
```{r cut-custom-set}
hdMTD_CUT(X, d = 40, S = c(1, 5, 17, 27, 30, 35), alpha = 0.13)
```
6. Estimate relevant lags using FSC method
```{r fsc}
hdMTD_FSC(X, d = 40, l = 4, alpha = 0.1, cut_fraction = 0.5)
```
FS method with halved sample
```{r fs-half-sample}
hdMTD_FS(X[501:1000], d = 40, l = 4)
```
7. Estimating transition probabilities
```{r empirical-probabilities-data-frame}
head(empirical_probs(X, S = c(1, 15, 30)), 6)
```
```{r empirical-probabilities-matrix}
empirical_probs(X, S = c(1, 15, 30), matrixform = TRUE)
```
8. Oscillations
Computing from MTD
```{r true-oscillations}
oscillation(MTD)
```
Estimating from sample
```{r estimated-oscillations}
oscillation(X, S = c(1, 15, 30))
```
9. Estimating MTD parameters through the EM algorithm
Initial parameters for EM method
```{r em-initial-values}
init <- list(
'lambdas'= c(0.01, 0.33, 0.33, 0.33),
'p0' = c(0.5, 0.5),
'pj' = rep(list(matrix(c(0.5, 0.5, 0.5, 0.5), ncol = 2, nrow = 2)), 3)
)
```
Run EM
```{r em-fit}
emMTD <- MTDest(X, S = c(1, 15, 30), init = init, iter = TRUE)
summary(emMTD)
```
Stops after $9$ iterations
```{r em-fixed-iterations}
emMTD <- MTDest(X, S = c(1, 15, 30), M = NULL, nIter = 9, init = init, oscillations = TRUE)
summary(emMTD)
```
Compute estimated global transition matrix
```{r unnamed-chunk-26}
transitP(emMTD)
```
10. A three-state MTD example
All examples presented so far have used the binary state space $\mathcal{A}=\{0,1\}$. We now illustrate the use of the package with a three-state MTD model.
```{r three-state-model}
set.seed(11)
Lambda3 <- c(2, 5)
A3 <- c(1, 2, 3)
lam0.3 <- 0.05
lamj.3 <- c(0.20, 0.75)
p0.3 <- c(0.30, 0.30, 0.40)
MTD3 <- MTDmodel(
Lambda = Lambda3,
A = A3,
lam0 = lam0.3,
lamj = lamj.3,
p0 = p0.3
)
summary(MTD3)
```
```{r three-state-sample}
Y <- perfectSample(MTD3, N = 2000)
oscillation(MTD3)
```
```{r three-state-fs}
Sfs3 <- hdMTD_FS(Y, d = 20, l = 3)
Sfs3
```
```{r three-state-bic}
Sbic3 <- hdMTD_BIC(Y, S = Sfs3, d = 20)
Sbic3
```
```{r three-state-probabilities}
P3 <- empirical_probs(Y, S = Sbic3, matrixform = TRUE)
P3
```
### 5.3 Testing hdMTD
1. MTD model specification:
```{r simulation-model}
set.seed(123)
Lambda <- c(1, 5)
A <- c(0, 1)
lam0 <- 0.01
p0 <- c(0.5, 0.5)
MTD <- MTDmodel(Lambda, A, lam0, p0 = p0) # Generates an MTD model
```
2. Simulation settings and results
Simulation parameters:
```{r simulation-settings}
n <- 100 # Number of replications
N <- 10000 # Full sample size
m <- c(1000, 1500, 2000, 2500, 3000, 5000, 10000) # Subsample sizes
d <- 100 # Max order for FS and Oracle
dNaive <- 5 # Max order for Naive
pairList <- t(combn(d, 2)) # All possible pairs with digits from 1 to 100
npairs <- nrow(pairList)
minP11_P12 <- min(MTD$P[1, 1], MTD$P[1, 2])
```
The following code reproduces the complete simulation study. It is not evaluated when the vignette is built because the computation may take approximately one day, depending on the number of workers.
```{r simulation-recompute, eval=FALSE}
FS <- matrix(0, ncol = length(m), nrow = n)
Naive <- matrix(0, ncol = length(m), nrow = n)
Oracle <- matrix(0, ncol = length(m), nrow = n)
SFS <- matrix(0, ncol = length(m) * 2, nrow = n)
ZOracle <- matrix(0, ncol = length(m) * 2, nrow = n)
X_list <- vector("list", n)
for (i in seq_len(n)) {
X_list[[i]] <- perfectSample(MTD, N = N) # Generates n samples of size N
}
# Parallel
# Limit internal BLAS/OpenMP threads to avoid oversubscription across worker
Sys.setenv(OMP_NUM_THREADS = "1",
MKL_NUM_THREADS = "1",
OPENBLAS_NUM_THREADS = "1",
BLIS_NUM_THREADS = "1")
# Detect cores and choose a safe number of workers
get_smart_workers <- function() {
physical_cores <- parallel::detectCores(logical = FALSE)
if (physical_cores >= 10) {
return(6)
} else if (physical_cores >= 6) {
return(4)
} else if (physical_cores >= 4) {
return(2)
} else {
return(1)
}
}
# Manually assign number of workers (n_workers) or leave NULL for automatic choice.
n_workers <- NULL
workers <- ifelse(is.null(n_workers), get_smart_workers(), n_workers)
message(sprintf("Using %d workers", workers))
future::plan(future::multisession, workers = workers)
op <- options(future.scheduling = 1)
# Parallelize over replications
one_rep <- function(X) {
FS_i <- Naive_i <- Oracle_i <- numeric(length(m))
SFS_row <- ZOracle_row <- integer(length(m) * 2)
for (k in seq_along(m)) {
Y <- X[seq_len(m[k])]
ct <- countsTab(Y, d = d)
# FS
S <- hdMTD_FS(Y, d = d, l = 2)
SFS_row[(k * 2 - 1):(k * 2)] <- S
p_FS <- freqTab(S = S, A = A, countsTab = ct)$qax_Sj[1]
FS_i[k] <- abs(p_FS - MTD$P[1, 1])
# Naive
ct_dNaive <- countsTab(Y, dNaive)
p_Naive <- freqTab(S = seq_len(dNaive), A = A, countsTab = ct_dNaive)$qax_Sj[1]
Naive_i[k] <- abs(p_Naive - MTD$P[1, 1])
# Oracle
p_pairs <- numeric(npairs)
for (s in seq_len(npairs)) {
p_pairs[s] <- freqTab(S = pairList[s, ], A = A, countsTab = ct)$qax_Sj[1]
}
minpos <- which.min(abs(p_pairs - MTD$P[1, 1]))
ZOracle_row[(k * 2 - 1):(k * 2)] <- pairList[minpos, ]
p_Oracle <- p_pairs[minpos]
Oracle_i[k] <- abs(p_Oracle - MTD$P[1, 1])
}
list(FS = FS_i, Naive = Naive_i, Oracle = Oracle_i,
SFS = SFS_row, ZOracle = ZOracle_row)
}
indices_to_run <- seq_len(n)
chunks <- split(indices_to_run, ceiling(seq_along(indices_to_run) / workers))
# Note that, given X_list the following loop is deterministic
for (bi in seq_along(chunks)) { #takes ~ceiling(100/workers)*90 minutes to run)
rows <- chunks[[bi]]
res_block <- future.apply::future_lapply(X_list[rows], one_rep, future.seed = TRUE)
#Fill block lines
FS[rows, ] <- do.call(rbind, lapply(res_block, `[[`, "FS"))
Naive[rows, ] <- do.call(rbind, lapply(res_block, `[[`, "Naive"))
Oracle[rows, ] <- do.call(rbind, lapply(res_block, `[[`, "Oracle"))
SFS[rows, ] <- do.call(rbind, lapply(res_block, `[[`, "SFS"))
ZOracle[rows, ] <- do.call(rbind, lapply(res_block, `[[`, "ZOracle"))
}
future::plan(future::sequential)
options(op)
FSP <- FS/minP11_P12
OracleP <- Oracle/minP11_P12
NaiveP <- Naive/minP11_P12
# Store all simulated data in a list
simulated_data <- list(
FS = FS, FSP = FSP,
Oracle = Oracle, OracleP = OracleP,
Naive = Naive, NaiveP = NaiveP,
SFS = SFS, ZOracle = ZOracle
)
```
```{r simulation-load, echo=FALSE}
FS <- simulated_data$FS
FSP <- simulated_data$FSP
Oracle <- simulated_data$Oracle
OracleP <- simulated_data$OracleP
Naive <- simulated_data$Naive
NaiveP <- simulated_data$NaiveP
SFS <- simulated_data$SFS
ZOracle <- simulated_data$ZOracle
```
3. Generate Table 1
```{r unnamed-chunk-31}
means_table <- rbind(
round(apply(FS, 2, mean), 5),
round(apply(Oracle, 2, mean), 5),
round(apply(Naive, 2, mean), 5),
round(apply(FSP, 2, mean), 5),
round(apply(OracleP, 2, mean), 5),
round(apply(NaiveP, 2, mean), 5)
)
rownames(means_table) <- c(
"Delta_FS(m)",
"Delta_Oracle(m)",
"Delta_Naive,5(m)",
"std Delta_FS(m)",
"std Delta_Oracle(m)",
"std Delta_Naive,5(m)"
)
colnames(means_table) <- m
```
Table 1: Mean error of estimators
```{r mean-error-table, results='asis', message=FALSE, warning=FALSE}
suppressWarnings(
tbl_1 <- knitr::kable(
means_table,
caption = "Mean error of estimators",
format = "markdown",
booktabs = TRUE,
escape = FALSE
)
)
suppressWarnings(print(tbl_1))
```
4. Compute how often the FS output differs from Oracle by subsample size
```{r unnamed-chunk-33}
m_index <- seq(1, length(m)*2, by = 2)
names(m_index) <- as.character(m)
SFS_vs_ZOracle_diff <- sapply(m_index, function(idx) {
SFS_set <- apply(SFS[, c(idx, idx + 1)], 1, function(x) paste(sort(x), collapse = "-"))
ZOracle_set <- apply(ZOracle[, c(idx, idx + 1)], 1, function(x) paste(sort(x), collapse = "-"))
sum(SFS_set != ZOracle_set)
})
SFS_vs_ZOracle_diff
```
5. Generate Figure 1:
Data arrangement
FS
```{r unnamed-chunk-34}
tab <- FS
FStab <- rbind(apply(tab, 2, summary),'sd'=apply(tab, 2, sd))
FStab <- rbind(FStab,'sdLo'=FStab[4,]-FStab[7,],'sdUp'=FStab[4,]+FStab[7,])
Fmean <- FStab[4,]
FsdUp <- FStab[9,]
FsdLo <- FStab[8,]
Fq1 <- FStab[2,]
Fq2 <- FStab[3,]
Fq3 <- FStab[5,]
```
NAIVE
```{r unnamed-chunk-35}
tab <- Naive
Naivetab <- rbind(apply(tab, 2, summary),'sd'=apply(tab, 2, sd))
Naivetab <- rbind(Naivetab,'sdLo'=Naivetab[4,]-Naivetab[7,],'sdUp'=Naivetab[4,]+Naivetab[7,])
Nmean <- Naivetab[4,]
NsdUp <- Naivetab[9,]
NsdLo <- Naivetab[8,]
Nq1 <- Naivetab[2,]
Nq2 <- Naivetab[3,]
Nq3 <- Naivetab[5,]
```
ORACLE
```{r unnamed-chunk-36}
tab <- Oracle
Oracletab <- rbind(apply(tab, 2, summary),'sd'=apply(tab, 2, sd))
Oracletab <- rbind(Oracletab,'sdLo'=Oracletab[4,]-Oracletab[7,],'sdUp'=Oracletab[4,]+Oracletab[7,])
Omean <- Oracletab[4,]
OsdUp <- Oracletab[9,]
OsdLo <- Oracletab[8,]
Oq1 <- Oracletab[2,]
Oq2 <- Oracletab[3,]
Oq3 <- Oracletab[5,]
```
### Plot Figure 1: Estimators mean error across $N_{rep}=100$ replications.
```{r fig1-plot, fig.width=10, fig.height=6, message=FALSE, warning=FALSE}
par(mfrow = c(1,2), oma = c(0,0,0,0))
## --- Left panel: Mean error with standard deviation bands ---
par(mar = c(5,5,3,4), xpd = NA)
plot(m/100, Fmean, type = "l", col = "#377EB8",
xlab = "m (x100)", ylab = "Mean error", ylim = c(0, 0.12), lwd = 3,
frame.plot = FALSE, xaxt = "n", yaxt = "n", xlim = c(10,100),
cex.axis = 1.4, cex.lab = 1.6)
lines(m/100, Omean, col = "#E41A1C", lwd = 3)
lines(m/100, Nmean, col = "#4DAF4A", lwd = 3)
points(m/100, Fmean, col = "#377EB8", pch = 19, cex = 0.7)
points(m/100, Omean, col = "#E41A1C", pch = 19, cex = 0.7)
points(m/100, Nmean, col = "#4DAF4A", pch = 19, cex = 0.7)
lines(m/100, FsdUp, col = "#377EB8", lty = 2)
lines(m/100, FsdLo, col = "#377EB8", lty = 2)
lines(m/100, OsdUp, col = "#E41A1C", lty = 2)
lines(m/100, OsdLo, col = "#E41A1C", lty = 2)
cap <- 0.12
NsdUp_cut <- ifelse(NsdUp > cap, NA, NsdUp)
lines(m/100, NsdUp_cut, col = "#4DAF4A", lty = 2)
lines(m/100, NsdLo, col = "#4DAF4A", lty = 2)
axis(side = 1, at = m/100, labels = m/100, cex.axis = 1.4)
axis(side = 2, cex.axis = 1.4)
legend(
"topright",
inset = c(0.05, 0),
legend = c(expression(bar(Delta) ~ "FS"),
expression(bar(Delta) ~ "FS" %+-% "sd"),
expression(bar(Delta) ~ "Oracle"),
expression(bar(Delta) ~ "Oracle" %+-% "sd"),
expression(bar(Delta) ~ "Naive"),
expression(bar(Delta) ~ "Naive" %+-% "sd")),
col = c("#377EB8","#377EB8","#E41A1C","#E41A1C","#4DAF4A","#4DAF4A"),
lty = c(1,2,1,2,1,2),
lwd = c(3,1.5,3,1.5,3,1.5), bty = "n",
y.intersp = 0.95, x.intersp = 0.5, seg.len = 3, cex = 1.4
)
## --- Right panel: Median and quartiles ---
par(mar = c(5,5,3,4), xpd = NA)
plot(m/100, Fq2, type = "l", col = "#377EB8",
xlab = "m (x100)", ylab = "Quartiles of mean error",
ylim = c(0, 0.12), lwd = 3, frame.plot = FALSE, xaxt = "n", yaxt = "n",
xlim = c(10,100), cex.axis = 1.4, cex.lab = 1.6)
lines(m/100, Oq2, col = "#E41A1C", lwd = 3)
lines(m/100, Nq2, col = "#4DAF4A", lwd = 3)
points(m/100, Fq2, col = "#377EB8", pch = 19, cex = 0.7)
points(m/100, Oq2, col = "#E41A1C", pch = 19, cex = 0.7)
points(m/100, Nq2, col = "#4DAF4A", pch = 19, cex = 0.7)
lines(m/100, Fq1, col = "#377EB8", lty = 2)
lines(m/100, Fq3, col = "#377EB8", lty = 2)
lines(m/100, Oq1, col = "#E41A1C", lty = 2)
lines(m/100, Oq3, col = "#E41A1C", lty = 2)
lines(m/100, Nq1, col = "#4DAF4A", lty = 2)
lines(m/100, Nq3, col = "#4DAF4A", lty = 2)
axis(side = 1, at = m/100, labels = m/100, cex.axis = 1.4)
axis(side = 2, cex.axis = 1.4)
legend(
"topright",
inset = c(0.09, 0),
legend = c(expression("Med " ~ bar(Delta) ~ "FS"),
expression("q1,q3" ~ bar(Delta) ~ "FS"),
expression("Med " ~ bar(Delta) ~ "Oracle"),
expression("q1,q3" ~ bar(Delta) ~ "Oracle"),
expression("Med " ~ bar(Delta) ~ "Naive"),
expression("q1,q3" ~ bar(Delta) ~ "Naive")),
col = c("#377EB8","#377EB8","#E41A1C","#E41A1C","#4DAF4A","#4DAF4A"),
lty = c(1,2,1,2,1,2),
lwd = c(3,1.5,3,1.5,3,1.5), bty = "n",
y.intersp = 0.95, x.intersp = 0.5, seg.len = 3, cex = 1.4
)
```
### 5.4 Analysis of Real-World Data
```{r unnamed-chunk-38}
data("tempdata")
```
1. Treat NA data:
Removing days before "2010-08-05"
```{r unnamed-chunk-39}
tempdata <- hdMTD::tempdata %>% filter(DATE >= "2010-08-05")
```
Identify remaining $155$ NA positions
```{r unnamed-chunk-40}
posNA <- which(is.na(tempdata$MAXTEMP))
```
Fill short sequences of NAs ( $≤6$ NAs) using nearest neighbors mean
```{r unnamed-chunk-41}
for (i in posNA) {
if(!is.na(tempdata$MAXTEMP[i - 1]) && !all(is.na(tempdata$MAXTEMP[(i + 1):(i + 6)]))) {
aux <- which(!is.na(tempdata$MAXTEMP[(i + 1):(i + 6)]))[1]
tempdata$MAXTEMP[i] <- mean(tempdata$MAXTEMP[i - 1],tempdata$MAXTEMP[i + aux])
}
}
```
Identify remaining $88$ NA positions
```{r unnamed-chunk-42}
posNA <- which(is.na(tempdata$MAXTEMP))
```
Fill remaining NA with the mean of the previous hour temperature, next hour
temperature, and same hour of previous day temperature.
```{r unnamed-chunk-43}
for (i in posNA) {
tempdata$MAXTEMP[i] <- mean(c(tempdata$MAXTEMP[i - 1], tempdata$MAXTEMP[i + 1],
tempdata$MAXTEMP[i - 24]),na.rm = TRUE)
}
```
2. Compute mean daily maximum temperatures:
```{r unnamed-chunk-44}
temp <- tempdata %>%
group_by(DATE) %>%
summarize(MAXTEMP = mean(MAXTEMP), .groups = 'drop')
head(temp, 4)
```
### Plot Figure 2: Time series with quarterly mean of daily maximum temperatures
```{r temp-plot, fig.width=9, fig.height=6, message=FALSE, warning=FALSE}
TRIM_DATA <- temp %>%
mutate(
Y_TRIMESTER = paste0(year(DATE), "-T", quarter(DATE))
) %>%
group_by(Y_TRIMESTER) %>%
summarise(
MEAN_TEMP = mean(MAXTEMP),
DATA_REF = min(DATE)
) %>%
ungroup() %>%
arrange(DATA_REF)
TRIM_DATA <- TRIM_DATA[-c(1, nrow(TRIM_DATA)),]
ggplot(TRIM_DATA, aes(x = DATA_REF, y = MEAN_TEMP)) +
geom_line(color = "steelblue", linewidth = 0.5) +
geom_point(color = "steelblue", size = 1.2) +
scale_x_date(
date_breaks = "1 year",
date_labels = "%Y",
minor_breaks = NULL
) +
theme_minimal() +
theme(
axis.text.x = element_text(angle = 45, hjust = 1, size = 12),
axis.text.y = element_text(size = 12),
panel.grid.major = element_line(color = "gray90"),
axis.title.x = element_text(size = 15),
axis.title.y = element_text(size = 15),
plot.title = element_text(hjust = 0.5, size = 17, face = "bold")
) +
labs(
title = "Quarterly mean of daily maximum temperatures across the years",
x = "Year",
y = "Mean Temperature (°C)"
)
```
3. Create categories of temperature:
```{r unnamed-chunk-46}
xn <- max(temp$MAXTEMP)
x1 <- min(temp$MAXTEMP)
maxAmp <- xn - x1
temp$MAXTEMP1 <- ifelse(temp$MAXTEMP < x1 + maxAmp/2, 1, 2)
head(temp, 4)
prop.table(table(temp$MAXTEMP1)) # frequency of thermal regimes
```
4. Run FS for temp:
The lag-selection calls in this subsection take several minutes. Their code is displayed, while the vignette uses the corresponding pre-computed results.
```{r unnamed-chunk-47}
Temp12 <- rev(temp$MAXTEMP1)
```
hdMTD functions assume the sample is sorted from the latest observations to
oldest.
```{r temperature-fs-d400, eval=FALSE}
FS_Temp12_d400 <- hdMTD_FS(Temp12, d = 400, l = 3)
FS_Temp12_d400
```
```{r temperature-fs-d400-output, echo=FALSE}
FS_Temp12_d400 <- precomputed$FS_Temp12_d400
print(FS_Temp12_d400)
```
Note: The next code line is mentioned in the article but without a CodeChunk
Reduce maximum order to improve estimation
```{r temperature-fs-d364, eval=FALSE}
FS_Temp12_d364 <- hdMTD_FS(Temp12, d = 364, l = 3)
FS_Temp12_d364
```
```{r temperature-fs-d364-output, echo=FALSE}
FS_Temp12_d364 <- precomputed$FS_Temp12_d364
print(FS_Temp12_d364)
```
5. Split sample in Train and Test data:
```{r unnamed-chunk-52}
ndays <- nrow(temp %>%
filter(DATE >= "2023-09-01")) # 366 days in the latest year of the sample
Temp12_Train <- Temp12[-seq_len(ndays)] # Training data with 4775 days
Temp12_Test <- Temp12[seq_len(ndays)] # Test data
```
6. Rerun FS for Train data:
```{r temperature-train-fs, eval=FALSE}
FS_Temp12Train <- hdMTD_FS(Temp12_Train, d = 364, l = 3)
FS_Temp12Train
```
```{r temperature-train-fs-output, echo=FALSE}
FS_Temp12Train <- precomputed$FS_Temp12Train
print(FS_Temp12Train)
```
7. Trim out irrelevant lags:
With CUT method
```{r unnamed-chunk-55}
hdMTD_CUT(Temp12_Train, d = 364, S = c(1, 364, 6))
```
With BIC method
```{r unnamed-chunk-56}
hdMTD_BIC(Temp12_Train, d = 364, S = c(1, 364, 6), minl = 1, maxl = 3,
byl = TRUE, BICvalue = TRUE )
```
8. Estimated transition matrix for FS method output:
```{r unnamed-chunk-59}
P_FS <- empirical_probs(Temp12_Train, S = c(1, 6, 364), matrixform = TRUE)
P_FS
```
### Classic method for choosing relevant lag set:
9. Compute models:
```{r unnamed-chunk-60}
ct <- countsTab(Temp12_Train, d = 6) # Table with size 6 sequence counts
head(ct,4)
```
MC1 (does not explicitly appear in the article)
```{r unnamed-chunk-61}
ft <- freqTab(S = 1, A = c(1, 2), countsTab = ct)
LL <- sum(log(ft$qax_Sj) * ft$Nxa_Sj)
freeParam <- 2 * 1
BICMC1 <- -LL + 0.5 * log(length(Temp12_Train)) * freeParam
BICMC1
```
Comparable BIC if the model is a Markov chain of order $1$: $1869.162$
MC2 (does not explicitly appear in the article)
```{r unnamed-chunk-62}
ft <- freqTab(S = c(1, 2), A = c(1, 2), countsTab = ct)
LL <- sum(log(ft$qax_Sj) * ft$Nxa_Sj)
freeParam <- 2^2 * 1
BICMC2 <- -LL + 0.5 * log(length(Temp12_Train)) * freeParam
BICMC2
```
Comparable BIC if the model is a Markov chain of order $2$: $1850.598$
MC3. Used as example in the article
```{r unnamed-chunk-63}
ft <- freqTab(S = c(1, 2, 3), A = c(1, 2), countsTab = ct)
head(ft, 4)
LL <- sum(log(ft$qax_Sj) * ft$Nxa_Sj)
freeParam <- 2^3 * 1
BICMC3 <- -LL + 0.5 * log(length(Temp12_Train)) * freeParam
BICMC3
```
Comparable BIC if the model is a Markov chain of order $3$: $1854.029$
MC4 (does not explicitly appear in the article)
```{r unnamed-chunk-64}
ft <- freqTab(S = c(1, 2, 3, 4), A = c(1, 2), countsTab = ct)
LL <- sum(log(ft$qax_Sj) * ft$Nxa_Sj)
freeParam <- 2^4 * 1
BICMC4 <- -LL + 0.5 * log(length(Temp12_Train)) * freeParam
BICMC4
```
Comparable BIC if the model is a Markov chain of order $4$: $1877.888$
MC5 (does not explicitly appear in the article)
```{r unnamed-chunk-65}
ft <- freqTab(S = c(1, 2, 3, 4, 5), A = c(1, 2), countsTab = ct)
pos <- which(ft$Nxa_Sj > 0)
LL <- sum(log(ft$qax_Sj[pos]) * ft$Nxa_Sj[pos])
freeParam <- 2^5 * 1
BICMC5 <- -LL + 0.5 * log(length(Temp12_Train)) * freeParam
BICMC5
```
Comparable BIC if the model is a Markov chain of order $5$: $1925.962$
MC6 (does not explicitly appear in the article)
```{r unnamed-chunk-66}
ft <- freqTab(S = c(1, 2, 3, 4, 5, 6), A = c(1, 2), countsTab = ct)
pos <- which(ft$Nxa_Sj > 0)
LL <- sum(log(ft$qax_Sj[pos]) * ft$Nxa_Sj[pos])
freeParam <- 2^6 * 1
BICMC6 <- -LL + 0.5 * log(length(Temp12_Train)) * freeParam
BICMC6
```
Comparable BIC if the model is a Markov chain of order $6$: $2031.679$
10. Comparing models:
```{r unnamed-chunk-67}
BIC_vals <- c(BICMC1, BICMC2, BICMC3, BICMC4, BICMC5, BICMC6)
model_names <- paste0("MC", 1:6)
minBIC_idx <- which.min(BIC_vals)
BIC_fmt <- sprintf("%.3f", BIC_vals)
BIC_fmt[minBIC_idx] <- paste0("**", BIC_fmt[minBIC_idx], "**")
minBIC_idx
```
The classic method chooses order $2$ ($S=\{-2,-1\}$).
Generating BIC values Table
```{r unnamed-chunk-68}
bic_matrix <- data.frame(matrix(BIC_fmt, nrow = 1))
colnames(bic_matrix) <- model_names
rownames(bic_matrix) <- "BIC"
```
Table 2: BIC values
```{r bic-table, results='asis', message=FALSE, warning=FALSE}
tbl_2 <- suppressWarnings(
knitr::kable(
bic_matrix,
caption = "BIC values computed for classical Markov chain models of different orders."
)
)
suppressWarnings(print(tbl_2))
```
Estimated matrix for $S=\{-2,-1\}$
```{r unnamed-chunk-70}
P_MC2 <- empirical_probs(Temp12_Train, S = c(1, 2), matrixform = TRUE)
P_MC2
```
Independent model distribution:
```{r unnamed-chunk-71}
P_Ind <- prop.table(table(Temp12_Train))
P_Ind
```
### Comparing methods
11. Computing values for Table 3.
```{r unnamed-chunk-72}
Days1 <- which(Temp12_Test == 1)
lenDays1 <- length(Days1)
lenDays1 / ndays # frequency of low temperature days
Temp12_Test <- c(Temp12_Test, Temp12_Train[seq_len(364)])
```
```{r unnamed-chunk-73}
set.seed(1)
hitInd <- numeric(1000)
hitMC2 <- numeric(1000)
hitFS <- numeric(1000)
T1Ind <- numeric(1000)
T1MC2 <- numeric(1000)
T1FS <- numeric(1000)
F1Ind <- numeric(1000)
F1MC2 <- numeric(1000)
F1FS <- numeric(1000)
pasts2 <- rownames(P_MC2)
pastsFS <- rownames(P_FS)
```
```{r unnamed-chunk-74}
for (j in seq_len(1000)){
u <- runif(ndays)
predInd <- numeric(ndays)
predMC2 <- numeric(ndays)
predFS <- numeric(ndays)
for (i in ndays:1) {
predInd[i] <- ifelse(u[i] <= P_Ind[1], 1, 2)
pastRow <- which(pasts2 == paste0(rev(Temp12_Test[c(i+1, i+2)]), collapse = ""))
predMC2[i] <- ifelse(u[i] <= P_MC2[pastRow, 1], 1, 2)
pastRow <- which(pastsFS == paste0(rev(Temp12_Test[c(i+1, i+6, i+364)]), collapse = ""))
predFS[i] <- ifelse(u[i] <= P_FS[pastRow, 1], 1, 2)
}
hitInd[j] <- sum(predInd == Temp12_Test[seq_len(ndays)])
hitMC2[j] <- sum(predMC2 == Temp12_Test[seq_len(ndays)])
hitFS[j] <- sum(predFS == Temp12_Test[seq_len(ndays)])
T1Ind[j] <- sum(predInd[Days1] == 1)
T1MC2[j] <- sum(predMC2[Days1] == 1)
T1FS[j] <- sum(predFS[Days1] == 1)
F1Ind[j] <- sum(predInd[-Days1] == 1)
F1MC2[j] <- sum(predMC2[-Days1] == 1)
F1FS[j] <- sum(predFS[-Days1] == 1)
}
```
### Accuracy
```{r unnamed-chunk-75}
AccInd <- mean(hitInd)/ndays
AccMC2 <- mean(hitMC2)/ndays
AccFS <- mean(hitFS)/ndays
AccInd; AccMC2; AccFS
```
### Precision
```{r unnamed-chunk-76}
PrecInd <- mean(T1Ind/(T1Ind + F1Ind))
PrecMC2 <- mean(T1MC2/(T1MC2 + F1MC2))
PrecFS <- mean(T1FS/(T1FS + F1FS))
PrecInd; PrecMC2; PrecFS
```
### Sensitivity (Recall)
```{r unnamed-chunk-77}
SensInd <- mean(T1Ind)/lenDays1
SensMC2 <- mean(T1MC2)/lenDays1
SensFS <- mean(T1FS)/lenDays1
SensInd; SensMC2; SensFS
```
### Specificity
```{r unnamed-chunk-78}
SpecInd <- 1 - mean(F1Ind)/(ndays - lenDays1)
SpecMC2 <- 1 - mean(F1MC2)/(ndays - lenDays1)
SpecFS <- 1 - mean(F1FS)/(ndays - lenDays1)
SpecInd; SpecMC2; SpecFS
```
### F1-Score
```{r unnamed-chunk-79}
F1ScoreInd <- 2 * (PrecInd * SensInd) / (PrecInd + SensInd)
F1ScoreMC2 <- 2 * (PrecMC2 * SensMC2) / (PrecMC2 + SensMC2)
F1ScoreFS <- 2 * (PrecFS * SensFS) / (PrecFS + SensFS)
F1ScoreInd; F1ScoreMC2; F1ScoreFS
metric <- c("Accuracy", "Precision", "Sensitivity (Recall)", "Specificity", "F1-Score")
formula <- c("(TP+TN)/(TP+TN+FP+FN)",
"TP/(TP+FP)",
"TP/(TP+FN)",
"TN/(TN+FP)",
"2(PPV*Recall)/(PPV+Recall)")
performance_table <- data.frame(
Metric = metric,
Formula = formula,
indc = round(c(AccInd, PrecInd, SensInd, SpecInd, F1ScoreInd) * 100, 2),
mc2c = round(c(AccMC2, PrecMC2, SensMC2, SpecMC2, F1ScoreMC2) * 100, 2),
fsc = round(c(AccFS, PrecFS, SensFS, SpecFS, F1ScoreFS) * 100, 2),
check.names = FALSE
)
names(performance_table) <- c("Metric", "Formula", "Ind (\\%)", "MC2 (\\%)", "FS (\\%)")
```
Table 3: Model performance metrics
```{r performance-table, results='asis', message=FALSE, warning=FALSE}
suppressWarnings(
tbl_3 <- knitr::kable(performance_table, align = "l",
caption = "Model performance metrics.")
)
suppressWarnings(print(tbl_3))
```
### Plot Figure 3: Exploratory analysis of accuracies
```{r accuracy-plot, fig.width=9, fig.height=6, message=FALSE, warning=FALSE}
accuracy_data <- data.frame(
MC2 = hitMC2 / ndays,
FS = hitFS / ndays
) %>%
pivot_longer(
everything(),
names_to = "Model",
values_to = "Accuracy"
)
ggplot(accuracy_data, aes(x = Model, y = Accuracy, fill = Model)) +
geom_boxplot() +
labs(
title = "Accuracy distribution (1000 replications)",
x = "Model",
y = "Accuracy"
) +
theme_minimal() +
scale_fill_brewer(palette = "Paired") +
theme(
plot.title = element_text(hjust = 0.5, size = 18, face = "bold"),
axis.title = element_text(size = 16),
axis.text = element_text(size = 14),
legend.title = element_text(size = 16),
legend.text = element_text(size = 14)
)
```
### Empirical $\nu$ Analysis
12. FS sequential selection based on $\hat{\nu}_{n,j,S}$ values:
```{r sequential-selection-function, eval=FALSE}
run_sequential_lag_selection <- function(Temp12_Train, d = 364) {
# Initialization
A <- sort(unique(Temp12_Train))
lenA <- length(A)
lenX <- length(Temp12_Train)
A_pairs <- matrix(A, ncol = 2) # All unique state pairs
ct <- countsTab(X = Temp12_Train, d = 364) # Sequence counts table
# Initialize storage
results <- list(
nuj1 = numeric(d),
nuj2 = numeric(d-1),
nuj3 = numeric(d-2),
selected_lags = numeric(3)
)
# Helper function for empirical distribution calculation
PI <- function(S, groupTab, x_S, lenX, d) {
if (length(S) > 0) {
filtr_S <- paste0("x", S)
groupTab <- groupTab %>%
dplyr::mutate(match = purrr::pmap_lgl(dplyr::pick(dplyr::all_of(filtr_S)),
~ all(c(...) == x_S))) %>%
dplyr::filter(match) %>%
dplyr::select(-match)
}
PI <- matrix(groupTab$Nx_Sj/(lenX - d),ncol = 1)
PI
}
# Sequential Selection Process
cat("=== Starting Sequential Lag Selection ===\n")
# Step 1: Initial selection (S = ∅)
cat("\n[Step 1] Selecting first lag (S = ∅)...\n")
S <- NULL
Sc <- sort(setdiff(seq_len(d), S), decreasing = TRUE) # Complement of S in 1:d
for (z in seq_along(Sc)) { # Runs across all available lags
j <- Sc[z]
# Frequency tables
b_Sja <- freqTab(S = S, j = j, A = A, countsTab = ct)
b_Sj <- b_Sja %>%
dplyr::group_by(dplyr::across(dplyr::all_of(paste0("x", j)))) %>%
dplyr::summarise(Nx_Sj = sum(Nxa_Sj), .groups = "drop")
# Compute νj
PIs <- PI(S = S, groupTab = b_Sj, x_S = S, lenX = lenX, d = 364)
dTVs <- dTV_sample(S = S, j = j, lenA = lenA, base = b_Sja,
A_pairs = A_pairs, x_S = S)
results$nuj1[z] <- prod(PIs) * dTVs
}
results$selected_lags[1] <- Sc[which.max(results$nuj1)]
cat(sprintf("Selected: j = %d (ν = %.4f)\n",
results$selected_lags[1], max(results$nuj1)))
# Step 2: Second selection (S = {1})
cat(sprintf("\n[Step 2] Selecting second lag (S = {%d})...\n",
results$selected_lags[1]))
S <- results$selected_lags[1]
Sc <- sort(setdiff(seq_len(d), S), decreasing = TRUE)
for (z in seq_along(Sc)) {
j <- Sc[z]
Sj <- sort(c(S, j), decreasing = TRUE)
# Frequency tables
b_Sja <- freqTab(S = S, j = j, A = A, countsTab = ct)
b_Sj <- b_Sja %>%
dplyr::group_by(dplyr::across(dplyr::all_of(paste0("x", Sj)))) %>%
dplyr::summarise(Nx_Sj = sum(Nxa_Sj), .groups = "drop")
b_S <- b_Sja %>%
dplyr::group_by(dplyr::across(dplyr::all_of(paste0("x", S)))) %>%
dplyr::summarise(Nx_Sj = sum(Nxa_Sj), .groups = "drop")
subx <- b_S[, -ncol(b_S)]
# Compute ν_j
for (t in which(b_S$Nx_Sj > 0)) {
PIs <- PI(S = S, groupTab = b_Sj, x_S = subx[t, ],
lenX = lenX, d = d)
dTVs <- dTV_sample(S = S, j = j, lenA = lenA, base = b_Sja,
A_pairs = A_pairs, x_S = subx[t, ])
PI_xS <- as.numeric(b_S[t, ncol(b_S)]/(lenX - d))
results$nuj2[z] <- results$nuj2[z] + prod(PIs) * dTVs/PI_xS
}
}
results$selected_lags[2] <- Sc[which.max(results$nuj2)]
cat(sprintf("Selected: j = %d (ν = %.4f)\n",
results$selected_lags[2], max(results$nuj2)))
# Step 3: Third selection (S = {1,364})
cat(sprintf("\n[Step 3] Selecting third lag (S = {%d,%d})...\n",
results$selected_lags[1], results$selected_lags[2]))
S <- c(S, results$selected_lags[2])
Sc <- sort(setdiff(seq_len(d), S), decreasing = TRUE)
for (z in seq_along(Sc)) {
j <- Sc[z]
Sj <- sort(c(S, j), decreasing = TRUE)
dec_S <- rev(S) # S in decreasing order
# Frequency tables
b_Sja <- freqTab(S = dec_S, j = j, A = A, countsTab = ct)
b_Sj <- b_Sja %>%
dplyr::group_by(dplyr::across(dplyr::all_of(paste0("x", Sj)))) %>%
dplyr::summarise(Nx_Sj = sum(Nxa_Sj), .groups = "drop")
b_S <- b_Sja %>%
dplyr::group_by(dplyr::across(dplyr::all_of(paste0("x", dec_S)))) %>%
dplyr::summarise(Nx_Sj = sum(Nxa_Sj), .groups = "drop")
subx <- b_S[, -ncol(b_S)]
# Compute ν_j
for (t in which(b_S$Nx_Sj > 0)) {
PIs <- PI(S = dec_S, groupTab = b_Sj, x_S = subx[t, ],
lenX = lenX, d = d)
dTVs <- dTV_sample(S = dec_S, j = j, lenA = lenA, base = b_Sja,
A_pairs = A_pairs, x_S = subx[t, ])
PI_xS <- as.numeric(b_S[t, ncol(b_S)]/(lenX - d))
results$nuj3[z] <- results$nuj3[z] + prod(PIs) * dTVs/PI_xS
}
}
results$selected_lags[3] <- Sc[which.max(results$nuj3)]
cat(sprintf("Selected: j = %d (ν = %.4f)\n",
results$selected_lags[3], max(results$nuj3)))
# Final Results
cat("\n=== Final Selection Results ===\n")
print(data.frame(Step = 1:3, Selected_Lag = results$selected_lags,
nu = c(max(results$nuj1), max(results$nuj2), max(results$nuj3))))
return(results)
}
```
```{r sequential-selection-call, eval=FALSE}
results <- run_sequential_lag_selection(Temp12_Train)
```
```{r sequential-selection-load, echo=FALSE}
results <- results_sequential
cat("\n=== Final Selection Results ===\n")
print(data.frame(
Step = 1:3,
Selected_Lag = results$selected_lags,
nu = c(max(results$nuj1), max(results$nuj2), max(results$nuj3))
))
```
### Plot Figure 4: FS sequential step analysis through $\hat{\nu}_{n,j,S}$.
```{r nu-plot, fig.width=11, fig.height=6, message=FALSE, warning=FALSE}
par(mfrow = c(1, 3), mar = c(5, 6, 4, 2), oma = c(0, 0, 4, 0))
palette <- c("#E41A1C", "#377EB8", "#4DAF4A")
with(results, {
# Graph 1
Sc <- 364:1
plot(1:364, rev(nuj1), type = "p", pch = 19, cex = 0.8, col = "gray70",
ylab = "", xlab = "Lag (-j)", cex.lab = 1.8, cex.axis = 1.3,
ylim = c(0,0.13), main = "", panel.first = grid())
title(main = expression(paste("S = ", Ø)), cex.main = 1.5, font.main = 1)
title(ylab = expression(widehat(nu)[n*","*j*","*S]/2), line = 3, cex.lab = 1.5)
points(Sc[which.max(nuj1)], nuj1[which.max(nuj1)], pch = 21, bg = palette[1], cex = 1.5, lwd = 1)
text(Sc[which.max(nuj1)], nuj1[which.max(nuj1)], labels = paste0(Sc[which.max(nuj1)]),
pos = 3, col = palette[1], font = 2, cex = 1.4)
# Graph 2
Sc <- 364:2
plot(2:364, rev(nuj2), type = "p", pch = 19, cex = 0.8, col = "gray70",
ylab = "", xlab = "Lag (-j)", cex.lab = 1.8, cex.axis = 1.3,
ylim = c(0,0.025), main = "", panel.first = grid())
title(main = "With S = {-1}", cex.main = 1.5, font.main = 1)
title(ylab = expression(widehat(nu)[n*","*j*","*S]/2), line = 3, cex.lab = 1.5)
points(Sc[which.max(nuj2)], nuj2[which.max(nuj2)], pch = 21, bg = palette[2], cex = 1.5, lwd = 1)
text(Sc[which.max(nuj2)]-5, nuj2[which.max(nuj2)], labels = paste0(Sc[which.max(nuj2)]),
pos = 3, col = palette[2], font = 2, cex = 1.4)
# Graph 3
Sc <- 363:2
plot(2:363, rev(nuj3), type = "p", pch = 19, cex = 0.8, col = "gray70",
ylab = "", xlab = "Lag (-j)", cex.lab = 1.8, cex.axis = 1.3,
ylim = c(0,0.025), main = "", panel.first = grid())
title(main = "With S = {-364, -1}", cex.main = 1.5, font.main = 1)
title(ylab = expression(widehat(nu)[n*","*j*","*S]/2), line = 3, cex.lab = 1.5)
points(Sc[which.max(nuj3)], nuj3[which.max(nuj3)], pch = 21, bg = palette[3], cex = 1.5, lwd = 1)
text(Sc[which.max(nuj3)], nuj3[which.max(nuj3)], labels = paste0(Sc[which.max(nuj3)]),
pos = 3, col = palette[3], font = 2, cex = 1.4)
mtext(expression(paste("Sequential lag selection based on ", widehat(nu)[n*","*j*","*S])),
outer = TRUE, cex = 1.6, font = 2, line = 1.4)
})
```
## Session info
```{r session-info}
sessionInfo()
```