Documents de cours 2021-2022
Le but de ce TP est de voir l’utilisation des commandes MongoDB dans Python.
Nous allons utiliser le module pymongo
. Pour l’utiliser (après l’avoir installée), on l’importe classiquement comme ci-dessous.
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 une adresse de serveur. Si elle n’y est pas, elle se connecte en local.
URI = 'mongodb+srv://user:user@cluster0.ougec.mongodb.net/test'
client = pymongo.MongoClient(URI) # enlever le paramètre URI si connexion locale
On peut liste l’ensemble des bases de données du serveur en utilisant le code suivant :
for db in client.list_databases():
print(db["name"] + "(" + db["sizeOnDisk"] + " octets)")
Pour accéder à la base de données test
, nous pouvons simplement créer un objet db
comme suit :
db = client.test
Par le biais de l’objet ainsi créé (db
) et en ajoutant le nom de la collection, on a accès aux différentes fonctions que l’on a vu dans Mongo (précisemment countDocuments()
, distinct()
, find()
et aggregate()
).
Pour lister les connexions, on utilise un code similaire à précédemment.
for coll in db.list_collections():
print(coll["name"])
python
Les données JSON
sont similaires à un dictionnaire python
. Pour récupérer le premier document, nous utilisons la fonction find()
.
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. Une fois le résultat retourné (un seul élément ici), le curseur ne renvoie plus rien.
list(d)
Il existe la fonction count_documents()
qui compte directement le nombre de document. Pour information, la fonction estimated_document_count()
pour estimer le nombre de documents, à utiliser de préférence en cas de multiples serveurs et de données massives. Dans le cas où l’on veut compter les documents qui respectent une certaine condition, nous utilisons le paramètre obligatoire filter
. Notez que les noms des champs sont entre quotes.
db.restaurants.count_documents({})
db.restaurants.estimated_document_count()
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. On peut ainsi vouloir les valeurs distinctes présentes dans un sous-ensemble de documents (respectant une contrainte particulière). Voici quelques exemples :
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()
(restriction et projection)Cette fonction 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
). Pour n’avoir que le premier document, on utilise le paramètre limit
. Pour le tri, on utilise le paramètre sort
.
Pour améliorer la lisibilité du résultat et l’utilisation ultérieure des données, on transforme celles-ci en DataFrame
(du module pandas
). Ce format, bien que souvent pratique, n’est pas forcément idéal pour certains champs complexes.
Voici quelques exemples :
address
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 = 10
)
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)
Bien évidemment, on peut faire des calculs d’agrégats, avec la fonction aggregate()
, prenant en paramètre donc le pipeline (tableau composé d’une suite d’opérations), avec les opérateurs suivants :
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 |
… |
Voici encore quelques 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)
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 = NA
)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.
df = pandas.DataFrame(db.restaurants.find(limit = 10))
df
Le champs address
est une liste de dictionnaires, ayant chacun plusieurs champs (ici tous les mêmes).
df
df.assign(info = [e["building"] + ", " + e["street"] for e in df.address])
DataFrame
pandas.DataFrame([e for e in df.address])
DataFrame
originalpandas.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)
df.assign(derniere = [e[0] for e in df.grades], premiere = [e[-1] for e in df.grades]).drop("grades", axis = 1)
DataFrame
zip()
permet d’itérer sur plusieurs tableaux en même tempsconcat()
permet de concaténer les tableaux entre euxdfgrades = pandas.concat([pandas.DataFrame(g).assign(_id = i) for (i, g) in zip(df._id, df.grades)])
dfgrades
DataFrames
avec merge()
pandas.merge(df.drop("grades", axis = 1), dfgrades.reset_index())
Nous allons travailler sur des données AirBnB. Celles-ci sont stockées sur le serveur Mongo dans la collection listingsAndReviews
de la base sample_airbnb
.
Une fois créée la connexion à la collection dans Python, répondre aux questions suivantes :
room_type
)amenities
)amenities
)room_type
) le prix (price
)reviews
)