Données Tables

Identification

Infoforall

9 - Recherche dans une table de données


Prérequis : l'activité OpenData de Python et les deux activités sur le parcours séquentiel de la partie Algo.

Vous savez maintenant

  • Importer un fichier CSV en mémoire en le transformant
    • soit en un tableau de tableaux (list avec Python) : un tableau dont les éléments sont des tableaux
    • soit en un tableau de n-uplets nommés (dict avec Python) : un tableau dont les éléments sont des n-uplets partageant les mêmes descripteurs ou clés.
  • Utiliser des algorithmes pour réaliser des recherches ou des calculs dans un tableau.

Il est donc temps de voir comment gérer tout cela en même temps !

Voyons comment trouver le meilleur Pokemon parmi tous ceux à notre dispostion.

Evaluation ✎ : questions 01-08-11-13-16-17-19-20.

1 - Rappel : collection et table

Nous avons vu avec l'activité OpenData la notion de collection.

Il s'agit de stocker nos données structurées dans un tableau dont chaque élément contient un enregistrement (ou objet).

1
maCollec = [enr0, enr1, enr2]

Nous nommerons ici les collections des tables car elles correspondent bien à des sortes de tables de données :

Collection / Table Attributs ou Descripteurs
Pokemon n° Nom Type Points de vie
Enregistrement d'index 0 1 Bulbasaur Grass 318
Enregistrement d'index 1 2 Ivysaur Grass 405
Enregistrement d'index 2 3 Venusaur Grass 525
Enregistrement d'index 3 3 VenusaurMega Venusaur Grass 625

On notera qu'il peut exister un décalage entre l'index réel de l'enregistrement dans le tableau et le numéro d'identifiation de l'enregistrement dans la tableau. C'est normal : ce n'est pas la même chose.

L'index correspond à la position de l'enregistrement dans la collection.

Le numéro du Pokemon sert à faire le lien entre différentes formes d'un même Pokemon.

Nous avons vu trois manières de stocker nos enregistrements.

Tableau de tableaux

On crée des tableaux dont l'index 0 est l'identifiant, l'index 1 le nom ...

On les rajoute au fur et à mesure à notre collection avec la méthode des tableaux append.

1 2 3 4 5 6 7 8 9
pokemons = [] # Variable contenant la table pokemons.append( [1, 'Bulbasaur', 'Grass', 318] ) pokemons.append( [2, 'Ivysaur', 'Grass', 405 ] ) nbr_enr = len(pokemons) # contient le nombre d'enregistrements dans la table for index in range(nbr_enr): print(pokemons[index])

Ici, le problème de cette structure est qu'elle fonctionne en Python mais pas si vous utilisez un langage utilisant de vrais tableaux : les éléments n'ont pas tous le même type. Le type list de Python permet de le faire mais ce n'est pas possible sinon avec de vrais tableaux.

Que faire alors ?

Tableau de tuples (p-uplets en français)

On crée des tuples dont l'index 0 est l'identifiant, l'index 1 le nom ...

On les rajoute au fur et à mesure à notre collection avec la méthode des tableaux append.

1 2 3 4 5 6 7 8 9
pokemons = [] # Variable contenant la table pokemons.append( (1, 'Bulbasaur', 'Grass', 318) ) pokemons.append( (2, 'Ivysaur', 'Grass', 405 ) ) nbr_enr = len(pokemons) # contient le nombre d'enregistrements dans la table for index in range(nbr_enr): print(pokemons[index])

✎ 01° Utiliser le programme pour voir comment il fonctionne. Rajouter alors la ligne de code permettant d'obtenir l'affichage suivant : j'ai rajouté une nouvelle entrée.

(1, 'Bulbasaur', 'Grass', 318) (2, 'Ivysaur', 'Grass', 405) (3, 'Venusaur', 'Grass', 525)

Tableau de n-uplets nommés partageant les mêmes descripteurs

En Python, on peut utiliser les dictionnaires pour faire office de n-uplets nommés.

On crée des dictionnaires avec des clés id, nom ...

On les rajoute au fur et à mesure à notre collection avec la méthode des tableaux append.

1 2 3 4 5 6 7 8 9 10
pokemons = [] # Variable contenant la table pokemons.append( {'id':1, 'nom':'Bulbasaur', 'type':'Grass','pv':318} ) pokemons.append( {'id':2, 'nom':'Ivysaur', 'type':'Grass','pv':405} ) pokemons.append( {'id':3, 'nom':'Venusaur', 'type':'Grass','pv':525} ) nbr_enr = len(pokemons) # contient le nombre d'enregistrements dans la table for index in range(nbr_enr): print(pokemons[index])

On notera que le fait que les enregistrements soient des dictionnaires ne change rien à la gestion de la table / collection : la variable pokemons est toujours un tableau. C'est juste que les éléments du tableau sont maintenant des dictionnaires.

02° Utiliser le programme pour voir comment il fonctionne. A quoi voit-on que les enregistrements sont des dictionnaires ?

{'id': 1, 'nom': 'Bulbasaur', 'type': 'Grass', 'pv': 318} {'id': 2, 'nom': 'Ivysaur', 'type': 'Grass', 'pv': 405} {'id': 3, 'nom': 'Venusaur', 'type': 'Grass', 'pv': 525}

...CORRECTION...

A la présence d'accolades plutôt que de crochets (tableaux) ou parenthèses (tuples non nommés)

Si on ne modifie pas les enregistrements, on pourrait même se contenter de l'utilisation d'une boucle FOR nominative :

9 10
for enregistrement in pokemons : print(enregistrement)

Cela donne le même résultat que la boucle FOR numérique :

9 10
for index in range(nbr_enr): print(pokemons[index])

2 - Création de la collection des Pokemons

La première fois, nous avons juste trouvé un fichier CSV en ligne et nous l'avons téléchargé ou créé manuellement.

Nous allons ici simplement utiliser un programme qui va aller télécharger les données et créer le fichier CSV dans le même dossier que celui du programme Python que vous allez créer.

03° Téléchargement avec le module urllib. Utilser ce programme pour télécharger le fichier CSV. Le fichier se trouvera dans le même dossier que votre fichier Python.

1 2
import urllib.request urllib.request.urlretrieve('https://gist.githubusercontent.com/armgilles/194bcff35001e7eb53a2a8b441e8b2c6/raw/92200bc0a673d5ce2110aaad4544ed6c4010f687/pokemon.csv', 'mesdonnees.csv')

04° Téléchargement avec le module Requests. Dans Thonny, aller dans le menu TOOLS - MANAGE PACKAGES. Lancer une recherche sur et installer le module requests. Si vous utilisez directement Python via IDLE, il faut l'installer avec pip via la console.

Enregistrer le programme ci-dessous sous le nom download_csv.py dans un dossier dont vous connaissez la localisation. Lancer le programme. Vous devrirez constater l'existence d'un fichier CSV nommé pokemon.csv dans ce même dossier.

1 2 3 4 5
import requests reponse = requests.request(url='https://gist.githubusercontent.com/armgilles/194bcff35001e7eb53a2a8b441e8b2c6/raw/92200bc0a673d5ce2110aaad4544ed6c4010f687/pokemon.csv', method='GET') fichier = open('mesdonnees2.csv', 'w', encoding='utf-8') fichier.write(reponse.text) fichier.close()

Vérifiez que le fichier CSV mesdonnees2.csv a bien été créé automatiquement.

Vous devriez vous retrouver avec deux fichiers identiques, à part au niveau du nom.

Si vous ne parvenez pas à utiliser au moins l'un des deux programmes, vous pouvez toujours télécharger une copie non actualisée du fichier à l'adresse suivante : LE FICHIER CSV

L'intérêt des dépôts est que les erreurs éventuelles sont corrigées au fil du temps.

Maintenant que nous avons récupéré les données, nous allons créer la table correspondante. J'ai choisi d'utiliser la méthode du tableau de p-uplets nommés pour cette activité, mais le tableau de tableaux ou le tableau de tuples fonctionnent tout aussi bien.

05° Enregistrer le programme ci-dessous sous le nom creation_collec.py dans le même dossier que les autres.

Il s'agit juste des fonctions de l'activité OpenData avec Python qui crée une collection de dictionnaires dont le nom des clés sont fournis sur la première ligne du fichier CSV.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
def decomposer(chaine, separateur): """Fonction qui décompose la chaine et créant un tableau contenant les valeurs :: chaine (str) :: la chaîne de caractères à décomposer :: separateur (str) :: le caractère de séparation :: return (list) :: un tableau contenant les différents éléments :: exemple :: >>> decomposer('1,voiture,sport,rouge', ',') ['1', 'voiture', 'sport', 'rouge'] """ chaine = chaine.replace('\n', '') # On supprime le passage à la ligne chaine = chaine.replace('\r', '') # On supprime le retour chariot tableau = chaine.split(separateur) # On crée un tableau à partir de la chaine return tableau def creer(entree, attributs): """Renvoie un dictionnaire à partir de entree avec les clés fournies dans attributs :: param entree(list) :: un tableau de X éléments :: param attributs(list) :: un tableau de X éléments également :: return (dict) :: le dictionnaire voulu :: exemple :: >>> creer([0, 'voiture', 'sport', 'rouge'], ['id', 'type', 'utilisation', 'couleur']) {'id': 0, 'type': 'voiture', 'utilisation': 'sport', 'couleur': 'rouge'} """ reponse = {} if len(entree) == len(attributs): for index in range(len(entree)) : reponse[attributs[index]] = entree[index] return reponse def creer_collection(nom_fichier): """Fonction qui renvoie un tableau de dictionnaire à partir d'un fichier CSV :: param nom_fichier(str) :: le nom du fichier CSV correct à traiter :: return (list) :: le tableau de dictionnaires """ # 1 - On crée une tableau vide laCollec = [] # 2 - On ouvre le fichier en lecture monFichier = open(nom_fichier, 'r', encoding='utf-8') # 3 - On lit la première ligne pour créer le tableau des attributs entete = monFichier.readline() # entete est un simple string qui finit par \n attributs = decomposer(entete, ',') # création d'un tableau contenant les attributs # 4 - On lit la suite du fichier ligne par ligne for ligne in monFichier : valeurs = decomposer(ligne, ',') # tableau contenant les valeurs nouveau = creer(valeurs, attributs) # dictionnaire clé-valeur laCollec.append(nouveau) # rajout à la collection # 5 - On ferme le fichier monFichier.close() # On ferme le fichier # 6 - On renvoie la collection return laCollec

06° Toujours au même endroit, créer un fichier Python gestion_tables.py contenant simplement ceci :

1 2 3
from creation_collec import creer_collection pokemons = creer_collection('mesdonnees.csv')

Vous devriez avoir ceci dans votre dossier :

📁 votre_dossier

📄 mesdonnees.csv

📄 creation_collec.py

📄 download_csv.py

📄 gestion_tables.py

Lancer l'exécution du fichier.

Vous devriez constater si l'onglet VARIABLE de Thonny est ouvert (Menu View-Variables) que la variable pokemons contient bien les enregistrements lus à partir du fichier CSV.

Si vous voulez voir le contenu de votre collection, tapez donc ceci dans la console :

