10 Работа с геоданными: leaflet

library("tidyverse")

10.1 Векторная и растровая графика

Перед тем как обсуждать карты, следует сначала обсудить разницу между векторной и растровой графикой.

  • Растровые изображения представляют собой набор упорядоченных пикселей, про каждый из которых храниться информация о цвете. Векторное изображение нельзя бесконечно увеличивать — в какой-то момент станут видны пиксели, которые в каком-то смысле являются пределом увелечения. Наиболее популярные форматы растровых изображений: JPEG, GIF, PNG, BMP, TIFF и другие.
  • В векторных изображениях инормация храниться как собрани точек, линий и полигонов в некоторой системе координат, что позволяет бесконечно увеличивать такие изображения не теряя в качестве. Наиболее популярные форматы векторных изображений: PDF, SVG, EPS и другие.

Современные технологии позволяют соединять растровые и векторные изображения, а также трансформировать их друг в друга. Картографические данные могут попадать в разные типы: точки (столицы всех стран), линии (улицы в каком-нибудь городе), полигоны (границы стран и меньших регионов) обычно имеют некоторую геопривязку (для простоты давайте считать такими, все, что имеет широту и долготу), так что могут быть представлены векторно, однако существует достаточно много информации, которую невозможно представить никак подругому, кроме как векторно: спутниковые снимки, существующие физические/политические/климатические/исторические и т. п. карты, выдача картографических сервисов, таких как Google Maps. Кроме того, занимаясь любыми типами визуализации следует помнить о разнице статической визаулизации, которую после создания нельзя изменить, и динамической визуализации, которая позволяет пользователям изменять себя (увеличиваться и уменьшаться, кликать на собрание точек и видеть их значения и т. п.). В данной главе, в отличие от предыдущих мы сосредоточимся на пакете для динамичского картографирования leaflet. Достаточно много тем останется за пределами этой главы: изменение проекции, манипуляции с географическими данными, работа с растровыми изображениями и другие (см., например, (Lovelace, Nowosad, and Muenchow 2019), доступная здесь).

10.2 Картографические примитивы

В картографии существуют свои элементарные единицы:

Эти единицы поддерживают популярные пакеты для манипуляции с георграфическими объектами: sp, sf и другие. В данном разделе мы не будем учиться операциям с этими объектами (объединение, вычитание и т. п., подробности смотрите в документации к пакету sp или в уже упомянавшейся книжке (Lovelace, Nowosad, and Muenchow 2019)).

10.3 leaflet

Для начала включим библиотеку:

library("leaflet")

Здесь доступен cheatsheet, посвященный пакету leaflet.

10.3.1 .csv файлы

Источником географических данных могут быть обычные привычные нам csv файлы. Например, вот здесь, хранится датасет из проекта The Unwelcomed Мохамада А. Вэйкда (Mohamad A. Waked), содержащий информацию о месте и причинах смерти мигрантов и беженцев по всему миру с января 2014 года по июнь 2019 года.

unwelcomed <- read_csv("https://raw.githubusercontent.com/agricolamz/DS_for_DH/master/data/death_of_migrants_and_refugees_from_the_Unwelcomed_project.csv")
  • id — идентификационный номер;
  • date — дата происшедшего;
  • total_death_missing — количество погибших/пропавших;
  • location — место происшедшего;
  • lat — широта;
  • lon — долгота;
  • collapsed_region — обобщенная информация о регионе;
  • region — информация о регионе;
  • collapsed_cause — обобщенная информация о причине смерти;
  • cause_of_death — информация о причине смерти.

Информация о широте и долготе иногда записывают в градусах, минутах и секундах, а иногда в десятичной записи, в R обычно используется десятичная запись. В интернете легко найти конвертеры из одного формата в другой и обратно.

Самый простой способ нанести на карту координаты, это использовать комбинацию функций leaflet() %>% addCircles():

unwelcomed %>% 
  leaflet() %>% 
  addCircles(lng = ~lon, # обратите внимание на особый синтаксис с тильдой
             lat = ~lat)

Чтобы точки не “висели в воздухе” можно добавить подложку:

