Connexion à MongoDB sous 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.

https://jupyter.ens.math-info.univ-paris5.fr/hub/login

Présentation de MongoDB

MongoDB est une base de données NoSQL distribué de type Document Store, qui répond à 2 objectifs :

  • Gérer de gros volumes ;
  • Faciliter le déploiement et d'utilisation.

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.

Utilisation générale

Nous allons voir dans ce TP comment nous pouvons récupérer des données stockées dans une base MongoDB, par le biais de la librairie pymongo. Pour l'utiliser, on l'importer classiquement comme ci-dessous.

In [1]:
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.

In [2]:
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().

In [3]:
con.list_database_names()
Out[3]:
['admin',
 'config',
 'gym',
 'horodateurs',
 'local',
 'medicaments',
 'test',
 'trafic',
 'trafic_safe',
 'vlsstat',
 'world']

Nous allons utiliser la base gym. Pour choisir la base sur laquelle vous voulez travailler, il faut créer un connecteur directement à cette base.

In [4]:
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().

In [5]:
db.list_collection_names()
Out[5]:
['Sportifs', 'Gymnases']

Vous devriez avoir une liste à deux éléments : Gymnases et Sportifs.

Fonctions de base

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().

In [6]:
db.Sportifs.estimated_document_count()
Out[6]:
150

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.

In [7]:
db.Sportifs.find_one()
Out[7]:
{'_id': '566eec5f662b388eba464203',
 'IdSportif': 1,
 'Nom': 'BOUTAHAR',
 'Prenom': 'Abderahim',
 'Sexe': 'm',
 'Age': 30,
 'Sports': {'Jouer': ['Volley ball', 'Tennis', 'Football'],
  'Arbitrer': ['Basket ball', 'Volley ball', 'Hockey'],
  'Entrainer': ['Basket ball',
   'Volley ball',
   'Hand ball',
   'Hockey',
   'Badmington']}}
In [8]:
db.Gymnases.find_one()
Out[8]:
{'_id': '566eec69662b388eba464299',
 'IdGymnase': 1,
 'NomGymnase': 'PAUL ELUARD',
 'Adresse': '2 rue des pépines',
 'Ville': 'STAINS',
 'Surface': 200,
 'Seances': [{'IdSportifEntraineur': 149,
   'Jour': 'Samedi',
   'Horaire': 9.0,
   'Duree': 60,
   'Libelle': 'Basket ball'},
  {'IdSportifEntraineur': 1,
   'Jour': 'Lundi',
   'Horaire': 9.0,
   'Duree': 60,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 1,
   'Jour': 'Lundi',
   'Horaire': 10.0,
   'Duree': 60,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 1,
   'Jour': 'Lundi',
   'Horaire': 11.3,
   'Duree': 60,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 1,
   'Jour': 'Lundi',
   'Horaire': 14.0,
   'Duree': 90,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 1,
   'Jour': 'lundi',
   'Horaire': 17.3,
   'Duree': 120,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 1,
   'Jour': 'Lundi',
   'Horaire': 19.3,
   'Duree': 120,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 2,
   'Jour': 'Dimanche',
   'Horaire': 17.3,
   'Duree': 120,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 2,
   'Jour': 'Dimanche',
   'Horaire': 19.3,
   'Duree': 120,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 2,
   'Jour': 'mardi',
   'Horaire': 17.3,
   'Duree': 120,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 2,
   'Jour': 'mercredi',
   'Horaire': 17.3,
   'Duree': 120,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 2,
   'Jour': 'Samedi',
   'Horaire': 15.3,
   'Duree': 60,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 2,
   'Jour': 'Samedi',
   'Horaire': 16.3,
   'Duree': 60,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 2,
   'Jour': 'Samedi',
   'Horaire': 17.3,
   'Duree': 120,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 3,
   'Jour': 'jeudi',
   'Horaire': 20.0,
   'Duree': 30,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 3,
   'Jour': 'lundi',
   'Horaire': 14.0,
   'Duree': 60,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 3,
   'Jour': 'lundi',
   'Horaire': 18.0,
   'Duree': 30,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 3,
   'Jour': 'lundi',
   'Horaire': 19.0,
   'Duree': 30,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 3,
   'Jour': 'lundi',
   'Horaire': 20.0,
   'Duree': 30,
   'Libelle': 'Hand ball'},
  {'IdSportifEntraineur': 7,
   'Jour': 'mercredi',
   'Horaire': 17.0,
   'Duree': 90,
   'Libelle': 'Hockey'}]}

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.

