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 NA 2 NA
main NA NA NA NA NA 1 NA
story story 6.03 6.31 0.28 0.00 2 7.14
tonight tonight 6.31 6.60 0.29 0.00 2 6.90
concerns concerns 6.60 7.18 0.58 0.11 2 3.45
land land 7.29 7.86 0.57 0.10 1 1.75
specifically specifically 7.96 8.68 0.72 0.07 4 5.56
the the 8.75 8.93 0.18 0.00 1 5.56
history history 8.93 9.47 0.54 0.02 3 5.56
of of 9.49 9.69 0.20 0.00 1 5.00

Explanations:

  • Recognised indicates if Gentle was successful in locating the word in the audio file. If NA, that’s an alignment error — no further metrics can be determined.
  • Syllables per second is a word-level metric — number of syllables divided by the word’s duration.
  • The main segments on YouTube almost always begin with a 4s version of the show’s long intro. Hence, this artifact probably only has a negligible effect.

It is worth noting that (longer) pauses between words generally occur (i) at phrase boundaries, much like a “spoken” comma you’d expect after specifically, or (ii) for dramatic effect, like before land — both are clearly audible in the segment.


If we now average speed and pauses over all words in each segment, we get a visual confirmation that John’s speech — and arguably the show’s pace in general — have become much faster.

Over time, words are spoken more quickly and there are shorter pauses between words:

Of course in faster-paced speech, pauses between words also get shorter. That being said — in connected speech, pauses between words are usually 0 at any speech rate, especially within phrases. Therefore, an average decrease from 120ms to 90ms does suggest John is combining phrases and sentences increasingly more quickly, adding to the big-picture impression of the show’s faster pace in general.

