diff --git a/(Study Case V) Film Recommendation System/Film Recommender.ipynb b/(Study Case V) Film Recommendation System/Film Recommender.ipynb new file mode 100644 index 0000000..4669bde --- /dev/null +++ b/(Study Case V) Film Recommendation System/Film Recommender.ipynb @@ -0,0 +1,4488 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QXLJDtxsguzf" + }, + "source": [ + "> Nama : Muhammad Ammar Nabil
\n", + "Kelas  : M03
\n", + "Email  : mammarnabil1@gmail.com" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xt9VQpBWz-XA" + }, + "source": [ + "
\n", + "\n", + "# **Movie Recommender**\n", + "###### [Zahra Nazari, Hamidreza Koohi, Javad Mousavi](https://jad.shahroodut.ac.ir/article_2390.html)\n", + "---\n", + "\n", + "
\n", + "\n", + "In this notebook, we learn how to build a recommender model to recommend a simillar film based on what they like (Content Based Filtering) or community like (Collaborative Filtering). In this model, i'll use MovieLens dataset in kaggle that includes:\n", + "* Film Title \n", + "* Genre\n", + "* Tag\n", + "* Rating\n", + "\n", + "> Number Film listed is **27262 data**\n", + "\n", + "> Number ratings listed is **20.000.000 data**\n", + "\n", + "
\n", + "\n", + "## • ***Background***\n", + "\n", + "I choose this problem because it's can help to improve experience of stream app company to recommend similar film that they like or community like. This is can improve satisfaction of client that can impact revenue of company.\n", + "\n", + "My reference comes from **Zahra Nazari, Hamidreza Koohi, Javad Mousavi** in the journal entitled _**\"Increasing Performance of Recommender Systems by Combining Deep Learning and Extreme Learning Machine\"**_. In the journal, they applied new deep learning-based clustering methods in order to overcome the data sparsity problem, and increment the efficiency of the recommender systems based on precision, accuracy, F-measure, and recal. They use dataset from kaggle [MovieLens 20M Dataset](https://www.kaggle.com/datasets/grouplens/movielens-20m-dataset). For more details about my model, download this [Details Report](https://colab.research.google.com/drive/14zROOHUuS7qmjisQAtCLewyGE_GZ5ML1?usp=sharing) Only available in Bahasa Indonesia\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "FSKDD17gzGf0" + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import seaborn as sns\n", + "import tensorflow as tf\n", + "import matplotlib.pyplot as plt\n", + "from keras import layers\n", + "from tensorflow import keras\n", + "from google.colab import files\n", + "from sklearn.preprocessing import Normalizer\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.metrics.pairwise import cosine_similarity\n", + "from sklearn.feature_extraction.text import TfidfVectorizer\n", + "from tensorflow.keras.callbacks import EarlyStopping, CSVLogger" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "h_7HniFhz08I" + }, + "source": [ + "## **Import and Understanding Dataset**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5_FM0R6A-V88" + }, + "source": [ + "### *1. Data Loading*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 74 + }, + "collapsed": true, + "id": "PiqjgZaZqeHu", + "outputId": "cab22b3b-6fc8-44da-d598-f059d8c9152e" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " \n", + " Upload widget is only available when the cell has been executed in the\n", + " current browser session. Please rerun this cell to enable.\n", + " \n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Saving kaggle.json to kaggle.json\n" + ] + } + ], + "source": [ + "# Upload kaggle.json API\n", + "!mkdir ~/.kaggle\n", + "files.upload()\n", + "!mv kaggle.json ~/.kaggle/" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "collapsed": true, + "id": "0Wa0VGWxJRj3", + "outputId": "857d8b28-1c6c-4e0d-eb0d-3c05477f1795" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "total 16\n", + "drwxr-xr-x 2 root root 4096 Sep 25 16:58 .\n", + "drwx------ 1 root root 4096 Sep 25 16:58 ..\n", + "-rw------- 1 root root 63 Sep 25 16:58 kaggle.json\n" + ] + } + ], + "source": [ + "# Change permission\n", + "!chmod 600 ~/.kaggle/kaggle.json\n", + "!ls ~/.kaggle/ -la" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "collapsed": true, + "id": "QN_62ntdKq11", + "outputId": "ec3bed35-d4eb-4253-ba91-2f406536835e" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Downloading movielens-20m-dataset.zip to /content\n", + " 97% 189M/195M [00:08<00:00, 34.6MB/s]\n", + "100% 195M/195M [00:08<00:00, 24.9MB/s]\n", + "Archive: movielens-20m-dataset.zip\n", + " inflating: genome_scores.csv \n", + " inflating: genome_tags.csv \n", + " inflating: link.csv \n", + " inflating: movie.csv \n", + " inflating: rating.csv \n", + " inflating: tag.csv \n" + ] + } + ], + "source": [ + "# Download and extract kaggle dataset\n", + "!kaggle datasets download -d grouplens/movielens-20m-dataset\n", + "!unzip movielens-20m-dataset.zip\n", + "!rm movielens-20m-dataset.zip" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true, + "id": "PN-F2Sfey7yy" + }, + "outputs": [], + "source": [ + "# load the dataset\n", + "movies = pd.read_csv('movie.csv')\n", + "ratings = pd.read_csv('rating.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "JAahsD9kj0Y2", + "outputId": "647cc67a-ca5a-4c68-f153-148341313a63" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Total movies \t\t= 27262 movie\n", + "Total rating count \t= 26744 rating\n" + ] + } + ], + "source": [ + "print(f'Total movies \\t\\t= {(len(movies.title.unique()))} movie')\n", + "print(f'Total rating count \\t= {(len(ratings.movieId.unique()))} rating')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V4bj8rD0-PdR" + }, + "source": [ + "### *2. Exploratory Data Analysis - Variable Description*" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "80_TMAaBl6sm" + }, + "source": [ + "#### **EDA Movies**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "NZnSsxMjmGz8", + "outputId": "67ae125d-b2d5-4617-8dea-103b635553ac" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Shape of movies (27278, 3)\n", + "\n", + "\n", + "RangeIndex: 27278 entries, 0 to 27277\n", + "Data columns (total 3 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 movieId 27278 non-null int64 \n", + " 1 title 27278 non-null object\n", + " 2 genres 27278 non-null object\n", + "dtypes: int64(1), object(2)\n", + "memory usage: 639.5+ KB\n" + ] + } + ], + "source": [ + "# Check data type each atribute\n", + "print(f'Shape of movies {movies.shape}\\n')\n", + "movies.info()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "poBQrSCy1C5W", + "outputId": "e7f73eb1-590b-4f86-ac8c-b5c6b1677cee" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\n", + "RangeIndex: 27278 entries, 0 to 27277\n", + "Data columns (total 3 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 movieId 27278 non-null category\n", + " 1 title 27278 non-null object \n", + " 2 genres 27278 non-null category\n", + "dtypes: category(2), object(1)\n", + "memory usage: 1.6+ MB\n" + ] + } + ], + "source": [ + "# Change dataType of movieId\n", + "movies = movies.astype({'movieId': 'category', 'genres': 'category'})\n", + "movies.info()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "VvBpuPtxnSTC", + "outputId": "72f00982-8b5f-4b5d-9766-126526f01192" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Total movies : 27262 movie\n", + "\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "array(['Toy Story (1995)', 'Jumanji (1995)', 'Grumpier Old Men (1995)',\n", + " ..., 'The Pirates (2014)', 'Rentun Ruusu (2001)',\n", + " 'Innocence (2014)'], dtype=object)" + ] + }, + "metadata": {}, + "execution_count": 9 + } + ], + "source": [ + "# Check title attribute\n", + "print(f'Total movies : {len(movies.title.unique())} movie\\n') \n", + "movies.title.unique()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "JDHIRePrnsHB", + "outputId": "51f32f0e-0c7b-418e-de92-05dc86348473" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Total genres : 1342 genre\n", + "\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "['Adventure|Animation|Children|Comedy|Fantasy', 'Adventure|Children|Fantasy', 'Comedy|Romance', 'Comedy|Drama|Romance', 'Comedy', ..., 'Adventure|Children|Drama|Sci-Fi', 'Children|Documentary|Drama', 'Action|Adventure|Animation|Fantasy|Horror', 'Animation|Children|Comedy|Fantasy|Sci-Fi', 'Animation|Children|Comedy|Western']\n", + "Length: 1342\n", + "Categories (1342, object): ['(no genres listed)', 'Action', 'Action|Adventure',\n", + " 'Action|Adventure|Animation', ..., 'Thriller|Western', 'War', 'War|Western', 'Western']" + ] + }, + "metadata": {}, + "execution_count": 10 + } + ], + "source": [ + "# Check genre attribute\n", + "print(f'Total genres : {len(movies.genres.unique())} genre\\n') \n", + "movies.genres.unique()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "znMufTthmIyf" + }, + "source": [ + "#### **EDA Ratings**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "DdK7qhCDmHXM", + "outputId": "7a9f4b97-285b-4414-8a11-aa0893de4d7f" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Shape of Ratings (20000263, 4)\n", + "\n", + "\n", + "RangeIndex: 20000263 entries, 0 to 20000262\n", + "Data columns (total 4 columns):\n", + " # Column Dtype \n", + "--- ------ ----- \n", + " 0 userId int64 \n", + " 1 movieId int64 \n", + " 2 rating float64\n", + " 3 timestamp object \n", + "dtypes: float64(1), int64(2), object(1)\n", + "memory usage: 610.4+ MB\n" + ] + } + ], + "source": [ + "# Check data type each attribute\n", + "print(f'Shape of Ratings {ratings.shape}\\n')\n", + "ratings.info()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "i5koKrww1_mY", + "outputId": "863d3d77-7959-4af7-c7fc-bf3acf07af91" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\n", + "RangeIndex: 20000263 entries, 0 to 20000262\n", + "Data columns (total 4 columns):\n", + " # Column Dtype \n", + "--- ------ ----- \n", + " 0 userId category \n", + " 1 movieId category \n", + " 2 rating float32 \n", + " 3 timestamp datetime64[ns]\n", + "dtypes: category(2), datetime64[ns](1), float32(1)\n", + "memory usage: 349.6 MB\n" + ] + } + ], + "source": [ + "# Change datatype to get less memory\n", + "ratings = ratings.astype({'movieId': 'category', 'userId': 'category', \n", + " 'rating': 'float32', 'timestamp': 'datetime64[ns]'})\n", + "ratings.info()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "a2fvOjE4o6nb", + "outputId": "14be5312-13e2-43e0-c5ab-3f39a7e9b190" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "count 20000263.0\n", + "mean 4.0\n", + "std 1.0\n", + "min 0.0\n", + "25% 3.0\n", + "50% 4.0\n", + "75% 4.0\n", + "max 5.0\n", + "Name: rating, dtype: float64" + ] + }, + "metadata": {}, + "execution_count": 13 + } + ], + "source": [ + "# Range of ratings\n", + "ratings['rating'].describe().round()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "stGZ4WzqoAZn", + "outputId": "a70a97d6-8915-484e-c665-56ce40b8ee5c" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Total Ratings count : 26744 rating\n", + "\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "0 3.5\n", + "1 3.5\n", + "2 3.5\n", + "3 3.5\n", + "4 3.5\n", + "Name: rating, dtype: float32" + ] + }, + "metadata": {}, + "execution_count": 14 + } + ], + "source": [ + "# Check rating attribute\n", + "print(f'Total Ratings count : {len(ratings.movieId.unique())} rating\\n') \n", + "ratings.rating.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xXMgxzzPXqsA" + }, + "source": [ + "### *3. Exploratory Data Analysis - Checking Missing Value*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "8fHcuJG9XP5e", + "outputId": "65a994ed-313c-4229-a16d-6140aa399071" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "movieId 0\n", + "title 0\n", + "genres 0\n", + "dtype: int64" + ] + }, + "metadata": {}, + "execution_count": 15 + } + ], + "source": [ + "# Check Null value in movies\n", + "movies.isna().sum()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "wz1thDRKXJQc", + "outputId": "c1ea24ea-8567-4908-f8c8-81e6d5cab825" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "userId 0\n", + "movieId 0\n", + "rating 0\n", + "timestamp 0\n", + "dtype: int64" + ] + }, + "metadata": {}, + "execution_count": 16 + } + ], + "source": [ + "# Check null value in ratings\n", + "ratings.isna().sum()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "z0RYJwW2R6W2" + }, + "source": [ + "## **Data Preprocessing**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VEtZKK2YR8gI" + }, + "source": [ + "### *1. Content Based Filtering*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 480 + }, + "id": "Ixvj9ynhQ4Ef", + "outputId": "292bca8d-fe03-430e-d4f8-a7614805f533" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:12: FutureWarning: In a future version of pandas all arguments of DataFrame.drop except for the argument 'labels' will be keyword-only\n", + " if sys.path[0] == '':\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " movieId film genre\n", + "0 1 toy story Adventure\n", + "1 2 jumanji Adventure\n", + "2 3 grumpier old men Comedy\n", + "3 4 waiting to exhale Comedy\n", + "4 5 father of the bride part ii Comedy\n", + "... ... ... ...\n", + "27273 131254 kein bund für's leben Comedy\n", + "27274 131256 feuer, eis & dosenbier Comedy\n", + "27275 131258 the pirates Adventure\n", + "27276 131260 rentun ruusu NaN\n", + "27277 131262 innocence Adventure\n", + "\n", + "[27278 rows x 3 columns]" + ], + "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", + "
movieIdfilmgenre
01toy storyAdventure
12jumanjiAdventure
23grumpier old menComedy
34waiting to exhaleComedy
45father of the bride part iiComedy
............
27273131254kein bund für's lebenComedy
27274131256feuer, eis & dosenbierComedy
27275131258the piratesAdventure
27276131260rentun ruusuNaN
27277131262innocenceAdventure
\n", + "

27278 rows × 3 columns

\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 21 + } + ], + "source": [ + "# Make a film only have one genre\n", + "genre_fix = movies['genres'].map(lambda genre: genre.split('|')[0])\n", + "genre_fix = pd.DataFrame(genre_fix.replace('(no genres listed)', np.nan))\n", + "\n", + "# Rename genres and rename film title\n", + "genre_fix = genre_fix.replace({'genres': {'Sci-Fi': 'Scifi', 'Film-Noir': 'Noir'}})\n", + "title_fix = pd.DataFrame(movies['title'].map(lambda title: title.lower()[:-7]))\n", + "\n", + "movies_fix = movies.copy()\n", + "movies_fix['film'] = title_fix\n", + "movies_fix['genre'] = genre_fix\n", + "movies_fix.drop(['title', 'genres'], 1, inplace=True)\n", + "movies_fix['genre'] = movies_fix['genre'].astype('category')\n", + "movies_fix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 638 + }, + "id": "9Tsud3emVO6O", + "outputId": "b83a7d11-3f45-4700-d11e-c2e92ffe449d" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Before droping null value\n", + "movieId 0\n", + "film 0\n", + "genre 246\n", + "dtype: int64 \n", + "\n", + "After droping null value\n", + "movieId 0\n", + "film 0\n", + "genre 0\n", + "dtype: int64 \n", + "\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " movieId film genre\n", + "0 1 toy story Adventure\n", + "1 2 jumanji Adventure\n", + "2 3 grumpier old men Comedy\n", + "3 4 waiting to exhale Comedy\n", + "4 5 father of the bride part ii Comedy\n", + "... ... ... ...\n", + "27272 131252 forklift driver klaus: the first day on the job Comedy\n", + "27273 131254 kein bund für's leben Comedy\n", + "27274 131256 feuer, eis & dosenbier Comedy\n", + "27275 131258 the pirates Adventure\n", + "27277 131262 innocence Adventure\n", + "\n", + "[27032 rows x 3 columns]" + ], + "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", + "
movieIdfilmgenre
01toy storyAdventure
12jumanjiAdventure
23grumpier old menComedy
34waiting to exhaleComedy
45father of the bride part iiComedy
............
27272131252forklift driver klaus: the first day on the jobComedy
27273131254kein bund für's lebenComedy
27274131256feuer, eis & dosenbierComedy
27275131258the piratesAdventure
27277131262innocenceAdventure
\n", + "

27032 rows × 3 columns

\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 22 + } + ], + "source": [ + "# Drop null value\n", + "print('Before droping null value')\n", + "print(movies_fix.isna().sum(), '\\n')\n", + "\n", + "print('After droping null value')\n", + "movies_fix.dropna(inplace=True)\n", + "print(movies_fix.isna().sum(), '\\n')\n", + "movies_fix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 424 + }, + "id": "NbTmyWDuX9X9", + "outputId": "40a28f4b-60f9-4ea1-8ba6-c8ec8d759cfc" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " movieId film genre\n", + "0 1 toy story Adventure\n", + "1 2 jumanji Adventure\n", + "2 3 grumpier old men Comedy\n", + "3 4 waiting to exhale Comedy\n", + "4 5 father of the bride part ii Comedy\n", + "... ... ... ...\n", + "27271 131250 no more school Comedy\n", + "27272 131252 forklift driver klaus: the first day on the job Comedy\n", + "27273 131254 kein bund für's leben Comedy\n", + "27274 131256 feuer, eis & dosenbier Comedy\n", + "27275 131258 the pirates Adventure\n", + "\n", + "[25966 rows x 3 columns]" + ], + "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", + "
movieIdfilmgenre
01toy storyAdventure
12jumanjiAdventure
23grumpier old menComedy
34waiting to exhaleComedy
45father of the bride part iiComedy
............
27271131250no more schoolComedy
27272131252forklift driver klaus: the first day on the jobComedy
27273131254kein bund für's lebenComedy
27274131256feuer, eis & dosenbierComedy
27275131258the piratesAdventure
\n", + "

25966 rows × 3 columns

\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 23 + } + ], + "source": [ + "# Drop duplacate data\n", + "movies_fix.drop_duplicates('film', inplace=True)\n", + "movies_fix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 424 + }, + "id": "63h1PdlteBBh", + "outputId": "fc8d11bd-61d0-48b5-c025-8cf9b78c5660" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " id film genre\n", + "0 1 toy story Adventure\n", + "1 2 jumanji Adventure\n", + "2 3 grumpier old men Comedy\n", + "3 4 waiting to exhale Comedy\n", + "4 5 father of the bride part ii Comedy\n", + "... ... ... ...\n", + "27271 131250 no more school Comedy\n", + "27272 131252 forklift driver klaus: the first day on the job Comedy\n", + "27273 131254 kein bund für's leben Comedy\n", + "27274 131256 feuer, eis & dosenbier Comedy\n", + "27275 131258 the pirates Adventure\n", + "\n", + "[25966 rows x 3 columns]" + ], + "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", + "
idfilmgenre
01toy storyAdventure
12jumanjiAdventure
23grumpier old menComedy
34waiting to exhaleComedy
45father of the bride part iiComedy
............
27271131250no more schoolComedy
27272131252forklift driver klaus: the first day on the jobComedy
27273131254kein bund für's lebenComedy
27274131256feuer, eis & dosenbierComedy
27275131258the piratesAdventure
\n", + "

25966 rows × 3 columns

\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 24 + } + ], + "source": [ + "# Create data variable\n", + "data = movies_fix.copy().rename(columns={'movieId':'id'})\n", + "data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "inLs88GJSKXV" + }, + "source": [ + "### *2. Collaborative Filtering*" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a-qeCNoQqx8l" + }, + "source": [ + "#### **Generate movie, user to index variable**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 661 + }, + "collapsed": true, + "id": "ulQFfTL-S21d", + "outputId": "c3a027d3-1d6b-492e-8026-98035422e619" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " userId movieId rating timestamp\n", + "0 1 2 3.5 2005-04-02 23:53:47\n", + "1 1 29 3.5 2005-04-02 23:31:16\n", + "2 1 32 3.5 2005-04-02 23:33:39\n", + "3 1 47 3.5 2005-04-02 23:32:07\n", + "4 1 50 3.5 2005-04-02 23:29:40\n", + "... ... ... ... ...\n", + "20000258 138493 68954 4.5 2009-11-13 15:42:00\n", + "20000259 138493 69526 4.5 2009-12-03 18:31:48\n", + "20000260 138493 69644 3.0 2009-12-07 18:10:57\n", + "20000261 138493 70286 5.0 2009-11-13 15:42:24\n", + "20000262 138493 71619 2.5 2009-10-17 20:25:36\n", + "\n", + "[20000263 rows x 4 columns]" + ], + "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", + "
userIdmovieIdratingtimestamp
0123.52005-04-02 23:53:47
11293.52005-04-02 23:31:16
21323.52005-04-02 23:33:39
31473.52005-04-02 23:32:07
41503.52005-04-02 23:29:40
...............
20000258138493689544.52009-11-13 15:42:00
20000259138493695264.52009-12-03 18:31:48
20000260138493696443.02009-12-07 18:10:57
20000261138493702865.02009-11-13 15:42:24
20000262138493716192.52009-10-17 20:25:36
\n", + "

20000263 rows × 4 columns

\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 41 + } + ], + "source": [ + "# Create rate dataset\n", + "rate_raw = ratings.copy()\n", + "rate_raw" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "lDD2DmZJhgWv", + "outputId": "7529143a-c812-45f2-a03b-eb817ba71449" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Total userID: 138493\n", + "Total restoID: 26744 \n", + "\n" + ] + } + ], + "source": [ + "# Get unique userID\n", + "user_id = rate_raw['userId'].unique().tolist()\n", + "movie_id = rate_raw['movieId'].unique().tolist()\n", + "print('Total userID: ', len(user_id))\n", + "print('Total restoID: ', len(movie_id), '\\n')\n", + " \n", + "# Create dic user:index\n", + "user_to_index = {x: i for i, x in enumerate(user_id)}\n", + "# print('Encoded userID : ', user_to_index)\n", + " \n", + "# Create dic index:user\n", + "index_to_user = {i: x for i, x in enumerate(user_id)}\n", + "# print('Encoded index:userID: ', index_to_user)\n", + "\n", + "# Create dic movie:index\n", + "movie_to_index = {x: i for i, x in enumerate(movie_id)}\n", + "# print('Encoded movieID: ', movie_to_index)\n", + "\n", + "# Create dic index:movie\n", + "index_to_movie = {i: x for i, x in enumerate(movie_id)}\n", + "# print('Encoded index:movieID: ', index_to_movie)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kh-qi7hblkqB" + }, + "source": [ + "Mohon maaf saya tidak menampilkan hasil output diatas dikarenakan membuat crash aplikasi karena memuat data yang besar (>200000 data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "CCy0UYH22U-O", + "outputId": "c2ba9b65-02c1-46ff-fc6f-ad434fa786a6" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Total of users = 138493\n", + "Total of movies = 26744\n" + ] + } + ], + "source": [ + "# Create length of user and movie\n", + "num_user = len(user_to_index)\n", + "num_movie = len(index_to_movie)\n", + "print('Total of users = ', num_user)\n", + "print('Total of movies = ', num_movie)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eVrgPqW1oaoB" + }, + "outputs": [], + "source": [ + "# Add collumns user and movie based on user and movie index\n", + "rate_raw['user'] = rate_raw['userId'].map(user_to_index)\n", + "rate_raw['movie'] = rate_raw['movieId'].map(movie_to_index)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 357 + }, + "id": "Mt0bn317qi1H", + "outputId": "3114af17-a32e-44b4-dad4-0a9893c71491" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " userId movieId rating timestamp user movie\n", + "0 1 2 3.5 2005-04-02 23:53:47 0 0\n", + "1 1 29 3.5 2005-04-02 23:31:16 0 1\n", + "2 1 32 3.5 2005-04-02 23:33:39 0 2\n", + "3 1 47 3.5 2005-04-02 23:32:07 0 3\n", + "4 1 50 3.5 2005-04-02 23:29:40 0 4" + ], + "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", + "
userIdmovieIdratingtimestampusermovie
0123.52005-04-02 23:53:4700
11293.52005-04-02 23:31:1601
21323.52005-04-02 23:33:3902
31473.52005-04-02 23:32:0703
41503.52005-04-02 23:29:4004
\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 45 + } + ], + "source": [ + "rate_raw.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IjXngGc7vJhn" + }, + "source": [ + "#### **Normalize**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YDXzpupTyI1t" + }, + "outputs": [], + "source": [ + "# Create min max rate_raw to normalize targed\n", + "min_rate = min(rate_raw['rating'])\n", + "max_rate = max(rate_raw['rating'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 318 + }, + "collapsed": true, + "id": "9-taeuarvJhw", + "outputId": "2ee29525-ab7a-43ea-f9ec-27ff95417528" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Before normalize : \n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " rating\n", + "count 20000263.00\n", + "mean 3.53\n", + "std 1.05\n", + "min 0.50\n", + "25% 3.00\n", + "50% 3.50\n", + "75% 4.00\n", + "max 5.00" + ], + "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", + "
rating
count20000263.00
mean3.53
std1.05
min0.50
25%3.00
50%3.50
75%4.00
max5.00
\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 77 + } + ], + "source": [ + "# Divide data and label then Normalize\n", + "print('Before normalize : ')\n", + "\n", + "x = rate_raw[['user', 'movie']].to_numpy()\n", + "y = rate_raw[\"rating\"].apply(lambda x: (x - min_rate) / (max_rate - min_rate)).to_numpy()\n", + "\n", + "rate_raw.describe().round(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 300 + }, + "collapsed": true, + "id": "KxjLG9KZvJh0", + "outputId": "70361749-2a13-4436-bf20-1174ba134520" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " 0\n", + "count 20000263.0\n", + "mean 0.7\n", + "std 0.2\n", + "min 0.0\n", + "25% 0.6\n", + "50% 0.7\n", + "75% 0.8\n", + "max 1.0" + ], + "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", + "
0
count20000263.0
mean0.7
std0.2
min0.0
25%0.6
50%0.7
75%0.8
max1.0
\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 78 + } + ], + "source": [ + "# After Normalize\n", + "pd.DataFrame(y).describe().round(1)" + ] + }, + { + "cell_type": "code", + "source": [ + "# Change x dtype to get less memory\n", + "x = x.astype('int32')\n", + "x.dtype" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "mRcddjD3c5AL", + "outputId": "d83b0e25-ea92-4754-87c8-a10b2f3bba1b" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "dtype('int32')" + ] + }, + "metadata": {}, + "execution_count": 80 + } + ] + }, + { + "cell_type": "code", + "source": [ + "# Change y dtype to get less memory\n", + "y = y.astype('float16')\n", + "y.dtype" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "rIBN5H5Rd29D", + "outputId": "a830303d-4805-419b-90ce-4356bd1d2815" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "dtype('float16')" + ] + }, + "metadata": {}, + "execution_count": 81 + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hwE9MmNyXDuo" + }, + "source": [ + "#### **Split Dataset**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BYQWJqlmXJyE" + }, + "outputs": [], + "source": [ + "# Split train and test set\n", + "x_train, x_test, y_train, y_test = train_test_split(x, y, test_size = 0.001, \n", + " random_state = 123)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "collapsed": true, + "id": "qjZmCZf7XTDc", + "outputId": "b3fd6b8c-e6f4-4957-9d36-2e33d53e33d5" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Total # of sample in whole dataset: 20000263\n", + "Total # of sample in train dataset: 19980262\n", + "Total # of sample in test dataset: 20001\n" + ] + } + ], + "source": [ + "# Check total of test and train set\n", + "print(f'Total # of sample in whole dataset: {len(x)}')\n", + "print(f'Total # of sample in train dataset: {len(x_train)}')\n", + "print(f'Total # of sample in test dataset: {len(x_test)}')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9zpiVQdCZpou" + }, + "source": [ + "## **Model Development**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0SLgSmWLfwzp" + }, + "source": [ + "### *1. Content Based Filtering*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "U004XzEwf4iw", + "outputId": "86633ba6-5768-4d2a-9407-e66ed99e0abd" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "['Adventure', 'Comedy', 'Action', 'Drama', 'Crime', ..., 'Romance', 'War', 'Scifi', 'Musical', 'IMAX']\n", + "Length: 19\n", + "Categories (19, object): ['Action', 'Adventure', 'Animation', 'Children', ..., 'Scifi', 'Thriller',\n", + " 'War', 'Western']" + ] + }, + "metadata": {}, + "execution_count": 26 + } + ], + "source": [ + "data.genre.unique()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "collapsed": true, + "id": "cn5nPatEZu2Q", + "outputId": "ef0b6d36-43b2-4ade-d8c0-33c2e0bfa2ba" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Shape of TF-IDF Matrix = (25966, 19) \n", + "\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "array(['action', 'adventure', 'animation', 'children', 'comedy', 'crime',\n", + " 'documentary', 'drama', 'fantasy', 'horror', 'imax', 'musical',\n", + " 'mystery', 'noir', 'romance', 'scifi', 'thriller', 'war',\n", + " 'western'], dtype=object)" + ] + }, + "metadata": {}, + "execution_count": 27 + } + ], + "source": [ + "# Vectorize data with TF-IDF\n", + "tf = TfidfVectorizer()\n", + "tfidf_matrix = tf.fit_transform(data['genre']) \n", + "\n", + "print('Shape of TF-IDF Matrix =', tfidf_matrix.shape, '\\n')\n", + "tf.get_feature_names_out()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "QWmqupDtjiBZ", + "outputId": "9038b96d-a7b4-45bb-efb0-777242707e6a" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "matrix([[0., 1., 0., ..., 0., 0., 0.],\n", + " [0., 1., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " ...,\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 1., 0., ..., 0., 0., 0.]])" + ] + }, + "metadata": {}, + "execution_count": 28 + } + ], + "source": [ + "# Dense TF-IDF Matrix\n", + "tfidf_matrix.todense()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "gSquNe3gjpZ5", + "outputId": "237ec34f-63ce-472f-cbc8-27c0997729c2" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "array([[1., 1., 0., ..., 0., 0., 1.],\n", + " [1., 1., 0., ..., 0., 0., 1.],\n", + " [0., 0., 1., ..., 1., 1., 0.],\n", + " ...,\n", + " [0., 0., 1., ..., 1., 1., 0.],\n", + " [0., 0., 1., ..., 1., 1., 0.],\n", + " [1., 1., 0., ..., 0., 0., 1.]])" + ] + }, + "metadata": {}, + "execution_count": 29 + } + ], + "source": [ + "# Calculate cosine similarity of tfidf_matrix\n", + "cosine_matrix= cosine_similarity(tfidf_matrix) \n", + "cosine_matrix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 540 + }, + "collapsed": true, + "id": "L0-oM3Hfj2O9", + "outputId": "b4b28684-e3ef-4b35-aec3-85d441b43056" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Shape: (25966, 25966)\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "film toy story jumanji grumpier old men \\\n", + "film \n", + "toy story 1.0 1.0 0.0 \n", + "jumanji 1.0 1.0 0.0 \n", + "grumpier old men 0.0 0.0 1.0 \n", + "waiting to exhale 0.0 0.0 1.0 \n", + "father of the bride part ii 0.0 0.0 1.0 \n", + "\n", + "film waiting to exhale father of the bride part ii \\\n", + "film \n", + "toy story 0.0 0.0 \n", + "jumanji 0.0 0.0 \n", + "grumpier old men 1.0 1.0 \n", + "waiting to exhale 1.0 1.0 \n", + "father of the bride part ii 1.0 1.0 \n", + "\n", + "film heat sabrina tom and huck sudden death \\\n", + "film \n", + "toy story 0.0 0.0 1.0 0.0 \n", + "jumanji 0.0 0.0 1.0 0.0 \n", + "grumpier old men 0.0 1.0 0.0 0.0 \n", + "waiting to exhale 0.0 1.0 0.0 0.0 \n", + "father of the bride part ii 0.0 1.0 0.0 0.0 \n", + "\n", + "film goldeneye ... what men talk about \\\n", + "film ... \n", + "toy story 0.0 ... 0.0 \n", + "jumanji 0.0 ... 0.0 \n", + "grumpier old men 0.0 ... 1.0 \n", + "waiting to exhale 0.0 ... 1.0 \n", + "father of the bride part ii 0.0 ... 1.0 \n", + "\n", + "film three quarter moon ants in the pants \\\n", + "film \n", + "toy story 0.0 0.0 \n", + "jumanji 0.0 0.0 \n", + "grumpier old men 1.0 1.0 \n", + "waiting to exhale 1.0 1.0 \n", + "father of the bride part ii 1.0 1.0 \n", + "\n", + "film werner - gekotzt wird später brother bear 2 \\\n", + "film \n", + "toy story 0.0 1.0 \n", + "jumanji 0.0 1.0 \n", + "grumpier old men 0.0 0.0 \n", + "waiting to exhale 0.0 0.0 \n", + "father of the bride part ii 0.0 0.0 \n", + "\n", + "film no more school \\\n", + "film \n", + "toy story 0.0 \n", + "jumanji 0.0 \n", + "grumpier old men 1.0 \n", + "waiting to exhale 1.0 \n", + "father of the bride part ii 1.0 \n", + "\n", + "film forklift driver klaus: the first day on the job \\\n", + "film \n", + "toy story 0.0 \n", + "jumanji 0.0 \n", + "grumpier old men 1.0 \n", + "waiting to exhale 1.0 \n", + "father of the bride part ii 1.0 \n", + "\n", + "film kein bund für's leben feuer, eis & dosenbier \\\n", + "film \n", + "toy story 0.0 0.0 \n", + "jumanji 0.0 0.0 \n", + "grumpier old men 1.0 1.0 \n", + "waiting to exhale 1.0 1.0 \n", + "father of the bride part ii 1.0 1.0 \n", + "\n", + "film the pirates \n", + "film \n", + "toy story 1.0 \n", + "jumanji 1.0 \n", + "grumpier old men 0.0 \n", + "waiting to exhale 0.0 \n", + "father of the bride part ii 0.0 \n", + "\n", + "[5 rows x 25966 columns]" + ], + "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", + " \n", + " \n", + " \n", + "
filmtoy storyjumanjigrumpier old menwaiting to exhalefather of the bride part iiheatsabrinatom and hucksudden deathgoldeneye...what men talk aboutthree quarter moonants in the pantswerner - gekotzt wird späterbrother bear 2no more schoolforklift driver klaus: the first day on the jobkein bund für's lebenfeuer, eis & dosenbierthe pirates
film
toy story1.01.00.00.00.00.00.01.00.00.0...0.00.00.00.01.00.00.00.00.01.0
jumanji1.01.00.00.00.00.00.01.00.00.0...0.00.00.00.01.00.00.00.00.01.0
grumpier old men0.00.01.01.01.00.01.00.00.00.0...1.01.01.00.00.01.01.01.01.00.0
waiting to exhale0.00.01.01.01.00.01.00.00.00.0...1.01.01.00.00.01.01.01.01.00.0
father of the bride part ii0.00.01.01.01.00.01.00.00.00.0...1.01.01.00.00.01.01.01.01.00.0
\n", + "

