library(tidyverse)
library(janitor)
library(readxl)
library(scales)16 Factors
This extra chapter outlines what factors are and why we might use them.
We’ll be using data from the Texas Immunization Registry (ImmTrac2), which tracks how often kids get immunized each year in a given county.
16.1 What is a factor
A factor is a categorical variable (words) where the values have a set order. The classic example we’ll use here is months of the year: January, February, March, etc. Months are words for sure, but they have an order. We want to see them listed starting with January and ending in December, instead of alphabetically starting with April, then August, etc. Days of the week are similar.
Survey results are another common use for factors, especially when considering ranges of categories. The example we’ll get into here is age groups, where we want these categories like “2-8 years” and “18+ years” to be listed in age order instead of alphabetically, which would put the 1 in “18” before it. But you could see the same with salary ranges and other categories you want listed in a specific order that isn’t alphabetical.

There is a tidyverse package called forcats that provides functions to work with factors. The use cases for these can get esoteric, so we’ll concentrate on a couple that are common in our work as journalists.
The package name “forcats” is an anagram for “factors”. Apparently Hadley Wickham is a cat guy with a dad’s taste in jokes.
16.2 Getting started
We have to import our data and do some cleanup, like with any project. We’ll just do that here without much explanation to avoid going too far astray, but it is also an example of using the readxl package.
16.2.1 Setup
16.2.2 Import & fix number of vaccinations
Import the data from the spreadsheet and clean the names. Also, the number of vaccinations is redacted with ** if there are fewer than five, so we have to fix that column.
tir_raw <- read_excel(
"data-original/immtrac2-doses-administered-for-web.xlsx",
sheet = "Data",
skip = 1) |>
clean_names()
tir <- tir_raw |>
mutate(number_of_vaccination = parse_number(number_of_vaccination),
county = replace_values(county,"Null" ~ "Unknown"))16.2.3 Peek at the data
We don’t need to get too much into the data, but you can poke around and see the columns. The data shows the number of immunizations by county, year, month, age and vaccine type from January 2020 through May 2026. We’ll mostly just be totaling the number_of_vaccination as an example.
tir |> head()16.3 Creating a new factor
16.3.1 The problem: Ages out of order
Let’s start by summarizing how many total vaccinations have been given based on our age variable age_at_vaccination.
tir |>
group_by(age_at_vaccination) |>
summarize(total_vacc = sum(number_of_vaccination, na.rm = T))The result is ordered by age_at_vacciation alphabetically, but that isn’t a logical way to view this. Starting at “0 to 24 months” is fine, but we want that to be followed by “2-8 years”, not “18+ years”. If we charted this result, we would have the same problem.
The solution is to create a version of this as a factor.
16.3.2 Using fct()
There are a number of ways that you can create a factor from an existing variable, including a series of Base R functions built around factor() that include as.factor().
However, like many other times in this book, we will favor using tidyverse functions designed to work together, like fct(), as_factor() and other functions from the forcats package, which is included in the tidyverse library.
Since we are changing or creating data, we’ll need to use our functions with a mutate() function. The fct() function turns a variable into a factor based on the unique values in the data. The order of the factors is called “levels”, and the default for this function is to create the levels based on how it initially finds them in the data. In our case here, they appear in the data in the order we want them, so this procedure is pretty simple.
- 1
-
This is where we create our new variable. By default the
fct()function creates the order (or “levels”) of the factor in the order it finds them in the data. (In the next example we’ll set our own order.) - 2
-
This is a
mutate()argument that places the new variable where I want it in the tibble, and I wanted it right after the source variable so we could compare them.
You can see the data type for our new variable age_fct is called <fct> instead of <chr> in the original.
16.3.3 Showing the resulting factor
So let’s look at that summary again, using the new age_fct variable.
tir_fct_sums <- tir_fct |>
group_by(age_fct) |>
summarize(total_vacc = sum(number_of_vaccination, na.rm = T))
tir_fct_sums16.4 Set factor order manually
Sometimes our data is not provided to us in the proper order, so we set the order ourselves using the level = argument. It’s pretty strict that you list all the possible level values carefully or it will error.
In this case we will create the levels in reverse order of our age groups, just to show this concept.
tir_rev <- tir |>
mutate(
age_fct_man = fct(age_at_vaccination,
levels = c("18+ years",
"9-17 years",
"2-8 years",
"0 to 24 months"))
)Our summary is ordered like this now, from oldest to youngest:
tir_rev_sums <- tir_rev |>
group_by(age_fct_man) |>
summarize(total_vacc = sum(number_of_vaccination, na.rm = T))
tir_rev_sums16.4.1 What the error looks like
Here I show you what the error looks like if you don’t list all the possible level values properly. The error I made below was not including “years” in “2-8 years”. I’ve put this in a try() function so it will run and you can see the error, but you wouldn’t use that in an analysis.
try(
tir_rev <- tir |>
mutate(
age_fct_man = fct(age_at_vaccination,
levels = c("18+ years",
"9-17 years",
"2-8", # this is where my error is
"0 to 24 months"))
)
)Error in mutate(tir, age_fct_man = fct(age_at_vaccination, levels = c("18+ years", :
ℹ In argument: `age_fct_man = fct(...)`.
Caused by error in `fct()`:
! All values of `x` must appear in `levels` or `na`
ℹ Missing level: "2-8 years"
16.5 More on factors
There are lots of other ways to use factors and change their levels, but ordering categories like this has been the most prevalent use for me within journalism.
If you need to learn more, check out the forcats package and the Factors chapter in the R for Data Science book.
16.6 Bonus: Months as a factor
In the Grouping by dates chapter of this book we used functions from the lubridate package to create a smart “month” variable that ordered them in the correct way, starting with January and ending in December.
Those special months are essentially a factor, so we’ll do that again here. Our challenge here is we don’t have a full date to pluck from here, so we’ll have to manufacture it from the month and year, also using lubridate functions.
16.6.1 The problem: Months as characters
In our data we have a column for year and month, but they are just text. If we try to summarize the total number of vaccinations by month, the order doesn’t make sense because the months are alphabetical.
tir |>
group_by(month, year) |>
summarize(total_vacc = sum(number_of_vaccination, na.rm = T),
.groups = "drop") |>
arrange(year, month) |>
filter(year == "2020")Our brain wants to see this starting with January, not April. If we tried to chart this it would also put the months in alphabetical order.
16.6.2 Create a “real” date and month
Here we have to manufacture a way to create a date variable so we can pull the month from it. We do that by
- 1
-
In this line, I’m creating a “real” date by using the
paste()function to put together the “year” and “month” to feed into lubridate’sym()function. If you viewed a valuepaste(year, month)it would look like this: “2020 January”, which theym()function can use to make the first date in that month: “2020-01-01”. I need this “real” date so I can use lubridate’smonth()function to get the “ordered” month. - 2
-
Here we use
month()to pluck our our ordered month into a new variable. - 3
-
This
.after =argument just specifies where in the tibble to add these new columns. I put them near the original one so we could see them.
Note the data type <ord> (for ordered) for mo compared to <chr> (for character) we had for month. That data type indicates this is a special “ordered factor”.
I’ll just do a little cleanup to drop and rename some things. I’m keeping out yr_mo as sometimes a full date is flexible with charting. You can probably figure out the specifics through the function names.
tir_dates <- tir_mo |>
select(-month) |>
rename(month = mo) |>
relocate(yr_mo, .before = year)
tir_dates |> glimpse()Rows: 170,999
Columns: 7
$ county <chr> "Anderson", "Anderson", "Anderson", "Anderson", …
$ yr_mo <date> 2020-01-01, 2020-01-01, 2020-01-01, 2020-01-01,…
$ year <dbl> 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, …
$ month <ord> January, January, January, January, January, Jan…
$ age_at_vaccination <chr> "0 to 24 months", "0 to 24 months", "2-8 years",…
$ vaccine_of_interest <chr> "Pediatric MMR", "All Other", "Pediatric MMR", "…
$ number_of_vaccination <dbl> 32, 670, 28, 69, 14, 22, 18, NA, 48, 31, 566, 27…
16.6.3 Show the result
Now let’s go back to that original summary we were doing.
tir_dates |>
group_by(month, year) |>
summarize(total_vacc = sum(number_of_vaccination, na.rm = T),
.groups = "drop") |>
arrange(year, month) |>
filter(year == "2020")Now these are ordered the way we want them.