In data terms, it is worth pointing out that the linear regression line for syllables per word (as shown here in pink (method = "lm") is near identical to a line from a method that would detect non-linear relationships (i.e., method = "gam"). Hence, we can turn to a linear regression as a suitable modelling technique.

Predictive modelling

Of course, by now we have a pretty solid picture that the initial auditory impression was indeed correct. Nonetheless, we can use this data set to illustrate the advantage of a linear regression. Because at face value, all we have in data terms is this: “an increase in words per minute” and “an increase in amount of text”. In other words, we don’t know if the increase in words per minute is an artifact of the increase in amount of text.

We could model (or predict) either words per minute or syllables per second. The latter is more fine-grained, thus truer to the original question, and has vastly more data points to work with. However, the word-level is also vastly more complex to model, as it comes with additional complications. For instance, we would have to control for word properties and/or the words themselves (of which there are over 22,000 types). That’s for another day — or for another expert.

Alternatively, if we predict the aggregate syllables per seconds at the episode-level, we only have roughly a third of the data points than we have for words per minute.

Correlations & predictors

From the original question, it is clear which factors to include: we want to predict wpm as a proxy of speaking rate from season as a proxy of time, controlled for length of segments.

But for the sake of illustration, let’s discuss a few additional predictors. They also explain why some files were excluded for the analysis:

  • ep_within — the episode number within a season — for model stability, we exclude 6 segments from the only season with those high number of episodes,
  • align_errors — measures the proportion of words in an episode which Gentle could not align properly, and,
  • pause_mean — the average pause between the end of one word and the beginning of the next.

A correlation matrix shows:

  1. The factors of interest — wpm, length and season — are all expectedly highly correlated.
  2. Alignment errors (align errors) and pauses (pause_mean) are strongly correlated. This is almost entirely due to a handful of files with extreme alignment errors — leading to 30s+ pauses. I thus remove(d) a handful of segments with an implausibly high average pause of over 200ms.
  3. The number of an episode within a season is not correlated with any other variable. We use this variable to “test” if speech rate is associated with a kind of “fluency” effect across a season. Conceptually speaking, there is no reason to believe that this should be the case (cuz, why?).

Linear model

Our simple model is wpm ~ season + length + ep_within, over the 294 segments. It tries to ‘explain’ the increase in words per minute with time, segment length, and the phase of a season.

Linear model (wpm ~ season + length + ep_within)
term estimate std.error statistic p.value
(Intercept) 153.948 1.431 107.564 0.000
season 2.297 0.226 10.187 0.000
length -0.620 0.678 -0.914 0.361
ep_within 0.539 0.517 1.042 0.298

Reading this table is straightforward: where the estimate (coefficient) is positive, an increase in that variable leads to an increase in the outcome variable. If a predictor’s p-value is less than 0.05, it is conventionally assumed that this is influence beyond chance.

In essence: only season is a significant predictor of wpm. Neither the increased segment length over time, nor the number of an episodes within a season are significantly associated with the increased speaking rate above chance.

Since season was not numerically transformed, we can interpret its coefficient 2.297 directly and in ‘raw’ terms:

On average, with every season, John's speaking rate increased by 2.3 words per minute. Since we have 11 seasons, that is a rather intuitive statistical confirmation of the increase from roughly 150 to almost 180 wpm that we eye-balled off the scatterplot in the beginning.

Note: for those more versed in linear models, the variance explained is moderate (R2 = 0.37). That being said, I’m used to far lower values in linguistics. A low R2 is usually the result of complexity that is operationalised by extreme simplification. Model performance tools detected no issue, suggesting the linear model is an adequate approach, albeit simple.

For what it’s worth: running a comparable model on the smaller sample with the more fine-grained predictors syllables per second or pause_mean leads to an identical interpretation: substantially faster-paced speech over time, and no effect of segment length.

Hence, we go with the simpler model that is easier to understand and more intuitive to interpret.

Concluding remarks

Overall, the auditory impression is confirmed.

The actual numbers — 25 wpm more in 2024 than in 2014 — need to be taken with a grain of salt. Let’s not forget that this average is measured across an entire segment, including intros/outros, audience reactions, pauses for dramatic effect, or the segments of other people. In general, we assume that this ‘noise’ is relatively constant across segments (or episodes, probably), but many of the mentioned factors probably ‘drag’ the average down a bit when compared to the speaking rates of other people or contexts.

As for the reasons why we see John’s pattern — pure speculation, and not my area of expertise at all.

An natural reaction is that John is indeed also angrier — we intuitively associate emotional speech with greater speed. And there is after all a significant amount of stuff that’s been getting progressively more worth being angry about since 2014.

Whether emotion of this kind can be measured in this data? May be. It’ll probably need some form of pitch analysis and a skilled computational phonetician to get something out of audio files of very poor quality.

A topic-modelling analysis might be interesting to answer whether more political segment topics (but how define?) are associated with more emotional speech styles. The obvious issue: all of John’s stories are somewhat political and equally enraging. Exploring the patterns — see the interactive plot below — doesn’t seem to suggest that it has anything to do with the topic. But that’d be for another day.

Appendix

Plot code

This code generated the graph at the beginning:

# plot
episodes |> 
  mutate(wpm = wrds/length * 60) |> 
  # filter episodes without words (missing subtitle file)
  filter(!(is.na(wpm))) |> 
  # this episode is an extreme outlier - with Vietnamese subtitles...
  filter(ytdate != "2021-08-16") |> 
  select(wpm, ytdate, wrds, length, period) |> 
  # plot
  ggplot(aes(ytdate, wpm, color = wrds)) +
  geom_point(alpha = 1, size = 3) +
  labs(
    title = "Last Week Tonight's John Oliver: faster! ...angrier?",
    subtitle = "John's pace has increased from 150 to almost 180 words per minute",
    x = '',
    y = 'Words per minute',
    caption = "Each dot one episode; based on 298 auto-generated subtitle files."
  ) +
  scale_color_viridis_c(option = "viridis") +
  coord_cartesian(ylim = c(100, 200)) +
  scale_x_date(date_breaks = '1 year', date_labels = "%Y") +
  scale_y_continuous(breaks = seq(100, 200, by = 10)) +
  hrbrthemes::theme_ipsum_gs() +
  theme(
    legend.position = "top",
    legend.direction = "horizontal",
    panel.grid.minor = element_blank()
  ) +
  # the legend for the segment duration (and only that legend)
  guides(
    size = "none",
    color = guide_colorbar(
      title = "Segment length (in words)",
      title.position = "top",
      direction = "horizontal",
      barwidth = unit(12, "cm"),
      barheight = unit(0.2, "cm")
    )
  ) +
  # pre-covid, covid, post-covid episode indicator
  geom_vline(
    xintercept = as.Date(c("2020-03-10", "2021-09-01")),
    linetype = "dashed",
    color = "black",
    linewidth = 0.8
  ) +
  annotate(
    "label",
    x = as.Date("2020-12-01"),
    y = 150,
    label = "Covid",
    fill = "white",
    color = "grey45"
  )


ggsave("data/graphs/LWT_overall.png", width = 7.2, height = 5, dpi = 300)

Interactive plot

Or perhaps an experimental version of interactive plotly? Hover over the scatters for information on the episodes.

It’s experimental because, at the time of writing, plotly cannot properly display several features that were used above.

## Warning in geom2trace.default(dots[[1L]][[1L]], dots[[2L]][[1L]], dots[[3L]][[1L]]): geom_GeomLabel() has yet to be implemented in plotly.
##   If you'd like to see this geom implemented,
##   Please open an issue with your example code at
##   https://github.com/ropensci/plotly/issues

AI Use Statement

I use AI — in pretty much the same way I have (always) been using strackoverflow. I use it for inspiration on how to get code to do what I want and how to interpret error messages — in any coding language I use. I then test and amend relevant output for the purpose at hand.

I am proficiently functional bordering fluent in R, but I don’t eat and sleep its thousands of cool packages for the varied tasks I use it for alongside all my other jobs and interests, so there’s that.

As for writing, I write all my own stuff. There was a period where I used AI for proof-reading, as to not make me look like a fool when claiming I am a thorough person with attention to detail. But I discontinued that use of AI considering how much time I wasted undoing AI’s patronising suggestions of “style” and “conciseness” — and accepted that I’ll happily live with my own occasional typos in the same way I happily live with everybody else’s typos. They usually result from honest attempts at making a piece of text more legible — it’s not a reflection on anyone’s carelessness.


  1. If anyone’s rightfully and critically wondering: yes, starting the y-axis above 0 can be bad and deceptive practice. However, 100 wpm is the far more natural starting point for one’s pace of speaking than 0 — or at least I should hope so! In other words, the y-axis in this graph starts at “slow speech”.↩︎

  2. To assess if other people’s contributions affected the patterns, I manually checked about two dozen randomly chosen transcripts. Non-John data did in fact change the numbers, but in the other direction — John’s speaking rate would be even higher.↩︎