<jupyter_start><jupyter_text>Recommendations with IBMIn this notebook, you will be putting your recommendation skills to use on real data from the IBM Watson Studio platform. You may either submit your notebook through the workspace here, or you may work from your local machine and submit through the next page.  Either way assure that your code passes the project [RUBRIC](Need to update this).  **Please save regularly.**By following the table of contents, you will build out a number of different methods for making recommendations that can be used for different situations.  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)At the end of the notebook, you will find directions for how to submit your work.  Let's get started by importing the necessary libraries and reading in the data.<jupyter_code>import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import project_tests as t
import seaborn as sns
import nltk
import re
import random
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
from sklearn.feature_extraction.text import CountVectorizer
nltk.download(['punkt', 'wordnet','stopwords'])

%matplotlib inline




## Reading in and initial look at data
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 shape , head to get an idea of what the data 
# looks like
print("user-item df shape = ",df.shape)
print("user-item df w/o duplicates shape = ",df.drop_duplicates().shape)
print("There are repeated interactions.")
## Check for duplicates
df.head()
# Show df_content to get an idea of the data,
# Check duplicates
print("articles-community df_content shape = ",df_content.shape)
print("articles-community df_content w/o duplicates shape = ", df_content.drop_duplicates().shape)
print("No duplicates here.")
df_content.head()<jupyter_output>articles-community df_content shape =  (1056, 5)
articles-community df_content w/o duplicates shape =  (1056, 5)
No duplicates here.<jupyter_text>Perform Further Cleanup<jupyter_code>## df_content.doc_status only contains 'Live', no useful info, so drop
## Ensure df.email is string stype

print("Unique df_content.doc_status values = ",np.unique(df_content.doc_status))
df_content.drop(['doc_status'],axis=1,inplace=True)
df.email      = df.email.astype(str).copy()<jupyter_output>Unique df_content.doc_status values =  ['Live']<jupyter_text>Part I : Exploratory Data AnalysisUse the dictionary and cells below to provide some insight into the descriptive statistics of the data.`1.` What is the distribution of how many articles a user interacts with in the dataset?  Provide a visual and descriptive statistics to assist with giving a look at the number of times each user interacts with an article.<jupyter_code>### Make a histogram of user interaction. 
### The question is looking for 'how many articles',so we count multiple
### interactions with an article by a single user only once.

df_u = df[['article_id','email']].drop_duplicates()
print("Number of total article interactions in dataset = ",df.shape[0])
print("Number of unique user - article pairs = ",df_u.shape[0])
## shows that some users have interacted with the same article multiple times

## Number of :unique: user-article interactions  
user_article_cnt = df_u.email.value_counts().sort_values()

## Describe and plot here
print(user_article_cnt.describe())
plt.figure(figsize=(12, 4))
ax = sns.distplot(user_article_cnt[0:5000], kde = False, bins = 40)
ax.set_xlabel("Number of Users")
ax.set_ylabel("Number of Articles")
ax.set_title("Histogram of Unique User-Article Pairs")
### The second statement above is a question about the number of times each user interacts
### with an article (user statistics). For this question provide a description based on the
### original df. 
user_article_cnt = df.email.value_counts().sort_values()

## Describe and plot here
print("User statistics --------------------------------")
print(user_article_cnt.describe())
plt.figure(figsize=(12, 4))
ax = sns.distplot(user_article_cnt[0:5000], kde = False, bins = 40)
ax.set_xlabel("Number of Users")
ax.set_ylabel("Total Number of Article Interactions")
ax.set_title("Histogram of Number of User-Article Interactions")
# Fill in the median and maximum number of user_article interactions below
median_val = user_article_cnt.median() # 50% of individuals interact with ____ number of articles or fewer.
max_views_by_user = user_article_cnt.max() # The maximum number of user-article interactions by any 1 user is ______.

print("50% of individuals interact with ", median_val, "number of articles or fewer.")
print("The maximum number of user-article interactions by any 1 user is ", max_views_by_user)<jupyter_output>50% of individuals interact with  3.0 number of articles or fewer.
The maximum number of user-article interactions by any 1 user is  364<jupyter_text>`2.` Explore and remove duplicate articles from the **df_content** dataframe.<jupyter_code># Find and explore duplicate articles
# Show duplicate articles from df_content
a  = df_content.article_id.value_counts()
a  = a[a.values > 1]
l  = list(a.keys())
print("Duplicate article_ids from df_content ---------------------")
df_content[df_content.article_id.map(lambda x: x in l)].sort_values('article_id')
# Remove any rows that have the same article_id - only keep the first
df_content.drop_duplicates(subset = ['article_id'],keep = 'first',inplace=True)<jupyter_output><empty_output><jupyter_text>`3.` Use the cells below to find:**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>## The following code shows that there are 17 email ids that are different
## from the rest. These are 'nan's -- so we will remove
## We'll keep two dataframes df contains the nans, dfw does not
print("Length of email id vs number found")
tns = df.email.map(lambda x: len(x))
print(tns.value_counts())

