## ----setup, include = FALSE---------------------------------------------------
# the models run when the tarball is built; R CMD check re-knits only the text
run_vignette <- !nzchar(Sys.getenv("_R_CHECK_PACKAGE_NAME_"))
if (!run_vignette) knitr::knit_hooks$set(evaluate.inline = function(code, envir) "")
knitr::opts_chunk$set(
	fig.retina = 1,
	dpi = 72,
	collapse = TRUE,
	comment = "#>",
	fig.width = 7,
	fig.height = 5,
	eval = run_vignette
)

## ----fit----------------------------------------------------------------------
library(lame)
set.seed(2026)

n_fc <- 30
T_fc <- 6
rho_true <- 0.7
beta_bar <- 0.5
beta_t_true <- numeric(T_fc)
beta_t_true[1] <- 0.9
for (t in 2:T_fc) {
	beta_t_true[t] <- beta_bar + rho_true * (beta_t_true[t - 1] - beta_bar) +
		rnorm(1, 0, 0.15)
}

Xdyad <- lapply(seq_len(T_fc), function(t) {
	x <- matrix(rnorm(n_fc * n_fc), n_fc, n_fc)
	array(
		x,
		dim = c(n_fc, n_fc, 1),
		dimnames = list(NULL, NULL, "trade")
	)
})
a_fc <- rnorm(n_fc, 0, 0.4)
b_fc <- rnorm(n_fc, 0, 0.4)
Y <- lapply(seq_len(T_fc), function(t) {
	eta <- -0.5 + beta_t_true[t] * Xdyad[[t]][, , 1] + outer(a_fc, b_fc, "+")
	Yt <- matrix(rbinom(n_fc * n_fc, 1, pnorm(eta)), n_fc, n_fc)
	diag(Yt) <- NA
	rownames(Yt) <- colnames(Yt) <- sprintf("a%02d", seq_len(n_fc))
	Yt
})
names(Y) <- paste0("t", seq_len(T_fc))

round(beta_t_true, 3) # simulated coefficient path
sapply(Y, function(y) round(mean(y, na.rm = TRUE), 3)) # per-period density

fit <- lame(
	Y,
	Xdyad = Xdyad,
	family = "binary",
	R = 0,
	dynamic_beta = "dyad", # the trade coefficient is AR(1)
	dynamic_beta_kind = "ar1", # mean-reverting; switch to "rw1" for drift
	nscan = 150,
	burn = 30,
	odens = 5,
	verbose = FALSE
)

# estimated path of the trade coefficient
coef_path <- coef(fit)
trade_row <- grep("^trade[._]dyad$|trade", rownames(coef_path), value = TRUE)[1]
round(coef_path[trade_row, ], 3)

## ----forecast-----------------------------------------------------------------
# forecasts draw the AR(1) state at random, so seed them
set.seed(1)
# 3-step-ahead forecast on the link scale
fc_link <- predict(fit, h = 3, type = "link")
length(fc_link) # 3, one matrix per future period
dim(fc_link[[1]]) # n x n (30 x 30 here)

# response scale: the inverse link is applied to each draw
fc_resp <- predict(fit, h = 3, type = "response")
# posterior-mean predicted probabilities
range(fc_resp[[1]], na.rm = TRUE)

# by_draw = TRUE returns the full [n, n, h, n_draws] array
fc_full <- predict(fit, h = 3, type = "response", by_draw = TRUE)
dim(fc_full) # n x n x 3 x n_draws

# lower, median and upper matrices per period (95% by default)
fc_ci <- predict(fit, h = 3, type = "response", interval = "credible")
str(fc_ci[[1]]) # lower, median and upper, each n x n

## ----forecast-plot, fig.width = 7, fig.height = 3.4, fig.alt="Left: heatmap of predicted tie probabilities among the 30 actors for the first forecast period, darker cells indicating higher predicted probability. Right: the across-dyad average predicted probability at horizons h = 1, 2, 3 with a shaded 95 percent credible band that widens with the horizon."----
library(ggplot2)
library(patchwork)

# left: forecast heatmap for the first future period
P1 <- fc_resp[[1]]
rn <- rownames(P1); if (is.null(rn)) rn <- sprintf("a%02d", seq_len(nrow(P1)))
cn <- colnames(P1); if (is.null(cn)) cn <- sprintf("a%02d", seq_len(ncol(P1)))
heat_df <- data.frame(
	sender = factor(rn[row(P1)], levels = rev(rn)),
	receiver = factor(cn[col(P1)], levels = cn),
	prob = as.vector(P1)
)
p_heat <- ggplot(heat_df, aes(receiver, sender, fill = prob)) +
	geom_tile() +
	scale_fill_distiller(palette = "Blues", direction = 1, limits = c(0, 1), name = "P(tie)") +
	labs(title = "Forecast for t7", x = "Receiver", y = "Sender") +
	theme_bw(base_size = 9) +
	theme(
		panel.border = element_blank(),
		axis.line = element_blank(),
		axis.text = element_blank(),
		axis.ticks = element_blank(),
		legend.position = "top",
		plot.title = element_text(hjust = 0),
		plot.subtitle = element_text(hjust = 0),
		plot.caption = element_text(hjust = 0)
	)

