DATA 607 - Assignment 3A: Global Baseline Estimate

Bikash Bhowmik —- 19 Sep 2026

Objective

The goal of this assignment is to build a non-personalized movie recommendation system in R using the Global Baseline Estimate method. The system will use the movie ratings dataset created in the previous assignment and stored in PostgreSQL. The purpose is to use the existing ratings data to calculate baseline ratings for movies and generate recommendations without making recommendations based on individual user preferences.

Data Description

The dataset contains movie ratings collected from a small survey. The data is organized into three related tables in PostgreSQL:

• users – contains the user ID and user name.
• movies – contains the movie ID, movie title, and release year.
• ratings – contains the rating ID, user ID, movie ID, and rating given by the user.

Participants rated the movies on a scale from 1 to 5. If a user has not watched a particular movie, there is no rating for that movie. The PostgreSQL database was used to store and manage the data.

For reproducibility, the GitHub repository will include the SQL scripts needed to create the tables and insert the survey data into the PostgreSQL database.

Database Design

I have created below tables in Assignment 2A

• users – participant information
• movies – movie titles and release years
• ratings – ratings connecting users and movies

Primary keys and foreign keys will be used to maintain relationships between the tables. Ratings will also be limited to values from 1 to 5, with a unique constraint to prevent duplicate ratings for the same user and movie.

Methodology

e recommendation system will use the Global Baseline Estimate method to predict movie ratings. I will follow the calculation steps provided in the spreadsheet and apply any regularization parameters included in the instructions.

The implementation will follow these steps:

  1. Load the movie ratings data from PostgreSQL into R.
  2. Calculate the global average rating (μ) across all ratings.
  3. Calculate the user bias, which measures how much each user’s average rating differs from the global average.
  4. Calculate the movie bias, which measures how much each movie’s average rating differs from the global average after accounting for user bias.
  5. Use the global average, user bias, and movie bias to calculate the predicted rating for each user-movie combination.
  6. Compare the predicted ratings with the actual ratings to evaluate how well the model performs.

The basic prediction formula is:

Predicted Rating = Global Average + User Bias + Movie Bias

This approach provides a non-personalized baseline recommendation model that can be used to estimate ratings for movies that a user has not yet rated.

