{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Topic Diversification\n", "** *\n", "*Note: if you are visualizing this notebook directly from GitHub, some mathematical symbols might display incorrectly or not display at all. This same notebook can be rendered from nbviewer by following [this link.](http://nbviewer.jupyter.org/github/david-cortes/datascienceprojects/blob/master/machine_learning/topic_diversification.ipynb)*\n", "\n", "This project consists of taking a recommendation list (can be applied to anything, in this case it’s movies) that is sorted by a score produced by some recommendation formula, containing more elements than the final recommendation list that will be shown to the user, and selecting a subset of those elements that contains more diversity than the original top-N elements.\n", "\n", "The objective is to make the list more diverse in the sense of containing elements that are not all too similar to each other. As recommendation formulas tend to be based on estimating the rating that a user would give to an item or the probability that he/she would click or buy it, these lists can end up containing elements that are all too similar – e.g. only books from the same author, only movies from a certain genre, etc. – and, despite having good predictive accuracy, might not be as valuable to the user as lists of recommendations that cater to more different interests.\n", "\n", "There are different ways of making a list more diverse, and all of these require some information about the elements included, such as the genres of a movie or the tags that they have received. Here I’ll use movies' genres and PCA-reduced tags assigned by users. The data is taken from the [MovieLens latest dataset](https://grouplens.org/datasets/movielens/latest/) (at the time this was produced, the last update was on 10/2016), which includes 24M movie ratings from 260K users to 40K movies, the movies genres, and 1,128 possible tags applied to most of those movies, in a numerical scale as explained in [Vig, J., Sen, S., & Riedl, J. (2012). The tag genome: Encoding community knowledge to support novel interaction. ACM Transactions on Interactive Intelligent Systems (TiiS), 2(3), 13.](http://dl.acm.org/citation.cfm?id=2362395).\n", "\n", "I’ll make a simple recommendation list by taking the average rating that a movie has received, multiplied by the number of users who have rated it, and sorting by this score. The final recommendation will be limited to 10 movies, and the candidate pool from which to diversify will be limited to the top 50 movies by this score.\n", "** *\n", "## Sections\n", "\n", "[1. Producing the recommendation list](#p1)\n", "\n", "[2. Diversification as Maximum Coverage Problem](#p2)\n", "\n", "[3. Diversification as Maximal Marginal Relevance](#p3)\n", "\n", "[4. Topic Diversifcation heuristic](#p4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "## 1. Producing the recommendation list\n", "\n", "This is a generic non-personalized recommendation list, accordignto the movies' average rating and people who have rated them, as explained at the beginning:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
ratingratersscoretitlegenres
movieId
3184.43308984455.0374396.5Shawshank Redemption, The (1994)Crime|Drama
3564.04710986629.0350597.0Forrest Gump (1994)Comedy|Drama|Romance|War
2964.16338683523.0347738.5Pulp Fiction (1994)Comedy|Crime|Drama|Thriller
5934.15385480274.0333446.5Silence of the Lambs, The (1991)Crime|Horror|Thriller
2604.14286572215.0299177.0Star Wars: Episode IV - A New Hope (1977)Action|Adventure|Sci-Fi
25714.16047671450.0297266.0Matrix, The (1999)Action|Sci-Fi|Thriller
5274.27596363889.0273187.0Schindler's List (1993)Drama|War
4803.65606372147.0263774.0Jurassic Park (1993)Action|Adventure|Sci-Fi|Thriller
1104.02271663920.0257132.0Braveheart (1995)Action|Drama|War
13.88930063469.0246850.0Toy Story (1995)Adventure|Animation|Children|Comedy|Fantasy
\n", "
" ], "text/plain": [ " rating raters score \\\n", "movieId \n", "318 4.433089 84455.0 374396.5 \n", "356 4.047109 86629.0 350597.0 \n", "296 4.163386 83523.0 347738.5 \n", "593 4.153854 80274.0 333446.5 \n", "260 4.142865 72215.0 299177.0 \n", "2571 4.160476 71450.0 297266.0 \n", "527 4.275963 63889.0 273187.0 \n", "480 3.656063 72147.0 263774.0 \n", "110 4.022716 63920.0 257132.0 \n", "1 3.889300 63469.0 246850.0 \n", "\n", " title \\\n", "movieId \n", "318 Shawshank Redemption, The (1994) \n", "356 Forrest Gump (1994) \n", "296 Pulp Fiction (1994) \n", "593 Silence of the Lambs, The (1991) \n", "260 Star Wars: Episode IV - A New Hope (1977) \n", "2571 Matrix, The (1999) \n", "527 Schindler's List (1993) \n", "480 Jurassic Park (1993) \n", "110 Braveheart (1995) \n", "1 Toy Story (1995) \n", "\n", " genres \n", "movieId \n", "318 Crime|Drama \n", "356 Comedy|Drama|Romance|War \n", "296 Comedy|Crime|Drama|Thriller \n", "593 Crime|Horror|Thriller \n", "260 Action|Adventure|Sci-Fi \n", "2571 Action|Sci-Fi|Thriller \n", "527 Drama|War \n", "480 Action|Adventure|Sci-Fi|Thriller \n", "110 Action|Drama|War \n", "1 Adventure|Animation|Children|Comedy|Fantasy " ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd, numpy as np\n", "import warnings\n", "\n", "warnings.filterwarnings('ignore')\n", "\n", "ratings=pd.read_csv('D:\\\\Downloads\\\\movielens\\\\ml-latest\\\\ml-latest\\\\ratings.csv')\n", "movies=pd.read_csv('D:\\\\Downloads\\\\movielens\\\\ml-latest\\\\ml-latest\\\\movies.csv')\n", "\n", "top_rated=(ratings.assign(raters=ratings.rating>=0).groupby('movieId').agg({'rating':np.mean,'raters':np.sum})\n", " .assign(score=lambda x: x.rating*x.raters).sort_values('score',ascending=False).head(50))\n", "movies=movies.loc[movies.movieId.map(lambda x: x in top_rated.index)]\n", "del ratings\n", "top_rated.join(movies.set_index('movieId')).head(10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "## 2. Diversification as Maximum Coverage Problem\n", "\n", "One of the simplest ways to diversify the recommendation list is by making them cover the largest possible set of genres, plus some penalization for the drop in score that this diversification would produce. This would of course tend to favor movies with more genres (as each movie can belong to more than 1 genre).\n", "\n", "Such a process can be modeled in a similar way to the [maximum coverage problem](https://en.wikipedia.org/wiki/Maximum_coverage_problem), by adding to the maximization objective a term dependent on the scaled movie score, such as this:\n", "\n", "$$ \\sum_{g \\in genres} Genre_g + \\sum_{m \\in TopMovies} Score_m \\times Movie_m $$\n", "$$ s.t. $$\n", "$$ \\sum_{m \\in TopMovies} Movie_m = \\left\\vert Final\\, Recommended\\, List \\right\\vert $$\n", "$$ \\sum_{m \\in g} Movie_m >= Genre_g \\,\\, \\forall g \\in genres $$\n", "$$ Movie_m, Genre_g \\in \\{0,1\\} \\,\\, \\forall m \\in TopMovies, g \\in genres $$\n", "\n", "\n", "(Where $Movie_m$ and $Genre_g$ are the decision variables determining whehter a particular movie ends up in the final recommendation list and whether this list contains each genre).\n", "\n", "\n", "While solving this problem is NP-hard, branch-and-cut algorithms for mixed-integer programming can solve it very quickly if the recommendation list is short. In this case, I'll use Google's or-tools for modeling the problem, and solve it using coin-or's CBC - both of these are free software:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": false }, "outputs": [], "source": [ "# adding genres and rescaling year\n", "import re\n", "\n", "movies['hasYear']=movies.title.map(lambda x: bool(re.search(\"\\s\\((\\d{4})\\)$\",x.strip())))\n", "movies['Year']='unknown'\n", "movies['Year'].loc[movies.hasYear]=movies.title.loc[movies.hasYear].map(lambda x: re.search(\"\\s\\((\\d{4})\\)$\",x.strip()).group(1))\n", "del movies['hasYear']\n", "\n", "movies['genres']=movies.genres.map(lambda x: set(x.split('|')))\n", "present_genres=set()\n", "for movie in movies.itertuples():\n", " present_genres=present_genres.union(movie.genres)\n", "for genre in present_genres:\n", " movies['genre'+genre]=movies.genres.map(lambda x: 1.0*(genre in x))\n", " \n", "# switching movieId to a column in the top ratings df\n", "top_rated=top_rated.reset_index()\n", "\n", "# adding a movie's genres to the top rated df\n", "top_rated=pd.merge(top_rated,movies[['movieId','title','genres']],on='movieId')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Creating the decision variables:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false }, "outputs": [], "source": [ "from ortools.linear_solver import pywraplp\n", "\n", "# starting solver\n", "solver = pywraplp.Solver('diversify_list',pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING)\n", "\n", "# converting movie IDs to sequential integers\n", "movie_to_int=dict()\n", "int_to_movie=dict()\n", "cnt=0\n", "for movie in movies.movieId:\n", " movie_to_int[movie]=cnt\n", " int_to_movie[cnt]=movie\n", " cnt+=1\n", " \n", "# variables indicating which movie is included in the final recommendation list\n", "rec_movie=[solver.BoolVar('movie'+str(m)) for m in range(cnt)]\n", "\n", "# assigning integer IDs to genres\n", "genre_to_int=dict()\n", "cnt=0\n", "for genre in present_genres:\n", " genre_to_int[genre]=cnt\n", " cnt+=1\n", "\n", "# variables indicating the present genres\n", "genres=[solver.BoolVar('genre'+str(g)) for g in range(cnt)]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Modeling the problem in or-tools:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# maximize the number of included genres plus the score that the included movies bring\n", "solver.Maximize(solver.Sum(genres) + solver.Sum((m.score/300000)*rec_movie[movie_to_int[m.movieId]] for m in top_rated.itertuples()))\n", "\n", "# only 10 movies are selected\n", "solver.Add(solver.Sum(rec_movie)==10)\n", "\n", "# genres are only selected when a recommended movie has that genre\n", "for genre in present_genres:\n", " solver.Add(solver.Sum(rec_movie[movie_to_int[m.movieId]] for m in top_rated.itertuples() if (genre in m.genres))>=genres[genre_to_int[genre]])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Timing the solver:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Wall time: 35 ms\n" ] }, { "data": { "text/plain": [ "0" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%time\n", "solver.Solve()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Checking how the recommendation list was altered:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
movieIdratingratersscoretitlegenrespreviously_here
03184.43308984455.0374396.5Shawshank Redemption, The (1994){Drama, Crime}Shawshank Redemption, The (1994)
13564.04710986629.0350597.0Forrest Gump (1994){Drama, Comedy, War, Romance}Forrest Gump (1994)
22964.16338683523.0347738.5Pulp Fiction (1994){Drama, Comedy, Crime, Thriller}Pulp Fiction (1994)
35934.15385480274.0333446.5Silence of the Lambs, The (1991){Thriller, Crime, Horror}Silence of the Lambs, The (1991)
42604.14286572215.0299177.0Star Wars: Episode IV - A New Hope (1977){Action, Sci-Fi, Adventure}Star Wars: Episode IV - A New Hope (1977)
525714.16047671450.0297266.0Matrix, The (1999){Action, Sci-Fi, Thriller}Matrix, The (1999)
65274.27596363889.0273187.0Schindler's List (1993){Drama, War}Schindler's List (1993)
10504.30863656348.0242783.0Usual Suspects, The (1995){Thriller, Crime, Mystery}Jurassic Park (1993)
285903.74108850803.0190058.5Dances with Wolves (1990){Drama, Adventure, Western}Braveheart (1995)
445953.67451240195.0147697.0Beauty and the Beast (1991){Musical, IMAX, Animation, Children, Romance, ...Toy Story (1995)
\n", "
" ], "text/plain": [ " movieId rating raters score \\\n", "0 318 4.433089 84455.0 374396.5 \n", "1 356 4.047109 86629.0 350597.0 \n", "2 296 4.163386 83523.0 347738.5 \n", "3 593 4.153854 80274.0 333446.5 \n", "4 260 4.142865 72215.0 299177.0 \n", "5 2571 4.160476 71450.0 297266.0 \n", "6 527 4.275963 63889.0 273187.0 \n", "10 50 4.308636 56348.0 242783.0 \n", "28 590 3.741088 50803.0 190058.5 \n", "44 595 3.674512 40195.0 147697.0 \n", "\n", " title \\\n", "0 Shawshank Redemption, The (1994) \n", "1 Forrest Gump (1994) \n", "2 Pulp Fiction (1994) \n", "3 Silence of the Lambs, The (1991) \n", "4 Star Wars: Episode IV - A New Hope (1977) \n", "5 Matrix, The (1999) \n", "6 Schindler's List (1993) \n", "10 Usual Suspects, The (1995) \n", "28 Dances with Wolves (1990) \n", "44 Beauty and the Beast (1991) \n", "\n", " genres \\\n", "0 {Drama, Crime} \n", "1 {Drama, Comedy, War, Romance} \n", "2 {Drama, Comedy, Crime, Thriller} \n", "3 {Thriller, Crime, Horror} \n", "4 {Action, Sci-Fi, Adventure} \n", "5 {Action, Sci-Fi, Thriller} \n", "6 {Drama, War} \n", "10 {Thriller, Crime, Mystery} \n", "28 {Drama, Adventure, Western} \n", "44 {Musical, IMAX, Animation, Children, Romance, ... \n", "\n", " previously_here \n", "0 Shawshank Redemption, The (1994) \n", "1 Forrest Gump (1994) \n", "2 Pulp Fiction (1994) \n", "3 Silence of the Lambs, The (1991) \n", "4 Star Wars: Episode IV - A New Hope (1977) \n", "5 Matrix, The (1999) \n", "6 Schindler's List (1993) \n", "10 Jurassic Park (1993) \n", "28 Braveheart (1995) \n", "44 Toy Story (1995) " ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "final_recs=set()\n", "for m in range(len(rec_movie)):\n", " if rec_movie[m].solution_value()>.5:\n", " final_recs.add(int_to_movie[m])\n", " \n", "recs_maxcover=list(top_rated.title.loc[top_rated.movieId.map(lambda x: x in final_recs)])\n", "new_list=top_rated.loc[top_rated.movieId.map(lambda x: x in final_recs)]\n", "new_list['previously_here']=list(top_rated.head(10).title)\n", "new_list" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "## 3. Diversification as Maximal Marginal Relevance\n", "\n", "Given a function that determines the similarity between two elements in the recommended list, it's possible to reorder them using the maximal marginal relevance heuristic, as described in [Carbonell, J., & Goldstein, J. (1998, August). The use of MMR, diversity-based reranking for reordering documents and producing summaries. In Proceedings of the 21st annual international ACM SIGIR conference on Research and development in information retrieval (pp. 335-336). ACM.](http://repository.cmu.edu/cgi/viewcontent.cgi?article=1330&context=compsci).\n", "\n", "Intuitively, this aims to rerank the list in such a way that each next element in the list gets a penalty according to it's maximal similarity to any element ranked above it. This method was originally thought for the case of search result rankings, where there is a function determining the relevance of a document in regards to a query, but here the list is fixed, thus this relevance score is the same as the score they get in the list.\n", "\n", "In order to get the pairwise similarities between two movies, I'll take their genres, year of production and tags. As there are too many tags and I won't want them to weight too much in the formula, I'll first recude their dimensionality by taking their first principal components. The pairwise similarity will then be calculated as the cosine distance of such features (vectors of movie's one-hot encoded genres, scaled year of production and some principal components of tags)." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# rescaling the year in order to give it more weight:\n", "def scaled_year(x):\n", " if x=='unknown':\n", " return 1\n", " else:\n", " x=int(x)\n", " return (x-1990)/4\n", "\n", "movies['ScaledYear']=movies.Year.map(scaled_year)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Converting the tags to their principal components:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false }, "outputs": [ { "data": { "image/png": "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\nP7+yg5xvr6rHquq+Hcfm5qyqnxy6f7Cq/npHmX+6qh4Z+r6nqr6zl8xV9byq+q2q+lBV3V9VPzYc\n77bnfTL33PNlVfX7VfWBIfPPDMd77nle5m573pFjpar+oKp+ZXjebc/7ZO6654t9H+k4c+89P6uq\nfqmqPlxVD1TVt0yg570yd9tzVb1wR657qurzVfWmnnveJ3O3PQ8Z/tHwfnJfVb1zeJ/ptud9Mvfe\n848Nee+vqjcNx3rvea/M3fVcC/psUlUvr9n70UNV9a+rqnrIXFXXVNUXd3R+e0eZv3/4/tiuqtO7\nzl96z08rrTVfT+EryUqSjya5LsnxJB9I8qJl55qT9RNJnrPr2D9Nctvw+LYkb+kg52uSvCzJfRfK\nmeRFQ+cnklw7/LdY6STzTyf5iT3OXXrmJM9N8rLh8TOSfGTI1W3P+2TuuedKcnJ4fGmS30vyys57\nnpe52553ZPnxJO9I8ivD82573idz1z3nIt5HOs/ce8//IckbhsfHkzxrAj3vlbnrnnfkWUnymSRf\n03vPczJ323OSq5J8PMnlw/NfTPJ3e+55n8w99/yNSe5LckWSY0l+I8nXdt7zvMzd9ZwFfTZJ8vuZ\n/UxVSf5nkps6yXzNzvN2/e8sO/M3JHlhkrNJTh/k+2HMzE+nLyuEnrpXJHmotfax1tp6kjNJbl5y\npotxc2Y/xGX483uWmCVJ0lr7nST/Z9fheTlvTnKmtbbWWvt4kocy+28yqjmZ51l65tbap1tr7x8e\nfyHJA5n9ANRtz/tknqeHzK21tjo8vXT4aum753mZ51l65iSpqquT/I0kb92Vrcuek7mZ5+ki8xxd\n93yRlp65qr4isx+K35YkrbX11tr/Tcc975N5nqVn3uW1ST7aWvujdNzzLjszz9NL5mNJLq+qY5l9\n+P/j9N/zXpnn6SHzNyT5vdba/2utbSb57SR/K333PC/zPEvLvIjPJlX13CTPbK39bmutJfmP+XP8\n3HWRmffUQ+bW2gOttQf3OL2Lnp9ODISeuquSfGrH84ez/4fUZWpJfqOq7q6qW4djp1prnx4efybJ\nqeVEu6B5OXvv/x9W1b3DEsjzS0m7ylxV1yR5aWYrQSbR867MScc912xL0D1JHkvy66217nuekznp\nuOck/zLJP06yveNY1z1n78xJ3z1fzPtIz5mTfnu+Nslnk/z7mm0nfGtVXZm+e56XOem3551uSfLO\n4XHPPe+0M3PSac+ttUeS/LMkn0zy6SR/1lr7tXTc8z6Zk057zmylzaur6tlVdUWS70zyvHTcc+Zn\nTvrteaeL7faq4fHu42Pa73PftcN2sd+uqlcPx3rIPE/PPU+SgdDR8KrW2kuS3JTkH1TVa3a+OExR\n91sJ0IWp5Ezy85ltJXxJZj9Q/PPlxvlyVXUyyX9N8qbW2ud3vtZrz3tk7rrn1trW8P+7qzP7zcU3\n7nq9u57nZO6256r6m0kea63dPe+c3nreJ3O3PQ+m+D6yV+aeez6W2ZL5n2+tvTTJucyW9j+uw57n\nZe655yRJVR1P8t1J/svu1zrsOcmembvtefgwf3NmQ8O/lOTKqvrBnef01vM+mbvtubX2QJK3JPm1\nJL+a5J4kW7vO6arnfTJ32/M8vXV7ELsyfzrJ84f3yh9P8o6qeubSwrEUBkJP3SN5YpqdzD5APbKk\nLPsafuOR1tpjSf5bZsssHx2W1p1fFvjY8hLua17ObvtvrT06fLDeTvLv8sSy1i4yV9WlmQ1W/nNr\n7V3D4a573itz7z2fN2yf+K0kN6bzns/bmbnznr81yXdX1Scy27b716rqF9J3z3tm7rzni30f6TZz\n5z0/nOThHSvzfimzYUvPPe+ZufOez7spyftba48Oz3vu+bwnZe68529L8vHW2mdbaxtJ3pXkr6Tv\nnvfM3HnPaa29rbX28tbaa5L8aWbXWuy55z0z997zDhfb7SPD493Hx7Rn5mHb1eeGx3dndj2er0sf\nmefpuedJMhB66u5K8oKqunb4jc0tSe5ccqYvU1VXVtUzzj9O8h2ZLdW8M8kPD6f9cJJfXk7CC5qX\n884kt1TViaq6NskLMruQ2NKd/wd38LrM+k46yDxcbf9tSR5orf2LHS912/O8zJ33/NVV9azh8eVJ\nvj3Jh9N3z3tm7rnn1tpPttaubq1dk9m/wf+rtfaD6bjneZl77vkpvI90m7nnnltrn0nyqap64XDo\ntUk+lI57npe55553+IE8eetVtz3v8KTMnff8ySSvrKorhvfx12Z2DcCee94zc+c9p6r+wvDn8zO7\nFs870nfPe2buvecdLqrbYavW56vqlcP31Q9l/M9de2YefvZbGR5fN2T+WCeZ5+m552lqHVzZeqpf\nme15/Uhm09SfWnaeORmvy+xK7B9Icv/5nEmeneQ3k/xhZlf3/6oOsr4zs6WLG5n91vFH9suZ5KeG\n7h/Mkq4iPyfzf0rywST3ZvaP1nN7yZzkVZktE703syW69wzfx932vE/mnnt+cZI/GLLdl+TNw/Ge\ne56Xudued+W/Pk/csavbnvfJ3G3PeQrvIx1n7rbnIcNLkrxvyPffk3xlzz3vk7n3nq9M8rkkX7Hj\nWO8975W5955/JrNfhtw3ZD0xgZ73ytx7z/87s+HxB5K8diLfz3tl7q7nLOizSZLTw/fUR5P8myTV\nQ+Yk35vZe+Q9Sd6f5Ls6yvy64fFakkeTvKennp9OXzWUBwAAAMARYcsYAAAAwBFjIAQAAABwxBgI\nAQAAABwxBkIAAAAAR4yBEAAAAMARYyAEAAAAcMQYCAEAAAAcMf8f6Pfh6e7YOB0AAAAASUVORK5C\nYII=\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from sklearn.decomposition import PCA\n", "import matplotlib.pyplot as plt\n", "%matplotlib inline\n", "\n", "tags=pd.read_csv('D:\\\\Downloads\\\\movielens\\\\ml-latest\\\\ml-latest\\\\genome-scores.csv')\n", "tags_wide=tags.pivot(index='movieId', columns='tagId', values='relevance')\n", "tags_wide=tags_wide.fillna(0)\n", "pca=PCA(svd_solver='full')\n", "pca.fit(tags_wide)\n", "\n", "plt.figure(figsize=(20,8))\n", "plt.plot(np.cumsum(pca.explained_variance_ratio_))\n", "plt.yticks(np.arange(0.2, 1.1, .1))\n", "plt.xticks(np.arange(0, 1128, 50))\n", "plt.grid()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "From the plot above, using the first 50 principal components seems to carry most of the information contained by the tags, so I'll take these only:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
genreComedygenreDramagenreMysterygenreIMAXgenreAdventuregenreChildrengenreCrimegenreHorrorgenreWesterngenreMusical...pc40pc41pc42pc43pc44pc45pc46pc47pc48pc49
movieId
11.00.00.00.01.01.00.00.00.00.0...0.0531800.074886-0.1007500.049117-0.008817-0.002725-0.016538-0.065847-0.014171-0.083305
320.00.01.00.00.00.00.00.00.00.0...-0.048473-0.003494-0.120764-0.173214-0.001152-0.1917890.0890150.078067-0.0529950.021039
470.00.01.00.00.00.00.00.00.00.0...0.0732530.008341-0.1250620.1150220.044525-0.1033250.020353-0.100851-0.023210-0.001501
500.00.01.00.00.00.01.00.00.00.0...0.1073800.042832-0.065706-0.046828-0.004257-0.245711-0.0120250.020237-0.1159890.088346
1100.01.00.00.00.00.00.00.00.00.0...0.039152-0.020539-0.2167450.049616-0.054584-0.1479810.0760410.0110750.0445950.048206
\n", "

5 rows × 68 columns

\n", "
" ], "text/plain": [ " genreComedy genreDrama genreMystery genreIMAX genreAdventure \\\n", "movieId \n", "1 1.0 0.0 0.0 0.0 1.0 \n", "32 0.0 0.0 1.0 0.0 0.0 \n", "47 0.0 0.0 1.0 0.0 0.0 \n", "50 0.0 0.0 1.0 0.0 0.0 \n", "110 0.0 1.0 0.0 0.0 0.0 \n", "\n", " genreChildren genreCrime genreHorror genreWestern genreMusical \\\n", "movieId \n", "1 1.0 0.0 0.0 0.0 0.0 \n", "32 0.0 0.0 0.0 0.0 0.0 \n", "47 0.0 0.0 0.0 0.0 0.0 \n", "50 0.0 1.0 0.0 0.0 0.0 \n", "110 0.0 0.0 0.0 0.0 0.0 \n", "\n", " ... pc40 pc41 pc42 pc43 pc44 pc45 \\\n", "movieId ... \n", "1 ... 0.053180 0.074886 -0.100750 0.049117 -0.008817 -0.002725 \n", "32 ... -0.048473 -0.003494 -0.120764 -0.173214 -0.001152 -0.191789 \n", "47 ... 0.073253 0.008341 -0.125062 0.115022 0.044525 -0.103325 \n", "50 ... 0.107380 0.042832 -0.065706 -0.046828 -0.004257 -0.245711 \n", "110 ... 0.039152 -0.020539 -0.216745 0.049616 -0.054584 -0.147981 \n", "\n", " pc46 pc47 pc48 pc49 \n", "movieId \n", "1 -0.016538 -0.065847 -0.014171 -0.083305 \n", "32 0.089015 0.078067 -0.052995 0.021039 \n", "47 0.020353 -0.100851 -0.023210 -0.001501 \n", "50 -0.012025 0.020237 -0.115989 0.088346 \n", "110 0.076041 0.011075 0.044595 0.048206 \n", "\n", "[5 rows x 68 columns]" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "tags_pca=pd.DataFrame(pca.transform(tags_wide)[:,:50]/3)\n", "tags_pca.columns=[\"pc\"+str(x) for x in tags_pca.columns.values]\n", "tags_pca['movieId']=tags_wide.index\n", "del tags\n", "del tags_wide\n", "movies=pd.merge(movies,tags_pca,how='inner',on='movieId')\n", "movies_features=movies.copy()\n", "del movies_features['title']\n", "del movies_features['genres']\n", "del movies_features['Year']\n", "movies_features.set_index('movieId',inplace=True)\n", "movies_features.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Constructing the similarity function;" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": false }, "outputs": [], "source": [ "from sklearn.metrics.pairwise import cosine_similarity\n", "\n", "def sim_movies(movie1,movie2):\n", " vec1=movies_features.loc[movie1].reshape(1,-1)\n", " vec2=movies_features.loc[movie2].reshape(1,-1)\n", " return cosine_similarity(vec1,vec2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Implementing the heuristic and checking the results - Here I'll set the diversification factor at 0.3, meaning that the final list is more geared towards diversified elements than original scores:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
movieIdratingratersscoretitlegenresreorder_maxcoveroriginally_here
03184.43308984455.0374396.5Shawshank Redemption, The (1994){Drama, Crime}Shawshank Redemption, The (1994)Shawshank Redemption, The (1994)
525714.16047671450.0297266.0Matrix, The (1999){Action, Sci-Fi, Thriller}Forrest Gump (1994)Forrest Gump (1994)
65274.27596363889.0273187.0Schindler's List (1993){Drama, War}Pulp Fiction (1994)Pulp Fiction (1994)
74803.65606372147.0263774.0Jurassic Park (1993){Action, Sci-Fi, Adventure, Thriller}Silence of the Lambs, The (1991)Silence of the Lambs, The (1991)
191503.88328556077.0217763.0Apollo 13 (1995){Drama, Adventure, IMAX}Star Wars: Episode IV - A New Hope (1977)Star Wars: Episode IV - A New Hope (1977)
2049934.10944551871.0213161.0Lord of the Rings: The Fellowship of the Ring,...{Fantasy, Adventure}Matrix, The (1999)Matrix, The (1999)
216084.10545450463.0207173.5Fargo (1996){Drama, Comedy, Crime, Thriller}Schindler's List (1993)Schindler's List (1993)
2212703.91352852219.0204360.5Back to the Future (1985){Comedy, Sci-Fi, Adventure}Usual Suspects, The (1995)Jurassic Park (1993)
23474.06385950142.0203770.0Seven (a.k.a. Se7en) (1995){Thriller, Mystery}Dances with Wolves (1990)Braveheart (1995)
445953.67451240195.0147697.0Beauty and the Beast (1991){Musical, IMAX, Animation, Children, Romance, ...Beauty and the Beast (1991)Toy Story (1995)
\n", "
" ], "text/plain": [ " movieId rating raters score \\\n", "0 318 4.433089 84455.0 374396.5 \n", "5 2571 4.160476 71450.0 297266.0 \n", "6 527 4.275963 63889.0 273187.0 \n", "7 480 3.656063 72147.0 263774.0 \n", "19 150 3.883285 56077.0 217763.0 \n", "20 4993 4.109445 51871.0 213161.0 \n", "21 608 4.105454 50463.0 207173.5 \n", "22 1270 3.913528 52219.0 204360.5 \n", "23 47 4.063859 50142.0 203770.0 \n", "44 595 3.674512 40195.0 147697.0 \n", "\n", " title \\\n", "0 Shawshank Redemption, The (1994) \n", "5 Matrix, The (1999) \n", "6 Schindler's List (1993) \n", "7 Jurassic Park (1993) \n", "19 Apollo 13 (1995) \n", "20 Lord of the Rings: The Fellowship of the Ring,... \n", "21 Fargo (1996) \n", "22 Back to the Future (1985) \n", "23 Seven (a.k.a. Se7en) (1995) \n", "44 Beauty and the Beast (1991) \n", "\n", " genres \\\n", "0 {Drama, Crime} \n", "5 {Action, Sci-Fi, Thriller} \n", "6 {Drama, War} \n", "7 {Action, Sci-Fi, Adventure, Thriller} \n", "19 {Drama, Adventure, IMAX} \n", "20 {Fantasy, Adventure} \n", "21 {Drama, Comedy, Crime, Thriller} \n", "22 {Comedy, Sci-Fi, Adventure} \n", "23 {Thriller, Mystery} \n", "44 {Musical, IMAX, Animation, Children, Romance, ... \n", "\n", " reorder_maxcover \\\n", "0 Shawshank Redemption, The (1994) \n", "5 Forrest Gump (1994) \n", "6 Pulp Fiction (1994) \n", "7 Silence of the Lambs, The (1991) \n", "19 Star Wars: Episode IV - A New Hope (1977) \n", "20 Matrix, The (1999) \n", "21 Schindler's List (1993) \n", "22 Usual Suspects, The (1995) \n", "23 Dances with Wolves (1990) \n", "44 Beauty and the Beast (1991) \n", "\n", " originally_here \n", "0 Shawshank Redemption, The (1994) \n", "5 Forrest Gump (1994) \n", "6 Pulp Fiction (1994) \n", "7 Silence of the Lambs, The (1991) \n", "19 Star Wars: Episode IV - A New Hope (1977) \n", "20 Matrix, The (1999) \n", "21 Schindler's List (1993) \n", "22 Jurassic Park (1993) \n", "23 Braveheart (1995) \n", "44 Toy Story (1995) " ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "rec_mmr=[top_rated.movieId.iloc[0]]\n", "l=.3\n", "\n", "for i in range(9):\n", " movies_left=top_rated.loc[top_rated.movieId.map(lambda x: x not in rec_mmr)]\n", " movies_orig_score=l*movies_left.score/300000\n", " movies_adj_score=np.array([np.max([sim_movies(m1,m2) for m2 in rec_mmr]) for m1 in movies_left.movieId])\n", " movies_new_score=movies_orig_score-(1-l)*movies_adj_score\n", " rec_mmr.append(movies_left.movieId.iloc[np.argmax(movies_new_score)])\n", " \n", "new_list=top_rated.loc[top_rated.movieId.map(lambda x: x in rec_mmr)]\n", "new_list['reorder_maxcover']=recs_maxcover\n", "new_list['originally_here']=list(top_rated.head(10).title)\n", "new_list" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "## 4. Topic Diversifcation heuristic\n", "\n", "Another this is another heuristic based on [Ziegler, C. N., McNee, S. M., Konstan, J. A., & Lausen, G. (2005, May). Improving recommendation lists through topic diversification. In Proceedings of the 14th international conference on World Wide Web (pp. 22-32). ACM.](https://www.researchgate.net/profile/Cai-Nicolas_Ziegler/publication/200110416_Improving_recommendation_lists_through_topic_diversification/links/0fcfd510f5bfa4aa48000000.pdf) (written by some of the same people who develop and mantain the GroupLens datasets, including the MovieLens data used here).\n", "\n", "The idea is to rerank elements according to the rank that they would get when ordering them by the recommendation formula's score, and when ordering them inversely according to their sum of similarity to the elements ranked above. Just like the Maximal Marginal Relevance heuristic, this is based on element's pairwise similarities, which I'll take in the same way as before.\n", "\n", "Again, I'll set the diversification factor at 0.3, making the final list is more geared towards diversified elements than original scores:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
movieIdratingratersscoretitlegenresreorder_mmrreorder_maxcoveroriginally_here
03184.43308984455.0374396.5Shawshank Redemption, The (1994){Drama, Crime}Shawshank Redemption, The (1994)Shawshank Redemption, The (1994)Shawshank Redemption, The (1994)
22964.16338683523.0347738.5Pulp Fiction (1994){Drama, Comedy, Crime, Thriller}Matrix, The (1999)Forrest Gump (1994)Forrest Gump (1994)
35934.15385480274.0333446.5Silence of the Lambs, The (1991){Thriller, Crime, Horror}Schindler's List (1993)Pulp Fiction (1994)Pulp Fiction (1994)
525714.16047671450.0297266.0Matrix, The (1999){Action, Sci-Fi, Thriller}Jurassic Park (1993)Silence of the Lambs, The (1991)Silence of the Lambs, The (1991)
81104.02271663920.0257132.0Braveheart (1995){Drama, Action, War}Apollo 13 (1995)Star Wars: Episode IV - A New Hope (1977)Star Wars: Episode IV - A New Hope (1977)
913.88930063469.0246850.0Toy Story (1995){Fantasy, Comedy, Adventure, Animation, Children}Lord of the Rings: The Fellowship of the Ring,...Matrix, The (1999)Matrix, The (1999)
10504.30863656348.0242783.0Usual Suspects, The (1995){Thriller, Crime, Mystery}Fargo (1996)Schindler's List (1993)Schindler's List (1993)
1111964.14840857625.0239052.0Star Wars: Episode V - The Empire Strikes Back...{Action, Sci-Fi, Adventure}Back to the Future (1985)Usual Suspects, The (1995)Jurassic Park (1993)
135893.93733359409.0233913.0Terminator 2: Judgment Day (1991){Action, Sci-Fi}Seven (a.k.a. Se7en) (1995)Dances with Wolves (1990)Braveheart (1995)
158584.34362353547.0232588.0Godfather, The (1972){Drama, Crime}Beauty and the Beast (1991)Beauty and the Beast (1991)Toy Story (1995)
\n", "
" ], "text/plain": [ " movieId rating raters score \\\n", "0 318 4.433089 84455.0 374396.5 \n", "2 296 4.163386 83523.0 347738.5 \n", "3 593 4.153854 80274.0 333446.5 \n", "5 2571 4.160476 71450.0 297266.0 \n", "8 110 4.022716 63920.0 257132.0 \n", "9 1 3.889300 63469.0 246850.0 \n", "10 50 4.308636 56348.0 242783.0 \n", "11 1196 4.148408 57625.0 239052.0 \n", "13 589 3.937333 59409.0 233913.0 \n", "15 858 4.343623 53547.0 232588.0 \n", "\n", " title \\\n", "0 Shawshank Redemption, The (1994) \n", "2 Pulp Fiction (1994) \n", "3 Silence of the Lambs, The (1991) \n", "5 Matrix, The (1999) \n", "8 Braveheart (1995) \n", "9 Toy Story (1995) \n", "10 Usual Suspects, The (1995) \n", "11 Star Wars: Episode V - The Empire Strikes Back... \n", "13 Terminator 2: Judgment Day (1991) \n", "15 Godfather, The (1972) \n", "\n", " genres \\\n", "0 {Drama, Crime} \n", "2 {Drama, Comedy, Crime, Thriller} \n", "3 {Thriller, Crime, Horror} \n", "5 {Action, Sci-Fi, Thriller} \n", "8 {Drama, Action, War} \n", "9 {Fantasy, Comedy, Adventure, Animation, Children} \n", "10 {Thriller, Crime, Mystery} \n", "11 {Action, Sci-Fi, Adventure} \n", "13 {Action, Sci-Fi} \n", "15 {Drama, Crime} \n", "\n", " reorder_mmr \\\n", "0 Shawshank Redemption, The (1994) \n", "2 Matrix, The (1999) \n", "3 Schindler's List (1993) \n", "5 Jurassic Park (1993) \n", "8 Apollo 13 (1995) \n", "9 Lord of the Rings: The Fellowship of the Ring,... \n", "10 Fargo (1996) \n", "11 Back to the Future (1985) \n", "13 Seven (a.k.a. Se7en) (1995) \n", "15 Beauty and the Beast (1991) \n", "\n", " reorder_maxcover \\\n", "0 Shawshank Redemption, The (1994) \n", "2 Forrest Gump (1994) \n", "3 Pulp Fiction (1994) \n", "5 Silence of the Lambs, The (1991) \n", "8 Star Wars: Episode IV - A New Hope (1977) \n", "9 Matrix, The (1999) \n", "10 Schindler's List (1993) \n", "11 Usual Suspects, The (1995) \n", "13 Dances with Wolves (1990) \n", "15 Beauty and the Beast (1991) \n", "\n", " originally_here \n", "0 Shawshank Redemption, The (1994) \n", "2 Forrest Gump (1994) \n", "3 Pulp Fiction (1994) \n", "5 Silence of the Lambs, The (1991) \n", "8 Star Wars: Episode IV - A New Hope (1977) \n", "9 Matrix, The (1999) \n", "10 Schindler's List (1993) \n", "11 Jurassic Park (1993) \n", "13 Braveheart (1995) \n", "15 Toy Story (1995) " ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "rec_td=[top_rated.movieId.iloc[0]]\n", "theta=.3\n", "\n", "for i in range(9):\n", " movies_left=top_rated.loc[top_rated.movieId.map(lambda x: x not in rec_td)]\n", " movies_orig_rank=movies_left.index\n", " movies_dissim_rank=np.argsort([np.sum([sim_movies(m1,m2) for m2 in rec_td]) for m1 in movies_left.movieId])\n", " rec_td.append(movies_left.movieId.iloc[np.argmin(np.array(theta*movies_orig_rank+(1-theta)*movies_dissim_rank))])\n", " \n", "new_list=top_rated.loc[top_rated.movieId.map(lambda x: x in rec_td)]\n", "new_list['reorder_mmr']=list(top_rated.title.loc[top_rated.movieId.map(lambda x: x in rec_mmr)])\n", "new_list['reorder_maxcover']=recs_maxcover\n", "new_list['originally_here']=list(top_rated.head(10).title)\n", "new_list" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "All these methods seem to be doing something. Unfortunately, it's impossible to know which final list is better without testing them live on some users, but at least intuitively, the final recommendation lists seem better (at least for me), especially under the MMR and TD heuristics." ] } ], "metadata": { "kernelspec": { "display_name": "Python [Root]", "language": "python", "name": "Python [Root]" }, "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.5.2" } }, "nbformat": 4, "nbformat_minor": 0 }