Attributes and Class

1 Introduction

1.1 Libraries we will use

Load the packages we will use by running this code chunk:

library(package = tidyverse)
library(package = haven)
library(package = labelled)
library(package = lubridate)

If package not yet installed, then must install before you load. Install in “console” rather than .Rmd file:

  • Generic syntax: install.packages(pkgs = "package_name")
  • Install “tidyverse”: install.packages(pkgs = "tidyverse")

Note: When we load package, name of package is not in quotes; but when we install package, name of package is in quotes:

  • install.packages(pkgs = "tidyverse")
  • library(package = tidyverse)

1.2 Dataset we will use

Recall from Into the tidyverse lecture, we used the data set wwlist. This data set represents “prospects” purchased by Western Washington U.

  • Essentially, universities identify/target “prospect” by buying student lists from College Board/ACT (e.g., $.50+ per prospect).
  • Data universities purchase on students includes their contact info (e.g., address, email), academics (e.g., GPA, AP coursework), socioeconomic and demographic characteristics.
  • Universities choose which prospective students’ information to purchase by filtering on criteria like their zip code, GPA, test score range, etc.
rm(list = ls()) # remove all objects

load(
  file = url(
    description = "https://github.com/anyone-can-cook/rclass1/raw/master/data/prospect_list/wwlist_merged.RData"
  )
)

2 Attributes and augmented vectors

2.1 Review data structures: Vectors


Two types of vectors:

  1. Atomic vectors
  2. Lists


Overview of data structures (Grolemund and Wickham, 2018)

2.1.1 Atomic vectors


An atomic vector is a collection of values

  • Each value in an atomic vector is an element
  • All elements within vector must have the same data type
(vec_x <- c(1,2,3)) # parentheses () assign and print object in one step
[1] 1 2 3
length(x = vec_x) # length = number of elements
[1] 3
typeof(x = vec_x) # numeric atomic vector, type=double
[1] "double"
str(object = vec_x) # investigate structure of object
 num [1:3] 1 2 3

Can assign names to vector elements, creating a named atomic vector

(vec_x <- c(v1=1,v2=2,v3=3))
v1 v2 v3 
 1  2  3 
length(x = vec_x)
[1] 3
typeof(x = vec_x)
[1] "double"
str(object = vec_x)
 Named num [1:3] 1 2 3
 - attr(*, "names")= chr [1:3] "v1" "v2" "v3"

2.1.2 Lists


  • Like atomic vectors, lists are objects that contain elements
  • However, data type can differ across elements within a list
    • E.g., an element of a list can be another list
list_x <- list(1,2,"apple")
typeof(x = list_x)
[1] "list"
length(x = list_x)
[1] 3
str(object = list_x)
List of 3
 $ : num 1
 $ : num 2
 $ : chr "apple"
list_y <- list(1, c("apple", "orange"), list(1, 2))
length(x = list_y)
[1] 3
str(object = list_y)
List of 3
 $ : num 1
 $ : chr [1:2] "apple" "orange"
 $ :List of 2
  ..$ : num 1
  ..$ : num 2

Like atomic vectors, elements within a list can be named, thereby creating a named list

# not named
str(object = list_y)
List of 3
 $ : num 1
 $ : chr [1:2] "apple" "orange"
 $ :List of 2
  ..$ : num 1
  ..$ : num 2
# named
list_z <- list(v1=1, v2=c("apple", "orange"), v3=list(1, 2, 3))
str(object = list_z)
List of 3
 $ v1: num 1
 $ v2: chr [1:2] "apple" "orange"
 $ v3:List of 3
  ..$ : num 1
  ..$ : num 2
  ..$ : num 3

2.1.3 Data frames


A data frame is a list with the following characteristics:

  • All the elements must be vectors with the same length
  • Data frames are augmented lists because they have additional attributes
# a regular list
(list_x <- list(col_a = c(1,2,3), col_b = c(4,5,6), col_c = c(7,8,9)))
$col_a
[1] 1 2 3

$col_b
[1] 4 5 6

$col_c
[1] 7 8 9
typeof(x = list_x)
[1] "list"
attributes(x = list_x)
$names
[1] "col_a" "col_b" "col_c"
# a data frame
(df_x <- data.frame(col_a = c(1,2,3), col_b = c(4,5,6), col_c = c(7,8,9)))
  col_a col_b col_c
1     1     4     7
2     2     5     8
3     3     6     9
typeof(x = df_x)
[1] "list"
attributes(x = df_x)
$names
[1] "col_a" "col_b" "col_c"

$class
[1] "data.frame"

$row.names
[1] 1 2 3

2.2 Converting between types and data structures

The functions below convert an object to a different data type or data structure:

  • as.logical(): Convert to logical
  • as.numeric(): Convert to numeric
  • as.integer(): Convert to integer
  • as.character(): Convert to character
  • as.list(): Convert to the list data structure
  • as.data.frame(): Convert to the data.frame data structure

When values of different types are combined in an atomic vector, R converts them to a common type that can accommodate all the values. In the examples below, the usual progression is logical → integer → double → character.

typeof(x = c(TRUE, 3L))
[1] "integer"
#> [1] "integer"
typeof(x = c(2L, 4.5))
[1] "double"
#> [1] "double"
typeof(x = c(2.5, "j"))
[1] "character"
#> [1] "character"

Credit: R for Data Science, section on Coercion


Convert to logical using as.logical()

Character vector coerced to logical vector:

# Only "TRUE"/"FALSE", "True"/"False", "T"/"F", "true"/"false" are able to be coerced to logical type
as.logical(x = c("TRUE", "FALSE", "True", "False", "true", "false", "T", "F", "t", "f", ""))
 [1]  TRUE FALSE  TRUE FALSE  TRUE FALSE  TRUE FALSE    NA    NA    NA

Numeric vector coerced to logical vector:

# 0 is treated as FALSE, while all other numeric values are treated as TRUE
as.logical(x = c(0, 0.0, 1, -1, 20, 5.5))
[1] FALSE FALSE  TRUE  TRUE  TRUE  TRUE


Convert to numeric using as.numeric()

Logical vector coerced to numeric vector:

# FALSE is mapped to 0 and TRUE is mapped to 1
as.numeric(x = c(FALSE, TRUE))
[1] 0 1

Character vector coerced to numeric vector:

# Strings containing numeric values can be coerced to numeric (leading 0's are dropped) 
# All other characters become NA
as.numeric(x = c("0", "007", "2.5","2.6", "abc", "."))
[1] 0.0 7.0 2.5 2.6  NA  NA


Convert to integer using as.integer()

Logical vector coerced to integer vector:

# FALSE is mapped to 0 and TRUE is mapped to 1
as.integer(x = c(FALSE, TRUE))
[1] 0 1

Character vector coerced to integer vector:

# Strings containing numeric values can be coerced to integer
# Leading 0's are dropped; decimals are truncated, not rounded
# All other characters become NA
as.integer(x = c("0", "007", "2.5","2.6", "abc", "."))
[1]  0  7  2  2 NA NA

Numeric vector coerced to integer vector:

# All decimal places are truncated
as.integer(x = c(0, 2.1, 10.5, 8.8, -1.8))
[1]  0  2 10  8 -1


Convert to character using as.character()

Logical vector coerced to character vector:

as.character(x = c(FALSE, TRUE))
[1] "FALSE" "TRUE" 

Numeric vector coerced to character vector:

as.character(x = c(-5, 0, 2.5))
[1] "-5"  "0"   "2.5"

Integer vector coerced to character vector:

as.character(x = c(-2L, 0L, 10L))
[1] "-2" "0"  "10"


Convert to list using as.list()

Atomic vectors coerced to list:

# Logical vector
as.list(x = c(TRUE, FALSE))
[[1]]
[1] TRUE

[[2]]
[1] FALSE
# Character vector
as.list(x = c("a", "b", "c"))
[[1]]
[1] "a"

[[2]]
[1] "b"

[[3]]
[1] "c"
# Numeric vector
as.list(x = 1:3)
[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] 3


Convert to data.frame using as.data.frame()

Lists coerced to dataframe:

# Create a list
list_x <- list(A = c("x", "y", "z"), B = c(1, 2, 3))
str(object = list_x)
List of 2
 $ A: chr [1:3] "x" "y" "z"
 $ B: num [1:3] 1 2 3
# Convert to class `data.frame`
df_x <- as.data.frame(x = list_x, stringsAsFactors = F)
str(object = df_x)
'data.frame':   3 obs. of  2 variables:
 $ A: chr  "x" "y" "z"
 $ B: num  1 2 3