# right: across-dyad mean probability with the mean 95% band
horizon_df <- data.frame(
	period = factor(
		paste0("t", T_fc + seq_along(fc_ci)),
		levels = paste0("t", T_fc + seq_along(fc_ci))
	),
	median = sapply(fc_ci, function(m) mean(m$median, na.rm = TRUE)),
	lower = sapply(fc_ci, function(m) mean(m$lower, na.rm = TRUE)),
	upper = sapply(fc_ci, function(m) mean(m$upper, na.rm = TRUE))
)
p_horizon <- ggplot(horizon_df, aes(period, median, group = 1)) +
	geom_ribbon(aes(ymin = lower, ymax = upper), fill = "grey85") +
	geom_line() + geom_point(size = 2) +
	labs(
		title = "Average forecast by horizon",
		x = "Forecast period", y = "Mean P(tie)"
	) +
	theme_bw(base_size = 9) +
	theme(
		panel.border = element_blank(),
		axis.line = element_blank(),
		axis.ticks = element_blank(),
		legend.position = "top",
		plot.title = element_text(hjust = 0),
		plot.subtitle = element_text(hjust = 0),
		plot.caption = element_text(hjust = 0)
	)

p_heat + p_horizon

## ----counterfactual-----------------------------------------------------------
# seed the forecast draws
set.seed(1)
# baseline: hold trade at its last observed value
last_X <- Xdyad[[length(Xdyad)]]
X_future <- list(last_X, last_X, last_X)
fc_cf <- predict(fit, h = 3, type = "response", newdata = X_future)

# counterfactual: one more unit (one standard deviation) of trade for every dyad
X_future_up <- lapply(X_future, function(x) {
	x[, , 1] <- x[, , 1] + 1
	x
})
fc_up <- predict(fit, h = 3, type = "response", newdata = X_future_up)

# change in each dyad's predicted probability at h = 3
delta_h3 <- fc_up[[3]] - fc_cf[[3]]
summary(as.vector(delta_h3))

## ----autoplot, fig.width = 7, fig.height = 4, fig.alt="In-sample posterior coefficient paths from autoplot.lame: each panel shows the posterior median line and a 95 percent credible interval ribbon across the fitted training periods (t1-t6) for one regression coefficient. This plots the estimated in-sample path, not a forecast."----
library(ggplot2)
autoplot(fit, probs = c(0.025, 0.5, 0.975)) +
	labs(
		title = "Posterior coefficient paths",
		subtitle = "median + 95% interval",
		x = "Time period", y = "Coefficient value"
	)

## ----sparse-fail--------------------------------------------------------------
set.seed(2026)
n_sp <- 20
T_sp <- 4
beta_drift <- c(0.4, 0.9, 1.4, 1.9) # drifts upward without mean reversion
X_sp <- lapply(seq_len(T_sp), function(t) {
	x <- matrix(rnorm(n_sp * n_sp), n_sp, n_sp)
	array(
		x,
		dim = c(n_sp, n_sp, 1),
		dimnames = list(NULL, NULL, "sanction_risk")
	)
})
Y_sp <- lapply(seq_len(T_sp), function(t) {
	eta <- -2 + beta_drift[t] * X_sp[[t]][, , 1]
	Yt <- matrix(rbinom(n_sp * n_sp, 1, pnorm(eta)), n_sp, n_sp)
	diag(Yt) <- NA
	rownames(Yt) <- colnames(Yt) <- sprintf("s%02d", seq_len(n_sp))
	Yt
})
names(Y_sp) <- paste0("t", seq_len(T_sp))
sapply(Y_sp, function(y) round(mean(y, na.rm = TRUE), 3)) # sparse early on

fit_sparse <- lame(
	Y_sp,
	Xdyad = X_sp,
	family = "binary",
	R = 0,
	dynamic_beta = "dyad",
	dynamic_beta_kind = "ar1",
	nscan = 400,
	burn = 100,
	odens = 5,
	verbose = FALSE
)
round(quantile(fit_sparse$RHO_BETA, c(0.5, 0.975)), 3)

## ----sparse-fail-warn---------------------------------------------------------
set.seed(1)
fc_sparse <- predict(
	fit_sparse,
	h = 6,
	type = "response",
	interval = "credible"
)

## ----sparse-fail-width, fig.width = 6, fig.height = 3.2, fig.alt="Line plot comparing the across-dyad mean 95 percent forecast interval width at horizons one through six for the Step 1 fit and the sparse drifting-panel fit. These intervals are on the bounded probability scale."----
set.seed(1)
fc_healthy <- predict(
	fit,
	h = 6,
	type = "response",
	interval = "credible"
)
width_by_h <- function(fc) {
	sapply(fc, function(m) mean(m$upper - m$lower, na.rm = TRUE))
}
w_healthy <- width_by_h(fc_healthy)
w_sparse <- width_by_h(fc_sparse)
round(rbind(mean_reverting = w_healthy, near_unit_root = w_sparse), 3)

