Skip to content

Instantly share code, notes, and snippets.

@avli
Last active January 29, 2017 17:19
Show Gist options
  • Save avli/2eedae2229ac5a6200c0ff774c584e21 to your computer and use it in GitHub Desktop.
Save avli/2eedae2229ac5a6200c0ff774c584e21 to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from sklearn.model_selection import train_test_split\n",
"from sklearn.metrics import mean_absolute_error"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We will use the data from the MovieLens project. The latest version of the dataset can be obtained from [here](http://grouplens.org/datasets/movielens/)."
]
},
{
"cell_type": "code",
"execution_count": 303,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"df = pd.read_csv(\"/Users/andrey/Downloads/ml-latest-small/ratings.csv\")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>userId</th>\n",
" <th>movieId</th>\n",
" <th>rating</th>\n",
" <th>timestamp</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>1</td>\n",
" <td>31</td>\n",
" <td>2.5</td>\n",
" <td>1260759144</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>1</td>\n",
" <td>1029</td>\n",
" <td>3.0</td>\n",
" <td>1260759179</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>1</td>\n",
" <td>1061</td>\n",
" <td>3.0</td>\n",
" <td>1260759182</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>1</td>\n",
" <td>1129</td>\n",
" <td>2.0</td>\n",
" <td>1260759185</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>1</td>\n",
" <td>1172</td>\n",
" <td>4.0</td>\n",
" <td>1260759205</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" userId movieId rating timestamp\n",
"0 1 31 2.5 1260759144\n",
"1 1 1029 3.0 1260759179\n",
"2 1 1061 3.0 1260759182\n",
"3 1 1129 2.0 1260759185\n",
"4 1 1172 4.0 1260759205"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"df.head()"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Adjust indexes (Python indexes arrays starting from zero)\n",
"df.userId -= 1; df.movieId -= 1"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>userId</th>\n",
" <th>movieId</th>\n",
" <th>rating</th>\n",
" <th>timestamp</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>0</td>\n",
" <td>30</td>\n",
" <td>2.5</td>\n",
" <td>1260759144</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>0</td>\n",
" <td>1028</td>\n",
" <td>3.0</td>\n",
" <td>1260759179</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>0</td>\n",
" <td>1060</td>\n",
" <td>3.0</td>\n",
" <td>1260759182</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>0</td>\n",
" <td>1128</td>\n",
" <td>2.0</td>\n",
" <td>1260759185</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>0</td>\n",
" <td>1171</td>\n",
" <td>4.0</td>\n",
" <td>1260759205</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" userId movieId rating timestamp\n",
"0 0 30 2.5 1260759144\n",
"1 0 1028 3.0 1260759179\n",
"2 0 1060 3.0 1260759182\n",
"3 0 1128 2.0 1260759185\n",
"4 0 1171 4.0 1260759205"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"df.head()"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"collapsed": false,
"scrolled": true
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>userId</th>\n",
" <th>movieId</th>\n",
" <th>rating</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>0.0</td>\n",
" <td>0.0</td>\n",
" <td>2.5</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>0.0</td>\n",
" <td>1.0</td>\n",
" <td>3.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>0.0</td>\n",
" <td>2.0</td>\n",
" <td>3.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>0.0</td>\n",
" <td>3.0</td>\n",
" <td>2.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>0.0</td>\n",
" <td>4.0</td>\n",
" <td>4.0</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" userId movieId rating\n",
"0 0.0 0.0 2.5\n",
"1 0.0 1.0 3.0\n",
"2 0.0 2.0 3.0\n",
"3 0.0 3.0 2.0\n",
"4 0.0 4.0 4.0"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"user_mappings = {}; movie_mappings = {}\n",
"for i, user_id in enumerate(df.userId.unique()):\n",
" user_mappings[user_id] = i\n",
" \n",
"movie_mappings = {}\n",
"for i, movie_id in enumerate(df.movieId.unique()):\n",
" movie_mappings[movie_id] = i\n",
" \n",
"new_users = []; new_movies = []\n",
"for row in df.iterrows():\n",
" new_users.append(user_mappings[row[1].userId])\n",
" new_movies.append(movie_mappings[row[1].movieId])\n",
" \n",
" \n",
"new_df = pd.DataFrame([pd.Series(new_users), pd.Series(new_movies), df.rating]).transpose()\n",
"new_df.columns = [\"userId\", \"movieId\", \"rating\"]\n",
"new_df.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we need to construct a sparse matrix from the data we have. The matrix should have $m$ row and $n$ columns, where $m$ is the number of users and $n$ is the number of movies. The ratiging $m^{th}$ user gave to $n^{th}$ movie is the cross of $m^{th}$ row and $n^{th}$ column."
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"num_users = new_df.userId.unique().size;\n",
"num_movies = new_df.movieId.unique().size\n",
"M = np.matrix(np.zeros(shape=(num_users, num_movies)))\n",
"for row in new_df.iterrows():\n",
" M[int(row[1].userId), int(row[1].movieId)] = row[1].rating"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# It's time to decompose the matrix\n",
"U, S, V = np.linalg.svd(M, full_matrices=False)"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"collapsed": false,
"scrolled": true
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAZAAAAEKCAYAAAA8QgPpAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzt3XmcTnX/x/HXZxaGIWvkTnfIvg6DUrJNRLLUINwxkS1L\n3W1ChbRHC9nLUpQlUkLc0Yyyb9nXyZJpR8mQbJ/fH9fhN02My2WuOdc183k+HucxZ/me63pfY8xn\nzvmec76iqhhjjDFXKsTtAMYYY4KTFRBjjDE+sQJijDHGJ1ZAjDHG+MQKiDHGGJ9YATHGGOMTKyDG\nGGN8YgXEGGOMT6yAGGOM8UmY2wHSS8GCBbVYsWI+73/8+HEiIyPTL1AGCMbMEJy5gzEzBGfuYMwM\nwZn7+PHj7Ny585CqXuvTC6hqppiio6P1asTHx1/V/m4IxsyqwZk7GDOrBmfuYMysGpy54+PjFVin\nPv7etVNYxhhjfGIFxBhjjE+sgBhjjPGJFRBjjDE+sQJijDHGJ1ZAjDHG+MQKiDHGGJ9YAQFOnD7B\nzIMz2fHrDk6cPoHaML/GGHNZVkCAebvnMWbvGNrMakPkS5EMXTHU7UjGGBPwsnwBWZ20mvtm3QfA\n1l+2AjB23Vg3IxljTFDI8gXkpvw3/WPdOT3HzkM77VSWMcakwa8FREQai8guEUkUkX4X2V5HRDaI\nyBkRaZVqW5yI7HGmOH9lLJiz4N+Ws4Vm48DRA5QbVY7BCYP99bbGGBP0/FZARCQUGAU0AcoD7USk\nfKpm3wEPAB+m2jc/MAi4GagJDBKRfP7KuvWhrfw7578BaFa62YX1Q74awvZft/vrbY0xJqj58wik\nJpCoqntV9RQwHWiRsoGq7lfVzcC5VPveCXyhqkdU9TfgC6Cxv4JWKFSB5ys8zwNRD9A9uvvft42u\nQJmRZVjz/Rp/vb0xxgQlf44Hcj1wMMVyEp4jCl/3vT51IxHpBnQDKFy4MAkJCT4FBch/Lj9xeeI4\n8u0RAMrmLsvdRe7m2JljfPL9J9wx+Q5a/qslrYu2Jnd4bp/fJz0lJydf1Wd2SzDmDsbMEJy5gzEz\nBGfu5OTkq9rfnwVELrLO215pr/ZV1fHAeIDq1atrvXr1vA6XWkJCAuf3P3jLQa7LdR1hIZ5vT9/f\n+tLts25M2TeFKd9N4ZP7PqFF2RZpvFrGSJk5mARj7mDMDMGZOxgzQ3DmvtqC589TWEnADSmWiwI/\nZMC+V63oNUUvFA+AEvlKsLjjYr7s+CU5w3PSckZL6kyqw8LEhRz761hGxTLGmIDizwKyFiglIsVF\nJBvQFpjr5b6LgEYiks/pPG/krHNV/eL1SXo0iR7RPfj6u69p8kETKo6pyE/JP7kdzRhjMpzfCoiq\nngF64/nFvwOYqarbRGSIiDQHEJEaIpIEtAbGicg2Z98jwPN4itBaYIizznX5cuRjzN1jONz3MLPb\nzOan5J8o8noRHl/0OMmnru58ojHGBBN/9oGgqguABanWDUwxvxbP6amL7TsRmOjPfFcjf4783Fvu\nXua3n8+49eN4Y9UbzNw+k8ktJhNTIsbteMYY43dZ/k70q3VHiTv4qPVHrOi8glzZcnHHlDuInRl7\n4bEoxhiTWVkBSSe1bqjFmi5rGFx3MF98+wVVxlbhteWvcU5T3+JijDGZgxWQdJQ7e24G1RvEvkf2\n0bxMc55a/BRd53bl9NnTbkczxph0ZwXEDwrkLMDHbT5mQO0BTNw4kRvfupHx68fz15m/3I5mjDHp\nxgqIn4gILzR4gQXtF/Cv3P+i+7zuVH+nOquTVrsdzRhj0oUVED8SEZqUasKqLquY1XoWPyX/RO1J\ntZmwYYLb0Ywx5qpZAckAYSFhxJaPJbFPIjHFY+j6WVeeS3iOE6dPuB3NGGN8ZgUkA+WJyMMnbT+h\nTYU2DF46mDIjyzB181S7UssYE5SsgGSwiLAIpreaztIHllI4sjAd5nSgxjs1mPTNJM6cO+N2PGOM\n8ZoVEJfUubEOa7quYco9Uzh+6jid53am6riqJOxPcDuaMcZ4xQqIi0IkhPsr38+OXjuY3WY2x/46\nRv336tN2Vlt7yq8xJuBZAQkAIsK95e5lR68dDK47mI+2f0TDKQ3ZeWin29GMMeaSrIAEkBzhORhU\nbxDTYqex+/Buqo6rypsr3+TsubNuRzPGmH+wAhKA2lRow/Ze27mjxB089r/HqDWhFpt+2uR2LGOM\n+RsrIAHqulzXMbftXKbFTuPA0QNEj4+m3+J+du+IMSZgWAEJYCJC24pt2dFrB3FV4nh1+atUGlOJ\nL779wu1oxhhjBSQY5M+RnwktJvBlxy8JlVAaTW1ExzkdOXr6qNvRjDFZmBWQIFK/eH02P7SZp29/\nmmlbpxG3No5J30xCVd2OZozJgqyABJmIsAheaPACG7pt4IYcN9B5bmdazmjJoROH3I5mjMlirIAE\nqUqFKzE8ajhv3fkWCxMXUnZkWV786kUbc8QYk2GsgASxEAnhkVseYU2XNdxS9BaeiX+GqHFRfHXg\nK7ejGWOyACsgmUCV66owr/08Pv/P55w8c5K6k+vS/bPu/Hn6T7ejGWMyMSsgmUjjko3Z+tBWHq/1\nOO9seIc6k+vw/R/fux3LGJNJWQHJZCKzRTKs0TA+bfspOw/tpPo71Vm6f6nbsYwxmZAVkEyqWZlm\nrHpwFXmy56HB+w14ddmrNnCVMSZdWQHJxCoUqsDarmtpVb4V/Zb0o+X0lhz584jbsYwxmYQVkEwu\nd/bcTI+dzttN3mZh4kIqjK7Apzs/dTuWMSYTsAKSBYgIvWv2ZnWX1RSOLEzLGS1pN7sdh08cdjua\nMSaIWQHJQqoWqcrarmt5vv7zzN4+mypjqxC/L97tWMaYIOXXAiIijUVkl4gkiki/i2zPLiIznO2r\nRaSYsz5cRN4TkS0iskNE+vszZ1YSHhrOM3WeYVWXVeTKlouY92MYFD/IOtiNMVfMbwVEREKBUUAT\noDzQTkTKp2r2IPCbqpYE3gRedda3BrKraiUgGuh+vriY9FGtSDXWd1tPxyodGfLVEGJnxpJ8Ktnt\nWMaYIOLPI5CaQKKq7lXVU8B0oEWqNi2A95z5WUCMiAigQKSIhAE5gFPAH37MmiVFZotkUotJDG88\nnLm75nLrhFvZdWiX27GMMUHCnwXkeuBgiuUkZ91F26jqGeAoUABPMTkO/Ah8BwxTVbv+1A9EhIdv\nfpjP//M5Pxz7gSpjq/DS1y/ZKS1jzGWJv8aSEJHWwJ2q2sVZ7gDUVNU+Kdpsc9okOcvf4jlyKQv0\nBB4A8gFfA01UdW+q9+gGdAMoXLhw9PTp033Om5ycTK5cuXze3w3pnfnQX4cYmTiSpYeWUqdgHfqX\n7U9EaES6vf559r3OOMGYOxgzQ3DmTk5OplmzZutVtbpPL6CqfpmAWsCiFMv9gf6p2iwCajnzYcAh\nQPD0nXRI0W4i0Cat94uOjtarER8ff1X7u8Efmc+dO6dvrHhDZbBoqRGldNXBVen+Hva9zjjBmDsY\nM6sGZ+74+HgF1qmPv+f9eQprLVBKRIqLSDagLTA3VZu5QJwz3wr4UlUVz2mrBuIRCdwC7PRjVuMQ\nER6t9SiLOy7mr7N/UXtSbYatGGantIwx/+C3AqKePo3eeI4ydgAzVXWbiAwRkeZOswlAARFJBB4D\nzl/qOwrIBWzFU4gmqepmf2U1/9SgeAM2dt9I8zLNefKLJ2k+rbmNemiM+ZuwtDY6l+K+p6r3+/Li\nqroAWJBq3cAU8yfxXLKber/ki603GStfjnzMaj2LMevG8OiiR4kaG8WUe6ZQv3h9t6MZYwJAmkcg\nqnoWuNY5BWWyIBGhZ42erHpwFZHZIol5P4ZHFz7K8VPH3Y5mjHGZN6ew9gPLReRZEXns/OTnXCbA\nVC1SlQ3dNvBQ9Yd4a/VbRI2LYtl3y9yOZYxxkTcF5AdgntM2d4rJZDGR2SIZ1XQUCXEJ/HXmL26f\ndDuDEwZz+uxpt6MZY1yQZh8IgKo+ByAiuT2Las+7yOLqFqvLjl476LmgJ88tfY65u+Yys/VMSuYv\n6XY0Y0wGuuwRiIhUFJFv8FwRtU1E1otIBf9HM4EsMlskk1tMZnab2ez/fT/Vx1dn7q7UV2kbYzIz\nb05hjQceU9UbVfVG4HHgHf/GMsFARLi33L1s6L6BkvlL0mJ6CwYsGcCZc2fcjmaMyQDeFJBIVb0w\naISqJgCRfktkgk6xvMVY1nkZXat15eVlLxPzfgxJfyS5HcsY42feFJC9zhVYxZzpGWCfv4OZ4BIR\nFsH4ZuN5r+V7rP9hPVFjo1iwZ8HldzTGBC1vCkhn4FrgY2cqCHTyZygTvDpW6ciG7hsoek1Rmn7Y\nlH6L+9kpLWMyKW/uRB+gqg9nUB6TCZQuUJqVD67k0UWP8uryV1nz/Rqmt5pOochCbkczxqQjb+5E\nj86gLCYTyRGeg7F3j2Vyi8msTFpJtXHVWHlwpduxjDHpyJtTWN+IyFwR6SAi956f/J7MZApxUXGs\nfHAl2UKzUXdyXUauGXn+Ef3GmCB32RsJgfzAYaBBinWKpz/EmMuKui6K9d3W02FOB/p83oe619al\n0s2VKJizoNvRjDFXIc0jEKcPZLOqdko1dc6gfCaTyJcjH3PbzeXlmJdZfmg5FUZXYM6OOW7HMsZc\nBW/6QJqn1cYYb4VICP1q92NstbFcn/t67p15L//5+D/89udvbkczxvjAmz6QFSIyUkRuF5Fq5ye/\nJzOZ1k25bmJ1l9U8V+85Zm6bSbXx1Vj3wzq3YxljrpA3BeRWoAIwBHjdmYb5M5TJ/MJDwxlYdyDL\nOi3jnJ7jtom3MWL1COtgNyaIePM0Xht+zvjNzUVvZkO3DTzw6QM8svARFn27iInNJ1I4V2G3oxlj\nLuOSRyAi8laK+UdSbZvsx0wmiymQswBz285lZJORfLnvSyqPrWyPQTEmCKR1CqtOivm4VNsq+yGL\nycJEhF41e7Gu6zoKRxam6YdNeeJ/T9hgVcYEsLQKiFxi3hi/qVCoAmu6rqFn9Z68vvJ16r1Xz57s\na0yASquAhIhIPhEpkGI+v4jkB0IzKJ/JgiLCIhjVdBTTYqex+efNVBlbhSmbplgHuzEBJq0CkgdY\nD6wDrgE2OMvrsTHRTQZoW7Et67quo3SB0nT8pCN3fXgXB34/4HYsY4zjkgVEVYupaglVLX6RqURG\nhjRZV5mCZVjWaRnDGw/n6wNfU2F0BSZvnGxHI8YEAG/uAzHGVaEhoTx888Ns67mN6v+qTqdPO9Fh\nTgeO/XXM7WjGZGlWQEzQuDHvjSzpuIQh9YYwbes0qo6ryprv17gdy5gsywqICSqhIaE8W/dZEuIS\nOHX2FLdOuJVB8YPscl9jXOBVARGR2iLSyZm/VkSK+zeWMWm7/cbb2fzQZtpXas+Qr4ZQa0Itdh7a\n6XYsY7KUyxYQERkEPAX0d1aFA1P9GcoYb+SNyMv797zPrNaz2P/7fqqOq8rbq9/mnJ5zO5oxWYI3\nRyD34Hmk+3EAVf0BLy/jFZHGIrJLRBJFpN9FtmcXkRnO9tUiUizFtsoislJEtonIFhGJ8OY9TdYT\nWz6WLQ9toX6x+jy88GHunHonB48edDuWMZmeNwXklHqumVQAEYn05oWdwahGAU2A8kA7ESmfqtmD\nwG+qWhJ4E3jV2TcMz1FOD1WtANQD7CS3uaQiuYswv/18xjYdy4qDK6g0phIfbP7ALvc1xo+8KSAz\nRWQckFdEugKLgXe92K8mkKiqe1X1FDAdaJGqTQvgPWd+FhAjIgI0wjMS4iYAVT3sDG5lzCWJCN2r\nd2dTj01UKFSB++fcT8sZLTl84rDb0YzJlC5bQFR1GJ5f7rOBMsBAVR3hxWtfD6Q8j5DkrLtoG1U9\nAxwFCgClARWRRSKyQUT6evF+xgBQMn9JvnrgK4Y2HMrCxIVEj4+2AauM8QO53CG+iLyqqk9dbt1F\n9msN3KmqXZzlDkBNVe2Tos02p02Ss/wtniOXTkAvoAZwAlgCPKOqS1K9RzegG0DhwoWjp0+ffvlP\nfAnJycnkypXL5/3dEIyZIWNz7/xjJ4O2D+LwqcN0Kd6FNkXbECJXfvW6fa8zTjBmhuDMnZycTLNm\nzdaranWfXkBV05yADRdZt9mL/WoBi1Is9wf6p2qzCKjlzIcBh/A8+bctMDlFu2eBJ9N6v+joaL0a\n8fHxV7W/G4Ixs2rG5z5y4ojGzohVBqMN32+oP/zxwxW/hn2vM04wZlYNztzx8fEKrNPL/D6/1JTW\ngFIPicgWoIyIbE4x7QM2e1Gb1gKlRKS4iGRzisLcVG3m8v9jjbQCvlRVdQpLZRHJ6XSo1wW2e/Ge\nxvxDvhz5+Kj1R4y7exzLvltGlbFVbMAqY9JBWsfyHwLN8PySb5ZiilbV+y/3wurp0+iNpxjsAGaq\n6jYRGSIizZ1mE4ACIpIIPAb0c/b9DXgDTxHaiOcoaL4Pn88YwNPB3i26G+u6reO6XNfR9MOmPLrw\nUf4685fb0YwJWpccE11VjwJHRSR1X0cuEcmlqt9d7sVVdQGwINW6gSnmTwKtL7HvVOyGRZPOyl9b\nnjVd19D3i768tfotEg4kMC12GmULlnU7mjFBx5vexPnAPOfrEmAv8Lk/QxnjTxFhEYxoMoK5bedy\n8OhBosdHM2HDBLtnxJgr5M1lvJVUtbLztRSeq6SW+T+aMf7VrEwzNj+0mVpFa9Hlsy7cN+s+fj/5\nu9uxjAkaV3w9o6puwHN5rTFB71+5/8X/OvyPV2JeYc7OOUSNjWL5d8vdjmVMUPDmYYqPpZieEJEP\ngV8zIJsxGSJEQniq9lMs77yc0JBQ6kyuw/NLn+fsOXv4gTFp8eYIJHeKKTuevpDUjyQxJujVvL4m\n33T/hnYV2zEwYSAN3m9gD2U0Jg2XvArrPFV9LiOCGBMIrsl+DVPvncqdN91JzwU9qTK2Cu82f5f8\n5Hc7mjEB55IFREQ+w3kC78WoavNLbTMm2HWo0oFaN9Si3ex2xM6MpVmRZtS8rSY5w3O6Hc2YgJHW\nEciwDEthTAAqmb8kyzsv59kvn+W1Fa9R450aTIudRuXCld2OZkxAuGQfiKouPT8BK4HDzrTCWWdM\nppctNBuvNnyVoZWGcuTPI9R8pyaj1462e0aMwbursOoBe/AMDjUa2C0idfycy5iAUj1/dTb12ESD\n4g3otaAXsTNjOXTikNuxjHGVN1dhvQ40UtW6qloHuBPP6IHGZCmFIgsxr/08hjUcxvw986k4uqI9\nlNFkad4UkHBV3XV+QVV3A+H+i2RM4AqREB6/9XHWdl1LochCNP2wKb0X9LaHMposyZsCsk5EJohI\nPWd6F1jv72DGBLLKhSuztutaHr3lUUatHcVtE29j72973Y5lTIbypoA8BGwDHgYeceZ7+DOUMcEg\ne1h23rjzDT657xO+/e1bqo2rxqzts9yOZUyG8eZhin+p6huqei/wILBEVe143RhHi7It2NBtA2UK\nlqH1R63pOrcrx08ddzuWMX7nzVVYCSJyjYjkxzO40yQRecP/0YwJHsXzFWdZp2X0u60fE76ZQPV3\nqrPxp41uxzLGr7w5hZVHVf8A7gUmqWo0cId/YxkTfMJDw3n5jpf5osMXHD15lJvfvZm3Vr1l94yY\nTMubAhImIkWANngGljLGpCGmRAybH9rMnTfdyaOLHqXph0355fgvbscyJt15U0CG4BnX/FtVXSsi\nJfDcWGiMuYSCOQvyadtPebvJ23y570uqjK1C/L54t2MZk6686UT/yBmR8CFnea+qxvo/mjHBTUTo\nXbM3a7quIW9EXmLej2Fg/EBOnz3tdjRj0oU3neglROQzEflVRH4RkU9FpHhGhDMmMzh/z0jHKh15\n/qvnqfluTbb9ss3tWMZcNW9OYX0IzASKAP8CPgKm+zOUMZlNrmy5mNxyMnPum8MPx36gxjs1eGf9\nO9bBboKaNwVEVHWKqp5xpqmkMU6IMebSWpZtyaYem6j979p0m9eNtrPbcvTkUbdjGeOTSxYQEcnv\n3PsRLyL9RKSYiNwoIn3xDGtrjPHBdbmuY+H9C3kl5hVmb59N1LgoVietdjuWMVcsrSOQ9cA64D6g\nOxAPJOB5tEknvyczJhMLkRCeqv0UX3f6GlWl9qTaDF0+lHN6zu1oxngtrQGliqtqCefr3yagTAZm\nNCbTqnVDLTb22EjLsi3pu7gvd31wFz8n/+x2LGO84k0fCADi0cB5Gm+SHzMZk6XkjcjLzFYzGdt0\nLEsPLKXK2Cp88e0Xbscy5rK8uYz3ZhEZDhwA5gJfA2X9HcyYrERE6F69O2u6rCF/jvw0mtqIRz5/\nhBOnT7gdzZhLSqsT/UUR2QO8BGwBqgK/qup7qvpbRgU0JiupVLgS67qt4+GaDzNizQiqjqtqHewm\nYKV1BNIN+BkYA0xV1cPY5bvG+F3O8JwMbzKcJR2X8OfpP7lt4m0MThhsd7CbgJNWAbkOeBFoDiSK\nyBQgh4iEefviItJYRHaJSKKI9LvI9uwiMsPZvlpEiqXa/m8RSRaRJ7x9T2MyiwbFG7D5oc20q9SO\n55Y+x+2TbrdRD01ASesqrLOq+rmqdgRKAp8CK4DvReTDy72wiIQCo4AmQHmgnYiUT9XsQeA3VS0J\nvAm8mmr7m8Dn3n4YYzKbvBF5mXLPFGa0msHOQzupOq4q07ZMszvYTUDw6iosVT2pqrOchyiWwvN0\n3supCSQ6D188hefxJy1StWkBvOfMzwJiREQARKQlsBfPELrGZGltKrRhY4+NlL+2PO0/bk+bWW34\n9fivbscyWZz46y8ZEWkFNFbVLs5yB+BmVe2dos1Wp02Ss/wtcDPwJ7AYaAg8ASSr6rCLvEc3PH01\nFC5cOHr6dN8f0ZWcnEyuXLl83t8NwZgZgjN3oGQ+q2eZcXAGk/dPJjIskv+W+i91r617yfaBkvtK\nBGNmCM7cycnJNGvWbL2qVvfpBVTVLxPQGng3xXIH4O1UbbYBRVMsfwsUAIYBbZx1g4EnLvd+0dHR\nejXi4+Ovan83BGNm1eDMHWiZt/y8RaPHRSuD0faz2+uRE0cu2i7QcnsjGDOrBmfu+Ph4Bdapj7/n\nvb6R0AdJwA0plosCP1yqjdM5nwc4guco5DUR2Q/8FxggIr0xxgBQsVBFVj64kiH1hjBz20wqjqnI\nwsSFbscyWYxXBUREbhWR9iLS8fzkxW5rgVIiUlxEsgFt8dyImNJcIM6ZbwV86RTG21W1mKoWA94C\nXlLVkV59ImOyiPDQcJ6t+yyru6wmb0RemnzQhB7zepB8KtntaCaL8OZO9Cl4TinVBmo402XPl6nq\nGaA3ng73HcBMVd0mIkNEpLnTbAJQQEQSgceAf1zqa4xJW7Ui1VjfbT1P1HqC8evH2/C5JsN4c09H\ndaC86pX3tqvqAmBBqnUDU8yfxNNXktZrDL7S9zUmq4kIi2Boo6E0L9OcTp92osH7DYirEkdspI0+\nbfzHm1NYW/HcVGiMCXC333g7Wx7awoDaA5i6eSqd1nXis12fuR3LZFLeFJCCwHYRWSQic89P/g5m\njPFNjvAcvBjzImu6riFPeB6aT29O+9nt7b4Rk+68OYU12N8hjDHpr1qRaoytNpYVISt48esXWfTt\nIl5v9DpxVeJw7tc15qpc9ghEVZdebMqIcMaYqxMeEs6geoPY2GMj5QqWo9OnnagzuQ5bft7idjST\nCXhzFdYtIrLWeajhKRE5KyJ/ZEQ4Y0z6KH9teb7q9BUTmk9g56GdVBtfjf6L+/Pn6T/djmaCmDd9\nICOBdsAeIAfQxVlnjAkiIRJC56qd2dlrJ/dXvp9Xlr9CpTGVWLx3sdvRTJDy9mGKiUCoep7QOwmo\n59dUxhi/KZCzAJNaTGJJxyWESAgNpzSk45yO1slurpg3BeSEcyf5RhF5TUQeBSL9nMsY42fnxxt5\n5vZnmLZ1GuVGleO9je/Zo+KN17wpIB2cdr2B43ieXWV3JxmTCUSERfB8g+fZ2H0jZQqW4YFPH+CO\nKXeQeCTR7WgmCHhzFdYBQIAiqvqcqj7mnNIyxmQSFQpV4OtOXzOm6RjW/bCOiqMr8vzS5/nrzF9u\nRzMBzJursJoBG4GFznKU3UhoTOYTIiH0qN6Dnb120qJsCwYmDKTK2Cok7E9wO5oJUN6cwhqMZ3TB\n3wFUdSNQzH+RjDFuKpK7CDNazeDz/3zOqbOnqP9efeI+iePQiUNuRzMBxpsCckZVj/o9iTEmoDQu\n2ZitPbcyoPYApm3xdLJ/sPkD62Q3F3j1MEURaQ+EikgpEXkbWOHnXMaYAJAzPCcvxrzIhu4buCnf\nTdw/537u+vAu9v++3+1oJgB4U0D6ABWAv4BpwB94Rgk0xmQRFQtVZHnn5YxoPIKvD3xN+VHleWXZ\nK5w6e8rtaMZF3lyFdUJVn1bVGqpa3Zk/mRHhjDGBIzQklD4392FHrx00LtmY/kv6EzU2yjrZs7BL\nPo33cldaqWrztLYbYzKnG/LcwMf3fcz83fPp/Xlv6r9Xnw6VOzCs0TAKRRZyO57JQGk9zr0WcBDP\naavVeO4FMcYYAJqWbkr94vV56euXeG35a3y2+zNeavAS3aK7ERoS6nY8kwHSOoV1HTAAqAgMBxoC\nh+xx7saY83KG5+SFBi+w+aHNVL2uKj0X9KTWhFps+HGD29FMBrhkAXEenLhQVeOAW4BEIEFE+mRY\nOmNMUChbsCxLOi7hg3s/4Luj31HjnRr0WdCHoyftDoDMLM1OdBHJLiL3AlOBXsAI4OOMCGaMCS4i\nQvtK7dnZeyc9q/dk1NpRlB1Vlmlbptm9I5nUJQuIiLyH536PasBzzlVYz6vq9xmWzhgTdPJG5OXt\nu95mbde1FL2mKO0/bk/DKQ3ZdWiX29FMOkvrCKQDUBp4BFghIn840zEbkdAYcznR/4pm1YOrGH3X\naNb9sI5KYyoxYMkATpw+4XY0k07S6gMJUdXcznRNiim3ql6TkSGNMcEpNCSUh2o8xO4+u2lfqT0v\nL3uZCqMrMH/3fLejmXTg1YiExhhzNQpFFmJyy8ksfWApOcJycPe0u7nrg7vY8esOt6OZq2AFxBiT\nYercWIccBxjDAAATtUlEQVSNPTbyeqPXWXFwBZXGVOLxRY9z7K9jbkczPrACYozJUNlCs/FYrcfY\n02cPnaI68eaqNyk7qizTt063q7WCjBUQY4wrro28lneav8PKB1dSJFcR2s1uR8z7Mew/vt/taMZL\nVkCMMa66uejNrO6ymjFNx7Dxp410Wd+Fp754iuOnjrsdzVyGXwuIiDQWkV0ikigi/S6yPbuIzHC2\nrxaRYs76hiKyXkS2OF8b+DOnMcZdoSGh9Kjeg129d9GocCNeW/Ea5UeX5+MdH9tprQDmtwIiIqHA\nKKAJUB5oJyLlUzV7EPhNVUsCbwKvOusPAc1UtRIQB0zxV05jTOC4NvJa+pbpy9edviZP9jzEzowl\n5v0YNv+82e1o5iL8eQRSE0hU1b2qegqYDrRI1aYF8J4zPwuIERFR1W9U9Qdn/TYgQkSy+zGrMSaA\n1P53bTZ038Cou0ax6edNVB1XlZ7ze9q47AHGnwXkejyPgz8vyVl30TaqegY4ChRI1SYW+EZV//JT\nTmNMAAoLCaNnjZ7s6bOHXjV6MX79eEq9XYoRq0dw+uxpt+MZQPx1flFEWgN3qmoXZ7kDUFNV+6Ro\ns81pk+Qsf+u0OewsVwDmAo1U9duLvEc3oBtA4cKFo6dPn+5z3uTkZHLlyuXz/m4IxswQnLmDMTME\nZ+5LZd53fB+jvx3Nut/WUSxnMXqX7E10vmgXEl5csH6vmzVrtl5Vq/v0AqrqlwnPgFSLUiz3B/qn\narMIqOXMh+Hp+zhf1IoCu4HbvHm/6OhovRrx8fFXtb8bgjGzanDmDsbMqsGZO63M586d0092fKIl\nhpdQBqP3zrhX9x7Zm3Hh0hCs32tgnfr4e96fp7DWAqVEpLiIZAPa4jmaSGkunk5ygFbAl6qqIpIX\nmO8UnOV+zGiMCSIiQouyLdjWcxsvNniRRYmLKDeqHE8veZrkU8lux8ty/FZA1NOn0RvPUcYOYKaq\nbhORISJyfjz1CUABEUkEHgPOX+rbGygJPCsiG53JBls2xgAQERbBgNsHsKv3LtpUaMNLy16izMgy\nTN08lXN6zu14WYZf7wNR1QWqWlpVb1LVF511A1V1rjN/UlVbq2pJVa2pqnud9S+oaqSqRqWYfvFn\nVmNM8Ln+mut5/573WdF5Bdfnvp4Oczpw28TbWPv9WrejZQl2J7oxJujVuqEWq7qsYlKLSez7bR81\n361J508783Pyz25Hy9SsgBhjMoUQCeGBqAfY3Wc3T976JFM3T6X0yNK8vuJ1Tp095Xa8TMkKiDEm\nU7km+zW81vA1tvbcSu1/1+aJL56g8pjKfL7nc7ejZTpWQIwxmVLpAqWZ334+89rN45ye464P76LZ\ntGYkHkl0O1qmYQXEGJOpNS3dlK09t/LaHa+RsD+BCqMr0G9xPxvEKh1YATHGZHrZQrPx5G1Psrv3\nbtpVbMery1+l9MjSTNgwgbPnzrodL2hZATHGZBlFchdhcsvJrHpwFcXyFqPLZ12IGhfFwsSFbkcL\nSlZAjDFZzs1Fb2ZF5xV81PojTpw+QZMPmtBsWjP2/bbP7WhBxQqIMSZLEhFalW/F9p7bee2O14jf\nF0/50eUZnDDYHoviJSsgxpgsLXtYdp687Ul29t5J8zLNeW7pc5R6uxTj14/nzLkzbscLaFZAjDEG\nKHpNUWa0msGKzisoka8E3ed1p8rYKszbPc+G1b0EKyDGGJNCrRtqsazTMma3mc3ps6dpNq0ZMe/H\n8M2P37gdLeBYATHGmFREhHvL3cu2ntt4u8nbbP55M9Hjo+n8aWd+PPaj2/EChhUQY4y5hPDQcHrX\n7E3iw4k8Xutxpm6eSqm3S/HCVy9w4vQJt+O5zgqIMcZcRt6IvAxtNJQdvXZwZ8k7eTb+WcqNKsfM\nbTOzdP+IFRBjjPHSTflvYnab2STEJZAvIh/3zbqPWhNqsey7ZW5Hc4UVEGOMuUJ1i9Vlfbf1TGw+\nkYN/HOT2SbczcNtAdh/e7Xa0DGUFxBhjfBAaEkqnqp3Y3Xs3z9d/nnW/raPC6Ar0mNeDH4794Ha8\nDGEFxBhjrkJktkieqfMMU2tOpXt0dyZ+M5GSI0ry1BdPceTPI27H8ysrIMYYkw7yZ8vPyLtGsrP3\nTmLLxzJ0xVBuGnETQ5cP5eSZk27H84swtwP40+nTp0lKSuLkycv/4+XJk4cdO3ZkQKr0E0iZIyIi\nKFq0KOHh4W5HMcZVJfKVYMo9U3jy1ifpv6Q/fRf3Zfjq4Txb51k6V+1MeGjm+T+SqQtIUlISuXPn\nplixYohImm2PHTtG7ty5MyhZ+giUzKrK4cOHSUpKonjx4m7HMSYgVC5cmfnt5xO/L56nv3yaHvN7\nMGzlMIY2HEqLMi0u+zspGGTqU1gnT56kQIECmeIfKpCJCAUKFPDqSM+YrKZ+8fos77ycz9p9RrbQ\nbNwz4x5i3o9h6y9b3Y521TJ1AQGseGQQ+z4bc2kiwt2l72ZTj02MumsUm37eRNTYKHrN78Uvx39x\nO57PMn0BcVtoaChRUVEXpldeecUv73P8+HEKFCjA0aNH/7a+ZcuWzJw585L7JSQkcPfdd/slkzHm\n78JCwuhZoye7e++me3R3xq0fR8kRJRmydEhQjkFiBcTPcuTIwcaNGy9M/fr1+0ebs2f/PibzmTPe\njUGQsl1kZCSNGjXik08+ubDu6NGjLFu2zAqEMQGmQM4CjGo6im09t3FHiTsYlDCIm0bcxKg1ozh1\n9pTb8bxmBcQlxYoVY8iQIdSuXZuPPvqIevXqMWDAAOrWrcvw4cM5cOAAMTExVK5cmZiYGL777jsA\nHnjgAR577DHq16/PwIED//aa7dq1Y/r06ReW58yZQ+PGjcmZMydr1qzh1ltvpWrVqtx6663s2rXr\nH5kGDx7MsGHDLixXrFiR/fv3AzB16lRq1qxJVFQU3bt3/0fRM8ZcuTIFy/DxfR+z8sGVlCtYjt6f\n96bcqHJM2zKNc3rO7XiXlamvwkrpvwv/y8afNl5y+9mzZwkNDb2i14y6Loq3Gr+VZps///yTqKio\nC8v9+/fnvvvuAzyXvi5b5nmGztixY/n9999ZunQpAM2aNaNjx47ExcUxceJEHn744QtHF7t372bx\n4sWcOPH3p4E2btyYLl26cPjwYQoUKMD06dPp06cPAGXLluWrr74iLCyMxYsXM2DAAGbPnu3V59yx\nYwczZsxg+fLlhIeH07NnTz744AM6duzo1f7GmLTdUvQW4uPiWZi4kH5L+tH+4/YMXTGUgXUH0rxM\nc0IkMP/WzzIFxC3nT2FdzPlCcrHllStX8vHHHwPQoUMH+vbte2Fb69atL1rssmXLRvPmzZk1axax\nsbFs3LiRRo0aAZ7TWXFxcezZswcR4fTp015/hiVLlrB+/Xpq1KgBeIpioUKFvN7fGHN5IkKTUk24\ns+SdfLjlQwYlDOKeGfdQuXBlnr79aWLLxRIacmV/5PqbXwuIiDQGhgOhwLuq+kqq7dmB94Fo4DBw\nn6rud7b1Bx4EzgIPq+qiq8lyuSMFN+6piIyMTHM5pZRXOaXVrl27drzwwguoKi1atLhwY9+zzz5L\n/fr1mTNnDvv376devXr/2DcsLIxz5/7/sPn8ZbmqSlxcHC+//LJXn8sY47sQCeH+yvfTtmJbpm2Z\nxotfv8h9s+6jbMGy9K/dn3YV2wXMzYh+Oy4SkVBgFNAEKA+0E5HyqZo9CPymqiWBN4FXnX3LA22B\nCkBjYLTzelnGrbfeeqE/44MPPqB27dpe7Ve/fn327NnDqFGjaNeu3YX1R48e5frrrwdg8uTJF923\nWLFibNiwAYANGzawb98+AGJiYpg1axa//OK53PDIkSMcOHDAp89ljPFOWEgYHap0YFvPbcxoNYPw\nkHDiPonjphE38cbKNzh68ujlX8TP/HlirSaQqKp7VfUUMB1okapNC+A9Z34WECOeP7VbANNV9S9V\n3QckOq8XdM73gZyfLnYV1sWMGDGCSZMmUblyZaZMmcLw4cO92i8kJITY2FgOHz5MnTp1Lqzv27cv\n/fv357bbbrtkB3hsbCxHjhwhKiqKMWPGULp0aQDKly/PCy+8QKNGjahcuTINGzbkxx9tWE9jMkJo\nSChtKrRhU49NzG8/n+L5ivP4/x7n+jeup/eC3uz9ba974VTVLxPQCs9pq/PLHYCRqdpsBYqmWP4W\nKAiMBO5PsX4C0Cqt94uOjtbUtm/f/o91l/LHH3943TZQBFpmb7/f8fHx/g3iB8GYWTU4cwdjZtWM\nzb3u+3UaNydOsz2fTUOfC9XHFj7m0+vEx8crsE59/D3vzz6Qi92anHrsx0u18WZfRKQb0A2gcOHC\nJCQk/G17njx5OHbsmDdZOXv2rNdtA0WgZT558uQ//g0uJjk52at2gSQYM0Nw5g7GzJDxuR/I+wB3\n17ibmUkzOX3otE/vnZx8dTcv+rOAJAE3pFguCqQeZeV8myQRCQPyAEe83BdVHQ+MB6hevbqm7hje\nsWOH1x3jgfJgwisRaJkjIiKoWrXqZdslJCRctBM/kAVjZgjO3MGYGdzL3YpWPu97tQXPn30ga4FS\nIlJcRLLh6RSfm6rNXCDOmW8FfKmq6qxvKyLZRaQ4UApY48esxhhjrpDfjkBU9YyI9AYW4bmMd6Kq\nbhORIXjOuc3F07cxRUQS8Rx5tHX23SYiM4HtwBmgl6r6dOuzqtqD/jKAp+4bY7ISv94HoqoLgAWp\n1g1MMX8SaH2JfV8EXrya94+IiLhwV7YVEf9RZzyQiIgIt6MYYzJQpr4TvWjRoiQlJfHrr79etu3J\nkyeD7hdgIGU+PyKhMSbryNQFJDw83OsR8hISErzqAA4kwZjZGJN5BOYTuowxxgQ8KyDGGGN8YgXE\nGGOMTySzXH4pIr8CV/OEv4LAoXSKk1GCMTMEZ+5gzAzBmTsYM0Nw5i4IRKrqtb7snGkKyNUSkXWq\nWt3tHFciGDNDcOYOxswQnLmDMTMEZ+6rzWynsIwxxvjECogxxhifWAH5f+PdDuCDYMwMwZk7GDND\ncOYOxswQnLmvKrP1gRhjjPGJHYEYY4zxSZYvICLSWER2iUiiiHg33mwGEZGJIvKLiGxNsS6/iHwh\nInucr/mc9SIiI5zPsVlEqrmU+QYRiReRHSKyTUQeCfTcIhIhImtEZJOT+TlnfXERWe1knuEMS4Az\nzMAMJ/NqESmW0ZlT5Q8VkW9EZF4w5BaR/SKyRUQ2isg6Z13A/nykyJ1XRGaJyE7n57tWIOcWkTLO\n9/j89IeI/DddM/s6lGFmmPA8Zv5boASQDdgElHc7V4p8dYBqwNYU614D+jnz/YBXnfm7gM/xjOZ4\nC7DapcxFgGrOfG5gN1A+kHM7753LmQ8HVjtZZgJtnfVjgYec+Z7AWGe+LTDD5Z+Tx4APgXnOckDn\nBvYDBVOtC9ifjxQZ3wO6OPPZgLzBkNvJEwr8BNyYnpld+0CBMAG1gEUplvsD/d3OlSpjsVQFZBdQ\nxJkvAuxy5scB7S7WzuX8nwINgyU3kBPYANyM56awsNQ/K3jGuKnlzIc57cSlvEWBJUADYJ7znz+g\nc1+igAT0zwdwDbAv9fcr0HOneP9GwPL0zpzVT2FdDxxMsZzkrAtkhVX1RwDnayFnfcB9FucUSVU8\nf9EHdG7nNNBG4BfgCzxHpr+r6pmL5LqQ2dl+FCiQsYkveAvoC5xzlgsQ+LkV+J+IrBeRbs66gP75\nwHOW4ldgknO68F0RiSTwc5/XFpjmzKdb5qxeQC42ylSwXpYWUJ9FRHIBs4H/quofaTW9yLoMz62q\nZ1U1Cs9f9DWBchdr5nwNiMwicjfwi6quT7n6Ik0DKjdwm6pWA5oAvUSkThptAyVzGJ7TyWNUtSpw\nHM/pn0sJlNw4fWDNgY8u1/Qi69LMnNULSBJwQ4rlosAPLmXx1s8iUgTA+fqLsz5gPouIhOMpHh+o\n6sfO6oDPDaCqvwMJeM4B5xWR82PmpMx1IbOzPQ+eIZkz2m1AcxHZD0zHcxrrLQI8t6r+4Hz9BZiD\np2AH+s9HEpCkqqud5Vl4Ckqg5wZPod6gqj87y+mWOasXkLVAKeeqlWx4DvPmupzpcuYCcc58HJ4+\nhvPrOzpXUtwCHD1/mJqRRETwjHW/Q1XfSLEpYHOLyLUikteZzwHcAewA4oFWl8h8/rO0Ar5U56Rx\nRlLV/qpaVFWL4fnZ/VJV/0MA5xaRSBHJfX4ez7n5rQTwzweAqv4EHBSRMs6qGGA7AZ7b0Y7/P30F\n6ZnZrU6dQJnwXHmwG88576fdzpMq2zTgR+A0nr8OHsRzznoJsMf5mt9pK8Ao53NsAaq7lLk2nsPe\nzcBGZ7orkHMDlYFvnMxbgYHO+hLAGiARz+F/dmd9hLOc6GwvEQA/K/X4/6uwAja3k22TM207/38u\nkH8+UmSPAtY5PyefAPkCPTeei0IOA3lSrEu3zHYnujHGGJ9k9VNYxhhjfGQFxBhjjE+sgBhjjPGJ\nFRBjjDE+sQJijDHGJ1ZAjPEjESkmKZ6mbExmYgXEGGOMT6yAGJNBRKSE8yC+Gm5nMSY9WAExJgM4\nj8CYDXRS1bVu5zEmPYRdvokx5ipdi+d5Q7Gqus3tMMakFzsCMcb/juIZZ+E2t4MYk57sCMQY/zsF\ntAQWiUiyqn7odiBj0oMVEGMygKoedwaA+kJEjqvqp5fdyZgAZ0/jNcYY4xPrAzHGGOMTKyDGGGN8\nYgXEGGOMT6yAGGOM8YkVEGOMMT6xAmKMMcYnVkCMMcb4xAqIMcYYn/wfpNBwvZaWkDMAAAAASUVO\nRK5CYII=\n",
"text/plain": [
"<matplotlib.figure.Figure at 0x1150eb050>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"error_values = []\n",
"K = min(len(S), len(S))\n",
"for k in range(1,K):\n",
" X_pred = U[:, 0:k] * np.matrix(np.diag(S[0:k])) * V[0:k, :]\n",
" error_values.append(mean_absolute_error(M, X_pred))\n",
" \n",
"plt.plot(error_values, color=\"green\", label=\"Error Value\")\n",
"plt.legend(loc=3)\n",
"plt.grid()\n",
"plt.xlabel(\"k\"); plt.ylabel(\"Mean Absolute Error\")\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"It looks like $k \\approx 25$ is a good choice for us because the MEAN is quite small (< 0.1). Thus the model looks as follows:"
]
},
{
"cell_type": "code",
"execution_count": 68,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"k = 25\n",
"user_preferences = U[:, 0:k]; S_approx = np.matrix(np.diag(S[0:k])); movie_features = V[0:k, :]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A movie similarity matrix is matrix of size `num_movies x num_movies`. Similarity of the movie `i` with the movie `j` is movie similarity marix's $ij^\\mbox{th}$ element."
]
},
{
"cell_type": "code",
"execution_count": 143,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"movie_similarity_matrix = np.matrix(np.zeros(shape=(num_movies, num_movies)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We calculate similarity between movies as the cosine between their feature vectors."
]
},
{
"cell_type": "code",
"execution_count": 228,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"movie_similarity_matrix = movie_features.T * movie_features\n",
"movie_similarity_matrix = movie_similarity_matrix.T\n",
"feature_norms = np.linalg.norm(movie_features, axis=0)\n",
"movie_similarity_matrix /= feature_norms # rows\n",
"movie_similarity_matrix /= feature_norms.reshape(feature_norms.size, 1) # cols"
]
},
{
"cell_type": "code",
"execution_count": 301,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"movie_mappings_df = pd.DataFrame(movie_mappings.items(), columns=[\"Original ID\", \"Mapped ID\"])\n",
"movie_mappings_df[\"Original ID\"] += 1"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now let's read the list of movie names."
]
},
{
"cell_type": "code",
"execution_count": 304,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"movies_df = pd.read_csv(\"/Users/andrey/Downloads/ml-latest-small/movies.csv\")"
]
},
{
"cell_type": "code",
"execution_count": 306,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>movieId</th>\n",
" <th>title</th>\n",
" <th>genres</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>1</td>\n",
" <td>Toy Story (1995)</td>\n",
" <td>Adventure|Animation|Children|Comedy|Fantasy</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>2</td>\n",
" <td>Jumanji (1995)</td>\n",
" <td>Adventure|Children|Fantasy</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>3</td>\n",
" <td>Grumpier Old Men (1995)</td>\n",
" <td>Comedy|Romance</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>4</td>\n",
" <td>Waiting to Exhale (1995)</td>\n",
" <td>Comedy|Drama|Romance</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>5</td>\n",
" <td>Father of the Bride Part II (1995)</td>\n",
" <td>Comedy</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" movieId title \\\n",
"0 1 Toy Story (1995) \n",
"1 2 Jumanji (1995) \n",
"2 3 Grumpier Old Men (1995) \n",
"3 4 Waiting to Exhale (1995) \n",
"4 5 Father of the Bride Part II (1995) \n",
"\n",
" genres \n",
"0 Adventure|Animation|Children|Comedy|Fantasy \n",
"1 Adventure|Children|Fantasy \n",
"2 Comedy|Romance \n",
"3 Comedy|Drama|Romance \n",
"4 Comedy "
]
},
"execution_count": 306,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"movies_df.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The function below takes the id of a movie from the movies.csv file and returns a list of similar ones."
]
},
{
"cell_type": "code",
"execution_count": 357,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def similar_movies(movie_id, n=10):\n",
" mapped_movie_id = movie_mappings_df[movie_mappings_df[\"Original ID\"] == movie_id][\"Mapped ID\"].values[0]\n",
" mapped_ids_of_similar_movies = movie_similarity_matrix[movie_id].argsort().tolist()[0][-n-1:-1] # The latest value is the movie itself\n",
" return movie_mappings_df[movie_mappings_df[\"Mapped ID\"].isin(mapped_ids_of_similar_movies)][\"Original ID\"].values.tolist()"
]
},
{
"cell_type": "code",
"execution_count": 358,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"def get_movie_names(movie_ids):\n",
" return movies_df[movies_df[\"movieId\"].isin(movie_ids)]"
]
},
{
"cell_type": "code",
"execution_count": 360,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>movieId</th>\n",
" <th>title</th>\n",
" <th>genres</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>46</th>\n",
" <td>48</td>\n",
" <td>Pocahontas (1995)</td>\n",
" <td>Animation|Children|Drama|Musical|Romance</td>\n",
" </tr>\n",
" <tr>\n",
" <th>526</th>\n",
" <td>594</td>\n",
" <td>Snow White and the Seven Dwarfs (1937)</td>\n",
" <td>Animation|Children|Drama|Fantasy|Musical</td>\n",
" </tr>\n",
" <tr>\n",
" <th>819</th>\n",
" <td>1015</td>\n",
" <td>Homeward Bound: The Incredible Journey (1993)</td>\n",
" <td>Adventure|Children|Drama</td>\n",
" </tr>\n",
" <tr>\n",
" <th>826</th>\n",
" <td>1022</td>\n",
" <td>Cinderella (1950)</td>\n",
" <td>Animation|Children|Fantasy|Musical|Romance</td>\n",
" </tr>\n",
" <tr>\n",
" <th>837</th>\n",
" <td>1033</td>\n",
" <td>Fox and the Hound, The (1981)</td>\n",
" <td>Animation|Children|Drama</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1580</th>\n",
" <td>2018</td>\n",
" <td>Bambi (1942)</td>\n",
" <td>Animation|Children|Drama</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1638</th>\n",
" <td>2078</td>\n",
" <td>Jungle Book, The (1967)</td>\n",
" <td>Animation|Children|Comedy|Musical</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1640</th>\n",
" <td>2080</td>\n",
" <td>Lady and the Tramp (1955)</td>\n",
" <td>Animation|Children|Comedy|Romance</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1645</th>\n",
" <td>2085</td>\n",
" <td>101 Dalmatians (One Hundred and One Dalmatians...</td>\n",
" <td>Adventure|Animation|Children</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2437</th>\n",
" <td>3034</td>\n",
" <td>Robin Hood (1973)</td>\n",
" <td>Adventure|Animation|Children|Comedy|Musical</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" movieId title \\\n",
"46 48 Pocahontas (1995) \n",
"526 594 Snow White and the Seven Dwarfs (1937) \n",
"819 1015 Homeward Bound: The Incredible Journey (1993) \n",
"826 1022 Cinderella (1950) \n",
"837 1033 Fox and the Hound, The (1981) \n",
"1580 2018 Bambi (1942) \n",
"1638 2078 Jungle Book, The (1967) \n",
"1640 2080 Lady and the Tramp (1955) \n",
"1645 2085 101 Dalmatians (One Hundred and One Dalmatians... \n",
"2437 3034 Robin Hood (1973) \n",
"\n",
" genres \n",
"46 Animation|Children|Drama|Musical|Romance \n",
"526 Animation|Children|Drama|Fantasy|Musical \n",
"819 Adventure|Children|Drama \n",
"826 Animation|Children|Fantasy|Musical|Romance \n",
"837 Animation|Children|Drama \n",
"1580 Animation|Children|Drama \n",
"1638 Animation|Children|Comedy|Musical \n",
"1640 Animation|Children|Comedy|Romance \n",
"1645 Adventure|Animation|Children \n",
"2437 Adventure|Animation|Children|Comedy|Musical "
]
},
"execution_count": 360,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"get_movie_names(similar_movies(1)) # Movie 1 is \"Toy Story\""
]
},
{
"cell_type": "code",
"execution_count": 361,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>movieId</th>\n",
" <th>title</th>\n",
" <th>genres</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>420</th>\n",
" <td>472</td>\n",
" <td>I'll Do Anything (1994)</td>\n",
" <td>Comedy|Drama</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1215</th>\n",
" <td>1508</td>\n",
" <td>Traveller (1997)</td>\n",
" <td>Drama</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1270</th>\n",
" <td>1598</td>\n",
" <td>Desperate Measures (1998)</td>\n",
" <td>Crime|Drama|Thriller</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1392</th>\n",
" <td>1783</td>\n",
" <td>Palmetto (1998)</td>\n",
" <td>Crime|Drama|Mystery|Romance|Thriller</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1396</th>\n",
" <td>1791</td>\n",
" <td>Twilight (1998)</td>\n",
" <td>Crime|Drama|Thriller</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2196</th>\n",
" <td>2741</td>\n",
" <td>No Mercy (1986)</td>\n",
" <td>Action|Crime|Thriller</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2389</th>\n",
" <td>2977</td>\n",
" <td>Crazy in Alabama (1999)</td>\n",
" <td>Comedy|Drama</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3546</th>\n",
" <td>4496</td>\n",
" <td>D.O.A. (1988)</td>\n",
" <td>Film-Noir|Mystery|Thriller</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4172</th>\n",
" <td>5506</td>\n",
" <td>Blood Work (2002)</td>\n",
" <td>Crime|Drama|Mystery|Thriller</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4496</th>\n",
" <td>6180</td>\n",
" <td>Q &amp; A (1990)</td>\n",
" <td>Crime|Drama</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" movieId title genres\n",
"420 472 I'll Do Anything (1994) Comedy|Drama\n",
"1215 1508 Traveller (1997) Drama\n",
"1270 1598 Desperate Measures (1998) Crime|Drama|Thriller\n",
"1392 1783 Palmetto (1998) Crime|Drama|Mystery|Romance|Thriller\n",
"1396 1791 Twilight (1998) Crime|Drama|Thriller\n",
"2196 2741 No Mercy (1986) Action|Crime|Thriller\n",
"2389 2977 Crazy in Alabama (1999) Comedy|Drama\n",
"3546 4496 D.O.A. (1988) Film-Noir|Mystery|Thriller\n",
"4172 5506 Blood Work (2002) Crime|Drama|Mystery|Thriller\n",
"4496 6180 Q & A (1990) Crime|Drama"
]
},
"execution_count": 361,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"get_movie_names(similar_movies(2571)) # Movie 2571 is \"Matrix\""
]
},
{
"cell_type": "code",
"execution_count": 362,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>movieId</th>\n",
" <th>title</th>\n",
" <th>genres</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>935</th>\n",
" <td>1176</td>\n",
" <td>Double Life of Veronique, The (Double Vie de V...</td>\n",
" <td>Drama|Fantasy|Romance</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2066</th>\n",
" <td>2575</td>\n",
" <td>Dreamlife of Angels, The (Vie rêvée des anges,...</td>\n",
" <td>Drama</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2478</th>\n",
" <td>3083</td>\n",
" <td>All About My Mother (Todo sobre mi madre) (1999)</td>\n",
" <td>Drama</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2872</th>\n",
" <td>3598</td>\n",
" <td>Hamlet (2000)</td>\n",
" <td>Crime|Drama|Romance|Thriller</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3034</th>\n",
" <td>3794</td>\n",
" <td>Chuck &amp; Buck (2000)</td>\n",
" <td>Comedy|Drama</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3082</th>\n",
" <td>3854</td>\n",
" <td>Aimée &amp; Jaguar (1999)</td>\n",
" <td>Drama|Romance|War</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4005</th>\n",
" <td>5222</td>\n",
" <td>Kissing Jessica Stein (2001)</td>\n",
" <td>Comedy|Romance</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4314</th>\n",
" <td>5792</td>\n",
" <td>Roger Dodger (2002)</td>\n",
" <td>Comedy|Drama</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4738</th>\n",
" <td>6641</td>\n",
" <td>Code Unknown (Code inconnu: Récit incomplet de...</td>\n",
" <td>Drama</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5738</th>\n",
" <td>26131</td>\n",
" <td>Battle of Algiers, The (La battaglia di Algeri...</td>\n",
" <td>Drama|War</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" movieId title \\\n",
"935 1176 Double Life of Veronique, The (Double Vie de V... \n",
"2066 2575 Dreamlife of Angels, The (Vie rêvée des anges,... \n",
"2478 3083 All About My Mother (Todo sobre mi madre) (1999) \n",
"2872 3598 Hamlet (2000) \n",
"3034 3794 Chuck & Buck (2000) \n",
"3082 3854 Aimée & Jaguar (1999) \n",
"4005 5222 Kissing Jessica Stein (2001) \n",
"4314 5792 Roger Dodger (2002) \n",
"4738 6641 Code Unknown (Code inconnu: Récit incomplet de... \n",
"5738 26131 Battle of Algiers, The (La battaglia di Algeri... \n",
"\n",
" genres \n",
"935 Drama|Fantasy|Romance \n",
"2066 Drama \n",
"2478 Drama \n",
"2872 Crime|Drama|Romance|Thriller \n",
"3034 Comedy|Drama \n",
"3082 Drama|Romance|War \n",
"4005 Comedy|Romance \n",
"4314 Comedy|Drama \n",
"4738 Drama \n",
"5738 Drama|War "
]
},
"execution_count": 362,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"get_movie_names(similar_movies(858)) # Movie 858 is \"Godfather\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 2",
"language": "python",
"name": "python2"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.13"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment