Exploring Gymflation with AI

sebg1 pts0 comments

Exploring Gymflation with AI – Chris Bowdon

You might be familiar with super hero inflation? Over time, super hero physiques on screen have become increasingly exaggerated. Batman’s progression from Adam West to Ben Affleck is a great example of this.

Selected Batman portrayals, from oldest to more recent. LEGO Batman is not included, but is reported to have a 9-pack.

Gymflation is much the same idea. As gym culture has skyrocketed, it seems like so too have people’s feats of strength - as recorded on social media. Going on YouTube or Instagram one gets the feeling that a 200kg deadlift is really rather average nowadays.

But is it really true? Or are we just feeling the effects of the Algorithm, pushing extreme examples in our blue-lit faces?

What follows is a little project to find out. We will:

Scrape the YouTube API for 10 years of deadlift personal record (PR) videos with httr2

Use structured responses to extract the lifted weight with AI

Evaluate the quality of our extraction and improve it with ICL

Use a multimodal model to judge the lifter’s gender

Make extensive use of caching so errors don’t set us back

Plot our results with good old ggplot2

Come away with a sense of tremendous satisfaction

My secret ulterior motive is that this can serve as a guide to using APIs and LLMs in an R project, demonstrating some techniques and practices that have been helpful to me. As usual the full code is below the folds.

Code<br>library(tidyverse)<br>library(knitr)<br>library(httr2)<br>library(ellmer)<br>#library(memoise)<br>#library(cachem)<br>library(caret)

Scraping YouTube data with httr2 and a disk cache

The YouTube API is relatively straightforward to use with httr2 and speaks for itself. Because the requests we need to make are GET requests, we can use the handy req_cache function to avoid burning quota on repeat requests.

Code<br>YOUTUBE_API_KEY Sys.getenv("YOUTUBE_API_KEY")

search_videos function(<br>query,<br>published_after = "2026-01-01T00:00:00Z",<br>published_before = "2026-01-02T00:00:00Z"<br>) {<br>req request("https://youtube.googleapis.com/youtube/v3/search") |><br>req_cache(".cache/httr2") |> # happily this endpoint is a GET so we can cache<br>req_url_query(<br>part = "snippet",<br>q = query,<br>type = "video",<br>maxResults = 50,<br>duration = "short",<br>region = "UK",<br>published_after = published_after,<br>published_before = published_before,<br>key = YOUTUBE_API_KEY<br>) |><br>req_headers(Accept = "application/json") |><br>req_timeout(30)

resp req_perform(req)<br>if (resp_is_error(resp)) {<br>stop("YouTube API request failed: HTTP ", resp_status(resp))

resp_body_json(resp, simplifyVector = TRUE)

query "\"deadlift PR\" -anatoly"

The -anatoly term is to get rid of the wildly popular but irrelevant Anatoly videos. (He’s an elite powerlifter who dresses as a cleaner to prank bodybuilders. Apparently the Internet can’t get enough of this.)

The YouTube free API is quite restrictive so we are forced to compromise. We sample only the first day of each month, and only the first page of results (up to 50). If anyone is willing to fund my stupid research I will gladly resolve these issues.

Code<br>rds.file "data/youtube_df.rds"<br>df {<br>if (file.exists(rds.file)) {<br>readRDS(rds.file)<br>} else {<br>dates seq(as.Date("2015-01-01"), as.Date("2025-12-01"), by = "month")

afters dates[-length(dates)]<br>befores dates[-1]

query_results purrr::map2(<br>afters,<br>befores,<br>.f = function(a, b) {<br>search_videos(<br>query,<br>published_after = sprintf("%sT00:00:00Z", a),<br>published_before = sprintf("%sT00:00:00Z", b)<br>},<br>.progress = TRUE

df query_results |><br>purrr::map(\(r) unnest(r$items, snippet)) |><br>bind_rows() |><br>mutate(publish.date = as.Date(publishedAt)) |><br>unnest(c(id, thumbnails), names_sep = ".") |><br>unnest(starts_with("thumb"), names_sep = ".") |><br>unique()<br>saveRDS(df, file = rds.file, compress = FALSE)<br>df

Somewhow when writing this up I managed to nuke the cache. Oof. But it’s not my first rodeo, I had an RDS file of the results as a fallback. Let’s check out the daily result counts to ensure we haven’t messed up so far.

Code<br>ggplot(df) +<br>aes(x = publish.date) +<br>geom_histogram() +<br>theme_minimal() +<br>labs(x = "Publish date", y = "Count of videos")

Figure 1: Distribution of videos by date. The uniformity indicates that there are usually other pages that we haven’t fetched, though it’s not guaranteed that these are relevant results.

Evaluating extraction accuracy

We will need to check accuracy, otherwise we’ve not delegated a task to AI we’ve delegated it to a wish and a prayer. I’m dumping a small sample into a CSV to be labelled manually, which I’d always recommend in the first instance so that you get to know your own data.

If you need a larger sample then it could be delegated to a larger LLM than the one you are using (which may or may not equal human accuracy on this task… you may need to evaluate your evaluator…) Another approach is to (vibe) code your own labelling app so that a human can label as efficiently as possible. You can buy a labelling app like Prodigy, but in 2026 it’s...

youtube library file date code httr2

Related Articles