AIC with missing values: the silent row drop

R
model selection
AIC
missing data
ecology tutorial
R fits every model to whatever rows are complete for it, so an AIC table over covariates with different missingness ranks the gaps, not the ecology. Shown in R.
Author

Tidy Ecology

Published

2026-08-06

Modified

2026-10-14

Corrected 14 October 2026: the post said R runs such a comparison without complaint; it now says that AIC called on several models at once warns when they were fitted to different numbers of observations, though a table built one model at a time does not. It also no longer says that comparing row counts catches every case, since two models can drop the same number of different plots, and it ties densities above one to a small spread in the recorded units rather than to small units.

Corrected 1 October 2026: a section heading still called the direction of the artefact fixed without its condition, and now says it is fixed here; the post also said the four models separate only once multiplied by their row counts, and it now says most of the separation comes from that, since their fits per observation differ a little too.

Corrected 30 September 2026: the post said the direction of the row-drop artefact is fixed and general; it now says the direction holds when every row adds a negative log-likelihood term, as for counts and this response, and can reverse for a continuous response with densities above one.

Corrected 25 September 2026: the honest limits said complete-case analysis is unbiased only when values are missing completely at random, which is too strict for a regression. A new section measures what averaging AIC across imputed data sets does, and what to pool instead.

Field data has holes. The soil chemistry came back for most plots but not all, the canopy measurement was skipped when the weather turned, one logger failed in July. You build a candidate model set out of the covariates you have, run AIC, and read off the winner.

R fits every candidate without complaint, and the answer can be entirely an artefact of where the holes are. Called on several models at once, AIC does warn that they were not all fitted to the same number of observations, but it still returns the scores, and a table that calls it on one model at a time, as below, prints no warning at all. Each model is fitted to the rows that are complete for the variables in that model, so different candidates are fitted to different data sets. Their log-likelihoods are sums over different numbers of rows, which makes them incomparable, and whenever each row adds a negative term, as it does for counts and for the response here, the incomparability points in a fixed direction: the model with the most missing data gets the best score.

A candidate set where the answer is known

Two hundred plots. The response depends on x1 and nothing else. x2 is a covariate with no effect at all that is missing for thirty per cent of plots; x3 is another null covariate missing for ten per cent.

library(ggplot2)

te_paper  <- "#f5f4ee"
te_ink    <- "#16241d"
te_body   <- "#2c3a31"
te_forest <- "#275139"
te_rust   <- "#b5534e"
te_gold   <- "#c9b458"
te_line   <- "#dad9ca"

theme_datasheet <- function() {
  theme_minimal(base_size = 12) +
    theme(plot.background  = element_rect(fill = te_paper, colour = NA),
          panel.background = element_rect(fill = te_paper, colour = NA),
          panel.grid.major = element_line(colour = te_line, linewidth = 0.3),
          panel.grid.minor = element_blank(),
          text             = element_text(colour = te_body),
          plot.title       = element_text(colour = te_ink, face = "bold"),
          axis.text        = element_text(colour = te_body))
}

make_plots <- function(n = 200, miss2 = 0.30, miss3 = 0.10) {
  d <- data.frame(x1 = rnorm(n), x2 = rnorm(n), x3 = rnorm(n))
  d$y <- 1.5 * d$x1 + rnorm(n)              # x2 and x3 do nothing
  d$x2[sample(n, round(miss2 * n))] <- NA
  d$x3[sample(n, round(miss3 * n))] <- NA
  d
}

set.seed(20260806)
plots <- make_plots()
colSums(is.na(plots))
x1 x2 x3  y 
 0 60 20  0 

The four candidates are the true model, the true model plus each null covariate, and the full model. Nothing here is subtle: x2 and x3 are noise, and any honest procedure should put y ~ x1 first.

forms <- list(true      = y ~ x1,
              plus_x3   = y ~ x1 + x3,
              plus_x2   = y ~ x1 + x2,
              full      = y ~ x1 + x2 + x3)

fits <- lapply(forms, lm, data = plots)
aic_tab <- data.frame(model = names(forms),
                      rows  = sapply(fits, nobs),
                      aic   = sapply(fits, AIC))
aic_tab$delta <- aic_tab$aic - min(aic_tab$aic)
aic_tab[order(aic_tab$aic), ]
          model rows      aic     delta
full       full  125 322.5271   0.00000
plus_x2 plus_x2  140 369.8157  47.28857
plus_x3 plus_x3  180 477.4162 154.88914
true       true  200 539.8538 217.32669

The full model wins, by 217.3 AIC units over the model that generated the data. On the usual reading that is overwhelming support: a difference of ten is normally treated as decisive, and this is more than twenty times that.

Look at the rows column. The four models were fitted to 200, 180, 140 and 125 plots respectively. The AIC ordering is the row-count ordering read backwards. No warning was printed, since each AIC was computed on its own, and nothing in the printed table would tell a reader which models saw which plots unless they asked.

shown <- c(true = "y ~ x1", plus_x3 = "y ~ x1 + x3",
           plus_x2 = "y ~ x1 + x2", full = "y ~ x1 + x2 + x3")
mirror <- rbind(
  data.frame(model = aic_tab$model, panel = "delta AIC", value = aic_tab$delta),
  data.frame(model = aic_tab$model, panel = "plots used", value = aic_tab$rows))
mirror$model <- factor(shown[mirror$model],
                       levels = shown[aic_tab$model[order(aic_tab$rows)]])
mirror$panel <- factor(mirror$panel, levels = c("delta AIC", "plots used"))

ggplot(mirror, aes(y = model, x = value, fill = panel)) +
  geom_col(width = 0.6) +
  geom_text(aes(label = round(value, 1)), hjust = -0.15, size = 3.4,
            colour = te_body) +
  facet_wrap(~ panel, scales = "free_x") +
  scale_x_continuous(expand = expansion(mult = c(0, 0.22))) +
  scale_fill_manual(values = c("delta AIC" = te_rust, "plots used" = te_forest)) +
  labs(x = NULL, y = NULL, fill = NULL,
       title = "The ranking is the missingness, upside down") +
  theme_datasheet() +
  theme(legend.position = "none",
        strip.text = element_text(colour = te_ink, face = "bold"))
Two horizontal bar panels sharing the same four model labels. In the left panel the delta AIC bars grow as the row count in the right panel grows, so the best-scoring model is the one fitted to the fewest plots.
Figure 1: Delta AIC for the four candidates beside the number of plots each was actually fitted to.

Why the direction is fixed here

The log-likelihood of a fitted model is a sum with one term per observation, and each term is negative for a continuous response with a density below one. Drop rows and you drop terms, so the log-likelihood rises towards zero and the AIC falls. The penalty for parameters is two per parameter, which is nowhere near enough to compensate for losing sixty rows.

The arithmetic is worth seeing once, because it makes clear that this is not a subtle statistical effect but a bookkeeping error.

per_row <- sapply(fits, function(m) as.numeric(logLik(m)) / nobs(m))
round(rbind(total_loglik = sapply(fits, function(m) as.numeric(logLik(m))),
            rows         = sapply(fits, nobs),
            per_row      = per_row), 3)
                 true  plus_x3  plus_x2     full
total_loglik -266.927 -234.708 -180.908 -156.264
rows          200.000  180.000  140.000  125.000
per_row        -1.335   -1.304   -1.292   -1.250

Per observation the four models fit much alike, spanning 0.085 of a log-likelihood unit. Most of the separation comes from multiplying each by its own row count, and that part is in proportion to how much data each one threw away.

Refit on the same plots and the ordering reverses

The repair is to decide the data set before the model set. Take the rows that are complete for every variable that appears anywhere in the candidate list, and fit all four to those.

vars <- c("y", "x1", "x2", "x3")
common <- plots[complete.cases(plots[, vars]), ]

fits_c <- lapply(forms, lm, data = common)
tab_c  <- data.frame(model = names(forms),
                     rows  = sapply(fits_c, nobs),
                     aic   = sapply(fits_c, AIC))
tab_c$delta <- tab_c$aic - min(tab_c$aic)
tab_c[order(tab_c$aic), ]
          model rows      aic     delta
true       true  125 320.2521 0.0000000
plus_x2 plus_x2  125 321.1502 0.8981466
plus_x3 plus_x3  125 321.5921 1.3400483
full       full  125 322.5271 2.2750146

All four are now fitted to the same 125 plots, and the true model is first with the two single-additions within 1.3 AIC units and the full model 2.3 behind. That is the pattern AIC is supposed to produce for a null covariate: a penalty of about two per useless parameter, and no support for the elaboration.

The cost is visible too. The analysis now uses 125 of the original 200 plots, because the union of the missingness patterns is larger than either alone. That is a real loss and it is the subject of a different decision, but it is a loss you can see and argue about, rather than one that quietly rewrites the ranking.

