rguides

Handling Factors with forcats

Handling factors with forcats is the tidyverse approach to categorical data in R. Factors have ordered levels that make them powerful for statistical modeling, data visualization, and any analysis involving grouped data. The forcats package provides a consistent set of functions for creating, reordering, lumping, and recoding factors — making what used to be fiddly base R operations intuitive and pipeline-friendly.

Creating factors

The base R way to create factors uses the factor() function:

# Create a factor from a character vector
education <- c("High School", "Bachelor", "Master", "PhD", "Bachelor", "Master")
education_factor <- factor(education)

education_factor
# [1] High School Bachelor Master   PhD       Bachelor Master  
# Levels: Bachelor High School Master PhD

The tidyverse approach with forcats gives you more control:

library(forcats)

# Create with explicit level order
education <- c("High School", "Bachelor", "Master", "PhD", "Bachelor", "Master")
education_factor <- factor(education, levels = c("High School", "Bachelor", "Master", "PhD"))

education_factor
# [1] High School Bachelor Master   PhD       Bachelor Master  
# Levels: High School Bachelor Master PhD

Reordering factor levels with fct_reorder

When creating visualizations, you often want factor levels ordered by another variable rather than alphabetically:

library(ggplot2)
library(dplyr)

# Sample data
survey <- data.frame(
  department = c("Sales", "Engineering", "HR", "Marketing", "Engineering", 
                 "Sales", "Marketing", "HR"),
  salary = c(55000, 95000, 52000, 61000, 98000, 53000, 64000, 51000)
)

# Reorder department by median salary
ggplot(survey, aes(x = fct_reorder(department, salary, .fun = median), y = salary)) +
  geom_boxplot() +
  labs(x = "Department", y = "Salary") +
  theme_minimal()

fct_reorder() automatically sorts levels by the summary function you specify, making your plots more readable.

Combining rare levels with fct_lump

When you have many categories, some with few observations, fct_lump() combines rare levels into “Other”:

# Sample data with many categories
countries <- c("USA", "UK", "Germany", "France", "Japan", "China", "India", 
               "Brazil", "Canada", "USA", "UK", "Germany", "France", "Japan")

# Keep top 3 most common, lump rest into "Other"
fct_lump(countries, n = 3)
# [1] USA        UK        Germany   France    Japan     China    India   
# [8] Brazil     Canada    USA       UK        Germany  France   Japan   
# Levels: USA UK Germany France Japan Other

You can also lump by proportion:

# Keep categories that appear more than 10% of the time
fct_lump(countries, prop = 0.1)

Recoding levels with fct_recode

Sometimes you need to rename categories for clarity or consistency:

# Original data
response <- c("Yes", "No", "Maybe", "Yes", "No", "Yes")

# Recode levels
fct_recode(response,
  "Agree" = "Yes",
  "Disagree" = "No",
  "Uncertain" = "Maybe"
)
# [1] Agree      Disagree   Uncertain  Agree      Disagree   Agree    
# Levels: Agree Disagree Uncertain

Collapsing levels with fct_collapse

For grouping related categories:

# Original data
region <- c("NY", "CA", "TX", "FL", "WA", "NY", "CA", "TX", "FL")

# Collapse into broader categories
fct_collapse(region,
  West = c("CA", "WA"),
  South = c("TX", "FL"),
  Northeast = c("NY")
)
# [1] Northeast West    South    South    West     Northeast West    South   
# [9] South   
# Levels: West South Northeast

Counting levels with fct_count

Quickly see your level distribution:

# Sample data
colors <- c("Blue", "Red", "Green", "Blue", "Blue", "Red", "Yellow", "Blue")

fct_count(colors, sort = TRUE)
# # A tibble: 4 × 2
#   f         n
#   <fct> <int>
# 1 Blue      4
# 2 Red       2
# 3 Green     1
# 4 Yellow    1

Practical example: survey analysis

Putting it all together in a real analysis:

library(dplyr)
library(ggplot2)
library(forcats)

# Simulated survey data
survey_data <- tibble(
  education = sample(c("High School", "Bachelor", "Master", "PhD", "Some College"), 
                    200, replace = TRUE),
  income = rnorm(200, mean = 50000, sd = 15000)
)

# Clean up: lump rare categories and reorder
survey_clean <- survey_data %>%
  mutate(
    education = fct_lump(education, n = 3),
    education = fct_reorder(education, income, .fun = median)
  )

