@@ -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(" \n Selected 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