Documents de cours 2019-2020 - FX Jollois
Dans ce document est l’utilisation du package mongolite
permettant la connection à une base de données MongoDB.
R
On peut interroger une base de données de ce type via le package mongolite
dans R
. Dans la suite, nous allons nous connecter sur un serveur distant, et travailler pour l’exemple sur une base des restaurants New-Yorkais.
library(mongolite)
m = mongo(db = "du_abd",
collection = "restaurants")
Le premier document est présenté ci-dessous. La base contient les informations de plus de 25000 restaurants new-yorkais (base de test fournie par Mongo).
{
"_id" : ObjectId("58ac16d1a251358ee4ee87de"),
"address" : {
"building" : "469",
"coord" : [
-73.961704,
40.662942
],
"street" : "Flatbush Avenue",
"zipcode" : "11225"
},
"borough" : "Brooklyn",
"cuisine" : "Hamburgers",
"grades" : [
{
"date" : ISODate("2014-12-30T00:00:00Z"),
"grade" : "A",
"score" : 8
},
{
"date" : ISODate("2014-07-01T00:00:00Z"),
"grade" : "B",
"score" : 23
},
{
"date" : ISODate("2013-04-30T00:00:00Z"),
"grade" : "A",
"score" : 12
},
{
"date" : ISODate("2012-05-08T00:00:00Z"),
"grade" : "A",
"score" : 12
}
],
"name" : "Wendy'S",
"restaurant_id" : "30112340"
}
R
R
ne gérant pas nativement les données JSON
, les documents sont traduits, pour la librairie mongolite
, en data.frame
. Pour récupérer le premier document, nous utilisons la fonction find()
de l’objet créé m
.
d = m$find(limit = 1)
d
class(d)
Les objets address
et grades
sont particuliers, comme on peut le voir dans le JSON
. Le premier est une liste, et le deuxième est un tableau. Voila leur classe en R
.
class(d$address)
d$address
class(d$grades)
d$grades
On peut aussi voir la liste des valeurs distinctes d’un attribut, avec la fonction distinct()
.
m$distinct("borough")
La fonction find()
de l’objet m
permet de retourner tous les documents. On peut se limiter à un certain nombre de documents avec l’option limit
, comme précédemment.
Pour faire une restriction sur la valeur d’un attribut, il faut utiliser l’option query
, avec un formalisme particulier. Il faut écrire au format JSON
dans une chaîne, avec pour les champs à comparer leur nom suivi de la valeur (pour l’égalité) ou d’un objet complexe pour les autres tests (infériorité, supériorité, présence dans une liste).
Pour une projection, c’est l’option fields
à renseigner. On écrit au format JSON
, avec la valeur 1
pour les champs qu’on souhaite avoir en retour. Par défaut, l’identifiant (_id
) est toujours présent, mais on peut le supprimer en indiquant 0
.
Dans cet exemple, on recherche le document dont l’attribut "name"
est égal à "Shake Shack"
, et on affiche uniquement les attributs "street"
et "borough"
. Dans la deuxième expression, on supprime l’affichage de l’identifiant interne à MongoDB.
m$find(query = '{"name": "Shake Shack"}',
fields = '{"address.street": 1, "borough": 1}')
m$find(query = '{"name": "Shake Shack"}',
fields = '{"_id": 0, "address.street": 1, "borough": 1}')
Ici, on recherche les 10 premiers restaurants du quartier Queens, avec une note A et un score supérieure à . Et on affiche le nom et la rue du restaurant. Remarquez l’affichage des scores.
m$find(query = '{"borough": "Queens", "grades.score": { "$gte": 50}}',
fields = '{"_id": 0, "name": 1, "grades.score": 1, "address.street": 1}',
limit = 10)
On veut chercher les restaurants Shake Shack dans différents quartiers (Queens et Brooklyn).
m$find(query = '{"name": "Shake Shack", "borough": {"$in": ["Queens", "Brooklyn"]}}',
fields = '{"_id": 0, "address.street": 1, "borough": 1}')
Il est aussi posible de trier les documents retournés, via l’option sort
. Toujours en JSON
, on indique 1
pour un tri croissant et -1
pour un tri décroissant.
m$find(query = '{"borough": "Queens", "grades.score": { "$gte": 50}}',
fields = '{"_id": 0, "name": 1, "address.street": 1}',
sort = '{"address.street": -1, "name": 1}',
limit = 10)
Il est possible de définir un curseur (de même type que PL/SQL par exemple), qui va itérer sur la liste de résultats (celle-ci sera stocké sur le serveur). Cela permet de récupérer les documents un par un, ce qui est judicieux en cas de gros volume. De plus, ceux-ci sont récupérés au format list
pure, ce qui peut simplifier la manipulation en cas de données fortement imbriquées.
cursor = m$iterate(
query = '{"borough": "Queens", "grades.score": { "$gte": 50}}',
fields = '{"_id": 0, "name": 1, "address.street": 1}',
sort = '{"address.street": -1, "name": 1}',
limit = 10)
while(!is.null(doc <- cursor$one())){
cat(sprintf("%s (%s)\n", doc$name, doc$`address`$`street`))
}
Plutôt que d’avoir les documents un par un, il est ausi possible de les avoir par paquets avec la fonction batch(n)
sur le curseur (n
étant donc le nombre de documents renvoyés).
On peut déjà faire un dénombrement avec la fonction count()
de l’objet m
. Sans option, on obtient le nombre de documents de la collection. On peut aussi ajouter une restriction pour avoir le nombre de documents respectant ces conditions. Les requêtes s’écrivent de la même manière que pour la fonction find()
.
m$count()
m$count(query = '{"name": "Shake Shack"}')
m$count(query = '{"borough": "Queens"}')
Il existe la fonction aggregate()
pour tous les calculs d’agrégat (et même plus). Il faut passer dans le paramètre pipeline
un tableau d’actions, 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 tableauxm$aggregate(pipeline = '[
{"$group": {"_id": "Total", "NbRestos": {"$sum": 1}}}
]')
m$aggregate(pipeline = '[
{"$group": {"_id": "$borough", "NbRestos": {"$sum": 1}}}
]')
En plus de la somme, il est bien évidemment possible de faire d’autres calculs statistiques de base (moyenne, minimum, maximum) comme nous le verrons par la suite.
Si on veut faire des calculs sur les évaluations, il est nécessaire d’utiliser l’opération $unwind
. Celle-ci permet de dupliquer les lignes de chaque document, avec chaque valeur du tableau indiqué. Voici son application sur le premier document.
m$aggregate('[ { "$limit": 1 } ]')
m$aggregate('[
{ "$limit": 1 },
{ "$unwind": "$grades" }
]')
Du coup, pour faire le calcul des notes moyennes des restaurants du Queens, on exécute le code suivant.
m$aggregate('[
{ "$match": { "borough": "Queens" }},
{ "$unwind": "$grades" },
{ "$group": { "_id": "null", "score": { "$avg": "$grades.score" }}}
]')
Il est bien évidemment possible de faire ce calcul par quartier et de les trier selon les notes obtenues (dans l’ordre décroissant).
m$aggregate('[
{ "$unwind": "$grades" },
{ "$group": { "_id": "$borough", "score": { "$avg": "$grades.score" }}},
{ "$sort": { "score": -1}}
]')
Bien que dans l’esprit NoSQL, il est plutôt déconseillé de faire appel aux jointures, celles-ci sont parfois incontournables. Il est ainsi possible de réaliser une jointure entre deux collections, dans un aggrégat, avec l’opérateur $lookup
.
Nous disposons aussi d’une collection de documents nous indiquant l’étendue des notes prévues pour chaque score A, B et C.
g = mongo(db = "du_abd",
collection = "grades")
g$find()
Si nous souhaitons chercher les restaurants ayant un score en-dehors de ce qui est attendu, nous pouvons nous baser sur le code ci-dessous (on se limite ici à 10 restaurants).
m$aggregate('[
{ "$limit": 10 },
{"$project": { "_id": 0, "name": 1, "grades": { "$slice": [ "$grades", -1] }}},
{ "$lookup": {
"from": "grades",
"localField": "grades.grade",
"foreignField": "grade",
"as": "info"
}}
]')
Le paradigme Map-Reduce permet de décomposer une tâche en deux étapes :
Exemple classique : décompte des mots présents dans un ensemble de texte
<mot, 1>
(un document = beaucoup de résultats générés)On utilise la fonction mapreduce()
de m
pour appliquer l’algorithme Map-Reduce sur les documents de la collection, avec les paramètres suivants :
map
: fonction JavaScript
emit(key, value)
pour créer un couple résultatreduce
: fonction JavaScript
key
et values
(tableau des valeurs créés à l’étape précédente)return result
pour renvoyer le résultatout
: collection éventuelle dans laquelle stocker les résultats dans MongoDBDans la fonction concernant l’étape Map, on utilise l’objet this
pour accéder aux attributs du document. Le langage utilisé est le JavaScript
.
Dans l’exemple ci-dessous, nous calculons pour chaque quartier le nombre de restaurants.
m$mapreduce(
map = 'function() { emit(this.borough, 1)}',
reduce = 'function(cont, nb) { return Array.sum(nb) }'
)
Il est préférable d’utiliser ce paradigme pour réaliser des calculs impossibles à faire avec la fonction aggregate()
. Dans les autres cas, il est préférable d’utiliser le calcul d’agrégat, plus rapide. Dans la comparaison ci-dessous, c’est bien le temps écoulé qui indique que le calcul est plus long avec mapreduce()
.
# Map-Reduce
system.time({
m$mapreduce(
map = 'function() { emit(this.borough, 1)}',
reduce = 'function(cont, nb) { return Array.sum(nb) }'
)
})
# Agrégat
system.time({
m$aggregate('[ { "$group": { "_id": "$borough", "nb": { "$sum": 1}}}]')
})
leaflet
Dans un premier temps, nous allons récupérer les longitudes et latitudes des restaurants ci-desssous.
restos.coord = m$aggregate(
'[
{ "$project": {
"name": 1, "borough": 1,
"lng": { "$arrayElemAt": ["$address.coord", 0]},
"lat": { "$arrayElemAt": ["$address.coord", 1]}
}}
]')
head(restos.coord)
Si on regarde les coordonnées obtenues, on remarque rapidement qu’il y a des outliers (les restaurants sont à New-York normalement).
library(tidyverse)
restos.coord %>%
select(name, lng, lat) %>%
gather(var, val, -name) %>%
group_by(var) %>%
summarise(
min = min(val, na.rm = T),
max = max(val, na.rm = T)
)
Ce que l’on peut montrer grâce à la librairie leaflet
. Nous allons afficher les différents restaurants sur la carte du monde.
library(leaflet)
leaflet(restos.coord) %>%
addTiles() %>%
addCircles(lng = ~lng, lat = ~lat)
En se centrant sur la ville de New-York, et en ajoutant une couleur en fonction du quartier, on visualise mieux les restaurants.
pal = colorFactor("Accent", restos.coord$borough)
leaflet(restos.coord) %>%
addProviderTiles(providers$CartoDB.Positron) %>%
setView(lng = -73.9,
lat = 40.7,
zoom = 10) %>%
addCircles(lng = ~lng, lat = ~lat, color = ~pal(borough)) %>%
addLegend(pal = pal, values = ~borough, opacity = 1,
title = "Quartier")
Envoyez votre fichier (script R
ou markdown Rmd
- avec votre nom dans le nom du fichier) par mail à francois-xavier.jollois@u-paris.fr.
Nous allons découvrir dans ce TP les données utilisées dans le projet à rendre, qui sont l’ensemble des transactions sur les horodateurs dans la ville de Paris sur l’année 2014. Celles-ci proviennent du site Open Data Paris, répertoire des données ouvertes de la ville de Paris. Elles sont stockées sur le serveur MongoDB déjà utilisé, dans la base horodateurs.
Elle contient trois collections importantes :
transactions
: ensemble des paiementstransactions_small
: 1% des paiements (à utiliser avant de lancer sur un calcul sur transactions
)mobiliers
: liste de tous les horodateurs1234
(cf objectid
)arrondt
)regime
) et les arrondissements pour voir s’il y a des différences notables (idéalement en réalisant un graphique avec ggplot
)montant carte
) et des durées payées (durée payée (h)
) des transactionshorodateur
dans mobiliers
et numhoro
dans transactions
)