In [9]:
db.Sportifs.distinct("Sexe")
Out[9]:
['m', 'M', 'F']
In [10]:
db.Sportifs.distinct("Sports.Jouer")
Out[10]:
['Football',
 'Tennis',
 'Volley ball',
 'Basket ball',
 'Ping pong',
 'Badmington',
 'Hand ball']
In [11]:
db.Gymnases.distinct("Ville")
Out[11]:
['STAINS',
 'MONTMORENCY',
 'PIERREFITTE',
 'SARCELLES',
 'SAINT DENIS',
 'VILLETANEUSE',
 'GARGES']
In [12]:
db.Gymnases.distinct("Surface")
Out[12]:
[200, 450, 400, 500, 620, 360, 420, 300, 480, 600, 520, 350]
In [13]:
db.Gymnases.distinct("Seances.Libelle")
Out[13]:
['Basket ball', 'Hand ball', 'Hockey', 'Volley ball']
In [14]:
db.Gymnases.distinct("Seances.Jour")
Out[14]:
['Dimanche',
 'Lundi',
 'Samedi',
 'jeudi',
 'lundi',
 'mardi',
 'mercredi',
 'dimanche',
 'Mercredi',
 'Vendredi',
 'vendredi',
 'samedi',
 'Mardi',
 'Jeudi']

Affichage des résultats

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.

In [15]:
import pprint
pprint.pprint(db.Sportifs.find_one())
{'Age': 30,
 'IdSportif': 1,
 'Nom': 'BOUTAHAR',
 'Prenom': 'Abderahim',
 'Sexe': 'm',
 'Sports': {'Arbitrer': ['Basket ball', 'Volley ball', 'Hockey'],
            'Entrainer': ['Basket ball',
                          'Volley ball',
                          'Hand ball',
                          'Hockey',
                          'Badmington'],
            'Jouer': ['Volley ball', 'Tennis', 'Football']},
 '_id': '566eec5f662b388eba464203'}

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.

In [16]:
def affiche(res):
    pprint.pprint(list(res))

Et voici comment utiliser cette fonction.

In [17]:
res = db.Sportifs.find({ "Nom": "KERVADEC" }, { "_id": 0, "Nom": 1 })
affiche(res)
[{'Nom': 'KERVADEC'}]

Recherche d'informations

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 :

  • les critères de sélection des documents
  • les choix d’items des documents à afficher

Dans ce premier exemple, on cherche le (ou les) conseiller s’appelant "KERVADEC".

In [18]:
affiche(db.Sportifs.find({ "Nom": "KERVADEC" }))
[{'Age': 28,
  'IdSportif': 2,
  'IdSportifConseiller': 1,
  'Nom': 'KERVADEC',
  'Prenom': 'Yann',
  'Sexe': 'M',
  'Sports': {'Arbitrer': ['Hockey', 'Football'],
             'Entrainer': ['Basket ball',
                           'Volley ball',
                           'Hand ball',
                           'Tennis',
                           'Hockey',
                           'Badmington',
                           'Ping pong',
                           'Boxe'],
             'Jouer': ['Basket ball', 'Volley ball', 'Ping pong', 'Football']},
  '_id': '566eec5f662b388eba464204'}]

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).

In [19]:
affiche(db.Sportifs.find({ "Nom": "KERVADEC" }, { "Nom": 1 }))
[{'Nom': 'KERVADEC', '_id': '566eec5f662b388eba464204'}]

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.

In [20]:
affiche(db.Sportifs.find({ "Nom": "KERVADEC" }, { "_id": 0, "Nom": 1 }))
[{'Nom': 'KERVADEC'}]

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).

In [21]:
affiche(db.Sportifs.find({ "Age": 32 }, { "_id": 0, "Nom": 1, "Age": 1 }))
[{'Age': 32, 'Nom': 'DORLEANS'},
 {'Age': 32, 'Nom': 'TIZEGHAT'},
 {'Age': 32, 'Nom': 'BAZOUD'},
 {'Age': 32, 'Nom': 'BONE'}]

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.