Notice that the underlying type is still a list, but the object now has additional attributes that identify it as a data frame.

Example: Practical example of converting type

When working with data, it may be helpful to label values for certain variables. Data files often come with a code book that defines how values are coded. Let’s look at an example of labeling values and how converting data type may come into play.

We’ll look at the FIPS variable from the Integrated Postsecondary Education Data System (IPEDS) data. The state FIPS code is a numeric code that identifies a state. For example, 1 is the FIPS code for Alabama, 2 is the FIPS code for Alaska, etc. We’ll want to label each numeric value in the FIPS column with the corresponding state name.

# Library for labeling variables and values in a dataframe
library(package = labelled)

# Read in IPEDS data and codebook
ipeds_df <- read.csv(
  file = 'https://raw.githubusercontent.com/cyouh95/recruiting-chapter/master/data/ipeds_hd2017.csv',
  header = TRUE,
  na.strings = c('', 'NA'),
  stringsAsFactors = F
)
ipeds_values <- read.csv(
  file = 'https://raw.githubusercontent.com/cyouh95/recruiting-chapter/master/data/ipeds_hd2017_values.csv',
  header = TRUE,
  na.strings = c('', 'NA'),
  stringsAsFactors = F
)

# The codebook defines how variables are coded, such as STABBR, FIPS, and other variables
head(x = ipeds_values)
  varnumber varname codevalue valuelabel frequency percent
1     10016  STABBR        AL    Alabama        95    1.33
2     10016  STABBR        AK     Alaska        10    0.14
3     10016  STABBR        AZ    Arizona       137    1.92
4     10016  STABBR        AR   Arkansas        86    1.20
5     10016  STABBR        CA California       729   10.19
6     10016  STABBR        CO   Colorado       118    1.65
# Filter codebook for just the values for the FIPS variable
fips_values <- ipeds_values %>% filter(varname == 'FIPS') %>% select(varname, codevalue, valuelabel)
head(x = fips_values)
  varname codevalue valuelabel
1    FIPS         1    Alabama
2    FIPS         2     Alaska
3    FIPS         4    Arizona
4    FIPS         5   Arkansas
5    FIPS         6 California
6    FIPS         8   Colorado


When we read in the data from the CSV files, R automatically tries to determine the data type of each variable. As seen below, the FIPS column from the ipeds_df that we want to label is of type integer, while the codevalue column from the codebook is of type character (since not all values are numeric):

# Type of `FIPS` column
str(object = ipeds_df$FIPS)
 int [1:7153] 1 1 1 1 1 1 1 1 1 1 ...
# Type of `codevalue` column
str(object = fips_values$codevalue)
 chr [1:59] "1" "2" "4" "5" "6" "8" "9" "10" "11" "12" "13" "15" "16" "17" ...


This discrepancy becomes a problem when we try to label the value using the labelled library:

# Error: `x` and `labels` must be same type
val_label(ipeds_df$FIPS, fips_values[1, 'codevalue']) <- fips_values[1, 'valuelabel']


To resolve this, we can use as.integer() to convert the codevalue from character type to integer before trying to label the value:

# This now works
val_label(ipeds_df$FIPS, as.integer(x = fips_values[1, 'codevalue'])) <- fips_values[1, 'valuelabel']

# Check value labels
val_labels(x = ipeds_df$FIPS)
Alabama 
      1 
# We can use as.integer() to convert the entire vector (ie. codevalue column) to integer
fips_values$codevalue <- as.integer(x = fips_values$codevalue)

# Type of `codevalue` column
str(object = fips_values$codevalue)
 int [1:59] 1 2 4 5 6 8 9 10 11 12 ...
# Use loop to label the rest of the values
for (i in 1:nrow(x = fips_values)) {
  val_label(ipeds_df$FIPS, fips_values[i, 'codevalue']) <- fips_values[i, 'valuelabel']
}

# Check value labels
val_labels(x = ipeds_df$FIPS)
                       Alabama                         Alaska 
                             1                              2 
                       Arizona                       Arkansas 
                             4                              5 
                    California                       Colorado 
                             6                              8 
                   Connecticut                       Delaware 
                             9                             10 
          District of Columbia                        Florida 
                            11                             12 
                       Georgia                         Hawaii 
                            13                             15 
                         Idaho                       Illinois 
                            16                             17 
                       Indiana                           Iowa 
                            18                             19 
                        Kansas                       Kentucky 
                            20                             21 
                     Louisiana                          Maine 
                            22                             23 
                      Maryland                  Massachusetts 
                            24                             25 
                      Michigan                      Minnesota 
                            26                             27 
                   Mississippi                       Missouri 
                            28                             29 
                       Montana                       Nebraska 
                            30                             31 
                        Nevada                  New Hampshire 
                            32                             33 
                    New Jersey                     New Mexico 
                            34                             35 
                      New York                 North Carolina 
                            36                             37 
                  North Dakota                           Ohio 
                            38                             39 
                      Oklahoma                         Oregon 
                            40                             41 
                  Pennsylvania                   Rhode Island 
                            42                             44 
                South Carolina                   South Dakota 
                            45                             46 
                     Tennessee                          Texas 
                            47                             48 
                          Utah                        Vermont 
                            49                             50 
                      Virginia                     Washington 
                            51                             53 
                 West Virginia                      Wisconsin 
                            54                             55 
                       Wyoming                 American Samoa 
                            56                             60 
Federated States of Micronesia                           Guam 
                            64                             66 
              Marshall Islands              Northern Marianas 
                            68                             69 
                         Palau                    Puerto Rico 
                            70                             72 
                Virgin Islands 
                            78 

2.3 Atomic vectors, augmented vectors, and augmented lists

Atomic vectors [our focus so far]

  • I think of atomic vectors as “just the data”
  • Atomic vectors are the building blocks for augmented vectors


Augmented atomic vectors

  • Augmented atomic vectors are atomic vectors with additional attributes attached
    • Factors and dates are examples we will encounter later in this lecture

Augmented lists

  • Augmented lists are lists with additional attributes attached
    • A data frame is a common example of an augmented list

Attributes

  • Attributes are additional “metadata” that can be attached to any object (e.g., vector or list)
  • Metadata is information that describes the data. For example, a variable’s name, category labels, or how R should treat it.

Example: Variables of a dataset

  • A data frame is a list
  • Each element in the list is a variable, which consists of:
    • Atomic vector (“just the data”)
    • Any attributes we want to attach to each element/variable
  • Variable name, an attribute of the data frame object

Other examples of attributes in R

  • Value labels: Character labels (e.g., “Charter School”) attached to numeric values
  • Object class: Specifies how object should be treated by object oriented programming language

Main takeaway:

  • Attributes add metadata to R objects
  • An augmented atomic vector or augmented list combines the underlying data structure with additional attributes

2.4 Attributes and functions to identify/modify attributes

Description of attributes from Grolemund and Wickham 20.6

  • “Any vector can contain arbitrary additional metadata through its attributes”
  • “You can think of attributes as named list of vectors that can be attached to any object”

Functions to identify and modify attributes

  • attributes(): gets or sets all attributes of an object
  • attr(): gets or sets one attribute of an object

2.4.1 Describe all attributes of an object

# pull up help file for the attributes() function
?attributes

Attributes of a named atomic vector:

# create named atomic vector
(vec_x <- c(a = 1, b = 2, c = 3, d = 4))
a b c d 
1 2 3 4 
attributes(x = vec_x)
$names
[1] "a" "b" "c" "d"
attributes(x = vec_x) %>% str() # a named list of vectors!
List of 1
 $ names: chr [1:4] "a" "b" "c" "d"
# remove all attributes from the object
attributes(vec_x) <- NULL
vec_x
[1] 1 2 3 4
attributes(x = vec_x)
NULL

In this example, removing all attributes only removes the vector’s names. Be careful when using this syntax with other objects: it can remove important information such as an object’s class or factor levels.

2.4.2 Attributes of a variable in a data frame


Accessing variable using [[]] subset operator

  • Recall object_name[["element_name"]] accesses contents of the element
  • If object is a data frame, df_name[["var_name"]] accesses contents of variable
    • For simple vars like firstgen, syntax yields an atomic vector (“just the data”)
  • Shorthand syntax for df_name[["var_name"]] is df_name$var_name
str(object = wwlist[["firstgen"]])
 chr [1:268396] NA "N" "N" "N" NA "N" "N" "Y" "Y" "N" "N" "N" "N" "N" "N" ...
attributes(x = wwlist[["firstgen"]])
NULL
str(object = wwlist$firstgen) # same same
 chr [1:268396] NA "N" "N" "N" NA "N" "N" "Y" "Y" "N" "N" "N" "N" "N" "N" ...
attributes(x = wwlist$firstgen)
NULL

Accessing variable using [] subset operator

  • object_name["element_name"] creates object of same type as object_name
  • If object is a data frame, df_name["var_name"] returns a data frame containing just the var_name column
str(object = wwlist["firstgen"])
attributes(x = wwlist["firstgen"])

2.4.3 Attributes of lists and data frames


Attributes of a named list:

list_x <- list(col_a = c(1,2,3), col_b = c(4,5,6))
str(object = list_x)
List of 2
 $ col_a: num [1:3] 1 2 3
 $ col_b: num [1:3] 4 5 6
attributes(x = list_x)
$names
[1] "col_a" "col_b"

Note that the names attribute is an attribute of the list, not an attribute of the elements within the list (which are atomic vectors)

list_x[['col_a']] # the element named 'col_a'
[1] 1 2 3
str(object = list_x[['col_a']]) # structure of the element named 'col_a'
 num [1:3] 1 2 3
attributes(x = list_x[['col_a']]) # attributes of element named 'col_a'
NULL

Attributes of a data frame:

df_x <- data.frame(col_a = c(1,2,3), col_b = c(4,5,6))
str(object = df_x)
'data.frame':   3 obs. of  2 variables:
 $ col_a: num  1 2 3
 $ col_b: num  4 5 6
attributes(x = df_x)
$names
[1] "col_a" "col_b"

$class
[1] "data.frame"

$row.names
[1] 1 2 3

Note: attributes names, class and row.names are attributes of the data frame

  • they are not attributes of the elements (variables) within the data frame, which are atomic vectors (i.e., just the data)
str(object = df_x[['col_a']]) # structure of the element named 'col_a'
 num [1:3] 1 2 3
attributes(x = df_x[['col_a']]) # attributes of element named 'col_a'
NULL

2.4.4 attr() function to get or set specific attributes of an object

Syntax

  • Get: attr(x = obj_name, which = attribute_name, exact = FALSE)
  • Set: attr(obj_name, attribute_name) <- value

Arguments

  • x: an object whose attributes are to be accessed
  • which: a non-empty character string specifying which attribute is to be accessed
  • exact (logical): should which be matched exactly? default is exact = FALSE
  • value: an object, new value of attribute, or NULL to remove attribute


Using attr() to get specific attribute of an object

vec_x <- c(a = 1, b= 2, c= 3, d = 4)
attributes(x = vec_x)
$names
[1] "a" "b" "c" "d"
attr(x = vec_x, which = "names", exact = FALSE)
[1] "a" "b" "c" "d"
attr(x = vec_x, which = "names")
[1] "a" "b" "c" "d"
attr(x = vec_x, which = "name") # we don't provide exact name of attribute
[1] "a" "b" "c" "d"
attr(x = vec_x, which = "name", exact = TRUE) # don't provide exact name of attribute
NULL


Using attr() to set specific attribute of an object

(vec_x <- c(a = 1, b= 2, c= 3, d = 4))
attributes(x = vec_x) # see all attributes

attr(x = vec_x, which = "greeting") <- "Hi!" # create new attribute
attr(x = vec_x, which = "greeting") # see attribute

attr(vec_x, "farewell") <- "Bye!" # create attribute

attributes(x = vec_x) # see all attributes

# remember that the attribute object is just a named list of vectors
attributes(x = vec_x) %>% str() # each attribute is an element of a list; 
  # for each attribute, we have an attribute name and an atomic vector that contains the values of that attribute

attr(x = vec_x, which = "names") # see names attribute
attr(x = vec_x, which = "names") <- NULL # delete names attribute

attributes(x = vec_x) # see all attributes


2.4.5 attr() function on data frames


Using wwlist, create data frame with three variables

wwlist_small <- wwlist[1:25, ] %>% select(hs_state,firstgen,med_inc_zip)
str(object = wwlist_small)
tibble [25 × 3] (S3: tbl_df/tbl/data.frame)
 $ hs_state   : chr [1:25] "WA" "WA" "WA" "WA" ...
 $ firstgen   : chr [1:25] NA "N" "N" "N" ...
 $ med_inc_zip: num [1:25] 92320 63653 88344 88408 82895 ...
attributes(x = wwlist_small)
$names
[1] "hs_state"    "firstgen"    "med_inc_zip"

$row.names
 [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

$class
[1] "tbl_df"     "tbl"        "data.frame"
attributes(x = wwlist_small) %>% str()
List of 3
 $ names    : chr [1:3] "hs_state" "firstgen" "med_inc_zip"
 $ row.names: int [1:25] 1 2 3 4 5 6 7 8 9 10 ...
 $ class    : chr [1:3] "tbl_df" "tbl" "data.frame"


Get/set attribute of a data frame

#get/examine names attribute
attr(x = wwlist_small, which = "names")
[1] "hs_state"    "firstgen"    "med_inc_zip"
str(object = attr(x = wwlist_small, which = "names")) # names attribute is character atomic vector, length=3
 chr [1:3] "hs_state" "firstgen" "med_inc_zip"
#add new attribute to data frame
attr(x = wwlist_small, which = "new_attribute") <- "contents of new attribute"
attributes(x = wwlist_small)
$names
[1] "hs_state"    "firstgen"    "med_inc_zip"

$row.names
 [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

$class
[1] "tbl_df"     "tbl"        "data.frame"

$new_attribute
[1] "contents of new attribute"

Get/set attribute of a variable in data frame (output omitted)

str(object = wwlist_small$med_inc_zip)
attributes(x = wwlist_small$med_inc_zip)
#create attribute for variable med_inc_zip
attr(wwlist_small$med_inc_zip, "inc attribute") <- "inc attribute contents"
#investigate attribute for variable med_inc_zip
attributes(x = wwlist_small$med_inc_zip)
str(object = wwlist_small$med_inc_zip)
attr(x = wwlist_small$med_inc_zip, which = "inc attribute")

2.4.6 Why add attributes to data frame or variables of data frame?

Pedagogical reasons

  • Important to know how you can apply attributes() and attr() to data frames and to variables within data frames


Practical application: changing variable names from lower-case to upper-case or vice versa

There are other ways to change variable names, but here is one.

# first we investigate
  attributes(x = wwlist_small)
  
  #let's take a gander at the names attribute
  attr(x = wwlist_small, which = "names")
  attr(x = wwlist_small, which = "names") %>% str() # a character atomic vector of length 3
  
  wwlist_small %>% length() # not coincidentally, the object wwlist_small also has length = 3
  
# without assignment, let's see if we can apply str_to_upper() function to names attribute
  attr(x = wwlist_small, which = "names")
  
  attr(x = wwlist_small, which = "names") %>% str_to_upper()
  
# now, let's change variable names to uppercase [PLEASE NEVER DO THIS!]
  attr(x = wwlist_small, which = "names") <- attr(x = wwlist_small, which = "names") %>% str_to_upper()
  
  attributes(wwlist_small)
  wwlist_small %>% print(n=3)
  
# now, let sanity previal and change the names back to lowercase!
  
  attr(x = wwlist_small, which = "names") # always inspect before we modify!
  
  attr(x = wwlist_small, which = "names") <- attr(x = wwlist_small, which = "names") %>% str_to_lower()
  
  attributes(wwlist_small) # thank goodness!
  wwlist_small %>% print(n=3)


Practical application: in interactive dashboards

  • When creating “dashboard” you might want to add “tooltips”
    • “Tooltip” is a message that appears when cursor is positioned over an icon
    • The text in the tooltip is the contents of an attribute
  • Example dashboard: LINK

2.4.7 Student exercise

  1. Using wwlist, create data frame of 30 observations with three variables: state, zip5, pop_total_zip

  2. Return all attributes of this new data frame using attributes(). Then, get the names attribute of the data frame using attr().

  3. Add a new attribute to the data frame called attribute_data whose content is "new attribute of data". Then, return all attributes of the data frame as well as get the value of the newly created attribute_data.

  4. Return the attributes of the variable pop_total_zip in the data frame.

  5. Add a new attribute to the variable pop_total_zip called attribute_variable whose content is "new attribute of variable". Then, return all attributes of the variable as well as get the value of the newly created attribute_variable.

Solutions to student exercise
# Part 1
wwlist_exercise <- wwlist[1:30, ] %>% select(state, zip5, pop_total_zip)

# Part 2
attributes(x = wwlist_exercise)
attr(x = wwlist_exercise, which = "names")

# Part 3
attr(x = wwlist_exercise, which = "attribute_data") <- "new attribute of data"

attributes(x = wwlist_exercise)
attr(x = wwlist_exercise, which = "attribute_data")

# Part 4
attributes(x = wwlist_exercise$pop_total_zip)

# Part 5
attr(wwlist_exercise$pop_total_zip, "attribute_variable") <- "new attribute of variable"

attributes(x = wwlist_exercise$pop_total_zip)
attr(x = wwlist_exercise$pop_total_zip, which = "attribute_variable")


3 Object class


Every object in R has a class. An object’s type describes how its values are stored, while its class helps R interpret the object and determine how functions behave when applied to it.

  • Common classes include numeric, character, factor, Date, and data.frame
  • An object’s class helps determine:
    • Which functions are appropriate for that object
    • How functions interpret, display, or transform the object
  • Type and class are related, but they are not the same:
    • Type describes how an object’s values are stored
    • Class helps describe what the object represents and how R should treat it
  • For many objects, class is stored in a class attribute
    • For simple atomic vectors, R can infer the class from the object’s type even when there is no explicit class attribute
  • An object can also have more than one class

Use the class() function to identify an object’s class:

(vec_x <- c(a = 1, b= 2, c= 3, d = 4))
a b c d 
1 2 3 4 
typeof(x = vec_x)
[1] "double"
class(x = vec_x)
[1] "numeric"

When I encounter a new object I often investigate object by applying typeof(), class(), and attributes() functions:

typeof(x = vec_x)
[1] "double"
class(x = vec_x)
[1] "numeric"
attributes(x = vec_x)
$names
[1] "a" "b" "c" "d"

3.1 Why is object class important?


When you apply a function to an object, the object’s type and class can both matter.

  • Different functions use type and class in different ways:
    • Date functions often rely on class to interpret stored values as dates
    • String functions operate on objects with character type; when an object’s type is not character, its class can affect how it is converted to character
    • Mathematical functions generally require a numeric or logical type, but an object’s class can change or prevent the calculation
  • Some functions can work with more than one class but behave differently depending on the object’s class
  • Some functions try to convert an object to an appropriate type or class before operating on it

Therefore, an object’s class can affect:

  • Whether a function can work with the object
  • How the function interprets the object
  • What kind of result the function returns

3.1.1 Class and object-oriented programming


R is an object-oriented programming language.

In plain language, this means that functions can use the class of the object they receive to decide how to behave. The same function may do one thing when applied to an object of one class and something different when applied to an object of another class.

You do not need to learn how to create an object-oriented system for this course. The practical idea is:

  • Objects have classes
  • Functions can use those classes
  • The same function can behave differently for different classes

Some R functions are generic functions. A generic function examines an object’s class and selects a class-specific version of the function to use.

Examples of generic functions include:

  • print(), which displays an object
  • summary(), which produces a summary of an object

The class-specific version of a generic function is called a method. Method names often combine the generic function’s name and the class name.

For example, when you call print() on an object with class Date:

  1. You call the generic function print()
  2. R examines the object’s class and sees that it is Date
  3. R executes the print.Date() method, which displays the stored values as calendar dates

When you call print() on an object with class data.frame, R instead executes the print.data.frame() method, which displays the object in rows and columns.

The same process occurs when you call summary():

  • For an object with class factor, R executes summary.factor(), which reports the number of observations in each factor level
  • For an object with class data.frame, R executes summary.data.frame(), which produces an appropriate summary for each variable

You normally call the generic function, such as print() or summary(). R examines the object’s class and executes the appropriate method for you.

Many object classes already exist in R, and packages can provide additional classes.

  • Examples include numeric, character, factor, Date, and data.frame
  • The haven package provides classes for variables imported with value labels
  • In this course, we will work with classes that have already been created for us

3.1.2 Implicit versus explicit class


An object’s class can be implicit or explicit:

  • An implicit class is inferred by R from the object’s underlying type
    • Examples include ordinary numeric, integer, logical, and character atomic vectors
  • An explicit class is stored in the object’s class attribute
    • Examples include objects with class Date, factor, and data.frame

To see the difference, let’s compare two atomic vectors that contain the same underlying values and have the same underlying type.

# Create an ordinary numeric vector
vec_num <- c(19723, 19724, 19725)

# Create a Date vector using ymd() from lubridate
vec_date <- ymd(
  c("2024-01-01", "2024-01-02", "2024-01-03")
)

vec_num
[1] 19723 19724 19725
vec_date
[1] "2024-01-01" "2024-01-02" "2024-01-03"

Both objects have type double:

typeof(x = vec_num)
[1] "double"
typeof(x = vec_date)
[1] "double"

However, the objects have different classes:

class(x = vec_num)
[1] "numeric"
class(x = vec_date)
[1] "Date"

Now inspect whether each object has a stored class attribute:

attr(x = vec_num, which = "class")
NULL
attr(x = vec_date, which = "class")
[1] "Date"
attributes(x = vec_num)
NULL
attributes(x = vec_date)
$class
[1] "Date"
  • vec_num has an implicit numeric class. R infers this class from the vector’s double type, even though the object does not have a stored class attribute.
  • vec_date has an explicit Date class stored in its class attribute. This attribute tells R to interpret and display the underlying numbers as dates.

Main takeaway: good practice to use class() to identify an object’s class.

  • using attr() or attributes() function to identify class attribute will miss the implicit classes of simple atomic vectors.

3.1.3 Functions can depend on object type, class, or both

The examples below show that different functions use type and class in different ways.

Example: sum()—underlying type matters

The sum() function is designed to operate on logical, integer, double, or complex data.

Apply sum() to a logical vector:

vec_logical <- c(TRUE, FALSE, NA, TRUE)

typeof(x = vec_logical)
[1] "logical"
class(x = vec_logical)
[1] "logical"
sum(vec_logical, na.rm = TRUE)
[1] 2

For mathematical calculations, R treats TRUE as 1 and FALSE as 0. Therefore, this sum counts the two TRUE values.

A character vector cannot be summed because its underlying type is not appropriate for mathematical calculation.

vec_character <- c("1", "2", "3")

typeof(x = vec_character)
class(x = vec_character)

sum(vec_character)

Class can change function behavior even when the underlying type is the same

Apply sum() to the ordinary numeric vector created in the previous section:

typeof(x = vec_num)
[1] "double"
class(x = vec_num)
[1] "numeric"
sum(vec_num)
[1] 59172

Now try to apply sum() to the Date vector:

typeof(x = vec_date)
class(x = vec_date)

sum(vec_date) # won't run because not an appropriate class 

vec_num and vec_date have the same underlying values and the same double type. However, vec_date has an explicit Date class, and adding calendar dates together is not a meaningful operation. Its class therefore prevents sum() from performing the calculation.

Example: year()—class supplies meaning

The year() function from lubridate is designed to extract the year from an object with a date or date-time class.

typeof(x = vec_date)
[1] "double"
class(x = vec_date)
[1] "Date"
year(x = vec_date)
[1] 2024 2024 2024

Although vec_date is stored as double values, its Date class tells year() to interpret those values as calendar dates.

Example: tolower()—a function can convert its input

The tolower() function operates on character data:

vec_character <- c("SEATTLE", "BELLINGHAM")

typeof(x = vec_character)
[1] "character"
class(x = vec_character)
[1] "character"
tolower(x = vec_character)
[1] "seattle"    "bellingham"

tolower() can also accept some non-character objects by converting them to character first. An object’s class can affect how that conversion occurs.

Main takeaway:

  • A function may rely primarily on an object’s type
  • A function may rely primarily on an object’s class
  • A function may use both type and class
  • A function may convert an object before operating on it

4 Class == factor

Recoding variable ethn_code from data frame wwlist

Let’s first recode the ethn_code variable:

As introduced in the mutate lecture, recode_values() maps each existing value on the left-hand side of ~ to its new value on the right-hand side.

wwlist <- wwlist %>%  
  mutate(ethn_code = 
    recode_values(x = ethn_code,
      "american indian or alaska native" ~ "nativeam",
      "asian or native hawaiian or other pacific islander" ~ "api",
      "black or african american" ~ "black",
      "cuban" ~ "latinx",
      "mexican/mexican american" ~ "latinx",
      "not reported" ~ "not_reported",
      "other-2 or more" ~ "multirace",
      "other spanish/hispanic" ~ "latinx",
      "puerto rican" ~ "latinx",
      "white" ~ "white",
      default = ethn_code
    )
  )

str(object = wwlist$ethn_code)
wwlist %>% count(ethn_code)

4.1 Factors

Factors are an object class used to represent categorical data (e.g., marital status)

  • A factor is an augmented atomic vector built from an integer vector with two important attributes:
    • A levels attribute that associates the underlying integers with category labels
    • An explicit class = "factor" attribute that tells R to treat the object as a factor

Categorical variables can be stored as character or factor variables:

  • Character variables are often convenient while cleaning and recoding data
  • Factor variables are especially useful when category order, display order, reference categories, or modeling behavior matters
  • So far in the course I have made all categorical variables character variables because we have not introduced factors yet

Create factor version of character variable ethn_code using base R factor() function:

str(object = wwlist$ethn_code)
 chr [1:268396] "multirace" "white" "white" "multirace" "white" "multirace" ...
class(x = wwlist$ethn_code)
[1] "character"
# create factor variable using factor() within mutate()
wwlist <- wwlist %>% mutate(ethn_code_fac = factor(x = ethn_code))
#wwlist$ethn_code_fac <- factor(x = wwlist$ethn_code) # base r approach

str(object = wwlist$ethn_code)
 chr [1:268396] "multirace" "white" "white" "multirace" "white" "multirace" ...
str(object = wwlist$ethn_code_fac)
 Factor w/ 7 levels "api","black",..: 4 7 7 4 7 4 4 4 4 7 ...

Character variable ethn_code:

typeof(x = wwlist$ethn_code)
[1] "character"
class(x = wwlist$ethn_code)
[1] "character"
attributes(x = wwlist$ethn_code)
NULL
str(object = wwlist$ethn_code)
 chr [1:268396] "multirace" "white" "white" "multirace" "white" "multirace" ...

Factor variable ethn_code_fac:

typeof(x = wwlist$ethn_code_fac)
[1] "integer"
class(x = wwlist$ethn_code_fac)
[1] "factor"
attributes(x = wwlist$ethn_code_fac)
$levels
[1] "api"          "black"        "latinx"       "multirace"    "nativeam"    
[6] "not_reported" "white"       

$class
[1] "factor"
str(object = wwlist$ethn_code_fac)
 Factor w/ 7 levels "api","black",..: 4 7 7 4 7 4 4 4 4 7 ...

4.2 Working with factor variables

Main things to note about variable ethn_code_fac

  • type = integer
  • class = factor, stored in the explicit class attribute
  • The underlying data are integers, while the levels attribute supplies the category labels that are displayed:
# Print first few obs of ethn_code_fac
wwlist$ethn_code_fac[1:5]
[1] multirace white     white     multirace white    
Levels: api black latinx multirace nativeam not_reported white
# Print count for each category in ethn_code_fac
wwlist %>% count(ethn_code_fac)
# A tibble: 7 × 2
  ethn_code_fac      n
  <fct>          <int>
1 api             2385
2 black            563
3 latinx          9245
4 multirace      90584
5 nativeam         202
6 not_reported    5737
7 white         159680

Apply as.integer() to display the underlying integer codes of the factor variable

  • This is useful for understanding how factors are stored internally
  • Do not use these integer codes to identify categories in an analysis because the codes can change if the factor levels change or are reordered

Investigate as.integer() function:

typeof(x = wwlist$ethn_code_fac)
[1] "integer"
class(x = wwlist$ethn_code_fac)
[1] "factor"
typeof(x = as.integer(x = wwlist$ethn_code_fac))
[1] "integer"
class(x = as.integer(x = wwlist$ethn_code_fac))
[1] "integer"

Display underlying integer values of variable ethn_code_fac:

wwlist %>% count(as.integer(x = ethn_code_fac))
# A tibble: 7 × 2
  `as.integer(x = ethn_code_fac)`      n
                            <int>  <int>
1                               1   2385
2                               2    563
3                               3   9245
4                               4  90584
5                               5    202
6                               6   5737
7                               7 159680


Refer to categories of a factor (e.g., when filtering observations) using values of the levels attribute rather than the underlying integer codes

  • Values of levels attribute for ethn_code_fac (output omitted)
attributes(x = wwlist$ethn_code_fac)


Example: Count the number of prospects in wwlist who identify as “white”

# referring to an underlying integer code; this doesn't work
wwlist %>% filter(ethn_code_fac==7) %>% count() 
# A tibble: 1 × 1
      n
  <int>
1     0
# referring to a value of the levels attribute; this works
wwlist %>% filter(ethn_code_fac=="white") %>% count()
# A tibble: 1 × 1
       n
   <int>
1 159680

Although applying as.integer() first would make the code below run, do not use this approach to identify categories. The code 7 describes the current position of "white" in the levels, and that position can change.

# not recommended: relies on the current ordering of the factor levels
attributes(x = wwlist$ethn_code_fac)
wwlist %>% filter(as.integer(x = ethn_code_fac)==7) %>% count()

4.3 How to identify the variable values associated with factor levels

Create a factor version of the character variable psat_range

wwlist %>% count(psat_range)
wwlist <- wwlist %>% mutate(psat_range_fac = factor(x = psat_range))
wwlist %>% count(psat_range_fac)
attributes(x = wwlist$psat_range_fac)

Investigate values associated with factor levels using levels() and nlevels()

levels(x = wwlist$psat_range_fac) #starts at 1
nlevels(x = wwlist$psat_range_fac) #7 levels total
levels(x = wwlist$psat_range_fac)[1:3] #prints levels 1-3

Once values associated with factor levels are known, filter using the level label rather than its underlying integer code:

wwlist %>% filter(psat_range_fac=="1270-1520") %>% count()
# A tibble: 1 × 1
      n
  <int>
1  8348

4.3.1 Student exercise working with factors

  1. After running the code below, use typeof(), class(), str(), and attributes() functions to check the new variable receive_year
  2. Create a factor variable from the input variable receive_year and name it receive_year_fac
  3. Run the same functions (typeof(), class(), etc.) from the first question using the new variable you created
  4. Get a count of receive_year_fac. (hint: you could also run this in the console to see values associated with each factor)

Run this code to create a year variable from the input variable receive_date:

# wwlist %>% glimpse()

library(package = lubridate) # load library if you haven't already
wwlist <- wwlist %>%
  mutate(receive_year = year(x = receive_date)) # create year variable with lubridate

# Check variable
wwlist %>% 
  count(receive_year)

wwlist %>%
  group_by(receive_year) %>% 
  count(receive_date)
Solutions to student exercise working with factors %>%
  1. After running the code below, use typeof(), class(), str(), and attributes() functions to check the new variable receive_year
typeof(x = wwlist$receive_year)
[1] "double"
class(x = wwlist$receive_year)
[1] "numeric"
str(object = wwlist$receive_year)
 num [1:268396] 2016 2016 2016 2016 2016 ...
attributes(x = wwlist$receive_year)
NULL
  1. Create a factor variable from the input variable receive_year and name it receive_year_fac
# create factor var; tidyverse approach
wwlist <- wwlist %>%
  mutate(receive_year_fac = factor(x = receive_year))
  1. Run the same functions (typeof(), class(), etc.) from the first question using the new variable you created
typeof(x = wwlist$receive_year_fac)
[1] "integer"
class(x = wwlist$receive_year_fac)
[1] "factor"
str(object = wwlist$receive_year_fac)
 Factor w/ 3 levels "2016","2017",..: 1 1 1 1 1 1 1 1 1 1 ...
attributes(x = wwlist$receive_year_fac)
$levels
[1] "2016" "2017" "2018"

$class
[1] "factor"
  1. Get a count of receive_year_fac. (hint: you could also run this in the console to see values associated with each factor)
wwlist %>%
  count(receive_year_fac)
# A tibble: 3 × 2
  receive_year_fac     n
  <fct>            <int>
1 2016             89637
2 2017             89816
3 2018             88943


5 Haven-labelled vectors

5.1 Data we will use to introduce haven-labelled vectors

High school longitudinal surveys from National Center for Education Statistics (NCES)

  • Follow U.S. students from high school through college, labor market


We will be working with High School Longitudinal Study of 2009 (HSLS:09)

  • Follows 9th graders from 2009
  • Data collection waves
    • Base Year (2009)
    • First Follow-up (2012)
    • 2013 Update (2013)
    • High School Transcripts (2013-2014)
    • Second Follow-up (2016)

HSLS is one of the data sources we used for article, A Sociological Analysis of Structural Racism in ‘Student List’ Lead Generation Products

5.2 Using haven package to read SAS/SPSS/Stata datasets into R

haven, which is part of tidyverse, “enables R to read and write various data formats” from the following statistical packages:

  • SAS
  • SPSS
  • Stata


If you haven’t installed haven run the following code in your console.

  • Generic syntax: install.packages(pkgs = "package_name")
  • Install “haven”: install.packages(pkgs = "haven")

Make sure to load haven (e.g., library(package = haven))

When using haven to read data, resulting R objects have these characteristics:

  • Data frames are tibbles, Tidyverse’s preferred class of data frames
  • Variables with “value labels” are imported as haven-labelled vectors
    • class() generally returns a class vector beginning with "haven_labelled", followed by other classes that describe the object
    • This is an example of one object having more than one class
    • Haven-labelled vectors preserve the original values and metadata imported from another statistical package
    • Helpful description of haven-labelled vectors HERE
  • Dates and times converted to R date/time classes
  • Character vectors not converted to factors

Use read_dta() function from haven package to import Stata dataset into R

hsls <- read_dta(file = "https://github.com/ozanj/rclass/raw/master/data/hsls/hsls_stu_small.dta")

Must run this code chunk; changes the variable names in the in-memory hsls object from uppercase to lowercase

  • This does not permanently change the variable names in the source Stata file
names(x = hsls)
names(hsls) <- tolower(x = names(x = hsls)) # convert names to lowercase
names(x = hsls) # names now lowercase

str(object = hsls) # ugh

Investigate variable s3classes from data frame hsls

  • Identifies whether respondent taking postsecondary classes as of 11/1/2013
typeof(x = hsls$s3classes)
class(x = hsls$s3classes)
str(object = hsls$s3classes)

Investigate attributes of s3classes

attributes(x = hsls$s3classes) # all attributes

#specific attributes: using syntax: attr(x = obj_name, which = attribute_name, exact = FALSE)
attr(x = hsls$s3classes, which = "label") # label attribute
attr(x = hsls$s3classes, which = "labels") # labels attribute

5.3 What is a haven-labelled vector?


Variable labels are descriptions attached to a specific variable (e.g., “marital status”).

Value labels are descriptions attached to specific stored values of a variable, e.g.:

  • Var value 1 attached to value label “married”, 2=“single”, 3=“divorced”


Haven-labelled vectors preserve variables with value labels imported from SAS, SPSS, or Stata.

  • The haven package provides the haven_labelled class
  • A haven-labelled vector can have underlying type integer, double, or character
  • The object can have multiple classes, so class() may return more than one value
  • To see value labels associated with each value:
    • attr(x = df_name$var_name, which = "labels")
    • E.g., attr(x = hsls$s3classes, which = "labels")

Investigate the attributes of hsls$s3classes

typeof(x = hsls$s3classes)
class(x = hsls$s3classes)
str(object = hsls$s3classes)
attributes(x = hsls$s3classes)

Use attr(x = obj_name, which = "attribute_name") to refer to each attribute

attr(x = hsls$s3classes, which = "label")
attr(x = hsls$s3classes, which = "format.stata")
attr(x = hsls$s3classes, which = "class")
attr(x = hsls$s3classes, which = "labels")

5.4 labelled package

Purpose of the labelled package is to work with data imported from SPSS, Stata, or SAS using the haven package

  • labelled contains functions for working with variable labels, value labels, and defined missing values stored as metadata
  • More info on the labelled package: LINK

Functions in labelled package

5.5 Get variable and value labels

Get variable labels using var_label()

hsls %>% select(s3classes) %>% var_label()
$s3classes
[1] "S3 B01A Taking postsecondary classes as of Nov 1 2013"


Get value labels using val_labels()

hsls %>% select(s3classes) %>% val_labels()
$s3classes
                                     Missing 
                                          -9 
                           Unit non-response 
                                          -8 
                     Item legitimate skip/NA 
                                          -7 
                    Component not applicable 
                                          -6 
Item not administered: abbreviated interview 
                                          -4 
                                         Yes 
                                           1 
                                          No 
                                           2 
                                  Don't know 
                                           3 

5.6 Working with haven-labelled data


Create frequency tables with haven-labelled variables using count()

  • Current tibble output shows the stored variable values together with their value labels
hsls %>% count(s3classes)
# A tibble: 5 × 2
  s3classes                  n
  <dbl+lbl>              <int>
1 -9 [Missing]              59
2 -8 [Unit non-response]  4945
3  1 [Yes]               13477
4  2 [No]                 3401
5  3 [Don't know]         1621


To use the value labels themselves as the displayed category values, add %>% as_factor() to the pipe

  • as_factor() is a function from haven that converts a haven-labelled object to a factor
  • Earlier, we used base R factor() for ordinary character or numeric vectors. Here, we use haven::as_factor() because the starting variable is haven-labelled; its value labels become the factor levels.
hsls %>% count(s3classes) %>% as_factor()
# A tibble: 5 × 2
  s3classes             n
  <fct>             <int>
1 Missing              59
2 Unit non-response  4945
3 Yes               13477
4 No                 3401
5 Don't know         1621

To isolate values of haven-labelled variables in filter():

  • Refer to variable value, not the value label

Important note about missing-value codes:

  • A label such as "Unit non-response" or "Missing" does not automatically make the stored value missing in R
  • Values such as -8 and -9 remain ordinary numbers unless we explicitly exclude them or convert them to NA
  • If we do not handle these codes, R may include them in calculations such as means and regression models

Task

  • How many observations in var s3classes associated with “Unit non-response”
  • How many observations in var s3classes associated with “Yes”

General steps to follow:

  1. Investigate object
  2. Use filter() to isolate desired observations

Investigate object

class(x = hsls$s3classes)
hsls %>% select(s3classes) %>% var_label() #show variable label
hsls %>% select(s3classes) %>% val_labels() #show value label

hsls %>% count(s3classes) # stored values shown with their value labels
hsls %>% count(s3classes) %>% as_factor() # labels converted to factor values

Filter specific values

hsls %>% filter(s3classes==-8) %>% count() # -8 = unit non-response
hsls %>% filter(s3classes==1) %>% count() # 1 = yes

5.7 Set variable and value labels


Set variable labels using var_label() or set_variable_labels()

# Set one variable label
var_label(df_name$var_name) <- 'variable label'

# Set multiple variable labels
df_name <- df_name %>%
  set_variable_labels(
    var_name_1 = 'variable label 1',
    var_name_2 = 'variable label 2',
    var_name_3 = 'variable label 3'
  )


Set value labels using val_label() or set_value_labels()

# Set one value label
val_label(df_name$var_name, 'variable_value') <- 'value_label'

# Set multiple value labels
df_name <- df_name %>%
  set_value_labels(
    var_name_1 = c(
      'value_label_1' = 'variable_value_1',
      'value_label_2' = 'variable_value_2'
    ),
    var_name_2 = c(
      'value_label_3' = 'variable_value_3',
      'value_label_4' = 'variable_value_4'
    )
  )

5.7.1 Create example data frame

df_x <- tribble(
  ~id, ~edu, ~sch,
  #--|--|----
  1, 2, 2,
  2, 1, 1,
  3, 3, 2,
  4, 4, 2,
  5, 1, 2
)
df_x
# A tibble: 5 × 3
     id   edu   sch
  <dbl> <dbl> <dbl>
1     1     2     2
2     2     1     1
3     3     3     2
4     4     4     2
5     5     1     2
str(object = df_x)
tibble [5 × 3] (S3: tbl_df/tbl/data.frame)
 $ id : num [1:5] 1 2 3 4 5
 $ edu: num [1:5] 2 1 3 4 1
 $ sch: num [1:5] 2 1 2 2 2

5.7.2 Set variable labels

Use set_variable_labels() or var_label() to manually set variable labels

str(object = df_x$sch)
 num [1:5] 2 1 2 2 2
var_label(x = df_x$sch)
NULL
# Using set_variable_labels()
df_x <- df_x %>%
  set_variable_labels(
    id = "Unique identification number",
    edu = "Education level"
  )

# Using var_label()
var_label(df_x$sch) <- 'Type of school attending'

str(object = df_x$sch)
 num [1:5] 2 1 2 2 2
 - attr(*, "label")= chr "Type of school attending"
var_label(x = df_x$sch)
[1] "Type of school attending"

5.7.3 Set value labels

Use set_value_labels() or val_label() to manually set value labels

val_labels(x = df_x$sch)
NULL
# Using set_value_labels()
df_x <- df_x %>%
  set_value_labels(
    edu = c('High School' = 1,
            'AA degree' = 2,
            'BA degree' = 3,
            'MA or higher' = 4),
    sch = c('Private' = 1))

# Using val_label()
val_label(df_x$sch, 2) <- 'Public'

str(object = df_x$sch)
 dbl+lbl [1:5] 2, 1, 2, 2, 2
 @ labels: Named num [1:2] 1 2
  ..- attr(*, "names")= chr [1:2] "Private" "Public"
 @ label : chr "Type of school attending"
val_labels(x = df_x$sch)
Private  Public 
      1       2 

5.7.4 View the set variable and value labels

# View variable and value labels using attributes()
attributes(x = df_x$sch)
$labels
Private  Public 
      1       2 

$label
[1] "Type of school attending"

$class
[1] "haven_labelled" "vctrs_vctr"     "double"        
# View variable label
var_label(x = df_x$sch)
[1] "Type of school attending"
attr(x = df_x$sch, which = 'label')
[1] "Type of school attending"
# View value labels
val_labels(x = df_x$sch)
Private  Public 
      1       2 
attr(x = df_x$sch, which = 'labels')
Private  Public 
      1       2 

5.7.5 labelled student exercise

  1. Get variable and value labels of the variable s3hs in the hsls data frame
  2. Get a count of the variable s3hs showing the values and the value labels (hint: use as_factor())
  3. Get a count of the rows whose value for s3hs is associated with “Missing” (hint: use filter())
  4. Get a count of the rows whose value for s3hs is associated with “Missing” or “Unit non-response”
  5. Add variable label for pop_asian_zip & pop_asian_state in data frame wwlist
  6. Add value labels for ethn_code in data frame wwlist
labelled student exercise solutions
  1. Get variable and value labels of the variable s3hs in the hsls data frame
hsls %>% 
  select(s3hs) %>% 
  var_label() 
$s3hs
[1] "S3 B01F Attending high school or homeschool as of Nov 1 2013"
hsls %>% 
  select(s3hs) %>% 
  val_labels()
$s3hs
                                     Missing 
                                          -9 
                           Unit non-response 
                                          -8 
                     Item legitimate skip/NA 
                                          -7 
                    Component not applicable 
                                          -6 
Item not administered: abbreviated interview 
                                          -4 
                                         Yes 
                                           1 
                                          No 
                                           2 
                                  Don't know 
                                           3 
  1. Get a count of the variable s3hs showing the values and the value labels (hint: use as_factor())
hsls %>% 
  count(s3hs) 
# A tibble: 6 × 2
  s3hs                             n
  <dbl+lbl>                    <int>
1 -9 [Missing]                    22
2 -8 [Unit non-response]        4945
3 -7 [Item legitimate skip/NA] 16770
4  1 [Yes]                       624
5  2 [No]                        985
6  3 [Don't know]                157
hsls %>% 
  count(s3hs) %>% 
  as_factor() 
# A tibble: 6 × 2
  s3hs                        n
  <fct>                   <int>
1 Missing                    22
2 Unit non-response        4945
3 Item legitimate skip/NA 16770
4 Yes                       624
5 No                        985
6 Don't know                157
  1. Get a count of the rows whose value for s3hs is associated with “Missing” (hint: use filter())
hsls %>%
  filter(s3hs== -9) %>% 
  count()
# A tibble: 1 × 1
      n
  <int>
1    22
  1. Get a count of the rows whose value for s3hs is associated with “Missing” or “Unit non-response”
hsls %>%
  filter(s3hs== -9 | s3hs== -8) %>% 
  count()
# A tibble: 1 × 1
      n
  <int>
1  4967
  1. Add variable label for pop_asian_zip & pop_asian_state in data frame wwlist
# variable labels
wwlist %>% select(pop_asian_zip, pop_asian_state) %>% var_label()
$pop_asian_zip
NULL

$pop_asian_state
NULL
# set variable labels
wwlist <- wwlist %>% 
  set_variable_labels(
    pop_asian_zip = "total asian population in zip",
    pop_asian_state ="total asian population in state"
  )

# attribute of variable
attributes(x = wwlist$pop_asian_zip)
$label
[1] "total asian population in zip"
attributes(x = wwlist$pop_asian_state)
$label
[1] "total asian population in state"
  1. Add value labels for ethn_code in data frame wwlist
# count
wwlist %>% count(ethn_code)

# value labels
wwlist %>% select(ethn_code) %>% val_labels

# set value labels to ethn_code variable
wwlist <- wwlist %>% 
  set_value_labels(
    ethn_code = c("asian or native hawaiian or other pacific islander" = "api",
                  "black or african american" = "black",
                  "cuban or mexican/mexican american or other spanish/hispanic or puerto rican" = "latinx",
                  "other-2 or more" = "multirace",
                  "american indian or alaska native" = "nativeam",
                  "not reported" = "not_reported",
                  "white" = "white"
    )
  )


6 Comparing haven-labelled vectors to factors

Haven-labelled vector Factor
underlying type integer, double, or character integer
attribute containing categories labels levels
primary purpose preserve imported values and metadata represent categorical data in R
refer to categories using stored variable values level labels

Which conversion function should I use?

The function we use depends on the kind of object we start with and what we want to create.

Starting object What we want Preferred function
Ordinary character or numeric vector Factor factor()
Haven-labelled variable Factor that uses its value labels haven::as_factor()
Factor Character vector containing its level labels as.character()
Character vector containing numbers Numeric vector as.numeric()
Factor See its internal integer storage codes as.integer() for inspection only—not analysis

The final row is for looking “under the hood.” When filtering or analyzing a factor, refer to its level labels rather than its internal integer codes.

So should you work with haven-labelled vectors or factors?

  • They serve different purposes, so this is not simply a right-or-wrong choice
  • Personally, I prefer factor class when analyzing categorical variables
    • Easier to run analysis (see section below)
  • Haven-labelled vectors
    • Feels more suited to working with survey data variables, where there are usually several different values that represent different kinds of “missing” values.
  • You can use both! A useful workflow is to retain haven-labelled variables while inspecting and cleaning imported data, then deliberately convert categorical variables to factors for analysis.

6.0.1 Why convert categorical haven-labelled variables to factors for analysis


When a haven-labelled numeric variable represents categories (e.g., marital status: single, married, divorced, etc.), convert it to a factor before using it as a categorical variable in a model.

To demonstrate a quick example, let’s load data from ELS. We will use this data set later in the graphing and ggplot lecture, so don’t worry about understanding it now.

Say we are interested in the relationship between having an internship or co-op job (X) (in 2005-2006) on subsequent earnings (Y) (in 2011) after controlling for standardized reading test score (as a measure for high school achievement), sex, and race/ethnicity.

  • We have two linear models below, the first using numeric haven-labelled variables and the second using factor variables.

Before fitting either model, we exclude the documented survey codes that mean missing and rows with an actual NA for the internship variable. This is a separate step from converting categorical variables to factors.

df_els_model <- df_els_stu_allobs %>%
  filter(
    f2enroll0506 == 1,
    !f3ern2011 %in% c(-8, -4), # -8 and -4 are missing-value codes
    bytxrstd != -8, # -8 is a missing-value code
    !is.na(x = f2intern0506)
  )

# Convert the categorical predictors only after excluding missing-value codes
df_els_model_fac <- df_els_model %>%
  mutate(
    across(
      .cols = c(f2intern0506, f1sex, f1race_v2),
      .fns = as_factor
    )
  )


Linear model lm() treating haven-labelled categorical variables as numeric codes

$label
[1] "F1 sex-composite"

$format.stata
[1] "%8.0g"

$class
[1] "haven_labelled" "vctrs_vctr"     "double"        

$labels
  Male Female 
     1      2 
$labels
 white    api  black latinx native  multi 
     1      2      3      4      5      6 

$class
[1] "haven_labelled" "vctrs_vctr"     "double"        

$label
[1] "categorical measure of race based on variable f1race"
intern_mod1 <- lm(formula = f3ern2011 ~ f2intern0506 + bytxrstd + f1sex + f1race_v2, data = df_els_model)

summary(object = intern_mod1)

Call:
lm(formula = f3ern2011 ~ f2intern0506 + bytxrstd + f1sex + f1race_v2, 
    data = df_els_model)

Residuals:
   Min     1Q Median     3Q    Max 
-40385 -16139  -2848  11274 229059 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept)  25890.21    1955.68  13.238  < 2e-16 ***
f2intern0506  6092.37    1111.71   5.480 4.37e-08 ***
bytxrstd       262.55      30.27   8.673  < 2e-16 ***
f1sex        -6019.49     541.93 -11.108  < 2e-16 ***
f1race_v2    -1078.54     201.21  -5.360 8.53e-08 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 24430 on 8245 degrees of freedom
Multiple R-squared:  0.03243,   Adjusted R-squared:  0.03196 
F-statistic: 69.09 on 4 and 8245 DF,  p-value: < 2.2e-16


Linear model lm() after converting the categorical predictors to factors

$levels
[1] "Male"   "Female"

$class
[1] "factor"

$label
[1] "F1 sex-composite"
$levels
[1] "white"  "api"    "black"  "latinx" "native" "multi" 

$class
[1] "factor"

$label
[1] "categorical measure of race based on variable f1race"
intern_mod2 <- lm(formula = f3ern2011 ~ f2intern0506 + bytxrstd + f1sex + f1race_v2, data = df_els_model_fac)

summary(object = intern_mod2)

Call:
lm(formula = f3ern2011 ~ f2intern0506 + bytxrstd + f1sex + f1race_v2, 
    data = df_els_model_fac)

Residuals:
   Min     1Q Median     3Q    Max 
-40514 -16072  -2888  11218 226346 

Coefficients:
                Estimate Std. Error t value Pr(>|t|)    
(Intercept)     20589.82    1794.21  11.476  < 2e-16 ***
f2intern0506yes  6097.71    1111.52   5.486 4.24e-08 ***
bytxrstd          234.19      31.08   7.535 5.39e-14 ***
f1sexFemale     -5952.19     541.81 -10.986  < 2e-16 ***
f1race_v2api    -1259.75     866.13  -1.454   0.1459    
f1race_v2black  -4671.78     920.19  -5.077 3.92e-07 ***
f1race_v2latinx -4405.54     895.12  -4.922 8.74e-07 ***
f1race_v2native -9410.73    3882.96  -2.424   0.0154 *  
f1race_v2multi  -2007.62    1362.22  -1.474   0.1406    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 24410 on 8241 degrees of freedom
Multiple R-squared:  0.0345,    Adjusted R-squared:  0.03356 
F-statistic: 36.81 on 8 and 8241 DF,  p-value: < 2.2e-16

Why does this matter?

In intern_mod1, lm() sees the stored numeric codes and treats them as quantitative values. The value labels do not tell lm() that the numbers represent categories. For a variable with several unordered categories, such as race, this incorrectly treats a one-unit increase in the numeric code as meaningful.

In intern_mod2, the factor class tells lm() to treat the values as categories. R uses one category as the reference category and estimates coefficients comparing the other categories with that reference. A factor with k categories therefore contributes k - 1 category coefficients rather than one quantitative slope.

6.1 Converting haven-labelled categorical variables to factors

The as_factor() function from haven converts haven-labelled variables to factors

  • Can be used for descriptive statistics
hsls %>% select(s3classes) %>% count(s3classes)
hsls %>% select(s3classes) %>% count(s3classes) %>% as_factor()
  • Can create an object with some or all haven-labelled variables converted to factors

Important: Before converting an entire data frame, inspect the variables and decide which ones genuinely represent categories. Some variables may carry value labels only for special codes and should remain numeric.

  • as_factor() does not convert values labelled "Missing" or "Unit non-response" to NA; it converts those labels to ordinary factor levels
  • Handle missing-value codes separately, either before or after converting the variable to a factor

For this teaching example, we convert all haven-labelled variables:

hsls_f <- as_factor(x = hsls, only_labelled = TRUE)

Let’s examine this object

glimpse(x = hsls_f)
hsls_f %>% select(s3classes,s3clglvl) %>% str()
typeof(x = hsls_f$s3classes)
class(x = hsls_f$s3classes)
attributes(x = hsls_f$s3classes)

hsls_f %>% select(s3classes) %>% var_label()
hsls_f %>% select(s3classes) %>% val_labels()

6.2 Working with factor data

Showing factor levels associated with a factor variable

hsls_f %>% count(s3classes)
# A tibble: 5 × 2
  s3classes             n
  <fct>             <int>
1 Missing              59
2 Unit non-response  4945
3 Yes               13477
4 No                 3401
5 Don't know         1621

Showing the underlying integer codes associated with a factor variable

  • This is useful for understanding storage, but use the factor level labels—not these codes—to identify categories
hsls_f %>% count(as.integer(x = s3classes))
# A tibble: 5 × 2
  `as.integer(x = s3classes)`     n
                        <int> <int>
1                           1    59
2                           2  4945
3                           6 13477
4                           7  3401
5                           8  1621

When sub-setting observations (e.g., filtering), refer to the level attribute, not the underlying variable integer value.

hsls_f %>% filter(s3classes=="Yes") %>% count(s3classes)
# A tibble: 1 × 2
  s3classes     n
  <fct>     <int>
1 Yes       13477

7 Appendix: Creating factor variables

7.1 Create factors [from string variables]

To create a factor variable from string variable:

  1. Create a character vector containing underlying data
  2. Create a vector containing valid levels
  3. Attach levels to the data using the factor() function
# Underlying data: months my fam is born
vec_x <- c("Jan", "Aug", "Apr", "Mar")
# Create vector with valid levels
month_levels <- c("Jan", "Feb", "Mar", "Apr", "May", "Jun", 
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
# Attach levels to data
vec_y <- factor(x = vec_x, levels = month_levels)

Note how attributes differ:

str(object = vec_x)
 chr [1:4] "Jan" "Aug" "Apr" "Mar"
str(object = vec_y)
 Factor w/ 12 levels "Jan","Feb","Mar",..: 1 8 4 3

Sorting also differs:

sort(x = vec_x)
[1] "Apr" "Aug" "Jan" "Mar"
sort(x = vec_y)
[1] Jan Mar Apr Aug
Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec

Let’s create a character version of variable hs_state and then turn it into a factor:

#wwlist %>%
#  count(hs_state)
# Subset obs to West Coast states 
wwlist_temp <- wwlist %>%
  filter(hs_state %in% c("CA", "OR", "WA"))

# Create character version of high school state for West Coast states only
wwlist_temp$hs_state_char <- as.character(x = wwlist_temp$hs_state)

# Investigate character variable
str(object = wwlist_temp$hs_state_char)
table(wwlist_temp$hs_state_char)

# Create new variable that assigns levels
wwlist_temp$hs_state_fac <- factor(x = wwlist_temp$hs_state_char, levels = c("CA","OR","WA"))
str(object = wwlist_temp$hs_state_fac)
attributes(x = wwlist_temp$hs_state_fac)

#wwlist_temp %>%
#  count(hs_state_fac)
rm(wwlist_temp)

How the levels argument works when underlying data is character:

  • Matches value of underlying data to value of the level attribute
  • Converts underlying data to integer, with level attribute attached


See Chapter 15 of Wickham for more on factors (e.g., modifying factor order, modifying factor levels)

7.2 Creating factors [from integer vectors]

Factors are integer vectors with a levels attribute and an explicit class = "factor" attribute. To create a factor from integer values:

  1. Create a vector for the underlying data
  2. Specify the valid underlying values using the levels argument
  3. Specify the category labels corresponding to those values using the labels argument
  4. Create the factor using the factor() function
vec_x <- c(1,1,1,0,1,1,0) # A vector of data
vec_y <- c("zero","one") # A vector of labels

# Explicitly attach each label to its corresponding value
vec_z <- factor(
  x = vec_x,
  levels = c(0, 1),
  labels = vec_y
)
vec_z
[1] one  one  one  zero one  one  zero
Levels: zero one
str(object = vec_z)
 Factor w/ 2 levels "zero","one": 2 2 2 1 2 2 1

Note: The order of levels and labels defines the mapping: value 0 receives label "zero", and value 1 receives label "one". Stating both arguments explicitly prevents the mapping from depending on which values happen to appear in the data.

Let’s turn an integer variable into a factor variable in the wwlist data frame

Create integer version of receive_year:

#typeof(x = wwlist_temp$receive_year)
wwlist$receive_year_int <- as.integer(x = wwlist$receive_year)
str(object = wwlist$receive_year_int)
 int [1:268396] 2016 2016 2016 2016 2016 2016 2016 2016 2016 2016 ...
typeof(x = wwlist$receive_year_int)
[1] "integer"

Assign levels to values of integer variable:

wwlist$receive_year_fac <- factor(x = wwlist$receive_year_int,
      levels = c(2016, 2017, 2018),
      labels = c("Twenty-sixteen","Twenty-seventeen","Twenty-eighteen"))
str(object = wwlist$receive_year_fac)
str(object = wwlist$receive_year)

#Check variable
wwlist %>%
  count(receive_year_fac)

wwlist %>%
  count(receive_year)