Пространственные взаимосвязи и соседство¶
В этой главе мы рассмотрим, как формализуются пространственные взаимосвязи между объектами. Мы обсудим основные подходы к определению пространственного соседства:
по смежности,
по расстоянию,
по k-ближайшим соседям.
Также познакомимся с понятием матриц пространственных весов, и разберём, как выбор схемы соседства может потенциально влиять на результаты пространственного анализа
Понимание пространственного соседства является ключевым для пространственной статистики, поскольку большинство методов, изучаемых в рамках курса, требуют явного определения того, какие объекты и каким образом связаны между собой.
0. Подготовка данных¶
Импортируем библиотеки
import geopandas as gpd
import pandas as pd
import matplotlib.pyplot as plt
import hvplot
Загрузим данные, чтобы дальше не отвлекаться на их подготовку :)
Мы поработаем:
со статистическими данными по муниципальным образованиям Краснодарского края и Республики Адыгея (источник — БДМО, Росстат);
с данными о плотности населения в Краснодаре, агрегированными на регулярную квадратную сетку;
с точечными данными о ДТП с участием пешеходов в Краснодаре (2017).
В этом разделе мы не будем анализировать значения показателей, а сосредоточимся на пространственной конфигурации данных. К самим показателям вернёмся позже.
Загружаем данные о муниципалитетах для Краснодарского края и республики Адыгея
muni = gpd.read_file('./data/krasnodar_adygea_muni.geojson')
muni.explore(tiles='cartodbpositron')Загружаем данные о плотности населения Краснодара, агрегированные по квадратной сетке регулярных ячеек
sq_grid = gpd.read_file('./data/krs_pop_sqgrid.geojson')
sq_grid.explore(tiles='cartodbpositron')
Загружаем точечные данные о ДТП в Краснодаре с участием пешеходов (в 2017 году)
dtp_points = gpd.read_file('./data/krs_dtp_ped2017.geojson')
dtp_points = dtp_points.to_crs(dtp_points.estimate_utm_crs())
dtp_points.explore(tiles='cartodbpositron')Skipping field Time: unsupported OGR type: 10
1. Пространственное соседство (Spatial Neighborhood)¶
Подходы к определению пространственного соседства
В пространственной статистике соседство можно определять на основе трёх базовых групп подходов:
Соседство по смежности (contiguity-based) —
взаимодействия определяются наличием общей границы
(административные районы, кварталы, ячейки сетки).Соседство по расстоянию (distance-based) —
предполагается физический радиус влияния
(доступность).Соседство по k-ближайшим соседям (kNN) —
важно, чтобы у каждого объекта было одинаковое число соседей.
Эти подходы по-разному отвечают на вопрос:
какие объекты считать «близкими» и как формально это зафиксировать
Матрица пространственных весов
Для формального описания соседства используется
матрица пространственных весов.
На основе выбранного типа соседства строится матрица, в которой для каждой пары пространственных объектов определяются веса. В простом случае:
значение
1означает наличие соседства между объектами;значение
0— его отсутствие.
В умолчанию рассматриваются соседи первого порядка —
объекты, являющиеся непосредственными соседями
(в соответствии с выбранным типом соседства).
При необходимости могут учитываться и соседи более высоких порядков:
второго порядка — соседи соседей первого порядка;
третьего порядка — соседи соседей второго порядка;
и так далее.
Веса связей
Важно отметить, что матрица весов не всегда бинарная (0/1).
Часто веса отражают силу пространственной связи, например:
обратная зависимость от расстояния:
w_ij = 1 / d_ij(чем ближе, тем сильнее связь);доля общей границы (для полигональных объектов);
нормировка по строкам (row-standardization),
чтобы суммарное влияние соседей для разных объектов было сопоставимым
В рамках данного раздела основное внимание будет уделяться
соседям первого порядка, однако также будут рассмотрены
отдельные примеры для соседей более высоких порядков
Соседство по смежности (contiguity-based)¶
Идея:
Объекты считаются соседями, если между ними существует общая граница.
Подход применяется преимущественно для площадных объектов – полигонов.
Основные варианты
Rook (ладья) — соседство определяется наличием общей стороны.
Queen (ферзь) — соседство определяется наличием общей стороны или вершины.
Rook¶
Два объекта считаются соседями по типу Rook, если они имеют общую сторону.