unwelcomed %>% 
  leaflet() %>% 
  addTiles() %>% 
  addCircles(lng = ~lon,
             lat = ~lat)

Функция addCircles() имеет массу аргументов, которая отвечает за отображение:

  • radius
  • color
  • opacity
  • fill
  • fillColor
  • label
  • popup

К сожалению, в пакете leaflet нет такого удобного автоматического раскрашивания по некоторой переменной, поэтому для решения такой задачи нужно сначала создать свою функцию раскрашивания. Это делается при помощи функций colorNumeric(), colorFactor(), colorBin() или colorQuantile().

pal_cat <- colorFactor("Set3", domain = unwelcomed$collapsed_cause)
pal_cat(unwelcomed$collapsed_cause[1])
[1] "#D9D9D9"

Теперь в переменную pal_cat записана функция, которая возварщает цвета в зависимости от значения. В качестве первого аргумента в фукнций colorNumeric(), colorFactor(), colorBin() или colorQuantile() отправляется палитра, которую пользователь может задать сам или использовать уже имеющуюся (их можно посмотреть при помощи команды RColorBrewer::display.brewer.all()):

RColorBrewer::display.brewer.all()

Теперь мы готовы сделать нашу первую осмысленную карту

unwelcomed %>% 
  filter(str_detect(date, "2014")) %>% 
  leaflet() %>% 
  addTiles() %>% 
  addCircles(lng = ~lon,
             lat = ~lat,
             label = ~total_death_missing, # пусть возникает подпись с количеством
             color  = ~pal_cat(collapsed_cause), # это обобщенная причина
             opacity = 0.9,
             popup = ~cause_of_death) %>%  # а это конкретная причина, появляется при клике мышкой
  addLegend(pal = pal_cat,
            values = ~collapsed_cause,
            title = "")

Вообще цветовая схема не очень сочетается с подложкой, так что можно поменять подложку при помощи функции addProviderTiles() (галлерею подложек можно посмотреть вот здесь):

unwelcomed %>% 
  filter(str_detect(date, "2014")) %>% 
  leaflet() %>% 
  addProviderTiles("Stamen.TonerLite") %>% 
  addCircles(lng = ~lon,
             lat = ~lat,
             label = ~total_death_missing, # пусть возникает подпись с количеством
             color  = ~pal_cat(collapsed_cause), # это обобщенная причина
             opacity = 0.9,
             popup = ~cause_of_death) %>%  # а это конкретная причина, появляется при клике мышкой
  addLegend(pal = pal_cat,
            values = ~collapsed_cause,
            title = "")

Существует проект Карта ДТП, в котором собран датасет c дорожными происшествиями в России за некоторый временной промежуток. Визуализируйте все столкновения из датасета. Что можно увидеть на получившейся карте?

📋 список подсказок ➡

👁 Все забыто… Как скачать датасет? ➡ Надо использовать функцию read_csv() из пакета tidyverse.
👁 Карта получилась, но есть какие-то точки на Чукотке, которые не стой стороны… ➡ Да, это стандартная проблема с Чукоткой. Прибавьте к значениям долготы 360.
👁 А как исправить значения на Чукотке? ➡ Ну нужно использовать функцию mutate(), а в ней ifelse(). Если значения меньше нуля — прибавляем 360, если больше — оставляем как есть.

10.3.2 Комбинация карт: leafsync

Карты, как и все объекты в R тоже можно записать в переменную:

unwelcomed %>% 
  filter(str_detect(date, "2014")) %>% 
  leaflet() %>% 
  addProviderTiles("Stamen.TonerLite") %>% 
  addCircles(lng = ~lon,
             lat = ~lat,
             label = ~total_death_missing, # пусть возникает подпись с количеством
             color  = ~pal_cat(collapsed_cause), # это обобщенная причина
             opacity = 0.9,
             popup = ~cause_of_death) %>%  # а это конкретная причина, появляется при клике мышкой
  addLegend(pal = pal_cat,
            values = ~collapsed_cause,
            title = "2014") ->
  m_2014

Теперь если вызвать переменную m_2014, появится карта, которую мы сделали. Но, что если мы хотим отобразить рядом карты 2014 года и 2015 года? Как сделать фасетизацию? К сожалению, функции для фасетизации в пакете не предусмотрена, но мы можем сделать ее самостоятельно. Для начала создадим вторую карту:

