Заказчик называет адрес … Как искусственный интеллект определит район, чтобы предложить интервалы доставки?

Математически задача определения района сводится к определению принадлежности точки к многоугольнику. Часто используется термин полигон – массив вершин замкнутого многоугольника. В сервисе Яндекс, например, можно прочертить многоугольник и получить полигон – координаты вершин.

На картах это может выглядеть так:

Пример полигона на карте
Пример полигона на карте

Массив координат может выглядеть так:

[

[55.5398240358901,86.06198408166499],

[55.53923998368585,86.18901350061027],

[55.498529562499215,86.23501874963371],

[55.47260090273308,86.2381086544189],

[55.48839408166666,86.14300825158683],

[55.500478393141144,86.05649091760245],

[55.5398240358901,86.06198408166499]

]

Географические координаты, соответствующие адресу, можно получить через соответствующие интернет-сервисы, например, dadata, и останется лишь определить, попадает ли данная точка в заданный многоугольник.

Принцип определения принадлежности точки полигону следующий:

  1. Запускаем из данной точки бесконечный луч по любому направлению

  2. Считаем, сколько раз луч пересек границу полигона

  3. Если пересек нечетное количество раз, то точка находится внутри полигона, если четное, в том числе 0, то точка не находится внутри полигона

  4. Рассматриваем совпадения точки с вершинами полигона и нахождение точки на границе полигона.

Алгоритм простой, доступно для любого языка программирования.

Рассмотрим данный алгоритм на js

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

Зададим координаты точки, координаты полигона и установим счетчик пересечений в начальное нулевое значение.

var point = [7,6];
var polygon = [[5,5], [10,10], [15,10], [14,6], [5,5]];
var point_counter = 0;

Будем поочередно перебирать вершины полигона и определять линии – границы полигона.

polygon.forEach(function(item_polygon, i_polygon) {
	if ( i_polygon < (polygon.length-1) ) { 
  	var polyline = [item_polygon, polygon[i_polygon + 1]];
  	}
  });

Для удобства направим бесконечный луч из точки вправо.

Пересечение с границами
Пересечение с границами

Очевидно, что если точка находится выше или ниже косой линии, то луч линию не пересекает. Также, если точка находится правее косой линии, то луч линию не пересекает.

Луч точно пересекает косую линию, если точка находится левее самой крайней слева вершины.

Если же точка находится между вершинами и по вертикали, и по горизонтали, то тут уже нужно смотреть, с какой стороны от линии она находится – слева/справа (выше/ниже). Так, если вертикальная координата точки больше, чем вертикальная координата точки на линии, соответствующей данной горизонтальной координате, то луч пересекает линию, если меньше – то не пересекает.

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

            // косая линия 
            
            if ( polyline[0][0] != polyline[1][0] & polyline[0][1] != polyline[1][1] ) {
    
                // проверка по горизонтали
                var check1 = ( point[0] - polyline[0][0] ) * ( point[0] - polyline[1][0] );
    
                // проверка по вертикали
                var check2 = ( point[1] - polyline[0][1] ) * ( point[1] - polyline[1][1] );
    
                // если попала слева по горизонтали и между по вертикали
                if ( check2 < 0 & point[0] < polyline[0][0] & point[0] < polyline[1][0]) {
                    point_counter++;                    
                    }
                    
                // если попала между по горизонтали и между по вертикали    
                if ( check1 < 0 & check2 < 0) { 
                    var delta0 = (point[0] - polyline[0][0])/(polyline[1][0]-polyline[0][0]);
                    var coord1 = (polyline[1][1]- polyline[0][1]) * delta0 + polyline[0][1];   
                    
                    // проверка нахождения с нужной стороны от косой линии
                    var check3 = ( coord1 - point[1]) * ( polyline[1][1] - polyline[0][1] );
                    
                    // если попала с нужной стороны
                    if ( check3 < 0) {
                        point_counter++;
                        }                        
                    } 
                }

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

            // перпендикулярная вертикальная линия   
            
            if ( polyline[0][0] == polyline[1][0] ) {
    
                // проверка по вертикали
                var check = ( point[1] - polyline[0][1] ) * ( point[1] - polyline[1][1] );
                
                // если попала слева от линии по горизонтали и между по вертикали  
                if ( point[0] < polyline[0][0] & check < 0 ) { 
                    point_counter++;
                    }
                }

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

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

Пересечение с вершинами
Пересечение с вершинами
            // проверяем пересечение с вершиной
            
            // если точка левее вершины по горизонтали и на уровне по по горизонтали
            if ( point[1] == item_polygon[1] && point[0] < item_polygon[0] ) {
                
                //проверяем, точка внутри угла или снаружи
                 if ( i_polygon > 0) { 
                     // произведение разниц вертикальных координат точки и соседених вершин
                     var check = (point[1] - polygon[i_polygon-1][1]) * (point[1] - polygon[i_polygon+1][1]);  
                     }
                if ( i_polygon == 0) { 
                     // произведение разниц вертикальных координат точки и соседених вершин
                    var check = (point[1] - polygon[polygon.length-2][1]) * (point[1] - polygon[i_polygon+1][1]);  
                    }
    
                // если точка находится между по вертикали
                if (check < 0) {
                    point_counter++;
                    }
    
                }

Для удобства можно обернуть подсчет в единую функцию и остается только проверить на четность.

var polygon = [[5,5], [10,10], [15,10], [14,6], [5,5]];
var point = [7,6];
var point_counter = 0;

point_counter = Counter(point,polygon);

if ( point_counter % 2 == 0 ) {
    // вывод или сохранение переменной
    console.log('точка: ' + point + ' находится не внутри полигона'); 
    }
else {
    // вывод или сохранение переменной
    console.log('точка: ' + point + ' находится внутри полигона'); 
    }

function Counter(point,polygon) {
    polygon.forEach(function(item_polygon, i_polygon) {
        if ( i_polygon < (polygon.length-1) ) { 
            var polyline = [item_polygon, polygon[i_polygon + 1]]; 
            if ( polyline[0][0] != polyline[1][0] & polyline[0][1] != polyline[1][1] ) {
                var check1 = ( point[0] - polyline[0][0] ) * ( point[0] - polyline[1][0] );
                var check2 = ( point[1] - polyline[0][1] ) * ( point[1] - polyline[1][1] );
                if ( check2 < 0 & point[0] < polyline[0][0] & point[0] < polyline[1][0]) {
                    point_counter++;                    
                    }
                if ( check1 < 0 & check2 < 0) { 
                    var delta0 = (point[0] - polyline[0][0])/(polyline[1][0]-polyline[0][0]);
                    var coord1 = (polyline[1][1]- polyline[0][1]) * delta0 + polyline[0][1];   
                    var check3 = ( coord1 - point[1]) * ( polyline[1][1] - polyline[0][1] );
                    if ( check3 < 0) {
                        point_counter++;
                        }
                    }
                }
            if ( polyline[0][0] == polyline[1][0] ) {
                var check = ( point[1] - polyline[0][1] ) * ( point[1] - polyline[1][1] );
                if ( point[0] < polyline[0][0] & check < 0 ) { 
                    point_counter++;
                    }
                }
            if ( point[1] == item_polygon[1] && point[0] < item_polygon[0] ) {
                 if ( i_polygon > 0) { 
                     var check = (point[1] - polygon[i_polygon-1][1]) * (point[1] - polygon[i_polygon+1][1]);  
                     }
                if ( i_polygon == 0) { 
                    var check = (point[1] - polygon[polygon.length-2][1]) * (point[1] - polygon[i_polygon+1][1]);  
                    }
                if (check < 0) {
                    point_counter++;
                    }
                }
            }  
        });
    return point_counter;
    }

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