5 rows × 25966 columns

\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 30 + } + ], + "source": [ + "# Create cosine similary dataframe with film\n", + "cosine_df = pd.DataFrame(cosine_matrix, index=data['film'], columns=data['film'])\n", + "print('Shape:', cosine_df.shape)\n", + " \n", + "cosine_df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QuF4iwhf0RzL" + }, + "source": [ + "### *2. Collaborate Filtering*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "SwKfyUIe0UGd" + }, + "outputs": [], + "source": [ + "# Modify RecommenderNet Class\n", + "class RecommenderNet(tf.keras.Model):\n", + " \n", + " # Define Constructor\n", + " def __init__(self, num_users, num_movie, embedding_size, **kwargs):\n", + " super(RecommenderNet, self).__init__(**kwargs)\n", + " self.num_users = num_users\n", + " self.num_movie = num_movie\n", + " self.embedding_size = embedding_size\n", + " self.user_embedding = layers.Embedding( num_users,\n", + " embedding_size,\n", + " embeddings_initializer = 'he_normal',\n", + " embeddings_regularizer = keras.regularizers.l2(1e-6)\n", + " )\n", + " self.user_bias = layers.Embedding(num_users, 1) \n", + " self.resto_embedding = layers.Embedding( \n", + " num_movie,\n", + " embedding_size,\n", + " embeddings_initializer = 'he_normal',\n", + " embeddings_regularizer = keras.regularizers.l2(1e-6)\n", + " )\n", + " self.resto_bias = layers.Embedding(num_movie, 1)\n", + " \n", + " def call(self, inputs):\n", + " user_vector = self.user_embedding(inputs[:,0]) \n", + " user_bias = self.user_bias(inputs[:, 0]) \n", + " resto_vector = self.resto_embedding(inputs[:, 1]) \n", + " resto_bias = self.resto_bias(inputs[:, 1]) \n", + " \n", + " dot_user_resto = tf.tensordot(user_vector, resto_vector, 2) \n", + " \n", + " x = dot_user_resto + user_bias + resto_bias\n", + " \n", + " return tf.nn.sigmoid(x) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EcLEC-Xm0dOA" + }, + "outputs": [], + "source": [ + "# Create Model\n", + "model = RecommenderNet(num_user, num_movie, 50)\n", + "model.compile('Adam', 'binary_crossentropy', \n", + " [tf.keras.metrics.MeanSquaredError()])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NYPeZ1L1hMHZ" + }, + "outputs": [], + "source": [ + "# Create Callback\n", + "callback= [\n", + " EarlyStopping('val_mean_squared_error', 0.1, 8, 1),\n", + "]" + ] + }, + { + "cell_type": "code", + "source": [ + "# Train Model\n", + "history = model.fit(\n", + " x_train, \n", + " y_train, \n", + " 200000, \n", + " 50, \n", + " callbacks=callback,\n", + " validation_data = (x_test, y_test)\n", + ")" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "B4_HCA1NeX5e", + "outputId": "7c6dd734-a0d3-4aae-ffa2-e9a543c366e0" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Epoch 1/50\n", + "100/100 [==============================] - 29s 265ms/step - loss: 0.6538 - mean_squared_error: 0.0639 - val_loss: 0.6472 - val_mean_squared_error: 0.0611\n", + "Epoch 2/50\n", + "100/100 [==============================] - 27s 267ms/step - loss: 1.4197 - mean_squared_error: 0.1478 - val_loss: 0.8099 - val_mean_squared_error: 0.1398\n", + "Epoch 3/50\n", + "100/100 [==============================] - 33s 331ms/step - loss: 6.1476 - mean_squared_error: 0.2614 - val_loss: 1.3699 - val_mean_squared_error: 0.1526\n", + "Epoch 4/50\n", + "100/100 [==============================] - 26s 262ms/step - loss: 6.5779 - mean_squared_error: 0.1901 - val_loss: 2.0804 - val_mean_squared_error: 0.4447\n", + "Epoch 5/50\n", + "100/100 [==============================] - 26s 258ms/step - loss: 4.5104 - mean_squared_error: 0.2610 - val_loss: 0.8934 - val_mean_squared_error: 0.1162\n", + "Epoch 6/50\n", + "100/100 [==============================] - 27s 266ms/step - loss: 4.1191 - mean_squared_error: 0.1977 - val_loss: 2.6865 - val_mean_squared_error: 0.4805\n", + "Epoch 7/50\n", + "100/100 [==============================] - 33s 324ms/step - loss: 3.3527 - mean_squared_error: 0.2311 - val_loss: 0.6211 - val_mean_squared_error: 0.0457\n", + "Epoch 8/50\n", + "100/100 [==============================] - 26s 259ms/step - loss: 4.7516 - mean_squared_error: 0.2012 - val_loss: 4.0370 - val_mean_squared_error: 0.5024\n", + "Epoch 9/50\n", + "100/100 [==============================] - 29s 284ms/step - loss: 6.0048 - mean_squared_error: 0.2688 - val_loss: 0.6586 - val_mean_squared_error: 0.0594\n", + "Epoch 9: early stopping\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7rcvfAibTuI6" + }, + "source": [ + "## **Model Evaluation**" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### *1. Content Based Filtering*" + ], + "metadata": { + "id": "HF4rnIr8oi2n" + } + }, + { + "cell_type": "markdown", + "source": [ + "Sebelum menjalankan code dibawah, jalankan terlebih dahulu testing Content Based Filtering di cell terakhir" + ], + "metadata": { + "id": "YX2hMvT8pBvx" + } + }, + { + "cell_type": "code", + "source": [ + "# Check genre of toy story\n", + "genre_target = data[data.film.eq('toy story')]['genre'][0]\n", + "genre_target" + ], + "metadata": { + "id": "jL5h-fFReF5w", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 35 + }, + "outputId": "063ef040-a773-4522-b051-111171e1eae8" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "'Adventure'" + ], + "application/vnd.google.colaboratory.intrinsic+json": { + "type": "string" + } + }, + "metadata": {}, + "execution_count": 33 + } + ] + }, + { + "cell_type": "code", + "source": [ + "# Get recommendations\n", + "num_recommendation = 15\n", + "\n", + "result = film_recommendations('toy story', num_recommendation)\n", + "num_correct = result[result.genre == genre_target].genre.count()\n", + "pred_score = (num_correct / num_recommendation)*100\n", + "num_all_genre_target = data[data.genre == genre_target].genre.count()\n", + "recall_score = (num_correct / num_all_genre_target)*100\n", + "\n", + "\n", + "print(f'Precission of model is : {int(pred_score)} %')\n", + "print(f'Recall of model is : {recall_score} ')" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "dni1uUiPmTcd", + "outputId": "2d7d780c-4eaa-4569-c908-3c590a31a9ac" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Precission of model is : 100 %\n", + "Recall of model is : 1.1727912431587177 \n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "film_recommendations('toy story', num_recommendation)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 520 + }, + "id": "zZRyCax7tmUk", + "outputId": "0c8b2067-5c3e-408c-93ae-51114b85b64b" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " film genre\n", + "0 The Pirates Adventure\n", + "1 Toy Story 3 Adventure\n", + "2 Descent: Part 2, The Adventure\n", + "3 The Black Rose Adventure\n", + "4 Young Winston Adventure\n", + "5 St Trinian'S 2: The Legend Of Fritton'S Gold Adventure\n", + "6 Sky Crawlers, The (Sukai Kurora) Adventure\n", + "7 Shrek Forever After (A.K.A. Shrek: The Final C... Adventure\n", + "8 Percy Jackson & The Olympians: The Lightning T... Adventure\n", + "9 B.N.B. (Bunty Aur Babli) Adventure\n", + "10 Agora Adventure\n", + "11 When Dinosaurs Ruled The Earth Adventure\n", + "12 North Face (Nordwand) Adventure\n", + "13 Men Who Tread On The Tiger'S Tail, The (Tora N... Adventure\n", + "14 How To Train Your Dragon Adventure" + ], + "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", + "
filmgenre
0The PiratesAdventure
1Toy Story 3Adventure
2Descent: Part 2, TheAdventure
3The Black RoseAdventure
4Young WinstonAdventure
5St Trinian'S 2: The Legend Of Fritton'S GoldAdventure
6Sky Crawlers, The (Sukai Kurora)Adventure
7Shrek Forever After (A.K.A. Shrek: The Final C...Adventure
8Percy Jackson & The Olympians: The Lightning T...Adventure
9B.N.B. (Bunty Aur Babli)Adventure
10AgoraAdventure
11When Dinosaurs Ruled The EarthAdventure
12North Face (Nordwand)Adventure
13Men Who Tread On The Tiger'S Tail, The (Tora N...Adventure
14How To Train Your DragonAdventure
\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 68 + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "### *2. Collaborative Filtering*" + ], + "metadata": { + "id": "tGr1gqR5omsa" + } + }, + { + "cell_type": "code", + "source": [ + "plt.style.use('dark_background')\n", + "\n", + "plt.plot(history.history['mean_squared_error'], '#1f77b4')\n", + "plt.plot(history.history['val_mean_squared_error'], '#ff7f0e')\n", + "plt.title('Mean Squared Error')\n", + "plt.ylabel('MSE')\n", + "plt.xlabel('epoch')\n", + "plt.legend(['train', 'test'], loc='upper left')\n", + "plt.show()" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 295 + }, + "outputId": "d3732c35-9da7-4d07-b595-232cb6aa0912", + "id": "OEFtKSGbomsd" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYIAAAEWCAYAAABrDZDcAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nOydd3xUddb/3/femfReJxBSgAQQEBKqYoIKiIIBy/4eu1geXdvad637uJa1PqK76u4+i7uuHVnXAiJioiBI0SQEpLckEJJJ7z1z7/39MUxIQhJSps99v17zIjNz586ZYe73fL/n8z3nCICKhoaGhobHIjraAA0NDQ0Nx6I5Ag0NDQ0PR3MEGhoaGh6O5gg0NDQ0PBzNEWhoaGh4OJoj0NDQ0PBwNEegoeFCvPPOOzz77LOONkPDzdAcgYbNKSgooK2tjfDw8G6P79ixA1VViY+Pt7tNjz32GPn5+TQ0NFBUVMTKlSvtboO1WbZsGSaTiYaGhm63mJgYR5um4eRojkDDLhQUFHDNNdd03p80aRJ+fn4OseXGG2/khhtuYP78+QQGBjJ9+nS+++47u9shSZLVz7lt2zYCAwO73YxG44Dee7D22MJ+DcegOQINu/D+++9z4403dt5ftmwZ7733XrdjvLy8eOWVVzh27BilpaX89a9/xcfHB4CQkBDWrFlDeXk51dXVrFmzhpEjR3a+dsOGDTzzzDP8+OOP1NfXs379+tNWIBZmzJjB+vXryc/PB6CsrIwVK1Z0Pp+QkMDGjRupr6/n22+/5Y033uD9998HYO7cuRQVFXU7X0FBAfPmzes899atW6mpqaGkpIQ33ngDvV7feayqqtx1110cOnSIw4cPA7B48WLy8vKoqalhy5YtTJ48ufP4qVOnkpubS319PStXruz8PoZCQUEBv/vd79i1axdNTU2MGTMGVVW55ZZbOHbsGN9//z2CIPDEE09QWFhIWVkZ7777LkFBQQDEx8efdryG+6BqN+1my1tBQYE6b9489cCBA+r48eNVURTVoqIiNS4uTlVVVY2Pj1cBdfny5eqXX36phoaGqgEBAerq1avV559/XgXUsLAw9YorrlB9fX3VgIAAddWqVernn3/e+R4bNmxQjxw5oiYlJak+Pj7qhg0b1BdeeKFXe6677jq1qqpKffjhh9Vp06apoih2e37r1q3qq6++qnp5ealpaWlqfX29+v7776uAOnfuXLWoqKjXzweoqamp6qxZs1RJktT4+Hh137596n333dd5rKqq6rfffquGhoaqPj4+6tSpU9WysjJ15syZqiiK6o033qgWFBSoXl5eql6vVwsLC9X7779f1el06pVXXqm2t7erzz77bK+fa9myZermzZv7/X/Iy8tTY2NjVR8fHzU+Pl5VVVV99913VT8/P9XHx0e9+eab1cOHD6uJiYmqv7+/+p///Ed97733VKDX4x3929JuVrs53ADt5uY3y0D5xBNPqM8//7y6cOFC9dtvv1UlSermCBobG9XRo0d3vm727Nlqfn5+r+ecMmWKWl1d3Xl/w4YN6hNPPNF5/84771TXrVvXp03XXnutmpmZqTY2NqqVlZXq7373OxVQR40apXZ0dKh+fn6dx3744YcDdgQ9b/fdd5/62Wefdd5XVVW94IILOu//5S9/UZ955plurzlw4ICanp6upqWlqcXFxd2e27JlS7+OoKOjQ62pqem8HTlypJudN998c+d9y8CemJjY+VhWVpZ65513dt5PTk5W29vbOx1bz+O1m3vcdGho2In333+fTZs2kZiYeFpYKDIyEn9/f3JzczsfEwShMw7t6+vLa6+9xsUXX0xoaCgAQUFBiKKIoigAlJaWdr62ubmZgICAPm356KOP+Oijj9DpdFx22WV8+OGH7Ny5k7q6Ompqamhubu489tixY4waNWpAnzEpKYnly5czffp0/Pz80Ol03T4T0C20FB8fz7Jly/jNb37T+ZiXlxcjRoxAVVWKi4u7vfbYsWP9vv/27dtJS0vr8/meYa2ej40YMaLbexw7dgy9Xk90dHS/59BwbTSNQMNuHD9+nIKCAhYtWsRnn33W7bnKykqam5uZOHEioaGhhIaGEhISQmBgIAAPPfQQ48aNY9asWQQHB5Oeng6YncVwMJlMfPrpp/zyyy9MmjQJo9FIaGhoNyE7Li6u8++mpqZuz4miSGRkZOf9v/71rxw4cICkpCSCg4N5/PHHT7NRVdXOv4uKivjjH//Y+ZlDQ0Px9/dn5cqVGI3GbjpIT1uGQtf37u2xkpKSbru44uLi6OjooKysrN9zaLg2miPQsCu33norF154YbcZN5gHlxUrVvDaa691DqwjRozgoosuAiAwMJCWlhZqa2sJDQ3lqaeeGrINy5YtY9GiRQQEBCAIAhdffDETJ07kp59+4vjx4+Tk5PD000+j1+uZM2cOGRkZna89dOgQPj4+LFq0CJ1Ox5NPPom3t3fn84GBgdTX19PY2Mi4ceO48847+7VlxYoV3HHHHcycORMAPz+/Ttu2bduGyWTi3nvvRafTcfnll3ceZys+/vhjHnjgARISEvD39+f555/nk08+QZZlm76vhmPRHIGGXcnPzz8tVGLhkUce4ciRI2zfvp26ujqysrIYN24cAK+//jq+vr5UVlayfft2vvnmmyHbUF9fz+OPP87x48epra3l5Zdf5s4772TLli0AXHvttcyaNYvq6mqeeuqpbmGs+vp67rrrLt5++22Ki4tpamrixIkTnc8//PDDXHvttTQ0NLBixQo++eSTfm3Jzc3ltttu480336SmpoYjR45w0003AdDR0cEVV1zBTTfdRHV1NVddddVpK6menHPOOaflEUyfPn3A380///nPzhBeQUEBra2t3cJWGu6JgFks0NDQ6IOnnnqKsWPHcsMNNzjaFA0Nm6CtCDQ0NDQ8HM0RaGhoaHg4WmhIQ0NDw8PRVgQaGhoaHo7LJZSVl5efMalGQ0NDQ6M78fHxREVF9fqcyzmCY8eOMWPGDEeboaGhoeFSZGdn9/mcFhrS0NDQ8HA0R6ChoaHh4WiOQENDQ8PDcTmNoDdCQ0O5//77SUhIGHYRMmdFVVUKCwt5/fXXqampcbQ5GhoaboRbOIL777+fnJwcnnnmGbctjiVJEosXL+b+++8fVsE1DQ0NjZ64RWgoISGBr7/+2m2dAIAsy6xdu5aEhARHm6KhoeFm2NQRLFy4kAMHDnD48GEeeeSR055ftmwZ5eXl5OXlkZeXx6233jqk9xEEwa2dgAVZlt029KWhoeE4bBYaEkWRt956iwULFnDixAmys7NZvXo1+/fv73bcJ598opW51dDQcBpumqrn030dNLY72hL7YbMVwcyZMzly5AgFBQV0dHSwcuVKli5daqu3cyjBwcFnbEDSG2vXriU4ONgGFmloaAyFaTEi7yz15aapekebYlds5ghGjhzZrbfpiRMnTmu7B3DllVeya9cu/v3vfxMbG9vruW677Tays7PJzs4mIiLCViYPmZCQEO66667THrf02+2LxYsXU1dXZyuzNDQ0BklqjPmanRbT/7XrbjhULF6z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\n" + }, + "metadata": {} + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vxHfPGCs8RTV" + }, + "source": [ + "## **Testing**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "l0lc1ZeODqCa" + }, + "source": [ + "### *1. Content Based Filtering*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pjPmobAQ8Ulv" + }, + "outputs": [], + "source": [ + "# Create recommendations function\n", + "def film_recommendations(film: str, \n", + " n: int=5,\n", + " similarity_data: pd.DataFrame=cosine_df, \n", + " items: pd.DataFrame=data[['film', 'genre']]):\n", + " \"\"\"\n", + " Recommends top N-Recommendation of similar film based on genre.\n", + " \n", + " Parameter:\n", + " ---\n", + " film : string (str)\n", + " A Film that will be a reference for recommendations.\n", + " similarity_data : pd.DataFrame (object)\n", + " Dataframe similarity, symmetric, resto as a index and \n", + " collumns\n", + " items : pd.DataFrame (object)\n", + " Dataframe of film info\n", + " k : integer (int)\n", + " Total of return recommendations\n", + "\n", + " Returns\n", + " ---\n", + " Returns a dataframe of top N-Recommendation.\n", + " \"\"\"\n", + "\n", + " # Locate similarity restaurant\n", + " index = similarity_data.loc[:,film.lower()].to_numpy().argpartition(\n", + " range(-1, -n, -1))\n", + " \n", + " # Sort closest similarity\n", + " closest = similarity_data.columns[index[-1:-(n+2):-1]]\n", + " \n", + " # Drop unused data restaurant\n", + " closest = closest.drop(film.lower(), errors='ignore')\n", + "\n", + " result = pd.DataFrame(closest).merge(items).head(n)\n", + " film_fix = result['film'].map(lambda title: title.title())\n", + " result['film'] = film_fix\n", + " \n", + " return result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 81 + }, + "id": "57632FgrBTOh", + "outputId": "babff8b0-add9-46b5-824f-d8f9b1dd6423" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " id film genre\n", + "0 1 toy story Adventure" + ], + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idfilmgenre
01toy storyAdventure
\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 36 + } + ], + "source": [ + "# Check genre of toy story\n", + "data[data.film.eq('toy story')]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 520 + }, + "id": "B_hq1_O7BepY", + "outputId": "d7c80c38-6ea1-4737-aaec-bbeba37bd22e" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " film genre\n", + "0 The Pirates Adventure\n", + "1 Toy Story 3 Adventure\n", + "2 Descent: Part 2, The Adventure\n", + "3 The Black Rose Adventure\n", + "4 Young Winston Adventure\n", + "5 St Trinian'S 2: The Legend Of Fritton'S Gold Adventure\n", + "6 Sky Crawlers, The (Sukai Kurora) Adventure\n", + "7 Shrek Forever After (A.K.A. Shrek: The Final C... Adventure\n", + "8 Percy Jackson & The Olympians: The Lightning T... Adventure\n", + "9 B.N.B. (Bunty Aur Babli) Adventure\n", + "10 Agora Adventure\n", + "11 When Dinosaurs Ruled The Earth Adventure\n", + "12 North Face (Nordwand) Adventure\n", + "13 Men Who Tread On The Tiger'S Tail, The (Tora N... Adventure\n", + "14 How To Train Your Dragon Adventure" + ], + "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", + "
filmgenre
0The PiratesAdventure
1Toy Story 3Adventure
2Descent: Part 2, TheAdventure
3The Black RoseAdventure
4Young WinstonAdventure
5St Trinian'S 2: The Legend Of Fritton'S GoldAdventure
6Sky Crawlers, The (Sukai Kurora)Adventure
7Shrek Forever After (A.K.A. Shrek: The Final C...Adventure
8Percy Jackson & The Olympians: The Lightning T...Adventure
9B.N.B. (Bunty Aur Babli)Adventure
10AgoraAdventure
11When Dinosaurs Ruled The EarthAdventure
12North Face (Nordwand)Adventure
13Men Who Tread On The Tiger'S Tail, The (Tora N...Adventure
14How To Train Your DragonAdventure
\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 37 + } + ], + "source": [ + "# Get recommendations\n", + "film_recommendations('toy story', 15)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7pbOlhvDDv-H" + }, + "source": [ + "### *2. Collaborate Filtering*" + ] + }, + { + "cell_type": "code", + "source": [ + "dataset = data.copy()" + ], + "metadata": { + "id": "fLaPor3LZe9l" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TmdckyZFLpwi" + }, + "outputs": [], + "source": [ + "def highRatedMovie(user: int=user_id, \n", + " n: int=5,\n", + " data_raw: pd.DataFrame=dataset, \n", + " data_rate: pd.DataFrame=ratings):\n", + " '''\n", + " Show N-High Rated Movie from user\n", + "\n", + " Parameter\n", + " ---\n", + " user: int\n", + " Id of user. Must member of dataset.\n", + " n: int\n", + " Total returns of high rate movies. Default 5\n", + " data_raw: pd.DataFrame=dataset\n", + " Dataset of movies. Must contain movieId and genres. Default\n", + " dataset\n", + " data_rate: pd.DataFrame\n", + " Dataset of rating. Must contain rating, movieId, and userId.\n", + " Default ratings\n", + "\n", + " Return\n", + " ---\n", + " Prompt of N-High Rated Movie from user\n", + " '''\n", + " # Prompt Result\n", + " print('===' * 13)\n", + " print(f'Movie with high ratings from user {user}')\n", + " print('===' * 13)\n", + "\n", + " watched_movie_by_user = data_rate[ratings.userId == user]\n", + " top_movie_user = (watched_movie_by_user.sort_values(\n", + " by = 'rating',\n", + " ascending=False\n", + " )\n", + " .head(n)\n", + " .movieId.values\n", + " )\n", + " \n", + " dataset_rows = data_raw[data_raw['id'].isin(top_movie_user)]\n", + " for row in dataset_rows.itertuples():\n", + " print(f' •', (row.film).title(), ':', row.genre)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "4n2-0tlMODeI", + "outputId": "4baaebcd-8180-4863-9923-6d4e7f95929b" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "=======================================\n", + "Movie with high ratings from user 546\n", + "=======================================\n", + " • Dr. Strangelove Or: How I Learned To Stop Worrying And Love The Bomb : Comedy\n", + " • Godfather, The : Crime\n", + " • William Shakespeare'S Romeo + Juliet : Drama\n", + " • Reservoir Dogs : Crime\n", + " • Ice Storm, The : Drama\n", + " • American Beauty : Comedy\n", + " • Ghost Dog: The Way Of The Samurai : Crime\n", + " • City Of God (Cidade De Deus) : Action\n", + " • Lost In Translation : Comedy\n", + " • Garden State : Comedy\n" + ] + } + ], + "source": [ + "highRatedMovie(546,10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "g5WKFOkcGMJ2" + }, + "outputs": [], + "source": [ + "def movieRecommendation(user: int=user_id, \n", + " n: int=5,\n", + " data_raw: pd.DataFrame=dataset,\n", + " data_rate: pd.DataFrame=ratings,\n", + " movieIdx: dict=movie_to_index,\n", + " userIdx: dict=user_to_index):\n", + " '''\n", + " Show N-Recommendation of Movie from user\n", + "\n", + " Parameter\n", + " ---\n", + " user: int\n", + " Id of user. Must member of dataset.\n", + " n: int\n", + " Total returns of high rate movies. Default 5\n", + " data_raw: pd.DataFrame=dataset\n", + " Dataset of movies. Must contain movieId and genres. Default\n", + " dataset\n", + " data_rate: pd.DataFrame\n", + " Dataset of rating. Must contain rating, movieId, and userId.\n", + " Default ratings\n", + " movieIdx: dict\n", + " Movie to movie encode (index) dataframe. Default movie_to_index\n", + " userIdx: dict\n", + " User to user encode (index) dataframe. Default user_to_index\n", + "\n", + " Return\n", + " ---\n", + " Prompt of N-Recommendation of Movie from user\n", + " '''\n", + " # Filter not visited movie\n", + " watched_movie_by_user = data_rate[data_rate.userId == user]\n", + " \n", + " movie_not_watched = data_raw[~data_raw['id'].isin(watched_movie_by_user.movieId.values)]['id'] \n", + " movie_not_watched = list(\n", + " set(movie_not_watched)\n", + " .intersection(set(movieIdx.keys()))\n", + " )\n", + " movie_not_watched = [[movieIdx.get(x)] for x in movie_not_watched]\n", + "\n", + " user_encoder = userIdx.get(user)\n", + " user_movie_array = np.hstack(\n", + " ([[user_encoder]] * len(movie_not_watched), movie_not_watched)\n", + " )\n", + " recommendation = model.predict(user_movie_array).flatten()\n", + " top_ratings_indices = recommendation.argsort()[-n:][::-1]\n", + "\n", + " recommended_movie_ids = [\n", + " index_to_movie.get(movie_not_watched[x][0]) for x in top_ratings_indices\n", + " ]\n", + " \n", + " # Prompt result\n", + " print('====' * 11)\n", + " print(f'Top {n} movie recommendation for user {user}')\n", + " print('====' * 11)\n", + " \n", + " recommended_resto = data_raw[data_raw['id'].isin(recommended_movie_ids)]\n", + " for row in recommended_resto.itertuples():\n", + " print(f' •', (row.film).title(), ':', row.genre)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "-HJBNHpTLsB8", + "outputId": "aeb03bea-0550-4fe1-ea53-6cb96fa70014" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "============================================\n", + "Top 10 movie recommendation for user 546\n", + "============================================\n", + " • Vertigo : Drama\n", + " • Rear Window : Mystery\n", + " • It Happened One Night : Comedy\n", + " • Sunset Blvd. (A.K.A. Sunset Boulevard) : Drama\n", + " • 12 Angry Men : Drama\n", + " • Best Years Of Our Lives, The : Drama\n", + " • On The Waterfront : Crime\n", + " • 400 Blows, The (Les Quatre Cents Coups) : Crime\n", + " • Rashomon (Rashômon) : Crime\n", + " • Secret In Their Eyes, The (El Secreto De Sus Ojos) : Crime\n" + ] + } + ], + "source": [ + "movieRecommendation(546, 10)" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [ + "h_7HniFhz08I", + "5_FM0R6A-V88", + "V4bj8rD0-PdR", + "80_TMAaBl6sm", + "znMufTthmIyf", + "xXMgxzzPXqsA", + "z0RYJwW2R6W2", + "VEtZKK2YR8gI", + "inLs88GJSKXV", + "a-qeCNoQqx8l", + "IjXngGc7vJhn", + "hwE9MmNyXDuo", + "0SLgSmWLfwzp", + "QuF4iwhf0RzL", + "7rcvfAibTuI6", + "tGr1gqR5omsa" + ], + "provenance": [], + "authorship_tag": "ABX9TyO2ZIy/QtQU/4ia5H4B9vy2", + "include_colab_link": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file