dfw = df.copy() ## dfw will not include nans 
dfw = dfw[dfw.email != 'nan']
dfw.shape
unique_articles = len(np.unique(dfw.article_id))# The number of unique articles that have at least one interaction
total_articles  = len(df_content.article_id)# The number of unique articles on the IBM platform
#unique_users    = len(np.unique(dfw.email.dropna().str.strip()))    # The number of unique users
unique_users    = dfw.email.dropna().nunique()
user_article_interactions =  df.shape[0]# The number of user-article interactions
print("The number of unique articles that have at least one interaction = ",unique_articles)
print("The number of unique articles on the IBM platform = ",total_articles)
print("The number of unique users = ",unique_users)
print("The number of user-article interactions = ",user_article_interactions)<jupyter_output>The number of unique articles that have at least one interaction =  714
The number of unique articles on the IBM platform =  1051
The number of unique users =  5148
The number of user-article interactions =  45993<jupyter_text>`4.` Use the cells below to find the most viewed **article_id**, as well as how often it was viewed.  After talking to the company leaders, the `email_mapper` function was deemed a reasonable way to map users to ids.  There were a small number of null values, and it was find using other information that all of these null values likely belonged to a single user.<jupyter_code>df1 = df.article_id.value_counts().sort_values(ascending = False)

most_viewed_article_id = str(df1.keys()[0]) 
max_views = df1.values[0]# The most viewed article in the dataset was viewed how many times?
print("The most viewed article in the dataset = ",most_viewed_article_id)
print("The most viewed article was viewed ",max_views, " times.")
## No need to change the code here - this will be helpful for later parts of the notebook
## Run this cell to 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()
## If you stored all your results in the variable names above, 
## you shouldn't need to change anything in this cell

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 your 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>Part II: Rank-Based RecommendationsUnlike in the earlier lessons, we don't actually have ratings for whether a user liked an article or not.  We only know that a user has interacted with an article.  In these cases, the popularity of an article can really only be based on how often an article was interacted with.`1.` Fill in the function below to return the **n** top articles ordered with most interactions as the top. Test your function using the tests below.<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 
    
    '''
    
    nmax = len(df.title.value_counts().keys())
    n = min(n,nmax)
    if(n < 1):
        n=1
    top_articles = list(df.title.value_counts().sort_values(ascending=False).keys()[: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 ids
    
    '''

    nmax = len(df.article_id.value_counts().keys())
    n = min(n,nmax)
    if(n < 1):
        n=1
    top_articles = list(df.article_id.value_counts().sort_values(ascending=False).keys()[:n])
    
    return top_articles # Return the top article ids
dfTop10 = pd.DataFrame({'article_id':get_top_article_ids(10),'title':get_top_articles(10)})
print('Top 10 articles ids ------------------------------')
dfTop10.head(10)
# Test your 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 your 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`1.` Use the function below to reformat the **df** dataframe to be shaped with users as the rows and articles as the columns.  * 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**. Use the tests to make sure the basic structure of your matrix matches what is expected by the solution.<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
    '''
    # Fill in the function here
    df1 = df.drop_duplicates().copy()
    # Group and unstack
    mat = df1.groupby(['user_id', 'article_id'])['title'].max().unstack()
    # Convert NaNs (no interaction) to False, existing interaction to True
    mat = mat.notna().astype(int)
    # True/False --> 1/0
    mat.index = mat.index.astype(int)
    
    return mat # return the user_item matrix 
    
user_item = create_user_item_matrix(df)

## Tests: You should just need to run this cell.  Don't change the code.
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!")
## Save to pickle file
user_item.to_pickle("user_item_matrix.p")<jupyter_output><empty_output><jupyter_text>`2.` Complete the function below which should take a user_id and provide an ordered list of the most similar users to that user (from most similar to least similar).  The returned result should not contain the provided user_id, as we know that each user is similar to him/herself. Because the results for each user here are binary, it (perhaps) makes sense to compute similarity as the dot product of two users. Use the tests to test your function.<jupyter_code>def find_similar_users(user_id, user_item=user_item,ret_series=False):
    '''
    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:
    most_similar_users - (list), if ret_series =False, an ordered list where the closest users (largest dot product users)
                    are listed first
    most_similar_users - (pd.Series) if ret_series =True, an ordered pd.Series where the closest users (largest dot product users)
                    are listed first
    
    Note my change:   ret_series =True then return pandas Series with user_ids 
                      similarity measure, instead of list 
                    
    Description:
    Computes the similarity of every pair of users based on the dot product
    Returns an ordered
    
    '''
    # compute similarity of each user to the provided user
    # compute as overlap (dot_product) over vectors
    a = np.dot(user_item ,user_item[user_item.index==user_id].transpose())
    b = a[:,0]
    p = pd.Series(data = b,index = user_item.index)
    
    # remove own user_id
    p.drop(labels = [user_id],inplace=True)
    p.sort_values(ascending=False,inplace=True)
    
    # Option to return the pandas series, not just keys
    most_similar_users  = p 
    if(ret_series==False):
        most_similar_users = list(p.keys())
    
       
    return most_similar_users # return a list of the users in order from most to least similar
        
# Do a spot check of your 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]))
## Ensure df.article_id is string type
## 
df.article_id = df.article_id.astype(str).copy()<jupyter_output><empty_output><jupyter_text>`3.` Now that you have a function that provides the most similar users to each user, you will want to use these users to find articles you can recommend.  Complete the functions below to return the articles you would recommend to each user.<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)
    '''
    # Your code here
    dfu = df[['article_id','title']].drop_duplicates().copy()
    
    ## Slow loop to ensure order, there is a better way to do this, ensure proper order
    article_names = []
    dfu = dfu[dfu.article_id.isin(article_ids)]

    for article_id in article_ids:
        dfus = dfu[dfu.article_id==article_id]
        if(dfus.shape[0] > 0): 
            #print(dfus.loc[0,1])
            article_names.append(dfus.iloc[0,1])
        else:
            article_names.append("")
    return article_names # Return the article names associated with list of article ids



