TP7 - Connexion à MongoDB sous Python

Analyse de Données Massives - Master 1ère année

Après avoir vu MongoDB d'un côté et python de l'autre, nous allons voir dans ce TP comment les deux peuvent communiquer, par le biais de la librairie pymongo. Il faut bien évidemment installer ce module (éventuellement localement). Pour l'utiliser, on l'importer classiquement comme ci-dessous.

In [23]:
import pymongo

Utilisation générale

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 [56]:
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 database_name().

In [25]:
con.database_names()
Out[25]:
['essai',
 'gym',
 'horodateurs',
 'local',
 'medicaments',
 'test',
 'vlsstat',
 'world']

Nous allons utiliser la base gym, comme dans les deux premiers TP du cours. Nous allons créer un connecteur directement à cette base.

In [26]:
db = con.gym

L'objet db est doté de différentes fonctions, et permet une utilisation très proche de celle qu'on a dans le shell de MongoDB. Pour voir la liste des collections présentes, on utilise la fonction collection_names().

In [27]:
db.collection_names()
Out[27]:
['Sportifs', 'Gymnases']

Fonctions de base

Ensuite, pour accéder aux collections, et aux fonctions à utiliser dans celle-ci, nous utilisont le même formalisme que dans MongoDB.

Nous avons le dénombrement des collections comme suit, avec count().

In [28]:
db.Sportifs.count()
Out[28]:
150
In [29]:
db.Gymnases.count()
Out[29]:
28

Pour obtenir le premier document d'une collection, on utilise ici la fonction find_one().

In [30]:
db.Sportifs.find_one()
Out[30]:
{'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'}
In [31]:
db.Gymnases.find_one()
Out[31]:
{'Adresse': '2 rue des pépines',
 'IdGymnase': 1,
 'NomGymnase': 'PAUL ELUARD',
 'Seances': [{'Duree': 60,
   'Horaire': 9.0,
   'IdSportifEntraineur': 149,
   'Jour': 'Samedi',
   'Libelle': 'Basket ball'},
  {'Duree': 60,
   'Horaire': 9.0,
   'IdSportifEntraineur': 1,
   'Jour': 'Lundi',
   'Libelle': 'Hand ball'},
  {'Duree': 60,
   'Horaire': 10.0,
   'IdSportifEntraineur': 1,
   'Jour': 'Lundi',
   'Libelle': 'Hand ball'},
  {'Duree': 60,
   'Horaire': 11.3,
   'IdSportifEntraineur': 1,
   'Jour': 'Lundi',
   'Libelle': 'Hand ball'},
  {'Duree': 90,
   'Horaire': 14.0,
   'IdSportifEntraineur': 1,
   'Jour': 'Lundi',
   'Libelle': 'Hand ball'},
  {'Duree': 120,
   'Horaire': 17.3,
   'IdSportifEntraineur': 1,
   'Jour': 'lundi',
   'Libelle': 'Hand ball'},
  {'Duree': 120,
   'Horaire': 19.3,
   'IdSportifEntraineur': 1,
   'Jour': 'Lundi',
   'Libelle': 'Hand ball'},
  {'Duree': 120,
   'Horaire': 17.3,
   'IdSportifEntraineur': 2,
   'Jour': 'Dimanche',
   'Libelle': 'Hand ball'},
  {'Duree': 120,
   'Horaire': 19.3,
   'IdSportifEntraineur': 2,
   'Jour': 'Dimanche',
   'Libelle': 'Hand ball'},
  {'Duree': 120,
   'Horaire': 17.3,
   'IdSportifEntraineur': 2,
   'Jour': 'mardi',
   'Libelle': 'Hand ball'},
  {'Duree': 120,
   'Horaire': 17.3,
   'IdSportifEntraineur': 2,
   'Jour': 'mercredi',
   'Libelle': 'Hand ball'},
  {'Duree': 60,
   'Horaire': 15.3,
   'IdSportifEntraineur': 2,
   'Jour': 'Samedi',
   'Libelle': 'Hand ball'},
  {'Duree': 60,
   'Horaire': 16.3,
   'IdSportifEntraineur': 2,
   'Jour': 'Samedi',
   'Libelle': 'Hand ball'},
  {'Duree': 120,
   'Horaire': 17.3,
   'IdSportifEntraineur': 2,
   'Jour': 'Samedi',
   'Libelle': 'Hand ball'},
  {'Duree': 30,
   'Horaire': 20.0,
   'IdSportifEntraineur': 3,
   'Jour': 'jeudi',
   'Libelle': 'Hand ball'},
  {'Duree': 60,
   'Horaire': 14.0,
   'IdSportifEntraineur': 3,
   'Jour': 'lundi',
   'Libelle': 'Hand ball'},
  {'Duree': 30,
   'Horaire': 18.0,
   'IdSportifEntraineur': 3,
   'Jour': 'lundi',
   'Libelle': 'Hand ball'},
  {'Duree': 30,
   'Horaire': 19.0,
   'IdSportifEntraineur': 3,
   'Jour': 'lundi',
   'Libelle': 'Hand ball'},
  {'Duree': 30,
   'Horaire': 20.0,
   'IdSportifEntraineur': 3,
   'Jour': 'lundi',
   'Libelle': 'Hand ball'},
  {'Duree': 90,
   'Horaire': 17.0,
   'IdSportifEntraineur': 7,
   'Jour': 'mercredi',
   'Libelle': 'Hockey'}],
 'Surface': 200,
 'Ville': 'STAINS',
 '_id': '566eec69662b388eba464299'}

