diff --git "a/Week16_\353\263\265\354\212\265\352\263\274\354\240\234_\352\263\240\354\235\200\353\271\204.ipynb" "b/Week16_\353\263\265\354\212\265\352\263\274\354\240\234_\352\263\240\354\235\200\353\271\204.ipynb" new file mode 100644 index 0000000..042ecf4 --- /dev/null +++ "b/Week16_\353\263\265\354\212\265\352\263\274\354\240\234_\352\263\240\354\235\200\353\271\204.ipynb" @@ -0,0 +1,3027 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3b6eb191-f309-4201-a056-5618cacec839", + "metadata": {}, + "source": [ + "# **9.5 컨텐츠 기반 필터링 실습 – TMDB 5000 Movie Dataset**" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "706c3e6e-fdbf-4379-98b9-4cb4d0711896", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(4803, 20)\n" + ] + }, + { + "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", + "
budgetgenreshomepageidkeywordsoriginal_languageoriginal_titleoverviewpopularityproduction_companiesproduction_countriesrelease_daterevenueruntimespoken_languagesstatustaglinetitlevote_averagevote_count
0237000000[{\"id\": 28, \"name\": \"Action\"}, {\"id\": 12, \"nam...http://www.avatarmovie.com/19995[{\"id\": 1463, \"name\": \"culture clash\"}, {\"id\":...enAvatarIn the 22nd century, a paraplegic Marine is di...150.437577[{\"name\": \"Ingenious Film Partners\", \"id\": 289...[{\"iso_3166_1\": \"US\", \"name\": \"United States o...2009-12-102787965087162.0[{\"iso_639_1\": \"en\", \"name\": \"English\"}, {\"iso...ReleasedEnter the World of Pandora.Avatar7.211800
\n", + "
" + ], + "text/plain": [ + " budget genres \\\n", + "0 237000000 [{\"id\": 28, \"name\": \"Action\"}, {\"id\": 12, \"nam... \n", + "\n", + " homepage id \\\n", + "0 http://www.avatarmovie.com/ 19995 \n", + "\n", + " keywords original_language \\\n", + "0 [{\"id\": 1463, \"name\": \"culture clash\"}, {\"id\":... en \n", + "\n", + " original_title overview \\\n", + "0 Avatar In the 22nd century, a paraplegic Marine is di... \n", + "\n", + " popularity production_companies \\\n", + "0 150.437577 [{\"name\": \"Ingenious Film Partners\", \"id\": 289... \n", + "\n", + " production_countries release_date revenue \\\n", + "0 [{\"iso_3166_1\": \"US\", \"name\": \"United States o... 2009-12-10 2787965087 \n", + "\n", + " runtime spoken_languages status \\\n", + "0 162.0 [{\"iso_639_1\": \"en\", \"name\": \"English\"}, {\"iso... Released \n", + "\n", + " tagline title vote_average vote_count \n", + "0 Enter the World of Pandora. Avatar 7.2 11800 " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "import warnings; warnings.filterwarnings('ignore')\n", + "\n", + "movies =pd.read_csv('./tmdb_5000_movies.csv')\n", + "print(movies.shape)\n", + "movies.head(1)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "16992f15-2619-47e5-9f01-a504daac0c9f", + "metadata": {}, + "outputs": [], + "source": [ + "movies_df = movies[['id','title', 'genres', 'vote_average', 'vote_count',\n", + " 'popularity', 'keywords', 'overview']]" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "85144216-977e-441a-8945-0093108d480f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
genreskeywords
0[{\"id\": 28, \"name\": \"Action\"}, {\"id\": 12, \"name\": \"Adventure\"}, {\"id\": 14, \"name\": \"Fantasy\"}, {...[{\"id\": 1463, \"name\": \"culture clash\"}, {\"id\": 2964, \"name\": \"future\"}, {\"id\": 3386, \"name\": \"sp...
\n", + "
" + ], + "text/plain": [ + " genres \\\n", + "0 [{\"id\": 28, \"name\": \"Action\"}, {\"id\": 12, \"name\": \"Adventure\"}, {\"id\": 14, \"name\": \"Fantasy\"}, {... \n", + "\n", + " keywords \n", + "0 [{\"id\": 1463, \"name\": \"culture clash\"}, {\"id\": 2964, \"name\": \"future\"}, {\"id\": 3386, \"name\": \"sp... " + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pd.set_option('max_colwidth', 100)\n", + "movies_df[['genres','keywords']][:1]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "a69837ff-d290-4ea9-9d72-339ec3a98b14", + "metadata": {}, + "outputs": [], + "source": [ + "from ast import literal_eval\n", + "\n", + "movies_df['genres'] = movies_df['genres'].apply(literal_eval)\n", + "movies_df['keywords'] = movies_df['keywords'].apply(literal_eval)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "a178dcab-cda5-4519-abfc-795014998d05", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
genreskeywords
0[Action, Adventure, Fantasy, Science Fiction][culture clash, future, space war, space colony, society, space travel, futuristic, romance, spa...
\n", + "
" + ], + "text/plain": [ + " genres \\\n", + "0 [Action, Adventure, Fantasy, Science Fiction] \n", + "\n", + " keywords \n", + "0 [culture clash, future, space war, space colony, society, space travel, futuristic, romance, spa... " + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "movies_df['genres'] = movies_df['genres'].apply(lambda x : [ y['name'] for y in x])\n", + "movies_df['keywords'] = movies_df['keywords'].apply(lambda x : [ y['name'] for y in x])\n", + "movies_df[['genres', 'keywords']][:1]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "b3a96d3d-6076-4698-a5e2-9f0d07f91952", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(4803, 276)\n" + ] + } + ], + "source": [ + "from sklearn.feature_extraction.text import CountVectorizer\n", + "\n", + "movies_df['genres_literal'] = movies_df['genres'].apply(lambda x : (' ').join(x))\n", + "count_vect = CountVectorizer(min_df=0.0, ngram_range=(1,2))\n", + "genre_mat = count_vect.fit_transform(movies_df['genres_literal'])\n", + "print(genre_mat.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "56f53fc2-971a-42e8-95a0-d058541e2119", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(4803, 4803)\n", + "[[1. 0.59628479 0.4472136 ... 0. 0. 0. ]\n", + " [0.59628479 1. 0.4 ... 0. 0. 0. ]]\n" + ] + } + ], + "source": [ + "from sklearn.metrics.pairwise import cosine_similarity\n", + "\n", + "genre_sim = cosine_similarity(genre_mat, genre_mat)\n", + "print(genre_sim.shape)\n", + "print(genre_sim[:2])" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "dfac9c40-bad9-4786-8bfe-bac24ae8edae", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[ 0 3494 813 ... 3038 3037 2401]]\n" + ] + } + ], + "source": [ + "genre_sim_sorted_ind = genre_sim.argsort()[:, ::-1]\n", + "print(genre_sim_sorted_ind[:1])" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "daeab561-9cf2-4c0b-9f85-d603e99454b8", + "metadata": {}, + "outputs": [], + "source": [ + "def find_sim_movie(df, sorted_ind, title_name, top_n=10):\n", + " \n", + " # 인자로 입력된 movies_df DataFrame에서 'title' 컬럼이 입력된 title_name 값인 DataFrame추출\n", + " title_movie = df[df['title'] == title_name]\n", + " \n", + " title_index = title_movie.index.values\n", + " similar_indexes = sorted_ind[title_index, :(top_n)]\n", + " \n", + " # 추출된 top_n index들 출력. top_n index는 2차원 데이터 \n", + " print(similar_indexes)\n", + " similar_indexes = similar_indexes.reshape(-1)\n", + " \n", + " return df.iloc[similar_indexes]" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "0326f046-1772-4ae4-8de8-a91cbc8c1f2d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[2731 1243 3636 1946 2640 4065 1847 4217 883 3866]]\n" + ] + }, + { + "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", + "
titlevote_average
2731The Godfather: Part II8.3
1243Mean Streets7.2
3636Light Sleeper5.7
1946The Bad Lieutenant: Port of Call - New Orleans6.0
2640Things to Do in Denver When You're Dead6.7
4065Mi America0.0
1847GoodFellas8.2
4217Kids6.8
883Catch Me If You Can7.7
3866City of God8.1
\n", + "
" + ], + "text/plain": [ + " title vote_average\n", + "2731 The Godfather: Part II 8.3\n", + "1243 Mean Streets 7.2\n", + "3636 Light Sleeper 5.7\n", + "1946 The Bad Lieutenant: Port of Call - New Orleans 6.0\n", + "2640 Things to Do in Denver When You're Dead 6.7\n", + "4065 Mi America 0.0\n", + "1847 GoodFellas 8.2\n", + "4217 Kids 6.8\n", + "883 Catch Me If You Can 7.7\n", + "3866 City of God 8.1" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "similar_movies = find_sim_movie(movies_df, genre_sim_sorted_ind, 'The Godfather',10)\n", + "similar_movies[['title', 'vote_average']]" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "8c7f1ec3-0a0d-4108-b73d-551d3b9a3541", + "metadata": {}, + "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", + "
titlevote_averagevote_count
3519Stiff Upper Lips10.01
4247Me You and Five Bucks10.02
4045Dancer, Texas Pop. 8110.01
4662Little Big Top10.01
3992Sardaarji9.52
2386One Man's Hero9.32
2970There Goes My Baby8.52
1881The Shawshank Redemption8.58205
2796The Prisoner of Zenda8.411
3337The Godfather8.45893
\n", + "
" + ], + "text/plain": [ + " title vote_average vote_count\n", + "3519 Stiff Upper Lips 10.0 1\n", + "4247 Me You and Five Bucks 10.0 2\n", + "4045 Dancer, Texas Pop. 81 10.0 1\n", + "4662 Little Big Top 10.0 1\n", + "3992 Sardaarji 9.5 2\n", + "2386 One Man's Hero 9.3 2\n", + "2970 There Goes My Baby 8.5 2\n", + "1881 The Shawshank Redemption 8.5 8205\n", + "2796 The Prisoner of Zenda 8.4 11\n", + "3337 The Godfather 8.4 5893" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "movies_df[['title','vote_average','vote_count']].sort_values('vote_average', ascending=False)[:10]" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "43472b24-65fe-4339-9d76-d659ced06462", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "C: 6.092 m: 370.2\n" + ] + } + ], + "source": [ + "C = movies_df['vote_average'].mean()\n", + "m = movies_df['vote_count'].quantile(0.6)\n", + "print('C:',round(C,3), 'm:',round(m,3))" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "d8c48e18-1f1a-4b5e-bc34-fe8fa94245b2", + "metadata": {}, + "outputs": [], + "source": [ + "percentile = 0.6\n", + "m = movies_df['vote_count'].quantile(percentile)\n", + "C = movies_df['vote_average'].mean()\n", + "\n", + "def weighted_vote_average(record):\n", + " v = record['vote_count']\n", + " R = record['vote_average']\n", + " \n", + " return ( (v/(v+m)) * R ) + ( (m/(m+v)) * C ) \n", + "\n", + "movies_df['weighted_vote'] = movies_df.apply(weighted_vote_average, axis=1) " + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "4addb6af-3216-41ff-a335-005cb964dcca", + "metadata": {}, + "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", + "
titlevote_averageweighted_votevote_count
1881The Shawshank Redemption8.58.3960528205
3337The Godfather8.48.2635915893
662Fight Club8.38.2164559413
3232Pulp Fiction8.38.2071028428
65The Dark Knight8.28.13693012002
1818Schindler's List8.38.1260694329
3865Whiplash8.38.1232484254
809Forrest Gump8.28.1059547927
2294Spirited Away8.38.1058673840
2731The Godfather: Part II8.38.0795863338
\n", + "
" + ], + "text/plain": [ + " title vote_average weighted_vote vote_count\n", + "1881 The Shawshank Redemption 8.5 8.396052 8205\n", + "3337 The Godfather 8.4 8.263591 5893\n", + "662 Fight Club 8.3 8.216455 9413\n", + "3232 Pulp Fiction 8.3 8.207102 8428\n", + "65 The Dark Knight 8.2 8.136930 12002\n", + "1818 Schindler's List 8.3 8.126069 4329\n", + "3865 Whiplash 8.3 8.123248 4254\n", + "809 Forrest Gump 8.2 8.105954 7927\n", + "2294 Spirited Away 8.3 8.105867 3840\n", + "2731 The Godfather: Part II 8.3 8.079586 3338" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "movies_df[['title','vote_average','weighted_vote','vote_count']].sort_values('weighted_vote',\n", + " ascending=False)[:10]" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "399ead78-67b2-4bca-8a63-62b0c4e810ba", + "metadata": {}, + "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", + "
titlevote_averageweighted_vote
2731The Godfather: Part II8.38.079586
1847GoodFellas8.27.976937
3866City of God8.17.759693
1663Once Upon a Time in America8.27.657811
883Catch Me If You Can7.77.557097
281American Gangster7.47.141396
4041This Is England7.46.739664
1149American Hustle6.86.717525
1243Mean Streets7.26.626569
2839Rounders6.96.530427
\n", + "
" + ], + "text/plain": [ + " title vote_average weighted_vote\n", + "2731 The Godfather: Part II 8.3 8.079586\n", + "1847 GoodFellas 8.2 7.976937\n", + "3866 City of God 8.1 7.759693\n", + "1663 Once Upon a Time in America 8.2 7.657811\n", + "883 Catch Me If You Can 7.7 7.557097\n", + "281 American Gangster 7.4 7.141396\n", + "4041 This Is England 7.4 6.739664\n", + "1149 American Hustle 6.8 6.717525\n", + "1243 Mean Streets 7.2 6.626569\n", + "2839 Rounders 6.9 6.530427" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def find_sim_movie(df, sorted_ind, title_name, top_n=10):\n", + " title_movie = df[df['title'] == title_name]\n", + " title_index = title_movie.index.values\n", + " \n", + " similar_indexes = sorted_ind[title_index, :(top_n*2)]\n", + " similar_indexes = similar_indexes.reshape(-1)\n", + " similar_indexes = similar_indexes[similar_indexes != title_index]\n", + " \n", + " return df.iloc[similar_indexes].sort_values('weighted_vote', ascending=False)[:top_n]\n", + "\n", + "similar_movies = find_sim_movie(movies_df, genre_sim_sorted_ind, 'The Godfather',10)\n", + "similar_movies[['title', 'vote_average', 'weighted_vote']]" + ] + }, + { + "cell_type": "markdown", + "id": "9c8deac6-dd7d-48a9-bab3-0893d39438b0", + "metadata": {}, + "source": [ + "# **9.6 아이템 기반 인접 이웃 협업 필터링 실습**" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "d320578d-2cc7-4296-ace4-6d59e632849c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(9742, 3)\n", + "(100836, 4)\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "movies = pd.read_csv('./ml-latest-small/movies.csv')\n", + "ratings = pd.read_csv('./ml-latest-small/ratings.csv')\n", + "print(movies.shape)\n", + "print(ratings.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "e68ef709-4770-49db-82df-52ee9f2a6922", + "metadata": {}, + "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", + "
movieIdtitlegenres
01Toy Story (1995)Adventure|Animation|Children|Comedy|Fantasy
12Jumanji (1995)Adventure|Children|Fantasy
23Grumpier Old Men (1995)Comedy|Romance
34Waiting to Exhale (1995)Comedy|Drama|Romance
45Father of the Bride Part II (1995)Comedy
\n", + "
" + ], + "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": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "movies.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "02e22b7f-6867-4de1-b646-802ec5f11962", + "metadata": {}, + "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", + "
userIdmovieIdratingtimestamp
0114.0964982703
1134.0964981247
2164.0964982224
31475.0964983815
41505.0964982931
\n", + "
" + ], + "text/plain": [ + " userId movieId rating timestamp\n", + "0 1 1 4.0 964982703\n", + "1 1 3 4.0 964981247\n", + "2 1 6 4.0 964982224\n", + "3 1 47 5.0 964983815\n", + "4 1 50 5.0 964982931" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ratings.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "01865090-ceb2-49d8-8f48-b54b91c36e4a", + "metadata": {}, + "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", + "
movieId12345678910...193565193567193571193573193579193581193583193585193587193609
userId
14.0NaN4.0NaNNaN4.0NaNNaNNaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
2NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
3NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
\n", + "

3 rows × 9724 columns

\n", + "
" + ], + "text/plain": [ + "movieId 1 2 3 4 5 6 7 8 \\\n", + "userId \n", + "1 4.0 NaN 4.0 NaN NaN 4.0 NaN NaN \n", + "2 NaN NaN NaN NaN NaN NaN NaN NaN \n", + "3 NaN NaN NaN NaN NaN NaN NaN NaN \n", + "\n", + "movieId 9 10 ... 193565 193567 193571 193573 193579 193581 \\\n", + "userId ... \n", + "1 NaN NaN ... NaN NaN NaN NaN NaN NaN \n", + "2 NaN NaN ... NaN NaN NaN NaN NaN NaN \n", + "3 NaN NaN ... NaN NaN NaN NaN NaN NaN \n", + "\n", + "movieId 193583 193585 193587 193609 \n", + "userId \n", + "1 NaN NaN NaN NaN \n", + "2 NaN NaN NaN NaN \n", + "3 NaN NaN NaN NaN \n", + "\n", + "[3 rows x 9724 columns]" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ratings = ratings[['userId', 'movieId', 'rating']]\n", + "ratings_matrix = ratings.pivot_table('rating', index='userId', columns='movieId')\n", + "ratings_matrix.head(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "dba1c136-da36-4ed6-8af1-28ccf2d01581", + "metadata": {}, + "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", + "
title'71 (2014)'Hellboy': The Seeds of Creation (2004)'Round Midnight (1986)'Salem's Lot (2004)'Til There Was You (1997)'Tis the Season for Love (2015)'burbs, The (1989)'night Mother (1986)(500) Days of Summer (2009)*batteries not included (1987)...Zulu (2013)[REC] (2007)[REC]² (2009)[REC]³ 3 Génesis (2012)anohana: The Flower We Saw That Day - The Movie (2013)eXistenZ (1999)xXx (2002)xXx: State of the Union (2005)¡Three Amigos! (1986)À nous la liberté (Freedom for Us) (1931)
userId
10.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.04.00.0
20.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
30.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
\n", + "

3 rows × 9719 columns

\n", + "
" + ], + "text/plain": [ + "title '71 (2014) 'Hellboy': The Seeds of Creation (2004) \\\n", + "userId \n", + "1 0.0 0.0 \n", + "2 0.0 0.0 \n", + "3 0.0 0.0 \n", + "\n", + "title 'Round Midnight (1986) 'Salem's Lot (2004) \\\n", + "userId \n", + "1 0.0 0.0 \n", + "2 0.0 0.0 \n", + "3 0.0 0.0 \n", + "\n", + "title 'Til There Was You (1997) 'Tis the Season for Love (2015) \\\n", + "userId \n", + "1 0.0 0.0 \n", + "2 0.0 0.0 \n", + "3 0.0 0.0 \n", + "\n", + "title 'burbs, The (1989) 'night Mother (1986) (500) Days of Summer (2009) \\\n", + "userId \n", + "1 0.0 0.0 0.0 \n", + "2 0.0 0.0 0.0 \n", + "3 0.0 0.0 0.0 \n", + "\n", + "title *batteries not included (1987) ... Zulu (2013) [REC] (2007) \\\n", + "userId ... \n", + "1 0.0 ... 0.0 0.0 \n", + "2 0.0 ... 0.0 0.0 \n", + "3 0.0 ... 0.0 0.0 \n", + "\n", + "title [REC]² (2009) [REC]³ 3 Génesis (2012) \\\n", + "userId \n", + "1 0.0 0.0 \n", + "2 0.0 0.0 \n", + "3 0.0 0.0 \n", + "\n", + "title anohana: The Flower We Saw That Day - The Movie (2013) \\\n", + "userId \n", + "1 0.0 \n", + "2 0.0 \n", + "3 0.0 \n", + "\n", + "title eXistenZ (1999) xXx (2002) xXx: State of the Union (2005) \\\n", + "userId \n", + "1 0.0 0.0 0.0 \n", + "2 0.0 0.0 0.0 \n", + "3 0.0 0.0 0.0 \n", + "\n", + "title ¡Three Amigos! (1986) À nous la liberté (Freedom for Us) (1931) \n", + "userId \n", + "1 4.0 0.0 \n", + "2 0.0 0.0 \n", + "3 0.0 0.0 \n", + "\n", + "[3 rows x 9719 columns]" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rating_movies = pd.merge(ratings, movies, on='movieId')\n", + "\n", + "ratings_matrix = rating_movies.pivot_table('rating', index='userId', columns='title')\n", + "\n", + "ratings_matrix = ratings_matrix.fillna(0)\n", + "ratings_matrix.head(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "41c356d2-c767-449c-9662-81f73809dd5b", + "metadata": {}, + "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", + "
userId12345678910...601602603604605606607608609610
title
'71 (2014)0.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.04.0
'Hellboy': The Seeds of Creation (2004)0.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
'Round Midnight (1986)0.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
\n", + "

3 rows × 610 columns

\n", + "
" + ], + "text/plain": [ + "userId 1 2 3 4 5 6 7 \\\n", + "title \n", + "'71 (2014) 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "'Hellboy': The Seeds of Creation (2004) 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "'Round Midnight (1986) 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "\n", + "userId 8 9 10 ... 601 602 603 \\\n", + "title ... \n", + "'71 (2014) 0.0 0.0 0.0 ... 0.0 0.0 0.0 \n", + "'Hellboy': The Seeds of Creation (2004) 0.0 0.0 0.0 ... 0.0 0.0 0.0 \n", + "'Round Midnight (1986) 0.0 0.0 0.0 ... 0.0 0.0 0.0 \n", + "\n", + "userId 604 605 606 607 608 609 610 \n", + "title \n", + "'71 (2014) 0.0 0.0 0.0 0.0 0.0 0.0 4.0 \n", + "'Hellboy': The Seeds of Creation (2004) 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "'Round Midnight (1986) 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "\n", + "[3 rows x 610 columns]" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ratings_matrix_T = ratings_matrix.transpose()\n", + "ratings_matrix_T.head(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "6c28d33e-ee02-4144-b07b-af33728a1364", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(9719, 9719)\n" + ] + }, + { + "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", + "
title'71 (2014)'Hellboy': The Seeds of Creation (2004)'Round Midnight (1986)'Salem's Lot (2004)'Til There Was You (1997)'Tis the Season for Love (2015)'burbs, The (1989)'night Mother (1986)(500) Days of Summer (2009)*batteries not included (1987)...Zulu (2013)[REC] (2007)[REC]² (2009)[REC]³ 3 Génesis (2012)anohana: The Flower We Saw That Day - The Movie (2013)eXistenZ (1999)xXx (2002)xXx: State of the Union (2005)¡Three Amigos! (1986)À nous la liberté (Freedom for Us) (1931)
title
'71 (2014)1.00.0000000.0000000.00.00.00.0000000.00.1416530.0...0.00.3420550.5433050.7071070.00.00.1394310.3273270.00.0
'Hellboy': The Seeds of Creation (2004)0.01.0000000.7071070.00.00.00.0000000.00.0000000.0...0.00.0000000.0000000.0000000.00.00.0000000.0000000.00.0
'Round Midnight (1986)0.00.7071071.0000000.00.00.00.1767770.00.0000000.0...0.00.0000000.0000000.0000000.00.00.0000000.0000000.00.0
\n", + "

3 rows × 9719 columns

\n", + "
" + ], + "text/plain": [ + "title '71 (2014) \\\n", + "title \n", + "'71 (2014) 1.0 \n", + "'Hellboy': The Seeds of Creation (2004) 0.0 \n", + "'Round Midnight (1986) 0.0 \n", + "\n", + "title 'Hellboy': The Seeds of Creation (2004) \\\n", + "title \n", + "'71 (2014) 0.000000 \n", + "'Hellboy': The Seeds of Creation (2004) 1.000000 \n", + "'Round Midnight (1986) 0.707107 \n", + "\n", + "title 'Round Midnight (1986) \\\n", + "title \n", + "'71 (2014) 0.000000 \n", + "'Hellboy': The Seeds of Creation (2004) 0.707107 \n", + "'Round Midnight (1986) 1.000000 \n", + "\n", + "title 'Salem's Lot (2004) \\\n", + "title \n", + "'71 (2014) 0.0 \n", + "'Hellboy': The Seeds of Creation (2004) 0.0 \n", + "'Round Midnight (1986) 0.0 \n", + "\n", + "title 'Til There Was You (1997) \\\n", + "title \n", + "'71 (2014) 0.0 \n", + "'Hellboy': The Seeds of Creation (2004) 0.0 \n", + "'Round Midnight (1986) 0.0 \n", + "\n", + "title 'Tis the Season for Love (2015) \\\n", + "title \n", + "'71 (2014) 0.0 \n", + "'Hellboy': The Seeds of Creation (2004) 0.0 \n", + "'Round Midnight (1986) 0.0 \n", + "\n", + "title 'burbs, The (1989) \\\n", + "title \n", + "'71 (2014) 0.000000 \n", + "'Hellboy': The Seeds of Creation (2004) 0.000000 \n", + "'Round Midnight (1986) 0.176777 \n", + "\n", + "title 'night Mother (1986) \\\n", + "title \n", + "'71 (2014) 0.0 \n", + "'Hellboy': The Seeds of Creation (2004) 0.0 \n", + "'Round Midnight (1986) 0.0 \n", + "\n", + "title (500) Days of Summer (2009) \\\n", + "title \n", + "'71 (2014) 0.141653 \n", + "'Hellboy': The Seeds of Creation (2004) 0.000000 \n", + "'Round Midnight (1986) 0.000000 \n", + "\n", + "title *batteries not included (1987) ... \\\n", + "title ... \n", + "'71 (2014) 0.0 ... \n", + "'Hellboy': The Seeds of Creation (2004) 0.0 ... \n", + "'Round Midnight (1986) 0.0 ... \n", + "\n", + "title Zulu (2013) [REC] (2007) \\\n", + "title \n", + "'71 (2014) 0.0 0.342055 \n", + "'Hellboy': The Seeds of Creation (2004) 0.0 0.000000 \n", + "'Round Midnight (1986) 0.0 0.000000 \n", + "\n", + "title [REC]² (2009) \\\n", + "title \n", + "'71 (2014) 0.543305 \n", + "'Hellboy': The Seeds of Creation (2004) 0.000000 \n", + "'Round Midnight (1986) 0.000000 \n", + "\n", + "title [REC]³ 3 Génesis (2012) \\\n", + "title \n", + "'71 (2014) 0.707107 \n", + "'Hellboy': The Seeds of Creation (2004) 0.000000 \n", + "'Round Midnight (1986) 0.000000 \n", + "\n", + "title anohana: The Flower We Saw That Day - The Movie (2013) \\\n", + "title \n", + "'71 (2014) 0.0 \n", + "'Hellboy': The Seeds of Creation (2004) 0.0 \n", + "'Round Midnight (1986) 0.0 \n", + "\n", + "title eXistenZ (1999) xXx (2002) \\\n", + "title \n", + "'71 (2014) 0.0 0.139431 \n", + "'Hellboy': The Seeds of Creation (2004) 0.0 0.000000 \n", + "'Round Midnight (1986) 0.0 0.000000 \n", + "\n", + "title xXx: State of the Union (2005) \\\n", + "title \n", + "'71 (2014) 0.327327 \n", + "'Hellboy': The Seeds of Creation (2004) 0.000000 \n", + "'Round Midnight (1986) 0.000000 \n", + "\n", + "title ¡Three Amigos! (1986) \\\n", + "title \n", + "'71 (2014) 0.0 \n", + "'Hellboy': The Seeds of Creation (2004) 0.0 \n", + "'Round Midnight (1986) 0.0 \n", + "\n", + "title À nous la liberté (Freedom for Us) (1931) \n", + "title \n", + "'71 (2014) 0.0 \n", + "'Hellboy': The Seeds of Creation (2004) 0.0 \n", + "'Round Midnight (1986) 0.0 \n", + "\n", + "[3 rows x 9719 columns]" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from sklearn.metrics.pairwise import cosine_similarity\n", + "\n", + "item_sim = cosine_similarity(ratings_matrix_T, ratings_matrix_T)\n", + "\n", + "item_sim_df = pd.DataFrame(data=item_sim, index=ratings_matrix.columns,\n", + " columns=ratings_matrix.columns)\n", + "print(item_sim_df.shape)\n", + "item_sim_df.head(3)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "7918389b-8bae-4041-9508-7fadc1240c06", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "title\n", + "Godfather, The (1972) 1.000000\n", + "Godfather: Part II, The (1974) 0.821773\n", + "Goodfellas (1990) 0.664841\n", + "One Flew Over the Cuckoo's Nest (1975) 0.620536\n", + "Star Wars: Episode IV - A New Hope (1977) 0.595317\n", + "Fargo (1996) 0.588614\n", + "Name: Godfather, The (1972), dtype: float64" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "item_sim_df[\"Godfather, The (1972)\"].sort_values(ascending=False)[:6]" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "219b7a7c-5b6c-4351-be53-31b23cbb25c2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "title\n", + "Dark Knight, The (2008) 0.727263\n", + "Inglourious Basterds (2009) 0.646103\n", + "Shutter Island (2010) 0.617736\n", + "Dark Knight Rises, The (2012) 0.617504\n", + "Fight Club (1999) 0.615417\n", + "Name: Inception (2010), dtype: float64" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "item_sim_df[\"Inception (2010)\"].sort_values(ascending=False)[1:6]" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "f8ae8dc2-d81e-4d3f-8bc8-e79d440822f6", + "metadata": {}, + "outputs": [], + "source": [ + "def predict_rating(ratings_arr, item_sim_arr ):\n", + " ratings_pred = ratings_arr.dot(item_sim_arr)/ np.array([np.abs(item_sim_arr).sum(axis=1)])\n", + " return ratings_pred" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "bc8c8b07-857b-406f-897a-0b6736ddaf26", + "metadata": {}, + "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", + "
title'71 (2014)'Hellboy': The Seeds of Creation (2004)'Round Midnight (1986)'Salem's Lot (2004)'Til There Was You (1997)'Tis the Season for Love (2015)'burbs, The (1989)'night Mother (1986)(500) Days of Summer (2009)*batteries not included (1987)...Zulu (2013)[REC] (2007)[REC]² (2009)[REC]³ 3 Génesis (2012)anohana: The Flower We Saw That Day - The Movie (2013)eXistenZ (1999)xXx (2002)xXx: State of the Union (2005)¡Three Amigos! (1986)À nous la liberté (Freedom for Us) (1931)
userId
10.0703450.5778550.3216960.2270550.2069580.1946150.2498830.1025420.1570840.178197...0.1136080.1817380.1339620.1285740.0061790.2120700.1929210.1360240.2929550.720347
20.0182600.0427440.0188610.0000000.0000000.0359950.0134130.0023140.0322130.014863...0.0156400.0208550.0201190.0157450.0499830.0148760.0216160.0245280.0175630.000000
30.0118840.0302790.0644370.0037620.0037490.0027220.0146250.0020850.0056660.006272...0.0069230.0116650.0118000.0122250.0000000.0081940.0070170.0092290.0104200.084501
\n", + "

3 rows × 9719 columns

\n", + "
" + ], + "text/plain": [ + "title '71 (2014) 'Hellboy': The Seeds of Creation (2004) \\\n", + "userId \n", + "1 0.070345 0.577855 \n", + "2 0.018260 0.042744 \n", + "3 0.011884 0.030279 \n", + "\n", + "title 'Round Midnight (1986) 'Salem's Lot (2004) \\\n", + "userId \n", + "1 0.321696 0.227055 \n", + "2 0.018861 0.000000 \n", + "3 0.064437 0.003762 \n", + "\n", + "title 'Til There Was You (1997) 'Tis the Season for Love (2015) \\\n", + "userId \n", + "1 0.206958 0.194615 \n", + "2 0.000000 0.035995 \n", + "3 0.003749 0.002722 \n", + "\n", + "title 'burbs, The (1989) 'night Mother (1986) (500) Days of Summer (2009) \\\n", + "userId \n", + "1 0.249883 0.102542 0.157084 \n", + "2 0.013413 0.002314 0.032213 \n", + "3 0.014625 0.002085 0.005666 \n", + "\n", + "title *batteries not included (1987) ... Zulu (2013) [REC] (2007) \\\n", + "userId ... \n", + "1 0.178197 ... 0.113608 0.181738 \n", + "2 0.014863 ... 0.015640 0.020855 \n", + "3 0.006272 ... 0.006923 0.011665 \n", + "\n", + "title [REC]² (2009) [REC]³ 3 Génesis (2012) \\\n", + "userId \n", + "1 0.133962 0.128574 \n", + "2 0.020119 0.015745 \n", + "3 0.011800 0.012225 \n", + "\n", + "title anohana: The Flower We Saw That Day - The Movie (2013) \\\n", + "userId \n", + "1 0.006179 \n", + "2 0.049983 \n", + "3 0.000000 \n", + "\n", + "title eXistenZ (1999) xXx (2002) xXx: State of the Union (2005) \\\n", + "userId \n", + "1 0.212070 0.192921 0.136024 \n", + "2 0.014876 0.021616 0.024528 \n", + "3 0.008194 0.007017 0.009229 \n", + "\n", + "title ¡Three Amigos! (1986) À nous la liberté (Freedom for Us) (1931) \n", + "userId \n", + "1 0.292955 0.720347 \n", + "2 0.017563 0.000000 \n", + "3 0.010420 0.084501 \n", + "\n", + "[3 rows x 9719 columns]" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ratings_pred = predict_rating(ratings_matrix.values , item_sim_df.values)\n", + "ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index= ratings_matrix.index,\n", + " columns = ratings_matrix.columns)\n", + "ratings_pred_matrix.head(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "a203cf5c-4905-4527-b1fd-d1e2935d2abb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "아이템 기반 모든 인접 이웃 MSE: 9.895354759094706\n" + ] + } + ], + "source": [ + "from sklearn.metrics import mean_squared_error\n", + "\n", + "def get_mse(pred, actual):\n", + " # Ignore nonzero terms.\n", + " pred = pred[actual.nonzero()].flatten()\n", + " actual = actual[actual.nonzero()].flatten()\n", + " return mean_squared_error(pred, actual)\n", + "\n", + "print('아이템 기반 모든 인접 이웃 MSE: ', get_mse(ratings_pred, ratings_matrix.values ))" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "390cf3db-de62-4e48-9d66-3cad222f4b05", + "metadata": {}, + "outputs": [], + "source": [ + "def predict_rating_topsim(ratings_arr, item_sim_arr, n=20):\n", + " pred = np.zeros(ratings_arr.shape)\n", + "\n", + " for col in range(ratings_arr.shape[1]):\n", + " top_n_items = [np.argsort(item_sim_arr[:, col])[:-n-1:-1]]\n", + " # 개인화된 예측 평점을 계산\n", + " for row in range(ratings_arr.shape[0]):\n", + " pred[row, col] = item_sim_arr[col, :][top_n_items].dot(ratings_arr[row, :][top_n_items].T) \n", + " pred[row, col] /= np.sum(np.abs(item_sim_arr[col, :][top_n_items])) \n", + " return pred" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "631a483a-fecf-45c6-b1d4-f4db582d00d5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "아이템 기반 인접 TOP-20 이웃 MSE: 3.694999233129397\n" + ] + } + ], + "source": [ + "ratings_pred = predict_rating_topsim(ratings_matrix.values , item_sim_df.values, n=20)\n", + "print('아이템 기반 인접 TOP-20 이웃 MSE: ', get_mse(ratings_pred, ratings_matrix.values ))\n", + "\n", + "\n", + "ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index= ratings_matrix.index,\n", + " columns = ratings_matrix.columns)" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "id": "2697b246-97f1-47f9-ba5f-6f9bff8f3559", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "title\n", + "Adaptation (2002) 5.0\n", + "Citizen Kane (1941) 5.0\n", + "Raiders of the Lost Ark (Indiana Jones and the Raiders of the Lost Ark) (1981) 5.0\n", + "Producers, The (1968) 5.0\n", + "Lord of the Rings: The Two Towers, The (2002) 5.0\n", + "Lord of the Rings: The Fellowship of the Ring, The (2001) 5.0\n", + "Back to the Future (1985) 5.0\n", + "Austin Powers in Goldmember (2002) 5.0\n", + "Minority Report (2002) 4.0\n", + "Witness (1985) 4.0\n", + "Name: 9, dtype: float64" + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "user_rating_id = ratings_matrix.loc[9, :]\n", + "user_rating_id[ user_rating_id > 0].sort_values(ascending=False)[:10]" + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "id": "131746d1-2384-436f-9a25-cf624f055188", + "metadata": {}, + "outputs": [], + "source": [ + "def get_unseen_movies(ratings_matrix, userId):\n", + " user_rating = ratings_matrix.loc[userId,:]\n", + " \n", + " already_seen = user_rating[ user_rating > 0].index.tolist()\n", + " \n", + " movies_list = ratings_matrix.columns.tolist()\n", + " unseen_list = [ movie for movie in movies_list if movie not in already_seen]\n", + " \n", + " return unseen_list" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "id": "0afd76b7-e9d4-4095-aa0d-2b465a03720f", + "metadata": {}, + "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", + "
pred_score
title
Shrek (2001)0.866202
Spider-Man (2002)0.857854
Last Samurai, The (2003)0.817473
Indiana Jones and the Temple of Doom (1984)0.816626
Matrix Reloaded, The (2003)0.800990
Harry Potter and the Sorcerer's Stone (a.k.a. Harry Potter and the Philosopher's Stone) (2001)0.765159
Gladiator (2000)0.740956
Matrix, The (1999)0.732693
Pirates of the Caribbean: The Curse of the Black Pearl (2003)0.689591
Lord of the Rings: The Return of the King, The (2003)0.676711
\n", + "
" + ], + "text/plain": [ + " pred_score\n", + "title \n", + "Shrek (2001) 0.866202\n", + "Spider-Man (2002) 0.857854\n", + "Last Samurai, The (2003) 0.817473\n", + "Indiana Jones and the Temple of Doom (1984) 0.816626\n", + "Matrix Reloaded, The (2003) 0.800990\n", + "Harry Potter and the Sorcerer's Stone (a.k.a. Harry Potter and the Philosopher's Stone) (2001) 0.765159\n", + "Gladiator (2000) 0.740956\n", + "Matrix, The (1999) 0.732693\n", + "Pirates of the Caribbean: The Curse of the Black Pearl (2003) 0.689591\n", + "Lord of the Rings: The Return of the King, The (2003) 0.676711" + ] + }, + "execution_count": 85, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def recomm_movie_by_userid(pred_df, userId, unseen_list, top_n=10):\n", + " recomm_movies = pred_df.loc[userId, unseen_list].sort_values(ascending=False)[:top_n]\n", + " return recomm_movies\n", + " \n", + "unseen_list = get_unseen_movies(ratings_matrix, 9)\n", + "\n", + "recomm_movies = recomm_movie_by_userid(ratings_pred_matrix, 9, unseen_list, top_n=10)\n", + "\n", + "# 평점 데이터 DataFrame으로 생성\n", + "recomm_movies = pd.DataFrame(data=recomm_movies.values,index=recomm_movies.index,columns=['pred_score'])\n", + "recomm_movies" + ] + }, + { + "cell_type": "markdown", + "id": "b1f9d92a-7b1b-4c41-96fd-337f66ca9457", + "metadata": {}, + "source": [ + "# **9.7 행렬 분해 기반의 잠재 요인 협업 필터링 실습**" + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "id": "bf5e3170-2b00-4df9-afd9-89594ab0e488", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import numpy as np\n", + "from sklearn.metrics import mean_squared_error\n", + "\n", + "def get_rmse(R, P, Q, non_zeros):\n", + " error = 0\n", + " full_pred_matrix = np.dot(P, Q.T)\n", + " \n", + " x_non_zero_ind = [non_zero[0] for non_zero in non_zeros]\n", + " y_non_zero_ind = [non_zero[1] for non_zero in non_zeros]\n", + " R_non_zeros = R[x_non_zero_ind, y_non_zero_ind]\n", + " \n", + " full_pred_matrix_non_zeros = full_pred_matrix[x_non_zero_ind, y_non_zero_ind]\n", + " \n", + " mse = mean_squared_error(R_non_zeros, full_pred_matrix_non_zeros)\n", + " rmse = np.sqrt(mse)\n", + " \n", + " return rmse" + ] + }, + { + "cell_type": "code", + "execution_count": 90, + "id": "da99ef1f-9ef0-4706-b396-5bbf302d0561", + "metadata": {}, + "outputs": [], + "source": [ + "def matrix_factorization(R, K, steps=200, learning_rate=0.01, r_lambda = 0.01):\n", + " num_users, num_items = R.shape\n", + " np.random.seed(1)\n", + " P = np.random.normal(scale=1./K, size=(num_users, K))\n", + " Q = np.random.normal(scale=1./K, size=(num_items, K))\n", + " \n", + " non_zeros = [ (i, j, R[i,j]) for i in range(num_users) for j in range(num_items) if R[i,j] > 0 ]\n", + " \n", + " for step in range(steps):\n", + " for i, j, r in non_zeros:\n", + " eij = r - np.dot(P[i, :], Q[j, :].T)\n", + " P[i,:] = P[i,:] + learning_rate*(eij * Q[j, :] - r_lambda*P[i,:])\n", + " Q[j,:] = Q[j,:] + learning_rate*(eij * P[i, :] - r_lambda*Q[j,:])\n", + " \n", + " rmse = get_rmse(R, P, Q, non_zeros)\n", + " if (step % 10) == 0 :\n", + " print(\"### iteration step : \", step,\" rmse : \", rmse)\n", + " \n", + " return P, Q" + ] + }, + { + "cell_type": "code", + "execution_count": 92, + "id": "c8f46543-46a5-4d76-b596-b41003d98713", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "movies = pd.read_csv('./ml-latest-small/movies.csv')\n", + "ratings = pd.read_csv('./ml-latest-small/ratings.csv')\n", + "ratings = ratings[['userId', 'movieId', 'rating']]\n", + "ratings_matrix = ratings.pivot_table('rating', index='userId', columns='movieId')\n", + "\n", + "rating_movies = pd.merge(ratings, movies, on='movieId')\n", + "\n", + "ratings_matrix = rating_movies.pivot_table('rating', index='userId', columns='title')" + ] + }, + { + "cell_type": "code", + "execution_count": 93, + "id": "11e7107a-bb25-4030-b303-a955bfe21b1a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "### iteration step : 0 rmse : 2.9023619751336867\n", + "### iteration step : 10 rmse : 0.7335768591017927\n", + "### iteration step : 20 rmse : 0.5115539026853442\n", + "### iteration step : 30 rmse : 0.37261628282537446\n", + "### iteration step : 40 rmse : 0.29608182991810134\n", + "### iteration step : 50 rmse : 0.2520353192341642\n", + "### iteration step : 60 rmse : 0.22487503275269854\n", + "### iteration step : 70 rmse : 0.20685455302331537\n", + "### iteration step : 80 rmse : 0.19413418783028683\n", + "### iteration step : 90 rmse : 0.18470082002720403\n", + "### iteration step : 100 rmse : 0.17742927527209104\n", + "### iteration step : 110 rmse : 0.1716522696470749\n", + "### iteration step : 120 rmse : 0.1669518194687172\n", + "### iteration step : 130 rmse : 0.16305292191997542\n", + "### iteration step : 140 rmse : 0.15976691929679643\n", + "### iteration step : 150 rmse : 0.1569598699945732\n", + "### iteration step : 160 rmse : 0.15453398186715428\n", + "### iteration step : 170 rmse : 0.15241618551077643\n", + "### iteration step : 180 rmse : 0.1505508073962831\n", + "### iteration step : 190 rmse : 0.1488947091323209\n" + ] + } + ], + "source": [ + "P, Q = matrix_factorization(ratings_matrix.values, K=50, steps=200, learning_rate=0.01, r_lambda = 0.01)\n", + "pred_matrix = np.dot(P, Q.T)" + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "id": "0542b0cc-2acc-4285-b07d-cf173c2662b4", + "metadata": {}, + "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", + "
title'71 (2014)'Hellboy': The Seeds of Creation (2004)'Round Midnight (1986)'Salem's Lot (2004)'Til There Was You (1997)'Tis the Season for Love (2015)'burbs, The (1989)'night Mother (1986)(500) Days of Summer (2009)*batteries not included (1987)...Zulu (2013)[REC] (2007)[REC]² (2009)[REC]³ 3 Génesis (2012)anohana: The Flower We Saw That Day - The Movie (2013)eXistenZ (1999)xXx (2002)xXx: State of the Union (2005)¡Three Amigos! (1986)À nous la liberté (Freedom for Us) (1931)
userId
13.0550844.0920183.5641304.5021673.9812151.2716943.6032742.3332665.0917493.972454...1.4026084.2083823.7059572.7205142.7873313.4750763.2534582.1610874.0104950.859474
23.1701193.6579923.3087074.1665214.3118901.2754694.2379721.9003663.3928593.647421...0.9738113.5282643.3615322.6725352.4044564.2327892.9116021.6345764.1357350.725684
32.3070731.6588531.4435382.2088592.2294860.7807601.9970430.9249082.9707002.551446...0.5203541.7094942.2815961.7828331.6351731.3232762.8875801.0426182.2938900.396941
\n", + "

3 rows × 9719 columns

\n", + "
" + ], + "text/plain": [ + "title '71 (2014) 'Hellboy': The Seeds of Creation (2004) \\\n", + "userId \n", + "1 3.055084 4.092018 \n", + "2 3.170119 3.657992 \n", + "3 2.307073 1.658853 \n", + "\n", + "title 'Round Midnight (1986) 'Salem's Lot (2004) \\\n", + "userId \n", + "1 3.564130 4.502167 \n", + "2 3.308707 4.166521 \n", + "3 1.443538 2.208859 \n", + "\n", + "title 'Til There Was You (1997) 'Tis the Season for Love (2015) \\\n", + "userId \n", + "1 3.981215 1.271694 \n", + "2 4.311890 1.275469 \n", + "3 2.229486 0.780760 \n", + "\n", + "title 'burbs, The (1989) 'night Mother (1986) (500) Days of Summer (2009) \\\n", + "userId \n", + "1 3.603274 2.333266 5.091749 \n", + "2 4.237972 1.900366 3.392859 \n", + "3 1.997043 0.924908 2.970700 \n", + "\n", + "title *batteries not included (1987) ... Zulu (2013) [REC] (2007) \\\n", + "userId ... \n", + "1 3.972454 ... 1.402608 4.208382 \n", + "2 3.647421 ... 0.973811 3.528264 \n", + "3 2.551446 ... 0.520354 1.709494 \n", + "\n", + "title [REC]² (2009) [REC]³ 3 Génesis (2012) \\\n", + "userId \n", + "1 3.705957 2.720514 \n", + "2 3.361532 2.672535 \n", + "3 2.281596 1.782833 \n", + "\n", + "title anohana: The Flower We Saw That Day - The Movie (2013) \\\n", + "userId \n", + "1 2.787331 \n", + "2 2.404456 \n", + "3 1.635173 \n", + "\n", + "title eXistenZ (1999) xXx (2002) xXx: State of the Union (2005) \\\n", + "userId \n", + "1 3.475076 3.253458 2.161087 \n", + "2 4.232789 2.911602 1.634576 \n", + "3 1.323276 2.887580 1.042618 \n", + "\n", + "title ¡Three Amigos! (1986) À nous la liberté (Freedom for Us) (1931) \n", + "userId \n", + "1 4.010495 0.859474 \n", + "2 4.135735 0.725684 \n", + "3 2.293890 0.396941 \n", + "\n", + "[3 rows x 9719 columns]" + ] + }, + "execution_count": 95, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ratings_pred_matrix = pd.DataFrame(data=pred_matrix, index= ratings_matrix.index, columns = ratings_matrix.columns)\n", + "\n", + "ratings_pred_matrix.head(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "id": "d5ee350b-3ab7-42cb-b1a0-ce7fcb185993", + "metadata": {}, + "outputs": [], + "source": [ + "def get_unseen_movies(ratings_matrix, userId):\n", + " user_rating = ratings_matrix.loc[userId,:]\n", + " \n", + " already_seen = user_rating[ user_rating > 0].index.tolist()\n", + " \n", + " movies_list = ratings_matrix.columns.tolist()\n", + " \n", + " unseen_list = [ movie for movie in movies_list if movie not in already_seen]\n", + " \n", + " return unseen_list" + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "id": "b41f8b77-fced-463a-8e1f-32c143cbf8c7", + "metadata": {}, + "outputs": [], + "source": [ + "def recomm_movie_by_userid(pred_df, userId, unseen_list, top_n=10):\n", + " recomm_movies = pred_df.loc[userId, unseen_list].sort_values(ascending=False)[:top_n]\n", + " return recomm_movies" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "id": "bf82dd07-9cb4-4c40-a999-445cacc3c9a9", + "metadata": {}, + "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", + "
pred_score
title
Rear Window (1954)5.704612
South Park: Bigger, Longer and Uncut (1999)5.451100
Rounders (1998)5.298393
Blade Runner (1982)5.244951
Roger & Me (1989)5.191962
Gattaca (1997)5.183179
Ben-Hur (1959)5.130463
Rosencrantz and Guildenstern Are Dead (1990)5.087375
Big Lebowski, The (1998)5.038690
Star Wars: Episode V - The Empire Strikes Back (1980)4.989601
\n", + "
" + ], + "text/plain": [ + " pred_score\n", + "title \n", + "Rear Window (1954) 5.704612\n", + "South Park: Bigger, Longer and Uncut (1999) 5.451100\n", + "Rounders (1998) 5.298393\n", + "Blade Runner (1982) 5.244951\n", + "Roger & Me (1989) 5.191962\n", + "Gattaca (1997) 5.183179\n", + "Ben-Hur (1959) 5.130463\n", + "Rosencrantz and Guildenstern Are Dead (1990) 5.087375\n", + "Big Lebowski, The (1998) 5.038690\n", + "Star Wars: Episode V - The Empire Strikes Back (1980) 4.989601" + ] + }, + "execution_count": 98, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "unseen_list = get_unseen_movies(ratings_matrix, 9)\n", + "\n", + "recomm_movies = recomm_movie_by_userid(ratings_pred_matrix, 9, unseen_list, top_n=10)\n", + "\n", + "recomm_movies = pd.DataFrame(data=recomm_movies.values,index=recomm_movies.index,columns=['pred_score'])\n", + "recomm_movies" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (tf)", + "language": "python", + "name": "tf" + }, + "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.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git "a/Week16_\354\230\210\354\212\265\352\263\274\354\240\234_\352\263\240\354\235\200\353\271\204.ipynb" "b/Week16_\354\230\210\354\212\265\352\263\274\354\240\234_\352\263\240\354\235\200\353\271\204.ipynb" new file mode 100644 index 0000000..567dc2f --- /dev/null +++ "b/Week16_\354\230\210\354\212\265\352\263\274\354\240\234_\352\263\240\354\235\200\353\271\204.ipynb" @@ -0,0 +1,884 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "760ea216-66dc-4e5b-baf4-a9eb42ff88ce", + "metadata": {}, + "source": [ + "## **SGD 기반 행렬 분해**" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "514d1fc0-7be9-4bf4-80e0-d0385e94b7d9", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "R = np.array([[4, np.NaN, np.NaN, 2, np.NaN ],\n", + " [np.NaN, 5, np.NaN, 3, 1 ],\n", + " [np.NaN, np.NaN, 3, 4, 4 ],\n", + " [5, 2, 1, 2, np.NaN ]])\n", + "num_users, num_items = R.shape\n", + "K=3\n", + "\n", + "np.random.seed(1)\n", + "P = np.random.normal(scale=1./K, size=(num_users, K))\n", + "Q = np.random.normal(scale=1./K, size=(num_items, K))" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "1f99afac-bdef-4a9d-ba23-9fcefaf20530", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.metrics import mean_squared_error\n", + "\n", + "def get_rmse(R, P, Q, non_zeros):\n", + " error = 0\n", + " full_pred_matrix = np.dot(P, Q.T)\n", + " \n", + " x_non_zero_ind = [non_zero[0] for non_zero in non_zeros]\n", + " y_non_zero_ind = [non_zero[1] for non_zero in non_zeros]\n", + " R_non_zeros = R[x_non_zero_ind, y_non_zero_ind]\n", + " full_pred_matrix_non_zeros = full_pred_matrix[x_non_zero_ind, y_non_zero_ind]\n", + " \n", + " mse = mean_squared_error(R_non_zeros, full_pred_matrix_non_zeros)\n", + " rmse = np.sqrt(mse)\n", + " \n", + " return rmse" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "1e06976b-9971-4938-88ca-8e215dca3437", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "### iteration step : 0 rmse : 3.2388050277987723\n", + "### iteration step : 50 rmse : 0.4876723101369647\n", + "### iteration step : 100 rmse : 0.15643403848192478\n", + "### iteration step : 150 rmse : 0.07455141311978043\n", + "### iteration step : 200 rmse : 0.043252267985793194\n", + "### iteration step : 250 rmse : 0.029248328780879015\n", + "### iteration step : 300 rmse : 0.02262111614382958\n", + "### iteration step : 350 rmse : 0.019493636196525124\n", + "### iteration step : 400 rmse : 0.01802271909213296\n", + "### iteration step : 450 rmse : 0.017319685953442937\n", + "### iteration step : 500 rmse : 0.016973657887570857\n", + "### iteration step : 550 rmse : 0.016796804595895498\n", + "### iteration step : 600 rmse : 0.016701322901884686\n", + "### iteration step : 650 rmse : 0.016644736912476636\n", + "### iteration step : 700 rmse : 0.016605910068210022\n", + "### iteration step : 750 rmse : 0.016574200475704817\n", + "### iteration step : 800 rmse : 0.016544315829216127\n", + "### iteration step : 850 rmse : 0.016513751774734887\n", + "### iteration step : 900 rmse : 0.016481465738195242\n", + "### iteration step : 950 rmse : 0.016447171683479235\n" + ] + } + ], + "source": [ + "non_zeros = [ (i, j, R[i,j]) for i in range(num_users) for j in range(num_items) if R[i,j] > 0 ]\n", + "\n", + "steps=1000\n", + "learning_rate=0.01\n", + "r_lambda=0.01\n", + "\n", + "for step in range(steps):\n", + " for i, j, r in non_zeros:\n", + " eij = r - np.dot(P[i, :], Q[j, :].T)\n", + " P[i,:] = P[i,:] + learning_rate*(eij * Q[j, :] - r_lambda*P[i,:])\n", + " Q[j,:] = Q[j,:] + learning_rate*(eij * P[i, :] - r_lambda*Q[j,:])\n", + "\n", + " rmse = get_rmse(R, P, Q, non_zeros)\n", + " if (step % 50) == 0 :\n", + " print(\"### iteration step : \", step,\" rmse : \", rmse)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "7608c423-d57c-4b06-b354-13881af4f36d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "예측 행렬:\n", + " [[3.991 0.897 1.306 2.002 1.663]\n", + " [6.696 4.978 0.979 2.981 1.003]\n", + " [6.677 0.391 2.987 3.977 3.986]\n", + " [4.968 2.005 1.006 2.017 1.14 ]]\n" + ] + } + ], + "source": [ + "pred_matrix = np.dot(P, Q.T)\n", + "print('예측 행렬:\\n', np.round(pred_matrix, 3))" + ] + }, + { + "cell_type": "markdown", + "id": "fa51cefb-628e-4824-bc4c-bcdee6425cbf", + "metadata": {}, + "source": [ + "## **Surprise를 이용한 추천 시스템 구축**" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "6d0ef1ef-c90f-4370-893d-c003a0a87aa1", + "metadata": {}, + "outputs": [], + "source": [ + "from surprise import SVD\n", + "from surprise import Dataset \n", + "from surprise import accuracy \n", + "from surprise.model_selection import train_test_split" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "99be2801-6301-46a3-9380-5ca263bbb771", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dataset ml-100k could not be found. Do you want to download it? [Y/n] " + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + " ㅛ\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dataset ml-100k could not be found. Do you want to download it? [Y/n] " + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + " y\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Trying to download dataset from https://files.grouplens.org/datasets/movielens/ml-100k.zip...\n", + "Done! Dataset ml-100k has been saved to C:\\Users\\SAMSUNG/.surprise_data/ml-100k\n" + ] + } + ], + "source": [ + "data = Dataset.load_builtin('ml-100k')\n", + "trainset, testset = train_test_split(data, test_size=.25, random_state=0) " + ] + }, + { + "attachments": { + "da125617-94d7-421a-ab89-bb7d4870307c.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "id": "cbb45029-eac0-4310-95fc-6aebb9383460", + "metadata": {}, + "source": [ + "![image.png](attachment:da125617-94d7-421a-ab89-bb7d4870307c.png)" + ] + }, + { + "cell_type": "markdown", + "id": "68fc3b7a-a034-4e07-a3ee-0fa487bce667", + "metadata": {}, + "source": [ + "- 최신 무비렌즈 ratings.csv는 콤마(,)로 구분된 CSV 파일, 과거 버전은 탭(\\t) 문자로 구분\n", + "- 무비렌즈 사이트에서 받은 데이터와 Surprise에서 제공하는 데이터는 분리 문자 등 포맷이 다를 수 있으므로, 동일하게 적용하면 오류가 발생할 수 있음\n", + "- Surprise에 사용자-아이템 평점 데이터를 적용할 때는 데이터 포맷(분리 문자 등)을 확인하고 맞춰야 함\n", + "- Surprise는 자체적으로 데이터 포맷을 변환해 사용하므로, 외부 데이터셋을 사용할 경우 반드시 포맷을 확인하기\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "8f5b50a8-8438-4c48-9bc2-1a4d1e4d9ce6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "algo = SVD(random_state=0)\n", + "algo.fit(trainset) " + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "016e6fbe-0223-4164-ba51-d4b519169e01", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "prediction type : size: 25000\n", + "prediction 결과의 최초 5개 추출\n" + ] + }, + { + "data": { + "text/plain": [ + "[Prediction(uid='120', iid='282', r_ui=4.0, est=3.5114147666251547, details={'was_impossible': False}),\n", + " Prediction(uid='882', iid='291', r_ui=4.0, est=3.573872419581491, details={'was_impossible': False}),\n", + " Prediction(uid='535', iid='507', r_ui=5.0, est=4.033583485472447, details={'was_impossible': False}),\n", + " Prediction(uid='697', iid='244', r_ui=5.0, est=3.8463639495936905, details={'was_impossible': False}),\n", + " Prediction(uid='751', iid='385', r_ui=4.0, est=3.1807542478219157, details={'was_impossible': False})]" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "predictions = algo.test( testset )\n", + "print('prediction type :',type(predictions), ' size:',len(predictions))\n", + "print('prediction 결과의 최초 5개 추출')\n", + "predictions[:5]" + ] + }, + { + "cell_type": "markdown", + "id": "14725644-a885-4535-82e2-6b3c8c430af1", + "metadata": {}, + "source": [ + "- 입력 인자 데이터 세트 크기(예: 25,000개)만큼의 리스트 형태 결과가 반환\n", + "- 각 Prediction 객체에는 Surprise 패키지에서 제공하는 데이터 타입 포함\n", + " - 개별 사용자 아이디(uid)\n", + " - 영화(또는 아이템) 아이디(iid)\n", + " - 실제 평점(r_ui)\n", + " - Surprise가 예측한 평점(est)\n", + "- was_impossible이 True이면 예측값을 생성할 수 없는 데이터임을 의미하며, 여기서는 모두 False" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "b4c4bbd9-4341-4c29-b00c-3c914736ade3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[('120', '282', 3.5114147666251547),\n", + " ('882', '291', 3.573872419581491),\n", + " ('535', '507', 4.033583485472447)]" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[ (pred.uid, pred.iid, pred.est) for pred in predictions[:3] ]" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "15a79055-1342-450f-9a32-e47b4629ab42", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "user: 196 item: 302 r_ui = None est = 4.49 {'was_impossible': False}\n" + ] + } + ], + "source": [ + "uid = str(196)\n", + "iid = str(302)\n", + "pred = algo.predict(uid, iid)\n", + "print(pred)" + ] + }, + { + "cell_type": "markdown", + "id": "c3cf43fe-d2fd-4270-b542-6c5057de8ceb", + "metadata": {}, + "source": [ + "- 개별 사용자의 아이템에 대한 추천 평점을 예측함" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "994c471e-48de-4475-8f81-dd55ba9c5991", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RMSE: 0.9467\n" + ] + }, + { + "data": { + "text/plain": [ + "0.9466860806937948" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "accuracy.rmse(predictions)" + ] + }, + { + "cell_type": "markdown", + "id": "16ab3c7e-95f0-4d22-9699-4fc1343dd885", + "metadata": {}, + "source": [ + "## **Surprise 주요 모듈 소개**" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "1d527914-4de0-4bb8-a2ef-8ae159264ef5", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "ratings = pd.read_csv('./ml-latest-small/ratings.csv')\n", + "ratings.to_csv('./ml-latest-small/ratings_noh.csv', index=False, header=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "8bfe71ce-ce46-4881-a9f0-206a7824affb", + "metadata": {}, + "outputs": [], + "source": [ + "from surprise import Reader\n", + "\n", + "reader = Reader(line_format='user item rating timestamp', sep=',', rating_scale=(0.5, 5))\n", + "data=Dataset.load_from_file('./ml-latest-small/ratings_noh.csv',reader=reader)" + ] + }, + { + "cell_type": "markdown", + "id": "ada4d906-2371-4255-89aa-ff3bdac58252", + "metadata": {}, + "source": [ + "-> ratings_noh.csv 파일에 timestamp 컬럼이 포함되어 있어도, Surprise는 앞 3개 컬럼(user, item, rating)만 로딩\n", + "\n", + "\n", + "- Surprise 데이터 파일 형식 및 Reader 주요 파라미터\n", + "
Surprise 데이터 파일은 기본적으로 무비렌즈 등과 같은 무피헤더(헤더 없는) 데이터 형식을 요구함. 파일의 컬럼 순서와 구분자, 평점 범위 등은 Reader 클래스를 통해 반드시 명확히 지정해야 함.\n", + "- 주요 파라미터\n", + " - line_format (string):\n", + "데이터 각 컬럼의 순서를 공백으로 구분해 지정.\n", + "입력된 순서대로 데이터 파일의 컬럼을 인식함.\n", + " - sep (char) : \n", + "컬럼을 구분하는 문자\n", + " - rating_scale (tuple, optional):\n", + "평점의 최소, 최대값을 지정. \n", + "ratings.csv 파일의 평점 범위에 맞춰 설정해야 함." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "7228ee08-8643-40ee-a7bf-a309bfa816d9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RMSE: 0.8682\n" + ] + }, + { + "data": { + "text/plain": [ + "0.8681952927143516" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "trainset, testset = train_test_split(data, test_size=.25, random_state=0)\n", + "\n", + "algo = SVD(n_factors=50, random_state=0)\n", + "\n", + "algo.fit(trainset) \n", + "predictions = algo.test( testset )\n", + "accuracy.rmse(predictions)" + ] + }, + { + "cell_type": "markdown", + "id": "5e15ac75-2c05-4f97-90cf-ac69cc2adc58", + "metadata": {}, + "source": [ + "- 판다스 DataFrame에서 Suprise 데이터 세트로 로딩 " + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "bbd19599-c6ea-4603-a3c3-a980d0ef5fd2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RMSE: 0.8682\n" + ] + }, + { + "data": { + "text/plain": [ + "0.8681952927143516" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "from surprise import Reader, Dataset\n", + "\n", + "ratings = pd.read_csv('./ml-latest-small/ratings.csv') \n", + "reader = Reader(rating_scale=(0.5, 5.0))\n", + "\n", + "data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader)\n", + "trainset, testset = train_test_split(data, test_size=.25, random_state=0)\n", + "\n", + "algo = SVD(n_factors=50, random_state=0)\n", + "algo.fit(trainset) \n", + "predictions = algo.test( testset )\n", + "accuracy.rmse(predictions)" + ] + }, + { + "cell_type": "markdown", + "id": "27cf9f4e-0cd3-4d97-96a9-390a4d052183", + "metadata": {}, + "source": [ + "## **교차 검증과 하이퍼 파라미터 튜닝**" + ] + }, + { + "cell_type": "markdown", + "id": "79e4f83f-bd81-45ed-869d-649d42930d8f", + "metadata": {}, + "source": [ + "Surprise는 교차 검증과 하이퍼 파라미터 튜님을 위해 사이킷런과 유사한 cross_validate()와 CridSearchCV 클래스를 제공" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "5637d8bf-59d7-4290-9ee6-ad2322af5bfa", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Evaluating RMSE, MAE of algorithm SVD on 5 split(s).\n", + "\n", + " Fold 1 Fold 2 Fold 3 Fold 4 Fold 5 Mean Std \n", + "RMSE (testset) 0.8739 0.8639 0.8700 0.8787 0.8775 0.8728 0.0054 \n", + "MAE (testset) 0.6704 0.6644 0.6710 0.6747 0.6739 0.6709 0.0036 \n", + "Fit time 1.00 1.00 1.11 1.08 1.02 1.04 0.04 \n", + "Test time 0.08 0.10 0.19 0.09 0.10 0.11 0.04 \n" + ] + }, + { + "data": { + "text/plain": [ + "{'test_rmse': array([0.87386097, 0.86385394, 0.87001019, 0.87869206, 0.87754842]),\n", + " 'test_mae': array([0.67040961, 0.66442237, 0.67098542, 0.67468529, 0.67385167]),\n", + " 'fit_time': (1.0017001628875732,\n", + " 1.0029034614562988,\n", + " 1.1057696342468262,\n", + " 1.0764319896697998,\n", + " 1.0185282230377197),\n", + " 'test_time': (0.08444833755493164,\n", + " 0.10113239288330078,\n", + " 0.18674874305725098,\n", + " 0.09435153007507324,\n", + " 0.09557366371154785)}" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from surprise.model_selection import cross_validate\n", + "\n", + "ratings = pd.read_csv('./ml-latest-small/ratings.csv') # reading data in pandas df\n", + "reader = Reader(rating_scale=(0.5, 5.0))\n", + "data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader)\n", + "\n", + "algo = SVD(random_state=0)\n", + "cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=5, verbose=True)" + ] + }, + { + "cell_type": "markdown", + "id": "c39b0ed5-2108-44fa-8fb1-a31dfb988a6a", + "metadata": {}, + "source": [ + "-> 폴드별 성능 평가 수치와 전체 폴드의 평균 성능 평가 수치를 함께 보여줌 " + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "e9ff0904-57b5-49bc-b7dd-e85a3307392e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.8779338881055662\n", + "{'n_epochs': 20, 'n_factors': 50}\n" + ] + } + ], + "source": [ + "from surprise.model_selection import GridSearchCV\n", + "\n", + "# 최적화할 파라미터를 딕셔너리 형태로 지정.\n", + "param_grid = {'n_epochs': [20, 40, 60], 'n_factors': [50, 100, 200] }\n", + "\n", + "gs = GridSearchCV(SVD, param_grid, measures=['rmse', 'mae'], cv=3)\n", + "gs.fit(data)\n", + "\n", + "print(gs.best_score['rmse'])\n", + "print(gs.best_params['rmse'])" + ] + }, + { + "cell_type": "markdown", + "id": "bd133c4c-7fe8-4641-80ca-eebc30951793", + "metadata": {}, + "source": [ + "-> 20에폭, factor 50개일 때 3개의 폴드 검증 데이터 세트에서 최적 RMSE가 약 0.8779로 도출" + ] + }, + { + "cell_type": "markdown", + "id": "79489443-6d79-4bc4-aebd-52a1bee10a44", + "metadata": {}, + "source": [ + "## **Surprise를 이용한 개인화 영화 추천 시스템 구축**" + ] + }, + { + "cell_type": "markdown", + "id": "f908d936-6beb-40c0-9d6b-10746a0e5717", + "metadata": {}, + "source": [ + "Surprise 패키리를 이용해 학습된 추천 알고리즘을 기반으로 특정 사용자가 아직 평점을 매기지 않은 영화 중에서 개인 취향에 가장 적절한 영화를 추천해보자. " + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "968f1a5e-cea2-4920-9c02-dfc5b18221ff", + "metadata": {}, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'DatasetAutoFolds' object has no attribute 'n_users'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[60], line 3\u001b[0m\n\u001b[0;32m 1\u001b[0m data \u001b[38;5;241m=\u001b[39m Dataset\u001b[38;5;241m.\u001b[39mload_from_df(ratings[[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124muserId\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mmovieId\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mrating\u001b[39m\u001b[38;5;124m'\u001b[39m]], reader)\n\u001b[0;32m 2\u001b[0m algo \u001b[38;5;241m=\u001b[39m SVD(n_factors\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m50\u001b[39m, random_state\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m0\u001b[39m)\n\u001b[1;32m----> 3\u001b[0m \u001b[43malgo\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfit\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdata\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32m~\\anaconda3\\envs\\tf\\lib\\site-packages\\surprise\\prediction_algorithms\\matrix_factorization.pyx:155\u001b[0m, in \u001b[0;36msurprise.prediction_algorithms.matrix_factorization.SVD.fit\u001b[1;34m()\u001b[0m\n", + "File \u001b[1;32m~\\anaconda3\\envs\\tf\\lib\\site-packages\\surprise\\prediction_algorithms\\matrix_factorization.pyx:196\u001b[0m, in \u001b[0;36msurprise.prediction_algorithms.matrix_factorization.SVD.sgd\u001b[1;34m()\u001b[0m\n", + "\u001b[1;31mAttributeError\u001b[0m: 'DatasetAutoFolds' object has no attribute 'n_users'" + ] + } + ], + "source": [ + "data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader)\n", + "algo = SVD(n_factors=50, random_state=0)\n", + "algo.fit(data)" + ] + }, + { + "cell_type": "markdown", + "id": "8dc908b3-d7e8-433c-b15f-b91d9a9c8ad5", + "metadata": {}, + "source": [ + "-> Surprise는 데이터 세트를 내부에서 사용하는 TrainSet 클래스 객체로 변환하지 않으면 fit을 통해 학습 불가" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "e57c8ee8-6228-4f5d-adaa-3a762c415ee9", + "metadata": {}, + "outputs": [], + "source": [ + "from surprise.dataset import DatasetAutoFolds\n", + "\n", + "reader = Reader(line_format='user item rating timestamp', sep=',', rating_scale=(0.5, 5))\n", + "data_folds = DatasetAutoFolds(ratings_file='./ml-latest-small/ratings_noh.csv', reader=reader)\n", + "\n", + "trainset = data_folds.build_full_trainset()" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "b4e61062-94c1-41cc-9ad7-7a478581ee82", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "algo = SVD(n_epochs=20, n_factors=50, random_state=0)\n", + "algo.fit(trainset)" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "id": "7d276b3f-ebd4-49cb-a4cb-57741b35d262", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "사용자 아이디 9는 영화 아이디 42의 평점 없음\n", + " movieId title genres\n", + "38 42 Dead Presidents (1995) Action|Crime|Drama\n" + ] + } + ], + "source": [ + "movies = pd.read_csv('./ml-latest-small/movies.csv')\n", + "\n", + "# movieId=42 데이터가 있는지 확인. \n", + "movieIds = ratings[ratings['userId']==9]['movieId']\n", + "if movieIds[movieIds==42].count() == 0:\n", + " print('사용자 아이디 9는 영화 아이디 42의 평점 없음')\n", + "\n", + "print(movies[movies['movieId']==42])" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "f9d6007d-6ac7-483e-a09c-b780552b3255", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "user: 9 item: 42 r_ui = None est = 3.13 {'was_impossible': False}\n" + ] + } + ], + "source": [ + "uid = str(9)\n", + "iid = str(42)\n", + "\n", + "pred = algo.predict(uid, iid, verbose=True)" + ] + }, + { + "cell_type": "markdown", + "id": "22656d3c-65ef-4c4c-a701-d729a4de25ed", + "metadata": {}, + "source": [ + "-> 추천 예측 평점 est는 3.13" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "id": "94850149-c32d-4773-bbe7-f3414a999a7b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "평점 매긴 영화수: 46 추천대상 영화수: 9696 전체 영화수: 9742\n" + ] + } + ], + "source": [ + "def get_unseen_surprise(ratings, movies, userId):\n", + " seen_movies = ratings[ratings['userId']== userId]['movieId'].tolist()\n", + " \n", + " total_movies = movies['movieId'].tolist()\n", + " \n", + " unseen_movies= [movie for movie in total_movies if movie not in seen_movies]\n", + " print('평점 매긴 영화수:',len(seen_movies), '추천대상 영화수:',len(unseen_movies), \\\n", + " '전체 영화수:',len(total_movies))\n", + " \n", + " return unseen_movies\n", + "\n", + "unseen_movies = get_unseen_surprise(ratings, movies, 9)" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "id": "751032a5-fb92-44a1-82f9-202aab3380ca", + "metadata": {}, + "outputs": [], + "source": [ + "def recomm_movie_by_surprise(algo, userId, unseen_movies, top_n=10):\n", + " predictions = [algo.predict(str(userId), str(movieId)) for movieId in unseen_movies]\n", + " \n", + " def sortkey_est(pred):\n", + " return pred.est\n", + " \n", + " predictions.sort(key=sortkey_est, reverse=True)\n", + " top_predictions= predictions[:top_n]\n", + " \n", + " top_movie_ids = [ int(pred.iid) for pred in top_predictions]\n", + " top_movie_rating = [ pred.est for pred in top_predictions]\n", + " top_movie_titles = movies[movies.movieId.isin(top_movie_ids)]['title']\n", + " top_movie_preds = [ (id, title, rating) for id, title, rating in zip(top_movie_ids, top_movie_titles, top_movie_rating)]\n", + " \n", + " return top_movie_preds" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "d65167fa-d9a3-4a3d-ab11-046e7926594d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "평점 매긴 영화수: 46 추천대상 영화수: 9696 전체 영화수: 9742\n", + "##### Top-10 추천 영화 리스트 #####\n", + "Usual Suspects, The (1995) : 4.306302135700814\n", + "Star Wars: Episode IV - A New Hope (1977) : 4.281663842987387\n", + "Pulp Fiction (1994) : 4.278152632122759\n", + "Silence of the Lambs, The (1991) : 4.226073566460876\n", + "Godfather, The (1972) : 4.1918097904381995\n", + "Streetcar Named Desire, A (1951) : 4.154746591122658\n", + "Star Wars: Episode V - The Empire Strikes Back (1980) : 4.122016128534504\n", + "Star Wars: Episode VI - Return of the Jedi (1983) : 4.108009609093436\n", + "Goodfellas (1990) : 4.083464936588478\n", + "Glory (1989) : 4.07887165526957\n" + ] + } + ], + "source": [ + "unseen_movies = get_unseen_surprise(ratings, movies, 9)\n", + "top_movie_preds = recomm_movie_by_surprise(algo, 9, unseen_movies, top_n=10)\n", + "print('##### Top-10 추천 영화 리스트 #####')\n", + "\n", + "for top_movie in top_movie_preds:\n", + " print(top_movie[1], \":\", top_movie[2])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd79d250-4425-4bad-8e01-7e4383a052b6", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (tf)", + "language": "python", + "name": "tf" + }, + "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.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git "a/Week16_\354\230\210\354\212\265\352\263\274\354\240\234_\352\263\240\354\235\200\353\271\204.pdf" "b/Week16_\354\230\210\354\212\265\352\263\274\354\240\234_\352\263\240\354\235\200\353\271\204.pdf" new file mode 100644 index 0000000..66de548 Binary files /dev/null and "b/Week16_\354\230\210\354\212\265\352\263\274\354\240\234_\352\263\240\354\235\200\353\271\204.pdf" differ