21 May 2022 EU SPb meetup

Карен Валитов, “Пространственные операции, применяемые при подготовки данных для анализа”

План митапа

  1. Работа с геометриями
  2. Система координат для городов
  3. Геометрические операции
  4. Пространственные операции
  5. Пространственные соотношения
  6. Пространственные преобразования
  7. Сетки и симуляция карт плотности показателя

 

Секция Интро

Загрузим библиотеки:

pkgs <- c("sf", "tidyverse", "geojsonio", "geojsonsf", "readr", "mapview", "leaflet", "sfnetworks", "tidygraph", "data.table", "units", "openxlsx")

if (length(setdiff(pkgs, rownames(installed.packages()))) > 0) {
  
  install.packages(setdiff(pkgs, rownames(installed.packages())), quiet = T)
}

lapply(pkgs, library, character.only = T)

Загрузим датасеты. Для чтения векторных данных из основных форматов работы с пространственными данными (geojson, geopackage, shapefiles etc) есть функция ‘st_read’. Она загрузит ваш файл и представит его в виде объекта sf (который как мы помним также dataframe)

districts <- st_read('data/districts.geojson')
## Reading layer `districts' from data source 
##   `/builds/upravitelev/r_meetups/content/post/2022-05-21-eu_spb_meetup/data/districts.geojson' 
##   using driver `GeoJSON'
## Simple feature collection with 146 features and 3 fields
## Geometry type: MULTIPOLYGON
## Dimension:     XY
## Bounding box:  xmin: 36.80313 ymin: 55.14222 xmax: 37.96747 ymax: 56.02125
## Geodetic CRS:  WGS 84
districts <- st_make_valid(districts)

houses <- st_read('data/buildings.shp')
## Reading layer `buildings' from data source 
##   `/builds/upravitelev/r_meetups/content/post/2022-05-21-eu_spb_meetup/data/buildings.shp' 
##   using driver `ESRI Shapefile'
## Simple feature collection with 1035 features and 1 field
## Geometry type: POLYGON
## Dimension:     XY
## Bounding box:  xmin: 37.58994 ymin: 55.78266 xmax: 37.63449 ymax: 55.81012
## Geodetic CRS:  WGS 84
houses <- st_make_valid(houses)

social_objects <- st_read('data/education_poi.shp')
## Reading layer `education_poi' from data source 
##   `/builds/upravitelev/r_meetups/content/post/2022-05-21-eu_spb_meetup/data/education_poi.shp' 
##   using driver `ESRI Shapefile'
## Simple feature collection with 15 features and 2 fields
## Geometry type: POINT
## Dimension:     XY
## Bounding box:  xmin: 37.60057 ymin: 55.78663 xmax: 37.62163 ymax: 55.80323
## Geodetic CRS:  WGS 84
districts <- st_make_valid(districts)

streets <- st_read('data/highway.shp')
## Reading layer `highway' from data source 
##   `/builds/upravitelev/r_meetups/content/post/2022-05-21-eu_spb_meetup/data/highway.shp' 
##   using driver `ESRI Shapefile'
## Simple feature collection with 20290 features and 13 fields
## Geometry type: MULTILINESTRING
## Dimension:     XY
## Bounding box:  xmin: 362176.6 ymin: 6115267 xmax: 434921.9 ymax: 6209619
## Projected CRS: WGS 84 / UTM zone 37N
streets <- select(streets, OSM_ID, NAME, HIGHWAY)
districts <- st_make_valid(districts)

Первый - данные по районам города Москвы. Посмотрим кратко на эти данные

mapview(districts, zcol='name', legend=F)

Второй - данные расположения домов в районе Марьина Роща, где указано, жилой дом или нет.

mapview(houses, legend=F)

Третий сет - данные расположения школ и детских садов в районе Марьина Роща.

mapview(social_objects, zcol='type_ed', legend=F)

Посмотрим на все наборы вместе

