Rozwiązywanie problemu komiwojażera
Zoptymalizuj trasy dostarczania paczek za pomocą Pythona, aby obniżyć koszty dostawy (Przypadek studyjny: dostawa paczek w Bandung w Indonezji z prostymi danymi)
Problem komiwojażera lub TSP to kombinatoryczny problem optymalizacji, który próbuje odpowiedzieć na pytanie: „Biorąc pod uwagę listę miast i odległości między każdą parą miast, jaka jest najkrótsza możliwa trasa, która odwiedza każde miasto dokładnie raz i wraca do miasta początkowego? ”. TSP w prosty sposób ma za zadanie znaleźć najkrótszą trasę między zestawem punktów, które należy odwiedzić. Staje się prawdziwym problemem w łańcuchu dostaw i branży logistycznej, ponieważ gwałtownie wzrosły one po wzroście handlu elektronicznego.
Dlaczego więc TSP jest ważne? Zasadniczo pomaga firmie uzyskać większy zysk dzięki oszczędności czasu i zasobów. W branży logistycznej jednym ze sposobów generowania większych zysków jest oszczędność kosztów i czasu dostaw. Można to osiągnąć na różne sposoby, jednym z nich jest rozwiązanie problemu komiwojażera (TSP), w którym próbujesz znaleźć najbardziej efektywne trasy do pokonania na drodze. Im skuteczniej jest to rozwiązywane, tym mniej czasu spędza się na prowadzeniu pojazdu, tym więcej oszczędza się paliwa, tym mniej godzin potrzeba, a nawet więcej paczek jest dostarczanych . W rezultacie bilans każdej działalności biznesowej opartej na dostawach odniesie podwójną korzyść.
Dlatego próbuję stworzyć demo tego problemu komiwojażera dla kuriera firmy dostarczającej paczki w Bandung, aby obniżyć koszty dostawy .
Uwaga : ponieważ jest to prosta wersja demonstracyjna, użyte dane to tylko 8 fałszywych punktów dostawy, a dane są generowane losowo.
Scenariusz
Jesteś kierownikiem w centrum obsługi paczek w Bandung. Powinieneś dostarczać paczki do niektórych klientów ze swojego magazynu 1 pojazdem. Rozmieszczenie punktów dostaw i magazynu można zobaczyć poniżej (Rysunek 1).

**W każdym razie możesz dostosować liczbę pojazdów i liczbę punktów dostawy według własnego uznania, edytując poniższy kod źródłowy
Cele
Znajdź najlepsze trasy, aby dostarczyć wszystkie paczki do klientów.
Aby śledzić, jak dobry jest model lub rozwiązanie, które próbujemy wdrożyć, lepiej zdefiniować metryki biznesowe. Ponieważ dane dotyczące powiązanych przypadków nie są dostępne, wspomnę tylko o potencjale bez mierzenia, jak dobre jest rozwiązanie. Te potencjalne wskaźniki biznesowe to:
- Koszty dostawy
- Czas spędzony
- Szybkość dostawy na czas
- Wynik satysfakcji klientów (wynik CSAT)
Korzystając z Google OR-Tools , najlepsza trasa z jednym pojazdem to:
Zajezdnia -> Adres 5 -> Adres 6 -> Adres 7 -> Adres 4 -> Adres 3 -> Adres 2 -> Adres 1 -> Zajezdnia
Dystans trasy: 7,44 km

Kroki techniczne

Pełny kod znajduje się tutaj .
I. Importuj bibliotekę i czytaj dane
Przede wszystkim musimy zaimportować biblioteki do Google Colab. Będziemy korzystać z niektórych bibliotek.
- numpy i pandy : do operacji numerycznych i manipulacji danymi
- itertools : do tworzenia iteratorów dla wydajnego zapętlania
- ast : do przetwarzania drzew gramatyki składni abstrakcyjnej Pythona
- żądania : do wysyłania żądań HTTP
- json : do pracy z danymi JSON
- networkx : do tworzenia, manipulowania i badania struktury, dynamiki i funkcji złożonych sieci
- osmnx : do pobierania danych geoprzestrzennych z OpenStreetMap oraz modelowania, projektowania, wizualizacji i analizowania rzeczywistych sieci ulic i wszelkich innych geometrii geoprzestrzennych
- folium : do wizualizacji danych, którymi manipulowano w Pythonie, na interaktywnej mapie ulotek
- ortools : do rozwiązywania kombinatorycznych problemów optymalizacyjnych
# Install and Import Library
pip install osmnx
pip install ortools
import numpy as np
import pandas as pd
import itertools
import ast
import requests
import json
import networkx as nx
import osmnx as ox
import folium
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
from google.colab import drive
drive.mount('/content/drive')
import warnings
warnings.filterwarnings('ignore')
# Read data
df_points = pd.read_excel('/content/drive/MyDrive/dataset/delivery_destinations.xlsx')

