Scoring readability formulas

Readability scores and syllable-count error

sentences and paragraphs
readability scoring
workforce research
Learn how readability formulas use sentence, word, and syllable counts, and why their limits are severe.

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.

Note

The job board and help articles scored here are fictional teaching texts.

TipWhat you will learn

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.

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)
)
Hand-counted job-board syllables compared with the vowel-group heuristic
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
Syllable heuristic results for 30 job-board words
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
  )
)
Inputs for readability calculations
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
Readability formulas computed directly from counted parts
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
Effect of applying the observed net syllable-count error to the job-board score
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.

Sources