はじめに
HEREでエンジニアをしておりますkekishidaと申します。本記事ではChatGPTが生成した京都市内観光プランをもとに、HERE APIを駆使してよりリアルな京都市内観光プラン地図生成にチャレンジしてみたいと思います。今回、私個人としてははじめてのChatGPTプログラミングになります。いわゆるプロンプトエンジニアとはどう言ったものかということを体験しつつ、ChatGPTとHEREのAPIを応用することでどのようなことが実現できるかを考えてみました。
今回チャレンジした内容は、
京都駅からタクシーを使用して1日で京都市内を観光することができるプランを時間と場所と住所という順番で区切ったカンマ区切りのCSV形式で作ってください
という問いに対して京都市内観光プランをChatGPTに生成してもらい、
(正しいかどうかは別として)こちらで提供された京都市内観光プランに基づいて、HERE APIを駆使して地図付きのよりリアルな京都市内観光プランを生成するためのJupyter notebookを作成してみました。
そして、以下はその出力結果例です。
以下は生成されたルート地図付きの市内観光プランです。
さらに何度もスクリプトを流すといろいろなプランを作ってくれます。
さすがは、生成系AI! 私のアイデアでは下鴨神社を含めるコースはすぐには考えつきません(笑)。
このように、ChatGPTにアイデアの種だけ考えてもらって、実際のリアルなプランはHEREのAPIに考えてもらうというユースケースは今後十分考えられるのではと感じます。
例えば、ChatGPTの答えに基づいて、
- HERE Platformが管理するロケーションデータに基づいた最新地図の提供
- HERE Platformが管理するロケーションデータに基づいた住所情報の補完
- HERE Platformが管理するロケーションデータに基づいた目的地を回る最適な順序の提案
- HERE Platformが管理するロケーションデータに基づいたリアルな走行ルートの提案
- HERE Platformが管理するロケーションデータに基づいたETA(到着時刻予定)の提案
- HERE Platformが管理する渋滞情報に基づいたETA(到着時刻予定)の提案
などのような応用ができそうです。(ChatGPTへの聞き方次第では、まだまだ他の応用範囲もありそうです。)
それでは、コードの中身に入っていきたいと思います。
準備するもの
本記事で使用するコードは、Jupyterエコシステム上で動作します。以下の記事のプロトタイピングに必要なものの部分をご参考下さい。
また、本記事ではopenai APIを使用します。Qiita内にもいくつか記事が存在するため、詳しくはこれらをご参考下さい。
京都市内観光プランナー by Jupyter
以下にjupyterと注釈してありますコードスニペットをそのままJupyter notebook/labのセルにペーストするだけで京都観光プランの作成が可能なように配慮をしておりますが、コードが冗長であること、個別環境に依存する部分がありますことについてはご了承ください。
また、はじめてのChatGPTプログラミングなので、もっと適切なプロンプトエンジニアリングが可能なのかもしれません。そして、ChatGPTより毎回違った答えが返ってくるため、時には、後ほど解説するスクリプトでは処理できない場合もありますので、こちらについてもあらかじめご了承願います。
ChatGPT(gpt-3.5-turbo)による京都市内観光プラン
以下のコードにあるように解答をカンマ区切りのCSV形式で時間、場所、住所の観光プランを作成してもらうように仕向けてあげます。(OPENAI APIKEYの部分は取得しましたOpenAIのAPIKEYを入力してください。)
import openai
import requests
import json
openai.api_key = <OPENAI APIKEY>
response=openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are the smartest tour planner for programmer"},
{"role": "user", "content": "カンマ区切りのCSVデータが必要です。"},
{"role": "assistant", "content": "わかりました。カンマ区切りのCSVデータを提供します。"},
{"role": "user", "content": "住所に郵便番号は必要ありません。"},
{"role": "assistant", "content": "わかりました。住所に郵便番号を含めずに提供します。"},
{"role": "user", "content": "日本語のデータのみ必要です。"},
{"role": "assistant", "content": "わかりました。日本語のデータのみを提供します。"},
{"role": "user", "content": "CSVタイトルは、それぞれ時間、場所、住所としてください。"},
{"role": "assistant", "content": "わかりました。CSVタイトルは、それぞれ時間、場所、住所として提供します。"},
{"role": "user", "content": "京都駅からタクシーを使用して1日で京都市内を観光することができるプランを時間と場所と住所という順番で区切ったカンマ区切りのCSV形式で作ってください"}
]
)
print(response['choices'][0]['message']['content'])
ChatGPTからの答えはどうやら```区切りでCSVデータを提供する傾向が強いこと、また時には、csvという接頭辞を加えることが多いことから、以下のコードを追記しました。
csv_data = response['choices'][0]['message']['content'].split('```')[1]
if csv_data.startswith("csv"):
csv_data = csv_data.split("csv")[1]
with open("Data/京都Openai2.csv", "w") as file:
file.write(csv_data)
こちらの処理でChatGPT出力結果を、京都Openai2.csvというファイルに変換しました。
ChatGPTが提供した住所よりGeocoding(緯度経度変換)
まずは、ChatGPTより生成した京都Openai2.csvというファイルをPandasのDataFrameとします。
import pandas as pd
file = "Data/京都Openai2.csv"
df = pd.read_csv(file,sep=",",encoding="utf-8")
df=df.dropna()
df
以下の通り、時には住所の信頼度、粒度は低い可能性があります。
以下のコードで、HERE Geocoding & Search APIを使用して住所から緯度経度に変換します。
APIKEY = <HERE APIKEY>
from tqdm import tqdm
from here_location_services import LS
APIKEY = APIKEY
ls = LS(api_key=APIKEY)
addrList = df['住所'].tolist()
latlngList = []
for m in tqdm(addrList):
gc_response = ls.geocode(query=f"{m}")
data = gc_response.to_geojson()
if len(data["features"]) != 0:
latlngList.append(data["features"][0]["properties"]["position"])
else:
latlngList.append({'lat': None, 'lng': None})
変換した緯度経度を元の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
緯度経度変換に失敗している部分についてはNaNとなっています。これはChatGPTが提供したデータそのものに問題がある可能性が高いことを示しております。このような無効なデータを削除するため、以下のコードを追加します。
orders_df=orders_df.dropna()
本来は、無効なデータを削除するだけでなく、HERE Geocoding & Search APIと共に住所情報の正確性チェック及び補完をすることでより良いプランを生成できるはずですが、今回はプロトタイピングであるため、このような処理は割愛いたしました。
さらに、各観光地に1時間滞在する設定とし、さらに京都駅を出発地点及び最終地点という設定にするために次のコードを追加しPandas DataFrameを更新します。
kyoto_data = [['','京都駅','','34.98652499240876','135.75880393906627']]
kyoto_df = pd.DataFrame(kyoto_data, columns=["時間","場所","住所","Latitude","Longitude"])
orders_df = pd.concat([kyoto_df, orders_df, kyoto_df],axis=0)
orders_df['service time']=3600
orders_df
HERE Waypoint Sequence APIによる順序の決定
HERE Waypoint Sequence APIは本来は、主に物流のラストワンマイルソリューション向けのAPIですが、今回は観光プラン作成向けに使用してみます。このAPIの詳細については以下のリンクをご参考下さい。
今回は、以下の基本条件でWaypoint Sequence APIをコールします。
時間優先(fastest)
車種(car) => 普通車
渋滞などの交通状況考慮(enabled)
出発時間の指定(2023-06-04 08:00)
滞在時間の考慮(Dataframe内のservice time情報)
上記のリンク記事にも記載した通り、APIのインプットパラメータが複雑であるため、PandasのDataFrameよりデータを取り出しインプットパラメータに変換するコードを準備しました。
routingMode = "fastest"
transportMode = "car"
trafficMode = "enabled"
departureTime = "2023-06-04T09:00:00+09:00"
import numpy as np
import time
list = orders_df.to_numpy().tolist()
convertLists = []
params = {}
number = len(list)
count = 1
for m in list:
key = ""
acc = ""
convertList = ""
if count == 1:
key = "start"
convertList += m[1]
elif count == number:
key = "end"
convertList += m[1]+"-end"
else:
key += "destination"
key += str(count-1)
convertList += m[1]
convertList += ";"
convertList += str(m[3])
convertList += ","
convertList += str(m[4])
if count != 1:
if count != number:
convertList += ";st:"
if np.isnan(m[5]):
convertList += ""
else:
convertList += str(int(m[5]))
params[key] = convertList
if count == number:
params["departure"] = departureTime
params["improveFor"] = "time"
params["mode"] = routingMode+";"+transportMode+";traffic:"+trafficMode+";dirtRoad:-2"
count = count + 1
print(params)
このコードの出力結果は以下の通りです。これがWaypoint Sequence APIのインプットパラメータになります。
{'start': '京都駅;34.98652499240876,135.75880393906627', 'destination1': '金閣寺;35.03909,135.72928;;st:3600', 'destination2': '銀閣寺;35.02686,135.79827;;st:3600', 'destination3': '伏見稲荷大社;34.96781,135.77273;;st:3600', 'destination4': '清水寺;34.99587,135.78305;;st:3600', 'destination5': '二条城;35.01482,135.74658;;st:3600', 'destination6': '祇園;34.9971,135.77631;;st:3600', 'end': '京都駅-end;34.98652499240876,135.75880393906627', 'departure': '2023-06-04T09:00:00+09:00', 'improveFor': 'time', 'mode': 'fastest;car;traffic:enabled;dirtRoad:-2'}
それでは、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()))
出力結果は以下の通りです。
{
"results": [
{
"waypoints": [
{
"id": "京都駅",
"lat": 34.98652499240876,
"lng": 135.75880393906627,
"sequence": 0,
"estimatedArrival": null,
"estimatedDeparture": "2023-06-04T09:00:00+09:00",
"fulfilledConstraints": []
},
{
"id": "二条城",
"lat": 35.01482,
"lng": 135.74658,
"sequence": 1,
"estimatedArrival": "2023-06-04T09:12:01+09:00",
"estimatedDeparture": "2023-06-04T10:12:01+09:00",
"fulfilledConstraints": [
"st:3600"
]
},
{
"id": "金閣寺",
"lat": 35.03909,
"lng": 135.72928,
"sequence": 2,
"estimatedArrival": "2023-06-04T10:25:16+09:00",
"estimatedDeparture": "2023-06-04T11:25:16+09:00",
"fulfilledConstraints": [
"st:3600"
]
},
{
"id": "銀閣寺",
"lat": 35.02686,
"lng": 135.79827,
"sequence": 3,
"estimatedArrival": "2023-06-04T11:48:35+09:00",
"estimatedDeparture": "2023-06-04T12:48:35+09:00",
"fulfilledConstraints": [
"st:3600"
]
},
{
"id": "祇園",
"lat": 34.9971,
"lng": 135.77631,
"sequence": 4,
"estimatedArrival": "2023-06-04T13:03:46+09:00",
"estimatedDeparture": "2023-06-04T14:03:46+09:00",
"fulfilledConstraints": [
"st:3600"
]
},
{
"id": "清水寺",
"lat": 34.99587,
"lng": 135.78305,
"sequence": 5,
"estimatedArrival": "2023-06-04T14:08:45+09:00",
"estimatedDeparture": "2023-06-04T15:08:45+09:00",
"fulfilledConstraints": [
"st:3600"
]
},
{
"id": "伏見稲荷大社",
"lat": 34.96781,
"lng": 135.77273,
"sequence": 6,
"estimatedArrival": "2023-06-04T15:23:28+09:00",
"estimatedDeparture": "2023-06-04T16:23:28+09:00",
"fulfilledConstraints": [
"st:3600"
]
},
{
"id": "京都駅-end",
"lat": 34.98652499240876,
"lng": 135.75880393906627,
"sequence": 7,
"estimatedArrival": "2023-06-04T16:40:10+09:00",
"estimatedDeparture": null,
"fulfilledConstraints": []
}
],
"distance": "31768",
"time": "27610",
"interconnections": [
{
"fromWaypoint": "京都駅",
"toWaypoint": "二条城",
"distance": 4091.0,
"time": 721.0,
"rest": 0.0,
"waiting": 0.0
},
{
"fromWaypoint": "二条城",
"toWaypoint": "金閣寺",
"distance": 4171.0,
"time": 795.0,
"rest": 0.0,
"waiting": 0.0,
"warnings": [
{
"message": "Route violates requested 'avoid' options or legal/physical restrictions",
"code": 3
}
]
},
{
"fromWaypoint": "金閣寺",
"toWaypoint": "銀閣寺",
"distance": 8996.0,
"time": 1399.0,
"rest": 0.0,
"waiting": 0.0,
"warnings": [
{
"message": "Route violates requested 'avoid' options or legal/physical restrictions",
"code": 3
}
]
},
{
"fromWaypoint": "銀閣寺",
"toWaypoint": "祇園",
"distance": 4995.0,
"time": 911.0,
"rest": 0.0,
"waiting": 0.0
},
{
"fromWaypoint": "祇園",
"toWaypoint": "清水寺",
"distance": 980.0,
"time": 299.0,
"rest": 0.0,
"waiting": 0.0,
"warnings": [
{
"message": "Route violates requested 'avoid' options or legal/physical restrictions",
"code": 3
}
]
},
{
"fromWaypoint": "清水寺",
"toWaypoint": "伏見稲荷大社",
"distance": 4366.0,
"time": 883.0,
"rest": 0.0,
"waiting": 0.0,
"warnings": [
{
"message": "Route violates requested 'avoid' options or legal/physical restrictions",
"code": 3
}
]
},
{
"fromWaypoint": "伏見稲荷大社",
"toWaypoint": "京都駅-end",
"distance": 4169.0,
"time": 1002.0,
"rest": 0.0,
"waiting": 0.0
}
],
"description": "Targeted best time; with , improvement for traffic",
"timeBreakdown": {
"driving": 6010,
"service": 21600,
"rest": 0,
"waiting": 0
}
}
],
"errors": [],
"processingTimeDesc": "500ms",
"responseCode": "200",
"warnings": null,
"requestId": null
}
京都市内観光プランの順序表示 by HERE Waypoint Sequence API
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
それでは、ChatGPTの結果と比べてみましょう。微妙に順序が異なりますね。。。。(このケースではデータ不整合という都合上、嵐山を削除したので仕方ないのかもしれません。)
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>
と記載されている部分は取得されましたHEREの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
結果をフルスクリーンモードで確認しましょう。緑のマーカーをクリックすると各観光地の情報を確認することができます。
まあまあ良い感じの京都市内観光プランが生成されました!!
おわりに
いかがでしたでしょうか?今回の記事は、先月投稿しました以下の記事の番外編という様相も強いですが、ChatGPTのような生成系AIによって出力される不確かなデータに対して、HEREが提供するロケーションデータは相互補完することが可能であり、今回紹介しましたユースケース以外にも様々な応用が可能なのではないかと思いました。
AIへの問いかけに対して所望の地図が生成されるというユースケースは今回の例に限らず、様々なユースケースでも存在するに違いありません。次回はHERE APIがより活かせる物流系ソリューションのユースケースについても考察し、チャレンジしてみたいと思います。
ここまで読んでいただきありがとうございました。