Для регулярной сетки¶
Построим матрицу пространственных весов с помощью библиотеки libpysal.weights
from libpysal.weights import Rook
sq_grid = sq_grid.reset_index(drop=True) # сбросим индексы, чтобы потом использовать их как id
w_rook = Rook.from_dataframe(sq_grid, ids=sq_grid.index.tolist())# построим матрицу весов
Посмотрим на несколько первых элементов матрицы весов, чтобы понять её структуру
W = w_rook.full()[0]
n = 15
dfW = pd.DataFrame(W[:n, :n])
dfWПо сути матрица пространственных весов W — это таблица связей между объектами. В простейшем (бинарном) варианте: 1 — объект j является соседом i, 0 — не является
Для каждого объекта указан список id его соседей
list(w_rook.neighbors.items())[:5][(0, [1, 29]),
(1, [0, 2, 30]),
(2, [1, 3, 31]),
(3, [32, 2, 4]),
(4, [33, 3, 5])]Выберем одну ячейку и посмотрим, какие объекты являются её соседями
sample_id = 443 # выбираем индекс элемента, находящегося в середине набора данных
neighbors = w_rook.neighbors[sample_id]
print("ID ячейки:", sample_id)
print("Число rook-соседей 1-го порядка:", len(neighbors))
print("Соседи 1-го порядка:", neighbors)
ID ячейки: 443
Число rook-соседей 1-го порядка: 4
Соседи 1-го порядка: [472, 442, 444, 414]
Посмотрим расположение соседей на карте
fig, ax = plt.subplots(1, 1, figsize=(7, 7))
sq_grid.boundary.plot(ax=ax, linewidth=0.4) # все ячейки
sq_grid.loc[neighbors].plot(ax=ax, alpha=0.6) #соседи ячейки i
sq_grid.loc[[sample_id]].plot(ax=ax, edgecolor="black", linewidth=2) #ячейка i
ax.set_title("Square grid — Rook соседство (1-й порядок)")
ax.set_axis_off()
plt.show()

Для границ муниципалитетов¶
Повторим те же шаги для слоя с муниципалитетами Краснодарского края и Реуспублики Адыгея
Построим матрицу пространственных весов с помощью библиотеки libpysal.weights
muni = muni.reset_index(drop=True) # сбросим индексы, чтобы потом использовать их как id
w_rook_muni = Rook.from_dataframe(muni, ids=muni.index.tolist())# построим матрицу весовВыберем один муниципалитет и посмотрим, какие объекты являются её соседями
muni_id = 48
neighbors_muni = w_rook_muni.neighbors[muni_id]
print("ID ячейки:", muni_id)
print("Число rook-соседей 1-го порядка:", len(neighbors_muni))
print("Соседи 1-го порядка:", neighbors_muni)ID ячейки: 48
Число rook-соседей 1-го порядка: 6
Соседи 1-го порядка: [51, 4, 37, 39, 7, 52]
Посмотрим расположение соседей на карте
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
muni.boundary.plot(ax=ax, linewidth=0.6) # все границы
muni.loc[neighbors_muni].plot(ax=ax, alpha=0.6) # соседи муниципалитета i
muni.loc[[muni_id]].plot(ax=ax, edgecolor="black", linewidth=2) # муниципалитет i
ax.set_title("Муниципалитеты — Rook соседство (1-й порядок)")
ax.set_axis_off()
plt.show()

Queen¶
Два объекта считаются соседями по типу Queen, если они имеют общую сторону или вершину

Для регулярной сетки¶
Построим матрицу пространственных весов с помощью библиотеки libpysal.weights
from libpysal.weights import Queen
w_queen_sq = Queen.from_dataframe(sq_grid, ids=sq_grid.index.tolist())
w_queen_sq.neighborsВыберем одну ячейку и посмотрим, какие объекты являются её соседями
sample_id = 443
neighbors_queen = w_queen_sq.neighbors[sample_id]
print("ID ячейки:", sample_id)
print("Число queen-соседей 1-го порядка:", len(neighbors_queen))
print("Соседи 1-го порядка:", neighbors_queen)ID ячейки: 443
Число queen-соседей 1-го порядка: 8
Соседи 1-го порядка: [471, 472, 473, 442, 444, 413, 414, 415]
Посмотрим расположение соседей на карте
fig, ax = plt.subplots(1, 1, figsize=(7, 7))
sq_grid.boundary.plot(ax=ax, linewidth=0.4) # все границы
sq_grid.loc[neighbors_queen].plot(ax=ax, alpha=0.6) # соседи ячейки i
sq_grid.loc[[sample_id]].plot(ax=ax, edgecolor="black", linewidth=2) # ячейка i
ax.set_title("Square grid — Queen соседство (1-й порядок)")
ax.set_axis_off()
plt.show()
Для границ муниципалитетов¶
Этот шаг предлагается выполнить самостоятельно.
Будет ли матрица пространственных весов, построенная по типу Queen, отличаться от матрицы типа Rook для рассматриваемых муниципалитетов?
Если да, в чём именно будут заключаться эти различия?
Соседство по смежности основано на геометрическом контакте объектов и чаще всего применяется для полигональных данных (административные единицы, зоны, ячейки сетки). Тип Rook учитывает только общую границу и формирует более “строгое” локальное соседство. Тип Queen дополнительно учитывает касание по вершинам и, как правило, приводит к большему числу соседей и более плотной структуре связей. Выбор между Rook и Queen влияет на число соседей у объектов и связность всей системы, а значит — на результаты последующего анализа пространственной автокорреляции и моделирования, о которых будем говорить в следующих разделах.
Соседи второго порядка¶
Соседи второго порядка — это объекты, которые не являются непосредственными соседями, но граничат с соседями первого порядка (то есть «соседи соседей»).
Иногда пространственный эффект выходит за пределы ближайших границ: влияние может передаваться через цепочки соседних территорий (например, при миграциях, учёте транспортных связей и пр.). В таких случаях используют соседство более высоких порядков, позволяющее учитывать более «длинные» пространственные связи.
На примере соседей второго порядка, определённых по типу Rook для муниципалитетов, мы разберём общий подход к работе с соседями более высоких порядков, который можно аналогично применять к другим типам соседства по смежности.
Определим соседей первого порядка, как мы это делали выше
w = Rook.from_dataframe(muni, ids=muni.index.tolist())С помощью метода higher_order из модуля libpysal.weights определим соседей второго порядка — соседей соседей для каждого муниципалитета
from libpysal.weights import higher_order
w2 = higher_order(w, k=2)
list(w2.neighbors.items())[:5]
[(0, [41, 50, 40, 7, 30, 39, 25, 33, 42, 10]),
(1, [53, 48, 52, 24, 51, 46, 50, 45, 35, 44, 21]),
(2, [32, 22, 16, 9]),
(3, [15, 10, 28, 37, 32, 8, 7, 16, 25, 20, 29, 38]),
(4, [39, 43, 47, 24, 33, 37, 50, 18, 45, 21, 7])]Посмотрим на карте соседей первого и второго порядков для одного из муниципалитетов
muni_id = 48
#Выбираем соседей первого порядка
neighbors_1 = w.neighbors[muni_id]
#Выбираем соседей второго порядка
neighbors_2 = w2.neighbors[muni_id]# Показываем их на карте
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
muni.boundary.plot(ax=ax, linewidth=0.6)# все границы
muni.loc[neighbors_1].plot(ax=ax, alpha=0.35) # границы второго порядка муниципалитета i
muni.loc[neighbors_2 ].plot(ax=ax, alpha=0.70) # границы первого порядка муниципалитета i
muni.loc[[muni_id]].plot(ax=ax, edgecolor="black", linewidth=2) # ячейка i
ax.set_title("Муниципалитеты — Rook соседство: 1-й и 2-й порядок")
ax.set_axis_off()
plt.show()