## Convenient modification here
## seen = True   return articles that user_id has seen
##      = False  return articles that user_id has NOT seen
def get_user_articles(user_id, seen = True, user_item=user_item):
    '''
    INPUT:
    user_id - (int) a user id
    seen    - (True,False): return articles user (has,has not) seen 
    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 
    
    Description:
    Provides a list of the article_ids and article titles that have been seen by a user
    '''
    ### Alternate approach
    ## a = a.transpose().to_numpy()
    ## b = a[:,0]
    ## p = pd.Series(data=a,index=cols)
    ##list(p.keys()[np.where(p == 1)])
      
    # Your code here
    if(not user_id in user_item.index):
        return [],[]
    
    s = int(seen)
    article_ids = []
    cols = list(user_item.columns)
    for col in cols:
        if(user_item.loc[user_id,col] == s):
            article_ids.append(str(col))
            
    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
    
    '''
    
    ## Get articles user has read and has not read
    user_read_article_ids, names   = get_user_articles(user_id,True)
    user_unread_article_ids, names = get_user_articles(user_id,False)
    ## Get total num articles
    N = len(user_read_article_ids + user_unread_article_ids)
    
    m = min(m,N)
    ## Return most similar users
    rec_article_ids = []
    sim_user_ids = find_similar_users(user_id)
    #print(sim_user_ids[0:2])
    
    if(m > 0):
        for sim_user_id in sim_user_ids:
            sim_user_article_ids, sim_user_article_names = get_user_articles(sim_user_id,True)
            article_ids = list(set(user_unread_article_ids) & set(sim_user_article_ids))


            if(len(article_ids) > 0):
                rec_article_ids += article_ids
                # Update user_unread_article_ids, remove article_ids
                user_unread_article_ids =list(set(user_unread_article_ids) ^ set(article_ids))
                if(len(rec_article_ids) >= m):
                    #print("at return")
                    return rec_article_ids[0:m]
            
    
    ## This return should never be reached
    return [] # return your recommendations for this user_id    
# Test your functions here - No need to change this code - just run this cell

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>`4.` Now we are going to improve the consistency of the **user_user_recs** function from above.  * Instead of arbitrarily choosing when we obtain users who are all the same closeness to a given user - choose the users that have the most total article interactions before choosing those with fewer article interactions.* Instead of arbitrarily choosing articles from the user where the number of recommended articles starts below m and ends exceeding m, 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 you wrote earlier.<jupyter_code>def get_top_sorted_users(user_id, df=df, user_item=user_item):
    '''
    INPUT:
    user_id - (int)
    df - (pandas dataframe) df as defined at the top of the notebook 
    user_item - (pandas dataframe) matrix of users by articles: 
            1's when a user has interacted with an article, 0 otherwise
    
            
    OUTPUT:
    neighbors_df - (pandas dataframe) a dataframe with:
                    neighbor_id - is a neighbor user_id
                    similarity - measure of the similarity of each user to the provided user_id
                    num_interactions - the number of articles viewed by the user - if a u
                    
    Other Details - sort the neighbors_df by the similarity and then by number of interactions where 
                    highest of each is higher in the dataframe
     
    '''

    ## For a given user_id get similar 
    similar_s    = find_similar_users(user_id, user_item=user_item,ret_series=True)
    ## This is not necessarily number of articles viewed, but number of interactions
    ## by the user
    interact_s   = df.user_id.value_counts().copy()
    neighbors_df = similar_s.to_frame().join(interact_s)
    neighbors_df.columns  = ['similarity','n_interact']
    neighbors_df.reset_index(inplace=True)
    neighbors_df.sort_values(by = ['similarity','n_interact'], ascending = [False,False],inplace=True)
    
    return neighbors_df # Return the dataframe specified in the doc_string


def sorted_articles(df=df):
    '''
    Input: user-article interaction dataframe, df
    Output: dataframe containing article_id,number of interactions with article
            sorted descending on the number of interactions
    '''
    
    sorted_articles_df         = df.article_id.value_counts().to_frame().reset_index().copy()
    sorted_articles_df.columns = ['article_id','n_interact']
    sorted_articles_df.sort_values('n_interact',ascending=False,inplace=True)
    return sorted_articles_df


def user_user_recs_part2(user_id, m=10, df=df):
    '''
    INPUT:
    user_id - (int) a user id
    m - (int) the number of recommendations you want for the user
    df - user-article interaction dataframe
    
    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
    
    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:
    * 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. 
   
    * To be clear the sorting order is 
        1. User similarity
        2. Users with the most interactions
        3. Articles with the most total interactions
    '''
    
    ## Get articles user has read and has not read
    user_read_article_ids, names   = get_user_articles(user_id,True)
    user_unread_article_ids, names = get_user_articles(user_id,False)
    ## Get total num articles
    N = len(user_read_article_ids + user_unread_article_ids)
    
    m = min(m,N)
    ## Get most similar users
    rec_article_ids = []
    sim_user_ids = find_similar_users(user_id)
    
    
    ## Get dataframe with article_ids sorted according to num interactions
    sorted_articles_df = sorted_articles(df)
    
    rec_article_ids = [];ret_article_names = [];
    if(m > 0):
        ## Get closest users
        neighbors_df = get_top_sorted_users(user_id)
        sim_user_ids = list(neighbors_df.user_id)
        for sim_user_id in sim_user_ids:
            
            ## Get articles from this similar user, users here are sorted according
            ## to most user interactions. Sorting order is  similarity, then user_num_interactions
            sim_user_article_ids, sim_user_article_names = get_user_articles(sim_user_id)
            
            ## Get articles from the similar user that are unread by user_id
            article_ids = list(set(user_unread_article_ids) & set(sim_user_article_ids))
            
            ## Sort these articles according to number of :article: interactions
            dfs = sorted_articles_df[sorted_articles_df.article_id.isin(article_ids)]
            article_ids = list(dfs.sort_values(by = 'n_interact',ascending = False).article_id)


            if(len(article_ids) > 0):

                # Add found articles to the list
                rec_article_ids += article_ids  
                # Update unread articles
                user_unread_article_ids = list(set(user_unread_article_ids) ^ set(article_ids))

                ## Check if we found enough
                if(len(rec_article_ids) >= m):
                    rec_article_ids = rec_article_ids[0:m]
                    break;

  
    rec_article_names = get_article_names(rec_article_ids)
    return rec_article_ids, rec_article_names
# Quick spot check - don't change this code - just use it to test your functions
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)<jupyter_output>The top 10 recommendations for user 20 are the following article ids:
['1330.0', '1427.0', '1364.0', '1170.0', '1162.0', '1304.0', '1351.0', '1160.0', '1354.0', '1368.0']

The top 10 recommendations for user 20 are the following article names:
['insights from new york car accident reports', 'use xgboost, scikit-learn & ibm watson machine learning apis', 'predicting churn with the spss random tree algorithm', 'apache spark lab, part 1: basic concepts', 'analyze energy consumption in buildings', 'gosales transactions for logistic regression model', 'model bike sharing data with spss', 'analyze accident reports on amazon emr spark', 'movie recommender system with spark machine learning', 'putting a human face on machine learning']<jupyter_text>`5.` Use your functions from above to correctly fill in the solutions to the dictionary below.  Then test your dictionary against the solution.  Provide the code you need to answer each following the comments below.<jupyter_code>### Tests with a dictionary of results
user1_most_sim =  find_similar_users(1)[0]    # Find the user that is most similar to user 1 
user131_10th_sim = find_similar_users(131)[9] # 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.` If we were given a new user, which of the above functions would you be able to use to make recommendations?  Explain.  Can you think of a better way we might make recommendations?  Use the cell below to explain a better method for new users. **Provide your response here.***If the user is brand new, and we do not have any relevant information for that user(for use in content recommendations for example), then the best we can do is to recommend the most popular articles.* `7.` Using your existing functions, provide the top 10 recommended articles you would provide for the a new user below.  You can test your function against our thoughts to make sure we are all on the same page with how we might make a recommendation.<jupyter_code>new_user = '0.0'

# What would your recommendations be for this new user '0.0'? 
# As a new user, they have no observed articles, so all we can do is provide the most
# popular articles 

# For a new user, return the top 10 most popular articles
sorted_articles_df = sorted_articles()
#sorted_articles_df.head(13)
new_user_recs = sorted_articles_df.article_id[0:10] # Your recommendations here
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 Recommendations (EXTRA - NOT REQUIRED)Another method we might use to make recommendations is to perform a ranking of the highest ranked articles associated with some term.  You might 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.  `1.` Use the function body below to create a content based recommender.  Since there isn't one right answer for this recommendation tactic, no test functions are provided.  Feel free to change the function inputs if you decide you want to try a method that requires more input values.  The input values are currently set with one idea in mind that you may use to make content based recommendations.  One additional idea is that you might want to choose the most popular recommendations that meet your 'content criteria', but again, there is a lot of flexibility in how you might make these recommendations. This part is NOT REQUIRED to pass this project.  However, you may choose to take this on as an extra way to show off your skills.<jupyter_code>## First test out
def tokenize(text):

    """                                                                     
    Tokenize (Clean/Normalize etc...) text input                            
                                                                            
    Keyword Arguments:                                                      
    text: Message string to be tokenized                                    
                                                                            
    Returns:                                                                
    clean_tokens: List of clean tokens                                      
                                                                            
    """

    ## Remove punctuation                                                   
    text = re.sub(r"[^a-zA-Z0-9]", " ",text)

    tokens = word_tokenize(text)
    ## Remove stop words                                                    
    tokens = [w for w in tokens if w not in stopwords.words("english")]
    
    lemmatizer = WordNetLemmatizer()
    
    clean_tokens = []
    for tok in tokens:
        clean_tok = lemmatizer.lemmatize(tok,pos = 'n').lower().strip()
        ## Don't include tokens that are just numbers
        if(not clean_tok.isdigit()): 
            clean_tok = lemmatizer.lemmatize(clean_tok,pos = 'v')
            clean_tok = lemmatizer.lemmatize(clean_tok,pos = 'a')
            clean_tok = lemmatizer.lemmatize(clean_tok,pos = 'r')
        
            clean_tokens.append(clean_tok)

    
    return clean_tokens


def get_wordcnt_df(dfc):
    '''
    INPUT:
    dfc -- article content dataframe, at this point with duplicates
    dropped
    OUTPUT:
    wordcnt_df -- each row is an article name, each column is a word
                   entries are the number of times column word appears in
                   row article
    '''
    
    ## User doc_full_name 
    corpus = list(dfc.doc_full_name)
    
    # article_ids are rows
    # vect.get_feature_names() will be columns
    vect = CountVectorizer(tokenizer=tokenize)
    X = vect.fit_transform(corpus)
    wordcnt_df = pd.DataFrame(X.toarray(),index = list(dfc.article_id),columns = vect.get_feature_names())
    
    return wordcnt_df
    
    


def get_sim_articles(article_id,wordcnt_df):
    '''
    INPUT:
    article_id  -- id of article for which we want to return most 
                  similar articles

    wordcnt_mat -- each row is an article name, each column is a word
                   entries are the number of times column word appears in
                   row article
                   
    
    
    OUTPUT:
    sim_article_ids -- list of similar article_ids, ordered from
    most to least similar
    '''
    
    if(article_id not in wordcnt_df.index):
        ## article was not found, an empty list
        return []
    a = np.dot(wordcnt_df,wordcnt_df[wordcnt_df.index==article_id].transpose())
    
    b = a[:,0]
    p = pd.Series(data = b,index = wordcnt_df.index)
    
    # remove own article_id
    p.drop(labels = [article_id],inplace=True)
    p.sort_values(ascending=False,inplace=True)
    
   
    #most_similar_articles  = p 
    most_similar_articles = list(p.keys())
    
    
    return most_similar_articles
        

def make_content_recs(user_id,wordcnt_df,m=5):
    '''
    INPUT:
    user_id -- input user for which to get article recommendations
    wordcnt_df -- the word count matrix (see above)
    m -- the number of recommendations requested
    
    OUTPUT:
    list of article_ids,article_titles recommended
    '''
    ## NUM_SIM = number of similar articles to consider 
    ## per user article read
    NUM_SIM = 10
    ## Get articles user has read and has not read
    user_read_article_ids, names   = get_user_articles(user_id,True)
    if(len(user_read_article_ids)==0):
        ## User has no articles, just get most popular articles
        rec_article_ids  = get_top_article_ids(m)
        rec_article_ids = [int(float(x)) for x in rec_article_ids]
        return rec_article_ids
    
    user_unread_article_ids, names = get_user_articles(user_id,False)
    
    
    ## Recommend up to what the user has not read
    ## Convert type, in user_(un)read_article_ids  are not ints
    ##
    user_read_article_ids = [int(float(x)) for x in user_read_article_ids]
    user_unread_article_ids = [int(float(x)) for x in user_unread_article_ids]
    
    N = len(user_unread_article_ids)
    
    ## Return number of articles equal to the min of these
    n = min(m,N) 
    
    ## do a random shuffle of these articles
    random.shuffle(user_read_article_ids)
    
    ## loop through user read articles and attach NUM_SIM that 
    ## are unread
    rec_article_ids = []
       
    for article_id in user_read_article_ids:
        ## Get NUM_SIM similar articles
        sim_article_ids = get_sim_articles(article_id,wordcnt_df)[0:NUM_SIM]
        
        for sim_article_id in sim_article_ids:
            if(sim_article_id in user_unread_article_ids):
                ## add to articles to be recommended
                rec_article_ids.append(sim_article_id)
                ## update unread article list
                user_unread_article_ids.remove(sim_article_id)
                if(len(rec_article_ids) >= n):
                    return rec_article_ids[0:n]
                
                
    ## Not enough articles get most popular
    ## would have to remove duplicates
    #if (len(rec_article_ids) < n):
    #    extra_article_ids = get_top_article_ids(n - len(article_ids))
    #    extra_article_ids = [int(float(x)) for x in extra_article_ids] 
    #    rec_article_ids += extra_article_ids
        
    return rec_article_ids

## Simple Test
## Create the wordcnt_df matrix, this runs CountVectorizer (bag of words)
wordcnt_df =  get_wordcnt_df(df_content)

## Get recommended articles based on article similarity,
## overlap of article entries in the word cnt matrix
id = 2
rec_article_ids = make_content_recs(id,wordcnt_df)
rec = pd.DataFrame({'article_id':rec_article_ids})
rec_df = pd.merge(rec,df_content,on='article_id',how='inner')
rec_df


## Note the repeated appearance of 'Deep Learning' in doc_full_name<jupyter_output><empty_output><jupyter_text>`2.` Now that you have put together your content-based recommendation system, use the cell below to write a summary explaining how your content based recommender works.  Do you see any possible improvements that could be made to your function?  Is there anything novel about your content based recommender? This part is NOT REQUIRED to pass this project.  However, you may choose to take this on as an extra way to show off your skills. **Write an explanation of your content based recommendation system here.***The simple approach we have here uses only the doc_full_name attributefor each article. We treat the set of doc_full_names as a corpus and theword content of these is cleaned and lemmatized to produce a set of significant words for the corpus. We then produce a count matrix from this set where each row is an article, each column is a significant word, and each entry is the number of times the word is found in the article.Article similarity is scored according to the dot product of article (row) entries in the count matrix.**The make_content_recs routine returns 'm' article recommendations for user = user_id and works as follows.*    1. Get articles read and unread by user_id. If none read (user    is brand new) return the most popular articles    2. Make a random shuffle of read articles to make things interesting    3. Loop read articles        a. For each retrieve the NUM_SIM most similar articles        b. For those articles that are also unread by the user,           append to recommended articles list.        *There are many alternative and better approaches, e.g. instead ofuser_item matrix, we could take into account the number of times a userhas interacted with an article. We might want to find similar articles first for articles the user has interacted with the most. Moreover, doc_description and possibly doc_body could be used in the similarity scoring. The code is above is not thoroughly tested and doesn't coversome edge cases probably.* `3.` Use your content-recommendation system to make recommendations for the below scenarios based on the comments.  Again no tests are provided here, because there isn't one right answer that could be used to find these content based recommendations. This part is NOT REQUIRED to pass this project.  However, you may choose to take this on as an extra way to show off your skills.<jupyter_code># make recommendations for a brand new user
id = 100000
rec_100000_article_ids = make_content_recs(id,wordcnt_df)
print('Article ids recommended to new user')
print(rec_100000_article_ids)
# make a recommendations for a user who only has interacted with article id '1427.0'
# For this just use the get_sim_articles() routine
rec_1427_article_ids = get_sim_articles(int(float('1427.0')),wordcnt_df)
print(len(rec_1427_article_ids))

# We get zero here because article 1427 does not have an entry in df_content
# So we would just default again to recommending top articles<jupyter_output>Article ids recommended to new user
[1429, 1330, 1431, 1427, 1364]
0<jupyter_text>Part V: Matrix FactorizationIn this part of the notebook, you will build use matrix factorization to make article recommendations to the users on the IBM Watson Studio platform.`1.` You should have already created a **user_item** matrix above in **question 1** of **Part III** above.  This first question here will just require that you run the cells to get things set up for the rest of **Part V** of the notebook.<jupyter_code># Load the matrix here
user_item_matrix = pd.read_pickle('user_item_matrix.p')
# quick look at the matrix
user_item_matrix.head()<jupyter_output><empty_output><jupyter_text>`2.` In this situation, you 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.  Use the cell to perfrom SVD, and explain why this is different than in the lesson.<jupyter_code># Perform SVD on the User-Item Matrix Here

u, s, vt = np.linalg.svd(user_item_matrix)# use the built in to get the three matrices<jupyter_output><empty_output><jupyter_text>**Provide your response here.**In the lesson, we were dealing with breaking down a user-movie rating matrix (**R**) into components that express the user-latent_feature (**S**) and movie-latent_feature (**V**) relationships, along with a weighting (**D**) for the significance of each feature. The values in the matrix are numerical (ratings). When all values are present, the usual SVD algorithm can be applied, but not when even one value is missing. We saw that in the missing values case we could create a similar breakdown, by writing down the equations **R = SVD**, for only the existing values in R and calculating SVD via the generalized gradient method. This is sort of like the optimization problem *min*(**R - SVD** ). Here we are dealing with an interaction matrix, so the values are boolean (0 or 1). No valueswill be missing of course, either a user has interacted with an article or not. Therefore,the conventional decomposition approach is applicable. `3.` Now for the tricky part, how do we choose the number of latent features to use?  Running the below cell, you 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.  Run the cell below to get an idea of how the accuracy improves as we increase the number of latent features.<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_matrix, 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>`4.` 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.  Use the code from question 3 to understand the impact on accuracy of the training and test sets of data with different numbers of latent features. Using the split below: * 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
    
    '''
    # Your code here
    user_item_train = create_user_item_matrix(df_train)
    user_item_test  = create_user_item_matrix(df_test)
    test_idx        =  list(np.unique(df_test.user_id))
    test_arts       =  list(np.unique(df_test.article_id))
    
    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)