ggplot()+
  geom_sf(data=districts,fill="#b8dae0")+
  geom_sf(data=houses,color="#f5f53d")+
  geom_sf(data=social_objects,color="red")+
  geom_sf(data=streets)+
  theme_minimal()

Комментарий Карена: Эти данные совершенно обычны для урбанистов, их можно найти в разных местах: OSM, Портал Открытых Данных Москвы, Реформа ЖКХ и другие

1. Топологические операции

Изменение типа геометрий

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

st_cast()

districts_center <- filter(districts, name %in% c("Арбат", "Басманный", "Замоскворечье", "Красносельский", "Мещанский", "Пресненский", "Таганский", "Тверской", "Хамовники", "Якиманка"))

polygon <- st_cast(districts_center,"POLYGON")

ggplot()+
  geom_sf(data=polygon)+
  ggtitle('Москва POLYGON')

polylines <- st_cast(polygon,"LINESTRING")

ggplot()+
  geom_sf(data=polylines)+
  ggtitle('Москва LINESTRING')

multipoint <-st_cast(polylines,"MULTIPOINT")

ggplot()+
  geom_sf(data=multipoint)+
 ggtitle('Москва MULTIPOINT')

# Ступенчатая логика - от более сложной геометрии к простой. Не советуем перескакивать через "уровни".

 

Генерализация

 

st_simplify

Moscow_simp <- list()

for (i in c(100, 300, 500)) {
  simplif_list <- list(st_simplify(st_transform(districts_center, 32637), dTolerance =  i))
  names(simplif_list) <- i
  Moscow_simp <- append(Moscow_simp,
                        list(simplif_list))
}
plot(Moscow_simp[[1]][["100"]][1], main = paste("Simplification =", names(Moscow_simp[[1]]), sep = " "))

plot(Moscow_simp[[2]][["300"]][1], main = paste("Simplification =", names(Moscow_simp[[2]]), sep = " "))

plot(Moscow_simp[[3]][["500"]][1], main = paste("Simplification =", names(Moscow_simp[[3]]), sep = " "))

Что же такое генерализация? Это упрощение геометрий в зависимости от задач (иногда в случае уменьшения масштаба, иногда для упрощения расчетов и так далее)

2. Выбор проекции для городских проектов

Как подобрать рабочую проекцию?

Немного снова вспомним про проекции? При работе в городах можно использовать локальную проекцию UTM. Что это значит? Вспомним про проекцию Меркатор и как ее построили.

А теперь мы повернем цилиндр на бок и начнем его вращать!

Итого получили 60 зон (по 6 градусов), где нет почти нет никаких искажений. Подходит для городов, поскольку они обычно лежат в одной зоне (редко на границе двух, но не критично).

3. Геометрические вычисления и операции

Работая с геометриями, мы можем делать обычные геометрические вычисления. Это могут быть площади, длины и другие. Важно, что эти значения зависят от системы координат. Библиотека SF перешла на использования библиотеки S2 от Google и теперь в WGS84 ведет геодезические вычисления. У этого есть плюс и минусы.

st_area()

В случае, если нам нужно узнать размер полигона, мы используем st_area(). Рассмотрим примеры с площадями

districts <- st_transform(districts, crs = 32637)
districts <- mutate(districts, area=st_area(districts))
districts <- st_transform(districts, crs = 4326)
districts <- mutate(districts, area_s2=st_area(districts))