Et on peut lister les valeurs prises par un champs dans une collection par la fonction ditinct().

In [32]:
db.Sportifs.distinct("Sexe")
Out[32]:
['m', 'M', 'F']
In [33]:
db.Sportifs.distinct("Sports.Jouer")
Out[33]:
['Football',
 'Tennis',
 'Volley ball',
 'Basket ball',
 'Ping pong',
 'Badmington',
 'Hand ball']
In [34]:
db.Gymnases.distinct("Ville")
Out[34]:
['STAINS',
 'MONTMORENCY',
 'PIERREFITTE',
 'SARCELLES',
 'SAINT DENIS',
 'VILLETANEUSE',
 'GARGES']
In [35]:
db.Gymnases.distinct("Surface")
Out[35]:
[200, 450, 400, 500, 620, 360, 420, 300, 480, 600, 520, 350]
In [36]:
db.Gymnases.distinct("Seances.Libelle")
Out[36]:
['Basket ball', 'Hand ball', 'Hockey', 'Volley ball']
In [37]:
db.Gymnases.distinct("Seances.Jour")
Out[37]:
['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 [38]:
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'}

Par contre, pour l'utiliser sur le résultat de la fonction find() (qui renvoie un curseur, puisqu'il peut y avoir plusieurs documents correspondants), il faut soit faire une boucle sur ce curseur, soit le transformer en list.

In [39]:
res = db.Sportifs.find({ "Nom": "KERVADEC" })
print(res)
<pymongo.cursor.Cursor object at 0x105b58a20>
In [40]:
print(res.count())
1

Voici comment réaliser une boucle sur le résultat pour l'affichage.

In [41]:
for r in res:
    pprint.pprint(r)
{'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'}

Et voici un exemple de transformation en list. Une fois que le curseur est lu, comme ici dans la boucle, il est vide. Il faut donc le recalculer.

In [42]:
res = db.Sportifs.find({ "Nom": "KERVADEC" })
pprint.pprint(list(res))
[{'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'}]

On créé ici une fonction simple, nommée affichage(), qui va prendre en paramètre un curseur renvoyé par la fonction find() et qui affichera proprement le résultat via pprint().

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

Et voici comment utiliser cette fonction.

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

Recherche d'informations

Comme nous allons le voir par la suite, nous allons utiliser exactement le même formalisme que dans MongoDB, avec la fonction find(). Ceci est possible avec l'utilisation de dictionnaires pour les paramètres de critères de sélection des documents et pour les choix d’items des documents à afficher. La seule obligation est de mettre les noms des champs entre "...", alors qu'il était possible de ne pas le faire dans MongoDB.

Ici, nous recherchons les sportifs d'au moins 32 ans, en affichant que leur nom et leur âge. 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 [45]:
res = db.Sportifs.find({ "Age": { "$gte": 32 } }, { "_id": 0, "Nom": 1, "Age": 1 })
affiche(res)
[{'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'}]

On peut aussi stocker les critères et les choix en sortie dans des variables, pour les utiliser dans find(). Ici, nous recherchons les sportives jouant au Basket.

In [46]:
criteres = { "Sports.Jouer" : "Basket ball", "Sexe" : "F" }
sortie = { "_id": 0, "Nom": 1, "Sexe": 1, "Sports.Jouer": 1}
res = db.Sportifs.find(criteres, sortie)
affiche(res)
[{'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': 'LEJEUNE',
  'Sexe': 'F',
  'Sports': {'Jouer': ['Basket ball',
                       'Volley ball',
                       'Badmington',
                       'Ping pong']}},
 {'Nom': 'HEDDI',
  'Sexe': 'F',
  'Sports': {'Jouer': ['Basket ball', 'Badmington', 'Ping pong']}},
 {'Nom': 'JOUVE',
  'Sexe': 'F',
  'Sports': {'Jouer': ['Basket ball', 'Ping pong']}},
 {'Nom': 'GUERRAOUI',
  'Sexe': 'F',
  'Sports': {'Jouer': ['Basket ball', 'Ping pong']}},
 {'Nom': 'CLERICE',
  'Sexe': 'F',
  'Sports': {'Jouer': ['Basket ball', 'Badmington', 'Ping pong']}},
 {'Nom': 'MARIE',
  'Sexe': 'F',
  'Sports': {'Jouer': ['Basket ball', 'Hand ball', 'Ping pong']}},
 {'Nom': 'BELZ', 'Sexe': 'F', 'Sports': {'Jouer': 'Basket ball'}}]

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 spécifiques du module : DESCENDING (pour descendant) et ASCENDING (pour ascendant).

