{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "47d52d68",
   "metadata": {},
   "source": [
    "\n",
    "<a id='chap-tppandaspolarsplotly'></a>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b4d59862",
   "metadata": {},
   "source": [
    "# Travaux pratiques - Pandas, Polars et visualisation avec Plotly\n",
    "\n",
    "Références externes utiles :\n",
    "\n",
    "> - [Documentation Pandas](https://pandas.pydata.org/docs/)  \n",
    "- [Documentation Polars](https://docs.pola.rs/)  \n",
    "- [Documentation Plotly Express](https://plotly.com/python/plotly-express/)  \n",
    "- [Guide de migration Pandas → Polars](https://docs.pola.rs/user-guide/migration/pandas/)  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d3dfc31f",
   "metadata": {},
   "source": [
    "# Objectif de cette séance\n",
    "\n",
    "Cette séance a trois objectifs :\n",
    "\n",
    "1. Rappeler les opérations usuelles de **Pandas** (lecture, filtrage, agrégation,\n",
    "  jointure) qui se retrouvent avec une syntaxe similaire dans Polars et Spark.  \n",
    "1. Découvrir **Polars** : comparer son API à celle de Pandas et mesurer le gain\n",
    "  de performance par rapport à Pandas.  \n",
    "1. Explorer **Plotly Express** pour la visualisation interactive de données, en abordant\n",
    "  (brièvement ici) la question du passage à l’échelle des visualisations.  \n",
    "\n",
    "\n",
    "Tout au long de cette séance nous travaillons sur le jeu de données **MovieLens**,\n",
    "qui recense des notes attribuées par des utilisateurs à des films."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ae2002c4",
   "metadata": {},
   "source": [
    "# Données de la séance\n",
    "\n",
    "Téléchargez et décompressez les données MovieLens (version *small*, ~1 million de notes) :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1b4b03bd",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "!mkdir -p tpPandasPolarsPlotly/data\n",
    "!wget -nc --no-check-certificate https://files.grouplens.org/datasets/movielens/ml-latest-small.zip \\\n",
    "      -P tpPandasPolarsPlotly/\n",
    "!unzip -n tpPandasPolarsPlotly/ml-latest-small.zip -d tpPandasPolarsPlotly/\n",
    "!mv tpPandasPolarsPlotly/ml-latest-small/*.csv tpPandasPolarsPlotly/data/\n",
    "!rm tpPandasPolarsPlotly/ml-latest-small.zip"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "54958194",
   "metadata": {},
   "source": [
    "Les fichiers disponibles sont :\n",
    "\n",
    "- `ratings.csv` : notes (userId, movieId, rating, timestamp), env. 100 000 lignes;  \n",
    "- `movies.csv`  : métadonnées des films (movieId, title, genres);  \n",
    "- `tags.csv`    : étiquettes libres attribuées par les utilisateurs;  \n",
    "- `links.csv`   : correspondances avec IMDb et TMDb.  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4b398f05",
   "metadata": {},
   "source": [
    "## Manipulation de données avec Pandas\n",
    "\n",
    "[Pandas](https://pandas.pydata.org/) est la bibliothèque DataFrame de référence en Python.\n",
    "Un `DataFrame` Pandas est un tableau à deux dimensions avec des colonnes nommées, stocké\n",
    "en mémoire sur une seule machine. Nous en rappelons ici les opérations fondamentales qui\n",
    "réapparaissent dans Polars comme dans Spark."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03a3c93d",
   "metadata": {},
   "source": [
    "### Chargement et exploration"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1d0095b4",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "\n",
    "ratings = pd.read_csv(\"tpPandasPolarsPlotly/data/ratings.csv\")\n",
    "movies  = pd.read_csv(\"tpPandasPolarsPlotly/data/movies.csv\")\n",
    "\n",
    "print(ratings.shape)   # (nombre de lignes, nombre de colonnes)\n",
    "ratings.head()         # affiche les 5 premières lignes dans Jupyter"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16a6c116",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Types des colonnes et valeurs manquantes\n",
    "ratings.info()\n",
    "\n",
    "# Statistiques descriptives des colonnes numériques\n",
    "ratings.describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e95230c",
   "metadata": {},
   "source": [
    "### Question\n",
    "\n",
    "1. Combien y a-t-il de films **distincts** dans `ratings` ? Et d’utilisateurs **distincts** ?\n",
    "  Employez `.nunique()`.  \n",
    "1. Quelle est la distribution des notes (valeurs de `rating`) ? Utilisez\n",
    "  `.value_counts()` ou `.describe()`.  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a7080dc9",
   "metadata": {},
   "source": [
    "### Filtrage et sélection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e9776d8e",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Sélectionner une colonne\n",
    "notes = ratings[\"rating\"]\n",
    "\n",
    "# Filtrer les lignes : ne garder que les très bonnes notes\n",
    "top_notes = ratings[ratings[\"rating\"] >= 4.5]\n",
    "print(top_notes.shape)\n",
    "\n",
    "# Sélectionner plusieurs colonnes\n",
    "sous_ensemble = ratings[[\"userId\", \"movieId\", \"rating\"]]\n",
    "\n",
    "# Filtrer sur plusieurs conditions\n",
    "bons_recents = ratings[(ratings[\"rating\"] >= 4.0) & (ratings[\"timestamp\"] > 1_000_000_000)]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c92aa641",
   "metadata": {},
   "source": [
    "### Question\n",
    "\n",
    "Extrayez toutes les notes attribuées par l’utilisateur `userId = 1`. Combien\n",
    "de films a-t-il notés ? Quelle est la moyenne des notes qu’il a données ?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b90800c1",
   "metadata": {},
   "source": [
    "### Agrégation avec `groupby`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b425d3b0",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Note moyenne par film\n",
    "moy_par_film = (\n",
    "    ratings\n",
    "    .groupby(\"movieId\")[\"rating\"]\n",
    "    .mean()\n",
    "    .reset_index()\n",
    "    .rename(columns={\"rating\": \"note_moy\"})\n",
    ")\n",
    "moy_par_film.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "67306443",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Nombre de notes par film\n",
    "nb_par_film = (\n",
    "    ratings\n",
    "    .groupby(\"movieId\")[\"rating\"]\n",
    "    .count()\n",
    "    .reset_index()\n",
    "    .rename(columns={\"rating\": \"nb_notes\"})\n",
    ")\n",
    "nb_par_film.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d762045b",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Plusieurs agrégations en une seule passe\n",
    "stats_par_film = (\n",
    "    ratings\n",
    "    .groupby(\"movieId\")[\"rating\"]\n",
    "    .agg(note_moy=\"mean\", nb_notes=\"count\", note_min=\"min\", note_max=\"max\")\n",
    "    .reset_index()\n",
    ")\n",
    "stats_par_film.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "382f64e1",
   "metadata": {},
   "source": [
    "### Jointure"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4eaf89f9",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Jointure à gauche entre statistiques (issues de ratings) et titres et genres\n",
    "stats_films = stats_par_film.merge(movies, on=\"movieId\", how=\"left\")\n",
    "stats_films.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a000f008",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Films les plus populaires (ayant reçu le plus de notes)\n",
    "stats_films.sort_values(\"nb_notes\", ascending=False).head(10)[[\"title\", \"nb_notes\", \"note_moy\"]]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9bc4915a",
   "metadata": {},
   "source": [
    "### Questions\n",
    "\n",
    "1. Quel est le film le **mieux noté** parmi ceux ayant reçu **au moins 50 notes** ?  \n",
    "1. Combien de films du genre `\"Drama\"` (`genres` contient la chaîne `\"Drama\"`)\n",
    "  ont une note moyenne supérieure à 4.0 ?  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "84bb8653",
   "metadata": {},
   "source": [
    "### Colonnes calculées et transformations"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cbc38ae1",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Convertir le timestamp Unix en datetime\n",
    "ratings[\"date\"] = pd.to_datetime(ratings[\"timestamp\"], unit=\"s\")\n",
    "ratings[\"annee\"] = ratings[\"date\"].dt.year\n",
    "\n",
    "# Note centrée-réduite par utilisateur (pour éliminer les biais individuels)\n",
    "ratings[\"rating_centree\"] = (\n",
    "    ratings.groupby(\"userId\")[\"rating\"]\n",
    "    .transform(lambda x: (x - x.mean()) / x.std())\n",
    ")\n",
    "ratings[[\"userId\", \"movieId\", \"rating\", \"date\", \"annee\", \"rating_centree\"]].head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3c6ed458",
   "metadata": {},
   "source": [
    "### Question\n",
    "\n",
    "Calculez, pour chaque **année**, la note moyenne et le nombre de notes attribuées.\n",
    "Triez par année. Observez-vous une évolution de la note moyenne au fil du temps ?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c1133f56",
   "metadata": {},
   "source": [
    "## Passage à Polars\n",
    "\n",
    "Nous reprenons maintenant les mêmes opérations avec Polars pour constater les\n",
    "différences d’API et mesurer les gains de performance."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6907bd6a",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "import polars as pl"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8f0a61ab",
   "metadata": {},
   "source": [
    "### Chargement et exploration"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2799ebdd",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Mode eager (lecture immédiate)\n",
    "ratings_pl = pl.read_csv(\"tpPandasPolarsPlotly/data/ratings.csv\")\n",
    "movies_pl  = pl.read_csv(\"tpPandasPolarsPlotly/data/movies.csv\")\n",
    "\n",
    "print(ratings_pl.shape)\n",
    "ratings_pl.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "056069aa",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Schéma (types des colonnes)\n",
    "print(ratings_pl.schema)\n",
    "\n",
    "# Statistiques descriptives\n",
    "ratings_pl.describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "813bba63",
   "metadata": {},
   "source": [
    "### Filtrage et sélection\n",
    "\n",
    "En Polars, les colonnes sont désignées par des **expressions** `pl.col()`, ce qui permet\n",
    "à l’optimiseur de requêtes de raisonner sur les opérations avant de les exécuter :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d46b8e0e",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Filtrage\n",
    "top_notes_pl = ratings_pl.filter(pl.col(\"rating\") >= 4.5)\n",
    "print(top_notes_pl.shape)\n",
    "\n",
    "# Sélection de colonnes\n",
    "sous_ensemble_pl = ratings_pl.select([\"userId\", \"movieId\", \"rating\"])\n",
    "\n",
    "# Combinaison\n",
    "bons_recents_pl = ratings_pl.filter(\n",
    "    (pl.col(\"rating\") >= 4.0) & (pl.col(\"timestamp\") > 1_000_000_000)\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "35a60367",
   "metadata": {},
   "source": [
    "### Agrégation avec `group_by`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f7bc0655",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "stats_pl = (\n",
    "    ratings_pl\n",
    "    .group_by(\"movieId\")\n",
    "    .agg([\n",
    "        pl.col(\"rating\").mean().alias(\"note_moy\"),\n",
    "        pl.col(\"rating\").count().alias(\"nb_notes\"),\n",
    "        pl.col(\"rating\").min().alias(\"note_min\"),\n",
    "        pl.col(\"rating\").max().alias(\"note_max\"),\n",
    "    ])\n",
    ")\n",
    "stats_pl.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2e0d152e",
   "metadata": {},
   "source": [
    "### Note\n",
    "\n",
    "`group_by` en Polars ne garantit **pas** l’ordre des groupes dans le résultat\n",
    "(contrairement à `groupby` de Pandas qui préserve l’ordre d’apparition).\n",
    "Ajoutez un `.sort(\"movieId\")` si l’ordre est important."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f73ef0b7",
   "metadata": {},
   "source": [
    "### Jointure et colonnes calculées"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "364e55ad",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Jointure\n",
    "stats_films_pl = stats_pl.join(movies_pl, on=\"movieId\", how=\"left\")\n",
    "\n",
    "# Colonne calculée : conversion du timestamp en année\n",
    "ratings_pl = ratings_pl.with_columns(\n",
    "    (pl.col(\"timestamp\") * 1_000)      # car Polars attend des millisecondes\n",
    "    .cast(pl.Datetime)\n",
    "    .dt.year()\n",
    "    .alias(\"annee\")\n",
    ")\n",
    "\n",
    "# Top 10 films les plus populaires\n",
    "(stats_films_pl\n",
    " .sort(\"nb_notes\", descending=True)\n",
    " .head(10)\n",
    " .select([\"title\", \"nb_notes\", \"note_moy\"]))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "64d4b4d0",
   "metadata": {},
   "source": [
    "### Question\n",
    "\n",
    "Reproduisez en Polars le calcul de la note moyenne par année. Comparez votre code\n",
    "Polars avec le code Pandas équivalent : quelles différences syntaxiques notez-vous ?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "43bae2e2",
   "metadata": {},
   "source": [
    "### Mode `lazy` et optimiseur de requêtes\n",
    "\n",
    "En mode *lazy*, Polars accumule les opérations dans un graphe de requête et les optimise\n",
    "avant exécution. C’est le mode recommandé pour les traitements sur des fichiers volumineux :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3f8313ed",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Lecture lazy : le fichier n'est pas lu immédiatement et entièrement\n",
    "lf = pl.scan_csv(\"tpPandasPolarsPlotly/data/ratings.csv\")\n",
    "\n",
    "# Construction de la requête (aucun calcul n'est fait immédiatement)\n",
    "requete = (\n",
    "    lf\n",
    "    .filter(pl.col(\"rating\") >= 4.0)\n",
    "    .group_by(\"movieId\")\n",
    "    .agg([\n",
    "        pl.col(\"rating\").mean().alias(\"note_moy\"),\n",
    "        pl.col(\"rating\").count().alias(\"nb_notes\"),\n",
    "    ])\n",
    "    .sort(\"note_moy\", descending=True)\n",
    ")\n",
    "\n",
    "# Afficher le plan d'exécution optimisé\n",
    "print(requete.explain())\n",
    "\n",
    "# Déclencher l'exécution (et la lecture de ce qui est indispensable)\n",
    "result = requete.collect()\n",
    "result.head(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2a936c03",
   "metadata": {},
   "source": [
    "Notez dans le plan d’exécution les optimisations appliquées : le filtre est poussé au plus\n",
    "près de la lecture (*predicate pushdown*), et seules les colonnes `movieId` et `rating`\n",
    "sont lues depuis le fichier (*projection pushdown*)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b70f8aeb",
   "metadata": {},
   "source": [
    "### Comparaison de performances Pandas vs Polars\n",
    "\n",
    "Mesurons concrètement le gain de performance sur une opération d’agrégation. Pour rendre\n",
    "la comparaison plus significative, nous générons un jeu de données plus grand (le nombre\n",
    "N de lignes peut être modifié). Noter que dans notre exemple les données sont générées\n",
    "directement en mémoire, l’accélération potentielle obtenue par Polars grâce à une lecture\n",
    "sélective depuis le stockage de masse n’est donc pas prise en compte."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13251938",
   "metadata": {},
   "source": [
    "### Génération d’un jeu de données synthétique plus grand"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "85b5ae69",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "rng = np.random.default_rng(42)\n",
    "N = 5_000_000   # 5 millions de lignes\n",
    "\n",
    "data_synth = {\n",
    "    \"userId\":   rng.integers(1, 10_001, N),\n",
    "    \"movieId\":  rng.integers(1, 50_001, N),\n",
    "    \"rating\":   rng.choice([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0], N),\n",
    "    \"timestamp\": rng.integers(800_000_000, 1_700_000_000, N),\n",
    "}\n",
    "\n",
    "# Version Pandas\n",
    "import pandas as pd\n",
    "df_pd = pd.DataFrame(data_synth)\n",
    "\n",
    "# Version Polars\n",
    "import polars as pl\n",
    "df_pl = pl.DataFrame(data_synth)\n",
    "\n",
    "print(f\"Taille : {N:,} lignes, {df_pd.memory_usage(deep=True).sum() / 1e6:.0f} Mo (Pandas)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "11a3f1fe",
   "metadata": {},
   "source": [
    "Mesurons maintenant le temps d’exécution d’une agrégation `groupby` (les résultats dépendent,\n",
    "naturellement, des ressources de l’ordinateur sur lequel le code est exécuté) :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c91b2a23",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "import time\n",
    "\n",
    "# Pandas\n",
    "t0 = time.perf_counter()\n",
    "res_pd = (\n",
    "    df_pd\n",
    "    .groupby(\"movieId\")[\"rating\"]\n",
    "    .agg([\"mean\", \"count\", \"std\"])\n",
    ")\n",
    "t_pandas = time.perf_counter() - t0\n",
    "print(f\"Pandas  : {t_pandas:.2f} s\")\n",
    "\n",
    "# Polars eager\n",
    "t0 = time.perf_counter()\n",
    "res_pl = (\n",
    "    df_pl\n",
    "    .group_by(\"movieId\")\n",
    "    .agg([\n",
    "        pl.col(\"rating\").mean().alias(\"mean\"),\n",
    "        pl.col(\"rating\").count().alias(\"count\"),\n",
    "        pl.col(\"rating\").std().alias(\"std\"),\n",
    "    ])\n",
    ")\n",
    "t_polars = time.perf_counter() - t0\n",
    "print(f\"Polars  : {t_polars:.2f} s\")\n",
    "print(f\"Accélération : x{t_pandas / t_polars:.1f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fefbe69c",
   "metadata": {},
   "source": [
    "### Question\n",
    "\n",
    "1. Quel facteur d’accélération observez-vous sur votre machine ? Est-il cohérent avec\n",
    "  les facteurs mentionnés en cours ?  \n",
    "1. Répétez la mesure avec une opération de **jointure** entre `df_pl` et\n",
    "  `movies_pl` (utilisez `df_pl.join(movies_pl, on=\"movieId\", how=\"left\")`).\n",
    "  Comparez avec Pandas (`df_pd.merge(...)`).  \n",
    "1. Essayez de faire varier `N` (1 million, 5 millions, 20 millions). Comment évolue\n",
    "  le rapport de performance ?  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c3028374",
   "metadata": {},
   "source": [
    "## Visualisation interactive avec Plotly Express\n",
    "\n",
    "[Plotly Express](https://plotly.com/python/plotly-express/) est l’API haut niveau de la\n",
    "bibliothèque Plotly. Elle permet de créer des graphiques interactifs (zoom, survol, sélection,\n",
    "export) en une seule ligne de code et s’intègre nativement dans Jupyter. Plotly Express\n",
    "accepte **indifféremment** des DataFrames Pandas ou Polars."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b85cb27b",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "import plotly.express as px\n",
    "\n",
    "# Si nécessaire : !pip install plotly\n",
    "# Dans Jupyter : les graphiques sont affichés directement dans le carnet"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "197a6305",
   "metadata": {},
   "source": [
    "### Distributions : histogrammes et boîtes à moustaches\n",
    "\n",
    "Distribution des notes :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3c671adf",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "fig = px.histogram(\n",
    "    ratings,\n",
    "    x=\"rating\",\n",
    "    nbins=20,\n",
    "    title=\"Distribution des notes MovieLens\",\n",
    "    labels={\"rating\": \"Note\", \"count\": \"Nombre de notes\"},\n",
    "    color_discrete_sequence=[\"steelblue\"],\n",
    ")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "27fec5d9",
   "metadata": {},
   "source": [
    "Boîte à moustaches des notes par année :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9d611d9f",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "fig = px.box(\n",
    "    ratings,\n",
    "    x=\"annee\",\n",
    "    y=\"rating\",\n",
    "    title=\"Distribution des notes par année\",\n",
    "    labels={\"annee\": \"Année\", \"rating\": \"Note\"},\n",
    ")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61fccc2b",
   "metadata": {},
   "source": [
    "### Question\n",
    "\n",
    "Tracez un histogramme du **nombre de notes par film** à partir de `stats_films`.\n",
    "Que constatez-vous sur la forme de la distribution ?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f2a0aaac",
   "metadata": {},
   "source": [
    "### Nuages de points interactifs\n",
    "\n",
    "Nous pouvons examiner la relation entre la note moyenne d’un film et le nombre de notes reçues pour ce film. On sélectionne les films avec au moins 10 notes pour réduire le bruit :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e2f80865",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "df_plot = stats_films[stats_films[\"nb_notes\"] >= 10].copy()\n",
    "\n",
    "fig = px.scatter(\n",
    "    df_plot,\n",
    "    x=\"nb_notes\",\n",
    "    y=\"note_moy\",\n",
    "    hover_name=\"title\",          # info-bulle au survol\n",
    "    color=\"note_moy\",\n",
    "    color_continuous_scale=\"RdYlGn\",\n",
    "    size=\"nb_notes\",             # taille des points proportionnelle au nombre de notes\n",
    "    size_max=20,\n",
    "    log_x=True,\n",
    "    title=\"Note moyenne vs popularité (films avec ≥ 10 notes)\",\n",
    "    labels={\"nb_notes\": \"Nombre de notes (log)\", \"note_moy\": \"Note moyenne\"},\n",
    ")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3d7e3722",
   "metadata": {},
   "source": [
    "Survolez les points pour voir le titre du film correspondant. Zoomez sur des zones\n",
    "d’intérêt. C’est là que Plotly apporte une valeur significative par rapport à Matplotlib\n",
    "pour l’exploration interactive des données."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1a83fed2",
   "metadata": {},
   "source": [
    "### Question\n",
    "\n",
    "1. Identifiez visuellement deux ou trois films très populaires (beaucoup de notes)\n",
    "  **et** très bien notés. Retrouvez-les dans `stats_films`.  \n",
    "1. Reprenez le même *scatter plot* en colorant les points selon le **genre principal**\n",
    "  du film. Pour simplifier, extrayez le premier genre de la colonne `genres`\n",
    "  (`df_plot[\"genre_principal\"] = df_plot[\"genres\"].str.split(\"|\").str[0]`)\n",
    "  et utilisez `color=\"genre_principal\"`.  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19cee35c",
   "metadata": {},
   "source": [
    "### Graphiques d’évolution temporelle\n",
    "\n",
    "Évolution de la note moyenne par année :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "509e5371",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "par_annee = (\n",
    "    ratings\n",
    "    .groupby(\"annee\")[\"rating\"]\n",
    "    .agg(note_moy=\"mean\", nb_notes=\"count\")\n",
    "    .reset_index()\n",
    "    .sort_values(\"annee\")\n",
    ")\n",
    "\n",
    "fig = px.line(\n",
    "    par_annee,\n",
    "    x=\"annee\",\n",
    "    y=\"note_moy\",\n",
    "    title=\"Évolution de la note moyenne par année\",\n",
    "    labels={\"annee\": \"Année\", \"note_moy\": \"Note moyenne\"},\n",
    "    markers=True,\n",
    ")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "38c84236",
   "metadata": {},
   "source": [
    "Graphique à deux séries, note et volume :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b334f94a",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "from plotly.subplots import make_subplots\n",
    "import plotly.graph_objects as go\n",
    "\n",
    "fig = make_subplots(specs=[[{\"secondary_y\": True}]])\n",
    "fig.add_trace(\n",
    "    go.Scatter(x=par_annee[\"annee\"], y=par_annee[\"note_moy\"],\n",
    "               name=\"Note moyenne\", mode=\"lines+markers\"),\n",
    "    secondary_y=False,\n",
    ")\n",
    "fig.add_trace(\n",
    "    go.Bar(x=par_annee[\"annee\"], y=par_annee[\"nb_notes\"],\n",
    "           name=\"Nombre de notes\", opacity=0.4),\n",
    "    secondary_y=True,\n",
    ")\n",
    "fig.update_layout(title=\"Note moyenne et volume par année\")\n",
    "fig.update_yaxes(title_text=\"Note moyenne\", secondary_y=False)\n",
    "fig.update_yaxes(title_text=\"Nombre de notes\", secondary_y=True)\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6982dbb4",
   "metadata": {},
   "source": [
    "### Visualisation à grande échelle : la limite du rendu côté client\n",
    "\n",
    "Plotly Express envoie les données brutes au navigateur en JSON pour les rendre côté client.\n",
    "Cela fonctionne bien jusqu’à **quelques dizaines de milliers de points** ; au-delà, le\n",
    "navigateur devient lent et la figure illisible car les points sont superposés.\n",
    "\n",
    "Illustrons ce problème sur les données synthétiques de 5 millions de lignes :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "833f8659",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Tentative d'affichage de 5 millions de points — NE PAS exécuter tel quel !\n",
    "# fig = px.scatter(df_pd, x=\"movieId\", y=\"rating\")   # navigateur bloqué\n",
    "\n",
    "# Solution 1 : échantillonnage avant visualisation\n",
    "echantillon = df_pd.sample(n=10_000, random_state=42)\n",
    "fig = px.scatter(\n",
    "    echantillon,\n",
    "    x=\"movieId\",\n",
    "    y=\"rating\",\n",
    "    opacity=0.3,\n",
    "    title=\"Échantillon de 10 000 notes (sur 5 millions)\",\n",
    ")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b44a238b",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Solution 2 : pré-agrégation, c'est à dire visualiser les statistiques plutôt que les données brutes\n",
    "stats_aggr = (\n",
    "    df_pd\n",
    "    .groupby(\"movieId\")[\"rating\"]\n",
    "    .agg(note_moy=\"mean\", nb_notes=\"count\")\n",
    "    .reset_index()\n",
    ")\n",
    "# stats_aggr a ~50 000 lignes (nombre de films distincts), gérable\n",
    "fig = px.scatter(\n",
    "    stats_aggr,\n",
    "    x=\"movieId\",\n",
    "    y=\"note_moy\",\n",
    "    size=\"nb_notes\",\n",
    "    size_max=10,\n",
    "    opacity=0.5,\n",
    "    title=\"Note moyenne par film (données agrégées de 50 000 points)\",\n",
    ")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "14526b88",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Solution 3 : rendu WebGL pour des centaines de milliers de points\n",
    "import plotly.graph_objects as go\n",
    "\n",
    "echantillon_large = df_pd.sample(n=200_000, random_state=42)\n",
    "fig = go.Figure(go.Scattergl(      # Scattergl utilise WebGL au lieu de SVG\n",
    "    x=echantillon_large[\"movieId\"],\n",
    "    y=echantillon_large[\"rating\"],\n",
    "    mode=\"markers\",\n",
    "    marker=dict(size=6, opacity=0.2, color=\"steelblue\"),\n",
    "))\n",
    "fig.update_layout(title=\"200 000 notes, rendu WebGL (Scattergl)\")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1deab849",
   "metadata": {},
   "source": [
    "### Note sur Datashader\n",
    "\n",
    "Pour des volumes vraiment massifs (millions à milliards de points), la solution\n",
    "de référence est [Datashader](https://datashader.org/), qui **rasterise les données\n",
    "côté serveur** en une image agrégée avant de l’envoyer au navigateur. **Datashader**\n",
    "s’intègre avec Dask pour traiter des données qui ne tiennent pas en RAM et avec\n",
    "Bokeh ou Plotly pour l’affichage interactif. Nous l’aborderons lors de la séance\n",
    "consacrée à la visualisation de données massives."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "535631bf",
   "metadata": {},
   "source": [
    "### Question\n",
    "\n",
    "Comparez visuellement les trois solutions présentées (échantillonnage, agrégation,\n",
    "WebGL) sur les données synthétiques de 5 millions de lignes :\n",
    "\n",
    "1. Pour l”**échantillonnage** : essayez des tailles d’échantillon de 1 000, 10 000\n",
    "  et 50 000 points. À partir de quelle taille la structure globale des données\n",
    "  est-elle bien restituée ?  \n",
    "1. Pour la **pré-agrégation** : expliquez pourquoi cette approche est souvent\n",
    "  préférable à l’échantillonnage pour la visualisation exploratoire.  \n",
    "1. Pour **WebGL** : mesurez le temps de rendu avec `%%time` pour 50 000,\n",
    "  200 000 et 500 000 points. À partir de quel volume le rendu devient-il lent ?  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "88ce6666",
   "metadata": {},
   "source": [
    "## Exercice de synthèse\n",
    "\n",
    "Nous terminons cette séance par un exercice qui fait appel à la fois à Polars et Plotly Express."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3e4ef777",
   "metadata": {},
   "source": [
    "## Contexte\n",
    "\n",
    "Nous souhaitons identifier les **genres de films** qui ont la meilleure réputation auprès\n",
    "des utilisateurs, en tenant compte à la fois de la note moyenne et du volume de notes.\n",
    "Certains genres sont peut-être bien notés mais peu vus, d’autres très populaires mais\n",
    "moyennement appréciés."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6cac3a2",
   "metadata": {},
   "source": [
    "## Question 1 : Préparation des données avec Polars\n",
    "\n",
    "En utilisant Polars (mode *lazy* recommandé) :\n",
    "\n",
    "1. Chargez `ratings.csv` et `movies.csv`.  \n",
    "1. Faites une jointure sur `movieId`.  \n",
    "1. La colonne `genres` contient des genres séparés par `\"|\"`. Utilisez\n",
    "  `pl.col(\"genres\").str.split(\"|\").explode()` (après un `with_columns`) pour\n",
    "  obtenir une ligne par genre (une note peut contribuer à plusieurs genres).  \n",
    "1. Calculez pour chaque genre : note moyenne, nombre de notes, nombre de films\n",
    "  distincts.  \n",
    "1. Filtrez les genres avec au moins 1 000 notes. Triez par note moyenne décroissante.  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5b282cd4",
   "metadata": {},
   "source": [
    "## Question 2 : Visualisation avec Plotly Express\n",
    "\n",
    "À partir du résultat de la question 1 :\n",
    "\n",
    "1. Créez un **graphique à barres** (`px.bar`) de la note moyenne par genre, trié\n",
    "  par note décroissante. Colorez les barres selon la note moyenne.  \n",
    "1. Créez un **nuage de points** avec en abscisse le nombre de films, en ordonnée la\n",
    "  note moyenne, la taille des points proportionnelle au nombre de notes et le nom\n",
    "  du genre en info-bulle. Quel genre offre le meilleur équilibre popularité/qualité ?  "
   ]
  }
 ],
 "metadata": {
  "date": 1789032447.3500514,
  "filename": "tpPandasPolarsPlotly.rst",
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python"
  },
  "title": "Travaux pratiques - Pandas, Polars et visualisation avec Plotly"
 },
 "nbformat": 4,
 "nbformat_minor": 5
}