Nothing
compare_matrices <- function(actual, expected, tols = NULL) {
if (is.null(tols)) {
expect_equal(actual, expected)
} else {
identical(rownames(actual), rownames(expected))
identical(colnames(actual), colnames(expected))
if (ncol(actual) != length(tols)) {
stop("#tols != #columns in actual")
}
expect_equal(is.na(actual), is.na(expected))
within_tolerance <-
sapply(
1:length(tols),
function(x) abs(actual[, x] - expected[, x]) < tols[x]
)
if (!all(within_tolerance, na.rm = TRUE)) {
got <- paste(capture.output(print(within_tolerance)), collapse = "\n")
expct <- paste(capture.output(print(expected)), collapse = "\n")
act <- paste(capture.output(print(actual)), collapse = "\n")
msg <- paste(
"Discrepancies between matrices exceed given tolerances.",
"- FALSE represents a mismatch",
"- NA is ok (it was checked that they are in the same place)",
"Discrepancies at:",
got,
"Expected:",
expct,
"Actual:",
act,
sep = "\n"
)
testthat::fail(msg)
}
}
}
test_that("optim_model_space_params correctly computes small_economic_growth_ms", {
set.seed(23)
data_prepared <- badp::economic_growth[,1:6] %>%
badp::feature_standardization(
excluded_cols = c(country, year, gdp)
) %>%
badp::feature_standardization(
group_by_col = year,
excluded_cols = country,
scale = FALSE
)
params <- optim_model_space_params(
df = data_prepared,
dep_var_col = gdp,
timestamp_col = year,
entity_col = country,
init_value = function(n) rep(0.5, n),
nested = TRUE
)
compare_matrices(params, small_model_space$params, tols = rep(0.01, 8))
})
non_zero_stats_mask_generator <- function(n_lin_features, n_rows = NULL) {
lin_features_mask <- t(rje::powerSetMat(n_lin_features))
# One entry per model, i.e. one per column of lin_features_mask, so that
# rbind() below does not have to recycle these rows.
n_models <- ncol(lin_features_mask)
ones <- rep(1, n_models)
zeros <- rep(0, n_models)
mask_where_nonzero <- rbind(
ones, ones, ones,
lin_features_mask,
ones,
lin_features_mask
)
mask_where_greater_than_zero <- rbind(
zeros, zeros, zeros,
lin_features_mask,
zeros,
lin_features_mask
)
# Model spaces fitted from badp 0.6.0 onwards carry two further rows,
# tr(H^-1 J) and dim(theta), both of which are strictly positive. Pad the
# masks when the matrix under test has them, so that the same helper works
# for stored objects fitted before the change and for freshly computed ones.
if (!is.null(n_rows) && n_rows > nrow(mask_where_nonzero)) {
extra <- n_rows - nrow(mask_where_nonzero)
mask_where_nonzero <- rbind(mask_where_nonzero, matrix(1, extra, n_models))
mask_where_greater_than_zero <-
rbind(mask_where_greater_than_zero, matrix(1, extra, n_models))
}
list(
nonzero = mask_where_nonzero,
greater_than_zero = mask_where_greater_than_zero
)
}
test_that(
paste(
"compute_model_space_stats computes correct likelihoods and standard",
"deviations based on small_model_space"
),
{
set.seed(23)
n_lin_features <- 3
data_prepared <- badp::economic_growth[, 1:(3+n_lin_features)] %>%
badp::feature_standardization(
excluded_cols = c(country, year, gdp)
) %>%
badp::feature_standardization(
group_by_col = year,
excluded_cols = country,
scale = FALSE
)
model_space_stats <- compute_model_space_stats(
df = data_prepared,
dep_var_col = gdp,
timestamp_col = year,
entity_col = country,
params = small_model_space$params
)
masks <- non_zero_stats_mask_generator(n_lin_features,
n_rows = nrow(model_space_stats))
# The bundled small_model_space may predate the diagnostic rows added in
# badp 0.6.0, so compare the rows the stored object has. Once it is
# regenerated this covers the whole matrix.
n_ref <- nrow(small_model_space$stats)
expect_equal(model_space_stats[seq_len(n_ref), ], small_model_space$stats)
expect_true(all(model_space_stats[masks$nonzero == 1] != 0))
expect_true(all(model_space_stats[masks$greater_than_zero == 1] > 0))
}
)
test_that(paste("model_space computes correct model_space list"), {
data_prepared <- badp::economic_growth[,1:5] %>%
badp::feature_standardization(
excluded_cols = c(country, year, gdp)
) %>%
badp::feature_standardization(
group_by_col = year,
excluded_cols = country,
scale = FALSE
)
model_space <- optim_model_space(
df = data_prepared,
dep_var_col = gdp,
timestamp_col = year,
entity_col = country,
init_value = function(n) rep(0.5, n)
)
expect_equal(length(model_space), 7)
expect_s3_class(model_space, "badp_model_space")
expect_equal(class(model_space[[1]]), c("matrix","array"))
expect_equal(class(model_space[[2]]), c("matrix","array"))
convergence <- model_space$convergence
expect_equal(
rownames(convergence),
c("converged", "optim_code", "n_restarts", "max_abs_gradient",
"n_init_draws")
)
expect_equal(ncol(convergence), ncol(model_space$params))
expect_true(all(convergence["converged", ] %in% c(0, 1)))
expect_true(all(convergence["n_init_draws", ] >= 1))
})
# Note on what is deliberately NOT tested here.
#
# Re-estimating the 512-model economic growth space takes about five minutes,
# which is too slow for the test suite and well beyond what CRAN allows. The
# replication of Moral-Benito (2016) Table II is therefore a release-time
# check rather than a unit test: run
#
# source("data-raw/published_table_ii.R")
# check_published_replication(badp::full_model_space)
#
# which compares posterior means and inclusion probabilities against the
# values transcribed from the published paper.
#
# What remains below is fast: bma() is exercised against the bundled model
# space, so any change in the averaging step is caught immediately, while the
# estimation step is covered by the small model space tests above.
test_that("bma() reproduces the bundled results for the bundled model space", {
actual <- badp::bma(badp::full_model_space, round = 5)[[1]]
expected <- badp::full_bma_results[[1]]
expect_equal(dim(actual), dim(expected))
expect_equal(dimnames(actual), dimnames(expected))
expect_equal(actual, expected, tolerance = 1e-4)
})
test_that("the bundled model space still matches the published moments", {
# Moral-Benito (2016), "Model averaging in economics: an overview", Journal
# of Applied Econometrics 31(4): 584-602, Table II (p. 594), columns (1) and
# (3), transcribed from the paper. No re-estimation: this checks that the
# bundled model space and bma() together still reproduce the published
# figures, which is the claim made in the vignette and the JSS manuscript.
#
# Column (2), the posterior standard deviation, is deliberately not used. It
# is the robust (sandwich) standard deviation, built from J = sum_i s_i s_i'
# and hence of rank at most N = 73, while every model here has 88 to 106
# parameters. The published column is not identified. See the "Rank of the
# sandwich covariance" section of ?optim_model_space.
published_PM <- c(gdp_lag = 0.918, ish = 0.063, sed = 0.031, pgrw = 0.018,
pop = 0.121, ipr = -0.033, opem = 0.034, gsh = -0.013,
lnlex = 0.086, polity = -0.056)
published_PIP <- c(gdp_lag = NA, ish = 0.77, sed = 0.72, pgrw = 0.71,
pop = 0.98, ipr = 0.66, opem = 0.77, gsh = 0.75,
lnlex = 0.86, polity = 0.68)
actual <- badp::bma(badp::full_model_space, round = 5)[[1]]
# Posterior means to within 0.005, inclusion probabilities to within two
# percentage points.
expect_lt(max(abs(actual[, "PM"] - published_PM)), 0.005)
expect_lt(max(abs(actual[, "PIP"] - published_PIP), na.rm = TRUE), 0.02)
})
test_that("a rank-deficient sandwich is detected and reported", {
ms <- badp::full_model_space
K <- length(ms$reg_names)
# J is built from the variation of the entity-level scores, so parameters
# entering the likelihood only through terms common to all entities
# contribute nothing and every model is rank deficient.
expect_equal(
badp:::n_rank_deficient_models(ms$stats, K = K),
ncol(ms$stats)
)
n_theta <- ms$stats[4 + 2 * K, ]
rank_j <- ms$stats[5 + 2 * K, ]
expect_true(all(rank_j < n_theta))
expect_true(all(rank_j > 0))
# Model spaces fitted before the rank was stored must not be reported as
# affected.
old_stats <- ms$stats[seq_len(4 + 2 * K), , drop = FALSE]
expect_equal(badp:::n_rank_deficient_models(old_stats, K = K), 0L)
# summary() reports it rather than warning, since it always applies.
expect_output(print(summary(ms)), "Score directions spanned")
})
test_that("score_rank ignores components common to all entities", {
set.seed(1)
# Three coordinates varying across entities, two constant.
varying <- matrix(rnorm(50 * 3), nrow = 50)
constant <- matrix(rep(c(2, -1), each = 50), nrow = 50)
G <- cbind(varying, constant)
expect_equal(badp:::score_rank(G), 3L)
expect_equal(badp:::score_rank(varying), 3L)
})
test_that("eta overrides weighting and is validated", {
ms <- badp::migration_model_space
expect_error(badp::bma(ms, eta = 0), "positive")
expect_error(badp::bma(ms, eta = -1), "positive")
expect_error(badp::bma(ms, eta = c(1, 2)), "single")
# eta = 1 must reproduce the mb2012 weighting exactly.
by_eta <- badp::bma(ms, eta = 1, round = 5)
by_name <- badp::bma(ms, weighting = "mb2012", round = 5)
expect_equal(by_eta[[1]], by_name[[1]])
expect_equal(by_eta$eta, 1)
expect_identical(by_eta$weighting, "user")
# eta = 1/N must reproduce mb2016.
n_entities <- length(unique(ms$df[[2]]))
expect_equal(
badp::bma(ms, eta = 1 / n_entities, round = 5)[[1]],
badp::bma(ms, weighting = "mb2016", round = 5)[[1]]
)
# supplying both is reported
expect_warning(badp::bma(ms, weighting = "mb2012", eta = 1), "overrides")
# curvature is no longer offered
expect_error(badp::bma(ms, weighting = "curvature"), "should be one of")
})
test_that("init_model_space_params draws starting values from init_value", {
df <- badp::economic_growth[, 1:5]
constant_params <- init_model_space_params(df, year, country, gdp,
init_value = function(n) rep(0.7, n))
set.seed(42)
random_params <- init_model_space_params(
df, year, country, gdp, init_value = function(n) runif(n, 0.1, 1))
# same shape and the same exclusion (NA) pattern as the constant version
expect_equal(dim(random_params), dim(constant_params))
expect_equal(is.na(random_params), is.na(constant_params))
values <- random_params[!is.na(random_params)]
expect_true(all(values >= 0.1 & values <= 1))
expect_gt(length(unique(values)), 1)
# reproducible under a seed
set.seed(42)
random_params_again <- init_model_space_params(
df, year, country, gdp, init_value = function(n) runif(n, 0.1, 1))
expect_equal(random_params, random_params_again)
})
test_that("init_value accepts a single number as a constant generator", {
df <- badp::economic_growth[, 1:5]
from_generator <- init_model_space_params(
df, year, country, gdp, init_value = function(n) rep(0.5, n))
from_scalar <- init_model_space_params(
df, year, country, gdp, init_value = 0.5)
expect_equal(from_scalar, from_generator)
# an integer is fine too
expect_equal(
init_model_space_params(df, year, country, gdp, init_value = 2L),
init_model_space_params(df, year, country, gdp,
init_value = function(n) rep(2L, n))
)
})
test_that("init_value rejects zero and other invalid inputs", {
df <- badp::economic_growth[, 1:5]
# 0 is reserved to mark an excluded parameter
expect_error(
init_model_space_params(df, year, country, gdp, init_value = 0),
"cannot be 0"
)
expect_error(
init_model_space_params(df, year, country, gdp,
init_value = function(n) rep(0, n)),
"reserved"
)
# neither a function nor a single finite number
expect_error(
init_model_space_params(df, year, country, gdp, init_value = c(0.5, 0.7)),
"must be a function"
)
expect_error(
init_model_space_params(df, year, country, gdp, init_value = "0.5"),
"must be a function"
)
expect_error(
init_model_space_params(df, year, country, gdp, init_value = NA_real_),
"must be a function"
)
})
test_that("starting points at which the likelihood is undefined are redrawn", {
# stand-in for an infeasible point: the likelihood is undefined (NA)
# wherever the first parameter is negative
local_mocked_bindings(
sem_likelihood = function(params, ...) if (params[1] < 0) NA_real_ else 1
)
n_calls <- 0
init_value <- function(n) {
n_calls <<- n_calls + 1
rep(if (n_calls < 3) -1 else 0.5, n)
}
init <- feasible_init_params(
c(-1, -1, -1), data = NULL, exact_value = FALSE, init_value = init_value,
max_init_attempts = 100, regressors_subset = c("ish", "sed"))
expect_equal(init$par, c(0.5, 0.5, 0.5))
# the point passed in, plus the three redraws it took to find a feasible one
expect_equal(init$n_init_draws, 4)
})
test_that("drawing feasible starting points gives up after max_init_attempts", {
local_mocked_bindings(sem_likelihood = function(params, ...) NA_real_)
expect_error(
feasible_init_params(
c(-1, -1, -1), data = NULL, exact_value = FALSE,
init_value = function(n) rep(-1, n), max_init_attempts = 5,
regressors_subset = c("ish", "sed")),
"Could not draw a feasible starting point for the model with regressors: ish, sed in 5 attempts"
)
})
test_that("BFGS stepping out of the region where the likelihood is defined does not abort", {
# Stand-in for the SEM likelihood: chol() throws once the parameter stops
# being positive, and the maximum lies just inside that edge, so from this
# starting point the line search is bound to step across it.
lik_tape <- RTMB::MakeTape(
function(p) {
"[<-" <- RTMB::ADoverload("[<-")
m <- matrix(0, 1, 1)
m[1, 1] <- p[1]
-100 * (p[1] + 5)^2 + sum(log(diag(chol(m))))
},
1
)
gr <- function(p) as.numeric(lik_tape$jacobian(p))
# handed to optim() unguarded, the error escapes and would abort the whole
# model space
expect_error(
stats::optim(1, lik_tape, gr = gr, method = "BFGS",
control = list(fnscale = -1)),
"leading minor"
)
fit <- optim_with_restarts(1, lik_tape,
control = list(fnscale = -1, maxit = 200),
max_restarts = 2, restart_tol = 1e-3)
# the maximizer of -100 (p + 5)^2 + log(p) / 2 over p > 0
expect_equal(fit$par, 5e-4, tolerance = 0.01)
expect_equal(unname(fit$diagnostics["converged"]), 1)
})
test_that("usable_solution accepts only invertible observed information", {
# a proper maximum: positive definite and well conditioned
expect_true(usable_solution(diag(c(2, 3))))
# a stationary point that is not a maximum
expect_false(usable_solution(diag(c(2, -3))))
# positive definite in theory, singular in floating point
expect_false(usable_solution(diag(c(1, 1e-20))))
})
test_that("a solution no standard errors can be computed from is re-optimized", {
# -(x - 1)^2 - (x y)^2 is taped for real, so the observed information below
# is the genuine Hessian of it. At (0, 0) it is singular in the second
# coordinate; at (1, 0) it is an ordinary maximum. Written with explicit
# multiplication rather than ^2: CppAD's pow() evaluates x^y as
# exp(y * log(x)) for non-integer-literal y, so its second derivative is
# NaN wherever the tape meets log(0), even where the true derivative is
# finite - which both (0, 0) and (1, 0) do here, through the x * y term.
local_mocked_bindings(
sem_likelihood = function(params, ...) {
-(params[1] - 1) * (params[1] - 1) -
(params[1] * params[2]) * (params[1] * params[2])
}
)
attempts <- 0
solutions <- list(c(0, 0), c(1, 0))
local_mocked_bindings(
optim_with_restarts = function(par, lik_tape, ...) {
attempts <<- attempts + 1
list(par = solutions[[min(attempts, length(solutions))]],
diagnostics = c(converged = 1, optim_code = 0, n_restarts = 0,
max_abs_gradient = 0))
}
)
control <- list(trace = 0, maxit = 1000, fnscale = -1, scale = 0.05)
fit <- optim_from_usable_start(
c(2, 2), data = NULL, exact_value = FALSE,
init_value = function(n) rep(2, n), max_init_attempts = 10,
control = control, max_restarts = 2, restart_tol = 1e-3,
max_reoptimizations = 3, regressors_subset = "x")
# the first solution was discarded and the second one kept
expect_equal(attempts, 2)
expect_equal(fit$par, c(1, 0))
expect_equal(unname(fit$diagnostics["converged"]), 1)
})
test_that("re-optimization gives up after max_reoptimizations without erroring", {
local_mocked_bindings(
sem_likelihood = function(params, ...) {
-(params[1] - 1) * (params[1] - 1) -
(params[1] * params[2]) * (params[1] * params[2])
}
)
attempts <- 0
local_mocked_bindings(
optim_with_restarts = function(par, lik_tape, ...) {
attempts <<- attempts + 1
list(par = c(0, 0),
diagnostics = c(converged = 1, optim_code = 0, n_restarts = 0,
max_abs_gradient = 0))
}
)
control <- list(trace = 0, maxit = 1000, fnscale = -1, scale = 0.05)
fit <- optim_from_usable_start(
c(2, 2), data = NULL, exact_value = FALSE,
init_value = function(n) rep(2, n), max_init_attempts = 10,
control = control, max_restarts = 2, restart_tol = 1e-3,
max_reoptimizations = 2, regressors_subset = "x")
# the first attempt plus the two allowed re-optimizations
expect_equal(attempts, 3)
# the model is kept, but is not passed off as converged
expect_equal(fit$par, c(0, 0))
expect_equal(unname(fit$diagnostics["converged"]), 0)
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.