cours-2022-2023 | Documents de mes cours pour l'année 2022-2023 | FX Jollois

Système pour la Data Science

Master AMSD/MLSD

Introduction à MongoDB

Intéragir avec MongoDB dans Python

Nous allons ici utiliser le module pymongo, que l’on importe classiquement

import pymongo

Connexion à un serveur distant

Il faut utiliser l’adresse du serveur dans la fonction pymongo.MongoClient(), si celui-ci est distant. Sinon, on peut le faire directement, comme c’est notre cas ici.

client = pymongo.MongoClient()

L’objet client permet ainsi de se connecter à toutes les bases existantes. Pour simplifier la suite des opérations, nous allons pointer vers notre base test.

db = client.test

Document dans python

Les données JSON sont similaires à un dictionnaire python. Pour récupérer le premier document, nous utilisons la fonction find() de l’objet créé m.

d = db.restaurants.find(limit = 1)
d

L’objet retourné est un curseur, et non le résultat. Nous avons celui-ci lorsque nous utilisons d dans une commande telle qu’une transformation en list par exemple.

list(d)

Une fois le résultat retourné (un seul élément ici), le curseur ne renvoie plus rien (à tester pour bien intégrer ce comportement).

list(d)

Dénombrement

Ici, nous avons accès à deux fonctions pour dénombrer les documents. La première est count_documents({}), dont le paramètre {} est à mettre obligatoirement (il sert à spécifier le filtre de restriction sur les documents, mais doit être spécifier tout de même s’il n’y a aucune restriction). La deuxième fonction (estimated_document_count()) sert à estimer le nombre de documents, à utiliser de préférence en cas de multiples serveurs et de données massives.

db.restaurants.count_documents({})
db.restaurants.estimated_document_count()

Pour sélectionner les documents, nous allons utiliser le paramètre dans la fonction count_documents() (ainsi que dans les fonctions distinct() et find() que nous verrons plus tard).

db.restaurants.count_documents({ "borough": "Brooklyn" })
db.restaurants.count_documents({ "borough": "Brooklyn", "cuisine": "French" })
db.restaurants.count_documents({ "borough": "Brooklyn", "cuisine": { "$in": ["French", "Italian"]} })
db.restaurants.count_documents(
  { 
    "borough": "Brooklyn", 
    "cuisine": { "$in": ["French", "Italian"]}
  }
)
db.restaurants.count_documents(
  { 
    "address.street": "Franklin Street"
  }
)
db.restaurants.count_documents(
  { 
    "grades.score": 0
  }
)
db.restaurants.count_documents(
  { 
    "grades.score": { "$lte": 5 }
  }
)

Valeurs distinctes

Il existe la même fonction distinct(), avec les mêmes possibilités. Le paramètre key indique pour quel champs nous souhaitons avoir les valeurs distinctes. On peut aussi se restreindre à un sous-ensemble de documents respectant une contrainte particulière indiquée dans le paramètre query (syntaxe identique à count()).

db.restaurants.distinct(key = "borough")
db.restaurants.distinct(
  key = "cuisine",
  query = { "borough": "Brooklyn" }
)
db.restaurants.distinct(
  key = "grades.grade",
  query = { "borough": "Brooklyn" }
)

Récupération de données avec find()

Cette fonction est similaire à celle de Mongo, et permet donc de récupérer tout ou partie des documents, selon éventuellement un critère de restriction (dans le paramètre query) et un critère de projection (dans le paramètre fields). Le paramètre query se gère comme pour le dénombrement. Pour sélectionner les champs à afficher ou non, on utilise donc le deuxième paramètre, permettant ainsi de faire une projection, avec les critères suivants :

Cette fonction renvoie un curseur, qu’il faut donc gérer pour avoir le résultat. Une possibilité est de le transformer en DataFrame (du module pandas) (ce format pas forcément idéal pour certains champs).

Dans cette fonction find(), il est aussi possible de faire le tri des documents, avec le paramètre sort qui prend un tuple composé de 1 ou plusieurs tuples indiquant les critères de tri