How far the problem scales

The size of the artefact is a function of how much data the extra covariate is missing, and it is monotone.

sweep_miss <- function(fracs, reps = 40) {
  out <- data.frame()
  for (f in fracs) {
    gaps <- replicate(reps, {
      d  <- make_plots(miss2 = f, miss3 = 0)
      AIC(lm(y ~ x1, data = d)) - AIC(lm(y ~ x1 + x2, data = d))
    })
    out <- rbind(out, data.frame(missing = f, gap = mean(gaps),
                                 sd = sd(gaps),
                                 wrong = mean(gaps > 0)))
  }
  out
}

set.seed(2)
sw <- sweep_miss(c(0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6))
round(sw, 3)
  missing     gap     sd wrong
1     0.0  -0.798  1.500 0.225
2     0.1  55.896  5.008 1.000
3     0.2 112.423 10.330 1.000
4     0.3 171.064 11.258 1.000
5     0.4 224.521 12.676 1.000
6     0.5 281.359 15.168 1.000
7     0.6 340.800 18.095 1.000

With no missing values at all the true model is ahead by 0.8 AIC units on average: the two-unit penalty for the extra parameter, offset by the improvement in fit a null covariate buys by chance. The elaboration still wins 22 per cent of the time on that margin, which is ordinary AIC behaviour and the reason a gap of one or two units settles nothing.

Then the missingness starts. At a tenth of plots missing the elaboration already wins every one of the 40 simulated data sets, by an average of 56 AIC units. At sixty per cent missing the margin is 341 units in favour of a covariate that does nothing.

ggplot(sw, aes(x = 100 * missing, y = gap)) +
  geom_hline(yintercept = 0, colour = te_ink, linewidth = 0.5) +
  geom_errorbar(aes(ymin = gap - sd, ymax = gap + sd), width = 2.2,
                linewidth = 0.4, colour = te_forest) +
  geom_line(linewidth = 0.9, colour = te_forest) +
  geom_point(size = 2.8, colour = te_forest) +
  annotate("text", x = 1, y = max(sw$gap), hjust = 0, vjust = 1, size = 3.4,
           colour = te_ink,
           label = "everything above the dark line is\nsupport for a covariate with no effect") +
  labs(x = "per cent of plots missing the useless covariate",
       y = "AIC advantage of the useless covariate",
       title = "Support bought with absent data") +
  theme_datasheet()
A rising curve with error bars, starting slightly below the zero line when nothing is missing and climbing steeply above it, so a covariate with no effect gains hundreds of AIC units of apparent support purely from being missing more often.
Figure 2: Mean AIC advantage of a useless covariate over the true model, as a function of how much of that covariate is missing. Forty simulated data sets per point.

Making R refuse

The habit that prevents all of this is one line at the top of an analysis script. Setting the default action on missing values to na.fail turns the silent drop into an error, so a model that cannot use every row of the frame you handed it stops rather than guessing.

old <- options(na.action = "na.fail")
attempt <- try(lm(y ~ x1 + x2, data = plots), silent = TRUE)
cat(class(attempt), ":", conditionMessage(attr(attempt, "condition")), "\n")
try-error : missing values in object 
options(old)

With that set, the workflow becomes: build the analysis frame, subset it to complete cases over every variable in the candidate set, then fit. The models cannot disagree about their data because the frame no longer contains any disagreement.

A lighter check, if you would rather not change a global option, is to compare nobs across the fitted models before touching the AIC table. One line, and it catches every difference in row counts, though not two models that drop the same number of different plots; comparing the row names of each model’s model.frame catches those too.

same_rows <- function(models) length(unique(sapply(models, nobs))) == 1
c(as_fitted = same_rows(fits), on_common_frame = same_rows(fits_c))
      as_fitted on_common_frame 
          FALSE            TRUE 

If you impute: pool the test, not the AIC

Multiple imputation removes the row-count problem, since every completed data set contains every plot. It does not make AIC usable as it stands. Meng and Rubin (1992) showed how likelihood ratio tests can be combined across imputed data sets, and Wood, White and Royston (2008) compared ways of selecting variables after imputation and found that only selection based on Rubin’s rules kept the type 1 error where it should be. The shortcut that is easy to reach for is different: fit each candidate to each completed data set and average the AIC.

