Getting started with missknn

Overview

missknn imputes missing values in tabular data with a masked k-nearest-neighbor engine: distances are computed only on jointly observed variables, the compute-heavy steps (distance, neighbor ranking, aggregation) run in C++, and each column’s neighborhood size and estimator are chosen automatically from a small internal holdout evaluation rather than fixed globally.

library(missknn)

Single imputation

set.seed(1)
x <- data.frame(
  a = c(1, 2, NA, 4, 5),
  b = c(2, NA, 3, 4, 6),
  g = factor(c("x", "x", "y", NA, "y"))
)

missknn(x, k = 2)
#>         a        b g
#> 1 1.00000 2.000000 x
#> 2 2.00000 2.336285 x
#> 3 2.74969 3.000000 y
#> 4 4.00000 4.000000 y
#> 5 5.00000 6.000000 y

With m = 1 (the default) missknn() returns the completed data.frame directly, with the same columns and column types as the input.

missknn() detects numeric and categorical (factor/character/logical) columns automatically, and imputes both from the same masked-distance neighborhood: numeric targets get a distance-weighted mean or a local regression fit (whichever validates better for that column), categorical targets get a distance-weighted majority vote.

Multiple imputation

Setting m > 1 draws m completed datasets by sampling (rather than averaging) from each receiver’s nearest donors, using the same distance model throughout:

imps <- missknn(x, k = 2, m = 5, seed = 1, parallel_cores = 1L)
class(imps)
#> [1] "mimar_mi" "list"
length(imps)
#> [1] 5

The result is a list of m completed data frames (class mimar_mi). missknn is an imputer only: fit the analysis model on each completed data set, then combine the fits with Rubin’s rules, for example with mimar::pool_lm():

fits <- lapply(imps, function(d) lm(a ~ b + g, data = d))
mimar::pool_lm(fits)

Per-column adaptive tuning

Rather than using one fixed k for every column, missknn() runs a small internal holdout evaluation per column to choose:

The choices are returned in the "tuning" attribute of the result:

attr(missknn(x, k = 2), "tuning")
#>   variable missing k  estimator global
#> a        a       1 2 regression  FALSE
#> b        b       1 2 regression  FALSE
#> g        g       1 2       vote  FALSE

Why this matters for larger data

Naive KNN imputation searches every donor for every missing value: cost grows quadratically with the number of rows. missknn bounds the neighbor-search candidate pool with donor_cap (default 2000), so cost stays close to linear in n even for large datasets, while the per-column global fast path above further skips neighbor search for columns where it wouldn’t help anyway. See vignette companions in inst/benchmark/ for runtime and accuracy comparisons against missForest, missRanger, and VIM::kNN, including a scaling study from n = 1,000 to n = 100,000.

Options at a glance

missknn(
  data,
  k = 5L,                 # default neighbor count (tuned per column)
  m = 1L,                 # 1 = single imputation, >1 = multiple imputation
  scale = TRUE,            # standardize numeric variables before distances
  weights = "distance",    # "distance" (inverse-distance) or "uniform"
  numeric_estimator = "regression", # "regression" or "mean"
  donor_cap = 2000L,       # cap on the neighbor-search candidate pool
  add_indicator = FALSE,   # append missingness indicator columns
  seed = NULL              # reproducible multiple imputation
)