У «Важных историй» вышел текст о том, как в 2021 году минимум 100 тысяч российских детей с трудом добираются до школ. Для части этого небольшого исследования мы считали радиусы доступности, то есть сколько километров от каждого населенного пункта России до ближайшей школы.

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

Важно, что расстояния в этом уроке считаются без учета дорог и естественных преград в виде гор, рек, болот и пр., поэтому дают общее представление о дальности объектов. Чтобы точно рассчитать дистанцию и время в пути по дорогам, можно воспользоваться API матрицы расстояний Яндекс.Карт (платно, либо можно попробовать попросить бесплатно в исследовательских и научных целях по индивидуальному запросу) или OSRM — Open Source Routing Machine (бесплатно).

Jupyter ноутбук с уроком доступен на странице Мастерской на GitHub.

Видео: Глеб Лиманский

1. Скачивание данных

Нам нужно два набора данных — с населенными пунктами и со школами.

1. Данные по населенным пунктам скачиваем на сайте ИНИД (Инфраструктура научно-исследовательских данных).

2. Данные по школам — с помощью парсинга с сайта https://schoolotzyv.ru/schools/9-russia/ (скачанные данные доступны в формате json по ссылке). По некоторым регионам на сайте есть не все школы. Можно дополнить данными с сайтов https://arhangelsk.fulledu.ru/https://russiaedu.ru/, набора открытых данных с лицензиями Рособрнадзора (требуется парсинг xml) и других источников.

2. Загрузка библиотек

Загружаем 4 библиотеки: pandas для работы с датафреймами — данными в табличном виде, numpy и scikit-learn для математических операций, json для работы с файлами в формате json.

3. Загрузка и подготовка данных

Датасет со школами в формате json. Загружаем его в Pandas. В нем более 50 тысяч строк.

В файле есть геокоординаты (широта, долгота), ссылка на школу и ее название. Наименований регионов нет, а нам желательно для примера выбрать только один регион, иначе, если оставить всю Россию, это будет очень долго считаться. Регион можно узнать из ссылки. Например, в строке с индексом 1 школа из Камчатского края: в ссылке есть фрагмент «131-kamchatskij».

Выберем, например, Нижегородскую область. В ссылках она помечена как «146-nizhegorodskaya». С помощью метода contains («содержит»), выберем только те ссылки, в которых есть эта запись. Так у нас останутся только школы Нижегородской области. Подробнее о том, как фильтровать датафреймы — в первом уроке по Pandas.

Поскольку мы выбрали только некоторые строки, индексы датафрейма сбились и теперь идут не сначала:

Чтобы индексы сбросились и снова начинались с нуля, обновим их методом reset_index(). Drop = True означает, что старые индексы нам не нужны.

Далее загружаем файл с населенными пунктами. В нем есть регион, район, название, численность населения, сколько в них детей, координаты, код ОКТМО (Общероссийского классификатора территорий муниципальных образований).

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

Снова обновляем индексы, чтобы шли с нуля.

4. Создание словарей

Прежде чем считать расстояния, нам понадобится создать два словаря — с населенными пунктами и со школами. Это нужно, чтобы в дальнейшем мы могли сопоставить разные датафреймы по какому-то уникальному параметру, доставать и добавлять нужные нам данные. Нам это пригодится на последних этапах.

Для населенных пунктов таким уникальным параметром, по которому можно будет объединять разные датафреймы, станет код ОКТМО (Общероссийского классификатора территорий муниципальных образований).

Перед этим нам нужно убедиться, что:

1) Во всех нужных нам столбцах указан правильный тип данных.

2) Удалены дубликаты в тех столбцах, которые будут уникальным ключом для словаря (для населенных пунктов это поле oktmo, для школ — url), поскольку ключи словаря не должны дублироваться.

Смотрим методом dtype, какой тип данных в столбце oktmo. Это float, число с плавающей точкой.

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

Смотрим, есть ли в столбце oktmo дубликаты.

Удаляем дубликаты и обновляем индексы.

Cобираем датафрейм с населенными пунктами в словарь, где ключом будет код ОКТМО, а значением — строка с информацией о соответствующем населенном пункте. Сначала мы создаем пустой словарь. Затем переменной i проходимся по всем индексам от 0 до последнего в нашем датафрейме. В переменную el записываем строку, которую получаем методом loc.

Переходим к датафрейму со школами. Сначала меняем тип данных в столбцах с широтой и долгой, потому что сейчас там строки, а нам для расчетов нужны числа (float).

Удаляем дубликаты и обновляем индексы.

И теперь для школ собираем такой же словарь, как и для населенных пунктов. Ключом будет url.

5. Расчет матрицы расстояний

Переходим к расчету расстояний между каждым населенным пунктом и каждой школой (матрица расстояний). За основу взят туториал Dana Lindquist.

Сначала переводим координаты из градусов в радианы, потому что математические формулы обычно требуют значения координат в радианах, а не в градусах. Делаем это с помощью библиотеки numpy.

Далее считаем расстояния с помощью формулы из библиотеки scikit-learn. Эта формула вычисляет гаверсинусное расстояние, то есть представляет форму Земли как идеальную сферу (а не геоид, как на самом деле) и за счет этого обеспечивает быстрые вычисления. Если требуется измерение в километрах, то в конце формулы нужно умоножать 6371, если в милях — на 3959. Мы получим расстояния от каждого населенного пункта до каждой школы в километрах.

Строим матрицу расстояний — таблицу, в которой индексами будут url школ, колонками - ОКТМО населенных пунктов, а в ячейках будет расстояние.

Но нам нужно расстояние от населенных пунктов до школ, а не наоборот, поэтому транспонируем таблицу — поменяем индексы и колонки местами.

6. Выбор ближайшего объекта

Выбираем ближайшую школу к каждому населенному пункту. Для этого сравниваем каждое значение в строке с предыдущим, чтобы проверить, больше оно или меньше. Меньшее записываем в переменную. Так проходим циклом по каждой строке, выбирая самое меньшее значение. И создаем словарь, в котором ключ — код ОКТМО населенного пункта, значения — url ближайшей школы и расстояние до нее.

Далее добавляем колонки с минимальным расстоянием и url школы в датафрейм places_voskresensk (с населенными пунктами). Для этого берем из созданного выше словаря schools_and_min_value_by_oktmo ссылку (url) и расстояние до школы (distns). Добавляем их в словари school_url_column и min_distance_column в том порядке, в каком соответствующие им коды ОКТМО расположены в датафрейме places_voskresensk. И добавляем соответствующие новые колонки к датафрейму.

Точно так же из словаря со школами достаем название школ и добавляем их к датафрейму.

В конце появились 3 новые колонки — url ближайшей школы, сколько до нее километров по прямой, название школы.

Сохраним полученный результат в csv.

7. Анализ данных

Дальше мы можем анализировать данные. Например, выбрать 15 наиболее удаленных от школ населенных пунктов методом nlarest. Подробнее об nlargest и nsmallest — в 3 уроке по Pandas.

Получили топ самых удаленных населенных пунктов. Или можем посчитать, сколько детей в Воскресенском районе Нижегородской области живут дальше 5 км от школы.

Или среднее расстояние от населенного пункта до школы.

Готово!