<jupyter_start><jupyter_text>KNN Recommenders DSC 672: Data Science Capstone Group 2: <NAME>, <NAME>, <NAME><jupyter_code>from google.colab import drive
drive.mount('/drive')
!pip install scikit-surprise<jupyter_output>Collecting scikit-surprise
[?25l  Downloading https://files.pythonhosted.org/packages/97/37/5d334adaf5ddd65da99fc65f6507e0e4599d092ba048f4302fe8775619e8/scikit-surprise-1.1.1.tar.gz (11.8MB)
[K     |████████████████████████████████| 11.8MB 341kB/s 
[?25hRequirement already satisfied: joblib>=0.11 in /usr/local/lib/python3.6/dist-packages (from scikit-surprise) (0.16.0)
Requirement already satisfied: numpy>=1.11.2 in /usr/local/lib/python3.6/dist-packages (from scikit-surprise) (1.18.5)
Requirement already satisfied: scipy>=1.0.0 in /usr/local/lib/python3.6/dist-packages (from scikit-surprise) (1.4.1)
Requirement already satisfied: six>=1.10.0 in /usr/local/lib/python3.6/dist-packages (from scikit-surprise) (1.12.0)
Building wheels for collected packages: scikit-surprise
  Building wheel for scikit-surprise (setup.py) ... [?25l[?25hdone
  Created wheel for scikit-surprise: filename=scikit_surprise-1.1.1-cp36-cp36m-linux_x86_64.whl size=1646979 sha256=aa706e8f591313f19b43780e51822dc[...]<jupyter_text>Libraries<jupyter_code># Data Loading 
import numpy as np
import pandas as pd
import sqlite3 as sql
from surprise import Dataset, Reader

# Recommender Systems (KNN)
from surprise import KNNBasic, KNNBaseline
from surprise import KNNWithMeans, KNNWithZScore

# Model Selection
from sklearn.model_selection import train_test_split

# Data Viz
import matplotlib.pyplot as plt
import seaborn as sns 

# Default Dictionary
from collections import defaultdict<jupyter_output>/usr/local/lib/python3.6/dist-packages/statsmodels/tools/_testing.py:19: FutureWarning: pandas.util.testing is deprecated. Use the functions in the public API at pandas.testing instead.
  import pandas.util.testing as tm<jupyter_text>Data Loading<jupyter_code># Path to Database
db_path = '/drive/My Drive/DSC672_Project/datasets/climbing_full.sqlite'

# Load User Ratings
with sql.connect(db_path) as conn:
  ratings = pd.read_sql_query('''
    SELECT user_id, route_id, AVG(user_star) as rating
    FROM ticks
    WHERE user_star 
    BETWEEN 0 AND 4
    GROUP BY user_id, route_id;
  ''',conn)
# Read In Routes
route_csv = '/drive/My Drive/DSC672_Project/datasets/cleaned_routes.csv'
clean_routes = pd.read_csv(route_csv,index_col=0)
clean_routes.head()
# Remove any Non-Clean Routes
ratings = ratings[ratings.route_id.isin(clean_routes.id.unique())]
ratings.head()<jupyter_output><empty_output><jupyter_text>Utility Functions<jupyter_code>def rating_info(
    df:pd.DataFrame,
    uid_col:str = 'user_id',
    iid_col:str = 'route_id'
  ) -> None:
  '''
  Prints out simple info about ratings DataFrame
    - Total Number of Ratings
    - Number of Users
    - Number of Items
    - Sparsity of User Item Matrix
  '''
  n_users = len(df[uid_col].unique()) 
  n_items = len(df[iid_col].unique())
  n_rating = df.shape[0]

  sparsity = 1 - n_rating / (n_users * n_items)
  print(f'Number of Ratings: {df.shape[0]}')
  print(f'Number of Users: {n_users}')
  print(f'Number of Items: {n_items}')
  print(f'Sparsity: {sparsity:0.5f}')

def min_interactions(
    df : pd.DataFrame,
    n : int,
    uid_col : str = 'user_id',
    iid_col : str = 'route_id'  
  ) -> pd.DataFrame:
  '''
  Removes Items and Users with less than n ratings
  '''
  unique_uid = set(df[uid_col].value_counts(sort=False).loc[lambda x: x > n].index)
  unique_iid = set(df[iid_col].value_counts(sort=False).loc[lambda x: x > n].index)

  return df[df[uid_col].isin(unique_uid) & df[iid_col].isin(unique_iid)]<jupyter_output><empty_output><jupyter_text>Determine Size of Rating Table<jupyter_code>rating_info(ratings)
rating_info(min_interactions(ratings,25))
rating_subset = min_interactions(ratings,25)
route_ids = rating_subset.route_id.unique()
route_ids<jupyter_output><empty_output><jupyter_text>Train Test Split<jupyter_code>train_ds, test_ds = train_test_split(rating_subset,test_size=0.2)
train_ds, val_ds  = train_test_split(train_ds,test_size=(1/8))<jupyter_output><empty_output><jupyter_text>Format Pandas DFs to Surprise Format<jupyter_code>data_reader = Reader(rating_scale=(0,4))

train_ds = Dataset.load_from_df(train_ds,data_reader)<jupyter_output><empty_output><jupyter_text>Hyper Parameter and Model Search<jupyter_code>def eval_val_set(model,ds):
  '''
  Evaluate Surpise Model
  Inputs:
    model: Surpise Model
    ds: pd.DataFrame - columns = uid | iid | rating
  Returns MAE, RMSE
  '''
  absolute_error = np.empty(len(ds))
  squared_error  = np.empty(len(ds))
  for idx, row in enumerate(ds.to_numpy()):
    uid, iid, rui = row  
    _,_,_,est,_ = model.predict(uid,iid)
    absolute_error[idx] = np.abs(rui-est)
    squared_error[idx]  = (rui-est)**2  
  return absolute_error.mean(), np.sqrt(squared_error.mean())<jupyter_output><empty_output><jupyter_text>Basic KNN<jupyter_code>params = {'k':[5,10,20,30,40],'sim':['msd','cosine','pearson']}

results = np.empty(shape=(len(params['k'])*len(params['sim']),4),dtype=object)
i = 0
for k in params['k']:
  for sim in params['sim']:
    model = KNNBasic(k=k,sim_options={'name':sim},verbose=0)
    model.fit(train_ds.build_full_trainset())
    mae, rmse = eval_val_set(model,val_ds)

    results[i] = [k,sim,mae,rmse]
    i = i+1
results_df = pd.DataFrame(results)
results_df.columns = ['K','Similarity','MAE','RMSE']
results_df = results_df.astype({'K':np.int,'MAE':np.float64,'RMSE':np.float64})
print(results_df)
fig, axs = plt.subplots(ncols=2,figsize=(16,8))
sns.lineplot(x='K',y='MAE',hue='Similarity',data=results_df, ax=axs[0])
sns.lineplot(x='K',y='RMSE',hue='Similarity',data=results_df, ax=axs[1])
fig.suptitle('Basic KNN Collaborative Filtering Results')
plt.show()
print('Min MAE')
print(results_df[results_df.MAE == results_df.MAE.min()])
print('Min RMSE')
print(results_df[results_df.RMSE == results_df.RMSE.min()])<jupyter_output>Min MAE
    K Similarity       MAE      RMSE
9  30        msd  0.490904  0.627155
Min RMSE
    K Similarity       MAE      RMSE
9  30        msd  0.490904  0.627155<jupyter_text>KNN with Mean<jupyter_code>params = {'k':[5,10,20,30,40],'sim':['msd','cosine','pearson']}

results_knn_mean = np.empty(shape=(len(params['k'])*len(params['sim']),4),dtype=object)
i = 0
for k in params['k']:
  for sim in params['sim']:
    model = KNNWithMeans(k=k,sim_options={'name':sim},verbose=0)
    model.fit(train_ds.build_full_trainset())
    mae, rmse = eval_val_set(model,val_ds)

    results_knn_mean[i] = [k,sim,mae,rmse]
    i = i+1
results_df = pd.DataFrame(results_knn_mean)
results_df.columns = ['K','Similarity','MAE','RMSE']
results_df = results_df.astype({'K':np.int,'MAE':np.float64,'RMSE':np.float64})
print(results_df)
fig, axs = plt.subplots(ncols=2,figsize=(16,8))
sns.lineplot(x='K',y='MAE',hue='Similarity',data=results_df, ax=axs[0])
sns.lineplot(x='K',y='RMSE',hue='Similarity',data=results_df, ax=axs[1])
fig.suptitle('KNN With Means Collaborative Filtering Results')
plt.show()
print('Min MAE')
print(results_df[results_df.MAE == results_df.MAE.min()])
print('Min RMSE')
print(results_df[results_df.RMSE == results_df.RMSE.min()])<jupyter_output>Min MAE
     K Similarity       MAE      RMSE
12  40        msd  0.496024  0.635878
Min RMSE
     K Similarity       MAE      RMSE
12  40        msd  0.496024  0.635878<jupyter_text>KNN with Z-Score<jupyter_code>params = {'k':[5,10,20,30,40,50],'sim':['msd','cosine','pearson']}

results_knn_zs = np.empty(shape=(len(params['k'])*len(params['sim']),4),dtype=object)
i = 0
for k in params['k']:
  for sim in params['sim']:
    model = KNNWithZScore(k=k,sim_options={'name':sim},verbose=0)
    model.fit(train_ds.build_full_trainset())
    mae, rmse = eval_val_set(model,val_ds)

    results_knn_zs[i] = [k,sim,mae,rmse]
    i = i+1
results_df = pd.DataFrame(results_knn_zs)
results_df.columns = ['K','Similarity','MAE','RMSE']
results_df = results_df.astype({'K':np.int,'MAE':np.float64,'RMSE':np.float64})
print(results_df)
fig, axs = plt.subplots(ncols=2,figsize=(16,8))
sns.lineplot(x='K',y='MAE',hue='Similarity',data=results_df, ax=axs[0])
sns.lineplot(x='K',y='RMSE',hue='Similarity',data=results_df, ax=axs[1])
fig.suptitle('KNN With Z-Score Collaborative Filtering Results')
plt.show()
print('Min MAE')
print(results_df[results_df.MAE == results_df.MAE.min()])
print('Min RMSE')
print(results_df[results_df.RMSE == results_df.RMSE.min()])<jupyter_output>Min MAE
     K Similarity      MAE      RMSE
15  50        msd  0.49256  0.632603
Min RMSE
     K Similarity      MAE      RMSE
15  50        msd  0.49256  0.632603<jupyter_text>Test Set Score<jupyter_code>def relevant_items(df,user_col,item_col,rate_col,threshold=2.5):
  '''
  Generates Users' Set of relevant items 
  where the explicit rating is greater than 
  the threshold which defaults to 2.5
  '''
  df = df[df[rate_col] > threshold]
  df = df.groupby(user_col).agg({item_col:lambda x: set(x)})
  return df 
def precision_recall_at_k(model,ds,item_set,k_vals=[1,5,10,25]):
  precision, recall = defaultdict(np.array), defaultdict(np.array)
  for k in k_vals:
    precision[k] = np.empty(len(ds))
    recall[k] = np.empty(len(ds))
  for pdx, uid in enumerate(ds.index):
    est_ratings = np.empty(len(item_set))
    for idx,iid in enumerate(item_set):
      _,_,_,est,_ = model.predict(uid,iid)
      est_ratings[idx] = est 
    top_k = np.argsort(est_ratings)[::-1][:max(k_vals)]
    for k in k_vals:
      top_k_prime = top_k[:k]
      top_k_prime = set(item_set[top_k_prime])
      rel = ds.loc[uid]['route_id'] 
      rel_and_rec = top_k_prime & rel
      precision[k][pdx] = len(rel_and_rec) / float(k)
      recall[k][pdx] = len(rel_and_rec) / float(len(rel)) if len(rel) > 0 else 0
  for k in k_vals:
    precision[k] = precision[k].mean() 
    recall[k] = recall[k].mean()
  return precision, recall<jupyter_output><empty_output><jupyter_text>Build Model with Best Parameters<jupyter_code>final_knn_model = KNNBasic(k=30)
final_knn_model.fit(train_ds.build_full_trainset())<jupyter_output>Computing the msd similarity matrix...
Done computing similarity matrix.<jupyter_text>MAE & RMSE<jupyter_code>eval_val_set(final_knn_model,test_ds)<jupyter_output><empty_output><jupyter_text>Precision & Recall @ k<jupyter_code># Build Test Dataset to Calculate Precision / Recall @ k 
test_ds = relevant_items(test_ds,'user_id','route_id','rating')
test_ds.head()
%%time
precision, recall = precision_recall_at_k(
    final_knn_model,test_ds,route_ids,
    k_vals=[1,5,10,25]
)
precision
recall<jupyter_output><empty_output><jupyter_text>Save KNN Model<jupyter_code>from surprise.dump import dump
recommender_path = '/drive/My Drive/DSC672_Project/Recommenders/'
dump(file_name=recommender_path + 'knn_recommender.pickle',
     algo=final_knn_model)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.14842674983946427, 'fraction_numerical': 0.03440051371433813, 'mean_word_length': 5.500894454382826, 'pattern_counts': {'":': 0, '<': 62, '<?xml version=': 0, '>': 72, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '16', 'dir': 'jupyter-structured-clean-dedup', 'id': '538379', 'license': 'CC0-1.0', 'n_tokens_mistral': 4565, 'n_tokens_neox': 4306, 'n_words': 730, 'path': 'ClimbingRecommender_KNN_Models.ipynb', 'repo_name': 'rmpollo1/Climbing_Recommenders', 'size': '10919'}