# Analyze by education level
survey_clean %>%
  group_by(education) %>%
  summarise(
    n = n(),
    median_income = median(income)
  )

Why factors matter

Understanding when to use factors versus character vectors is crucial for effective R programming. Factors are essential when:

  1. Statistical modeling, Many modeling functions in R automatically treat factor variables as categorical predictors and create dummy variables appropriately.

  2. Ordinal data, When the order of categories matters (like “Low”, “Medium”, “High”), factors preserve this ordering.

  3. Memory efficiency, For columns with many repeated values, factors store integers internally rather than storing each string repeatedly.

  4. Consistent groupings, Factors ensure that your categories remain consistent across subsets of data, preventing typos or case mismatches from creating new unintended levels.

# Example: Factors in modeling
library(dplyr)

# Create sample data
model_data <- tibble(
  treatment = factor(rep(c("Control", "Drug A", "Drug B"), each = 30)),
  outcome = c(rnorm(30, mean = 10), rnorm(30, mean = 15), rnorm(30, mean = 12))
)

# Linear model automatically treats factor as categorical
lm(outcome ~ treatment, data = model_data)

The treatment variable is automatically converted to dummy variables, with one level (Control) as the reference category.

Working with missing levels

Sometimes you want to keep levels that don’t appear in your data, or handle missing values explicitly:

# Keep all levels even if not present in data
colors <- factor(c("Red", "Blue", "Red"), levels = c("Red", "Blue", "Green", "Yellow"))
levels(colors)
# [1] "Red"    "Blue"   "Green"  "Yellow"

# Drop unused levels
colors <- fct_drop(colors)
levels(colors)
# [1] "Red" "Blue"

These functions give you complete control over your categorical data, ensuring your analysis is both correct and efficient.

Reordering factors

fct_reorder(f, x) orders factor levels by the values in x, perfect for sorting bar charts by height. fct_reorder2(f, x, y) orders by y values at the maximum x, useful for line charts with multiple groups. fct_infreq(f) orders by frequency descending. fct_rev(f) reverses level order. fct_relevel(f, "first_level") moves specific levels to the front.

Combining and collapsing

fct_lump_n(f, n = 5) keeps the top 5 most frequent levels and collapses the rest into “Other”. fct_lump_prop(f, prop = 0.05) keeps levels above 5% frequency. fct_other(f, keep = c("A", "B")) collapses everything except A and B into “Other”. fct_collapse(f, new_level = c("old1", "old2")) merges levels explicitly.

Adding and dropping levels

fct_expand(f, "new_level") adds a level without adding data. fct_drop(f) removes unused levels (levels with zero observations after filtering). droplevels(f) does the same in base R. Unused levels persist in factor objects and appear in table() output and ggplot2 legends, always drop them after filtering to keep summaries clean.

Renaming levels

fct_recode(f, "New Name" = "old_name") renames specific levels. fct_relabel(f, toupper) applies a function to all level names. Unnamed levels in fct_recode() keep their original names. levels(f)[levels(f) == "old"] <- "new" is the base R equivalent.

Factors in ggplot2

ggplot2 uses factor level order for axis order, legend order, and color assignment. Wrap the variable in fct_reorder() inside aes() to sort without modifying the data frame: aes(x = fct_reorder(category, value), y = value). scale_x_discrete(limits = rev) reverses axis order without touching the data.

Why factor level order matters

When you create a factor with factor(x), R orders levels alphabetically by default. This default affects plots (bars appear alphabetically, not meaningfully) and model output (the first level alphabetically becomes the reference category). forcats provides tools to reorder levels to match your analytic intent.

fct_reorder(f, x) reorders levels by the median (or another summary) of a numeric variable x. For a bar chart of mean scores by category, mutate(category = fct_reorder(category, score)) before ggplot() orders bars from low to high score, the most useful default for comparisons.

fct_reorder2(f, x, y) reorders levels by two variables, commonly used for line charts where you want the legend to match the right endpoint of the lines. mutate(group = fct_reorder2(group, date, value)) reorders legend entries to match the final value of each line, eliminating the need to match colors to labels.

Collapsing and combining levels

fct_collapse(f, new_level = c("level1", "level2")) collapses multiple levels into one. This is useful after recoding responses: fct_collapse(satisfaction, satisfied = c("very satisfied", "satisfied"), dissatisfied = c("very dissatisfied", "dissatisfied")).