Ici, nous reprenons la recherche précédent 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 [47]:
res = db.Sportifs.find({ "Age": { "$gte": 32} }, { "_id": 0, "Nom": 1, "Age": 1 })
res_tri = res.sort([ ("Age", pymongo.DESCENDING), ("Nom", pymongo.ASCENDING) ])
affiche(res_tri)
[{'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

Nous avons aussi la possibilité de calculer des agrégats, avec la fonction aggregate(). Celle-ci s'utilise de manière très similaire à celle de MongoDB.

Ci-dessous, nous calculons le nombre de gymnases et la surface moyenne de celle-ci. Pour ne pas mettre de critère d'agrégations, nous mettons une chaîne de caractère comme identifiant.

In [48]:
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 des statistiques simples sur la surface de ceux-ci (totale, moyenne, minimum et maximum).

In [49]:
res = db.Gymnases.aggregate([ 
    { "$group": { 
        "_id": "$Ville", 
        "nb": { "$sum": 1 }, 
        "surfaceTotale": { "$sum": "$Surface" },
        "surfaceMoyenne": { "$avg": "$Surface" },
        "surfaceMinimum": { "$min": "$Surface" },
        "surfaceMaximum": { "$max": "$Surface" }
    }}
])
affiche(res)
[{'_id': 'SAINT DENIS',
  'nb': 3,
  'surfaceMaximum': 520,
  'surfaceMinimum': 450,
  'surfaceMoyenne': 490.0,
  'surfaceTotale': 1470},
 {'_id': 'SARCELLES',
  'nb': 5,
  'surfaceMaximum': 620,
  'surfaceMinimum': 400,
  'surfaceMoyenne': 548.0,
  'surfaceTotale': 2740},
 {'_id': 'GARGES',
  'nb': 1,
  'surfaceMaximum': 400,
  'surfaceMinimum': 400,
  'surfaceMoyenne': 400.0,
  'surfaceTotale': 400},
 {'_id': 'VILLETANEUSE',
  'nb': 3,
  'surfaceMaximum': 620,
  'surfaceMinimum': 350,
  'surfaceMoyenne': 523.3333333333334,
  'surfaceTotale': 1570},
 {'_id': 'PIERREFITTE',
  'nb': 5,
  'surfaceMaximum': 450,
  'surfaceMinimum': 300,
  'surfaceMoyenne': 382.0,
  'surfaceTotale': 1910},
 {'_id': 'MONTMORENCY',
  'nb': 5,
  'surfaceMaximum': 500,
  'surfaceMinimum': 420,
  'surfaceMoyenne': 470.0,
  'surfaceTotale': 2350},
 {'_id': 'STAINS',
  'nb': 6,
  'surfaceMaximum': 400,
  'surfaceMinimum': 200,
  'surfaceMoyenne': 333.3333333333333,
  'surfaceTotale': 2000}]

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

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

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

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

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

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

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

Exercices

Nous allons utiliser la base de données test, dans laquelle se trouve la collection restaurants. Celle-ci est composée de plus de 25000 retaurants new-yorkais, avec des évaluations de ceux-ci.

  1. Créer une connexion vers cette collection
  2. Déterminer le nombre exact de restaurants.
  3. Lister les différents quartiers représentés (borough) ainsi que les différents types de cuisine (cuisine).
  4. Lister les noms des restaurants, ainsi que leur quartier, ne proposant que des soupes (cuisine égale à Soups).
  5. Donner les nombre de restaurants ayant eu au moins une fois le grade (dans grades) Z.
  6. Calculer le nombre de restaurants pour chaque quartier
  7. Donner les cinq types de cuisine les plus présentes dans New-York (avec le plus de restaurants donc)
  8. Pour chaque grade possible, donner le nombre d'évaluation avec ce grade, ainsi que le score moyen, minimum et maximum.
  9. Créer un DataFrame contenant les restaurants en ligne et en colonne le score moyen obtenu pour chaque grade possible. Si un restaurant n'a pas eu d'évaluation d'un grade, nous aurons une valeur absente.
In [ ]: