Labelled data, and the two ways out of it
Reference · as_factor(), zap_labels(), and SPSS user-missing values
Reference page — nothing here goes into your lab notebook, and none of it is graded.
You met <dbl+lbl> columns in the M05 lab, where you took one of the two routes out of labelled data. This page covers both, and the SPSS missing-value machinery behind them. Come back to it when your Project 1 group opens a .sav of its own.
You will meet <dbl+lbl> and other labelled columns again in many .sav, .dta, and .sas7bdat files you open — including the Pew file your group project runs on. Today you used one way out of labelled-land. There is a second, and knowing which one a variable needs is the difference between a clean analysis and a quietly wrong one.
| When… | Use | Because the meaning lives in the… | Example |
|---|---|---|---|
| the label is the meaning | as_factor() | label — 1 is not substantively meaningful on its own; "A woman" is the category you want to analyze |
The five demographics — this is what you did today |
| the number is the meaning | zap_labels() | number — 3 really is a score of 3 |
Ratings, counts, and scales: anything you will average, correlate, or put on a numeric axis |
as_factor() turns labelled values into factor levels. For an ordinary labelled variable like the Pew variables here, zap_labels() takes the other route: it removes the value-label dictionary and gives you the underlying numbers. A stored 3 is still 3; it simply stops carrying its "Somewhat important" label. SPSS variables with formally declared user-missing values are a special case; we will separate those below.
Here is why the second route matters: labelled numeric columns can still behave like numbers. mean() can run on them. cor() can run on them. ggplot() can draw them. Nothing necessarily errors — so nothing necessarily warns you that a special code such as 99 = Don't know/Refused/Web blank has come along for the ride.
The three SM11 items are scored 1–4. Ask the raw columns for their maximum:
pew_selected |>
summarize(across(starts_with("SM11"), ~ max(.x, na.rm = TRUE)))99 [Don't know/Refused/Web blank] 99 [Don't know/...] 99 [Don't know/...]
R even prints the label telling you that 99 is not part of the substantive scale — and still hands it back as the maximum. Now clean the sentinel code and remove the value labels:
pew_selected |>
mutate(
across(
starts_with("SM11"),
~ zap_labels(na_if(.x, 99))
)
) |>
summarize(across(starts_with("SM11"), ~ max(.x, na.rm = TRUE)))4 4 4
Our workflow is na_if() first, then zap_labels(). We send the documented sentinel code to missing while its meaning is still visible, then strip the value labels and continue with an ordinary numeric variable. For these Pew columns you could reverse those two operations and still obtain the same numeric result; this order is simply easier to audit.
And note what makes this dangerous rather than merely wrong: only 3 to 7 people refused each of these items. A mean computed on the raw column is off by only a few hundredths — small enough that it may not look suspicious in a table, but still wrong. The max() check makes the problem obvious, which is exactly why inspecting the range of a cleaned variable is worth doing every time.
The damage is not always subtle, either. Correlate the three items with the refusal codes still present and you get r between 0.45 and 0.54; clean them first and the same three items give 0.66 to 0.75. Fifteen stray values — sitting 95 points beyond the end of a 1–4 scale — are enough to move a correlation by about 0.2 without producing a single warning.
One pair of names to keep straight: zap_labels() (plural) removes the value labels — the codes-to-text dictionary. zap_label() (singular) removes the variable label — the descriptive label attached to the variable itself, often based on the survey question wording. In this workflow you usually want the plural: clear the value labels while keeping the variable label available for tools such as tbl_summary().
A third function you’ll find — and why it is different
Search for ways to handle missing codes in survey files and you will also encounter zap_missing(). It has a different job: it converts special missing-value representations into ordinary R NAs. That includes tagged missing values from SAS or Stata and formally declared user-missing values from SPSS.
SPSS can formally declare particular values or ranges as user-defined missing. That declaration is metadata stored separately from a value label. When a .sav file contains those declarations, read_sav() gives you two useful choices:
| How you read it | What you get |
|---|---|
| read_sav(file) — the default | Formally declared SPSS user-missing values are converted to ordinary NA during import |
| read_sav(file, user_na = TRUE) | The original missing codes and declarations are preserved in a special labelled_spss vector, so categories such as Refused and Don’t know can remain distinguishable |
| …then zap_missing() | Converts those preserved special missings to ordinary NA when you are finished inspecting or reporting them |
That middle option is useful when the distinction among missing categories matters. You might, for example, want to report how many respondents refused an item versus answered Don’t know before collapsing both to ordinary missingness for analysis.
Why Pew’s 99 is different
Pew’s 99 is not an SPSS-declared user-missing value in this file. Two things tell you so. First, what happened on import: formally declared SPSS user-missing values would already have become NA under the default read_sav(), but Pew’s 99 survived as an actual numeric value carrying the label "Don't know/Refused/Web blank" — and the column came back with zero NAs. Second, you can check the declarations directly: reading the file with user_na = TRUE and inspecting the column’s attributes shows its na_values and na_range are both empty. There is nothing there to zap.
That gives you the distinction to remember:
A value label documents what a code means. A missing-value declaration tells software to treat that code as missing.
Pew gave 99 the first kind of metadata but not the second. So na_if(.x, 99) is not a workaround for failing to use zap_missing(); it is the correct cleaning step for a variable encoded this way.
If you ever need to inspect a new SPSS file’s original missing-value declarations directly, read it with user_na = TRUE. Haven will then preserve declared missing values or ranges in the imported labelled_spss object. Once you have inspected or reported those distinctions, zap_missing() converts them to ordinary NAs.
The practical rule:
- If the labels are the categories you want to analyze, use as_factor().
- If the underlying numbers are the scores you want to analyze, first deal explicitly with any non-substantive codes, then use zap_labels().
- If the file contains special missing values that you intentionally preserved during import, zap_missing() converts them to ordinary
NAs.
That distinction will matter again in Project 1, because real survey files often arrive with exactly this mixture of numeric codes, human-readable labels, and missing-data conventions.