Introduction

Every summer, Hollywood bets big on blockbusters. But which genres dominate? Do longer movies actually score better with audiences? Has “summer movie” quality improved over the decades or are we drowning in nostalgia?

This report analyzes the TidyTuesday Summer Movies dataset (Week 31, 2024), sourced from IMDb via the TidyTuesday project. It consists of two tables from the same release:

  • summer_movies — one row per movie, with title, year, runtime, average rating, and vote count.
  • summer_movie_genres — one row per movie-genre pair (movies can belong to multiple genres), used to examine what kinds of films dominate the season.

I merge these two tables to explore the intersection of genre, ratings, runtime, and era.


1. Setup & Data Loading

library(data.table)    # fast filtering and aggregation
library(ggplot2)       # visualization
library(RColorBrewer)  # ColorBrewer palettes (colorbrewer2.org)
library(scales)        # axis formatting helpers
library(ggrepel)       # non-overlapping text labels
base_url <- paste0(
  "https://raw.githubusercontent.com/rfordatascience/tidytuesday/",
  "main/data/2024/2024-07-30/"
)

movies <- fread(paste0(base_url, "summer_movies.csv"))
genres <- fread(paste0(base_url, "summer_movie_genres.csv"))

cat("summer_movies rows:", nrow(movies), "| cols:", ncol(movies), "\n")
## summer_movies rows: 905 | cols: 10
cat("summer_movie_genres rows:", nrow(genres), "| cols:", ncol(genres), "\n")
## summer_movie_genres rows: 1585 | cols: 2
head(movies, 3)
##       tconst title_type                   primary_title
##       <char>     <char>                          <char>
## 1: tt0011462      movie               Midsummer Madness
## 2: tt0026714      movie       A Midsummer Night's Dream
## 3: tt0033864      movie The Teachers on Summer Vacation
##               original_title  year runtime_minutes                 genres
##                       <char> <int>           <int>                 <char>
## 1:         Midsummer Madness  1920              60                  Drama
## 2: A Midsummer Night's Dream  1935             133 Comedy,Fantasy,Romance
## 3:  Magistrarna på sommarlov  1941              86                 Comedy
##                       simple_title average_rating num_votes
##                             <char>          <num>     <int>
## 1:               midsummer madness            7.4        19
## 2:        a midsummer nights dream            6.8      3931
## 3: the teachers on summer vacation            5.5        78
head(genres, 3)
##       tconst  genres
##       <char>  <char>
## 1: tt0011462   Drama
## 2: tt0026714  Comedy
## 3: tt0026714 Fantasy

2. Data Transformation with data.table

2.1 Cleaning

# Ensure correct types
movies[, year          := as.integer(year)]
movies[, runtime_minutes := as.numeric(runtime_minutes)]
movies[, average_rating  := as.numeric(average_rating)]
movies[, num_votes       := as.integer(num_votes)]

# Remove entries with no rating or fewer than 50 votes (too noisy)
movies_clean <- movies[!is.na(average_rating) & !is.na(year) & num_votes >= 50]

cat("Movies after cleaning:", nrow(movies_clean), "\n")
## Movies after cleaning: 611

2.2 Filtering rows with data.table

# Focus on movies from 1970 onward (enough data, modern era)
movies_modern <- movies_clean[year >= 1970]

cat("Modern era movies (1970+):", nrow(movies_modern), "\n")
## Modern era movies (1970+): 555

2.3 Derived columns

# Era bucketing and vote tier
movies_modern[, era := fcase(
  year < 1980, "1970s",
  year < 1990, "1980s",
  year < 2000, "1990s",
  year < 2010, "2000s",
  year < 2020, "2010s",
  default     = "2020s"
)]

movies_modern[, vote_tier := fcase(
  num_votes >= 10000, "Very popular (10k+)",
  num_votes >= 1000,  "Popular (1k–10k)",
  num_votes >= 100,   "Niche (100–1k)",
  default             = "Obscure (<100)"
)]

