Book Recommendation Engine Using KNN

5 minute read

Book Recommendation Engine Using KNN

This project entailed creating a book recommendation algorithm using K-Nearest Neighbors.

I was able to use the Book-Crossings dataset which was provided and created a impressively accurate book recommendation engine. This dataset contains 1.1 million ratings (scale of 1-10) of 270,000 books by 90,000 users.

Project Instructions

Note: You are currently reading this using Google Colaboratory which is a cloud-hosted version of Jupyter Notebook. This is a document containing both text cells for documentation and runnable code cells. If you are unfamiliar with Jupyter Notebook, watch this 3-minute introduction before starting this challenge: https://www.youtube.com/watch?v=inN8seMm7UI


In this challenge, you will create a book recommendation algorithm using K-Nearest Neighbors.

You will use the Book-Crossings dataset. This dataset contains 1.1 million ratings (scale of 1-10) of 270,000 books by 90,000 users.

After importing and cleaning the data, use NearestNeighbors from sklearn.neighbors to develop a model that shows books that are similar to a given book. The Nearest Neighbors algorithm measures distance to determine the “closeness” of instances.

Create a function named get_recommends that takes a book title (from the dataset) as an argument and returns a list of 5 similar books with their distances from the book argument.

This code:

get_recommends("The Queen of the Damned (Vampire Chronicles (Paperback))")

should return:

[
  'The Queen of the Damned (Vampire Chronicles (Paperback))',
  [
    ['Catch 22', 0.793983519077301], 
    ['The Witching Hour (Lives of the Mayfair Witches)', 0.7448656558990479], 
    ['Interview with the Vampire', 0.7345068454742432],
    ['The Tale of the Body Thief (Vampire Chronicles (Paperback))', 0.5376338362693787],
    ['The Vampire Lestat (Vampire Chronicles, Book II)', 0.5178412199020386]
  ]
]

Notice that the data returned from get_recommends() is a list. The first element in the list is the book title passed in to the function. The second element in the list is a list of five more lists. Each of the five lists contains a recommended book and the distance from the recommended book to the book passed in to the function.

If you graph the dataset (optional), you will notice that most books are not rated frequently. To ensure statistical significance, remove from the dataset users with less than 200 ratings and books with less than 100 ratings.

The first three cells import libraries you may need and the data to use. The final cell is for testing. Write all your code in between those cells.

# import libraries (you may add additional imports but you may not have to)
import numpy as np
import pandas as pd
from scipy.sparse import csr_matrix
from sklearn.neighbors import NearestNeighbors
import matplotlib.pyplot as plt
# get data files
!wget https://cdn.freecodecamp.org/project-data/books/book-crossings.zip

!unzip book-crossings.zip

books_filename = 'BX-Books.csv'
ratings_filename = 'BX-Book-Ratings.csv'
--2020-08-25 11:04:20--  https://cdn.freecodecamp.org/project-data/books/book-crossings.zip
Resolving cdn.freecodecamp.org (cdn.freecodecamp.org)... 157.230.103.136, 167.99.137.12, 2a03:b0c0:3:e0::26f:c001, ...
Connecting to cdn.freecodecamp.org (cdn.freecodecamp.org)|157.230.103.136|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 26085508 (25M) [application/zip]
Saving to: ‘book-crossings.zip’

book-crossings.zip  100%[===================>]  24.88M  8.20MB/s    in 3.0s    

2020-08-25 11:04:23 (8.20 MB/s) - ‘book-crossings.zip’ saved [26085508/26085508]

Archive:  book-crossings.zip
  inflating: BX-Book-Ratings.csv     
  inflating: BX-Books.csv            
  inflating: BX-Users.csv            
# import csv data into dataframes
df_books = pd.read_csv(
    books_filename,
    encoding = "ISO-8859-1",
    sep=";",
    header=0,
    names=['isbn', 'title', 'author'],
    usecols=['isbn', 'title', 'author'],
    dtype={'isbn': 'str', 'title': 'str', 'author': 'str'})

df_ratings = pd.read_csv(
    ratings_filename,
    encoding = "ISO-8859-1",
    sep=";",
    header=0,
    names=['user', 'isbn', 'rating'],
    usecols=['user', 'isbn', 'rating'],
    dtype={'user': 'int32', 'isbn': 'str', 'rating': 'float32'})
# add your code here - consider creating a new cell for each section of code
print(df_books)
              isbn  ...                author
0       0195153448  ...    Mark P. O. Morford
1       0002005018  ...  Richard Bruce Wright
2       0060973129  ...          Carlo D'Este
3       0374157065  ...      Gina Bari Kolata
4       0393045218  ...       E. J. W. Barber
...            ...  ...                   ...
271374  0440400988  ...        Paula Danziger
271375  0525447644  ...            Teri Sloat
271376  006008667X  ...      Christine Wicker
271377  0192126040  ...                 Plato
271378  0767409752  ...   Christopher  Biffle

[271379 rows x 3 columns]
print(df_ratings)
           user         isbn  rating
0        276725   034545104X     0.0
1        276726   0155061224     5.0
2        276727   0446520802     0.0
3        276729   052165615X     3.0
4        276729   0521795028     6.0
...         ...          ...     ...
1149775  276704   1563526298     9.0
1149776  276706   0679447156     0.0
1149777  276709   0515107662    10.0
1149778  276721   0590442449    10.0
1149779  276723  05162443314     8.0