In [22]:
affiche(db.Sportifs.find({ "Age": { "$gte": 32 } }, { "_id": 0, "Nom": 1, "Age": 1 }))
[{'Age': 32, 'Nom': 'DORLEANS'},
 {'Age': 40, 'Nom': 'RABAHI'},
 {'Age': 32, 'Nom': 'TIZEGHAT'},
 {'Age': 32, 'Nom': 'BAZOUD'},
 {'Age': 39, 'Nom': 'SARRAZIN'},
 {'Age': 40, 'Nom': 'HOUEL'},
 {'Age': 36, 'Nom': 'LEROUX'},
 {'Age': 32, 'Nom': 'BONE'}]

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.

In [23]:
affiche(db.Sportifs.find({ "Age": { "$in": [ 32, 39 ]} }, { "_id": 0, "Nom": 1, "Age": 1 }))
[{'Age': 32, 'Nom': 'DORLEANS'},
 {'Age': 32, 'Nom': 'TIZEGHAT'},
 {'Age': 32, 'Nom': 'BAZOUD'},
 {'Age': 39, 'Nom': 'SARRAZIN'},
 {'Age': 32, 'Nom': 'BONE'}]

Si l'on veut les sportifs entre 32 et 39 (compris), il faut donc coupler 2 conditions, comme ci-dessous

In [24]:
affiche(db.Sportifs.find({ "Age": { "$gte": 32, "$lte": 39 } }, { "_id": 0, "Nom": 1, "Age": 1 }))
[{'Age': 32, 'Nom': 'DORLEANS'},
 {'Age': 32, 'Nom': 'TIZEGHAT'},
 {'Age': 32, 'Nom': 'BAZOUD'},
 {'Age': 39, 'Nom': 'SARRAZIN'},
 {'Age': 36, 'Nom': 'LEROUX'},
 {'Age': 32, 'Nom': 'BONE'}]

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.

In [25]:
affiche(db.Sportifs.find({ "Sports.Jouer" : "Basket ball" }, { "_id": 0, "Nom": 1, "Sexe": 1, "Sports.Jouer": 1}, limit = 5))
[{'Nom': 'KERVADEC',
  'Sexe': 'M',
  'Sports': {'Jouer': ['Basket ball', 'Volley ball', 'Ping pong', 'Football']}},
 {'Nom': 'COMES',
  'Sexe': 'F',
  'Sports': {'Jouer': ['Basket ball',
                       'Volley ball',
                       'Badmington',
                       'Ping pong']}},
 {'Nom': 'RETALDI',
  'Sexe': 'F',
  'Sports': {'Jouer': ['Basket ball',
                       'Volley ball',
                       'Hand ball',
                       'Ping pong']}},
 {'Nom': 'CAILLIOT',
  'Sexe': 'F',
  'Sports': {'Jouer': ['Basket ball', 'Volley ball', 'Ping pong']}},
 {'Nom': 'TIENER',
  'Sexe': 'M',
  'Sports': {'Jouer': ['Basket ball',
                       'Volley ball',
                       'Hand ball',
                       'Ping pong',
                       'Football']}}]

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.

In [26]:
affiche(db.Sportifs.find({ "Sports.Jouer" : "Basket ball", "Sexe": "F" }, { "_id": 0, "Nom": 1}))
[{'Nom': 'COMES'},
 {'Nom': 'RETALDI'},
 {'Nom': 'CAILLIOT'},
 {'Nom': 'LEJEUNE'},
 {'Nom': 'HEDDI'},
 {'Nom': 'JOUVE'},
 {'Nom': 'GUERRAOUI'},
 {'Nom': 'CLERICE'},
 {'Nom': 'MARIE'},
 {'Nom': 'BELZ'}]

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.

In [27]:
affiche(db.Sportifs.find({ "$or": [ { "Age" : { "$gte" : 36 } }, { "Sexe": "F" } ] }, 
                         { "_id" : 0, "Nom" : 1, "Age" : 1, "Sexe" : 1 }))
