library(readr)
library(dplyr)
library(tibble)
library(purrr)
library(stringr)
library(tidytext)
library(SnowballC)
library(hunspell)
sentences <- read_csv(
"data/workforce/workforce_sentences.csv",
na = character(),
col_types = cols(
sentence_id = col_character(),
document_id = col_character(),
source_line = col_character(),
text = col_character(),
reference_label = col_character(),
uncertainty = col_character(),
annotator_id = col_character(),
rationale = col_character(),
codebook_version = col_character(),
codebook_hash = col_character(),
derived = col_character(),
transformation = col_character()
)
)
vocabulary <- sentences |>
select(sentence_id, text) |>
unnest_tokens(word, text) |>
distinct(word) |>
arrange(word) |>
pull(word)
known_to_hunspell <- \(words) map_lgl(hunspell(words), \(result) length(result) == 0L)
stemmed_vocabulary <- tibble(
word = vocabulary,
stem = wordStem(vocabulary, language = "en")
) |>
mutate(
changed = word != stem,
stem_in_dictionary = known_to_hunspell(stem)
)Removing word endings with stemming rules
Use Snowball stems without mistaking them for words
A short vocabulary list from the job-board and flyer text is the teamβs next goal. The team sees shifts, skills, and schedules and asks whether plural endings should count as separate words.
That choice affects the summary. Keeping every surface spelling separate increases the list length. Excessive removal of endings can assign one code to words with different meanings.
Stemming removes or changes word endings using fixed rules. In linguistics, a stem is the part of a word that an ending attaches to. In this lesson, a stem is whatever string these rules return. It is a comparison key. It may or may not be a word.
The author created the fictional Riverton Workforce Lab, its job board, and its training flyer for teaching. SnowballC produces the stems locally.
By the end of this lesson, you will be able to:
- describe what a stemmer does.
- find stems that are not dictionary words.
- find collisions, where several words share one stem.
- name related words that a suffix rule leaves apart.
Build the Riverton vocabulary
The vocabulary build uses readr, dplyr, tibble, tidytext, SnowballC, hunspell, purrr, and stringr. A word type is one distinct lowercased form in these 28 text units. The count uses Lesson 15βs tokenizers::tokenize_words() rule. A different tokenizer would change the count.
| Word type | Stem | Changed | Stem in dictionary |
|---|---|---|---|
| provided | provid | TRUE | FALSE |
| required | requir | TRUE | FALSE |
| shifts | shift | TRUE | TRUE |
| skills | skill | TRUE | TRUE |
| training | train | TRUE | TRUE |
The stemmer changes 43 of the 96 word types. shifts becomes shift, a useful result for a count that should group singular and plural forms. required becomes requir, which is not a word a reader should see.
Watch the rule on a nearby word
SnowballCβs English stemmer, often called Porter2, uses fixed rules. The word requires is absent from the Riverton vocabulary. It illustrates the same ending removal as required.
example_stems <- tibble(
word = c("shifts", "requires", "required"),
stem = wordStem(word, language = "en")
)| Word | Snowball stem |
|---|---|
| shifts | shift |
| requires | requir |
| required | requir |
Treat the returned stem as a machine comparison string. Do not present it as the wordβs dictionary form.
Hunspell uses a spelling dictionary and affix rules. SnowballC returns stems without consulting a dictionary. It can therefore return requir, provid, and schedul.
Count stems that are words and stems that are not
The next table separates changed stems that are dictionary words from changed stems that are unknown to hunspell.
stem_quality <- stemmed_vocabulary |>
summarise(
word_types = n(),
changed_by_stemmer = sum(changed),
unchanged_by_stemmer = sum(!changed),
changed_to_dictionary_word = sum(changed & stem_in_dictionary),
changed_to_non_word = sum(changed & !stem_in_dictionary),
.groups = "drop"
)
non_word_examples <- stemmed_vocabulary |>
filter(changed, !stem_in_dictionary) |>
arrange(word) |>
slice_head(n = 8)| Word types | Changed | Unchanged | Changed to dictionary word | Changed to non-word |
|---|---|---|---|---|
| 96 | 43 | 53 | 21 | 22 |
| Word type | Stem | Changed | Stem in dictionary |
|---|---|---|---|
| able | abl | TRUE | FALSE |
| applicants | applic | TRUE | FALSE |
| apply | appli | TRUE | FALSE |
| certificate | certif | TRUE | FALSE |
| certification | certif | TRUE | FALSE |
| communication | communic | TRUE | FALSE |
| daytime | daytim | TRUE | FALSE |
| essential | essenti | TRUE | FALSE |
Among these 96 word types, 21 transformed forms are dictionary words. Another 22 transformed forms are unknown to hunspell. A dictionary-word stem can still be an unsuitable grouping key for a workforce question.
Find collisions
A collision happens when different word types reduce to the same stem. A collision can help when the words are close enough for the question. It can also hide a useful distinction.
collision_groups <- stemmed_vocabulary |>
group_by(stem) |>
filter(n() > 1L) |>
summarise(
word_types = paste(word, collapse = ", "),
n_words = n(),
.groups = "drop"
) |>
arrange(stem)| Stem | Word types that share it | Word types |
|---|---|---|
| certif | certificate, certification | 2 |
| includ | included, includes | 2 |
| schedul | schedule, schedules | 2 |
| skill | skill, skills | 2 |
The four collision groups contain eight word types. schedule and schedules are a helpful group. certificate and certification may or may not be close enough, depending on whether the Lab counts credentials or training topics.
Find misses
A miss is a related pair that receives different stems. The team selected three pairs with suspected differences, not a survey sample. These examples show limitations of suffix rules.
miss_pairs <- tribble(
~word_a, ~word_b, ~reason,
"applicants", "apply", "Both point to applying, but their stems differ.",
"paid", "pays", "Snowball strips suffixes. 'Paid' has no suffix to strip, so no rule can reach 'pay'.",
"work", "workers", "'-er' names the person doing the activity. Whether to group them depends on the question."
) |>
mutate(
stem_a = wordStem(word_a, language = "en"),
stem_b = wordStem(word_b, language = "en"),
same_stem = stem_a == stem_b
)| Word A | Word B | Reason for comparison | Stem A | Stem B | Same stem |
|---|---|---|---|---|---|
| applicants | apply | Both point to applying, but their stems differ. | applic | appli | FALSE |
| paid | pays | Snowball strips suffixes. βPaidβ has no suffix to strip, so no rule can reach βpayβ. | paid | pay | FALSE |
| work | workers | β-erβ names the person doing the activity. Whether to group them depends on the question. | work | worker | FALSE |
All three selected pairs receive different stems. Suffix rules can miss irregular forms and relationships whose meaning depends on more than an ending.
What to remember
- Stemming removes or changes word endings with fixed rules.
- The Snowball English stemmer is a Porter2 rule system.
- SnowballC changed 43 of the 96 Riverton word types.
- In this vocabulary, 22 changed stems are non-words.
- Collisions and selected misses both need human judgment.
Stems can provide internal keys for approximate grouping. A public list should show recognizable words. A person must examine disputed groups.