<jupyter_start><jupyter_text>Recommendations with IBM Table of ContentsI. [Exploratory Data Analysis](Exploratory-Data-Analysis)II. [Rank Based Recommendations](Rank)III. [User-User Based Collaborative Filtering](User-User)IV. [Content Based Recommendations (EXTRA - NOT REQUIRED)](Content-Recs)V. [Matrix Factorization](Matrix-Fact)VI. [Extras & Concluding](conclusions)<jupyter_code>import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from unit_tests import project_test as t
#import Unit_Tests\project_test as t
import pickle
from pandas_profiling import ProfileReport
from sklearn.feature_extraction.text import TfidfVectorizer
import re

# nltk
import nltk
from nltk.corpus import stopwords
from nltk.stem.wordnet import WordNetLemmatizer
from nltk.tokenize import word_tokenize
nltk.download(['punkt', 'wordnet', 'stopwords',
               'averaged_perceptron_tagger'])
df = pd.read_csv('data/user-item-interactions.csv')
df_content = pd.read_csv('data/articles_community.csv')
del df['Unnamed: 0']
del df_content['Unnamed: 0']

# Show df to get an idea of the data
df.head()
# Show df_content to get an idea of the data
df_content.head()<jupyter_output><empty_output><jupyter_text>Part I : Exploratory Data Analysis<jupyter_code># Count user interaction
user_interaction = df.email.value_counts(dropna=False)
# Distribution of how many articles a user interacts with in the dataset
plt.figure()
plt.hist(user_interaction.values, bins=100)
plt.title('Distribution of how many articles a user interacts with in the dataset')
plt.xlabel('interactions')
plt.ylabel('count')
plt.show()
#Profiling Report 1
profile=ProfileReport(df,title="profiling report",explorative=True)
profile.to_file("df.html")

#Profiling Report 2
profile=ProfileReport(df_content,title="profiling report",explorative=True)
profile.to_file("df_content.html")
df.email.value_counts()
np.percentile(df.email.value_counts(),50)
# Fill in the median and maximum number of user_article interactios below

median_val = np.percentile(df.email.value_counts(),50) # 50% of individuals interact with ____ number of articles or fewer.
max_views_by_user = 364 # The maximum number of user-article interactions by any 1 user is ______.<jupyter_output><empty_output><jupyter_text>Explore and remove duplicate articles from the **df_content** dataframe.<jupyter_code># Display duplicate rows
df_content[df_content.article_id.duplicated()]
# Remove duplicate rows
print(df_content.shape)

df_content=df_content[~df_content.article_id.duplicated()]

print('\n',df_content.shape)
# unique_articles
df.article_id.nunique()
# total_articles
df_content.article_id.nunique()
# unique_users
df.email.nunique()
# number of user-article interactions
df.shape[0]<jupyter_output><empty_output><jupyter_text>Find the following:**a.** The number of unique articles that have an interaction with a user.  **b.** The number of unique articles in the dataset (whether they have any interactions or not).**c.** The number of unique users in the dataset. (excluding null values) **d.** The number of user-article interactions in the dataset.<jupyter_code>unique_articles = 714 # The number of unique articles that have at least one interaction
total_articles = 1051 #The number of unique articles on the IBM platform
unique_users = 5148 # The number of unique users
user_article_interactions = 45993 # The number of user-article interactions<jupyter_output><empty_output><jupyter_text>Find the most viewed **article_id**, as well as how often it was viewed.<jupyter_code>df.article_id=df.article_id.astype(str)
df.email.groupby(df.article_id).size().sort_values(ascending=False)
most_viewed_article_id = '1429.0' # The most viewed article in the dataset as a string with one value following the decimal 
max_views = 937 # The most viewed article in the dataset was viewed how many times?
sol_1_dict = {
    '`50% of individuals have _____ or fewer interactions.`': median_val,
    '`The total number of user-article interactions in the dataset is ______.`': user_article_interactions,
    '`The maximum number of user-article interactions by any 1 user is ______.`': max_views_by_user,
    '`The most viewed article in the dataset was viewed _____ times.`': max_views,
    '`The article_id of the most viewed article is ______.`': most_viewed_article_id,
    '`The number of unique articles that have at least 1 rating ______.`': unique_articles,
    '`The number of unique users in the dataset is ______`': unique_users,
    '`The number of unique articles on the IBM platform`': total_articles
}

