Всем привет!
Это моя первая статья на ресурсе.
Обо мне: Владимир, действующий сотрудник одной из компаний в сфере транспортного инжиниринга всеми видами транспорта. Стаж в сфере: с июня 2015 года, 9 лет. Начинал с должности менеджера по логистике, затем пошагово вырос до роли руководителя отдела продаж и директора филиала.
Этой статьей хотел поделится со своими коллегами по рынку, для понимания возможностей использования исскуственного интеллекта в вашей работе.
Лирическое отступление
Помню услышал одну фразу: "не переживайте, исскуственный интеллект не отберёт вашу работу, вашу работу отберёт человек, умеющий пользоваться исскуственным интеллектом".
Решил попробовать, пощупать руками, прошёл базовый курс ChatPGT от OpenAI. Сложно передать весь восторг, который я испытал, выполняя рутинные задачи с его помощью. Надеюсь, этой статьёй смогу передать часть тех возможностей, которые мы с вами упускаем из виду.
Задача
Обратился ко мне мой старый товарищ с просьбой помочь в планировании логистики, он регулярно занимается производством ЖБИ изделий и требуется их доставка, а также выгрузка на территории Грузополучателя.
Известно:
- Всего 1115 точек выгрузки по Татарстану.
- Завод отгрузки в г. Нижнекамске.
- Груз: ЖБИ изделия, массой по 500 кг, размером 0,5 на 0,5 на 0,2 метра.
- Срок выполнения задачи: 2 месяца.
Предварительный логический разбор задачи:
Республика Татарстан является одним из Субъектов Россиийской Федерации, население которого составляет 4 млн. человек (2024).
Понимая очевидную взаимосвязь (где люди - там и экономика, а где экономика там и склады, а где склады там и грузовая техника), и, как следствие, необходимость выбора нескольких точек складирования, были выбраны 2 основных города регионального значения - г. Казань и г. Набережные Челны, совокупным населением 2 млн. человек (50% от общего населения РТ).
В нашем случае, город Нижнекамск находится в 48 км от г. Набережные Челны, потому приняли решение не создавать отдельный склад хранения, а использовать возможности завода как склада отгрузки.
В тоже время, г. Казань находится в 225 км. от завода производителя, что говорит о необходимости создания склада отгрузки, так как возврат пустого грузовика займёт у нас лишнее время (и деньги).
Первичный технический анализ задачи.
К счастью, мой дорогой друг предоставил мне данные о точках выгрузки в Google Maps, отчего мне лишь осталось экспортировать данные карты в формате .kmz, затем предоставить файл ChatGPT.
Прим.: ChatGPT умеет вычислять логические задания, но он не имеет вашего опыта в понимании нюансов, и задачи вроде "само собой разумеещеся" ему необходимо описывать в деталях. Это как стажёр, который не знает вас и ваш рынок, не умеет догадываться, но исключительно исполнительный, если его научить.
Работаем с координатами:
Здесь мы попросили его обработать файл с картой, и предоставить нам координаты в формате .xlsx, для дальнейшей удобной работы.
Затем, нам нужно распределить, какие точки выгрузки ближе к точкам погрузки (Казани и Нижнекамску).
Прим.: к сожалению, у меня нет опыта работы с Яндекс Карты API либо аналогичными навигационными системами, для корректного расчёта расстояний по автодорогам, в том числе с учётом пробок, потому полагаюсь на возможности ChatGPT в части расчётов координат "в лоб", то есть по-ветру, прямое расстояние от точки до точки, без учёта дорог и пробок. В этом подходе есть и минусы - страдает объективность расчётов, но это в любом случае лучше чем ничего.
ChatGPT определил, что для Казани наиболее приближены 520 точек, оставшиеся 595 точек наименее удалены к Нижнекамску.
Добавляем технических деталей
Известно, что завод в г. Нижнекамске:
- работает с 8-00 до 17-00 5 дней в неделю
- возможная погрузка на ТС до 80 шт. в течение рабочего дня
- производительность завода: 50 шт. в течение рабочего дня, текущий склад полон на 600 шт.
Аналогичные требования предъявим и к складу в г. Казани, для упрощения планирования.
Что известно о пунктах выгрузки:
- также работают с 08-00 до 17-00, 5 дней в неделю.
- выгрузка требуется силами перевозчика, то есть нам нужен грузовик с КМУ
- на 1 пункт выгрузки - 1 единица ЖБИ
- средняя скорость выгрузки, с учётом прошлого опыта, составляет от 15 до 30 минут.
Используемые ТС:
- к работе нами выбраны грузовики с КМУ, тяговой грузоподъемностью 10 тн, КМУ устройство с грузоподъемностью 1,5 тонны.
Все эти параметры, разумеется, сообщаем для ChatGPT.
К сожалению, ChatGPT имеет ограничения среды по обработке ваших данных, потому в моем случае мне было предложено использовать либо локальный Python либо виртуальные сервисы вроде Google Collab, и предоставлен исполнительный код для загрузки моих данных и получения готового файла.
Я остановился на последнем варианте.
import pandas as pd
from geopy.distance import geodesic
from google.colab import files
from datetime import datetime, timedelta
import time
# Загрузка нового файла
uploaded = files.upload()
# Убедитесь, что имя файла соответствует загруженному
file_path = 'coordinates_baza_retry.xlsx'
df = pd.read_excel(file_path)
# Координаты базы
loading_point_baza = (55.78991698207294, 49.14626903715072)
# Константы
loading_time_per_batch = 4 * 60 # 4 часа в минутах
units_per_batch = 20
average_unloading_time_per_stop = 30 # 30 минут на выгрузку
average_speed_kmh = 60 # км/ч
working_hours_per_day = 9 * 60 # рабочий день в минутах
end_of_working_day = 17 * 60 # 17:00 в минутах с начала дня (8:00)
start_of_working_day = 8 * 60 # 8:00 в минутах с начала дня (0:00)
latest_return_time = 12 * 60 # 12:00 в минутах с начала дня (0:00)
# Функция для вычисления времени пути
def compute_travel_time(distance_km, speed_kmh):
return (distance_km / speed_kmh) * 60 # Перевод в минуты
# Функция для конвертации минут в формат времени (24-часовой формат)
def convert_minutes_to_time(minutes):
return (datetime(2024, 7, 12, 0, 0) + timedelta(minutes=minutes)).strftime('%H:%M')
# Функция для планирования маршрута
def plan_routes(df, start_point, units_per_batch, unloading_time, speed, loading_time, start_of_day, end_of_day, latest_return_time):
routes = []
distances_per_day = []
remaining_df = df.copy()
current_day = 1
total_units = 0
current_time = start_of_day
current_point = start_point
total_distance = 0
total_time = 0
# Начало первого дня с погрузкой
start_time = current_time
current_time += loading_time
routes.append({
'Day': current_day,
'Name': 'База / Погрузка на Базе',
'Coordinates': f"{loading_point_baza[0]},{loading_point_baza[1]}",
'Distance from Previous (km)': 0,
'Start Time': convert_minutes_to_time(start_time),
'End Time': convert_minutes_to_time(current_time),
'Total Distance (km)': total_distance,
'Total Time (min)': total_time + loading_time
})
total_time += loading_time
while not remaining_df.empty:
day_route = []
day_distances = []
while total_units < units_per_batch and current_time < end_of_working_day and not remaining_df.empty:
remaining_df['Distance'] = remaining_df['Coordinates'].apply(
lambda x: geodesic(current_point, tuple(map(float, x.split(',')))).kilometers
)
nearest_point_idx = remaining_df['Distance'].idxmin()
nearest_point = remaining_df.loc[nearest_point_idx]
travel_time = compute_travel_time(nearest_point['Distance'], speed)
if current_time + travel_time + unloading_time > end_of_working_day:
break
start_time = current_time
current_time += travel_time
end_time = current_time + unloading_time
day_route.append({
'Day': current_day,
'Name': nearest_point['Name'],
'Coordinates': nearest_point['Coordinates'],
'Distance from Previous (km)': nearest_point['Distance'],
'Start Time': convert_minutes_to_time(start_time),
'End Time': convert_minutes_to_time(end_time),
'Total Distance (km)': total_distance + nearest_point['Distance'],
'Total Time (min)': total_time + travel_time + unloading_time
})
day_distances.append(nearest_point['Distance'])
current_point = tuple(map(float, nearest_point['Coordinates'].split(',')))
remaining_df = remaining_df.drop(nearest_point_idx)
total_units += 1
total_distance += nearest_point['Distance']
total_time += travel_time + unloading_time
current_time = end_time
routes.extend(day_route)
distances_per_day.extend(day_distances)
if total_units >= units_per_batch or current_time >= end_of_working_day:
return_time = compute_travel_time(geodesic(current_point, start_point).kilometers, speed)
current_time += return_time
routes.append({
'Day': current_day,
'Name': 'База / Возвращение пустого грузовика на Базу',
'Coordinates': f"{loading_point_baza[0]},{loading_point_baza[1]}",
'Distance from Previous (km)': return_time,
'Start Time': convert_minutes_to_time(current_time - return_time),
'End Time': convert_minutes_to_time(current_time),
'Total Distance (km)': total_distance + return_time,
'Total Time (min)': total_time + return_time
})
distances_per_day.append(return_time)
current_point = start_point
total_units = 0
if current_time > end_of_working_day or current_time > latest_return_time:
current_day += 1
current_time = start_of_working_day
start_time = current_time
current_time += loading_time
routes.append({
'Day': current_day,
'Name': 'База / Погрузка на Базе',
'Coordinates': f"{loading_point_baza[0]},{loading_point_baza[1]}",
'Distance from Previous (km)': 0,
'Start Time': convert_minutes_to_time(start_time),
'End Time': convert_minutes_to_time(current_time),
'Total Distance (km)': 0,
'Total Time (min)': loading_time
})
total_distance = 0
total_time = loading_time
else:
current_time += loading_time
current_day += 1
current_time = start_of_working_day
return routes, distances_per_day
# Планирование маршрутов
start_time = time.time()
routes, distances_per_day = plan_routes(
df, loading_point_baza, units_per_batch,
average_unloading_time_per_stop, average_speed_kmh,
loading_time_per_batch, start_of_working_day, end_of_working_day, latest_return_time
)
end_time = time.time()
print(f"Time taken: {end_time - start_time} seconds")
# Создание итогового списка с дополнительными строками
final_route_summary = []
for i, stop in enumerate(routes):
final_route_summary.append({
'Day': stop['Day'],
'Name': stop['Name'],
'Coordinates': stop['Coordinates'],
'Distance from Previous (km)': stop['Distance from Previous (km)'],
'Distance to Next (km)': distances_per_day[i+1] if i < len(distances_per_day) - 1 else 0,
'Start Time': stop['Start Time'],
'End Time': stop['End Time'],
'Total Distance (km)': stop['Total Distance (km)'],
'Total Time (min)': stop['Total Time (min)']
})
final_route_summary_df = pd.DataFrame(final_route_summary)
# Сохранение результатов в Excel файл
output_file_path_final_baza_xlsx = 'final_route_summary_baza_with_loading_return_with_times.xlsx'
final_route_summary_df.to_excel(output_file_path_final_baza_xlsx, index=False)
# Скачать результат
files.download(output_file_path_final_baza_xlsx)
Вторичный технический анализ
Разумеется, в последствии нужно будет добавить ему деталей.
К примеру:
- если грузовик приехал на одну из точек выгрузки имея в запасе ещё ЖБИ, то он не возвращается на пункт погрузки раньше чем не выполнит выгрузку всех ЖБИ изделий и остаётся ночевать в грузовике
- если грузовик вернулся на пункт отгрузки позже чем в 12-00, то его уже не успеют погрузить в этот день, значит он будет погружен с 08-00 следующего рабочего дня
- необходимость отразить расстояние пробега после погрузки до этапа последней выгрузки и пустого хода до погрузки
- и так далее, добавляя еще и ещё технических деталей, вплоть до того что вы работаете в 24 часовом формате, а не в 12 часовом.
Предварительный итог
На выходе, после обработки кода через Google Collab, я получил следующий файл:
Здесь предварительно описан график движения грузовика относительно Завода в г. Нижнекамске в первые 2 дня, с учетом погрузки в первый день и возврата пустым только на следующий день в 13:41.
Разумеется, есть погрешность в расстоянии и затраченном времени, так как мы используем прямые расчёты расстояний по координатам без учёта автодорог и пробок, но тем не менее, это позволяет определить особо важные детали, к примеру:
Разумеется, есть погрешность в расстоянии и затраченном времени, так как мы используем прямые расчёты расстояний по координатам без учёта автодорог и пробок, но тем не менее, это позволяет определить особо важные детали, к примеру:
- Даже при таком грубом расчёте расстояний между локациями, для выполнения задачи по вывозу 595 единиц ЖБИ из Нижнекамска нам потребуется 83 рабочих дня.
При сроке исполнения 2 календарных месяца (то есть 44 рабочих дня в среднем), нам необходимо как минимум 2 грузовика с КМУ для того чтобы уложится в срок
- В некоторые дни у нас цикл (погрузился - везде выгрузился- вернулся) может занимать до 3-х дней, а значит при среднесуточных ожиданиях грузовиков с КМУ не менее 30 тысяч рублей в сутки, стоимость доставки таковых ЖБИ составит 30*3/20=4500 рублей за штуку.
А значит, и бюджет всей перевозки необходимо считать не столько в "рублей за километр", а даже "рублей в сутки с учётом затраченного времени".
Послесловие
Это пока предварительный итог расчётов, в настоящий момент я его дорабатываю. В близжашие дни я его доработаю, и буду внимательно смотреть на реализацию по этому плану, о чем и сообщу позже.
До встречи!
Хэштеги:
#логистика #исскуственный_интеллект #chatgpt