--- title: "Missing data that depends on the missing value" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Missing data that depends on the missing value} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4.5, dpi = 150, out.width = "100%" ) ``` ```{r library} library(proxymix) ``` ```{r engines} has_ggplot2 <- requireNamespace("ggplot2", quietly = TRUE) ``` ```{r stored-results, include = FALSE} ## The comparison table reads stored simulation results. They must come ## from the same major.minor version of proxymix as this build. res <- readRDS("results/missing_data_mnar.rds") major_minor <- function(v) paste(unlist(package_version(v))[1:2], collapse = ".") if (major_minor(res$proxymix_version) != major_minor(as.character(packageVersion("proxymix")))) { stop("results/missing_data_mnar.rds was built under proxymix ", res$proxymix_version, ", but this is proxymix ", packageVersion("proxymix"), ". Rerun the simulation and ", "data-raw/vignette_results/missing_data_mnar.R.", call. = FALSE) } ## Small numbers are written as plain decimals rather than in the ## scientific notation that knitr's inline hook would otherwise use. fixed <- function(v, digits) { format(round(v, digits), nsmall = digits, scientific = FALSE) } ``` ## The problem *Imputing missing data with a mixture* shows how to fill the holes in a dataset under one assumption, called missing at random. The chance that a value is missing may depend on the other values in its row, but not on the missing value itself. Often the missing values are mostly the large ones, or mostly the small ones. An assay saturates above some level. A yield is never recorded on the paddocks that did badly. The patients who are most unwell miss their follow-up visit. The values that remain are then unrepresentative, and their mean is biased. Imputation under missing at random removes only part of this bias, because its model is fitted to the same unrepresentative values. Two such cases are common. The first is censoring: a value is missing because it fell below (or above) a known limit, such as the detection limit of an assay. The missing value is then known to lie on the far side of the limit. The second is missing not at random: the chance that a value is missing rises or falls with the value itself, by an unknown amount. The observed data cannot settle how strong the link is. The usual remedy is a sensitivity analysis (Little, 1993): the analysis is repeated under several assumed strengths, and all the results are reported. This vignette works through both cases on simulated data. Because the deleted values are kept, every estimate can be checked against the truth. The vignette then compares proxymix with established packages for each case. ## Package capabilities - `gmm_impute()` fits a Gaussian mixture, a sum of a few normal distributions, to data with holes. It returns `m` completed datasets, as in *Imputing missing data with a mixture*. Its `mechanism` argument specifies how the values came to be missing. - `mar()` is missing at random, the default. - `censored()` is for a missing value known to lie in a range. For example, `censored("y", upper = 0.3)` specifies that each missing `y` lies below 0.3. - `mnar()` is for a value of `y` whose chance of being missing depends on `y` itself. You supply the strength of the link. - `proxy_mnar_sensitivity()` repeats the imputation for a range of strengths and returns the pooled mean of `y` for each. - `proxy_pool()` pools the mean of a column over the completed datasets. Each missing value is drawn from its conditional distribution: the distribution of the missing entry given the observed entries in the same row. Under `censored()`, this distribution is cut off at the limit, and every imputed value falls on the censored side of it. The cut-off distribution has an exact formula. Under `mnar()`, the conditional distribution is weighted by the chance of being missing at each value. Values that were more likely to go missing are then drawn more often. `mnar()` models the chance that `y` is missing as `plogis(alpha + beta * y)`, the logistic curve used in logistic regression. This is the selection model of Diggle and Kenward (1994). The slope `beta` is the strength of the link, and `beta = 0` is missing at random. You supply `beta`. The package sets the intercept `alpha` so that the model gives the observed share of missing values. ```{r mechanism-table, echo = FALSE} knitr::kable( data.frame( mechanism = c("missing at random", "censored", "missing not at random"), known = c("nothing beyond the rest of its row", "it lies beyond a known limit", "its chance of going missing depended on its size"), call = c("`mar()`", "`censored()`", "`mnar()`"), settled = c("yes, once the mixture is assumed", "yes, the limit is known", "only through the assumed shape of `y`"), stringsAsFactors = FALSE ), col.names = c("Mechanism", "What is known about a missing value", "Function", "Can the observed data fix the imputation model?"), caption = "The three mechanisms that `gmm_impute()` accepts." ) ``` ## Addressing the problem ### Data in which the large values go missing The data have 600 rows and two columns, `x1` and `y`. They are drawn from a mixture of two normal distributions of equal size. In each, `x1` and `y` have unit variance and a correlation of 0.6. The chance that `y` is deleted is `plogis(-0.5 + 0.7 * y)`. Larger values of `y` are therefore deleted more often. The complete data are kept as the truth. ```{r dgp} set.seed(20260622) n <- 600L comp <- sample(1:2, n, replace = TRUE) mu <- rbind(c(0, 0), c(1.5, 0.5)) chol_r <- chol(matrix(c(1, 0.6, 0.6, 1), 2L)) z_full <- matrix(rnorm(2 * n), n, 2L) %*% chol_r + mu[comp, ] colnames(z_full) <- c("x1", "y") truth <- mean(z_full[, 2L]) beta_true <- 0.7 miss <- runif(n) < plogis(-0.5 + beta_true * z_full[, 2L]) dat <- z_full dat[miss, "y"] <- NA ``` ### Impute under two assumptions The first imputation assumes missing at random. The second supplies the slope that generated the data, `beta = 0.7`, to `mnar()`. Both make 5 completed datasets. Both allow up to 500 fitting rounds (`max_iter`), the default of `proxy_mnar_sensitivity()` below. Near missing at random, the fitting can take more rounds than the `gmm_impute()` default of 100. ```{r recover} m_draws <- 5L mar_fit <- gmm_impute(dat, N = 2L, m = m_draws, mechanism = mar(), seed = 1L, max_iter = 500L) mnar_fit <- gmm_impute(dat, N = 2L, m = m_draws, mechanism = mnar("y", beta = beta_true), seed = 1L, max_iter = 500L) mar_est <- proxy_pool(mar_fit, "y")$estimate mnar_est <- proxy_pool(mnar_fit, "y", method = "rubin")$estimate ``` For data missing at random, `proxy_pool()` computes the pooled standard error exactly from the fitted mixture. That exact formula does not hold under `censored()` or `mnar()`. These fits are pooled by Rubin's rules instead (Rubin, 1987). These rules average the estimates over the completed datasets and add their spread to the average variance. Called without `method = "rubin"`, `proxy_pool()` switches to Rubin's rules for such fits and prints a message. ```{r recover-table, echo = FALSE} rec_tbl <- data.frame( data_used = c("complete data, before deletion", "rows not deleted", "imputed, missing at random", "imputed, missing not at random (slope 0.7)"), estimate = c(truth, mean(dat[!miss, "y"]), mar_est, mnar_est), stringsAsFactors = FALSE ) rec_tbl$diff <- rec_tbl$estimate - truth knitr::kable( rec_tbl, digits = 3L, col.names = c("Data used", "Mean of y", "Difference from complete data"), caption = paste0( "The mean of y from the complete data, from the rows not deleted, and ", "pooled over each imputation. ", sum(miss), " of ", n, " values of y ", "were deleted." ) ) ``` ### Sweep the assumed slope In real data the slope is unknown. Under this model, the observed data carry some information about the slope, but only through the assumed shape of the distribution of `y`. A different shape could fit the observed data equally well with a different slope. An estimate at a single slope would rest on that shape. `proxy_mnar_sensitivity()` therefore repeats the imputation at each slope in a grid. For each slope, it returns the pooled mean with its 95% confidence interval (CI), the log-likelihood, and whether the fit converged. The log-likelihood measures how well the model with that slope fits the observed data. Higher values mean a better fit. A fit has converged when its rounds settle within the limit of 500. ```{r sweep} sweep <- proxy_mnar_sensitivity(dat, "y", beta_grid = seq(0, 1.2, by = 0.3), N = 2L, m = m_draws, seed = 1L) covers <- sweep$conf.low <= truth & sweep$conf.high >= truth beta_first <- sweep$beta[min(which(covers))] beta_best <- sweep$beta[which.max(sweep$loglik)] ll_gain <- max(sweep$loglik) - sweep$loglik[1L] ``` ```{r sweep-table, echo = FALSE} sweep_tbl <- as.data.frame(sweep)[, c("beta", "estimate", "conf.low", "conf.high", "loglik", "converged")] sweep_tbl$converged <- ifelse(sweep_tbl$converged, "yes", "no") knitr::kable( sweep_tbl, digits = c(1L, 3L, 3L, 3L, 1L, 0L), col.names = c("Assumed slope", "Pooled mean of y", "CI lower", "CI upper", "Log-likelihood", "Converged"), caption = paste0( "The pooled mean of y at each assumed slope. The mean of y in the ", "complete data is ", round(truth, 3), "." ) ) ``` ```{r fig-sweep, eval = has_ggplot2, echo = has_ggplot2, fig.cap = "The pooled mean of y and its 95% confidence interval at each assumed slope. The dashed line is the mean of the complete data. The dotted line marks the slope that generated the data.", fig.alt = "Pooled mean of y against the assumed slope, rising from left to right, with a shaded confidence band, a point at each assumed slope, a dashed horizontal line at the complete-data mean, and a dotted vertical line at the generating slope of 0.7."} sweep_df <- as.data.frame(sweep) ggplot2::ggplot(sweep_df, ggplot2::aes(beta, estimate)) + ggplot2::geom_ribbon( ggplot2::aes(ymin = conf.low, ymax = conf.high), fill = "#56B4E9", alpha = 0.3 ) + ggplot2::geom_line(colour = "#0072B2", linewidth = 0.9) + ggplot2::geom_point(colour = "#0072B2", size = 2) + ggplot2::geom_hline(yintercept = truth, linetype = "dashed", colour = "#000000") + ggplot2::geom_vline(xintercept = beta_true, linetype = "dotted", colour = "#D55E00") + ggplot2::annotate("text", x = min(sweep_df$beta), y = truth, label = "complete-data mean", hjust = 0, vjust = -0.6, size = 3.2) + ggplot2::annotate("text", x = beta_true, y = min(sweep_df$conf.low), label = "generating slope", hjust = 1.05, vjust = 0, size = 3.2, colour = "#D55E00") + ggplot2::labs( x = "assumed slope", y = "pooled mean of y", title = "The pooled mean of y under each assumed slope" ) + ggplot2::theme_minimal(base_size = 11) ``` ```{r fig-sweep-skip, eval = !has_ggplot2, echo = FALSE, results = "asis"} cat("ggplot2 is not installed on this build, so the sensitivity figure", "is skipped. The table above gives the same values.\n") ``` The row at slope 0 makes the same missing-at-random assumption as the earlier table, but `proxy_mnar_sensitivity()` refits the mixture and draws its own completions. Its mean of `r fixed(sweep$estimate[sweep$beta == 0], 3)` therefore differs from the earlier `r fixed(mar_est, 3)` by `r fixed(abs(sweep$estimate[sweep$beta == 0] - mar_est), 3)`. ### Censoring at a known limit When values are missing because they fell beyond a known limit, the mechanism is known and no sweep is needed. Here every value of `y` below 0.3 is hidden, as if 0.3 were the detection limit of an assay. `censored("y", upper = 0.3)` draws each hidden value from the mixture's conditional distribution cut off at 0.3. A common alternative replaces each hidden value with half the detection limit. ```{r censor} thr <- 0.3 cmiss <- z_full[, 2L] < thr cdat <- z_full cdat[cmiss, "y"] <- NA cfit <- gmm_impute(cdat, N = 2L, m = m_draws, mechanism = censored("y", upper = thr), seed = 1L) cens_est <- proxy_pool(cfit, "y", method = "rubin")$estimate half_est <- mean(ifelse(cmiss, thr / 2, z_full[, 2L])) below_half <- mean(z_full[cmiss, 2L] < thr / 2) ``` ```{r censor-table, echo = FALSE} cens_tbl <- data.frame( data_used = c("complete data, before censoring", "rows not censored", "hidden values set to half the limit", "imputed, censored below the limit"), estimate = c(truth, mean(cdat[!cmiss, "y"]), half_est, cens_est), stringsAsFactors = FALSE ) cens_tbl$diff <- cens_tbl$estimate - truth knitr::kable( cens_tbl, digits = 3L, col.names = c("Data used", "Mean of y", "Difference from complete data"), caption = paste0( "The mean of y when values below ", thr, " are hidden. ", sum(cmiss), " of ", n, " values were hidden." ) ) ``` ### Comparison with mice, Amelia, sampleSelection and the Tobit model ```{r compare-facts, include = FALSE} sim_value <- function(design, estimand, method, what) { s1 <- res$sim_tab$design == design & res$sim_tab$estimand == estimand & res$sim_tab$method == method res$sim_tab[[what]][s1] } mv <- function(method, what) sim_value("mnar", "mean", method, what) cv <- function(method, what) sim_value("censored", "slope", method, what) mar_methods <- c("proxymix, slope 0", "mice, shift 0", "Amelia") mar_bias <- vapply(mar_methods, mv, numeric(1L), what = "bias") mar_cov <- vapply(mar_methods, mv, numeric(1L), what = "coverage") and_list <- function(v) { paste(paste(v[-length(v)], collapse = ", "), "and", v[length(v)]) } ``` The examples above use one dataset each. To check proxymix against established tools, a simulation repeated both analyses on `r res$n_rep` datasets of `r res$n` rows each. The first design is the missing-not-at-random example above, at half the size. The target is the mean of `y`. proxymix swept the slopes `r and_list(res$beta_grid)`. `mice` (van Buuren and Groothuis-Oudshoorn, 2011) drew each missing `y` from a normal regression on `x1` and then added a fixed shift to every imputed value. This is called delta adjustment, and a shift of 0 is missing at random. `mice` used the shifts `r and_list(res$delta_grid)`, in units of `y`, in place of proxymix's slopes. `Amelia` (Honaker, King and Blackwell, 2011) assumes missing at random. The Heckman two-step estimator (Heckman, 1979) in `sampleSelection` (Toomet and Henningsen, 2008) models whether `y` is observed and the value of `y` together. It was run with `x1` in both parts and without an exclusion restriction, that is, without a variable that affects whether `y` is observed but not `y` itself. This design has no such variable, and without one the Heckman estimator is known to be unreliable. In the second design, `y` is recorded as zero whenever it falls below zero, which happens to 30% of the values. The target is the slope of `y` on a predictor `x`. Its true value is 1. The Tobit model (Tobin, 1958) treats each zero as a value known only to lie at or below zero. `tobit()` in `AER` (Kleiber and Zeileis, 2008) and `survreg()` in `survival` (Therneau and Grambsch, 2000) fit this model and gave identical results. With proxymix, the zeros were set to missing and imputed with `censored("y", upper = 0)`, and the regression was pooled by Rubin's rules. Each imputer made `r res$m` completed datasets. Every interval is a 95% interval. Coverage is the share of datasets whose interval contained the true value, and it should be close to 0.95. ```{r compare-table, echo = FALSE} cmp_rows <- data.frame( design = c(rep("mnar", 6L), rep("censored", 4L)), method = c("complete data", "available cases", "Amelia", "proxymix, slope 0.7", "mice, shift 0.4", "sampleSelection heckit", "complete data", "zeros at face value", "AER tobit", "proxymix"), label = c("Mean of y: complete data, before deletion", "Mean of y: rows not deleted", "Mean of y: Amelia (missing at random)", "Mean of y: proxymix, slope 0.7", "Mean of y: mice, shift 0.4", "Mean of y: Heckman two-step, no exclusion restriction", "Slope: complete data, before censoring", "Slope: zeros taken at face value", "Slope: Tobit model (AER, survival)", "Slope: proxymix, censored imputation"), stringsAsFactors = FALSE ) cmp_rows$estimand <- ifelse(cmp_rows$design == "mnar", "mean", "slope") cmp_tbl <- data.frame( label = cmp_rows$label, bias = mapply(sim_value, cmp_rows$design, cmp_rows$estimand, cmp_rows$method, "bias"), rmse = mapply(sim_value, cmp_rows$design, cmp_rows$estimand, cmp_rows$method, "rmse"), coverage = mapply(sim_value, cmp_rows$design, cmp_rows$estimand, cmp_rows$method, "coverage"), width = mapply(sim_value, cmp_rows$design, cmp_rows$estimand, cmp_rows$method, "width"), stringsAsFactors = FALSE ) knitr::kable( cmp_tbl, digits = 3L, row.names = FALSE, align = c("l", "r", "r", "r", "r"), col.names = c("Target and method", "Bias", "Error", "Coverage", "Interval width"), caption = paste0( "Results over ", res$n_rep, " simulated datasets per design. Bias is ", "the average difference from the true value, and error is the root ", "mean squared error. The mean of y is ", res$truth[["mnar_mean"]], " in the population, and the true slope is ", res$truth[["cens_slope"]], ". The proxymix and mice rows are the grid ", "values closest to the truth. With ", res$n_rep, " datasets, a ", "coverage near 0.95 has a simulation standard error of about ", fixed(sqrt(0.95 * 0.05 / res$n_rep), 3L), ". The Heckman coverage is ", "over the ", res$n_rep - res$n_undefined, " datasets in which its ", "estimated variance was positive." ) ) ``` Assuming missing at random, proxymix at slope 0, `mice` at shift 0 and `Amelia` all understated the mean by about `r fixed(-mean(mar_bias), 2)`, with intervals that contained it in only `r fixed(min(mar_cov), 2)` to `r fixed(max(mar_cov), 2)` of datasets. At the grid values closest to the truth, proxymix and `mice` did equally well, with coverages of `r fixed(mv("proxymix, slope 0.7", "coverage"), 3)` and `r fixed(mv("mice, shift 0.4", "coverage"), 3)`, although with real data neither the right slope nor the right shift is known. Without an exclusion restriction, the Heckman estimator broke down, with an error of `r round(mv("sampleSelection heckit", "rmse"))` and no valid interval in `r res$n_undefined` of `r res$n_rep` datasets. In the censored design the Tobit model did better than proxymix, with an error of `r fixed(cv("AER tobit", "rmse"), 3)` against `r fixed(cv("proxymix", "rmse"), 3)` and a coverage of `r fixed(cv("AER tobit", "coverage"), 3)` against `r fixed(cv("proxymix", "coverage"), 3)`, even though the proxymix intervals were wider. proxymix is the slowest method here. On one dataset, its sweep over four slopes took `r fixed(res$time_secs[["proxymix sweep"]], 1)` seconds against `r fixed(res$time_secs[["mice sweep"]], 2)` for the four `mice` shifts, and its censored imputation took `r fixed(res$time_secs[["proxymix censored"]], 2)` seconds against `r fixed(res$time_secs[["tobit"]], 3)` for the Tobit fit (median of five runs on one computer). The code below analyses one dataset from each design with every method. It repeats the simulation code for a single dataset. ```{r compare-code, eval = FALSE} library(proxymix) library(mice) library(Amelia) library(AER) library(survival) library(sampleSelection) # one dataset of 300 rows in which larger values of y are more often deleted set.seed(1L) n <- 300L comp <- sample(1:2, n, replace = TRUE) mu <- rbind(c(0, 0), c(1.5, 0.5)) chol_r <- chol(matrix(c(1, 0.6, 0.6, 1), 2L)) z <- matrix(rnorm(2 * n), n, 2L) %*% chol_r + mu[comp, ] full <- data.frame(x1 = z[, 1L], y = z[, 2L]) obs <- full obs$y[runif(n) < plogis(-0.5 + 0.7 * full$y)] <- NA # proxymix: the pooled mean of y at four assumed slopes proxy_mnar_sensitivity(obs, "y", beta_grid = c(0, 0.35, 0.7, 1.05), m = 10L, seed = 1L) # mice: add a fixed shift to every imputed y, then pool the mean lapply(c(0, 0.2, 0.4, 0.6), function(delta) { post <- make.post(obs) post["y"] <- paste0("imp[[j]][, i] <- imp[[j]][, i] + ", delta) imp <- mice(obs, m = 10L, method = "norm", post = post, seed = 1L, printFlag = FALSE) summary(pool(with(imp, lm(y ~ 1))), conf.int = TRUE) }) # Amelia: ten completed datasets, pooled by mice::pool() fits <- lapply(amelia(obs, m = 10L, p2s = 0L)$imputations, function(d) lm(y ~ 1, data = d)) summary(pool(fits), conf.int = TRUE) # sampleSelection: Heckman two-step with x1 in both parts; the mean of y # is the fitted model for y at the mean of x1, with an approximate interval obs$seen <- !is.na(obs$y) fit <- heckit(seen ~ x1, y ~ x1, data = obs) x_bar <- c(1, mean(obs$x1)) est <- sum(x_bar * coef(fit)[3:4]) se <- sqrt(as.numeric(t(x_bar) %*% vcov(fit)[3:4, 3:4] %*% x_bar)) c(estimate = est, conf.low = est - qnorm(0.975) * se, conf.high = est + qnorm(0.975) * se) # one dataset of 300 rows with y recorded as 0 whenever it falls below 0 set.seed(1L) a_cens <- -sqrt(2) * qnorm(0.3) x <- rnorm(n) y_star <- a_cens + x + rnorm(n) cens <- data.frame(x = x, y = pmax(y_star, 0)) # proxymix: set the zeros to missing, draw them below 0, pool the regression holes <- cens holes$y[cens$y == 0] <- NA imp <- gmm_impute(holes, m = 10L, mechanism = censored("y", upper = 0), seed = 1L) summary(pool(lapply(complete(as_mids(imp), "all"), function(d) lm(y ~ x, data = d))), conf.int = TRUE) # the Tobit model, fitted by AER and by survival coef(tobit(y ~ x, data = cens)) coef(survreg(Surv(y, y > 0, type = "left") ~ x, data = cens, dist = "gaussian")) ``` The code needs `mice`, `Amelia`, `AER`, `survival` and `sampleSelection`, all on CRAN, and it is not run when this vignette is built. The [extended version of this article](https://max578.github.io/proxymix/articles/extended/missing_data_mnar.html) gives the full simulation and applies both mechanisms to two real datasets. ## Interpretation Of the `r n` values of `y`, `r sum(miss)` were deleted. Because the larger values were deleted more often, the rows not deleted give a mean of `r fixed(mean(dat[!miss, "y"]), 3)`, against `r fixed(truth, 3)` in the complete data. Imputing under missing at random gives `r fixed(mar_est, 3)`, which is still `r fixed(truth - mar_est, 3)` too low. That imputation model is fitted to the rows that remain, which have too few large values. Supplying the true slope to `mnar()` gives `r fixed(mnar_est, 3)`, within `r fixed(abs(mnar_est - truth), 3)` of the complete-data mean. In the sweep, the pooled mean `r if (all(diff(sweep$estimate) > 0)) "rises at every step" else "changes"` from `r fixed(sweep$estimate[1L], 3)` at slope 0 to `r fixed(sweep$estimate[nrow(sweep)], 3)` at slope `r max(sweep$beta)`. Its interval first contains the complete-data mean at a slope of `r beta_first`. The data were generated with a slope of `r beta_true`. The fit `r if (all(sweep$converged)) "converged at every slope" else paste0("did not converge at slope ", paste(sweep$beta[!sweep$converged], collapse = ", "))`. The log-likelihood is highest at slope `r beta_best`, where it is about `r round(ll_gain)` above its value at slope 0. This difference reflects the assumed two-component shape of `y`, not why the values went missing. A report should show the whole curve and leave the choice of a plausible slope to the reader. In the censoring example, `r sum(cmiss)` values fell below the limit of `r thr`. The rows not censored give a mean of `r fixed(mean(cdat[!cmiss, "y"]), 3)`, which is `r fixed(mean(cdat[!cmiss, "y"]) - truth, 3)` too high. Setting each hidden value to half the limit gives `r fixed(half_est, 3)`, which is still `r fixed(half_est - truth, 3)` too high. Half the limit is a convention for concentrations, which cannot fall below zero. Here `y` can be negative, and `r round(100 * below_half)` per cent of the hidden values lie below `r thr / 2`. The censored imputation gives `r fixed(cens_est, 3)`, within `r fixed(abs(cens_est - truth), 3)` of the complete-data mean. ## Limitations `censored()` and `mnar()` act on one column. A row missing that column is assumed to have all its other columns observed. This suits a detection limit or a single outcome, not holes spread across many columns. Estimates other than a column mean are pooled by passing the completed datasets to `mice` through `as_mids()`. They then rely on the large-sample assumptions of Rubin's rules. The sweep checks one kind of departure from missing at random. The chance of being missing is a logistic curve in the missing value alone, with the intercept set from the observed share of missing values. A mechanism that also depends on a variable not in the data is outside what the sweep covers. The sweep is not a test, and its log-likelihood cannot tell you which slope is right. The range of results is only as wide as the grid you choose. The single-dataset examples use one simulated dataset of `r n` rows with `r m_draws` completed datasets. Their numbers would change with another seed. The settings that matter most are the number of components and the assumed slope. A mixture with too few components cannot represent the two groups in the data. The pooled mean moves steadily as the assumed slope moves away from the truth, as the sweep table shows. With data censored at a known limit, the Tobit model was more accurate than proxymix in the simulation. The proxymix slope was biased by `r fixed(cv("proxymix", "bias"), 3)`, and its 95% intervals contained the true slope in only `r fixed(cv("proxymix", "coverage"), 3)` of datasets. When the outcome follows a normal linear regression below the limit, as in this design, the Tobit model is the better choice. The simulation covers one mixture shape, one sample size, a logistic mechanism on one column, censoring at one known limit, and grids that contain a value near the truth. It does not show how either sweep behaves when its grid does not reach the truth. ## Further reading *Imputing missing data with a mixture* covers data missing at random, the case this vignette sets aside. It shows how a mixture and a single normal distribution differ in the values they impute. *The closed-form operator calculus on a mixture* explains the formulas behind the conditional distributions used here. ## References Diggle, P. and Kenward, M. G. (1994). *Informative drop-out in longitudinal data analysis.* Journal of the Royal Statistical Society C 43(1), 49--93. . Heckman, J. J. (1979). *Sample selection bias as a specification error.* Econometrica 47(1), 153--161. . Honaker, J., King, G. and Blackwell, M. (2011). *Amelia II: A program for missing data.* Journal of Statistical Software 45(7), 1--47. . Hoek, J. van der and Elliott, R. J. (2024). *Mixtures of multivariate Gaussians.* Stochastic Analysis and Applications. . Kleiber, C. and Zeileis, A. (2008). *Applied Econometrics with R.* Springer. . Little, R. J. A. (1993). *Pattern-mixture models for multivariate incomplete data.* Journal of the American Statistical Association 88(421), 125--134. . Rubin, D. B. (1987). *Multiple Imputation for Nonresponse in Surveys.* Wiley. Therneau, T. M. and Grambsch, P. M. (2000). *Modeling Survival Data: Extending the Cox Model.* Springer. . Tobin, J. (1958). *Estimation of relationships for limited dependent variables.* Econometrica 26(1), 24--36. . Toomet, O. and Henningsen, A. (2008). *Sample selection models in R: Package sampleSelection.* Journal of Statistical Software 27(7), 1--23. . van Buuren, S. and Groothuis-Oudshoorn, K. (2011). *mice: Multivariate imputation by chained equations in R.* Journal of Statistical Software 45(3), 1--67. . ## Reproduce The data are generated with seed `20260622`. Every call to `gmm_impute()` and `proxy_mnar_sensitivity()` is given `seed = 1L`, so the completed datasets are reproducible, and the seed does not change the random-number state outside the call. The comparison is read from stored results of a simulation run under proxymix `r res$proxymix_version`, `mice` `r res$versions_desc[["mice"]]`, `Amelia` `r res$versions_desc[["Amelia"]]`, `AER` `r res$versions_desc[["AER"]]`, `survival` `r res$versions_desc[["survival"]]` and `sampleSelection` `r res$versions_desc[["sampleSelection"]]`. It took about `r round(res$elapsed_secs / 60)` minutes on one core and raised `r if (res$n_warnings == 0L) "no warnings" else paste(res$n_warnings, "warnings")`. ```{r session-info, collapse = FALSE, class.output = "session-info"} sessionInfo() ```