>>> pokemons
Importation

Rien de neuf par rapport à l'activité OpenData ?

Si ! Nous venons de voir comment on peut importer des fonctions contenues dans d'autres fichiers. Très pratique.

Maintenant que nous avons notre table, nous allons pouvoir traiter les informations qu'elle contient.

Question : Existe-il des pokemons dont l'attaque ET la défense dépassent la moyenne ?

3 - Tests de cohérence

Première chose à faire : transformer l'attaque et la défense en nombres. Pour l'instant, toutes les valeurs associées aux clés sont des strings. Si le fichier-texte CSV est bien construit, il ne devrait donc y avoir que des strings de style '54' interprétable en entier dans les emplacements qui sont censés contenir des "nombres".

A nous maintenant de vérifier que nos données sont bien cohérentes par rapport à l'attendu pour une valeur de ce type.

07° Vérifions en tapant ceci dans la console de Thonny :

>>> pokemons[0] {'#': '1', 'Name': 'Bulbasaur', 'Type 1': 'Grass', 'Type 2': 'Poison', 'Total': '318', 'HP': '45', 'Attack': '49', 'Defense': '49', 'Sp. Atk': '65', 'Sp. Def': '65', 'Speed': '45', 'Generation': '1', 'Legendary': 'False'} >>> pokemons[0]['Attack'] '49'

✎ 08° Analyser l'instruction Console suivante :

>>> for pokemon in pokemons : pokemon['Attack'] = int(pokemon['Attack'])

La variable pokemons (avec un s) est bien entendu la table contenant les dictionnaires.

Que va alors contenir l'enregistrement pokemon (sans s) : un tableau ou un dictionnaire ?

Que fait alors l'instruction pokemon['Attack'] = int(pokemon['Attack']) ?

Vous pouvez vérifier vos réponses à l'aide des commandes Console suivantes par exemple :

>>> pokemons[0] >>> pokemons[0]['Attack']
Tests de cohérence

Il s'agit d'une opération fondamentale à faire sur les tables avant d'accepter un enregistrement : il faut veiller à ce que l'enregistrement contienne bien ce qu'on veut ! Si on attend un nombre entier, il faut voir si on peut effectivement le transformer en entier. Sinon, il faudra tout simplement refuser de traiter cet enregistrement !

Imaginons que l'une des valeurs de l'attaque contienne 'A!', comment faire ? A terme, l'entrée va provoquer une erreur. Autant la supprimer.

Quelques uns des tests de cohérence à vérifier avant de faire du traitement sur une table :

  • Le type des valeurs
  • La présence des descripteurs
  • L'absence de doublons (deux enregistrements identiques) (traité en fin d'activité)

Voici deux fonctions :

  • voir_enregistrement vous permet de visualiser avec un fString trois des descripteurs d'un dictionnaire-enregistrement en fournissant 25 caractères d'espace pour le nom, 5 caractères pour l'attaque et 5 caractères pour la défense. Comme enregistrement c'est long à écrire, je l'ai remplacé par data. Plus court.
  • coherence_type vous permet de transformer certaines valeurs associées à des clés en integer et de ne renvoyer que les enregistrements (data) qui ne posent pas problème. Comment ? En utilisant une structure qu'on nomme try : on tente de faire les instructions.

    14 15 16 17 18 19 20 21 22 23 24 25
    nouvelle = [] for data in table : # Pour chaque enreg. dans la table try : data['Attack'] = int(data['Attack']) data['Defense'] = int(data['Defense']) data['Total'] = int(data['Total']) data['HP'] = int(data['HP']) data['Name'] = str(data['Name']) if data['Name'] != '' : nouvelle.append(data) except : print(data)

    Si réaliser les instructions déclenche une erreur, on exécute à la place ce qu'on a indiqué dans le bloc indenté sous except. Ici, on affiche alors juste les entrées dans la console.

Voici le programme que vous allez utiliser maintenant dans gestion_tables.py.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
# 1 -Importation depuis des modules from creation_collec import creer_collection # 2 - Déclaration des fonctions def voir_enregistrement(data): """Visualisation de trois descripteurs sur l'enregistrement""" print(f"{data['Name']:25}{data['Attack']:5}{data['Defense']:5}") def coherence_type(table): """Vérifie et transforme si possible les valeurs des enregistrements dans le bon type :: param table (list) :: une table contenant des dictionnaires :: return (list) :: table avec uniquement les enregistrements valides """ nouvelle = [] for data in table : # Pour chaque enreg. dans la table try : data['Attack'] = int(data['Attack']) data['Defense'] = int(data['Defense']) data['Total'] = int(data['Total']) data['HP'] = int(data['HP']) data['Name'] = str(data['Name']) if data['Name'] != '' : nouvelle.append(data) except : print(data) return nouvelle # 3 - Programme if __name__ == '__main__' : pokemons = creer_collection('mesdonnees.csv') pokemons = coherence_type(pokemons)

09° La fonction coherence_type va renvoyer une nouvelle table. Elle crée une table vide nouvelle ligne 14. Ensuite, on teste un par un tous les enregistrements data de la table initiale. Si on parvient à réaliser les différentes conversions de type, on peut alors rajouter l'enregistrement data à la suite de nouvelle (ligne 23).

Question :Comment se nomme la façon de créer la table avec append dans la fonction coherence_type ?

  • A : Par déclaration
  • B : Par extension
  • C : Par compréhension
  • D : Par appréhension

...CORRECTION...

Il s'agit d'une création par EXTENSION (réponse B)

La déclaration consiste à fournir directement le contenu dans les crochets.

Nous allons revoir la création par compréhension juste après.

