{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ecaf9dd1",
   "metadata": {},
   "source": [
    "# Film: Recommender System\n",
    "\n",
    "Vi importerer først de nødvendige kodepakker."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "47973e59",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from sklearn.metrics.pairwise import cosine_similarity"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "06aa0941",
   "metadata": {},
   "source": [
    "Her defineres listerne med film og brugere samt datamatricen med anmeldelser."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6b2650ea",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Liste af filmnavne\n",
    "movies = [\n",
    "    \"Barbie\",\n",
    "    \"Star Wars\",\n",
    "    \"Avengers\",\n",
    "    \"Løvernes Konge\",\n",
    "    \"Druk\",\n",
    "    \"Alene Hjemme\",\n",
    "    \"Inderst Inde\",\n",
    "    \"Love Actually\",\n",
    "    \"Oppenheimer\",\n",
    "    \"Ternet Ninja\"\n",
    "]\n",
    "\n",
    "R = np.array([\n",
    "    [1, 1, 0, -1, 0, 1, 0, -1, 1, 0],\n",
    "    [0, 1, 1, 1, 1, -1, 0, -1, 0, 1],\n",
    "    [-1, 0, 0, 1, 1, 1, 1, 1, 0, 1],\n",
    "    [1, -1, 0, 1, 0, 1, 1, 1, 0, 1],\n",
    "    [0, 0, 1, 1, 0, 1, 1, 0, 1, 1],\n",
    "    [0, 1, 1, 0, 0, 1, 0, 0, 0, 1],\n",
    "    [1, 0, 0, 1, 1, 1, 1, 0, 0, 1],\n",
    "    [0, 1, 1, 1, 0, 1, -1, 0, 0, 1],\n",
    "    [1, -1, 0, 1, 0, 1, 1, 1, 0, 1],\n",
    "    [1, 1, 0, 1, 1, 1, 1, 0, -1, 1],\n",
    "    [1, 1, 1, 1, 1, 1, 0, 0, 1, 1],\n",
    "    [0, 1, 1, 0, 1, -1, 0, -1, 0, 1],\n",
    "    [1, 0, 1, 1, 1, 1, -1, 0, -1, 1],\n",
    "    [0, 1, 1, 0, 1, 1, 0, 1, 0, 1],\n",
    "    [-1, 1, 1, 1, 0, 1, -1, -1, 0, 1],\n",
    "    [1, 1, 0, 1, 0, 1, 1, 0, 1, 0],\n",
    "    [0, 1, 0, 1, 0, 1, 1, -1, 1, 1],\n",
    "    [0, 1, 1, 1, 1, 1, 1, 1, 1, 1],\n",
    "    [0, 1, 1, 1, 1, 1, 1, 0, 1, 1],\n",
    "    [0, 1, 1, 1, 1, 1, 1, 1, 1, 1],\n",
    "    [1, 0, 0, 1, 1, 1, 1, 1, 0, 1],\n",
    "    [0, 0, 1, 1, 1, 0, 0, 1, 0, 1],\n",
    "    [-1, 1, 1, 0, 1, 1, 1, 0, 1, -1],\n",
    "    [1, 1, 1, 1, 0, 0, 1, 0, 0, 0],\n",
    "    [-1, 1, 1, 1, 1, 1, 0, 0, 1, 1],\n",
    "    [0, 1, 0, 1, 1, 1, 0, 1, 1, 0],\n",
    "    [0, 1, 1, 0, 1, 1, -1, -1, 1, 1],\n",
    "    [1, 0, 1, 0, 0, -1, 1, 0, 0, 0],\n",
    "    [1, 1, 1, 0, 0, 1, 0, 0, 0, 1],\n",
    "    [1, 1, 1, 1, 1, 1, 1, -1, 1, 1],\n",
    "    [-1, 1, 1, 1, -1, 0, -1, -1, 1, 1],\n",
    "    [-1, 1, 1, 1, -1, 1, 0, -1, 1, 0],\n",
    "    [0, 1, 0, 1, 1, 1, 1, 1, 1, 0],\n",
    "    [-1, 1, 1, 1, 1, 1, 0, 0, 1, -1],\n",
    "    [-1, 1, 1, 1, 1, 1, -1, -1, 1, 1],\n",
    "    [-1, 1, 1, 0, 1, 1, 1, 0, 1, 1],\n",
    "    [1, -1, 1, 1, -1, 1, 1, 0, 1, 1],\n",
    "    [-1, 1, 1, 1, 0, -1, 1, 0, 1, 1],\n",
    "    [1, 1, 0, 1, 0, 1, -1, 0, 0, 1],\n",
    "    [1, 0, 1, 0, -1, 1, 1, 0, 1, 0],\n",
    "    [1, 1, 1, 1, 0, 1, 1, -1, 0, 0],\n",
    "    [1, 0, 1, 1, 0, 1, 1, 0, 0, 1],\n",
    "    [0, 0, 1, 1, 1, 1, 0, 0, 1, 1],\n",
    "    [0, 0, 0, 1, 1, 1, 1, 0, -1, 1],\n",
    "    [1, 0, 1, 1, 1, 1, 1, 0, 0, -1],\n",
    "    [-1, 0, 1, 1, 0, 1, 1, 0, 0, 1],\n",
    "    [-1, -1, 0, 1, 0, 1, 0, 0, 0, 1],\n",
    "    [1, 1, 0, 1, 1, 1, 1, 1, 0, 1],\n",
    "    [0, 0, 0, 1, 0, 1, 1, 0, 1, 0],\n",
    "    [0, 1, 0, 1, 0, -1, 0, -1, 0, 0],\n",
    "    [1, 0, 0, 1, 0, 1, 1, 0, -1, 1],\n",
    "    [1, 1, 1, 0, 0, -1, -1, -1, 1, 1],\n",
    "    [0, 1, 0, 1, 1, 1, 0, 1, 0, 1],\n",
    "    [0, -1, -1, 1, 1, 1, 1, 0, 1, 1],\n",
    "    [0, 0, 0, 0, 1, 1, 0, 1, 0, 1],\n",
    "    [-1, 1, 1, 1, 0, -1, 1, 0, 1, 1],\n",
    "    [1, 1, 1, 1, 1, 1, 1, 0, 1, 1],\n",
    "    [-1, 1, 1, 1, 1, -1, -1, -1, 1, 1]\n",
    "], dtype=float)\n",
    "\n",
    "N, M = R.shape\n",
    "\n",
    "users = [f\"U{i}\" for i in range(1, N+1)]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d6ac72d",
   "metadata": {},
   "source": [
    "Datamatricen centreres. Vi ignorerer ikke-sete film, altså værdien 0."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "09c28cd7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# -----------------------------\n",
    "# Preprocessing\n",
    "# -----------------------------\n",
    "R_nan = R.copy()\n",
    "R_nan[R_nan == 0] = np.nan\n",
    "\n",
    "global_mean = np.nanmean(R_nan)\n",
    "R_centered = R_nan - global_mean\n",
    "R_filled = np.nan_to_num(R_centered, nan=0.0)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "25ec3651",
   "metadata": {},
   "source": [
    "Vi beregner SVD på den centrerede data-matrix."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e711bc04",
   "metadata": {},
   "outputs": [],
   "source": [
    "# -----------------------------\n",
    "# SVD\n",
    "# -----------------------------\n",
    "U, S, Vt = np.linalg.svd(R_filled, full_matrices=False)\n",
    "V = Vt.T"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b27a5fd2",
   "metadata": {},
   "source": [
    "Vi kan vælge en bruger og undersøge lighed (similarity) til andre brugere. Der kan eksperimenteres med forskellige valg af brugere."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b8ec343c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# -----------------------------\n",
    "# User similarity\n",
    "# -----------------------------\n",
    "# EKSPERIMETER MED FORSKELLIGE BRUGERE HER:\n",
    "user_index = 11\n",
    "\n",
    "k = 3\n",
    "SimUsers = cosine_similarity(U[:, :k])\n",
    "np.fill_diagonal(SimUsers, 0)\n",
    "\n",
    "plt.figure()\n",
    "plt.scatter(range(1, N + 1), SimUsers[user_index, :], s=100, edgecolor='k')\n",
    "step = 3\n",
    "plt.xticks(range(1, N + 1, step), [users[i] for i in range(0, N, step)], rotation=45)\n",
    "plt.xlabel('Users')\n",
    "plt.ylabel('Similarity')\n",
    "plt.title(f'Similar users to {users[user_index]}')\n",
    "plt.grid(True, which='both')\n",
    "plt.axis([0, N + 1, -1, 1])\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "closest_user_ind = np.argmax(SimUsers[user_index, :])\n",
    "print('-' * 80)\n",
    "print(f\"Selected user: {users[user_index]}, Movies: {R[user_index, :]}\")\n",
    "print(f\"Closest user: {users[closest_user_ind]}, Movies: {R[closest_user_ind, :]}\")\n",
    "print('-' * 80)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d33fc8a5",
   "metadata": {},
   "source": [
    "Ligeledes kan vi se på ligheden mellem filmene."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ccf5b7ff",
   "metadata": {},
   "outputs": [],
   "source": [
    "# -----------------------------\n",
    "# Movie similarity\n",
    "# -----------------------------\n",
    "# EKSPERIMETER MED FORSKELLIGE FILMER HER:\n",
    "movie_index = 2  # Python is 0-based\n",
    "\n",
    "SimMovies = cosine_similarity(V[:, :k])\n",
    "np.fill_diagonal(SimMovies, 0)\n",
    "\n",
    "plt.figure()\n",
    "plt.scatter(range(1, M + 1), SimMovies[movie_index, :], s=100, edgecolor='k')\n",
    "plt.xticks(range(1, M + 1), movies, rotation=45)\n",
    "plt.xlabel('Movies')\n",
    "plt.ylabel('Similarity')\n",
    "plt.title(f'Similar movies to {movies[movie_index]}')\n",
    "plt.grid(True, which='both')\n",
    "plt.axis([0, M + 1, -1, 1])\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "sorted_inds = np.argsort(SimMovies[movie_index, :])[::-1]\n",
    "similar_movies = [movies[i] for i in sorted_inds if i != movie_index]\n",
    "print('-' * 80)\n",
    "print(f\"Selected movie: {movies[movie_index]}\")\n",
    "print(f\"Similar movies in order: {', '.join(similar_movies)}\")\n",
    "print('-' * 80)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "376b4e2c",
   "metadata": {},
   "source": [
    "Vi kan bruge heatmaps til at se på ligheden mellem alle brugere og alle film på en gang."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "68ac749d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# -----------------------------\n",
    "# Heatmaps\n",
    "# -----------------------------\n",
    "plt.figure()\n",
    "plt.imshow(SimMovies, vmin=-1, vmax=1, cmap='bwr')\n",
    "plt.colorbar()\n",
    "plt.xticks(range(M), movies, rotation=45)\n",
    "plt.yticks(range(M), movies)\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "plt.figure()\n",
    "plt.imshow(SimUsers, vmin=-1, vmax=1, cmap='bwr')\n",
    "plt.colorbar()\n",
    "step = 3\n",
    "plt.xticks(range(0, N, step), [users[i] for i in range(0, N, step)], rotation=90)\n",
    "plt.yticks(range(0, N, step), [users[i] for i in range(0, N, step)])\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b528b031",
   "metadata": {},
   "source": [
    "Mere tydeligt kan vi se lighederne når vi projicerer dataen på et lavere dimensionelt rum - her $2$ dimensioner - som er udspændt af de to første *Principal Components*."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "50af293e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# -----------------------------\n",
    "# Low-dimensional embeddings\n",
    "# -----------------------------\n",
    "dim = 2\n",
    "normalize_emb = False\n",
    "\n",
    "VS = V[:, :dim].copy()\n",
    "if normalize_emb:\n",
    "    VS /= np.linalg.norm(VS, axis=1, keepdims=True)\n",
    "\n",
    "offset = np.random.randn(M, dim)\n",
    "offset = 0.1 * offset / np.linalg.norm(offset, axis=1, keepdims=True)\n",
    "\n",
    "plt.figure()\n",
    "plt.scatter(VS[:, 0], VS[:, 1], s=100, edgecolor='k')\n",
    "for i in range(M):\n",
    "    plt.text(VS[i, 0] + offset[i, 0], VS[i, 1] + offset[i, 1], movies[i], fontsize=12)\n",
    "\n",
    "US = U[:, :dim].copy()\n",
    "US /= np.linalg.norm(US, axis=1, keepdims=True)\n",
    "if normalize_emb:\n",
    "    US /= np.linalg.norm(US, axis=1, keepdims=True)\n",
    "\n",
    "offset = np.random.randn(N, dim)\n",
    "offset = 0.15 * offset / np.linalg.norm(offset, axis=1, keepdims=True)\n",
    "\n",
    "plt.scatter(US[:, 0], US[:, 1], s=100, edgecolor='k')\n",
    "for i in range(N):\n",
    "    plt.text(US[i, 0] + offset[i, 0], US[i, 1] + offset[i, 1], users[i], fontsize=12)\n",
    "\n",
    "plt.axis([-1.25, 1.25, -1.25, 1.25])\n",
    "plt.grid(True, which='both')\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5f61852f",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "jupytext": {
   "text_representation": {
    "extension": ".md",
    "format_name": "myst",
    "format_version": 0.13,
    "jupytext_version": "1.18.1"
   }
  },
  "kernelspec": {
   "display_name": "base",
   "language": "python",
   "name": "python3"
  },
  "source_map": [
   12,
   18,
   22,
   26,
   105,
   109,
   119,
   123,
   129,
   133,
   161,
   165,
   192,
   196,
   216,
   220,
   257
  ]
 },
 "nbformat": 4,
 "nbformat_minor": 5
}