{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.7.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":30203,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# **Prac 08 - Collaborative Filtering with FastAI - By Samuel Barrett**\n# Competition - Anime Recommendation System by TAH ","metadata":{}},{"cell_type":"markdown","source":"**Based on**\n> *This is chapter 8 of the book [Practical Deep Learning for Coders](https://www.amazon.com/Deep-Learning-Coders-fastai-PyTorch/dp/1492045527), provided courtesy of O'Reilly Media. The full book is [available as Jupyter Notebooks](https://github.com/fastai/fastbook/). A free course that covers the book is [available here](https://course.fast.ai/).*\n>","metadata":{}},{"cell_type":"markdown","source":"## Create the dataframe and explore the data","metadata":{}},{"cell_type":"code","source":"# imports\nfrom fastai.collab import *\nfrom fastai.tabular.all import *\nimport pandas as pd\nfrom pathlib import Path\nset_seed(42)","metadata":{"execution":{"iopub.status.busy":"2026-08-02T02:21:26.042568Z","iopub.execute_input":"2026-08-02T02:21:26.042967Z","iopub.status.idle":"2026-08-02T02:21:26.050067Z","shell.execute_reply.started":"2026-08-02T02:21:26.042941Z","shell.execute_reply":"2026-08-02T02:21:26.049200Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# create paths\nratings_path = Path('/kaggle/input/competitions/anime-recommendation-system-by-tah/rating_complete.csv')\ndetails_path = Path('/kaggle/input/competitions/anime-recommendation-system-by-tah/anime.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T02:21:26.051301Z","iopub.execute_input":"2026-08-02T02:21:26.051761Z","iopub.status.idle":"2026-08-02T02:21:26.060913Z","shell.execute_reply.started":"2026-08-02T02:21:26.051725Z","shell.execute_reply":"2026-08-02T02:21:26.060075Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# create ratings df\nratings = pd.read_csv(ratings_path)\nratings.head()","metadata":{"execution":{"iopub.status.busy":"2026-08-02T02:21:26.061801Z","iopub.execute_input":"2026-08-02T02:21:26.062213Z","iopub.status.idle":"2026-08-02T02:21:35.989008Z","shell.execute_reply.started":"2026-08-02T02:21:26.062189Z","shell.execute_reply":"2026-08-02T02:21:35.987927Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# create movies df\nmovies = pd.read_csv(details_path)\nmovies.head()","metadata":{"execution":{"iopub.status.busy":"2026-08-02T02:21:39.064851Z","iopub.execute_input":"2026-08-02T02:21:39.066015Z","iopub.status.idle":"2026-08-02T02:21:39.231457Z","shell.execute_reply.started":"2026-08-02T02:21:39.065980Z","shell.execute_reply":"2026-08-02T02:21:39.230545Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# drop the columns not needed\nmovies = movies[['MAL_ID', 'English name']]\nmovies.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T02:21:39.251246Z","iopub.execute_input":"2026-08-02T02:21:39.251578Z","iopub.status.idle":"2026-08-02T02:21:39.261839Z","shell.execute_reply.started":"2026-08-02T02:21:39.251548Z","shell.execute_reply":"2026-08-02T02:21:39.260704Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# merge the dfs\nratings = pd.merge(ratings, movies, left_on='anime_id', right_on='MAL_ID')\nratings.head()","metadata":{"execution":{"iopub.status.busy":"2026-08-02T02:21:39.444624Z","iopub.execute_input":"2026-08-02T02:21:39.445505Z","iopub.status.idle":"2026-08-02T02:21:47.765382Z","shell.execute_reply.started":"2026-08-02T02:21:39.445446Z","shell.execute_reply":"2026-08-02T02:21:47.764451Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# drop unecessary columns\nratings = ratings.drop(columns=['MAL_ID'])\nratings.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T02:21:47.767598Z","iopub.execute_input":"2026-08-02T02:21:47.768137Z","iopub.status.idle":"2026-08-02T02:21:50.100640Z","shell.execute_reply.started":"2026-08-02T02:21:47.768097Z","shell.execute_reply":"2026-08-02T02:21:50.099698Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# check for empty rows\nratings.isnull().sum()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T02:21:50.102050Z","iopub.execute_input":"2026-08-02T02:21:50.102434Z","iopub.status.idle":"2026-08-02T02:21:52.751066Z","shell.execute_reply.started":"2026-08-02T02:21:50.102396Z","shell.execute_reply":"2026-08-02T02:21:52.750087Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# check shape\nratings.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T02:21:52.752919Z","iopub.execute_input":"2026-08-02T02:21:52.754070Z","iopub.status.idle":"2026-08-02T02:21:52.759627Z","shell.execute_reply.started":"2026-08-02T02:21:52.754030Z","shell.execute_reply":"2026-08-02T02:21:52.758732Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Data notes: This dataset is huge, I will use some AI recommended methods to reduce the size by filtering out Anime movies with less than 5 ratings","metadata":{}},{"cell_type":"code","source":"# filter out animes that have fewer than 5 total ratings\nanime_counts = ratings['English name'].value_counts()\nratings = ratings[ratings['English name'].isin(anime_counts[anime_counts >= 5].index)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T02:21:52.760747Z","iopub.execute_input":"2026-08-02T02:21:52.761097Z","iopub.status.idle":"2026-08-02T02:21:57.828369Z","shell.execute_reply.started":"2026-08-02T02:21:52.761074Z","shell.execute_reply":"2026-08-02T02:21:57.827429Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# take a random 15% slice of the entire dataset\n# this keeps the vast vocabulary of animes, but drops the row count to ~8 million rows\nsampled_ratings = ratings.sample(frac=0.15, random_state=42).reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T02:21:57.829613Z","iopub.execute_input":"2026-08-02T02:21:57.830047Z","iopub.status.idle":"2026-08-02T02:22:02.718299Z","shell.execute_reply.started":"2026-08-02T02:21:57.830019Z","shell.execute_reply":"2026-08-02T02:22:02.717386Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# clean out ultra-rare noise specifically from this sample to keep embeddings stable\nanime_counts = sampled_ratings['English name'].value_counts()\nsampled_ratings = sampled_ratings[sampled_ratings['English name'].isin(anime_counts[anime_counts >= 3].index)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T02:22:02.719502Z","iopub.execute_input":"2026-08-02T02:22:02.719957Z","iopub.status.idle":"2026-08-02T02:22:03.603616Z","shell.execute_reply.started":"2026-08-02T02:22:02.719929Z","shell.execute_reply":"2026-08-02T02:22:03.602432Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# now check shape\nsampled_ratings.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T02:22:03.605042Z","iopub.execute_input":"2026-08-02T02:22:03.605352Z","iopub.status.idle":"2026-08-02T02:22:03.611998Z","shell.execute_reply.started":"2026-08-02T02:22:03.605325Z","shell.execute_reply":"2026-08-02T02:22:03.610911Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Create the DataLoader and Learner","metadata":{}},{"cell_type":"code","source":"# create the dataloaders\n# NOTE: Due to this being a large dataset, I will use AI recommended bs=4096 to get better use out of the Kaggle GPU T4 x 2\ndls = CollabDataLoaders.from_df(sampled_ratings, item_name='English name', bs=4096)\ndls.show_batch()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T02:22:14.936716Z","iopub.execute_input":"2026-08-02T02:22:14.937140Z","iopub.status.idle":"2026-08-02T02:22:28.326202Z","shell.execute_reply.started":"2026-08-02T02:22:14.937107Z","shell.execute_reply":"2026-08-02T02:22:28.325135Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# create the learner using range 0 - 10.5 to cover the TAH Competition ratings of 0-10\nlearn = collab_learner(dls, n_factors=50, y_range=(0, 10.5))","metadata":{"execution":{"iopub.status.busy":"2026-08-02T02:35:34.124942Z","iopub.execute_input":"2026-08-02T02:35:34.126077Z","iopub.status.idle":"2026-08-02T02:35:34.442106Z","shell.execute_reply.started":"2026-08-02T02:35:34.126028Z","shell.execute_reply":"2026-08-02T02:35:34.440929Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Remember to look for a good lr\nlearn.lr_find()","metadata":{"execution":{"iopub.status.busy":"2026-08-02T02:35:37.265730Z","iopub.execute_input":"2026-08-02T02:35:37.266175Z","iopub.status.idle":"2026-08-02T02:35:48.053376Z","shell.execute_reply.started":"2026-08-02T02:35:37.266143Z","shell.execute_reply":"2026-08-02T02:35:48.052193Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# add in a bit past the recommended lr, keep weight decay wd=0.1\n# learn.fit_one_cycle(8, 2e-2, wd=0.1) - started overfitting after 5\nlearn.fit_one_cycle(5, 2e-2, wd=0.1)","metadata":{"execution":{"iopub.status.busy":"2026-08-02T02:36:08.084796Z","iopub.execute_input":"2026-08-02T02:36:08.085819Z","iopub.status.idle":"2026-08-02T02:39:27.269728Z","shell.execute_reply.started":"2026-08-02T02:36:08.085782Z","shell.execute_reply":"2026-08-02T02:39:27.268911Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# see the model Embeddings\nlearn.model","metadata":{"execution":{"iopub.status.busy":"2026-08-02T02:39:57.257509Z","iopub.execute_input":"2026-08-02T02:39:57.258607Z","iopub.status.idle":"2026-08-02T02:39:57.264815Z","shell.execute_reply.started":"2026-08-02T02:39:57.258563Z","shell.execute_reply":"2026-08-02T02:39:57.263573Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Analyse the data","metadata":{}},{"cell_type":"code","source":"# see the bias\nmovie_bias = learn.model.i_bias.weight.squeeze()\nidxs = movie_bias.argsort(descending=True)[:5]\n[dls.classes['English name'][i] for i in idxs]","metadata":{"execution":{"iopub.status.busy":"2026-08-02T02:40:03.620891Z","iopub.execute_input":"2026-08-02T02:40:03.621957Z","iopub.status.idle":"2026-08-02T02:40:03.641556Z","shell.execute_reply.started":"2026-08-02T02:40:03.621909Z","shell.execute_reply":"2026-08-02T02:40:03.640557Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# PCA\ng = ratings.groupby('English name')['rating'].count()\ntop_movies = g.sort_values(ascending=False).index.values[:1000]\ntop_idxs = tensor([learn.dls.classes['English name'].o2i[m] for m in top_movies])\n# movie_w = learn.model.movie_factors[top_idxs].cpu().detach()\n# New corrected line using the built-in layer weights\nmovie_w = learn.model.i_weight.weight[top_idxs].cpu().detach()\nmovie_pca = movie_w.pca(3)\nfac0,fac1,fac2 = movie_pca.t()\nidxs = list(range(50))\nX = fac0[idxs]\nY = fac2[idxs]\nplt.figure(figsize=(12,12))\nplt.scatter(X, Y)\nfor i, x, y in zip(top_movies[idxs], X, Y):\n    plt.text(x,y,i, color=np.random.rand(3)*0.7, fontsize=11)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T02:40:04.049303Z","iopub.execute_input":"2026-08-02T02:40:04.050295Z","iopub.status.idle":"2026-08-02T02:40:09.786093Z","shell.execute_reply.started":"2026-08-02T02:40:04.050258Z","shell.execute_reply":"2026-08-02T02:40:09.784913Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Create the submission","metadata":{}},{"cell_type":"code","source":"# see what the competition expects\ntest_template = pd.read_csv('/kaggle/input/competitions/anime-recommendation-system-by-tah/sample.csv')\ntest_template.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T02:51:08.415259Z","iopub.execute_input":"2026-08-02T02:51:08.416326Z","iopub.status.idle":"2026-08-02T02:51:08.443951Z","shell.execute_reply.started":"2026-08-02T02:51:08.416277Z","shell.execute_reply":"2026-08-02T02:51:08.442919Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Count how many item IDs are expected per user\nnum_recommendations = len(test_template.iloc[0]['anime_ids'].split())\nprint(f\"The competition expects exactly {num_recommendations} recommendations per user.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T02:56:06.067009Z","iopub.execute_input":"2026-08-02T02:56:06.068358Z","iopub.status.idle":"2026-08-02T02:56:06.076263Z","shell.execute_reply.started":"2026-08-02T02:56:06.068314Z","shell.execute_reply":"2026-08-02T02:56:06.075199Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### **NOTE:** This is a Top-K recommendation system (specifically focused on the Item Retrieval / Item Ranking task)\n- Instead of a regression system where your model predicts an exact numerical rating (like predicting if a user will give a show a 7.4 vs an 8.2), this competition uses a Retrieval and Ranking paradigm.\n- The next few cells have been created with the assistance of AI as this Competition submission format is unfamiliar to me at this point","metadata":{}},{"cell_type":"code","source":"import torch\nimport numpy as np\n\n# 1. Map the text names in your model's vocabulary back to their numerical MAL_IDs\nunique_animes = list(learn.dls.classes['English name'])\nname_to_id = dict(zip(movies['English name'], movies['MAL_ID']))\nanime_ids_vocab = np.array([name_to_id.get(name, 1691) for name in unique_animes])\n\n# 2. Extract the trained embedding weights and biases directly from your GPU\nuser_weights = learn.model.u_weight.weight.detach()  # Shape: (num_users, 50)\nitem_weights = learn.model.i_weight.weight.detach()  # Shape: (num_animes, 50)\nuser_biases = learn.model.u_bias.weight.detach().squeeze()  # Shape: (num_users,)\nitem_biases = learn.model.i_bias.weight.detach().squeeze()  # Shape: (num_animes,)\n\n# 3. Safely grab fastai's internal user look-up maps\nuser_classes = learn.dls.classes.get('user', learn.dls.classes.get('user_id'))\nuser_o2i = user_classes.o2i\n\n# 4. Loop through the test users and generate recommendations\nfinal_recommendations = []\n\nfor idx, row in test_template.iterrows():\n    u_id = row['user_id']\n    \n    # Check if the user exists in our training vocabulary (handles int or string matching)\n    if u_id in user_o2i:\n        u_idx = user_o2i[u_id]\n    elif str(u_id) in user_o2i:\n        u_idx = user_o2i[str(u_id)]\n    else:\n        u_idx = None\n        \n    if u_idx is not None:\n        # High-speed Dot Product formula on the GPU: \n        # Predicted Score = (User_Weights · Item_Weights) + User_Bias + Item_Biases\n        scores = torch.matmul(item_weights, user_weights[u_idx]) + user_biases[u_idx] + item_biases\n        \n        # Grab the indexes of the 5 highest scoring items\n        top_5_indices = torch.topk(scores, k=5).indices.cpu().numpy()\n        \n        # Convert those internal indexes back to real MAL_ID numbers\n        rec_ids = [str(anime_ids_vocab[i]) for i in top_5_indices]\n    else:\n        # Fallback for brand-new users: provide the competition's default baseline list\n        rec_ids = [\"1691\"] * 5\n        \n    # Join the 5 IDs with spaces to match Kaggle's format exactly\n    final_recommendations.append(\" \".join(rec_ids))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T03:13:06.711374Z","iopub.execute_input":"2026-08-02T03:13:06.712504Z","iopub.status.idle":"2026-08-02T03:13:13.051799Z","shell.execute_reply.started":"2026-08-02T03:13:06.712448Z","shell.execute_reply":"2026-08-02T03:13:13.050513Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# assemble the final official submission dataframe\nsubmission = pd.DataFrame({\n    'user_id': test_template['user_id'],\n    'anime_ids': final_recommendations})","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T03:13:43.443603Z","iopub.execute_input":"2026-08-02T03:13:43.444818Z","iopub.status.idle":"2026-08-02T03:13:43.452901Z","shell.execute_reply.started":"2026-08-02T03:13:43.444777Z","shell.execute_reply":"2026-08-02T03:13:43.451729Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# check format is correct\nsubmission.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T03:14:09.073697Z","iopub.execute_input":"2026-08-02T03:14:09.074721Z","iopub.status.idle":"2026-08-02T03:14:09.083850Z","shell.execute_reply.started":"2026-08-02T03:14:09.074684Z","shell.execute_reply":"2026-08-02T03:14:09.082742Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# save it to your Kaggle output directory\nsubmission.to_csv('submission.csv', index=False)\nprint(\"Submission file successfully created and saved as 'submission.csv'\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-02T03:14:24.384246Z","iopub.execute_input":"2026-08-02T03:14:24.385051Z","iopub.status.idle":"2026-08-02T03:14:24.419588Z","shell.execute_reply.started":"2026-08-02T03:14:24.385014Z","shell.execute_reply":"2026-08-02T03:14:24.418379Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}