cassionData Analysis

Lesson 3 of 8

Unit · Data that keeps its meaning

Stata and SPSS files keep their labels

haven, the labelled class, and why a survey firm's .dta should be read in R even when the analysis happens elsewhere — plus the two ways to lose a codebook without noticing.

R75 min

What a CSV throws away

A survey firm delivers a .dta or a .sav. Inside it, a column is not just values — it carries a variable label (“Household head consented to interview”) and value labels (1 = Yes, 2 = No, 9 = Don't know).

Export it to CSV and both are gone. You get a column of 1, 2 and 9, a separate PDF codebook somebody has to keep, and a mean of 1.7 that means nothing at all.

library(haven)

survey <- read_dta(here("data", "raw", "household-survey.dta"))

haven reads the labels along with the values. That is the reason this lesson exists, and the reason to read a survey file in R even when the analysis will happen in Python: R preserves more of the file than pandas does, and a CSV plus a codebook you wrote yourself is a worse artefact than the original.

The labelled class

read_dta() gives you columns of class haven_labelled: the underlying values, with their labels attached.

class(survey$consent)
#> [1] "haven_labelled" "vctrs_vctr" "double"

attr(survey$consent, "labels")
#>       Yes        No Don't know
#>         1         2          9

attr(survey$consent, "label")
#> [1] "Household head consented to interview"

The values are still there — arithmetic works — which is exactly the trap. A labelled column is a number as far as mean() is concerned:

mean(survey$consent, na.rm = TRUE)
#> [1] 1.7

That figure is the average of 1, 2 and 9. It is meaningless and it does not warn.

Converting deliberately

haven gives you three moves, and which one is right depends on what the column is.

A categorical variable becomes a factor.

library(dplyr)

survey <- survey |>
  mutate(consent = as_factor(consent))

levels(survey$consent)
#> [1] "Yes" "No" "Don't know"

as_factor() — haven’s, not base R’s as.factor() — uses the value labels as levels, in the order the file declares them rather than alphabetically. That ordering is a gift: the codebook’s order is usually the order a report wants.

A genuine number loses its labels.

survey <- survey |> mutate(hh_size = zap_labels(hh_size))

zap_labels() strips the labelling and leaves the values. Use it where the column really is a quantity that happened to carry a label.

Everything at once, when the file is uniformly categorical:

survey <- read_dta(path) |> as_factor()

Applying as_factor() to the whole tibble converts every labelled column. Fast, and wrong for any file mixing categories with quantities — check with glimpse() before reaching for it.

The two ways to lose a codebook

Reading with the labels already applied

survey <- read_dta(path, .name_repair = "unique")
survey <- as_factor(survey)          # values are gone; only labels remain

Now the underlying codes are unavailable. That matters more than it sounds: the codebook, the questionnaire, the tabulation plan and every previous analysis all refer to 1 and 2. A colleague asking “how many were coded 9?” cannot be answered from a factor.

Keep both:

survey <- read_dta(path)

codes <- survey |> mutate(across(where(is.labelled), zap_labels))
labels <- survey |> as_factor()

Writing the CSV and moving on

readr::write_csv(as_factor(survey), "survey.csv")

The CSV now holds "Yes" and "No", which is readable and lossy in a different direction — the codes are gone, and so is the distinction between “Don’t know” and a genuine missing value.

If you must hand a CSV to a Python analysis, hand a codebook with it, generated from the file rather than typed:

codebook <- tibble::tibble(
  variable = names(survey),
  label    = vapply(survey, function(x) attr(x, "label") %||% NA_character_, character(1))
)

readr::write_csv(codebook, "survey-codebook.csv")

The value labels need one row per level, which labelled::look_for() produces directly if you have that package. Either way, the codebook is derived, not transcribed — a transcribed codebook drifts from the file it describes.

User-defined missing values

Stata and SPSS distinguish kinds of missing: refused, not applicable, not asked. haven preserves them as tagged NAs rather than collapsing them.

survey$income
#> <labelled<double>[3]>
#> [1]  4500 NA(a) NA(b)

NA(a) and NA(b) are still NA to is.na(), so nothing silently becomes a number. But the distinction survives, and it is often the finding: a question refused by 40% of respondents is a different problem from one that did not apply to them.

survey <- survey |> mutate(income = zap_missing(income))

zap_missing() flattens them to plain NA once you have counted them. Count first.

SPSS and the other formats

read_sav(path)     # SPSS .sav
read_por(path)     # SPSS portable
read_dta(path)     # Stata
read_xpt(path)     # SAS transport

All return the same labelled structure, so everything above applies unchanged.

Writing back is symmetrical, and occasionally necessary when a ministry expects a .dta:

write_dta(survey, "cleaned.dta")

Stata has variable-name rules R does not — no dots, 32 characters — and write_dta() will error rather than silently mangle. That is the behaviour you want.

Where this fits with the Python course

The Python for Programme Data course says, in its lesson on codes and sentinels, that this is the one place where the answer is “use the other language”. This is the lesson it points at.

pandas reads a .dta and gives you either the codes or the labels, not both, and does not preserve tagged missing values at all. If the labels matter — and on a survey file they always do — read in R, convert deliberately, and export both the codes and a derived codebook.

What comes next

The labels are attached and converted into factors. The next lesson is about what a factor actually is in R, and about the ordering problem as_factor() solved for you here and will not solve for a column you build yourself.

Teach this lesson

The lesson as a slide deck, with the prose kept in the speaker notes rather than on the slide. Generated from this page, so it cannot fall out of step with it.

Start the slideshowRead the slides

The PDF needs no software and projects from any machine. The PowerPoint file is there to be edited — add your organisation's branding, cut a section for a shorter session, or merge two lessons into a workshop.