II. Geokodowanie
Geokodowanie to proces przekształcania opisu lokalizacji — takiego jak para współrzędnych, adres lub nazwa miejsca — w lokalizację na powierzchni ziemi. Krótko mówiąc, próbujemy pobrać szerokość i długość geograficzną z adresu zestawu danych. Ta szerokość i długość geograficzna zostaną później wykorzystane do obliczenia odległości podróży między punktami i określenia najlepszej trasy dostawy.
Używam HERE API do Geokodowania, biorąc pod uwagę, że jest dość dokładny i darmowy (z ograniczoną liczbą połączeń). Możesz przeczytać ten artykuł , aby poznać szczegóły tego procesu.
# Define variables
apikey = '<YOUR-API-KEY-HERE>'
url = 'https://discover.search.hereapi.com/v1/discover'
headers = {'Content-Type': 'application/json'}
origin_point = '-6.8918336,107.6103212'
latlong_origin = []
latlong_points = []
# Get lat long using HERE Map API
for i, data in df_points.iterrows():
# your place name is defined below
place_name = data.address
my_params = {'at' : origin_point,
'limit' : 1,
'apikey' : apikey,
'q' : place_name}
r = requests.get(url, params=my_params, headers=headers)
output = json.loads(r.text)
latlong_dict = output['items'][0]['position']
latlong = ', '.join(map(str, latlong_dict.values()))
latlong_points += [latlong]
# Create dataframe
df_points['lat_lng'] = latlong_points
df_points['lat_lng'] = df_points.lat_lng.apply(ast.literal_eval)

Zauważ, że teraz mamy pole o nazwie lat_lng , które zawiera szerokość i długość geograficzną punktów.
III. Oblicz odległość podróży między punktami
Następnym krokiem jest obliczenie odległości podróży między punktami. Jest to ważne w problemie komiwojażera, ponieważ najkrótsza trasa jest generowana z całkowitej odległości pobranej z każdej możliwej trasy. Zamiast używać odległości euklidesowej (rysowanie linii między punktami), użyjemy bardziej odpowiedniej odległości, która tworzy ścieżkę wzdłuż drogi między punktami. Ta ścieżka będzie reprezentować odległość między punktami.
W tym celu wykorzystamy metodę shortest_path z networkx. Ale zanim to nastąpi, powinniśmy stworzyć DataFrame początek-cel, aby ułatwić obliczanie odległości między punktami. Ta ramka danych jest generowana przez wykonanie wszystkich możliwych kombinacji/permutacji punktów początkowych i docelowych.
# Create dataframe of origin-destination #
# Create variables
list_points_name = df_points['point_name']
# list_points_latlong = df_points['lat_long']
list_points_latlong = df_points['lat_lng']
# All points combinations
list_pair_name = [i for i in itertools.product(list_points_name, repeat=2)]
list_pair_latlong = [i for i in itertools.product(list_points_latlong, repeat=2)]
df_nodes = pd.DataFrame({'pair_name': list_pair_name, 'pair_latlong': list_pair_latlong})
# Source/Target
df_nodes['origin'] = df_nodes['pair_name'].apply(lambda t: t[0])
df_nodes['destination'] = df_nodes['pair_name'].apply(lambda t: t[1])
df_nodes['latlong_origin'] = df_nodes['pair_latlong'].apply(lambda t: t[0])
df_nodes['latlong_destination'] = df_nodes['pair_latlong'].apply(lambda t: t[1])
df_nodes.drop(['pair_name', 'pair_latlong'], axis =1, inplace = True)