10° Mettre ces nouvelles fonctions en mémoire. Lancer. Utiliser de nouveau les instructions suivantes dans la console 

>>> pokemons[0] {'#': '1', 'Name': 'Bulbasaur', 'Type 1': 'Grass', 'Type 2': 'Poison', 'Total': 318, 'HP': 45, 'Attack': 49, 'Defense': 49, 'Sp. Atk': '65', 'Sp. Def': '65', 'Speed': '45', 'Generation': '1', 'Legendary': 'False'} >>> pokemons[0]['Attack'] 49

On voit bien qu'aucune table n'est pas apparue dans la console : aucune table n'a été exclue.

Lorsqu'on visualise l'un des enregistrements, on voit que les clés Total, HP, Attack, Defense sont bien associées à des entiers.

✎ 11° Finir la fonction coherence_type : il faut tenter de gérer les clés #, Sp. Atk, Sp. Def, Speed et Generation en entier (fonction int). Faire de même avec la clé Legendary qu'on transformera en booléen en testant data['Legendary'] == 'True'.

Après réalisation, voilà la réponse que vous devriez obtenir avec les instructions précédentes :

>>> pokemons[0] {'#': 1, 'Name': 'Bulbasaur', 'Type 1': 'Grass', 'Type 2': 'Poison', 'Total': 318, 'HP': 45, 'Attack': 49, 'Defense': 49, 'Sp. Atk': 65, 'Sp. Def': 65, 'Speed': 45, 'Generation': 1, 'Legendary': False}

Voilà : nous avons maintenant une table issue d'un fichier CSV et nous avons transformé correctement les types des variables. Il nous reste à chercher la valeur moyenne par exemple.

4 - Valeur maximum

Pour cela, il suffit d'utiliser les fonctions vues dans la partie Algorithmique.

La seule différence, c'est que le tableau contient des dictionnaires et pas directement des nombres.

Dans les fonctions créées, il va donc falloir transformer

  • tableau[index] en
  • tableau[index][descripteur]
1 2 3 4 5
maxi = tableau[0][descripteur] for index in range( 1, len(tableau) ): if tableau[index][descripteur] > maxi : maxi = tableau[index][descripteur] return maxi

12° Utiliser la fonction trouverMax telle que noté ligne 52 : en fournissant la table et la clé Attack de façon à stocker cette valeur max. Regarder l'onglet Variables de Thonny pour visualiser cette valeur.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
# 1 -Importation depuis des modules from creation_collec import creer_collection # 2 - Déclaration des fonctions def voir_enregistrement(data): """Visualisation de trois descripteurs sur l'enregistrement""" print(f"{data['Name']:25}{data['Attack']:5}{data['Defense']:5}") def coherence_type(table): """Vérifie et transforme si possible les valeurs des enregistrements dans le bon type :: param table (list) :: une table contenant des dictionnaires :: return (list) :: table avec uniquement les enregistrements valides """ nouvelle = [] for data in table : # Pour chaque enreg. dans la table try : data['#'] = int(data['#']) data['Name'] = str(data['Name']) data['Type 1'] = str(data['Type 1']) data['Type 2'] = str(data['Type 2']) data['Total'] = int(data['Total']) data['HP'] = int(data['HP']) data['Attack'] = int(data['Attack']) data['Defense'] = int(data['Defense']) data['Sp. Atk'] = int(data['Sp. Atk']) data['Sp. Def'] = int(data['Sp. Def']) data['Speed'] = int(data['Speed']) data['Generation'] = int(data['Generation']) data['Legendary'] = data['Legendary'] == 'True' if data['Name'] != '' : nouvelle.append(data) except : print(data) return nouvelle def trouverMax(tableau, descripteur): """Renvoie la valeur maximale lue dans la clé descripteur des dict. du tableau :: param tableau(list) :: un tableau d'éléments qu'on peut comparer :: param descripteur(type valide) :: une clé valide pour les dict du tableau :: return (type en fonction du tableau) :: la valeur maximale """ maxi = tableau[0][descripteur] for index in range( 1, len(tableau) ): if tableau[index][descripteur] > maxi : maxi = tableau[index][descripteur] return maxi # 3 - Programme if __name__ == '__main__' : pokemons = creer_collection('mesdonnees.csv') pokemons = coherence_type(pokemons) atk_max = trouverMax(pokemons, 'Attack')

✎ 13° Récupérer maintenant également la valeur maximale de la défense. On placera le résultat dans une variable dfc_max

5 - Recherche et extraction

Et maintenant, c'est fini !

Nous n'avons plus qu'à créer un tableau en compréhension en fonction de certaines conditions.

Un premier exemple : trouver les meilleurs Pokemons en attaque. Par exemple, ceux qui ont au moins 80% de l'attaque maximale.

Et ça, c'est super simple !

14° Vérifier que les variables pokemons, atk_max et dfc_max soient bien en mémoire.

Si vous n'avez pas trouvé la question 13, rajouter simplement ceci à la fin du code : dfc_max = 230.

Si on tape ceci, cela veut dire qu'on récupère tous les pokemons de la table pokemon et donc, c'est un peu nul :

>>> reponses = [pokemon for pokemon in pokemons]

Attention : il faut appuyer deux fois sur entrée avec la commande Console !

Comment ne récupérer que ceux qui ont au moins 80% de l'attaque maximale ?

>>> reponses = [pokemon for pokemon in pokemons if pokemon['Attack'] > (0.8 * atk_max) ]

Cette fois, on a rajouté une condition.

Demander d'afficher les résultats de deux manières :

>>> for pokemon in reponses : print(pokemon) >>> for pokemon in reponses : voir_enregistrement(pokemon)

Pas mal, non  ?

Voici les résultats pour informations.

>>> for pokemon in reponses : print(pokemon)

{'#': 127, 'Name': 'PinsirMega Pinsir', 'Type 1': 'Bug', 'Type 2': 'Flying', 'Total': 600, 'HP': 65, 'Attack': 155, 'Defense': 120, 'Sp. Atk': 65, 'Sp. Def': 90, 'Speed': 105, 'Generation': 1, 'Legendary': False}

{'#': 130, 'Name': 'GyaradosMega Gyarados', 'Type 1': 'Water', 'Type 2': 'Dark', 'Total': 640, 'HP': 95, 'Attack': 155, 'Defense': 109, 'Sp. Atk': 70, 'Sp. Def': 130, 'Speed': 81, 'Generation': 1, 'Legendary': False}

{'#': 150, 'Name': 'MewtwoMega Mewtwo X', 'Type 1': 'Psychic', 'Type 2': 'Fighting', 'Total': 780, 'HP': 106, 'Attack': 190, 'Defense': 100, 'Sp. Atk': 154, 'Sp. Def': 100, 'Speed': 130, 'Generation': 1, 'Legendary': True}

{'#': 214, 'Name': 'HeracrossMega Heracross', 'Type 1': 'Bug', 'Type 2': 'Fighting', 'Total': 600, 'HP': 80, 'Attack': 185, 'Defense': 115, 'Sp. Atk': 40, 'Sp. Def': 105, 'Speed': 75, 'Generation': 2, 'Legendary': False}

{'#': 248, 'Name': 'TyranitarMega Tyranitar', 'Type 1': 'Rock', 'Type 2': 'Dark', 'Total': 700, 'HP': 100, 'Attack': 164, 'Defense': 150, 'Sp. Atk': 95, 'Sp. Def': 120, 'Speed': 71, 'Generation': 2, 'Legendary': False}

{'#': 257, 'Name': 'BlazikenMega Blaziken', 'Type 1': 'Fire', 'Type 2': 'Fighting', 'Total': 630, 'HP': 80, 'Attack': 160, 'Defense': 80, 'Sp. Atk': 130, 'Sp. Def': 80, 'Speed': 100, 'Generation': 3, 'Legendary': False}

{'#': 289, 'Name': 'Slaking', 'Type 1': 'Normal', 'Type 2': '', 'Total': 670, 'HP': 150, 'Attack': 160, 'Defense': 100, 'Sp. Atk': 95, 'Sp. Def': 65, 'Speed': 100, 'Generation': 3, 'Legendary': False}

{'#': 354, 'Name': 'BanetteMega Banette', 'Type 1': 'Ghost', 'Type 2': '', 'Total': 555, 'HP': 64, 'Attack': 165, 'Defense': 75, 'Sp. Atk': 93, 'Sp. Def': 83, 'Speed': 75, 'Generation': 3, 'Legendary': False}

{'#': 383, 'Name': 'GroudonPrimal Groudon', 'Type 1': 'Ground', 'Type 2': 'Fire', 'Total': 770, 'HP': 100, 'Attack': 180, 'Defense': 160, 'Sp. Atk': 150, 'Sp. Def': 90, 'Speed': 90, 'Generation': 3, 'Legendary': True}

{'#': 384, 'Name': 'RayquazaMega Rayquaza', 'Type 1': 'Dragon', 'Type 2': 'Flying', 'Total': 780, 'HP': 105, 'Attack': 180, 'Defense': 100, 'Sp. Atk': 180, 'Sp. Def': 100, 'Speed': 115, 'Generation': 3, 'Legendary': True}

{'#': 386, 'Name': 'DeoxysAttack Forme', 'Type 1': 'Psychic', 'Type 2': '', 'Total': 600, 'HP': 50, 'Attack': 180, 'Defense': 20, 'Sp. Atk': 180, 'Sp. Def': 20, 'Speed': 150, 'Generation': 3, 'Legendary': True}

{'#': 409, 'Name': 'Rampardos', 'Type 1': 'Rock', 'Type 2': '', 'Total': 495, 'HP': 97, 'Attack': 165, 'Defense': 60, 'Sp. Atk': 65, 'Sp. Def': 50, 'Speed': 58, 'Generation': 4, 'Legendary': False}

{'#': 445, 'Name': 'GarchompMega Garchomp', 'Type 1': 'Dragon', 'Type 2': 'Ground', 'Total': 700, 'HP': 108, 'Attack': 170, 'Defense': 115, 'Sp. Atk': 120, 'Sp. Def': 95, 'Speed': 92, 'Generation': 4, 'Legendary': False}

{'#': 475, 'Name': 'GalladeMega Gallade', 'Type 1': 'Psychic', 'Type 2': 'Fighting', 'Total': 618, 'HP': 68, 'Attack': 165, 'Defense': 95, 'Sp. Atk': 65, 'Sp. Def': 115, 'Speed': 110, 'Generation': 4, 'Legendary': False}

{'#': 486, 'Name': 'Regigigas', 'Type 1': 'Normal', 'Type 2': '', 'Total': 670, 'HP': 110, 'Attack': 160, 'Defense': 110, 'Sp. Atk': 80, 'Sp. Def': 110, 'Speed': 100, 'Generation': 4, 'Legendary': True}

{'#': 646, 'Name': 'KyuremBlack Kyurem', 'Type 1': 'Dragon', 'Type 2': 'Ice', 'Total': 700, 'HP': 125, 'Attack': 170, 'Defense': 100, 'Sp. Atk': 120, 'Sp. Def': 90, 'Speed': 95, 'Generation': 5, 'Legendary': True}

{'#': 719, 'Name': 'DiancieMega Diancie', 'Type 1': 'Rock', 'Type 2': 'Fairy', 'Total': 700, 'HP': 50, 'Attack': 160, 'Defense': 110, 'Sp. Atk': 160, 'Sp. Def': 110, 'Speed': 110, 'Generation': 6, 'Legendary': True}

{'#': 720, 'Name': 'HoopaHoopa Unbound', 'Type 1': 'Psychic', 'Type 2': 'Dark', 'Total': 680, 'HP': 80, 'Attack': 160, 'Defense': 60, 'Sp. Atk': 170, 'Sp. Def': 130, 'Speed': 80, 'Generation': 6, 'Legendary': True}

>>> for pokemon in reponses : voir_enregistrement(pokemon) PinsirMega Pinsir 155 120 GyaradosMega Gyarados 155 109 MewtwoMega Mewtwo X 190 100 HeracrossMega Heracross 185 115 TyranitarMega Tyranitar 164 150 BlazikenMega Blaziken 160 80 Slaking 160 100 BanetteMega Banette 165 75 GroudonPrimal Groudon 180 160 RayquazaMega Rayquaza 180 100 DeoxysAttack Forme 180 20 Rampardos 165 60 GarchompMega Garchomp 170 115 GalladeMega Gallade 165 95 Regigigas 160 110 KyuremBlack Kyurem 170 100 DiancieMega Diancie 160 110 HoopaHoopa Unbound 160 60

Le plus beau avec la création par compréhension, c'est qu'on peut mettre plusieurs conditions liées par des or ou des and, des not ...

15° Tester ce petit programme qui comporte une requête un peu plus avancée en dernière ligne. On voit qu'on cherche les pokemons dont l'attaque est au moins 80% du maximum disponible et (and) la défense 60% du maximum disponible.

54
reponses = [pok for pok in pokemons if pok['Attack'] > (0.8*atk_max) and pok['Defense'] > (0.6*dfc_max)]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
# 1 -Importation depuis des modules from creation_collec import creer_collection # 2 - Déclaration des fonctions def voir_enregistrement(data): """Visualisation de trois descripteurs sur l'enregistrement""" print(f"{data['Name']:25}{data['Attack']:5}{data['Defense']:5}") def coherence_type(table): """Vérifie et transforme si possible les valeurs des enregistrements dans le bon type :: param table (list) :: une table contenant des dictionnaires :: return (list) :: table avec uniquement les enregistrements valides """ nouvelle = [] for data in table : # Pour chaque enreg. dans la table try : data['#'] = int(data['#']) data['Name'] = str(data['Name']) data['Type 1'] = str(data['Type 1']) data['Type 2'] = str(data['Type 2']) data['Total'] = int(data['Total']) data['HP'] = int(data['HP']) data['Attack'] = int(data['Attack']) data['Defense'] = int(data['Defense']) data['Sp. Atk'] = int(data['Sp. Atk']) data['Sp. Def'] = int(data['Sp. Def']) data['Speed'] = int(data['Speed']) data['Generation'] = int(data['Generation']) data['Legendary'] = data['Legendary'] == 'True' if data['Name'] != '' : nouvelle.append(data) except : print(data) return nouvelle def trouverMax(tableau, descripteur): """Renvoie la valeur maximale lue dans la clé descripteur des dict. du tableau :: param tableau(list) :: un tableau d'éléments qu'on peut comparer :: param descripteur(type valide) :: une clé valide pour les dict du tableau :: return (type en fonction du tableau) :: la valeur maximale """ maxi = tableau[0][descripteur] for index in range( 1, len(tableau) ): if tableau[index][descripteur] > maxi : maxi = tableau[index][descripteur] return maxi # 3 - Programme if __name__ == '__main__' : pokemons = creer_collection('mesdonnees.csv') pokemons = coherence_type(pokemons) atk_max = trouverMax(pokemons, 'Attack') dfc_max = trouverMax(pokemons, 'Defense') reponses = [pok for pok in pokemons if pok['Attack'] > (0.8*atk_max) and pok['Defense'] > (0.6*dfc_max)]

Si on cherche à lancer le programme et à afficher le résultat :

>>> for pokemon in reponses : voir_enregistrement(pokemon) TyranitarMega Tyranitar 164 150 GroudonPrimal Groudon 180 160

On remarquera qu'on aurait pu faire les requêtes l'une après l'autre également puisque c'est un et.

54 55
reponses = [pok for pok in pokemons if pok['Attack'] > (0.8*atk_max)] reponses = [pok for pok in reponses if pok['Defense'] > (0.6*dfc_max)]

✎ 16° Fournir les requêtes à faire pour obtenir la liste des pokemons ayant plus de 180 en attaque OU 180 en défense.

A partir de là, vous voyez qu'on peut s'amuser assez facilement en créant des filtres faisant exactement ce qu'on veut.

On pourrait ainsi obtenir les pokemons par génération.

Deuxième partie de la question : fournir les requêtes à faire pour obtenir la liste des pokemons de génération 2 ET légendaires.

6 - Doublons

Les doublons sont l'un des plus grands problèmes du stockage de données : des données identiques présentes en double (ou pire !). Ils peuvent mettre votre système de gestion par terre assez facilement avec un peu de malchance. Il faut donc veiller à toujours surveiller les enregistrements lorsqu'on les rajoute au fur et à mesure.

C'est pour cela qu'on rajoute toujours un descripteur / attribut supplémentaire dans nos tables : un identifiant qu'on veut absolument n'attribuer qu'à un seul enregistrement.

Dans le cas de notre collection de Pokemons, il n'y en a pas pour l'instant. La clé # permet juste de faire le lien entre plusieurs formes d'un même Pokemon.

