{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# KNN (K-Nearest-Neighbors)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"KNN is a simple concept: define some distance metric between the items in your dataset, and find the K closest items. You can then use those items to predict some property of a test item, by having them somehow \"vote\" on it.\n",
"\n",
"As an example, let's look at the MovieLens data. We'll try to guess the rating of a movie by looking at the 10 movies that are closest to it in terms of genres and popularity.\n",
"\n",
"To start, we'll load up every rating in the data set into a Pandas DataFrame:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>user_id</th>\n",
" <th>movie_id</th>\n",
" <th>rating</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>0</td>\n",
" <td>50</td>\n",
" <td>5</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>0</td>\n",
" <td>172</td>\n",
" <td>5</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>0</td>\n",
" <td>133</td>\n",
" <td>1</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>196</td>\n",
" <td>242</td>\n",
" <td>3</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>186</td>\n",
" <td>302</td>\n",
" <td>3</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" user_id movie_id rating\n",
"0 0 50 5\n",
"1 0 172 5\n",
"2 0 133 1\n",
"3 196 242 3\n",
"4 186 302 3"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import pandas as pd\n",
"\n",
"r_cols = ['user_id', 'movie_id', 'rating']\n",
"ratings = pd.read_csv('ml-100k/u.data', sep='\\t', names=r_cols, usecols=range(3))\n",
"ratings.head()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now, we'll group everything by movie ID, and compute the total number of ratings (each movie's popularity) and the average rating for every movie:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead tr th {\n",
" text-align: left;\n",
" }\n",
"\n",
" .dataframe thead tr:last-of-type th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr>\n",
" <th></th>\n",
" <th colspan=\"2\" halign=\"left\">rating</th>\n",
" </tr>\n",
" <tr>\n",
" <th></th>\n",
" <th>size</th>\n",
" <th>mean</th>\n",
" </tr>\n",
" <tr>\n",
" <th>movie_id</th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>452</td>\n",
" <td>3.878319</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>131</td>\n",
" <td>3.206107</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>90</td>\n",
" <td>3.033333</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>209</td>\n",
" <td>3.550239</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>86</td>\n",
" <td>3.302326</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" rating \n",
" size mean\n",
"movie_id \n",
"1 452 3.878319\n",
"2 131 3.206107\n",
"3 90 3.033333\n",
"4 209 3.550239\n",
"5 86 3.302326"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import numpy as np\n",
"\n",
"movieProperties = ratings.groupby('movie_id').agg({'rating': [np.size, np.mean]})\n",
"movieProperties.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The raw number of ratings isn't very useful for computing distances between movies, so we'll create a new DataFrame that contains the normalized number of ratings. So, a value of 0 means nobody rated it, and a value of 1 will mean it's the most popular movie there is."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>size</th>\n",
" </tr>\n",
" <tr>\n",
" <th>movie_id</th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>0.773585</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>0.222985</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>0.152659</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>0.356775</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>0.145798</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" size\n",
"movie_id \n",
"1 0.773585\n",
"2 0.222985\n",
"3 0.152659\n",
"4 0.356775\n",
"5 0.145798"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"movieNumRatings = pd.DataFrame(movieProperties['rating']['size'])\n",
"movieNormalizedNumRatings = movieNumRatings.apply(lambda x: (x - np.min(x)) / (np.max(x) - np.min(x)))\n",
"movieNormalizedNumRatings.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now, let's get the genre information from the u.item file. The way this works is there are 19 fields, each corresponding to a specific genre - a value of '0' means it is not in that genre, and '1' means it is in that genre. A movie may have more than one genre associated with it.\n",
"\n",
"While we're at it, we'll put together everything into one big Python dictionary called movieDict. Each entry will contain the movie name, list of genre values, the normalized popularity score, and the average rating for each movie:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"movieDict = {}\n",
"with open(r'ml-100k/u.item', encoding=\"ISO-8859-1\") as f:\n",
" temp = ''\n",
" for line in f:\n",
" #line.decode(\"ISO-8859-1\")\n",
" fields = line.rstrip('\\n').split('|')\n",
" movieID = int(fields[0])\n",
" name = fields[1]\n",
" genres = fields[5:25]\n",
" genres = map(int, genres)\n",
" movieDict[movieID] = (name, np.array(list(genres)), movieNormalizedNumRatings.loc[movieID].get('size'), movieProperties.loc[movieID].rating.get('mean'))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For example, here's the record we end up with for movie ID 1, \"Toy Story\":"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"('Toy Story (1995)', array([0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), 0.7735849056603774, 3.8783185840707963)\n"
]
}
],
"source": [
"print(movieDict[1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now let's define a function that computes the \"distance\" between two movies based on how similar their genres are, and how similar their popularity is. Just to make sure it works, we'll compute the distance between movie ID's 2 and 4:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"0.8004574042309892"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from scipy import spatial\n",
"\n",
"def ComputeDistance(a, b):\n",
" genresA = a[1]\n",
" genresB = b[1]\n",
" genreDistance = spatial.distance.cosine(genresA, genresB)\n",
" popularityA = a[2]\n",
" popularityB = b[2]\n",
" popularityDistance = abs(popularityA - popularityB)\n",
" return genreDistance + popularityDistance\n",
" \n",
"ComputeDistance(movieDict[2], movieDict[4])\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Remember the higher the distance, the less similar the movies are. Let's check what movies 2 and 4 actually are - and confirm they're not really all that similar:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"('GoldenEye (1995)', array([0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]), 0.22298456260720412, 3.2061068702290076)\n",
"('Get Shorty (1995)', array([0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), 0.3567753001715266, 3.550239234449761)\n"
]
}
],
"source": [
"print(movieDict[2])\n",
"print(movieDict[4])\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now, we just need a little code to compute the distance between some given test movie (Toy Story, in this example) and all of the movies in our data set. When the sort those by distance, and print out the K nearest neighbors:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Liar Liar (1997) 3.156701030927835\n",
"Aladdin (1992) 3.8127853881278537\n",
"Willy Wonka and the Chocolate Factory (1971) 3.6319018404907975\n",
"Monty Python and the Holy Grail (1974) 4.0664556962025316\n",
"Full Monty, The (1997) 3.926984126984127\n",
"George of the Jungle (1997) 2.685185185185185\n",
"Beavis and Butt-head Do America (1996) 2.7884615384615383\n",
"Birdcage, The (1996) 3.4436860068259385\n",
"Home Alone (1990) 3.0875912408759123\n",
"Aladdin and the King of Thieves (1996) 2.8461538461538463\n"
]
}
],
"source": [
"import operator\n",
"\n",
"def getNeighbors(movieID, K):\n",
" distances = []\n",
" for movie in movieDict:\n",
" if (movie != movieID):\n",
" dist = ComputeDistance(movieDict[movieID], movieDict[movie])\n",
" distances.append((movie, dist))\n",
" distances.sort(key=operator.itemgetter(1))\n",
" neighbors = []\n",
" for x in range(K):\n",
" neighbors.append(distances[x][0])\n",
" return neighbors\n",
"\n",
"K = 10\n",
"avgRating = 0\n",
"neighbors = getNeighbors(1, K)\n",
"for neighbor in neighbors:\n",
" avgRating += movieDict[neighbor][3]\n",
" print (movieDict[neighbor][0] + \" \" + str(movieDict[neighbor][3]))\n",
" \n",
"avgRating /= K"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"While we were at it, we computed the average rating of the 10 nearest neighbors to Toy Story:"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"3.3445905900235564"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"avgRating"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"How does this compare to Toy Story's actual average rating?"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"('Toy Story (1995)',\n",
" array([0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),\n",
" 0.7735849056603774,\n",
" 3.8783185840707963)"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"movieDict[1]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Not too bad!\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Activity"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Our choice of 10 for K was arbitrary - what effect do different K values have on the results?\n",
"\n",
"Our distance metric was also somewhat arbitrary - we just took the cosine distance between the genres and added it to the difference between the normalized popularity scores. Can you improve on that?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.5"
}
},
"nbformat": 4,
"nbformat_minor": 1
}