Доставка последней мили является самой дорогостоящей частью любой цепочки поставок. Отраслевые ориентиры неизменно указывают на то, что затраты на последнюю милю составляют 53% от общей стоимости доставки. Из этого объёма наибольшей управляемой переменной является эффективность маршрута. Водитель, выполняющий 15 остановок в неправильном порядке, может проехать на 40% больше километров, чем необходимо, сжигая топливо, изнашивая транспортное средство и пропуская временные окна доставки, что влечёт штрафы за повторную доставку.
Оптимизация маршрутов больше не является сложной задачей для программирования. То, что раньше требовало дорогостоящего специализированного логистического программного обеспечения, теперь доступно через вызов API. В этом руководстве создаётся полный оптимизатор маршрутов с несколькими остановками с использованием MapAtlas Routing API: скрипт Python, который отправляет список пунктов доставки, получает обратно оптимизированную последовательность с общим расстоянием и временем, применяет ограничения временных окон и обрабатывает ограничения Зон с низким уровнем выбросов (LEZ) в ЕС для городских доставок. Затем фрагмент JavaScript отображает результат на карте.
Реализация на Python занимает менее 55 строк. Отображение на карте JavaScript добавляет ещё 30.
Проблема стоимости последней мили
Чтобы понять, что реально экономит оптимизация, рассмотрим цифры для реалистичного сценария доставки:
- Автопарк: 10 фургонов
- Остановок на фургон в день: 18
- Текущее среднее расстояние: 210 км/фургон/день
- Стоимость топлива: €0,38/км (дизель, среднее по ЕС)
- Стоимость водителя: €22/час
- Текущее среднее время маршрута: 7,5 часов/день
Текущие дневные расходы на фургон: (210 × €0,38) + (7,5 × €22) = €79,80 + €165 = €244,80/фургон/день
Снижение расстояния на 30% (достижимо при хорошей оптимизации в плотной городской сети) и экономия времени на 20% даёт:
- Оптимизированное расстояние: 147 км → стоимость топлива: €55,86
- Оптимизированное время: 6 часов → стоимость водителя: €132
- Оптимизированные дневные расходы на фургон: €187,86/фургон/день
Экономия на фургон в день: €56,94. Для 10 фургонов за 250 рабочих дней: €142 350/год за счёт одной интеграции API.
Приведённые выше ориентиры отражают реальные опубликованные данные из исследований логистики последней мили. Ваши конкретные цифры будут варьироваться в зависимости от географии, типа транспортного средства и плотности остановок. В плотных городских районах наибольший выигрыш достигается потому, что наивные последовательные маршруты теряют больше всего расстояния из-за ненужного петляния.
Наивные vs оптимизированные маршруты: визуальное сравнение
Разница между наивным (последовательным) маршрутом и оптимизированным наглядно видна на карте.
Наивная маршрутизация происходит, когда вы подаёте остановки в том порядке, в котором они были введены: первый клиент, сделавший заказ, — первая остановка в маршруте, независимо от географии. В таком городе, как Амстердам или Берлин, это создаёт проблему «макаронного маршрута»: водитель постоянно пересекает собственный путь.
Оптимизация решает Задачу коммивояжёра (TSP) для вашего набора остановок. Для 15–20 остановок это вычислительно выполнимо за миллисекунды. Для более крупных автопарков с сотнями остановок решатели задачи маршрутизации транспортных средств (VRP) обрабатывают дополнительные ограничения нескольких транспортных средств и вместимости.
Шаг 1: Структурируйте данные доставки
Каждая остановка нуждается в местоположении и, для доставок с временными окнами, в time_window, указывающем, когда доставка приемлема.
import requests
import json
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.mapatlas.eu/v1"
# Депо (начальная и конечная точка)
depot = {
"lat": 52.3402,
"lng": 4.8952,
"name": "Склад - Слотердейк"
}
# Пункты доставки с необязательными временными окнами
stops = [
{ "lat": 52.3726, "lng": 4.8971, "name": "Albert Heijn Jordaan",
"time_window": { "start": "09:00", "end": "12:00" } },
{ "lat": 52.3601, "lng": 4.9123, "name": "Café De Jaren",
"time_window": { "start": "08:00", "end": "11:00" } },
{ "lat": 52.3780, "lng": 4.8801, "name": "Westergasfabriek Events",
"time_window": { "start": "10:00", "end": "14:00" } },
{ "lat": 52.3545, "lng": 4.9041, "name": "Hotel V Nesplein",
"time_window": None },
{ "lat": 52.3620, "lng": 4.8820, "name": "Vondelpark Paviljoen",
"time_window": { "start": "07:00", "end": "10:00" } }
]
Шаг 2: Вызовите эндпоинт оптимизации маршрута
Отправьте депо и список остановок на эндпоинт оптимизированной маршрутизации методом POST. API возвращает остановки в наиболее эффективном порядке посещения вместе с общим расстоянием и длительностью маршрута.
def optimise_route(depot, stops, vehicle_profile="van-euro6"):
"""
Запрашивает оптимизированный многоостановочный маршрут из MapAtlas Routing API.
Параметры vehicle_profile: van-euro6, van-diesel-euro5, electric-van, bike
"""
waypoints = [
{
"lat": s["lat"],
"lng": s["lng"],
"name": s["name"],
**({("time_window": s["time_window"]} if s.get("time_window") else {})
}
for s in stops
]
payload = {
"origin": { "lat": depot["lat"], "lng": depot["lng"] },
"destination": { "lat": depot["lat"], "lng": depot["lng"] }, # возврат в депо
"waypoints": waypoints,
"optimise": True,
"vehicle_profile": vehicle_profile,
"avoid_low_emission_zones": True # автоматически избегает LEZ для несоответствующих профилей
}
response = requests.post(
f"{BASE_URL}/routing/optimise",
json=payload,
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
)
response.raise_for_status()
return response.json()
result = optimise_route(depot, stops)
Шаг 3: Разберите и отобразите оптимизированный маршрут
Ответ API включает остановки в оптимизированном порядке, накопленное расчётное время прибытия для каждой остановки, общее расстояние и общую длительность.
def display_route_summary(result):
route = result["route"]
print(f"\n--- Сводка оптимизированного маршрута ---")
print(f"Общее расстояние : {route['total_distance_km']:.1f} км")
print(f"Общая длительность: {route['total_duration_min']:.0f} мин")
print(f"Остановок : {len(route['waypoints'])}\n")
print(f" НАЧАЛО {depot['name']}")
for i, stop in enumerate(route["waypoints"], 1):
eta = stop["eta"]
tw = stop.get("time_window")
on_time = "(вовремя)" if tw and tw["start"] <= eta <= tw["end"] else ""
print(f" {i:>2}. {stop['name']:<35} РПВ {eta} {on_time}")
print(f" КОНЕЦ {depot['name']}")
print(f"\nОценочная экономия топлива vs последовательный: "
f"{result.get('saving_vs_naive_km', 0):.1f} км "
f"({result.get('saving_pct', 0):.0f}%)")
display_route_summary(result)
Пример вывода для пяти остановок выше:
--- Сводка оптимизированного маршрута ---
Общее расстояние : 38.4 км
Общая длительность: 94 мин
Остановок : 5
НАЧАЛО Склад - Слотердейк
1. Vondelpark Paviljoen РПВ 07:48 (вовремя)
2. Café De Jaren РПВ 08:31 (вовремя)
3. Albert Heijn Jordaan РПВ 09:15 (вовремя)
4. Hotel V Nesplein РПВ 10:02
5. Westergasfabriek Events РПВ 10:44 (вовремя)
КОНЕЦ Склад - Слотердейк
Оценочная экономия топлива vs последовательный: 14.2 км (27%)
Шаг 4: Обработка Зон с низким уровнем выбросов в ЕС
Зона ZTL в Амстердаме, система Crit'Air в Париже и Umweltzone в Берлине ограничивают въезд определённых типов транспортных средств в центральные районы в указанное время. Маршрут, который выглядит эффективным по расстоянию, может оказаться недействительным для вашего транспортного средства.
Параметр avoid_low_emission_zones: true в сочетании с vehicle_profile автоматически прокладывает маршрут в обход ограниченных зон для несоответствующих транспортных средств. Для электрических и автомобилей Евро 6 зоны LEZ проходимы, и параметр не оказывает никакого эффекта.
# Пример: дизельный фургон Евро 5, маршрут будет проложен в обход ZTL Амстердама
result_euro5 = optimise_route(depot, stops, vehicle_profile="van-diesel-euro5")
# Пример: электрический фургон, ограничения LEZ не применяются
result_electric = optimise_route(depot, stops, vehicle_profile="electric-van")
print(f"Расстояние маршрута Евро 5 : {result_euro5['route']['total_distance_km']:.1f} км")
print(f"Расстояние маршрута электро: {result_electric['route']['total_distance_km']:.1f} км")
# Маршрут электромобиля, как правило, будет короче, так как он может использовать дороги в зонах LEZ
Для логистических операций, планирующих переход с дизеля на электромобили, сравнение этих двух результатов по каждому маршруту обеспечивает прямое количественное определение улучшения дальности, доступного при электрификации.
Шаг 5: Отображение оптимизированного маршрута на карте
Возьмите геометрию маршрута из ответа API и отобразите её как линейный слой в JavaScript.
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
// routeResult — это разобранный JSON ответа API, переданный во фронтенд
function renderOptimisedRoute(map, routeResult) {
const { waypoints, geometry, total_distance_km, total_duration_min } = routeResult.route;
map.on('load', () => {
// Линия маршрута
map.addSource('optimised-route', { type: 'geojson', data: { type: 'Feature', geometry } });
map.addLayer({
id: 'route-line',
type: 'line',
source: 'optimised-route',
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: { 'line-color': '#2563EB', 'line-width': 4 }
});
// Маркеры остановок с порядковыми номерами
waypoints.forEach((stop, i) => {
const el = document.createElement('div');
el.textContent = i + 1;
el.style.cssText = `
width:28px;height:28px;border-radius:50%;background:#2563EB;color:#fff;
display:flex;align-items:center;justify-content:center;font-weight:700;
font-size:13px;border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.3)
`;
new mapmetricsgl.Marker({ element: el })
.setLngLat([stop.lng, stop.lat])
.setPopup(
new mapmetricsgl.Popup().setHTML(`
<strong>${i + 1}. ${stop.name}</strong>
<p>РПВ: ${stop.eta}</p>
`)
)
.addTo(map);
});
// Подгонка карты под границы маршрута
const coords = geometry.coordinates;
const bounds = coords.reduce(
(b, c) => b.extend(c),
new mapmetricsgl.LngLatBounds(coords[0], coords[0])
);
map.fitBounds(bounds, { padding: 48 });
// Панель сводки
document.getElementById('route-summary').innerHTML = `
<strong>${total_distance_km.toFixed(1)} км</strong> ·
<strong>${total_duration_min.toFixed(0)} мин</strong> ·
${waypoints.length} остановок
`;
});
}
const map = new mapmetricsgl.Map({
container: 'route-map',
style: 'https://tiles.mapatlas.eu/styles/basic/style.json?key=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 12
});
renderOptimisedRoute(map, routeResult);
Расчёт реальной экономии
Когда ответ API у вас на руках, расчёт экономии прост. Поле saving_vs_naive_km в ответе напрямую даёт сэкономленное расстояние. Из него выводите экономию затрат:
def calculate_savings(result, fuel_cost_per_km=0.38, driver_cost_per_hour=22.0,
days_per_year=250, fleet_size=10):
saving_km = result.get("saving_vs_naive_km", 0)
saving_hours = saving_km / 50 # предполагаем среднюю скорость 50 км/ч
daily_fuel_saving = saving_km * fuel_cost_per_km
daily_driver_saving = saving_hours * driver_cost_per_hour
daily_total = daily_fuel_saving + daily_driver_saving
annual_fleet_saving = daily_total * days_per_year * fleet_size
print(f"Сэкономленное расстояние на маршрут : {saving_km:.1f} км")
print(f"Сэкономленное время на маршрут : {saving_hours * 60:.0f} мин")
print(f"Дневная экономия (1 ТС) : €{daily_total:.2f}")
print(f"Годовая экономия ({fleet_size} ТС) : €{annual_fleet_saving:,.0f}")
calculate_savings(result)
Оптимизация временных окон
Доставка в пекарню в 06:00 и в ресторан в 14:00 при минимизации общего расстояния маршрута — это задача с ограничениями оптимизации. API обрабатывает это автоматически, вам нужно только предоставить временные окна:
# Срочные остановки, API запланирует их в пределах указанных временных окон
stops_with_windows = [
{ "lat": 52.3726, "lng": 4.8971, "name": "Пекарня",
"time_window": { "start": "05:30", "end": "07:00" } },
{ "lat": 52.3620, "lng": 4.8820, "name": "Кафе",
"time_window": { "start": "07:00", "end": "09:00" } },
{ "lat": 52.3545, "lng": 4.9041, "name": "Ресторан",
"time_window": { "start": "13:00", "end": "15:00" } }
]
Если какое-либо ограничение временного окна не может быть выполнено с учётом времени отправления из депо и текущей модели трафика, API возвращает массив constraint_violations, перечисляющий остановки, к которым не удалось успеть вовремя. Ваше диспетчерское программное обеспечение может затем предупредить водителя или предложить более раннее отправление.
Что строить поверх этого
Оптимизация маршрутов является основой. После её запуска естественными расширениями являются:
- Отслеживание водителя в реальном времени: передайте координаты оптимизированного маршрута в руководство по карте отслеживания водителя в реальном времени и показывайте клиентам обновления РПВ в режиме реального времени.
- Планирование охвата на основе изохрон: используйте Travel Time API для визуализации почтовых индексов, которые ваш автопарк может достичь в пределах вашего окна доставки. В статье Isochrone Maps Explained показано, как это сделать.
- Массовая проверка адресов: перед запуском оптимизации проверьте все адреса доставки с помощью Geocoding API для выявления опечаток и устаревших почтовых индексов. Смотрите How to Use the Geocoding API to Validate 10,000 Addresses in Bulk.
На странице отрасли «Логистика и доставка» и странице отрасли «Управление автопарком» описаны дополнительные возможности MapAtlas, актуальные для диспетчерского программного обеспечения, включая VRP с несколькими транспортными средствами и оптимизацию возврата в депо.
Начало работы
- Зарегистрируйтесь для получения бесплатного API-ключа MapAtlas, бесплатный тариф включает вызовы маршрутизации и оптимизации, кредитная карта не требуется
- Ознакомьтесь с документацией Routing API для полного списка профилей транспортных средств, параметров временных окон и опций с несколькими транспортными средствами
- Изучите страницу возможностей планирования маршрутов и навигации для обзора продукта
Часто задаваемые вопросы
Как оптимизация маршрутов снижает затраты на доставку?
Оптимизация маршрутов переупорядочивает последовательности многоостановочной доставки для минимизации общего расстояния и времени в пути. Исследования неизменно показывают снижение пройденного расстояния на 20–35% по сравнению с наивным последовательным маршрутом. Для транспортного средства, проезжающего 200 км/день при стоимости топлива €0,35/км, снижение на 30% экономит около €21 на транспортное средство в день, примерно €5 000 в год на транспортное средство.
Что такое временные окна в оптимизации маршрутов?
Временные окна — это ограничения доставки, требующие посещения остановки в указанный временной диапазон, например, предприятие, принимающее доставки только с 09:00 до 12:00. Оптимизатор должен соблюдать все временные окна, при этом минимизируя общее расстояние маршрута, что является значительно более сложной вычислительной задачей, чем безусловная оптимизация.
Обрабатывает ли MapAtlas Routing API Зоны с низким уровнем выбросов в ЕС?
Да. MapAtlas Routing API включает данные об ограничениях дорожного движения для Зон с низким уровнем выбросов ЕС, включая зону ZTL Амстердама, зону Crit'Air в Париже и Umweltzone в Берлине. Передайте профиль транспортного средства (дизель Евро 5, бензин, электро) в качестве параметра, и маршрутизатор автоматически будет избегать ограниченных зон для несоответствующих транспортных средств.