[{'Age': 25, 'Nom': 'HUE', 'Sexe': 'F'},
 {'Age': 22, 'Nom': 'COMES', 'Sexe': 'F'},
 {'Age': 30, 'Nom': 'RETALDI', 'Sexe': 'F'},
 {'Age': 23, 'Nom': 'ANTUNES', 'Sexe': 'F'},
 {'Age': 24, 'Nom': 'CAILLIOT', 'Sexe': 'F'},
 {'Age': 25, 'Nom': 'HOSNI', 'Sexe': 'F'},
 {'Age': 23, 'Nom': 'LEJEUNE', 'Sexe': 'F'},
 {'Age': 23, 'Nom': 'MICHEL', 'Sexe': 'F'},
 {'Age': 40, 'Nom': 'RABAHI', 'Sexe': 'M'},
 {'Age': 22, 'Nom': 'ROUSSEL', 'Sexe': 'F'},
 {'Age': 25, 'Nom': 'LAZZARI', 'Sexe': 'F'},
 {'Age': 22, 'Nom': 'DELOVINA', 'Sexe': 'F'},
 {'Age': 23, 'Nom': 'MATHIEU', 'Sexe': 'F'},
 {'Age': 25, 'Nom': 'GROEN', 'Sexe': 'F'},
 {'Age': 23, 'Nom': 'HEDDI', 'Sexe': 'F'},
 {'Age': 24, 'Nom': 'JOUVE', 'Sexe': 'F'},
 {'Age': 25, 'Nom': 'GUERRAOUI', 'Sexe': 'F'},
 {'Age': 23, 'Nom': 'CLERICE', 'Sexe': 'F'},
 {'Age': 30, 'Nom': 'LANOE', 'Sexe': 'F'},
 {'Age': 26, 'Nom': 'VONTHRON', 'Sexe': 'F'},
 {'Age': 30, 'Nom': 'REGNAULD', 'Sexe': 'F'},
 {'Age': 25, 'Nom': 'MARIE', 'Sexe': 'F'},
 {'Age': 39, 'Nom': 'SARRAZIN', 'Sexe': 'F'},
 {'Age': 30, 'Nom': 'HALGATTE', 'Sexe': 'F'},
 {'Age': 30, 'Nom': 'DROULLON', 'Sexe': 'F'},
 {'Age': 40, 'Nom': 'HOUEL', 'Sexe': 'M'},
 {'Age': 36, 'Nom': 'LEROUX', 'Sexe': 'M'},
 {'Age': 30, 'Nom': 'SEHIER', 'Sexe': 'F'},
 {'Age': 22, 'Nom': 'MICHEL', 'Sexe': 'F'},
 {'Age': 30, 'Nom': 'BELUAU', 'Sexe': 'F'},
 {'Age': 30, 'Nom': 'FERREIRA', 'Sexe': 'F'},
 {'Age': 30, 'Nom': 'BECQUET', 'Sexe': 'F'},
 {'Age': 30, 'Nom': 'SWERTVAEGER', 'Sexe': 'F'},
 {'Age': 30, 'Nom': 'GALLOIS', 'Sexe': 'F'},
 {'Age': 26, 'Nom': 'LABOULAIS', 'Sexe': 'F'},
 {'Age': 26, 'Nom': 'DUDOUIT', 'Sexe': 'F'},
 {'Age': 26, 'Nom': 'MADELAINE', 'Sexe': 'F'},
 {'Age': 26, 'Nom': 'BESNARD', 'Sexe': 'F'},
 {'Age': 26, 'Nom': 'BELZ', 'Sexe': 'F'},
 {'Age': 26, 'Nom': 'BONNET', 'Sexe': 'F'},
 {'Age': 26, 'Nom': 'CORNET', 'Sexe': 'F'},
 {'Age': 26, 'Nom': 'BEUZELIN', 'Sexe': 'F'},
 {'Age': 26, 'Nom': 'GRANDIDIER', 'Sexe': 'F'},
 {'Age': 25, 'Nom': 'LENEVEU', 'Sexe': 'F'},
 {'Age': 25, 'Nom': 'CLERICE', 'Sexe': 'F'},
 {'Age': 27, 'Nom': 'COMES', 'Sexe': 'F'},
 {'Age': 27, 'Nom': 'BELZ', 'Sexe': 'F'}]

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.

