{
  "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.",
    "parties": [
      {
        "id": "structure",
        "title": "Structure d'une table"
      },
      {
        "id": "operations",
        "title": "Rechercher, sélectionner, trier"
      },
      {
        "id": "croisement",
        "title": "Fusionner et transformer"
      }
    ],
    "prerequisites": [],
    "references": []
  },
  "questions": [
    {
      "id": "q01",
      "difficulty": 1,
      "skills": [
        "definition"
      ],
      "title": "Qu'est-ce qu'une table ?",
      "partie": "structure",
      "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 plus répandue pour organiser des données : tableurs, fichiers CSV et bases de données reposent tous sur ce modèle."
    },
    {
      "id": "q02",
      "difficulty": 1,
      "skills": [
        "vocabulaire"
      ],
      "title": "Vocabulaire",
      "partie": "structure",
      "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 valeur",
          "correct": false,
          "feedback": "Erreur : une valeur est le contenu d'une seule case, à l'intersection d'une ligne et d'une colonne. Une colonne regroupe toutes les valeurs d'un même descripteur.\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",
      "partie": "structure",
      "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",
        "descripteur"
      ],
      "title": "Accéder à une cellule",
      "partie": "structure",
      "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.",
      "variants": [
        {
          "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."
        },
        {
          "statement": "Soit\n```python\ntable = [\n    {\"nom\": \"Alice\", \"age\": 17},\n    {\"nom\": \"Bob\", \"age\": 16}\n]\n```\nComment accéder à l'âge de Alice ?",
          "options": [
            {
              "text": "`table[0]['age']`",
              "correct": true,
              "feedback": "Bonne réponse : Alice est la première ligne\n(indice 0), et `'age'` est le descripteur. On\nobtient `17`.\n"
            },
            {
              "text": "`table['Alice']['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[0]`",
              "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."
        },
        {
          "statement": "Soit\n```python\ntable = [\n    {\"nom\": \"Chloé\", \"age\": 15},\n    {\"nom\": \"Dan\", \"age\": 18},\n    {\"nom\": \"Emma\", \"age\": 16}\n]\n```\nComment accéder à l'âge de Emma ?",
          "options": [
            {
              "text": "`table[2]['age']`",
              "correct": true,
              "feedback": "Bonne réponse : Emma est la troisième ligne\n(indice 2), et `'age'` est le descripteur. On\nobtient `16`.\n"
            },
            {
              "text": "`table['Emma']['age']`",
              "correct": false,
              "feedback": "Erreur : `table` est une **liste**, pas un\ndictionnaire indexé par nom. Cela lève\n`TypeError`.\n"
            },
            {
              "text": "`table[3]['age']`",
              "correct": false,
              "feedback": "Erreur : indice **hors-borne**. Avec trois\nlignes, les indices valides sont 0, 1 et 2, pas\n3 (`IndexError`).\n"
            },
            {
              "text": "`table.age[2]`",
              "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",
      "partie": "operations",
      "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",
      "partie": "structure",
      "statement": "D'où peut provenir une table de données dans un\nprojet réel ?",
      "options": [
        {
          "text": "D'un fichier CSV, d'un tableur, de données ouvertes...",
          "correct": true,
          "feedback": "Bonne réponse : les tables sont partout en informatique. Données ouvertes (data.gouv.fr), exports de tableur, relevés de capteurs, fichiers CSV : autant de sources possibles.\n"
        },
        {
          "text": "Uniquement de données saisies à la main au clavier",
          "correct": false,
          "feedback": "Trop restrictif : une table peut aussi provenir d'un fichier, d'un tableur ou d'un capteur, sans aucune saisie 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 d'un tableur comme LibreOffice Calc",
          "correct": false,
          "feedback": "Trop restrictif : un tableur est une source fréquente, mais pas la seule.\n"
        }
      ],
      "explanation": "Une compétence clé de NSI : savoir lire des données réelles, souvent en CSV, les transformer en table Python, puis les manipuler."
    },
    {
      "id": "q07",
      "difficulty": 1,
      "skills": [
        "recherche"
      ],
      "title": "Recherche par critère",
      "partie": "operations",
      "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",
      "partie": "structure",
      "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` fournit une suite de dictionnaires, un par ligne, avec pour clés les noms de l'en-tête ; `list()` les rassemble en une 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",
      "partie": "structure",
      "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.length`",
          "correct": false,
          "feedback": "Erreur : les listes Python n'ont pas d'attribut `length`, nom utilisé dans d'autres langages comme JavaScript. Cette écriture lève `AttributeError` ; en Python, on utilise la fonction `len`.\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",
      "partie": "structure",
      "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",
      "partie": "operations",
      "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",
      "partie": "operations",
      "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 : un dictionnaire ne s'indexe pas par une liste de clés, `l[[\"nom\", \"age\"]]` lève `TypeError`. Il faut extraire 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",
      "partie": "croisement",
      "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 : les listes n'ont pas de méthode `.merge`, cette écriture lève `AttributeError`.\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",
      "partie": "operations",
      "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 : la compréhension construit la liste des âges, `sum` les additionne, puis on divise par le nombre de lignes.\n"
        }
      ],
      "explanation": "Penser au cas de la table **vide**, qui provoquerait une division par zéro : tester `len(table) > 0` avant le calcul."
    },
    {
      "id": "q15",
      "difficulty": 2,
      "skills": [
        "jointure-simple"
      ],
      "title": "Jointure simple",
      "partie": "croisement",
      "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",
      "partie": "structure",
      "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",
      "partie": "operations",
      "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] = 1\n```\n",
          "correct": false,
          "feedback": "Erreur : sans test, chaque rencontre d'une ville remet sa valeur à `1`, même quand la clé existe déjà. Lyon, rencontrée deux fois, reste à `1` : on obtient `{\"Lyon\": 1, \"Paris\": 1}`.\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",
        "fonction-cle"
      ],
      "title": "Une clé de tri négative",
      "partie": "operations",
      "statement": "Soit\n```python\ncoureurs = [{\"nom\": \"Lou\", \"temps\": 52},\n            {\"nom\": \"Bob\", \"temps\": 47},\n            {\"nom\": \"Ana\", \"temps\": 58}]\n\ndef cle(c):\n    return -c[\"temps\"]\n\nclassement = sorted(coureurs, key=cle)\nprint(classement[0][\"nom\"])\n```\nQu'affiche ce programme ?",
      "options": [
        {
          "text": "`Bob`",
          "correct": false,
          "feedback": "Erreur : ce serait le premier d'un tri par temps croissant. Ici la clé vaut `-47` pour Bob, la plus grande des trois : Bob arrive en dernier.\n"
        },
        {
          "text": "`Lou`",
          "correct": false,
          "feedback": "Erreur : `sorted` renvoie une nouvelle liste réellement triée, l'ordre initial n'est pas conservé. Le premier élément est celui dont la clé est la plus petite.\n"
        },
        {
          "text": "`Ana`",
          "correct": true,
          "feedback": "Bonne réponse : les clés valent `-52`, `-47` et `-58`. Le tri croissant place en tête la plus petite, `-58`, celle d'Ana : la table est rangée par temps décroissant.\n"
        },
        {
          "text": "Rien, une clé de tri négative provoquant une erreur",
          "correct": false,
          "feedback": "Erreur : une clé peut être n'importe quel nombre, négatif compris. `sorted` compare simplement les valeurs renvoyées par `cle`, sans erreur.\n"
        }
      ],
      "explanation": "`sorted` range la table par valeurs croissantes de la fonction clé. Renvoyer l'opposé d'une colonne numérique inverse donc l'ordre : on obtient un tri décroissant sur cette colonne."
    },
    {
      "id": "q19",
      "difficulty": 2,
      "skills": [
        "exemple-reel"
      ],
      "title": "Exemple concret",
      "partie": "structure",
      "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",
      "partie": "structure",
      "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 table est adaptée ; pour des données **hiérarchiques**, comme un arbre généalogique, une représentation où les données s'emboîtent, à la manière d'un arbre, est plus naturelle."
    },
    {
      "id": "q21",
      "difficulty": 3,
      "skills": [
        "comprehension",
        "filtre",
        "somme"
      ],
      "title": "Trace d'accès",
      "partie": "structure",
      "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": "9",
          "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 `{\"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": "1",
          "correct": false,
          "feedback": "Erreur : tu as sommé les `a` des lignes rejetées,\n`{\"a\": 1, \"b\": 2}`, dont le `b` est strictement inférieur à 4.\n"
        }
      ],
      "explanation": "Méthode : (1) appliquer le filtre `l[\"b\"] >= 4`\nà chaque ligne ; (2) extraire `l[\"a\"]` pour les\nlignes restantes ; (3) sommer.",
      "variants": [
        {
          "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": "9",
              "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 `{\"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": "1",
              "correct": false,
              "feedback": "Erreur : tu as sommé les `a` des lignes rejetées,\n`{\"a\": 1, \"b\": 2}`, dont le `b` est strictement inférieur à 4.\n"
            }
          ],
          "explanation": "Méthode : (1) appliquer le filtre `l[\"b\"] >= 4`\nà chaque ligne ; (2) extraire `l[\"a\"]` pour les\nlignes restantes ; (3) sommer."
        },
        {
          "statement": "Soit\n```python\nt = [\n    {\"a\": 2, \"b\": 1},\n    {\"a\": 4, \"b\": 7},\n    {\"a\": 6, \"b\": 3},\n    {\"a\": 8, \"b\": 5}\n]\n```\nQue vaut `sum([l[\"a\"] for l in t if l[\"b\"] >= 4])` ?",
          "options": [
            {
              "text": "20",
              "correct": false,
              "feedback": "Erreur : tu as additionné les `a` de toutes\nles lignes (2 + 4 + 6 + 8) sans filtrer.\n"
            },
            {
              "text": "12",
              "correct": true,
              "feedback": "Bonne réponse : les lignes filtrées sont\n`{\"a\": 4, \"b\": 7}` (b=7 ≥ 4) et `{\"a\": 8, \"b\": 5}` (b=5 ≥ 4). Somme des `a` :\n4 + 8 = 12.\n"
            },
            {
              "text": "0",
              "correct": false,
              "feedback": "Erreur : il y a bien des lignes qui\nsatisfont `l[\"b\"] >= 4`.\n"
            },
            {
              "text": "8",
              "correct": false,
              "feedback": "Erreur : tu as sommé les `a` des lignes rejetées,\n`{\"a\": 2, \"b\": 1}` et `{\"a\": 6, \"b\": 3}`, dont le `b` est strictement inférieur à 4.\n"
            }
          ],
          "explanation": "Méthode : (1) appliquer le filtre `l[\"b\"] >= 4`\nà chaque ligne ; (2) extraire `l[\"a\"]` pour les\nlignes restantes ; (3) sommer."
        },
        {
          "statement": "Soit\n```python\nt = [\n    {\"a\": 10, \"b\": 5},\n    {\"a\": 20, \"b\": 9},\n    {\"a\": 30, \"b\": 4}\n]\n```\nQue vaut `sum([l[\"a\"] for l in t if l[\"b\"] >= 6])` ?",
          "options": [
            {
              "text": "60",
              "correct": false,
              "feedback": "Erreur : tu as additionné les `a` de toutes\nles lignes (10 + 20 + 30) sans filtrer.\n"
            },
            {
              "text": "20",
              "correct": true,
              "feedback": "Bonne réponse : les lignes filtrées sont\n`{\"a\": 20, \"b\": 9}` (b=9 ≥ 6). Somme des `a` :\n20 = 20.\n"
            },
            {
              "text": "0",
              "correct": false,
              "feedback": "Erreur : il y a bien des lignes qui\nsatisfont `l[\"b\"] >= 6`.\n"
            },
            {
              "text": "40",
              "correct": false,
              "feedback": "Erreur : tu as sommé les `a` des lignes rejetées,\n`{\"a\": 10, \"b\": 5}` et `{\"a\": 30, \"b\": 4}`, dont le `b` est strictement inférieur à 6.\n"
            }
          ],
          "explanation": "Méthode : (1) appliquer le filtre `l[\"b\"] >= 6`\nà chaque ligne ; (2) extraire `l[\"a\"]` pour les\nlignes restantes ; (3) sommer."
        }
      ]
    },
    {
      "id": "q22",
      "difficulty": 3,
      "skills": [
        "transformation"
      ],
      "title": "Transformer la structure",
      "partie": "croisement",
      "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",
      "partie": "croisement",
      "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 ensuite les deux boucles imbriquées, qui font toujours n × m comparaisons. Un tri n'accélère une jointure que si l'algorithme en tire parti, par un parcours simultané des deux tables triées, et le tri a lui-même un coût.\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": "L'idée est de remplacer la recherche par parcours de toute une table par un accès direct à la ligne voulue grâce à sa clé : chaque table n'est alors parcourue qu'une seule fois."
    },
    {
      "id": "q24",
      "difficulty": 3,
      "skills": [
        "debug",
        "selection",
        "return"
      ],
      "title": "Une sélection qui s'arrête trop tôt",
      "partie": "operations",
      "statement": "Un élève écrit une fonction qui renvoie les noms des élèves d'une classe :\n```python\ndef eleves_de(table, classe):\n    resultat = []\n    for l in table:\n        if l[\"classe\"] == classe:\n            resultat.append(l[\"nom\"])\n        return resultat\n\nt = [{\"nom\": \"Ana\", \"classe\": \"1A\"},\n     {\"nom\": \"Bob\", \"classe\": \"1B\"},\n     {\"nom\": \"Eva\", \"classe\": \"1B\"}]\nprint(eleves_de(t, \"1B\"))\n```\nQu'affiche ce programme ?",
      "options": [
        {
          "text": "`['Bob', 'Eva']`",
          "correct": false,
          "feedback": "Erreur : ce serait le cas si `return resultat` était placé après la boucle. Indenté dans la boucle, il s'exécute dès le premier tour.\n"
        },
        {
          "text": "`['Bob']`",
          "correct": false,
          "feedback": "Erreur : tu fais arrêter la fonction à la première ligne retenue, mais le `return` ne dépend pas du test : il est atteint dès la ligne d'Ana, avant que Bob ne soit examiné.\n"
        },
        {
          "text": "`None`",
          "correct": false,
          "feedback": "Erreur : la fonction atteint bien un `return`, celui de la boucle, qui renvoie la liste `resultat` telle qu'elle est à ce moment, c'est-à-dire vide.\n"
        },
        {
          "text": "`[]`",
          "correct": true,
          "feedback": "Bonne réponse : au premier tour, Ana n'est pas en 1B, rien n'est ajouté, puis `return resultat` renvoie la liste vide. Bob et Eva ne sont jamais examinés. Il faut désindenter le `return` au niveau du `for`.\n"
        }
      ],
      "explanation": "Dans une sélection de lignes, la table doit être parcourue en entier avant de renvoyer le résultat : le `return` se place après la boucle. Une indentation de trop suffit à arrêter la fonction dès la première ligne."
    },
    {
      "id": "q25",
      "difficulty": 3,
      "skills": [
        "synthese"
      ],
      "title": "Synthèse",
      "partie": "croisement",
      "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."
    },
    {
      "id": "q26",
      "difficulty": 2,
      "skills": [
        "fusion",
        "cle",
        "jointure"
      ],
      "title": "La clé d'une fusion",
      "partie": "croisement",
      "statement": "La table `eleves` a pour descripteurs `id`, `nom` et `classe` ; la\ntable `notes` a pour descripteurs `id_eleve`, `matiere` et `note`.\nSur quelle égalité repose la fusion des deux tables ?",
      "options": [
        {
          "text": "`e[\"id\"] == n[\"id_eleve\"]`",
          "correct": true,
          "feedback": "Bonne réponse : l'identifiant de l'élève figure dans les deux\ntables, sous deux noms de colonnes différents. C'est lui qui\nrelie une note à un élève.\n"
        },
        {
          "text": "`e[\"nom\"] == n[\"matiere\"]`",
          "correct": false,
          "feedback": "Erreur : un nom d'élève et une matière n'ont rien à voir.\n"
        },
        {
          "text": "`e[\"classe\"] == n[\"note\"]`",
          "correct": false,
          "feedback": "Erreur : une classe et une note ne sont pas comparables.\n"
        },
        {
          "text": "`e[\"id\"] == n[\"note\"]`",
          "correct": false,
          "feedback": "Erreur : un identifiant et une note ne se correspondent pas ;\non compare la colonne commune aux deux tables.\n"
        }
      ],
      "explanation": "Fusionner deux tables exige une colonne dont les valeurs se\ncorrespondent, en général un identifiant."
    },
    {
      "id": "q27",
      "difficulty": 3,
      "skills": [
        "fusion",
        "trace",
        "boucles-imbriquees"
      ],
      "title": "Le nombre de lignes d'une fusion",
      "partie": "croisement",
      "statement": "La table `eleves` contient deux lignes, d'identifiants `1` et `2`.\nLa table `notes` contient des lignes dont les `id_eleve` sont\n`1`, `1` et `2`. Combien de lignes contient `resultat` ?\n```python\nresultat = []\nfor e in eleves:\n    for n in notes:\n        if e[\"id\"] == n[\"id_eleve\"]:\n            resultat.append({\"nom\": e[\"nom\"], \"note\": n[\"note\"]})\n```",
      "options": [
        {
          "text": "`3` lignes",
          "correct": true,
          "feedback": "Bonne réponse : chaque note est associée à l'élève dont\nl'identifiant correspond, soit deux notes pour l'élève 1 et une pour l'élève 2. Une ligne par\ncorrespondance trouvée.\n"
        },
        {
          "text": "`2` lignes, une par élève",
          "correct": false,
          "feedback": "Erreur : un élève ayant plusieurs notes apparaît plusieurs fois,\net un élève sans note n'apparaît pas.\n"
        },
        {
          "text": "`6` lignes, une par couple de la double boucle",
          "correct": false,
          "feedback": "Erreur : seuls les couples dont les identifiants coïncident sont\najoutés.\n"
        },
        {
          "text": "`1` ligne",
          "correct": false,
          "feedback": "Erreur : chaque correspondance ajoute une ligne, et il y en a\nplusieurs.\n"
        }
      ],
      "explanation": "Une fusion produit une ligne par couple de lignes qui se\ncorrespondent : ni plus, ni moins.",
      "variants": [
        {
          "statement": "La table `eleves` contient deux lignes, d'identifiants `1` et `2`.\nLa table `notes` contient des lignes dont les `id_eleve` sont\n`1`, `1` et `2`. Combien de lignes contient `resultat` ?\n```python\nresultat = []\nfor e in eleves:\n    for n in notes:\n        if e[\"id\"] == n[\"id_eleve\"]:\n            resultat.append({\"nom\": e[\"nom\"], \"note\": n[\"note\"]})\n```",
          "options": [
            {
              "text": "`3` lignes",
              "correct": true,
              "feedback": "Bonne réponse : chaque note est associée à l'élève dont\nl'identifiant correspond, soit deux notes pour l'élève 1 et une pour l'élève 2. Une ligne par\ncorrespondance trouvée.\n"
            },
            {
              "text": "`2` lignes, une par élève",
              "correct": false,
              "feedback": "Erreur : un élève ayant plusieurs notes apparaît plusieurs fois,\net un élève sans note n'apparaît pas.\n"
            },
            {
              "text": "`6` lignes, une par couple de la double boucle",
              "correct": false,
              "feedback": "Erreur : seuls les couples dont les identifiants coïncident sont\najoutés.\n"
            },
            {
              "text": "`1` ligne",
              "correct": false,
              "feedback": "Erreur : chaque correspondance ajoute une ligne, et il y en a\nplusieurs.\n"
            }
          ],
          "explanation": "Une fusion produit une ligne par couple de lignes qui se\ncorrespondent : ni plus, ni moins."
        },
        {
          "statement": "La table `eleves` contient deux lignes, d'identifiants `1` et `2`.\nLa table `notes` contient des lignes dont les `id_eleve` sont\n`2` et `2`. Combien de lignes contient `resultat` ?\n```python\nresultat = []\nfor e in eleves:\n    for n in notes:\n        if e[\"id\"] == n[\"id_eleve\"]:\n            resultat.append({\"nom\": e[\"nom\"], \"note\": n[\"note\"]})\n```",
          "options": [
            {
              "text": "`2` lignes",
              "correct": true,
              "feedback": "Bonne réponse : chaque note est associée à l'élève dont\nl'identifiant correspond, soit deux notes pour l'élève 2 et aucune pour l'élève 1. Une ligne par\ncorrespondance trouvée.\n"
            },
            {
              "text": "`2` lignes, une par élève",
              "correct": false,
              "feedback": "Erreur : un élève ayant plusieurs notes apparaît plusieurs fois,\net un élève sans note n'apparaît pas.\n"
            },
            {
              "text": "`6` lignes, une par couple de la double boucle",
              "correct": false,
              "feedback": "Erreur : seuls les couples dont les identifiants coïncident sont\najoutés.\n"
            },
            {
              "text": "`1` ligne",
              "correct": false,
              "feedback": "Erreur : chaque correspondance ajoute une ligne, et il y en a\nplusieurs.\n"
            }
          ],
          "explanation": "Une fusion produit une ligne par couple de lignes qui se\ncorrespondent : ni plus, ni moins."
        },
        {
          "statement": "La table `eleves` contient deux lignes, d'identifiants `1` et `2`.\nLa table `notes` contient des lignes dont les `id_eleve` sont\n`1`, `2`, `1` et `2`. Combien de lignes contient `resultat` ?\n```python\nresultat = []\nfor e in eleves:\n    for n in notes:\n        if e[\"id\"] == n[\"id_eleve\"]:\n            resultat.append({\"nom\": e[\"nom\"], \"note\": n[\"note\"]})\n```",
          "options": [
            {
              "text": "`4` lignes",
              "correct": true,
              "feedback": "Bonne réponse : chaque note est associée à l'élève dont\nl'identifiant correspond, soit deux notes pour chaque élève. Une ligne par\ncorrespondance trouvée.\n"
            },
            {
              "text": "`2` lignes, une par élève",
              "correct": false,
              "feedback": "Erreur : un élève ayant plusieurs notes apparaît plusieurs fois,\net un élève sans note n'apparaît pas.\n"
            },
            {
              "text": "`6` lignes, une par couple de la double boucle",
              "correct": false,
              "feedback": "Erreur : seuls les couples dont les identifiants coïncident sont\najoutés.\n"
            },
            {
              "text": "`1` ligne",
              "correct": false,
              "feedback": "Erreur : chaque correspondance ajoute une ligne, et il y en a\nplusieurs.\n"
            }
          ],
          "explanation": "Une fusion produit une ligne par couple de lignes qui se\ncorrespondent : ni plus, ni moins."
        }
      ]
    },
    {
      "id": "q28",
      "difficulty": 2,
      "skills": [
        "fusion",
        "correspondance-manquante"
      ],
      "title": "Un élève sans note",
      "partie": "croisement",
      "statement": "Dans la fusion des tables `eleves` et `notes` par identifiant, un\nélève n'a encore aucune note. Que devient-il dans la table\nfusionnée ?",
      "options": [
        {
          "text": "Il n'y figure pas, faute de correspondance",
          "correct": true,
          "feedback": "Bonne réponse : la fusion ne produit une ligne que lorsqu'une\ncorrespondance existe. Sans note, aucune ligne n'est créée pour\nlui.\n"
        },
        {
          "text": "Il y figure avec une note vide, ajoutée automatiquement",
          "correct": false,
          "feedback": "Erreur : ce serait un autre traitement, à programmer\nexplicitement ; la fusion simple ne complète rien.\n"
        },
        {
          "text": "Il y figure avec la note `0`, valeur par défaut",
          "correct": false,
          "feedback": "Erreur : aucune valeur n'est inventée par la fusion.\n"
        },
        {
          "text": "La fusion échoue avec une erreur de clé absente",
          "correct": false,
          "feedback": "Erreur : l'absence de correspondance ne provoque aucune erreur,\nseulement une ligne absente.\n"
        }
      ],
      "explanation": "Avant d'exploiter une fusion, il faut savoir quelles lignes ont pu\ndisparaître faute de correspondance."
    },
    {
      "id": "q29",
      "difficulty": 2,
      "skills": [
        "transformation",
        "colonne-calculee",
        "dictionnaire"
      ],
      "title": "Une colonne calculée",
      "partie": "croisement",
      "statement": "La table `table` contient, pour chaque élève, les descripteurs\n`nom`, `n1` et `n2`, deux notes lues comme chaînes. Que fait ce\nprogramme ?\n```python\nfor ligne in table:\n    ligne[\"moyenne\"] = (int(ligne[\"n1\"]) + int(ligne[\"n2\"])) / 2\n```",
      "options": [
        {
          "text": "Il ajoute à chaque ligne un descripteur `moyenne`",
          "correct": true,
          "feedback": "Bonne réponse : affecter une nouvelle clé dans chaque\ndictionnaire ajoute une colonne à la table, remplie ligne par\nligne.\n"
        },
        {
          "text": "Il remplace `n1` et `n2` par leur moyenne dans chaque ligne",
          "correct": false,
          "feedback": "Erreur : `n1` et `n2` ne sont pas modifiées ; une clé\nsupplémentaire est créée.\n"
        },
        {
          "text": "Il crée une nouvelle table sans toucher à `table`",
          "correct": false,
          "feedback": "Erreur : les dictionnaires de `table` sont modifiés sur place.\n"
        },
        {
          "text": "Il échoue, une clé absente ne pouvant être affectée",
          "correct": false,
          "feedback": "Erreur : affecter une clé absente est précisément la façon\nd'ajouter une entrée à un dictionnaire.\n"
        }
      ],
      "explanation": "Ajouter une colonne calculée transforme la table sans la recréer :\nchaque ligne reçoit une clé de plus."
    },
    {
      "id": "q30",
      "difficulty": 3,
      "skills": [
        "index",
        "dictionnaire",
        "acces-direct"
      ],
      "title": "Indexer une table par identifiant",
      "partie": "croisement",
      "statement": "Avant de fusionner deux grandes tables, Nora construit :\n```python\nindex = {}\nfor e in eleves:\n    index[e[\"id\"]] = e\n```\nQuel est l'intérêt de ce dictionnaire ?",
      "options": [
        {
          "text": "Retrouver un élève par identifiant sans parcours",
          "correct": true,
          "feedback": "Bonne réponse : `index[n[\"id_eleve\"]]` donne directement la\nligne cherchée. La fusion ne parcourt plus qu'une table au lieu\nd'imbriquer deux boucles.\n"
        },
        {
          "text": "Trier les élèves par identifiant croissant",
          "correct": false,
          "feedback": "Erreur : un dictionnaire n'est pas un tri ; il sert à accéder\ndirectement.\n"
        },
        {
          "text": "Supprimer les élèves en double dans la table",
          "correct": false,
          "feedback": "Erreur : si deux élèves avaient le même identifiant, l'un\nécraserait l'autre, mais ce n'est pas le but.\n"
        },
        {
          "text": "Réduire la mémoire occupée par la table des élèves",
          "correct": false,
          "feedback": "Erreur : l'index s'ajoute à la table, il ne la remplace pas.\n"
        }
      ],
      "explanation": "Indexer une table par une clé transforme une recherche linéaire en\naccès direct : c'est ce qui rend une fusion rapide sur de grandes\ntables."
    },
    {
      "id": "q31",
      "difficulty": 1,
      "skills": [
        "selection",
        "filtre",
        "comptage"
      ],
      "title": "Combien de lignes sont retenues",
      "partie": "operations",
      "statement": "Les âges de la table sont, dans l'ordre, `\"15\"`, `\"17\"`, `\"16\"` et\n`\"18\"`. Combien de lignes la sélection suivante garde-t-elle ?\n```python\nmajeurs = []\nfor ligne in table:\n    if int(ligne[\"age\"]) >= 17:\n        majeurs.append(ligne)\n```",
      "options": [
        {
          "text": "`2`",
          "correct": true,
          "feedback": "Bonne réponse : la condition `>= 17` est vraie pour 2 des\nquatre âges.\n"
        },
        {
          "text": "`4`",
          "correct": false,
          "feedback": "Erreur : la condition écarte les âges inférieurs à 17.\n"
        },
        {
          "text": "`0`",
          "correct": false,
          "feedback": "Erreur : au moins un âge atteint 17.\n"
        },
        {
          "text": "Une erreur, un âge ne pouvant être comparé à un entier",
          "correct": false,
          "feedback": "Erreur : `int` convertit l'âge avant la comparaison.\n"
        }
      ],
      "explanation": "Sélectionner des lignes, c'est garder celles qui vérifient une\ncondition, après conversion des valeurs textuelles.",
      "variants": [
        {
          "statement": "Les âges de la table sont, dans l'ordre, `\"15\"`, `\"17\"`, `\"16\"` et\n`\"18\"`. Combien de lignes la sélection suivante garde-t-elle ?\n```python\nmajeurs = []\nfor ligne in table:\n    if int(ligne[\"age\"]) >= 17:\n        majeurs.append(ligne)\n```",
          "options": [
            {
              "text": "`2`",
              "correct": true,
              "feedback": "Bonne réponse : la condition `>= 17` est vraie pour 2 des\nquatre âges.\n"
            },
            {
              "text": "`4`",
              "correct": false,
              "feedback": "Erreur : la condition écarte les âges inférieurs à 17.\n"
            },
            {
              "text": "`0`",
              "correct": false,
              "feedback": "Erreur : au moins un âge atteint 17.\n"
            },
            {
              "text": "Une erreur, un âge ne pouvant être comparé à un entier",
              "correct": false,
              "feedback": "Erreur : `int` convertit l'âge avant la comparaison.\n"
            }
          ],
          "explanation": "Sélectionner des lignes, c'est garder celles qui vérifient une\ncondition, après conversion des valeurs textuelles."
        },
        {
          "statement": "Les âges de la table sont, dans l'ordre, `\"15\"`, `\"17\"`, `\"16\"` et\n`\"18\"`. Combien de lignes la sélection suivante garde-t-elle ?\n```python\nmajeurs = []\nfor ligne in table:\n    if int(ligne[\"age\"]) >= 16:\n        majeurs.append(ligne)\n```",
          "options": [
            {
              "text": "`3`",
              "correct": true,
              "feedback": "Bonne réponse : la condition `>= 16` est vraie pour 3 des\nquatre âges.\n"
            },
            {
              "text": "`4`",
              "correct": false,
              "feedback": "Erreur : la condition écarte les âges inférieurs à 16.\n"
            },
            {
              "text": "`0`",
              "correct": false,
              "feedback": "Erreur : au moins un âge atteint 16.\n"
            },
            {
              "text": "Une erreur, un âge ne pouvant être comparé à un entier",
              "correct": false,
              "feedback": "Erreur : `int` convertit l'âge avant la comparaison.\n"
            }
          ],
          "explanation": "Sélectionner des lignes, c'est garder celles qui vérifient une\ncondition, après conversion des valeurs textuelles."
        },
        {
          "statement": "Les âges de la table sont, dans l'ordre, `\"15\"`, `\"17\"`, `\"16\"` et\n`\"18\"`. Combien de lignes la sélection suivante garde-t-elle ?\n```python\nmajeurs = []\nfor ligne in table:\n    if int(ligne[\"age\"]) >= 18:\n        majeurs.append(ligne)\n```",
          "options": [
            {
              "text": "`1`",
              "correct": true,
              "feedback": "Bonne réponse : la condition `>= 18` est vraie pour 1 des\nquatre âges.\n"
            },
            {
              "text": "`4`",
              "correct": false,
              "feedback": "Erreur : la condition écarte les âges inférieurs à 18.\n"
            },
            {
              "text": "`0`",
              "correct": false,
              "feedback": "Erreur : au moins un âge atteint 18.\n"
            },
            {
              "text": "Une erreur, un âge ne pouvant être comparé à un entier",
              "correct": false,
              "feedback": "Erreur : `int` convertit l'âge avant la comparaison.\n"
            }
          ],
          "explanation": "Sélectionner des lignes, c'est garder celles qui vérifient une\ncondition, après conversion des valeurs textuelles."
        }
      ]
    },
    {
      "id": "q32",
      "difficulty": 2,
      "skills": [
        "tri",
        "reverse",
        "sorted"
      ],
      "title": "Un tri décroissant",
      "partie": "operations",
      "statement": "Que change l'argument `reverse=True` dans\n`sorted(table, key=cle, reverse=True)` ?",
      "options": [
        {
          "text": "Les lignes vont de la plus grande clé à la plus petite",
          "correct": true,
          "feedback": "Bonne réponse : le tri reste fondé sur `cle`, mais l'ordre est\ninversé, du plus grand au plus petit.\n"
        },
        {
          "text": "Les lignes sont inversées sans être triées par la clé",
          "correct": false,
          "feedback": "Erreur : `reverse` s'applique au tri, pas à l'ordre initial.\n"
        },
        {
          "text": "La fonction `cle` est appliquée aux lignes dans l'ordre inverse",
          "correct": false,
          "feedback": "Erreur : `cle` est appelée normalement sur chaque ligne.\n"
        },
        {
          "text": "La table d'origine est modifiée et inversée",
          "correct": false,
          "feedback": "Erreur : `sorted` renvoie une nouvelle liste et laisse `table`\nintacte.\n"
        }
      ],
      "explanation": "`sorted` combine une clé, qui dit sur quoi trier, et `reverse`,\nqui dit dans quel sens."
    },
    {
      "id": "q33",
      "difficulty": 2,
      "skills": [
        "maximum",
        "parcours",
        "trace"
      ],
      "title": "La meilleure note de la table",
      "partie": "operations",
      "statement": "Les notes de la table sont, dans l'ordre, `\"12\"`, `\"18\"`, `\"15\"`. Qu'affiche ce\nprogramme ?\n```python\nmaxi = 0\nfor ligne in table:\n    if int(ligne[\"note\"]) > maxi:\n        maxi = int(ligne[\"note\"])\nprint(maxi)\n```",
      "options": [
        {
          "text": "`18`",
          "correct": true,
          "feedback": "Bonne réponse : `maxi` est remplacé à chaque note strictement\nsupérieure ; il finit par contenir la plus grande.\n"
        },
        {
          "text": "`15`",
          "correct": false,
          "feedback": "Erreur : 15 n'est pas la plus grande note ; relis le parcours.\n"
        },
        {
          "text": "`12`",
          "correct": false,
          "feedback": "Erreur : 12 est une note intermédiaire, remplacée dès qu'une\nnote plus grande apparaît.\n"
        },
        {
          "text": "`0`",
          "correct": false,
          "feedback": "Erreur : `maxi` est mis à jour dès la première note, supérieure\nà 0.\n"
        }
      ],
      "explanation": "Chercher un maximum dans une table suit le schéma de la recherche\nde maximum dans une liste, avec une conversion des valeurs.",
      "variants": [
        {
          "statement": "Les notes de la table sont, dans l'ordre, `\"12\"`, `\"18\"`, `\"15\"`. Qu'affiche ce\nprogramme ?\n```python\nmaxi = 0\nfor ligne in table:\n    if int(ligne[\"note\"]) > maxi:\n        maxi = int(ligne[\"note\"])\nprint(maxi)\n```",
          "options": [
            {
              "text": "`18`",
              "correct": true,
              "feedback": "Bonne réponse : `maxi` est remplacé à chaque note strictement\nsupérieure ; il finit par contenir la plus grande.\n"
            },
            {
              "text": "`15`",
              "correct": false,
              "feedback": "Erreur : 15 n'est pas la plus grande note ; relis le parcours.\n"
            },
            {
              "text": "`12`",
              "correct": false,
              "feedback": "Erreur : 12 est une note intermédiaire, remplacée dès qu'une\nnote plus grande apparaît.\n"
            },
            {
              "text": "`0`",
              "correct": false,
              "feedback": "Erreur : `maxi` est mis à jour dès la première note, supérieure\nà 0.\n"
            }
          ],
          "explanation": "Chercher un maximum dans une table suit le schéma de la recherche\nde maximum dans une liste, avec une conversion des valeurs."
        },
        {
          "statement": "Les notes de la table sont, dans l'ordre, `\"9\"`, `\"11\"`, `\"20\"`. Qu'affiche ce\nprogramme ?\n```python\nmaxi = 0\nfor ligne in table:\n    if int(ligne[\"note\"]) > maxi:\n        maxi = int(ligne[\"note\"])\nprint(maxi)\n```",
          "options": [
            {
              "text": "`20`",
              "correct": true,
              "feedback": "Bonne réponse : `maxi` est remplacé à chaque note strictement\nsupérieure ; il finit par contenir la plus grande.\n"
            },
            {
              "text": "`11`",
              "correct": false,
              "feedback": "Erreur : 11 n'est pas la plus grande note ; relis le parcours.\n"
            },
            {
              "text": "`9`",
              "correct": false,
              "feedback": "Erreur : 9 est une note intermédiaire, remplacée dès qu'une\nnote plus grande apparaît.\n"
            },
            {
              "text": "`0`",
              "correct": false,
              "feedback": "Erreur : `maxi` est mis à jour dès la première note, supérieure\nà 0.\n"
            }
          ],
          "explanation": "Chercher un maximum dans une table suit le schéma de la recherche\nde maximum dans une liste, avec une conversion des valeurs."
        },
        {
          "statement": "Les notes de la table sont, dans l'ordre, `\"14\"`, `\"7\"`, `\"13\"`. Qu'affiche ce\nprogramme ?\n```python\nmaxi = 0\nfor ligne in table:\n    if int(ligne[\"note\"]) > maxi:\n        maxi = int(ligne[\"note\"])\nprint(maxi)\n```",
          "options": [
            {
              "text": "`14`",
              "correct": true,
              "feedback": "Bonne réponse : `maxi` est remplacé à chaque note strictement\nsupérieure ; il finit par contenir la plus grande.\n"
            },
            {
              "text": "`13`",
              "correct": false,
              "feedback": "Erreur : 13 n'est pas la plus grande note ; relis le parcours.\n"
            },
            {
              "text": "`7`",
              "correct": false,
              "feedback": "Erreur : 7 est une note intermédiaire, remplacée dès qu'une\nnote plus grande apparaît.\n"
            },
            {
              "text": "`0`",
              "correct": false,
              "feedback": "Erreur : `maxi` est mis à jour dès la première note, supérieure\nà 0.\n"
            }
          ],
          "explanation": "Chercher un maximum dans une table suit le schéma de la recherche\nde maximum dans une liste, avec une conversion des valeurs."
        }
      ]
    },
    {
      "id": "q34",
      "difficulty": 3,
      "skills": [
        "tri",
        "deux-criteres",
        "stabilite"
      ],
      "title": "Trier par classe puis par nom",
      "partie": "operations",
      "statement": "Lina veut une table triée par classe, et, à l'intérieur de chaque\nclasse, par nom. `sorted` conserve l'ordre relatif des lignes de\nmême clé. Dans quel ordre doit-elle enchaîner les deux tris ?",
      "options": [
        {
          "text": "D'abord par nom, puis par classe",
          "correct": true,
          "feedback": "Bonne réponse : le second tri regroupe les classes sans\ndéfaire l'ordre des noms à l'intérieur de chacune, puisque les\nlignes de même classe gardent leur ordre relatif.\n"
        },
        {
          "text": "D'abord par classe, puis par nom",
          "correct": false,
          "feedback": "Erreur : le tri par nom mélangerait les classes.\n"
        },
        {
          "text": "Un seul tri par classe suffit",
          "correct": false,
          "feedback": "Erreur : les noms resteraient dans leur ordre d'origine à\nl'intérieur de chaque classe.\n"
        },
        {
          "text": "L'ordre des deux tris n'a pas d'importance",
          "correct": false,
          "feedback": "Erreur : c'est le dernier tri qui domine ; il doit porter sur le\ncritère principal.\n"
        }
      ],
      "explanation": "Avec un tri qui conserve l'ordre des égaux, on trie du critère\nsecondaire au critère principal."
    },
    {
      "id": "q35",
      "difficulty": 2,
      "skills": [
        "dictionnaire",
        "cle-absente",
        "erreur"
      ],
      "title": "Un descripteur absent",
      "partie": "structure",
      "statement": "La table a pour descripteurs `nom`, `age` et `ville`. Que produit\n`ligne[\"taille\"]` pour une ligne de cette table ?",
      "options": [
        {
          "text": "Une erreur, la clé `taille` n'existant pas",
          "correct": true,
          "feedback": "Bonne réponse : accéder à une clé absente d'un dictionnaire lève\nune `KeyError`. Les descripteurs disponibles sont ceux de\nl'en-tête.\n"
        },
        {
          "text": "Une chaîne vide, valeur par défaut d'un dictionnaire",
          "correct": false,
          "feedback": "Erreur : Python ne renvoie pas de valeur par défaut pour une\nclé absente.\n"
        },
        {
          "text": "`None`, l'absence de valeur",
          "correct": false,
          "feedback": "Erreur : l'accès par crochets échoue au lieu de renvoyer\n`None`.\n"
        },
        {
          "text": "La valeur de la colonne la plus proche, `ville`",
          "correct": false,
          "feedback": "Erreur : les clés sont exactes, il n'y a aucune approximation.\n"
        }
      ],
      "explanation": "Les descripteurs d'une table sont fixés par son en-tête ; tout\naccès doit employer exactement ces noms."
    },
    {
      "id": "q36",
      "difficulty": 3,
      "skills": [
        "structure",
        "homogeneite",
        "descripteurs"
      ],
      "title": "Des lignes aux mêmes descripteurs",
      "partie": "structure",
      "statement": "Pourquoi exige-t-on que toutes les lignes d'une table aient\nexactement les mêmes descripteurs ?",
      "options": [
        {
          "text": "Pour qu'un même traitement s'applique à chaque ligne sans vérification",
          "correct": true,
          "feedback": "Bonne réponse : trier par `age`, filtrer par `ville` ou calculer\nune moyenne suppose que chaque ligne possède ces clés. Une ligne\ndifférente ferait échouer le traitement.\n"
        },
        {
          "text": "Pour que le fichier CSV soit plus court",
          "correct": false,
          "feedback": "Erreur : la taille du fichier n'est pas en cause.\n"
        },
        {
          "text": "Parce que Python refuse des dictionnaires de clés différentes dans une liste",
          "correct": false,
          "feedback": "Erreur : Python l'accepte ; c'est le traitement des données qui\nl'exige.\n"
        },
        {
          "text": "Pour pouvoir trier la table par n'importe quelle colonne du fichier",
          "correct": false,
          "feedback": "Erreur : le tri est un cas particulier ; la raison est plus\ngénérale, l'uniformité de tous les traitements.\n"
        }
      ],
      "explanation": "Une table est une collection homogène : mêmes descripteurs pour\ntoutes les lignes, ce qui rend les opérations prévisibles."
    }
  ]
}