threshold = 0.7 a numeric pair
was cut at |r| > 0.7 but a numeric-factor pair only at
|r| > 0.837, so re-encoding a binary column from 0/1
numeric to a two-level factor could change which variables survived.
Numeric-categorical pairs now use eta, the correlation ratio, which is
the multiple correlation between the numeric variable and the factor and
equals the absolute point-biserial correlation for a two-level factor.
Every pair type is now thresholded on one scale, and mixed-type results
may differ from previous versions (#129).criterion = "condition_number" paired each predictor with
the singular value sitting at its column position. Since
svd()$d is ordered by principal direction rather than by
column, the overall condition number always landed on the last column of
the design matrix and pruning stripped terms from the end of the formula
whatever the collinearity structure was. Predictors are now scored
through the Belsley-Kuh-Welsch variance decomposition, as the condition
index of the principal directions carrying their coefficient variance.
On mtcars with limit = 10 this removes
disp instead of gear and carb
(#126).NA whenever a factor carried a level with no observations,
because tapply() fills an empty level with NA
and the between-group sum of squares propagated it. The term for an
empty level is n_g * (xbar_g - xbar)^2 with
n_g = 0, so the association was well defined all along.
This aborted corrPrune(by = ...) on mixed-type data
whenever a single group was missing one factor level, which is the
ordinary case for small groups (#127).std::sort, which leaves
tied elements in an unspecified relative order that can differ between
compilers; the ordering corrSubset(which = "best") and
print() read is now stable and follows the enumeration
order. Separately, corrPrune()’s exact-mode lexicographic
tie-break used the session’s collation locale, so variable names mixing
case or punctuation could select a different subset in two sessions on
the same machine; it now compares in C order (#128).method = "eta" computed
eta (the square root) instead of eta-squared, contradicting its own
documentation.car::vif().(1 | group)) via
unparenthesized term | group fragments was silently
reinterpreted by lme4/glmmTMB as a random
slope instead of a random intercept, and rejected outright for two or
more random-effect terms.NA instead of
floating-point noise or Inf), and fixed a crash when the
surviving fixed-effect terms included interactions or transformations
(poly(), log(), :).data
(character/logical coerced to factor, integer to numeric), so a kept
column’s type could silently change; it is now subset from the caller’s
original, untouched columns.[0, 1]) was validated only when mode = "auto"
routed through MatSelect(); threshold > 1
silently succeeded in mode = "greedy". Now validated once
up front for every mode.by =) aggregation
quantiled signed correlations, letting a strong negative
association in one group be averaged away by a weak positive one in
another; associations are now abs()-clamped before aggregation, matching
the mixed-type code path.NA silently dropped from the group_q
quantile (breaking the documented “group_q = 1 holds in
every group” guarantee), a suppressWarnings() call hid the
informative NA-row-removal warning, n_rows_used
double-counted rows from skipped groups, and NA in the
by column itself silently excluded rows with no warning at
all.force_in association
magnitude is now compared with abs() rather than the signed
value, so negatively-correlated force_in pairs are
correctly rejected in exact mode (greedy mode already matched).by names every
column, instead of silently returning a zero-predictor result.distance/maximal association metrics leaked
upstream package warnings for constant columns, unlike every other
method in the same dispatch; now suppressed to match.assoc_methods_used attribute instead of reusing the table
actually used to compute the matrix, risking drift between the two.corrPrune()’s all-numeric branch while
assocSelect() handled the identical input gracefully;
constant-column zeroing is now applied structurally for every
caller.corrSelect() excluded constant (zero-variance) columns with
a warning, while assocSelect() and corrPrune()
instead kept them and treated their association with everything as
0, so the same data set could return a different variable
set depending on which function was used (#117). All three now exclude
constant columns with a warning; a force_in variable
excluded for being constant now errors with a specific message instead
of falling through to a generic one. Columns constant only within
one group of corrPrune()’s
by/group_q aggregation are unaffected and
still use the previous zero-association handling.site in
(1|site)) for mixed-model engines, so refitting from the
returned data and attr(., "selected_vars") failed with
“object not found”; those columns are now included (#112)..compute_vif()/.compute_condition_indices()
scored a constant single remaining predictor as a “perfect”
1.0 instead of the documented NA, because
their single-predictor shortcut ran before the zero-variance guard; the
guard now runs first (#113).avg_corr/min_corr/max_corr
lengths but not that threshold, search_type,
and cor_method are scalars, so a malformed object (e.g. a
vectorized threshold, or an invalid
search_type) built successfully and produced garbled
print() output; the validator now rejects these at
construction (#114).force_in was resolved
against mat’s column names/count before mat
was validated as a numeric matrix, so an invalid mat
combined with force_in produced a misleading
force_in-flavored error instead of the real “must be a
numeric matrix” error; matrix validation now runs first (#115).corrSelect() already
had.force_in
mutual-violation warning now names the offending pair and value.use_pivot = now errors on
non-coercible input instead of silently falling back to the default, and
warns when supplied together with method = "els" (a no-op
there).force_in is
now validated as whole numbers, matching corrSelect()’s
existing check, so non-integer indices error instead of silently
truncating to a different column.validateMatrixStructure()
let NA/NaN correlation entries silently pass
symmetry and diagonal checks (IEEE-754 comparisons against
NaN are always false), and checked the diagonal only on the
upper-triangular path, letting a symmetric matrix with a wrong diagonal
bypass validation entirely when a backend was called directly.validateForcedIndices()
now deduplicates force_in so
runELS()/runBronKerbosch() can’t return a
variable twice.bron-kerbosch with pivoting) returned scrambled combos,
silently reordering corrPrune()/corrSubset()
output columns.greedyPruneBackend(), the
fourth Rcpp-exported backend, was missed by an earlier shared-validation
refactor and skipped
validateCorMatrix()/validateForcedIndices()
entirely.NAs.corrSelect(cor(mat)) where
MatSelect(cor(mat)) was intended, silently computing
correlations of the correlation matrix rather than using it
directly.force_in
mutual-violation warning (naming the offending pair when forced
variables exceed the threshold against each other) lived only in
MatSelect()’s R layer, so calling the exported
runELS()/runBronKerbosch() directly gave no
signal at all. The check now lives in a shared C++ helper
(utils.cpp) called by both, so every entry point that
forces such variables in also warns about it (#111).summary.CorrCombo(), reporting aggregate
statistics (size range, median, avg_corr range) distinct
from print()’s per-subset listing.runELS()’s degeneracy ordering uses a bucket-queue
instead of an O(m^2) repeated min-degree scan, and no longer
materializes the full n x n compatibility matrix when
force_in restricts the search to a small induced
subgraph.test-brute-force-ground-truth.R: an independent
brute-force maximal-subset enumerator, checked against ELS and
Bron-Kerbosch (with and without pivoting) across 40 random seeds, plus
25 more under force_in constraints – validating maximality
and exhaustiveness simultaneously.force_in cases.corrSelect(),
assocSelect(), and both corrPrune() branches,
with divergent NA/constant-column policies) into shared primitives in
R/assoc-metrics.R, now the single source of truth for every
caller.runELS() was a single
greedy expansion per seed vertex, not an implementation of
Eppstein-Loffler-Strash, and could silently miss valid maximal subsets.
Replaced with a genuine ELS implementation (degeneracy ordering +
per-vertex bounded expansion), sharing a verified Bron-Kerbosch pivot
core with the "bron-kerbosch" method. Verified against
brute-force enumeration.NaN > threshold is
false in C++); NaN now always registers as a threshold
violation.1e-8 tolerance, and could reject matrices the R layer had
already accepted as symmetric. Added a minimum ncol >= 2
guard, and n_rows_used no longer reports a fabricated row
count for matrix input (now NA).force_in indices
were checked against the final (filtered) correlation matrix but never
remapped from the original data frame’s column positions, so a numeric
index could silently force the wrong variable into every subset after
non-numeric or constant columns were dropped.measure argument had no
effect on mixed-type data (numeric-numeric pairs always used Pearson
regardless of the requested measure). It now customizes numeric-numeric
pairs as documented, and the measure actually used per pair-type is
reported via a new assoc_methods_used attribute."x1" matching the
"x10" column). Columns are now resolved via the model’s own
assign bookkeeping.log(mpg) ~ .) crashed during formula parsing.which = "best" on a
CorrCombo with no subsets raised an uninformative
“subscript out of bounds” error instead of a clear message.runELS,
runBronKerbosch) now validate force_in bounds
directly rather than relying solely on the R-level dispatcher.force_in/by overlap detection, and a coverage
warning when most groups are skipped during grouped aggregation in
corrPrune.corrPrune and modelPrune: hand-computed
grouped quantile aggregation, exact-value tie-break tests (lexicographic
and greedy), a greedy-vs-exact identity check, VIF verified against
car::vif(), condition-number verified against a manual SVD
reference, and seed-repeated recovery tests against simulated ground
truth.modelPrune tests that silently passed a
nonexistent threshold argument instead of
limit..Rbuildignore to exclude non-standard hidden
directories flagged by CRAN incoming checks.CorrCombo result class now uses the modern S7 object system
instead of S4. This brings cleaner construction
(CorrCombo(...) instead of
new("CorrCombo", ...)), built-in validation, and
forward-looking OOP design.names property renamed to
var_names: S7 reserves names as a
property name. Code accessing result@names must be updated
to result@var_names. All other @ property
access (@subset_list, @avg_corr, etc.) is
unchanged.methods package is no longer imported;
S7 is now a dependency.Coverage improved from 92% to 94%:
skip_if_not_installed() guardsby parameter
computes association matrices per group and aggregates using the
group_q quantile (default: 0.5 = median). Useful when
correlations vary across experimental conditions or subpopulations.bicor: Biweight midcorrelation (requires WGCNA
package)distance: Distance correlation (requires energy
package)maximal: Maximal information coefficient (requires
minerva package)criterion = "condition_number" option uses SVD-based
condition indices for detecting multicollinearity. Higher values
indicate greater collinearity.isValidAddition,
isValidCombination) from utils.cpp/utils.hThis release addresses reviewer feedback from the JOSS submission.
caret::findCorrelation() to emphasize the key difference
(single solution vs. all maximal subsets)install.packages("corrselect"))suppressWarnings() to hide expected VIF computation
warnings.gitignore to exclude build artifacts
(*.Rcheck/, *.tar.gz,
CRAN-SUBMISSION)modelPrune(): Fixed infinite loop when
VIF computation encountered perfect multicollinearity
Inf and NA VIF
values in pruning loopmodelPrune(): Fixed design matrix
extraction for lme4 and glmmTMB engines
stats::model.matrix() for all engines (more
robust)Version 3.0.0 represents a major expansion of corrselect from a specialized subset enumeration tool into a comprehensive predictor pruning toolkit. Fully backward compatible with 2.x - all existing code continues to work.
corrPrune(): High-level
association-based predictor pruning
measure = "auto")force_in parameter to protect important predictorsmodelPrune(): Model-based predictor
pruning using diagnostics
lm, glm,
lme4, glmmTMBforce_in parameter for protecting important
variablescorrPrune(mode = "greedy") and
mode = "auto"corrSelect(),
assocSelect()) now integrate seamlessly with
corrPrune()modelPrune(): Users can define custom modeling backends
with fit and diagnostics functions, enabling
integration with any R modeling packagecorrPrune() and
modelPrune()lme4 and glmmTMB to Suggests
(required for respective engines)corrPrune(mode = "auto") for best performanceinstall.packages(c("lme4", "glmmTMB"))force_in in MatSelect() now correctly
accepts character column names.els now correctly lists all valid subsets when a single
variable is forced in.corrSelect() now displays an appropriate warning if
only one variable remains after dropping unsupported columns.assocSelect() now
safely falls back to 0 for failed or meaningless associations
(e.g. empty chi-squared tables due to sparse combinations or unused
factor levels).assocSelect() now supports logical columns by
automatically converting them to factors.Version 2.0.0 introduces support for mixed-type data through the new
assocSelect() function, enabling subset selection on
datasets containing numeric, factor, and ordered variables.
assocSelect(): New function for
mixed-type data frame interface