Recommender Systems, Window Functions, and Character Data
Author
Darwhin Gomez
DATA 607
Week 3: Character Data and String Manipulation
Tonight we will work with text as data. Our examples use Jane Austen novels, but the same tools work for names, survey responses, addresses, product titles, and log files.
Tonight’s agenda
Due this week and next week
Global baseline estimates for recommenders
Window functions in SQL
Why character data needs cleaning
Working with strings in R
janeaustenr examples
A first look at regular expressions
Practice and next steps
Due this week
Thursday
Assignment 3A Global Baseline Estimate: Approach |
Thursday
Assignment 3B Window Functions: Approach
Sunday
Quiz: Data Centric AI
Sunday
Discussion: Data Centric AI
Sunday
Assignment 3A Global Baseline Estimate: Code Base
Sunday
Assignment 3B Window Functions: Code Base
Monday
Assignment 3A Global Baseline Estimate: Video Explainer
Monday
Assignment 3B Global Baseline Estimate: Video Explainer
Due next week
Thursday: Project 1 approach
Sunday: Project 1 code base
Sunday: Scenario Design quiz and discussion
Monday: Project 1 video explainer
Global baseline estimates
A recommender system estimates how a user might rate an item they have not yet rated.
The simplest useful prediction starts with a baseline:
the typical rating across the whole dataset
how a particular critic tends to rate
how a particular item tends to be rated
The bias-based formula
\[
\widehat{r}_{ui} = \mu + b_u + b_i
\]
Term
Meaning
\(\mu\)
Global mean rating
\(b_u\)
User bias: this user rates above or below average
\(b_i\)
Item bias: this item receives above or below average ratings
The hat means “predicted,” not an observed rating.
Critic-movie ratings data
Your ratings matrix has one row per critic and one column per movie. Blank cells mean a rating was not observed; the question mark is the rating we want to predict.
Code
# Load dplyr and tidyr functions for this example.library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr 1.1.4 ✔ readr 2.1.5
✔ forcats 1.0.0 ✔ stringr 1.5.1
✔ ggplot2 3.5.1 ✔ tibble 3.2.1
✔ lubridate 1.9.3 ✔ tidyr 1.3.1
✔ purrr 1.0.2
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag() masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
# A tibble: 16 × 7
critic CaptainAmerica Deadpool Frozen JungleBook PitchPerfect2 StarWarsForce
<chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Burton NA NA NA 4 NA 4
2 Charley 4 5 4 3 2 3
3 Dan NA 5 NA NA NA 5
4 Dieudo… 5 4 NA NA NA 5
5 Matt 4 NA 2 NA 2 5
6 Mauric… 4 NA 3 3 4 NA
7 Max 4 4 4 2 2 4
8 Nathan NA NA NA NA NA 4
9 Param 4 4 1 NA NA 5
10 Parshu 4 3 5 5 2 3
11 Prasha… 5 5 5 5 NA 4
12 Shipra NA NA 4 5 NA 3
13 Sreeja… 5 5 5 4 4 5
14 Steve 4 NA NA NA NA 4
15 Vuthy 4 5 3 3 3 NA
16 Xingjia NA NA 5 5 NA NA
A negative value means the movie tends to receive lower ratings than the typical rating.
Predict Param’s PitchPerfect2 rating
Do the arithmetic by hand before writing any code.
Quantity
Calculation
Rounded value
Global mean, \(\mu\)
mean of all observed ratings
3.93
Param bias, \(b_u\)
\(3.50 - 3.93\)
-0.43
PitchPerfect2 bias, \(b_i\)
\(2.71 - 3.93\)
-1.22
Prediction:3.93 + (-0.43) + (-1.22) = 2.28
Your task is to create the tibble of biases and implement this arithmetic. Then score every movie Param has not rated and recommend the highest predicted rating.
Recommend a movie for Param
First, identify movies Param has not rated. Then calculate the baseline prediction for every candidate and sort from highest to lowest.
afc_east_df is now an R tibble. The PostgreSQL table remains unchanged in the database.
Window functions keep every game row
GROUP BY reduces many game rows to one row per team. A window function adds a calculation while preserving every game row.
AVG(home_points) OVER (PARTITIONBY home_team)
Read the query result directly into a tibble with dbGetQuery().
Overall average and differential
Code
overall_scores_df <-dbGetQuery(con, " SELECT home_team, game_date, home_points, ROUND(AVG(home_points) OVER (), 1) AS home_avg, ROUND(home_points - AVG(home_points) OVER (), 1) AS home_differential FROM afc_east ORDER BY home_team, game_date") |>as_tibble()overall_scores_df
OVER () means every row belongs to one window: the full result set.
Average by home team
Code
team_average_df <-dbGetQuery(con, " SELECT home_team, game_date, home_points, ROUND(AVG(home_points) OVER (PARTITION BY home_team), 1) AS home_team_avg FROM afc_east ORDER BY home_team, game_date") |>as_tibble()team_average_df
PARTITION BY home_team restarts the calculation for each team.
Rank scores within each team
Code
ranked_games_df <-dbGetQuery(con, " SELECT home_team, game_date, home_points, RANK() OVER ( PARTITION BY home_team ORDER BY home_points DESC ) AS home_rank FROM afc_east ORDER BY home_team, home_rank") |>as_tibble()ranked_games_df
The highest home score for each team receives rank 1.
Running total by team
Code
running_scores_df <-dbGetQuery(con, " SELECT home_team, game_date, home_points, SUM(home_points) OVER ( PARTITION BY home_team ORDER BY game_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cumulative_home_points FROM afc_east ORDER BY home_team, game_date") |>as_tibble()running_scores_df
The SQL version calculates in PostgreSQL. The R version calculates after the table has been imported.
PostgreSQL window functions mapped to dplyr
A PostgreSQL window function is usually a dplyrmutate() after the data has been grouped and ordered. Both approaches add a value to every original row.
PostgreSQL idea
PostgreSQL pattern
dplyr counterpart
What it does
All rows in one window
AVG(x) OVER ()
mutate(avg_x = mean(x))
Repeats one overall value on every row.
A window for each group
PARTITION BY team
group_by(team)
Restarts a calculation for each team.
Order inside a window
ORDER BY game_date
arrange(game_date, .by_group = TRUE)
Sets the order used by ranks, lags, and running totals.
Keep every original row
... OVER (...)
mutate(...)
Adds a calculated column without reducing rows.
Reduce to one row per group
GROUP BY team
group_by(team) |> summarise(...)
Produces a summary table instead of a row-by-row result.
Aggregate functions: PostgreSQL to dplyr
PostgreSQL
dplyr inside mutate()
dplyr inside summarise()
AVG(x) OVER (...)
mean(x, na.rm = TRUE)
mean(x, na.rm = TRUE)
SUM(x) OVER (...)
sum(x, na.rm = TRUE)
sum(x, na.rm = TRUE)
MIN(x) OVER (...) / MAX(x) OVER (...)
min(x, na.rm = TRUE) / max(x, na.rm = TRUE)
Same functions
COUNT(*) OVER (...)
n()
n()
Use these functions inside mutate() when you want to keep each game row. Use them inside summarise() when you want one result per group.
Ranks, offsets, and running totals: PostgreSQL to dplyr
PostgreSQL
dplyr counterpart
Meaning
RANK() OVER (ORDER BY x DESC)
min_rank(desc(x))
Equal values share a rank, and later ranks have gaps.
DENSE_RANK() OVER (ORDER BY x DESC)
dense_rank(desc(x))
Equal values share a rank, with no gaps afterward.
ROW_NUMBER() OVER (ORDER BY x DESC)
row_number(desc(x))
Gives every row a unique position, including tied values.
LAG(x) OVER (ORDER BY date)
lag(x) after arrange(date)
Reads the previous row’s value.
LEAD(x) OVER (ORDER BY date)
lead(x) after arrange(date)
Reads the next row’s value.
SUM(x) OVER (... ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
cumsum(x) after arrange()
Adds values from the first ordered row through the current row.
Read this pipeline from top to bottom: group_by() matches PARTITION BY; arrange() matches ORDER BY; and mutate() matches the OVER (...) calculation that keeps each game row.
When SQL and dplyr differ
Use PostgreSQL when the data is stored in the database and you want the database to do the work. Use dplyr after importing data when you need to continue analyzing it in R. In both tools, choose mutate()/OVER() to keep rows and summarise()/GROUP BY to collapse rows.
Close the database connection
Code
dbDisconnect(con)
Close the connection when you are done querying the database.
Window-function checklist
Before writing the query, state in words:
What does one row represent?
Which rows should be compared together?
What order matters?
Should the output preserve every original row?
For example: “For each team, ordered by game date, calculate home points scored to date.” ## Why character data matters
Text is everywhere in data work:
names, emails, and addresses
survey comments and reviews
categories imported from files
dates stored as text
IDs such as NY-2026-0142
Before analysis, text often needs to be standardized, searched, split, or combined.
Learning goals
By the end of this session, you should be able to:
recognize a character vector in R
clean inconsistent capitalization and whitespace
find text with str_detect()
extract or replace part of a string
use simple regular expressions to describe a pattern
Packages for tonight
Code
# Install once if needed:# install.packages(c("tidyverse", "janeaustenr"))library(tidyverse)library(janeaustenr)
stringr is included when you load the tidyverse.
What is a string?
A string is text inside quotation marks.
Code
movie <-"Pride and Prejudice"year <-"1813"rating <-"4.5"
All three objects are character strings, even when their contents look like a number.
# A tibble: 3 × 3
line_number text characters
<int> <chr> <int>
1 6504 declaration affected me in any other way, than as it s… 74
2 2010 convenience of the world. I cannot forget the follies … 73
3 3507 that he should have attentive and conciliatory manners… 73
Change capitalization
Use a predictable case before matching names or categories.
# A tibble: 5 × 2
line_number text
<int> <chr>
1 139 "\"But you forget, mamma,\" said Elizabeth, \"that we shall meet …
2 308 "Elizabeth Bennet had been obliged, by the scarcity of gentlemen,…
3 337 "Elizabeth, till catching her eye, he withdrew his own and coldly…
4 343 "Mr. Bingley followed his advice. Mr. Darcy walked off; and Eliza…
5 352 "her mother could be, though in a quieter way. Elizabeth felt Jan…
This search is case-sensitive: "elizabeth" would not match "Elizabeth".
Ignore capitalization in a search
Wrap the pattern in regex(..., ignore_case = TRUE).
# A tibble: 5 × 2
line_number text
<int> <chr>
1 281 "looked the gentleman; but his friend Mr. Darcy soon drew the att…
2 298 "themselves. What a contrast between him and his friend! Mr. Darc…
3 309 "down for two dances; and during part of that time, Mr. Darcy had…
4 314 "\"Come, Darcy,\" said he, \"I must have you dance. I hate to see…
5 330 "Darcy, looking at the eldest Miss Bennet."
Use this when you want Darcy, DARCY, and darcy to count as the same text.
Count matches
str_count() counts how often a pattern appears in each string.
Code
str_count(c("ha", "ha ha", "perhaps"),"ha")
[1] 1 2 1
Unlike str_detect(), this returns a number rather than TRUE or FALSE.
# A tibble: 5 × 3
line_number darcy_mentions text
<int> <int> <chr>
1 6918 2 of seeing Mr. Darcy--that Mr. Darcy might leave th…
2 8849 2 to Mr. Darcy were by no means over. Miss Darcy, on…
3 281 1 looked the gentleman; but his friend Mr. Darcy soo…
4 298 1 themselves. What a contrast between him and his fr…
5 309 1 down for two dances; and during part of that time,…
This keeps the original line and adds a new measurement.