Potion Bottle Icon Manuel d'alchimie du code Potion Bottle Icon

Une forêt de données — Enrichir des données ouvertes avec les API publiques

- 804 mots - Temps de lecture estimé: 4 minutes


Sun Face IconComment enrichir des données ouvertes avec les API publiques ?Sun Face Icon


Ce projet explore le jeu de données Arbres répertoriés de la Ville de Québec, l’enrichit via l’API du GBIF (Global Biodiversity Information Facility) et le croise avec les shapefiles des quartiers et arrondissements. Tu verras comment ajouter des noms latins, des médias et des informations géospatiales à un fichier CSV brut, puis produire des cartes de densité et des visualisations par quartier avec R.

C’était en janvier 2016. Je venais de tomber sur le catalogue de données ouvertes de la Ville de Québec et je me suis dit : et si on poussait l’exercice plus loin que la simple extraction ?

Les données sont sous licence Creative Commons Attribution 4.0. Je me suis donné un objectif : tirer le maximum d’information du jeu de données Arbres répertoriés.

Le projet complet et la compilation originale sont sur RPubs. J’ai présenté ces travaux le 5 mars 2016 à la JIDO2016, au CAMP.

Tout le code source du projet est dans le dossier d’accompagnement : static/images/blogue/2016/2016-03-04-foret-donnees-arbres-quebec/.

🌘 Chargement du jeu de données

Le fichier source ARBRE.csv est un CSV séparé par des | avec une virgule comme séparateur décimal. Les dates sont stockées dans un format numérique YYYYMMDDHHMMSS qu’il faut interpréter.

dA <- read.csv("ARBRE.csv", sep = "|", dec = ",", stringsAsFactors = FALSE) %>%
  mutate(DATE_PLANTE = parse_date_time(as.character(DATE_PLANTE / 1000000),
                                       orders = "%Y%m%d"))

Le jeu de données contient 29 140 observations de 15 variables.

🌘 Classe de chaque variable

Voici la classe attribuée par R à chaque colonne :

Variable Classe
CODE_ARR character
CODE_PARC character
EMPLACEMENT character
NOM_FRAN character
NOM_LAT character
TYPE_ARBRE character
POS_MESURE character
CIBLE character
DATE_PLANTE POSIXlt
DIAMETRE numeric
LONGITUDE numeric
LATITUDE numeric
OBJECTID integer
ID_VILLE integer
QUARTIER integer

🌘 Exploration de base

Les dix valeurs les plus fréquentes pour chaque variable qualitative donnent un bon aperçu. Par exemple, l’érable de Norvège (Acer platanoides) domine largement le lot, suivi du frêne de Pennsylvanie et du peuplier deltoïde.

Côté quantitatif, le diamètre moyen des arbres est d’environ 23 cm, avec des valeurs minimales et maximales qui suggèrent quelques erreurs de saisie (certaines valeurs semblent inscrites en millimètres plutôt qu’en centimètres).

dA %>% summarise(
  moyDIA = mean(DIAMETRE, na.rm = TRUE),
  minDIA = min(DIAMETRE, na.rm = TRUE),
  maxDIA = max(DIAMETRE, na.rm = TRUE),
  minLON = min(LONGITUDE, na.rm = TRUE),
  minLAT = min(LATITUDE, na.rm = TRUE),
  maxLON = max(LONGITUDE, na.rm = TRUE),
  maxLAT = max(LATITUDE, na.rm = TRUE)
)

🌘 Distribution des diamètres

range <- quantile(dA$DIAMETRE, probs = c(0.02, 0.98), na.rm = TRUE)
ggplot(
  data = dA %>% filter(range[1] <= DIAMETRE & DIAMETRE < range[2] & POS_MESURE != ""),
  mapping = aes(x = DIAMETRE, fill = POS_MESURE)
) + geom_histogram() + facet_wrap("POS_MESURE")

Distribution des diamètres

🌘 Distribution des dates de plantation

ggplot(
  data = dA %>% filter(TYPE_ARBRE != "NON DISPONIBLE"),
  mapping = aes(x = DATE_PLANTE, fill = TYPE_ARBRE)
) + geom_histogram() + facet_wrap("TYPE_ARBRE")

Distribution des dates de plantation

🌘 Enrichissement des variétés d’arbres avec l’API GBIF

Le Système mondial d’information sur la biodiversité (GBIF) offre une API publique pour retrouver les noms scientifiques, les classifications, les médias et la répartition des espèces. C’est l’occasion parfaite pour enrichir notre jeu de données.

🌘 Construction des requêtes

Chaque nom d’arbre est transformé en URL compatible avec l’API species/match du GBIF.

