every uncorrelated subset of predictors
Exact enumeration of maximal predictor sets under a correlation threshold.
Hand it a data frame and a threshold. corrselect finds every maximal set of predictors whose pairwise correlations all stay under that threshold, by graph enumeration (Bron–Kerbosch / Eppstein–Löffler–Strash) in C++, and returns whichever view you need: a single pruned data frame, or every valid subset to choose from.
library(corrselect)
# prune to one low-correlation set, in a single call
corrPrune(mtcars, threshold = 0.7)
# or enumerate *every* maximal low-correlation set
corrSelect(mtcars, threshold = 0.7)Every valid choice, ranked
corrSelect() returns every maximal low-correlation subset, ranked by size then average correlation:
res <- corrSelect(mtcars, threshold = 0.7)
res
#> CorrCombo object
#> -----------------
#> Method: bron-kerbosch
#> Correlation: pearson
#> Threshold: 0.700
#> Subsets: 15 maximal subsets
#> Data Rows: 32 used in correlation
#> Pivot: TRUE
#>
#> Top combinations:
#> No. Variables Avg Max Size
#> ------------------------------------------------------------
#> [ 1] mpg, drat, qsec, gear, carb 0.416 0.700 5
#> [ 2] cyl, drat, qsec, gear, carb 0.434 0.700 5
#> [ 3] mpg, drat, vs, gear, carb 0.466 0.700 5
#> ... (12 more combinations)
corrSubset(res, mtcars, which = "best") # pull the top-ranked subset back out as a data frameKeeping variables you already trust, across groups
force_in protects variables that must survive pruning regardless of what else gets dropped; by requires the threshold to hold separately inside every group:
Mixed-type data
assocSelect() picks the right association measure for each pair of columns (Pearson for numeric-numeric, eta-squared for numeric-factor, Cramer’s V for factor-factor), and enumerates maximal subsets under all of them at once:
df <- data.frame(
height = rnorm(30, 170, 10),
weight = rnorm(30, 70, 12),
group = factor(sample(c("A", "B"), 30, TRUE)),
rating = ordered(sample(c("low", "med", "high"), 30, TRUE))
)
assocSelect(df, threshold = 0.6)corrPrune() runs the same mixed-type dispatch under the hood, so a single pruning call works whether the input is all-numeric or a mix of numeric, factor, and ordered columns. Six numeric-numeric measures are available, selectable via measure / method_num_num: "pearson", "spearman", "kendall", "bicor" (WGCNA), "distance" (energy), and "maximal" (minerva).
Model-based pruning with any engine
modelPrune() removes predictors by VIF or condition number, refitting after each removal. Built-in engines cover lm, glm, lme4, and glmmTMB; anything else plugs in as a two-function engine:
# linear model, VIF threshold
modelPrune(mpg ~ cyl + disp + hp + wt, data = mtcars, limit = 5)
# any modeling package, via a custom engine (here: INLA)
inla_engine <- list(
name = "inla",
fit = function(formula, data, ...) {
INLA::inla(formula, data = data, family = "gaussian", ...)
},
diagnostics = function(model, fixed_effects) {
scores <- model$summary.fixed[, "sd"] # posterior SD as the badness score
setNames(scores, rownames(model$summary.fixed))[fixed_effects]
}
)
modelPrune(y ~ x1 + x2, data = df, engine = inla_engine, limit = 0.5)
corrPrune or modelPrune?
corrPrune() |
modelPrune() |
|
|---|---|---|
| Needs a model? | No | Yes |
| Based on | Pairwise correlation / association | Model diagnostics (VIF) |
| Works without a response? | Yes | No |
| Mixed models? | No | Yes (lme4, glmmTMB) |
| Best for | Exploratory analysis, large p
|
Regression workflows, VIF reduction |
Use corrPrune() first to cut dimensionality, then modelPrune() for final cleanup inside a modeling framework.
Exact where it matters, greedy where it scales
corrPrune() picks a search mode for you: exact enumeration up to max_exact_p predictors (100 by default), and a greedy C++ backend beyond that. Exact mode guarantees the largest subset satisfying the threshold; greedy mode trades that guarantee for speed on wide data. Both are available directly, so you can pin one explicitly:
corrPrune(mtcars, threshold = 0.7, mode = "exact") # guaranteed-maximal, small/medium p
corrPrune(mtcars, threshold = 0.7, mode = "greedy") # fast, approximate, any pcaret::findCorrelation() solves this with greedy iterative removal, which is order-dependent and can vary between runs on the same data. corrPrune(mode = "exact") solves the same threshold constraint by maximal-clique enumeration, retaining at least as many variables and returning the same answer every run. The Comparison vignette walks through this and three other alternatives (Boruta, glmnet, manual VIF removal) side by side on the same dataset.
What’s in the box
-
corrPrune(): association-based pruning, model-free. Exact mode forp <= 100, greedy mode for largerp, protect variables withforce_in, hold the threshold across groups withby. -
modelPrune(): VIF-based pruning forlm,glm,lme4,glmmTMB, or any custom engine (INLA, mgcv, brms, …). -
corrSelect()/MatSelect(): exhaustive enumeration of all maximal sets, on a data frame or directly on a correlation matrix. -
corrSubset(): pull one or more maximal subsets back out of aCorrComboas data frames. -
assocSelect(): mixed-type data (numeric, factor, ordered), with the right association metric chosen per pair.
Installation
install.packages("corrselect") # CRAN
install.packages("pak") # development version
pak::pak("gcol33/corrselect")Support
“Software is like sex: it’s better when it’s free.” — Linus Torvalds
I’m a PhD student who builds R packages in my free time because I believe good tools should be free and open. I started these projects for my own work and figured others might find them useful too.
If this package saved you some time, buying me a coffee is a nice way to say thanks. It helps with my coffee addiction.
Citation
@article{corrselect,
author = {Colling, Gilles},
title = {corrselect: Fast and flexible predictor pruning for data analysis and modeling},
journal = {Journal of Open Source Software},
year = {2026},
volume = {11},
number = {118},
pages = {9539},
doi = {10.21105/joss.09539},
url = {https://joss.theoj.org/papers/10.21105/joss.09539}
}