x <- NA
x >= 5[1] NA
x / 2[1] NA
A missing answer is not a known zero or a negative answer. This lesson explains the NA values encountered in summaries and revisits the explicit selection decisions introduced in Section 6.4. We begin with plain values, then inspect missing questionnaire answers using the prepared GSS extract.
By the end of this chapter, you will be able to:
The first examples use base R and need no packages. The survey application uses selection, filtering, and the tabulations and summaries already taught; creating new variables is not a prerequisite.
Use NA to represent a missing value. In the next example, >= asks whether a value is at least 5, and / divides it by 2. A known comparison returns TRUE or FALSE; an unknown answer is NA:
x <- NA
x >= 5[1] NA
x / 2[1] NA
Both results are NA. We cannot determine whether an unknown value is at least 5 or calculate half of it. Missing is different from FALSE, which says a condition is known not to hold, and from zero, which is a known number.
Use is.na() to ask whether a value is missing. The comparison operator == tests equality; it does not assign a value:
is.na(x)[1] TRUE
x == NA[1] NA
is.na("NA")[1] FALSE
is.na(x) returns TRUE. Comparing a value with NA using == returns NA, because equality with an unknown value cannot be established. The literal string "NA" has two known characters, so its missingness test is FALSE.
c() combines values in their written order. A vector can contain both known and missing values:
c(2, NA, 6) >= 5[1] FALSE NA TRUE
The results are FALSE, NA, and TRUE. The middle position retains an unknown answer.
As Section 7.3.1 and Chapter 9 showed, na.rm = TRUE excludes missing inputs from a calculation. It does not fill in missing values or delete rows from the source. Report the number of observed answers alongside a summary.
Does each summary describe the same respondents? Prepare the extract following Appendix A, then restore the saved file:
library(tidyverse)
library(tidystata)
gss24 <- readRDS("gss24.rds")Family income is unavailable for some respondents. misstable() from tidystata takes a table and selected columns and returns a coverage report. Begin with one variable, realinc, family income in constant 1986 dollars:
misstable(gss24, realinc)# A tibble: 1 × 4
variable miss_n miss_pct valid_n
<chr> <int> <dbl> <int>
1 realinc 423 10.6 3563
valid_n counts recorded incomes and miss_n counts missing incomes. They add to the total number of respondents. miss_pct divides the missing count by all respondents and expresses it on a 0–100 scale. The report does not fill values or change gss24. By default, misstable() displays selected variables that have missing values.
An overall missing percentage can conceal unequal coverage. Add .by = degree to report missing and observed incomes separately within each educational group:
misstable(gss24, realinc, .by = degree)# A tibble: 6 × 5
degree variable miss_n miss_pct valid_n
<fct> <chr> <int> <dbl> <int>
1 less than high school realinc 75 20.7 287
2 high school realinc 196 10.8 1621
3 associate/junior college realinc 24 6.67 336
4 bachelor's realinc 80 9.27 783
5 graduate realinc 47 8.15 530
6 NA realinc 1 14.3 6
Each row now describes income coverage in a degree category. Its miss_pct uses all respondents in that category as its denominator. A missing degree is a separate group. These percentages therefore answer a within-group question, rather than using everyone in the extract as a common denominator.
The .all = TRUE option keeps every requested variable in each group, including groups without missing measurements. It controls which report rows appear, not which respondents are in the data. Save the complete report for comparison:
income_by_degree <- misstable(gss24, realinc, .by = degree, .all = TRUE)
income_by_degree# A tibble: 6 × 5
degree variable miss_n miss_pct valid_n
<fct> <chr> <int> <dbl> <int>
1 less than high school realinc 75 20.7 287
2 high school realinc 196 10.8 1621
3 associate/junior college realinc 24 6.67 336
4 bachelor's realinc 80 9.27 783
5 graduate realinc 47 8.15 530
6 NA realinc 1 14.3 6
For every row, valid_n + miss_n gives the group’s total, including people whose income is unavailable. These 0–100 missing percentages differ from the 0–1 valid_pct proportions shown by summ(). The original gss24 remains unchanged.
Income is missing more often among respondents without a high school diploma (20.7%) than among those with a graduate degree (8.1%). The income comparisons in Section 9.4 describe income reporters within each group. Unequal coverage does not establish why answers are missing or what unreported incomes would have been.
Keep ID and the three confidence items used again in the variable-manipulation lessons. The same misstable() reports their missingness without changing the source:
confidence_coverage <- gss24 |>
select(id, conbus, coneduc, conmedic)
misstable(confidence_coverage, conbus:conmedic)# A tibble: 3 × 4
variable miss_n miss_pct valid_n
<chr> <int> <dbl> <int>
1 conbus 1398 35.1 2588
2 coneduc 1372 34.4 2614
3 conmedic 1376 34.5 2610
tab(confidence_coverage, conbus)# A tibble: 4 × 6
conbus n percent cum valid valid_cum
<fct> <int> <dbl> <dbl> <dbl> <dbl>
1 a great deal 372 9.33 9.33 14.4 14.4
2 only some 1555 39.0 48.3 60.1 74.5
3 hardly any 661 16.6 64.9 25.5 100
4 NA 1398 35.1 100 NA NA
The missing row in the frequency table is separate from every recorded response, including “hardly any” confidence. The percent column uses all 3986 respondents. The valid column excludes missing answers and describes those with a recorded response to that item. Items with different coverage can therefore have different valid-response denominators.
Inspect recorded and missing answers separately using the familiar filter():
observed_business <- confidence_coverage |>
filter(!is.na(conbus))
missing_business <- confidence_coverage |>
filter(is.na(conbus))
head(missing_business)# A tibble: 6 × 4
id conbus coneduc conmedic
<int> <fct> <fct> <fct>
1 4 NA NA NA
2 10 NA NA NA
3 15 NA NA NA
4 16 NA NA NA
5 18 NA NA NA
6 20 NA NA NA
nrow(observed_business)[1] 2588
nrow(missing_business)[1] 1398
nrow(observed_business) + nrow(missing_business)[1] 3986
There are 2588 recorded answers and 1398 missing answers, together accounting for all respondents. A missing business-confidence answer does not determine whether education or medicine was answered. Read those columns alongside it. Selecting recorded business answers changes who remains; it does not recover the unavailable answers.
The graph in Chapter 11 will place each respondent using schooling and family income. Having either answer alone is insufficient: both coordinates must be known. summ() reports each variable’s recorded count separately:
nrow(gss24)[1] 3986
summ(gss24, educ, realinc)# A tibble: 2 × 8
name type n unique min mean sd max
<chr> <chr> <int> <int> <dbl> <dbl> <dbl> <dbl>
1 educ dbl 3952 21 0 14.2 2.92 20
2 realinc dbl 3563 26 182. 34252. 34697. 117565.
Schooling is recorded for 3952 respondents and income for 3563. These marginal counts do not identify how many respondents have both. Even the smaller count could include people missing the other measurement.
Use the familiar filter() with two conditions separated by a comma to require both recorded answers. $ is unnecessary inside filter() because column names refer to its input table:
schooling_income <- gss24 |>
filter(!is.na(educ), !is.na(realinc))
nrow(schooling_income)[1] 3551
There are 3551 respondents with both measurements. The selection retains their original IDs and columns. It does not change gss24, replace a missing income with zero, or make a claim about the excluded people’s income.
To account for everyone else, select respondents missing schooling or income. | includes either condition and also those missing both. The two selections do not overlap:
incomplete_schooling_income <- gss24 |>
filter(is.na(educ) | is.na(realinc))
nrow(incomplete_schooling_income)[1] 435
nrow(schooling_income) + nrow(incomplete_schooling_income)[1] 3986
nrow(gss24)[1] 3986
The 435 respondents missing at least one measurement and the 3551 complete pairs together account for every respondent. The count applies to this pair of variables. A different pair, such as age and income, can have a different set of complete observations.
Survey files may distinguish “don’t know”, refusal, questions not asked, and other unavailable responses with designated codes or labelled missing values. Their meanings come from the questionnaire and codebook, not from whether the stored number is negative. A negative number can be a valid measurement in another variable.
The appendix preparation converts the GSS source missing values to NA. The saved extract does not retain every original reason, so this table cannot separate refusal from a question absent on a respondent’s questionnaire. Consult the original documentation and preparation recipe when the reason matters. Do not invent numeric missing codes in the prepared extract or reinterpret unavailable answers as “hardly any” confidence.
Replacing missing values with zero asserts a known quantity or a known negative response. That requires evidence from the question’s recording rules. It is not a general data-cleaning step. Likewise, na.rm = TRUE excludes unavailable measurements from a calculation; it does not explain why they are missing or establish that the observed answers represent everyone.
NA represents an unknown value, distinct from zero, FALSE, and the text "NA".is.na() tests for missingness; equality with NA does not.misstable() describes overall and grouped coverage; frequency tables distinguish missing answers from recorded categories.Section 6.4 provided the selection conditions used here. The next chapter, Chapter 11, uses joint availability to identify its plotting sample. Chapter 13 later checks missingness across a block of items.
is.na(NA), NA == NA, and is.na("NA"). Why are zero and FALSE different from a missing value?valid column use?is.na(NA)[1] TRUE
NA == NA[1] NA
is.na("NA")[1] FALSE
The results are TRUE, NA, and FALSE. The middle comparison cannot establish equality with an unknown value. The last input is known text. Zero is a known number and FALSE is a known logical result; neither means that the answer is unavailable.
Load the packages and saved extract in a fresh session:
library(tidyverse)
library(tidystata)
answer_gss24 <- readRDS("gss24.rds")answer_items <- answer_gss24 |>
select(id, conbus, coneduc, conmedic)
misstable(answer_items, conbus:conmedic)# A tibble: 3 × 4
variable miss_n miss_pct valid_n
<chr> <int> <dbl> <int>
1 conbus 1398 35.1 2588
2 coneduc 1372 34.4 2614
3 conmedic 1376 34.5 2610
answer_recorded <- answer_items |>
filter(!is.na(coneduc))
answer_missing <- answer_items |>
filter(is.na(coneduc))
nrow(answer_recorded)[1] 2614
nrow(answer_missing)[1] 1372
nrow(answer_recorded) + nrow(answer_missing) == nrow(answer_items)[1] TRUE
tab(answer_items, coneduc)# A tibble: 4 × 6
coneduc n percent cum valid valid_cum
<fct> <int> <dbl> <dbl> <dbl> <dbl>
1 a great deal 513 12.9 12.9 19.6 19.6
2 only some 1479 37.1 50.0 56.6 76.2
3 hardly any 622 15.6 65.6 23.8 100
4 NA 1372 34.4 100 NA NA
The final equality is TRUE. Valid percentages use the 2614 recorded education-confidence answers, whereas overall percentages include all 3986 respondents. Excluding missing answers changes the denominator without filling any response.
The replacement assigns the lowest confidence category to people whose answer is unavailable. It changes both category counts and the set treated as having answered. The prepared extract does not retain every original missing-value reason, so it cannot identify refusals from NA alone. The questionnaire and original source coding must justify any treatment of those reasons.
library(tidyverse)
library(tidystata)
answer_gss24 <- readRDS("gss24.rds")misstable(answer_gss24, realinc, .by = degree, .all = TRUE)# A tibble: 6 × 5
degree variable miss_n miss_pct valid_n
<fct> <chr> <int> <dbl> <int>
1 less than high school realinc 75 20.7 287
2 high school realinc 196 10.8 1621
3 associate/junior college realinc 24 6.67 336
4 bachelor's realinc 80 9.27 783
5 graduate realinc 47 8.15 530
6 NA realinc 1 14.3 6
Each group’s denominator is its own total. The same missing count produces a larger missing percentage in a smaller group. A coverage difference alone cannot tell us what caused missing answers or what their values would have been.
Restore the source independently before selecting the two sets:
library(tidyverse)
library(tidystata)
answer_gss24 <- readRDS("gss24.rds")answer_complete <- answer_gss24 |>
filter(!is.na(age), !is.na(realinc))
answer_incomplete <- answer_gss24 |>
filter(is.na(age) | is.na(realinc))
nrow(answer_complete)[1] 3496
nrow(answer_incomplete)[1] 490
nrow(answer_complete) + nrow(answer_incomplete) == nrow(answer_gss24)[1] TRUE
summ(answer_gss24, age, realinc)# A tibble: 2 × 8
name type n unique min mean sd max
<chr> <chr> <int> <int> <dbl> <dbl> <dbl> <dbl>
1 age dbl 3859 72 18 49.0 17.7 89
2 realinc dbl 3563 26 182. 34252. 34697. 117565.
The final equality is TRUE: complete pairs and respondents missing at least one answer partition the source. Separate n values in summ() do not say which people provided both answers. Each selection retains original source values in respondent order; neither changes answer_gss24 or recovers an unavailable measurement.