get_url_gfib <- function(x)
  httr::GET(url = paste0("http://api.gbif.org/v1/species/match/?name=", x))

build_name <- function(x)
  gsub(pattern = " ",
       replacement = "+",
       x %>%
         strsplit("'") %>%
         unlist %>%
         '['(1) %>%
         trimws())

nomsUniques <-
  dA %>%
  select(NOM_LAT) %>%
  distinct() %>%
  mutate(nom_url = sapply(NOM_LAT, build_name) %>% tolower())

nomsUrlUniques <-
  nomsUniques %>%
  select(nom_url) %>%
  distinct()

🌘 Transformation des résultats JSON

Les réponses JSON sont converties en une table qu’on peut joindre aux données sources.

load("data_json_gfib.RData")
json_content <- sapply(data_json_gfib %>%
                         as.data.frame %>%
                         '$'(content), rawToChar)
json_content2 <- data.frame(
  nom_url = names(json_content),
  json_content,
  row.names = NULL,
  stringsAsFactors = FALSE
)
json_content3 <- json_content2$json_content %>%
  lapply(fromJSON, flatten = TRUE) %>%
  lapply(as.data.frame) %>%
  (function(x) do.call(smartbind, x)) %>%
  cbind(nom_url = json_content2$nom_url)
json_content4 <- merge(json_content3, nomsUniques, by = c("nom_url"))

🌘 Jointure avec les données source

dA2 <- dA %>% partition() %>% merge(json_content4, by = c("NOM_LAT")) %>% collect()

🌘 Médias par espèce

Une deuxième API permet de récupérer les photos associées à chaque espèce.

get_url_media <- function(x)
  httr::GET(url = paste0("http://api.gbif.org/v1/species/", x, "/media"))

disct_speciesKey <- dA2 %>% select(speciesKey) %>% filter(!is.na(speciesKey)) %>% distinct()

load("json_media.RData")

json_media1 <- data.frame(
  json_content = sapply(json_media %>%
                         as.data.frame %>%
                         '$'(content), rawToChar),
  stringsAsFactors = FALSE
) %>%
  mutate(json_content1 = json_content %>%
           lapply(fromJSON, flatten = TRUE) %>%
           sapply('[', "results"))

json_media1$speciesKey <- disct_speciesKey
json_media2 <- json_media1[lapply(json_media1$json_content1, class) == 'data.frame', ]

json_media3 <- json_media2 %>%
  '$'(json_content1) %>%
  sapply(as.data.frame) %>%
  reshape2::melt() %>%
  select(value, type, format, identifier, references, title,
         description, source, creator, publisher, license) %>%
  distinct(value)

json_media4 <- cbind(json_media3, json_media2$speciesKey)

🌘 Jointure avec les données médias

dA3 <- merge(dA2, json_media4, all.x = TRUE)

🌘 Ajout du quartier et de l’arrondissement

Les shapefiles des quartiers et arrondissements (QUARTIERS/ et ARROND/ dans le dossier du projet) sont chargés avec rgdal, transformés en coordonnées WGS84, puis les points des arbres y sont superposés.

qrtqc <- readOGR("QUARTIERS/", layer = "QUARTIER") %>%
  spTransform(CRS("+proj=longlat +datum=WGS84"))
arrqc <- readOGR("ARROND/", layer = "ARROND") %>%
  spTransform(CRS("+proj=longlat +datum=WGS84"))

names(qrtqc@data) <- paste0(names(qrtqc@data), "_QRT")
names(arrqc@data) <- paste0(names(arrqc@data), "_ARR")

coordinates(dA3) <- ~ LONGITUDE + LATITUDE
proj4string(dA3) <- CRS("+proj=longlat +datum=WGS84")

dA4.1 <- dA3 %>% over(qrtqc) %>% cbind(dA3@data)
dA4   <- dA3 %>% over(arrqc) %>% cbind(dA4.1) %>% cbind(dA3@coords)

save(dA4, file = "dA4.RData")
readr::write_csv(dA4, "arbres-augmented.csv")

Le fichier arbres-augmented.csv contient maintenant les données originales enrichies des informations taxonomiques, des médias et des divisions administratives.

🌘 Arbre le plus courant par quartier (avec photo)

NOM_QRT scientificName freq
Chute-Montmorency Acer platanoides 30
Fargy—Du Parc Acer platanoides 92

Pas de surprise : l’érable de Norvège est l’espèce la plus représentée dans presque tous les quartiers.

🌘 Cartes de densité

🌘 Fond de carte OpenStreetMap

