Skip to content

Instantly share code, notes, and snippets.

@deton
Last active October 26, 2025 07:38
Show Gist options
  • Save deton/ac80db5e4b464169a764b524d3377c96 to your computer and use it in GitHub Desktop.
Save deton/ac80db5e4b464169a764b524d3377c96 to your computer and use it in GitHub Desktop.
make_gtfsを使って仮のGTFS(時刻表)を生成する

make_gtfsを使って仮のGTFS(時刻表)を生成する (山陽電鉄の一部)

実際の発車時刻とは異なるが、1時間あたりの本数はなるべく合わせた時刻表。

駅の緯度経度や線路(ルート)形状の情報は、 国土数値情報 鉄道データを利用。

指定したbounding box内の駅のみを対象とする。

shapes.geojson

uv run python withinbbox.py 134.963436,34.623885,135.170288,34.738791 N02-24_RailroadSection.shp
grep -e '本線.*山陽電気鉄道' N02-24_RailroadSection.134.963436,34.623885,135.170288,34.738791.geojson \
| sed -e '1i {"type":"FeatureCollection","features":[' -e '$s/,$/]}/' >sanden.geojson
uv run python linemerge.py sanden.geojson
sed -e '1i {"type":"Feature","properties":{"shape_id":"shp1"},"geometry":' -e '$a }' sanden.linemerge.geojson >shapes.geojson