# Test the dictionary against the solution
t.sol_1_test(sol_1_dict)<jupyter_output>It looks like you have everything right here! Nice job!<jupyter_text>The `email_mapper` function seemed a reasonable way to map users to ids. There were a small number of null values, and it was found that all of these null values likely belonged to a single user (which is how they are stored using the function below).<jupyter_code># Map the user email to a user_id column and remove the email column

def email_mapper():
    coded_dict = dict()
    cter = 1
    email_encoded = []
    
    for val in df['email']:
        if val not in coded_dict:
            coded_dict[val] = cter
            cter+=1
        
        email_encoded.append(coded_dict[val])
    return email_encoded

email_encoded = email_mapper()
del df['email']
df['user_id'] = email_encoded

# show header
df.head()<jupyter_output><empty_output><jupyter_text>Part II: Rank-Based Recommendations<jupyter_code>def get_top_articles(n, df=df):
    '''
    INPUT:
    n - (int) the number of top articles to return
    df - (pandas dataframe) df as defined at the top of the notebook 
    
    OUTPUT:
    top_articles - (list) A list of the top 'n' article titles 
    
    '''
        
    top_articles=list(df.title.value_counts().index)[:n]
    
    return top_articles # Return the top article titles from df (not df_content)

def get_top_article_ids(n, df=df):
    '''
    INPUT:
    n - (int) the number of top articles to return
    df - (pandas dataframe) df as defined at the top of the notebook 
    
    OUTPUT:
    top_articles - (list) A list of the top 'n' article titles 
    
    '''

    top_articles=list(df.article_id.value_counts().index)[:n]
    
    return top_articles # Return the top article ids
# Test the function by returning the top 5, 10, and 20 articles
#top_5 = get_top_articles(5)
top_10 = get_top_articles(10)
top_20 = get_top_articles(20)

# Test each of the three lists from above
t.sol_2_test(get_top_articles)<jupyter_output>Your top_5 looks like the solution list! Nice job.
Your top_10 looks like the solution list! Nice job.
Your top_20 looks like the solution list! Nice job.<jupyter_text>Part III: User-User Based Collaborative Filtering* Each **user** should only appear in each **row** once.* Each **article** should only show up in one **column**.  * If a user has interacted with an article, then place a 1 where the user-row meets for that article-column.  It does not matter how many times a user has interacted with the article, all entries where a user has interacted with an article should be a 1.  * If a user has not interacted with an item, then place a zero where the user-row meets for that article-column.<jupyter_code># create the user-article matrix with 1's and 0's

def create_user_item_matrix(df):
    '''
    INPUT:
    df - pandas dataframe with article_id, title, user_id columns
    
    OUTPUT:
    user_item - user item matrix 
    
    Description:
    Return a matrix with user ids as rows and article ids on the columns with 1 values where a user interacted with 
    an article and a 0 otherwise
    '''
    user_item=pd.pivot_table(df,index='user_id',aggfunc=np.max,columns='article_id',fill_value=0)
    
    for i in range(user_item.shape[0]):
        for j in range(user_item.shape[1]):
            if user_item.iloc[i,j]!=0:
                user_item.iloc[i,j]=1
    
    # Drop multi-level index
    user_item.columns = user_item.columns.droplevel()
    
    # Convert datatype of columns from str to int
    for i in user_item.columns:
        user_item[i]=user_item[i].astype(int)
        
    return user_item # return the user_item matrix 

