library(readr)
library(dplyr)
library(tibble)
library(tidyr)
library(purrr)
library(stringr)
library(ggplot2)
library(tidymodels)
library(textrecipes)
library(glmnet)
inbox <- read_csv(
"data/riverton/riverton-inbox.csv",
na = character(),
col_types = cols(
message_id = col_character(),
text = col_character(),
is_spam = col_logical(),
intent = col_character(),
author_note = col_character()
)
) |>
mutate(intent = factor(intent))
intent_counts <- inbox |>
count(intent, name = "messages")Sorting messages by purpose
Intent prediction and label design
Luis manages the shared inbox while the front desk is closed. One message asks for a password reset. Another asks whether a class accepts GED applicants. A third reports a suspicious recruiter.
Routing the message is useful only if the label matches a real next step. A worker can reset access, read application status, answer eligibility questions, record a problem, or return a call. The label set does not describe actions outside that list.
The Riverton Workforce Lab, its job board, and its training flyer are fictional teaching materials.
A dataset written by the classifierβs author cannot establish whether the method works generally. This lesson demonstrates calculations with the invented inbox. It does not give an estimate of intake-system performance for real residents.
By the end of this lesson, you will be able to:
- explain intent classification in plain language.
- fit a small multi-class text model.
- examine multi-class probability output.
- find messages that need more than one routing label.
- explain why coarser taxonomies can make accuracy look better.
Load the intent labels
An intent is what the sender wants the system to do. Each message in this fixed teaching dataset has exactly one recorded intent, even when a person might find several.
readr reads the file, while dplyr and tidyr reshape results. purrr selects probability columns, and stringr evaluates simple rules. tidymodels, textrecipes, and glmnet fit the model.
| Recorded intent | Messages |
|---|---|
| ask_eligibility | 12 |
| check_status | 13 |
| report_problem | 13 |
| request_callback | 11 |
| reset_access | 11 |
The label schema states the distinctions that the Lab can use. If staff cannot act on a distinction, adding its label creates disagreement without improving service.
Fit a multi-class model
Multi-class classification chooses one label from more than two possible labels. The seed before this fit is 5002. The model demonstrates the calculations for a multi-class prediction.
intent_recipe <- recipe(intent ~ text, data = inbox) |>
step_tokenize(text) |>
step_tokenfilter(text, max_tokens = 100) |>
step_tfidf(text)
mechanics_rows <- inbox
intent_model <- multinom_reg(penalty = 0.01, mixture = 1) |>
set_engine("glmnet") |>
set_mode("classification")
set.seed(5002)
intent_fit <- workflow() |>
add_recipe(intent_recipe) |>
add_model(intent_model) |>
# The lesson reports nothing performance-related from these rows; it shows the
# shape of a multi-class fit and states plainly why it scores nothing.
fit(data = mechanics_rows) # resubstitution-ok: no score is reported
intent_predictions <- mechanics_rows |>
select(message_id, text, intent) |>
bind_cols(predict(intent_fit, mechanics_rows, type = "prob")) |>
bind_cols(predict(intent_fit, mechanics_rows))
probability_columns <- names(intent_predictions) |>
keep(\(column) str_starts(column, ".pred_") & column != ".pred_class")
probability_check <- intent_predictions |>
transmute(
message_id,
probability_sum = rowSums(pick(all_of(probability_columns)))
)
probability_display <- intent_predictions |>
slice_head(n = 3) |>
select(message_id, all_of(probability_columns)) |>
rename_with(
\(column) str_remove(column, "^\\.pred_"),
all_of(probability_columns)
)
predicted_distribution <- intent_predictions |>
count(.pred_class, name = "messages") |>
arrange(.pred_class)| Message | Ask eligibility | Read application status | Report problem | Request callback | Reset access |
|---|---|---|---|---|---|
| M001 | 0.014 | 0.940 | 0.039 | 0.003 | 0.004 |
| M002 | 0.061 | 0.881 | 0.007 | 0.022 | 0.029 |
| M003 | 0.016 | 0.934 | 0.034 | 0.007 | 0.009 |
| Predicted intent | Messages |
|---|---|
| ask_eligibility | 12 |
| check_status | 13 |
| report_problem | 13 |
| request_callback | 11 |
| reset_access | 11 |
The fitted model has five classes. It returns five probabilities per message, one per class, with a row sum of 1. The model predicts the same 60 rows it uses for fitting. The table therefore demonstrates output structure rather than performance. This lesson reports no performance score.
The classifierβs author also wrote all 60 messages. A score on those messages would describe this fixture rather than the method. Independent evaluation requires messages from other authors and labels from reviewers who did not build the model. It also requires a holdout set excluded from fitting.
Find gaps in the taxonomy
A taxonomy specifies the labels and the rules for choosing them. Label design can be harder than model fitting. If the taxonomy does not match the work, accurate label predictions can still send messages to the wrong service.
boundary_messages <- inbox |>
filter(message_id %in% c("M034", "M055", "M044")) |>
mutate(
other_plausible_action = case_when(
message_id == "M034" ~ "ask_eligibility or fraud review",
message_id == "M055" ~ "ask_eligibility or fraud review",
message_id == "M044" ~ "none without the missing request"
),
why_it_strains_the_schema = case_when(
message_id == "M034" ~
"reports a suspicious listing but also mentions training and bank access",
message_id == "M055" ~
"asks whether an offer is real and reports a recruiter asking for ID",
message_id == "M044" ~
"gives call instructions without a full service request"
)
) |>
select(message_id, intent, other_plausible_action, why_it_strains_the_schema, text)| Message | Recorded intent | Other plausible action | Why one label is hard | Text |
|---|---|---|---|---|
| M034 | report_problem | ask_eligibility or fraud review | reports a suspicious listing but also mentions training and bank access | A listing says paid training, but the attachment asks for my bank password. |
| M044 | request_callback | none without the missing request | gives call instructions without a full service request | Please leave a voicemail if I miss the call. |
| M055 | report_problem | ask_eligibility or fraud review | asks whether an offer is real and reports a recruiter asking for ID | I am not sure whether this offer is real; the recruiter wants a photo of my ID. |
Messages M034 and M055 genuinely fit two actions. A staff member may both record a problem and answer whether the offer is safe. If the Lab has a separate queue for fraud checks, these two messages should probably go there. If it does not, the worker must choose the nearest available label and record the information that the label omits.
M044 fits none of the main service questions by itself. It gives a call preference without the reason for the call. An intake system needs the missing purpose before it can route the message. Lesson 11, βWorking with several annotators,β explains why multiple reviewers should examine boundary cases before choosing reference labels.
An other label covers messages that the designer did not anticipate. Staff must examine those messages to find new service needs.
Broader labels can increase measured accuracy
Combining intents into broader queues can increase the score for unchanged predictions. The scoring task becomes easier without better classification. This lesson does not compute that score. A rule fitted to these same 60 messages would provide another resubstitution result, a score calculated on its training examples.
Broader categories may be appropriate when the organization cannot act on finer distinctions. A report must not present the resulting increase in accuracy as better understanding.
What to remember
- Intent classification routes text by what the sender wants.
- The intent list must match actions the system can take.
- Probability output shows the shape of a multi-class prediction, not its quality.
- Boundary cases need human adjudication and clearer instructions.
- Coarser categories can raise measured accuracy by making the task easier.