In [28]:
affiche(db.Sportifs.find({ "Sports.Arbitrer" : { "$exists" : True } }, { "_id": 0, "Nom": 1 }))
[{'Nom': 'BOUTAHAR'},
 {'Nom': 'KERVADEC'},
 {'Nom': 'HUE'},
 {'Nom': 'DORLEANS'},
 {'Nom': 'COMES'},
 {'Nom': 'RETALDI'},
 {'Nom': 'GOMEZ'},
 {'Nom': 'TESTEMONT'},
 {'Nom': 'TIENER'},
 {'Nom': 'ROUSSEL'},
 {'Nom': 'VAN CAUTER'},
 {'Nom': 'TANQUE'},
 {'Nom': 'CLERICE'},
 {'Nom': 'TIZEGHAT'},
 {'Nom': 'LEROUX'},
 {'Nom': 'RICHARD'},
 {'Nom': 'STILO'},
 {'Nom': 'BELZ'},
 {'Nom': 'HENRY'}]

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.

In [29]:
db.Sportifs.count_documents ({ "Sexe" : "F" })
Out[29]:
44

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).

In [30]:
res = db.Sportifs.find({ "Age": { "$gte": 32} }, { "_id": 0, "Nom": 1, "Age": 1 }).sort([ ("Age", -1), ("Nom", 1) ])
affiche(res)
[{'Age': 40, 'Nom': 'HOUEL'},
 {'Age': 40, 'Nom': 'RABAHI'},
 {'Age': 39, 'Nom': 'SARRAZIN'},
 {'Age': 36, 'Nom': 'LEROUX'},
 {'Age': 32, 'Nom': 'BAZOUD'},
 {'Age': 32, 'Nom': 'BONE'},
 {'Age': 32, 'Nom': 'DORLEANS'},
 {'Age': 32, 'Nom': 'TIZEGHAT'}]

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.

In [31]:
res = db.Sportifs \
    .find({ "Age": { "$gte": 32} }, { "_id": 0, "Nom": 1, "Age": 1 }) \
    .sort([ ("Age", -1), ("Nom", 1) ])
affiche(res)
[{'Age': 40, 'Nom': 'HOUEL'},
 {'Age': 40, 'Nom': 'RABAHI'},
 {'Age': 39, 'Nom': 'SARRAZIN'},
 {'Age': 36, 'Nom': 'LEROUX'},
 {'Age': 32, 'Nom': 'BAZOUD'},
 {'Age': 32, 'Nom': 'BONE'},
 {'Age': 32, 'Nom': 'DORLEANS'},
 {'Age': 32, 'Nom': 'TIZEGHAT'}]

En combinant les 2 possibilités, on peut ainsi écrire l'instruction comme ceci :

In [32]:
res = db.Sportifs \
    .find({ 
        "Age": { "$gte": 32} 
        }, 
        { 
            "_id": 0, 
            "Nom": 1, 
            "Age": 1 
        }) \
    .sort([ 
        ("Age", -1), 
        ("Nom", 1) 
    ])
affiche(res)
[{'Age': 40, 'Nom': 'HOUEL'},
 {'Age': 40, 'Nom': 'RABAHI'},
 {'Age': 39, 'Nom': 'SARRAZIN'},
 {'Age': 36, 'Nom': 'LEROUX'},
 {'Age': 32, 'Nom': 'BAZOUD'},
 {'Age': 32, 'Nom': 'BONE'},
 {'Age': 32, 'Nom': 'DORLEANS'},
 {'Age': 32, 'Nom': 'TIZEGHAT'}]

Agrégats

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 tableaux

Regroupement

Ici, nous réalisons un dénombrement (la somme de la valeur 1 pour chaque document). Nous calculons donc le nombre de gymnases.

In [33]:
res = db.Gymnases.aggregate([ 
    { "$group": { "_id": "Total", "nb": { "$sum": 1 }}}
])
affiche(res)
[{'_id': 'Total', 'nb': 28}]

Bien évidemment, les calculs peuvent être plus complexes. Par exemple, nous cherchons ici en plus la surface moyenne des gymnases.

In [34]:
res = db.Gymnases.aggregate([ 
    { "$group": { "_id": "Total", "nb": { "$sum": 1 }, "surfmoy": { "$avg": "$Surface" }}}
])
affiche(res)
[{'_id': 'Total', 'nb': 28, 'surfmoy': 444.2857142857143}]

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.

In [35]:
res = db.Gymnases.aggregate([ 
    { "$group": { 
        "_id": "$Ville", 
        "nb": { "$sum": 1 }, 
        "surfaceMoyenne": { "$avg": "$Surface" }
    }}
])
affiche(res)
[{'_id': 'SAINT DENIS', 'nb': 3, 'surfaceMoyenne': 490.0},
 {'_id': 'STAINS', 'nb': 6, 'surfaceMoyenne': 333.3333333333333},
 {'_id': 'MONTMORENCY', 'nb': 5, 'surfaceMoyenne': 470.0},
 {'_id': 'GARGES', 'nb': 1, 'surfaceMoyenne': 400.0},
 {'_id': 'VILLETANEUSE', 'nb': 3, 'surfaceMoyenne': 523.3333333333334},
 {'_id': 'PIERREFITTE', 'nb': 5, 'surfaceMoyenne': 382.0},
 {'_id': 'SARCELLES', 'nb': 5, 'surfaceMoyenne': 548.0}]

Tri

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).

In [36]:
res = db.Gymnases.aggregate([ 
    { "$group": { "_id": "$Ville", "nb": { "$sum": 1 }}},
    { "$sort": { "nb": -1 }}
])
affiche(res)
[{'_id': 'STAINS', 'nb': 6},
 {'_id': 'MONTMORENCY', 'nb': 5},
 {'_id': 'PIERREFITTE', 'nb': 5},
 {'_id': 'SARCELLES', 'nb': 5},
 {'_id': 'SAINT DENIS', 'nb': 3},
 {'_id': 'VILLETANEUSE', 'nb': 3},
 {'_id': 'GARGES', 'nb': 1}]

Restriction

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.

In [37]:
res = db.Gymnases.aggregate([ 
    { "$match": { "Seances.Libelle" : "Volley ball" }},
    { "$group": { "_id": "$Ville", "nb": { "$sum": 1 }}}
])
affiche(res)
[{'_id': 'SAINT DENIS', 'nb': 2},
 {'_id': 'MONTMORENCY', 'nb': 5},
 {'_id': 'VILLETANEUSE', 'nb': 1},
 {'_id': 'PIERREFITTE', 'nb': 3},
 {'_id': 'STAINS', 'nb': 4},
 {'_id': 'SARCELLES', 'nb': 2}]

Projection

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).

In [38]:
res = db.Sportifs.aggregate([ 
    { "$group": { "_id": "$Sexe", "nb": { "$sum": 1 }}}
])
affiche(res)
[{'_id': 'F', 'nb': 44}, {'_id': 'M', 'nb': 105}, {'_id': 'm', 'nb': 1}]

En redéfinissant le champs Sexe, nous remédions à ce problème (ici, la clause $toUpper permet de mettre en majuscules donc).

In [39]:
res = db.Sportifs.aggregate([ 
    { "$project": { "Sexe": { "$toUpper": "$Sexe" }}},
    { "$group": { "_id": "$Sexe", "nb": { "$sum": 1 }}}
])
affiche(res)
[{'_id': 'F', 'nb': 44}, {'_id': 'M', 'nb': 106}]

Séparation de documents

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.

In [40]:
res = db.Gymnases.aggregate([ 
    { "$match": { "Seances": { "$size": 2 } }},
    { "$limit": 5 },
    { "$project": { "Seances": 1 }},
    { "$group": { "_id": "$Seances.Jour", "nb": { "$sum": 1 }}}
])
affiche(res)
[{'_id': ['Jeudi', 'mercredi'], 'nb': 1},
 {'_id': ['Mardi', 'dimanche'], 'nb': 1},
 {'_id': ['lundi', 'jeudi'], 'nb': 1},
 {'_id': ['Vendredi', 'mercredi'], 'nb': 1},
 {'_id': ['vendredi', 'jeudi'], 'nb': 1}]

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.

