R
et MongoDBDans ce document est l’utilisation du package mongolite
permettant la connection à une base de données MongoDB. ## NoSQL
MongoDB est une base de données NoSQL distribué de type Document Store (site web)
Objectifs :
Principe de base : les données sont des documents
collections
JSON
JavaScript Object Notation
{}
{ "nom": "jollois", "prenom": "fx" }
[]
[ 1, 5, 10]
string
et number
) et trois constantes (true
, false
, null
)Validation possible du JSON sur jsonlint.com/
{
"formation": "DU Analyste Big Data",
"responsable": { "nom": "Poggi", "prenom": "JM" },
"etudiants" : [
{ "id": 1, "nom": "jollois", "prenom": "fx" },
{ "id": 2, "nom": "aristote", "details": "délégué" },
{ "id": 5, "nom": "platon" }
],
"ouverte": true
}
{
"formation": "DU Data Visualisation",
"ouverte": false,
"todo": [
"Creation de la maquette",
"Validation par le conseil"
],
"responsable": { "nom": "Métivier" }
}
BSON
: extension de JSON
Schéma dynamique
ALTER TABLE
ou de redesign de la basePas de jointures entre les collections
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 pays du monde (datant de fin 99).
library(mongolite)
m = mongo(url = "mongodb://193.51.82.104:2343",
db = "test",
collection = "restaurants")
Le premier document est présenté ci-dessous. La base contient les informations de 2.535910^{4} restaurants (base de test fournie par Mongo).
{
"_id" : ObjectId("58ac16d1a251358ee4ee87dd"),
"address" : {
"building" : "1007",
"coord" : [
-73.856077,
40.848447
],
"street" : "Morris Park Ave",
"zipcode" : "10462"
},
"borough" : "Bronx",
"cuisine" : "Bakery",
"grades" : [
{
"date" : ISODate("2014-03-03T00:00:00Z"),
"grade" : "A",
"score" : 2
},
{
"date" : ISODate("2013-09-11T00:00:00Z"),
"grade" : "A",
"score" : 6
},
{
"date" : ISODate("2013-01-24T00:00:00Z"),
"grade" : "A",
"score" : 10
},
{
"date" : ISODate("2011-11-23T00:00:00Z"),
"grade" : "A",
"score" : 9
},
{
"date" : ISODate("2011-03-10T00:00:00Z"),
"grade" : "B",
"score" : 14
}
],
"name" : "Morris Park Bake Shop",
"restaurant_id" : "30075445"
}
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
## address.building address.coord address.street address.zipcode
## 1 1007 -73.85608, 40.84845 Morris Park Ave 10462
## borough cuisine
## 1 Bronx Bakery
## grades
## 1 1393804800, 1378857600, 1358985600, 1322006400, 1299715200, A, A, A, A, B, 2, 6, 10, 9, 14
## name restaurant_id
## 1 Morris Park Bake Shop 30075445
class(d)
## [1] "data.frame"
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)
## [1] "data.frame"
d$address
## building coord street zipcode
## 1 1007 -73.85608, 40.84845 Morris Park Ave 10462
class(d$grades)
## [1] "list"
d$grades
## [[1]]
## date grade score
## 1 2014-03-03 01:00:00 A 2
## 2 2013-09-11 02:00:00 A 6
## 3 2013-01-24 01:00:00 A 10
## 4 2011-11-23 01:00:00 A 9
## 5 2011-03-10 01:00:00 B 14
On peut aussi voir la liste des valeurs distinctes d’un attribut, avec la fonction distinct()
.
m$distinct("borough")
## [1] "Bronx" "Brooklyn" "Manhattan" "Queens"
## [5] "Staten Island" "Missing"
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}')
## _id street borough
## 1 58ac16d2a251358ee4eea9be Columbus Avenue Manhattan
## 2 58ac16d2a251358ee4eeb3bb West 44 Street Manhattan
## 3 58ac16d2a251358ee4eeb3bc East 86 Street Manhattan
## 4 58ac16d2a251358ee4eebb85 North End Avenue Manhattan
## 5 58ac16d2a251358ee4eebccf Fulton Street Brooklyn
## 6 58ac16d2a251358ee4eed18c Jfk International Airport Queens
## 7 58ac16d2a251358ee4eed648 Grand Central Terminal Manhattan
## 8 58ac16d2a251358ee4eed6d3 Jfk International Airport Queens
## 9 58ac16d2a251358ee4eeded1 Old Fulton Street Brooklyn
## 10 58ac16d2a251358ee4eeded3 Flatbush Avenue Brooklyn
## 11 58ac16d2a251358ee4eee6b0 3Rd Ave Manhattan
m$find(query = '{"name": "Shake Shack"}',
fields = '{"_id": 0, "address.street": 1, "borough": 1}')
## street borough
## 1 Columbus Avenue Manhattan
## 2 West 44 Street Manhattan
## 3 East 86 Street Manhattan
## 4 North End Avenue Manhattan
## 5 Fulton Street Brooklyn
## 6 Jfk International Airport Queens
## 7 Grand Central Terminal Manhattan
## 8 Jfk International Airport Queens
## 9 Old Fulton Street Brooklyn
## 10 Flatbush Avenue Brooklyn
## 11 3Rd Ave Manhattan
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)
## street grades
## 1 Horace Harding Boulevard 12, 4, 11, 7, 11, 66
## 2 Bell Boulevard 52, 12, 22, 23, 3, 12
## 3 Rockaway Beach Boulevard 10, 2, 10, 2, 0, 56
## 4 Broadway 13, 13, 13, 12, 52
## 5 Woodhaven Boulevard 2, 64, 9, 12, 8, 3
## 6 Northern Boulevard 10, 22, 39, 4, 50
## 7 Cross Bay Boulevard 13, 12, 2, 51
## 8 Roosevelt Avenue 12, 20, 13, 59, 34, 25, 14
## 9 Van Dam St 12, 2, 54, 27, 13, 5, 12
## 10 102 Street 23, 21, 26, 12, 20, 56, 13
## name
## 1 Richer'S Bakery
## 2 Tequilla Sunrise
## 3 Rockaway Beach Inn
## 4 Alfonso'S Bar
## 5 Pio Pio
## 6 E-Dah Korean Bbq Lounge
## 7 Roma View Catering
## 8 Hornado Ecuatoriano
## 9 Van Dam Diner
## 10 Tacos Mexico
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}')
## street borough
## 1 Fulton Street Brooklyn
## 2 Jfk International Airport Queens
## 3 Jfk International Airport Queens
## 4 Old Fulton Street Brooklyn
## 5 Flatbush Avenue Brooklyn
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)
## street name
## 1 Woodward Avenue Sabores Restaurant & Bar
## 2 Woodside Avenue Salza Pizza
## 3 Woodside Avenue Spicy Shallot
## 4 Woodhaven Boulevard Fresh To You
## 5 Woodhaven Boulevard Pio Pio
## 6 Vernon Boulevard Masso
## 7 Van Dam St Van Dam Diner
## 8 Union Turnpike Koyla
## 9 Union Turnpike New Golden Star
## 10 Union Street K & D Internet Inc
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()
## [1] 25359
m$count(query = '{"name": "Shake Shack"}')
## [1] 11
m$count(query = '{"borough": "Queens"}')
## [1] 5656
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": "null", "NbRestos": {"$sum": 1}}}
]')
## _id NbRestos
## 1 null 25359
m$aggregate(pipeline = '[
{"$group": {"_id": "$borough", "NbRestos": {"$sum": 1}}}
]')
## _id NbRestos
## 1 Missing 51
## 2 Staten Island 969
## 3 Brooklyn 6086
## 4 Bronx 2338
## 5 Queens 5656
## 6 Manhattan 10259
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 } ]')
## _id address.building address.coord
## 1 58ac16d1a251358ee4ee87dd 1007 -73.85608, 40.84845
## address.street address.zipcode borough cuisine
## 1 Morris Park Ave 10462 Bronx Bakery
## grades
## 1 1393804800, 1378857600, 1358985600, 1322006400, 1299715200, A, A, A, A, B, 2, 6, 10, 9, 14
## name restaurant_id
## 1 Morris Park Bake Shop 30075445
m$aggregate('[
{ "$limit": 1 },
{ "$unwind": "$grades" }
]')
## _id address.building address.coord
## 1 58ac16d1a251358ee4ee87dd 1007 -73.85608, 40.84845
## 2 58ac16d1a251358ee4ee87dd 1007 -73.85608, 40.84845
## 3 58ac16d1a251358ee4ee87dd 1007 -73.85608, 40.84845
## 4 58ac16d1a251358ee4ee87dd 1007 -73.85608, 40.84845
## 5 58ac16d1a251358ee4ee87dd 1007 -73.85608, 40.84845
## address.street address.zipcode borough cuisine grades.date
## 1 Morris Park Ave 10462 Bronx Bakery 2014-03-03 01:00:00
## 2 Morris Park Ave 10462 Bronx Bakery 2013-09-11 02:00:00
## 3 Morris Park Ave 10462 Bronx Bakery 2013-01-24 01:00:00
## 4 Morris Park Ave 10462 Bronx Bakery 2011-11-23 01:00:00
## 5 Morris Park Ave 10462 Bronx Bakery 2011-03-10 01:00:00
## grades.grade grades.score name restaurant_id
## 1 A 2 Morris Park Bake Shop 30075445
## 2 A 6 Morris Park Bake Shop 30075445
## 3 A 10 Morris Park Bake Shop 30075445
## 4 A 9 Morris Park Bake Shop 30075445
## 5 B 14 Morris Park Bake Shop 30075445
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" }}}
]')
## _id score
## 1 null 11.63487
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}}
]')
## _id score
## 1 Queens 11.634865
## 2 Brooklyn 11.447976
## 3 Manhattan 11.418151
## 4 Staten Island 11.370958
## 5 Bronx 11.036186
## 6 Missing 9.632911
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) }'
)
## _id value
## 1 Bronx 2338
## 2 Brooklyn 6086
## 3 Manhattan 10259
## 4 Missing 51
## 5 Queens 5656
## 6 Staten Island 969
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) }'
)
})
## user system elapsed
## 0.00 0.00 0.21
# Agrégat
system.time({
m$aggregate('[ { "$group": { "_id": "$borough", "nb": { "$sum": 1}}}]')
})
## user system elapsed
## 0.00 0.00 0.08
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)
## _id borough name
## 1 58ac16d1a251358ee4ee87dd Bronx Morris Park Bake Shop
## 2 58ac16d1a251358ee4ee87de Brooklyn Wendy'S
## 3 58ac16d1a251358ee4ee87df Manhattan Dj Reynolds Pub And Restaurant
## 4 58ac16d1a251358ee4ee87e0 Brooklyn Riviera Caterer
## 5 58ac16d1a251358ee4ee87e1 Queens Tov Kosher Kitchen
## 6 58ac16d1a251358ee4ee87e2 Queens Brunos On The Boulevard
## lng lat
## 1 -73.85608 40.84845
## 2 -73.96170 40.66294
## 3 -73.98514 40.76769
## 4 -73.98242 40.57950
## 5 -73.86012 40.73117
## 6 -73.88038 40.76431
Si on regarde les coordonnées obtenues, on remarque rapidement qu’il y a des * outliers* (les restaurants sont à New-York normalement).
library(tidyverse)
## -- Attaching packages ------------------------- tidyverse 1.2.1 --
## v ggplot2 2.2.1 v purrr 0.2.4
## v tibble 1.3.4 v dplyr 0.7.4
## v tidyr 0.7.2 v stringr 1.2.0
## v readr 1.1.1 v forcats 0.2.0
## -- Conflicts ---------------------------- tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag() masks stats::lag()
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)
)
## # A tibble: 2 x 3
## var min max
## <chr> <dbl> <dbl>
## 1 lat -28.01686 52.53888
## 2 lng -157.88879 153.16288
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)
## Warning in validateCoords(lng, lat, funcName): Data contains 2 rows with
## either missing or invalid lat/lon values and will be ignored
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")
## Warning in validateCoords(lng, lat, funcName): Data contains 2 rows with
## either missing or invalid lat/lon values and will be ignored
Nous allons utiliser la base de données trafic
, dans laquelle il y a trois collections :
capteurs
: liste des capteurs permanents https://opendata.paris.fr/explore/dataset/referentiel-comptages-routiers/information/capteurs_geo
: idem, mais au format GeoJson (qu’on ne va pas utiliser a priori)trafic
: débit et taux d’occupation par heure sur les différents capteurs https://opendata.paris.fr/explore/dataset/comptages-routiers-permanents/information/Répondre aux questions suivantes :
capteurs
et trafic
, ainsi que le nombre de documents existantsggplot2
)