Working with Factors

Author

Dr. D

Published

September 3, 2026

In this lesson we will discuss ways to organize and deal with categorical data, also known as factor data types.

Reference: Math 130 - Chapter 5

Learning Objectives

After completing this lesson students will be able to

  • Convert a numeric variable to a factor variable.
  • Apply and change labels to factor
  • Understand and control the ordering of the factor.
  • Combine multiple levels of a factor variable into one level
  • Learn how to use the forcats package

Also part of the tidyverse!

The email data set contains information on emails received by one of the OpenIntro authors for the first three months in 2012. See ?email for more details.The fastfood data set from the openintro package describes nutrition amounts in 515 fast food items. See ?fastfood for more details.
library(tidyverse) 
email <- openintro::email
ff    <- openintro::fastfood

The goal of the forcats package is to provide a suite of useful tools that solve common problems with factors.

What is a factor?

Categorical data often are made up of words and letters.

table(ff$restaurant)

      Arbys Burger King Chick Fil-A Dairy Queen   Mcdonalds       Sonic 
         55          70          27          42          57          53 
     Subway   Taco Bell 
         96         115 
table(email$number)

 none small   big 
  549  2827   545 

However, they can have different data types.

class(ff$restaurant)
[1] "character"
class(email$number)
[1] "factor"

Factor variables are character data types, but typically with a finite number of possible responses. Think about our class survey.

Code
survey <- googlesheets4::read_sheet("https://docs.google.com/spreadsheets/d/1nUvOJN8z91c3dPN5UrV653qJxlbMCvvj846JDgOscdM")
survey <- survey[,-c(1,2)]
names(survey) <- c("location", "programming_experience", "qual_research_experience", "qual_data_analysis",  "data_interests",   "learn_best",   "hopes")
  • Character: “How do you learn best?” – open ended response
head(survey$learn_best)
[1] "I'm not sure. I like to read about things I'll need to know, but I also try to practice things I'm learning about."      
[2] "Solo projects & putting stuff into practice"                                                                             
[3] "With more smaller assignments rather than fewer larger assignments"                                                      
[4] "Hands on activities and learning by observation"                                                                         
[5] "I learn best from detailed instructions , hands on  experience and being able to ask questions  if and when in get stuck"
[6] "Visual learner, with detailed explanations spoken slowly"                                                                
  • Factor: “Where were you come from?” – pre-specified number of options
head(survey$location)
[1] "Butte County"                   "Rest of California"            
[3] "Outside of the United States"   "Butte County"                  
[5] "Broader North State (Sac & up)" "Butte County"                  

These pre-specified options are called levels. Factor variables have them, character variables do not.

We can use the levels() function to get to know factor variables.

levels(email$number)
[1] "none"  "small" "big"  

There are three levels: none, small, and big.

Let’s look at the variable restaurant from the fast food (ff) data set.

levels(ff$restaurant)
NULL
  • An ordinal variable is a categorical variable where order matters: first, second third
  • A nominal variable is a categorical variable where order does not matter: red, green, blue

Factor data types allow us to control the ordering of the levels.

Be the boss of your factors!

Convert a character variable to factor

We use the as_factor() function from the forcats package.

ff$restaurant <- as_factor(ff$restaurant)
levels(ff$restaurant)
[1] "Mcdonalds"   "Chick Fil-A" "Sonic"       "Arbys"       "Burger King"
[6] "Dairy Queen" "Subway"      "Taco Bell"  

Factor (re)naming

What if the variable is already a factor, but has names we don’t prefer.

Use the fct_recode("NEW" = "old") function here.

email$my_forcats_number <- fct_recode(email$number, 
                                      "1M+" = "big", 
                                      "None" = "none", 
                                      "<1M" = "small")

table(email$number, email$my_forcats_number, useNA="always")
       
        None  <1M  1M+ <NA>
  none   549    0    0    0
  small    0 2827    0    0
  big      0    0  545    0
  <NA>     0    0    0    0

The big factor is now labeled 1M+, none is named None, and small is <1M.

Factor ordering

Let’s look back at the variable restaurant from the fast food (ff) data set.

levels(ff$restaurant)
[1] "Mcdonalds"   "Chick Fil-A" "Sonic"       "Arbys"       "Burger King"
[6] "Dairy Queen" "Subway"      "Taco Bell"  

R defaults to alphabetical order in other cases, so beware! You may need to correct the ordering for other data sets.

We need to take control of these factors! We can do that by re-factoring the existing factor variable, but this time specifying the levels of the factor (since it already has labels). Say we decide to order the restaurants by putting all the places that sell burgers together.

Original Order: Arbys, Burger King, Chick Fil-A, Dairy Queen, Mcdonalds, Sonic, Subway, Taco Bell

Desired Order: Arbys, Burger King, Dairy Queen, Mcdonalds, Sonic, Chick Fil-A, Subway, Taco Bell

Since I did not use the assignment operator (<-) here, these changes were not made to the variable in the ff data set. The examples below demonstrate making an adjustment to a factor variable and saving that adjustment as a new variable in the data set.

Using the fct_relevel function, specify the levels that you want to move.

# results not saved
ff$restaurant |> 
  fct_relevel("Arbys", "Burger King", "Dairy Queen", "Mcdonalds", "Sonic") |> 
  fct_count() # new function - acts like table
# A tibble: 8 × 2
  f               n
  <fct>       <int>
1 Arbys          55
2 Burger King    70
3 Dairy Queen    42
4 Mcdonalds      57
5 Sonic          53
6 Chick Fil-A    27
7 Subway         96
8 Taco Bell     115

The fct_relevel function has nice shortcuts as well for example to keep the rest of the ordering but only one or two items. See ?fct_relevel for other options.

ff$restaurant |> 
  fct_relevel("Subway", after = Inf) |> # move to end
  fct_relevel("Taco Bell") |> # move to front
  levels()
[1] "Taco Bell"   "Mcdonalds"   "Chick Fil-A" "Sonic"       "Arbys"      
[6] "Burger King" "Dairy Queen" "Subway"     

Decreasing number of levels

For analysis purposes, sometimes you want to work with a smaller number of factor variables. Let’s look at the restaurants that are included in the fastfood data set.

table(ff$restaurant)

  Mcdonalds Chick Fil-A       Sonic       Arbys Burger King Dairy Queen 
         57          27          53          55          70          42 
     Subway   Taco Bell 
         96         115 

Combining multiple categories into one

Let’s combine all the sandwich, and burger joints together. I am going to save this new variable as restaurant_new.

The syntax for the fct_collapse function is new level = "old level", where the “old level” is in quotes. As always, it is good practice to create a two way table to make sure the code typed does what we expected it to do.

ff$restaurant_new <- fct_collapse(ff$restaurant, 
                                    BurgerJoint = c("Burger King", "Mcdonalds", "Sonic"), 
                                    Sandwich = c("Arbys", "Subway"))

table(ff$restaurant, ff$restaurant_new, useNA="always")
             
              BurgerJoint Chick Fil-A Sandwich Dairy Queen Taco Bell <NA>
  Mcdonalds            57           0        0           0         0    0
  Chick Fil-A           0          27        0           0         0    0
  Sonic                53           0        0           0         0    0
  Arbys                 0           0       55           0         0    0
  Burger King          70           0        0           0         0    0
  Dairy Queen           0           0        0          42         0    0
  Subway                0           0       96           0         0    0
  Taco Bell             0           0        0           0       115    0
  <NA>                  0           0        0           0         0    0

Keeping the most frequent categories

Sometimes we only want to keep the most frequent categories and then lump uncommon factor together levels into “other”. The fct_lump_n function

fct_lump_n(ff$restaurant, n=5) |>
  fct_count()
# A tibble: 6 × 2
  f               n
  <fct>       <int>
1 Mcdonalds      57
2 Arbys          55
3 Burger King    70
4 Subway         96
5 Taco Bell     115
6 Other         122

Removing categories entirely

Sometimes, you don’t even want to consider certain levels. This often occurs in survey data where the respondent provides an answer of “Refuse to answer” or the data is coded as the word “missing”. The word “missing’ is fundamentally different than the NA code for a missing value.

For demonstration purposes, let’s get rid of the data from DQ. Who eats something other than ice cream at that place anyhow?

ff$restaurant[ff$restaurant == "Dairy Queen"] <- NA
table(ff$restaurant)

  Mcdonalds Chick Fil-A       Sonic       Arbys Burger King Dairy Queen 
         57          27          53          55          70           0 
     Subway   Taco Bell 
         96         115 

Even though there are no records with the level Dairy Queen, the level itself still is there. R does not assume just because there are no records with that level, that the named level itself should be removed. We use the function fct_drop to drop the levels with no records.

ff$restaurant <- fct_drop(ff$restaurant)
table(ff$restaurant)

  Mcdonalds Chick Fil-A       Sonic       Arbys Burger King      Subway 
         57          27          53          55          70          96 
  Taco Bell 
        115