head(movies_modern[, .(primary_title, year, era, average_rating, vote_tier)], 5)
##         primary_title  year    era average_rating      vote_tier
##                <char> <int> <char>          <num>         <char>
## 1:     Dead of Summer  1970  1970s            6.3 Niche (100–1k)
## 2: Erika's Hot Summer  1971  1970s            4.1 Niche (100–1k)
## 3:        Summer Love  1970  1970s            4.7 Obscure (<100)
## 4: Summer in the City  1971  1970s            5.8 Niche (100–1k)
## 5:  In the Summertime  1971  1970s            7.6 Obscure (<100)
# Create movies_popular after era column exists
movies_popular <- movies_modern[num_votes >= 500]
cat("Popular movies (>=500 votes):", nrow(movies_popular), "\n")
## Popular movies (>=500 votes): 202

2.4 Aggregating with data.table

# Average rating and count by era
era_summary <- movies_modern[, .(
  n_movies    = .N,
  avg_rating  = round(mean(average_rating, na.rm = TRUE), 2),
  median_rating = round(median(average_rating, na.rm = TRUE), 2),
  avg_runtime = round(mean(runtime_minutes, na.rm = TRUE), 1)
), by = era][order(era)]

era_summary
##       era n_movies avg_rating median_rating avg_runtime
##    <char>    <int>      <num>         <num>       <num>
## 1:  1970s       51       5.95          6.00        92.8
## 2:  1980s       57       6.10          6.20        92.1
## 3:  1990s       58       6.20          6.40        92.0
## 4:  2000s      122       6.00          6.25        92.7
## 5:  2010s      184       6.10          6.20        95.0
## 6:  2020s       83       5.97          6.30        92.3

2.5 Merging the two TidyTuesday datasets

Both datasets share the tconst column (the IMDb title ID), making the join clean and natural.

# Rename the genres table's column to avoid clash with the table name itself
genre_tags <- copy(genres)
setnames(genre_tags, "genres", "genre")

# Left join: keep all movies, attach genre info
movies_with_genre <- merge(
  movies_modern,
  genre_tags,
  by    = "tconst",
  all.x = TRUE
)

# How many movies have at least one genre tag?
cat("Movies with genre info:", uniqueN(movies_with_genre[!is.na(genre), tconst]), "\n")
## Movies with genre info: 553
cat("Unique genres:", uniqueN(movies_with_genre$genre, na.rm = TRUE), "\n")
## Unique genres: 22
head(movies_with_genre[, .(primary_title, year, genre, average_rating)], 8)
##         primary_title  year    genre average_rating
##                <char> <int>   <char>          <num>
## 1:     Dead of Summer  1970    Drama            6.3
## 2:     Dead of Summer  1970  Mystery            6.3
## 3:     Dead of Summer  1970 Thriller            6.3
## 4: Erika's Hot Summer  1971    Drama            4.1
## 5: Erika's Hot Summer  1971  Romance            4.1
## 6:        Summer Love  1970   Action            4.7
## 7:        Summer Love  1970    Crime            4.7
## 8:        Summer Love  1970    Drama            4.7

2.6 Genre-level aggregation

# Keep the top genres by movie count
genre_summary <- movies_with_genre[
  !is.na(genre),
  .(
    n_movies    = .N,
    avg_rating  = round(mean(average_rating, na.rm = TRUE), 2),
    avg_runtime = round(mean(runtime_minutes, na.rm = TRUE), 1),
    avg_votes   = round(mean(num_votes, na.rm = TRUE))
  ),
  by = genre
][order(-n_movies)]

top_genres <- genre_summary[1:12]   # top 12 by count
top_genres
##           genre n_movies avg_rating avg_runtime avg_votes
##          <char>    <int>      <num>       <num>     <num>
##  1:       Drama      340       6.21        96.6      3498
##  2:      Comedy      192       5.89        94.4      5861
##  3:     Romance      129       6.11        96.5      8500
##  4:      Family       58       5.88        90.3       921
##  5: Documentary       38       7.39        78.7       588
##  6:      Horror       34       4.46        87.4      9514
##  7:    Thriller       29       5.18        92.9      5553
##  8:     Mystery       23       5.64        97.7     13958
##  9:   Adventure       23       6.08        96.6      9829
## 10:       Crime       20       5.88       109.4      4824
## 11:       Music       19       6.80        92.7      1274
## 12:      Action       16       5.83        85.6      2641

3. Visualizations

my_theme <- theme_minimal(base_size = 13) +
  theme(
    plot.title       = element_text(face = "bold", size = 15),
    plot.subtitle    = element_text(colour = "grey40", size = 11),
    plot.caption     = element_text(colour = "grey55", size = 9),
    axis.title       = element_text(face = "bold"),
    panel.grid.minor = element_blank(),
    legend.position  = "bottom"
  )

# ColorBrewer palette from colorbrewer2.org
era_colors   <- brewer.pal(6, "RdYlGn")   
genre_colors <- brewer.pal(8, "Set2")

Plot 1 — Bar Chart: Number of Summer Movies by Era

ggplot(era_summary, aes(x = era, y = n_movies, fill = era)) +
  geom_col(alpha = 0.9, width = 0.7) +
  geom_text(aes(label = n_movies), vjust = -0.4, fontface = "bold", size = 4) +
  scale_fill_manual(values = era_colors, guide = "none") +
  labs(
    title    = "How Many Summer Movies Were Made Each Decade?",
    subtitle = "Filtered to movies with ≥ 50 IMDb votes, released 1970–present",
    x        = "Era",
    y        = "Number of Movies",
    caption  = "Source: TidyTuesday 2024 W31 · IMDb via summer_movies.csv"
  ) +
  my_theme

Insight: Summer movie production exploded in the 2010s, reflecting both the rise of streaming platforms boosting release volume and IMDb’s wider adoption driving more votes per film. The 2020s count is lower simply because the decade is still young.


Plot 2 — Line + Point: Average Rating Over Time (Dual Geom Layer)

This plot uses two geom layers: geom_line + geom_point.

# Year-by-year rating trend (popular movies only for stability)
year_rating <- movies_popular[, .(
  avg_rating = mean(average_rating, na.rm = TRUE),
  n          = .N
), by = year][n >= 3][order(year)]

ggplot(year_rating, aes(x = year, y = avg_rating)) +
  geom_line(colour = brewer.pal(3, "Set1")[2], linewidth = 0.9, alpha = 0.8) +
  geom_point(aes(size = n), colour = brewer.pal(3, "Set1")[1], alpha = 0.7) +
  scale_size_continuous(name = "# of films", range = c(1, 6)) +
  scale_x_continuous(breaks = seq(1970, 2025, by = 5)) +
  labs(
    title    = "Average IMDb Rating of Summer Movies Over Time",
    subtitle = "Movies with ≥ 500 votes; point size = number of films that year",
    x        = "Release Year",
    y        = "Average IMDb Rating",
    caption  = "Source: TidyTuesday 2024 W31"
  ) +
  my_theme

Insight: Ratings were highly volatile in the early 1980s due to small sample sizes (tiny dots). The most striking feature is a sharp trough around 2011–2012, possibly reflecting the peak of low-quality sequel and franchise filler. Ratings recovered modestly afterward and stabilised in the 6–6.5 range through the 2020s.


Plot 3 — Boxplot: Rating Distribution by Era

ggplot(movies_modern, aes(x = era, y = average_rating, fill = era)) +
  geom_boxplot(outlier.alpha = 0.3, outlier.size = 1, alpha = 0.85) +
  scale_fill_manual(values = era_colors, guide = "none") +
  labs(
    title    = "IMDb Rating Distribution by Era",
    subtitle = "All movies with ≥ 50 votes; boxes show IQR, line = median",
    x        = "Era",
    y        = "Average IMDb Rating",
    caption  = "Source: TidyTuesday 2024 W31"
  ) +
  my_theme

Insight: The median rating is remarkably stable across eras (~6–6.5), but the spread widens in modern decades reflecting that streaming has enabled both cult classics and forgotten flops to accumulate votes at unprecedented scale.


Plot 4 — Horizontal Bar: Top 12 Genres by Movie Count

