{
  "chapter": {
    "id": "tables-de-donnees",
    "level": "premiere",
    "theme": "Données",
    "title": "Tables de données",
    "description": "Représentation tabulaire des données, descripteurs\n(colonnes), enregistrements (lignes), opérations courantes\n(recherche, tri, sélection, fusion, jointure simple), en\nPython avec listes de dictionnaires ou listes de listes.",
    "prerequisites": [],
    "references": []
  },
  "questions": [
    {
      "id": "q01",
      "difficulty": 1,
      "skills": [
        "definition"
      ],
      "title": "Qu'est-ce qu'une table ?",
      "statement": "En informatique, qu'appelle-t-on une **table de données** ?",
      "options": [
        {
          "text": "Un fichier texte dont les valeurs sont séparées par des virgules",
          "correct": false,
          "feedback": "Erreur : c'est la description du format\n**CSV**, qui n'est qu'un moyen de stocker\nune table dans un fichier. La table\nelle-même est la structure abstraite\n(lignes et colonnes), indépendante de son\nformat d'enregistrement.\n"
        },
        {
          "text": "Un type primitif fondamental fourni nativement par Python",
          "correct": false,
          "feedback": "Erreur : Python n'a pas de type natif \"table\".\nOn peut la simuler avec des listes ou\ndictionnaires.\n"
        },
        {
          "text": "Un tableau de lignes (enregistrements) et de colonnes (descripteurs)",
          "correct": true,
          "feedback": "Bonne réponse : c'est la définition standard,\nidentique à celle d'un tableur ou d'une table\nde base de données. Chaque ligne décrit une\nentité (élève, livre...), chaque colonne un\nattribut (nom, âge...).\n"
        },
        {
          "text": "Une fonction Python qui renvoie un dictionnaire",
          "correct": false,
          "feedback": "Erreur : c'est éventuellement un moyen de\n**représenter** une table, pas la définition\nelle-même.\n"
        }
      ],
      "explanation": "Les tables sont la structure la plus simple et la\nplus répandue pour organiser des données : tableurs,\nbases relationnelles, fichiers CSV, DataFrames pandas\nreposent tous sur ce modèle."
    },
    {
      "id": "q02",
      "difficulty": 1,
      "skills": [
        "vocabulaire"
      ],
      "title": "Vocabulaire",
      "statement": "Comment appelle-t-on une **colonne** dans une table\nde données ?",
      "options": [
        {
          "text": "Une ligne",
          "correct": false,
          "feedback": "Erreur : confusion ligne/colonne. Une ligne\ndécrit une entité.\n"
        },
        {
          "text": "Une clé primaire",
          "correct": false,
          "feedback": "Erreur : une clé primaire est un **descripteur particulier** qui identifie de manière unique\nchaque ligne. Toutes les colonnes ne sont pas\ndes clés primaires.\n"
        },
        {
          "text": "Un enregistrement, c'est-à-dire une entité complète de la table",
          "correct": false,
          "feedback": "Erreur : un enregistrement est une **ligne**,\npas une colonne.\n"
        },
        {
          "text": "Un descripteur (ou attribut, ou champ)",
          "correct": true,
          "feedback": "Bonne réponse : la colonne décrit une propriété\ncommune à toutes les entités (par exemple\n\"nom\", \"age\", \"ville\"). Les termes\n\"descripteur\", \"attribut\", \"champ\" sont\nsynonymes selon le contexte.\n"
        }
      ],
      "explanation": "Vocabulaire à maîtriser : ligne = enregistrement\n= entité ; colonne = descripteur = attribut =\nchamp. Selon le manuel, le mot peut varier."
    },
    {
      "id": "q03",
      "difficulty": 1,
      "skills": [
        "representation-python"
      ],
      "title": "Liste de dictionnaires",
      "statement": "Comment représente-t-on couramment une table en\nPython (avec en-tête) ?",
      "options": [
        {
          "text": "Comme une seule et longue chaîne de caractères unique",
          "correct": false,
          "feedback": "Erreur : ce ne serait pas pratique pour\nmanipuler les données.\n"
        },
        {
          "text": "Comme une liste de dictionnaires, un par ligne",
          "correct": true,
          "feedback": "Bonne réponse : exemple\n`[{\"nom\":\"Alice\",\"age\":17}, {\"nom\":\"Bob\",\"age\":16}]`.\nC'est ce que renvoie `csv.DictReader` quand on\nle convertit en liste. Très lisible\n(`ligne[\"nom\"]`).\n"
        },
        {
          "text": "Comme une fonction",
          "correct": false,
          "feedback": "Erreur : une fonction décrit un calcul, pas\ndes données.\n"
        },
        {
          "text": "Comme un entier",
          "correct": false,
          "feedback": "Erreur : un entier ne stocke aucune\nstructure.\n"
        }
      ],
      "explanation": "Autres représentations possibles : liste de\ntuples, liste de listes, dict de listes (« colonne\npar colonne »). Le choix dépend de l'usage. Pour\nle NSI Première, on utilise principalement la liste\nde dictionnaires."
    },
    {
      "id": "q04",
      "difficulty": 1,
      "skills": [
        "acces-cellule"
      ],
      "title": "Accéder à une cellule",
      "statement": "Soit\n```python\ntable = [\n    {\"nom\": \"Alice\", \"age\": 17},\n    {\"nom\": \"Bob\", \"age\": 16}\n]\n```\nComment accéder à l'âge de Bob ?",
      "options": [
        {
          "text": "`table[1]['age']`",
          "correct": true,
          "feedback": "Bonne réponse : Bob est la deuxième ligne\n(indice 1), et `'age'` est le descripteur. On\nobtient `16`.\n"
        },
        {
          "text": "`table['Bob']['age']`",
          "correct": false,
          "feedback": "Erreur : `table` est une **liste**, pas un\ndictionnaire indexé par nom. Cela lève\n`TypeError`.\n"
        },
        {
          "text": "`table[2]['age']`",
          "correct": false,
          "feedback": "Erreur : indice **hors-borne**. Avec deux\nlignes, les indices valides sont 0 et 1, pas\n2 (`IndexError`).\n"
        },
        {
          "text": "`table.age[1]`",
          "correct": false,
          "feedback": "Erreur : aucune méthode ni attribut `.age` sur\nune liste (`AttributeError`).\n"
        }
      ],
      "explanation": "Schéma général : `table[indice_ligne][descripteur]`.\nL'ordre des accès reflète la structure : d'abord\nla liste, puis le dictionnaire."
    },
    {
      "id": "q05",
      "difficulty": 1,
      "skills": [
        "parcours"
      ],
      "title": "Parcourir une table",
      "statement": "Pour afficher tous les noms d'élèves d'une table\n(liste de dictionnaires avec une clé `\"nom\"`), quel\ncode utilise-t-on ?",
      "options": [
        {
          "text": "```python\nfor ligne in table:\n    print(ligne.nom)\n```\n",
          "correct": false,
          "feedback": "Erreur : un dictionnaire n'a pas d'attribut\n`.nom`, cette écriture lève `AttributeError`.\nLa valeur d'une clé s'obtient avec des\ncrochets : `ligne[\"nom\"]`.\n"
        },
        {
          "text": "```python\nfor ligne in table:\n    print(ligne[\"nom\"])\n```\n",
          "correct": true,
          "feedback": "Bonne réponse : la boucle parcourt chaque\ndictionnaire, et on accède à la valeur de la\nclé `\"nom\"`.\n"
        },
        {
          "text": "```python\nfor nom in table:\n    print(nom)\n```\n",
          "correct": false,
          "feedback": "Erreur : la variable `nom` recevrait chaque\n**dictionnaire** entier, pas le nom seulement.\n"
        },
        {
          "text": "```python\nfor ligne in table:\n    print(table[\"nom\"])\n```\n",
          "correct": false,
          "feedback": "Erreur : dans la boucle, c'est `ligne` qu'il\nfaut indexer. `table` est une liste, on ne\npeut pas l'indexer par une chaîne : `TypeError`.\n"
        }
      ],
      "explanation": "Schéma classique de parcours d'une liste de\ndictionnaires. À retenir."
    },
    {
      "id": "q06",
      "difficulty": 1,
      "skills": [
        "origine-donnees"
      ],
      "title": "Origine des tables",
      "statement": "D'où peut provenir une table de données dans un\nprojet réel ?",
      "options": [
        {
          "text": "D'un fichier CSV ou JSON, d'un tableur, d'une base, d'une API...",
          "correct": true,
          "feedback": "Bonne réponse : les tables sont partout en\ninformatique. Open Data (data.gouv.fr),\nextracts SQL, exports Excel, scraping web...\nautant de sources possibles.\n"
        },
        {
          "text": "Uniquement de données saisies à la main au clavier",
          "correct": false,
          "feedback": "Trop restrictif : une table peut aussi provenir\nd'un fichier, d'une base ou d'une API, sans aucune\nsaisie manuelle.\n"
        },
        {
          "text": "Uniquement d'un fichier CSV créé à la main",
          "correct": false,
          "feedback": "Trop restrictif : il existe de nombreuses\nautres sources.\n"
        },
        {
          "text": "Uniquement à partir d'une base de données relationnelle SQL",
          "correct": false,
          "feedback": "Trop restrictif : SQL est une source\nfréquente mais pas la seule.\n"
        }
      ],
      "explanation": "Une compétence clé du NSI : savoir lire des\ndonnées réelles (souvent en CSV ou JSON), les\ntransformer en table Python, puis les manipuler."
    },
    {
      "id": "q07",
      "difficulty": 1,
      "skills": [
        "recherche"
      ],
      "title": "Recherche par critère",
      "statement": "Pour trouver toutes les lignes où l'âge est\nstrictement supérieur à 16, quel code écrit-on ?",
      "options": [
        {
          "text": "```python\nfor ligne in table:\n    if ligne[\"age\"] > 16:\n        print(ligne)\n```\n",
          "correct": false,
          "feedback": "Cela **affiche** les lignes mais ne renvoie\npas une liste. Si la question est \"trouver\",\non attend une valeur de retour, pas un\naffichage.\n"
        },
        {
          "text": "```python\n[ligne for ligne in table if ligne[\"age\"] > 16]\n```\n",
          "correct": true,
          "feedback": "Bonne réponse : compréhension de liste avec\ncondition. Forme idiomatique en Python pour\nfiltrer.\n"
        },
        {
          "text": "```python\ntable.find(age > 16)\n```\n",
          "correct": false,
          "feedback": "Erreur : la méthode `.find()` n'existe pas\nsur les listes.\n"
        },
        {
          "text": "```python\ntable[ligne[\"age\"] > 16]\n```\n",
          "correct": false,
          "feedback": "Erreur : syntaxe invalide. `ligne` n'est pas\ndéfini hors d'une boucle.\n"
        }
      ],
      "explanation": "Compréhension de liste : schéma Python élégant\npour filtrer, transformer et combiner. Très\nutilisé sur les tables."
    },
    {
      "id": "q08",
      "difficulty": 1,
      "skills": [
        "csv-vers-table"
      ],
      "title": "Construire depuis CSV",
      "statement": "Quel est le moyen le plus simple de construire une\ntable (liste de dictionnaires) à partir d'un fichier\nCSV avec en-tête ?",
      "options": [
        {
          "text": "Lire le fichier ligne par ligne et coder à la main le découpage",
          "correct": false,
          "feedback": "Erreur : possible mais fastidieux et fragile\n(séparateur, guillemets, en-tête). Le module\n`csv` fait ce découpage pour nous.\n"
        },
        {
          "text": "`csv.load('data.csv')` (cette fonction existe-t-elle ?)",
          "correct": false,
          "feedback": "Erreur : cette fonction n'existe pas dans le\nmodule `csv` (`AttributeError`).\n"
        },
        {
          "text": "`list(csv.DictReader(open('data.csv', encoding='utf-8')))`",
          "correct": true,
          "feedback": "Bonne réponse : `csv.DictReader` produit un\nitérateur de dictionnaires (un par ligne, avec\npour clés les noms de l'en-tête) ; `list()` le\nmatérialise en table prête à l'emploi.\n"
        },
        {
          "text": "`list(csv.reader(open('data.csv', encoding='utf-8')))`",
          "correct": false,
          "feedback": "Erreur : `csv.reader` produit des **listes**,\npas des dictionnaires, et la ligne d'en-tête\nest lue comme les autres. Sur un fichier\n`nom,age` on obtient\n`[['nom', 'age'], ['Alice', '17'], ...]`, sans\naccès par descripteur.\n"
        }
      ],
      "explanation": "Idiome standard. Penser à fermer le fichier\nproprement avec un `with`, ou utiliser cette\nécriture compacte si le fichier est petit."
    },
    {
      "id": "q09",
      "difficulty": 1,
      "skills": [
        "taille"
      ],
      "title": "Nombre de lignes",
      "statement": "Comment obtenir le **nombre d'enregistrements**\n(lignes) d'une table représentée comme liste de\ndictionnaires ?",
      "options": [
        {
          "text": "`size(table)`",
          "correct": false,
          "feedback": "Erreur : pas de fonction `size` en Python\nstandard.\n"
        },
        {
          "text": "`len(table.keys())`",
          "correct": false,
          "feedback": "Erreur : `.keys()` n'existe pas sur une\nliste. Cela lève `AttributeError`.\n"
        },
        {
          "text": "`len(table)`",
          "correct": true,
          "feedback": "Bonne réponse : `len()` sur la liste donne le\nnombre de dictionnaires, donc de lignes.\n"
        },
        {
          "text": "`table.count`",
          "correct": false,
          "feedback": "Erreur : `.count` est une **méthode** (qui\ncompte les occurrences d'un élément), il faut\nl'appeler avec des parenthèses et un\nargument.\n"
        }
      ],
      "explanation": "Pour le nombre de **colonnes**, on regarde les\nclés du premier dictionnaire :\n`len(table[0])` (en supposant la table non vide)."
    },
    {
      "id": "q10",
      "difficulty": 1,
      "skills": [
        "vide"
      ],
      "title": "Table vide",
      "statement": "Que vaut `len(table)` pour une table vide ?",
      "options": [
        {
          "text": "Une exception est levée",
          "correct": false,
          "feedback": "Erreur : `len([])` renvoie `0` sans erreur.\n"
        },
        {
          "text": "`0`",
          "correct": true,
          "feedback": "Bonne réponse : `[]` (liste vide) contient\nzéro élément.\n"
        },
        {
          "text": "`-1`",
          "correct": false,
          "feedback": "Erreur : `len` n'est jamais négatif.\n"
        },
        {
          "text": "`None`",
          "correct": false,
          "feedback": "Erreur : `len` renvoie toujours un entier.\n"
        }
      ],
      "explanation": "Toujours penser au cas de la table vide dans les\ncalculs : `moyenne = total / len(table)` lèverait\n`ZeroDivisionError`."
    },
    {
      "id": "q11",
      "difficulty": 2,
      "skills": [
        "tri"
      ],
      "title": "Trier par descripteur",
      "statement": "On dispose de la fonction clé\n```python\ndef age(ligne):\n    return ligne[\"age\"]\n```\nComment trier la table par âge **croissant** ?",
      "options": [
        {
          "text": "`table.sort(key=age)`",
          "correct": true,
          "feedback": "Bonne réponse : `key` indique la fonction qui\ncalcule, pour chaque ligne, la valeur sur\nlaquelle comparer. Ici `age` renvoie l'âge de\nla ligne. Tri en place de la liste.\n"
        },
        {
          "text": "`table.sort(reverse=False)`",
          "correct": false,
          "feedback": "Erreur : `reverse=False` est déjà la valeur\npar défaut, et il manque `key` : sans lui,\n`sort` ne sait pas comparer deux\ndictionnaires entre eux (`TypeError`).\n"
        },
        {
          "text": "`sorted(table, by=age)`",
          "correct": false,
          "feedback": "Erreur : `sorted` n'a pas d'argument `by`\n(`TypeError`). C'est `key=` qu'il faut.\n"
        },
        {
          "text": "`table.sort(age)`",
          "correct": false,
          "feedback": "Erreur : `sort` n'accepte aucun argument\npositionnel (`TypeError`). La fonction de clé\ndoit être nommée : `key=age`.\n"
        }
      ],
      "explanation": "Pour trier sans modifier la table d'origine :\n`sorted(table, key=age)`. Pour un tri\n**décroissant**, ajouter `reverse=True`. On passe\nla fonction elle-même (`age`, sans parenthèses) :\nc'est `sort` qui l'appelle sur chaque ligne."
    },
    {
      "id": "q12",
      "difficulty": 2,
      "skills": [
        "selection-colonnes"
      ],
      "title": "Sélectionner des colonnes",
      "statement": "Comment construire une nouvelle table `resultat` ne\ncontenant que les colonnes `nom` et `age` de `table`\n(sans les autres descripteurs) ?",
      "options": [
        {
          "text": "```python\nresultat = []\nfor l in table:\n    resultat.append(l[[\"nom\", \"age\"]])\n```\n",
          "correct": false,
          "feedback": "Erreur : syntaxe inspirée de **pandas**, pas de\nPython standard. Un dictionnaire ne s'indexe pas\npar une liste de clés (`TypeError`) : il faut\nextraire chaque clé séparément.\n"
        },
        {
          "text": "```python\nresultat = {}\nfor l in table:\n    for k in [\"nom\", \"age\"]:\n        resultat[k] = l[k]\n```\n",
          "correct": false,
          "feedback": "Erreur : `resultat` est un **dictionnaire**\nunique dont on écrase les deux valeurs à chaque\nligne. On obtient le nom et l'âge de la dernière\nligne seulement, et non une table.\n"
        },
        {
          "text": "```python\nresultat = []\nfor l in table:\n    resultat.append({\"nom\": l[\"nom\"], \"age\": l[\"age\"]})\n```\n",
          "correct": true,
          "feedback": "Bonne réponse : pour chaque ligne, on construit\nun nouveau dictionnaire ne gardant que les deux\nclés voulues, et on l'ajoute à la nouvelle\ntable.\n"
        },
        {
          "text": "```python\nresultat = []\nfor l in table:\n    for k in [\"nom\", \"age\"]:\n        resultat.append({k: l[k]})\n```\n",
          "correct": false,
          "feedback": "Erreur : chaque tour ajoute un dictionnaire à\n**une seule** clé. La table obtenue compte deux\nfois plus de lignes que l'originale, le nom et\nl'âge d'un même élève étant séparés sur deux\nlignes.\n"
        }
      ],
      "explanation": "Cette opération consiste à **extraire un\nsous-ensemble de colonnes** d'une table (on parle\naussi de projection). C'est l'une des manipulations\nclassiques de tables, au même titre que la\nsélection de lignes, le tri ou la fusion. En\ncompréhension de liste :\n`[{\"nom\": l[\"nom\"], \"age\": l[\"age\"]} for l in table]`."
    },
    {
      "id": "q13",
      "difficulty": 2,
      "skills": [
        "fusion"
      ],
      "title": "Fusion de tables",
      "statement": "Soient deux tables `t1` et `t2` ayant les **mêmes descripteurs**. Comment obtenir une nouvelle table\ncontenant toutes les lignes de `t1` puis celles de\n`t2` ?",
      "options": [
        {
          "text": "`t1.append(t2)`",
          "correct": false,
          "feedback": "Attention : `append(t2)` ajouterait `t2`\ncomme **un seul élément** (une liste de\nlistes), pas en fusionnant. Pour ajouter\nles lignes une à une, il faudrait une\nboucle : `for ligne in t2: t1.append(ligne)`.\n"
        },
        {
          "text": "`fusion(t1, t2)`",
          "correct": false,
          "feedback": "Erreur : pas de fonction `fusion` en Python\nstandard.\n"
        },
        {
          "text": "`t1.merge(t2)`",
          "correct": false,
          "feedback": "Erreur : pas de méthode `.merge` sur les\nlistes (cela ressemble à du pandas).\n"
        },
        {
          "text": "`t1 + t2`",
          "correct": true,
          "feedback": "Bonne réponse : la concaténation de listes\n(`+`) crée une nouvelle liste contenant tous\nles éléments. Idiome Python standard.\n"
        }
      ],
      "explanation": "Pour modifier `t1` **en place** au lieu de créer\nune nouvelle table : `t1 += t2`, ou une boucle\nqui ajoute chaque ligne de `t2` avec `append`."
    },
    {
      "id": "q14",
      "difficulty": 2,
      "skills": [
        "moyenne"
      ],
      "title": "Calculer une moyenne",
      "statement": "Comment calculer la **moyenne** des âges d'une\ntable avec un descripteur `age` (entier) ?",
      "options": [
        {
          "text": "`sum(ligne.age for ligne in table) / len(table)`",
          "correct": false,
          "feedback": "Erreur : un dictionnaire n'a pas d'attribut\n`.age` (`AttributeError`). La valeur d'un\ndescripteur s'obtient par `ligne['age']`.\n"
        },
        {
          "text": "`sum(ligne['age'] for ligne in table) / len(table[0])`",
          "correct": false,
          "feedback": "Erreur : `len(table[0])` compte les\n**descripteurs** de la première ligne (le\nnombre de colonnes), pas le nombre de lignes.\nIl faut diviser par `len(table)`.\n"
        },
        {
          "text": "`sum(table['age']) / len(table)`",
          "correct": false,
          "feedback": "Erreur : `table` est une **liste**, on ne\npeut pas l'indexer par chaîne. Lève\n`TypeError`.\n"
        },
        {
          "text": "`sum(ligne['age'] for ligne in table) / len(table)`",
          "correct": true,
          "feedback": "Bonne réponse : on extrait la valeur `age`\npour chaque ligne via une expression\ngénératrice, puis on divise par le nombre de\nlignes.\n"
        }
      ],
      "explanation": "Penser à gérer le cas de la table **vide**\n(division par zéro) : tester `if table:` avant\nle calcul, ou utiliser\n`sum(...) / len(table) if table else 0`."
    },
    {
      "id": "q15",
      "difficulty": 2,
      "skills": [
        "jointure-simple"
      ],
      "title": "Jointure simple",
      "statement": "Soient\n```python\neleves = [\n    {\"id\": 1, \"nom\": \"Alice\"},\n    {\"id\": 2, \"nom\": \"Bob\"}\n]\nnotes = [\n    {\"id_eleve\": 1, \"matiere\": \"Maths\", \"note\": 15},\n    {\"id_eleve\": 2, \"matiere\": \"NSI\", \"note\": 18}\n]\n```\nPour associer chaque note au nom de l'élève, on\neffectue une **jointure**. Quel code l'implémente\ncorrectement ?",
      "options": [
        {
          "text": "```python\nfor n in notes:\n    for e in eleves:\n        print(e[\"nom\"], n[\"matiere\"], n[\"note\"])\n```\n",
          "correct": false,
          "feedback": "Erreur : sans la condition d'égalité\n`n[\"id_eleve\"] == e[\"id\"]`, on produit le\n**produit cartésien** : chaque note est\nassociée à **tous** les élèves, pas au sien\n(quatre lignes affichées au lieu de deux).\n"
        },
        {
          "text": "```python\nfor n in notes:\n    for e in eleves:\n        if n[\"id_eleve\"] == e[\"id_eleve\"]:\n            print(e[\"nom\"], n[\"matiere\"], n[\"note\"])\n```\n",
          "correct": false,
          "feedback": "Erreur : la clé commune ne porte pas le même\nnom dans les deux tables : `\"id\"` dans\n`eleves`, `\"id_eleve\"` dans `notes`.\n`e[\"id_eleve\"]` lève donc `KeyError` dès la\npremière comparaison.\n"
        },
        {
          "text": "```python\nfor n in notes:\n    for e in eleves:\n        if n[\"id_eleve\"] == e[\"id\"]:\n            print(e[\"nom\"], n[\"matiere\"], n[\"note\"])\n```\n",
          "correct": true,
          "feedback": "Bonne réponse : on parcourt les notes et,\npour chacune, on cherche l'élève\ncorrespondant. On affiche `Alice Maths 15`\npuis `Bob NSI 18`. Complexité O(n × m).\n"
        },
        {
          "text": "```python\nfor n in notes:\n    print(eleves[n[\"id_eleve\"]][\"nom\"], n[\"matiere\"], n[\"note\"])\n```\n",
          "correct": false,
          "feedback": "Erreur : `eleves[1]` accède à l'**indice 1**\nde la liste, pas à l'élève d'`id` 1. Ici la\nnote de Maths est attribuée à Bob, puis\n`eleves[2]` lève `IndexError`. Un identifiant\nn'est pas un indice.\n"
        }
      ],
      "explanation": "C'est l'opération de **fusion** de deux tables sur\nune clé commune. Pour gagner en performance, on\npeut indexer `eleves` par `id` dans un dictionnaire\nau préalable."
    },
    {
      "id": "q16",
      "difficulty": 2,
      "skills": [
        "conversion-csv"
      ],
      "title": "Pièges de conversion",
      "statement": "Après lecture d'un CSV avec `csv.DictReader`, on\nveut calculer la moyenne des âges. Quel piège\nfaut-il éviter ?",
      "options": [
        {
          "text": "Le module `csv` ne gère pas les fichiers de plus de 100 lignes",
          "correct": false,
          "feedback": "Erreur : il gère sans problème des fichiers de\nplusieurs millions de lignes.\n"
        },
        {
          "text": "Toujours initialiser une variable globale",
          "correct": false,
          "feedback": "Pas un piège spécifique au CSV.\n"
        },
        {
          "text": "Il faut payer une licence pour utiliser `csv.DictReader`",
          "correct": false,
          "feedback": "Erreur : le module `csv` est gratuit et inclus\ndans Python standard.\n"
        },
        {
          "text": "Oublier de convertir les valeurs en `int` (les valeurs lues sont des chaînes)",
          "correct": true,
          "feedback": "Bonne réponse : `csv.DictReader` ne fait\naucune conversion de type. `\"17\" + \"16\"`\ndonne `\"1716\"`, pas `33`. Toujours convertir\navant de calculer.\n"
        }
      ],
      "explanation": "Piège classique chez les débutants : oublier la\nconversion. Symptôme : addition qui concatène\ndes chaînes ou comparaison qui plante."
    },
    {
      "id": "q17",
      "difficulty": 2,
      "skills": [
        "comptage-categorie"
      ],
      "title": "Compter par catégorie",
      "statement": "Soit\n```python\ntable = [\n    {\"nom\": \"Alice\", \"ville\": \"Lyon\"},\n    {\"nom\": \"Bob\", \"ville\": \"Paris\"},\n    {\"nom\": \"Chloé\", \"ville\": \"Lyon\"}\n]\n```\nQuel code construit le dictionnaire `c` donnant le\nnombre d'élèves dans chaque ville, ici\n`{\"Lyon\": 2, \"Paris\": 1}` ?",
      "options": [
        {
          "text": "```python\nc = {}\nfor ligne in table:\n    v = ligne[\"ville\"]\n    if v in c:\n        c[v] = 1\n    else:\n        c[v] = c[v] + 1\n```\n",
          "correct": false,
          "feedback": "Erreur : les deux branches sont inversées. À la\npremière rencontre d'une ville, la clé `v`\nn'existe pas encore dans `c`, et `c[v] + 1`\nlève `KeyError` (ici dès `\"Lyon\"`). C'est la\npremière rencontre qui doit créer la clé à `1`.\n"
        },
        {
          "text": "```python\nc = {}\nfor ligne in table:\n    v = ligne[\"ville\"]\n    if v in c:\n        c[v] = c[v] + 1\n    else:\n        c[v] = 1\n```\n",
          "correct": true,
          "feedback": "Bonne réponse : on construit progressivement\nun dictionnaire `ville → effectif`. À la\npremière rencontre d'une ville, on crée la clé\navec la valeur `1` ; ensuite, on incrémente.\n"
        },
        {
          "text": "```python\nc = {}\nfor ligne in table:\n    v = ligne[\"ville\"]\n    if v in table:\n        c[v] = c[v] + 1\n    else:\n        c[v] = 1\n```\n",
          "correct": false,
          "feedback": "Erreur : le test d'appartenance porte sur\n`table`, qui ne contient que des dictionnaires,\njamais une chaîne : il est toujours faux et\nchaque ville est remise à `1`. On obtient\n`{\"Lyon\": 1, \"Paris\": 1}` : une présence, pas\nun comptage.\n"
        },
        {
          "text": "```python\nc = {}\nfor ligne in table:\n    v = ligne[\"ville\"]\n    c[v] = table.count(v)\n```\n",
          "correct": false,
          "feedback": "Erreur : `count` cherche l'élément exact `v`\n(une chaîne) dans `table`, qui ne contient que\ndes dictionnaires : il renvoie toujours `0`.\nOn obtient `{\"Lyon\": 0, \"Paris\": 0}`.\n"
        }
      ],
      "explanation": "Schéma général du comptage par catégorie : un\ndictionnaire dont les clés sont les catégories et\nles valeurs les effectifs. Le test `if v in c`\ndistingue la première rencontre d'une catégorie\n(création de la clé) des suivantes (incrément)."
    },
    {
      "id": "q18",
      "difficulty": 2,
      "skills": [
        "tri-multicritere"
      ],
      "title": "Tri sur deux critères",
      "statement": "Soit\n```python\ntable = [\n    {\"nom\": \"Lou\", \"age\": 17},\n    {\"nom\": \"Bob\", \"age\": 16},\n    {\"nom\": \"Ana\", \"age\": 17}\n]\n```\nQuel code trie la table par **âge croissant**, puis\npar **nom alphabétique** en cas d'égalité ?",
      "options": [
        {
          "text": "```python\ndef cle(l):\n    return (l[\"age\"], l[\"nom\"])\ntable.sort(key=cle)\n```\n",
          "correct": true,
          "feedback": "Bonne réponse : la clé est un **tuple**.\nPython compare les tuples élément par\nélément : d'abord l'âge, puis le nom en cas\nd'égalité. On obtient Bob (16), Ana (17),\nLou (17).\n"
        },
        {
          "text": "```python\ndef cle(l):\n    return l[\"age\"] + l[\"nom\"]\ntable.sort(key=cle)\n```\n",
          "correct": false,
          "feedback": "Erreur : on ne peut pas additionner un entier\net une chaîne (`TypeError`). Et même entre\nchaînes, une concaténation ne définirait pas\nun ordre sur deux critères.\n"
        },
        {
          "text": "```python\ndef cle(l):\n    return (l[\"nom\"], l[\"age\"])\ntable.sort(key=cle)\n```\n",
          "correct": false,
          "feedback": "Erreur : l'ordre des critères est inversé. Le\npremier élément du tuple est le critère\nprincipal, ici le nom. On obtient Ana, Bob,\nLou, sans tenir compte de l'âge.\n"
        },
        {
          "text": "```python\ndef cle(l):\n    return l[\"age\"]\ntable.sort(key=cle)\n```\n",
          "correct": false,
          "feedback": "Erreur : le tri ne porte que sur l'âge. En cas\nd'égalité, `sort` conserve l'ordre initial des\nlignes : on obtient Bob, Lou, Ana, et non Bob,\nAna, Lou.\n"
        }
      ],
      "explanation": "Astuce : la comparaison des tuples en Python est\nlexicographique (premier élément, puis deuxième en\ncas d'égalité), ce qui simplifie beaucoup les tris\nsur plusieurs critères. La fonction clé renvoie le\ntuple des critères dans l'ordre de priorité."
    },
    {
      "id": "q19",
      "difficulty": 2,
      "skills": [
        "exemple-reel"
      ],
      "title": "Exemple concret",
      "statement": "Lequel des fichiers Open Data suivants se prête\nnaturellement à une représentation en table ?",
      "options": [
        {
          "text": "La liste des stations Vélib' au format CSV",
          "correct": true,
          "feedback": "Bonne réponse : un cas typique. Chaque ligne\n= une station, descripteurs = nom,\nlongitude, latitude, capacité, etc. Idéal\npour traitement en table.\n"
        },
        {
          "text": "Une carte des arrondissements de Paris au format SVG",
          "correct": false,
          "feedback": "Erreur : SVG est un format vectoriel, pas\ntabulaire.\n"
        },
        {
          "text": "Un PDF illustré",
          "correct": false,
          "feedback": "Erreur : un PDF est un format de\nprésentation, mal adapté à la manipulation\ntabulaire (sauf après extraction).\n"
        },
        {
          "text": "Une vidéo de présentation des différents transports urbains",
          "correct": false,
          "feedback": "Erreur : un fichier multimédia ne se\nstructure pas en table.\n"
        }
      ],
      "explanation": "Open Data : data.gouv.fr, opendata.paris.fr,\nINSEE... regorgent de jeux de données en CSV\ndirectement utilisables."
    },
    {
      "id": "q20",
      "difficulty": 2,
      "skills": [
        "structure-imbriquee"
      ],
      "title": "Limite des tables",
      "statement": "Pour quel type de données les tables sont-elles\n**mal adaptées** ?",
      "options": [
        {
          "text": "Une liste de produits avec prix",
          "correct": false,
          "feedback": "Données plates : table parfaitement adaptée.\n"
        },
        {
          "text": "Un classement sportif officiel regroupant plusieurs équipes",
          "correct": false,
          "feedback": "Données plates : table adaptée.\n"
        },
        {
          "text": "Un arbre généalogique avec des relations multiples",
          "correct": true,
          "feedback": "Bonne réponse : les relations\nparent/enfant/conjoint sont **hiérarchiques**.\nLes tables peuvent les stocker, mais ce n'est\npas la représentation la plus naturelle pour\nce type de données.\n"
        },
        {
          "text": "Une liste d'élèves avec nom, âge, ville",
          "correct": false,
          "feedback": "Données plates : parfaitement adaptées à\nune table.\n"
        }
      ],
      "explanation": "Règle générale : pour des données **plates**, la\ntable est adaptée ; pour des données **hiérarchiques**\n(comme un arbre généalogique), un format imbriqué\n(JSON par exemple) est plus naturel."
    },
    {
      "id": "q21",
      "difficulty": 3,
      "skills": [
        "trace-acces"
      ],
      "title": "Trace d'accès",
      "statement": "Soit\n```python\nt = [\n    {\"a\": 1, \"b\": 2},\n    {\"a\": 3, \"b\": 4},\n    {\"a\": 5, \"b\": 6}\n]\n```\nQue vaut `sum(l[\"a\"] for l in t if l[\"b\"] >= 4)` ?",
      "options": [
        {
          "text": "12",
          "correct": false,
          "feedback": "Erreur : tu as additionné les `a` de toutes\nles lignes (1 + 3 + 5) sans filtrer.\n"
        },
        {
          "text": "8",
          "correct": true,
          "feedback": "Bonne réponse : les lignes filtrées sont\n`{\"a\": 3, \"b\": 4}` (b=4 ≥ 4) et\n`{\"a\": 5, \"b\": 6}` (b=6 ≥ 4). Somme des `a` :\n3 + 5 = 8.\n"
        },
        {
          "text": "0",
          "correct": false,
          "feedback": "Erreur : il y a bien des lignes qui\nsatisfont `l[\"b\"] >= 4`.\n"
        },
        {
          "text": "9",
          "correct": false,
          "feedback": "Erreur : tu as inclus la première ligne\n(a=1) à tort. Or `b=2`, donc le filtre rejette.\n"
        }
      ],
      "explanation": "Méthode : (1) appliquer le filtre `l[\"b\"] >= 4`\nà chaque ligne ; (2) extraire `l[\"a\"]` pour les\nlignes restantes ; (3) sommer."
    },
    {
      "id": "q22",
      "difficulty": 3,
      "skills": [
        "transformation"
      ],
      "title": "Transformer la structure",
      "statement": "On veut convertir la **liste de dictionnaires**\n```python\ntable = [\n    {\"x\": 1, \"y\": 2},\n    {\"x\": 3, \"y\": 4}\n]\n```\nen **dictionnaire de listes** `{\"x\": [1, 3], \"y\": [2, 4]}`\n(représentation « colonne par colonne »), rangé\ndans `colonnes`. Quel code est correct ?",
      "options": [
        {
          "text": "```python\ncolonnes = {}\nfor k in table[0]:\n    colonnes[k] = [l[k] for l in table]\n```\n",
          "correct": true,
          "feedback": "Bonne réponse : pour chaque clé du premier\ndictionnaire (les descripteurs), on construit\nla liste des valeurs de cette clé sur toutes\nles lignes.\n"
        },
        {
          "text": "```python\ncolonnes = {}\nfor k in table:\n    colonnes[k] = [l[k] for l in table]\n```\n",
          "correct": false,
          "feedback": "Erreur : `for k in table` parcourt les\n**lignes** (des dictionnaires), pas les clés.\n`l[k]` avec `k` dictionnaire lève `TypeError`\n(un dictionnaire ne peut pas servir de clé).\nIl faut prendre les clés d'une ligne, par\nexemple `table[0]`.\n"
        },
        {
          "text": "```python\ncolonnes = {}\nfor l in table:\n    for k in l:\n        colonnes[k] = l[k]\n```\n",
          "correct": false,
          "feedback": "Erreur : on range une **valeur** et non une\nliste, écrasée à chaque ligne. On obtient\n`{\"x\": 3, \"y\": 4}` : seule la dernière ligne\nsubsiste.\n"
        },
        {
          "text": "```python\ncolonnes = []\nfor k in table[0]:\n    for l in table:\n        colonnes.append(l[k])\n```\n",
          "correct": false,
          "feedback": "Erreur : `colonnes` est une simple liste plate,\n`[1, 3, 2, 4]` : les noms de colonnes sont\nperdus. Il faut un dictionnaire dont chaque\nvaleur est la liste d'une colonne.\n"
        }
      ],
      "explanation": "La représentation colonne par colonne facilite les\ncalculs sur une colonne entière (somme, moyenne,\nmaximum de `colonnes[\"x\"]`) sans reparcourir les\nlignes. La transformation inverse (dictionnaire de\nlistes vers liste de dictionnaires) se fait de\nfaçon symétrique."
    },
    {
      "id": "q23",
      "difficulty": 3,
      "skills": [
        "optimisation"
      ],
      "title": "Performance d'une jointure",
      "statement": "Pour une jointure entre deux tables de tailles\n`n` et `m`, la version naïve à deux boucles\nimbriquées a une complexité de O(n × m). Comment\nl'améliorer ?",
      "options": [
        {
          "text": "En ne conservant que les colonnes utiles dans chacune des deux tables",
          "correct": false,
          "feedback": "Erreur : la complexité dépend du **nombre de\nlignes** parcourues, pas du nombre de colonnes.\nChaque ligne est plus légère, mais les deux\nboucles imbriquées font toujours n × m\ncomparaisons.\n"
        },
        {
          "text": "En triant d'abord les deux tables, tout en gardant les deux boucles imbriquées",
          "correct": false,
          "feedback": "Erreur : trier ne sert à rien si l'on garde\nensuite les deux boucles imbriquées, qui font\ntoujours n × m comparaisons. Un tri n'accélère\nune jointure que si l'algorithme en tire parti\n(parcours simultané des deux tables triées), et\nil coûte lui-même O(n log n).\n"
        },
        {
          "text": "En indexant l'une des deux tables par sa clé dans un dictionnaire",
          "correct": true,
          "feedback": "Bonne réponse : l'accès à un dictionnaire par\nsa clé est en O(1). On construit l'index en un\nseul parcours, avec `idx[e[\"id\"]] = e` pour\nchaque élève `e`, puis chaque note `n` ne\ncoûte qu'un accès : `e = idx[n[\"id_eleve\"]]`.\nLa jointure passe de O(n × m) à O(n + m),\nbeaucoup plus rapide pour de gros volumes.\n"
        },
        {
          "text": "En représentant chaque ligne par un tuple plutôt que par un dictionnaire",
          "correct": false,
          "feedback": "Erreur : changer la représentation d'une ligne\nne change pas le nombre de comparaisons, les\ndeux boucles imbriquées font toujours n × m\ntours. Ce n'est pas une optimisation\nalgorithmique.\n"
        }
      ],
      "explanation": "C'est exactement ce que font les SGBD avec leurs\n**index** : transformer une recherche linéaire\nen accès direct quasi instantané (table de\nhachage ou arbre B)."
    },
    {
      "id": "q24",
      "difficulty": 3,
      "skills": [
        "debug"
      ],
      "title": "Bug subtil",
      "statement": "Un élève construit une table à partir d'une liste\nde noms :\n```python\nnoms = [\"Alice\", \"Bob\", \"Chloé\"]\nligne = {\"nom\": \"\", \"age\": 16}\ntable = []\nfor nom in noms:\n    ligne[\"nom\"] = nom\n    table.append(ligne)\nprint(table[0][\"nom\"])\n```\nQu'affiche ce programme, et pourquoi ?",
      "options": [
        {
          "text": "`Chloé`, car les trois lignes sont le même dictionnaire, modifié à chaque tour",
          "correct": true,
          "feedback": "Bonne réponse : `append` range dans la table\nune référence vers `ligne`, jamais une copie.\nLes trois éléments de `table` désignent le\nmême dictionnaire, dont la clé `\"nom\"` vaut\n`\"Chloé\"` après le dernier tour. Correction :\ncréer un dictionnaire neuf à chaque tour, en\nplaçant `ligne = {\"nom\": nom, \"age\": 16}` dans\nla boucle.\n"
        },
        {
          "text": "`Alice`, car `append` range dans la table une copie de `ligne` à chaque tour",
          "correct": false,
          "feedback": "Erreur : `append` ne copie rien, il ajoute une\nréférence vers l'objet. Les trois lignes sont\nle même dictionnaire, et `table[0][\"nom\"]`\nvaut `\"Chloé\"`.\n"
        },
        {
          "text": "`Chloé`, car `append` insère chaque nouvelle ligne en tête de la table",
          "correct": false,
          "feedback": "Erreur : `append` ajoute toujours en **fin**\nde liste. La valeur affichée est bien `Chloé`,\nmais pour une autre raison : les trois lignes\nsont le même dictionnaire.\n"
        },
        {
          "text": "Rien, car ajouter plusieurs fois le même dictionnaire à une liste lève une erreur",
          "correct": false,
          "feedback": "Erreur : une liste peut contenir plusieurs fois\nle même objet sans aucune erreur. Le programme\ns'exécute et affiche `Chloé`.\n"
        }
      ],
      "explanation": "Piège classique de construction d'une table : un\ndictionnaire est un objet modifiable, et une liste\nne stocke que des références. Réutiliser le même\ndictionnaire pour toutes les lignes produit une\ntable de lignes identiques. Il faut créer un\ndictionnaire neuf pour chaque enregistrement."
    },
    {
      "id": "q25",
      "difficulty": 3,
      "skills": [
        "synthese"
      ],
      "title": "Synthèse",
      "statement": "Parmi les affirmations suivantes sur les tables de\ndonnées, laquelle est **fausse** ?",
      "options": [
        {
          "text": "Les opérations classiques sont la recherche, le tri, la sélection de lignes (filtrage), la sélection de colonnes et la fusion",
          "correct": false,
          "feedback": "Vrai : ce sont les opérations canoniques sur\nles tables, prévues par le programme.\n"
        },
        {
          "text": "Une fusion permet de combiner deux tables sur la base d'un descripteur commun",
          "correct": false,
          "feedback": "Vrai : c'est exactement le rôle de la fusion,\nqui rapproche les lignes de deux tables\npartageant une clé.\n"
        },
        {
          "text": "En Python, on représente couramment une table par une liste de dictionnaires",
          "correct": false,
          "feedback": "Vrai : c'est la représentation la plus\nfréquente en NSI Première.\n"
        },
        {
          "text": "Le module `csv` produit toujours des données typées (entiers, flottants, dates)",
          "correct": true,
          "feedback": "Faux (donc bonne réponse) : `csv` produit\nuniquement des **chaînes**. C'est au\nprogramme de convertir les types.\n"
        }
      ],
      "explanation": "Les tables de données et leurs opérations\nassociées (recherche, tri, fusion) sont au cœur\ndu traitement de données. Les maîtriser en\nPython est un atout majeur pour\nmanipuler de gros volumes."
    }
  ]
}