user_item = create_user_item_matrix(df)
user_item.head()
## Tests:
assert user_item.shape[0] == 5149, "Oops!  The number of users in the user-article matrix doesn't look right."
assert user_item.shape[1] == 714, "Oops!  The number of articles in the user-article matrix doesn't look right."
assert user_item.sum(axis=1)[1] == 36, "Oops!  The number of articles seen by user 1 doesn't look right."
print("You have passed our quick tests!  Please proceed!")
# create the user-article dictionary

def create_user_article_dict():
    '''
    INPUT: None
    OUTPUT: user_dict - a dictionary where each key is a user_id and the value is an array of articles interacted with.
    
    Creates the movies_seen dictionary
    '''
       
    # create a dictionary where the key is each user and the value is list of 'article_id' that user has interacted with.
    user_dict={}
    for row in user_item.index.values:
        temp_list=[]
        for col in list(user_item.columns):
            if user_item.loc[row,col]>0:
                temp_list.append(col)
        user_dict[row]=temp_list
    
    return user_dict # return the user_item dictionary

user_dict = create_user_article_dict()
# create the user-user matrix that holds number of common articles between each pair of users.

def create_user_user_matrix():
    '''
    INPUT: None
    OUTPUT: user_user_ - a dictionary where each key is a user_id and the value is an array of articles interacted with.
    
    Creates the movies_seen dictionary
    '''
    
    # Create a dataframe that holds number of common articles between each pair of users
    user_user_matrix=pd.DataFrame(data=np.zeros((user_item.shape[0],user_item.shape[0])),columns=user_item.index)

    for i in list(user_item.index):
        for j in list(user_item.index):
            user_user_matrix.loc[i,j]=len(np.intersect1d(user_dict[i],user_dict[j],assume_unique=True))
    
    return user_user_matrix # return the user_user matrix
#user_user_matrix = create_user_user_matrix()
#user_user_matrix.to_pickle('user_common_article')

# It takes a long time to create user_user_matrix.
# Pickled this file on disk to avoid time taken during re-runs and debugging.

# Reading pickled file holding user_user_matrix
user_user_matrix=pd.read_pickle('user_common_article')
user_user_matrix.head()
def find_similar_users(user_id, user_user_matrix=user_user_matrix):
    '''
    INPUT:
    user_id - (int) a user_id
    user_user_matrix - (pandas dataframe) matrix that holds number of common articles between each pair of users.
    
    OUTPUT:
    similar_users - (list) an ordered list where the closest users are listed first
    
    Description:
    Computes the similarity of every pair of users
    '''
    
    most_similar_users=list(user_user_matrix[user_id].sort_values(ascending=False).index)[1:]
    
    return most_similar_users # return a list of the users in order from most to least similar
# Do a spot check of the function
print("The 10 most similar users to user 1 are: {}".format(find_similar_users(1)[:10]))
print("The 5 most similar users to user 3933 are: {}".format(find_similar_users(3933)[:5]))
print("The 3 most similar users to user 46 are: {}".format(find_similar_users(46)[:3]))<jupyter_output>The 10 most similar users to user 1 are: [3933, 23, 3782, 203, 4459, 3870, 131, 4201, 46, 5041]
The 5 most similar users to user 3933 are: [3933, 3782, 23, 4459, 203]
The 3 most similar users to user 46 are: [4201, 23, 3782]<jupyter_text>Now that we have a function that provides the most similar users to each user, we will use these users to find articles that can be recommended.<jupyter_code>def get_article_names(article_ids, df=df):
    '''
    INPUT:
    article_ids - (list) a list of article ids
    df - (pandas dataframe) df as defined at the top of the notebook
    
    OUTPUT:
    article_names - (list) a list of article names associated with the list of article ids 
                    (this is identified by the title column)
    '''
    
    article_names=list(df[df.article_id.isin(article_ids)].title.groupby(df.title).size().index.values)
          
    return article_names # Return the article names associated with list of article ids