ggplot(top_genres, aes(x = reorder(genre, n_movies), y = n_movies,
                        fill = avg_rating)) +
  geom_col(alpha = 0.9) +
  geom_text(aes(label = n_movies), hjust = -0.2, size = 3.5) +
  scale_fill_distiller(palette = "RdYlGn", direction = 1,
                       name = "Avg Rating") +
  coord_flip() +
  expand_limits(y = max(top_genres$n_movies) * 1.12) +
  labs(
    title    = "Most Common Genres in Summer Movies",
    subtitle = "Bar colour = average IMDb rating of genre",
    x        = NULL,
    y        = "Number of Movies",
    caption  = "Source: TidyTuesday 2024 W31 · summer_movie_genres.csv merged with summer_movies.csv"
  ) +
  my_theme

Insight: Comedy and Drama dominate by raw count, but the colour gradient reveals that Documentary and Music tend to earn the highest average ratings, genres with more discerning, invested audiences. Drama and Comedy are abundant but rate more modestly.


Plot 5 — Scatter + Smooth: Runtime vs. Rating (Two Geom Layers)

movies_rt <- movies_popular[!is.na(runtime_minutes) & runtime_minutes < 240]

ggplot(movies_rt, aes(x = runtime_minutes, y = average_rating)) +
  geom_point(aes(colour = era), alpha = 0.4, size = 1.8) +
  geom_smooth(method = "loess", se = TRUE,
              colour = "grey15", fill = "grey85", linewidth = 1.2) +
  scale_colour_manual(values = era_colors, name = "Era") +
  labs(
    title    = "Does a Longer Movie Score Better?",
    subtitle = "Popular summer films (≥ 500 votes); LOESS smoother with 95% CI",
    x        = "Runtime (minutes)",
    y        = "Average IMDb Rating",
    caption  = "Source: TidyTuesday 2024 W31"
  ) +
  my_theme

Insight: There’s a gentle positive association, movies under 80 minutes tend to rate lower (quick cash-in productions), while films in the 90–130 minute sweet spot peak in quality. Very long films (>150 min) show another slight uptick, likely because only ambitious prestige films run that long and survive to accumulate votes.


Plot 6 — Scatter + Labels: Genre Rating vs. Popularity

Three geom layers: geom_point, geom_smooth, geom_text_repel.

# Use all genres with at least 20 films for stability
genre_stable <- genre_summary[n_movies >= 20]

ggplot(genre_stable, aes(x = avg_votes, y = avg_rating)) +
  geom_point(aes(size = n_movies, colour = avg_rating), alpha = 0.8) +
  geom_smooth(method = "lm", se = FALSE,
              colour = "grey30", linetype = "dashed", linewidth = 0.8) +
  geom_text_repel(aes(label = genre), size = 3.2, max.overlaps = 20) +
  scale_colour_distiller(palette = "RdYlGn", direction = 1, guide = "none") +
  scale_size_continuous(name = "# of Movies", range = c(2, 10)) +
  scale_x_log10(labels = label_comma()) +
  labs(
    title    = "Genre Rating vs. Audience Popularity",
    subtitle = "x-axis log scale; size = number of movies in genre; colour = avg rating",
    x        = "Average Number of IMDb Votes (log scale)",
    y        = "Average IMDb Rating",
    caption  = "Source: TidyTuesday 2024 W31"
  ) +
  my_theme

Insight: Horror, adventure, and Mystery command the most votes but sit at middling or lower ratings. Documentary occupies the “loved but niche” quadrant, fewer votes but enthusiastic audiences.


Plot 7 — Density: Vote Count Distribution by Era

movies_density <- movies_modern[num_votes < 100000]

ggplot(movies_density, aes(x = num_votes, fill = era, colour = era)) +
  geom_density(alpha = 0.3, linewidth = 0.8) +
  scale_x_log10(labels = label_comma()) +
  scale_fill_manual(values   = era_colors, name = "Era") +
  scale_colour_manual(values = era_colors, guide = "none") +
  labs(
    title    = "Distribution of IMDb Vote Counts by Era",
    subtitle = "Log scale — how much attention did each era's films receive?",
    x        = "Number of IMDb Votes (log scale)",
    y        = "Density",
    caption  = "Source: TidyTuesday 2024 W31"
  ) +
  my_theme

Insight: Older films (1970s–1980s) cluster at low vote counts, while 2010s and 2020s films spread much further right reflecting both bigger audiences and IMDb’s growth as a platform.


Plot 8 — Faceted Boxplot: Rating by Genre × Era

# Focus on top 6 genres for readability
top6 <- genre_summary[1:6, genre]

movies_genre_era <- movies_with_genre[
  genre %in% top6 & !is.na(era) & !is.na(average_rating)
]

ggplot(movies_genre_era, aes(x = era, y = average_rating, fill = era)) +
  geom_boxplot(alpha = 0.8, outlier.size = 0.8, outlier.alpha = 0.3) +
  scale_fill_manual(values = era_colors, guide = "none") +
  facet_wrap(~ genre, ncol = 3) +
  labs(
    title    = "Rating Distribution by Genre and Era",
    subtitle = "Top 6 genres · has audience taste shifted over time within genres?",
    x        = "Era",
    y        = "Average IMDb Rating",
    caption  = "Source: TidyTuesday 2024 W31 (merged datasets)"
  ) +
  my_theme +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1, size = 8),
    strip.text  = element_text(face = "bold")
  )

Insight: Comedy shows the steepest rating decline over time, consistent with the genre becoming heavily commercialised in the 2000s–2010s. Drama has remained the most stable, while Horror has actually improved.


4. Top 20 Highest-Rated Summer Movies

top20 <- movies_popular[order(-average_rating)][
  1:20,
  .(Rank = .I, Title = primary_title, Year = year, Rating = average_rating,
    Votes = format(num_votes, big.mark = ","), Runtime = runtime_minutes)
]
knitr::kable(top20, caption = "Top 20 Summer Movies by IMDb Rating (≥500 votes)")
Top 20 Summer Movies by IMDb Rating (≥500 votes)
Rank Title Year Rating Votes Runtime
1 A Limousine the Colour of Midsummer’s Eve 1981 8.6 1,326 84
2 The Elusive Summer of ’68 1984 8.5 6,140 91
3 A Midsummer Night’s Dream 2019 8.4 640 180
4 A Brighter Summer Day 1991 8.2 13,034 237
5 Spring, Summer, Fall, Winter… and Spring 2003 8.0 87,290 103
6 Summer of Soul (…Or, When the Revolution Could Not Be Televised) 2021 8.0 14,058 118
7 The Cold Summer of 1953 1988 7.8 2,595 101
8 Summer Snow 1995 7.8 841 101
9 Summer in Bethlehem 1998 7.8 1,625 143
10 Summer 1976 7.7 793 81
11 500 Days of Summer 2009 7.7 564,894 95
12 A Summer at Grandpa’s 1984 7.6 1,788 93
13 The Endless Summer 2 1994 7.6 2,239 109
14 A Summer’s Tale 1996 7.6 10,251 113
15 All Summer in a Day 1982 7.6 504 25
16 Little Forest: Summer/Autumn 2014 7.6 3,526 111
17 Summer Survivors 2018 7.5 1,642 91
18 Summer Days with Coo 2007 7.4 2,181 138
19 Summer Wars 2009 7.4 31,999 114
20 Bicycles Are for the Summer 1984 7.3 891 103

5. Summary & Conclusions

Question Finding
Most common genre? Comedy and Music dominate by count
Highest-rated genre? Documentary and Animation lead in average rating
Does runtime matter? Sweet spot is 90–130 min; very short films underperform
Are ratings declining? Median stable, but modern spread is wider — more extremes
Which era was best? 1970s–1980s show slightly higher medians, but survivorship bias is strong
Most voted films? 2010s and 2020s films far outpace older eras in vote counts

Caveats: IMDb data reflects who votes, not who watches. Older films are rated by nostalgic fans; newer ones get mass-market audiences including people who vote 1-star to protest. The “summer” definition here is IMDb titles with “summer” in the name, not theatrical release windows.

Next steps could include NLP on movie titles to find naming trends, or merging with box office data to compare critical vs. commercial success.


Datasets: TidyTuesday 2024 Week 31summer_movies.csv and summer_movie_genres.csv
Analysis in R with data.table, ggplot2, RColorBrewer, ggrepel, scales.