Dans 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 :

  • Gérer de gros volumes
  • Facilité de déploiement et d’utilisation
  • Possibilité de faire des choses complexes tout de même

Modèle des données

Principe de base : les données sont des documents

  • stocké en Binary JSON (`BSON)
  • documents similaires rassemblés dans des collections
  • pas de schéma des documents définis en amont
    • contrairement à un BD relationnel ou NoSQL de type Column Store
  • les documents peuvent n’avoir aucun point commun entre eux
  • un document contient (généralement) l’ensemble des informations
    • pas (ou très peu) de jointure à faire
  • BD respectant CP (dans le théorème CAP)
    • propriétés ACID au niveau d’un document

Point sur JSON

  • JavaScript Object Notation
  • Créé en 2005
  • On parle de littéral
  • Format d’échange de données structurées léger
  • Schéma des données non connu
    • contenu dans les données
  • Basé sur deux notions :
    • collection de couples clé/valeur
    • liste de valeurs ordonnées
  • Structures possibles :
    • objet (couples clé/valeur) :
    • {}
    • { "nom": "jollois", "prenom": "fx" }
    • tableau (collection de valeurs) :
    • []
    • [ 1, 5, 10]
    • une valeur dans un objet ou dans un tableau peut être elle-même un littéral
  • Deux types atomiques (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" }
}

Compléments

BSON : extension de JSON

  • Quelques types supplémentaires (identifiant spécifique, binaire, date, …)
  • Distinction entier et réel

Schéma dynamique

  • Documents variant très fortement entre eux, même dans une même collection
  • On parle de self-describing documents
  • Ajout très facile d’un nouvel élément pour un document, même si cet élément est inexistant pour les autres
  • Pas de ALTER TABLE ou de redesign de la base

Pas de jointures entre les collections

Langage d’interrogation

  • Pas de SQL (bien évidemment), ni de langage proche
  • Définition d’un langage propre
  • Langage permettant plus que les accès aux données
    • définition de variables
    • boucles

Utilisation avec 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"
}

Document dans 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"

Restriction et Projection

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

Agrégat

Dénombrement

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

Autre

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 tableaux
m$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

Map-Reduce

Le paradigme Map-Reduce permet de décomposer une tâche en deux étapes :

  1. Map : application d’un algorithme sur chaque document, celui-ci renvoyant un résultat ou une série de résultat
  2. Reduce : synthèse des résultats renvoyés dans l’étape précédente selon certains critères

Exemple classique : décompte des mots présents dans un ensemble de texte

  • Map : pour chaque texte, à chaque mot rencontré, on créé un couple <mot, 1> (un document = beaucoup de résultats générés)
  • Reduce : pour chaque mot, on fait la somme des valeurs pour obtenir le nombre de fois où chaque mot apparaît dans l’ensemble des textes à disposition

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
    • aucun paramètre
    • emit(key, value) pour créer un couple résultat
  • reduce : fonction JavaScript
    • deux paramètres : key et values (tableau des valeurs créés à l’étape précédente)
    • return result pour renvoyer le résultat
  • out : collection éventuelle dans laquelle stocker les résultats dans MongoDB

Dans 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

Un peu de cartographie avec 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

A faire

Restaurants

  1. Lister tous les restaurants de la chaîne “Burger King” (rue, quartier)
  2. Lister les trois chaînes de restaurant les plus présentes
  3. Lister les 10 restaurants les mieux notés (note moyenne la plus faible)
  4. Lister par quartier le nombre de restaurants, le score moyen et le pourcentage moyen d’évaluation A
  5. Afficher les restaurants sur une carte en mettant une couleur en fonction de la note moyenne des restaurants

Trafic parisien

Nous allons utiliser la base de données trafic, dans laquelle il y a trois collections :

Répondre aux questions suivantes :

  1. Afficher le premier document des collections capteurs et trafic, ainsi que le nombre de documents existants
  2. Calculer le nombre de mesures de trafic
    1. par année
    2. par mois
    3. par jour de la semaine
  3. Récupérer les coordonnées (point) de chaque capteur, puis les afficher sur une carte
  4. Récupérer les débits et les taux d’occupation du capteur 6756, et afficher les évolutions (avec ggplot2)
    1. depuis le début
    2. par année (une couleur par année)
    3. moyenne journalière globale
    4. moyenne sur une journée (par heure donc) par mois
    5. moyenne sur une journée (par heure donc) par jour de la semaine
    6. mélange des deux informations sur un même graphique
  5. Récupérer les coordonnées du tronçon de chaque capteur, puis les afficher sur une carte