Fast masked k-nearest neighbor imputation for mixed-type tabular data. Distances are computed only on the variables jointly observed by the receiver and the donor, and missing values are filled by local regression (numeric) or weighted vote (categorical) over the nearest donors. Returns completed data for single imputation or a list of completed data sets for multiple imputation.
missknn is an R package for fast masked k-nearest neighbor imputation
of missing values in tabular data.
Install the released version from CRAN with:
install.packages("missknn")
Or install the development version from GitHub with:
install.packages("remotes")
remotes::install_github("ielbadisy/missknn")
missknn() is now an imputer only and returns completed data
directly: a data.frame when m = 1, a list of m completed data
frames (class c("mimar_mi", "list")) when m >= 2.complete(), the missknn object class and its print()/summary()
methods are removed; pool analyses with a dedicated package such as
mimar (mimar::pool_lm()).data.table is no longer a dependency: tabular handling uses
basetable (>= 1.4.2) and outputs are plain data frames. Only NA
cells are imputed (blank strings stay a valid category) and ordered
factors are restored as ordered factors."tuning" attribute now reports the number of missing values
per variable.It is fast by design:
k selection and a batched per-column API
that avoids per-cell R/C++ round tripsk and estimator (distance-weighted mean vs. a local
weighted-regression fit) are chosen automatically from a holdout
evaluation, rather than fixed globallydonor_cap bounds the neighbor-search candidate pool so cost stays
roughly linear in n instead of quadratic, which matters at large nbasetable backend for tabular handling; input is a data frame or
matrix, output is plain data framesm > 1) can run multicore by default on
Unix-like systems via parallel::mclapplyk/estimator
tuning and the deterministic (non-stochastic) neighbor search are
parallelized across columns/receivers with RcppParallel, on all
platforms including WindowsIt supports:
mimarmissknn is developed and maintained by Imad El Badisy
(elbadisyimad@gmail.com), released under the MIT license.
The current implementation parallelizes the per-column k/estimator
tuning and the deterministic single-imputation path with RcppParallel,
including on Windows. The m > 1 stochastic path still uses
process-level parallelism via parallel::mclapply on Unix-like systems.
missknn parallelizes at two independent levels:
m > 1): each of the m completed datasets
is generated in a separate process via parallel::mclapply,
controlled by parallel_cores (defaults to detectCores() - 1). This
only applies on Unix-like systems; it falls back to serial on Windows.k and estimator, and the deterministic (m = 1)
neighbor search/aggregation, are split across threads with
RcppParallel::parallelFor (across target columns for tuning, across
receiver rows for imputation). This runs on every platform, including
Windows, and benefits m = 1 runs that get no benefit from
parallel_cores. The stochastic sampling path used when m > 1 stays
single-threaded per process, since R’s RNG isn’t thread-safe; that
case still parallelizes across the m processes instead.Thread count for the RcppParallel layer follows the usual
RcppParallel::setThreadOptions() / RCPP_PARALLEL_NUM_THREADS
conventions.
imp <- missknn(big_data, k = 5, progress = TRUE)
With progress = TRUE, missknn() shows a live, self-contained (base R
only, no extra dependency) progress bar for each tuning stage
(Tuning numeric columns, Tuning categorical columns) and for each
column that goes through the neighbor search (Imputing <column>),
updating in place with a percentage and an ETA, e.g.:
Imputing x2 [===========>------------------] 38% ETA: 4s
Columns with no locally exploitable signal (filled directly from the
global mean/mode, see the missknn() API entry below) don’t get a bar,
since they skip the neighbor search entirely.
Because RcppParallel worker threads can’t safely drive a progress
display, requesting progress = TRUE runs that column’s search on a
single thread instead of in parallel: an explicit, visible-vs-fast
tradeoff, not a bug. Leave progress = FALSE (the default) for the
fastest run. progress is also automatically disabled whenever m > 1
with parallel_cores > 1, since console output from
parallel::mclapply’s forked worker processes can’t be shown live.
Two benchmark scripts write reports, plots, and CSV tables to
inst/benchmark/output/:
# accuracy/runtime vs. missForest, missMDA, VIM::kNN at n = 200, 30 simulations
Rscript inst/benchmark/benchmark_30.R
# runtime/accuracy vs. missMDA and missForest as n scales from 1,000 to 100,000
Rscript inst/benchmark/benchmark_bigdata.R
missknn(data, k = 5L, m = 1L, scale = TRUE, add_indicator = FALSE, seed = NULL, weights = c("distance", "uniform"), numeric_estimator = c("regression", "mean"), ridge = 1e-4, donor_cap = 2000L, max_iter = 1L, parallel_cores = missknn_default_cores(), progress = FALSE)Fits masked KNN imputation and returns the completed data.
| Argument | Description |
|---|---|
data |
A data.frame or matrix. |
k |
Number of nearest donors used per target column. |
m |
Number of completed datasets. m = 1 is single (deterministic) imputation; m > 1 is multiple (stochastic donor-sampling) imputation. |
scale |
Standardize numeric variables before computing distances. |
add_indicator |
Append 0/1 integer missingness-indicator columns (<name>_missing) to the completed output. |
seed |
Integer seed for reproducible multiple imputation. |
weights |
"distance" (inverse-distance weighted aggregation) or "uniform" (equal weights) over the k donors. |
numeric_estimator |
"regression" fits a distance-weighted local linear regression of the target on the receiver’s observed numeric predictors over the k donors, falling back to the weighted mean when the local design is degenerate; "mean" always uses the weighted mean. |
ridge |
Ridge penalty added to the predictor covariance under numeric_estimator = "regression". |
donor_cap |
Caps the donor pool searched per target column, subsampling once per column when exceeded, so search cost stays roughly linear in n. Does not affect the global mean/mode fallback, which always uses every donor. |
max_iter |
Number of single-pass refinement iterations over the whole dataset. |
parallel_cores |
Cores used across the m completed datasets when m > 1 (via parallel::mclapply, Unix-like only). Defaults to detectCores() - 1. Independent of the RcppParallel threading used within a single imputation (see Parallel computing). |
progress |
Show a live console progress bar per tuning stage and per imputed column (see Progress reporting). Default FALSE. |
Columns with no locally exploitable signal are detected during fitting and filled from the global mean/mode directly, without a neighbor search.
Returns completed data directly:
m = 1: a data.frame with the same columns and column types as the
input.m >= 2: a list of m completed data frames with class
c("mimar_mi", "list").missknn is an imputer only. Fit the analysis model on each completed
data set and pool the fits with Rubin’s rules using a dedicated package
such as mimar (mimar::pool_lm()).
library(missknn)
x <- data.frame(
a = c(1.2, 2.1, NA, 4.3, 5.0, 6.4, NA, 8.1),
b = c(2.5, NA, 3.1, 5.2, 4.8, NA, 7.9, 8.3),
g = factor(c("x", "x", "y", NA, "y", "x", "y", NA))
)
missknn(x, k = 2)
#> a b g
#> 1 1.200000 2.500000 x
#> 2 2.100000 2.641362 x
#> 3 3.450772 3.100000 y
#> 4 4.300000 5.200000 y
#> 5 5.000000 4.800000 y
#> 6 6.400000 8.117406 x
#> 7 7.621611 7.900000 y
#> 8 8.100000 8.300000 y
Each result carries a "tuning" attribute giving, per variable, the
number of missing values, the tuned k, the estimator used and
whether it was filled from the global mean/mode:
attr(missknn(x, k = 2), "tuning")
#> variable missing k estimator global
#> a a 2 2 regression FALSE
#> b b 2 2 regression FALSE
#> g g 2 2 vote FALSE
Multiple imputation returns a list of completed data frames, ready for
analysis and pooling with mimar:
# multiple imputation: a list of 5 completed data frames
imps <- missknn(x, k = 2, m = 5, seed = 1, parallel_cores = 1L)
class(imps)
#> [1] "mimar_mi" "list"
imps[[2]]
#> a b g
#> 1 1.2 2.5 x
#> 2 2.1 2.5 x
#> 3 1.2 3.1 y
#> 4 4.3 5.2 y
#> 5 5.0 4.8 y
#> 6 6.4 8.3 x
#> 7 8.1 7.9 y
#> 8 8.1 8.3 y
fits <- lapply(imps, function(d) lm(a ~ b, data = d))
mimar::pool_lm(fits)
#> Pooled linear model
#> Pooled across m = 5 imputations (Rubin's rules)
#>
#> Estimate Std. Error df t value Pr(>|t|)
#> (Intercept) -0.2727 1.6125 2.123 -0.169 0.8805
#> b 0.9660 0.2412 3.110 4.004 0.0261 *
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Runtime and NRMSE from n = 1,000 up to n = 100,000 (5 numeric columns,
20% MCAR per column). missForest and VIM::kNN are only run up to n =
5,000 (per-tree cost and O(n^2) distance search respectively make larger
n impractical); missknn and missRanger run across the full grid.
Reproduce with:
Rscript inst/benchmark/benchmark_bigdata.R
Runtime plot
NRMSE plot
| n | method | runtime_sec | nrmse |
|---|---|---|---|
| 1,000 | missknn | 0.0131 | 1.00223 |
| 1,000 | missRanger | 0.1630 | 1.15781 |
| 1,000 | missForest | 3.1537 | 1.07461 |
| 1,000 | VIM::kNN | 0.6088 | 1.16132 |
| 5,000 | missknn | 0.0321 | 1.00020 |
| 5,000 | missRanger | 1.3632 | 1.14478 |
| 5,000 | missForest | 75.1881 | 1.03685 |
| 5,000 | VIM::kNN | 10.9076 | 1.15992 |
| 20,000 | missknn | 0.0456 | 0.99998 |
| 20,000 | missRanger | 10.3372 | 1.14941 |
| 100,000 | missknn | 0.0938 | 1.00000 |
| 100,000 | missRanger | 85.6539 | 1.14435 |
Runtime and NRMSE by method and sample size
missForest’s runtime already becomes impractical at n = 5,000 (over a
minute) and would take hours at n = 100,000; VIM::kNN’s distance
search is similarly infeasible past a few thousand rows. missknn’s
capped-donor masked-KNN search stays close to linear in n and keeps
running in well under a second even at n = 100,000. missRanger also
scales to n = 100,000 but at substantially higher runtime.
citation("missknn")
#> To cite missknn in publications, use:
#>
#> El Badisy I (2026). _missknn: Fast Masked K-Nearest Neighbor
#> Imputation_. R package version 1.2.0,
#> <https://CRAN.R-project.org/package=missknn>.
#>
#> A BibTeX entry for LaTeX users is
#>
#> @Manual{,
#> title = {missknn: Fast Masked K-Nearest Neighbor Imputation},
#> author = {Imad {El Badisy}},
#> year = {2026},
#> note = {R package version 1.2.0},
#> url = {https://CRAN.R-project.org/package=missknn},
#> }