library(sf)
## Linking to GEOS 3.13.1, GDAL 3.10.2, PROJ 9.5.1; sf_use_s2() is TRUE
library(dplyr)
##
## Присоединяю пакет: 'dplyr'
## Следующие объекты скрыты от 'package:stats':
##
## filter, lag
## Следующие объекты скрыты от 'package:base':
##
## intersect, setdiff, setequal, union
library(readxl)
library(leaflet)
## Warning: пакет 'leaflet' был собран под R версии 4.5.1
library(scales)
library(rmapshaper)
## Warning: пакет 'rmapshaper' был собран под R версии 4.5.1
library(htmltools)
sf::sf_use_s2(FALSE)
## Spherical geometry (s2) switched off
knitr::opts_chunk$set(message = FALSE, warning = FALSE)
library(sf)
library(dplyr)
library(readxl)
library(leaflet)
library(scales)
library(rmapshaper)
library(htmltools)
sf::sf_use_s2(FALSE)
knitr::opts_chunk$set(message = FALSE, warning = FALSE)
# Пути к данным (оставлены как у тебя)
gpkg_path <- "G:/Мой диск/ВКР/R_data/Сбериндекс/t_dict_municipal_districts_poly.gpkg"
gpkg_layer <- "t_dict_municipal_districts_poly"
xlsx_path <- "./analysis_df4.xlsx"
# Геометрия МО
g <- st_read(gpkg_path, layer = gpkg_layer, quiet = TRUE) |>
mutate(territory_id = as.character(territory_id)) |>
dplyr::select(territory_id, dplyr::everything()) |>
st_transform(4326) |>
st_make_valid() |>
rmapshaper::ms_simplify(keep = 0.05, keep_shapes = TRUE)
# Показатели
df <- read_excel(xlsx_path) |>
mutate(
territory_id = as.character(territory_id),
pickup_per_100k = as.numeric(pickup_per_100k),
salary = as.numeric(salary),
avg_age = as.numeric(avg_age),
rural_population_share = as.numeric(rural_population_share),
urbanization = pmax(pmin(1 - rural_population_share, 1), 0)
) |>
dplyr::select(
territory_id, region, municipal_district_name,
pickup_per_100k, salary, avg_age, urbanization
)
# Соединяем
dat <- g |> left_join(df, by = "territory_id")
# Попапы
nm <- ifelse(is.na(dat$municipal_district_name), "(неизвестно)", dat$municipal_district_name)
rg <- ifelse(is.na(dat$region), "", dat$region)
popup <- sprintf(
"<b>%s</b><br/>%s<hr style='margin:4px 0;'/>
ПВЗ на 100 тыс.: <b>%s</b><br/>
Зарплата: <b>%s ₽</b><br/>
Возраст: <b>%s</b><br/>
Урбанизация: <b>%s%%</b>",
nm, rg,
number(dat$pickup_per_100k, accuracy = 0.1),
number(dat$salary, accuracy = 1, big.mark = " "),
number(dat$avg_age, accuracy = 0.1),
number(100 * dat$urbanization, accuracy = 0.1)
)
# ---------- НОРМАЛИЗАЦИЯ ПРЕДИКТОРОВ ----------
z <- function(x) (x - mean(x, na.rm = TRUE)) / sd(x, na.rm = TRUE)
dat$salary_z <- z(dat$salary)
dat$age_z <- z(dat$avg_age)
dat$urb_z <- z(dat$urbanization)
# ---------- Цвета ----------
na_col <- "#F2E2B5" # бежевый для NA (чётко отличается от серой «середины»)
brks <- c(-Inf, -1.5, -0.5, 0.5, 1.5, Inf)
pal <- colorBin(
palette = rev(RColorBrewer::brewer.pal(5, "RdBu")),
domain = c(-3, 3),
bins = brks,
na.color = na_col
)
# дружелюбные подписи к границам легенды
lab_sigma <- function(type, cuts, p) {
b <- cuts; n <- length(b); out <- character(n - 1)
for (i in seq_len(n - 1)) {
L <- b[i]; R <- b[i + 1]
if (is.infinite(L)) out[i] <- "≤ −1.5σ (ниже)"
else if (is.infinite(R)) out[i] <- "≥ +1.5σ (выше)"
else if (L == -0.5 && R == 0.5) out[i] <- "средний уровень (±0.5σ)"
else out[i] <- paste0(ifelse(L > 0, paste0("+", L), L), "σ … ",
ifelse(R > 0, paste0("+", R), R), "σ")
}
out
}
# ---------- Кружки ПВЗ ----------
pts <- sf::st_point_on_surface(dat)
rad <- function(v) scales::rescale(sqrt(pmax(v, 0)), to = c(3, 18))
# Легенда размеров (Q1/медиана/90-й перц.) + пояснение
qv <- quantile(dat$pickup_per_100k, c(.25, .5, .9), na.rm = TRUE)
qs <- rad(qv)
size_leg <- paste0(
"<div style='background:rgba(255,255,255,0.95);padding:8px 10px;border:1px solid #e5e7eb;border-radius:8px;font-size:12px;'>",
"<div style='font-weight:600;margin-bottom:4px;'>Размер кружка: ПВЗ на 100 тыс.</div>",
"<div style='display:flex;gap:12px;align-items:flex-end;'>",
paste0(
vapply(seq_along(qv), function(i) {
d <- round(qv[i], 1); s <- round(qs[i])
sprintf(
"<div style='text-align:center;'>
<div style='width:%dpx;height:%dpx;border-radius:50%%;border:1px solid #333;background:#1111;margin:0 auto;'></div>
<div style='font-size:11px;margin-top:2px;'>≈ %s</div>
</div>",
s * 2, s * 2, d
)
}, character(1L)), collapse = ""
),
"<div style='font-size:11px;color:#666;margin-top:6px;'>Опорные значения по всем муниципалитетам РФ</div>",
"</div>"
)
# краткая подсказка (что такое цвет и z-баллы)
help_box <- paste0(
"<div style='background:rgba(255,255,255,0.95);padding:8px 10px;border:1px solid #e5e7eb;border-radius:8px;font-size:12px;max-width:380px;'>",
"<b>Как читать карту</b><br/>",
"Цвет полигона — отклонение выбранного предиктора от среднего по всем муниципалитетам (z-баллы). ",
"Красный — выше среднего, серый — около среднего, синий — ниже. ",
"Бежевый цвет означает, что данных нет. ",
"Размер серых кружков — ПВЗ на 100 тыс. жителей.",
"</div>"
)
# ---------- КАРТА ----------
m <- leaflet(options = leafletOptions(minZoom = 3, maxZoom = 12)) |>
addProviderTiles(providers$CartoDB.Positron, group = "Светлая подложка") |>
addProviderTiles(providers$CartoDB.DarkMatter, group = "Тёмная подложка") |>
addPolygons(
data = dat, group = "Зарплата",
fillColor = pal(dat$salary_z),
color = "#777", weight = 0.4, fillOpacity = 0.9,
popup = popup,
highlightOptions = highlightOptions(weight = 2, color = "#000", bringToFront = TRUE)
) |>
addPolygons(
data = dat, group = "Возраст",
fillColor = pal(dat$age_z),
color = "#777", weight = 0.4, fillOpacity = 0.9,
popup = popup
) |>
addPolygons(
data = dat, group = "Урбанизация",
fillColor = pal(dat$urb_z),
color = "#777", weight = 0.4, fillOpacity = 0.9,
popup = popup
) |>
addCircleMarkers(
data = pts, group = "ПВЗ (кружки)",
radius = rad(pts$pickup_per_100k),
stroke = TRUE, weight = 0.8, color = "#ffffff",
fillColor = "#333333", fillOpacity = 0.30
) |>
addLayersControl(
baseGroups = c("Светлая подложка", "Тёмная подложка"),
overlayGroups = c("Зарплата", "Возраст", "Урбанизация", "ПВЗ (кружки)"),
options = layersControlOptions(collapsed = FALSE)
) |>
# единая цветовая легенда + понятная подпись
addLegend(
position = "bottomleft", pal = pal, values = c(-3, 3),
title = HTML("Цвет: отклонение предиктора от среднего (z)<br/>
<span style='font-size:11px;'>Красный — выше среднего • Серый — около среднего • Синий — ниже среднего</span>"),
labFormat = lab_sigma, opacity = 0.95
) |>
# отдельная отметка «Нет данных» (бежевый)
addLegend(
position = "bottomleft",
colors = na_col, labels = "Нет данных", opacity = 1
) |>
addControl(html = HTML(size_leg), position = "bottomright") |>
addControl(html = HTML(help_box), position = "topleft") |>
hideGroup(c("Возраст", "Урбанизация"))
m