Debugging R: warnings, factors, NA and performance traps
Tell the four kinds of missing value apart, avoid factor and comparison traps, use the debugger, and stop writing loops that should be vectorised.
NA, NaN, NULL and empty strings
| Value | Is | Test | In a data frame |
|---|---|---|---|
NA | Unknown, typed | is.na(x) | A missing cell |
NaN | Not a number, from 0/0 | is.nan(x) | Also true for is.na |
NULL | Absence of a value | is.null(x) | A dropped column |
"" | An empty string, not missing | x == "" | A present but empty cell |
Inf | Infinity from division by zero | is.infinite(x) | Often a bug upstream |
x <- c(1, NA, 3, NaN)
mean(x) # NA: any missing value poisons the result
mean(x, na.rm = TRUE) # 2, once NaN and NA are excluded
x == NA # all NA: comparison with a missing value is unknown
is.na(x) # the only correct test
# filtering with a condition containing NA silently drops rows
df[df$amount > 100, ] # NA rows vanish; is that what you intended?
subset(df, amount > 100)⚠️
x == NA evaluates to NA, and if (x == NA) therefore raises missing value where TRUE/FALSE needed. Use is.na(), and for set membership use x %in% c(1, 2), which returns FALSE for missing values rather than propagating them.Factors and coercion
f <- factor(c("low", "high", "low"), levels = c("low", "medium", "high"))
as.numeric(f) # 1 3 1 - the LEVEL CODES, not the labels
as.numeric(as.character(f)) # wrong for numbers too, but shows the idea
as.integer(f) # same trap
# binding rows or joining with a factor column can produce a character column
df1 <- data.frame(g = factor("a"))
df2 <- data.frame(g = factor("b", levels = c("a", "b")))
str(rbind(df1, df2))
# use forcats to change levels deliberately
library(forcats)
f2 <- fct_relevel(f, "high")
fct_collapse(f, small = "low", large = c("medium", "high"))stringsAsFactorswas TRUE by default before R 4.0, which is why older scripts behave differently from new ones.- Setting levels that are not present still affects ordering, modelling and plotting; a level with no data is not an error.
- Summing or averaging a factor without converting is a silent category-code calculation.
Debugging and performance
# inspect the call stack after an error
options(error = recover) # then run the failing call
traceback() # after an error, without the option
options(error = NULL)
f <- function(x) { browser(); x + 1 } # pause with full access to the frame
debug(lm) # step through a function
undebug(lm)
# timing and profiling
system.time(Sys.sleep(0.2))
Rprof("prof.out"); heavy_call(); Rprof(NULL)
summaryRprof("prof.out")
# vectorise instead of looping
set.seed(1)
n <- 1e6
x <- runif(n)
y <- numeric(n)
system.time(for (i in seq_len(n)) y[i] <- sqrt(x[i]))
system.time(y2 <- sqrt(x)) # one vectorised call, two orders of magnitude faster- Find the failing line:
traceback()oroptions(error = recover). - Inspect the state:
browser()inside the function, oroptions(error = NULL)plus manual re-runs. - Check types first:
str(),class()andsapply(df, class)reveal factor and integer surprises immediately. - Only then look at speed: profile with
Rprofbefore rewriting anything, because intuition about R bottlenecks is usually wrong. - For genuinely large data, reach for
data.tableor a database rather than micro-optimising a data frame loop.
FAQ
Why does my mean return NA when I can see no missing values?
A column may hold
NaN or NA introduced by a failed coercion, often when a factor or a text column met an arithmetic operation. Run sapply(df, function(x) sum(is.na(x))) to find which column it is.Is data.table worth learning?
For repeated operations on millions of rows, yes: its syntax is compact and it avoids copying. For analysis that runs in seconds on modest data, dplyr's clarity is worth more than the speed difference.
Related
Vectors, factors and data frames Writing functions, control flow and the apply family
Last refreshed 2026-09-18.