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.
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.
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.
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:
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.
`
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.
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.
library(DBI)
library(RPostgres)
library(dplyr)
library(tidyr)
library(tidyverse)
library(ggplot2)
con <- dbConnect(
RPostgres::Postgres(),
host = "localhost",
port = 5432,
dbname = "postgres",
user = "postgres",
password = "admin"
)
dbListTables(con) [1] "movies" "ratings" "users"
users_df <- dbGetQuery(con, "SELECT * FROM users;")
movies_df <- dbGetQuery(con, "SELECT * FROM movies;")
ratings_df <- dbGetQuery(con, "SELECT * FROM ratings;")Basic sanity checks
Rows: 5
Columns: 2
$ user_id <int> 1, 2, 3, 4, 5
$ name <chr> "Prakash", "Das", "Anush", "Arpi", "Mihir"
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
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 (μ)
[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
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
# 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
[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)
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)
)# 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
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.
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
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.