Жилье в 500м от сетевых продуктовых магазинов в Москве. Или как публикация на Лента.ру избегает Хабр и дезинформирует

от автора

Приятно когда живешь там где у тебя вход в метро в 15 минутах ходьбы и с комфортом жизни неплохо. Но кушать хочется всегда, а на рабочей неделе уж совсем нет времени и желания ехать в супермаркет чтобы сварить гречневую кашу и взять что-нибудь на завтрак. Магазины шаговой доступности есть везде, но мне хотелось бы чтобы это было что-нибудь более привычное, типа Пятёрочки, Дикси, Магнита, Перекрёстка, Магнолии, Атак или Ленты.

Жизнь по этому фактору в Серебряном бору( это один из победителей в категории удаленности от неблагоприятных факторов) отпадает сразу — там и до метро не близко! Найдем более удобные места для ежедневной жизни и похода в продуктовый магазин.

Это как в дилеме что первее: курица или яйцо, так же сетевой продуктовый/жилой район. Компании владельцы торговых сетей тратят деньги на геоанализ где им лучше расположить продуктовый магазин. Так что в каком-то виде мы переиспользуем их аналитику и найдем где живут мои соседи. Если же нужна другая группа соседей, то нужно поискать окрестности Азбуки Вкуса и бывшего Елисеевского.

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

Для анализа буду как обычно использовать геоданные OSM, и вот для выбора сетевых магазинов без «черной магии» мне не удастся обойтись, потому что в исходных данных полно опечаток, разных написаний и разношёрстности в обозначениях. После получаса экспериментов мне удалось подобрать нужное заклинание:

create temporary table supermarket_entrance as    select id, centre, tags      from geometry_global_view      where (tags@>'shop=>supermarket' or tags@>'shop=>convenience') and      (tags->'brand' in ('Пятёрочка','ВкусВилл','Дикси','Магнит',                                'Перекрёсток','Магнолия','Перекресток',                                'Лента','Eurospar','Ашан','Супер Лента',                                'Атак','СуперЛента','О’КЕЙ','Spar',                                'Перекресток Экспресс','SPAR',                                'METRO Cash & Carry','EuroSPAR','АТАК',                                'Ашан Сити','Мини Лента','EUROSPAR',                                'МиниЛента','METRO','Auchan','супер Лента')      or tags@>'brand:wikipedia=>ru:Auchan');

Подробнее про подготовку данных к анализу можете почитать в прошлой статье. Чтобы исключить из геоанализа мавзолей на Красной площади и Кремлевские башни добавлю фильтр:not(b.tags?'tomb') and not(b.tags?'man_made') и найду жилые здания у интересующих магазинов:

explain create table living_building_near as select m.id supermarket_entrance_id,st_x(m.centre) supermarket_entrance_x,st_y(m.centre) supermarket_entrance_y,        array_agg((b.id,b.type,st_x(b.centre),st_y(b.centre))::building_info) buildings   from supermarket_entrance m inner join geometry_global_view b on     b.tags?'building' and     not(b.tags?'amenity') and     not(b.tags?'shop') and     not(b.tags?'tomb') and     not(b.tags?'man_made') and     b.tags->'building' not in --в перечисленных ниже зданиях не живут на постоянной основе         ('service','garages','industrial','retail','office','roof','commercial','garage','kiosk','warehouse','church',         'parking','public','shed','hangar','train_station','guardhouse','transportation','terrace','greenhouse','bridge',         'government','chapel','gazebo','civic','ruins','supermarket','sports_centre','semidetached_house','toilets',         'sports_hall','clinic','farm_auxiliary','stable','grandstand','bunker','gatehouse','store','temple','ventilation_kiosk',         'carport','cowshed','barracks','shop','cabin','barn','cathedral','wall','townhouse','manufacture','shelter',         'fire_station','stadium','stands','sport_hall','theatre','storage_tank','checkpoint','houseboat','abandoned','dovecote',         'mosque','museum','military','container','observatory','lift','tent','factory','sport','mall','riding_hall','depot',         'prison','gate','triumphal_arch','water_works','public_building','pavilion','bank','institute','works','collapsed',         'car_repair','crossing_box','fuel','tree_house','presbytery','yesq','farm','outbuilding','police','porch','sauna',         'monastery','cinema','tower','boathouse','library','transformer_tower','heat_exchange_station','ice_rink','entrance','construction','transformer'         ) and      ST_DWithin(b.geom::geography,m.centre::geography,500)    group by 1,2,3; -- create table living_building_near as select m.id supermarket_entrance_id,st_x(m.centre) supermarket_entrance_x,st_y(m.centre) supermarket_entrance_y,array_agg((b.id,b.type,st_x(b.centre),st_y(b.centre))::building_info) buildings from supermarket_entrance m inner join geometry_global_view b on b.tags?'_lb' and ST_DWithin(b.geom::geography,m.centre::geography,500) group by 1,2,3;

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

