Changes in version 1.2.0.9000 Changes in version 1.2.0 (2026-09-28) - generate_model_set() gains null.cov.cutoff, defaulting to 0.8, and returns null.term.correlations. Behaviour change: a predictor correlated with a variable named in null.terms above that cutoff is now dropped from the model set, and a warning names it, what it correlates with, and how strongly (FSSgam_package#23). null.terms forces a term into every candidate, and those terms were outside the cov.cutoff screen, which covers pred.vars.cont, pred.vars.fact and linear.vars only. So a candidate could be arbitrarily strongly correlated with a forced term and appear in every model in the set. That inflates the variance of the forced term's estimate, which is frequently the term the analysis exists to estimate, and nothing in the output indicated it. A separate cutoff, with a much looser default than cov.cutoff's 0.28, because the two screens answer different questions. cov.cutoff decides which predictors may appear together; this decides which predictor is so nearly a restatement of a forced term that fitting both is not informative. Set it to 1 to admit every predictor whatever its correlation with a forced term. A variable inside a bs='re' smooth is excluded from the screen. A random-effect grouping factor is correlated with the predictors measured within it by construction: in the companion repository's case study 2, null.terms is s(Location,Site,bs='re') and Status is nested in Location, so their correlation is 1. That is the design of the study rather than collinearity to screen out, and dropping Status there would be wrong. The screen is for a forced term that competes with a candidate for the same variation. Correlations among the null.terms variables themselves are neither computed nor screened. Those terms are forced in by the user's decision, and dropping one is what must not happen. null.term.correlations is returned whether or not anything is dropped, so the correlations can be inspected where no warning is raised; it is NULL where there is nothing to report. A supplied cor.matrix is used for any forced term it names in either dimension, overriding the computed estimate for that term; the rest of the block is computed from use.dat, which is the ordinary case, a supplied matrix being indexed by predictor and a forced term not being one. Requiring the matrix to name the term would disable this screen for every caller who supplies one. Where the computation fails -- which is what a predictor of a class check_correlations() cannot classify causes -- the screen is skipped with a warning rather than the call stopping, so a caller relying on FSSgam_package#13 still gets their model set and is told their forced terms were not screened. A pair a supplied cor.matrix gives no value for is treated as not supplied, and computed from use.dat. That is NA in both directions, or NA in the one direction a matrix naming the forced term in a single dimension has. Read as a correlation of zero, such a pair admitted the predictor to every candidate alongside the forced term -- the outcome this screen exists to prevent -- decided by a cell the user left empty rather than by a correlation, while the same NA between two predictors stops the call (FSSgam_package#27). Where one direction gives a value, that value is used, unchanged. A forced term can be part supplied and part computed, the two being merged cell by cell rather than row by row (FSSgam_package#41). Where the correlation estimate is asymmetric, as check_non_linear_correlations() returns it, both directions are read and the larger is used -- what the cov.cutoff screen already does. Reading one direction admitted a candidate that is a deterministic function of a forced term: measured on a squared forced term in the committed test, [forced, candidate] is 0.280 -- below the default cutoff -- while [candidate, forced] is 0.991. The smaller figure depends on the random draw and the test asserts only that it is below the cutoff, which is the property that matters. Each cell of null.term.correlations is the value screened on rather than one direction of it; on the symmetric default path the two are equal. null.cov.cutoff is validated: it must be a single non-negative number. Unvalidated, an NA gave an internal "missing value where TRUE/FALSE needed" and a length-2 value was accepted silently. Where dropping predictors leaves fewer than max.predictors, the error says so and names them, rather than reporting only that max.predictors exceeds the number of predictors. - Behaviour change: a test.fit produced by mgcv::gamm() directly is rejected, with a message naming the remedy. Such a fit records no call, so stats::update() has nothing to re-evaluate and every candidate refit fails. The error it produced instead -- "need an object with call component" -- named neither the argument nor the remedy, and the message that followed advised the user to stop using uGamm, which is what they would have had to use to succeed (FSSgam_package#34). The documentation is corrected with it. generate_model_set()'s null.terms text directed the user to gamm for a correlation structure, its @details contrasts gam and gamm at length, and fit_model_set()'s r2.type names gamm among the classes it reports for. All three now say that a gamm fit reaches this package only through MuMIn::uGamm, which supplies the call that a bare mgcv::gamm() fit lacks. - Behaviour change: generate_model_set() rejects a predictor named twice within pred.vars.cont, pred.vars.fact or linear.vars, naming it. Such a name produced a candidate holding it twice, ZONE+ZONE, together with two "no non-missing arguments to max; returning -Inf" warnings naming nothing: enumerate_candidate_models() deduplicates a candidate's terms before indexing the correlation matrix, so a candidate holding one term twice gives a 1x1 sub-matrix whose triangles are empty, and max() of an empty vector returns -Inf, which is below any cov.cutoff (FSSgam_package#28). The warnings had a second cause, which rejecting the repeated name does not address: naming a predictor in both pred.vars.cont and linear.vars, the documented idiom below, puts the name in the candidate pool twice and reaches the same empty triangle at any max.predictors of 2 or more. Both screens now test the sub-matrix through a helper that returns FALSE for an empty triangle rather than calling max() on it, so no candidate set emits the warning. Which combinations survive is unchanged, -Inf having been below every cutoff. A name appearing in both pred.vars.fact and pred.vars.cont or linear.vars is rejected too, as is a name repeated within the character form of factor.factor.interactions or smooth.smooth.interactions. That last is a third route to the same defect and the one the others miss: factor.factor.interactions = c("fa", "fa", "fb") built the interaction column fa.I.fb twice, so use.dat gained two columns of that name and the candidate set gained fa.I.fb+fa.I.fb. A name appearing in both pred.vars.cont and linear.vars is not rejected: that is the documented way to fit a continuous predictor linearly, setdiff(pred.vars.cont, linear.vars) deciding which predictors get a smooth. - Behaviour change: a variable named in the character form of factor.factor.interactions or smooth.smooth.interactions must be a predictor of the model set. Each argument was checked against the wrong set -- the first against colnames(use.dat), so any column was accepted, and the second against rownames(cor.matrix), so what was accepted depended on whether a cor.matrix was supplied and what names it carried. A non-predictor named in either was screened against cov.cutoff and contributed interaction terms, while appearing in no other candidate, in included.vars, or in the variable-inclusion columns fit_model_set() returns (FSSgam_package#37). - Behaviour change: a factor predictor with fewer than two observed levels is rejected, naming it and its level count. Such a factor explains nothing, cannot be screened for collinearity, and produces an interaction column that is a copy of the other factor, so the model set holds the same model under two names. Levels declared but taken by no row are counted as absent (FSSgam_package#33). - Bug fix: check_correlations() and check_non_linear_correlations() now wrap their lm() call in try(), as every other fit in both already was. It was the one unguarded fit in each, so a single-level factor stopped the whole call with "contrasts can be applied only to factors with 2 or more levels" instead of leaving that cell NA. Both are exported and callable directly, so the guard matters beyond the generate_model_set() path the check above covers (FSSgam_package#33). - Behaviour change: a hard coded factor interaction column whose generated name is already a column of use.dat is not created, and a warning names it. cbind() accepted the duplicate, and every later stage selects predictors by name: use.dat[, "a.I.b"] returned the first of the two, which is the user's own column, while the model formulas and the correlation matrix were built for the generated one. Nothing reported the collision. Declining rather than overwriting, because overwriting silently changes a predictor the user named (FSSgam_package#22). This is reached without contrivance: passing the used.data of one call as the use.dat of the next presents every generated name as an existing column. Two generated names can also collide with each other, which is the same defect reached from inside rather than outside: (a, b.I.c) and (a.I.b, c) both paste to a.I.b.I.c, and the two columns are different variables. That is declined and warned about as well, though it is no longer reachable through the public API, the predictor name it requires being rejected by the check below. - Behaviour change: a predictor whose name contains .by., .te., .t., .I., + or * is rejected, as is one named null. Those strings separate the parts of a generated term name and are found with grep(fixed = TRUE) over every term, so a predictor named catch.by.effort -- an ordinary variable name -- was parsed as an interaction. The candidate became ~s(catch.by.effort) + s(catch, by = effort), which cannot be fitted, so the predictor vanished from the model set. The only indication was the -Inf warnings, which this release removes for unrelated reasons (FSSgam_package#39). + joins the terms of a candidate name and * writes a linear interaction, so either is parsed as more than one term in the same way. null is the name given to the null model's own candidate, so a predictor of that name produced two candidates called null: mod.formula[["null"]] returned the first, and the variable importance table matched the wrong row. factor.smooth.interactions in its character form is validated alongside the other two interaction arguments, which it was not. This rejects the failure rather than fixing the parse. Making a term's structure explicit instead of recovering it from its name is a larger change, and the issue records what it would involve. One consequence is worth knowing: passing the used.data of one call as the use.dat of the next presents generated names, which contain .I. by construction, so those columns must be dropped or renamed before they can be named as predictors. - Bug fix: a user-supplied cor.matrix with exactly one cell was silently discarded and recomputed from use.dat. A model set with a single predictor is the only case in which a supplied matrix is 1x1, and length(cor.matrix) == 1 was the test for "nothing supplied", which a 1x1 matrix also satisfies. The supplied value governed no screen and did not appear in predictor.correlations, which is the opposite of the documented contract that a supplied matrix replaces the automatic estimate (FSSgam_package#26). The test is now on the default value rather than on length, in both places it was made -- build_predictor_correlation_matrix() and the factor_correlations() helper inside resolve_factor_interactions(), which had its own copy. It tests dimensionality rather than a list of accepted classes, so the S4 matrix Matrix::Matrix() returns is accepted, as it was before this argument was validated at all. Note that Matrix::nearPD() is not such a case: it returns a list whose mat component is the matrix, and only that component was ever accepted. Behaviour change: cor.matrix is now validated, and a value that is neither a matrix, a data.frame, nor NA is rejected naming its class. What happened to such a value before depended on its length, and neither outcome was right. A length-one value of any class -- a string, or a list holding a matrix -- satisfied the sentinel and was silently treated as nothing supplied, so the model set was built from a computed matrix as though the argument had not been given. A value of any other length fell through to the supplied branch and failed against the missing-predictor check, reporting predictors rather than the argument. Calls of the first kind used to run and now error. - Behaviour change: an NA between two terms of a supplied cor.matrix is now reported by generate_model_set(), naming the pairs, where it previously stopped the call with missing value where TRUE/FALSE needed — naming neither the matrix, the argument, nor the pair (FSSgam_package#27). The report is made where each sub-matrix is formed, immediately before the screen takes max(abs(.)) of it, rather than at a single earlier point. Two earlier placements were tried and both were wrong, in opposite directions. Checking only in build_predictor_correlation_matrix() missed every pair screened before it, resolve_factor_interactions() running first. Checking a union of every name that might be screened then reported pairs that are not: a name given in the character form of factor.factor.interactions is screened against the others in that argument and never against pred.vars.cont. Reporting at the screening site covers exactly the pairs that are screened. A consequence, and a departure from what the issue asked for. The report covers exactly the pairs some screen compares, so it depends on max.predictors and on the interaction arguments. The issue asked for the report not to depend on max.predictors; that would mean rejecting a matrix over a cell nothing reads, which is a false failure. An NA is reported whenever it is read and not otherwise. Note that "read" is not the same as "in a candidate together". A .by. term is one term of a candidate but splits into two for the screen, so at max.predictors = 1 a continuous predictor and a factor are still compared; two continuous predictors are not. - Bug fix: the correlation matrix resolve_factor_interactions() computes when no cor.matrix is supplied is now zero-filled, as the one build_predictor_correlation_matrix() computes already was. Without it a computed matrix reached the factor-factor screen with NA in it -- check_correlations() returns NA for a pair involving a single-level factor (FSSgam_package#33) -- and the call stopped with missing value where TRUE/FALSE needed (FSSgam_package#27). Behaviour change, and a poor outcome that is better than the error it replaces. An NA becoming zero means "uncorrelated", so a factor-factor pair that previously stopped the call is now admitted. Where the NA came from a single-level factor the interaction column is a copy of the other factor: fa.I.const is paste(fa, "a"), so fa and fa.I.const are the same model under two names, and both enter the AICc table and the variable importance scores. That is not a good model set, and the real defect is that a single-level factor is accepted as a predictor at all, which is FSSgam_package#33. Measured over a grid of 432 scenarios -- six factor sets, three continuous sets, both forms of factor.factor.interactions, max.predictors 1 to 3, both settings of non.linear.correlations, two forms of factor.smooth.interactions -- comparing this version against the previous one: | outcome | cells | |---|---| | identical model set | 104 | | identical error | 208 | | error before, model set now | 40 | | error before, a different error now | 80 | | model set before, a different model set now | 0 | | model set before, an error now | 0 | No call that produced a model set before produces a different one, or fails, now. Two things bound the change. Every one of the 40 cells that now build has pred.vars.cont = NA: name a continuous predictor and the call still stops, in check_correlations(), on the same single-level factor (FSSgam_package#33). And all 80 differing errors are single-level-factor scenarios reaching a later failure than before; the scenarios whose predictors contain NA are 144 of the 208 that error identically, rejected before any of this code runs. Two earlier counts of this grid were wrong and are recorded so the figures are not taken from them. The first reported 392 identical, of which 288 cells passed an invalid null.terms and were identical only in the error string validate_null_terms() returns, so no model set was compared in them at all. The second reported 144 identical, which double-counted the 40: 144 is the number of cells that build under this version, not the number that are unchanged. combine_uncorrelated() and enumerate_candidate_models() are otherwise unchanged: stopping on an NA is the right behaviour at both, the alternative being to drop the combination silently, and this only makes the message name the pair. - DESCRIPTION now declares Depends: R (>= 4.4.0), raised from R (>= 3.5). The old floor was not reachable: MuMIn and mgcv, both hard Imports, each declare Depends: R (>= 4.4.0) at the versions current when this was measured (MuMIn 1.48.19, mgcv 1.9-4), so the package could not be installed on anything lower whatever this field said. Nothing about what the package can do changes; the declaration now states what was already true. A user pinned to an older MuMIn and mgcv may find the new floor excludes a configuration that did work, which is the reason the change is noted here rather than treated as a correction (FSSgam_package#31). The floor is now checked rather than asserted. tools/check-r-floor.R compares the declared floor against the Depends of every hard dependency, transitively, and fails if the declared one is lower; it runs in CI as a new r-floor-consistency job. The closure matters: Matrix declares R (>= 4.4) and is not a direct dependency of this package, arriving through mgcv and MuMIn, so a check of the direct Imports alone would miss it. The script also stops rather than passing wherever it cannot count a constraint: a dependency that is not installed, an R constraint written in a form it does not read, and a DESCRIPTION declaring no floor at all. .github/workflows/R-CMD-check.yaml also gains an ubuntu-latest, R 4.4.0 matrix entry, so the package is built, installed and tested on an R at the declared floor for the first time. The two check different things, and only the first detects what was wrong here. Installing a package rejects a floor set above the running R and accepts any floor below it, so no matrix of R versions can see a floor that is too low: reverting DESCRIPTION to R (>= 3.5) leaves every matrix job green. Consequently the test suite uses deparse1() directly and the local deparse_one() in tests/testthat/helper-fixtures.R, which existed only because deparse1() arrived in R 4.0.0, is removed. - fit_model_set()'s r2.vals.unique column is now documented. It was returned in mod.data.out and described nowhere. The documentation states that the column is the model R2 minus the null model's R2, so it is the variance explained beyond whatever was supplied in null.terms; that it is a per-model quantity rather than a variance partition among terms, and so is a per-predictor contribution only at max.predictors = 1; that it is on whatever scale r2.type produced, and so can be negative; and that values are comparable only within a model set sharing the same null.terms and r2.type (FSSgam_package#24). - check_non_linear_correlations() now guards the intercept-only multinom() fit against try() failure, as check_correlations() already did. Where only the fitted model was tested, a failed null was passed to round() as the character vector try() returns, raising "non-numeric argument to mathematical function", which propagated out of the function rather than leaving the cell NA, which is how a failed fit is already handled. The guard is defensive: both models are fitted on the same rows, so no case reaching it is known, and no test accompanies it (FSSgam_package#19). This does not make the function degrade to NA in every failure. The lm() branch of the same function, and the equivalent one in check_correlations(), are not guarded at all, and a single-level factor predictor still aborts both: FSSgam_package#33. - The two github.io URLs in DESCRIPTION, and the one of them also written in README.md, are rewritten with a trailing slash. Without it both redirect with a 301 and R CMD check --as-cran reports them under checking CRAN incoming feasibility, which devtools::check() does not run (FSSgam_package#20). - gamm4 moves from Imports to Suggests. A fresh install of this package no longer installs gamm4 or lme4. If you fit through MuMIn::uGamm(lme4 = TRUE), as the companion repository's case study 2 does, install gamm4 yourself. Nothing in this package calls it: the only uses are inherits(x, "gamm4") class tests, which need no package, and an @importFrom gamm4 gamm4 directive for a function never called. That directive is what made every parallel worker load gamm4 and lme4, through the FSSgam namespace, for every model set regardless of the family fitted. - fit_model_set(parallel = TRUE) no longer names gamm4 in the packages it attaches on the doSNOW workers. Together with the change above this is the only direction recorded on FSSgam_package#14 that measured as reducing the dispatch stall, in which the call hangs indefinitely with no output and no error. Running fit_model_set(parallel = TRUE) on case_study1 from installed copies of each version, alternating in blocks of ten so that both saw the same machine load, one cluster per fresh R process, gave 44 stalls in 140 runs before and 25 in 140 after (chi-squared p = 0.013, this host, 2026-09-01). The narrower comparison agrees. A foreach() whose body is i * 2, so that no package code runs at all, was run 50 times with gamm4 in the packages the workers attach and 50 times without, alternating run by run: 34 stalls against 19 (chi-squared p = 0.005, same host and date). Three qualifications. That is a reduction and not a removal, and FSSgam_package#14 stays open. Both figures are one session's measurement of a rate on one host, and an earlier run of the narrower comparison with its two arms taken one after the other rather than alternated gave a result an independent replication could not reproduce; only alternating designs are quoted here. And neither extends to a uGamm(lme4 = TRUE) test.fit: unserialising such an object loads gamm4 and lme4 on the worker by itself, before the packages are attached, so for those users the worker side is substantially unchanged. check_correlations(parallel = TRUE) benefits as well, without any change of its own: its workers load the FSSgam namespace and so used to load gamm4 through it. - Bug fix: check_correlations() estimated the correlation between two factor predictors from deviances computed on different sets of rows whenever either factor contained missing values. The fitted model was fitted on the pair's complete cases and the intercept-only model on the whole column, so the ratio the estimate is built from was not comparable and the reported correlation was wrong by an amount that depended on how much of the column was missing. Both models are now fitted on the pair's complete cases. Values are unchanged for data with no missing predictor values, which is every call made by generate_model_set(), since it rejects predictors containing NA (FSSgam_package#16). - Bug fix: generate_model_set() screened the wrong pairs for collinearity when a supplied cor.matrix carried the same names in a different order in its rows and its columns. The correlation sub-matrix for a candidate was built from two positional indices, one in rowname order and one in colname order, so where the two orders differed the diagonal of that sub-matrix held cross correlations and the triangles held the ones from the original diagonal, and the candidate was dropped whatever its predictors' correlation. Both sites now index by name in one order. Measured on master, with a supplied matrix over depth, complexity and ZONE, pred.vars.fact = "ZONE" and everything else at its default: the matrix with its columns permuted returned 5 candidates where the same matrix unpermuted returned 9 at max.predictors = 2 and 13 at max.predictors = 3. On this branch the two orders agree, at 9 and 13. The same change means a factor named twice in pred.vars.fact no longer yields an interaction of that factor with itself (FSSgam_package#15). - generate_model_set() no longer requires a user-supplied cor.matrix to name the hard coded factor interaction columns that factor.factor.interactions causes to be created. Which of those columns exist depends on the supplied matrix itself, through the collinearity screen that decides which factor combinations survive, so a user could not know in advance which names to supply; the call stopped partway through model set construction demanding them. Rows and columns for any interaction column the matrix does not carry are now computed from use.dat and appended, and every value the user supplied is kept as supplied. A predictor the user named and did not supply is still an error, so a misspelled name is still reported rather than quietly computed (FSSgam_package#15). - fit_model_set() and full_subsets_gam() gain logLik.fn, a function of one fitted model returning a single log-likelihood value, defaulting to NULL. One restriction applies under parallel = TRUE with save.model.fits = FALSE, the only combination that evaluates it on a worker process: a function written at the top level of a script has its environment replaced before it is sent, so anything it refers to and does not define is not found there. Build it with a constructor, or write it to refer to nothing outside itself. This is the restriction beckyfisher/FSSgam#10 reports for the family argument, and it has the same cause. AICc and BIC are ordinarily read from MuMIn::AICc() and stats::BIC(), which both resolve to the fitted family's own aic slot. Where supplied, logLik.fn replaces the log-likelihood alone: the criterion is built at the degrees of freedom and sample size the default route uses, so every model in the set is scored the same way and the delta values, the weights and variable importance follow. extract_mod_dat() takes the same argument. - Behaviour change: a model set whose test.fit was fitted with one of mgcv's censored families, cnorm() or clog(), is now ranked on a censored log-likelihood computed by this package, and a message says so. The ranking, the weights and variable importance therefore differ from those of 1.1.0 and earlier for those two families and no other (FSSgam_package#42). Both families' aic slots return a value that is not a censored log-likelihood in mgcv 1.9-4, and AICc and BIC were read from them. The fits themselves are unaffected: dev.resids and ls contain none of the defects, so the smoothing parameters, the coefficients and the scale are as before, and only the reported criterion changes. Measured on Debian WSL2, R 4.6.1, mgcv 1.9-4 and MuMIn 1.48.19, on the simulated set the issue reports: n = 300 with the lowest 20 per cent left censored, four candidate predictors of which two are noise, and eleven candidates: the previous criterion selected x, and the censored log-likelihood selects x+z, which is the generating model. The spread of delta.AICc across the set was 35.8 against 310.8, so a nominal delta.AICc <= 5 admitted four models against two. Summed variable importance put the noise predictor v at 0.253 above z at 0.182, where z is in the generating model; it now reports 0.035 and 0.838. The log-likelihood is computed from the fitted mean, the scale and the response's censoring coding rather than from the family's aic slot, so it is independent of the defects and stays correct if they are repaired. Left, right and interval censoring, either column order, and non-unit prior weights are all covered, and every quantity is computed in logs so that an observation far into a tail does not underflow. An infinite bound written in the first column is refused instead: mgcv reads a censored case from the second column alone, so under clog() such a row matches none of its censored cases and is dropped from the fit without a message, and scoring it would count observations the fit ignores. cnorm() does not fit that coding at all. It is checked in the test suite against a second computation built from mgcv's own saturated log-likelihood and deviance residuals, which shares no code with it. - Behaviour change: fit_model_set() stops, before any candidate is fitted, on a test.fit whose criterion is not defined, naming the family. A quasi-likelihood such as quasipoisson() or quasibinomial() has no log-likelihood and so no AIC, and MuMIn::AICc() returns NA for every candidate of such a set. The whole of mod.data.out was then unusable -- delta.AICc, delta.BIC and both weight columns NA, and variable importance with nothing to weight -- and nothing named the cause. The fits succeeded, and the only sign was four no non-missing arguments to min warnings, two raised by compute_model_weights() taking the minimum of the all-NA AICc and BIC columns and two by wi() doing the same. Measured on the eight-model case_study1 set at 023cd1a (FSSgam_package#44). Two checks, because neither covers the other. The value MuMIn::AICc() returns for the test.fit is the general one: it does not depend on a list of family names, so any family whose criterion is undefined reaches it. The fitted family's name is checked as well, because a quasi-likelihood supplied through MuMIn::uGamm() or mgcv::gamm() passes the first. AICc for such a fit is read from the internal lme fit of the PQL working model and is a number, and a PQL log-likelihood is a function of the working response, which itself changes with the fixed effects, so those differences rank nothing. That second check is a behaviour change beyond the NA case. Measured on case_study1, uGamm(Herbivore.abundance ~ s(depth, k = 3, bs = 'cr'), random = list(site = ~1), family = quasipoisson()) with three candidates: 1.1.0 returned a complete table, AICc 170.261, 176.030 and 138.582 and a weight of 1.000 on the best; this release stops instead, naming the family. Supplying logLik.fn is what allows either set to be fitted and ranked; the message says so, and names nb(), tw() and betar() as families that have a likelihood. generate_model_set() fits the null model, so on the full_subsets_gam() route that one fit precedes the refusal. fit_model_set() also stops where every candidate that fitted was given no criterion, and warns naming the candidates where only some were, which a supplied logLik.fn that gives the test.fit a value and the candidates none would otherwise produce silently. - Bug fix: summed variable importance came back NA for every predictor whenever any candidate in the table had an NA model weight, including predictors appearing in no such candidate. The weights are now summed with na.rm = TRUE, which is what corrects VI.mods = "all". Under the default VI.mods = "min.n" the correction is elsewhere: the count of models per predictor is taken over the candidates that have a weight rather than over every row, so the two fitting paths give the same scores for the same model set. The route to it on 1.1.0 is a candidate that failed to fit with save.model.fits = FALSE, which keeps a row for it; the save.model.fits = TRUE path drops the row and was unaffected, so which of the two a user saw depended on a memory setting, and max.models changes that setting without being asked. A supplied logLik.fn that gives some candidates no value reaches it as well. - Bug fix: fit_model_set(save.model.fits = FALSE) recorded a candidate that fitted and was given no criterion as a model that failed to fit, and reported it in failed.models. A failed model is now identified by whether fit_mod_l() returned a fitted object, which is the test the save.model.fits = TRUE path has always used, so the two paths agree on which candidates succeeded. max.models forces save.model.fits = FALSE above 200 candidates, so which path ran was not always the user's choice. - The declared testthat floor is raised from 3.1.5 to 3.2.0, for local_mocked_bindings(), which arrived as experimental in 3.1.7 and was declared stable in 3.2.0. It is a Suggests, so nothing a user installs changes. - Package metadata prepared for a CRAN submission. The Title drops "in R", which CRAN reads as redundant. Authors@R declares the Australian Institute of Marine Science as copyright holder (cph); every file in R/ states that copyright and the holder was previously undeclared. No Language field is declared, which was measured rather than assumed. The documentation uses Australian spelling, but DESCRIPTION names "Generalized Additive Models", the standard term. Measured with hunspell 3.0.5 on this tree: en_GB flags six words in the Title and Description against en_US's four, adding "Generalized" and "GAMs", and 57 against 56 across man/. CRAN reports the DESCRIPTION words, so declaring en-GB would report more of them, not fewer. - The reference to Fisher et al. (2018) is written as \doi{10.1002/ece3.4134} in all four places it appears, rather than as a link to the publisher's page. The publisher refuses automated requests, so the URL check reported that link as possibly invalid; a DOI is resolved instead. - README.md states what the package does, how to install it, and a worked two-step call of generate_model_set() and fit_model_set(). The CRAN status badge is removed until the package is accepted, its link having been a 404 that the URL check reported. Corrected "FFSgam" to "FSSgam" and "calaculation" to "calculation" in check_correlations()'s parallel argument. Changes in version 1.1.0 - Completed the snake_case rename across the public API. full.subsets.gam() is now a deprecated wrapper around the new full_subsets_gam() (same treatment as generate.model.set() and fit.model.set(), since it's used directly in the companion docs repository's published case studies). check.correlations(), check.non.linear.correlations(), build.inclusion.mat(), extract.mod.dat(), and fit.mod.l() are renamed outright to check_correlations(), check_non_linear_correlations(), build_inclusion_mat(), extract_mod_dat(), and fit_mod_l() with no backward-compatible alias -- a breaking change for any code calling these five directly, but none of them appear anywhere in the companion repo's published vignettes/FAQ. - Bug fix: fit_model_set() (and full.subsets.gam()/full_subsets_gam(), which call it) failed to fit most candidate models when test.fit used an mgcv extended family that estimates an extra parameter, such as nb() (negative binomial). Each candidate refit reused the already-fitted family object from test.fit, which carries that estimated parameter in its own mutable environment; re-using it warm-started every refit from an unrelated formula's estimate and destabilised the fit for most (but not all) candidates. Candidate models now get a fresh, independent family object for every fit, the same way the null model already did -- and, unlike an earlier attempt at this same fix, this now holds regardless of how family was originally specified on test.fit (a literal call such as family = nb(), or a variable/list element such as family = my.families[[2]]), and no longer crashes every candidate under parallel = TRUE when family was supplied via a variable. - Bug fix: generate_model_set() (and full_subsets_gam(), which calls it) did not validate the null.terms argument, so passing anything other than a single character string (e.g. NA, NULL, a number, a logical, a character vector of length > 1, or a factor) produced a confusing, type-dependent low-level error -- or, for a bare numeric/logical value, was silently accepted and spliced into a nonsense model formula instead of failing. null.terms is now validated up front with a clear error message (beckyfisher/FSSgam#7). - Bug fix: generate_model_set() (and full_subsets_gam(), which calls it) failed on model sets with exactly one predictor. use.dat[, all.predictors] dropped to a bare vector before being passed to check_correlations() / check_non_linear_correlations(), which report on the columns of a data.frame, so the call failed with an unrelated "argument is of length zero" (or, for non.linear.correlations = TRUE, "invalid 'row.names' length") error instead of building a 1x1 correlation matrix. check_non_linear_correlations() now also returns the 1x1 unit matrix for a single-column input, matching check_correlations(). - Bug fix: generate_model_set() silently dropped the interaction term from every linear.vars predictor after the first. The pattern used to identify .t. interaction terms was built as paste(linear.vars, ".t."), a vector whenever more than one linear predictor was supplied, and grep() used only its first element (with a warning). Affected candidates were still named as interactions but fitted as the corresponding factor main-effect model, so the model table reported duplicate fits under distinct names. - Bug fix: supplying factor predictors with pred.vars.cont = NA -- the documented way to run without smooth predictors -- made generate_model_set() construct a phantom NA.by. interaction term out of the NA itself. With max.predictors = 1 the term was discarded again before the model set was returned and the only symptom was two spurious "no non-missing arguments to max; returning -Inf" warnings, but from max.predictors = 2 it survived into the candidate set as a model whose formula smoothed the literal NA. - Bug fix: case_study1 was documented as having 28 variables; it has 27. - Bug fix: generate_model_set()'s @return documented a generated.models element that it has never returned, and omitted n.mods, mod.formula, test.fit and included.vars, which it does. fit_model_set()'s @return documented a used.data element that it has never returned -- full_subsets_gam() is what carries that. - Bug fix: a candidate model named as an interaction between a factor and a linear predictor was fitted as the factor main effect alone, whenever the .t. term came from the list form of factor.smooth.interactions naming a linear predictor absent from linear.vars. The model table then reported two identical fits under different names. - Substantially expanded the testthat suite, from 105 passing expectations at 1.0.0 to 568 at this release, and from 71.33% to 94.4% line coverage (FSSgam_package#5 covered the first 447 of those; the rest accompany the fixes below). New coverage includes: every non-default argument of generate_model_set(), fit_model_set() and full_subsets_gam(); the save.model.fits = FALSE fitting path; gamm4, uGamm and gamm test.fits; the cyclic.vars, linear.vars and bs.arg formula construction; the bundled datasets; every documented error and warning message; and numerical snapshot tests pinning fitted values for five end-to-end scenarios. - Added a test-coverage GitHub Actions workflow (.github/workflows/test-coverage.yaml, using covr + Codecov) and a coverage badge to the README (FSSgam_package#3). - Bug fix: fit_model_set() (and full_subsets_gam(), which calls it) failed on a model set with exactly one predictor, with 'x' must be an array of at least two dimensions. The variable importance calculation indexed the model table by column name without drop = FALSE, so a single predictor collapsed it to a vector before colSums(). generate_model_set() was fixed for the single-predictor case earlier in this development cycle; fit_model_set() was not. - Bug fix: with VI.mods = 'min.n', a predictor present in no successfully fitted model was given the weight of the single best model containing any predictor, rather than none. The number of models to sum over is zero in that case, and 1:0 counts backwards. - Bug fix: with save.model.fits = FALSE and parallel = TRUE, a single candidate that could not be fitted aborted the whole run instead of being recorded in failed.models. The foreach() on that path had its .errorhandling disabled; the save.model.fits = TRUE path was unaffected. - Bug fix: an unrecognised r2.type was accepted silently and produced a column of NA r2 values. fit_model_set() and full_subsets_gam() now reject anything other than "r2.lm.est", "r2" or "dev". extract_mod_dat() is unchanged, since it documents r2.type. as passed through. - Bug fix: an unrecognised VI.mods failed with object 'aic.var.weights' not found. fit_model_set() and full_subsets_gam() now reject anything other than "min.n" or "all". - Bug fix: full_subsets_gam()'s deprecated factor.interactions, smooth.interactions and size arguments raised an error on values they are documented to accept. A character vector of length greater than one gave the condition has length > 1, and NA gave missing value where TRUE/FALSE needed. All three are now detected with missing(), which is correct for every type, length and NA. As a result smooth.interactions = NA now warns about the deprecation, which it did not do reliably before. - Behaviour change: full_subsets_gam()'s max.models default changes from 500 to 200, matching fit_model_set(). The two disagreed and neither documented its default, so a candidate set of between 201 and 500 models saved its model fits through full_subsets_gam() but not through generate_model_set() plus fit_model_set(). Anyone relying on the old wrapper default for a set in that range will now receive the "model fits will not be saved" warning and an empty success.models; pass max.models = 500 explicitly to keep the previous behaviour. The default is now stated in the documentation of both functions. - full_subsets_gam() gains VI.mods, which it previously did not forward to fit_model_set(), so VI.mods = 'all' was unreachable through the wrapper (FSSgam_package#7). - fit_model_set() and full_subsets_gam() gain progress, defaulting to interactive(). The progress bar was previously written to stdout unconditionally, so suppressing it in a script or report required wrapping the call in capture.output() (FSSgam_package#9). - check_correlations() and check_non_linear_correlations() no longer emit spurious "NaNs produced" warnings when a factor-factor pair is perfectly separated. The deviance is read directly off the nnet::multinom() fit rather than from its summary(), which computes standard errors that were discarded (FSSgam_package#10). Reported correlation values are unchanged. - check_correlations()'s factor-factor diagonal is now exactly 1. It was previously a fitted multinomial pseudo-R2 of about 0.999999, because each factor was regressed on itself (FSSgam_package#12). Off-diagonal values are unchanged. - Bug fix: max.predictors = 1 with factor.factor.interactions built interaction columns it should not have. The enumeration was written as for (i in 2:n), and at n = 1 that sequence counts backwards to c(2, 1), so a size-1 "combination" was enumerated as well. Its pasted name is just the original variable name, so used.data gained a duplicate column per factor, and four "no non-missing arguments to max; returning -Inf" warnings were emitted. No interaction terms are now generated at max.predictors = 1, with a warning that says so; on a two-factor set used.data loses three spurious columns. - Bug fix: factor.factor.interactions given as a character vector failed with arguments imply differing number of rows when every named combination exceeded cov.cutoff. It now warns and continues, as the TRUE form always did. The warning text for both forms now names cov.cutoff and its value, replacing a message that referred to a non-existent cor.cuttoff. - Behaviour change: the collinearity screen that decides which interaction terms are built now tests both halves of the correlation matrix in every case. Three of the four places that perform it tested only the upper triangle, so a pair could be admitted whose correlation exceeded cov.cutoff in the other direction. The matrices are not symmetric: check_correlations() estimates a factor-factor value by fitting multinom() separately in each direction, and check_non_linear_correlations() is asymmetric by construction. In practice this affects only a pair whose two estimates straddle cov.cutoff, which for factor pairs means a difference of under 0.001; a model set containing such a pair will now exclude the interaction it previously included. - factor.factor.interactions given as a character vector no longer skips silently when every named pair exceeds cov.cutoff. An internal guard counted cells of the correlation matrix rather than pairs, so no interaction was built and nothing was reported; it now warns, as the TRUE form does. The model set produced is unchanged. - Bug fix: a name in cyclic.vars was matched against the assembled model terms with an unanchored grep(), so it captured every predictor whose name contained it, and the name was used as a regular expression. Declaring depth cyclic also fitted depthx with bs = "cc", and a predictor name containing a full stop matched any character in that position. The affected models were fitted with the wrong smoothing basis, without error, under a candidate name that looked correct. The basis is now chosen from the variable names as each term is built. - Bug fix: a te() term over three or more predictors was given only two bs values, because the code that assigned them took the first two variables. mgcv warns "bs wrong length and ignored" and substitutes its own default, so both bs.arg and any cyclic.vars were silently discarded for that term. Each marginal now carries its own basis. Reachable through smooth.smooth.interactions given as a character vector with max.predictors >= 3. - Behaviour change: candidate model names are now sorted in byte order, via sort(method = "radix"), and no longer depend on the session's collation locale (FSSgam_package#8). A model named complexity+ZONE in an en_US.UTF-8 session was named ZONE+complexity in a C-locale one, so a saved analysis was not reproducible across machines with different locales. Names are now the C-locale form everywhere. The fitted results are identical, but modname values and the row order of mod.data.out change for anyone working in a non-C locale; a saved model table matched on modname needs regenerating. - Behaviour change: a user-supplied cor.matrix now governs every stage that screens on correlation, not only the exclusion of assembled candidate models (FSSgam_package#13). There are three such stages: which factor-factor interaction columns are built, which te() smooth-smooth interaction terms are built, and which candidates survive. The first two previously recomputed correlations from use.dat and ignored the supplied matrix, so a user who supplied a matrix saying two predictors were uncorrelated still found their te() term absent. Users who do not supply a cor.matrix see no change: the continuous-continuous block does not depend on which other predictors are present, verified against both check_correlations() and check_non_linear_correlations(). A supplied matrix must now include any hard coded factor interaction it causes to be created, which ?generate_model_set already required; missing predictors are reported by name. - Bug fix: a user-supplied cor.matrix did not replace the automatic estimate, it only overrode it. check_correlations() (or check_non_linear_correlations()) was called over every predictor and its result discarded whenever a matrix was supplied, so supplying one neither avoided the multinom() and gam() fits nor allowed a predictor of a class those functions reject -- a Date column, say -- to be used at all. The estimate is now computed only when no matrix is supplied (FSSgam_package#13). - Bug fix: with save.model.fits = FALSE, a run in which no candidate fitted returned a model table of NA with delta.AICc and wi.AICc filled with NaN, rather than the "None of your models fitted successfully" error raised on the save.model.fits = TRUE path. Which of the two happened depended only on a memory setting. Both paths now raise the error. - fit_model_set() and full_subsets_gam() reject a progress value that is not a single TRUE or FALSE, and full_subsets_gam() now validates VI.mods at entry as it already did r2.type, so an unrecognised value is reported before the candidate set is built rather than after. - smooth.smooth.interactions naming a column of use.dat that is not among the predictors is now reported as such. It was previously screened against an empty sub-matrix of the correlation matrix, so combine_uncorrelated() took max() of nothing, warned "no non-missing arguments to max", and built the te() term regardless of its correlations. The error message names the variable and the requirement (it previously said the variable was not supplied in use.dat, which was not what was being checked). - fit_mod_l() is no longer exported. It is an internal helper documented as not called directly, and its arguments changed as recently as the family resolution fix. It remains available as FSSgam:::fit_mod_l(). wi(), extract_mod_dat() and build_inclusion_mat() stay exported, being more plausible to call directly. - Documentation corrections. Twenty-six spelling errors across the reference pages, of which three affected meaning: the correlation values are the square root of an R squared, not the "square-route"; full_subsets_gam()'s description of what it sums referred to "the ?i values" where a character had been lost from "the wi values"; and check_non_linear_correlations()'s note told users to increase cor.cutoff, which is not an argument -- cov.cutoff is. A non-breaking hyphen in the case_study2 documentation is replaced with an ASCII one. - The sixteen argument descriptions shared between generate_model_set() and full_subsets_gam() were duplicated verbatim and had already drifted; they are now inherited, so they cannot drift again. full_subsets_gam()'s null.terms entry gains the sentence about fitting a correlation structure that only generate_model_set() carried. Changes in version 1.0.0 Modernisation release ahead of CRAN submission. - DESCRIPTION updated to use Authors@R, explicit Imports (doSNOW, foreach, gamm4, mgcv, MuMIn, nnet, parallel, stats, utils), and added the package repository to URL. - Removed the unused R.rsp vignette builder declaration; this package does not ship vignettes (see the companion repository at https://github.com/beckyfisher/FSSgam for worked examples). - Added runnable @examples to all exported functions. - Added data-raw/, build_package_FFSgam.R, and CLAUDE.md to .Rbuildignore. - Renamed the core implementations to generate_model_set() and fit_model_set(). generate.model.set() and fit.model.set() remain exported but are now deprecated wrappers (see R/deprecated.R) that warn via .Deprecated() and forward to the new names; full.subsets.gam() calls the new names internally so it does not itself emit deprecation warnings. - Replaced require()/bare calls throughout R/ with explicit package::function() calls, and class(x) == "y" checks with inherits(x, "y"). Note: smooth-term constructors (s(), te(), t2()) are kept unqualified inside model formulas, because mgcv resolves them by literal symbol name during formula parsing rather than through normal function dispatch -- mgcv::s(...) inside a formula does not work. - Bug fix: full.subsets.gam()'s deprecated size argument (superseded by max.predictors) was a no-op -- it warned correctly but never actually fed its value into max.predictors. Calling full.subsets.gam(size = n) now actually constrains the model set to n predictors, as documented. - Bug fix: full.subsets.gam()'s used.data and predictor.correlations output fields were always NULL (a field-name mismatch referenced model.set$use.dat/$cor.matrix, which don't exist -- the actual fields are $used.data/$predictor.correlations). Both are now populated as documented.