This reference covers inject_missingness(), a transformer that takes a complete data frame and injects NAs under a chosen missingness mechanism from the Rubin taxonomy — MCAR (completely at random), MAR (at random, driven by an observed predictor), and MNAR (not at random, driven by the missing value itself). The returned object carries a "missing_info" attribute recording exactly which cells were made missing.

inject_missingness()

Inject missing values into target columns under a known mechanism. MAR requires an observed predictor column (which is itself never made missing); MNAR sets larger numeric values missing more often. All three mechanisms are calibrated so the realized missing fraction is approximately prop.

Signature

args(inject_missingness)
#> function (data, mechanism = c("MCAR", "MAR", "MNAR"), prop = 0.1, 
#>     cols = NULL, predictor = NULL, seed = NULL) 
#> NULL

We start from a small complete data frame with a regression structure so that x1 can serve as a MAR predictor of y.

df <- simulate_prediction(
  n = 500,
  coefs = c("(Intercept)" = 1, x1 = 2, x2 = -1),
  seed = 1
)$data
head(df)
colSums(is.na(df))   # no missingness yet
#>  y x1 x2 
#>  0  0  0

MCAR — missing completely at random

Every cell of the target columns is independently set missing with probability prop.

mcar <- inject_missingness(df, mechanism = "MCAR", prop = 0.2, seed = 7)
colSums(is.na(mcar))
#>   y  x1  x2 
#>  96 104 108
attr(mcar, "missing_info")$realized_prop
#> [1] 0.2053333
str(attr(mcar, "missing_info"), max.level = 1)
#> List of 6
#>  $ mechanism    : chr "MCAR"
#>  $ prop         : num 0.2
#>  $ cols         : chr [1:3] "y" "x1" "x2"
#>  $ n_missing    : int 308
#>  $ realized_prop: num 0.205
#>  $ indicator    : logi [1:500, 1:3] FALSE FALSE TRUE TRUE FALSE FALSE ...
#>   ..- attr(*, "dimnames")=List of 2

MAR — missing at random (driven by observed x1)

Here only y is targeted, and its missingness probability depends on the observed predictor x1 — never on y’s own value.

mar <- inject_missingness(df, mechanism = "MAR", prop = 0.2,
                          cols = "y", predictor = "x1", seed = 7)
colSums(is.na(mar))
#>  y x1 x2 
#> 96  0  0
attr(mar, "missing_info")$realized_prop
#> [1] 0.192

The dependence on the predictor is visible: rows with missing y have a systematically different mean x1 than rows with observed y.

miss_y <- is.na(mar$y)
tapply(df$x1, ifelse(miss_y, "y missing", "y observed"), mean)
#>  y missing y observed 
#>  0.7204732 -0.1431767

MNAR — missing not at random (driven by y itself)

The probability that y is missing depends on y’s own (latent) value: larger values are more likely to go missing.

mnar <- inject_missingness(df, mechanism = "MNAR", prop = 0.2,
                           cols = "y", seed = 7)
colSums(is.na(mnar))
#>  y x1 x2 
#> 92  0  0
attr(mnar, "missing_info")$realized_prop
#> [1] 0.184

The MNAR signature: the observed (non-missing) y mean is shifted relative to the full-data mean, because the largest values were preferentially removed.

c(full_mean     = mean(df$y),
  observed_mean = mean(mnar$y, na.rm = TRUE))
#>     full_mean observed_mean 
#>     1.0882401     0.6747463

Visualising the missingness pattern

An image() of the missing-cell indicator shows where the NAs landed. Under MCAR they scatter uniformly across all columns; under MAR/MNAR they concentrate in the single targeted column y.

op <- par(mfrow = c(1, 3), mar = c(3, 4, 3, 1))
show_pattern <- function(x, ttl) {
  m <- is.na(x[, c("y", "x1", "x2")])
  image(seq_len(ncol(m)), seq_len(nrow(m)), t(m),
        col = c("#ece7f2", "#d95f0e"), axes = FALSE,
        xlab = "", ylab = "row", main = ttl)
  axis(1, at = seq_len(ncol(m)), labels = colnames(m))
}
show_pattern(mcar, "MCAR")
show_pattern(mar,  "MAR (y ~ x1)")
show_pattern(mnar, "MNAR (y ~ y)")

par(op)