The check below has 150 plots per study, x1 and x2 correlated at 0.5, and y = 1.5 x1 + e as before, so x2 does nothing. x2 goes missing more often on plots with high x1, which is missing at random. Each study gets 10 proper normal-linear imputations of x2 from x1 and y, built the same way as in the chained equations post. Three rules then decide whether to keep x2: AIC on the common complete-case frame (the repair above), the AIC difference averaged over the 10 imputations, and the pooled Wald statistic from Rubin’s rules compared with 2, which is the threshold AIC sets for one extra parameter. AIC on the data before any values were deleted is the reference. A second set of studies gives x2 a real coefficient of 0.2, to measure power.

mi_n <- 150
mi_m <- 10

impute_x2 <- function(x2, mi_miss, predictors) {
  obs_rows <- predictors[!mi_miss, ]
  qr_fit   <- lm.fit(obs_rows, x2[!mi_miss])
  dfree    <- sum(!mi_miss) - ncol(predictors)
  s2star   <- sum(qr_fit$residuals^2) / rchisq(1, dfree)
  bstar    <- qr_fit$coefficients +
    drop(t(chol(s2star * chol2inv(chol(crossprod(obs_rows))))) %*% rnorm(ncol(predictors)))
  x2[mi_miss] <- drop(predictors[mi_miss, ] %*% bstar) + rnorm(sum(mi_miss), 0, sqrt(s2star))
  x2
}

loglik_lm <- function(design, y) {
  res <- lm.fit(design, y)$residuals
  -length(y) / 2 * (log(2 * pi * mean(res^2)) + 1)
}
keeps_x2 <- function(design, y) {
  (-2 * loglik_lm(design, y) + 2 * 4) < (-2 * loglik_lm(design[, 1:2], y) + 2 * 3)
}

one_study <- function(b2) {
  x1 <- rnorm(mi_n)
  x2 <- 0.5 * x1 + sqrt(0.75) * rnorm(mi_n)
  y  <- 1.5 * x1 + b2 * x2 + rnorm(mi_n)
  mi_miss <- runif(mi_n) < plogis(-0.55 + 1.5 * x1)
  full_design <- cbind(1, x1, x2)
  delta_aic <- est <- within_var <- numeric(mi_m)
  for (k in seq_len(mi_m)) {
    design_k <- cbind(1, x1, impute_x2(x2, mi_miss, cbind(1, x1, y)))
    delta_aic[k] <- (-2 * loglik_lm(design_k, y) + 8) - (-2 * loglik_lm(design_k[, 1:2], y) + 6)
    fit_k <- lm.fit(design_k, y)
    est[k]    <- fit_k$coefficients[3]
    within_var[k] <- sum(fit_k$residuals^2) / (mi_n - 3) * chol2inv(qr.R(fit_k$qr))[3, 3]
  }
  between <- var(est)
  total   <- mean(within_var) + (1 + 1 / mi_m) * between
  c(full      = keeps_x2(full_design, y),
    common_cc = keeps_x2(full_design[!mi_miss, ], y[!mi_miss]),
    mean_aic  = mean(delta_aic) < 0,
    rubin     = mean(est)^2 / total > 2,
    r         = between / mean(within_var),
    missing   = mean(mi_miss))
}

n_study <- 250
set.seed(1992)
mi_null  <- replicate(n_study, one_study(0))
mi_power <- replicate(n_study, one_study(0.2))

mi_keep  <- cbind(null = rowMeans(mi_null[1:4, ]), power = rowMeans(mi_power[1:4, ]))
mi_r     <- mi_null["r", ]
mi_closed <- mean(pchisq((2 - mi_r * (mi_m - 1) / mi_m) / (1 + mi_r * (1 + 1 / mi_m)),
                         1, lower.tail = FALSE))
mi_keep_se <- sqrt(mi_keep * (1 - mi_keep) / n_study)
mi_ratio   <- mi_keep["mean_aic", "null"] / mi_keep["full", "null"]
mi_missing <- mean(mi_null["missing", ])
round(t(mi_keep), 3)
       full common_cc mean_aic rubin
null  0.132     0.152    0.388 0.160
power 0.764     0.572    0.784 0.556
round(c(closed_form = mi_closed, median_r = median(mi_r)), 3)
closed_form    median_r 
      0.374       0.607 

Across 250 studies with 41 per cent of x2 missing on average, the averaged AIC keeps the useless x2 in 38.8 per cent, 2.9 times the 13.2 per cent that AIC gives on the complete data (the textbook rate for one extra parameter is 15.7 per cent). The pooled Wald keeps it in 16.0 per cent and the common complete-case frame in 15.2 per cent, with a Monte Carlo standard error of up to 3.1 points on each rate.