# share of high-|sanction_risk| dyads whose h = 3 interval spans nearly [0, 1]
x_last <- X_sp[[T_sp]][, , 1]
hi_x <- abs(x_last) >= quantile(abs(x_last), 0.9)
deg_h3 <- mean(
	fc_sparse[[3]]$lower[hi_x] < 0.05 &
		fc_sparse[[3]]$upper[hi_x] > 0.95,
	na.rm = TRUE
)
round(deg_h3, 2)

width_df <- data.frame(
	h = rep(seq_along(w_healthy), 2),
	fit = rep(
		c(
			"mean-reverting (Step 1 fit)",
			"near-unit-root (sparse fit)"
		),
		each = length(w_healthy)
	),
	width = c(w_healthy, w_sparse)
)
ggplot(width_df, aes(h, width, linetype = fit)) +
	geom_line() + geom_point(size = 1.8) +
	labs(
		title = "Forecast interval width by horizon",
		x = "Forecast horizon h", y = "Mean 95% interval width"
	) +
	theme_bw(base_size = 9) +
	theme(
		panel.border = element_blank(),
		axis.line = element_blank(),
		axis.ticks = element_blank(),
		legend.position = "top",
		plot.title = element_text(hjust = 0),
		plot.subtitle = element_text(hjust = 0),
		plot.caption = element_text(hjust = 0),
		legend.title = element_blank()
	)

## ----lfo----------------------------------------------------------------------
# refit before each of the last two periods and score that period
# seed the forecast draws inside lfo()
set.seed(1)
T_fit <- length(fit$YPM)
lfo_periods <- tail(seq_len(T_fit), max(1L, min(2L, T_fit - 2L)))
lfo_res <- lfo(
	fit,
	periods = lfo_periods,
	refit = TRUE,
	nscan = 100,
	burn = 25,
	odens = 5,
	verbose = FALSE
)
print(lfo_res)

## ----pit----------------------------------------------------------------------
# fit on the first five periods, hold out the sixth
fit_train <- lame(
	Y[1:5],
	Xdyad = Xdyad[1:5],
	family = "binary",
	R = 0,
	dynamic_beta = "dyad",
	nscan = 100,
	burn = 30,
	odens = 5,
	verbose = FALSE
)
# seed the randomised PIT
set.seed(1)
pit <- forecast_pit(fit_train, y_future = list(Y[[6]]))
print(pit)

## ----pit-plot, fig.height = 3.6, fig.alt="PIT calibration plot for the held-out period: the empirical distribution of probability-integral-transform values compared with the Uniform(0,1) reference. Bars or a curve tracking the reference indicate calibration; a U-shape signals an over-confident (too-narrow) forecast and a central hump signals an under-confident (too-wide) one."----
plot(pit)

## ----loo-compare, eval = FALSE------------------------------------------------
# # two candidates with independent residuals and saved log-likelihoods
# fit_static <- lame(
# 	Y,
# 	Xdyad = Xdyad,
# 	family = "binary",
# 	R = 0,
# 	dcor = FALSE,
# 	nscan = 5000,
# 	burn = 1000,
# 	odens = 25,
# 	save_log_lik = TRUE,
# 	verbose = FALSE
# )
# fit_dyn <- lame(
# 	Y,
# 	Xdyad = Xdyad,
# 	family = "binary",
# 	R = 0,
# 	dcor = FALSE,
# 	dynamic_beta = "dyad",
# 	nscan = 5000,
# 	burn = 1000,
# 	odens = 25,
# 	save_log_lik = TRUE,
# 	verbose = FALSE
# )
# 
# # the best model has elpd_diff = 0; a named list labels the rows
# cmp <- loo::loo_compare(list(
# 	fit_static = loo::loo(fit_static),
# 	fit_dyn = loo::loo(fit_dyn)
# ))
# cmp

## ----loo-compare-read, eval = FALSE-------------------------------------------
# best <- rownames(cmp)[1]
# runnerup <- rownames(cmp)[2]
# diff_val <- abs(round(cmp[2, "elpd_diff"], 1))
# se_val <- round(cmp[2, "se_diff"], 1)
# ratio <- round(diff_val / se_val, 1)

## ----chunked, eval = FALSE----------------------------------------------------
# fit_chk <- lame(
# 	Y,
# 	Xdyad = Xdyad,
# 	family = "binary",
# 	R = 0,
# 	dcor = FALSE,
# 	dynamic_beta = "dyad",
# 	nscan = 2000,
# 	burn = 500,
# 	odens = 5,
# 	save_log_lik = "chunked", # binary files, one per chunk of columns
# 	log_lik_chunk_size = 5000L,
# 	verbose = FALSE
# )
# loo::loo(fit_chk) # reads the chunks transparently

