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": [
+ ""
+ ]
+ },
+ {
+ "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",
+ " "
+ ]
+ },
+ "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",
+ " movieId | \n",
+ " film | \n",
+ " genre | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 1 | \n",
+ " toy story | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " 2 | \n",
+ " jumanji | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " 3 | \n",
+ " grumpier old men | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " 4 | \n",
+ " waiting to exhale | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 4 | \n",
+ " 5 | \n",
+ " father of the bride part ii | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ "
\n",
+ " \n",
+ " 27273 | \n",
+ " 131254 | \n",
+ " kein bund für's leben | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 27274 | \n",
+ " 131256 | \n",
+ " feuer, eis & dosenbier | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 27275 | \n",
+ " 131258 | \n",
+ " the pirates | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 27276 | \n",
+ " 131260 | \n",
+ " rentun ruusu | \n",
+ " NaN | \n",
+ "
\n",
+ " \n",
+ " 27277 | \n",
+ " 131262 | \n",
+ " innocence | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " movieId | \n",
+ " film | \n",
+ " genre | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 1 | \n",
+ " toy story | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " 2 | \n",
+ " jumanji | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " 3 | \n",
+ " grumpier old men | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " 4 | \n",
+ " waiting to exhale | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 4 | \n",
+ " 5 | \n",
+ " father of the bride part ii | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ "
\n",
+ " \n",
+ " 27272 | \n",
+ " 131252 | \n",
+ " forklift driver klaus: the first day on the job | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 27273 | \n",
+ " 131254 | \n",
+ " kein bund für's leben | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 27274 | \n",
+ " 131256 | \n",
+ " feuer, eis & dosenbier | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 27275 | \n",
+ " 131258 | \n",
+ " the pirates | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 27277 | \n",
+ " 131262 | \n",
+ " innocence | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " movieId | \n",
+ " film | \n",
+ " genre | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 1 | \n",
+ " toy story | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " 2 | \n",
+ " jumanji | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " 3 | \n",
+ " grumpier old men | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " 4 | \n",
+ " waiting to exhale | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 4 | \n",
+ " 5 | \n",
+ " father of the bride part ii | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ "
\n",
+ " \n",
+ " 27271 | \n",
+ " 131250 | \n",
+ " no more school | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 27272 | \n",
+ " 131252 | \n",
+ " forklift driver klaus: the first day on the job | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 27273 | \n",
+ " 131254 | \n",
+ " kein bund für's leben | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 27274 | \n",
+ " 131256 | \n",
+ " feuer, eis & dosenbier | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 27275 | \n",
+ " 131258 | \n",
+ " the pirates | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " id | \n",
+ " film | \n",
+ " genre | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 1 | \n",
+ " toy story | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " 2 | \n",
+ " jumanji | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " 3 | \n",
+ " grumpier old men | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " 4 | \n",
+ " waiting to exhale | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 4 | \n",
+ " 5 | \n",
+ " father of the bride part ii | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ "
\n",
+ " \n",
+ " 27271 | \n",
+ " 131250 | \n",
+ " no more school | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 27272 | \n",
+ " 131252 | \n",
+ " forklift driver klaus: the first day on the job | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 27273 | \n",
+ " 131254 | \n",
+ " kein bund für's leben | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 27274 | \n",
+ " 131256 | \n",
+ " feuer, eis & dosenbier | \n",
+ " Comedy | \n",
+ "
\n",
+ " \n",
+ " 27275 | \n",
+ " 131258 | \n",
+ " the pirates | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " userId | \n",
+ " movieId | \n",
+ " rating | \n",
+ " timestamp | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 1 | \n",
+ " 2 | \n",
+ " 3.5 | \n",
+ " 2005-04-02 23:53:47 | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 29 | \n",
+ " 3.5 | \n",
+ " 2005-04-02 23:31:16 | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " 1 | \n",
+ " 32 | \n",
+ " 3.5 | \n",
+ " 2005-04-02 23:33:39 | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " 1 | \n",
+ " 47 | \n",
+ " 3.5 | \n",
+ " 2005-04-02 23:32:07 | \n",
+ "
\n",
+ " \n",
+ " 4 | \n",
+ " 1 | \n",
+ " 50 | \n",
+ " 3.5 | \n",
+ " 2005-04-02 23:29:40 | \n",
+ "
\n",
+ " \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ "
\n",
+ " \n",
+ " 20000258 | \n",
+ " 138493 | \n",
+ " 68954 | \n",
+ " 4.5 | \n",
+ " 2009-11-13 15:42:00 | \n",
+ "
\n",
+ " \n",
+ " 20000259 | \n",
+ " 138493 | \n",
+ " 69526 | \n",
+ " 4.5 | \n",
+ " 2009-12-03 18:31:48 | \n",
+ "
\n",
+ " \n",
+ " 20000260 | \n",
+ " 138493 | \n",
+ " 69644 | \n",
+ " 3.0 | \n",
+ " 2009-12-07 18:10:57 | \n",
+ "
\n",
+ " \n",
+ " 20000261 | \n",
+ " 138493 | \n",
+ " 70286 | \n",
+ " 5.0 | \n",
+ " 2009-11-13 15:42:24 | \n",
+ "
\n",
+ " \n",
+ " 20000262 | \n",
+ " 138493 | \n",
+ " 71619 | \n",
+ " 2.5 | \n",
+ " 2009-10-17 20:25:36 | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " userId | \n",
+ " movieId | \n",
+ " rating | \n",
+ " timestamp | \n",
+ " user | \n",
+ " movie | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 1 | \n",
+ " 2 | \n",
+ " 3.5 | \n",
+ " 2005-04-02 23:53:47 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 29 | \n",
+ " 3.5 | \n",
+ " 2005-04-02 23:31:16 | \n",
+ " 0 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " 1 | \n",
+ " 32 | \n",
+ " 3.5 | \n",
+ " 2005-04-02 23:33:39 | \n",
+ " 0 | \n",
+ " 2 | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " 1 | \n",
+ " 47 | \n",
+ " 3.5 | \n",
+ " 2005-04-02 23:32:07 | \n",
+ " 0 | \n",
+ " 3 | \n",
+ "
\n",
+ " \n",
+ " 4 | \n",
+ " 1 | \n",
+ " 50 | \n",
+ " 3.5 | \n",
+ " 2005-04-02 23:29:40 | \n",
+ " 0 | \n",
+ " 4 | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " rating | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " count | \n",
+ " 20000263.00 | \n",
+ "
\n",
+ " \n",
+ " mean | \n",
+ " 3.53 | \n",
+ "
\n",
+ " \n",
+ " std | \n",
+ " 1.05 | \n",
+ "
\n",
+ " \n",
+ " min | \n",
+ " 0.50 | \n",
+ "
\n",
+ " \n",
+ " 25% | \n",
+ " 3.00 | \n",
+ "
\n",
+ " \n",
+ " 50% | \n",
+ " 3.50 | \n",
+ "
\n",
+ " \n",
+ " 75% | \n",
+ " 4.00 | \n",
+ "
\n",
+ " \n",
+ " max | \n",
+ " 5.00 | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " 0 | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " count | \n",
+ " 20000263.0 | \n",
+ "
\n",
+ " \n",
+ " mean | \n",
+ " 0.7 | \n",
+ "
\n",
+ " \n",
+ " std | \n",
+ " 0.2 | \n",
+ "
\n",
+ " \n",
+ " min | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " 25% | \n",
+ " 0.6 | \n",
+ "
\n",
+ " \n",
+ " 50% | \n",
+ " 0.7 | \n",
+ "
\n",
+ " \n",
+ " 75% | \n",
+ " 0.8 | \n",
+ "
\n",
+ " \n",
+ " max | \n",
+ " 1.0 | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " film | \n",
+ " toy story | \n",
+ " jumanji | \n",
+ " grumpier old men | \n",
+ " waiting to exhale | \n",
+ " father of the bride part ii | \n",
+ " heat | \n",
+ " sabrina | \n",
+ " tom and huck | \n",
+ " sudden death | \n",
+ " goldeneye | \n",
+ " ... | \n",
+ " what men talk about | \n",
+ " three quarter moon | \n",
+ " ants in the pants | \n",
+ " werner - gekotzt wird später | \n",
+ " brother bear 2 | \n",
+ " no more school | \n",
+ " forklift driver klaus: the first day on the job | \n",
+ " kein bund für's leben | \n",
+ " feuer, eis & dosenbier | \n",
+ " the pirates | \n",
+ "
\n",
+ " \n",
+ " film | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " toy story | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " ... | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ "
\n",
+ " \n",
+ " jumanji | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " ... | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ "
\n",
+ " \n",
+ " grumpier old men | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " ... | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " waiting to exhale | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " ... | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ " father of the bride part ii | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " ... | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ " 0.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 1.0 | \n",
+ " 0.0 | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " film | \n",
+ " genre | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " The Pirates | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " Toy Story 3 | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " Descent: Part 2, The | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " The Black Rose | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 4 | \n",
+ " Young Winston | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 5 | \n",
+ " St Trinian'S 2: The Legend Of Fritton'S Gold | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 6 | \n",
+ " Sky Crawlers, The (Sukai Kurora) | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 7 | \n",
+ " Shrek Forever After (A.K.A. Shrek: The Final C... | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 8 | \n",
+ " Percy Jackson & The Olympians: The Lightning T... | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 9 | \n",
+ " B.N.B. (Bunty Aur Babli) | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 10 | \n",
+ " Agora | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 11 | \n",
+ " When Dinosaurs Ruled The Earth | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 12 | \n",
+ " North Face (Nordwand) | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 13 | \n",
+ " Men Who Tread On The Tiger'S Tail, The (Tora N... | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 14 | \n",
+ " How To Train Your Dragon | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " id | \n",
+ " film | \n",
+ " genre | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 1 | \n",
+ " toy story | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " film | \n",
+ " genre | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " The Pirates | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " Toy Story 3 | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " Descent: Part 2, The | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " The Black Rose | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 4 | \n",
+ " Young Winston | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 5 | \n",
+ " St Trinian'S 2: The Legend Of Fritton'S Gold | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 6 | \n",
+ " Sky Crawlers, The (Sukai Kurora) | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 7 | \n",
+ " Shrek Forever After (A.K.A. Shrek: The Final C... | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 8 | \n",
+ " Percy Jackson & The Olympians: The Lightning T... | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 9 | \n",
+ " B.N.B. (Bunty Aur Babli) | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 10 | \n",
+ " Agora | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 11 | \n",
+ " When Dinosaurs Ruled The Earth | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 12 | \n",
+ " North Face (Nordwand) | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 13 | \n",
+ " Men Who Tread On The Tiger'S Tail, The (Tora N... | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ " 14 | \n",
+ " How To Train Your Dragon | \n",
+ " Adventure | \n",
+ "
\n",
+ " \n",
+ "
\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