`

Recommendation Process

The recommendation system will generate predicted ratings for movies that a selected user has not yet rated. These predictions will be based on the Global Baseline Estimate model, using the global average rating along with the user and movie biases.

After calculating the predicted ratings, the system will rank the unrated movies from highest to lowest predicted rating. The movies with the highest predicted ratings will then be selected as the recommendations for the user.

Possible Challenges

There are several challenges that may come up while developing the movie recommendation system. Since the ratings are collected from a small survey, the dataset may have a relatively small number of users and ratings. This could make it more difficult for the model to identify reliable patterns in user and movie preferences.

Another challenge is sparse ratings. Users may have rated only some of the available movies, leaving many user and movie combinations without ratings. The system will need to handle these missing ratings carefully when generating predictions for movies that a user has not seen or rated.

A third challenge is making sure that the data can be loaded consistently from PostgreSQL into R. The database connection, table structure, data types, and joins between the users, movies, and ratings tables must be handled correctly. Any problems during the data loading or data cleaning process could affect the model results.

Finally, I will need to make sure the analysis is reproducible. The SQL scripts, R code, database structure, and calculation steps should be clearly documented so that the same results can be obtained when the project is run again.

Loading Libraries

library(DBI)
library(RPostgres)
library(dplyr)
library(tidyr)
library(tidyverse)
library(ggplot2)

Connect to PosgreSQL

con <- dbConnect(
  RPostgres::Postgres(),
  host = "localhost",
  port = 5432,
  dbname = "postgres",
  user = "postgres",
  password = "admin"
)

dbListTables(con) 
[1] "movies"  "ratings" "users"  

Load table in R

users_df  <- dbGetQuery(con, "SELECT * FROM users;")
movies_df <- dbGetQuery(con, "SELECT * FROM movies;")
ratings_df <- dbGetQuery(con, "SELECT * FROM ratings;")

Basic sanity checks

glimpse(users_df)
Rows: 5
Columns: 2
$ user_id <int> 1, 2, 3, 4, 5
$ name    <chr> "Prakash", "Das", "Anush", "Arpi", "Mihir"
glimpse(movies_df)
Rows: 6
Columns: 3
$ movie_id     <int> 1, 2, 3, 4, 5, 6
$ title        <chr> "Dune Part Two", "The Holdovers", "Barbie", "The Batman",…
$ release_year <int> 2024, 2023, 2023, 2022, 2025, 2022
glimpse(ratings_df)
Rows: 17
Columns: 4
$ rating_id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17
$ user_id   <int> 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5
$ movie_id  <int> 1, 2, 5, 2, 3, 6, 1, 4, 5, 6, 2, 3, 5, 1, 4, 2, 6
$ rating    <int> 5, 5, 4, 4, 3, 5, 4, 3, 4, 4, 5, 4, 5, 3, 4, 5, 5

Prepare a clean ratings dataset

ratings_clean <- ratings_df %>%
  select(user_id, movie_id, rating) %>%
  mutate(rating = as.numeric(rating)) %>%
  filter(!is.na(rating))

Join in names/titles for readability

ratings_joined <- ratings_clean %>%
  left_join(users_df,  by = "user_id") %>%
  left_join(movies_df, by = "movie_id") %>%
  select(user_id, name, movie_id, title, release_year, rating) %>%
  arrange(name, title)

head(ratings_joined, 10)
   user_id  name movie_id                   title release_year rating
1        3 Anush        6 Avatar The Way of Water         2022      4
2        3 Anush        1           Dune Part Two         2024      4
3        3 Anush        5           Lilo & Stitch         2025      4
4        3 Anush        4              The Batman         2022      3
5        4  Arpi        3                  Barbie         2023      4
6        4  Arpi        5           Lilo & Stitch         2025      5
7        4  Arpi        2           The Holdovers         2023      5
8        2   Das        6 Avatar The Way of Water         2022      5
9        2   Das        3                  Barbie         2023      3
10       2   Das        2           The Holdovers         2023      4

This step joins the ratings data with the users and movies tables to make the results easier to understand. Instead of using numeric IDs, we can see the user names and movie titles along with their ratings. This makes it easier to review and interpret the predicted ratings in the following steps.

Global Baseline Estimate pieces

r_hat(u,i) = μ + b_u + b_i

Global mean (μ)

mu <- mean(ratings_clean$rating)
mu
[1] 4.235294

Here, the global average rating (μ) is computed across all observed ratings in the dataset. This value represents the overall baseline rating level and serves as the starting point for the Global Baseline Estimate prediction model.

Regularization

Stable baseline for small datasets, keep lambda > 0 (e.g., 5 or 10) Simplest baseline, set lambda <- 0

lambda <- 5

A regularization parameter (lambda) is introduced to stabilize bias estimates when users or movies have only a small number of ratings. This prevents extreme bias values caused by limited observations and produces more reliable predictions in small datasets.

User bias (b_u): regularized deviation from global mean

user_bias <- ratings_clean %>%
  group_by(user_id) %>%
  summarise(
    n_user = n(),
    user_mean = mean(rating),
    b_u = (sum(rating - mu)) / (n_user + lambda),
    .groups = "drop"
  )

In this step, user specific bias values are calculated to capture how individual users tend to rate movies relative to the global average. Some users consistently give higher ratings, while others rate more strictly. The regularized formula adjusts these deviations to avoid overfitting.

Movie bias (b_i): regularized deviation from global mean

movie_bias <- ratings_clean %>%
  group_by(movie_id) %>%
  summarise(
    n_movie = n(),
    movie_mean = mean(rating),
    b_i = (sum(rating - mu)) / (n_movie + lambda),
    .groups = "drop"
  )

head(user_bias)
# A tibble: 5 × 4
  user_id n_user user_mean      b_u
    <int>  <int>     <dbl>    <dbl>
1       1      3      4.67  0.162  
2       2      3      4    -0.0882 
3       3      4      3.75 -0.216  
4       4      3      4.67  0.162  
5       5      4      4.25  0.00654
head(movie_bias)
# A tibble: 6 × 4
  movie_id n_movie movie_mean     b_i
     <int>   <int>      <dbl>   <dbl>
1        1       3       4    -0.0882
2        2       4       4.75  0.229 
3        3       2       3.5  -0.210 
4        4       2       3.5  -0.210 
5        5       3       4.33  0.0368
6        6       3       4.67  0.162 

Here, movie-specific bias values are computed to reflect how certain movies tend to be rated relative to the global mean. Popular or well-received movies may have positive bias values, while lower-rated movies may have negative bias values.

Predicted ratings for observed pairs (for sanity checking

predicted_observed <- ratings_clean %>%
  left_join(user_bias,  by = "user_id") %>%
  left_join(movie_bias, by = "movie_id") %>%
  mutate(
    predicted_rating = mu + b_u + b_i,
    error = rating - predicted_rating
  ) %>%
  left_join(users_df,  by = "user_id") %>%
  left_join(movies_df, by = "movie_id") %>%
  select(name, title, rating, predicted_rating, error) %>%
  arrange(desc(abs(error)))

head(predicted_observed, 15)
      name                   title rating predicted_rating       error
1    Mihir           Dune Part Two      3         4.153595 -1.15359477
2      Das                  Barbie      3         3.936975 -0.93697479
3    Anush              The Batman      3         3.809524 -0.80952381
4  Prakash           Dune Part Two      5         4.308824  0.69117647
5      Das Avatar The Way of Water      5         4.308824  0.69117647
6    Mihir Avatar The Way of Water      5         4.403595  0.59640523
7     Arpi           Lilo & Stitch      5         4.433824  0.56617647
8    Mihir           The Holdovers      5         4.470588  0.52941176
9  Prakash           Lilo & Stitch      4         4.433824 -0.43382353
10     Das           The Holdovers      4         4.375817 -0.37581699
11 Prakash           The Holdovers      5         4.625817  0.37418301
12    Arpi           The Holdovers      5         4.625817  0.37418301
13    Arpi                  Barbie      4         4.186975 -0.18697479
14   Anush Avatar The Way of Water      4         4.181373 -0.18137255
15   Anush           Dune Part Two      4         3.931373  0.06862745

This section uses the global mean, user bias, and movie bias to calculate predicted ratings using the Global Baseline Estimate model. By comparing the predicted ratings with the actual ratings, we can see how well the model performs and identify any differences or prediction errors.

Quick metric: RMSE on observed ratings

rmse <- sqrt(mean((predicted_observed$error)^2, na.rm = TRUE))
rmse
[1] 0.5668401

The Root Mean Squared Error (RMSE) is used to measure how accurately the baseline model predicts ratings. It gives an overall measure of the difference between the predicted and actual ratings, which helps us evaluate how well the model performs.

Make recommendations all users (Top-N unseen movies)

top_n <- 5

This section generates movie recommendations for every user in the dataset. For each user, we predict ratings for movies they have not yet rated using the Global Baseline Estimate model, then return the top-N highest predicted movies per user.

Create all user–movie combinations and attach existing ratings

all_pairs <- users_df %>%
  select(user_id, name) %>%
  tidyr::crossing(movies_df %>% select(movie_id, title, release_year)) %>%
  left_join(ratings_clean, by = c("user_id", "movie_id"))

Get the user’s bias (if missing, assume 0) and compute predicted ratings

all_scored <- all_pairs %>%
  left_join(user_bias, by = "user_id") %>%
  left_join(movie_bias, by = "movie_id") %>%
  mutate(
    b_u = ifelse(is.na(b_u), 0, b_u),
    b_i = ifelse(is.na(b_i), 0, b_i),
    predicted_rating = mu + b_u + b_i
  )

recommendations_all <- all_scored %>%
  filter(is.na(rating)) %>%   # unseen movies only
  group_by(user_id, name) %>%
  arrange(desc(predicted_rating), .by_group = TRUE) %>%
  slice_head(n = top_n) %>%
  ungroup() %>%
  select(name, title, release_year, predicted_rating) %>%
  mutate(
    predicted_rating = round(predicted_rating, 2)
  )

Recommendations for all users

recommendations_all
# A tibble: 13 × 4
   name    title                   release_year predicted_rating
   <chr>   <chr>                          <int>            <dbl>
 1 Prakash Avatar The Way of Water         2022             4.56
 2 Prakash Barbie                          2023             4.19
 3 Prakash The Batman                      2022             4.19
 4 Das     Lilo & Stitch                   2025             4.18
 5 Das     Dune Part Two                   2024             4.06
 6 Das     The Batman                      2022             3.94
 7 Anush   The Holdovers                   2023             4.25
 8 Anush   Barbie                          2023             3.81
 9 Arpi    Avatar The Way of Water         2022             4.56
10 Arpi    Dune Part Two                   2024             4.31
11 Arpi    The Batman                      2022             4.19
12 Mihir   Lilo & Stitch                   2025             4.28
13 Mihir   Barbie                          2023             4.03

The table above shows the top movie recommendations for each user based on the Global Baseline Estimate model. For each user, the model predicts ratings for movies that they have not rated yet.

The movies are then ranked based on their predicted ratings, and the highest-rated movies are selected as the top recommendations for each user.

These results show that even a simple baseline recommender can provide useful movie recommendations by combining the overall average rating with user-specific and movie-specific rating patterns. Although the dataset is small, the model is still able to generate reasonable recommendations that reflect both movie popularity and individual user preferences.

Extension for some visualization for better analysis

1. Movie bias visualization

movie_bias %>%
  left_join(movies_df, by = "movie_id") %>%
  ggplot(aes(x = reorder(title, b_i), y = b_i)) +
  geom_col(fill = "lightblue") +
  geom_hline(yintercept = 0, linewidth = 0.7) +
  coord_flip() +
  labs(
    title = "Movie Bias (b_i) from Global Baseline Estimate",
    subtitle = "Positive values indicate higher than average movie ratings",
    x = "Movie",
    y = "Movie Bias (b_i)"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold"),
    axis.text.y = element_text(size = 9)
  )

This plot displays the movie-specific bias values from the Global Baseline Estimate model. Movies with positive bias values generally receive ratings higher than the overall average, while movies with negative bias values tend to receive lower ratings. The visualization shows how the recommender system adjusts its predictions based on differences in movie ratings.

2. User bias visualization

user_bias %>%
  left_join(users_df, by = "user_id") %>%
  ggplot(aes(x = reorder(name, b_u), y = b_u)) +
  geom_col(fill = "lightblue") +
  geom_hline(yintercept = 0, linewidth = 0.7) +
  coord_flip() +
  labs(
    title = "User Rating Bias",
    subtitle = "Positive values indicate higher than average rating tendencies",
    x = "User",
    y = "User Bias (b_u)"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold"),
    axis.text.y = element_text(size = 9)
  )

This chart shows the user bias values calculated by the Global Baseline model. User bias shows how each users rating behavior differs from the overall average. Positive values mean the user generally gives higher ratings, while negative values mean the user tends to give lower ratings. This helps show how the model adjusts predictions based on each users rating behavior.

3. Actual vs predicted ratings

ggplot(predicted_observed, aes(x = rating, y = predicted_rating)) +
  geom_point(alpha = 0.6) +
  geom_abline(
    slope = 1,
    intercept = 0,
    linetype = "dashed",
    linewidth = 0.8
  ) +
  labs(
    title = "Actual vs Predicted Ratings",
    subtitle = "Points closer to the dashed line indicate more accurate predictions",
    x = "Actual Rating",
    y = "Predicted Rating"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold"),
    axis.text = element_text(size = 10)
  )

This scatter plot compares the actual ratings with the ratings predicted by the Global Baseline Estimate model. Points closer to the diagonal line represent predictions that are closer to the actual ratings, while points farther from the line show larger prediction errors. This plot gives a visual way to understand the model’s performance and works together with the RMSE value to show how closely the model predicts the observed ratings.

Disconnecting Database

dbDisconnect(con)

Conclusion

For this assignment, I constructed a movie recommendation engine using the Global Baseline Estimate method in R programming language. The model consists of three important parts: the average rating, the rating tendency of the user, and the rating tendency of the movie. These are then used to make predictions of ratings of those movies which have not been rated by the user. Based on these predictions, recommendations were made to the users.

From my experience, even a simple recommendation engine that is not personalized can generate some good predictions based on the rating tendencies of all users. I found it useful to look into user and movie bias plots to see how these factors influence the predicted ratings. For model evaluation, I used RMSE and actual versus predicted plots.