{
"cells": [
{
"cell_type": "markdown",
"id": "ed18addc",
"metadata": {},
"source": [
"# Lesson 23: recommendation systems demonstration\n",
"\n",
"\n",
"\n",
"This notebook demonstrates key concepts for recommendation systems\n",
"\n",
"\n",
"\n",
"1. Notebook set up\n",
"\n",
"\n",
"\n",
"2. Collaborative filtering\n",
"\n",
" - Memory based\n",
"\n",
" - Model based\n",
"\n",
"\n",
"\n",
"3. Content-based filtering\n",
"\n",
"4. Hybrid filtering\n",
"\n",
"5. Evaluation: GetTopN and hit rate\n",
"\n",
"6. Export for web app deployment\n",
"\n",
"7. Challenges: cold start, implicit vs explicit feedback\n",
"\n",
"8. More advanced techniques\n",
"\n",
"\n",
"\n",
"## 1. Notebook set up\n",
"\n",
"\n",
"\n",
"### Imports"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "97e3fe09",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import pickle\n",
"\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"from sklearn.decomposition import TruncatedSVD\n",
"from sklearn.metrics.pairwise import cosine_similarity"
]
},
{
"cell_type": "markdown",
"id": "5b162041",
"metadata": {},
"source": [
"### Dataset\n",
"\n",
"Load animes & ratings data."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "3dd318b6",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" anime_id | \n",
" name | \n",
" genre | \n",
" type | \n",
" episodes | \n",
" rating | \n",
" members | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" 32281 | \n",
" Kimi no Na wa. | \n",
" Drama, Romance, School, Supernatural | \n",
" Movie | \n",
" 1 | \n",
" 9.37 | \n",
" 200630 | \n",
"
\n",
" \n",
" | 1 | \n",
" 5114 | \n",
" Fullmetal Alchemist: Brotherhood | \n",
" Action, Adventure, Drama, Fantasy, Magic, Mili... | \n",
" TV | \n",
" 64 | \n",
" 9.26 | \n",
" 793665 | \n",
"
\n",
" \n",
" | 2 | \n",
" 28977 | \n",
" Gintama° | \n",
" Action, Comedy, Historical, Parody, Samurai, S... | \n",
" TV | \n",
" 51 | \n",
" 9.25 | \n",
" 114262 | \n",
"
\n",
" \n",
" | 3 | \n",
" 9253 | \n",
" Steins;Gate | \n",
" Sci-Fi, Thriller | \n",
" TV | \n",
" 24 | \n",
" 9.17 | \n",
" 673572 | \n",
"
\n",
" \n",
" | 4 | \n",
" 9969 | \n",
" Gintama' | \n",
" Action, Comedy, Historical, Parody, Samurai, S... | \n",
" TV | \n",
" 51 | \n",
" 9.16 | \n",
" 151266 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" anime_id name \\\n",
"0 32281 Kimi no Na wa. \n",
"1 5114 Fullmetal Alchemist: Brotherhood \n",
"2 28977 Gintama° \n",
"3 9253 Steins;Gate \n",
"4 9969 Gintama' \n",
"\n",
" genre type episodes rating \\\n",
"0 Drama, Romance, School, Supernatural Movie 1 9.37 \n",
"1 Action, Adventure, Drama, Fantasy, Magic, Mili... TV 64 9.26 \n",
"2 Action, Comedy, Historical, Parody, Samurai, S... TV 51 9.25 \n",
"3 Sci-Fi, Thriller TV 24 9.17 \n",
"4 Action, Comedy, Historical, Parody, Samurai, S... TV 51 9.16 \n",
"\n",
" members \n",
"0 200630 \n",
"1 793665 \n",
"2 114262 \n",
"3 673572 \n",
"4 151266 "
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Load anime information from CSV file\n",
"animes = pd.read_csv('https://media.githubusercontent.com/media/gperdrizet/fullstack-2605/refs/heads/main/data/anime.csv')\n",
"animes.head()"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "08dac3b4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"RangeIndex: 12294 entries, 0 to 12293\n",
"Data columns (total 7 columns):\n",
" # Column Non-Null Count Dtype \n",
"--- ------ -------------- ----- \n",
" 0 anime_id 12294 non-null int64 \n",
" 1 name 12294 non-null str \n",
" 2 genre 12232 non-null str \n",
" 3 type 12269 non-null str \n",
" 4 episodes 12294 non-null str \n",
" 5 rating 12064 non-null float64\n",
" 6 members 12294 non-null int64 \n",
"dtypes: float64(1), int64(2), str(4)\n",
"memory usage: 672.5 KB\n"
]
}
],
"source": [
"animes.info()"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "3f7ddb1f",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" user_id | \n",
" anime_id | \n",
" rating | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" 1 | \n",
" 20 | \n",
" -1 | \n",
"
\n",
" \n",
" | 1 | \n",
" 1 | \n",
" 24 | \n",
" -1 | \n",
"
\n",
" \n",
" | 2 | \n",
" 1 | \n",
" 79 | \n",
" -1 | \n",
"
\n",
" \n",
" | 3 | \n",
" 1 | \n",
" 226 | \n",
" -1 | \n",
"
\n",
" \n",
" | 4 | \n",
" 1 | \n",
" 241 | \n",
" -1 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" user_id anime_id rating\n",
"0 1 20 -1\n",
"1 1 24 -1\n",
"2 1 79 -1\n",
"3 1 226 -1\n",
"4 1 241 -1"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Load user ratings from CSV file\n",
"ratings = pd.read_csv('https://media.githubusercontent.com/media/gperdrizet/fullstack-2605/refs/heads/main/data/rating.csv')\n",
"ratings.head()"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "9c706aec",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"RangeIndex: 7813737 entries, 0 to 7813736\n",
"Data columns (total 3 columns):\n",
" # Column Dtype\n",
"--- ------ -----\n",
" 0 user_id int64\n",
" 1 anime_id int64\n",
" 2 rating int64\n",
"dtypes: int64(3)\n",
"memory usage: 178.8 MB\n"
]
}
],
"source": [
"ratings.info()"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "597c6fe7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Index: 50000 entries, 5012406 to 6767779\n",
"Data columns (total 3 columns):\n",
" # Column Non-Null Count Dtype\n",
"--- ------ -------------- -----\n",
" 0 user_id 50000 non-null int64\n",
" 1 anime_id 50000 non-null int64\n",
" 2 rating 50000 non-null int64\n",
"dtypes: int64(3)\n",
"memory usage: 1.5 MB\n"
]
}
],
"source": [
"# Randomly sample 50,000 ratings for faster computation\n",
"sample_ratings = ratings.sample(n=50000, random_state=315)\n",
"sample_ratings.info()"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "6dd75400",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Number of unique users in sample: 26940\n",
"Number of unique animes in sample: 4863\n"
]
}
],
"source": [
"# Count unique users and animes in the sample\n",
"num_users = sample_ratings['user_id'].nunique()\n",
"num_animes = sample_ratings['anime_id'].nunique()\n",
"\n",
"print(f\"Number of unique users in sample: {num_users}\")\n",
"print(f\"Number of unique animes in sample: {num_animes}\")"
]
},
{
"cell_type": "markdown",
"id": "10117302",
"metadata": {},
"source": [
"## 2. Collaborative filtering\n",
"\n",
"\n",
"\n",
"### 2.1. Memory based collaborative filtering"
]
},
{
"cell_type": "markdown",
"id": "a4648ac3",
"metadata": {},
"source": [
"Create a user-item matrix where each row is a user, each column is an anime, and values are ratings."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "39baff1e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"User-Item Matrix shape: (26940, 4863)\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | anime_id | \n",
" 1 | \n",
" 5 | \n",
" 6 | \n",
" 7 | \n",
" 8 | \n",
" 15 | \n",
" 16 | \n",
" 17 | \n",
" 18 | \n",
" 19 | \n",
" ... | \n",
" 33524 | \n",
" 33558 | \n",
" 33569 | \n",
" 33606 | \n",
" 33740 | \n",
" 33741 | \n",
" 33798 | \n",
" 33964 | \n",
" 34103 | \n",
" 34240 | \n",
"
\n",
" \n",
" | user_id | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 3 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 4 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 5 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 7 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 11 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
"
\n",
"
5 rows × 4863 columns
\n",
"
"
],
"text/plain": [
"anime_id 1 5 6 7 8 15 16 17 18 \\\n",
"user_id \n",
"3 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"4 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"5 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"7 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"11 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"\n",
"anime_id 19 ... 33524 33558 33569 33606 33740 33741 33798 33964 \\\n",
"user_id ... \n",
"3 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"4 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"5 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"7 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"11 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"\n",
"anime_id 34103 34240 \n",
"user_id \n",
"3 0.0 0.0 \n",
"4 0.0 0.0 \n",
"5 0.0 0.0 \n",
"7 0.0 0.0 \n",
"11 0.0 0.0 \n",
"\n",
"[5 rows x 4863 columns]"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Create a user-item matrix using pivot table\n",
"user_item_matrix = sample_ratings.pivot_table(\n",
" index='user_id', # Users as rows\n",
" columns='anime_id', # Animes as columns\n",
" values='rating' # Ratings as values\n",
")\n",
"\n",
"# Fill missing values (unrated animes) with 0\n",
"user_item_filled = user_item_matrix.fillna(0)\n",
"\n",
"print('User-Item Matrix shape:', user_item_filled.shape)\n",
"user_item_filled.head()"
]
},
{
"cell_type": "markdown",
"id": "86d3392e",
"metadata": {},
"source": [
"Compute cosine similarity between all pairs of animes to find which animes have similar rating patterns across users."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "79630a7f",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | user_id | \n",
" 3 | \n",
" 4 | \n",
" 5 | \n",
" 7 | \n",
" 11 | \n",
" 13 | \n",
" 14 | \n",
" 17 | \n",
" 21 | \n",
" 26 | \n",
" ... | \n",
" 73490 | \n",
" 73491 | \n",
" 73495 | \n",
" 73499 | \n",
" 73500 | \n",
" 73501 | \n",
" 73503 | \n",
" 73507 | \n",
" 73510 | \n",
" 73515 | \n",
"
\n",
" \n",
" | anime_id | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 1 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 5 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 6 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 7 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 8 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
"
\n",
" \n",
" | 33741 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 33798 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 33964 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 34103 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 34240 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
"
\n",
"
4863 rows × 26940 columns
\n",
"
"
],
"text/plain": [
"user_id 3 4 5 7 11 13 14 17 21 \\\n",
"anime_id \n",
"1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"5 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"6 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"7 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"8 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"... ... ... ... ... ... ... ... ... ... \n",
"33741 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"33798 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"33964 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"34103 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"34240 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"\n",
"user_id 26 ... 73490 73491 73495 73499 73500 73501 73503 73507 \\\n",
"anime_id ... \n",
"1 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"5 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"6 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"7 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"8 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"... ... ... ... ... ... ... ... ... ... ... \n",
"33741 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"33798 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"33964 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"34103 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"34240 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"\n",
"user_id 73510 73515 \n",
"anime_id \n",
"1 0.0 0.0 \n",
"5 0.0 0.0 \n",
"6 0.0 0.0 \n",
"7 0.0 0.0 \n",
"8 0.0 0.0 \n",
"... ... ... \n",
"33741 0.0 0.0 \n",
"33798 0.0 0.0 \n",
"33964 0.0 0.0 \n",
"34103 0.0 0.0 \n",
"34240 0.0 0.0 \n",
"\n",
"[4863 rows x 26940 columns]"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"user_item_filled.T"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "252d8945",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Item similarity matrix shape: (4863, 4863)\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | anime_id | \n",
" 1 | \n",
" 5 | \n",
" 6 | \n",
" 7 | \n",
" 8 | \n",
" 15 | \n",
" 16 | \n",
" 17 | \n",
" 18 | \n",
" 19 | \n",
" ... | \n",
" 33524 | \n",
" 33558 | \n",
" 33569 | \n",
" 33606 | \n",
" 33740 | \n",
" 33741 | \n",
" 33798 | \n",
" 33964 | \n",
" 34103 | \n",
" 34240 | \n",
"
\n",
" \n",
" | anime_id | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 1 | \n",
" 1.000000 | \n",
" 0.026386 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 5 | \n",
" 0.026386 | \n",
" 1.000000 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 6 | \n",
" 0.000000 | \n",
" 0.000000 | \n",
" 1.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 7 | \n",
" 0.000000 | \n",
" 0.000000 | \n",
" 0.0 | \n",
" 1.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 8 | \n",
" 0.000000 | \n",
" 0.000000 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 1.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" ... | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
" 0.0 | \n",
"
\n",
" \n",
"
\n",
"
5 rows × 4863 columns
\n",
"
"
],
"text/plain": [
"anime_id 1 5 6 7 8 15 16 17 18 \\\n",
"anime_id \n",
"1 1.000000 0.026386 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"5 0.026386 1.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"6 0.000000 0.000000 1.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"7 0.000000 0.000000 0.0 1.0 0.0 0.0 0.0 0.0 0.0 \n",
"8 0.000000 0.000000 0.0 0.0 1.0 0.0 0.0 0.0 0.0 \n",
"\n",
"anime_id 19 ... 33524 33558 33569 33606 33740 33741 33798 33964 \\\n",
"anime_id ... \n",
"1 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"5 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"6 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"7 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"8 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n",
"\n",
"anime_id 34103 34240 \n",
"anime_id \n",
"1 0.0 0.0 \n",
"5 0.0 0.0 \n",
"6 0.0 0.0 \n",
"7 0.0 0.0 \n",
"8 0.0 0.0 \n",
"\n",
"[5 rows x 4863 columns]"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Compute cosine similarity between animes (transpose to compare columns)\n",
"item_similarity = cosine_similarity(user_item_filled.T)\n",
"\n",
"# Convert to DataFrame with anime IDs as row and column labels\n",
"item_similarity_df = pd.DataFrame(\n",
" item_similarity,\n",
" index=user_item_matrix.columns,\n",
" columns=user_item_matrix.columns\n",
")\n",
"\n",
"print('Item similarity matrix shape:', item_similarity_df.shape)\n",
"item_similarity_df.head()"
]
},
{
"cell_type": "markdown",
"id": "7eeff8cf",
"metadata": {},
"source": [
"Create helper functions to convert between anime IDs and names for more readable output."
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "430a9e69",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Anime ID 1: Cowboy Bebop\n"
]
}
],
"source": [
"def get_anime_name(anime_id):\n",
" \"\"\"Get anime name from ID\"\"\"\n",
"\n",
" result = animes[animes['anime_id'] == anime_id]['name']\n",
" return result.values[0] if len(result) > 0 else f'Unknown (ID: {anime_id})'\n",
"\n",
"def get_anime_id(anime_name):\n",
" \"\"\"Get anime ID from name\"\"\"\n",
"\n",
" result = animes[animes['name'] == anime_name]['anime_id']\n",
" return result.values[0] if len(result) > 0 else None\n",
"\n",
"# Test the helper function\n",
"print(f\"Anime ID 1: {get_anime_name(1)}\")"
]
},
{
"cell_type": "markdown",
"id": "920fabc1",
"metadata": {},
"source": [
"Demonstrate memory-based collaborative filtering by finding the top 5 animes most similar to a target anime based on user rating patterns."
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "41195f2e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Top 5 animes similar to \"Cowboy Bebop\":\n",
"\n",
"Koi☆Sento: 0.1218\n",
"Hikyou Tanken Fam & Ihrlie: 0.1218\n",
"Houkago 2: Saiyuri: 0.1218\n",
"Lupin III: Ikiteita Majutsushi: 0.1206\n",
"Gyakuten Majo Saiban: Chijo na Majo ni Sabakarechau The Animation: 0.1200\n"
]
}
],
"source": [
"# Select target anime\n",
"anime_id = 1\n",
"\n",
"# Get similarity scores and sort in descending order\n",
"similar_animes = item_similarity_df[anime_id].sort_values(ascending=False)\n",
"\n",
"print(f'Top 5 animes similar to \"{get_anime_name(anime_id)}\":')\n",
"print()\n",
"\n",
"# Display top 5 (skip first one since it's the anime itself)\n",
"for anime_id_similar, score in similar_animes[1:6].items():\n",
" print(f'{get_anime_name(anime_id_similar)}: {score:.4f}')"
]
},
{
"cell_type": "markdown",
"id": "c1e0143d",
"metadata": {},
"source": [
"### 2.2. Model based collaborative filtering"
]
},
{
"cell_type": "markdown",
"id": "197457e8",
"metadata": {},
"source": [
"Use matrix factorization (SVD) to reduce dimensionality and fill in missing ratings by learning latent features of users and animes."
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "d0cfc633",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Predicted ratings matrix shape: (26940, 4863)\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | anime_id | \n",
" 1 | \n",
" 5 | \n",
" 6 | \n",
" 7 | \n",
" 8 | \n",
" 15 | \n",
" 16 | \n",
" 17 | \n",
" 18 | \n",
" 19 | \n",
" ... | \n",
" 33524 | \n",
" 33558 | \n",
" 33569 | \n",
" 33606 | \n",
" 33740 | \n",
" 33741 | \n",
" 33798 | \n",
" 33964 | \n",
" 34103 | \n",
" 34240 | \n",
"
\n",
" \n",
" | user_id | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 3 | \n",
" -0.123558 | \n",
" 0.001347 | \n",
" 0.015531 | \n",
" -0.001360 | \n",
" 1.970482e-05 | \n",
" -0.000653 | \n",
" 0.006206 | \n",
" 6.840352e-04 | \n",
" -6.027962e-05 | \n",
" -0.007176 | \n",
" ... | \n",
" -0.006508 | \n",
" 0.000537 | \n",
" -0.001496 | \n",
" 0.000805 | \n",
" -1.090123e-04 | \n",
" -5.796410e-05 | \n",
" 0.000618 | \n",
" 6.392568e-05 | \n",
" 0.000047 | \n",
" 0.008004 | \n",
"
\n",
" \n",
" | 4 | \n",
" -0.000143 | \n",
" -0.000030 | \n",
" 0.000164 | \n",
" -0.000010 | \n",
" -2.217778e-07 | \n",
" -0.000014 | \n",
" -0.000014 | \n",
" -8.815549e-07 | \n",
" -5.654995e-07 | \n",
" -0.000016 | \n",
" ... | \n",
" -0.000010 | \n",
" 0.000002 | \n",
" -0.000002 | \n",
" -0.000010 | \n",
" -3.455666e-07 | \n",
" 2.782329e-08 | \n",
" -0.000001 | \n",
" -2.231423e-08 | \n",
" -0.000003 | \n",
" 0.000010 | \n",
"
\n",
" \n",
" | 5 | \n",
" -0.002929 | \n",
" -0.000565 | \n",
" -0.000439 | \n",
" 0.000008 | \n",
" -9.950065e-07 | \n",
" 0.000218 | \n",
" 0.000513 | \n",
" 3.954197e-05 | \n",
" 2.285818e-05 | \n",
" -0.000223 | \n",
" ... | \n",
" -0.000115 | \n",
" 0.000107 | \n",
" 0.000014 | \n",
" 0.000066 | \n",
" -2.426508e-06 | \n",
" -6.658181e-07 | \n",
" 0.000047 | \n",
" 9.848932e-06 | \n",
" -0.000083 | \n",
" 0.000776 | \n",
"
\n",
" \n",
" | 7 | \n",
" -0.005738 | \n",
" -0.000151 | \n",
" -0.000507 | \n",
" 0.000005 | \n",
" 9.891335e-07 | \n",
" 0.000087 | \n",
" 0.000056 | \n",
" 8.833031e-06 | \n",
" 5.095776e-06 | \n",
" 0.000141 | \n",
" ... | \n",
" -0.000017 | \n",
" 0.000010 | \n",
" 0.000010 | \n",
" -0.000019 | \n",
" -1.102061e-06 | \n",
" -1.011350e-07 | \n",
" 0.000004 | \n",
" 3.406460e-07 | \n",
" 0.000005 | \n",
" -0.000029 | \n",
"
\n",
" \n",
" | 11 | \n",
" 0.292969 | \n",
" 0.043470 | \n",
" 0.075774 | \n",
" -0.001745 | \n",
" -4.406893e-05 | \n",
" 0.001446 | \n",
" -0.012093 | \n",
" -6.346332e-04 | \n",
" 1.169022e-03 | \n",
" 0.003752 | \n",
" ... | \n",
" -0.006507 | \n",
" 0.004725 | \n",
" -0.005896 | \n",
" 0.002050 | \n",
" -1.419158e-05 | \n",
" -9.158018e-05 | \n",
" -0.001123 | \n",
" 6.495072e-04 | \n",
" 0.000187 | \n",
" -0.022802 | \n",
"
\n",
" \n",
"
\n",
"
5 rows × 4863 columns
\n",
"
"
],
"text/plain": [
"anime_id 1 5 6 7 8 15 \\\n",
"user_id \n",
"3 -0.123558 0.001347 0.015531 -0.001360 1.970482e-05 -0.000653 \n",
"4 -0.000143 -0.000030 0.000164 -0.000010 -2.217778e-07 -0.000014 \n",
"5 -0.002929 -0.000565 -0.000439 0.000008 -9.950065e-07 0.000218 \n",
"7 -0.005738 -0.000151 -0.000507 0.000005 9.891335e-07 0.000087 \n",
"11 0.292969 0.043470 0.075774 -0.001745 -4.406893e-05 0.001446 \n",
"\n",
"anime_id 16 17 18 19 ... 33524 \\\n",
"user_id ... \n",
"3 0.006206 6.840352e-04 -6.027962e-05 -0.007176 ... -0.006508 \n",
"4 -0.000014 -8.815549e-07 -5.654995e-07 -0.000016 ... -0.000010 \n",
"5 0.000513 3.954197e-05 2.285818e-05 -0.000223 ... -0.000115 \n",
"7 0.000056 8.833031e-06 5.095776e-06 0.000141 ... -0.000017 \n",
"11 -0.012093 -6.346332e-04 1.169022e-03 0.003752 ... -0.006507 \n",
"\n",
"anime_id 33558 33569 33606 33740 33741 33798 \\\n",
"user_id \n",
"3 0.000537 -0.001496 0.000805 -1.090123e-04 -5.796410e-05 0.000618 \n",
"4 0.000002 -0.000002 -0.000010 -3.455666e-07 2.782329e-08 -0.000001 \n",
"5 0.000107 0.000014 0.000066 -2.426508e-06 -6.658181e-07 0.000047 \n",
"7 0.000010 0.000010 -0.000019 -1.102061e-06 -1.011350e-07 0.000004 \n",
"11 0.004725 -0.005896 0.002050 -1.419158e-05 -9.158018e-05 -0.001123 \n",
"\n",
"anime_id 33964 34103 34240 \n",
"user_id \n",
"3 6.392568e-05 0.000047 0.008004 \n",
"4 -2.231423e-08 -0.000003 0.000010 \n",
"5 9.848932e-06 -0.000083 0.000776 \n",
"7 3.406460e-07 0.000005 -0.000029 \n",
"11 6.495072e-04 0.000187 -0.022802 \n",
"\n",
"[5 rows x 4863 columns]"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Create SVD model with 50 latent features\n",
"svd_model = TruncatedSVD(n_components=50, random_state=315)\n",
"\n",
"# Fit model and transform user-item matrix to user features\n",
"user_features = svd_model.fit_transform(user_item_filled)\n",
"\n",
"# Reconstruct ratings matrix by multiplying user and item features\n",
"predicted_ratings = np.dot(user_features, svd_model.components_)\n",
"\n",
"# Convert back to DataFrame with original indices\n",
"predicted_ratings_df = pd.DataFrame(\n",
" predicted_ratings,\n",
" index=user_item_matrix.index,\n",
" columns=user_item_matrix.columns\n",
")\n",
"\n",
"print('Predicted ratings matrix shape:', predicted_ratings_df.shape)\n",
"predicted_ratings_df.head()"
]
},
{
"cell_type": "markdown",
"id": "fc4e48a1",
"metadata": {},
"source": [
"Demonstrate model-based collaborative filtering by recommending unwatched animes to a user based on predicted ratings from SVD."
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "d097343c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Top 5 recommended animes for user 3:\n",
"anime_id\n",
"18679 0.843389\n",
"2236 0.649259\n",
"223 0.471859\n",
"9989 0.449235\n",
"6880 0.438066\n",
"Name: 3, dtype: float64\n"
]
}
],
"source": [
"# Select first user from the matrix\n",
"user_id = user_item_matrix.index[0]\n",
"\n",
"# Get predicted ratings for this user\n",
"user_predictions = predicted_ratings_df.loc[user_id]\n",
"\n",
"# Find animes the user hasn't rated (missing values in original matrix)\n",
"unrated_animes = user_item_matrix.loc[user_id][user_item_matrix.loc[user_id].isna()]\n",
"\n",
"# Get predictions for unrated animes and sort by predicted rating\n",
"recommendations = user_predictions[unrated_animes.index].sort_values(ascending=False)\n",
"\n",
"print(f'Top 5 recommended animes for user {user_id}:')\n",
"print(recommendations.head())"
]
},
{
"cell_type": "markdown",
"id": "3550de28",
"metadata": {},
"source": [
"## 3. Content-based filtering"
]
},
{
"cell_type": "markdown",
"id": "e2bfac3e",
"metadata": {},
"source": [
"Examine the content features (genre, type) available for each anime to use in content-based filtering."
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "f17f98a0",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" anime_id | \n",
" name | \n",
" genre | \n",
" type | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" 32281 | \n",
" Kimi no Na wa. | \n",
" Drama, Romance, School, Supernatural | \n",
" Movie | \n",
"
\n",
" \n",
" | 1 | \n",
" 5114 | \n",
" Fullmetal Alchemist: Brotherhood | \n",
" Action, Adventure, Drama, Fantasy, Magic, Mili... | \n",
" TV | \n",
"
\n",
" \n",
" | 2 | \n",
" 28977 | \n",
" Gintama° | \n",
" Action, Comedy, Historical, Parody, Samurai, S... | \n",
" TV | \n",
"
\n",
" \n",
" | 3 | \n",
" 9253 | \n",
" Steins;Gate | \n",
" Sci-Fi, Thriller | \n",
" TV | \n",
"
\n",
" \n",
" | 4 | \n",
" 9969 | \n",
" Gintama' | \n",
" Action, Comedy, Historical, Parody, Samurai, S... | \n",
" TV | \n",
"
\n",
" \n",
" | 5 | \n",
" 32935 | \n",
" Haikyuu!!: Karasuno Koukou VS Shiratorizawa Ga... | \n",
" Comedy, Drama, School, Shounen, Sports | \n",
" TV | \n",
"
\n",
" \n",
" | 6 | \n",
" 11061 | \n",
" Hunter x Hunter (2011) | \n",
" Action, Adventure, Shounen, Super Power | \n",
" TV | \n",
"
\n",
" \n",
" | 7 | \n",
" 820 | \n",
" Ginga Eiyuu Densetsu | \n",
" Drama, Military, Sci-Fi, Space | \n",
" OVA | \n",
"
\n",
" \n",
" | 8 | \n",
" 15335 | \n",
" Gintama Movie: Kanketsu-hen - Yorozuya yo Eien... | \n",
" Action, Comedy, Historical, Parody, Samurai, S... | \n",
" Movie | \n",
"
\n",
" \n",
" | 9 | \n",
" 15417 | \n",
" Gintama': Enchousen | \n",
" Action, Comedy, Historical, Parody, Samurai, S... | \n",
" TV | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" anime_id name \\\n",
"0 32281 Kimi no Na wa. \n",
"1 5114 Fullmetal Alchemist: Brotherhood \n",
"2 28977 Gintama° \n",
"3 9253 Steins;Gate \n",
"4 9969 Gintama' \n",
"5 32935 Haikyuu!!: Karasuno Koukou VS Shiratorizawa Ga... \n",
"6 11061 Hunter x Hunter (2011) \n",
"7 820 Ginga Eiyuu Densetsu \n",
"8 15335 Gintama Movie: Kanketsu-hen - Yorozuya yo Eien... \n",
"9 15417 Gintama': Enchousen \n",
"\n",
" genre type \n",
"0 Drama, Romance, School, Supernatural Movie \n",
"1 Action, Adventure, Drama, Fantasy, Magic, Mili... TV \n",
"2 Action, Comedy, Historical, Parody, Samurai, S... TV \n",
"3 Sci-Fi, Thriller TV \n",
"4 Action, Comedy, Historical, Parody, Samurai, S... TV \n",
"5 Comedy, Drama, School, Shounen, Sports TV \n",
"6 Action, Adventure, Shounen, Super Power TV \n",
"7 Drama, Military, Sci-Fi, Space OVA \n",
"8 Action, Comedy, Historical, Parody, Samurai, S... Movie \n",
"9 Action, Comedy, Historical, Parody, Samurai, S... TV "
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Display relevant features for content-based filtering\n",
"animes[['anime_id', 'name', 'genre', 'type']].head(10)"
]
},
{
"cell_type": "markdown",
"id": "9610f83c",
"metadata": {},
"source": [
"Create a function to calculate similarity between animes based on their genres using Jaccard similarity (intersection over union)."
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "f1080bef",
"metadata": {},
"outputs": [],
"source": [
"# Convert genre strings to sets for easier comparison\n",
"animes['genre_set'] = animes['genre'].fillna('').apply(lambda x: set(x.split(', ')))\n",
"\n",
"def genre_similarity(genres1, genres2):\n",
" \"\"\"Calculate Jaccard similarity between two genre sets\"\"\"\n",
"\n",
" # Return 0 if either set is empty\n",
" if len(genres1) == 0 or len(genres2) == 0:\n",
" return 0\n",
"\n",
" # Calculate intersection (common genres) and union (all unique genres)\n",
" intersection = len(genres1.intersection(genres2))\n",
" union = len(genres1.union(genres2))\n",
"\n",
" # Jaccard similarity = intersection / union\n",
" return intersection / union if union > 0 else 0"
]
},
{
"cell_type": "markdown",
"id": "8e1a45fd",
"metadata": {},
"source": [
"Select a target anime to demonstrate content-based filtering using genre similarity."
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "4f4e926c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Target anime: Cowboy Bebop\n",
"Genres: Action, Adventure, Comedy, Drama, Sci-Fi, Space\n"
]
}
],
"source": [
"# Choose anime to find similar content for\n",
"target_anime_id = 1\n",
"target_anime = animes[animes['anime_id'] == target_anime_id].iloc[0]\n",
"target_genres = target_anime['genre_set']\n",
"\n",
"print(f\"Target anime: {target_anime['name']}\")\n",
"print(f\"Genres: {target_anime['genre']}\")"
]
},
{
"cell_type": "markdown",
"id": "0b7ab7a1",
"metadata": {},
"source": [
"Demonstrate content-based filtering by finding animes with the most similar genres to the target anime."
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "5778c65d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Top 5 similar animes based on genre:\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" name | \n",
" genre | \n",
" similarity | \n",
"
\n",
" \n",
" \n",
" \n",
" | 1465 | \n",
" Cowboy Bebop: Yose Atsume Blues | \n",
" Action, Adventure, Comedy, Drama, Sci-Fi, Space | \n",
" 1.000000 | \n",
"
\n",
" \n",
" | 6568 | \n",
" Seihou Tenshi Angel Links | \n",
" Action, Adventure, Comedy, Drama, Romance, Sci... | \n",
" 0.857143 | \n",
"
\n",
" \n",
" | 2735 | \n",
" Uchuu Kaizoku Captain Harlock: Arcadia-gou no ... | \n",
" Action, Adventure, Drama, Sci-Fi, Space | \n",
" 0.833333 | \n",
"
\n",
" \n",
" | 5721 | \n",
" Kaitei Choutokkyuu: Marine Express | \n",
" Action, Adventure, Comedy, Drama, Sci-Fi | \n",
" 0.833333 | \n",
"
\n",
" \n",
" | 1073 | \n",
" Waga Seishun no Arcadia | \n",
" Action, Adventure, Drama, Sci-Fi, Space | \n",
" 0.833333 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" name \\\n",
"1465 Cowboy Bebop: Yose Atsume Blues \n",
"6568 Seihou Tenshi Angel Links \n",
"2735 Uchuu Kaizoku Captain Harlock: Arcadia-gou no ... \n",
"5721 Kaitei Choutokkyuu: Marine Express \n",
"1073 Waga Seishun no Arcadia \n",
"\n",
" genre similarity \n",
"1465 Action, Adventure, Comedy, Drama, Sci-Fi, Space 1.000000 \n",
"6568 Action, Adventure, Comedy, Drama, Romance, Sci... 0.857143 \n",
"2735 Action, Adventure, Drama, Sci-Fi, Space 0.833333 \n",
"5721 Action, Adventure, Comedy, Drama, Sci-Fi 0.833333 \n",
"1073 Action, Adventure, Drama, Sci-Fi, Space 0.833333 "
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Calculate genre similarity for all animes\n",
"animes['similarity'] = animes['genre_set'].apply(\n",
" lambda x: genre_similarity(target_genres, x)\n",
")\n",
"\n",
"# Find top similar animes (excluding the target itself)\n",
"similar_animes = animes[animes['anime_id'] != target_anime_id].sort_values(\n",
" 'similarity', \n",
" ascending=False\n",
")[['name', 'genre', 'similarity']].head(5)\n",
"\n",
"print('Top 5 similar animes based on genre:')\n",
"similar_animes.head()"
]
},
{
"cell_type": "markdown",
"id": "3a7a3dfb",
"metadata": {},
"source": [
"## 4. Hybrid filtering"
]
},
{
"cell_type": "markdown",
"id": "80dd4fc8",
"metadata": {},
"source": [
"Combine collaborative filtering and content-based filtering using a weighted average to leverage both user behavior and content features."
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "a04a94c9",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Top 5 hybrid recommendations for anime_id 1:\n",
"Cowboy Bebop: Yose Atsume Blues: 0.5000\n",
"Seihou Tenshi Angel Links: 0.4294\n",
"Seihou Bukyou Outlaw Star: 0.4167\n",
"Ginga Tetsudou Monogatari: 0.4167\n",
"Waga Seishun no Arcadia: Mugen Kidou SSX: 0.4167\n"
]
}
],
"source": [
"# Get collaborative filtering scores (based on user ratings)\n",
"collab_score = item_similarity_df[target_anime_id]\n",
"\n",
"# Get content-based scores (based on genre similarity)\n",
"content_score = animes.set_index('anime_id')['similarity']\n",
"\n",
"# Find animes that exist in both scoring methods\n",
"common_animes = collab_score.index.intersection(content_score.index)\n",
"\n",
"# Combine scores with equal weights (50% each)\n",
"hybrid_score = (\n",
" 0.5 * collab_score[common_animes] + \n",
" 0.5 * content_score[common_animes]\n",
")\n",
"\n",
"# Sort and get top 5 (excluding the target anime itself)print(hybrid_recommendations)\n",
"hybrid_recommendations = hybrid_score.sort_values(ascending=False)[1:6]\n",
"print(f'Top 5 hybrid recommendations for anime_id {target_anime_id}:')\n",
"\n",
"for rec in hybrid_recommendations.items():\n",
" anime_id_rec, score = rec\n",
" print(f'{get_anime_name(anime_id_rec)}: {score:.4f}')"
]
},
{
"cell_type": "markdown",
"id": "903cf497",
"metadata": {},
"source": [
"## 5. Evaluation\n",
"\n",
"\n",
"\n",
"Evaluating a recommendation system requires simulating the real-world scenario where some ratings are known and others are withheld. A **leave-one-out** strategy holds out one randomly chosen rating per user as the test item and trains the model on the remaining ratings.\n",
"\n",
"\n",
"\n",
"One important detail: only **positively-rated items** (rating ≥ 7 out of 10) should be held out. If we hold out an anime the user rated poorly, finding it in the top-10 is not a success; the model would be recommending something the user disliked. Users with no positive ratings have no test item and are excluded from the evaluation.\n",
"\n",
"\n",
"\n",
"**Note on production practice:** two common refinements are used in rigorous benchmarks. First, a **time-based split** holds out each user's *most recent* positive interaction rather than a random one, preventing the model from using future data to predict the past. Second, many papers use a **sampled ranking** protocol (the model ranks the 1 test item against 100 randomly sampled unrated items) instead of ranking across all items, which is much cheaper to compute at scale.\n",
"\n",
"\n",
"\n",
"### 5.1. Train/test split"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "7094f6cc",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Train ratings: 29,609\n",
"Test ratings: 20,391 (one positively-rated item per user)\n"
]
}
],
"source": [
"# Only hold out positively-rated items; a hit on a disliked anime is not meaningful\n",
"# Note: ratings of -1 in this dataset indicate watched but not rated\n",
"positive_ratings = sample_ratings[sample_ratings['rating'] >= 7]\n",
"held_out = positive_ratings.groupby('user_id').sample(n=1, random_state=315)\n",
"test_ratings = held_out.reset_index(drop=True)\n",
"train_ratings = sample_ratings.drop(index=held_out.index)\n",
"\n",
"print(f'Train ratings: {len(train_ratings):,}')\n",
"print(f'Test ratings: {len(test_ratings):,} (one positively-rated item per user)')"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "5a926037",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Training predictions shape: (16064, 4508)\n"
]
}
],
"source": [
"# Retrain SVD on training data only — never use the held-out ratings during training\n",
"train_matrix = train_ratings.pivot_table(\n",
" index='user_id', columns='anime_id', values='rating'\n",
").fillna(0)\n",
"\n",
"svd_eval = TruncatedSVD(n_components=50, random_state=315)\n",
"train_preds = pd.DataFrame(\n",
" np.dot(svd_eval.fit_transform(train_matrix), svd_eval.components_),\n",
" index=train_matrix.index,\n",
" columns=train_matrix.columns\n",
")\n",
"print(f'Training predictions shape: {train_preds.shape}')"
]
},
{
"cell_type": "markdown",
"id": "2b3fdc91",
"metadata": {},
"source": [
"### 5.2. GetTopN\n",
"\n",
"\n",
"\n",
"`GetTopN` generates the ranked recommendation list for each user: it identifies anime the user has not rated in the training data, ranks them by predicted rating, and returns the top-N. It is the bridge between raw model predictions and a usable list of recommendations."
]
},
{
"cell_type": "code",
"execution_count": 22,
"id": "75159699",
"metadata": {},
"outputs": [],
"source": [
"def get_top_n(predicted_df, train_matrix, n=10):\n",
" '''For each user, return the top-N unrated anime IDs ranked by predicted rating.'''\n",
" top_n = {}\n",
" for user_id in predicted_df.index:\n",
" rated = train_matrix.loc[user_id][train_matrix.loc[user_id] > 0].index\n",
" preds = predicted_df.loc[user_id].drop(index=rated, errors='ignore')\n",
" top_n[user_id] = preds.sort_values(ascending=False).iloc[:n].index.tolist()\n",
" return top_n"
]
},
{
"cell_type": "code",
"execution_count": 23,
"id": "86a5f55f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Generated top-10 lists for 16064 users\n",
"\n",
"Example: top-10 for user 3:\n",
" Fate/Zero 2nd Season\n",
" Byousoku 5 Centimeter\n",
" Guilty Crown\n",
" Chuunibyou demo Koi ga Shitai!\n",
" Danganronpa: Kibou no Gakuen to Zetsubou no Koukousei The Animation\n",
" Fullmetal Alchemist\n",
" Ano Hi Mita Hana no Namae wo Bokutachi wa Mada Shiranai.\n",
" Toki wo Kakeru Shoujo\n",
" Kimi ni Todoke\n",
" Rosario to Vampire\n"
]
}
],
"source": [
"top_n_recs = get_top_n(train_preds, train_matrix, n=10)\n",
"\n",
"example_user = list(top_n_recs.keys())[0]\n",
"print(f'Generated top-10 lists for {len(top_n_recs)} users')\n",
"print(f'\\nExample: top-10 for user {example_user}:')\n",
"for name in [get_anime_name(aid) for aid in top_n_recs[example_user]]:\n",
" print(f' {name}')"
]
},
{
"cell_type": "markdown",
"id": "3bd901d9",
"metadata": {},
"source": [
"### 5.3. Hit rate\n",
"\n",
"\n",
"\n",
"Hit rate measures how often the model's top-N list contains the held-out test item:\n",
"\n",
"\n",
"\n",
"$$\\text{Hit Rate@}N = \\frac{|\\{u : \\text{test item} \\in \\text{top-}N(u)\\}|}{|U|}$$\n",
"\n",
"\n",
"\n",
"Where $U$ is the set of users with a positive test item and $\\text{top-}N(u)$ is the set of recommended items for user $u$. A hit rate of 0.05 at $N = 10$ means 5% of those users had their held-out anime appear in their top-10 list.\n",
"\n",
"\n",
"\n",
"Because we only held out positively-rated items, a hit genuinely means the model surfaced something the user liked. Without this filter, the metric would credit the model for recommending anime the user had already rated poorly.\n",
"\n",
"\n",
"\n",
"Hit rate rewards the model for placing the test item anywhere in the top-N. It is often paired with NDCG (normalised discounted cumulative gain), which additionally rewards higher placements within the list."
]
},
{
"cell_type": "code",
"execution_count": 24,
"id": "6e389116",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Hit rate @ 10: 0.018\n",
"About 1 in 56 users had their held-out anime in the top-10 list\n"
]
}
],
"source": [
"def hit_rate(top_n_dict, test_df):\n",
" '''Fraction of users whose held-out test anime appears in their top-N list.'''\n",
" eligible = test_df[test_df['user_id'].isin(top_n_dict)]\n",
" hits = eligible.apply(lambda row: row['anime_id'] in top_n_dict[row['user_id']], axis=1).sum()\n",
" return hits / len(eligible) if len(eligible) > 0 else 0.0\n",
"\n",
"\n",
"hr = hit_rate(top_n_recs, test_ratings)\n",
"print(f'Hit rate @ 10: {hr:.3f}')\n",
"print(f'About 1 in {1/hr:.0f} users had their held-out anime in the top-10 list')"
]
},
{
"cell_type": "markdown",
"id": "af2e6548",
"metadata": {},
"source": [
"## 6. Export for web app deployment\n",
"\n",
"\n",
"\n",
"The pre-computed similarity matrix and supporting data are saved to a single pickle file. A web app can load this file at startup and serve recommendations without re-running any computation. The file contains everything needed to:\n",
"\n",
"\n",
"\n",
"- Look up an anime by name and return memory-based (item-item similarity) recommendations.\n",
"\n",
"- Return model-based (SVD) recommendations for a given user.\n",
"\n",
"- Return content-based recommendations by genre similarity."
]
},
{
"cell_type": "code",
"execution_count": 25,
"id": "f270a179",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Saved anime_recommender.pkl (2180.3 MB)\n",
"Keys: ['item_similarity_df', 'predicted_ratings_df', 'user_item_matrix', 'animes']\n"
]
}
],
"source": [
"export = {\n",
" 'item_similarity_df': item_similarity_df,\n",
" 'predicted_ratings_df': predicted_ratings_df,\n",
" 'user_item_matrix': user_item_matrix,\n",
" 'animes': animes[['anime_id', 'name', 'genre']],\n",
"}\n",
"\n",
"output_path = 'anime_recommender.pkl'\n",
"\n",
"with open(output_path, 'wb') as f:\n",
" pickle.dump(export, f)\n",
"\n",
"size_mb = os.path.getsize(output_path) / (1024 * 1024)\n",
"print(f'Saved {output_path} ({size_mb:.1f} MB)')\n",
"print(f'Keys: {list(export.keys())}')"
]
},
{
"cell_type": "markdown",
"id": "ee3835a6",
"metadata": {},
"source": [
"## 7. Challenges\n",
"\n",
"\n",
"\n",
"Several fundamental challenges arise when building recommendation systems in practice."
]
},
{
"cell_type": "markdown",
"id": "c473c293",
"metadata": {},
"source": [
"### 7.1. Cold start problem\n",
"\n",
"\n",
"\n",
"The cold start problem occurs when the system has insufficient data to make reliable recommendations.\n",
"\n",
"\n",
"\n",
"**New user cold start**: a first-time user has no rating history. Collaborative filtering cannot compute user similarity or generate predictions.\n",
"\n",
"- Ask users to rate a small set of popular anime during onboarding\n",
"\n",
"- Use demographic information\n",
"\n",
"- Default to popularity-based recommendations until enough history is collected.\n",
"\n",
"\n",
"\n",
"**New item cold start**: a newly added anime has no user ratings and will not appear in collaborative filtering results.\n",
"\n",
"- Use content features (genre, type, studio) to seed similarity via content-based filtering\n",
"\n",
"- Use a hybrid model that blends collaborative and content signals\n",
"\n",
"- Apply knowledge transfer from similar established anime"
]
},
{
"cell_type": "markdown",
"id": "120fb60f",
"metadata": {},
"source": [
"### 7.2. Implicit vs explicit feedback\n",
"\n",
"\n",
"\n",
"**Explicit feedback** is a direct expression of preference (e.g. a 1-10 star rating). It is unambiguous but rare: most users never rate what they consume.\n",
"\n",
"\n",
"\n",
"**Implicit feedback** is collected passively from behaviour: clicks, views, watch time, completion rate, and list additions. It is abundant but noisy; an anime added to a watch-list may never be watched, and one started may have been dropped.\n",
"\n",
"\n",
"\n",
"The key challenge with implicit feedback is the absence of a negative signal. An unobserved (user, anime) pair could mean the user dislikes it, has never been exposed to it, or simply hasn't discovered it yet. Models must handle this uncertainty explicitly.\n",
"\n",
"\n",
"\n",
"| | Explicit | Implicit |\n",
"|---|---|---|\n",
"| Examples | 1-10 star ratings | Views, completions, list adds |\n",
"| Volume | Low | High |\n",
"| Negative signal | Direct (low ratings) | Absent |\n",
"| Noise | Low | High |\n",
"| Common models | SVD, NMF | BPR, WALS, EASE |"
]
},
{
"cell_type": "markdown",
"id": "c1a7bbe3",
"metadata": {},
"source": [
"### 7.3. Other challenges\n",
"\n",
"\n",
"\n",
"- **Scalability**: computing pairwise similarities over millions of users and items is expensive. Production systems use approximate nearest-neighbor (ANN) search and distributed matrix factorization.\n",
"\n",
"- **Filter bubbles and diversity**: accuracy-optimized systems tend to recommend more of what a user has already seen. Real-world systems add diversity, novelty, and serendipity constraints to broaden the recommendation space.\n",
"\n",
"- **Temporal drift**: user preferences and item relevance change over time. Models need regular retraining or online updates to stay current."
]
},
{
"cell_type": "markdown",
"id": "16260219",
"metadata": {},
"source": [
"## 8. More advanced techniques\n",
"\n",
"\n",
"\n",
"The approaches in this notebook are strong baselines, but production recommendation systems typically use more powerful methods:\n",
"\n",
"\n",
"\n",
"- **Alternating Least Squares (ALS)**: a matrix factorization algorithm designed for implicit feedback that scales to millions of users and items. Available in PySpark MLlib and the `implicit` Python library.\n",
"\n",
"- **Weighted Alternating Least Squares (WALS)**: extends matrix factorization to implicit feedback by assigning confidence weights to all (user, item) pairs. Unobserved pairs get a low but non-zero weight rather than being ignored; observed interactions get a higher weight proportional to frequency.\n",
"\n",
"- **Bayesian Personalized Ranking (BPR)**: optimizes directly for ranking rather than rating prediction, using pairwise comparisons between observed and unobserved items.\n",
"\n",
"- **EASE (Embarrassingly Shallow Autoencoders)**: learns a sparse item-item weight matrix from the interaction data using a closed-form solution, with no latent factors or neural networks. Simple but surprisingly competitive on standard benchmarks.\n",
"\n",
"- **Factorization Machines (FM)**: generalizes matrix factorization to arbitrary feature interactions, making it straightforward to incorporate user and item side information (demographics, genres, context).\n",
"\n",
"- **Neural Collaborative Filtering (NCF)**: replaces the dot product with a neural network, allowing the model to learn non-linear user-item interactions.\n",
"\n",
"- **Sequence-aware models** (SASRec, BERT4Rec): model the temporal order of interactions, capturing session-level intent and recent context.\n",
"\n",
"- **Graph Neural Networks** (LightGCN, NGCF): represent users and items as nodes in an interaction graph and propagate signals through multi-hop connections to capture higher-order collaborative patterns."
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv (3.12.3)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}