Соседи более высоких порядков позволяют учитывать косвенные пространственные связи, выходящие за рамки непосредственного соседства. Соседи второго порядка отражают влияние, передающееся через соседей первого порядка, и могут быть полезны, когда: пространственные процессы распространяются по цепочке территорий; влияние не ограничивается общей границей.
Соседство по расстоянию (distance-band)¶
Идея: объекты — соседи, если расстояние между ними меньше заданного порога d. Подходит и для точечных данных, но в целом универсален для любых типов геометрий
Важно: для построения соседства по расстоянию нужны метры (данные должны быть в метрических СК).
С помощью инструментов библиотеки libpysal.weights можно строить матрицы весов на основе заданного расстояния. Рассмотрим такой тип соседства на примере точечных данных о ДТП с участием пешеходов в Краснодаре
from libpysal.weights import DistanceBand
dtp_points = dtp_points.reset_index(drop=True) ## сбросим индексы, чтобы потом использовать их как id
# Задаём порог расстояния (в метрах) и строим веса distance band
d = 1000
w_dist = DistanceBand.from_dataframe(
dtp_points,
threshold=d,
silence_warnings=True # чтобы не ругался на "острова" при малом d
)
Посмотрим на результат: Для каждой из точек представлен список её “соседей” - тех точек, которые расположены в пределах заданного радиуса
list(w_dist.neighbors.items())[:6][(0, [2, 8, 70, 128, 179, 306, 329, 347]),
(1, [97, 135, 152, 185, 194, 218, 231, 294, 311, 316, 344]),
(2, [0, 6, 8, 13, 40, 101, 133, 179, 183, 192, 286, 306, 329, 347, 384]),
(3, [91, 150, 215, 270]),
(4, [36, 128, 212, 372, 383]),
(5, [299, 300, 321, 335, 386, 387])]Визуализируем выбранную точку и её соседей на карте
point_id = dtp_points.index[len(dtp_points)//2]
# выбираем её соседей
neighbors_1 = w_dist.neighbors[point_id]
#визуализируем результат
fig, ax = plt.subplots(1, 1, figsize=(7, 7))
dtp_points.plot(ax=ax, markersize=6, alpha=0.5) # все точки
dtp_points.loc[neighbors_1].plot(ax=ax, markersize=35, alpha=0.8) # соседи точки i
dtp_points.loc[[point_id]].plot(ax=ax, markersize=80, edgecolor="black") # точка i
dtp_points.loc[[point_id]].buffer(d).boundary.plot(ax=ax, linewidth=1) # круг радиуса d вокруг точки i
ax.set_title(f"ДТП — соседство по расстоянию (distance band), d = {d} м")
ax.set_axis_off()
plt.show()
Посчитаем количество соседей для каждой точки
n_neighbors = pd.Series(w_dist.neighbors).apply(len)n_neighbors0 8
1 11
2 15
3 4
4 5
..
383 5
384 16
385 5
386 1
387 1
Length: 388, dtype: int64Определим топ-10 точек с максимальным количеством соседей
top10 = n_neighbors.sort_values(ascending=False).head(10)
dtp_top10 = dtp_points.loc[top10.index].copy()
dtp_top10["n_neighbors"] = top10.values
dtp_top10
Посмотрим на точки на карте
# визуализируем результат
fig, ax = plt.subplots(1, 1, figsize=(7, 7))
dtp_points.plot(
ax=ax,
markersize=6,
alpha=0.4,
color="lightgrey"
) # все точки ДТП
dtp_top10.plot(
ax=ax,
markersize=40,
alpha=0.9,
color="red",
edgecolor="black"
) # точки с максимальным числом соседей
ax.set_axis_off()
Соседство по расстоянию связывает объекты, находящиеся друг от друга не дальше заданного порога. Такой подход можно применять как к точечным, так и к полигональным данным. Он хорошо подходит для процессов, у которых есть физический радиус влияния, но при этом чувствителен к выбору порога расстояния. Слишком маленький радиус приводит к появлению «островов» — объектов без соседей, а слишком большой делает связи слишком общими, где могут потеряться локальные закономерности
Соседство по k-ближайшим соседям (k-nearest neighbors, kNN)¶
Идея: у каждого объекта есть k-соседей – самых близких по расстоянию.
Рассмотрим этот тип соседства на примере данных о ДТП с участием пешеходов в Краснодаре.
С помощью инструментов библиотеки libpysal.weights можно также строить матрицы весов на основе k-ближайших соседей
from libpysal.weights import KNN
k = 8
w_knn = KNN.from_dataframe(dtp_points, k=k)Посмотрим, какие объекты являются соседями для каждой точки
list(w_knn.neighbors.items())[:5][(0, [329, 306, 8, 179, 128, 2, 70, 347]),
(1, [135, 311, 194, 316, 231, 344, 185, 218]),
(2, [179, 347, 101, 40, 183, 13, 133, 306]),
(3, [150, 270, 91, 215, 281, 203, 219, 158]),
(4, [372, 128, 212, 383, 36, 283, 226, 8])]Посмотрим на карте соседей для одной из точек.
point_id = dtp_points.index[len(dtp_points)//2]
neighbors_1 = w_knn.neighbors[point_id]
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
dtp_points.plot(ax=ax, markersize=6, alpha=0.35) # все точки
dtp_points.loc[neighbors_1].plot(ax=ax, markersize=45, alpha=0.8) # k-ближайших соседей точки i
dtp_points.loc[[point_id]].plot(ax=ax, markersize=90, edgecolor="black") # точка i
#Линии: точка i -> сосед
fx, fy = dtp_points.loc[point_id].geometry.x, dtp_points.loc[point_id].geometry.y
for j in neighbors_1:
x, y = dtp_points.loc[j].geometry.x, dtp_points.loc[j].geometry.y
ax.plot([fx, x], [fy, y], linewidth=0.8)
ax.set_title(f"ДТП — kNN соседство: {k} ближайших соседей")
ax.set_axis_off()
plt.show()
Соседство по k-ближайшим соседям обеспечивает фиксированное число соседей для каждого объекта, независимо от пространственной плотности данных. Этот подход особенно полезен: при неравномерном распределении точек; при наличии “островов” в distance-band соседстве; когда важно, чтобы все объекты были включены в анализ. Однако kNN может соединять достаточно удалённые объекты в разреженных областях, поэтому выбор параметра k должен быть обоснован.
2. Пространственные веса (Spatial Weights)¶
В первом разделе мы разобрали, как определить пространственное соседство. Теперь кратко рассмотрим особенности пространственных весов, которые важно учитывать при дальнейшем анализе.
Нас будут интересовать три ключевых аспекта:
у разных объектов может быть разное число соседей;
необходимость нормировки весов при сравнении объектов с разным числом соседей;
наличие «островов» — объектов без соседей.
2.1 Разное число соседей¶
Даже при одном и том же правиле (например, Rook) у объектов может быть разное число соседей, которое зависит от их положения в пространстве и геометрии объектов.
Посмотрим на количество соседей для каждого из муниципалитетов Краснодарского края и Республики Адыгея
# Определяем матрицу соседства
w_rook_muni = Rook.from_dataframe(muni, ids=muni.index.tolist())
# Извлекаем количество соседей для каждого муниципалитета
deg_m = pd.Series(w_rook_muni.cardinalities, name="n_neighbors")
# Добавляем число соседей в таблицу муниципалитетов
muni["n_neighbors_rook"] = muni.index.map(deg_m).fillna(0).astype(int)
# Визуализируем число соседей на карте
ax = muni.plot(column="n_neighbors_rook", legend=True, linewidth=0.3, edgecolor="white")
ax.set_title("Муниципалитеты: число соседей (Rook)")
ax.set_axis_off()
plt.show()
muni["n_neighbors_rook"].describe()
count 54.000000
mean 4.851852
std 1.686880
min 1.000000
25% 4.000000
50% 5.000000
75% 6.000000
max 8.000000
Name: n_neighbors_rook, dtype: float64У разных объектов может быть различное число соседей даже при одном и том же типе соседства. Если веса не стандартизированы, объекты с большим числом соседей оказывают более сильное суммарное влияние, что может искажать интерпретацию пространственных эффектов.
2.2 Стандартизация весов (row-standardization)¶
Часто веса нормируют по строкам, чтобы сумма весов для каждого объекта была равна 1. Это полезно, когда у объектов сильно различается число соседей.
Посмотрим на веса соседей для одного муниципалитета до стандартизации
print(w_rook_muni.weights[3])[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
# Стандартизация по строкам
w_rook_muni.transform = "R"
Посмотрим на веса соседей для одного муниципалитета после стандартизации
print(w_rook_muni.weights[3])
[0.16666666666666666, 0.16666666666666666, 0.16666666666666666, 0.16666666666666666, 0.16666666666666666, 0.16666666666666666]
При нормировке объект с большим числом соседей получает меньший вес на каждого соседа.
Стандартизация весов позволяет сделать вклад соседей сопоставимым для всех объектов независимо от их количества. После row-standardization сумма весов в каждой строке матрицы равна единице, поэтому влияние каждого объекта определяется не числом соседей, а их относительным вкладом. По этой причине стандартизированные веса чаще всего используются при расчёте показателей пространственной автокорреляции и в пространственных регрессионных моделях (о которых мы поговорим в следюущих разделах).
2.3 «Острова»: объекты без соседей¶
Иногда при выбранном типе и параметрах соседства часть объектов может не иметь ни одного соседа. Такие объекты принято называть «островами». Их наличие важно учитывать, так как они требуют особого внимания при дальнейшем анализе.
Ниже посчитаем число соседей для точек ДТП в радиусе 200 м и покажем точки с 0 соседей.
d = 200 # метров
w_dist = DistanceBand.from_dataframe(dtp_points, threshold=d, binary=True, silence_warnings=True)
deg = pd.Series(w_dist.cardinalities, name="n_neighbors")
dtp_points["n_neighbors_d200"] = dtp_points.index.map(deg).fillna(0).astype(int)
n_islands = (dtp_points["n_neighbors_d200"] == 0).sum()
print("Число 'островов' (0 соседей):", n_islands, "из", len(dtp_points))
ax = dtp_points.plot(markersize=6, alpha=0.35)
dtp_points[dtp_points["n_neighbors_d200"] == 0].plot(ax=ax, markersize=25, alpha=0.9, edgecolor="black")
ax.set_title("ДТП: точки без соседей (distance-band, d=200 м)")
ax.set_axis_off()
plt.show()
Число 'островов' (0 соседей): 209 из 388

«Островами» называют объекты, у которых отсутствуют соседи при выбранном типе и параметрах соседства. Чаще всего они возникают при использовании distance-band соседства со слишком малым порогом расстояния. Наличие «островов» требует отдельного внимания, так как такие объекты могут быть исключены из анализа или приводить к некорректной интерпретации результатов. На практике проблему решают изменением параметров соседства или выбором альтернативного подхода, например kNN.
3. Итог¶
В этом разделе мы рассмотрели основные способы задания пространственного соседства и построения матриц пространственных весов.
Пространственное соседство представляет собой формализацию того, какие объекты считаются связанными между собой. Различные типы соседства отражают разные предположения о механизмах пространственного взаимодействия и по-разному описывают структуру связей в данных. Поэтому выбор матрицы пространственных весов - один из важных шагов последющего анализа.
Один и тот же набор пространственных данных может приводить к различным результата в зависимости от выбранного типа соседства. Поэтому важно понимать разные подходы к определению пространственного соседства и их особенности