Skip to content

Commit 89357ce

Browse files
committed
autoselect functions added for betabinomial and shash
1 parent 782ae48 commit 89357ce

8 files changed

Lines changed: 686 additions & 2 deletions

File tree

CRAN-SUBMISSION

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Version: 3.5.4
2+
Date: 2026-05-15 06:18:27 UTC
3+
SHA: 782ae48044dd523416f426e2da74c60cce74106a

DESCRIPTION

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: cNORM
22
Title: Continuous Norming
3-
Version: 3.5.4
3+
Version: 3.6.0
44
Authors@R: c(
55
person("Alexandra", "Lenhard",
66
email = "lenhard@psychometrica.de",
@@ -43,7 +43,9 @@ Depends:
4343
R (>= 4.0.0)
4444
Imports:
4545
ggplot2 (>= 3.5.0),
46-
leaps (>= 3.1)
46+
leaps (>= 3.1),
47+
parallel,
48+
utils
4749
Suggests:
4850
DT,
4951
haven,

NAMESPACE

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ importFrom(stats,
88
runif, optimize, cor, quantile, coef)
99
importFrom(utils, menu, head, tail)
1010

11+
importFrom(parallel, clusterCall)
12+
importFrom(parallel, clusterExport)
13+
importFrom(parallel, detectCores)
14+
importFrom(parallel, makeCluster)
15+
importFrom(parallel, parLapply)
16+
importFrom(parallel, stopCluster)
17+
importFrom(utils, flush.console)
18+
1119
# Imports from suggested packages (with checks in code)
1220
importFrom(leaps, regsubsets)
1321

@@ -24,6 +32,8 @@ importFrom(ggplot2,
2432

2533
# Exported functions
2634
export(
35+
autoselect.betabinomial,
36+
autoselect.shash,
2737
bestModel,
2838
betaCoefficients,
2939
buildCnormObject,

NEWS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@ This file documents the development of the package as well as open issues or poi
33

44

55

6+
7+
### Version in 3.6.0
8+
Date: 15.05.2026 - in progress
9+
10+
This release includes autoselect methods for shash, betabinomial and taylor
11+
models using parallel code execution.
12+
13+
Changes:
14+
15+
* new methods: autoselect.betabinomial, autoselect.shash
16+
17+
18+
19+
20+
21+
622
### Version in 3.5.4
723
Date: 15.05.2026 - release
824

R/betaBinomial.R

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,6 +1603,238 @@ summary.cnormBetaBinomial <- function(object, ...) {
16031603
invisible(diag)
16041604
}
16051605

1606+
#' Automatic model selection for beta-binomial continuous norming via BIC
1607+
#'
1608+
#' Selects polynomial degrees for the two components of a beta-binomial model
1609+
#' (\eqn{\alpha}/\eqn{\beta} when \code{mode = 2} or \eqn{\mu}/\eqn{\sigma} when
1610+
#' \code{mode = 1}) by minimizing BIC.
1611+
#'
1612+
#' Parallel execution is attempted by default. If the workers cannot access the
1613+
#' \pkg{cNORM} namespace, the function transparently falls
1614+
#' back to sequential execution.
1615+
#'
1616+
#' @param age,score Numeric vectors of predictor and response values.
1617+
#' @param n Maximum score. Defaults to \code{max(score)}.
1618+
#' @param weights Optional numeric vector of weights.
1619+
#' @param mode 1 for \eqn{\mu}/\eqn{\sigma}, 2 for direct \eqn{\alpha}/\eqn{\beta} (default).
1620+
#' @param max_alpha,max_beta Maximum polynomial degrees. Default 4.
1621+
#' @param min_alpha,min_beta Minimum polynomial degrees. Default 1.
1622+
#' @param control Optional control list passed to \code{\link[stats]{optim}}.
1623+
#' @param scale Norm scale (default \code{"T"}).
1624+
#' @param parallel Logical; attempt parallel execution. Default \code{TRUE}.
1625+
#' @param n_cores Number of cores. Defaults to all logical cores.
1626+
#' @param plot Logical; plot the selected model. Default \code{TRUE}.
1627+
#' @param verbose Logical; print progress. Default \code{TRUE}.
1628+
#'
1629+
#' @return The selected fitted model
1630+
#'
1631+
#' @export
1632+
autoselect.betabinomial <- function(age,
1633+
score,
1634+
n = NULL,
1635+
weights = NULL,
1636+
mode = 2,
1637+
max_alpha = 4,
1638+
max_beta = 4,
1639+
min_alpha = 1,
1640+
min_beta = 1,
1641+
control = NULL,
1642+
scale = "T",
1643+
parallel = TRUE,
1644+
n_cores = NULL,
1645+
plot = TRUE,
1646+
verbose = TRUE) {
1647+
1648+
# ---- Input validation -------------------------------------------------
1649+
if (length(age) != length(score))
1650+
stop("Length of 'age' and 'score' must be the same.")
1651+
if (!is.null(weights) && length(weights) != length(age))
1652+
stop("Length of 'weights' must match length of 'age' and 'score'.")
1653+
if (max_alpha < min_alpha) stop("'max_alpha' must be >= 'min_alpha'.")
1654+
if (max_beta < min_beta) stop("'max_beta' must be >= 'min_beta'.")
1655+
if (min_alpha < 1 || min_beta < 1) stop("Minimum degrees must be >= 1.")
1656+
if (!(mode %in% c(1, 2))) stop("'mode' must be 1 or 2.")
1657+
1658+
if (is.null(n)) {
1659+
n <- max(score, na.rm = TRUE)
1660+
if (verbose) message("n not specified; using max(score) = ", n)
1661+
}
1662+
1663+
say <- function(...) {
1664+
if (verbose) { cat(..., sep = ""); utils::flush.console() }
1665+
}
1666+
1667+
# ---- Pair list -------------------------------------------------------
1668+
grid <- expand.grid(alpha = min_alpha:max_alpha,
1669+
beta = min_beta:max_beta,
1670+
KEEP.OUT.ATTRS = FALSE)
1671+
pairs <- lapply(seq_len(nrow(grid)),
1672+
function(i) c(grid$alpha[i], grid$beta[i]))
1673+
1674+
1675+
# ---- Parallel setup with dev-mode fallback --------------------------
1676+
use_parallel <- FALSE
1677+
cl <- NULL
1678+
if (isTRUE(parallel)) {
1679+
avail <- tryCatch(parallel::detectCores(logical = TRUE),
1680+
error = function(e) 1L)
1681+
if (is.null(n_cores)) n_cores <- avail # use all cores by default
1682+
n_cores <- max(1L, min(n_cores, length(pairs), avail))
1683+
1684+
if (n_cores > 1L && length(pairs) > 1L) {
1685+
cl <- tryCatch(parallel::makeCluster(n_cores),
1686+
error = function(e) NULL)
1687+
if (!is.null(cl)) {
1688+
# Probe workers: can they load cNORM (i.e. is the package installed)?
1689+
worker_ok <- tryCatch({
1690+
res <- parallel::clusterCall(cl, function() {
1691+
requireNamespace("cNORM", quietly = TRUE) &&
1692+
exists("cnorm.betabinomial", envir = asNamespace("cNORM")) &&
1693+
exists("diagnostics.betabinomial", envir = asNamespace("cNORM"))
1694+
})
1695+
all(vapply(res, isTRUE, logical(1)))
1696+
}, error = function(e) FALSE)
1697+
1698+
if (worker_ok) {
1699+
use_parallel <- TRUE
1700+
on.exit(try(parallel::stopCluster(cl), silent = TRUE), add = TRUE)
1701+
parallel::clusterExport(
1702+
cl,
1703+
varlist = c("age", "score", "n", "weights",
1704+
"mode", "control", "scale"),
1705+
envir = environment()
1706+
)
1707+
say(sprintf("Parallel mode: using %d cores.\n", n_cores))
1708+
} else {
1709+
try(parallel::stopCluster(cl), silent = TRUE); cl <- NULL
1710+
say("Note: cNORM is not installed in the worker library path ",
1711+
"(typical during devtools::load_all). ",
1712+
"Falling back to sequential execution.\n")
1713+
}
1714+
}
1715+
}
1716+
}
1717+
1718+
# ---- Worker function -------------------------------------------------
1719+
fit_worker <- function(p) {
1720+
tryCatch({
1721+
m <- suppressMessages(suppressWarnings(
1722+
cNORM::cnorm.betabinomial(
1723+
age = age, score = score, n = n, weights = weights,
1724+
mode = mode, alpha = p[1], beta = p[2],
1725+
control = control, scale = scale, plot = FALSE
1726+
)
1727+
))
1728+
d <- cNORM::diagnostics.betabinomial(m)
1729+
list(alpha = p[1], beta = p[2], model = m,
1730+
BIC = if (is.finite(d$BIC)) d$BIC else Inf,
1731+
AIC = d$AIC, logLik = d$log_likelihood,
1732+
converged = isTRUE(d$converged),
1733+
status = if (!is.finite(d$BIC)) "error"
1734+
else if (!isTRUE(d$converged)) "not_converged"
1735+
else "ok",
1736+
message = NA_character_)
1737+
}, error = function(e) {
1738+
list(alpha = p[1], beta = p[2], model = NULL,
1739+
BIC = Inf, AIC = NA_real_, logLik = NA_real_,
1740+
converged = FALSE, status = "error",
1741+
message = conditionMessage(e))
1742+
})
1743+
}
1744+
1745+
# ---- Cache + reporting -----------------------------------------------
1746+
cache <- new.env(hash = TRUE, parent = emptyenv())
1747+
ck <- function(a, b) sprintf("%d_%d", a, b)
1748+
1749+
report <- function(r) {
1750+
tag <- switch(r$status,
1751+
ok = "",
1752+
not_converged = " (not strictly converged)",
1753+
error = paste0(" [error: ", r$message, "]"))
1754+
say(sprintf(" alpha = %d, beta = %d : BIC = %s%s\n",
1755+
r$alpha, r$beta,
1756+
formatC(r$BIC, digits = 3, format = "f"), tag))
1757+
}
1758+
1759+
evaluate_pairs <- function(pairs) {
1760+
todo <- pairs[!vapply(pairs,
1761+
function(p) exists(ck(p[1], p[2]), envir = cache),
1762+
logical(1))]
1763+
if (length(todo) == 0L) return(invisible(NULL))
1764+
1765+
say(sprintf("Evaluating %d model%s ...\n",
1766+
length(todo), if (length(todo) == 1) "" else "s"))
1767+
1768+
if (use_parallel && length(todo) > 1L) {
1769+
# Process in chunks of size n_cores so output appears progressively
1770+
chunks <- split(todo,
1771+
ceiling(seq_along(todo) / n_cores))
1772+
for (chunk in chunks) {
1773+
results <- parallel::parLapply(cl, chunk, fit_worker)
1774+
for (r in results) {
1775+
assign(ck(r$alpha, r$beta), r, envir = cache); report(r)
1776+
}
1777+
}
1778+
} else {
1779+
for (p in todo) {
1780+
r <- fit_worker(p)
1781+
assign(ck(r$alpha, r$beta), r, envir = cache); report(r)
1782+
}
1783+
}
1784+
invisible(NULL)
1785+
}
1786+
1787+
get_res <- function(a, b) get(ck(a, b), envir = cache)
1788+
1789+
# ---- Run the chosen strategy -----------------------------------------
1790+
path <- NULL
1791+
1792+
evaluate_pairs(pairs)
1793+
all_res <- mget(ls(envir = cache), envir = cache)
1794+
bics <- vapply(all_res, `[[`, numeric(1), "BIC")
1795+
if (all(!is.finite(bics)))
1796+
stop("Selection failed: no model produced a finite BIC. ",
1797+
"Inspect $selection$evaluated for per-fit messages.")
1798+
current <- all_res[[which.min(bics)]]
1799+
1800+
# ---- Compile results --------------------------------------------------
1801+
evaluated <- do.call(rbind, lapply(
1802+
mget(ls(envir = cache), envir = cache),
1803+
function(r) data.frame(alpha = r$alpha, beta = r$beta,
1804+
BIC = r$BIC, AIC = r$AIC,
1805+
logLik = r$logLik,
1806+
converged = r$converged,
1807+
status = r$status,
1808+
message = r$message,
1809+
stringsAsFactors = FALSE)))
1810+
evaluated <- evaluated[order(evaluated$BIC), , drop = FALSE]
1811+
rownames(evaluated) <- NULL
1812+
1813+
path_df <- if (!is.null(path))
1814+
do.call(rbind, lapply(path, function(r)
1815+
data.frame(alpha = r$alpha, beta = r$beta, BIC = r$BIC)))
1816+
else NULL
1817+
1818+
say(sprintf("\nSelected model: alpha = %d, beta = %d (BIC = %.3f)\n",
1819+
current$alpha, current$beta, current$BIC))
1820+
1821+
final_model <- current$model
1822+
if (is.null(final_model))
1823+
stop("Selection failed: the best candidate did not produce a usable model.")
1824+
1825+
final_model$selection <- list(
1826+
evaluated = evaluated,
1827+
selected = list(alpha = current$alpha,
1828+
beta = current$beta,
1829+
BIC = current$BIC),
1830+
mode = mode,
1831+
path = path_df
1832+
)
1833+
1834+
if (plot) print(plot(final_model, age = age, score = score, weights = weights))
1835+
return(final_model)
1836+
}
1837+
16061838
#' Summarize a Beta-Binomial Continuous Norming Model
16071839
#'
16081840
#' This function provides a summary of a fitted beta-binomial continuous norming model,

0 commit comments

Comments
 (0)