Factors with forcats
Back at Fern & Co., the plant shop from the last lesson, every order records a size: "Small", "Medium" or "Large". You ask R for a simple count of how many of each you sold, and it hands you the answer in this order: Large, Medium, Small. Backwards. A bar chart built from it reads backwards too.
R is not being difficult. It is doing the only thing it can with plain text: sorting the words alphabetically, where "Large" beats "Medium" beats "Small". To make a category keep its real order, you store it as a factor, and the forcats package gives you a tidy toolkit for building and rearranging them. By the end of this lesson you will be able to:
- Explain what a factor is and create one with the exact order you want
- Reorder a factor's levels by frequency, by another column, or by hand
- Rename categories and pool a long tail of rare ones into a single "Other"
Prerequisites: you can run a line of R and store a result with <- (Your First R Session), and you know what a vector and the character type are (Atomic Vectors and Data Types). You just turned date text into a weekday; a weekday is a category with a natural order, so factors are the natural next step. Press Run to see the whole payoff at once; the rest of the lesson builds it up piece by piece.
Plain text falls in the wrong order
Let's make the problem concrete. Here are Fern & Co.'s 15 most recent orders. Each lesson runs in a fresh R session, so we build the little data frame right here (run this once and it stays available for the rest of the lesson):
Now ask for the count of each size. Because size is just text, R sorts the labels alphabetically:
"Large" lands first only because L comes before M and S in the alphabet. The order that actually means something, smallest to largest, is nowhere in the result. Sort it, filter it, plot it: it will stay stubbornly alphabetical until you tell R what the real order is.
What a factor really is
A factor is R's type for a category: a value that can only be one of a fixed, named set of possibilities, called the levels. When you create one, you list the levels in the order you want, and that order sticks from then on.
Notice the extra line: Levels: Small Medium Large. That is the factor remembering its allowed values, in the order you gave. Here is the part that explains everything else. Underneath, a factor does not store the words over and over. It stores a small integer code for each value, plus one lookup table of labels. Small is code 1, Medium is 2, Large is 3:
So there are really three jobs you will do with factors, and the rest of the lesson walks them in order:
Create a factor with the order you want
You already saw the move: factor(x, levels = ...). The trick is simply that you choose the order of levels, and R obeys it everywhere afterwards. Store the size column as a factor and the same count now comes out the way a human reads sizes:
Same numbers as before, finally in a sensible order, and any chart built from this column will follow suit. If the order is not just for display but genuinely meaningful (small really is less than large), add ordered = TRUE. That lets you compare levels with < and >:
ordered = TRUE only when the levels have a true ranking you want to compare, like Small < Medium < Large or Low < Medium < High.Where do the counts land?
A customer-satisfaction column holds the plain text values "High", "Low" and "Medium". You run table() on it without making it a factor first. In what order do the three counts appear?
Reorder the levels
Setting the order by hand at creation time is the start. Often you want to reorder an existing factor, and forcats has one small verb for each way you might want to. Each returns a new factor with the same data but a rearranged levels lookup. To see what each does, print the levels() afterwards. Below I use R's pipe, |>, which just feeds the result on its left into the function on its right, so x |> levels() means the same as levels(x), read left to right:
The most useful one in practice is fct_reorder(), which orders a category by another column. This is what makes a bar chart sort itself by value instead of by name. Order the plant varieties by their average price, cheapest first:
fct_infreq orders by how often each level occurs; fct_reorder orders by a summary (the mean here) of a second column. Both are the everyday fix for "my chart's bars are in alphabetical order and I want them sorted by size."Put the busiest size first
For a bar chart of sales, Fern & Co. wants the most common size first, then the next, and so on. The orders$size factor is ready below. Fill in the blank with the forcats verb that orders a factor's levels from most frequent to least frequent.
Show answer
library(forcats)
orders$size <- factor(orders$size, levels = c("Small", "Medium", "Large"))
levels(fct_infreq(orders$size))
#> [1] "Medium" "Small" "Large"Relabel and lump
Two more everyday jobs. First, renaming levels with fct_recode(). The rule to memorise is the direction: the new name goes on the left, the existing level it replaces (quoted) on the right.
Second, lumping. Fern & Co. sells a few popular varieties and a long tail of one-offs. For a clean summary you want the top sellers named and everything else pooled into a single "Other". fct_lump_n() keeps the n most common levels and lumps the rest:
Here fct_count() is just forcats' tidy version of table(): it returns each level with its count as a small data frame, biggest first when you pass sort = TRUE. The three one-off varieties (Aloe, Palm, Ivy) collapse into a single "Other" worth 3 orders, so the summary stays readable instead of trailing off into a list of singletons. The widget shows the same move on the variety counts:
Which way does fct_recode go?
You want to rename the level "Small" to "S". Which call does it correctly?
Where factors bite
Factors are friendly until two traps catch you. Trap one: a column of numbers that imported as a factor. Calling as.numeric() on it gives you the integer codes, not the numbers you see:
Trap two: filtering a factor keeps the unused levels around. Take only the small orders and the count still lists Medium and Large at zero, because the levels lookup is unchanged:
as.numeric(as.character(x)), never as.numeric(x). And after subsetting, call droplevels() (or fct_drop()) if you do not want empty categories haunting your tables and charts.References
Four trustworthy, free places to take factors further:
- forcats (tidyverse) - official site - the home page and full reference for every fct_ verb you used here.
- R for Data Science (2e): Factors - the canonical, example-led chapter, including reordering factors in plots.
- Posit forcats cheatsheet - a one-page visual map of creating, reordering, relabelling and combining factors.
- forcats function reference - the complete index, grouped by job (change order, change value, add or drop levels).
Lesson 4 complete
You took a column of plain-text categories that insisted on sorting alphabetically and made it behave. The key idea was the first one: a factor stores its categories as integer codes plus an ordered levels lookup, so once you set the levels, the order sticks everywhere. From there it was three jobs: create with factor(levels = ...), reorder with fct_infreq, fct_relevel, fct_rev and fct_reorder, and relabel with fct_recode and fct_lump_n, plus the two traps to sidestep (as.numeric(as.character()) and droplevels()).
That also closes the Strings, Dates and Factors course: you can now detect and reshape text with stringr, build patterns with regular expressions, turn date text into real dates with lubridate, and put categories in the order you mean with forcats. Together they are the everyday toolkit for cleaning the messy, human side of a dataset, the part that arrives as words rather than numbers, and getting it ready for the analysis and charts that come next.