In [41]:
res = db.Gymnases.aggregate([
    { "$match": { "Seances": { "$size": 2 } }},
    { "$limit": 1 },
    { "$unwind": "$Seances" }
])
affiche(res)
[{'Adresse': 'Allée J.B. Lulli',
  'IdGymnase': 4,
  'NomGymnase': 'PAUL ELUARD',
  'Seances': {'Duree': 30,
              'Horaire': 10.0,
              'IdSportifEntraineur': 149,
              'Jour': 'Vendredi',
              'Libelle': 'Basket ball'},
  'Surface': 500,
  'Ville': 'SARCELLES',
  '_id': '566eec69662b388eba46429c'},
 {'Adresse': 'Allée J.B. Lulli',
  'IdGymnase': 4,
  'NomGymnase': 'PAUL ELUARD',
  'Seances': {'Duree': 60,
              'Horaire': 19.0,
              'IdSportifEntraineur': 6,
              'Jour': 'mercredi',
              'Libelle': 'Hockey'},
  'Surface': 500,
  'Ville': 'SARCELLES',
  '_id': '566eec69662b388eba46429c'}]

Ainsi, nous pouvons donc maintenant faire l’opération de regroupement par jour de la semaine.

In [42]:
res = db.Gymnases.aggregate([
    { "$unwind": "$Seances" }, 
    { "$group": { "_id": "$Seances.Jour", "nb": { "$sum": 1 }} }
])
affiche(res)
[{'_id': 'Mardi', 'nb': 1},
 {'_id': 'samedi', 'nb': 5},
 {'_id': 'vendredi', 'nb': 5},
 {'_id': 'Dimanche', 'nb': 3},
 {'_id': 'dimanche', 'nb': 14},
 {'_id': 'lundi', 'nb': 14},
 {'_id': 'mardi', 'nb': 7},
 {'_id': 'mercredi', 'nb': 12},
 {'_id': 'Samedi', 'nb': 4},
 {'_id': 'jeudi', 'nb': 5},
 {'_id': 'Mercredi', 'nb': 1},
 {'_id': 'Jeudi', 'nb': 1},
 {'_id': 'Lundi', 'nb': 9},
 {'_id': 'Vendredi', 'nb': 1}]

Combinaison

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.

In [43]:
res = db.Gymnases.aggregate([
    { "$unwind": "$Seances" }, 
    { "$project": { "Jour": { "$toLower": "$Seances.Jour" } }},
    { "$group": { "_id": "$Jour", "nb": { "$sum": 1 }} },
    { "$sort": { "nb": -1 }}
])
affiche(res)
[{'_id': 'lundi', 'nb': 23},
 {'_id': 'dimanche', 'nb': 17},
 {'_id': 'mercredi', 'nb': 13},
 {'_id': 'samedi', 'nb': 9},
 {'_id': 'mardi', 'nb': 8},
 {'_id': 'vendredi', 'nb': 6},
 {'_id': 'jeudi', 'nb': 6}]

Importation dans un 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.

In [44]:
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).