def get_user_articles(user_id, user_item=user_item):
    '''
    INPUT:
    user_id - (int) a user id
    user_item - (pandas dataframe) matrix of users by articles: 
                1's when a user has interacted with an article, 0 otherwise
    
    OUTPUT:
    article_ids - (list) a list of the article ids seen by the user
    article_names - (list) a list of article names associated with the list of article ids 
                    (this is identified by the doc_full_name column in df_content)
    
    Description:
    Provides a list of the article_ids and article titles that have been seen by a user
    '''
    article_ids=user_dict[user_id]
        
    article_names=get_article_names(article_ids)
        
    return article_ids, article_names # return the ids and names


def user_user_recs(user_id, m=10):
    '''
    INPUT:
    user_id - (int) a user id
    m - (int) the number of recommendations you want for the user
    
    OUTPUT:
    recs - (list) a list of recommendations for the user
    
    Description:
    Loops through the users based on closeness to the input user_id
    For each user - finds articles the user hasn't seen before and provides them as recs
    Does this until m recommendations are found
    
    Notes:
    Users who are the same closeness are chosen arbitrarily as the 'next' user
    
    For the user where the number of recommended articles starts below m 
    and ends exceeding m, the last items are chosen arbitrarily
    
    '''
    art=[]
    for i in find_similar_users(user_id):
        art.extend(np.setdiff1d(user_dict[i],user_dict[user_id]))
        if len(set(art))>m:
            break
    recs=list(set(art[:m]))
    
    return recs # return recommendations for this user_id    
# Check Results
get_article_names(user_user_recs(1, 10)) # Return 10 recommendations for user 1
# List of articles seen by user 1
get_user_articles(1)[1]
# Test the functions here

assert set(get_article_names(['1024.0', '1176.0', '1305.0', '1314.0', '1422.0', '1427.0'])) == set(['using deep learning to reconstruct high-resolution audio', 'build a python app on the streaming analytics service', 'gosales transactions for naive bayes model', 'healthcare python streaming application demo', 'use r dataframes & ibm watson natural language understanding', 'use xgboost, scikit-learn & ibm watson machine learning apis']), "Oops! Your the get_article_names function doesn't work quite how we expect."
assert set(get_article_names(['1320.0', '232.0', '844.0'])) == set(['housing (2015): united states demographic measures','self-service data preparation with ibm data refinery','use the cloudant-spark connector in python notebook']), "Oops! Your the get_article_names function doesn't work quite how we expect."
assert set(get_user_articles(20)[0]) == set(['1320.0', '232.0', '844.0'])
assert set(get_user_articles(20)[1]) == set(['housing (2015): united states demographic measures', 'self-service data preparation with ibm data refinery','use the cloudant-spark connector in python notebook'])
assert set(get_user_articles(2)[0]) == set(['1024.0', '1176.0', '1305.0', '1314.0', '1422.0', '1427.0'])
assert set(get_user_articles(2)[1]) == set(['using deep learning to reconstruct high-resolution audio', 'build a python app on the streaming analytics service', 'gosales transactions for naive bayes model', 'healthcare python streaming application demo', 'use r dataframes & ibm watson natural language understanding', 'use xgboost, scikit-learn & ibm watson machine learning apis'])
print("If this is all you see, you passed all of our tests!  Nice job!")<jupyter_output>If this is all you see, you passed all of our tests!  Nice job!<jupyter_text>Now we are going to improve the consistency of the **user_user_recs** function from above.Instead of arbitrarily choosing articles from the user where the number of recommended articles starts below m and ends exceeding m, we will choose articles with the articles with the most total interactions before choosing those with fewer total interactions. This ranking should be what would be obtained from the **top_articles** function.<jupyter_code>def user_user_recs_part2(user_id, m=10):
    '''
    INPUT:
    user_id - (int) a user id
    m - (int) the number of recommendations you want for the user
    
    OUTPUT:
    recs - (list) a list of recommendations for the user by article id
    rec_names - (list) a list of recommendations for the user by article title
    
    Notes:
    * Choose the users that have the most total article interactions 
    before choosing those with fewer article interactions.

    * Choose articles with the articles with the most total interactions 
    before choosing those with fewer total interactions. 
   
    '''
    rec_names=list(df[df.article_id.isin(user_user_recs(user_id))].title.value_counts().index)
    recs= list(set(df[df.title.isin(rec_names)].article_id.values))
    
    return recs, rec_names
# Quick spot check

rec_ids, rec_names = user_user_recs_part2(20, 10)
print("The top 10 recommendations for user 20 are the following article ids:")
print(rec_ids)
print()
print("The top 10 recommendations for user 20 are the following article names:")
print(rec_names)
# List of articles seen by user 20
# Make sure that these articles don't appear in the recommended list of articles
get_user_articles(20)[1]
def all_recommendations():
    '''
    INPUT 
        num_recs (int) the (max) number of recommendations for each user
    OUTPUT
        all_recs - a dictionary where each key is a user_id and the value is an array of recommended movie titles
    '''
    all_recs={}
    for i in list(user_user_matrix.columns):
        all_recs[i]=user_user_recs_part2(i)[0]
      
    return all_recs

all_recs = all_recommendations()
all_recs[4]
# Check for users with no recommendations
# Should be empty list of recommendations have been made for all users
no_recs=[]
for i,v in all_recs.items():
    if len(v)==0:
        no_recs.append(i)
no_recs<jupyter_output><empty_output><jupyter_text>`5.` Use the functions from above to correctly fill in the solutions to the dictionary below.  Then test against the solution.<jupyter_code>print(find_similar_users(1)[0])
print(find_similar_users(131)[10])
### Tests with a dictionary of results

user1_most_sim = 3933
user131_10th_sim = 242 # Find the 10th most similar user to user 131
## Dictionary Test Here
sol_5_dict = {
    'The user that is most similar to user 1.': user1_most_sim, 
    'The user that is the 10th most similar to user 131': user131_10th_sim,
}

t.sol_5_test(sol_5_dict)<jupyter_output>This all looks good!  Nice job!<jupyter_text>`6.` Using existing functions, provide the top 10 recommended articles for a new user below.<jupyter_code>new_user = '0.0'

# As a new user, they have no observed articles. Provide a list of the top 10 article ids as recommendations
new_user_recs = get_top_article_ids(10)
assert set(new_user_recs) == set(['1314.0','1429.0','1293.0','1427.0','1162.0','1364.0','1304.0','1170.0','1431.0','1330.0']), "Oops!  It makes sense that in this case we would want to recommend the most popular articles, because we don't know anything about these users."

print("That's right!  Nice job!")<jupyter_output>That's right!  Nice job!<jupyter_text>Part IV: Content Based RecommendationsAnother method we might use to make recommendations is to perform a ranking of the highest ranked articles associated with some term. We can consider content to be the doc_body, doc_description, or doc_full_name. There isn't one way to create a content based recommendation, especially considering that each of these columns hold content related information.<jupyter_code>def tokenize(text):
    """Function to tokenize an article title

    Args:
        text (str) title

    return:
        tokens (list) a list of words

    """

    # normalize case and remove punctuation
    text = re.sub(r"[^a-zA-Z0-9]", " ", text.lower())

    # tokenize text
    tokens = word_tokenize(text)

    # lemmatize and remove stop words
    stop_words = stopwords.words("english")
    lemmatizer = WordNetLemmatizer()
    tokens = [lemmatizer.lemmatize(word) for word in tokens
              if word not in stop_words]
    # remove short words
    tokens = [token for token in tokens if len(token) > 2]

    return tokens
def create_article_content_dataframe(df=df, df_content=df_content):
    '''
    INPUT:
    df - pandas dataframe describe user interaction with the articles
    df_content - pandas dataframe describe articles on the platform
    
    OUTPUT:
    df_total - pandas dataframe contains all articles in the platform
    article_content -  pandas dataframe describe the content of each
                       article on the platform

    Description:
    Return a pandas dataframe containing all the articles in the platform
    with thier titles and a pandas dataframe describes the content of each
    article base on the article title.
    '''

    # Get a dataframe of the full articles
    df_1 = df[['article_id', 'title']]
    df_2 = pd.DataFrame({'article_id': df_content.article_id.values,
                         'title': df_content. doc_full_name})
    df_total = pd.concat([df_1, df_2], ignore_index=True)
    
    df_total.article_id=df_total.article_id.astype(float)
    df.article_id=df.article_id.astype(float)
    df_content.article_id=df_content.article_id.astype(float)
    
    df_total.drop_duplicates(subset=['article_id'], inplace=True)
    df_total.sort_values(by='article_id', inplace=True)
    df_total.reset_index(drop=True, inplace=True)

    # TfidfVectorizer
    vectorizer = TfidfVectorizer(tokenizer=tokenize)
    df_vec = pd.DataFrame(vectorizer.fit_transform(
                          df_total.title.values).toarray(),
                          columns=[*vectorizer.vocabulary_])

    # concat df_total and df_vec
    df_articles = pd.concat([df_total, df_vec], axis=1)
    
    return df_total, df_articles


df_total, df_articles = create_article_content_dataframe()
df_total.head()
df_articles.head()
def create_article_similarity_dataframe(df=df_articles):
    '''
    INPUT:
    df - pandas dataframe describe articles content
    OUTPUT:
    article_content -  pandas dataframe describe articles similarities

    Description:
    Return a pandas dataframe that describe the articles similarities
    using the dot product.
    '''

    # subset articles content
    article_content = np.array(df_articles.iloc[:, 2:])

    # Take the dot product to obtain a article x article matrix of similarities
    dot_prod_article = article_content.dot(np.transpose(article_content))

    # checks the dot product
    assert dot_prod_article.shape[0] == len(df_articles)
    assert dot_prod_article.shape[1] == len(df_articles)

    # make a Dataframe
    article_similarity = pd.DataFrame(dot_prod_article,
                                      index=df_articles.article_id,
                                      columns=df_articles.article_id)

    return article_similarity


article_similarity = create_article_similarity_dataframe()
article_similarity.head()
def top_user_articles(user_id, df=df):
    '''
    INPUT:
    user_id - (int) a user id
    user_item - (pandas dataframe) dataframe of user interaction

    OUTPUT:
    article_ids - (list) a sorted list of the article ids seen by the user

    Description:
    Provides a list of the  article_ids sorted by interactions number
    '''

    df_user = df[df.user_id == user_id]
    df_user = df_user.groupby('article_id').count()
    df_user.sort_values('user_id', ascending=False, inplace=True)

    count_article = df_user.user_id.unique()
    article_ids = []
    for k in count_article:
        ids = df_user[df_user.user_id == k].index
        article_ids.append(list(ids))

    return article_ids
def make_content_recs(user_id, m=10, df_smly=article_similarity, thd=1):
    '''
    INPUT:
    user_id - (int) a user id
    m - (int) the number of recommendations you want for the user
    df_smly - (pandas dataframe) pandas dataframe that describe the articles
    similarities using the dot product

    OUTPUT:
    recs - (list) a list of recommendations for the user by article id
    rec_names - (list) a list of recommendations for the user by article title
    '''

    list_ids = top_user_articles(user_id)
    recs = []

    for ids in list_ids:
        top_articles = article_similarity.loc[ids].sum()
        top_articles.sort_values(ascending=False, inplace=True)
        top_articles = top_articles[top_articles >= thd]
        article_not_recs = np.setdiff1d(np.array(top_articles.index),
                                        np.array(recs))
        recs.extend(list(article_not_recs))

        # If there are more than
        if len(recs) > m:
            break

    recs = recs[:10]
    rec_names = get_article_names(recs, df=df_total)

    return recs, rec_names<jupyter_output><empty_output><jupyter_text>In order to build content-based Recommendations, I have used the titlr variable to compute similarities between articles. Given a user id, we can make recommendations by looking for most similar articles to the articles seen by the user.<jupyter_code># Get user id who only has interacted with article id '1434.0'
