cours-2022-2023 | Documents de mes cours pour l'année 2022-2023 | FX Jollois
Nous allons ici utiliser le module pymongo
, que l’on importe classiquement
import pymongo
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
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)
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).
{}
: tous les documents{ "champs": valeur }
: documents ayant cette valeur pour ce champs{ condition1, condition2 }
: documents remplissant la condition 1 ET la condition 2"champs.sous_champs"
: permet d’accéder donc à un sous-champs d’un champs (que celui-ci soit un littéral ou un tableau){ "champs": { "$opérateur": expression }}
: utilisation d’opérateurs dans la recherche
$in
: comparaison à un ensemble de valeurs$gt
, $gte
, $lt
, $lte
, $ne
: comparaison (resp. greater than, greater than or equal, less than, less than or equal, not equal)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"]}
}
)
street
du champs address
db.restaurants.count_documents(
{
"address.street": "Franklin Street"
}
)
db.restaurants.count_documents(
{
"grades.score": 0
}
)
db.restaurants.count_documents(
{
"grades.score": { "$lte": 5 }
}
)
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()
).
borough
), pour tous les restaurantsdb.restaurants.distinct(key = "borough")
db.restaurants.distinct(
key = "cuisine",
query = { "borough": "Brooklyn" }
)
db.restaurants.distinct(
key = "grades.grade",
query = { "borough": "Brooklyn" }
)
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 :
_id
){ "champs": 1 }
: champs à afficher{ "champs": 0 }
: champs à ne pas afficher_id
)
{ "_id": 0, "champs": 1, ...}
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
( "champs", 1 )
: tri croissant( "champs", -1 )
: tri décroissantPour 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 :
limit
: restreint le nombre de résultats fournisskip
: ne considère pas les n premiers documentsaddress
et grades
.import pandas
pandas.DataFrame(db.restaurants.find(limit = 5))
"street"
et "borough"
)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)
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.
$limit
: un entier$sort
: identique à celle du paramètre sort
de la fonction find()
$match
: identique à celle du paramètre query
des autres fonctions$unwind
: nom du tableau servant de base pour le découpage (précédé d’un $
)
$sortByCount
: nom du champs sur lequel on veut le dénombrement et le tri décroissant selon le résultatLes opérateurs $project
et $addFields
servent à redéfinir les documents.
{ "champs" : 1 }
: conservation du champs (0 si suppression - idem que dans fields
, pas de mélange sauf pour _id
){ "champs": { "$opérateur" : expression }}
: permet de définir un nouveau champs{ "nouveau_champs": "$ancien_champs" }
: renommage d’un champsVoici quelques opérateurs utiles pour la projection (plus d’info ici)
$arrayElemAt
: élément d’un tableau$first
et $last
: premier ou dernier élément du tableau$size
: taille d’un tableau$substr
: sous-chaîne de caractères$cond
: permet de faire une condition (genre de if then else)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.
_id
: déclaration du critère de regroupement
$champs
: regroupement selon ce champs{ "a1": "$champs1", ... }
: regroupement multiple (avec modification des valeurs possible)$sum
: somme (soit de valeur fixe - 1 pour faire un décompte donc, soit d’un champs spécifique)$avg, $min, $max
$addToSet
: regroupement des valeurs distinctes d’un champs dans un tableau$push
: aggrégation de champs dans un tableauc = 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)
grades
)
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)
$unwind
et $match
, le résultat est clairement différentc = 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)
grades
)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)
grades
(indicé 0)c = db.restaurants.aggregate(
[
{ "$limit": 10 },
{ "$project": { "name": 1, "borough": 1, "grade": { "$arrayElemAt": [ "$grades", 0 ]} } }
]
)
pandas.DataFrame(c)
$first
permet aussi de garder uniquement le premier élément du tableau grades
de façon explicite ($last
pour le dernier)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)
$sortByCount
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)
$match
permet de supprimer les restaurants sans évaluations (ce qui engendrerait des moyennes = None
)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)
$addToSet
et $push
, on les applique sur les grades obtenus pour les 10 premiers restaurants
$addToSet
: valeurs distinctes$push
: toutes les valeurs présentesc = db.restaurants.aggregate(
[
{ "$limit": 10 },
{ "$unwind": "$grades" },
{ "$group": {
"_id": "$name",
"avec_addToSet": { "$addToSet": "$grades.grade" },
"avec_push": { "$push": "$grades.grade" }
}}
]
)
pandas.DataFrame(c)
Une fois importées dans un DataFrame
, les champs complexes (comme address
et grades
) sont des variables d’un type un peu particulier :
address
est un ensemble de dictionnairesgrades
est un ensemble de tableauxNous devons donc les traiter spécifiquement pour manipulare dans Python les informations contenues dedans.
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)
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())