{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "9aeaf08b-26da-4300-b4d4-e20e9b835876",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Note: you may need to restart the kernel to use updated packages.\n"
     ]
    }
   ],
   "source": [
    "%pip install datasets matplotlib numpy scikit-learn sentence_transformers wordcloud -q"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "07d7e876-b3ba-40ed-b6d6-81683e7ee513",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/matheo/.conda/envs/projet_ia5/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
      "  from .autonotebook import tqdm as notebook_tqdm\n"
     ]
    }
   ],
   "source": [
    "from datasets import load_dataset\n",
    "import matplotlib.pyplot as plt\n",
    "import random\n",
    "import numpy as np\n",
    "import re\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.feature_extraction.text import TfidfVectorizer\n",
    "import gc\n",
    "from sentence_transformers import SentenceTransformer\n",
    "from sklearn.linear_model import SGDClassifier\n",
    "from sklearn.metrics import log_loss, classification_report, accuracy_score\n",
    "import pandas as pd\n",
    "\n",
    "ds = load_dataset(\"PleIAs/French-PD-Books\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "1c5400ed-494e-4bae-a540-71655b000319",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 33712 characters]",
      "text/plain": [
       "<Figure size 1000x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Extraire les années de publication\n",
    "years = ds['train']['date']\n",
    "\n",
    "# Nettoyer et convertir en entiers (certaines dates peuvent être manquantes ou non numériques)\n",
    "years_clean = [int(y) for y in years if str(y).isdigit()]\n",
    "\n",
    "# Créer l'histogramme\n",
    "plt.figure(figsize=(10, 6))\n",
    "plt.hist(years_clean, bins=50, color='skyblue', edgecolor='black')\n",
    "plt.xlabel('Année de publication')\n",
    "plt.ylabel('Nombre de textes')\n",
    "plt.title('Répartition des textes dans le temps')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "acd36441",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "DatasetDict({\n",
      "    train: Dataset({\n",
      "        features: ['file_id', 'ocr', 'title', 'date', 'author', 'page_count', 'word_count', 'character_count', 'complete_text'],\n",
      "        num_rows: 289577\n",
      "    })\n",
      "})\n"
     ]
    }
   ],
   "source": [
    "print(ds)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "f1064659",
   "metadata": {},
   "outputs": [],
   "source": [
    "french_stopwords = set([\n",
    "    'a', 'ai', 'aie', 'aient', 'aies', 'ait', 'alors', 'as', 'au', 'aucun', 'aura', 'aurai', 'auraient', 'aurais', 'aurait', 'auras', 'aurez', 'auriez', 'aurions', 'aurons', 'auront', 'aussi', 'autre', 'aux', 'avaient', 'avais', 'avait', 'avant', 'avec', 'avez', 'aviez', 'avions', 'avoir', 'avons', 'ayant', 'ayez', 'ayons',\n",
    "    'bon',\n",
    "    'c', 'ce', 'ceci', 'cela', 'ces', 'cet', 'cette', 'chaque', 'comme', 'comment',\n",
    "    'd', 'dans', 'de', 'des', 'deux', 'donc', 'dont', 'du',\n",
    "    'elle', 'en', 'encore', 'es', 'est', 'et', 'etaient', 'etais', 'etait', 'etant', 'ete', 'etes', 'etiez', 'etions', 'etre', 'eu', 'eue', 'eues', 'eurent', 'eus', 'eusse', 'eussent', 'eusses', 'eussiez', 'eussions', 'eut', 'eux', 'eûmes', 'eût', 'eûtes',\n",
    "    'fait', 'fais', 'faisaient', 'faisais', 'faisait', 'faisant', 'faire', 'faites', 'fasse', 'fassent', 'fasses', 'fassiez', 'fassions', 'faut', 'fi', 'font', 'force', 'furent', 'fus', 'fusse', 'fussent', 'fusses', 'fussiez', 'fussions', 'fut', 'fûmes', 'fût', 'fûtes',\n",
    "    'hors',\n",
    "    'i', 'ici', 'il', 'ils',\n",
    "    'j', 'je',\n",
    "    'l', 'la', 'le', 'les', 'leur', 'leurs', 'lui',\n",
    "    'm', 'ma', 'mais', 'me', 'mes', 'moi', 'mon',\n",
    "    'n', 'ne', 'ni', 'nos', 'notre', 'nous',\n",
    "    'on', 'ont', 'ou', 'où',\n",
    "    'par', 'pas', 'pendant', 'peu', 'peut', 'peux', 'plus', 'point', 'pour', 'pourquoi',\n",
    "    'qu', 'quand', 'que', 'quel', 'quelle', 'quelles', 'quels', 'qui',\n",
    "    's', 'sa', 'sans', 'se', 'sera', 'serai', 'seraient', 'serais', 'serait', 'seras', 'serez', 'seriez', 'serions', 'serons', 'seront', 'ses', 'soi', 'soient', 'sois', 'soit', 'sommes', 'son', 'sont', 'soyez', 'soyons', 'suis', 'sur',\n",
    "    't', 'ta', 'te', 'tes', 'toi', 'ton', 'tous', 'tout', 'tu', 'un', 'une',\n",
    "    'va', 'vers', 'voici', 'voilà', 'vos', 'votre', 'vous',\n",
    "    'y', 'à'\n",
    "])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "18b534c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Ancienne version\n",
    "def clean_text_old(example):\n",
    "    text = example[\"complete_text\"]\n",
    "    date = example.get(\"date\", None)\n",
    "\n",
    "    # Si la date contient un \"-\", on essaie d'extraire l'année connue (format \"1234-????\" ou \"????-1234\") ou moyenne des deux années\n",
    "    if \"-\" in str(date) and date is not None:\n",
    "        parts = str(date).split(\"-\")\n",
    "        if (parts[1].isdigit() and len(parts[1]) == 4) and parts[0] == \"????\":\n",
    "            date = str(parts[1])\n",
    "        else:\n",
    "            date = str(parts[0])\n",
    "\n",
    "    # 1. Retirer les numéros de page\n",
    "    text = re.sub(r\"[—\\-–]\\s*\\d+\\s*[—\\-–]\", \" \", text)\n",
    "    \n",
    "    # 2. Corriger les apostrophes et guillemets échappés\n",
    "    text = text.replace(\"\\\\'\", \"'\")\n",
    "    text = text.replace(\"\\\\\\\"\", \"\\\"\")\n",
    "    text = text.replace(\"\\\\n\", \" \")\n",
    "    text = text.replace(\"\\\\r\", \" \")\n",
    "    text = text.replace(\"\\\\t\", \" \")\n",
    "    \n",
    "    # 3. Corriger les mots coupés (pattern plus précis)\n",
    "    text = re.sub(r'([a-zàâäæçéèêëïîôùûüœ])\\s+([a-zàâäæçéèêëïîôùûüœ]{2,})', \n",
    "                  r'\\1\\2', text)\n",
    "    \n",
    "    # 4. Corriger les cas avec plusieurs espaces\n",
    "    text = re.sub(r'([a-zàâäæçéèêëïîôùûüœ])\\s{2,}([a-zàâäæçéèêëïîôùûüœ])', \n",
    "                  r'\\1\\2', text)\n",
    "    \n",
    "    # 5. Normaliser les espaces multiples\n",
    "    text = re.sub(r\"\\s+\", \" \", text)\n",
    "    \n",
    "    # 6. Nettoyer les caractères spéciaux\n",
    "    text = re.sub(r\"[^\\w\\s\\.,;:\\?!'\\-\\\"«»À-ÖØ-öø-ÿœŒ]\", \" \", text)\n",
    "    \n",
    "    # 7. Re-normaliser après nettoyage\n",
    "    text = re.sub(r\"\\s+\", \" \", text)\n",
    "    \n",
    "    # 8. Corriger la ponctuation\n",
    "    text = re.sub(r\"\\s+([,.\\?!;:])\", r\"\\1\", text)\n",
    "    text = re.sub(r\"([,.\\?!;:])\\s*([,.\\?!;:])\", r\"\\1\\2\", text)\n",
    "    \n",
    "    text = text.strip()\n",
    "    return {\"text\": text, \"date\": str(date)}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "91c483b8-ae5b-4f1d-9f8a-dffc8eea16c9",
   "metadata": {},
   "outputs": [],
   "source": [
    "def clean_text(example):\n",
    "    \"\"\"\n",
    "        Nettoie le texte d'entrée\n",
    "    \"\"\"\n",
    "    text = example[\"complete_text\"]\n",
    "    date = example.get(\"date\", None)\n",
    "\n",
    "    # --- Nettoyage de la date\n",
    "    if \"-\" in str(date) and date is not None:\n",
    "        parts = str(date).split(\"-\")\n",
    "        if (parts[1].isdigit() and len(parts[1]) == 4) and parts[0] == \"????\":\n",
    "            date = str(parts[1])\n",
    "        else:\n",
    "            date = str(parts[0])\n",
    "\n",
    "    # --- Nettoyage de texte\n",
    "    text = (text.replace(\"\\\\n\", \" \")\n",
    "                .replace(\"\\\\r\", \" \")\n",
    "                .replace(\"\\\\t\", \" \"))\n",
    "    \n",
    "    text = text.lower()\n",
    "\n",
    "    text = re.sub(r\"[^a-zàâäæçéèêëïîôùûüœ\\s]\", \" \", text)\n",
    "    \n",
    "    text = re.sub(r\"\\s+\", \" \", text).strip()\n",
    "\n",
    "    words = text.split()\n",
    "    filtered_words = [word for word in words if word not in french_stopwords]\n",
    "    text = \" \".join(filtered_words)\n",
    "\n",
    "    return {\"text\": text, \"date\": str(date)}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "39e38bbf",
   "metadata": {},
   "outputs": [],
   "source": [
    "reduced_ds = ds['train'].shuffle(seed=42).select(range(5000))\n",
    "\n",
    "cleaned_ds = reduced_ds.map(clean_text, remove_columns=reduced_ds.column_names)\n",
    "# cleaned_ds = ds['train'].map(clean_text, remove_columns=ds['train'].column_names, num_proc=4)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "0474c7fd",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 49048 characters]",
      "text/plain": [
       "<Figure size 1000x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "def create_period_label(example, period_length=50):\n",
    "     \"\"\"\n",
    "          Crée une étiquette de période basée sur l'année de publication.\n",
    "     \"\"\"\n",
    "     try:\n",
    "          year = int(example['date'])\n",
    "          start_year = (year // period_length) * period_length\n",
    "          end_year = start_year + period_length - 1\n",
    "\n",
    "          return {\"period\": f\"{start_year}-{end_year}\"}\n",
    "     except (ValueError, TypeError):\n",
    "          return {\"period\": None}\n",
    "     \n",
    "dataset_with_labels = cleaned_ds.map(create_period_label)\n",
    "\n",
    "df = pd.DataFrame(dataset_with_labels)\n",
    "\n",
    "grouped_texts = df.groupby('period')['text'].apply(list)\n",
    "\n",
    "period_counts = grouped_texts.apply(len).sort_index()\n",
    "\n",
    "plt.figure(figsize=(10, 6))\n",
    "period_counts.plot(kind='bar', color='skyblue', edgecolor='black')\n",
    "\n",
    "plt.title('Nombre de textes par période', fontsize=16)\n",
    "plt.xlabel('Période', fontsize=12)\n",
    "plt.ylabel('Nombre de textes', fontsize=12)\n",
    "plt.xticks(rotation=45, ha='right')\n",
    "plt.grid(axis='y', linestyle='--', alpha=0.7)\n",
    "plt.tight_layout()\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "4acca337",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 3907136 characters]",
      "text/plain": [
       "<Figure size 2000x3000 with 10 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import os\n",
    "import math\n",
    "import matplotlib.pyplot as plt\n",
    "from sklearn.feature_extraction.text import TfidfVectorizer\n",
    "from wordcloud import WordCloud\n",
    "\n",
    "# --- Vos paramètres (inchangés) ---\n",
    "max_features_tfidf = 5000\n",
    "min_df_tfidf = 3         # Ignorer les mots trop rares\n",
    "max_df_tfidf = 0.85      # Ignorer les mots trop fréquents\n",
    "top_n_words = 100\n",
    "\n",
    "# S'assurer que le répertoire existe\n",
    "output_dir = \"wordclouds_by_period\"\n",
    "os.makedirs(output_dir, exist_ok=True)\n",
    "\n",
    "tfidf_results = {}\n",
    "wordcloud_files = []\n",
    "\n",
    "# --- Début des modifications pour le subplot ---\n",
    "\n",
    "# 1. Déterminer le nombre de périodes à afficher\n",
    "num_periods = len(grouped_texts)\n",
    "if num_periods == 0:\n",
    "    print(\"Aucune donnée à afficher.\")\n",
    "    # Quittez si vous êtes dans un script, ou continuez si c'est dans un notebook\n",
    "\n",
    "# 2. Définir la grille (ex: 2 colonnes)\n",
    "ncols = 2\n",
    "# Calculer le nombre de lignes nécessaires (arrondi au supérieur)\n",
    "nrows = math.ceil(num_periods / ncols)\n",
    "\n",
    "# 3. Créer la figure et la grille d'axes\n",
    "# Ajustez figsize au besoin: (largeur totale, hauteur totale)\n",
    "fig, axes = plt.subplots(nrows, ncols, figsize=(20, nrows * 6))\n",
    "\n",
    "# Aplatir le tableau d'axes pour une itération simple (ex: axes[0], axes[1]...)\n",
    "# S'il n'y a qu'un seul plot, le mettre dans une liste pour que la boucle fonctionne\n",
    "if num_periods > 1:\n",
    "    axes = axes.flatten()\n",
    "else:\n",
    "    axes = [axes] # Cas où nrows=1 et ncols=1\n",
    "\n",
    "# --- Fin des modifications ---\n",
    "\n",
    "\n",
    "# Calculer le TF-IDF et générer les nuages pour chaque période\n",
    "# Utiliser enumerate pour obtenir un index 'i' pour les axes\n",
    "for i, (period, texts) in enumerate(grouped_texts.items()):\n",
    "    \n",
    "    # Sélectionner l'axe courant\n",
    "    ax = axes[i]\n",
    "\n",
    "    if not texts:\n",
    "        print(f\"Pas de textes pour la période {period}, subplot désactivé.\")\n",
    "        ax.axis('off') # Cacher l'axe s'il est vide\n",
    "        continue\n",
    "\n",
    "    try:\n",
    "        tfidf_vectorizer_period = TfidfVectorizer(\n",
    "            max_features=max_features_tfidf,\n",
    "            min_df=min_df_tfidf,\n",
    "            max_df=max_df_tfidf,\n",
    "            stop_words=None # Assurez-vous d'avoir pré-traité vos stop words\n",
    "        )\n",
    "        tfidf_matrix = tfidf_vectorizer_period.fit_transform(texts)\n",
    "        feature_names = tfidf_vectorizer_period.get_feature_names_out()\n",
    "        sum_tfidf = tfidf_matrix.sum(axis=0)\n",
    "        tfidf_scores = [(feature_names[col], sum_tfidf[0, col]) for col in range(sum_tfidf.shape[1])]\n",
    "        tfidf_scores.sort(key=lambda x: x[1], reverse=True)\n",
    "        top_words_scores = dict(tfidf_scores[:top_n_words])\n",
    "        tfidf_results[period] = top_words_scores\n",
    "\n",
    "        if not top_words_scores:\n",
    "             print(f\"Aucun mot pertinent trouvé pour la période {period} après TF-IDF.\")\n",
    "             ax.axis('off') # Cacher l'axe s'il est vide\n",
    "             continue\n",
    "\n",
    "        # Générer le nuage de mots\n",
    "        wordcloud = WordCloud(width=800, height=400, background_color='white').generate_from_frequencies(top_words_scores)\n",
    "\n",
    "        # --- Modifications du plotting ---\n",
    "        # 4. Afficher le nuage sur l'axe (subplot) spécifique\n",
    "        ax.imshow(wordcloud, interpolation='bilinear')\n",
    "        ax.axis('off')\n",
    "        ax.set_title(f\"Nuage de mots pour la période {period}\")\n",
    "        # --- Fin des modifications du plotting ---\n",
    "\n",
    "        # (La sauvegarde de fichier reste inchangée si vous la décommentez)\n",
    "        # filename = os.path.join(output_dir, f\"wordcloud_{period}.png\")\n",
    "        # wordcloud.to_file(filename)\n",
    "        # wordcloud_files.append(filename)\n",
    "\n",
    "    except ValueError as e:\n",
    "         print(f\"Erreur lors du traitement de la période {period}: {e}\")\n",
    "         ax.axis('off') # Cacher l'axe en cas d'erreur\n",
    "    except Exception as e:\n",
    "        print(f\"Erreur inattendue pour la période {period}: {e}\")\n",
    "        ax.axis('off') # Cacher l'axe en cas d'erreur\n",
    "\n",
    "# --- Début des ajouts post-boucle ---\n",
    "\n",
    "# 5. Cacher les axes (subplots) non utilisés\n",
    "# Si vous avez 5 périodes et une grille 3x2 (6 plots), le dernier plot sera vide.\n",
    "for j in range(num_periods, nrows * ncols):\n",
    "    axes[j].axis('off')\n",
    "\n",
    "# 6. Ajuster la mise en page pour éviter les superpositions\n",
    "plt.tight_layout(pad=3.0)\n",
    "\n",
    "# 7. Afficher la figure complète\n",
    "plt.show()\n",
    "\n",
    "# print(\"\\n--- Analyse terminée ---\")\n",
    "# print(f\"{len(wordcloud_files)} nuages de mots ont été générés dans le dossier '{output_dir}'.\")\n",
    "\n",
    "# # Afficher les noms des fichiers générés pour que l'utilisateur sache ce qui est disponible\n",
    "# print(\"Fichiers générés :\")\n",
    "# for fname in wordcloud_files:\n",
    "#     print(f\"- {fname}\")\n",
    "\n",
    "# # Optionnel: Afficher un exemple de mots/scores TF-IDF pour une période\n",
    "# # (Choisir une période existante dans tfidf_results.keys())\n",
    "# example_period = list(tfidf_results.keys())[0] if tfidf_results else None\n",
    "# if example_period:\n",
    "#     print(f\"\\nExemple des mots les plus pertinents (TF-IDF) pour {example_period}:\")\n",
    "#     # Afficher les 10 premiers mots pour l'exemple\n",
    "#     print(list(tfidf_results[example_period].items())[:10])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "cd09a14e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Distribution des périodes (desc.):\n",
      "period\n",
      "1850-1899    2361\n",
      "1800-1849    1066\n",
      "1900-1949     910\n",
      "1750-1799     381\n",
      "1700-1749     122\n",
      "1600-1649      62\n",
      "1650-1699      44\n",
      "1950-1999      43\n",
      "2000-2049       6\n",
      "1550-1599       5\n",
      "Taille de l'ensemble d'entraînement : 4000 textes.\n",
      "Taille de l'ensemble de test : 1000 textes.\n"
     ]
    }
   ],
   "source": [
    "# Convertir le dataset en DataFrame pandas pour le split et la stratification\n",
    "df_labeled = dataset_with_labels.to_pandas()\n",
    "\n",
    "# Filtrer les périodes non nulles (au cas où certaines dates n'aient pas pu être converties)\n",
    "df_labeled = df_labeled.dropna(subset=['period'])\n",
    "\n",
    "# Vérifier la distribution par période et retirer les périodes trop rares\n",
    "period_counts = df_labeled['period'].value_counts().sort_values(ascending=False)\n",
    "print(\"Distribution des périodes (desc.):\")\n",
    "print(period_counts.head(20).to_string())\n",
    "\n",
    "# Identifier les périodes avec moins de 2 exemples (ces classes posent problème pour stratify)\n",
    "rare_periods = period_counts[period_counts < 2].index.tolist()\n",
    "if rare_periods:\n",
    "    print(f\"Found {len(rare_periods)} period(s) with <2 samples. They will be removed for a stratified split.\")\n",
    "    # Option: on retire ces périodes du DataFrame pour permettre split stratifié\n",
    "    df_labeled = df_labeled[~df_labeled['period'].isin(rare_periods)].reset_index(drop=True)\n",
    "\n",
    "# Vérifier qu'il reste des données après filtrage\n",
    "if df_labeled.empty:\n",
    "    raise ValueError(\"No data left after removing rare period classes. Adjust your preprocessing or choose stratify=None.\")\n",
    "\n",
    "# Maintenant effectuer le train_test_split avec stratification sûre\n",
    "train_texts, test_texts, train_labels, test_labels = train_test_split(\n",
    "    df_labeled['text'],\n",
    "    df_labeled['period'],\n",
    "    test_size=0.2,       # 20% pour le test\n",
    "    random_state=42,     # Pour la reproductibilité\n",
    "    stratify=df_labeled['period'] # Pour garder la même distribution de périodes\n",
    ")\n",
    "\n",
    "print(f\"Taille de l'ensemble d'entraînement : {len(train_texts)} textes.\")\n",
    "print(f\"Taille de l'ensemble de test : {len(test_texts)} textes.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "c0aa9e6d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Vectorisation TF-IDF (max_features=5000)...\n",
      "Dimensions de la matrice TF-IDF d'entraînement : (4000, 5000)\n",
      "Dimensions de la matrice TF-IDF de test : (1000, 5000)\n",
      "Vectorisation TF-IDF terminée.\n"
     ]
    }
   ],
   "source": [
    "# --- Vectorisation TF-IDF ---\n",
    "N_FEATURES_TFIDF = 5000\n",
    "MIN_DF_TFIDF = 3\n",
    "MAX_DF_TFIDF = 0.85\n",
    "\n",
    "print(f\"Vectorisation TF-IDF (max_features={N_FEATURES_TFIDF})...\")\n",
    "\n",
    "# Initialiser le vectoriseur TF-IDF\n",
    "tfidf_vectorizer = TfidfVectorizer(\n",
    "    max_features=N_FEATURES_TFIDF,\n",
    "    ngram_range=(1, 1),  # Utiliser des unigrammes et bigrammes\n",
    "    min_df=MIN_DF_TFIDF,\n",
    "    max_df=MAX_DF_TFIDF\n",
    ")\n",
    "\n",
    "# Adapter (fit) le vectoriseur sur les données d'entraînement et transformer les deux ensembles\n",
    "X_train_tfidf = tfidf_vectorizer.fit_transform(train_texts)\n",
    "X_test_tfidf = tfidf_vectorizer.transform(test_texts)\n",
    "\n",
    "print(f\"Dimensions de la matrice TF-IDF d'entraînement : {X_train_tfidf.shape}\")\n",
    "print(f\"Dimensions de la matrice TF-IDF de test : {X_test_tfidf.shape}\")\n",
    "print(\"Vectorisation TF-IDF terminée.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "6573fc59",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Fonction d'entraînement (version simple sans suivi de la perte)\n",
    "def train_sgd_classifier(X_train, y_train, X_test, y_test, method_name=\"\"):\n",
    "    \"\"\"\n",
    "    Entraîne un classifieur SGD (SVM linéaire) et affiche les métriques de base.\n",
    "    \"\"\"\n",
    "    print(f\"\\\\n==================================================\")\n",
    "    print(f\"Entraînement du modèle SGD (SVM) sur {method_name}\")\n",
    "    print(f\"==================================================\\\\n\")\n",
    "\n",
    "    # Initialiser le classifieur\n",
    "    # 'log' = Régression Logistique. 'hinge' = SVM linéaire (par défaut)\n",
    "    # 'modified_huber' est robuste aux outliers et fournit des probabilités.\n",
    "    classifier = SGDClassifier(loss='modified_huber', random_state=42, max_iter=1000, tol=1e-3, n_jobs=-1)\n",
    "\n",
    "    # Entraîner le modèle\n",
    "    classifier.fit(X_train, y_train)\n",
    "\n",
    "    # Évaluation\n",
    "    y_pred = classifier.predict(X_test)\n",
    "    print(\"📄 Rapport de classification détaillé :\")\n",
    "    print(classification_report(y_test, y_pred, zero_division=0))\n",
    "    \n",
    "    return classifier"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "a87bd3ff",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\\n==================================================\n",
      "Entraînement du modèle SGD (SVM) sur TF-IDF (simple)\n",
      "==================================================\\n\n",
      "📄 Rapport de classification détaillé :\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "   1550-1599       0.00      0.00      0.00         1\n",
      "   1600-1649       0.56      0.77      0.65        13\n",
      "   1650-1699       0.00      0.00      0.00         9\n",
      "   1700-1749       0.71      0.50      0.59        24\n",
      "   1750-1799       0.75      0.75      0.75        76\n",
      "   1800-1849       0.74      0.69      0.71       213\n",
      "   1850-1899       0.72      0.80      0.76       472\n",
      "   1900-1949       0.61      0.54      0.57       182\n",
      "   1950-1999       0.80      0.44      0.57         9\n",
      "   2000-2049       0.00      0.00      0.00         1\n",
      "\n",
      "    accuracy                           0.71      1000\n",
      "   macro avg       0.49      0.45      0.46      1000\n",
      "weighted avg       0.70      0.71      0.70      1000\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Entraîner et évaluer avec TF-IDF (méthode simple)\n",
    "model_tfidf_sgd = train_sgd_classifier(\n",
    "    X_train_tfidf, train_labels,\n",
    "    X_test_tfidf, test_labels,\n",
    "    \"TF-IDF (simple)\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "a738a063",
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "\n",
    "def sample_text_by_segment(text, n_words=100):\n",
    "    \"\"\"\n",
    "    Échantillonne N mots du début, du milieu et de la fin du texte,\n",
    "    plus N mots aléatoires pour capturer le style global.\n",
    "    Total = 4 * n_words.\n",
    "    \"\"\"\n",
    "    tokens = text.split()\n",
    "    if len(tokens) <= 4 * n_words:\n",
    "        return text\n",
    "\n",
    "    # Début, milieu, fin\n",
    "    start_tokens = tokens[:n_words]\n",
    "    middle_start = max(0, (len(tokens) // 2) - (n_words // 2))\n",
    "    middle_tokens = tokens[middle_start : middle_start + n_words]\n",
    "    end_tokens = tokens[-n_words:]\n",
    "\n",
    "    # Indices déjà utilisés\n",
    "    used_indices = set(range(n_words)) | \\\n",
    "                   set(range(len(tokens) - n_words, len(tokens))) | \\\n",
    "                   set(range(middle_start, middle_start + n_words))\n",
    "\n",
    "    # Indices restants\n",
    "    remaining_indices = [i for i in range(len(tokens)) if i not in used_indices]\n",
    "    \n",
    "    if len(remaining_indices) < n_words:\n",
    "        random_tokens = [tokens[i] for i in remaining_indices]\n",
    "    else:\n",
    "        random_tokens = [tokens[i] for i in sorted(random.sample(remaining_indices, n_words))]\n",
    "\n",
    "    # Combiner tous les échantillons\n",
    "    combined_tokens = start_tokens + middle_tokens + end_tokens + random_tokens\n",
    "    \n",
    "    return \" \".join(combined_tokens)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "cbaf6032",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Charger un modèle CamemBERT léger, spécialisé dans la création d'embeddings\n",
    "embedding_model = SentenceTransformer('dangvantuan/sentence-camembert-base')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "fd9690ef",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Fonction pour créer les embeddings par lots (pour optimiser l'usage de la RAM)\n",
    "def create_embeddings_batch(texts, model, batch_size=64):\n",
    "    \"\"\"\n",
    "    Crée les embeddings pour une liste de textes en utilisant le modèle fourni,\n",
    "    en traitant les textes par lots.\n",
    "    \"\"\"\n",
    "    all_embeddings = []\n",
    "    \n",
    "    # Traiter par lots\n",
    "    for i in range(0, len(texts), batch_size):\n",
    "        batch_texts = texts[i : i + batch_size]\n",
    "        # Créer les embeddings pour le lot\n",
    "        batch_embeddings = model.encode(batch_texts, show_progress_bar=False, convert_to_numpy=True)\n",
    "        all_embeddings.append(batch_embeddings)\n",
    "        \n",
    "        # Nettoyage mémoire (optionnel mais peut aider sur les grosses boucles)\n",
    "        # gc.collect() \n",
    "\n",
    "    # Concaténer les résultats des lots\n",
    "    return np.vstack(all_embeddings)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "0d4244c6",
   "metadata": {},
   "outputs": [],
   "source": [
    "import math\n",
    "from sklearn.metrics import log_loss\n",
    "\n",
    "def train_sgd_with_loss_tracking(X_train, X_test, y_train, y_test, method_name=\"\", n_epochs=50):\n",
    "    \"\"\"\n",
    "    Entraîne un classifieur SGD (loss='log' - Régression Logistique)\n",
    "    en utilisant partial_fit pour suivre la perte (loss) à chaque époque.\n",
    "    \"\"\"\n",
    "    print(f\"\\\\n==================================================\")\n",
    "    print(f\"Entraînement avec suivi de la perte ({method_name})\")\n",
    "    print(f\"==================================================\\\\n\")\n",
    "\n",
    "    # Utiliser 'log_loss' pour la régression logistique, ce qui permet d'utiliser predict_proba et log_loss\n",
    "    classifier = SGDClassifier(loss='log_loss', random_state=42, max_iter=1, tol=None, n_jobs=-1, warm_start=True)\n",
    "    \n",
    "    train_loss_history = []\n",
    "    test_loss_history = []\n",
    "\n",
    "    # --- CORRECTION 1 : Obtenir la liste complète de TOUS les labels ---\n",
    "    # On combine les labels du train et du test pour être sûr de n'en oublier aucun.\n",
    "    all_classes = np.unique(np.concatenate((y_train, y_test)))\n",
    "\n",
    "    print(f\"Début de l'entraînement pour {n_epochs} époques...\")\n",
    "    for epoch in range(n_epochs):\n",
    "        classifier.partial_fit(X_train, y_train, classes=all_classes)\n",
    "        \n",
    "        y_train_pred_proba = classifier.predict_proba(X_train)\n",
    "        y_test_pred_proba = classifier.predict_proba(X_test)\n",
    "        \n",
    "        # --- CORRECTION 2 : Passer `labels=all_classes` à log_loss ---\n",
    "        train_loss = log_loss(y_train, y_train_pred_proba, labels=all_classes)\n",
    "        test_loss = log_loss(y_test, y_test_pred_proba, labels=all_classes)\n",
    "        \n",
    "        train_loss_history.append(train_loss)\n",
    "        test_loss_history.append(test_loss)\n",
    "        \n",
    "        print(f\"Époque {epoch+1}/{n_epochs} - Perte entraînement: {train_loss:.4f} - Perte test: {test_loss:.4f}\")\n",
    "\n",
    "    # Visualisation de la fonction de perte\n",
    "    plt.figure(figsize=(10, 5))\n",
    "    plt.plot(train_loss_history, label=\"Perte d'entraînement (Train Loss)\")\n",
    "    plt.plot(test_loss_history, label=\"Perte de validation (Test Loss)\")\n",
    "    plt.title(f\"Évolution de la fonction de perte ({method_name})\")\n",
    "    plt.xlabel(\"Époques\")\n",
    "    plt.ylabel(\"Log Loss\")\n",
    "    plt.legend()\n",
    "    plt.grid(True)\n",
    "    plt.show()\n",
    "\n",
    "    # Évaluation finale sur l'ensemble de test\n",
    "    print(\"\\\\n------ ÉVALUATION FINALE ------\")\n",
    "    y_pred = classifier.predict(X_test)\n",
    "    \n",
    "    print(\"📄 Rapport de classification détaillé :\")\n",
    "    print(classification_report(y_test, y_pred, zero_division=0)) # Ajout de zero_division pour éviter les warnings\n",
    "    \n",
    "    return classifier"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "30e81f1e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Échantillonnage des textes pour les embeddings...\n",
      "Texte original (exemple) :\\nmémoire sdr dilution samg éther sulferïque soufré moyen préventif cura f choléra docteur roux paris imprimerie bailly divry plage sorëonnej mémoire dilution sang éther sulfurique soufré moyen préventif curatif do choléra r ii roux lorsque entrepris recherches conservation matières animales gt seul but arriver pratique embaumements découverte nouveaux antiseptiques tenais toute chose approfondir phénomènes fermentation putride puiser nouveaux enseignements étude cadavre mine féconde été suffisamm...\n",
      "\\nTexte échantillonné (exemple) :\\nmémoire sdr dilution samg éther sulferïque soufré moyen préventif cura f choléra docteur roux paris imprimerie bailly divry plage sorëonnej mémoire dilution sang éther sulfurique soufré moyen préventif curatif do choléra r ii roux lorsque entrepris recherches conservation matières animales gt seul but arriver pratique embaumements découverte nouveaux antiseptiques tenais toute chose approfondir phénomènes fermentation putride puiser nouveaux enseignements étude cadavre mine féconde été suffisamm...\n",
      "\\nLongueur moyenne texte original : 29960 mots\n",
      "Longueur moyenne texte échantillonné : 394 mots\n"
     ]
    }
   ],
   "source": [
    "print(\"Échantillonnage des textes pour les embeddings...\")\n",
    "N_WORDS_EMBEDDING = 400 # 100 * 4 segments\n",
    "\n",
    "# Appliquer l'échantillonnage\n",
    "train_texts_sampled = [sample_text_by_segment(text, n_words=100) for text in train_texts]\n",
    "test_texts_sampled = [sample_text_by_segment(text, n_words=100) for text in test_texts]\n",
    "\n",
    "print(f\"Texte original (exemple) :\\\\n{train_texts.iloc[0][:500]}...\")\n",
    "print(f\"\\\\nTexte échantillonné (exemple) :\\\\n{train_texts_sampled[0][:500]}...\")\n",
    "print(f\"\\\\nLongueur moyenne texte original : {np.mean([len(t.split()) for t in train_texts]):.0f} mots\")\n",
    "print(f\"Longueur moyenne texte échantillonné : {np.mean([len(t.split()) for t in train_texts_sampled]):.0f} mots\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "18462449",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Création des embeddings pour 4000 textes d'entraînement...\n",
      "Création des embeddings pour 1000 textes de test...\n",
      "\\nDimensions de la matrice d'Embeddings (entraînement) : (4000, 768)\n",
      "Dimensions de la matrice d'Embeddings (test) : (1000, 768)\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "3697"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# --- Création des Embeddings ---\n",
    "BATCH_SIZE = 128\n",
    "N_TRAIN = len(train_texts_sampled)\n",
    "N_TEST = len(test_texts_sampled)\n",
    "\n",
    "print(f\"Création des embeddings pour {N_TRAIN} textes d'entraînement...\")\n",
    "X_train_embedding = create_embeddings_batch(train_texts_sampled, embedding_model, batch_size=BATCH_SIZE)\n",
    "\n",
    "print(f\"Création des embeddings pour {N_TEST} textes de test...\")\n",
    "X_test_embedding = create_embeddings_batch(test_texts_sampled, embedding_model, batch_size=BATCH_SIZE)\n",
    "\n",
    "print(f\"\\\\nDimensions de la matrice d'Embeddings (entraînement) : {X_train_embedding.shape}\")\n",
    "print(f\"Dimensions de la matrice d'Embeddings (test) : {X_test_embedding.shape}\")\n",
    "\n",
    "# Nettoyer la mémoire (le modèle d'embedding n'est plus nécessaire)\n",
    "del embedding_model\n",
    "del train_texts_sampled\n",
    "del test_texts_sampled\n",
    "gc.collect()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "29c22acc",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\\n==================================================\n",
      "Entraînement avec suivi de la perte (TF-IDF)\n",
      "==================================================\\n\n",
      "Début de l'entraînement pour 50 époques...\n",
      "Époque 1/50 - Perte entraînement: 0.5986 - Perte test: 0.7902\n",
      "Époque 2/50 - Perte entraînement: 0.5790 - Perte test: 0.7754\n",
      "Époque 3/50 - Perte entraînement: 0.5723 - Perte test: 0.7676\n",
      "Époque 2/50 - Perte entraînement: 0.5790 - Perte test: 0.7754\n",
      "Époque 3/50 - Perte entraînement: 0.5723 - Perte test: 0.7676\n",
      "Époque 4/50 - Perte entraînement: 0.5687 - Perte test: 0.7629\n",
      "Époque 5/50 - Perte entraînement: 0.5664 - Perte test: 0.7598\n",
      "Époque 4/50 - Perte entraînement: 0.5687 - Perte test: 0.7629\n",
      "Époque 5/50 - Perte entraînement: 0.5664 - Perte test: 0.7598\n",
      "Époque 6/50 - Perte entraînement: 0.5649 - Perte test: 0.7576\n",
      "Époque 7/50 - Perte entraînement: 0.5637 - Perte test: 0.7560\n",
      "Époque 6/50 - Perte entraînement: 0.5649 - Perte test: 0.7576\n",
      "Époque 7/50 - Perte entraînement: 0.5637 - Perte test: 0.7560\n",
      "Époque 8/50 - Perte entraînement: 0.5629 - Perte test: 0.7548\n",
      "Époque 9/50 - Perte entraînement: 0.5623 - Perte test: 0.7538\n",
      "Époque 8/50 - Perte entraînement: 0.5629 - Perte test: 0.7548\n",
      "Époque 9/50 - Perte entraînement: 0.5623 - Perte test: 0.7538\n",
      "Époque 10/50 - Perte entraînement: 0.5619 - Perte test: 0.7531\n",
      "Époque 11/50 - Perte entraînement: 0.5616 - Perte test: 0.7524\n",
      "Époque 10/50 - Perte entraînement: 0.5619 - Perte test: 0.7531\n",
      "Époque 11/50 - Perte entraînement: 0.5616 - Perte test: 0.7524\n",
      "Époque 12/50 - Perte entraînement: 0.5613 - Perte test: 0.7519\n",
      "Époque 13/50 - Perte entraînement: 0.5611 - Perte test: 0.7514\n",
      "Époque 12/50 - Perte entraînement: 0.5613 - Perte test: 0.7519\n",
      "Époque 13/50 - Perte entraînement: 0.5611 - Perte test: 0.7514\n",
      "Époque 14/50 - Perte entraînement: 0.5609 - Perte test: 0.7510\n",
      "Époque 15/50 - Perte entraînement: 0.5607 - Perte test: 0.7507\n",
      "Époque 14/50 - Perte entraînement: 0.5609 - Perte test: 0.7510\n",
      "Époque 15/50 - Perte entraînement: 0.5607 - Perte test: 0.7507\n",
      "Époque 16/50 - Perte entraînement: 0.5606 - Perte test: 0.7504\n",
      "Époque 17/50 - Perte entraînement: 0.5605 - Perte test: 0.7501\n",
      "Époque 16/50 - Perte entraînement: 0.5606 - Perte test: 0.7504\n",
      "Époque 17/50 - Perte entraînement: 0.5605 - Perte test: 0.7501\n",
      "Époque 18/50 - Perte entraînement: 0.5604 - Perte test: 0.7499\n",
      "Époque 19/50 - Perte entraînement: 0.5604 - Perte test: 0.7497\n",
      "Époque 18/50 - Perte entraînement: 0.5604 - Perte test: 0.7499\n",
      "Époque 19/50 - Perte entraînement: 0.5604 - Perte test: 0.7497\n",
      "Époque 20/50 - Perte entraînement: 0.5603 - Perte test: 0.7495\n",
      "Époque 21/50 - Perte entraînement: 0.5602 - Perte test: 0.7493\n",
      "Époque 20/50 - Perte entraînement: 0.5603 - Perte test: 0.7495\n",
      "Époque 21/50 - Perte entraînement: 0.5602 - Perte test: 0.7493\n",
      "Époque 22/50 - Perte entraînement: 0.5602 - Perte test: 0.7492\n",
      "Époque 23/50 - Perte entraînement: 0.5602 - Perte test: 0.7491\n",
      "Époque 22/50 - Perte entraînement: 0.5602 - Perte test: 0.7492\n",
      "Époque 23/50 - Perte entraînement: 0.5602 - Perte test: 0.7491\n",
      "Époque 24/50 - Perte entraînement: 0.5601 - Perte test: 0.7489\n",
      "Époque 25/50 - Perte entraînement: 0.5601 - Perte test: 0.7488\n",
      "Époque 24/50 - Perte entraînement: 0.5601 - Perte test: 0.7489\n",
      "Époque 25/50 - Perte entraînement: 0.5601 - Perte test: 0.7488\n",
      "Époque 26/50 - Perte entraînement: 0.5601 - Perte test: 0.7487\n",
      "Époque 27/50 - Perte entraînement: 0.5600 - Perte test: 0.7486\n",
      "Époque 26/50 - Perte entraînement: 0.5601 - Perte test: 0.7487\n",
      "Époque 27/50 - Perte entraînement: 0.5600 - Perte test: 0.7486\n",
      "Époque 28/50 - Perte entraînement: 0.5600 - Perte test: 0.7485\n",
      "Époque 29/50 - Perte entraînement: 0.5600 - Perte test: 0.7485\n",
      "Époque 28/50 - Perte entraînement: 0.5600 - Perte test: 0.7485\n",
      "Époque 29/50 - Perte entraînement: 0.5600 - Perte test: 0.7485\n",
      "Époque 30/50 - Perte entraînement: 0.5600 - Perte test: 0.7484\n",
      "Époque 31/50 - Perte entraînement: 0.5600 - Perte test: 0.7483\n",
      "Époque 30/50 - Perte entraînement: 0.5600 - Perte test: 0.7484\n",
      "Époque 31/50 - Perte entraînement: 0.5600 - Perte test: 0.7483\n",
      "Époque 32/50 - Perte entraînement: 0.5600 - Perte test: 0.7482\n",
      "Époque 33/50 - Perte entraînement: 0.5599 - Perte test: 0.7482\n",
      "Époque 32/50 - Perte entraînement: 0.5600 - Perte test: 0.7482\n",
      "Époque 33/50 - Perte entraînement: 0.5599 - Perte test: 0.7482\n",
      "Époque 34/50 - Perte entraînement: 0.5599 - Perte test: 0.7481\n",
      "Époque 35/50 - Perte entraînement: 0.5599 - Perte test: 0.7481\n",
      "Époque 34/50 - Perte entraînement: 0.5599 - Perte test: 0.7481\n",
      "Époque 35/50 - Perte entraînement: 0.5599 - Perte test: 0.7481\n",
      "Époque 36/50 - Perte entraînement: 0.5599 - Perte test: 0.7480\n",
      "Époque 37/50 - Perte entraînement: 0.5599 - Perte test: 0.7480\n",
      "Époque 36/50 - Perte entraînement: 0.5599 - Perte test: 0.7480\n",
      "Époque 37/50 - Perte entraînement: 0.5599 - Perte test: 0.7480\n",
      "Époque 38/50 - Perte entraînement: 0.5599 - Perte test: 0.7480\n",
      "Époque 39/50 - Perte entraînement: 0.5599 - Perte test: 0.7479\n",
      "Époque 38/50 - Perte entraînement: 0.5599 - Perte test: 0.7480\n",
      "Époque 39/50 - Perte entraînement: 0.5599 - Perte test: 0.7479\n",
      "Époque 40/50 - Perte entraînement: 0.5599 - Perte test: 0.7479\n",
      "Époque 41/50 - Perte entraînement: 0.5599 - Perte test: 0.7478\n",
      "Époque 40/50 - Perte entraînement: 0.5599 - Perte test: 0.7479\n",
      "Époque 41/50 - Perte entraînement: 0.5599 - Perte test: 0.7478\n",
      "Époque 42/50 - Perte entraînement: 0.5599 - Perte test: 0.7478\n",
      "Époque 43/50 - Perte entraînement: 0.5599 - Perte test: 0.7478\n",
      "Époque 42/50 - Perte entraînement: 0.5599 - Perte test: 0.7478\n",
      "Époque 43/50 - Perte entraînement: 0.5599 - Perte test: 0.7478\n",
      "Époque 44/50 - Perte entraînement: 0.5599 - Perte test: 0.7478\n",
      "Époque 45/50 - Perte entraînement: 0.5599 - Perte test: 0.7477\n",
      "Époque 44/50 - Perte entraînement: 0.5599 - Perte test: 0.7478\n",
      "Époque 45/50 - Perte entraînement: 0.5599 - Perte test: 0.7477\n",
      "Époque 46/50 - Perte entraînement: 0.5599 - Perte test: 0.7477\n",
      "Époque 47/50 - Perte entraînement: 0.5599 - Perte test: 0.7477\n",
      "Époque 46/50 - Perte entraînement: 0.5599 - Perte test: 0.7477\n",
      "Époque 47/50 - Perte entraînement: 0.5599 - Perte test: 0.7477\n",
      "Époque 48/50 - Perte entraînement: 0.5599 - Perte test: 0.7477\n",
      "Époque 49/50 - Perte entraînement: 0.5599 - Perte test: 0.7476\n",
      "Époque 48/50 - Perte entraînement: 0.5599 - Perte test: 0.7477\n",
      "Époque 49/50 - Perte entraînement: 0.5599 - Perte test: 0.7476\n",
      "Époque 50/50 - Perte entraînement: 0.5599 - Perte test: 0.7476\n",
      "Époque 50/50 - Perte entraînement: 0.5599 - Perte test: 0.7476\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 42904 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\\n------ ÉVALUATION FINALE ------\n",
      "📄 Rapport de classification détaillé :\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "   1550-1599       0.00      0.00      0.00         1\n",
      "   1600-1649       0.60      0.92      0.73        13\n",
      "   1650-1699       0.00      0.00      0.00         9\n",
      "   1700-1749       0.69      0.38      0.49        24\n",
      "   1750-1799       0.74      0.75      0.75        76\n",
      "   1800-1849       0.80      0.64      0.71       213\n",
      "   1850-1899       0.69      0.89      0.78       472\n",
      "   1900-1949       0.65      0.37      0.47       182\n",
      "   1950-1999       1.00      0.11      0.20         9\n",
      "   2000-2049       0.00      0.00      0.00         1\n",
      "\n",
      "    accuracy                           0.70      1000\n",
      "   macro avg       0.52      0.41      0.41      1000\n",
      "weighted avg       0.70      0.70      0.68      1000\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Entraîner et évaluer avec TF-IDF et suivi de la perte\n",
    "model_tfidf_sgd_loss = train_sgd_with_loss_tracking(\n",
    "    X_train_tfidf, X_test_tfidf,\n",
    "    train_labels, test_labels,\n",
    "    \"TF-IDF\",\n",
    "    n_epochs=50 # Utiliser 50 époques comme pour les embeddings pour comparer\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "7b2a7bdd",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\\n==================================================\n",
      "Entraînement avec suivi de la perte (Embeddings (CamemBERT))\n",
      "==================================================\\n\n",
      "Début de l'entraînement pour 50 époques...\n",
      "Époque 1/50 - Perte entraînement: 4.0103 - Perte test: 4.3804\n",
      "Époque 2/50 - Perte entraînement: 1.7525 - Perte test: 2.0926\n",
      "Époque 3/50 - Perte entraînement: 1.2791 - Perte test: 1.5853\n",
      "Époque 4/50 - Perte entraînement: 1.0691 - Perte test: 1.3529\n",
      "Époque 5/50 - Perte entraînement: 0.9635 - Perte test: 1.2339\n",
      "Époque 3/50 - Perte entraînement: 1.2791 - Perte test: 1.5853\n",
      "Époque 4/50 - Perte entraînement: 1.0691 - Perte test: 1.3529\n",
      "Époque 5/50 - Perte entraînement: 0.9635 - Perte test: 1.2339\n",
      "Époque 6/50 - Perte entraînement: 0.9024 - Perte test: 1.1635\n",
      "Époque 7/50 - Perte entraînement: 0.8633 - Perte test: 1.1175\n",
      "Époque 8/50 - Perte entraînement: 0.8362 - Perte test: 1.0850\n",
      "Époque 6/50 - Perte entraînement: 0.9024 - Perte test: 1.1635\n",
      "Époque 7/50 - Perte entraînement: 0.8633 - Perte test: 1.1175\n",
      "Époque 8/50 - Perte entraînement: 0.8362 - Perte test: 1.0850\n",
      "Époque 9/50 - Perte entraînement: 0.8164 - Perte test: 1.0608\n",
      "Époque 10/50 - Perte entraînement: 0.8013 - Perte test: 1.0421\n",
      "Époque 11/50 - Perte entraînement: 0.7895 - Perte test: 1.0271\n",
      "Époque 9/50 - Perte entraînement: 0.8164 - Perte test: 1.0608\n",
      "Époque 10/50 - Perte entraînement: 0.8013 - Perte test: 1.0421\n",
      "Époque 11/50 - Perte entraînement: 0.7895 - Perte test: 1.0271\n",
      "Époque 12/50 - Perte entraînement: 0.7800 - Perte test: 1.0150\n",
      "Époque 13/50 - Perte entraînement: 0.7724 - Perte test: 1.0050\n",
      "Époque 14/50 - Perte entraînement: 0.7661 - Perte test: 0.9966\n",
      "Époque 12/50 - Perte entraînement: 0.7800 - Perte test: 1.0150\n",
      "Époque 13/50 - Perte entraînement: 0.7724 - Perte test: 1.0050\n",
      "Époque 14/50 - Perte entraînement: 0.7661 - Perte test: 0.9966\n",
      "Époque 15/50 - Perte entraînement: 0.7609 - Perte test: 0.9896\n",
      "Époque 16/50 - Perte entraînement: 0.7566 - Perte test: 0.9837\n",
      "Époque 17/50 - Perte entraînement: 0.7530 - Perte test: 0.9786\n",
      "Époque 15/50 - Perte entraînement: 0.7609 - Perte test: 0.9896\n",
      "Époque 16/50 - Perte entraînement: 0.7566 - Perte test: 0.9837\n",
      "Époque 17/50 - Perte entraînement: 0.7530 - Perte test: 0.9786\n",
      "Époque 18/50 - Perte entraînement: 0.7501 - Perte test: 0.9743\n",
      "Époque 19/50 - Perte entraînement: 0.7476 - Perte test: 0.9706\n",
      "Époque 20/50 - Perte entraînement: 0.7455 - Perte test: 0.9674\n",
      "Époque 18/50 - Perte entraînement: 0.7501 - Perte test: 0.9743\n",
      "Époque 19/50 - Perte entraînement: 0.7476 - Perte test: 0.9706\n",
      "Époque 20/50 - Perte entraînement: 0.7455 - Perte test: 0.9674\n",
      "Époque 21/50 - Perte entraînement: 0.7438 - Perte test: 0.9646\n",
      "Époque 22/50 - Perte entraînement: 0.7424 - Perte test: 0.9622\n",
      "Époque 23/50 - Perte entraînement: 0.7412 - Perte test: 0.9602\n",
      "Époque 21/50 - Perte entraînement: 0.7438 - Perte test: 0.9646\n",
      "Époque 22/50 - Perte entraînement: 0.7424 - Perte test: 0.9622\n",
      "Époque 23/50 - Perte entraînement: 0.7412 - Perte test: 0.9602\n",
      "Époque 24/50 - Perte entraînement: 0.7402 - Perte test: 0.9583\n",
      "Époque 25/50 - Perte entraînement: 0.7394 - Perte test: 0.9568\n",
      "Époque 26/50 - Perte entraînement: 0.7388 - Perte test: 0.9554\n",
      "Époque 24/50 - Perte entraînement: 0.7402 - Perte test: 0.9583\n",
      "Époque 25/50 - Perte entraînement: 0.7394 - Perte test: 0.9568\n",
      "Époque 26/50 - Perte entraînement: 0.7388 - Perte test: 0.9554\n",
      "Époque 27/50 - Perte entraînement: 0.7383 - Perte test: 0.9541\n",
      "Époque 28/50 - Perte entraînement: 0.7378 - Perte test: 0.9531\n",
      "Époque 29/50 - Perte entraînement: 0.7375 - Perte test: 0.9521\n",
      "Époque 27/50 - Perte entraînement: 0.7383 - Perte test: 0.9541\n",
      "Époque 28/50 - Perte entraînement: 0.7378 - Perte test: 0.9531\n",
      "Époque 29/50 - Perte entraînement: 0.7375 - Perte test: 0.9521\n",
      "Époque 30/50 - Perte entraînement: 0.7373 - Perte test: 0.9512\n",
      "Époque 31/50 - Perte entraînement: 0.7371 - Perte test: 0.9505\n",
      "Époque 32/50 - Perte entraînement: 0.7369 - Perte test: 0.9498\n",
      "Époque 30/50 - Perte entraînement: 0.7373 - Perte test: 0.9512\n",
      "Époque 31/50 - Perte entraînement: 0.7371 - Perte test: 0.9505\n",
      "Époque 32/50 - Perte entraînement: 0.7369 - Perte test: 0.9498\n",
      "Époque 33/50 - Perte entraînement: 0.7368 - Perte test: 0.9492\n",
      "Époque 34/50 - Perte entraînement: 0.7368 - Perte test: 0.9486\n",
      "Époque 35/50 - Perte entraînement: 0.7367 - Perte test: 0.9481\n",
      "Époque 33/50 - Perte entraînement: 0.7368 - Perte test: 0.9492\n",
      "Époque 34/50 - Perte entraînement: 0.7368 - Perte test: 0.9486\n",
      "Époque 35/50 - Perte entraînement: 0.7367 - Perte test: 0.9481\n",
      "Époque 36/50 - Perte entraînement: 0.7367 - Perte test: 0.9476\n",
      "Époque 37/50 - Perte entraînement: 0.7367 - Perte test: 0.9471\n",
      "Époque 38/50 - Perte entraînement: 0.7368 - Perte test: 0.9467\n",
      "Époque 36/50 - Perte entraînement: 0.7367 - Perte test: 0.9476\n",
      "Époque 37/50 - Perte entraînement: 0.7367 - Perte test: 0.9471\n",
      "Époque 38/50 - Perte entraînement: 0.7368 - Perte test: 0.9467\n",
      "Époque 39/50 - Perte entraînement: 0.7368 - Perte test: 0.9463\n",
      "Époque 40/50 - Perte entraînement: 0.7368 - Perte test: 0.9460\n",
      "Époque 41/50 - Perte entraînement: 0.7369 - Perte test: 0.9456\n",
      "Époque 39/50 - Perte entraînement: 0.7368 - Perte test: 0.9463\n",
      "Époque 40/50 - Perte entraînement: 0.7368 - Perte test: 0.9460\n",
      "Époque 41/50 - Perte entraînement: 0.7369 - Perte test: 0.9456\n",
      "Époque 42/50 - Perte entraînement: 0.7369 - Perte test: 0.9453\n",
      "Époque 43/50 - Perte entraînement: 0.7369 - Perte test: 0.9450\n",
      "Époque 44/50 - Perte entraînement: 0.7370 - Perte test: 0.9447\n",
      "Époque 42/50 - Perte entraînement: 0.7369 - Perte test: 0.9453\n",
      "Époque 43/50 - Perte entraînement: 0.7369 - Perte test: 0.9450\n",
      "Époque 44/50 - Perte entraînement: 0.7370 - Perte test: 0.9447\n",
      "Époque 45/50 - Perte entraînement: 0.7370 - Perte test: 0.9444\n",
      "Époque 46/50 - Perte entraînement: 0.7371 - Perte test: 0.9441\n",
      "Époque 47/50 - Perte entraînement: 0.7371 - Perte test: 0.9439\n",
      "Époque 45/50 - Perte entraînement: 0.7370 - Perte test: 0.9444\n",
      "Époque 46/50 - Perte entraînement: 0.7371 - Perte test: 0.9441\n",
      "Époque 47/50 - Perte entraînement: 0.7371 - Perte test: 0.9439\n",
      "Époque 48/50 - Perte entraînement: 0.7372 - Perte test: 0.9436\n",
      "Époque 49/50 - Perte entraînement: 0.7372 - Perte test: 0.9433\n",
      "Époque 50/50 - Perte entraînement: 0.7372 - Perte test: 0.9431\n",
      "Époque 48/50 - Perte entraînement: 0.7372 - Perte test: 0.9436\n",
      "Époque 49/50 - Perte entraînement: 0.7372 - Perte test: 0.9433\n",
      "Époque 50/50 - Perte entraînement: 0.7372 - Perte test: 0.9431\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 51820 characters]",
      "text/plain": [
       "<Figure size 1000x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\\n------ ÉVALUATION FINALE ------\n",
      "📄 Rapport de classification détaillé :\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "   1550-1599       0.00      0.00      0.00         1\n",
      "   1600-1649       1.00      0.54      0.70        13\n",
      "   1650-1699       0.17      0.11      0.13         9\n",
      "   1700-1749       0.82      0.38      0.51        24\n",
      "   1750-1799       0.67      0.62      0.64        76\n",
      "   1800-1849       0.54      0.54      0.54       213\n",
      "   1850-1899       0.65      0.70      0.67       472\n",
      "   1900-1949       0.50      0.51      0.51       182\n",
      "   1950-1999       1.00      0.22      0.36         9\n",
      "   2000-2049       0.00      0.00      0.00         1\n",
      "\n",
      "    accuracy                           0.60      1000\n",
      "   macro avg       0.53      0.36      0.41      1000\n",
      "weighted avg       0.61      0.60      0.60      1000\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Entraîner et évaluer avec Embeddings et suivi de la perte\n",
    "model_embedding_sgd_loss = train_sgd_with_loss_tracking(\n",
    "    X_train_embedding, X_test_embedding,\n",
    "    train_labels, test_labels,\n",
    "    \"Embeddings (CamemBERT)\",\n",
    "    n_epochs=50\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "9f8650a4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def predict_random_text(data_source_df, vectorizer, model_tfidf, model_embedding):\n",
    "    \"\"\"\n",
    "    Prend un texte aléatoire depuis le DataFrame source (df_labeled),\n",
    "    le nettoie (si nécessaire, mais il l'est déjà), le vectorise (TF-IDF),\n",
    "    crée son embedding (après échantillonnage), et affiche les prédictions\n",
    "    des deux modèles.\n",
    "    \"\"\"\n",
    "    \n",
    "    # Récupérer un échantillon aléatoire\n",
    "    random_sample_df = data_source_df.sample(1)\n",
    "    \n",
    "    # Extraire le texte et le vrai label\n",
    "    # .iloc[0] pour obtenir les valeurs de la première ligne\n",
    "    text_to_predict = random_sample_df['text'].iloc[0]\n",
    "    true_label = random_sample_df['period'].iloc[0]\n",
    "\n",
    "    print(f\"Texte : {text_to_predict[:250]}\")\n",
    "    print(f\"Période réelle : {true_label}\")\n",
    "\n",
    "    # --- Prédiction TF-IDF ---\n",
    "    text_tfidf = vectorizer.transform([text_to_predict])\n",
    "    prediction_tfidf = model_tfidf.predict(text_tfidf)[0]\n",
    "    print(f\"TF-IDF : {prediction_tfidf}\")\n",
    "\n",
    "    # --- Prédiction Embeddings ---\n",
    "    text_sampled_emb = sample_text_by_segment(text_to_predict, n_words=100)\n",
    "    try:\n",
    "        embedding_model\n",
    "    except NameError:\n",
    "        print(\"Rechargement du modèle d'embedding pour la prédiction...\")\n",
    "        embedding_model = SentenceTransformer('dangvantuan/sentence-camembert-base')\n",
    "    \n",
    "    text_embedding = embedding_model.encode([text_sampled_emb], show_progress_bar=False)\n",
    "    \n",
    "    prediction_embedding = model_embedding.predict(text_embedding)[0]\n",
    "    print(f\"Embeddings : {prediction_embedding}s\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "659ad949",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Texte : seconp tirage tr ks gmenté tire soixante dix exemplaires vingt seulement vendus notices bibliographiques famille pidot seules partie tome ii do iii partie vhutoirt livre france paris imprimé pare thdnot rne racine ext h ii ijustoiiu gt u jvhe fhaxce \n",
      "Période réelle : 1850-1899\n",
      "TF-IDF : 1850-1899\n",
      "Rechargement du modèle d'embedding pour la prédiction...\n",
      "Embeddings : 1850-1899s\n",
      "Embeddings : 1850-1899s\n"
     ]
    }
   ],
   "source": [
    "# --- Lancer le test ---\n",
    "# Utilisez df_labeled qui contient les données prétraitées\n",
    "predict_random_text(df_labeled, tfidf_vectorizer, model_tfidf_sgd_loss, model_embedding_sgd_loss)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "b84c20c4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def test_multiple_predictions(data_source_df, vectorizer, model_tfidf, model_embedding, num_tests=20):\n",
    "    \"\"\"\n",
    "    Exécute N tests de prédiction aléatoire et compare les résultats.\n",
    "    \"\"\"\n",
    "    print(f\"--- Lancement de {num_tests} essais de prédiction ---\")\n",
    "    \n",
    "    correct_tfidf = 0\n",
    "    correct_embedding = 0\n",
    "    \n",
    "    # Recharger le modèle d'embedding une seule fois\n",
    "    try:\n",
    "        embedding_model\n",
    "    except NameError:\n",
    "        embedding_model = SentenceTransformer('dangvantuan/sentence-camembert-base')\n",
    "\n",
    "    for i in range(num_tests):\n",
    "        # Récupérer un échantillon\n",
    "        sample_df = data_source_df.sample(1)\n",
    "        text = sample_df['text'].iloc[0]\n",
    "        true_label = sample_df['period'].iloc[0]\n",
    "\n",
    "        # TF-IDF\n",
    "        text_tfidf = vectorizer.transform([text])\n",
    "        pred_tfidf = model_tfidf.predict(text_tfidf)[0]\n",
    "        \n",
    "        # Embedding\n",
    "        text_sampled = sample_text_by_segment(text, n_words=100)\n",
    "        text_emb = embedding_model.encode([text_sampled], show_progress_bar=False)\n",
    "        pred_emb = model_embedding.predict(text_emb)[0]\n",
    "\n",
    "        if pred_tfidf == true_label:\n",
    "            correct_tfidf += 1\n",
    "        if pred_emb == true_label:\n",
    "            correct_embedding += 1\n",
    "            \n",
    "        print(f\"Essai {i+1}/{num_tests} | Réel : {true_label} | TF-IDF : {pred_tfidf} | Embeddings : {pred_emb}\")\n",
    "\n",
    "    print(\"\\\\n--- Résultats des tests ---\")\n",
    "    print(f\"TF-IDF Accuracy : {(correct_tfidf / num_tests) * 100:.2f}% ({correct_tfidf}/{num_tests})\")\n",
    "    print(f\"Embeddings Accuracy : {(correct_embedding / num_tests) * 100:.2f}% ({correct_embedding}/{num_tests})\")\n",
    "    \n",
    "    # Retourner les scores pour le graphique\n",
    "    return (correct_tfidf / num_tests), (correct_embedding / num_tests)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "85e8455b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "--- Lancement de 20 essais de prédiction ---\n",
      "Essai 1/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 1/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 2/20 | Réel : 1900-1949 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 2/20 | Réel : 1900-1949 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 3/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 3/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 4/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 4/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 5/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 6/20 | Réel : 1700-1749 | TF-IDF : 1700-1749 | Embeddings : 1700-1749\n",
      "Essai 5/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 6/20 | Réel : 1700-1749 | TF-IDF : 1700-1749 | Embeddings : 1700-1749\n",
      "Essai 7/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 7/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 8/20 | Réel : 1750-1799 | TF-IDF : 1750-1799 | Embeddings : 1750-1799\n",
      "Essai 8/20 | Réel : 1750-1799 | TF-IDF : 1750-1799 | Embeddings : 1750-1799\n",
      "Essai 9/20 | Réel : 1900-1949 | TF-IDF : 1900-1949 | Embeddings : 1900-1949\n",
      "Essai 10/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1800-1849\n",
      "Essai 9/20 | Réel : 1900-1949 | TF-IDF : 1900-1949 | Embeddings : 1900-1949\n",
      "Essai 10/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1800-1849\n",
      "Essai 11/20 | Réel : 1800-1849 | TF-IDF : 1800-1849 | Embeddings : 1850-1899\n",
      "Essai 11/20 | Réel : 1800-1849 | TF-IDF : 1800-1849 | Embeddings : 1850-1899\n",
      "Essai 12/20 | Réel : 1800-1849 | TF-IDF : 1800-1849 | Embeddings : 1850-1899\n",
      "Essai 12/20 | Réel : 1800-1849 | TF-IDF : 1800-1849 | Embeddings : 1850-1899\n",
      "Essai 13/20 | Réel : 1800-1849 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 13/20 | Réel : 1800-1849 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 14/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1900-1949\n",
      "Essai 14/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1900-1949\n",
      "Essai 15/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 15/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 16/20 | Réel : 1750-1799 | TF-IDF : 1750-1799 | Embeddings : 1750-1799\n",
      "Essai 16/20 | Réel : 1750-1799 | TF-IDF : 1750-1799 | Embeddings : 1750-1799\n",
      "Essai 17/20 | Réel : 1900-1949 | TF-IDF : 1900-1949 | Embeddings : 1850-1899\n",
      "Essai 17/20 | Réel : 1900-1949 | TF-IDF : 1900-1949 | Embeddings : 1850-1899\n",
      "Essai 18/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 18/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 19/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 19/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "Essai 20/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "\\n--- Résultats des tests ---\n",
      "TF-IDF Accuracy : 90.00% (18/20)\n",
      "Embeddings Accuracy : 65.00% (13/20)\n",
      "Essai 20/20 | Réel : 1850-1899 | TF-IDF : 1850-1899 | Embeddings : 1850-1899\n",
      "\\n--- Résultats des tests ---\n",
      "TF-IDF Accuracy : 90.00% (18/20)\n",
      "Embeddings Accuracy : 65.00% (13/20)\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 35720 characters]",
      "text/plain": [
       "<Figure size 800x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Lancer 20 essais\n",
    "tfidf_acc, emb_acc = test_multiple_predictions(\n",
    "    df_labeled, \n",
    "    tfidf_vectorizer, \n",
    "    model_tfidf_sgd_loss, \n",
    "    model_embedding_sgd_loss, \n",
    "    num_tests=20\n",
    ")\n",
    "\n",
    "# Visualisation finale des résultats des tests\n",
    "plt.figure(figsize=(8, 6))\n",
    "models = ['TF-IDF', 'Embeddings (CamemBERT)']\n",
    "accuracies = [tfidf_acc, emb_acc]\n",
    "colors = ['skyblue', 'lightgreen']\n",
    "\n",
    "bars = plt.bar(models, accuracies, color=colors, edgecolor='black')\n",
    "\n",
    "plt.ylabel('Accuracy (sur 20 essais)', fontsize=12)\n",
    "plt.title('Comparaison de l\\'Accuracy sur 20 essais aléatoires', fontsize=16)\n",
    "plt.ylim(0, 1)\n",
    "\n",
    "# Ajouter les pourcentages sur les barres\n",
    "for bar in bars:\n",
    "    yval = bar.get_height()\n",
    "    plt.text(bar.get_x() + bar.get_width()/2.0, yval + 0.02, f'{yval*100:.0f}%', ha='center', va='bottom', fontsize=12)\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "a06eba48",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Génération des prédictions sur l'ensemble de test...\n",
      "Affichage des 1000 échantillons de test, triés par période réelle.\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 201116 characters]",
      "text/plain": [
       "<Figure size 2000x1600 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "print(\"Génération des prédictions sur l'ensemble de test...\")\n",
    "\n",
    "# 1. Obtenir les prédictions pour les deux modèles\n",
    "y_pred_tfidf = model_tfidf_sgd_loss.predict(X_test_tfidf)\n",
    "y_pred_embeddings = model_embedding_sgd_loss.predict(X_test_embedding)\n",
    "\n",
    "# 2. Obtenir les étiquettes réelles (test_labels est un objet Series)\n",
    "y_actual = test_labels\n",
    "\n",
    "# 3. Créer un DataFrame pour une manipulation facile\n",
    "df_results = pd.DataFrame({\n",
    "    'Actual': y_actual,\n",
    "    'Pred_TFIDF': y_pred_tfidf,\n",
    "    'Pred_Embeddings': y_pred_embeddings\n",
    "})\n",
    "\n",
    "# 4. Trier le DataFrame par la valeur réelle (chronologiquement)\n",
    "# reset_index(drop=True) crée un nouvel index de 0 à N,\n",
    "# qui servira d'axe X pour notre graphique.\n",
    "df_sorted_results = df_results.sort_values(by='Actual').reset_index(drop=True)\n",
    "\n",
    "print(f\"Affichage des {len(df_sorted_results)} échantillons de test, triés par période réelle.\")\n",
    "\n",
    "# 5. Créer les graphiques de comparaison\n",
    "# Nous créons 2 sous-graphiques (un pour chaque modèle)\n",
    "fig, (ax1, ax2) = plt.subplots(\n",
    "    nrows=2,         # 2 graphiques l'un en dessous de l'autre\n",
    "    ncols=1,         # 1 colonne\n",
    "    figsize=(20, 16), # Taille globale de la figure (largeur, hauteur)\n",
    "    sharex=True,     # L'axe X est partagé\n",
    "    sharey=True      # L'axe Y est partagé (mêmes périodes)\n",
    ")\n",
    "\n",
    "# --- Graphique 1 : TF-IDF ---\n",
    "\n",
    "# Ligne de base (Réel) : Nous la traçons comme une ligne pointillée\n",
    "ax1.plot(df_sorted_results.index, df_sorted_results['Actual'], \n",
    "         '--', # Style de ligne pointillé\n",
    "         color='blue', \n",
    "         label='Période Réelle',\n",
    "         alpha=0.6)\n",
    "\n",
    "# Prédictions (Points) : Nous les traçons comme des 'x' rouges\n",
    "# 'markevery=5' ne trace qu'un point sur 5 pour éviter un graphique surchargé\n",
    "ax1.plot(df_sorted_results.index, df_sorted_results['Pred_TFIDF'], \n",
    "         'x',  # Marqueur 'x'\n",
    "         color='red', \n",
    "         label='Prédiction (TF-IDF)',\n",
    "         alpha=0.8,\n",
    "         markevery=5) \n",
    "\n",
    "ax1.set_title(\"Comparaison Réel vs Prédiction (Modèle TF-IDF)\", fontsize=16)\n",
    "ax1.set_ylabel(\"Période\", fontsize=12)\n",
    "ax1.legend(loc='upper left')\n",
    "ax1.grid(axis='y', linestyle=':', alpha=0.7)\n",
    "\n",
    "\n",
    "# --- Graphique 2 : Embeddings ---\n",
    "\n",
    "# Ligne de base (Réel)\n",
    "ax2.plot(df_sorted_results.index, df_sorted_results['Actual'], \n",
    "         '--', \n",
    "         color='blue', \n",
    "         label='Période Réelle',\n",
    "         alpha=0.6)\n",
    "\n",
    "# Prédictions (Points)\n",
    "ax2.plot(df_sorted_results.index, df_sorted_results['Pred_Embeddings'], \n",
    "         'o',  # Marqueur 'o' (cercle)\n",
    "         color='green', \n",
    "         label='Prédiction (Embeddings)',\n",
    "         alpha=0.7,\n",
    "         markersize=4, # Taille des marqueurs\n",
    "         markevery=5)\n",
    "\n",
    "ax2.set_title(\"Comparaison Réel vs Prédiction (Modèle Embeddings)\", fontsize=16)\n",
    "ax2.set_xlabel(\"Échantillons de test (triés par période réelle)\", fontsize=12)\n",
    "ax2.set_ylabel(\"Période\", fontsize=12)\n",
    "ax2.legend(loc='upper left')\n",
    "ax2.grid(axis='y', linestyle=':', alpha=0.7)\n",
    "\n",
    "# Ajuster l'espacement pour éviter que les titres se chevauchent\n",
    "plt.tight_layout()\n",
    "\n",
    "# Afficher le graphique\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "projet_ia5",
   "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.11.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