df_1434 = df[df.article_id == 1434.0].groupby('user_id').count()
user_1434 = df_1434.sort_values('article_id').index[0]

# Make recomendation
make_content_recs(user_1434)<jupyter_output><empty_output><jupyter_text>Part V: Matrix Factorization<jupyter_code># quick look at the user_item matrix created earlier
user_item.head()<jupyter_output><empty_output><jupyter_text>In this situation, we can use Singular Value Decomposition from [numpy](https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.linalg.svd.html) on the user-item matrix.  There are no missing values in the data so we can use the traditional SVD. If there were any missing values, traditional SVD would not have converged and errored out. In that case we would have used FunkSVD to handle missing values<jupyter_code># Perform SVD on the User-Item Matrix Here
u, s, vt = np.linalg.svd(user_item,full_matrices=False)
s1=np.diag(s)
u.shape,s1.shape,vt.shape<jupyter_output><empty_output><jupyter_text>Now for the tricky part, how do we choose the number of latent features to use?  Running the below cell, we can see that as the number of latent features increases, we obtain a lower error rate on making predictions for the 1 and 0 values in the user-item matrix.<jupyter_code>num_latent_feats = np.arange(10,700+10,20)
sum_errs = []

for k in num_latent_feats:
    # restructure with k latent features
    s_new, u_new, vt_new = np.diag(s[:k]), u[:, :k], vt[:k, :]
    
    # take dot product
    user_item_est = np.around(np.dot(np.dot(u_new, s_new), vt_new))
    
    # compute error for each prediction to actual value
    diffs = np.subtract(user_item, user_item_est)
    
    # total errors and keep track of them
    err = np.sum(np.sum(np.abs(diffs)))
    sum_errs.append(err)
    
    
plt.plot(num_latent_feats, 1 - np.array(sum_errs)/df.shape[0]);
plt.xlabel('Number of Latent Features');
plt.ylabel('Accuracy');
plt.title('Accuracy vs. Number of Latent Features');<jupyter_output><empty_output><jupyter_text>From the above, we can't really be sure how many features to use, because simply having a better way to predict the 1's and 0's of the matrix doesn't exactly give us an indication of if we are able to make good recommendations.  Instead, we might split our dataset into a training and test set of data, as shown in the cell below.  To understand the impact on accuracy of the training and test sets of data with different numbers of latent features, find the following:* How many users can we make predictions for in the test set?  * How many users are we not able to make predictions for because of the cold start problem?* How many articles can we make predictions for in the test set?  * How many articles are we not able to make predictions for because of the cold start problem?<jupyter_code>df_train = df.head(40000)
df_test = df.tail(5993)

def create_test_and_train_user_item(df_train, df_test):
    '''
    INPUT:
    df_train - training dataframe
    df_test - test dataframe
    
    OUTPUT:
    user_item_train - a user-item matrix of the training dataframe 
                      (unique users for each row and unique articles for each column)
    user_item_test - a user-item matrix of the testing dataframe 
                    (unique users for each row and unique articles for each column)
    test_idx - all of the test user ids
    test_arts - all of the test article ids
    
    '''
    user_item_train=create_user_item_matrix(df_train)
    
    user_item_test=create_user_item_matrix(df_test)
    
    test_idx=user_item_test.index.values
    
    test_arts=user_item_test.columns.values
    
    return user_item_train, user_item_test, test_idx, test_arts

user_item_train, user_item_test, test_idx, test_arts = create_test_and_train_user_item(df_train, df_test)
len(np.setdiff1d(user_item_test.index, user_item_train.index))
len(np.intersect1d(test_idx, user_item_train.index))
len(np.intersect1d(user_item_test.columns, user_item_train.columns))
len(np.setdiff1d(user_item_test.columns, user_item_train.columns))
# Spot check
sol_4_dict = {
    'How many users can we make predictions for in the test set?': 20, 
    'How many users in the test set are we not able to make predictions for because of the cold start problem?': 662 , 
    'How many movies can we make predictions for in the test set?': 574,
    'How many movies in the test set are we not able to make predictions for because of the cold start problem?': 0
}