print(user_item_train.shape)
print(user_item_test.shape)
print(len(test_idx))
print(len(test_arts))
## Determine users common to both sets
users_common = list(set(user_item_test.index) & set(user_item_train.index))
## Determine articles common to both sets
arts_common  = list(set(user_item_test.columns) & set(user_item_train.columns))
print("\nHow many users can we make predictions for in the test set? ", len(users_common))  
print("\nHow many users are we not able to make predictions for")
print("because of the cold start problem? ", user_item_test.shape[0] - len(users_common))
print("\nHow many articles can we make predictions for in the test set? ", len(arts_common))  
print("\nHow many articles are we not able to make predictions for because of the cold start problem? ",
      user_item_test.shape[1] - len(arts_common))

## Subset test set according to user_ids
user_item_test   = user_item_test[user_item_test.index.isin(users_common)]


## Get subsetting bool lists for subsetting estimate of user_item_train matrix
col_sub = list(user_item_train.columns.isin(arts_common))
row_sub = list(user_item_train.index.isin(users_common))

## Demonstrate alignment of indices to demonstrate that our usage of these lists
## is valid
print("Index alignment test ---- ")
user_item_train_sub = user_item_train.loc[row_sub,col_sub]
print(list(user_item_test.index)   == list(user_item_train_sub.index))
print(list(user_item_test.columns) == list(user_item_train_sub.columns))
# Replace the values in the dictionary below
a = 662 
b = 574 
c = 20 
d = 0 


sol_4_dict = {
    'How many users can we make predictions for in the test set?': c, 
    'How many users in the test set are we not able to make predictions for because of the cold start problem?': a, 
    'How many articles can we make predictions for in the test set?': b,
    'How many articles in the test set are we not able to make predictions for because of the cold start problem?': d
}

t.sol_4_test(sol_4_dict)<jupyter_output>Awesome job!  That's right!  All of the test articles 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>`5.` 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. This will require combining what was done in questions `2` - `4`.Use the cells below to explore how well SVD works towards making predictions for recommendations on the test data.<jupyter_code># fit SVD on the user_item_train matrix
u_train, s_train, vt_train = np.linalg.svd(user_item_train,full_matrices=False)

print(u_train.shape,len(s_train),vt_train.shape)
# Use these cells to see how well you can use the training 
# decomposition to predict on test data
#
# We iterate over latent features exactly as before just calculating the error
# for the test subset of users and articles 
#
# Note I used u_test_size in place of df.shape[0], just felt that
# this was more appropriate
#
u_test_size = user_item_test.shape[0]*user_item_test.shape[1]
num_latent_feats = np.arange(5,700+10,20)
sum_errs = []
neg_ones = []
for k in num_latent_feats:
    # restructure with k latent features
    s_new, u_new, vt_new = np.diag(s_train[:k]), u_train[:, :k], vt_train[:k, :]
    
    # take dot product
    user_item_est = np.around(np.dot(np.dot(u_new, s_new), vt_new))
    
    user_item_est_sub = user_item_est[row_sub,:]
    user_item_est_sub = user_item_est_sub[:,col_sub]
    
    # compute error for each prediction to actual value
    diffs = np.subtract(user_item_test, user_item_est_sub)
    
    neg_ones.append(len(np.where(diffs==-1)[0]))
    # total errors and keep track of them
    err = np.sum(np.sum(np.abs(diffs)))
    sum_errs.append(err)
    
