knitr::opts_chunk$set(echo = FALSE, collapse=TRUE)

# load libraries
library(tidyverse)
library(here)
library(broom)
library(knitr)
library(kableExtra)
library(plotly)
library(hrbrthemes)
library(htmltools)

# load raw data
alignments <- read_tsv(here("data", "alignments.csv"))
episodes <- read_tsv(here("data", "episodes.csv"))

# set theme
theme_set(hrbrthemes::theme_ipsum_gs())

The mystery

I recently stumbled across an old John Oliver episode from Season 1. The pace of that episode was noticeably slower — so slow in fact that I thought I had YouTube on half speed.

My interest was piqued: has John Oliver become more angry fast-paced over time?

(For attention I asked “Has John become more angry?” — but I’m a data scientist, not a loudmouthed journalist, so we’ll leave that inference to the expertise and analysis of a corpus phonetician. And regardless of whether John indeed has become more “angry”, I thank him for being angry with us and making everything slightly more bearable.)

The short story

Has John's way of speaking become faster?

Yes, it has. By a lot.

Over his first 11 seasons, John's speech has accelerated from an average of 150 to almost 180 words per minute.

For comparison: 100—120 wpm is generally considered “slow”, 120—150 “conversational”, and by 200+, we’re nearing auctioneer-like speech styles. A speaking rate of 160 wpm is a threshold for comfortably processing complex information. There is, after all, a recommendation to keep presentations to between 100—150 wpm.1

The increase in John’s speech is really quite noticeable — and keep in mind that it is measured across comedy segments with considerably more space taken by audience reactions than your average TED talk or business presentation.

See the Appendix for an interactive (experimental) version of this plot.

Moving on — let’s dive straight in to our main story!


The key insights:

  • There is a significant increase in speed over time.
  • Pauses between words are decreasing, partly as a result of faster speaking, but of course adding to the general “faster pace” impression.
  • During covid, when John was broadcasting without an audience, averages are slightly higher and episodes with low-ish speaking rates are suspiciously absent. This probably reflects the absence of interruptions by audience reactions; the period otherwise continues the overall trend.
  • The length of the main segments also increases over time — but is not a confounding factor.

The main story

Task: This type of questions and the data are straightforward corpus linguistic tasks: we count words and calculate the speech rate in words per minute, most simply at the episode-level. With more advanced NLP tools, we measure syllables per second and pauses between words at the word-level. The word-level is much more fine-grained, which allows us to exclude the effects of longer stretches of silence, for example.

Challenge: How do we deal with artifacts in the data unrelated to changing speech styles? For example, the color of the dots shows that the main segments in Last Week Tonight have also increased in duration. In statistical terms, that could be a confounding factor. It could well be that the increase in speaking rate is simply due to the unrelated fact that breaks, silences, intros/outros and/or stretches by other people are relatively less influential in longer segments of text.

tl;dr — in this data, text length has no association with style (and neither have clips by other people).2

But let’s see.

Data & processing

Using yt-dlp, I fetched the audio, the auto-generated subtitles and meta data such as file duration, viewers, likes and comments from a list of 314 episodes of Last Week Tonight.

The data is only from the main segments of each episode, which range in duration from 6 to 34 minutes. Until last year, those were the only doses of John available to most people in the outside world without a TV or cable subscription. (I fetched all data in late 2024.)

More information and the data is in the project’s Github repo.

Analysis

Episode-level (simple)

For the graph above, I used the simplest possible method, measuring speed in words per minute. NA in a row indicates that either the main segment or the auto-generated subtitles were unavailable.

The measurement Words per minute is simply the number of words per episode divided by the length of the episode, here expressed in seconds to make calculations easier:

# subset episodes, rename columns for readability, and determine wpm speech rate
episodes_clean <- episodes |> 
  select(season, episode_total, main_segment, duration, length, wrds) |> 
  rename(Season = season,
         Episode = episode_total,
         Title = main_segment,
         Duration = duration,
         Length = length,
         Words = wrds) |> 
  mutate(`Words per minute` = Words/(Length/60))

# output table
episodes_clean |> 
  head(10) |> 
  kable(digits = 0, caption = "Episode list for the first 10 episodes in Season 1.") |> 
  kable_styling(
    full_width = FALSE,
    bootstrap_options = c("striped", "hover", "condensed")) |> 
  row_spec(0, bold = TRUE, color = "white", background = "grey50")
Episode list for the first 10 episodes in Season 1.
Season Episode Title Duration Length Words Words per minute
1 1 POM Wonderful LLC v. Coca-Cola Co.  NA NA NA NA
1 2 Death Penalty 00:12:23 743 1757 142
1 3 Climate change denial NA NA NA NA
1 4 Right to be forgotten 00:06:59 419 1009 144
1 5 Net neutrality 00:13:18 798 1930 145
1 6 FIFA 00:13:13 793 1966 149
1 7 Immigration reform in the United States NA NA NA NA
1 8 Dr. Oz and Nutritional Supplements 00:16:25 985 2297 140
1 9 Hobby Lobby 00:06:10 370 976 158
1 10 Wealth Gap 00:14:10 850 2304 163

Word-level (advanced)

At the word-level, we can exclude some of the artefacts that affect the episode-level. Because if we measure syllables per second for each word or the pause between words, then silent periods or audience interruptions become largely irrelevant.

So, for a random subset of 108 segments, I used Gentle to force-align the audio with the subtitles. Gentle tries to determine the beginning and the end of each word, from which we can calculate its duration.

Gentle is the only forced-alinger that works reasonably well on audio files longer than a few seconds. This comes at the cost of accuracy, but patterns will be clear enough for over 370,000 data points. Gentle also returns all time stamps in centiseconds (1/100th of a second), whereas small-data “proper phonetics” usually works in milliseconds.

The package {nsyllables} counts the syllables to give us syllables per second of that particular word. Pauses are calculated as the difference between the end of one word and the beginning of the next, and are NA where Gentle could not align the next word.

This table illustrates this process for Episode 18 in Season 8, Housing Discrimination:

# adds duration, pauses, medians
alignments <- alignments |> 
  group_by(ep_id) |> 
  # normalize 
  mutate(beg = round(beg, 4),
         end = round(end, 4)) |> 
  mutate(pause = lead(beg) - end,
         dur = end - beg) |> 
  mutate(syl = nsyllable::nsyllable(word)) |> 
  mutate(sps = (syl/dur)) |> 
  ungroup()

# illustrate for one episode
alignments |> 
  filter(ytid == '_-0J49_9lwc') |> 
  # column names that are better to read
  rename(Date = ytdate, 
         Word = word,
         Recognised = recog,
         `No of syllables` = syl,
         Begin = beg,
         End = end,
         Duration = dur,
         `Syllables per second` = sps,
         `Pause to next word` = pause) |> 
  select(Word:End, Duration, `Pause to next word`, `No of syllables`, `Syllables per second`) |> 
  head(12) |> 
  kable(digits = 2, caption = "Forced-alignment. NA result from alignment errors.") |> 
  kable_styling(
    full_width = FALSE,
    bootstrap_options = c("striped", "hover", "condensed")) |> 
  row_spec(0, bold = TRUE, color = "white", background = "grey40") |> 
  # highlight rows with alignment errors
  row_spec(3:4, color = "grey70")
Forced-alignment. NA result from alignment errors.
Word Recognised Begin End Duration Pause to next word No of syllables Syllables per second
moving moving 4.82 5.17 0.35 0.00 2 5.71
on on 5.17 5.57 0.40 NA 1 2.50
our NA NA NA NA