Week 3A Assignment: Code Base

Author

Jocelyn Slater

Introduction

Using the survey data collected in Week 2, I have implemented a global baseline estimate (GBE) recommender system as below: \[\hat{r}_{ui} = \mu + b_u + b_i\] where

\(\hat{r}_{ui}\) = predicted rating for movie \(i\) and user \(u\)

\(\mu\) = mean rating for movie \(i\) across all users

\(b_u\) = user bias (\(\mu_u - \mu\) how this user rated movies compared to the average)

\(b_i\) = movie bias (\(\mu_i - \mu\) how this movie is rated compared to the average)

Data Import

Data is imported from .csv from the SQL database. See Assignment 2A for more details.

rating_raw <- read_csv("https://raw.githubusercontent.com/jocslater-code/DATA607/refs/heads/main/Week2/movie_rating_data.csv", show_col_types = FALSE)
glimpse(rating_raw)
Rows: 20
Columns: 3
$ user_name   <chr> "Heidi Smith", "Heidi Smith", "Michael Pento", "Michael Pe…
$ movie_title <chr> "Toy Story 5", "Goat", "Toy Story 5", "Goat", "Project Hai…
$ rating      <dbl> 4.3, 4.5, 4.3, 4.0, 4.8, 4.5, 3.9, 3.7, 4.2, 3.5, 4.2, 4.8…

Data Tidying

I create another dataframe that includes NaN ratings for unseen movies.

full_ratings_df <- rating_raw %>%
  complete(user_name, movie_title, fill = list(rating = NaN))

Data Presentation

Use great table and conditional formatting to display ratings before recommender.

# Reshape data into 5 rows (users) and 6 movie columns
ratings_matrix <- full_ratings_df |>
  pivot_wider(
    names_from = movie_title,
    values_from = rating
  )

# 2. Initialize base table
tbl <- ratings_matrix |>
  gt(rowname_col = "user_name") |>
  sub_missing(missing_text = "NaN") #

# 3. Apply background fill to missing cells per column
movie_cols <- setdiff(names(ratings_matrix), "user_name")

for (col in movie_cols) {
  # Get row indices where the current column has NA/NaN
  na_rows <- which(is.na(ratings_matrix[[col]]))
  
  if (length(na_rows) > 0) {
    tbl <- tbl |>
      tab_style(
        style = cell_fill(color = "#f8d7da"), # Soft red fill
        locations = cells_body(
          columns = all_of(col),
          rows = na_rows
        )
      )
  }
}

tbl
Goat Project Hail Mary Spiderman Brand New Day Superman The Odyssey Toy Story 5
Cody Slater 3.5 3.7 NaN 4.2 4.5 3.9
Heidi Smith 4.5 NaN NaN NaN NaN 4.3
Jacob OBrien 3.3 4.3 4.6 4.5 5.0 4.0
Jocelyn Slater 4.0 4.8 NaN 4.7 NaN 4.2
Michael Pento 4.0 4.8 NaN NaN NaN 4.3

Global Baseline Estimate

To calcuate the GBE, we must first calculate the metrics of each user avg rating, global average movie rating, and each movie average

# Convert to matrix 
mat <- as.matrix(ratings_matrix[, -1])
rownames(mat) <- ratings_matrix$user_name

# Compute necessary GBE metrics
mu <- mean(mat, na.rm = TRUE)                            # Overall mean movie rating
movie_bias <- colMeans(mat, na.rm = TRUE) - mu           # Movie relative rating (b_i)
user_bias  <- rowMeans(mat, na.rm = TRUE) - mu           # User relative rating (b_u)

# Create imputed matrix
imputed_mat <- mat

# If the cell is NaN (user has not seen the movie), perform GBE calculation and fill in the value
for (user in rownames(mat)) {
  for (movie in colnames(mat)) {
    if (is.na(mat[user, movie])) {
      est <- mu + movie_bias[movie] + user_bias[user]
      imputed_mat[user, movie] <- round(est, 2)
    }
  }
}

# Convert back to data frame, add back in user names
imputed_ratings_df <- as.data.frame(imputed_mat) |>
  tibble::rownames_to_column(var = "user_name")
# Display table with imputed values

# 1. Start with the imputed data frame
tbl_imputed <- imputed_ratings_df |>
  gt(rowname_col = "user_name")

# 2. Extract column names (excluding user_name)
movie_cols <- setdiff(names(imputed_ratings_df), "user_name")

# 3. Loop over each column and apply background fill ONLY to originally missing rows
for (col in movie_cols) {
  # Identify row indices that were NA/NaN in the original matrix
  na_rows <- which(is.na(ratings_matrix[[col]]))
  
  if (length(na_rows) > 0) {
    tbl_imputed <- tbl_imputed |>
      tab_style(
        style = cell_fill(color = "#e0f2fe"), 
        locations = cells_body(
          columns = all_of(col),
          rows = na_rows
        )
      )
  }
}

tbl_imputed
Goat Project Hail Mary Spiderman Brand New Day Superman The Odyssey Toy Story 5
Cody Slater 3.5 3.70 4.30 4.20 4.50 3.9
Heidi Smith 4.5 4.55 4.74 4.61 4.90 4.3
Jacob OBrien 3.3 4.30 4.60 4.50 5.00 4.0
Jocelyn Slater 4.0 4.80 4.77 4.70 4.92 4.2
Michael Pento 4.0 4.80 4.71 4.58 4.86 4.3

Summary and Next Steps

In order to make a recommendation, I need to decide which estimated rating will be the cutoff. I have decided 4.5 to be that cutoff. For any movie that has a global baseline estimate of below 4.5, I will not recommend, but at or above 4.5 I will recommend for that user. Therefore, I would recommend all of the listed movies to every user in my system, with the exception that Cody Slater would not be recommended Spiderman Brand New Day.

Next steps could include surveying many more people and deploying this algorithm on a much larger database. I could also build more of an interface that displayed recommendations automatically after a user filled out the survery, along the lines of “if you liked this, you might also like this”. I would also be interested in playing around with the ratings cutoff for a recommendation to see if 4.5 is the best value or if users would be happier with a lower or higher threshold.

LLM Model Citations

Google DeepMind. (2026). Gemini 3.6 Flash [Large language model]. https://gemini.google.com. Accessed Sept 16, 2026. Transcript available in Github as 3AGeminiTranscript.pdf