plt.figure(figsize=(5,5))
plt.plot(num_latent_feats, 1 - np.array(sum_errs)/u_test_size);
plt.xlabel('Number of Latent Features');
plt.ylabel('Accuracy');
plt.title('Accuracy vs. Number of Latent Features');

plt.figure(figsize=(5,5))
plt.plot(num_latent_feats, neg_ones)
plt.title("Number of -1s in Difference Matrix vs. Latent Features")
plt.ylabel('Number of -1s in Difference Matrix')
plt.xlabel('Number of Latent Features')
### Show accuracy from direct_comparison

user_item_est = user_item_train.to_numpy()
user_item_est = user_item_est[row_sub,:]
user_item_est = user_item_est[:,col_sub]
diffs = np.subtract(user_item_test, user_item_est)
err = np.sum(np.sum(np.abs(diffs)))
print("Asymptote Accuracy = ", 1- err/u_test_size)<jupyter_output>Asymptote Accuracy =  0.9644599303135889<jupyter_text>`6.` Use the cell below to comment on the results you found in the previous question. Given the circumstances of your results, discuss what you might do to determine if the recommendations you make with any of the above recommendation systems are an improvement to how users currently find articles?  *What I have done here is to simply show the change in error between increasingly better approximations to user_item_train (via the number eigen-modes included) and  user_item_test for the 20 in common users.* *It is a bit serendipitous that the result is slightly better with fewer eigenvalues. For this particular case, more eigenvectors contributing creates more **1**s in the relevant part of the estimate matrix that do not correspond to **1**s in the test matrix,  thereby  increasing the error. This is demonstrated in the second plot above, which shows the increase in the number of **-1s** in the difference matrix as the number of latent features increases. The error asymptotes once the estimate matrix is essentially the same as the  original user_item_train matrix.*  *Of course, we have the full user_item_train to begin with, so we can just compare it directly to user_item_test. It should be noticed that a careful train and test separation of the original data frame 'df', could possibly result in user_item_train = user_item_test for the in common users; for instance, if a unique set of all relevant user-article interactions exists in the train set, and all duplicate interactions are in the test set.*  *To determine the quality of and compare our recommendation system approaches, I would look at the number of user-article interactions over time. One recommendation system would be used for one group and another for a control group. The statistical methods discussed in the A/B testing section of the lesson would be relevant here. Multiple approaches might be tested simultaneously applied using ANOVA (Analysis of Variance), but we would then want to rotate the methods across the user population, to ensure the results are not due to variations in the user population itself.*    ExtrasUsing your workbook, you could now save your recommendations for each user, develop a class to make new predictions and update your results, and make a flask app to deploy your results.  These tasks are beyond what is required for this project.  However, from what you learned in the lessons, you certainly capable of taking these tasks on to improve upon your work here! Conclusion> Congratulations!  You have reached the end of the Recommendations with IBM project! > **Tip**: Once you are satisfied with your work here, check over your report to make sure that it is satisfies all the areas of the rubric (found on the project submission page at the end of the lesson). You should also probably remove all of the "Tips" like this one so that the presentation is as polished as possible. Directions to Submit> Before you submit your project, you need to create a .html or .pdf version of this notebook in the workspace here. To do that, run the code cell below. If it worked correctly, you should get a return code of 0, and you should see the generated .html file in the workspace directory (click on the orange Jupyter icon in the upper left).> Alternatively, you can download this report as .html via the **File** > **Download as** submenu, and then manually upload it into the workspace directory by clicking on the orange Jupyter icon in the upper left, then using the Upload button.> Once you've done this, you can submit your project by clicking on the "Submit Project" button in the lower right here. This will create and submit a zip file with this .ipynb doc and the .html or .pdf version you created. Congratulations!<jupyter_code>from subprocess import call
call(['python', '-m', 'nbconvert', 'Recommendations_with_IBM.ipynb'])<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07671140463002414, 'fraction_numerical': 0.01168157932111916, 'mean_word_length': 3.6258520160959185, 'pattern_counts': {'":': 0, '<': 75, '<?xml version=': 0, '>': 88, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 25, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '21', 'dir': 'jupyter-structured-clean-dedup', 'id': '231695', 'license': 'MIT', 'n_tokens_mistral': 16464, 'n_tokens_neox': 15651, 'n_words': 6550, 'path': 'Recommendations_with_IBM.ipynb', 'repo_name': 'jfude/DSND-Recommendations_with_IBM', 'size': '56328'}