Introduction

This analysis uses a Global Baseline Estimate recommender system, which is a non-personalized recommendation algorithm, using listener rating data for six songs from The Strokes’ album Reality Awaits. The goal is to predict what a listener would likely rate a song they haven’t heard yet, using only the overall average rating, that listener’s personal rating tendency, and that song’s overall rating tendency.

Body

con <- dbConnect(RSQLite::SQLite(), "album_ratings.db")

dbExecute(con, "DROP TABLE IF EXISTS ratings;")
## [1] 0
dbExecute(con, "DROP TABLE IF EXISTS songs;")
## [1] 0
dbExecute(con, "DROP TABLE IF EXISTS users;")
## [1] 0
dbExecute(con, "
CREATE TABLE users (
  user_id INTEGER PRIMARY KEY,
  name TEXT NOT NULL
);")
## [1] 0
dbExecute(con, "
CREATE TABLE songs (
  song_id INTEGER PRIMARY KEY,
  title TEXT NOT NULL
);")
## [1] 0
dbExecute(con, "
CREATE TABLE ratings (
  user_id INTEGER,
  song_id INTEGER,
  rating INTEGER CHECK (rating BETWEEN 1 AND 5),
  PRIMARY KEY (user_id, song_id),
  FOREIGN KEY (user_id) REFERENCES users(user_id),
  FOREIGN KEY (song_id) REFERENCES songs(song_id)
);")
## [1] 0
dbExecute(con, "INSERT INTO users (user_id, name) VALUES
  (1, 'Person 1'), (2, 'Person 2'), (3, 'Person 3'), (4, 'Person 4'), (5, 'Person 5');")
## [1] 5
dbExecute(con, "INSERT INTO songs (song_id, title) VALUES
  (1, 'Dine and Dash'), (2, 'Lonely in the Future'), (3, 'Going to Babylon'),
  (4, 'Going Shopping'), (5, \"Liar's Remorse\"), (6, 'Psycho Shit');")
## [1] 6
dbExecute(con, "INSERT INTO ratings (user_id, song_id, rating) VALUES
  (1,1,4), (1,2,5), (1,3,5), (1,4,3), (1,5,3), (1,6,2),
  (2,1,3), (2,2,4), (2,3,5), (2,4,3), (2,5,2), (2,6,2),
  (3,1,4), (3,2,5), (3,3,3), (3,4,3),          (3,6,2),
  (4,1,3), (4,2,5), (4,3,3), (4,4,3), (4,5,2), (4,6,2),
  (5,1,3), (5,2,4), (5,3,3),          (5,5,3), (5,6,3);")
## [1] 28
ratings_full <- dbGetQuery(con, "
  SELECT u.name AS listener, s.title AS song, r.rating
  FROM users u
  CROSS JOIN songs s
  LEFT JOIN ratings r
    ON r.user_id = u.user_id AND r.song_id = s.song_id
  ORDER BY u.name, s.title;
")

glimpse(ratings_full)
## Rows: 30
## Columns: 3
## $ listener <chr> "Person 1", "Person 1", "Person 1", "Person 1", "Person 1", "…
## $ song     <chr> "Dine and Dash", "Going Shopping", "Going to Babylon", "Liar'…
## $ rating   <int> 4, 3, 5, 3, 5, 2, 3, 3, 5, 2, 4, 2, 4, 3, 3, NA, 5, 2, 3, 3, …
global_mean <- mean(ratings_full$rating, na.rm = TRUE)
global_mean
## [1] 3.285714
user_bias <- ratings_full %>%
  group_by(listener) %>%
  summarize(user_avg = mean(rating, na.rm = TRUE)) %>%
  mutate(user_bias = user_avg - global_mean)
user_bias
## # A tibble: 5 × 3
##   listener user_avg user_bias
##   <chr>       <dbl>     <dbl>
## 1 Person 1     3.67    0.381 
## 2 Person 2     3.17   -0.119 
## 3 Person 3     3.4     0.114 
## 4 Person 4     3      -0.286 
## 5 Person 5     3.2    -0.0857
song_bias <- ratings_full %>%
  group_by(song) %>%
  summarize(song_avg = mean(rating, na.rm = TRUE)) %>%
  mutate(song_bias = song_avg - global_mean)
song_bias
## # A tibble: 6 × 3
##   song                 song_avg song_bias
##   <chr>                   <dbl>     <dbl>
## 1 Dine and Dash             3.4     0.114
## 2 Going Shopping            3      -0.286
## 3 Going to Babylon          3.8     0.514
## 4 Liar's Remorse            2.5    -0.786
## 5 Lonely in the Future      4.6     1.31 
## 6 Psycho Shit               2.2    -1.09
baseline_estimates <- ratings_full %>%
  left_join(user_bias, by = "listener") %>%
  left_join(song_bias, by = "song") %>%
  mutate(baseline_estimate = global_mean + user_bias + song_bias)

baseline_estimates
##    listener                 song rating user_avg   user_bias song_avg
## 1  Person 1        Dine and Dash      4 3.666667  0.38095238      3.4
## 2  Person 1       Going Shopping      3 3.666667  0.38095238      3.0
## 3  Person 1     Going to Babylon      5 3.666667  0.38095238      3.8
## 4  Person 1       Liar's Remorse      3 3.666667  0.38095238      2.5
## 5  Person 1 Lonely in the Future      5 3.666667  0.38095238      4.6
## 6  Person 1          Psycho Shit      2 3.666667  0.38095238      2.2
## 7  Person 2        Dine and Dash      3 3.166667 -0.11904762      3.4
## 8  Person 2       Going Shopping      3 3.166667 -0.11904762      3.0
## 9  Person 2     Going to Babylon      5 3.166667 -0.11904762      3.8
## 10 Person 2       Liar's Remorse      2 3.166667 -0.11904762      2.5
## 11 Person 2 Lonely in the Future      4 3.166667 -0.11904762      4.6
## 12 Person 2          Psycho Shit      2 3.166667 -0.11904762      2.2
## 13 Person 3        Dine and Dash      4 3.400000  0.11428571      3.4
## 14 Person 3       Going Shopping      3 3.400000  0.11428571      3.0
## 15 Person 3     Going to Babylon      3 3.400000  0.11428571      3.8
## 16 Person 3       Liar's Remorse     NA 3.400000  0.11428571      2.5
## 17 Person 3 Lonely in the Future      5 3.400000  0.11428571      4.6
## 18 Person 3          Psycho Shit      2 3.400000  0.11428571      2.2
## 19 Person 4        Dine and Dash      3 3.000000 -0.28571429      3.4
## 20 Person 4       Going Shopping      3 3.000000 -0.28571429      3.0
## 21 Person 4     Going to Babylon      3 3.000000 -0.28571429      3.8
## 22 Person 4       Liar's Remorse      2 3.000000 -0.28571429      2.5
## 23 Person 4 Lonely in the Future      5 3.000000 -0.28571429      4.6
## 24 Person 4          Psycho Shit      2 3.000000 -0.28571429      2.2
## 25 Person 5        Dine and Dash      3 3.200000 -0.08571429      3.4
## 26 Person 5       Going Shopping     NA 3.200000 -0.08571429      3.0
## 27 Person 5     Going to Babylon      3 3.200000 -0.08571429      3.8
## 28 Person 5       Liar's Remorse      3 3.200000 -0.08571429      2.5
## 29 Person 5 Lonely in the Future      4 3.200000 -0.08571429      4.6
## 30 Person 5          Psycho Shit      3 3.200000 -0.08571429      2.2
##     song_bias baseline_estimate
## 1   0.1142857          3.780952
## 2  -0.2857143          3.380952
## 3   0.5142857          4.180952
## 4  -0.7857143          2.880952
## 5   1.3142857          4.980952
## 6  -1.0857143          2.580952
## 7   0.1142857          3.280952
## 8  -0.2857143          2.880952
## 9   0.5142857          3.680952
## 10 -0.7857143          2.380952
## 11  1.3142857          4.480952
## 12 -1.0857143          2.080952
## 13  0.1142857          3.514286
## 14 -0.2857143          3.114286
## 15  0.5142857          3.914286
## 16 -0.7857143          2.614286
## 17  1.3142857          4.714286
## 18 -1.0857143          2.314286
## 19  0.1142857          3.114286
## 20 -0.2857143          2.714286
## 21  0.5142857          3.514286
## 22 -0.7857143          2.214286
## 23  1.3142857          4.314286
## 24 -1.0857143          1.914286
## 25  0.1142857          3.314286
## 26 -0.2857143          2.914286
## 27  0.5142857          3.714286
## 28 -0.7857143          2.414286
## 29  1.3142857          4.514286
## 30 -1.0857143          2.114286
recommendations <- baseline_estimates %>%
  filter(is.na(rating)) %>%
  select(listener, song, baseline_estimate) %>%
  arrange(listener, desc(baseline_estimate))

recommendations
##   listener           song baseline_estimate
## 1 Person 3 Liar's Remorse          2.614286
## 2 Person 5 Going Shopping          2.914286
dbDisconnect(con)

Conclusions

The global baseline estimate predicts a rating of 2.61 for Person 3’s unrated song, “Liar’s Remorse,” and 2.91 for Person 5’s unrated song, “Going Shopping.” Both predictions land below the overall average of 3.29, though for different reasons: Person 3 actually tends to rate songs slightly above average (+0.11 bias), but “Liar’s Remorse” is one of the two lowest-rated songs overall (-0.79 bias), so the song’s weakness pulls the prediction down despite the listener’s generosity. Person 5, on the other hand, rates close to the group average (-0.09 bias), and “Going Shopping” is also close to average (-0.29 bias), so that estimate lands nearer the global mean.

More broadly, “Lonely in the Future” stands out as the strongest song in the dataset (+1.31 bias), while “Psycho Shit” is the weakest (-1.09 bias) consistent with what a personalized recommender would also need to account for, though the global baseline approach intentionally ignores individual taste patterns in favor of these simple, broad averages.

Future work could compare these baseline estimates against a personalized method like item item collaborative filtering, to see whether accounting for individual listener similarity produces meaningfully different (and more accurate) predictions than this non-personalized approach.