GraphHopper 8 как движок расчета расстояний и Java код для него.
create table supermarket_distance(                 entrance_id bigint,                 building_id bigint,                 building_type table_reference,                 distance smallint,                 primary key(entrance_id,building_id,building_type) );

Основная проблема с расчетом расстояний — это время вычислений, в случае если сервис взаимодействует с программой как REST через TCP добавляется round trip пакетов к времени обработки каждого запроса, поэтому буду использовать Graphhopper как библиотеку внутри процесса без вызовов по HTTP:

package com.github.isuhorukov.routing;  import com.graphhopper.GHRequest; import com.graphhopper.GHResponse; import com.graphhopper.GraphHopper; import com.graphhopper.ResponsePath; import com.graphhopper.config.CHProfile; import com.graphhopper.config.Profile; import org.postgresql.jdbc.PgArray; import org.postgresql.util.PGobject;  import java.sql.*; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors;  public class Routing {      public static final int MAX_DISTANCE = 500;      public static void main(String[] args) throws Exception {         GraphHopper hopper = getGraphHopper();         try (Connection connection = DriverManager.getConnection(                 System.getenv("jdbc_url"), System.getenv("user"), System.getenv("password"))) {             connection.setAutoCommit(false);             try {                 var distances = calculateDistances(connection, hopper);                 saveDistance(connection, distances);             } catch (SQLException e) {                 connection.rollback();                 throw e;             }         }     }      private static GraphHopper getGraphHopper() {         var hopper = new GraphHopper();         hopper.setOSMFile(System.getenv("osm_file"));         hopper.setGraphHopperLocation("route");         hopper.getCHPreparationHandler().setCHProfiles(new CHProfile("foot"));         hopper.setProfiles(new Profile("foot").setVehicle("foot"));         hopper.importOrLoad();         return hopper;     }      private static List<Distance> calculateDistances(Connection connection, GraphHopper hopper) throws SQLException {         List<Distance> distances = new ArrayList<>();         try (Statement supermarketQuery = connection.createStatement()){             int step=0;             try (ResultSet livingBuilding = supermarketQuery.executeQuery("select * from living_building_near")){                 while (livingBuilding.next()){                     long supermarketEntranceId = livingBuilding.getLong("supermarket_entrance_id");                     double supermarketEntranceX = livingBuilding.getDouble("supermarket_entrance_x");                     double supermarketEntranceY = livingBuilding.getDouble("supermarket_entrance_y");                     PgArray buildingsArray = (PgArray) livingBuilding.getArray("buildings");                     List<Building> buildings = mapBuildings(buildingsArray);                     System.out.println(step++);                     distances.addAll(buildings.stream().map(building -> {                         short distance = calculateDistances(hopper, building, supermarketEntranceY, supermarketEntranceX);                         return new Distance(supermarketEntranceId,building.buildingId, building.buildingType, distance);                     }).collect(Collectors.toList()));                 }             }         }         return distances;     }      private static void saveDistance(Connection connection, List<Distance> distances) throws SQLException {         try (PreparedStatement insert = connection.prepareStatement(                 "insert into supermarket_distance(entrance_id,building_id,building_type,distance) " +                         "values (?,?,?::table_reference,?)")){             distances.forEach(distance -> {                 if(distance.distance> MAX_DISTANCE) return;                 try {                     insert.setLong(1, distance.supermarketEntranceId);                     insert.setLong(2, distance.buildingId);                     insert.setString(3, distance.buildingType);                     insert.setShort(4, distance.distance);                     insert.addBatch();                 } catch (SQLException e) {                     throw new RuntimeException(e);                 }             });             insert.executeBatch();             connection.commit();         }     }      private static List<Building> mapBuildings(PgArray buildingsArray) throws SQLException {         return getPgObjects(buildingsArray).stream().map(pGobject -> {             String value = pGobject.getValue();             if(value==null || value.length()<=2){                 throw new IllegalArgumentException();             }             String[] parts = value.substring(1, value.length() - 1).split(",");             long buildingId = Long.parseLong(parts[0]);             String buildingType = parts[1];             double buildingX = Double.parseDouble(parts[2]);             double buildingY = Double.parseDouble(parts[3]);             return new Building(buildingId, buildingType, buildingX, buildingY);         }).collect(Collectors.toList());     }      private static List<PGobject> getPgObjects(PgArray buildingsArray) throws SQLException {         Object[] array = (Object[]) buildingsArray.getArray();         buildingsArray.free();         return Arrays.stream(array).map(object -> (PGobject) object).collect(Collectors.toList());     }      private static short calculateDistances(GraphHopper hopper,                                       Building building, double supermarketEntranceY, double supermarketEntranceX) {         GHRequest request = new GHRequest(supermarketEntranceY, supermarketEntranceX, building.buildingY, building.buildingX);         request.setProfile("foot");         GHResponse route = hopper.route(request);         if(route.hasErrors()){             return Short.MAX_VALUE;         }         ResponsePath bestRoute = route.getBest();         return (short) Math.round(bestRoute.getDistance());     }      private static class Building{         long buildingId;         String buildingType;         double buildingX;         double buildingY;         public Building(long buildingId, String buildingType, double buildingX, double buildingY) {             this.buildingId = buildingId;             this.buildingType = buildingType;             this.buildingX = buildingX;             this.buildingY = buildingY;         }     }      private static class Distance{         long supermarketEntranceId;         long buildingId;         String buildingType;         short distance;         public Distance(long supermarketEntranceId, long buildingId, String buildingType, short distance) {             this.supermarketEntranceId = supermarketEntranceId;             this.buildingId = buildingId;             this.buildingType = buildingType;             this.distance = distance;         }     } }
Карта домов в 500м у сетевых продуктовых магазинов шаговой доступности в Москве