t.sol_4_test(sol_4_dict)<jupyter_output>Awesome job!  That's right!  All of the test movies are in the training data, but there are only 20 test users that were also in the training set.  All of the other users that are in the test set we have no data on.  Therefore, we cannot make predictions for these users using SVD.<jupyter_text>Now use the **user_item_train** dataset from above to find U, S, and V transpose using SVD. Then find the subset of rows in the **user_item_test** dataset that you can predict using this matrix decomposition with different numbers of latent features to see how many features makes sense to keep based on the accuracy on the test data.<jupyter_code># Number of rows in user_item_test for which prediction can be made
len(np.intersect1d(test_idx, user_item_train.index))
# Rows in dataset user_item_test for which prediction can be made
user_item_train[user_item_train.index.isin(list(np.intersect1d(test_idx, user_item_train.index)))]
# fit SVD on the user_item_train matrix
u_train, s_train, vt_train = np.linalg.svd(user_item_train,full_matrices=False)
s_train_1=np.diag(s_train)
u_train.shape, s_train_1.shape, vt_train.shape
# Users and articles in user_item_train matrix
train_idx = np.array(user_item_train.index)
train_arts = np.array(user_item_train.columns)

# Users and articles of test set in training set
test_idx_set = np.intersect1d(test_idx, train_idx)
test_arts_set = np.intersect1d(test_arts, train_arts)

# Users and articles positions of test subset in training matrix
train_indexes = np.where(np.in1d(train_idx, test_idx_set))[0]
train_articles = np.where(np.in1d(train_arts, test_arts_set))[0]

# Users positions of test subset in test matrix
test_indexes = np.where(np.in1d(test_idx, test_idx_set))[0]
# Find subset of User_Item Matrix containing only user and articles that are shared by train and test set
u_item_test_set = user_item_test.iloc[test_indexes,:]
u_item_train_set = user_item_train.iloc[train_indexes, train_articles]
num_latent_feats = np.arange(10, 500, 20)
sum_errs = []

for k in num_latent_feats:
    
    # Restructure train matrices using k features
    s_train_k, u_train_k, vt_train_k = np.diag(s_train[:k]), u_train[:, :k], vt_train[:k, :]
        
    # Restructure test matrices using k features
    s_test_k, u_test_k, vt_test_k = s_train_k, u_train_k[train_indexes,:], vt_train_k[:,train_articles]
    
    # Calculate dot product
    u_item_test_set_pred = np.around(np.dot(np.dot(u_test_k, s_test_k), vt_test_k))
    
    # Error (prediction - actual values)
    error = np.subtract(u_item_test_set, u_item_test_set_pred)
    
    # Total errors
    total_error = np.sum(np.sum(np.abs(error)))
    sum_errs.append(total_error)
    

# Plot test accuracy and latent features
n_interactions_set = u_item_test_set.shape[0] * u_item_test_set.shape[1]
plt.plot(num_latent_feats, 1 - np.array(sum_errs) / n_interactions_set);
plt.xlabel('Latent Features');
plt.ylabel('Accuracy');
plt.title('Test Accuracy vs Latent Features');<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.0829744300213836, 'fraction_numerical': 0.01665512152516339, 'mean_word_length': 4.0500380228136885, 'pattern_counts': {'":': 0, '<': 70, '<?xml version=': 0, '>': 75, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '19', 'dir': 'jupyter-structured-clean-dedup', 'id': '666406', 'license': 'MIT', 'n_tokens_mistral': 10677, 'n_tokens_neox': 10059, 'n_words': 3511, 'path': 'Recommendations_with_IBM.ipynb', 'repo_name': 'rahul385/Recommendations-with-IBM', 'size': '33203'}