はじめに
HEREでエンジニアをしておりますkekishidaと申します。本記事では、PythonとHEREのAPI群を利用して、以下の箇条書きに示した事項を実現するべく、物流向けの配送オペレーションについてJupyter上でシミュレーションしてみたいと思います。
- 配送計画の最適化
- 配送時間の短縮化
- 計画策定作業の効率化
- トラック規制考慮によるドライバーの安全確保
HEREでは物流関連ソリューション向けAPIにも注力しています。具体的には、複数の配送地点の配送ルートを検索するAPI、運搬荷物のアセットを追跡管理するAPIなどなどを取り揃えています。
一方で、HEREではData SDK for Pythonというロケーションデータを分析するプラットフォームを提供しており、こちらの機能を利用することでPythonユーザが、データ分析のみならず、HEREのAPI群を利用したアプリケーションの開発も可能にします。(本記事では、本SDKの機能の一部のみを使用しております。)
今回シミュレーションするシナリオは物流向けに特化していますが、インタラクティブに対話することが可能なJupyterエコシステムを利用して、いかに目的のシミュレーションができるのか?さらに、トラックドライバーの配送ルートをインタラクティブに疑似体験しながら、物流DXのプロトタイピングをしていきたいと思います。
シミュレーションするシナリオ
物流の最終拠点である営業所からエンドユーザに荷物を配送するラストワンマイルのシナリオとします。
場所
エリアは京都とし、配送する場所についても、イメージし易い有名観光スポットとしました。さらに、配送デポについては京都駅としました。こうすることで、配送順序の妥当性についてイメージがし易いかと思います。(京都に修学旅行に行かれた方は、そのルートを思い出してみてください。)
配送リスト
以下のような配送リストをCSVフォーマットで作成しました。(これから配達する荷物の宛先リストになります。)Time Restrictionというフィールドはいわゆる配達の時間指定になります。Service Timeというのは配送先毎にかかるサービス時間(分)となります。この配送リストの時点では、配送順序、配送ルートそして作業終了時間は確定していません。これらをDXします。
シナリオ
- 配送リスト(配達する荷物の宛先リスト)をもとに配送順序を決定します。(DX部分)
- 決定した配送順序をもとに配送ルートを決定します。 (DX部分)
- 決定した配送順序及び配送ルートをもとに配送作業を行います。
- 3地点(清水寺、銀閣寺、南禅寺)で不在のため、未配達というシナリオとします。
- 以上より、再配達リストを作成します。(DX部分)
- 再配達リストより配送順序を決定します。(DX部分)
- 決定した配送順序をもとに現在地からの配送ルートを決定します。(DX部分)
- 決定した配送順序及び配送ルートをもとに配送作業を行います。
プロトタイピングに必要なもの
それではプロトタイピングを開始したいと思いますが、その前に必要なものがいくつか存在します。
Jupyter notebookまたはJupyter lab
インストール方法についてはQiita内にいくつか有用な記事が存在します。
HEREのアカウント取得
以下の記事が大変参考になります。
HERE Data SDK for Pythonのインストール
インストール方法の詳細は下記のリンクに記載されておりますが、念の為、記事掲載時(2023年5月)の最新版のインストールコマンドconda install/pip install
両方について掲載しておきます。
https://developer.here.com/documentation/sdk-python-v2/dev_guide/topics/install.html
conda install -c conda-forge -c https://repo.platform.here.com/artifactory/api/conda/olp_analytics/analytics_sdk here-platform=2.20.0 here-geotiles=2.20.0 here-geopandas-adapter=2.20.0 here-content=2.20.0 here-inspector=2.20.0
pip install --extra-index-url https://repo.platform.here.com/artifactory/api/pypi/analytics-pypi/simple/ here-platform==2.20.0 here-geotiles==2.20.0 here-geopandas-adapter==2.20.0 here-content==2.20.0 here-inspector==2.20.0
HERE Credentialの設定
HERE Data SDK for Pythonを使用するためには、Credentialの設定が必要になります。
https://developer.here.com/documentation/sdk-python-v2/dev_guide/topics/credentials.html
詳細は上記リンクに記載されておりますが、HEREアカウントを取得し、OAuthキーを作成する際に以下のフォーマットのcredentials.properties
ファイルを入手することができます。
here.user.id = <example_here>
here.client.id = <example_here>
here.access.key.id = <example_here>
here.access.key.secret = <example_here>
here.token.endpoint.url = <example_here>
こちらのファイルをOSに応じたファイルパスに保管します。
$HOME/.here/credentials.properties
%USERPROFILE%\.here\credentials.properties
HERE Map Widget for Jupyterのインストール
HERE Data SDK for Pythonを使用して地図を描画するには、いくつかの方法が存在します。ここではHERE Map Widget for Jupyterを使用します。
https://developer.here.com/documentation/sdk-python-v2/dev_guide/topics/usage/here-inspector.html
上記のリンクにも説明がありますが、以下のリンクが参考になります。
インストール手順は以下のリンクに記載されておりますが、念の為、記事掲載時(2023年5月)の最新版のインストールコマンドconda install/pip install
両方について掲載しておきます。
conda install -c conda-forge here-map-widget-for-jupyter
pip install here-map-widget-for-jupyter
以上で最低限必要なものの準備は完了です。残りはJupyterエコシステムの考え方に乗っ取り、対話的に、その都度必要なモジュールをインストールする形で進めていきたいと思います。
配送シミュレーション by Jupyter
以下にjupyterと注釈してありますコードスニペットをそのままJupyter notebook/labのセルにペーストするだけでシミュレーションの実行が可能なように配慮をしておりますが、コードが冗長であること、個別環境に依存する部分がありますことについてはご了承ください。(あくまでもプロトタイピングという体です。)
配送リストCSVファイル
冒頭で説明しました配送リストです。
ID,Address,Time Restriction,Service Time,Job Type
京都駅,京都府京都市下京区東塩小路釜殿町,,,
東寺,京都府京都市南区九条町1,,5,配送
清水寺,京都府京都市東山区清水1丁目294,18-20,10,配送
金閣寺,京都府京都市北区金閣寺町1,8-12,5,配送
銀閣寺,京都府京都市左京区銀閣寺町2,,5,配送
渡月橋,京都府京都市右京区嵯峨天龍寺芒ノ馬場町1−5,8-12,10,配送
映画村,京都府京都市右京区太秦東蜂岡町10,8-12,10,配送
京都御所,京都府京都市上京区京都御苑3,18-20,10,配送
伏見稲荷,京都府京都市伏見区深草藪之内町68,18-20,5,配送
南禅寺,京都府京都市左京区南禅寺福地町,8-12,5,配送
二条城,京都府京都市中京区二条城町541,,10,配送
八坂神社,京都府京都市東山区祇園町北側625,14-16,5,配送
京都駅(戻り),京都府京都市下京区東塩小路釜殿町,,,
配送リストCSVファイルの読み込み
上記のCSVファイルを読み込みpandasのdataframeとして表示します。
import pandas as pd
file = "京都.csv"
df = pd.read_csv(file,sep=",",encoding="cp932")
df
HERE Geocoding & Search APIを使用した住所から緯度経度情報への変換
配送リストの住所情報を緯度経度情報に変換します。その前に、下記のコードスニペットを実行するために以下のモジュールが必要になります。tqdm
は経過情報を表示するためのツールになります。
pip install tqdm
一方、以下のモジュールはHEREのロケーションサービスを簡易的に使用するために作られたモジュールです。全く同じ様な機能がHERE Data SDK for Pythonにも準備されておりますが、HERE Geocoding & Search APIを実装する上ではこちらの方が簡単であったため、あえてこちらを使用しております。
pip install here-location-services
また、HERE Geocoding & Search APIの詳細については以下のリンクをご参照ください。
以下のコードスニペットで住所を緯度経度に変換しています。<APIKEY>
と記載されている部分は取得されましたAPIKEYに変換してください。
from tqdm import tqdm
from here_location_services import LS
APIKEY = <APIKEY>
ls = LS(api_key=APIKEY)
addrList = df['Address'].tolist()
latlngList = []
for m in tqdm(addrList):
gc_response = ls.geocode(query=m)
data = gc_response.to_geojson()
if len(data["features"]) != 0:
latlngList.append(data["features"][0]["properties"]["position"])
else:
latlngList.append({'lat': 0.0, 'lng': 0.0})
配送リストと緯度経度情報のマージ
取得した緯度経度情報を配送リストにマージし、新しいdataframeを作成します。
latList = []
lngList = []
for o in latlngList:
latList.append(o['lat'])
lngList.append(o['lng'])
idList = []
for p in range(len(addrList)):
p = p + 1
idList.append(str(p))
orders_df = df.copy()
orders_df['Latitude'] = latList
orders_df['Longitude'] = lngList
orders_df
HERE Waypoint Sequence APIによる配送順序の決定
HEREのラストワンマイル向けAPIとして以下の2つのAPIが存在しています。
今回は、トラック1台あたりの配送計画をシミュレーションするため、よりシンプルなHERE Waypoint Sequence APIを使用します。(複数台の配送計画にはTour Planning APIが最適になります。)
シンプルと申し上げましたが、実際は多くのインプットパラメータが存在しており、HEREが管理するロケーションデータを活用して様々な機能を実現することが可能です。(トラック規制、渋滞情報、配達時間の考慮など)
https://developer.here.com/documentation/routing-waypoints/api-reference.html
今回は、以下の基本条件でWaypoint Sequence APIをコールします。
- 時間優先(fastest)
- 車種(truck)
- 渋滞などの交通状況考慮(enabled)
- 出発時間の指定(2023-03-04 08:00)
- 配達指定時間の考慮(各配送リスト内の情報)
- サービス時間の考慮(各配送リスト内の情報)
以下のコードスニペットによりWaypoint Seaquence APIのインプットパラメータを生成します。
routingMode = "fastest"
transportMode = "truck"
trafficMode = "enabled"
departureTime = "2023-03-04T08:00:00+09:00"
import numpy as np
import time
list = orders_df.to_numpy().tolist()
dayOfWeek = ""
temp = pd.Timestamp(departureTime)
if temp.dayofweek == 0:
dayOfWeek = "mo"
elif temp.dayofweek == 1:
dayOfWeek = "tu"
elif temp.dayofweek == 2:
dayOfWeek = "we"
elif temp.dayofweek == 3:
dayOfWeek = "th"
elif temp.dayofweek == 4:
dayOfWeek = "fr"
elif temp.dayofweek == 5:
dayOfWeek = "sa"
elif temp.dayofweek == 6:
dayOfWeek = "su"
convertLists = []
params = {}
number = len(list)
count = 1
for m in list:
key = ""
acc = ""
convertList = ""
st = m[3]*60
if count == 1:
key = "start"
elif count == number:
key = "end"
else:
key += "destination"
key += str(count-1)
convertList += m[0]+":"+m[1]
convertList += ";"
convertList += str(m[5])
convertList += ","
convertList += str(m[6])
if count != 1:
if count != number:
if m[2]:
if m[2] == "8-12":
acc = "acc:"+dayOfWeek+"08:00:00+09:00|"+dayOfWeek+"12:00:00+09:00"
elif m[2] == "12-14":
acc = "acc:"+dayOfWeek+"12:00:00+09:00|"+dayOfWeek+"14:00:00+09:00"
elif m[2] == "14-16":
acc = "acc:"+dayOfWeek+"14:00:00+09:00|"+dayOfWeek+"16:00:00+09:00"
elif m[2] == "18-20":
acc = "acc:"+dayOfWeek+"18:00:00+09:00|"+dayOfWeek+"20:00:00+09:00"
elif m[2] == "20-22":
acc = "acc:"+dayOfWeek+"20:00:00+09:00|"+dayOfWeek+"22:00:00+09:00"
elif m[2] == "18-22":
acc = "acc:"+dayOfWeek+"18:00:00+09:00|"+dayOfWeek+"22:00:00+09:00"
convertList += ";"
convertList += acc
convertList += ";st:"
if np.isnan(st):
convertList += ""
else:
convertList += str(int(st))
params[key] = convertList
if count == number:
params["departure"] = departureTime
params["improveFor"] = "distance"
params["mode"] = routingMode+";"+transportMode+";traffic:"+trafficMode+";dirtRoad:-2"
count = count + 1
print(params)
出力結果は以下のとおりです。
{'start': '京都駅:京都府京都市下京区東塩小路釜殿町;34.98515,135.75709',
'destination1': '東寺:京都府京都市南区九条町1;34.98054,135.74664;;st:300',
'destination2': '清水寺:京都府京都市東山区清水1丁目294;34.99587,135.78305;acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600',
'destination3': '金閣寺:京都府京都市北区金閣寺町1;35.03909,135.72928;acc:sa08:00:00+09:00|sa12:00:00+09:00;st:300',
'destination4': '銀閣寺:京都府京都市左京区銀閣寺町2;35.02686,135.79827;;st:300',
'destination5': '渡月橋:京都府京都市右京区嵯峨天龍寺芒ノ馬場町1-5;35.01374,135.67753;acc:sa08:00:00+09:00|sa12:00:00+09:00;st:600',
'destination6': '映画村:京都府京都市右京区太秦東蜂岡町10;35.01553,135.70837;acc:sa08:00:00+09:00|sa12:00:00+09:00;st:600',
'destination7': '京都御所:京都府京都市上京区京都御苑3;35.02658,135.75989;acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600',
'destination8': '伏見稲荷:京都府京都市伏見区深草藪之内町68;34.96781,135.77273;acc:sa18:00:00+09:00|sa20:00:00+09:00;st:300',
'destination9': '南禅寺:京都府京都市左京区南禅寺福地町;35.01015,135.79144;acc:sa08:00:00+09:00|sa12:00:00+09:00;st:300',
'destination10': '二条城:京都府京都市中京区二条城町541;35.01482,135.74658;;st:600',
'destination11': '八坂神社:京都府京都市東山区祇園町北側625;35.00369,135.77861;acc:sa14:00:00+09:00|sa16:00:00+09:00;st:300',
'end': '京都駅(戻り):京都府京都市下京区東塩小路釜殿町;34.98515,135.75709',
'departure': '2023-03-04T08:00:00+09:00',
'improveFor': 'time',
'mode': 'fastest;truck;traffic:enabled;dirtRoad:-2'}
つまり、こちらのJSONフォーマットのデータがHERE Waypoint Sequence APIに与えられる実際のインプットパラメータになります。それでは、HERE Waypoint Sequence APIをコールしてみましょう。ここではHERE Data SDK for Pythonの作法に沿ってAPIをコールしています。詳しくは以下のリンクをご参照ください。
import json
from pygments import highlight
from pygments.lexers import JsonLexer
from pygments.formatters import TerminalFormatter
from here.platform import Platform
platform = Platform()
wps = platform.get_service("hrn:here:service::olp-here:waypoints-sequence-8")
result = wps.get("/findsequence2", params=params)
print(highlight(json.dumps(result, indent=4, ensure_ascii=False), JsonLexer(), TerminalFormatter()))
以下はその出力結果になります。
{
"results": [
{
"waypoints": [
{
"id": "京都駅:京都府京都市下京区東塩小路釜殿町",
"lat": 34.98515,
"lng": 135.75709,
"sequence": 0,
"estimatedArrival": null,
"estimatedDeparture": "2023-03-04T08:00:00+09:00",
"fulfilledConstraints": []
},
{
"id": "東寺:京都府京都市南区九条町1",
"lat": 34.98054,
"lng": 135.74664,
"sequence": 1,
"estimatedArrival": "2023-03-04T08:05:18+09:00",
"estimatedDeparture": "2023-03-04T08:10:18+09:00",
"fulfilledConstraints": [
"st:300"
]
},
{
"id": "渡月橋:京都府京都市右京区嵯峨天龍寺芒ノ馬場町1-5",
"lat": 35.01374,
"lng": 135.67753,
"sequence": 2,
"estimatedArrival": "2023-03-04T08:36:18+09:00",
"estimatedDeparture": "2023-03-04T08:46:18+09:00",
"fulfilledConstraints": [
"acc:sa08:00:00+09:00|sa12:00:00+09:00;st:600"
]
},
{
"id": "映画村:京都府京都市右京区太秦東蜂岡町10",
"lat": 35.01553,
"lng": 135.70837,
"sequence": 3,
"estimatedArrival": "2023-03-04T08:58:01+09:00",
"estimatedDeparture": "2023-03-04T09:08:01+09:00",
"fulfilledConstraints": [
"acc:sa08:00:00+09:00|sa12:00:00+09:00;st:600"
]
},
{
"id": "金閣寺:京都府京都市北区金閣寺町1",
"lat": 35.03909,
"lng": 135.72928,
"sequence": 4,
"estimatedArrival": "2023-03-04T09:24:19+09:00",
"estimatedDeparture": "2023-03-04T09:29:19+09:00",
"fulfilledConstraints": [
"acc:sa08:00:00+09:00|sa12:00:00+09:00;st:300"
]
},
{
"id": "二条城:京都府京都市中京区二条城町541",
"lat": 35.01482,
"lng": 135.74658,
"sequence": 5,
"estimatedArrival": "2023-03-04T09:41:50+09:00",
"estimatedDeparture": "2023-03-04T09:51:50+09:00",
"fulfilledConstraints": [
"st:600"
]
},
{
"id": "銀閣寺:京都府京都市左京区銀閣寺町2",
"lat": 35.02686,
"lng": 135.79827,
"sequence": 6,
"estimatedArrival": "2023-03-04T10:10:14+09:00",
"estimatedDeparture": "2023-03-04T10:15:14+09:00",
"fulfilledConstraints": [
"st:300"
]
},
{
"id": "南禅寺:京都府京都市左京区南禅寺福地町",
"lat": 35.01015,
"lng": 135.79144,
"sequence": 7,
"estimatedArrival": "2023-03-04T10:26:24+09:00",
"estimatedDeparture": "2023-03-04T10:31:24+09:00",
"fulfilledConstraints": [
"acc:sa08:00:00+09:00|sa12:00:00+09:00;st:300"
]
},
{
"id": "八坂神社:京都府京都市東山区祇園町北側625",
"lat": 35.00369,
"lng": 135.77861,
"sequence": 8,
"estimatedArrival": "2023-03-04T10:41:20+09:00",
"estimatedDeparture": "2023-03-04T14:05:00+09:00",
"fulfilledConstraints": [
"acc:sa14:00:00+09:00|sa16:00:00+09:00;st:300"
]
},
{
"id": "京都御所:京都府京都市上京区京都御苑3",
"lat": 35.02658,
"lng": 135.75989,
"sequence": 9,
"estimatedArrival": "2023-03-04T14:19:13+09:00",
"estimatedDeparture": "2023-03-04T18:10:00+09:00",
"fulfilledConstraints": [
"acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600"
]
},
{
"id": "清水寺:京都府京都市東山区清水1丁目294",
"lat": 34.99587,
"lng": 135.78305,
"sequence": 10,
"estimatedArrival": "2023-03-04T18:23:46+09:00",
"estimatedDeparture": "2023-03-04T18:33:46+09:00",
"fulfilledConstraints": [
"acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600"
]
},
{
"id": "伏見稲荷:京都府京都市伏見区深草藪之内町68",
"lat": 34.96781,
"lng": 135.77273,
"sequence": 11,
"estimatedArrival": "2023-03-04T18:48:24+09:00",
"estimatedDeparture": "2023-03-04T18:53:24+09:00",
"fulfilledConstraints": [
"acc:sa18:00:00+09:00|sa20:00:00+09:00;st:300"
]
},
{
"id": "京都駅(戻り):京都府京都市下京区東塩小路釜殿町",
"lat": 34.98515,
"lng": 135.75709,
"sequence": 12,
"estimatedArrival": "2023-03-04T19:06:32+09:00",
"estimatedDeparture": null,
"fulfilledConstraints": []
}
],
"distance": "52265",
"time": "39992",
"interconnections": [
{
"fromWaypoint": "京都駅:京都府京都市下京区東塩小路釜殿町",
"toWaypoint": "東寺:京都府京都市南区九条町1",
"distance": 1546.0,
"time": 318.0,
"rest": 0.0,
"waiting": 0.0
},
{
"fromWaypoint": "東寺:京都府京都市南区九条町1",
"toWaypoint": "渡月橋:京都府京都市右京区嵯峨天龍寺芒ノ馬場町1-5",
"distance": 9806.0,
"time": 1560.0,
"rest": 0.0,
"waiting": 0.0
},
{
"fromWaypoint": "渡月橋:京都府京都市右京区嵯峨天龍寺芒ノ馬場町1-5",
"toWaypoint": "映画村:京都府京都市右京区太秦東蜂岡町10",
"distance": 3100.0,
"time": 703.0,
"rest": 0.0,
"waiting": 0.0
},
{
"fromWaypoint": "映画村:京都府京都市右京区太秦東蜂岡町10",
"toWaypoint": "金閣寺:京都府京都市北区金閣寺町1",
"distance": 4509.0,
"time": 978.0,
"rest": 0.0,
"waiting": 0.0,
"warnings": [
{
"message": "Route violates requested 'avoid' options or legal/physical restrictions",
"code": 3
}
]
},
{
"fromWaypoint": "金閣寺:京都府京都市北区金閣寺町1",
"toWaypoint": "二条城:京都府京都市中京区二条城町541",
"distance": 4396.0,
"time": 751.0,
"rest": 0.0,
"waiting": 0.0,
"warnings": [
{
"message": "Route violates requested 'avoid' options or legal/physical restrictions",
"code": 3
}
]
},
{
"fromWaypoint": "二条城:京都府京都市中京区二条城町541",
"toWaypoint": "銀閣寺:京都府京都市左京区銀閣寺町2",
"distance": 6026.0,
"time": 1104.0,
"rest": 0.0,
"waiting": 0.0
},
{
"fromWaypoint": "銀閣寺:京都府京都市左京区銀閣寺町2",
"toWaypoint": "南禅寺:京都府京都市左京区南禅寺福地町",
"distance": 2431.0,
"time": 670.0,
"rest": 0.0,
"waiting": 0.0
},
{
"fromWaypoint": "南禅寺:京都府京都市左京区南禅寺福地町",
"toWaypoint": "八坂神社:京都府京都市東山区祇園町北側625",
"distance": 2568.0,
"time": 596.0,
"rest": 0.0,
"waiting": 11920.0
},
{
"fromWaypoint": "八坂神社:京都府京都市東山区祇園町北側625",
"toWaypoint": "京都御所:京都府京都市上京区京都御苑3",
"distance": 4585.0,
"time": 853.0,
"rest": 0.0,
"waiting": 13247.0
},
{
"fromWaypoint": "京都御所:京都府京都市上京区京都御苑3",
"toWaypoint": "清水寺:京都府京都市東山区清水1丁目294",
"distance": 5571.0,
"time": 826.0,
"rest": 0.0,
"waiting": 0.0,
"warnings": [
{
"message": "Route violates requested 'avoid' options or legal/physical restrictions",
"code": 3
}
]
},
{
"fromWaypoint": "清水寺:京都府京都市東山区清水1丁目294",
"toWaypoint": "伏見稲荷:京都府京都市伏見区深草藪之内町68",
"distance": 4366.0,
"time": 878.0,
"rest": 0.0,
"waiting": 0.0
},
{
"fromWaypoint": "伏見稲荷:京都府京都市伏見区深草藪之内町68",
"toWaypoint": "京都駅(戻り):京都府京都市下京区東塩小路釜殿町",
"distance": 3361.0,
"time": 788.0,
"rest": 0.0,
"waiting": 0.0
}
],
"description": "Targeted best time; with , improvement for traffic",
"timeBreakdown": {
"driving": 10025,
"service": 4800,
"rest": 0,
"waiting": 25167
}
}
],
"errors": [],
"processingTimeDesc": "1183ms",
"responseCode": "200",
"warnings": null,
"requestId": null
}
配送順序を表として表示
このHERE Waypoint Sequence APIのレスポンスを人間にとって理解し易いフォーマットとするために、pandasのdataframeとします。
wayPointList = []
for m in result["results"][0]["waypoints"]:
wayPointList.append(m)
pd.set_option('display.max_colwidth', 150)
waypointDf=pd.DataFrame(wayPointList)
waypointDf
以上から、配送順序と大まかな配送計画を俯瞰することができます。8:00に京都駅(デポ)を出発し、19:06に京都駅(デポ)に戻ります。また、最短時間という条件で順序を導きましたが、時間指定配達の制約条件により、八坂神社と京都御所の地点でそれぞれ4時間の空きが出来てしてしまうことが分かります。
HERE Routing APIによる配送ルートの決定
HERE Routing APIは、詳細は以下のリンクで記載しておりますが、基本的には出発地と目的地の最適な経路を探索するAPIになります。
先ほど作成した配送順序のdataframeを使用して再起的にRouting APIをコールさせます。
route = platform.get_service("hrn:here:service::olp-here:routing-8")
if routingMode == "fastest":
routingMode = "fast"
elif routingMode == "shortest":
routingMode = "short"
polyLineList = []
departureList = []
arrivalList = []
summaryList = []
waypointLocationList = []
transitDeparture = ""
count = 0
for n in wayPointList:
if count == 0:
estimatedDeparture = str(wayPointList[count-1]['estimatedDeparture'])
waypointLocation = {"order": count, "name": wayPointList[count]['id'], "lat": wayPointList[count]['lat'], "lng": wayPointList[count]['lng'], "estimatedDeparture": estimatedDeparture}
waypointLocationList.append(waypointLocation)
count = count + 1
continue
lat1 = str(wayPointList[count-1]['lat'])
lng1 = str(wayPointList[count-1]['lng'])
lat2 = str(wayPointList[count]['lat'])
lng2 = str(wayPointList[count]['lng'])
name = str(wayPointList[count]['id'])
estimatedDeparture = str(wayPointList[count-1]['estimatedDeparture'])
transitDeparture = str(wayPointList[count-1]['estimatedDeparture'])
waypointLocation = {"order": count, "name": name, "lat": wayPointList[count]['lat'], "lng": wayPointList[count]['lng'], "estimatedDeparture": estimatedDeparture}
waypointLocationList.append(waypointLocation)
params = {
"origin": f'{lat1},{lng1}',
"destination": f'{lat2},{lng2}',
"routingMode": f'{routingMode}',
"transportMode": f'{transportMode}',
"departureTime": f'{transitDeparture}',
"return": "polyline,actions,instructions,summary,passthrough,routeHandle"
}
result = route.get("/routes", params=params)
polyLineList.append(result["routes"][0]['sections'][0]["polyline"])
departureList.append(result["routes"][0]['sections'][0]["departure"])
arrivalList.append(result["routes"][0]['sections'][0]["arrival"])
summaryList.append(result["routes"][0]['sections'][0]["summary"])
count = count + 1
配送ルートを地図上に表示
それでは、配送ルートを地図上に表示しますが、まずは、HERE Routing APIを地図上に表示するために取得したPolylineを緯度経度情報にデコードする必要があります。そのためにflexpolylineモジュールのインストールが必要になります。
pip install flexpolyline
その上で、以下のコードを実行し緯度経度情報のリストに変換します。
import flexpolyline as fp
gpsLists = []
for o in polyLineList:
pl = fp.decode(o)
gpsList = []
for p in pl:
gpsList.append(p[0])
gpsList.append(p[1])
gpsList.append(0)
gpsLists.append(gpsList)
さあ、最後の仕上げです。HERE Map widget for Jupyterを使用して地図上に表示します。
こちらの使用方法については以下のリンクをご参考ください。
<APIKEY>
と記載されている部分は取得されましたAPIKEYに変換してください。
from here_map_widget import MultiLineString, LineString, Polyline, Point, Circle, WKT, Marker, Icon, FullscreenControl, InfoBubble
from here_map_widget import Map, OMV, Platform, Style, TileLayer, MapSettingsControl
from here_map_widget import ServiceNames, OMVUrl
from here_map_widget import LineString, Polyline
df = pd.DataFrame(waypointLocationList)
avg_lat = df['lat'].mean()
avg_lng = df['lng'].mean()
zoom = 13
apiKey = <APIKEY>
services_config = {
ServiceNames.omv: {
OMVUrl.scheme: "https",
OMVUrl.host: "vector.hereapi.com",
OMVUrl.path: "/v2/vectortiles/core/mc",
}
}
style = Style(
config="https://js.api.here.com/v3/3.1/styles/omv/oslo/japan/normal.day.yaml",
base_url="https://js.api.here.com/v3/3.1/styles/omv/oslo/japan/",
)
platform = Platform(api_key=apiKey,services_config=services_config)
omv_provider = OMV(path="v2/vectortiles/core/mc", platform=platform, style=style)
omv_layer = TileLayer(provider=omv_provider, style={"max": 22})
center = [avg_lat,avg_lng]
m = Map(api_key=apiKey,center=center, zoom=zoom, basemap=omv_layer)
for count in range(len(waypointLocationList)):
name = waypointLocationList[count]['name']
if count == 0:
info = InfoBubble(position=Point(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng']), \
content='<div>NAME: '+name+'</div><div>Order: '+str(waypointLocationList[count]['order'])+'</div><div>Departure: '+str(departureList[count]['time'])+'</div>')
elif 0 < count < len(waypointLocationList)-1:
info = InfoBubble(position=Point(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng']), \
content='<div>NAME: '+name+'</div><div>Order: '+str(waypointLocationList[count]['order'])+'</div><div>Departure: '+str(departureList[count]['time'])+'</div><div>Arrival: '+str(arrivalList[count-1]['time'])+'</div>')
else:
info = InfoBubble(position=Point(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng']), \
content='<div>NAME: '+name+'</div><div>Order: '+str(waypointLocationList[count]['order'])+'</div><div>Arrival: '+str(arrivalList[count-1]['time'])+'</div>')
marker = Marker(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng'], info=info, evt_type="tap", show_bubble=True)
m.add_object(marker)
count = count + 1
for q in gpsLists:
style = {"lineWidth": 5}
l = q
ls = LineString(points=l)
pl = Polyline(object=ls, style=style)
m.add_object(pl)
setttings = MapSettingsControl(
layers=[
{"label": "Original", "layer": omv_layer},
],
basemaps=["raster.satellite.map","vector.normal.map"],
)
m.add_control(setttings)
fs = FullscreenControl()
m.add_control(fs)
m
結果をフルスクリーンモードで確認しましょう。緑のマーカーをクリックすると各配送地点の情報を確認することができます。
未配達の処理
さて、最終配送地点の伏見稲荷に到着した時点で、3地点で不在のため未配達となりました。
- 清水寺
- 銀閣寺
- 南禅寺
まずは元の配送リストに不在という列を追加し、更新します。
プロトタイピングであるため、このような処理を行なっていますが、本番環境ではトラックドライバー様専用のスマートフォンアプリなどにより逐一更新するといったアプローチが一般的だと思います。
orders_df["不在"]=""
orders_df.loc[orders_df[orders_df["ID"] == "清水寺"].index, '不在'] = '不在'
orders_df.loc[orders_df[orders_df["ID"] == "銀閣寺"].index, '不在'] = '不在'
orders_df.loc[orders_df[orders_df["ID"] == "南禅寺"].index, '不在'] = '不在'
orders_df
上記の情報をベースにして、新たに再配送リストを作成します。
not_deliver_df = orders_df[orders_df.不在 == "不在"]
restart_df = orders_df[orders_df.ID == "伏見稲荷"]
end_df = orders_df[orders_df.ID == "京都駅(戻り)"]
not_deliver_df = pd.concat([restart_df, not_deliver_df, end_df])
not_deliver_df
再配達時間をシミュレーションするため、最初の配送計画の最終配送地点の出発時刻を取得します。(今回の場合は伏見稲荷です。)
プロトタイピングであるため、このような処理を行なっていますが、本番環境では実際に再配達を開始する時間を指定します。
waypointDf_restart = waypointDf.loc[[waypointDf.shape[0] - 2],['estimatedDeparture']]
list_restart = waypointDf_restart.to_numpy().tolist()
departureTime = list_restart[0][0]
departureTime
それでは改めて、以下のコードによりWaypoint Seaquence APIのインプットパラメータを生成します。
routingMode = "fastest"
transportMode = "truck"
trafficMode = "enabled"
import numpy as np
import time
list = orders_df.to_numpy().tolist()
dayOfWeek = ""
temp = pd.Timestamp(departureTime)
if temp.dayofweek == 0:
dayOfWeek = "mo"
elif temp.dayofweek == 1:
dayOfWeek = "tu"
elif temp.dayofweek == 2:
dayOfWeek = "we"
elif temp.dayofweek == 3:
dayOfWeek = "th"
elif temp.dayofweek == 4:
dayOfWeek = "fr"
elif temp.dayofweek == 5:
dayOfWeek = "sa"
elif temp.dayofweek == 6:
dayOfWeek = "su"
convertLists = []
params = {}
number = len(list)
count = 1
for m in list:
key = ""
acc = ""
convertList = ""
st = m[3]*60
if count == 1:
key = "start"
elif count == number:
key = "end"
else:
key += "destination"
key += str(count-1)
convertList += m[0]+":"+m[1]
convertList += ";"
convertList += str(m[5])
convertList += ","
convertList += str(m[6])
if count != 1:
if count != number:
if m[2]:
if m[2] == "8-12":
acc = "acc:"+dayOfWeek+"08:00:00+09:00|"+dayOfWeek+"12:00:00+09:00"
elif m[2] == "12-14":
acc = "acc:"+dayOfWeek+"12:00:00+09:00|"+dayOfWeek+"14:00:00+09:00"
elif m[2] == "14-16":
acc = "acc:"+dayOfWeek+"14:00:00+09:00|"+dayOfWeek+"16:00:00+09:00"
elif m[2] == "18-20":
acc = "acc:"+dayOfWeek+"18:00:00+09:00|"+dayOfWeek+"20:00:00+09:00"
elif m[2] == "20-22":
acc = "acc:"+dayOfWeek+"20:00:00+09:00|"+dayOfWeek+"22:00:00+09:00"
elif m[2] == "18-22":
acc = "acc:"+dayOfWeek+"18:00:00+09:00|"+dayOfWeek+"22:00:00+09:00"
convertList += ";"
convertList += acc
convertList += ";st:"
if np.isnan(st):
convertList += ""
else:
convertList += str(int(st))
params[key] = convertList
if count == number:
params["departure"] = departureTime
params["improveFor"] = "distance"
params["mode"] = routingMode+";"+transportMode+";traffic:"+trafficMode+";dirtRoad:-2"
count = count + 1
print(params)
出力結果は以下のとおりです。
{'start': '伏見稲荷:京都府京都市伏見区深草藪之内町68;34.96781,135.77273',
'destination1': '清水寺:京都府京都市東山区清水1丁目294;34.99587,135.78305;acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600',
'destination2': '銀閣寺:京都府京都市左京区銀閣寺町2;35.02686,135.79827;;st:300',
'destination3': '南禅寺:京都府京都市左京区南禅寺福地町;35.01015,135.79144;acc:sa08:00:00+09:00|sa12:00:00+09:00;st:300',
'end': '京都駅(戻り):京都府京都市下京区東塩小路釜殿町;34.98515,135.75709',
'departure': '2023-03-04T18:53:24+09:00',
'improveFor': 'time',
'mode': 'fastest;truck;traffic:enabled;dirtRoad:-2'}
このインプットパラメータを使用して、HERE Waypoint Sequence APIをコールしてみましょう。
import json
from pygments import highlight
from pygments.lexers import JsonLexer
from pygments.formatters import TerminalFormatter
from here.platform import Platform
platform = Platform()
wps = platform.get_service("hrn:here:service::olp-here:waypoints-sequence-8")
result = wps.get("/findsequence2", params=params)
print(highlight(json.dumps(result, indent=4, ensure_ascii=False), JsonLexer(), TerminalFormatter()))
先ほどと同じくその結果を再配送の順序としてdataframeにします。
wayPointList = []
for m in result["results"][0]["waypoints"]:
wayPointList.append(m)
pd.set_option('display.max_colwidth', 150)
waypointDf=pd.DataFrame(wayPointList)
waypointDf
この結果を見て、南禅寺には配達指定時間の制約があるため、当日中の配達ができないことがわかります。
再配達順序の決定(最終版)
ベースの配送リストの不在列を”翌日”と更新します。
orders_df.loc[orders_df[orders_df["ID"] == "南禅寺"].index, '不在'] = '翌日'
orders_df
先ほどと同じ要領で、再配送リスト(最終版)を作成します。
not_deliver_df = orders_df[orders_df.不在 == "不在"]
restart_df = orders_df[orders_df.ID == "伏見稲荷"]
end_df = orders_df[orders_df.ID == "京都駅(戻り)"]
not_deliver_df = pd.concat([restart_df, not_deliver_df, end_df])
not_deliver_df
先ほどと同様ですが、再配達時間をシミュレーションするため、最初の配送計画の最終配送地点の出発時刻を取得します。(今回の場合は伏見稲荷です。)
プロトタイピングであるため、このような処理を行なっていますが、本番環境では実際に再配達を開始する時間を指定します。
waypointDf_restart = waypointDf.loc[[waypointDf.shape[0] - 2],['estimatedDeparture']]
list_restart = waypointDf_restart.to_numpy().tolist()
departureTime = list_restart[0][0]
departureTime
それでは改めて(これが最後です)、以下のコードによりWaypoint Seaquence APIのインプットパラメータを生成します。
routingMode = "fastest"
transportMode = "truck"
trafficMode = "enabled"
import numpy as np
import time
list = orders_df.to_numpy().tolist()
dayOfWeek = ""
temp = pd.Timestamp(departureTime)
if temp.dayofweek == 0:
dayOfWeek = "mo"
elif temp.dayofweek == 1:
dayOfWeek = "tu"
elif temp.dayofweek == 2:
dayOfWeek = "we"
elif temp.dayofweek == 3:
dayOfWeek = "th"
elif temp.dayofweek == 4:
dayOfWeek = "fr"
elif temp.dayofweek == 5:
dayOfWeek = "sa"
elif temp.dayofweek == 6:
dayOfWeek = "su"
convertLists = []
params = {}
number = len(list)
count = 1
for m in list:
key = ""
acc = ""
convertList = ""
st = m[3]*60
if count == 1:
key = "start"
elif count == number:
key = "end"
else:
key += "destination"
key += str(count-1)
convertList += m[0]+":"+m[1]
convertList += ";"
convertList += str(m[5])
convertList += ","
convertList += str(m[6])
if count != 1:
if count != number:
if m[2]:
if m[2] == "8-12":
acc = "acc:"+dayOfWeek+"08:00:00+09:00|"+dayOfWeek+"12:00:00+09:00"
elif m[2] == "12-14":
acc = "acc:"+dayOfWeek+"12:00:00+09:00|"+dayOfWeek+"14:00:00+09:00"
elif m[2] == "14-16":
acc = "acc:"+dayOfWeek+"14:00:00+09:00|"+dayOfWeek+"16:00:00+09:00"
elif m[2] == "18-20":
acc = "acc:"+dayOfWeek+"18:00:00+09:00|"+dayOfWeek+"20:00:00+09:00"
elif m[2] == "20-22":
acc = "acc:"+dayOfWeek+"20:00:00+09:00|"+dayOfWeek+"22:00:00+09:00"
elif m[2] == "18-22":
acc = "acc:"+dayOfWeek+"18:00:00+09:00|"+dayOfWeek+"22:00:00+09:00"
convertList += ";"
convertList += acc
convertList += ";st:"
if np.isnan(st):
convertList += ""
else:
convertList += str(int(st))
params[key] = convertList
if count == number:
params["departure"] = departureTime
params["improveFor"] = "distance"
params["mode"] = routingMode+";"+transportMode+";traffic:"+trafficMode+";dirtRoad:-2"
count = count + 1
print(params)
出力結果は以下のとおりです。
{'start': '伏見稲荷:京都府京都市伏見区深草藪之内町68;34.96781,135.77273',
'destination1': '清水寺:京都府京都市東山区清水1丁目294;34.99587,135.78305;acc:sa18:00:00+09:00|sa20:00:00+09:00;st:600',
'destination2': '銀閣寺:京都府京都市左京区銀閣寺町2;35.02686,135.79827;;st:300',
'end': '京都駅(戻り):京都府京都市下京区東塩小路釜殿町;34.98515,135.75709',
'departure': '2023-03-04T18:53:24+09:00',
'improveFor': 'time',
'mode': 'fastest;truck;traffic:enabled;dirtRoad:-2'}
このインプットパラメータを使用して、HERE Waypoint Sequence APIをコールしてみましょう。
import json
from pygments import highlight
from pygments.lexers import JsonLexer
from pygments.formatters import TerminalFormatter
from here.platform import Platform
platform = Platform()
wps = platform.get_service("hrn:here:service::olp-here:waypoints-sequence-8")
result = wps.get("/findsequence2", params=params)
print(highlight(json.dumps(result, indent=4, ensure_ascii=False), JsonLexer(), TerminalFormatter()))
先ほどと同じくその結果を再配達の順序としてdataframeにします。
wayPointList = []
for m in result["results"][0]["waypoints"]:
wayPointList.append(m)
pd.set_option('display.max_colwidth', 150)
waypointDf=pd.DataFrame(wayPointList)
waypointDf
京都駅(デポ)に20:08に到着するという再配達順序が作成されました。
再配達ルートの決定
先ほどと同じコードを繰り返します。
作成した再配達順序のリストをベースに再帰的にHERE Routing APIをコールし、それぞれのルートを算出します。
route = platform.get_service("hrn:here:service::olp-here:routing-8")
if routingMode == "fastest":
routingMode = "fast"
elif routingMode == "shortest":
routingMode = "short"
polyLineList = []
departureList = []
arrivalList = []
summaryList = []
waypointLocationList = []
transitDeparture = ""
count = 0
for n in wayPointList:
if count == 0:
estimatedDeparture = str(wayPointList[count-1]['estimatedDeparture'])
waypointLocation = {"order": count, "name": wayPointList[count]['id'], "lat": wayPointList[count]['lat'], "lng": wayPointList[count]['lng'], "estimatedDeparture": estimatedDeparture}
waypointLocationList.append(waypointLocation)
count = count + 1
continue
lat1 = str(wayPointList[count-1]['lat'])
lng1 = str(wayPointList[count-1]['lng'])
lat2 = str(wayPointList[count]['lat'])
lng2 = str(wayPointList[count]['lng'])
name = str(wayPointList[count]['id'])
estimatedDeparture = str(wayPointList[count-1]['estimatedDeparture'])
transitDeparture = str(wayPointList[count-1]['estimatedDeparture'])
waypointLocation = {"order": count, "name": name, "lat": wayPointList[count]['lat'], "lng": wayPointList[count]['lng'], "estimatedDeparture": estimatedDeparture}
waypointLocationList.append(waypointLocation)
params = {
"origin": f'{lat1},{lng1}',
"destination": f'{lat2},{lng2}',
"routingMode": f'{routingMode}',
"transportMode": f'{transportMode}',
"departureTime": f'{transitDeparture}',
"return": "polyline,actions,instructions,summary,passthrough,routeHandle"
}
result = route.get("/routes", params=params)
polyLineList.append(result["routes"][0]['sections'][0]["polyline"])
departureList.append(result["routes"][0]['sections'][0]["departure"])
arrivalList.append(result["routes"][0]['sections'][0]["arrival"])
summaryList.append(result["routes"][0]['sections'][0]["summary"])
count = count + 1
再配達ルートを地図上に表示
先ほどと同様にHERE Map Widget for Jupyterを使用して地図上に描画します。<APIKEY>
と記載されている部分は取得されましたAPIKEYに変換してください。
from here_map_widget import MultiLineString, LineString, Polyline, Point, Circle, WKT, Marker, Icon, FullscreenControl, InfoBubble
from here_map_widget import Map, OMV, Platform, Style, TileLayer, MapSettingsControl
from here_map_widget import ServiceNames, OMVUrl
from here_map_widget import LineString, Polyline
df = pd.DataFrame(waypointLocationList)
avg_lat = df['lat'].mean()
avg_lng = df['lng'].mean()
zoom = 13
apiKey = <APIKEY>
services_config = {
ServiceNames.omv: {
OMVUrl.scheme: "https",
OMVUrl.host: "vector.hereapi.com",
OMVUrl.path: "/v2/vectortiles/core/mc",
}
}
style = Style(
config="https://js.api.here.com/v3/3.1/styles/omv/oslo/japan/normal.day.yaml",
base_url="https://js.api.here.com/v3/3.1/styles/omv/oslo/japan/",
)
platform = Platform(api_key=apiKey,services_config=services_config)
omv_provider = OMV(path="v2/vectortiles/core/mc", platform=platform, style=style)
omv_layer = TileLayer(provider=omv_provider, style={"max": 22})
center = [avg_lat,avg_lng]
m = Map(api_key=apiKey,center=center, zoom=zoom, basemap=omv_layer)
for count in range(len(waypointLocationList)):
name = waypointLocationList[count]['name']
if count == 0:
info = InfoBubble(position=Point(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng']), \
content='<div>NAME: '+name+'</div><div>Order: '+str(waypointLocationList[count]['order'])+'</div><div>Departure: '+str(departureList[count]['time'])+'</div>')
elif 0 < count < len(waypointLocationList)-1:
info = InfoBubble(position=Point(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng']), \
content='<div>NAME: '+name+'</div><div>Order: '+str(waypointLocationList[count]['order'])+'</div><div>Departure: '+str(departureList[count]['time'])+'</div><div>Arrival: '+str(arrivalList[count-1]['time'])+'</div>')
else:
info = InfoBubble(position=Point(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng']), \
content='<div>NAME: '+name+'</div><div>Order: '+str(waypointLocationList[count]['order'])+'</div>')
marker = Marker(lat=waypointLocationList[count]['lat'], lng=waypointLocationList[count]['lng'], info=info, evt_type="tap", show_bubble=True)
m.add_object(marker)
count = count + 1
for q in gpsLists:
style = {"lineWidth": 5}
l = q
ls = LineString(points=l)
pl = Polyline(object=ls, style=style)
m.add_object(pl)
setttings = MapSettingsControl(
layers=[
{"label": "Original", "layer": omv_layer},
],
basemaps=["raster.satellite.map","vector.normal.map"],
)
m.add_control(setttings)
fs = FullscreenControl()
m.add_control(fs)
m
結果をフルスクリーンモードで確認します。
上記のように、伏見稲荷を18:53に出発し、最終的に20:08に配達作業が終了しました。お疲れ様でした!
おまけ (トラックと普通車指定の比較)
今回使用しましたHERE Waypoint Sequence APIとRouting APIはトラック規制などの道路情報も考慮に入れて配送順序及びルートを決定します。以下は先ほどの再配達ルートをそれぞれトラックまたは普通車指定とした場合のルートになります。(トラック指定の場合、より幹線道路を選択している傾向を読み取れます。)
また、単純なトラック指定以外に車幅、車高、全長、重量などの規制情報にも対応していることも付け加えておきます。
おわりに
いかがでしたでしょうか?
このように、Jupyterを使用してHERE API群を駆使することで、物流DXのプロトタイピングが容易にできます。慣れてくれば1時間程度で今回紹介したシナリオのプロトタイピングは可能です。今回紹介したアプローチはJupyterエコシステムを使ってのRPA実現(プロトタイピング)のみとなります。一方で日々の配送データをビッグデータ化していき、Jupyterエコシステムを使用して、機械学習というアプローチを加えることで、より精度の高い物流DXを実現することも可能なのではないかと思います。
最後に、日々配送業務に携わるトラックドライバー様に頭が下がる思いですが、これらのHEREのソリューションを使用して是非とも物流DXに貢献できれば幸いと思います。
ここまで読んでいただきましてありがとうございました!