Комментарии (21)


  1. artemlight
    19.06.2022 14:44
    +10

    Тянет на олимпиадную задачку для средней школы (причем даже не старшие классы), но никак не на искусственный интеллект :)


    1. AnatolyBelov Автор
      19.06.2022 15:10
      -2

      Спасибо за комментарий :)

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

      В моем представлении Искусственный Интеллект - это не вопрос сложности, а вопрос применения.
      Сейчас вся базовая математическая модель нейронных сетей - старший класс школы с математическим уклоном. Перебор, суммирование, пара производных.
      Создание и обучение нейронных сетей на python также старший класс.
      Многие ИИ в принципе используют одну/две функцию - минимакс да уменьшение потерь.
      ИИ уже давно перестал быть сложностью, сейчас это вопрос практического применения.
      Цель статьи - показать не сложность, а как раз простой пример, как боты/роботы/ИИ решают обычные ежедневные задачи.


      1. atri24
        20.06.2022 09:23

        Извечный вопрос:
        - Обладает ли бачок унитаза интеллектом?

        Все таки интеллект подразумевает обучение и приспосабливаемость, а не только следование алгоритму.


  1. GennPen
    19.06.2022 14:47
    +5

    Так и не понял, причем тут упомянутый ИИ, когда все математически делается.


    1. AnatolyBelov Автор
      19.06.2022 15:19
      -4

      Спасибо за комментарий )

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


      1. GennPen
        19.06.2022 15:28
        +6

        ИИ - это в первую очередь обучаемость. Заложенный алгоритм вы не сможете пере-/до-обучить, только переписать заново. В случае с нейронными сетями - достаточно подкорректировать их коэффициенты с обученной модели.


      1. ganqqwerty
        19.06.2022 22:30

        Дык а нейронка где?


  1. Sild
    19.06.2022 14:53
    +16

    Ждем "как искусственный интеллект определяет вхождение подстроки в строку" =)


  1. nerudo
    19.06.2022 14:54
    +18

    Мало кто знает, что Искуственный Интеллект по английски называется AI, что расшифровывается как Advanced If.


  1. deadmoroz14
    19.06.2022 14:56
    +3

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

    Заказчик называет адрес … Как искусственный интеллект определит район, чтобы предложить интервалы доставки?

    Почему надо определять именно район? Модели доставки бывают разные, и не обязательно делать разделение по районам. Логистику можно по-разному разрешать

    PS: Я бы вообще не кодом проверял вхождение в полигон, а через PostGIS. Всё уже сделано до нас


    1. AnatolyBelov Автор
      19.06.2022 15:29
      -2

      Спасибо за комментарий )

      Была конкретная задача - определять район по названному адресу, причем не как на публичных картах, а как районы распределяет Поставщик с точки зрения своей логистики. В каждый район - свои дни и интервалы доставки. То есть это не стандартный Центральный, или Петропавловский, а Центр 1, Центр 2, Дальний правый и так далее.
      Не нашли, как в данном случае применить известные интернет-сервисы, так и дошли до полигонов.


      1. deadmoroz14
        20.06.2022 09:59
        +1

        Спасибо
        Вот эту бы информацию сразу в статью)

        А PostGIS это расширение к PostgreSQL. Надо только данные правильно сформировать и в БД положить. И можно считать вхождение в полигон без лишней мороки


        1. AnatolyBelov Автор
          20.06.2022 13:42

          Точно.
          Если вначале статьи описать данную задачу, а статью назвать примерно "Как определить район доставки по заданным границам Заказчика", то смысл статьи сильно бы поменялся - вот есть такая задача, решаем таким способом.
          Спасибо )


  1. Aquahawk
    19.06.2022 15:01
    +3

    А потом приходит 1000 регионов, нагрузка, и девопс который арендует 100 машин в амазоне, отличная же задача для serverless подхода. А всего-то надо было применить какой-нибудь пространственный индекс типа AABB и всё стало бы в 100 раз быстрее.


    1. AnatolyBelov Автор
      19.06.2022 15:38
      -1

      Спасибо за комментарий )

      Да, эксплуатация под хорошей нагрузкой - это вопрос.
      Дойдем до 1000 - посмотрим )


  1. Vsevo10d
    19.06.2022 15:41
    +1

    Ага, а теперь добро пожаловать с таким алгоритмом в Москву, например, где до открытия развязки с ул. Подольских курсантов из ЮАО в ЮВАО можно было попасть через третье кольцо, Нахимовский или МКАД. А через пересекающие город ЖД ветки проще летать на джетпаке - время на его разработку с лихвой окупит времязатраты на простой в пробках через полтора моста.

    А так-то на карте все рядом, да. Только связности в городе никакой. Нам на работу в какой-то момент деливери возить отказывался, а по другую сторону улицы привозил - видимо, они решили, что светофор слишком долгий и проще не возить вообще, чем систематически вылезать за гарантированное время доставки.


    1. AnatolyBelov Автор
      19.06.2022 16:04
      +1

      Правильно!
      Именно поэтому с точки зрения доставки город делится не на "административные" районы, а на "транспортные". Поставщик уже знает, что на карте это через дорогу, а на машине по факту это через полтора часа и развязку. Поэтому поставщик и рисует на карте свои полигоны, не имеющие никакого отношения к административному делению.


  1. Materializator
    19.06.2022 15:57

    Второе предложение не связано с третьим. Из (не всегда связного, однозначного, верного) текстового представления адреса со слов заказчика получить точку на карте - грубое допущение.


    1. AnatolyBelov Автор
      19.06.2022 16:16
      -1

      Действительно, получить верный адрес со слов Заказчика удается не всегда. Это уже вопрос вероятности и уменьшения ошибки.
      Заказчик может не произносить улицу, а только слово, например, Шахтеров 54. А в городе может быть и улица, и проспект, и площадь, и проезд.
      Может, в принципе, оговориться или запутаться. Например, "Шахтеров 54, а нет лучше Шахтеров 58".
      Были случаи, когда робот раскладывает адрес совершенно неожиданно. Например, улица Яна Фабрициуса раскладывалось как "я на", а с улицей Шестака было даже несколько вариантов чего угодно, только не Шестака.
      Такие случаи можно и нужно отслеживать и постепенно находить решения.

      Возможные решения:
      1. если робот не уверен в правильности принятия адреса - переспросить
      2. если робот не уверен, а уже был заказ с этого номера телефона или с этого устройства на похожий адрес - переспросить с предложением похожего адреса из прошлых заказов
      3. если с этого номера или этого устройства уже несколько заказов подряд идут на один и тот же адрес - сразу спрашивать про этот адрес доставки
      4. вести собственную пополняемую базу таких "неуверенных адресов" и свою расшифровку к ним


  1. KorP
    19.06.2022 16:04
    +7


  1. CagoBHuK
    21.06.2022 10:52

    Кривизну глобуса не учитываете. Возьмите полигон в несколько десятков градусов - будет удивительный результат.