knitr::kable(head(districts))
name population of_area geometry area area_s2
Савеловский 59189 2.70 MULTIPOLYGON (((37.54912 55… 2671450 [m^2] 2660531 [m^2]
Аэропорт 86771 4.58 MULTIPOLYGON (((37.51868 55… 4661389 [m^2] 4642294 [m^2]
Новогиреево 98132 4.45 MULTIPOLYGON (((37.80822 55… 4358550 [m^2] 4341039 [m^2]
Нагатино-Садовники 83750 8.17 MULTIPOLYGON (((37.62191 55… 8018021 [m^2] 7985615 [m^2]
Царицыно 128942 8.43 MULTIPOLYGON (((37.68594 55… 8392569 [m^2] 8358750 [m^2]
Академический 112067 5.83 MULTIPOLYGON (((37.55402 55… 5773643 [m^2] 5750188 [m^2]

Аналогично можно считать длины.

st_centroid()

Также давайте найдем центроиды полигонов с помощью команды st_centroid()

centroid <- st_centroid(districts_center)

st_coordinates(centroid)
##              X        Y
##  [1,] 37.57483 55.72911
##  [2,] 37.58998 55.75116
##  [3,] 37.63375 55.73363
##  [4,] 37.62874 55.78002
##  [5,] 37.60729 55.73057
##  [6,] 37.66681 55.74079
##  [7,] 37.55942 55.76089
##  [8,] 37.65453 55.77787
##  [9,] 37.67080 55.76668
## [10,] 37.60663 55.77024

4. Пространственные операции

Кроме вычислений мы можем по разному изменять наши объекты и их геометрии. Одним из самых популярных аналитических инструментов является буфер. Он часто имитирует зону доступности или зону влияния объекта. Построим буферы доступности от наших социальных объектов. У школ и садов они разные, поэтому сначала создадим поле, где укажем размер буфера

social_objects <- mutate(social_objects, dist = ifelse(type_ed == "школа", 500, 300))

social_objects <- st_transform(social_objects, crs = 32637)
social_buffer <- st_buffer(social_objects, social_objects$dist)
social_buffer <- st_transform(social_buffer, crs = 4326)

mapview(social_buffer, zcol='type_ed', legend=F)

Мы также можем резать объекты по геометриям друг друга, объединять их и многое другое. По ссылке представлены несколько операций, которые являются основными в рамках работы с геометриями (https://gisgeography.com/geoprocessing-tools/)

Наши дороги выходят за район исследования, давайте обрежем их

districts <- st_transform(districts, crs = 32637)
streets_mr <- st_intersection(streets, filter(districts, name == "Марьина Роща"))
districts <- st_transform(districts, crs = 4326)
streets_mr <- st_transform(streets_mr, crs = 4326)


mapview(streets_mr, zcol='NAME', legend=F)

5. Пространственные предикаты

Каждая пара слоев (и объектов к них) как-то пространственно соотносится друг с другом. Для ответа на вопрос как, есть набор пространственных предикатов. Каждый задает некоторый вопрос паре и в результате дает на ответ 0 или 1.

Внутри sf такие операции реализованы через следующие команды:

Table 1: Топологические отношения
Функция Топологическое.отношение
st_intersects(x, y) x имеет общие точки с y
st_disjoint(x, y) x не имеет общих точек с y
st_touches(x, y) x касается y (граница x имеет общие точки с границей y И внутренняя область x не имеет имеет общих точек с внутренней областью y)
st_crosses(x, y) x пересекает y (граница x имеет общие точки с границей y, при этом размерность их пересечения меньше размерности хотя бы одного из исходных объектов)
st_within(x, y) x внутри y (все точки x содержатся в y И внутренняя область x имеет общие точки с внутренней областью y)
st_contains(x, y) x содержит y (все точки y содержатся в x И внутренняя область y имеет общие точки с внутренней областью x)
st_contains_properly(x, y) x содержит y полностью (все точки y содержатся в x И граница x не имеет общих точек с границей y)
st_overlaps(x, y) x перекрывает y (внутренняя область x имеет как общие, так и не общие точки с внутренней областью y)
st_equals(x, y) x совпадает y (множества точек x и y совпадают)
st_covers(x, y) x покрывает y (все точки y содержатся в x)
st_covered_by(x, y) x покрыт y (все точки x содержатся в y)
st_equals_exact(x, y) x совпадает y точно (упорядоченные множества точек x и y совпадают)

Посмотрим просто на карте, как соотносятся буферы социальных объектов и дома района

 ggplot()+
  geom_sf(data = filter(districts, name == "Марьина Роща"), fill="#b8dae0")+
  geom_sf(data=social_buffer,color="red")+
  geom_sf(data=houses,color="orange", fill = "green")+
  geom_sf(data=streets_mr)+
  theme_minimal()

Можем перейти к центроидам и проверить уже их соотношение с буферами.

houses_centroid <- st_centroid(houses)

ggplot()+
  geom_sf(data = filter(districts, name == "Марьина Роща"), fill="#b8dae0")+
  geom_sf(data=social_buffer,color="red")+
  geom_sf(data= houses_centroid, color="orange")+
  geom_sf(data=streets_mr)+
  theme_minimal()

# 6. Пространственное присоединение

Обычный джоин работает по ключевому полю или по какому-то условия. Пространственный предикат может стать таким условием.

st_join()

Одной из самых частых комманд, используемых при работе с пространственными данными - перенос информации с одного геослоя на другой. Делается это с помощью команды st_join().

Давайте передадим буферам следующую информацию. Сколько объектов из слоя домов они заключают. Можем посчитать пересечения с полигонами, а можем включение центроидов

Id n type_ed dist geometry
1 59 школа 500 POLYGON ((37.62 55.79628, 3…
2 26 школа 500 POLYGON ((37.61083 55.78765…
3 36 школа 500 POLYGON ((37.60852 55.79579…
4 42 школа 500 POLYGON ((37.62395 55.7892,…
5 45 школа 500 POLYGON ((37.62263 55.7914,…
6 26 школа 500 POLYGON ((37.61242 55.78696…

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

7. Сетки и симуляция карт плотности показателя

Для расчета карт плотности можно использовать алгоритмы растрового анализа. Это - стандарт индустрии, которые много где используется. Но для решения это задачи можно также использовать сетки (растр по сети тоже сетка, но она статичная, не векторная).

st_make_grid()

Мы создаем “сетку” маленьких квадратов или гексов, чтобы в дальшейшем с помощью пространственного соединения перенести на них какую-то информацию. Сделать это можно с помощью st_make_grid():

grid <- st_as_sf(st_make_grid(st_transform(districts_center, crs = 32637), cellsize = 500)) 
grid <- mutate(grid, id=rownames(grid))

grid <- st_intersection(grid, st_union(st_transform(districts_center, crs = 32637)))
streets_grid <- st_intersection(streets, grid)
streets_grid <- select(streets_grid, OSM_ID, NAME, HIGHWAY)

Что мы сделали? Мы нарезали сетку по районам, а затем улицы по сетке. С помощью этого мы сможем посчитать плотность дорожной сети (м/м2)

streets_grid <- streets_grid |>
  mutate(len = drop_units(st_length(streets_grid)))
grid <- grid |> 
  mutate(area = drop_units(st_area(grid)))

# Расчет плотности сети
grid_count <- st_join(grid, streets_grid, join = st_contains)
grid_sum <- grid_count |> 
  group_by(id) |> 
  summarise(tot_len = sum(len),
            area = area[1]) |> 
  mutate(dens = tot_len / area)

knitr::kable(head(grid_sum))
id tot_len area x dens
10 NA 17803.334 POLYGON ((411268.3 6175280,… NA
100 NA 4045.907 POLYGON ((417268.3 6176780,… NA
101 NA 5596.633 POLYGON ((417985.9 6176780,… NA
109 503.3946 63284.337 POLYGON ((409268.3 6176780,… 0.0079545
110 2020.6914 230893.673 POLYGON ((409768.3 6177280,… 0.0087516
111 2338.8652 250000.000 POLYGON ((409768.3 6177280,… 0.0093555
library(ggplot2)
ggplot()+
  geom_sf(data=grid_sum, aes(fill=dens))+
  scale_fill_viridis_c(option = "magma",alpha = 0.6,direction = -1)+
  geom_sf(data=districts_center,fill=NA,color="#222277")+
  coord_sf(crs = 32637)+
  theme_void()+
  guides(fill = guide_bins())