Pour n’avoir que le premier document, on utilise le paramètre limit (pas de fonction type findOne() donc). On peut limiter l’exploration à une partie, avec les paramètres suivant :

Exemples

import pandas
pandas.DataFrame(db.restaurants.find(limit = 5))
c = db.restaurants.find({ "name": "Shake Shack" }, { "address.street": 1, "borough": 1 })
pandas.DataFrame(c)
c = db.restaurants.find(
    { "name": "Shake Shack" }, 
    { "_id": 0, "address.street": 1, "borough": 1 }
)
pandas.DataFrame(c)
c = db.restaurants.find(
    {"borough": "Queens", "grades.score": { "$gte":  50}},
    {"_id": 0, "name": 1, "grades.score": 1, "address.street": 1},
    limit = 5
)
pandas.DataFrame(c)
c = db.restaurants.find(
    {"name": "Shake Shack", "borough": {"$in": ["Queens", "Brooklyn"]}}, 
    {"_id": 0, "address.street": 1, "borough": 1}
)
pandas.DataFrame(c)
c = db.restaurants.find(
    {"borough": "Queens", "grades.score": { "$gt":  50}},
    {"_id": 0, "name": 1, "address.street": 1},
    sort = (("address.street", -1), ("name", 1))
)
pandas.DataFrame(c)

Aggrégation

Cette fonction va prendre en paramètre un pipeline : tableau composé d’une suite d’opérations

Fonction Opération  
$limit restriction à un petit nombre de documents (très utiles pour tester son calcul)  
$sort tri sur les documents  
$match restriction sur les documents à utiliser  
$unwind séparation d’un document en plusieurs sur la base d’un tableau  
$addFields ajout d’un champs dans les documents  
$project redéfinition des documents  
$group regroupements et calculs d’aggégrats  
$sortByCount regroupement, calcul de dénombrement et tri déccroissant en une opération  
$lookup jointure avec une autre collection  
   

Les opérations se font dans l’ordre d’écriture, et le même opérateur peut donc apparaître plusieurs fois. Voici quelques éléments indiquant ce qu’on passer en paramètre à ces fonctions.

Les opérateurs $project et $addFields servent à redéfinir les documents.

Voici quelques opérateurs utiles pour la projection (plus d’info ici)

Le calcul d’agrégats (tel que celui fait en SQL par exemple) se fait avec la fonction $group, dans laquelle on doit définir un identifiant critère de regroupement (id) et un ou plusieurs calculs.

Exemples