Карта домов в 500м у сетевых продуктовых магазинов шаговой доступности в Москве

Выгружаю результат анализа в GeoJSON файл и отправляю его на GitHub:

\copy (select json_build_object('type', 'FeatureCollection','features', json_agg(json_build_object('type', 'Feature','geometry', st_AsGeoJSON(centre)::json))) from (select distinct centre from supermarket_distance s inner join geometry_global_view b on b.id=s.building_id and b.type=s.building_type) buildings ) to '~/moscow_supermarket_footpath.json';

Интересные находки

Подъезды в жилых домах в Москве часто не размечены на OpenStreetMap.

Зеленые дома - подъезд указан, "цвет-румянец" (#DE5D83) подъезды не указаны. И это еще неплохой процент разметки подъездов для столицы

Зеленые дома — подъезд указан, «цвет-румянец» (#DE5D83) подъезды не указаны. И это еще неплохой процент разметки подъездов для столицы

Можно было бы увеличить точность вычисления пешеходной дистанции к магазину, но сначала нужно разметить подъезды в исходных данных. И уже внес свою посильную помощь в геоданные Москвы по подъездам в жилых домах. Пообщался c местным сообществом мэпперов на эту тему и умные люди подсказали сайт, где желающие могут найти дома в которых не указаны подъезды и разметить их в OpenStreetMap.

В Москве для зданий иногда не хватает информации о его типе. Как заметили в коментариях к карте само здание магазина может быть учтено как жилое. В этом случае правильным решением будет изменить тип здания на building=retail и пересчитать карту. Я могу найти все такие здания и сформировать список, чтобы люди живущие в этом районе перепроверили и если здание действительно только с коммерческими объектами/магазином, обновили данные в OSM.

Исходные данные OpenStreetMap доступны каждому и по лицензии являются свободными. Не надо быть data science специалистом чтобы посчитать расстояния от магазинов до домов.

Результат геоаналитики

Готовую карту жилых домов Москвы в 500м от сетевых продуктовых магазинов вы можете посмотреть по ссылке на GitHub Gist. Где точки на карте обозначают центры жилых зданий в шаговой доступности от магазинов.

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

Случайная популярность или как работает пресса

А теперь пару слов как работают журналисты или кто-то заказал очернение этой публикации про работу с открытыми гео данными или это резюме от ChatGPT.

Помните мем: Ученые научились лечить рак?

В этом случае все произошло так же. Лента ру опубликовала статью про то что удобное не удобно. Интересно случайно или в этом есть цель обесценить мой труд? Оставлю скриншот:

И самое интересное ссылка ведет сразу на Github, избегая описания на Хабре… Сравните оригинал и новость!)


ссылка на оригинал статьи https://habr.com/ru/articles/771184/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *