Unit 05.05: A factor is a category with a declared order
R sorts text alphabetically, which puts 'high' before 'low' and makes every chart of your rating scale nonsense.
A factor is a category with a declared order
As character data, the only order available is alphabetical, and alphabetical order almost never matches meaning. Low, medium, high sorts to high, low, medium; days of the week sort to Friday first.
A factor stores the categories with an explicit list of levels, and everything that orders things — tables, axes, legends — follows that list. Declaring the levels is how you tell R what the data means rather than letting it guess from the spelling.
Factors also keep levels that no row uses. That is a feature: a rating nobody gave should still appear in the table as a zero, because 'nobody chose this' is a finding. .drop = FALSE is what makes it visible.
This block contrasts alphabetical order with a declared one, then adds an unused level.
suppressPackageStartupMessages({library(dplyr); library(forcats)})
ratings <- data.frame(
ward = c("North", "South", "East", "West"),
rating = c("high", "low", "medium", "high")
)
# As character, the only order available is alphabetical.
cat("Alphabetical order:", paste(sort(unique(ratings$rating)), collapse = " < "), "\n")
# A factor lets you state the order the data actually has.
ordered <- ratings |>
mutate(rating = factor(rating, levels = c("low", "medium", "high")))
cat("Declared order :", paste(levels(ordered$rating), collapse = " < "), "\n\n")
cat("Counts follow the declared order, which is what a chart or table needs:\n")
print(count(ordered, rating, .drop = FALSE))
# A level that appears in no row still exists, and .drop = FALSE keeps it visible.
with_unused <- ordered |>
mutate(rating = fct_expand(rating, "not assessed"))
cat("\nLevels including one with no rows:",
paste(levels(with_unused$rating), collapse = ", "), "\n")
print(count(with_unused, rating, .drop = FALSE))
Alphabetical order gives high < low < medium, which is meaningless. The declared order gives low < medium < high, and the counts then print in that order — exactly what a chart axis needs. Adding a not assessed level that no row uses keeps it in the table with a count of 0, so a reader can see the category exists and was not chosen, rather than wondering whether it was ever offered.
The mistake this prevents
The mistake is fixing chart order by renaming categories — 1_low, 2_medium — so alphabetical order happens to work. It puts presentation artefacts into the data and every label needs cleaning afterwards.
Takeaway
Convert ordered categories to factors and declare the levels explicitly. Use .drop = FALSE when a zero count is meaningful, and never encode order into the category names.