[1149780 rows x 3 columns]
# Calculate user and book rating counts
user_RatingCount = df_ratings.groupby('user')['rating'].count().reset_index().rename(columns = {'rating':'userTotalRatingCount'})
book_RatingCount = df_ratings.groupby('isbn')['rating'].count().reset_index().rename(columns = {'rating':'bookTotalRatingCount'})

# Add to df_ratings
df_ratings = df_ratings.merge(user_RatingCount,how='left', left_on='user', right_on='user')
df_ratings = df_ratings.merge(book_RatingCount, how='left', left_on='isbn', right_on='isbn')

# Filter data for statistical significance
df_ratings_2 =df_ratings.loc[(df_ratings['userTotalRatingCount']>=200) & (df_ratings['bookTotalRatingCount']>=100)]
# merge data sets
books_with_ratings = pd.merge(df_ratings_2, df_books, on='isbn')
print(books_with_ratings)
         user  ...             author
0      277427  ...  James Finn Garner
1        3363  ...  James Finn Garner
2       11676  ...  James Finn Garner
3       12538  ...  James Finn Garner
4       13552  ...  James Finn Garner
...       ...  ...                ...
49512  238864  ...  Patricia Cornwell
49513  251843  ...  Patricia Cornwell
49514  253821  ...  Patricia Cornwell
49515  265115  ...  Patricia Cornwell
49516  266226  ...  Patricia Cornwell

[49517 rows x 7 columns]
# Remove duplicates
books_with_ratings_2 = books_with_ratings.drop_duplicates(['title', 'user'])
# Preparing data table for analysis
books_with_ratings_pivot = pd.pivot_table(data=books_with_ratings_2, values='rating', index='title', columns='user').fillna(0)
print(books_with_ratings_pivot)
user                                                254     ...  278418
title                                                       ...        
1984                                                   9.0  ...     0.0
1st to Die: A Novel                                    0.0  ...     0.0
2nd Chance                                             0.0  ...     0.0
4 Blondes                                              0.0  ...     0.0
A Beautiful Mind: The Life of Mathematical Geni...     0.0  ...     0.0
...                                                    ...  ...     ...
Without Remorse                                        0.0  ...     0.0
Year of Wonders                                        0.0  ...     0.0
You Belong To Me                                       0.0  ...     0.0
Zen and the Art of Motorcycle Maintenance: An I...     0.0  ...     0.0
\O\" Is for Outlaw"                                    0.0  ...     0.0

[673 rows x 888 columns]
# Convert to 2D matrıx
books_with_ratings_matrix = csr_matrix(books_with_ratings_pivot.values)
# Train Model
model_knn = NearestNeighbors(algorithm='auto', metric='cosine')
model_knn.fit(books_with_ratings_matrix)
NearestNeighbors(algorithm='auto', leaf_size=30, metric='cosine',
                 metric_params=None, n_jobs=None, n_neighbors=5, p=2,
                 radius=1.0)
# function to return recommended books - this will be tested
def get_recommends(book = ""):

  X = books_with_ratings_pivot[books_with_ratings_pivot.index == book]
  X = X.to_numpy().reshape(1,-1)
  distances, indices = model_knn.kneighbors(X,n_neighbors=8)
  recommended_books = []
  for x in reversed(range(1,6)):
      bookrecommended = [books_with_ratings_pivot.index[indices.flatten()[x]], distances.flatten()[x]]
      recommended_books.append(bookrecommended)
  recommended_books = [book, recommended_books]
  
  return recommended_books
books = get_recommends("Where the Heart Is (Oprah's Book Club (Paperback))")
print(books)
["Where the Heart Is (Oprah's Book Club (Paperback))", [["I'll Be Seeing You", 0.8016211], ['The Weight of Water', 0.77085835], ['The Surgeon', 0.7699411], ['I Know This Much Is True', 0.7677075], ['The Lovely Bones: A Novel', 0.7234864]]]

Use the cell below to test your function. The test_book_recommendation() function will inform you if you passed the challenge or need to keep trying.

books = get_recommends("Where the Heart Is (Oprah's Book Club (Paperback))")
print(books)

def test_book_recommendation():
  test_pass = True
  recommends = get_recommends("Where the Heart Is (Oprah's Book Club (Paperback))")
  if recommends[0] != "Where the Heart Is (Oprah's Book Club (Paperback))":
    test_pass = False
  recommended_books = ["I'll Be Seeing You", 'The Weight of Water', 'The Surgeon', 'I Know This Much Is True']
  recommended_books_dist = [0.8, 0.77, 0.77, 0.77]
  for i in range(2): 
    if recommends[1][i][0] not in recommended_books:
      test_pass = False
    if abs(recommends[1][i][1] - recommended_books_dist[i]) >= 0.05:
      test_pass = False
  if test_pass:
    print("You passed the challenge! 🎉🎉🎉🎉🎉")
  else:
    print("You havn't passed yet. Keep trying!")

test_book_recommendation()
["Where the Heart Is (Oprah's Book Club (Paperback))", [["I'll Be Seeing You", 0.8016211], ['The Weight of Water', 0.77085835], ['The Surgeon', 0.7699411], ['I Know This Much Is True', 0.7677075], ['The Lovely Bones: A Novel', 0.7234864]]]
You passed the challenge! 🎉🎉🎉🎉🎉