library(tidyverse)
library(tidystata)
gss24 <- readRDS("gss24.rds")12 Create variables
A survey’s recorded answers are often the starting point for an analysis. Before calculating a new measure, we need to know how to name a column, add one, and retain the returned table. These operations should preserve the connection between a respondent and their answers.
By the end of this chapter, you will be able to:
- Rename a variable and distinguish creating a column from replacing one.
- Create a column from an existing column while preserving row alignment.
- Save a returned table and distinguish it from the unchanged input.
- Convert numbers and text explicitly and diagnose information lost in conversion.
- Create descriptive text while preserving source answers and identifiers.
We build on selecting columns and observations, sorting, and the pipe. Our data are the prepared 2024 GSS respondents. This chapter establishes the mechanics of changing columns and text. Chapter 13 next applies an operation to several columns, before Chapter 14 develops calculations and conditions. The optional programming chapters are not prerequisites.
Prepare the extract following Appendix A, then restore the saved file in your project folder:
Keep the ID, hours worked last week, and years of schooling. These are observed answers, including their original missing values:
work <- gss24 |>
select(id, hrs1, educ)
head(work)# A tibble: 6 × 3
id hrs1 educ
<int> <dbl> <dbl>
1 1 43 16
2 2 NA 16
3 3 NA 14
4 4 20 12
5 5 NA 13
6 6 NA 14
There are 3986 respondents in work; only six are displayed. hrs1 refers to the previous week, not a respondent’s usual schedule or commuting time. Its missing values do not imply zero working hours. The examples below change table structure without changing the recorded answers.
12.1 Renaming variables
The abbreviation hrs1 is concise but gives little information outside the codebook. rename() returns a table with changed column names. Write the new name on the left of = and the existing name on the right:
work |>
rename(hours_last_week = hrs1)# A tibble: 3,986 × 3
id hours_last_week educ
<int> <dbl> <dbl>
1 1 43 16
2 2 NA 16
3 3 NA 14
4 4 20 12
5 5 NA 13
6 6 NA 14
7 7 80 13
8 8 NA 14
9 9 50 14
10 10 NA 12
# ℹ 3,976 more rows
The result replaces the heading hrs1 with hours_last_week. Values, row order, and dimensions are unchanged. work itself still uses hrs1, because we have not assigned the result back to it.
We can rename several columns in one call and save the returned table:
work_named <- work |>
rename(hours_last_week = hrs1, schooling_years = educ)
work_named# A tibble: 3,986 × 3
id hours_last_week schooling_years
<int> <dbl> <dbl>
1 1 43 16
2 2 NA 16
3 3 NA 14
4 4 20 12
5 5 NA 13
6 6 NA 14
7 7 80 13
8 8 NA 14
9 9 50 14
10 10 NA 12
# ℹ 3,976 more rows
Both new names describe the existing quantities; renaming has not converted their units or changed any answer. The original ID remains alongside each respondent’s measurements.
12.2 Creating and changing columns
mutate() takes a table and returns a table containing new or replaced columns. Bare names refer to existing columns. The expression to the right of = supplies values; the name on the left names the result. First copy the recorded hours into a separate column so that we can inspect the operation without changing the measurement:
work_copy <- work_named |>
mutate(hours_copy = hours_last_week)
head(work_copy)# A tibble: 6 × 4
id hours_last_week schooling_years hours_copy
<int> <dbl> <dbl> <dbl>
1 1 43 16 43
2 2 NA 16 NA
3 3 NA 14 NA
4 4 20 12 20
5 5 NA 13 NA
6 6 NA 14 NA
ncol(work_copy)[1] 4
The returned table has the same respondents and one additional column, hours_copy. Each copied value belongs to the respondent on that row. Missing hours remain missing, and the ID, original hours, and schooling values stay alongside the copy. mutate() does not sort or remove respondents. The copy demonstrates column creation; it is not a new measure for analysis.
Using an existing name replaces that column in the returned table rather than adding another column. Repeat the copy using the name that now exists:
work_copy |>
mutate(hours_copy = hours_last_week)# A tibble: 3,986 × 4
id hours_last_week schooling_years hours_copy
<int> <dbl> <dbl> <dbl>
1 1 43 16 43
2 2 NA 16 NA
3 3 NA 14 NA
4 4 20 12 20
5 5 NA 13 NA
6 6 NA 14 NA
7 7 80 13 80
8 8 NA 14 NA
9 9 50 14 50
10 10 NA 12 NA
# ℹ 3,976 more rows
The result still has four columns, because hours_copy already exists. Its values are unchanged in this particular demonstration because we copied the same source again. In an applied transformation, a different expression would supply replacement values. Check the meaning and unit of a replacement before saving it; Chapter 14 works through that decision with hours and minutes.
Assignment and column naming do different jobs. In work_copy <-, <- saves the entire returned table under an object name. Inside mutate(), hours_copy = names one column in that table. Neither call changes work_named: it still contains only the renamed original columns. Likewise, a pipeline that is printed without assignment returns a result but does not save it over its input.
Keep calculations inside the table whose rows supply their inputs. A column copied or calculated there stays aligned with its respondent. A separately sorted or filtered column may no longer have the same rows in the same order, even if its length looks plausible.
12.3 Text
Quotation marks tell R to treat their contents as text, also called a character value or string. Even digits become text inside quotation marks:
12[1] 12
"12"[1] "12"
The first is numeric and the second is character. Printed quotation marks help distinguish them. A string may contain digits, spaces, or punctuation; it need not be a single letter. Use straight quotation marks in code, either single or double.
var_type() takes one object and returns its type without changing it:
var_type(12)[1] "double"
var_type("12")[1] "character"
The results are "double" and "character". A double is R’s usual numeric type, including fractional values such as 2.75. Quoted digits remain character values even when they look numeric.
paste() joins its inputs, inserting a space by default:
text <- "hello"
greeting <- paste(text, "R")
greeting[1] "hello R"
text[1] "hello"
The result is "hello R". The original text remains "hello"; the joined string is stored separately as greeting.
Joining text can change how missingness appears. Consider an abstract label whose second component is unknown:
missing_component <- NA_character_
missing_label <- paste("Label:", missing_component)
missing_label[1] "Label: NA"
is.na(missing_component)[1] TRUE
is.na(missing_label)[1] FALSE
NA_character_ is a missing character value. The input check returns TRUE, but the joined label is the non-missing string "Label: NA", so its check returns FALSE. paste() has inserted visible text representing the unknown component. Checking only the finished label would miss that unavailable input. If a label requires every component, check their missingness before joining them; a readable string does not establish that all its information was recorded.
12.4 Convert text and numbers
To convert numeric text explicitly, use as.numeric():
number_text <- "12"
number <- as.numeric(number_text)
number[1] 12
var_type(number_text)[1] "character"
The returned number is 12. Assignment stores it in number; the original number_text remains character. as.character() makes the reverse conversion:
as.character(number)[1] "12"
It returns "12" without changing the numeric input. A string with no numeric representation cannot be converted successfully:
converted_text <- as.numeric("hello")Warning: NAs introduced by coercion
converted_text[1] NA
is.na(converted_text)[1] TRUE
R warns that NAs were introduced by coercion, its term for conversion. The output is NA, and is.na() returns TRUE. The failed conversion does not produce zero.
A conversion can succeed while losing part of the text:
code <- "01"
as.numeric(code)[1] 1
code[1] "01"
The converted result is 1, losing the leading zero. The original remains "01" because we did not overwrite it. Keep identifiers as text when their formatting matters. Keep a field’s meaning in view: numeric-looking identifiers and category codes are not automatically quantities to average. Chapter 15 develops factor levels and labelled survey scores later.
12.5 Prepare descriptive text
Extra spaces can prevent two otherwise identical labels from matching. str_squish() receives character values and returns character values with surrounding whitespace removed and repeated internal whitespace reduced to one space. It does not change an object unless we assign its result. Use a plain phrase to make the spacing rule visible:
spacing_example <- " A short phrase "
spacing_example[1] " A short phrase "
str_squish(spacing_example)[1] "A short phrase"
The quotation marks show where the original spaces occur. The returned phrase keeps the words and their order. This is a syntax demonstration with plain text. Spaces can be meaningful in identifiers, so inspecting the source and understanding its coding rules should precede any automatic cleaning.
Case is another feature of text. str_to_lower() returns a lowercase version without changing the supplied object:
case_example <- "A Short Phrase"
str_to_lower(case_example)[1] "a short phrase"
case_example[1] "A Short Phrase"
The result is "a short phrase"; the original retains its capitals. Lowercasing can provide a consistent search form. Neither spacing changes nor a consistent case establishes that two records identify the same person.
12.5.1 Labels from recorded answers
Can we describe respondents by their recorded sex and highest degree without changing those source answers? Select the three columns together so their rows remain aligned:
respondent_labels <- gss24 |>
select(id, sex, degree)
head(respondent_labels)# A tibble: 6 × 3
id sex degree
<int> <fct> <fct>
1 1 male bachelor's
2 2 male graduate
3 3 female associate/junior college
4 4 male high school
5 5 female high school
6 6 male associate/junior college
id identifies the respondent. sex and degree are the prepared category columns already encountered in the GSS tables. Use as.character() to obtain their displayed category text. Convert one column first and keep its source:
respondent_labels <- respondent_labels |>
mutate(sex_text = as.character(sex))
head(respondent_labels)# A tibble: 6 × 4
id sex degree sex_text
<int> <fct> <fct> <chr>
1 1 male bachelor's male
2 2 male graduate male
3 3 female associate/junior college female
4 4 male high school male
5 5 female high school female
6 6 male associate/junior college male
The new column contains the sex category names. Unknown answers remain missing. Repeat the same explicit conversion for degree:
respondent_labels <- respondent_labels |>
mutate(degree_text = as.character(degree))
head(respondent_labels)# A tibble: 6 × 5
id sex degree sex_text degree_text
<int> <fct> <fct> <chr> <chr>
1 1 male bachelor's male bachelor's
2 2 male graduate male graduate
3 3 female associate/junior college female associate/junior college
4 4 male high school male high school
5 5 female high school female high school
6 6 male associate/junior college male associate/junior college
Both text columns have one value for every respondent, in the same order. We can therefore join these equal-length inputs row by row. sep = " / " replaces paste()’s default separating space with the supplied separator:
respondent_labels <- respondent_labels |>
mutate(description = paste(sex_text, degree_text, sep = " / "))
head(respondent_labels)# A tibble: 6 × 6
id sex degree sex_text degree_text description
<int> <fct> <fct> <chr> <chr> <chr>
1 1 male bachelor's male bachelor's male / bac…
2 2 male graduate male graduate male / gra…
3 3 female associate/junior college female associate/junior c… female / a…
4 4 male high school male high school male / hig…
5 5 female high school female high school female / h…
6 6 male associate/junior college male associate/junior c… male / ass…
Each description combines the two answers belonging to that row. A label containing "NA" still has an unknown source component, as the plain-input example showed. Inspect the source answers when either component is required:
respondent_labels |>
filter(is.na(sex) | is.na(degree)) |>
head()# A tibble: 6 × 6
id sex degree sex_text degree_text description
<int> <fct> <fct> <chr> <chr> <chr>
1 18 NA high school NA high school NA / high sc…
2 202 NA graduate NA graduate NA / graduate
3 307 NA less than high school NA less than high school NA / less th…
4 705 NA less than high school NA less than high school NA / less th…
5 954 male NA male NA male / NA
6 998 NA high school NA high school NA / high sc…
The preview selects respondents with at least one unavailable input. It does not establish that the joined label itself is missing. We have retained every source answer so that this distinction remains checkable.
Do descriptions uniquely identify respondents? Use a familiar frequency table:
tab(respondent_labels, description)# A tibble: 17 × 6
description n percent cum valid valid_cum
<chr> <int> <dbl> <dbl> <dbl> <dbl>
1 NA / NA 2 0.0502 0.0502 0.0502 0.0502
2 NA / bachelor's 2 0.0502 0.100 0.0502 0.100
3 NA / graduate 2 0.0502 0.151 0.0502 0.151
4 NA / high school 5 0.125 0.276 0.125 0.276
5 NA / less than high school 19 0.477 0.753 0.477 0.753
6 female / NA 2 0.0502 0.803 0.0502 0.803
7 female / associate/junior college 219 5.49 6.30 5.49 6.30
8 female / bachelor's 466 11.7 18.0 11.7 18.0
9 female / graduate 322 8.08 26.1 8.08 26.1
10 female / high school 952 23.9 49.9 23.9 49.9
11 female / less than high school 208 5.22 55.2 5.22 55.2
12 male / NA 3 0.0753 55.2 0.0753 55.2
13 male / associate/junior college 141 3.54 58.8 3.54 58.8
14 male / bachelor's 395 9.91 68.7 9.91 68.7
15 male / graduate 253 6.35 75.0 6.35 75.0
16 male / high school 860 21.6 96.6 21.6 96.6
17 male / less than high school 135 3.39 100 3.39 100
Several respondents share each common sex-degree description. A category combination describes an observation; it does not replace id. Keep the recorded identifier and source categories alongside any description.
We can also prepare a separate search form using the two text operations just learned. Show each step before combining it with another operation:
respondent_labels <- respondent_labels |>
mutate(description_search = str_squish(description))This supplies a spacing rule; it does not claim that the recorded GSS categories contain a spacing error. Now lowercase only that derived search column:
respondent_labels <- respondent_labels |>
mutate(description_search = str_to_lower(description_search))
head(respondent_labels)# A tibble: 6 × 7
id sex degree sex_text degree_text description description_search
<int> <fct> <fct> <chr> <chr> <chr> <chr>
1 1 male bachelor's male bachelor's male / bac… male / bachelor's
2 2 male graduate male graduate male / gra… male / graduate
3 3 female associate/ju… female associate/… female / a… female / associat…
4 4 male high school male high school male / hig… male / high school
5 5 female high school female high school female / h… female / high sch…
6 6 male associate/ju… male associate/… male / ass… male / associate/…
description remains available for display, and description_search follows a consistent search convention. Neither field establishes identity or resolves conflicting records. Chapter 15 later uses these tools with original country names and codes.
12.6 Review
rename()changes column names without changing their values or row order.mutate()adds a column when its output name is new and replaces a column when that name already exists.- A column expression uses values from the same row; preserve the connection to the observation’s ID.
- Assignment saves the returned table. Printing a result leaves the input object unchanged.
as.numeric()andas.character()return explicit conversions; check failed conversions and formatting losses.paste()joins aligned text inputs; a visible label can conceal a missing component and need not identify a respondent.str_squish()andstr_to_lower()prepare derived text while source values remain available.
Chapter 13 next applies an operation to several selected variables. Chapter 14 then uses these mechanics for calculations, indicators, and numeric recoding.
12.7 Exercises
Keep ID and hours worked, then rename the hours column to
hours_last_week. Save the result under a new object name. Explain whether this changes the recorded hours or the original table.Copy
hours_last_weekintohours_copyin a saved result. Repeat that operation on the result. Explain why the first call adds a column, the second replaces one, and both preserve respondent order and unknown hours.Diagnose conversions and joins. Convert
"hello"to numeric and inspect the result. Convert"01"to numeric and compare it with the source string. Join"Label:"to a missing character input, then compare missingness checks on the input and output. Explain the warning, the lost zero, and the concealed missing component.Keep
id,sex, anddegree; convert the two category columns to text separately and join their equal-length values into a descriptive label. Tabulate the labels and create a separate lowercase search form. Which columns must remain available to diagnose missing inputs? Explain why repeated descriptions cannot replaceid.
Worked answers
Prepare the extract following Appendix A, then restore the saved file in your project folder:
library(tidyverse)
library(tidystata)
gss24 <- readRDS("gss24.rds")answer_work <- gss24 |>
select(id, hrs1)
answer_named <- answer_work |>
rename(hours_last_week = hrs1)
head(answer_named)# A tibble: 6 × 2
id hours_last_week
<int> <dbl>
1 1 43
2 2 NA
3 3 NA
4 4 20
5 5 NA
6 6 NA
ds(answer_work)[1] "id" "hrs1"
answer_named contains the same IDs and hours under the clearer heading. answer_work still has the original hrs1 name. Renaming changes neither the reference period nor the values, including missing answers.
Prepare the extract following Appendix A, then restore the saved file in your project folder:
library(tidyverse)
library(tidystata)
gss24 <- readRDS("gss24.rds")answer_named <- gss24 |>
select(id, hrs1) |>
rename(hours_last_week = hrs1)
answer_copy <- answer_named |>
mutate(hours_copy = hours_last_week)
answer_replaced <- answer_copy |>
mutate(hours_copy = hours_last_week)
head(answer_replaced)# A tibble: 6 × 3
id hours_last_week hours_copy
<int> <dbl> <dbl>
1 1 43 43
2 2 NA NA
3 3 NA NA
4 4 20 20
5 5 NA NA
6 6 NA NA
ncol(answer_named)[1] 2
ncol(answer_copy)[1] 3
ncol(answer_replaced)[1] 3
The column counts are two, three, and three. The first mutate() adds hours_copy; the second uses that existing name and therefore replaces it. Here both copies contain exactly the same source hours, so replacement changes no values. IDs and row order remain aligned, and unknown hours remain unknown in both columns. Saving each returned table makes it available without changing its input.
converted_text <- as.numeric("hello")Warning: NAs introduced by coercion
converted_text[1] NA
is.na(converted_text)[1] TRUE
code <- "01"
as.numeric(code)[1] 1
code[1] "01"
The first conversion warns and returns NA, confirmed by TRUE from is.na(). The string has no numeric representation, so the conversion cannot return a known number such as zero. Converting "01" succeeds but returns 1, discarding its leading zero. The original text remains "01".
missing_component <- NA_character_
missing_label <- paste("Label:", missing_component)
missing_label[1] "Label: NA"
is.na(missing_component)[1] TRUE
is.na(missing_label)[1] FALSE
The missing input gives TRUE, but "Label: NA" is a non-missing string, giving FALSE. Inspect the inputs before using a joined label as evidence of complete information.
Load the saved extract in a fresh session:
library(tidyverse)
library(tidystata)
answer_gss24 <- readRDS("gss24.rds")answer_labels <- answer_gss24 |>
select(id, sex, degree) |>
mutate(sex_text = as.character(sex)) |>
mutate(degree_text = as.character(degree)) |>
mutate(description = paste(sex_text, degree_text, sep = " / ")) |>
mutate(description_search = str_squish(description)) |>
mutate(description_search = str_to_lower(description_search))
tab(answer_labels, description)# A tibble: 17 × 6
description n percent cum valid valid_cum
<chr> <int> <dbl> <dbl> <dbl> <dbl>
1 NA / NA 2 0.0502 0.0502 0.0502 0.0502
2 NA / bachelor's 2 0.0502 0.100 0.0502 0.100
3 NA / graduate 2 0.0502 0.151 0.0502 0.151
4 NA / high school 5 0.125 0.276 0.125 0.276
5 NA / less than high school 19 0.477 0.753 0.477 0.753
6 female / NA 2 0.0502 0.803 0.0502 0.803
7 female / associate/junior college 219 5.49 6.30 5.49 6.30
8 female / bachelor's 466 11.7 18.0 11.7 18.0
9 female / graduate 322 8.08 26.1 8.08 26.1
10 female / high school 952 23.9 49.9 23.9 49.9
11 female / less than high school 208 5.22 55.2 5.22 55.2
12 male / NA 3 0.0753 55.2 0.0753 55.2
13 male / associate/junior college 141 3.54 58.8 3.54 58.8
14 male / bachelor's 395 9.91 68.7 9.91 68.7
15 male / graduate 253 6.35 75.0 6.35 75.0
16 male / high school 860 21.6 96.6 21.6 96.6
17 male / less than high school 135 3.39 100 3.39 100
answer_labels |>
filter(is.na(sex) | is.na(degree)) |>
head()# A tibble: 6 × 7
id sex degree sex_text degree_text description description_search
<int> <fct> <fct> <chr> <chr> <chr> <chr>
1 18 NA high school NA high school NA / high … na / high school
2 202 NA graduate NA graduate NA / gradu… na / graduate
3 307 NA less than hig… NA less than … NA / less … na / less than hi…
4 705 NA less than hig… NA less than … NA / less … na / less than hi…
5 954 male NA male NA male / NA male / na
6 998 NA high school NA high school NA / high … na / high school
The frequency table counts respondents sharing the same description; frequencies above one demonstrate that the labels are not unique identifiers. Keep id, sex, and degree. The source categories reveal unavailable components that joined text can conceal. Converting their displayed names to character supports text operations without changing the recorded answers in those source columns.