unwelcomed %>% 
  filter(str_detect(date, "2015")) %>% 
  leaflet() %>% 
  addProviderTiles("Stamen.TonerLite") %>% 
  addCircles(lng = ~lon,
             lat = ~lat,
             label = ~total_death_missing, # пусть возникает подпись с количеством
             color  = ~pal_cat(collapsed_cause), # это обобщенная причина
             opacity = 0.9,
             popup = ~cause_of_death) %>%  # а это конкретная причина, появляется при клике мышкой
  addLegend(pal = pal_cat,
            values = ~collapsed_cause,
            title = "2015") ->
  m_2015

Включим библиотеку:

library("leafsync")

И теперь соединим две карты воедино:

sync(m_2014, m_2015)

10.3.3 Работа с .geojson

В данном разделе мы будем анализировать датасет, содержащий данные по всем странам мира.

countries <- jsonlite::read_json("https://github.com/agricolamz/DS_for_DH/raw/master/data/countries.geojson")

Обратите внимание, как уже говорилось в разделе @ref{lists}, так как jsonlite конфликтует с одной из функций из tidyverse, я не загружаю библиотеку полностью при помощи команды library(jsonlite), а обращаюсь к функциям пакета при помощи выражения jsonlite::...().

В загруженном датасете достаточно много переменных, мы попробуем проанализировать количество населения и уровень доходов.

countries$features %>% 
  map("properties") %>% 
  tibble(name = map_chr(., "name"),
         pop_est = map_chr(., "pop_est"),
         income = map_chr(., "income_grp")) %>% 
  select(-1) %>% 
  mutate(pop_est = as.double(pop_est),
         income = as.factor(income)) ->
  country_features
country_features

Еще одно преимущество формата .geojson заключается в том, что его позволяет просматривать github (см. пример).

Самый простой способ визуализировать .geojson это используя функцию addGeoJSON(), которая в качестве аргумента принимает .geojson файл.

leaflet() %>% 
  addGeoJSON(geojson = countries)

Проблема этого подхода заключается в том, что файл .geojson содержит в себе форматирование, поэтому если пользователь хочет поменять отображение объектов, необходимо добавить список style к каждому узлу. Во-первых, нужно добавить список style в корень файла .geojson. В результате, это изменит отображение всех списков:

countries$style = list(
  weight = 1,
  color = "#555555",
  opacity = 1,
  fillOpacity = 0.8)

leaflet() %>% 
  addGeoJSON(geojson = countries)

Во-вторых, следует создать палитры для раскрашивания. Это делается при помощи функций colorNumeric(), colorFactor(), colorBin() или colorQuantile().

pal_num <- colorNumeric("Greens", domain = c(min(country_features$pop_est),
                                             max(country_features$pop_est)))
pal_cat <- colorFactor("RdYlBu", domain = country_features$income)

Созданные переменные pal_num() и pal_cat() сами являются функциями и возвращают раскраску в зависимости от значения:

pal_num(country_features$pop_est[1])
[1] "#F7FCF5"
pal_cat(country_features$income[1])
[1] "#FDAE61"

В-третьих, нужно создать векторы с новыми цветами:

country_features %>% 
  mutate(pop_est_color = pal_num(pop_est),
         income_color = pal_cat(income)) ->
  country_features
country_features

В-четвертых, нужно присвоить каждому узлу свой список style:

map(seq_along(countries$features), function(x){
  countries$features[[x]]$properties$style <- 
    list(fillColor = country_features$income_color[x])
  countries$features[[x]]
}) ->
  countries$features

И последний, пятый шаг, это нарисовать получивший .geojson:

leaflet() %>% 
  addGeoJSON(geojson = countries) %>% 
  addLegend(pal = pal_cat, 
            values = country_features$income, 
            title = "Income")

Повторите шаги 4 и 5 для числовой переменной (количество населения) из датасета.

Ссылки на литературу

Lovelace, R., J. Nowosad, and J. Muenchow. 2019. Geocomputation with R. CRC Press.