Python
¶ATTENTION
Pour pouvoir réaliser ce TP, vous devez absolument vous connecter sur le serveur de l'Université (cf ci-dessous). Vous ne pourrais pas vous connecter au serveur MongoDB sinon.
MongoDB
¶MongoDB est une base de données NoSQL distribué de type Document Store, qui répond à 2 objectifs :
Les données sont des documents stockés en Binary JSON (BSON - un dérivé de JSON, qui est similaire à la manipulation de dictionnaires et de listes en python). Il y a des bases de données, regroupant des collections, dans lesquelles il y a les documents. Un des points forts est qu'il n'y a pas de schéma des documents définis en amont,contrairement à une BD relationnelle ou NoSQL de type Column Store.
Les documents peuvent n'avoir aucun point commun entre eux. Un document contient (généralement) l'ensemble des informations, et il n'y a donc pas (ou très peu) de jointure à faire idéalement.
import pymongo
La première opération est de créer une connexion entre python
et MongoDB en utilisant la fonction MongoClient()
. Celle-ci prend en paramètre l'adresse du serveur (IP et port). La commande ci-après permet donc de se connecter au serveur déjà vu en cours précédemment.
con = pymongo.MongoClient("mongodb://193.51.82.104:2343/")
Par le biais de l'objet ainsi créé (con
), on a accès à la liste des bases de données présentes, avec la fonction list_database_name()
.
con.list_database_names()
Nous allons utiliser la base gym
. Pour choisir la base sur laquelle vous voulez travailler, il faut créer un connecteur directement à cette base.
db = con.gym
L'objet db
est doté de différentes fonctions. Une base de données est constitué d’une ou plusieurs collections. Chacune de celles-ci contient un ensemble de documents. Pour voir la liste des collections présentes, on utilise la fonction list_collection_names()
.
db.list_collection_names()
Vous devriez avoir une liste à deux éléments : Gymnases
et Sportifs
.
Ensuite, pour accéder aux collections, et aux fonctions à utiliser dans celle-ci, nous utilisont un formalisme de type db.collection.fonction()
(car nous avons nommer le connecteur db
) :
db
représente la base de données choisie grâce au connecteur ;collection
représente la collection dans laquelle nous allons effectuer l'opération, et doit donc correspondre à une des collections présentes dans la base ;fonction()
détermine l'opération à effectuer sur la collection.En premier lieu, on peut dénombrer le nombre de documents de chaque collection, avec count()
.
db.Sportifs.estimated_document_count()
Les documents présents dans une collection n’ont pas de schémas prédéfinis. Si nous souhaitons avoir une idée de ce que contient la collection, il est possible d’afficher un document (le premier trouvé), avec find_one()
. Cette opération permet de comprendre la structure global d’un document, même s’il peut y avoir des différences entre documents.
db.Sportifs.find_one()
db.Gymnases.find_one()
Il est possible d’inclure des critères de sélection dans cette fonction, que nous verrons dans la suite. De même pour la sélection des items à afficher.
Une autre fonction très utile pour mieux appréhender les données est de lister les valeurs prises par les différents items de la collection, grâce à distinct()
. Pour spécifier un sous-item d’un item, il est nécessaire d’utiliser le formalisme item.sousitem
.
db.Sportifs.distinct("Sexe")
db.Sportifs.distinct("Sports.Jouer")
db.Gymnases.distinct("Ville")
db.Gymnases.distinct("Surface")
db.Gymnases.distinct("Seances.Libelle")
db.Gymnases.distinct("Seances.Jour")
Nous l'avons vu précédemment, l'affichage du résultat de la fonction find_one()
n'est pas totalement lisible. Il est possible d'utiliser la fonction pprint()
du module pprint
, normalement installé dès l'installation de python
. Celle-ci améliore l'affichage, en ajoutant des indentations.
import pprint
pprint.pprint(db.Sportifs.find_one())
Pour l'utiliser sur les résultats des différentes fonctions que nous allons découvrir, nous allons créer une fonction permettant d'utiliser cet affichage, nommée affichage()
. Elle va prendre en paramètre le résultat de la fonction find()
, que nous allons découvrir juste ci-dessous.
def affiche(res):
pprint.pprint(list(res))
Et voici comment utiliser cette fonction.
res = db.Sportifs.find({ "Nom": "KERVADEC" }, { "_id": 0, "Nom": 1 })
affiche(res)
Pour faire des recherches, il existe la fonction find()
. Sans paramètre, elle renvoie l'ensemble des documents. Il faut donc l'utiliser avec précautions. Mais celle-ci peut aussi prendre deux paramètres :
Dans ce premier exemple, on cherche le (ou les) conseiller s’appelant "KERVADEC"
.
affiche(db.Sportifs.find({ "Nom": "KERVADEC" }))
Si l'on désire n'afficher que certains éléments, il est possible d'ajouter un deuxième argument spécifiant les items que l'on veut (avec 1) ou qu'on ne veut pas (avec 0).
affiche(db.Sportifs.find({ "Nom": "KERVADEC" }, { "Nom": 1 }))
Par défaut, l'identifiant du document, toujours nommé _id
, est renvoyé. Pour ne pas l'avoir, il faut ainsi le préciser avec "_id": 0
.
affiche(db.Sportifs.find({ "Nom": "KERVADEC" }, { "_id": 0, "Nom": 1 }))
Le test d'égalité est aussi réalisable avec une variable numérique, comme ici où on cherche les sportifs de 32 ans. On voit ici un effet de python
sur l'ordre d'affichage des items (ici, dans l'ordre alphabétique - comme toujours dans python
).
affiche(db.Sportifs.find({ "Age": 32 }, { "_id": 0, "Nom": 1, "Age": 1 }))
Pour les comparaisons, nous disposons des opérateurs $eq
(equal), $gt
(greater than), $gte
(greater than or equal), $lt
(less than), $lte
(less than or equal) et $ne
(not equal). Voici un exemple d'utilisation pour la recherche de sportifs de plus de 32 ans.
affiche(db.Sportifs.find({ "Age": { "$gte": 32 } }, { "_id": 0, "Nom": 1, "Age": 1 }))
En plus de ces comparaisons simples, nous disposons d'opérateurs de comparaisons à une liste : $in
(présent dans la liste) et $nin
(non présent dans la liste). Ici, nous cherchons donc les sportifs qui ont soit 32 ans, soit 39 ans.
affiche(db.Sportifs.find({ "Age": { "$in": [ 32, 39 ]} }, { "_id": 0, "Nom": 1, "Age": 1 }))
Si l'on veut les sportifs entre 32 et 39 (compris), il faut donc coupler 2 conditions, comme ci-dessous
affiche(db.Sportifs.find({ "Age": { "$gte": 32, "$lte": 39 } }, { "_id": 0, "Nom": 1, "Age": 1 }))
Les documents peuvent être complexes (c’est même le but), et les critères portent donc souvent sur des sous-items. Il faut utiliser le même formalisme que précédemment (item.sousitem
). Il faut noter qu’on peut aller aussi loin que nécessaire dans l’utilisation du "."
. Voici donc les 5 premiers sportifs jouant au Basket (limitation obtenue avec le paramètre limit
).
Nous voyons que le résultat inclu tous les sports joués par le sportif.
affiche(db.Sportifs.find({ "Sports.Jouer" : "Basket ball" }, { "_id": 0, "Nom": 1, "Sexe": 1, "Sports.Jouer": 1}, limit = 5))
Par défaut, si on ajoute des critères de restriction dans le premier paramètre, la recherche se fait avec un ET entre les critères. Nous cherchons ici les joueuses de Basket.
affiche(db.Sportifs.find({ "Sports.Jouer" : "Basket ball", "Sexe": "F" }, { "_id": 0, "Nom": 1}))
Mais si on veut faire des combinaisons autres, il existe des opérateurs logiques : $and
, $or
et $nor
. Ces trois opérations prennent un tableau de critères comme valeur. Nous cherchons ci-dessous les sportifs soit ayant au moins 36 ans, soit de sexe féminin.
affiche(db.Sportifs.find({ "$or": [ { "Age" : { "$gte" : 36 } }, { "Sexe": "F" } ] },
{ "_id" : 0, "Nom" : 1, "Age" : 1, "Sexe" : 1 }))
Comme précédemment indiqué, il est courant qu’un document ne contienne pas tous les items possibles. Si l’on cherche à tester la présence ou non d’un item, on utilise l’opérateur $exists
(avec True
si on teste la présence, et False
l’absence). Dans l'exemple qui suit, nous cherchons les sportifs qui arbitre un sport.
affiche(db.Sportifs.find({ "Sports.Arbitrer" : { "$exists" : True } }, { "_id": 0, "Nom": 1 }))
Il est souvent nécessaire de faire des dénombrements en amont d'opérations, soit pour faire des vérifications de code ou des estimations de charge (ou autre). La fonction count_documents()
peut ainsi prendre le paramètre de restriction de la fonction find()
pour connaître la taille du résultat. Nous cherchons ici le nombre de sportifs de sexe féminin dans la base.
db.Sportifs.count_documents ({ "Sexe" : "F" })
Pour le tri, on utilise la fonction sort()
sur le résultat. Par contre, nous devons ici mettre une liste de critères de tri. Ceux-ci doivent tous être des tuples à deux valeurs : le champ de tri et l'odre choisi (ascendant ou descendant). Pour le spécifier, nous devons utiliser deux valeurs : -1
(pour descendant) et 1
(pour ascendant).
Ici, nous reprenons la recherche des sportifs d'au moins 32 ans. Mais le résultat est trié par ordre décroissant sur l'âge, et par ordre alphabétique pour le nom (pour ceux ayant le même âge donc).
res = db.Sportifs.find({ "Age": { "$gte": 32} }, { "_id": 0, "Nom": 1, "Age": 1 }).sort([ ("Age", -1), ("Nom", 1) ])
affiche(res)
Comme on peut le voir, il est difficile de comprendre la commande, puisque tout est sur la même ligne. Il est ainsi possible de sauter des lignes. Pour la création des dictionnaires pour les paramètres, nous l'avons déjà vu. Mais on peut aussi le faire pour l'enchaînement des fonctions, en mettant le caractère "\"
en fin de ligne, comme ci-dessous.
res = db.Sportifs \
.find({ "Age": { "$gte": 32} }, { "_id": 0, "Nom": 1, "Age": 1 }) \
.sort([ ("Age", -1), ("Nom", 1) ])
affiche(res)
En combinant les 2 possibilités, on peut ainsi écrire l'instruction comme ceci :
res = db.Sportifs \
.find({
"Age": { "$gte": 32}
},
{
"_id": 0,
"Nom": 1,
"Age": 1
}) \
.sort([
("Age", -1),
("Nom", 1)
])
affiche(res)
En plus des recherches classiques d’informations, le calcul d’agrégat est très utilisé, pour l’analyse, la modélisation ou la visualisation de données. Ce calcul s’effectue avec la fonction aggregate()
. Celle-ci prend en paramètre un tableau d’opérations (appelé aussi pipeline
), pouvant contenir les éléments suivants :
$project
: redéfinition des documents (si nécessaire)$match
: restriction sur les documents à utiliser$group
: regroupements et calculs à effectuer$sort
: tri sur les agrégats$unwind
: découpage de tableauxIci, nous réalisons un dénombrement (la somme de la valeur 1 pour chaque document). Nous calculons donc le nombre de gymnases.
res = db.Gymnases.aggregate([
{ "$group": { "_id": "Total", "nb": { "$sum": 1 }}}
])
affiche(res)
Bien évidemment, les calculs peuvent être plus complexes. Par exemple, nous cherchons ici en plus la surface moyenne des gymnases.
res = db.Gymnases.aggregate([
{ "$group": { "_id": "Total", "nb": { "$sum": 1 }, "surfmoy": { "$avg": "$Surface" }}}
])
affiche(res)
Pour faire une agrégation sur un critère, on indique le champs toujours avec le symbole "$"
devant. Nous avons ici, pour chaque ville, le nombre de gymnases et la surface moyenne de ceux-ci.
res = db.Gymnases.aggregate([
{ "$group": {
"_id": "$Ville",
"nb": { "$sum": 1 },
"surfaceMoyenne": { "$avg": "$Surface" }
}}
])
affiche(res)
Ce résultat peut être trié en ajoutant l’action $sort
dans le tableau, avec le même mécanismes que précédemment (1 : ascendant, -1 : descendant).
res = db.Gymnases.aggregate([
{ "$group": { "_id": "$Ville", "nb": { "$sum": 1 }}},
{ "$sort": { "nb": -1 }}
])
affiche(res)
On peut aussi faire une restriction avant le calcul, avec l’opération $match
. Ici, nous nous restreignons aux gymnases dans lesquels il y a (au moins) une séance de Volley.
res = db.Gymnases.aggregate([
{ "$match": { "Seances.Libelle" : "Volley ball" }},
{ "$group": { "_id": "$Ville", "nb": { "$sum": 1 }}}
])
affiche(res)
Il est aussi possible de ré-écrire les documents via la clause $project
. Celle-ci permet de soit indiquer les champs que l'on souhaite garder (avec ": 1"
), soit en calculant un nouveau champs. Par exemple, ici, si nous souhaitons faire un calcul d'agrégat par sexe, la version simple n'est pas satisfaisante ("M"
et "m"
présents dans la base).
res = db.Sportifs.aggregate([
{ "$group": { "_id": "$Sexe", "nb": { "$sum": 1 }}}
])
affiche(res)
En redéfinissant le champs Sexe
, nous remédions à ce problème (ici, la clause $toUpper
permet de mettre en majuscules donc).
res = db.Sportifs.aggregate([
{ "$project": { "Sexe": { "$toUpper": "$Sexe" }}},
{ "$group": { "_id": "$Sexe", "nb": { "$sum": 1 }}}
])
affiche(res)
Imaginons maintenant que nous souhaitons calculer le nombre de séances journalières. La première idée serait de réaliser l’opération suivante. On se limite aux 5 premiers documents (gymnases donc), à 2 séances.
res = db.Gymnases.aggregate([
{ "$match": { "Seances": { "$size": 2 } }},
{ "$limit": 5 },
{ "$project": { "Seances": 1 }},
{ "$group": { "_id": "$Seances.Jour", "nb": { "$sum": 1 }}}
])
affiche(res)
Malheureusement, nous voyons que le regroupement se fait par couple de jours existant dans la base. Il faut donc faire ce qu'on appelle un découpage des tableaux Seances
dans chaque document. Pour cela, il existe l’opération $unwind
.
Pour montrer comment fonctionne cette opération, voici les documents obtenus lorsqu’on l’applique sur le premier gymnase à uniquement 2 séances. On s’apercoit que chaque document ne contient plus qu’une seule séance.
res = db.Gymnases.aggregate([
{ "$match": { "Seances": { "$size": 2 } }},
{ "$limit": 1 },
{ "$unwind": "$Seances" }
])
affiche(res)
Ainsi, nous pouvons donc maintenant faire l’opération de regroupement par jour de la semaine.
res = db.Gymnases.aggregate([
{ "$unwind": "$Seances" },
{ "$group": { "_id": "$Seances.Jour", "nb": { "$sum": 1 }} }
])
affiche(res)
On peut utiliser les commandes unwind
, project
et sort
pour réaliser des calculs d'agrégats complexes. Ici, nous cherchons le nombre total de séances par jour, ceux-ci étant triés dans l'ordre décroissant du nombre de séances.
res = db.Gymnases.aggregate([
{ "$unwind": "$Seances" },
{ "$project": { "Jour": { "$toLower": "$Seances.Jour" } }},
{ "$group": { "_id": "$Jour", "nb": { "$sum": 1 }} },
{ "$sort": { "nb": -1 }}
])
affiche(res)
DataFrame
¶Pour pouvoir utiliser les données recherchées, nous pouvons les transformer en DataFrame
(de type pandas
). Pour cela, nous les transformons en list
, puis en DataFrame
, comme dans l'exemple ci-dessous.
import pandas
res = db.Sportifs.find()
df = pandas.DataFrame(list(res))
Quand on regarde le DataFrame
obtenu, nous remarquons que pour certains colonnes (Sports
ici), le contenu est un objet complexe (un dictionnaire en l'occurence ici).
df.head()
Nous récupérons ici le premier sportif dans l'objet df0
.
df0 = df.loc[0,:]
Avec cet objet, nous accédons aux sports qu'il joue/entraîne/arbitre comme suit.
df0.Sports
Pour obtenir le tableau des sports joués (par exemple), nous le récupérons avec le code suivant.
df0.Sports["Jouer"]
gym
¶Répondre aux questions suivantes
Dans la base de données test
, nous disposons d'une collection contenant les informations de plus de 25000 restaurants new-yorkais. Pour chaque restaurant, nous avons des informations basiques (nom, quartier, adresse, type de cuisine) et l'ensemble des visites sanitaires (dans le champs grade
). Pour chaque visite, nous avons une date, un grade (A : restaurant sain, B : problèmes sanitaires à revoir rapidement, ...) et un score (nombre d'infractions sanitaires : plus il est élevé, moins le restaurant respecte les consignes - s'il est égal à -1, c'est qu'il n'y a sûrement pas encore eu de visite).
db2 = con.test
db2.list_collection_names()
db2.restaurants.find_one()