Building a Recommendation Algorithm in Ruby on Rails (Part 2)

Sean LaFlam
7 min readDec 29, 2020
Cosine Similarity is using the cosine of the angle between two vectors to measure how close they are

Continuing Where We Left Off

If you have not yet completed the steps in Part 1 of setting up a Rails environment and pulling down the data from the API, please go back and read my last article here. Assuming you’ve completed these steps, let's continue on to the next part.

Creating Custom Attribute Values for Each Game

As mentioned in the last article, in this example our four categories for comparison are a game’s:

  • Board Score (How much of a classic board game aspect it has)
  • Card Score (Whether there are cards involved, and how prominent)
  • Party Score (This is higher in non-traditional games such as Pictionary)
  • Players Score (A higher score reflects a higher number of max players)

These are custom attributes that we will be defining for each game. In the last article we generated random numbers between 1 and 10 for each of these, but in a real life example someone with product expertise would have to evaluate each and assign values for each of these attributes that will be used for comparison.

I went ahead and created a file with non-randomized values for each of these attributes that we can use for our example. You can go and grab the Ruby file from my Github at this link here. We’ll be using this data going forward.

In order to use this new “games_array_demo.rb” file to create our Game instances, we’ll need to require it at the top of our “seeds.rb” file. Make sure to save the seeds and games_array_demo files in the same folder, and then add this to the top of your seeds.rb file:

require 'rest-client'
require_relative 'games_array_demo.rb'
games_array = get_games

The require_relative command allows Ruby to draw a relative path to the file provided in quotes afterward and allows us to import and execute code present in that file in the file in which the require_relative was used.

As you can see, on the next line we then set our games_array equal to the return value of the get_games command that we imported from our games_array_demo file.

The final step now is to iterate over our games_array and create individual Game instances for each. I accomplished this by adding the following code to my seeds.rb file:

games_array.each do |game|Game.create(name: game[:name],image: game[:image],price: game[:price],max_players: game[:max_players],min_players: game[:min_players],play_time: game[:play_time],description: game[:description],genre: game[:genre],rating: game[:rating],rank: game[:rank],board_score: game[:board_score],players_score: game[:players_score],card_score: rand(10),party_score: rand(10))end

If you are using a POSTGRES database, you can run the following steps in order to recreate and reseed your database with this new info:

  • rails db:drop
  • rails db:create
  • rails db:migrate
  • rails db:seed

Now if you run rails c to open the console and type Game.all you should see 55 instances Game all with values for our 4 comparison attributes.

Actually Building The Algorithm

Ok, now that we have all the setup done, let’s actually get to building the cosine similarity algorithm. Before you start I HIGHLY recommend you go through this Google Developers course explaining how to build recommendation systems. Everything I’ve done in this tutorial is adapted from the knowledge learned through this course. It goes into much more in-depth recommendation systems than we’ll be building today, but it definitely will help to understand the pros and cons of each version before jumping into building one.

In our Game model, we’ll start by creating a custom instance method that can be called on a game to see how similar it is to another game. We’ll call it similarity_score_game:

def similarity_score_game(game)# create a vector for the game method is being called on# attributes needed are board_score, card_score, party_score, and players_scorevector_a = Vector[self.board_score, self.card_score, self.party_score, self.players_score]#next, create a vector for the game being passed in to the method#we will then compare the two vectors to see how similar the games arevector_b = Vector[game.board_score, game.card_score, game.party_score, game.players_score]#now that we have 2 vectors we will find the similarity by putting the dot product#in our numerator, and the product of each vectors magnitude in our denominatornumerator = vector_a.inner_product(vector_b)denominator = vector_a.r * vector_b.r#this will give the cosine similarityscore = numerator/denominator*100return score.to_i#since cosine is a decimal between 0 and 1, multiplying by 100 and then rounding to the nearest integer gives a nice scoring system from 0 to 100end
The method above is just this formula

And that’s pretty much the algorithm, I encourage you to try it out in the console and see if it works. You can do something like this in the console:

> first = Game.first
> second = Game.second
> first.similarity_score_game(second)
=> 97

This shows that these games have a similarity score of 97, and players of the first game have a very high chance of liking the second

Recommending Games based on a User’s Profile

