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