10  Missing values

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.

10.1 Recognize an unknown value

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.

10.2 Inspect questionnaire coverage

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")

10.2.1 Begin with one measurement

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.

10.2.2 Compare groups

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.

10.2.3 Check the confidence items

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.

10.3 Check joint coverage

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.

10.4 Interpret source missingness

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.

10.5 Review

  • NA represents an unknown value, distinct from zero, FALSE, and the text "NA".
  • is.na() tests for missingness; equality with NA does not.
  • Calculations and comparisons can preserve unknown inputs as unknown results.
  • misstable() describes overall and grouped coverage; frequency tables distinguish missing answers from recorded categories.
  • Separate recorded counts do not establish joint availability. Select both required measurements explicitly before counting complete pairs.
  • State whether a denominator includes everyone or only recorded answers. Source documentation determines what designated missing codes mean.

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.

10.6 Exercises

  1. Explain the results of is.na(NA), NA == NA, and is.na("NA"). Why are zero and FALSE different from a missing value?
  2. Reload the GSS extract, select ID and the three confidence items, and inspect missingness. For confidence in education, count recorded and missing answers separately. Which denominator does the frequency table’s valid column use?
  3. A researcher replaces every missing confidence answer with “hardly any”, saying this simply cleans the data. Explain the substantive change. Can the saved extract identify which missing answers were refusals? Then inspect income coverage by degree. Why can two degree groups have different coverage percentages even if they have the same missing count?
  4. Adapt the joint-coverage check to age and family income. Save complete and incomplete selections, count them, and verify that they account for every respondent. Explain why the separate summary counts alone do not supply the complete-pair count.

Worked answers

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.