In [45]:
df.head()
Out[45]:
_id IdSportif Nom Prenom Sexe Age Sports IdSportifConseiller
0 566eec5f662b388eba464203 1 BOUTAHAR Abderahim m 30 {'Jouer': ['Volley ball', 'Tennis', 'Football'... NaN
1 566eec5f662b388eba464204 2 KERVADEC Yann M 28 {'Jouer': ['Basket ball', 'Volley ball', 'Ping... 1.0
2 566eec5f662b388eba464205 3 HUE Pascale F 25 {'Jouer': ['Volley ball', 'Ping pong'], 'Arbit... 1.0
3 566eec5f662b388eba464206 4 DORLEANS Jean-michel M 32 {'Jouer': ['Volley ball', 'Football'], 'Arbitr... 1.0
4 566eec5f662b388eba464207 5 COMES Sylvie F 22 {'Jouer': ['Basket ball', 'Volley ball', 'Badm... 1.0

Nous récupérons ici le premier sportif dans l'objet df0.

In [46]:
df0 = df.loc[0,:]

Avec cet objet, nous accédons aux sports qu'il joue/entraîne/arbitre comme suit.

In [47]:
df0.Sports
Out[47]:
{'Jouer': ['Volley ball', 'Tennis', 'Football'],
 'Arbitrer': ['Basket ball', 'Volley ball', 'Hockey'],
 'Entrainer': ['Basket ball',
  'Volley ball',
  'Hand ball',
  'Hockey',
  'Badmington']}

Pour obtenir le tableau des sports joués (par exemple), nous le récupérons avec le code suivant.

In [48]:
df0.Sports["Jouer"]
Out[48]:
['Volley ball', 'Tennis', 'Football']

A faire

Base de données gym

Répondre aux questions suivantes

  1. Quels sont les sportifs (identifiant, nom et prénom) qui ont entre 20 et 30 ans ?
  2. Quels sont les gymnases de "Villetaneuse" ou de "Sarcelles" qui ont une surface de plus de 400 m2 ?
  3. Quels sont les sportifs (identifiant et nom) qui pratiquent du hand ball ?
  4. Dans quels gymnases et quels jours y a t-il des séances de hand ball ?
  5. Dans quels gymnases peut-on jouer au hockey le mercredi apres 15H ?
  6. Quels sportifs (identifiant et nom) ne pratiquent aucun sport ?
  7. Quels gymnases n'ont pas de séances le dimanche ?
  8. Quels sont les entraîneurs qui sont aussi joueurs ?
  9. Quels sont les sportifs qui sont des conseillers ?
  10. Pour le sportif "Kervadec" quel est le nom de son conseiller ?
  11. Quels entraîneurs entraînent du hand ball et du basket ball ?
  12. Quelle est la moyenne d'âge des sportives qui pratiquent du basket ball ?
  13. Quels sont les gymnases de "Stains" ou de "Montmorency" qui ont la plus grande surface ?
  14. Quels entraîneurs n'entraînent que du hand ball ou du basket ball ?
  15. Quels sportifs n'ont pas de conseillers ?
  16. Pour chaque sportif donner le nombre de sports qu'il arbitre
  17. Pour chaque entraîneurs de hand ball quel est le nombre de séances journalières qu'il assure ?
  18. Quels sont les gymnases ayant plus de 2 séances le mercredi ?
  19. Pour chaque gymnase de Montmorency : quel est le nombre de séances journalières de chaque sport propose ?
  20. Dans quels gymnases et quels jours y a t-il au moins 3 séances de volley ball dans la journée ?

Restaurants new-yorkais

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).

In [49]:
db2 = con.test
db2.list_collection_names()
Out[49]:
['restaurants', 'test', 'grades']
In [50]:
db2.restaurants.find_one()
Out[50]:
{'_id': ObjectId('58ac16d1a251358ee4ee87dd'),
 'address': {'building': '1007',
  'coord': [-73.856077, 40.848447],
  'street': 'Morris Park Ave',
  'zipcode': '10462'},
 'borough': 'Bronx',
 'cuisine': 'Bakery',
 'grades': [{'date': datetime.datetime(2014, 3, 3, 0, 0),
   'grade': 'A',
   'score': 2},
  {'date': datetime.datetime(2013, 9, 11, 0, 0), 'grade': 'A', 'score': 6},
  {'date': datetime.datetime(2013, 1, 24, 0, 0), 'grade': 'A', 'score': 10},
  {'date': datetime.datetime(2011, 11, 23, 0, 0), 'grade': 'A', 'score': 9},
  {'date': datetime.datetime(2011, 3, 10, 0, 0), 'grade': 'B', 'score': 14}],
 'name': 'Morris Park Bake Shop',
 'restaurant_id': '30075445'}

Recherche

  1. Lister les informations du restaurant “Cafe Henri”
  2. Lister tous les restaurants de la chaîne “Burger King” (rue, quartier)
  3. Lister les restaurants n’ayant pas de quartier connu (“Missing”)
  4. Lister les restaurants ayant eu un score de 0
  5. Lister les restaurants ayant eu un score entre 0 et 10 (inclus)
  6. Lister les restaurants qui ont le terme “Cafe” dans leur nom
  7. Lister les restaurants faisant de la cuisine de type “Pizza” dans “Brooklyn”

Agrégat

  1. Quelles sont les 10 plus grandes chaines de restaurants (nom identique) ?
  2. Lister par quartier le nombre de restaurants et le score moyen
  3. Donner le Top 5 et le Flop 5 des types de cuisine, en terme de nombre de restaurants
  4. Donner les dates de début et de fin des évaluations
  5. Quels sont les 10 restaurants (nom, quartier, addresse et score) avec le plus petit score moyen ?
  6. Quels sont les restaurants (nom, quartier et addresse) avec uniquement des grades “A” ?
  7. Compter le nombre d’évaluation par jour de la semaine
In [ ]: