Why Character and Factor Classes Matter
A column such as payment channel may contain labels like “Agent,” “Online,” and “Phone.” Those values describe categories, not numerical measurements. In R, the column’s class influences how functions interpret it: a character vector stores text, while a factor represents categories through a set of levels. The distinction matters when you inspect data, check which categories are possible, or ask R for a summary.
A character column can contain repeated strings, but it does not maintain a separate list of permitted categories. A factor has an explicit set of category labels called levels. This means a factor can retain a category even when no current row has that value. It also means that a factor’s internal integer storage must not be mistaken for numeric measurements.
These are both appropriate ways to represent categorical data, but they answer different inspection questions. Character values show the text recorded in rows. Factor levels show the categories the factor recognizes, including any levels that currently have no observations. The class also affects the output of common R summaries.
Inspecting Character Values and Factor Levels
Use class(x) to check whether a column is character or factor. For a factor, levels(x) returns its category labels in their stored order. Calling levels() on a character vector does not reveal a category list: characters do not have factor levels.
A factor’s levels are not necessarily just the values currently present in the data. For example, a factor may have levels “Basic,” “Plus,” and “Premium,” even if there are no “Premium” observations in a particular subset. That remaining level can be useful because it records the categories defined for the variable, but it should not be reported as evidence that the subset contains a Premium observation.
When a factor is created without explicitly specifying levels, R generally forms levels from the distinct non-missing values, in sorted order. The exact order can depend on the sorting rules in the R environment. If a specific order is important, supply the levels explicitly. For nominal categories, this order is a storage and display choice, not a claim that one category is greater than another.
Worked Example: Reading a Character Column
A hypothetical insurer records the payment channel for five service requests as the character vector `channel = c("Web", "Agent", "Web", "Phone", "Agent")`. Determine the class, the distinct observed labels, and what `summary(channel)` tells you about category counts.
Step 1: Check the class. The entries are stored as text, so `class(channel)` returns `"character"`. There is no factor-level list associated with this vector.
Step 2: Identify the distinct observed labels. Reading the five entries gives Web, Agent, Web, Phone, and Agent. The distinct labels are Agent, Phone, and Web. Their observed counts are Agent: 2, Phone: 1, and Web: 2.
Step 3: Interpret the class-specific summary. For a character vector, `summary(channel)` reports properties such as its length, class, and mode; it does not produce those category counts. The vector has length 5, class character, and mode character. Thus, a reader must not mistake this output for a frequency summary.
Step 4: Request counts when needed. `table(channel)` gives Agent: 2, Phone: 1, and Web: 2, in the displayed label order. The counts sum to 5, matching the number of non-missing entries. The method avoids the common mistake of expecting `summary()` to count the values of a character vector.
How Factor Summaries Work
For a factor, `summary()` reports counts for its levels, rather than only describing the vector’s storage mode. For a small factor, each level is shown, including a level with zero observations. If there are missing values, the summary also reports how many values are missing. The output is therefore a category-count summary tied to the factor’s declared levels.
This behavior follows from what a factor represents. Each non-missing entry is a code that points to one of the factor’s labels. Counting entries by their labels is equivalent to counting entries by their corresponding codes, while retaining the labels makes the result interpretable. A level with no matching code among the observed entries has count zero; it remains a valid declared level even though it was not observed.
Here, \(x_i\) is the factor value for row \(i\), \(L_j\) is one of the factor’s labels, and \(I(\cdot)\) equals 1 when the condition is true and 0 otherwise. Missing values are not equal to any category label, so they are counted separately when the summary includes them. This explains why the counts across non-missing levels sum to the number of non-missing entries, not necessarily to the vector’s full length.
Worked Example: A Factor with an Unused Level and Missing Data
A hypothetical benefits team defines a factor with levels “Core,” “Flex,” and “Legacy.” Its recorded values are Core, Flex, Core, missing, and Flex. Find the factor summary counts and check that they reconcile to the data.
Step 1: Count the recorded category labels. Core appears in rows 1 and 3, so its count is 2. Flex appears in rows 2 and 5, so its count is 2. Legacy does not appear, so its count is 0.
Step 2: Count missing entries. Row 4 is missing, giving 1 missing value. A factor summary therefore reports Core: 2, Flex: 2, Legacy: 0, and NA’s: 1.
Step 3: Check the total. The non-missing level counts sum to \(2 + 2 + 0 = 4\). Adding the one missing value gives \(4 + 1 = 5\), which matches the five rows.
Interpretation. Legacy is part of the factor’s defined categories, not an observed category in these five rows. The method avoids the trap of interpreting a displayed zero count as evidence of an observation.
Creating and Converting Factors Safely
Use `factor(x)` or `as.factor(x)` to represent character values as a factor. By default, the distinct non-missing labels become the levels. To preserve a specified category set or order, use the `levels` argument explicitly. For example, `factor(channel, levels = c("Agent", "Phone", "Web", "Mail"))` defines four levels. If “Mail” does not occur in `channel`, it remains a level with count zero.
There is an important consequence when a value is not among explicitly supplied levels. In the preceding construction, if a row contained “Kiosk,” that value would not match any specified level and would become missing in the resulting factor. Check the levels and the resulting missing values when defining levels deliberately; a spelling difference can create a category mismatch.
Use `as.character(f)` to recover the factor’s labels as character strings. Do not use `as.numeric(f)` to recover numeric-looking labels. That operation returns the factor’s integer codes, not the text labels or necessarily the numbers represented by those labels.
Worked Example: Avoiding the Factor-Code Trap
A hypothetical portfolio file stores the band labels “10,” “20,” and “40” as a factor whose levels are `c("10", "20", "40")`. The observed labels are “20,” “10,” and “40.” A reviewer wants to obtain the recorded labels as numbers. Show why direct numeric conversion gives the wrong result and how to perform the conversion correctly.
Step 1: Identify the factor codes. The first level, “10,” has code 1; “20” has code 2; and “40” has code 3. Therefore, `as.numeric(f)` returns 2, 1, and 3 for the three rows. These are positions in the levels list, not the represented values.
Step 2: Convert through the labels. First apply `as.character(f)`, giving the strings “20,” “10,” and “40.” Then apply `as.numeric()` to those strings, giving 20, 10, and 40.
Step 3: Check row by row. The first original label is “20” and the converted number is 20; the second is “10” and becomes 10; the third is “40” and becomes 40. The correct result preserves the values represented by the labels.
Common mistake and correction. The incorrect calculation is `as.numeric(f)`, which returns 2, 1, 3. The correction is `as.numeric(as.character(f))`, provided the labels are genuinely numeric text. The conversion should be checked for values that are not numeric strings, since such labels cannot be meaningfully converted to numbers.
Comparing the Classes in Practice
For a quick comparison, the key question is whether you need to inspect recorded text or the category set represented by a factor. A character vector has its observed strings, but it has no separate levels list. A factor has labels and may retain categories with no current observations. The behavior of `summary()` reflects that difference.
| Question | Character column | Factor column |
|---|---|---|
| What class does R report? | character | factor |
| Can `levels(x)` list defined categories? | No; a character vector has no factor levels | Yes; it returns the stored labels |
| What does `summary(x)` emphasize? | Vector properties such as length, class, and mode | Counts for levels, and missing values when present |
| Can an unobserved category still be listed? | Not as a separate level | Yes; it can remain a level with count zero |
| Does integer storage imply a numeric measurement? | Not applicable | No; codes identify labels and are not category measurements |
When reviewing a data frame, first establish the column class, then interpret the output accordingly. A factor summary provides counts for its declared categories, whereas a character summary is not a replacement for a category-count check. If a factor contains unexpected levels, zero counts, or missing values, investigate whether those results follow from the data definition, a subset, or a conversion.
Common Traps
- Expecting character summary output to list frequencies. `summary()` on character data reports vector properties, not counts by label. Use a method intended to count values if category counts are required.
- Confusing factor levels with observed values. A level can remain after filtering even when its count is zero. Report it as a defined but unobserved category in that subset.
- Treating factor codes as measurements. `as.numeric(f)` returns integer codes. Convert through `as.character(f)` before numeric conversion when labels truly represent numbers.
- Assuming factor order expresses a ranking. The order of levels can control display order, but it does not by itself establish that the categories are meaningfully ordered.
- Overlooking mismatched labels. When an explicit set of levels is supplied, values that do not match those labels become missing. Check spelling, capitalization, and the resulting missing count.
Check Your Understanding
For each problem, identify what the R output means before drawing a conclusion about the categories.
- A character vector contains “Desk,” “Mobile,” “Desk,” and “Tablet.” What does `class(x)` report, and what does `summary(x)` not tell you? (Answer: class is character; `summary(x)` does not give category frequencies.)
- A factor has levels “North,” “South,” and “West,” with observed values North, West, North, and one missing value. Give the level counts and verify the total. (Answer: North 2, South 0, West 1, NA’s 1; total 4.)
- A factor has levels `c("A", "B", "C")`, and its observed labels are C, A, C. What does `levels(x)` return, and which level is unused? (Answer: “A,” “B,” “C” in that stored order; B is unused.)
- A factor has levels `c("5", "15", "25")` and observed labels “15” and “25.” What does `as.numeric(x)` return, and what does `as.numeric(as.character(x))` return? (Answer: 2, 3; then 15, 25.)
- A character column contains “Email,” “Mail,” and “Email.” It is converted using `factor(x, levels = c("Email", "Phone"))`. What happens to “Mail,” and what are the resulting counts including missing values? (Answer: “Mail” becomes missing; Email 2, Phone 0, NA’s 1.)