Documents de cours 2020-2021 - FX Jollois
Le but de ce TP est de voir l’utilisation des commandes MongoDB dans R.
Nous allons utiliser la librairie mongolite
. Pour l’utiliser (après l’avoir installée), on l’importe classiquement comme ci-dessous.
library(mongolite)
La première opération est de créer une connexion entre R et MongoDB en utilisant la fonction mongo()
. Celle-ci prend en paramètre la base et la collection, plus si besoin l’adresse du serveur. S’elle n’y est pas, elle se connecte en local (ce qui est notre cas normalement).
m = mongo(
collection = "restaurants",
db = "test")
Par le biais de l’objet ainsi créé (m
), on a accès aux différentes fonctions que l’on a vu dans Mongo (précisemment count()
, distinct()
, find()
et aggregate()
).
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
Il existe la même fonction count()
, qui compte directement le nombre de document. Dans le cas où l’on veut compter les documents qui respectent une certaine condition, nous utilisons le paramètre query
. Comme vous pouvez le voir dans les exemples ci-dessous, il est nécessaire de passer la requête en JSON
, dans une chaîne de caractères.
m$count()
"
)m$count(query = '{ "borough": "Brooklyn" }')
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 restaurantsm$distinct(key = "borough")
m$distinct(
key = "cuisine",
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
). Pour n’avoir que le premier document, on utilise le paramètre limit
(pas de fonction type findOne()
donc). Pour le tri, on utilise le paramètre sort
, avec la même syntaxe que dans Mongo. Voici quelques exemples :
"street"
et "borough"
)m$find(query = '{ "name": "Shake Shack" }',
fields = '{ "address.street": 1, "borough": 1 }')
m$find(query = '{"borough": "Queens", "grades.score": { "$gt": 50}}',
fields = '{"_id": 0, "name": 1, "address.street": 1}',
sort = '{"address.street": -1, "name": 1}',
limit = 10)
Bien évidemment, on peut faire des calculs d’agrégats, avec la même fonction aggregate()
, prenant en paramètre le pipeline en chaîne de caractères.
m$aggregate('[
{ "$match": { "borough": "Queens" }},
{ "$unwind": "$grades" },
{ "$group": { "_id": "null", "score": { "$avg": "$grades.score" }}}
]')
Lorsqu’on travaille avec MongoDB dans R, nous pouvons rencontrer des problèmes sur 2 points particuliers :
query
ou pipeline
;data.frame
Comme indiqué dans ci-dessus, les paramètres doivent être des chaînes de caractères contenant le JSON. On peut donc soit les écrire comme précédemment, directement. Mais lorsqu’on veut intégrer dedans une variable (comme par exemple un input
d’une application Shiny), il faut créer automatiquement la chaîne de caractères.
Prenons l’exemple de la recherche du nombre de restaurants dans le Bronx :
m$count(query = '{ "borough": "Bronx" }')
Imaginons maintenant que notre quartier est dans une variable q
(on a donc q = "Bronx"
). Il faut donc créer la chaîne passée en paramètre ci-dessus. Pour cela, nous avons plusieurs possibilités. Mais il faut surtout faire attention à bien avoir les guillemets ("
) dans la chaîne.
paste()
(ou mieux paste0()
)Cette solution est la plus basique. Elle peut facilement être illisible si l’on doit intégrer beaucoup de variables dans la chaîne.
c = paste0('{ "borough": "', q, '" }')
m$count(query = c)
sprintf()
Cette fonction a l’avantage de rendre plus lisible la chaîne que l’on va construire. Elle prend 2 paramètres (ou plus) :
%x
(le x
désignant le format)
%s
: chaîne de caractères (s
pour string
)%f
: nombre réel (f
pour float
- %.2f
pour un arrondi à 2 décimales)Dans notre exemple, nous ferions comme ci-dessous.
c = sprintf('{ "borough": "%s" }', q)
m$count(query = c)
toJSON()
Cette fonction est fournie par la librairie jsonlite
, et permet de construire un objet JSON à partir d’un objet R (très souvent une liste). Par défaut, les valeurs simples sont rangées dans un vecteur. On peut modifier ce comportement en mettant à TRUE
le paramètre auto_unbox
(ce qu’on va faire ici).
l = list(borough = q)
c = toJSON(l, auto_unbox = T)
m$count(query = c)
Cette option est la plus versatile, car elle permet de gérer des créations de JSON très complexes, ce que ne permettent pas les autres options.
Comme indiqué plus haut, les fonctions de mongolite
transforment automatiquement le JSON renvoyé en data.frame
. Cela est globalement très pratique mais engendre des soucis lorsque les données sont complexes (i.e. dans des sous-champs et autres).
Prenons les 5 premiers restaurants.
df = m$find(limit = 5)
df
La colonne grades
est une liste de data.frames
.
df$grades
La colonne address
est elle un data.frame
(presque) simple.
df$address
En effet, la colonne coord
de address
est une liste de vecteurs.
df$address$coord
En l’état, il n’est pas possible d’utiliser ces attributs directement.
Pour obtenir ces coordonnées, nous pouvons faire de deux façons : séparément ou simultanément.
On utilise la fonction sapply()
, qui applique une fonction passée en deuxième paramètre à chaque élément de la liste passée en premier paramètre. Chaque fonction est ici définit directement (on parle alors de fonction anonyme). Celles-ci ne font que retourner le premier (ou le deuxième) élément du vecteur. L’intérêt de sapply()
ici est qu’elle simplifie le résultat en une matrice, que l’on transpose ensuite (avec t()
).
lng = sapply(df$address$coord, function(c) { return (c[1]) })
lat = sapply(df$address$coord, function(c) { return (c[2]) })
plot(lng, lat)
On pourrait bien évidemment garder ces retours dans le data.frame
plutôt que dans une variable.
df$lng = sapply(df$address$coord, function(c) { return (c[1]) })
df$lat = sapply(df$address$coord, function(c) { return (c[2]) })
mat = t(sapply(df$address$coord, function(c) { return(list(lng = c[1], lat = c[2]))}))
plot(mat)
On peut aussi ajouter ces 2 colonnes au data.frame
.
df = cbind(df, mat)
Pour travailler sur les grades, nous devons pouvoir récupérer pour chaque évaluation, toutes les informations du restaurant évalué. Ceci est assez complexe à faire en R. On va le faire en deux étapes :
data.frame
On utilise ici la fonction lapply()
, similaire à sapply()
qui applique une fonction passée en deuxième paramètre à chaque élément de la liste passée en premier paramètre. Nous utilisons ici la liste 1, 2, 3, ...
(jusqu’au nombre de lignes du data.frame
df
). La fonction passée en paramètre prend donc comme paramètre la position de l’élément qui nous intéressé. On récupère les évaluations (df$grades[[i]]
) et les informations du restaurant (normalement juste df[i,]
). Comme nous voulons joindre les deux (avec cbind()
), il faut avoir le même nombre de lignes. Nous dupliquons donc la ième ligne autant de fois qu’il y a de lignes dans grades
. On supprime ensuite les évaluations (grades
) dans infos
. Et enfin, on les colle ensemble.
liste = lapply(1:nrow(df), function (i) {
grades = df$grades[[i]]
infos = df[rep(i, nrow(grades)),]
infos$grades = NULL
cbind(infos, grades)
})
liste
data.frame
Nous utilisons ici la fonction Reduce()
. Celle-ci prend en premier paramètre (attention changement par rapport à lapply()
) une fonction indiquant comment regrouper 2 éléments entre eux, et en deuxième paramètre la liste à traiter. La fonction applique la fonction sur les deux premiers éléments, puis sur le résultat et le troisième élément, et ainsi de suite jusqu’à épuisement de la liste. A la fin, nous obtenons donc un seul élément. La fonction passée en paramètre ici est rbind()
, qui colle deux data.frames
l’un au-dessus de l’autre. Au final, nous avons bien un seul data.frame
.
df_grades = Reduce(rbind, liste)
df_grades
Nous avons vu ici qu’on pouvait traiter dans R les données obtenues, quelque soit le format. Pour autant, c’est fastidieux et parfois beaucoup plus compliqué. L’idéal est donc de penser à ce qu’on veut faire ensuite pour savoir comment récupérer les données. Et donc, de faire des pré-traitements directement dans MongoDB.
Pour récupérer proprement les coordonnées, on aurait pu faire comme ci-dessous.
df_coord = m$aggregate('
[
{ "$limit": 5 },
{ "$addFields" : {
"lng": { "$arrayElemAt" : [ "$address.coord", 0 ] },
"lat": { "$arrayElemAt" : [ "$address.coord", 1 ] }
} }
]')
df_coord
Par exemple, pour le travail sur les grades, nous aurions pu faire directement (avec toutefois le même comportement pour grades
que pour address
).
df_grades = m$aggregate('[ {"$limit": 5}, {"$unwind": "$grades" }]')
df_grades
Dans R, utiliser la connexion vers Mongo pour effectuer les demandes suivantes. Il faut faire en sorte que le maximum soit fait dans Mongo et pas dans R.
restaurants
data.frame
ggmap
)