The inflation is arithmetic, and the simulation only checks it. Each imputation produces its own estimate of the x2 coefficient, scattered around the pooled estimate with between-imputation variance B. The likelihood ratio statistic of one completed data set is close to that estimate squared over its within-imputation variance U, so the average over M imputations exceeds the pooled estimate’s own statistic by about v (M - 1) / M, where v = B / U. Under the null the pooled estimate has variance U (1 + v (1 + 1/M)). Averaged AIC keeps x2 when the average statistic is above 2, so it keeps it with probability P(chi2_1 > (2 - v (M - 1) / M) / (1 + v (1 + 1/M))). With the v measured in each study (median 0.607) that probability averages 0.374, against 0.388 in the simulation. The between-imputation variance enters twice, once as extra spread in the pooled estimate and once as a positive shift in every study; Rubin’s rules put it in the denominator instead. Van Ginkel and Molenaar (2026) proposed pooled estimators of AIC and BIC for multiply imputed data and found, in factor, multilevel and latent class models, that they pick the correct model more often than averaging the criteria across imputed data sets.

With a real coefficient of 0.2, AIC on the complete data keeps x2 in 76.4 per cent of studies. The pooled Wald keeps it in 55.6 per cent and the common complete-case frame in 57.2 per cent (standard errors up to 3.1 points). The averaged AIC reaches 78.4 per cent only because it keeps x2 too readily whether or not it has an effect. So in this design imputation buys no power for selecting the covariate that carries the gaps, and the common frame of the earlier section does as well as the imputation rule that holds its level. If you do impute, pool the test and not the AIC: Rubin’s rules for one coefficient, or Meng and Rubin’s combined likelihood ratio when a term has several parameters (not run here).

Honest limits

Restricting to complete cases fixes the comparability problem and introduces a different one. For a regression it is unbiased when whether a value is missing does not depend on the response once the covariates in the model are known: missing completely at random, or missing because of x1 as in the imputation section above, where the common frame kept its level. When the reason a value is absent is related to the response, dropping those rows biases the estimates, and the tidy AIC table is then a comparison of models fitted to a non-random subsample. Which repair is right depends on why the data are missing, which is not a question the data can answer on their own.

Multiple imputation is the usual alternative, and the likelihood of an imputed data set is not the likelihood of the observed one, so ordinary AIC does not transfer without modification. The section above measures one case: a single incomplete covariate, lm, one extra parameter and missingness at random given x1. Several incomplete covariates, glm and mixed models, and selection on stacked or weighted imputed data sets were not tested, and the imputation model uses y whether or not the candidate contains x2. None of this is a reason to prefer the silent drop, which has no defensible interpretation at all.

The demonstration uses lm on a continuous response. The mechanism is the same for glm and for mixed models, and in the mixed case there is a second version of the same trap: fits with different fixed-effect structures estimated by restricted maximum likelihood are also not comparable by AIC, for a related reason about what the likelihood is a likelihood of. AIC on REML fits: when units choose the model measures that second trap.

Finally, the row-drop numbers here belong to one generator with two hundred plots, a moderate effect and independent missingness. The direction of the artefact holds whenever every row adds a negative log-likelihood term, as it does for counts and for this response (a continuous response whose spread, in the units it is recorded in, is small enough for its densities to exceed one would reverse it), and the size of it is not general; run the sweep on your own frame if you want to know how much of a published ranking could have come from this.

References

Akaike H 1974 IEEE Transactions on Automatic Control 19(6):716-723 (10.1109/TAC.1974.1100705)

Nakagawa S, Freckleton RP 2008 Trends in Ecology and Evolution 23(11):592-596 (10.1016/j.tree.2008.06.014)

Burnham KP, Anderson DR 2002 Model Selection and Multimodel Inference, 2nd edition, Springer, ISBN 978-0-387-95364-9

Meng XL, Rubin DB 1992 Biometrika 79(1):103-111 (10.1093/biomet/79.1.103)

Wood AM, White IR, Royston P 2008 Statistics in Medicine 27(17):3227-3246 (10.1002/sim.3177)

van Ginkel JR, Molenaar D 2026 Behaviormetrika 53(2):459-490 (10.1007/s41237-025-00281-6)

Newsletter

Get updates by email

An occasional email when tutorials are added or substantially corrected. No spam; unsubscribe anytime.

By subscribing you agree to receive these emails and confirm your address once. See the privacy policy.