This is great for recommending similar products based on ones you’ve liked in the past, but what if you wanted to recommend products to a user based on their tendencies as a whole. Well, it’s not all that different to do so using cosine similarity.

Building a User Profile

The first step here will be to build a User Profile. Many companies accomplish this by something akin to a “Personality Quiz” when a new user first signs up to a service. You can collect information about the user’s tendencies (in this case what games they like and how many people they normally play with) and use that to generate attributes to a user, not unlike we did with attributes for each game.

In your Rails App, create a migration for a User model with attributes for:

  • name
  • board_score
  • card_score
  • party_score
  • players_score

Then head back to your seeds.rb file and add the following:

User.create(uname: ‘Sean’,board_score: 5,card_score: 6,party_score: 3,players_score: 3)

Drop, recreate, migrate, and seed your database, then head to your User.rb file and lets create another method.

def create_user_profile#Create vector that represents user profile@total_reviews = 2@user_profile = Vector[self.board_score, self.card_score, self.party_score, self.players_score]end

This will generate our user profile for a user that will be used to represent the user’s tendencies. This profile will be constantly evolving based on new ratings and games played by the user. In order to update the profile, we can add the following method:

def update_user_profile(new_rating_vector)#mean of user profile and weighted new item review# Vold = current user profile#Vnew = new user profile after rating is factored in#Vitem = vector representation of item being review#If the review is favorable, use it to calculate new mean#If review was negative, use the inverse vector to calculate new mean#ex - If a user profile consists of 5 reviews, the effect of a 6th would#looks like this:#(5 x Vold + Vitem)/6#formula: Vnew = ((n x Vold) + Vitem )/ n + 1new_vector_board = (@total_reviews * @user_profile[0] + new_rating_vector[0])/(@total_reviews + 1)new_vector_card = (@total_reviews * @user_profile[1] + new_rating_vector[1])/(@total_reviews + 1)new_vector_party = (@total_reviews * @user_profile[2] + new_rating_vector[2])/(@total_reviews + 1)new_vector_players = (@total_reviews * @user_profile[2] + new_rating_vector[2])/(@total_reviews + 1)@user_profile = Vector[new_vector_board, new_vector_card, new_vector_party, new_vector_players]@total_reviews += 1return @user_profileend

Take a moment to try and understand this before moving on. What it’s doing is creating an average value for each of our user’s attributes by adding up all of the values for each game, and dividing it by the total number of ratings.

In order for this code to be effective, we have to only update a User’s profile when they review a game positively (If we wanted to go even further, we could add the inverse of a games vector to the profile when there is a negative review, but thats too complicated for this tutorial today).

Finally, we want to create a comparison method to get the similarity between a user’s profile and a game. It will look very similar to our game similarity method earlier:

def similarity_score(game)# create a vector for the user the method is being called on# attributes needed are board_score, card_score, party_score and players_scoreuser_vector = @user_profile#next, create a vector for the game being passed in to the method#we will then compare the two vectors to see how similar the games aregame_vector = Vector[game.board_score, game.card_score, game.party_score, game.players_score ]#now that we have 2 vectors we will find the similarity by putting the dot product in our numerator, and the product of each vectors magnitude in our denominatornumerator = user_vector.inner_product(game_vector)denominator = user_vector.r * game_vector.r#this will give the cosine similarityscore = numerator/denominator*100return score.to_iend

Again, we will get a score in the range of 0 to 100, and they higher the score, the more like the user will be to enjoy that game. We can then generate a an array of game recommendations for your user with the following method:

def game_recsgame_rec_array = []self.create_user_profileGame.all.each do |game|score = game.similarity_score_user(self)game_rec_array.push({game: game, similarity: score})endranked = game_rec_array.sort_by{|rec| -rec[:similarity]}return rankedend

This will return a list of all of the games, sorted by their similarity score (high to low), and we can use this list to recommend games to the user.

And that wraps up the 2 part Series of building a Recommendation Algorithm using Ruby on Rails and Vectors.

I want to give a huge shout out to one of my fellow classmates Alyssa Lerner for helping me with this project! She also built an awesome project of her own based around Video Games comeple with its own recommendation backend called ‘The Perfect Game’ you can find her GitHub here and her Medium Profile here.

Check out my next article in the series on building a frontend to interact with this backend we’ve built in order to display our recommendations in the browser.

--

--