Teraz możemy obliczyć odległość między punktami za pomocą siecix.
# Calculate distance from point to point using networkx #
# Location where you want to find your route
place = 'Bandung, West Java, Indonesia'
# Find shortest route based on the mode of travel
mode = 'drive' # 'drive', 'bike', 'walk'
# Find shortest path based on distance or time
optimizer = 'length' # 'length','time'
# Create graph from OSM within the boundaries of some geocodable place(s)
graph = ox.graph_from_place(place, network_type = mode)
# Create lists
list_route = []
list_route_length = []
# Determining distance and route
for i in range(len(df_nodes)):
# find the nearest node to the start location
orig_node = ox.get_nearest_node(graph, df_nodes.iloc[i]['latlong_origin'])
# find the nearest node to the end location
dest_node = ox.get_nearest_node(graph, df_nodes.iloc[i]['latlong_destination'])
# find the shortest path
shortest_route = nx.shortest_path(graph,
orig_node,
dest_node,
weight=optimizer)
shortest_route_length = nx.shortest_path_length(graph, orig_node, dest_node, weight=optimizer)
# append list
list_route.append(shortest_route)
list_route_length.append(shortest_route_length)

IV. Określ najlepszą trasę pojazdu
Następnie możemy określić najlepszą trasę doręczenia przesyłek do wszystkich klientów za pomocą Google OR-Tools . Najpierw musimy zdefiniować funkcje, które pomogą nam znaleźć najkrótszą trasę. Odnosi się do tej strony z pewnymi modyfikacjami.
def create_data_model(distance_matrix, num_vehicles, depot_order):
"""Stores the data for the problem."""
data = {}
data['distance_matrix'] = distance_matrix
data['num_vehicles'] = num_vehicles
data['depot'] = depot_order
return data
def distance_callback(from_index, to_index):
"""Returns the distance between the two nodes."""
# Convert from routing variable Index to distance matrix NodeIndex.
from_node = manager.IndexToNode(from_index)
to_node = manager.IndexToNode(to_index)
return data['distance_matrix'][from_node][to_node]
def print_solution(data, manager, routing, solution):
"""Prints solution on console."""
print(f'Objective: {solution.ObjectiveValue()}')
max_route_distance = 0
for vehicle_id in range(data['num_vehicles']):
index = routing.Start(vehicle_id)
plan_output = 'Route for vehicle {}:\n'.format(vehicle_id)
route_distance = 0
while not routing.IsEnd(index):
plan_output += ' {} -> '.format(manager.IndexToNode(index))
previous_index = index
index = solution.Value(routing.NextVar(index))
route_distance += routing.GetArcCostForVehicle(
previous_index, index, vehicle_id)
plan_output += '{}\n'.format(manager.IndexToNode(index))
plan_output += 'Distance of the route: {}m\n'.format(route_distance)
print(plan_output)
max_route_distance = max(route_distance, max_route_distance)
print('Maximum of the route distances: {} m'.format(max_route_distance))
def get_routes(solution, routing, manager):
"""Get vehicle routes from a solution and store them in an array."""
# Get vehicle routes and store them in a two dimensional array whose
# i,j entry is the jth location visited by vehicle i along its route.
routes = []
for route_nbr in range(routing.vehicles()):
index = routing.Start(route_nbr)
route = [manager.IndexToNode(index)]
while not routing.IsEnd(index):
index = solution.Value(routing.NextVar(index))
route.append(manager.IndexToNode(index))
routes.append(route)
return routes
# Instantiate the data problem.
data = create_data_model(distance_matrix=distance_matrix,
num_vehicles=1,
depot_order=0)
# Create the routing index manager.
manager = pywrapcp.RoutingIndexManager(len(data['distance_matrix']),
data['num_vehicles'], data['depot'])
# Create Routing Model.
routing = pywrapcp.RoutingModel(manager)
# Create and register a transit callback.
transit_callback_index = routing.RegisterTransitCallback(distance_callback)
# Define cost of each arc.
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
# Add Distance constraint.
dimension_name = 'Distance'
routing.AddDimension(
transit_callback_index,
0, # no slack
50000, # vehicle maximum travel distance
True, # start cumul to zero
dimension_name)
distance_dimension = routing.GetDimensionOrDie(dimension_name)
distance_dimension.SetGlobalSpanCostCoefficient(100)
# Setting first solution heuristic.
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
search_parameters.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)
# Solve the problem.
solution = routing.SolveWithParameters(search_parameters)
# Print solution on console.
if solution:
print_solution(data, manager, routing, solution)
else:
print('No solution found !')
Zajezdnia -> Adres 5 -> Adres 6 -> Adres 7 -> Adres 4 -> Adres 3 -> Adres 2 -> Adres 1 -> Zajezdnia
Ta najkrótsza trasa będzie miała długość około 7,44 km.
V. Utwórz ścieżkę dla najbardziej efektywnej trasy
Wizualizacja danych jest jednym z najlepszych sposobów na ułatwienie zrozumienia tego przypadku. Dlatego w tym ostatnim kroku wykonamy wizualizację danych przestrzennych, aby przedstawić najlepszą trasę. Powinniśmy utworzyć nową ramkę DataFrame zawierającą tylko najlepszą trasę, a następnie utworzyć ścieżkę.
# Create path for the most effective route #
# Create DataFrame of route order
df_nodes_best = pd.DataFrame({'origin' : route_name[:-1],
'destination': route_name[1:]})
df_nodes_best = df_nodes_best.merge(df_nodes, on=['origin', 'destination'], how='inner')
df_nodes_best["route_order"] = [f"Route {i}" for i in range(1, len(df_nodes_best)+1)]
df_nodes_best
# Create lists
list_route_best = []
list_route_length_best = []
# Determining distance and route
for i in range(len(df_nodes_best)):
# find the nearest node to the start location
orig_node_best = ox.get_nearest_node(graph, df_nodes_best.iloc[i]['latlong_origin'])
# find the nearest node to the end location
dest_node_best = ox.get_nearest_node(graph, df_nodes_best.iloc[i]['latlong_destination'])
# find the shortest path
shortest_route_best = nx.shortest_path(graph,
orig_node_best,
dest_node_best,
weight=optimizer)
shortest_route_length_best = nx.shortest_path_length(graph, orig_node_best, dest_node_best, weight=optimizer)
# append list
list_route_best.append(shortest_route_best)
list_route_length_best.append(shortest_route_length_best)
# Store best route distance in a list
distance_result_best = list_route_length_best.copy()
# Convert nodes code into lat long
all_list_nodes_latlong = []
for i in range(len(list_route_best)):
list_nodes_latlong = []
for j in range(len(list_route_best[i])):
tuple_nodes_latlong = (graph.nodes[list_route_best[i][j]]['y'], graph.nodes[list_route_best[i][j]]['x'])
list_nodes_latlong.append(tuple_nodes_latlong)
all_list_nodes_latlong.append(list_nodes_latlong)
# Load map
my_map = folium.Map(location=[-6.897097, 107.621903],
zoom_start=15,
tiles='cartodbpositron')
# Plot route polyline
for i in range(len(all_list_nodes_latlong)):
folium.PolyLine(all_list_nodes_latlong[i],
tooltip=f"{df_nodes_best['route_order'].iloc[i].upper()}:\
{df_nodes_best['origin'].iloc[i]} to {df_nodes_best['destination'].iloc[i]}\
- Distance: {round(distance_result_best[i]/1000, 2)} km",
# color=random_color(),
weight=5).add_to(my_map)
# Plot points
for i in range(len(df_points)):
if i != 0:
folium.Marker(location = df_points['lat_lng'][i],
popup = df_points['point_name'][i]).add_to(my_map)
else:
folium.Marker(location = df_points['lat_lng'][i],
popup = df_points['point_name'][i],
icon = folium.Icon(color='crimson')).add_to(my_map) # mark depot with red symbology
print(f'Total distance: {round(sum(distance_result_best)/1000, 2)} km')
my_map
Figure 8. The shortest route
Rozwiązanie może pomóc nam znaleźć najlepsze trasy, aby dostarczyć wszystkie przesyłki do klientów. Najkrótsza odległość do pokonania to 7,44 km następującą trasą:
Zajezdnia -> Adres 5 -> Adres 6 -> Adres 7 -> Adres 4 -> Adres 3 -> Adres 2 -> Adres 1 -> Zajezdnia
Potencjalne implikacje biznesowe lub korzyści płynące z tego projektu to:
- oszczędność kosztów dostawy
- skrócić czas spędzony w drodze
- zwiększyć wskaźnik terminowości dostaw
- poprawić wynik CSAT
- stabilna/poprawiona umowa SLA
- zmniejszyć liczbę biletów obsługi klienta (CS).
https://www.sciencedirect.com/science/article/pii/S1877050921001125
https://www.samirsaci.com/optimize-e-commerce-last-mile-delivery-with-python/
https://en.wikipedia.org/wiki/Travelling_salesman_problem
https://en.wikipedia.org/wiki/Vehicle_routing_problem
https://blog.devgenius.io/creating-route-polyline-using-folium-and-here-api-256fcf46ef9c
https://desktop.arcgis.com/en/arcmap/latest/manage-data/geocoding/what-is-geocoding.htm