library(readr)
library(dplyr)
library(tibble)
library(purrr)
library(stringr)
library(tokenizers)
library(rvest)
job_page <- read_html("data/workforce/job-board.html")
job_all_blocks <- job_page |>
html_elements("h1, h2, p, li") |>
html_text2()
job_detail_sentences <- job_page |>
html_elements("li.job-detail") |>
html_text2()
job_context_blocks <- job_page |>
html_elements("h1, h2, p") |>
html_text2()
help_text <- list.files(
"data/help_articles",
pattern = "[.]txt$",
full.names = TRUE
) |>
sort() |>
map_chr(read_file) |>
str_c(collapse = "\n\n")
count_syllables <- function(words) {
clean_words <- words |>
str_to_lower() |>
str_replace_all("[^a-z]", "")
map_int(clean_words, \(word) {
if (identical(word, "")) {
return(0L)
}
adjusted_word <- if (str_detect(word, "[^aeiou]le$")) {
word
} else {
str_remove(word, "e$")
}
max(1L, str_count(adjusted_word, "[aeiouy]+"))
})
}
syllable_check <- tibble(
word = c(
"paid",
"training",
"provided",
"prior",
"data",
"experience",
"required",
"evening",
"schedules",
"available",
"applicants",
"basic",
"spreadsheet",
"skills",
"employer",
"certification",
"rotating",
"workers",
"portfolio",
"remote",
"receives",
"mentor",
"medical",
"certificate",
"preferred",
"daytime",
"apprenticeship",
"valid",
"license",
"customer"
),
hand_syllables = c(
1L, 2L, 3L, 2L, 2L, 4L, 2L, 3L, 2L, 4L,
3L, 2L, 2L, 1L, 3L, 5L, 3L, 2L, 4L, 2L,
2L, 2L, 3L, 4L, 2L, 2L, 4L, 2L, 2L, 3L
)
) |>
mutate(
heuristic_syllables = count_syllables(word),
comparison = case_when(
heuristic_syllables == hand_syllables ~ "exact",
heuristic_syllables > hand_syllables ~ "overcount",
TRUE ~ "undercount"
)
)
syllable_summary <- syllable_check |>
count(comparison, name = "words")
job_detail_words <- tokenize_words(
job_detail_sentences,
lowercase = TRUE,
strip_punct = TRUE
) |>
unlist(use.names = FALSE)
expected_syllable_summary <- tibble(
comparison = c("exact", "overcount", "undercount"),
words = c(21L, 5L, 4L)
)Scoring readability formulas
Readability scores and syllable-count error
The coordinator compares the job board with the help articles before publication. A readability score might help, but its interpretation depends on the underlying counts.
Readability formulas calculate estimates of text difficulty from surface counts such as sentences, words, and syllables. A syllable is one unit of pronunciation within a word. Hand calculation makes the formulasβ limits visible.
The job board and help articles scored here are fictional teaching texts.
By the end of this lesson, you will be able to:
- describe the counts used by Flesch Reading Ease.
- describe the counts used by Flesch-Kincaid Grade Level.
- write a small syllable-counting heuristic.
- measure that heuristic against hand-counted job-board words.
- explain why a readability score is not a reading level for a person.
Count syllables with an explicit rule
The lesson reads local files and extracts HTML with rvest. It tokenises text and uses dplyr, tibble, purrr, and stringr for count checks. The counter is a heuristic, a rule for an approximate result.
The rule converts words to lowercase and keeps only letters from a to z. It removes a final e unless the word ends in a consonant plus le. It then counts groups of vowel letters, including y. In the code, \(word) is R shorthand for a small function applied to one word.
| Word | Hand count | Heuristic count | Comparison |
|---|---|---|---|
| paid | 1 | 1 | exact |
| training | 2 | 2 | exact |
| provided | 3 | 3 | exact |
| prior | 2 | 1 | undercount |
| data | 2 | 2 | exact |
| experience | 4 | 3 | undercount |
| required | 2 | 3 | overcount |
| evening | 3 | 3 | exact |
| schedules | 2 | 3 | overcount |
| available | 4 | 4 | exact |
| applicants | 3 | 3 | exact |
| basic | 2 | 2 | exact |
| spreadsheet | 2 | 2 | exact |
| skills | 1 | 1 | exact |
| employer | 3 | 2 | undercount |
| certification | 5 | 5 | exact |
| rotating | 3 | 3 | exact |
| workers | 2 | 2 | exact |
| portfolio | 4 | 3 | undercount |
| remote | 2 | 2 | exact |
| receives | 2 | 3 | overcount |
| mentor | 2 | 2 | exact |
| medical | 3 | 3 | exact |
| certificate | 4 | 4 | exact |
| preferred | 2 | 3 | overcount |
| daytime | 2 | 2 | exact |
| apprenticeship | 4 | 5 | overcount |
| valid | 2 | 2 | exact |
| license | 2 | 2 | exact |
| customer | 3 | 3 | exact |
| Comparison | Words |
|---|---|
| exact | 21 |
| overcount | 5 |
| undercount | 4 |
On these 30 job-board words, the heuristic matches 21 of 30, overcounts 5 of 30, and undercounts 4 of 30. The hand counts sum to 78 syllables, while the heuristic sums to 79. That one-syllable error enters the readability formulas directly. Failures occur with adjacent vowels across syllable boundaries, final es endings, and y within vowel groups such as employer.
Compute the formulas directly
Flesch Reading Ease uses this formula:
206.835 - 1.015 * (words / sentences) - 84.6 * (syllables / words)
Flesch-Kincaid Grade Level uses this formula:
0.39 * (words / sentences) + 11.8 * (syllables / words) - 15.59
The code calculates both formulas directly rather than calling a readability package.
score_from_counts <- function(document, sentence_count, word_count, syllable_count) {
tibble(
document = document,
sentences = sentence_count,
words = word_count,
syllables = syllable_count,
words_per_sentence = round(word_count / sentence_count, 1),
syllables_per_word = round(syllable_count / word_count, 1),
flesch_reading_ease = round(
206.835 - 1.015 * (word_count / sentence_count) -
84.6 * (syllable_count / word_count)
),
flesch_kincaid_grade = round(
0.39 * (word_count / sentence_count) +
11.8 * (syllable_count / word_count) - 15.59
)
)
}
score_readability <- function(text, document) {
sentence_count <- tokenize_sentences(text) |>
map_int(length) |>
sum()
word_tokens <- tokenize_words(
text,
lowercase = TRUE,
strip_punct = TRUE
) |>
unlist(use.names = FALSE)
word_count <- length(word_tokens)
syllable_count <- sum(count_syllables(word_tokens))
score_from_counts(document, sentence_count, word_count, syllable_count)
}
preprocessing_counts <- tibble(
text_set = c(
"all HTML blocks collapsed",
"job-detail list items",
"headings, employers, and locations"
),
blocks = c(
length(job_all_blocks),
length(job_detail_sentences),
length(job_context_blocks)
),
sentences = c(
length(tokenize_sentences(str_c(job_all_blocks, collapse = " "))[[1]]),
sum(map_int(tokenize_sentences(job_detail_sentences), length)),
sum(map_int(tokenize_sentences(job_context_blocks), length))
),
words = c(
length(unlist(tokenize_words(str_c(job_all_blocks, collapse = " "), lowercase = TRUE, strip_punct = TRUE), use.names = FALSE)),
length(job_detail_words),
length(unlist(tokenize_words(job_context_blocks, lowercase = TRUE, strip_punct = TRUE), use.names = FALSE))
)
)
readability_scores <- bind_rows(
score_readability(job_detail_sentences, "job detail sentences"),
score_readability(help_text, "help articles")
)
collapsed_score <- score_readability(str_c(job_all_blocks, collapse = " "), "job board collapsed")
job_score <- readability_scores |>
filter(document == "job detail sentences")
help_score <- readability_scores |>
filter(document == "help articles")
net_bias_ratio <- sum(syllable_check$heuristic_syllables) / sum(syllable_check$hand_syllables)
adjusted_job_syllables <- round(job_score$syllables / net_bias_ratio)
syllable_sensitivity <- bind_rows(
job_score,
score_from_counts(
"job detail sentences, net-bias adjusted",
job_score$sentences,
job_score$words,
adjusted_job_syllables
)
)| Text set | Blocks | Sentence pieces | Words |
|---|---|---|---|
| all HTML blocks collapsed | 41 | 22 | 177 |
| job-detail list items | 22 | 22 | 133 |
| headings, employers, and locations | 19 | 19 | 44 |
| Document | Sentences | Words | Syllables | Words per sentence | Syllables per word | Flesch Reading Ease, rounded | Flesch-Kincaid grade, rounded |
|---|---|---|---|---|---|---|---|
| job detail sentences | 22 | 133 | 230 | 6.0 | 1.7 | 54 | 7 |
| help articles | 6 | 56 | 89 | 9.3 | 1.6 | 63 | 7 |
| Document | Syllables | Flesch Reading Ease, rounded | Flesch-Kincaid grade, rounded |
|---|---|---|---|
| job detail sentences | 230 | 54 | 7 |
| job detail sentences, net-bias adjusted | 227 | 56 | 7 |
The code scores only the 22 job-detail list items as job-board sentences. The 19 headings, employer names, and location labels contain 44 words, but they are not complete sentences. Combining all 41 HTML blocks into one string merges those labels with the list items. The resulting Flesch score rounds to 33. That score reflects a preprocessing artefact rather than reading difficulty.
With the list items kept separate, the job-board detail sentences have 22 sentences, 133 words, and 230 heuristic syllables. The help articles have 6 sentences, 56 words, and 89 syllables. The rounded scores are 54 and 7 for the job details, and 63 and 7 for the help articles.
Common Flesch bands classify the collapsed score of 33 as difficult. That college-level interpretation is implausible for these short job-board sentences. Syllable counts still strongly affect the split score, particularly long nouns such as certification and apprenticeship.
These values compare two teaching texts under one set of counting rules. Researchers fitted the formulas decades ago for specific populations, such as US military personnel or schoolchildren, and specific tasks. The formulas use surface features without meaning, layout, consequences, or the readerβs subject knowledge. They are heuristics rather than comprehension models.
Production calculations require established, extensively evaluated tools, such as quanteda.textstats in R or textstat in Python, rather than custom arithmetic.
The 30-word audit found errors on nearly a third of the examined words. The net syllable-count error alone changes the job-detail Flesch score from 54 to 56. The help article sample contains only 56 words. Even whole-number scores imply more precision than these inputs justify.
What to remember
- Readability formulas use sentence, word, and syllable counts.
- Preprocessing can change the sentence and word counts before formula calculation.
- The syllable shortcut matched 21 of 30 job-board words and missed 9.
- Whole-number scores are still rough because the syllable input is rough.
The help articles score slightly easier than the job-detail sentences. The samples are small, and the syllable counter makes errors. The scores indicate text for closer reading rather than replace that reading.