c = db.restaurants.aggregate(
    [
        {"$limit": 10 }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$sort": { "name": 1 }}
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$sort": { "name": 1 }},
        { "$match": { "borough": "Brooklyn" }}
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$match": { "borough": "Brooklyn" }},
        { "$limit": 10 },
        { "$sort": { "name": 1 }}
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 5 },
        { "$unwind": "$grades" }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$unwind": "$grades" },
        { "$match": { "grades.grade": "B" }}
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$match": { "grades.grade": "B" }},
        { "$unwind": "$grades" }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$project": { "name": 1, "borough": 1 } }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$project": { "address": 0, "grades": 0 } }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$project": { "name": 1, "borough": 1 , "street": "$address.street"} }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$project": { "name": 1, "borough": 1, "nb_grades": { "$size": "$grades" } } }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$project": { "name": 1, "borough": 1, "nb_grades": { "$size": "$grades" } } },
        { "$sort": { "nb_grades": -1 }},
        { "$limit": 10 }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$project": { "name": 1, "borough": 1, "grade": { "$arrayElemAt": [ "$grades", 0 ]} } }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$project": { "name": 1, "borough": 1, "grade": { "$first": "$grades" } } }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$project": { "nom": { "$toUpper": "$name" }, "borough": 1 } }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$addFields": { "nb_grades": { "$size": "$grades" } } }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$project": { 
            "nom": { "$toUpper": "$name" }, 
            "quartier": { "$substr": [ "$borough", 0, 3 ] } 
        } }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$addFields": { "quartier": { "$toUpper": { "$substr": [ "$borough", 0, 3 ] } } }},
        { "$project": { 
            "nom": { "$toUpper": "$name" }, 
            "quartier": { "$cond": { 
                "if": { "$eq": ["$borough", "Bronx"] }, 
                "then": "BRX", 
                "else": "$quartier" 
            } },
            "borough": 1
        } }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        {"$group": {"_id": "Total", "NbRestos": {"$sum": 1}}}
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        {"$group": {"_id": "$borough", "NbRestos": {"$sum": 1}}}
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        {"$group": {"_id": "$borough", "NbRestos": {"$sum": 1}}},
        {"$sort": { "NbRestos": -1}}
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        {"$sortByCount": "$borough"}
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$match": { "borough": "Queens" }},
        { "$unwind": "$grades" },
        { "$group": { "_id": "null", "score": { "$avg": "$grades.score" }}}
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$unwind": "$grades" },
        { "$group": { "_id": "$borough", "score": { "$avg": "$grades.score" }}},
        { "$sort": { "score": -1 }}
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$project": {
            "borough": 1, "street": "$address.street", 
            "eval": { "$arrayElemAt": [ "$grades", 0 ]} 
        } },
        { "$match": { "eval": { "$exists": True } } },
        { "$group": { 
            "_id": { "quartier": "$borough", "rue": "$street" }, 
            "score": { "$avg": "$eval.score" }
        }},
        { "$sort": { "score": 1 }},
        { "$limit": 10 }
    ]
)
pandas.DataFrame(c)
c = db.restaurants.aggregate(
    [
        { "$limit": 10 },
        { "$unwind": "$grades" },
        { "$group": { 
            "_id": "$name", 
            "avec_addToSet": { "$addToSet": "$grades.grade" },
            "avec_push": { "$push": "$grades.grade" }
        }}
    ]
)
pandas.DataFrame(c)

Difficultés potentielles liées à Python

Une fois importées dans un DataFrame, les champs complexes (comme address et grades) sont des variables d’un type un peu particulier :

Nous devons donc les traiter spécifiquement pour manipulare dans Python les informations contenues dedans.

Variables ayant des dictionnaires comme valeurs

Le champs address est une liste de dictionnaires, ayant chacun plusieurs champs (ici tous les mêmes). On peut déjà le manipuler avec l’utilisation de list comprehension. Par exemple, si on souhaite avoir le nom du bâtiment et la rue concaténés dans une nouvelle variable de df, on peut le faire comme ci-dessous :

df = df.assign(info = [e["building"] + ", " + e["street"] for e in df.address])

On peut aussi directement transformer la liste en un DataFrame (car les champs sont tous les mêmes).

pandas.DataFrame([e for e in df.address])

Puisqu’on a fait cela, on peut concaténer les deux DataFrames (l’original, sans le champs address) et le nouveau contenant toutes les informationds de address, en faisant comme suit :

pandas.concat([df.drop("address", axis = 1), pandas.DataFrame([e for e in df.address])], axis = 1)

Variables ayant des tableaux comme valeurs

Le champs grades est une liste de tableaux, ayant chacun potentiellement plusieurs valeurs (des dictionnaires de plus). Si on souhaite récupérer un élément particulier du tableau (premier ou dernier), on peut ainsi faire suit (NB : la dernière évaluation est la première dans le tableau) :

df.assign(derniere = [e[0] for e in df.grades], premiere = [e[-1] for e in df.grades]).drop("grades", axis = 1)

De façon plus complexe, on peut créer un seul DataFrame, avec l’ensemble des sous-tableaux. A savoir, zip() permet d’itérer sur plusieurs tableaux en même temps et concat() permet de concaténer les tableaux entre eux. On a ainsi la commande suivante :

dfgrades = pandas.concat([pandas.DataFrame(g).assign(_id = i) for (i, g) in zip(df._id, df.grades)])
dfgrades

Une fois cela réalisé, on peut combiner avec les données originales, en réalisant une jointure entre les deux DataFrames avec merge() :

pandas.merge(df.drop("grades", axis = 1), dfgrades.reset_index())