Assignment 3A: Global Baseline Estimate Recommender

Author

Noelle

Published

September 21, 2026

Introduction

This report implements a Global Baseline Estimate (GBE), a non-personalized recommender system, in R. Using a spreadsheet of movie ratings from 16 critics, it predicts how a critic would rate a movie they have not seen as global mean + movie bias + critic bias. The code is first validated against the spreadsheet’s known answer (2.28 for Param on Pitch Perfect 2), then reused on a small sample of books and movies from Week 2.

Data files are loaded from this GitHub repository by URL, so the report is reproducible: movie_ratings.csv (from the course spreadsheet) and week2_public_sample.csv.

Step 1: Load the data

We read the raw CSV from GitHub. Blank cells (and ?, N/A) become NA, never 0, because a blank means “not watched”, not “hated it”.

library(tidyverse)

url <- "https://raw.githubusercontent.com/NawelMe/DATA607-Fall-2026/main/week-03/movie_ratings.csv"
ratings_wide <- read_csv(url, na = c("", "NA", "?"), show_col_types = FALSE)

glimpse(ratings_wide)
Rows: 16
Columns: 7
$ Critic         <chr> "Burton", "Charley", "Dan", "Dieudonne", "Matt", "Mauri…
$ CaptainAmerica <dbl> NA, 4, NA, 5, 4, 4, 4, NA, 4, 4, 5, NA, 5, 4, 4, NA
$ Deadpool       <dbl> NA, 5, 5, 4, NA, NA, 4, NA, 4, 3, 5, NA, 5, NA, 5, NA
$ Frozen         <dbl> NA, 4, NA, NA, 2, 3, 4, NA, 1, 5, 5, 4, 5, NA, 3, 5
$ JungleBook     <dbl> 4, 3, NA, NA, NA, 3, 2, NA, NA, 5, 5, 5, 4, NA, 3, 5
$ PitchPerfect2  <dbl> NA, 2, NA, NA, 2, 4, 2, NA, NA, 2, NA, NA, 4, NA, 3, NA
$ StarWarsForce  <dbl> 4, 3, 5, 5, 5, NA, 4, 4, 5, 3, 4, 3, 5, 4, NA, NA
# Test: 61 real ratings summing to 240 (so the global mean is 240/61 = 3.934)
stopifnot(sum(!is.na(ratings_wide[, -1])) == 61)
stopifnot(sum(ratings_wide[, -1], na.rm = TRUE) == 240)

Step 2: Reshape from wide to long

One row per (critic, movie) pair makes per-critic and per-movie averages easy. We keep the NA rows on purpose: they are the cells we want to predict.

ratings_long <- ratings_wide |>
  pivot_longer(cols = -Critic, names_to = "movie", values_to = "rating")

stopifnot(nrow(ratings_long) == 96)                 # 16 critics x 6 movies
stopifnot(sum(!is.na(ratings_long$rating)) == 61)

Step 3: Global mean and biases

mu <- mean(ratings_long$rating, na.rm = TRUE)       # mean of ALL ratings

movie_bias <- ratings_long |>
  group_by(movie) |>
  summarise(movie_avg = mean(rating, na.rm = TRUE)) |>
  mutate(movie_bias = movie_avg - mu)

user_bias <- ratings_long |>
  group_by(Critic) |>
  summarise(user_avg = mean(rating, na.rm = TRUE)) |>
  mutate(user_bias = user_avg - mu)

mu
[1] 3.934426
knitr::kable(movie_bias, digits = 3)
movie movie_avg movie_bias
CaptainAmerica 4.273 0.338
Deadpool 4.444 0.510
Frozen 3.727 -0.207
JungleBook 3.900 -0.034
PitchPerfect2 2.714 -1.220
StarWarsForce 4.154 0.219
knitr::kable(user_bias, digits = 3)
Critic user_avg user_bias
Burton 4.000 0.066
Charley 3.500 -0.434
Dan 5.000 1.066
Dieudonne 4.667 0.732
Matt 3.250 -0.684
Mauricio 3.500 -0.434
Max 3.333 -0.601
Nathan 4.000 0.066
Param 3.500 -0.434
Parshu 3.667 -0.268
Prashanth 4.800 0.866
Shipra 4.000 0.066
Sreejaya 4.667 0.732
Steve 4.000 0.066
Vuthy 3.600 -0.334
Xingjia 5.000 1.066

Step 4: Predict one cell and validate

predict_gbe <- function(critic, movie_name) {
  b_movie <- movie_bias |> filter(movie == movie_name) |> pull(movie_bias)
  b_user  <- user_bias  |> filter(Critic == critic)    |> pull(user_bias)
  stopifnot(length(b_movie) == 1, length(b_user) == 1)
  mu + b_movie + b_user
}

param_pp2 <- predict_gbe("Param", "PitchPerfect2")
param_pp2
[1] 2.279859
# Validation against the spreadsheet answer (2.28)
stopifnot(near(param_pp2, 2.2798594847775178))

Param’s predicted rating for Pitch Perfect 2 is about 2.28, matching the spreadsheet.

Step 5: Predict every cell and recommend

predictions <- ratings_long |>
  left_join(movie_bias, by = "movie") |>
  left_join(user_bias,  by = "Critic") |>
  mutate(gbe        = mu + movie_bias + user_bias,
         gbe_capped = pmin(pmax(gbe, 1), 5))

recommendations <- predictions |>
  filter(is.na(rating)) |>
  group_by(Critic) |>
  slice_max(gbe, n = 1, with_ties = FALSE) |>
  ungroup() |>
  select(Critic, recommended_movie = movie, predicted = gbe_capped)

knitr::kable(recommendations, digits = 2)
Critic recommended_movie predicted
Burton Deadpool 4.51
Dan CaptainAmerica 5.00
Dieudonne JungleBook 4.63
Matt Deadpool 3.76
Mauricio Deadpool 4.01
Nathan Deadpool 4.51
Param JungleBook 3.47
Prashanth PitchPerfect2 3.58
Shipra Deadpool 4.51
Steve Deadpool 4.51
Vuthy StarWarsForce 3.82
Xingjia Deadpool 5.00

Some raw predictions exceed 5 (for example Xingjia on Deadpool). We rank on the raw score and display the value capped to the 1-5 scale.

Step 6: Reuse the model on the Week 2 data

We wrap the pipeline in one function so the same logic runs on any ratings table whose first column identifies the rater.

run_gbe <- function(ratings_wide) {
  id <- names(ratings_wide)[1]

  long <- ratings_wide |>
    rename(user = all_of(id)) |>
    pivot_longer(cols = -user, names_to = "item", values_to = "rating")

  mu <- mean(long$rating, na.rm = TRUE)

  item_bias <- long |> group_by(item) |>
    summarise(item_avg = mean(rating, na.rm = TRUE)) |>
    mutate(item_bias = item_avg - mu)

  user_bias <- long |> group_by(user) |>
    summarise(user_avg = mean(rating, na.rm = TRUE)) |>
    mutate(user_bias = user_avg - mu)

  long |>
    left_join(item_bias, by = "item") |>
    left_join(user_bias, by = "user") |>
    mutate(gbe = mu + item_bias + user_bias)
}

# Regression test: the refactored function must still give 2.28
movie_pred <- run_gbe(ratings_wide)
stopifnot(near(
  movie_pred |> filter(user == "Param", item == "PitchPerfect2") |> pull(gbe),
  2.2798594847775178
))

url2 <- "https://raw.githubusercontent.com/NawelMe/DATA607-Fall-2026/main/week-03/week2_public_sample.csv"
week2_wide <- read_csv(url2, na = c("", "NA", "N/A", "?"), show_col_types = FALSE)
week2_pred <- run_gbe(week2_wide)

knitr::kable(week2_pred |> filter(is.na(rating)) |> select(user, item, gbe), digits = 2)
user item gbe
Sample 4 Titanic 3.4
Sample 5 TheAlchemist 3.0

Data disclosure. The Week 2 ratings were not collected from people we surveyed. They are a representative sample constructed from public aggregate ratings (IMDb and Goodreads), on a 1-5 scale. Unavailable ratings are stored as missing values, not zero.

Conclusions and Findings

  • What did the model predict? TODO: e.g. what did it recommend for Param, and why is that different from Pitch Perfect 2?
  • What are the limits of GBE? TODO: think about the same bias applying to everyone, predictions above 5, and recommending “whatever is left” to someone who has seen almost everything.
  • What happened on the Week 2 data? TODO: why do the predictions there equal each sample’s own average?
  • How would you extend or verify this? TODO: e.g. hold out known ratings and measure error, or try a personalized method such as item-item collaborative filtering.

AI Use

Anthropic. (2026). Claude Sonnet 5 [Large language model]. https://claude.ai. Accessed September 21, 2026.

Claude was used mainly to proofread the report, improve the clarity of the explanations, and provide limited help with checking the code and results. The final code, analysis, testing, and interpretation were reviewed and completed by the authors.