1本のLineStringにマージしてshapes.geojsonを作成。 (linemerge.pyは https://gist.github.com/deton/3508b7a291b81d1f9bbedad8ea26dccf と同じ)

service_windows.csvとfrequencies.csv

NAVITIMEで駅の時刻表を見て(列車種別で絞り込みしつつ)、 各曜日・各時間帯・上り/下りの本数を調べて記入。

1時間あたりの本数が同じ時間が続く場合は1行にまとめられる。

(とりあえず、列車種別:普通、平日を対象に手作業)

stops.csv

駅のgeometryとして、midpointの緯度経度に変換

uv run python withinbbox.py 134.963436,34.623885,135.170288,34.738791 N02-24_Station.shp
uv run python midpoint.py N02-24_Station.134.963436,34.623885,135.170288,34.738791.geojson

とりあえず、重複するstop_idや駅名は手で削除。

grep -e '本線.*山陽電気鉄道' N02-24_Station.134.963436,34.623885,135.170288,34.738791.midpoint.geojson | sed -e 's/,$//' > stops.ndjson
vim stops.ndjson

(参考:同名駅のグループに属すものを検索。 緯度経度が同じなら、N02_005gの値をstop_idとして使う。)

grep -v 'N02_005c": "\([^"]*\)", "N02_005g": "\1"' N02-24_Station.134.963436,34.623885,135.170288,34.738791.midpoint.geojson
echo 'stop_id,stop_name,stop_lon,stop_lat,zone_id' > stops.csv
jq -r '[.properties.N02_005c,.properties.N02_005,.geometry.coordinates[0],.geometry.coordinates[1],.properties.N02_005c]|@csv' stops.ndjson >>stops.csv

make_gtfsを実行

uv run make_gtfs data/sanden_local output/sanden_local.zip

運賃ファイルfare_rules.txt, fare_attributes.txtを生成

運賃表PDFを見て、下記の内容のfare.csvを作成。

price,min_km,max_km
170,0,2
200,2,4
250,4,7
320,7,10
390,10,13
470,13,17
540,17,21
600,21,25
uv run python make_gtfs_fare.py output/sanden_local.zip fare.csv

生成されるfare_rules.txtとfare_attributes.txtをGTFS zipに含める。

TimestampedGeoJsonに変換して表示してみる

https://gist.github.com/deton/8c4bd41faa4060dfa4a68066a5403be3#file-gtfsshape2timestampedgeojson-ipynb

https://deton.github.io/HeatMapWithTime/IconPositionWithTime.html?tripWidth=6&iconScale=0.7&zoom=12&pointRadius=2&lineWidth=1&latlon=34.65959,135.067874&jsonurl=https://gist.githubusercontent.com/deton/ac80db5e4b464169a764b524d3377c96/raw/7400f6ae9b1e758ffa5da9fb9d42c09cade52385/zzGtfsTimestamped.geojson

import sys
import geopandas as gpd
# geojson from https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N02-2024.html
# 駅のgeometryとして、重心Pointの緯度経度に変換
infile = sys.argv[1]
outfile = '.'.join(infile.split('.')[:-1] + ['centroid', 'geojson'])
# TODO: 場所に応じた適切なCRSを使う
rails = gpd.read_file(infile, encoding="utf-8").to_crs('EPSG:6690')
rails['centroid'] = rails['geometry'].centroid.to_crs('EPSG:6668')
rails.drop(columns=['geometry'], inplace=True)
rails.to_file(outfile, driver='GeoJSON')
price min_km max_km
170 0 2
200 2 4
250 4 7
320 7 10
390 10 13
470 13 17
540 17 21
600 21 25
route_short_name route_long_name route_type shape_id service_window_id frequency direction
nd 山陽電鉄本線 普通下り 2 shp1 wdd0507 4 1
nd 山陽電鉄本線 普通下り 2 shp1 wdd0708 5 1
nd 山陽電鉄本線 普通下り 2 shp1 wdd0810 8 1
nd 山陽電鉄本線 普通下り 2 shp1 wdd1011 5 1
nd 山陽電鉄本線 普通下り 2 shp1 wdd1117 4 1
nd 山陽電鉄本線 普通下り 2 shp1 wdd1718 5 1
nd 山陽電鉄本線 普通下り 2 shp1 wdd1820 6 1
nd 山陽電鉄本線 普通下り 2 shp1 wdd2022 5 1
nd 山陽電鉄本線 普通下り 2 shp1 wdd2224 3 1
nu 山陽電鉄本線 普通上り 2 shp1 wdu0506 4 0
nu 山陽電鉄本線 普通上り 2 shp1 wdu0610 5 0
nu 山陽電鉄本線 普通上り 2 shp1 wdu1016 2 0
nu 山陽電鉄本線 普通上り 2 shp1 wdu1617 4 0
nu 山陽電鉄本線 普通上り 2 shp1 wdu1718 2 0
nu 山陽電鉄本線 普通上り 2 shp1 wdu1820 4 0
nu 山陽電鉄本線 普通上り 2 shp1 wdu2021 5 0
nu 山陽電鉄本線 普通上り 2 shp1 wdu2122 4 0
nu 山陽電鉄本線 普通上り 2 shp1 wdu2224 3 0
# make GTFS fare_attributes.txt and fare_rules.txt using following fare.csv.
#
# ```csv
# price,min_km,max_km
# 170,0,2
# 200,2,4
# 250,4,7
# 320,7,10
# 390,10,13
# 470,13,17
# 540,17,21
# 600,21,25
# ```
import argparse
import gtfs_kit as gk
import pandas as pd
parser = argparse.ArgumentParser()
parser.add_argument("gtfspath", help="GTFS path or URL")
parser.add_argument("farecsv", help="fare.csv file with columns: price,min_km,max_km")
parser.add_argument("--currency_type", default="JPY")
parser.add_argument("--payment_method", type=int, choices=[0, 1], default=1)
parser.add_argument("--transfers", type=int, choices=[0, 1, 2], default=0) # TODO: support empty
args = parser.parse_args()
CURRENCY_TYPE = args.currency_type
PAYMENT_METHOD = args.payment_method
TRANSFERS = args.transfers
feed = gk.read_feed(args.gtfspath, dist_units='m')
feed = feed.append_dist_to_stop_times()
target_triproute = feed.compute_trip_stats().groupby('route_id').first().reset_index()[['route_id','trip_id']].to_dict(orient='list')
week = feed.get_first_week()
date = week[0] # Monday
dfstops = feed.get_stops(date)
dfst = feed.get_stop_times(date)
dfst = dfst.merge(dfstops)
tripsdict = dfst.groupby('trip_id').apply(lambda x: x.to_dict(orient='records'), include_groups=False).to_dict()
dffare = pd.read_csv(args.farecsv)
dffare['fare_id'] = 'f' + dffare['price'].map(str)
dffare_attr = dffare[['fare_id','price']].copy()
dffare_attr['currency_type'] = CURRENCY_TYPE
dffare_attr['payment_method'] = PAYMENT_METHOD
dffare_attr['transfers'] = TRANSFERS
dffare_attr.to_csv('fare_attributes.txt', index=False)
dffare_rules = pd.DataFrame()
for trip_id, stop_times_list in tripsdict.items():
if trip_id not in target_triproute['trip_id']:
continue
route_id = target_triproute['route_id'][target_triproute['trip_id'].index(trip_id)]
#stop_times_list.sort(key=lambda x: x['arrival_time'])
for i in range(1, len(stop_times_list)):
distoffset = stop_times_list[i-1]['shape_dist_traveled']
origzoneid = stop_times_list[i-1]['zone_id']
for st in stop_times_list[i:]:
dist = st['shape_dist_traveled']
distkm = (dist - distoffset) / 1000.0
df = dffare[(distkm >= dffare['min_km']) & (distkm < dffare['max_km'])].copy()
df['route_id'] = route_id
df['origin_id'] = origzoneid
df['destination_id'] = st['zone_id']
#df['orig_stop'] = stop_times_list[i-1]['stop_name']
#df['dest_stop'] = st['stop_name']
dffare_rules = pd.concat([dffare_rules, df.drop(columns=['min_km', 'max_km', 'price'])])
dffare_rules.to_csv('fare_rules.txt', index=False)
agency_name agency_url agency_timezone start_date end_date
山陽電気鉄道 https://www.sanyo-railway.co.jp Asia/Tokyo 20250101 20260101
import sys
import geopandas as gpd
# geojson from https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N02-2024.html
# 駅のgeometryとして、midpointの緯度経度に変換
EPSG = "EPSG:6668"
infile = sys.argv[1]
outfile = '.'.join(infile.split('.')[:-1] + ['midpoint', 'geojson'])
rails = gpd.read_file(infile, encoding="utf-8")
utmcrs = rails.estimate_utm_crs()
railsutm = rails.to_crs(utmcrs)
railsutm['midpoint'] = railsutm['geometry'].interpolate(0.5, normalized=True).to_crs(EPSG)
railsutm.to_crs(EPSG).drop(columns=['geometry']).to_file(outfile, driver='GeoJSON')
service_window_id start_time end_time monday tuesday wednesday thursday friday saturday sunday
wdd0507 05:00:00 07:00:00 1 1 1 1 1 0 0
wdd0708 07:00:00 08:00:00 1 1 1 1 1 0 0
wdd0810 08:00:00 10:00:00 1 1 1 1 1 0 0
wdd1011 10:00:00 11:00:00 1 1 1 1 1 0 0
wdd1117 11:00:00 17:00:00 1 1 1 1 1 0 0
wdd1718 17:00:00 18:00:00 1 1 1 1 1 0 0
wdd1820 18:00:00 20:00:00 1 1 1 1 1 0 0
wdd2022 20:00:00 22:00:00 1 1 1 1 1 0 0
wdd2224 22:00:00 23:59:59 1 1 1 1 1 0 0
wdu0506 05:00:00 06:00:00 1 1 1 1 1 0 0
wdu0610 06:00:00 10:00:00 1 1 1 1 1 0 0
wdu1016 10:00:00 16:00:00 1 1 1 1 1 0 0
wdu1617 16:00:00 17:00:00 1 1 1 1 1 0 0
wdu1718 17:00:00 18:00:00 1 1 1 1 1 0 0
wdu1820 18:00:00 20:00:00 1 1 1 1 1 0 0
wdu2021 20:00:00 21:00:00 1 1 1 1 1 0 0
wdu2122 21:00:00 22:00:00 1 1 1 1 1 0 0
wdu2224 22:00:00 23:59:59 1 1 1 1 1 0 0
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
stop_id stop_name stop_lon stop_lat zone_id
007372 板宿 135.133288242230975 34.660156819922143 007372
007362 西代 135.143989998451048 34.662400000769487 007362
007426 西新町 134.980804999752081 34.64957500057676 007426
007480 須磨浦公園 135.100219998560675 34.637935000991746 007480
007420 月見山 135.122118330359285 34.650186405397477 007420
007512 山陽垂水 135.053739999694017 34.629335001332265 007512
007444 大蔵谷 135.008405000672781 34.646585000707546 007444
007437 人丸前 135.002600000284019 34.647545000758335 007437
007492 舞子公園 135.03385500044638 34.634225001004651 007492
007504 滝の茶屋 135.072919999421572 34.631235000724921 007504
007400 東須磨 135.1274849984909 34.65521500058918 007400
007429 山陽明石 134.992949761341464 34.648798424835398 007429
007449 須磨寺 135.116239999376177 34.646025000127366 007449
007508 霞ヶ丘 135.042720001598951 34.630325000696565 007508
007457 山陽須磨 135.112314999141461 34.64357500160429 007457
007511 東垂水 135.063589999696802 34.629375000624158 007511
007415 林崎松江海岸 134.965080000648328 34.652280000372073 007415
007496 山陽塩屋 135.082039999504104 34.633615000533077 007496
007474 西舞子 135.02813000127972 34.638700000694072 007474
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "板宿", "N02_005c": "007372", "N02_005g": "007372" }, "geometry": { "type": "Point", "coordinates": [ 135.133288242230975, 34.660156819922143 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "西代", "N02_005c": "007362", "N02_005g": "007361" }, "geometry": { "type": "Point", "coordinates": [ 135.143989998451048, 34.662400000769487 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "西新町", "N02_005c": "007426", "N02_005g": "007426" }, "geometry": { "type": "Point", "coordinates": [ 134.980804999752081, 34.64957500057676 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "須磨浦公園", "N02_005c": "007480", "N02_005g": "007480" }, "geometry": { "type": "Point", "coordinates": [ 135.100219998560675, 34.637935000991746 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "月見山", "N02_005c": "007420", "N02_005g": "007420" }, "geometry": { "type": "Point", "coordinates": [ 135.122118330359285, 34.650186405397477 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "山陽垂水", "N02_005c": "007512", "N02_005g": "007512" }, "geometry": { "type": "Point", "coordinates": [ 135.053739999694017, 34.629335001332265 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "大蔵谷", "N02_005c": "007444", "N02_005g": "007444" }, "geometry": { "type": "Point", "coordinates": [ 135.008405000672781, 34.646585000707546 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "人丸前", "N02_005c": "007437", "N02_005g": "007437" }, "geometry": { "type": "Point", "coordinates": [ 135.002600000284019, 34.647545000758335 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "舞子公園", "N02_005c": "007492", "N02_005g": "007492" }, "geometry": { "type": "Point", "coordinates": [ 135.03385500044638, 34.634225001004651 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "滝の茶屋", "N02_005c": "007504", "N02_005g": "007504" }, "geometry": { "type": "Point", "coordinates": [ 135.072919999421572, 34.631235000724921 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "東須磨", "N02_005c": "007400", "N02_005g": "007400" }, "geometry": { "type": "Point", "coordinates": [ 135.1274849984909, 34.65521500058918 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "山陽明石", "N02_005c": "007429", "N02_005g": "007429" }, "geometry": { "type": "Point", "coordinates": [ 134.992949761341464, 34.648798424835398 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "須磨寺", "N02_005c": "007449", "N02_005g": "007449" }, "geometry": { "type": "Point", "coordinates": [ 135.116239999376177, 34.646025000127366 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "霞ヶ丘", "N02_005c": "007508", "N02_005g": "007508" }, "geometry": { "type": "Point", "coordinates": [ 135.042720001598951, 34.630325000696565 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "山陽須磨", "N02_005c": "007457", "N02_005g": "007457" }, "geometry": { "type": "Point", "coordinates": [ 135.112314999141461, 34.64357500160429 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "東垂水", "N02_005c": "007511", "N02_005g": "007511" }, "geometry": { "type": "Point", "coordinates": [ 135.063589999696802, 34.629375000624158 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "林崎松江海岸", "N02_005c": "007415", "N02_005g": "007415" }, "geometry": { "type": "Point", "coordinates": [ 134.965080000648328, 34.652280000372073 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "山陽塩屋", "N02_005c": "007496", "N02_005g": "007496" }, "geometry": { "type": "Point", "coordinates": [ 135.082039999504104, 34.633615000533077 ] } }
{ "type": "Feature", "properties": { "N02_001": "12", "N02_002": "4", "N02_003": "本線", "N02_004": "山陽電気鉄道", "N02_005": "西舞子", "N02_005c": "007474", "N02_005g": "007474" }, "geometry": { "type": "Point", "coordinates": [ 135.02813000127972, 34.638700000694072 ] } }
import sys
import geopandas as gpd
from shapely.geometry import Polygon
# Usage: python3 withinbbox.py 134.963436,34.623885,135.170288,34.738791 railstation.centroid.geojson
bb = [float(x) for x in sys.argv[1].split(',')]
infile = sys.argv[2]
outfile = '.'.join(infile.split('.')[:-1] + [sys.argv[1], 'geojson'])
coords = ((bb[0], bb[1]),
(bb[0], bb[3]),
(bb[2], bb[3]),
(bb[2], bb[1]))
bbox = Polygon(coords)
gdf = gpd.read_file(infile, encoding='utf-8')
gdf[gdf.within(bbox)].to_file(outfile, driver='GeoJSON')
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment