NA Means "Unknown" - and It Spreads
Real datasets have holes: the survey question left blank, the sensor that dropped a reading. R represents each hole with NA - not available. The crucial mental model: NA is not zero, not an empty string, not a special number. It means "there is a value here, but we don't know what it is."
Take that seriously and R's behavior becomes logical. What's an unknown number plus one? Unknown. What's the average of 4, 8, and something unknown? Unknown - the missing value could be anything, so the mean could be anything:
All three print NA. This contagiousness is a feature, not a bug: R refuses to quietly pretend it knows an answer it doesn't. A spreadsheet that silently skips blanks can hide the fact that half a column is missing; R makes you notice and decide what missing values should mean for your analysis. The rest of this article is about making that decision deliberately.
Testing for NA: is.na(), Never ==
Here's the first trap everyone falls into. You want to find the missing values, so you write a comparison - and it doesn't work:
x == NA returns NA NA NA - not a single TRUE. Why? Follow the logic of "unknown": is 4 equal to some unknown value? Can't say - the unknown might be 4. Even the NA slot compares as NA: is one unknown equal to another? Unknown. Comparison with NA can never return TRUE or FALSE, so as a test it's useless - and worse, an all-NA mask inside [ ] doesn't select what you expect.
The correct tool is is.na(), which is built to answer exactly this question and returns honest logicals: FALSE TRUE FALSE. From there, two idioms you'll use constantly:
sum(is.na(x)) counts the missing values (TRUE sums as 1) - run it on every column of a new dataset before anything else. which(is.na(x)) locates them. And x[!is.na(x)] keeps only the observed values - manual removal via a logical mask, the same vector filtering pattern as always.
Skipping NA in Summaries: na.rm = TRUE
You usually don't need to remove NAs by hand, because R's summary functions have an escape hatch built in - the na.rm (NA remove) argument:
With na.rm = TRUE, the function drops the missing values and computes on what's left: the mean of 4 and 8 is 6. sum(), sd(), median(), min(), max(), var() - the whole family of descriptive statistics accepts it.
Notice the default is FALSE. That's R being opinionated in your favor: it wants ignoring missing data to be an explicit choice you write down, not a silent default. When you type na.rm = TRUE, you're asserting "the missing values are safe to ignore here" - which is true when a sensor missed a few readings at random, and dangerously false when, say, the lowest earners skipped the income question. The argument makes you own that call.
Dropping Incomplete Rows: na.omit() and complete.cases()
In a data frame, missingness lives in cells, but analysis often works row by row - so the common operation is dropping rows that have any NA:
na.omit(df) returns the data frame minus every row containing at least one NA - here, only Rosa survives. complete.cases(df) returns the logical row mask (TRUE FALSE FALSE) behind the same idea, and indexing with it gives identical results with two advantages: you can count what you're about to lose first (sum(!complete.cases(df))), and you can restrict which columns matter - df[complete.cases(df[, "age"]), ] drops only rows missing age, keeping Mia even though her score is unknown.
That column-restricted form matters more than it looks: na.omit() on a wide data frame can silently discard most of your rows because of NAs in columns you never planned to analyze. Know how many rows you're dropping, and why, before you drop them.
Replacing NA
Sometimes the right move is filling holes rather than deleting rows. The idiom combines is.na() with assignment:
Read it as: "in the slots where visits is missing, put 0." This is legitimate exactly when NA encodes a known value - a customer with no visit record genuinely had zero visits.
But be honest about when that's true. If NA means "we failed to measure it," substituting 0 fabricates data: replacing missing test scores with 0 drags the mean down as if those students scored zero, when in truth you don't know what they scored. Compare the mean above (2.4) with the na.rm mean of the original (4): same data, different claims. Replacing with the mean or median distorts less but still understates variability. The rule: impute a value only when you can say, in plain words, why that value is what the missing entry really was. Otherwise keep the NA and use na.rm - "unknown" is often the most truthful value in the dataset.
NA vs NULL vs NaN vs Inf
R has four look-alike special values that mean genuinely different things:
| Value | Meaning | Length | Typical source |
|---|---|---|---|
NA | A value exists but is unknown | 1 (fills a slot) | Missing data |
NULL | No object at all | 0 (no slot) | Deleted list element, empty result |
NaN | Math with an undefined answer | 1 | 0 / 0, log(-1) |
Inf | A number beyond representation | 1 | 1 / 0, overflow |
The distinctions that matter in practice: NULL disappears inside vectors (c(1, NULL, 3) has two elements - it can't represent a missing observation), while NA holds its place. NaN counts as missing (is.na(NaN) is TRUE, so na.rm removes it too), but the reverse is false - is.nan(NA) is FALSE. And Inf is not missing - it's a real, comparable number (Inf > 1e300 is TRUE), so na.rm won't remove it; use is.finite() to filter to ordinary numbers.
For completeness: NA quietly comes in typed flavors (NA_integer_, NA_real_, NA_character_) so it can sit in any vector without breaking the one-type rule. You'll rarely type these, but you'll see them in package code and dplyr error messages.
What You Take Away
NAmeans "unknown," and unknowns are contagious: any calculation touching NA returns NA - by design.- Test with
is.na(), never== NA; count holes withsum(is.na(x)). na.rm = TRUEmakes summary functions skip NAs - an explicit, per-call decision that the missing values are ignorable.na.omit()drops incomplete rows wholesale;complete.cases()gives you the mask, countable and restrictable to the columns that matter.- Replace NA (
x[is.na(x)] <- value) only when you can justify what the missing value really was. - NA ≠ NULL ≠ NaN ≠ Inf: missing value, absent object, undefined math, unbounded number.
Next up: functions - packaging your logic into reusable, named pieces.
Frequently Asked Questions
Why does mean() return NA in R?
Because at least one value in the vector is NA, and NA is contagious: if any input is unknown, R says the answer is unknown too. Pass na.rm = TRUE - mean(x, na.rm = TRUE) - to compute the mean of the values that are present. Most summary functions (sum, sd, median, min, max) accept the same argument.
How do you check for NA in R?
With is.na(x), which returns TRUE wherever a value is missing. Never test with x == NA - comparing anything to an unknown yields NA, not TRUE or FALSE, so the test silently fails. sum(is.na(x)) counts the missing values; which(is.na(x)) finds their positions.
How do you remove rows with NA from a data frame in R?
na.omit(df) drops every row containing at least one NA. For more control, complete.cases(df) returns a logical vector marking fully-observed rows, so df[complete.cases(df), ] does the same thing explicitly - and you can apply it to just the columns that matter, e.g. df[complete.cases(df[, c("age", "score")]), ].
What is the difference between NA and NULL in R?
NA is a missing value - it takes up a slot in a vector and has length 1. NULL is the absence of an object - it has length 0 and vanishes when put into a vector: c(1, NULL, 3) has just two elements. Use NA for a missing data point; NULL appears when removing list elements or as an empty return.