select_for_map <- dA4 %>% select(LONGITUDE, LATITUDE, order)
range_long <- range(select_for_map$LONGITUDE)
range_lat <- range(select_for_map$LATITUDE)

load("quebec_map.RData")

QuebecMap <- ggmap(quebec_map, base_layer = ggplot(aes(x = LONGITUDE, y = LATITUDE),
                   data = select_for_map))

map1 <- QuebecMap + scale_fill_gradient(low = "blue", high = "red")

🌘 Carte simple de densité

map1 +
  stat_density2d(aes(x = LONGITUDE, y = LATITUDE, fill = ..level..),
                 geom = "polygon", data = select_for_map)

Carte simple de densité

🌘 Carte composée par ordre botanique

table_order <- with(select_for_map, table(order))
select_for_map2 <- table_order %>%
  as.data.frame() %>%
  merge(select_for_map, all.y = TRUE) %>%
  filter(Freq > 500)

select_for_map2$order2 <- factor(select_for_map2$order)

map1 +
  stat_density2d(aes(x = LONGITUDE, y = LATITUDE, fill = ..level..),
                 geom = "polygon", data = select_for_map2) +
  facet_wrap(facets = "order2")

Carte composée par ordre botanique

🌘 Arbres recensés par quartier et arrondissement

Source de l’approche : plotting polygon shapefiles with ggplot2

qrtqc@data$id <- rownames(qrtqc@data)
qrtqc.df   <- as.data.frame(qrtqc)
qrtqc.fort <- fortify(qrtqc, region = "id")
qrtqc.line <- join(qrtqc.fort, qrtqc.df, by = "id")

arrqc@data$id <- rownames(arrqc@data)
arrqc.df   <- as.data.frame(arrqc)
arrqc.fort <- fortify(arrqc, region = "id")
arrqc.line <- join(arrqc.fort, arrqc.df, by = "id")

ggmap_quartiers <- ggplot(qrtqc.line) +
  aes(long, lat, group = group, fill = NOM_QRT) +
  geom_polygon() +
  geom_path(color = "white") +
  coord_equal()

ggmap_arrond <- ggplot(arrqc.line) +
  aes(long, lat, group = group, fill = NOM_ARR) +
  geom_polygon() +
  geom_path(color = "white") +
  coord_equal()

plot_data_quartiers <-
  ddply(dA4, .(NOM_QRT, order), summarise, freq = length(NOM_QRT)) %>%
  filter(!is.na(order) & freq >= 500)

plot_data_arrond <-
  ddply(dA4, .(NOM_ARR, order), summarise, freq = length(NOM_QRT)) %>%
  filter(!is.na(order) & freq >= 500)

gg_freq_ordre_quartier <- ggplot(data = plot_data_quartiers,
                                 aes(x = order, y = freq, fill = order)) +
  geom_bar(position = "stack", stat = "identity") +
  facet_wrap(facets = "NOM_QRT", ncol = 4) +
  scale_x_discrete(breaks = order, labels = NULL) +
  xlab("Ordre") + ylab("Fréquence") +
  ggtitle("Ordre par quartier")

gg_freq_ordre_arrond <- ggplot(data = plot_data_arrond,
                               aes(x = order, y = freq, fill = order)) +
  geom_bar(position = "stack", stat = "identity") +
  facet_wrap(facets = "NOM_ARR") +
  scale_x_discrete(breaks = order, labels = NULL) +
  xlab("Ordre") + ylab("Fréquence") +
  ggtitle("Ordre par arrondissement")
# Carte des quartiers
ggmap_quartiers

Carte des quartiers

# Fréquence par quartier
gg_freq_ordre_quartier

Fréquence des ordres par quartier

# Carte des arrondissements
ggmap_arrond

Carte des arrondissements

# Fréquence par arrondissement
gg_freq_ordre_arrond

Fréquence des ordres par arrondissement

🌘 Ce que tu retiendras

Un fichier CSV tout simple — des arbres sur le territoire de Québec — peut devenir un objet d’analyse riche dès qu’on le croise avec des données externes. L’API du GBIF ajoute une couche taxonomique mondiale, les shapefiles locaux ancrent les données dans le territoire, et quelques lignes de ggplot2 suffisent pour révéler des motifs cachés dans la répartition des espèces.

Les fichiers sources sont dans le dossier d’accompagnement : static/images/blogue/2016/2016-03-04-foret-donnees-arbres-quebec/. Tu y trouveras les données originales, les shapefiles, les fichiers RData pré-téléchargés et le script R complet.

🌘 Version Quarto

Le document original a été converti au format Quarto moderne :

Va, explore, enrichis.

Abonne-toi au fil RSS pour ne rien manquer.

Étiquettes