fct_lump_n(f, n = 5) keeps the 5 most frequent levels and combines all others into “Other”. fct_lump_prop(f, prop = 0.05) keeps levels that appear in at least 5% of observations. fct_lump_min(f, min = 100) keeps levels with at least 100 occurrences. These are useful for reducing cardinality before visualization or modeling.

fct_other(f, keep = c("A", "B")) keeps only the specified levels and recodes everything else as “Other”. fct_other(f, drop = c("Unknown", "Missing")) drops specific levels into “Other”.

Adding, dropping, and expanding levels

fct_expand(f, "new_level") adds a level that does not yet appear in the data. This is useful when you know a level should be present in future data and want it to appear in plots even with zero observations.

fct_drop(f) removes levels that do not appear in the data. droplevels(f) does the same. Call this after filtering a data frame to remove levels that no longer have data: empty levels appear as zero-count bars in bar charts and unused categories in model output.

fct_explicit_na(f, na_level = "(Missing)") converts NA values to an explicit factor level. This makes missing values visible in tables and plots rather than silently dropped.

Reversing and shifting order

fct_rev(f) reverses the level order. For horizontal bar charts where you want to read items top-to-bottom in a meaningful order, fct_rev(fct_reorder(f, x)) achieves ascending order in the natural reading direction.

fct_shift(f, n = 1) shifts levels cyclically by n positions. fct_relevel(f, "specific_level") moves one or more specific levels to the front. fct_relevel(f, "Total", after = Inf) moves “Total” to the last position, useful for summary categories that should appear at the end of a table or chart.

Recoding factor levels

fct_recode(f, new_name = "old_name") renames individual levels. This is cleaner than levels(f)[levels(f) == "old_name"] <- "new_name" because it handles multiple recodings in one call and makes the intent explicit.

fct_anon(f, prefix = "cat") replaces level labels with anonymized codes like “cat1”, “cat2”. Use this to prepare sensitive data for sharing while preserving the factor structure.

For complex recoding logic, use case_match() (dplyr 1.1+) before creating the factor: mutate(category = case_match(raw_code, c("1a", "1b") ~ "Group 1", c("2a", "2b") ~ "Group 2", .default = "Other")), then convert to factor with the desired level order.

Factor level management

Managing factor levels is one of the most common data cleaning tasks in R. When data arrives from a survey, a database, or a CSV, categorical columns often have levels in alphabetical order, which is not the same as the meaningful order for analysis or display. forcats provides a consistent set of tools for reordering, renaming, combining, and dropping factor levels without manually specifying all levels.

The most common operations are reordering levels for display — so that bar charts show categories in a meaningful order rather than alphabetical — and recoding levels for clarity — so that survey responses have readable labels rather than numeric codes. forcats makes both operations one-liner operations rather than multi-step manual factor construction.

Frequency-Based reordering

fct_infreq and fct_inorder reorder by frequency and first appearance. For bar charts and table summaries, ordering categories from most to least frequent makes the most common categories easiest to see. Reversing with fct_rev gives least to most frequent, which is the natural order for horizontal bar charts where you want the longest bar at the top.

fct_reorder orders levels based on a summary statistic of another variable. Sorting a category by its median value, maximum value, or count of another variable is the standard approach for ordered bar charts and Cleveland dot plots. The result is a chart where the visual order reflects the data order, not an arbitrary alphabetical order.

Combining rare levels

fct_lump and its variants consolidate rare levels into an “Other” category. This is useful for analysis and display when a factor has many levels but only a few matter: top products, top customers, top countries. The number of levels to retain is specified either as a count of the most frequent levels or as a minimum frequency threshold.

Lumping rare levels reduces visual clutter in charts and statistical tables. Modeling with high-cardinality factors is also more stable when rare levels are combined — a level with only two observations produces an unstable coefficient in regression. Lumping before modeling prevents this instability while retaining the most important categories.

Summary

The forcats package provides essential tools for working with categorical data:

FunctionPurpose
factor()Create factors with custom levels
fct_reorder()Reorder levels by another variable
fct_lump()Combine rare levels into “Other”
fct_recode()Rename individual levels
fct_collapse()Group levels into categories
fct_count()Count observations per level

Mastering these functions will make your R data wrangling much more efficient, especially when preparing data for visualization or statistical modeling.

Next steps

Now that you understand handling factors with forcats, explore these related topics to deepen your knowledge and apply these techniques in more complex scenarios.