Ce qu'on fait habituellement, c'est qu'on attribue une clé d'identification unique à chaque enregistrement APRES avoir vérifier que l'enregistrement n'existe pas déjà dans la collection. Comment se nomme cette clé par défaut ? la clé primaire.

✎ 17° Puisqu'on compare le nouveau enregistrement à tous ceux déjà présents, un par un, la vérification d'absence de doublons sur un nouvel enregistrement qu'on veut rajouter est un algorithme :

  • A : à coût constant (Θ(1))
  • B : à coût linéaire (Θ(n))
  • C : à coût quadratique (Θ(n2))
  • D : à coût cubique (Θ(n3))
Doublons absolus

Les doublons absolus : les deux entrées sont entièrement similaires, même l'identifiant unique ! Ces doublons peuvent apparaître si on fusionne des tables entre elles alors qu'elles sont issues initialement d'une même collection.

Exemple :

  • on extrait dans une nouvelle table les Pokemons ayant plus que 150 en Attaque.
    • Les pokemons obtenus dans la première table :
    • PinsirMega Pinsir - GyaradosMega Gyarados - MewtwoMega Mewtwo X - HeracrossMega Heracross - TyranitarMega Tyranitar - BlazikenMega Blaziken - Slaking - BanetteMega Banette - GroudonPrimal Groudon - RayquazaMega Rayquaza - DeoxysAttack Forme - Rampardos - GarchompMega Garchomp - GalladeMega Gallade - Regigigas - KyuremBlack Kyurem - DiancieMega Diancie - HoopaHoopa Unbound
  • On crée ensuite la table des Pokemons ayant plus de 150 en Défense.
    • Les pokemons obtenus dans la première table :
    • SlowbroMega Slowbro - Cloyster - Onix - Steelix - SteelixMega Steelix - Shuckle - Aggron - AggronMega Aggron - Regirock - GroudonPrimal Groudon - DeoxysDefense Forme - Bastiodon - Avalugg-
  • On fusionne les résultats dans une unique table : si on ne fait pas attention, on va nécessairement se retrouver avec des doublons absolus : tous les Pokemons qui ont une attaque supérieure à 150 ET une défense supérieure à 150.
    • La fusion des pokemons sans précaution :
    • PinsirMega Pinsir - GyaradosMega Gyarados - MewtwoMega Mewtwo X - HeracrossMega Heracross - TyranitarMega Tyranitar - BlazikenMega Blaziken - Slaking - BanetteMega Banette - GroudonPrimal Groudon - RayquazaMega Rayquaza - DeoxysAttack Forme - Rampardos - GarchompMega Garchomp - GalladeMega Gallade - Regigigas - KyuremBlack Kyurem - DiancieMega Diancie - HoopaHoopa Unbound - SlowbroMega Slowbro - Cloyster - Onix - Steelix - SteelixMega Steelix - Shuckle - Aggron - AggronMega Aggron - Regirock - GroudonPrimal Groudon - DeoxysDefense Forme - Bastiodon - Avalugg-

Nous verrons la fusion des tables plus tard. C'était juste pour vous montrer que le problème peut arriver rapidement si on ne fait pas attention.

Pour l'instant, nous allons juste modifier notre création de CSV : nous allons vérifier qu'un enregistrement n'existe pas déjà AVANT de le rajouter dans notre collection. Après tout, on ne peut pas faire une confiance absolue à un fichier qu'on a récupéré sur Internet.

Doublons relatifs

Les doublons relatifs : les deux entrées identifient un même objet mais sont sinon un peu différentes.

  • Cas des Pokemons : la personne renseignant le fichier CSV a peut-être rempli deux fois les informations sur un même Pokemon sur deux lignes très éloignées. On aura ainsi un identifiant différent MAIS le Pokemon présent deux fois.
  • Cas des listes de personnes : il faut vérifier si le p-uplet (nom, prénom, numéro de sécu) n'apparaît qu'une fois. Nom Prénom n'est pas suffisant pour savoir que c'est bien un doublon.
  • Cela peut arriver lors de rajouts : un utilisateur oublie qu'il a déjà crée un compte chez vous et se réinscrit sous le même nom, prénom, email... Si vous ne faites pas attention et que vous acceptez cette nouvelle inscription, votre table des clients risque rapidement de ne plus être exploitable : un même utilisateur va apparaître dans plusieurs enregistrements.

Moralité : demandez vous ce qui doit identifier à coup sur une entrée unique.

Dans le cas de nos Pokemons, c'est ici juste le nom qui importe. Il suffit juste de vérifier que le descripteur Name des autres enregistrements n'est pas identique à celui-ci.

18° Ouvrir le fichier creation_collec.py.

On y trouve dans la fonction creer_collection le code qui rajoute automatiquement le nouvel enregistrement sans vérifier s'il s'agit d'un doublon dont le double est déjà présent dans la collection.

50 51 52 53 54
# 4 - On lit la suite du fichier ligne par ligne for ligne in monFichier : valeurs = decomposer(ligne, ',') # tableau contenant les valeurs nouveau = creer(valeurs, attributs) # dictionnaire clé-valeur laCollec.append(nouveau) # rajout à la collection

Remplacer ce code par le suivant : on vérifie si un Pokemon portant le même nom existe déjà dans la collection avant de faire le append.

Pour cela, on crée une variable booléenne doublon. Si elle devient True, on sort de la boucle de lecture des enregistrements un à un : puisque qu'on sait que c'est un doublon, ce n'est plus la peine de vérifier les autres fiches, c'est une perte de temps.

Nouveauté : comment sortir d'une boucle avant qu'elle ne soit fini ? Il faut utiliser le mot-clé break. On arrête alors immédiatement la boucle et on continue le programme sur la première instruction qui ne soit pas dans la boucle de laquelle on sort.

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
# 4 - On lit la suite du fichier ligne par ligne numero_unique = 1 for ligne in monFichier : valeurs = decomposer(ligne, ',') # tableau contenant les valeurs nouveau = creer(valeurs, attributs) # dictionnaire clé-valeur doublon = False for enregistrement in laCollec : if nouveau['Name'] == enregistrement['Name'] : doublon = True break # Ligne 60 plutôt que retour à la 56 if not(doublon): nouveau['cle_primaire'] = numero_unique numero_unique = numero_unique + 1 laCollec.append(nouveau) # rajout à la collection else : print('Doublon relatif') print(nouveau)

✎ 19° Que doit contenir doublon pour que la condition de la ligne 60 autorise à effectuer le rajout de l'enregistrement avec append à la ligne 63 ?

Quasi-doublon

Ceux là sont plus difficile à déceler : il s'agit d'enregistrement dont la différence tient à un détail, une faute de frappe. Un prénom avec un é ou ï comme Héloïse qu'on trouve orthographié Héloise ou Hèloise. Une lettre oubliée ou déplacée : Sarah écrit Sara ou Sarha... Il existe un très grand nombre d'erreurs possibles.

Autre erreur courant : les majuscules - minuscules. C'est bien pour cela que la plupart des systèmes stockent toutes les entrées en minuscules, transformant au besoin les entrées utilisateurs en minuscules. C'est plus facile de vérifier la cohérence si on est certain que tout doit être normalement en lettres minuscules.

Elles sont bien plus difficiles à déceler que les erreurs de typage (la cohérence entre un type et une valeur est aussi facile à programmer) ou les erreurs de doublons classiques (assez facile à programmer également).

Sur les quasi-doublons, il faut donc un programme qui veille à étudier en détail chaque entrée, au besoin en demandant l'intervention d'un opérateur humain pour valider telle ou telle modification.

Python possède d'ailleurs 3 méthodes applicables aux strings :

  • upper renvoie une copie du string en mettant tout en majuscules.
  • lower renvoie une copie du string en mettant tout en minuscules.
  • title renvoie une copie au chaque début de mot est en majuscule, les autres en minuscules.

✎ 20° En parlant de cohérence des noms utilisés en vue de vérifier la présence de quasi-doublons, quelle critique pourrait-on faire au nom de la clé nommée cle_primaire par rapport aux autres ?

Voilà pour cette activité bien fournie.

En résumé :

  • On doit veiller à la cohérence des données avant d'insérer l'enregistrement dans la table (notamment à transformer les données dans le bon type dans la mesure où les données proviennent souvent d'un fichier texte)
  • On doit veiller à ce qu'un enregistrement ne soit pas un doublon avant de l'insérer dans la table
  • La recherche ou le calcul sur un descripteur n'est pas différent de la même tâche sur un tableau simple.
  • L'extraction de données est simplifiée en Python avec les créations de tableaux par compréhension.

7 - FAQ

Des compléments sur le mot-clé break

On utilise l'instruction break pour sortir définitivement d'une boucle avant sa fin naturelle.

Exemple avec un compteur évoluant normalement de 4 à 80 de 2 en 2. Mais on sort définitivement de la boucle lorsque le compteur vaut 40 :

1 2 3 4 5
for compteur in range(4,80,2): print(compteur, end= " ") if (compteur == 40): break print('Fini')
4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 Fini

Et le mot-clé continue

Lorsqu'on le rencontre, on sort aussi mais moins fortement : on considère juste que le tour de boucle en cours est terminé : on revient au début. Si on est à l'étape 6, on passe donc immédiatement au début de la 7.

Je voudrais compter de 0 à 7 mais sans passer par 5, c'est possible ? Oui, comme ça par exemple :

1 2 3 4 5
for compteur in range(8): if (compteur == 5): continue print(compteur) print('Fini')
0 1 2 3 4 6 7 Fini

Et la structure try except

Aucune connaissance n'est exigible là dessus en 1er NSI. Mais c'est très pratique.

La structure permet notamment de rendre vos applications solides face aux entrées des utilisateurs.

Demander un nombre et vous êtes presque certain que certains vont vous répondre AZERTY

Le principe de cette structure est de remplacé la levée d'exception (lorsqu'il y a une erreur), par ... autre chose qu'un arrêt du programme et un message dans la console.

On trouve 3 blocs dont les actions liées sous tabulées à droite, comme d'habitude avec Python.

Dans le bloc try, l'action qu'on veut tenter de réaliser mais qu'on soupçonne de pouvoir provoquer une erreur dans certaines conditions.

Si l'une des actions provoquent une erreur, on stoppe les instructions à cet endroit et on passe au bloc except. Cela veut dire que ce qui est situé sous l'erreur dans le bloc try ne va pas être exécuté.

Si aucune erreur n'intervient, on ne lève pas d'exception et on n'exécute donc pas le bloc except.

1 2 3 4 5 6 7 8 9 10 11 12
continuer = True while continuer : a = input('Entrez un nombre entier : ') b = input('Entrez un autre nombre entier : ') try : a = int(a) b = int(b) c = a + b continuer = False except : print('Des nombres entiers ! Nan mais quoi !') print(f'{a}+{b} = {c}')

Voici un exemple d'utilisation :

Entrez un nombre entier : 8 Entrez un autre nombre entier : A Des nombres entiers ! Nan mais quoi ! Entrez un nombre entier : 8 Entrez un autre nombre entier : 15 8+15 = 23

Activité publiée le 15 03 2020
Dernière modification : 15 03 2020
Auteur : ows. h.